from __future__ import annotations import io import json import math import tempfile import threading import time import unittest import wave from pathlib import Path from types import SimpleNamespace from cognitive_agent.infrastructure.speech.sensevoice import ( SpeechRecognitionError, SpeechTranscriber, ) from cognitive_agent.infrastructure.speech.understanding import ( UnderstandingTranscriptProcessor, ) from cognitive_agent.infrastructure.speech.wake_word import WakeWordMatcher REPOSITORY_ROOT = Path(__file__).resolve().parents[2] def pcm16_wav(*, frequency: float = 220, seconds: float = 0.8) -> bytes: frames = bytearray() for index in range(round(16_000 * seconds)): sample = round(math.sin(index * 2 * math.pi * frequency / 16_000) * 12_000) frames.extend(int(sample).to_bytes(2, "little", signed=True)) target = io.BytesIO() with wave.open(target, "wb") as output: output.setnchannels(1) output.setsampwidth(2) output.setframerate(16_000) output.writeframes(frames) return target.getvalue() class _FakeStream: def __init__(self, text: str) -> None: self.result = SimpleNamespace( text=f"<|zh|><|NEUTRAL|><|Speech|>{text}", lang="<|zh|>", emotion="<|NEUTRAL|>", event="<|Speech|>", ) def accept_waveform(self, sample_rate: int, samples: object) -> None: self.sample_rate = sample_rate self.samples = samples class _FakeRecognizer: def __init__(self, text: str) -> None: self.text = text self.decode_count = 0 self.decoded_sample_counts: list[int] = [] def create_stream(self) -> _FakeStream: return _FakeStream(self.text) def decode_stream(self, stream: _FakeStream) -> None: self.decode_count += 1 self.decoded_sample_counts.append(len(stream.samples)) class _BlockingRecognizer(_FakeRecognizer): def __init__(self, text: str) -> None: super().__init__(text) self.decode_started = threading.Event() self.release_decode = threading.Event() def decode_stream(self, stream: _FakeStream) -> None: super().decode_stream(stream) self.decode_started.set() if not self.release_decode.wait(timeout=5): raise RuntimeError("test recognizer was not released") class UnderstandingRegressionTests(unittest.TestCase): @classmethod def setUpClass(cls) -> None: cls.processor = UnderstandingTranscriptProcessor(REPOSITORY_ROOT) fixture = REPOSITORY_ROOT / "tests" / "fixtures" / "speech_text_regression.json" cls.cases = json.loads(fixture.read_text(encoding="utf-8")) def test_text_regression_suite(self) -> None: for case in self.cases: with self.subTest(case=case["name"]): result = self.processor.process(case["raw"]) self.assertEqual(result.raw_text, case["raw"]) self.assertEqual(result.clean_text, case["clean"]) self.assertEqual( result.assistant_text, case.get("assistant", case["clean"]), ) serialized = result.to_dict() self.assertEqual(serialized["text"], result.assistant_text) self.assertEqual(serialized["assistant_text"], result.assistant_text) self.assertEqual(serialized["control_text"], result.clean_text) self.assertEqual( [command.capability for command in result.commands], case["capabilities"], ) self.assertEqual(len(result.cancelled_fragments), case["cancelled"]) self.assertEqual(result.uncertain, case["uncertain"]) self.assertEqual( result.execution_eligible, case.get( "execution_eligible", bool(result.commands) and not result.uncertain, ), ) if preserved := case.get("preserved_filler"): self.assertNotIn(preserved, result.removed_fillers) def test_cancelled_commands_never_appear_as_active(self) -> None: result = self.processor.process( "打开空调,不对,把座椅靠背向后调24度。" ) self.assertEqual([command.domain for command in result.commands], ["seat"]) cancelled = result.cancelled_fragments[0].commands self.assertEqual([command.domain for command in cancelled], ["climate"]) self.assertTrue(all(command.status == "cancelled" for command in cancelled)) def test_invalid_seat_absolute_angle_fails_closed(self) -> None: result = self.processor.process("把座椅靠背调整到24度。") self.assertTrue(result.uncertain) self.assertFalse(result.execution_eligible) self.assertIn("低于工程下限95", "".join(result.reasons)) def test_regular_multi_command_is_not_treated_as_correction(self) -> None: result = self.processor.process("打开空调,然后打开氛围灯。") self.assertEqual(len(result.commands), 2) self.assertFalse(result.cancelled_fragments) def test_meaningful_non_control_speech_remains_available_to_assistant(self) -> None: cases = ( ("今天天气怎么样?", "今天天气怎么样", "no_command"), ("我们中午吃什么?", "我们中午吃什么", "no_command"), ("车上有点热。", "车上有点热", "no_command"), ("打开雨刷。", "打开雨刷", "incomplete"), ("空调挺凉快。", "空调挺凉快", "incomplete"), ) for raw, preserved, status in cases: with self.subTest(raw=raw): result = self.processor.process(raw) self.assertIn(preserved, result.assistant_text) self.assertTrue(result.assistant_text.strip()) self.assertEqual(result.semantic_status, status) self.assertFalse(result.execution_eligible) self.assertFalse(result.commands) def test_control_and_chat_in_same_spoken_block_are_both_preserved_for_assistant(self) -> None: result = self.processor.process("打开空调,今天天气怎么样?") self.assertIn("打开空调", result.assistant_text) self.assertIn("今天天气怎么样", result.assistant_text) self.assertEqual( [command.capability for command in result.commands], ["climate.set_power"], ) self.assertEqual(result.clean_text, "打开空调。") self.assertFalse(result.execution_eligible) self.assertTrue(result.uncertain) def test_mode_creation_stays_before_its_normalized_control_definition(self) -> None: result = self.processor.process( "创建一个躺平模式,副驾座椅水平向前移动到最大位置,车窗关闭" ) self.assertEqual( result.assistant_text, "创建一个躺平模式,将副驾座椅向前调整到0%,然后关闭车窗。", ) self.assertEqual(result.assistant_text.count("躺平模式"), 1) self.assertEqual( [command.capability for command in result.commands], ["seat.set_longitudinal_position", "window.set_motion"], ) self.assertEqual(result.commands[0].parameters, {"position_pct": 0}) self.assertEqual( result.clean_text, "将副驾座椅向前调整到0%,然后关闭车窗。", ) ordinary_controls = self.processor.process( "副驾座椅水平向前移动到最大位置,车窗关闭" ) self.assertEqual( ordinary_controls.assistant_text, "将副驾座椅向前调整到0%,然后关闭车窗。", ) def test_cancelled_control_can_be_followed_by_an_assistant_question(self) -> None: result = self.processor.process("打开空调,算了,我们聊聊空调为什么制冷。") self.assertIn("我们聊聊空调为什么制冷", result.assistant_text) self.assertNotIn("打开空调", result.assistant_text) self.assertFalse(result.commands) self.assertFalse(result.execution_eligible) self.assertEqual(len(result.cancelled_fragments), 1) self.assertEqual( [command.capability for command in result.cancelled_fragments[0].commands], ["climate.set_power"], ) def test_general_question_correction_keeps_only_latest_meaningful_request(self) -> None: result = self.processor.process("帮我查北京天气,不对,上海天气。") self.assertIn("上海天气", result.assistant_text) self.assertNotIn("北京天气", result.assistant_text) self.assertFalse(result.commands) self.assertFalse(result.execution_eligible) self.assertEqual(len(result.cancelled_fragments), 1) def test_general_questions_do_not_split_connector_characters_inside_words(self) -> None: cases = ( "再生能源是什么?", "并发编程是什么意思?", "先锋队是什么意思?", "最后生还者讲了什么?", "我想了解再生能源和并发编程。", "不对称加密是什么?", "如何把PDF改成Word?", ) for raw in cases: with self.subTest(raw=raw): result = self.processor.process(raw) self.assertEqual(result.assistant_text, raw) self.assertFalse(result.commands) self.assertFalse(result.execution_eligible) def test_control_and_chat_joined_by_coordination_are_both_preserved(self) -> None: result = self.processor.process("打开空调和告诉我天气") self.assertIn("打开空调", result.assistant_text) self.assertIn("告诉我天气", result.assistant_text) self.assertEqual( [command.capability for command in result.commands], ["climate.set_power"], ) self.assertEqual(result.clean_text, "打开空调。") self.assertTrue(result.uncertain) self.assertFalse(result.execution_eligible) def test_negated_control_remains_conversational_but_never_executable(self) -> None: result = self.processor.process("不要打开空调,告诉我空调为什么制冷。") self.assertIn("不要打开空调", result.assistant_text) self.assertIn("告诉我空调为什么制冷", result.assistant_text) self.assertFalse(result.commands) self.assertFalse(result.execution_eligible) self.assertTrue(result.cancelled_fragments) self.assertTrue( all( command.negated and command.status == "cancelled" for fragment in result.cancelled_fragments for command in fragment.commands ) ) def test_multiple_devices_numbers_and_decimal_values_keep_clause_binding(self) -> None: result = self.processor.process( "把主驾温度调到22.5度,副驾调到24度,再把风量调到2档。" ) self.assertTrue(result.execution_eligible) self.assertEqual( [ (command.capability, command.target_scope, command.parameters) for command in result.commands ], [ ("climate.set_temperature", "driver", {"temperature_c": 22.5}), ("climate.set_temperature", "passenger", {"temperature_c": 24}), ("climate.set_fan_level", "vehicle", {"level": 2}), ], ) for token in ("主驾", "22.5度", "副驾", "24度", "2档"): self.assertIn(token, result.assistant_text) def test_number_correction_does_not_steal_other_device_value(self) -> None: result = self.processor.process( "主驾温度调到22度,不对,22.5度,然后副驾调到24度。" ) self.assertTrue(result.execution_eligible) temperatures = [ (command.target_scope, command.parameters["temperature_c"]) for command in result.commands if command.capability == "climate.set_temperature" ] self.assertEqual(temperatures, [("driver", 22.5), ("passenger", 24)]) self.assertNotIn("22度", result.assistant_text) self.assertIn("22.5度", result.assistant_text) self.assertIn("24度", result.assistant_text) class SpeechTranscriberTests(unittest.TestCase): def make_transcriber(self, directory: str, text: str) -> tuple[SpeechTranscriber, _FakeRecognizer]: transcriber = SpeechTranscriber(Path(directory)) # Most unit calls intentionally run back-to-back. Production keeps a # 500 ms floor, which has dedicated rate-limit coverage below. transcriber.preview_min_interval_ms = 0 recognizer = _FakeRecognizer(text) transcriber._recognizer = recognizer return transcriber, recognizer def test_transcriber_preserves_raw_and_returns_structured_result(self) -> None: raw = "打开空调,不对,把座椅靠背向后调24度。" with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, raw) result = transcriber.transcribe_wav( pcm16_wav(), utterance_id="unit-structured" ) self.assertEqual(result["raw_text"], raw) self.assertEqual(result["clean_text"], "将座椅靠背向后调整24度。") self.assertEqual(result["commands"][0]["domain"], "seat") self.assertEqual(result["cancelled_fragments"][0]["commands"][0]["domain"], "climate") self.assertIn("timings", result) self.assertEqual(result["segments"][0]["state"], "final") def test_filler_only_is_evidence_but_not_a_control_error(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "嗯嗯。") result = transcriber.transcribe_wav( pcm16_wav(), utterance_id="unit-filler" ) self.assertEqual(result["raw_text"], "嗯嗯。") self.assertEqual(result["clean_text"], "") self.assertEqual(result["commands"], []) self.assertFalse(result["execution_eligible"]) def test_general_question_is_returned_as_assistant_text_without_control(self) -> None: raw = "什么是CAN FD?" with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, raw) result = transcriber.transcribe_wav( pcm16_wav(), utterance_id="unit-general-question" ) self.assertEqual(result["raw_text"], raw) self.assertIn("什么是CAN FD", result["assistant_text"]) self.assertEqual(result["text"], result["assistant_text"]) self.assertEqual(result["clean_text"], "") self.assertEqual(result["control_text"], "") self.assertEqual(result["commands"], []) self.assertFalse(result["execution_eligible"]) def test_final_wake_word_transcription_strips_only_the_wake_prefix(self) -> None: captured = "你好,双十一,车上有点热。" with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, captured) result = transcriber.transcribe_wav( pcm16_wav(), utterance_id="unit-wake-final", require_wake_word=True, ) self.assertEqual(result["captured_text"], captured) self.assertEqual(result["raw_text"], "车上有点热。") self.assertEqual(result["assistant_text"], "车上有点热。") self.assertTrue(result["wake_word"]["detected"]) self.assertEqual(result["wake_word"]["remainder"], "车上有点热。") self.assertFalse(result["execution_eligible"]) def test_final_transcription_rejects_missing_or_wake_only_audio(self) -> None: cases = ( ("车上有点热。", "wake_word_missing"), ("你好,双十一。", "empty_after_wake_word"), ) for index, (raw, code) in enumerate(cases): with self.subTest(raw=raw), tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, raw) with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_wav( pcm16_wav(), utterance_id=f"unit-wake-rejected-{index}", require_wake_word=True, ) self.assertEqual(caught.exception.code, code) 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, "你好双十一打开空" ) first_audio = pcm16_wav(seconds=0.5) first = transcriber.transcribe_preview_wav( first_audio, utterance_id="unit-preview-live", sequence=0, ) recognizer.text = "你好双十一打开空调到24.5度" second_audio = pcm16_wav(frequency=330, seconds=0.8) second = transcriber.transcribe_preview_wav( second_audio, utterance_id="unit-preview-live", sequence=1, ) self.assertEqual(first["state"], "partial") 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["live_text"], "打开空调到24.5度") self.assertTrue(second["wake_word"]["detected"]) self.assertEqual(recognizer.decode_count, 2) def test_live_preview_duplicate_stale_and_conflicting_sequences_are_safe(self) -> None: first_audio = pcm16_wav(seconds=0.5) second_audio = pcm16_wav(frequency=330, seconds=0.8) conflicting_audio = pcm16_wav(frequency=440, seconds=0.8) with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber( directory, "你好双十一打开空调" ) first = transcriber.transcribe_preview_wav( first_audio, utterance_id="unit-preview-order", sequence=0, ) recognizer.text = "你好双十一打开空调到24度" latest = transcriber.transcribe_preview_wav( second_audio, utterance_id="unit-preview-order", sequence=1, ) duplicate = transcriber.transcribe_preview_wav( second_audio, utterance_id="unit-preview-order", sequence=1, ) stale = transcriber.transcribe_preview_wav( first_audio, utterance_id="unit-preview-order", sequence=0, ) with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_preview_wav( conflicting_audio, utterance_id="unit-preview-order", sequence=1, ) self.assertFalse(first["idempotent_replay"]) self.assertFalse(latest["stale"]) self.assertTrue(duplicate["idempotent_replay"]) self.assertFalse(duplicate["stale"]) self.assertEqual(duplicate["live_text"], "打开空调到24度") self.assertTrue(stale["stale"]) self.assertTrue(stale["idempotent_replay"]) self.assertEqual(stale["requested_sequence"], 0) self.assertEqual(stale["sequence"], 1) for sensitive_key in ("raw_text", "text", "live_text", "wake_word"): self.assertNotIn(sensitive_key, stale) self.assertEqual(caught.exception.code, "preview_sequence_conflict") self.assertEqual(recognizer.decode_count, 2) def test_concurrent_identical_preview_decodes_once_and_inflight_conflict_is_409( self, ) -> None: audio = pcm16_wav(seconds=0.5) conflicting_audio = pcm16_wav(frequency=440, seconds=0.5) with tempfile.TemporaryDirectory() as directory: transcriber = SpeechTranscriber(Path(directory)) transcriber.preview_min_interval_ms = 0 recognizer = _BlockingRecognizer("你好双十一打开空调") transcriber._recognizer = recognizer waiter_entered = threading.Event() original_wait = transcriber._wait_for_preview def observed_wait(inflight: object) -> dict[str, object]: waiter_entered.set() return original_wait(inflight) # type: ignore[arg-type] transcriber._wait_for_preview = observed_wait # type: ignore[method-assign] results: list[dict[str, object]] = [] failures: list[BaseException] = [] def request() -> None: try: results.append( transcriber.transcribe_preview_wav( audio, utterance_id="unit-preview-concurrent", sequence=3, ) ) except Exception as exc: # noqa: BLE001 # pragma: no cover - assertion aid failures.append(exc) owner = threading.Thread(target=request) owner.start() self.assertTrue(recognizer.decode_started.wait(timeout=2)) with self.assertRaises(SpeechRecognitionError) as conflict: transcriber.transcribe_preview_wav( conflicting_audio, utterance_id="unit-preview-concurrent", sequence=3, ) waiter = threading.Thread(target=request) waiter.start() self.assertTrue(waiter_entered.wait(timeout=2)) recognizer.release_decode.set() owner.join(timeout=2) waiter.join(timeout=2) self.assertFalse(owner.is_alive()) self.assertFalse(waiter.is_alive()) self.assertEqual(conflict.exception.code, "preview_sequence_conflict") self.assertEqual(failures, []) self.assertEqual(recognizer.decode_count, 1) self.assertEqual(len(results), 2) self.assertEqual( sorted(bool(item["idempotent_replay"]) for item in results), [False, True], ) self.assertEqual({item["live_text"] for item in results}, {"打开空调"}) def test_preview_global_capacity_rejects_immediately(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber( directory, "你好双十一打开空调" ) transcriber._preview_request_slots = threading.BoundedSemaphore(1) self.assertTrue(transcriber._preview_request_slots.acquire(blocking=False)) started = time.perf_counter() try: with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.5), utterance_id="unit-preview-overload", sequence=0, ) finally: transcriber._preview_request_slots.release() self.assertLess(time.perf_counter() - started, 0.2) self.assertEqual(caught.exception.code, "preview_overloaded") self.assertEqual(caught.exception.status, 429) self.assertEqual(caught.exception.retry_after_seconds, 1) self.assertEqual(recognizer.decode_count, 0) def test_preview_per_utterance_interval_and_update_limit_are_enforced(self) -> None: audio = pcm16_wav(seconds=0.5) with tempfile.TemporaryDirectory() as directory: throttled, _ = self.make_transcriber( directory, "你好双十一打开空调" ) throttled.preview_min_interval_ms = 10_000 throttled.transcribe_preview_wav( audio, utterance_id="unit-preview-throttle", sequence=0 ) with self.assertRaises(SpeechRecognitionError) as rate_limited: throttled.transcribe_preview_wav( pcm16_wav(frequency=330, seconds=0.6), utterance_id="unit-preview-throttle", sequence=1, ) limited, _ = self.make_transcriber(directory, "你好双十一打开空调") limited.preview_max_updates = 1 limited.transcribe_preview_wav( audio, utterance_id="unit-preview-count", sequence=0 ) with self.assertRaises(SpeechRecognitionError) as count_limited: limited.transcribe_preview_wav( pcm16_wav(frequency=330, seconds=0.6), utterance_id="unit-preview-count", sequence=1, ) self.assertEqual(rate_limited.exception.code, "preview_rate_limited") self.assertEqual(rate_limited.exception.status, 429) self.assertGreaterEqual(rate_limited.exception.retry_after_seconds or 0, 1) self.assertEqual(count_limited.exception.code, "preview_limit_reached") self.assertEqual(count_limited.exception.status, 429) def test_windowed_preview_returns_complete_accumulated_live_text(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber( directory, "你好双十一打开空调" ) transcriber.preview_window_ms = 400 first = transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.3), utterance_id="unit-preview-window", sequence=0, ) # The first tail window may begin inside the spoken wake phrase. # Its remainder must not be appended to the assistant input. recognizer.text = "好双十一打开空调到24度然后打开遮阳帘" second = transcriber.transcribe_preview_wav( pcm16_wav(seconds=1.2), utterance_id="unit-preview-window", sequence=1, ) self.assertEqual(first["live_text"], "打开空调") self.assertEqual( second["raw_text"], "好双十一打开空调到24度然后打开遮阳帘" ) self.assertEqual(second["live_text"], "打开空调到24度然后打开遮阳帘") self.assertTrue(second["wake_word"]["detected"]) self.assertTrue(second["wake_word"]["latched"]) self.assertEqual( second["wake_word"]["remainder"], second["live_text"], ) self.assertEqual(recognizer.decoded_sample_counts, [4_800, 6_400]) self.assertTrue(second["preview_window"]["bounded"]) self.assertEqual(second["preview_window"]["start_ms"], 800) self.assertTrue(second["preview_window"]["complete_live_text"]) def test_unawakened_fixed_prefix_is_not_decoded_repeatedly(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber(directory, "普通环境声音") transcriber.preview_window_ms = 400 first = transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.6), utterance_id="unit-preview-unawakened-prefix", sequence=0, ) recognizer.text = "这段内容不应被再次解码" second = transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.9), utterance_id="unit-preview-unawakened-prefix", sequence=1, ) self.assertFalse(first["wake_word"]["detected"]) self.assertEqual(first["live_text"], "") self.assertEqual(second["live_text"], "") self.assertEqual(second["raw_text"], "普通环境声音") self.assertEqual(second["duration_ms"], 900) self.assertTrue(second["preview_window"]["reused"]) self.assertEqual(recognizer.decode_count, 1) def test_final_transcription_has_priority_over_new_preview_work(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber = SpeechTranscriber(Path(directory)) transcriber.preview_min_interval_ms = 0 recognizer = _BlockingRecognizer("什么是CAN FD?") transcriber._recognizer = recognizer final_failures: list[BaseException] = [] def finalize() -> None: try: transcriber.transcribe_wav( pcm16_wav(), utterance_id="unit-final-priority" ) except Exception as exc: # noqa: BLE001 # pragma: no cover - assertion aid final_failures.append(exc) final_thread = threading.Thread(target=finalize) final_thread.start() self.assertTrue(recognizer.decode_started.wait(timeout=2)) with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.5), utterance_id="unit-preview-during-final", sequence=0, ) recognizer.release_decode.set() final_thread.join(timeout=2) self.assertFalse(final_thread.is_alive()) self.assertEqual(final_failures, []) self.assertEqual(caught.exception.code, "preview_finalizing") self.assertEqual(caught.exception.status, 429) self.assertEqual(recognizer.decode_count, 1) def test_background_warmup_is_nonblocking_and_reports_loading_ready_error( self, ) -> None: with tempfile.TemporaryDirectory() as directory: transcriber = SpeechTranscriber(Path(directory)) recognizer = _FakeRecognizer("ready") loading = threading.Event() release = threading.Event() def delayed_load() -> _FakeRecognizer: loading.set() if not release.wait(timeout=5): raise RuntimeError("test warmup was not released") transcriber._recognizer = recognizer return recognizer transcriber._get_recognizer = delayed_load # type: ignore[method-assign] started = time.perf_counter() transcriber.start() self.assertLess(time.perf_counter() - started, 0.2) self.assertTrue(loading.wait(timeout=2)) self.assertEqual(transcriber.status()["load_state"], "loading") release.set() assert transcriber._warmup_thread is not None transcriber._warmup_thread.join(timeout=2) ready_status = transcriber.status() failed = SpeechTranscriber(Path(directory)) def failed_load() -> object: raise SpeechRecognitionError( "model_unavailable", "model missing", status=503 ) failed._get_recognizer = failed_load # type: ignore[method-assign] failed.start() assert failed._warmup_thread is not None failed._warmup_thread.join(timeout=2) failed_status = failed.status() self.assertFalse(transcriber._warmup_thread.is_alive()) self.assertEqual(ready_status["load_state"], "ready") self.assertTrue(ready_status["loaded"]) self.assertEqual(ready_status["warmup"]["state"], "ready") self.assertFalse(failed._warmup_thread.is_alive()) self.assertEqual(failed_status["load_state"], "error") self.assertEqual(failed_status["warmup"]["error"], "model_unavailable") def test_preview_without_wake_word_never_exposes_live_assistant_text(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "打开空调") result = transcriber.transcribe_preview_wav( pcm16_wav(seconds=0.5), utterance_id="unit-preview-not-awake", sequence=0, ) self.assertEqual(result["raw_text"], "打开空调") self.assertEqual(result["live_text"], "") self.assertFalse(result["wake_word"]["detected"]) def test_preview_and_final_can_share_utterance_without_cache_collision(self) -> None: audio = pcm16_wav(seconds=0.8) with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber( directory, "你好双十一打开空" ) preview = transcriber.transcribe_preview_wav( audio, utterance_id="unit-preview-final", sequence=0, ) recognizer.text = "你好双十一打开空调" final = transcriber.transcribe_wav( audio, utterance_id="unit-preview-final", require_wake_word=True, ) self.assertEqual(preview["state"], "partial") self.assertEqual(final["raw_text"], "打开空调") self.assertEqual(final["clean_text"], "打开空调。") self.assertEqual(final["assistant_text"], "打开空调。") self.assertEqual(recognizer.decode_count, 2) def test_same_utterance_and_audio_is_decoded_once(self) -> None: audio = pcm16_wav() with tempfile.TemporaryDirectory() as directory: transcriber, recognizer = self.make_transcriber(directory, "打开空调。") first = transcriber.transcribe_wav(audio, utterance_id="unit-idempotent") second = transcriber.transcribe_wav(audio, utterance_id="unit-idempotent") self.assertEqual(recognizer.decode_count, 1) self.assertFalse(first["idempotent_replay"]) self.assertTrue(second["idempotent_replay"]) def test_cached_result_is_server_owned_and_returned_as_a_copy(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "打开空调。") transcriber.transcribe_wav(pcm16_wav(), utterance_id="unit-cached") first = transcriber.cached_result("unit-cached") first["clean_text"] = "tampered" second = transcriber.cached_result("unit-cached") self.assertNotEqual(second["clean_text"], "tampered") def test_missing_cached_result_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "打开空调。") with self.assertRaises(SpeechRecognitionError) as caught: transcriber.cached_result("unit-expired") self.assertEqual(caught.exception.code, "speech_result_expired") def test_same_utterance_with_different_audio_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "打开空调。") transcriber.transcribe_wav(pcm16_wav(), utterance_id="unit-conflict") with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_wav( pcm16_wav(frequency=330), utterance_id="unit-conflict" ) self.assertEqual(caught.exception.code, "utterance_id_conflict") def test_invalid_utterance_id_is_rejected(self) -> None: with tempfile.TemporaryDirectory() as directory: transcriber, _ = self.make_transcriber(directory, "打开空调。") with self.assertRaises(SpeechRecognitionError) as caught: transcriber.transcribe_wav(pcm16_wav(), utterance_id="bad id/路径") self.assertEqual(caught.exception.code, "invalid_utterance_id") class WakeWordMatcherTests(unittest.TestCase): def test_default_phrase_accepts_punctuation_spacing_and_numeric_alias(self) -> None: matcher = WakeWordMatcher(enabled=True) cases = ( ("你好,双十一,打开空调", "打开空调"), ("你好双十一打开空调", "打开空调"), ("嗯, 你好 双 11,打开空调", "打开空调"), ("请,你好:双十一。什么是CAN FD", "什么是CAN FD"), ) for text, remainder in cases: with self.subTest(text=text): result = matcher.match(text) self.assertTrue(result.detected) self.assertEqual(result.remainder, remainder) def test_wake_phrase_is_anchored_to_the_start_and_avoids_false_positives(self) -> None: matcher = WakeWordMatcher(enabled=True) for text in ( "有人说你好双十一会唤醒吗", "双十一购物节有什么活动", "你好小十一打开空调", "你好十一打开空调", ): with self.subTest(text=text): result = matcher.match(text) self.assertFalse(result.detected) self.assertEqual(result.remainder, "") 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) self.assertTrue(result.detected) self.assertEqual(result.remainder, "打开空调") def test_disabled_matcher_preserves_input_without_activating(self) -> None: matcher = WakeWordMatcher(enabled=False) result = matcher.match("你好,双十一,打开空调") self.assertFalse(result.detected) self.assertEqual(result.remainder, "你好,双十一,打开空调") if __name__ == "__main__": unittest.main()