feat: integrate production cockpit AI assistant
This commit is contained in:
@@ -11,6 +11,11 @@ import wave
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cognitive_agent.application.mode_profile_intent import (
|
||||
is_mode_profile_query,
|
||||
parse_mode_profile_create,
|
||||
parse_mode_profile_update,
|
||||
)
|
||||
from cognitive_agent.infrastructure.speech.sensevoice import (
|
||||
SpeechRecognitionError,
|
||||
SpeechTranscriber,
|
||||
@@ -193,6 +198,83 @@ class UnderstandingRegressionTests(unittest.TestCase):
|
||||
"将副驾座椅向前调整到0%,然后关闭车窗。",
|
||||
)
|
||||
|
||||
def test_spoken_mode_creation_variants_keep_the_canonical_anchor_first(self) -> None:
|
||||
cases = (
|
||||
(
|
||||
"我想创建一个观影模式,把空调温度调到24度,遮阳帘变成100%",
|
||||
"观影模式",
|
||||
("climate.set_temperature", "roof.set_shade_position"),
|
||||
),
|
||||
(
|
||||
"请你帮我构建一套影院模式,副驾空调调到22.5度,车窗关闭",
|
||||
"影院模式",
|
||||
("climate.set_temperature", "window.set_motion"),
|
||||
),
|
||||
)
|
||||
|
||||
for raw, expected_name, expected_capabilities in cases:
|
||||
with self.subTest(raw=raw):
|
||||
result = self.processor.process(raw)
|
||||
parsed = parse_mode_profile_create(result.assistant_text)
|
||||
self.assertIsNotNone(parsed)
|
||||
self.assertEqual(parsed[0], expected_name)
|
||||
self.assertLess(
|
||||
result.assistant_text.index(expected_name),
|
||||
result.assistant_text.index("空调"),
|
||||
)
|
||||
self.assertEqual(
|
||||
tuple(command.capability for command in result.commands),
|
||||
expected_capabilities,
|
||||
)
|
||||
self.assertFalse(result.execution_eligible)
|
||||
|
||||
def test_mode_draft_update_keeps_name_correction_and_multiple_actions(self) -> None:
|
||||
result = self.processor.process(
|
||||
"把观影模式的空调调到24度,不对,25度,然后把遮阳帘变成100%"
|
||||
)
|
||||
|
||||
parsed = parse_mode_profile_update(result.assistant_text)
|
||||
self.assertIsNotNone(parsed)
|
||||
self.assertEqual(parsed[0], "观影模式")
|
||||
self.assertNotIn("24度", result.assistant_text)
|
||||
self.assertIn("25度", result.assistant_text)
|
||||
self.assertIn("遮阳帘", result.assistant_text)
|
||||
self.assertEqual(
|
||||
[command.capability for command in result.commands],
|
||||
["climate.set_temperature", "roof.set_shade_position"],
|
||||
)
|
||||
self.assertEqual(result.commands[0].parameters, {"temperature_c": 25})
|
||||
self.assertEqual(result.commands[1].parameters, {"position_pct": 100})
|
||||
self.assertFalse(result.execution_eligible)
|
||||
|
||||
def test_mode_query_without_question_mark_is_never_rendered_as_control(self) -> None:
|
||||
raw = "观影模式里遮阳帘是0%还是100%"
|
||||
result = self.processor.process(raw)
|
||||
|
||||
self.assertTrue(is_mode_profile_query(result.assistant_text))
|
||||
self.assertIn("还是", result.assistant_text)
|
||||
self.assertFalse(result.execution_eligible)
|
||||
self.assertNotEqual(result.assistant_text, "将遮阳帘打开到0%。")
|
||||
|
||||
def test_negated_or_cancelled_mode_update_never_becomes_executable(self) -> None:
|
||||
cases = (
|
||||
"把观影模式的遮阳帘变成100%,但不要执行",
|
||||
"把观影模式的遮阳帘变成100%,算了",
|
||||
"不要把观影模式的遮阳帘变成100%",
|
||||
)
|
||||
for raw in cases:
|
||||
with self.subTest(raw=raw):
|
||||
result = self.processor.process(raw)
|
||||
self.assertFalse(result.execution_eligible)
|
||||
self.assertTrue(
|
||||
any(
|
||||
marker in result.assistant_text
|
||||
for marker in ("不要", "算了")
|
||||
)
|
||||
)
|
||||
if "不要执行" in raw:
|
||||
self.assertIn("观影模式", result.assistant_text)
|
||||
|
||||
def test_cancelled_control_can_be_followed_by_an_assistant_question(self) -> None:
|
||||
result = self.processor.process("打开空调,算了,我们聊聊空调为什么制冷。")
|
||||
|
||||
@@ -350,7 +432,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
self.assertFalse(result["execution_eligible"])
|
||||
|
||||
def test_final_wake_word_transcription_strips_only_the_wake_prefix(self) -> None:
|
||||
captured = "你好,双十一,车上有点热。"
|
||||
captured = "你好,小迪,车上有点热。"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, _ = self.make_transcriber(directory, captured)
|
||||
result = transcriber.transcribe_wav(
|
||||
@@ -369,7 +451,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
def test_final_transcription_rejects_missing_or_wake_only_audio(self) -> None:
|
||||
cases = (
|
||||
("车上有点热。", "wake_word_missing"),
|
||||
("你好,双十一。", "empty_after_wake_word"),
|
||||
("你好,小迪。", "empty_after_wake_word"),
|
||||
)
|
||||
for index, (raw, code) in enumerate(cases):
|
||||
with self.subTest(raw=raw), tempfile.TemporaryDirectory() as directory:
|
||||
@@ -385,7 +467,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
def test_live_preview_is_raw_semantic_free_and_wake_prefix_aware(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, recognizer = self.make_transcriber(
|
||||
directory, "你好双十一打开空"
|
||||
directory, "你好小迪打开空"
|
||||
)
|
||||
first_audio = pcm16_wav(seconds=0.5)
|
||||
first = transcriber.transcribe_preview_wav(
|
||||
@@ -393,7 +475,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
utterance_id="unit-preview-live",
|
||||
sequence=0,
|
||||
)
|
||||
recognizer.text = "你好双十一打开空调到24.5度"
|
||||
recognizer.text = "你好小迪打开空调到24.5度"
|
||||
second_audio = pcm16_wav(frequency=330, seconds=0.8)
|
||||
second = transcriber.transcribe_preview_wav(
|
||||
second_audio,
|
||||
@@ -402,11 +484,11 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertEqual(first["state"], "partial")
|
||||
self.assertEqual(first["raw_text"], "你好双十一打开空")
|
||||
self.assertEqual(first["raw_text"], "你好小迪打开空")
|
||||
self.assertEqual(first["live_text"], "打开空")
|
||||
self.assertFalse(first["semantic_cleaned"])
|
||||
self.assertEqual(second["sequence"], 1)
|
||||
self.assertEqual(second["raw_text"], "你好双十一打开空调到24.5度")
|
||||
self.assertEqual(second["raw_text"], "你好小迪打开空调到24.5度")
|
||||
self.assertEqual(second["live_text"], "打开空调到24.5度")
|
||||
self.assertTrue(second["wake_word"]["detected"])
|
||||
self.assertEqual(recognizer.decode_count, 2)
|
||||
@@ -417,14 +499,14 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
conflicting_audio = pcm16_wav(frequency=440, seconds=0.8)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, recognizer = self.make_transcriber(
|
||||
directory, "你好双十一打开空调"
|
||||
directory, "你好小迪打开空调"
|
||||
)
|
||||
first = transcriber.transcribe_preview_wav(
|
||||
first_audio,
|
||||
utterance_id="unit-preview-order",
|
||||
sequence=0,
|
||||
)
|
||||
recognizer.text = "你好双十一打开空调到24度"
|
||||
recognizer.text = "你好小迪打开空调到24度"
|
||||
latest = transcriber.transcribe_preview_wav(
|
||||
second_audio,
|
||||
utterance_id="unit-preview-order",
|
||||
@@ -469,7 +551,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber = SpeechTranscriber(Path(directory))
|
||||
transcriber.preview_min_interval_ms = 0
|
||||
recognizer = _BlockingRecognizer("你好双十一打开空调")
|
||||
recognizer = _BlockingRecognizer("你好小迪打开空调")
|
||||
transcriber._recognizer = recognizer
|
||||
waiter_entered = threading.Event()
|
||||
original_wait = transcriber._wait_for_preview
|
||||
@@ -525,7 +607,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
def test_preview_global_capacity_rejects_immediately(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, recognizer = self.make_transcriber(
|
||||
directory, "你好双十一打开空调"
|
||||
directory, "你好小迪打开空调"
|
||||
)
|
||||
transcriber._preview_request_slots = threading.BoundedSemaphore(1)
|
||||
self.assertTrue(transcriber._preview_request_slots.acquire(blocking=False))
|
||||
@@ -550,7 +632,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
audio = pcm16_wav(seconds=0.5)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
throttled, _ = self.make_transcriber(
|
||||
directory, "你好双十一打开空调"
|
||||
directory, "你好小迪打开空调"
|
||||
)
|
||||
throttled.preview_min_interval_ms = 10_000
|
||||
throttled.transcribe_preview_wav(
|
||||
@@ -563,7 +645,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
sequence=1,
|
||||
)
|
||||
|
||||
limited, _ = self.make_transcriber(directory, "你好双十一打开空调")
|
||||
limited, _ = self.make_transcriber(directory, "你好小迪打开空调")
|
||||
limited.preview_max_updates = 1
|
||||
limited.transcribe_preview_wav(
|
||||
audio, utterance_id="unit-preview-count", sequence=0
|
||||
@@ -584,7 +666,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
def test_windowed_preview_returns_complete_accumulated_live_text(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, recognizer = self.make_transcriber(
|
||||
directory, "你好双十一打开空调"
|
||||
directory, "你好小迪打开空调"
|
||||
)
|
||||
transcriber.preview_window_ms = 400
|
||||
first = transcriber.transcribe_preview_wav(
|
||||
@@ -594,7 +676,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
)
|
||||
# The first tail window may begin inside the spoken wake phrase.
|
||||
# Its remainder must not be appended to the assistant input.
|
||||
recognizer.text = "好双十一打开空调到24度然后打开遮阳帘"
|
||||
recognizer.text = "好小迪打开空调到24度然后打开遮阳帘"
|
||||
second = transcriber.transcribe_preview_wav(
|
||||
pcm16_wav(seconds=1.2),
|
||||
utterance_id="unit-preview-window",
|
||||
@@ -603,7 +685,7 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(first["live_text"], "打开空调")
|
||||
self.assertEqual(
|
||||
second["raw_text"], "好双十一打开空调到24度然后打开遮阳帘"
|
||||
second["raw_text"], "好小迪打开空调到24度然后打开遮阳帘"
|
||||
)
|
||||
self.assertEqual(second["live_text"], "打开空调到24度然后打开遮阳帘")
|
||||
self.assertTrue(second["wake_word"]["detected"])
|
||||
@@ -740,14 +822,14 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
audio = pcm16_wav(seconds=0.8)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
transcriber, recognizer = self.make_transcriber(
|
||||
directory, "你好双十一打开空"
|
||||
directory, "你好小迪打开空"
|
||||
)
|
||||
preview = transcriber.transcribe_preview_wav(
|
||||
audio,
|
||||
utterance_id="unit-preview-final",
|
||||
sequence=0,
|
||||
)
|
||||
recognizer.text = "你好双十一打开空调"
|
||||
recognizer.text = "你好小迪打开空调"
|
||||
final = transcriber.transcribe_wav(
|
||||
audio,
|
||||
utterance_id="unit-preview-final",
|
||||
@@ -805,13 +887,15 @@ class SpeechTranscriberTests(unittest.TestCase):
|
||||
|
||||
|
||||
class WakeWordMatcherTests(unittest.TestCase):
|
||||
def test_default_phrase_accepts_punctuation_spacing_and_numeric_alias(self) -> None:
|
||||
def test_default_phrase_accepts_punctuation_and_spacing(self) -> None:
|
||||
matcher = WakeWordMatcher(enabled=True)
|
||||
self.assertEqual(matcher.primary, "你好,小迪")
|
||||
self.assertEqual(matcher.phrases, ("你好,小迪",))
|
||||
cases = (
|
||||
("你好,双十一,打开空调", "打开空调"),
|
||||
("你好双十一打开空调", "打开空调"),
|
||||
("嗯, 你好 双 11,打开空调", "打开空调"),
|
||||
("请,你好:双十一。什么是CAN FD", "什么是CAN FD"),
|
||||
("你好,小迪,打开空调", "打开空调"),
|
||||
("你好小迪打开空调", "打开空调"),
|
||||
("嗯, 你好 小 迪,打开空调", "打开空调"),
|
||||
("请,你好:小迪。什么是CAN FD", "什么是CAN FD"),
|
||||
)
|
||||
for text, remainder in cases:
|
||||
with self.subTest(text=text):
|
||||
@@ -822,8 +906,9 @@ class WakeWordMatcherTests(unittest.TestCase):
|
||||
def test_wake_phrase_is_anchored_to_the_start_and_avoids_false_positives(self) -> None:
|
||||
matcher = WakeWordMatcher(enabled=True)
|
||||
for text in (
|
||||
"有人说你好双十一会唤醒吗",
|
||||
"双十一购物节有什么活动",
|
||||
"有人说你好小迪会唤醒吗",
|
||||
"你好双十一打开空调",
|
||||
"小迪最近有什么活动",
|
||||
"你好小十一打开空调",
|
||||
"你好十一打开空调",
|
||||
):
|
||||
@@ -835,8 +920,8 @@ class WakeWordMatcherTests(unittest.TestCase):
|
||||
def test_wake_phrase_recovers_common_self_repetition_without_leaking_it(self) -> None:
|
||||
matcher = WakeWordMatcher(enabled=True)
|
||||
for text in (
|
||||
"你你好双十一打开空调",
|
||||
"你好你好双十一打开空调",
|
||||
"你你好小迪打开空调",
|
||||
"你好你好小迪打开空调",
|
||||
):
|
||||
with self.subTest(text=text):
|
||||
result = matcher.match(text)
|
||||
@@ -845,9 +930,9 @@ class WakeWordMatcherTests(unittest.TestCase):
|
||||
|
||||
def test_disabled_matcher_preserves_input_without_activating(self) -> None:
|
||||
matcher = WakeWordMatcher(enabled=False)
|
||||
result = matcher.match("你好,双十一,打开空调")
|
||||
result = matcher.match("你好,小迪,打开空调")
|
||||
self.assertFalse(result.detected)
|
||||
self.assertEqual(result.remainder, "你好,双十一,打开空调")
|
||||
self.assertEqual(result.remainder, "你好,小迪,打开空调")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user