feat: integrate production cockpit AI assistant

This commit is contained in:
vee
2026-09-08 17:46:07 +08:00
parent 5e228e12ae
commit 93103c2579
66 changed files with 11046 additions and 374 deletions
+113 -28
View File
@@ -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__":