"""MockASRService 单测 — Mock ASR服务纯逻辑部分.""" from __future__ import annotations from pathlib import Path import pytest from packages.adapters.asr.mock_asr_service import MockASRService from packages.ports.asr_service import ASRServiceError # ── Fixtures ──────────────────────────────────────────────────────────────── @pytest.fixture def service(): return MockASRService() # ── transcribe 基础行为 ──────────────────────────────────────────────────────── class TestTranscribeBasic: """transcribe 基本行为.""" def test_audio_not_found_raises(self, service, tmp_path): """音频文件不存在时抛 ASRServiceError.""" with pytest.raises(ASRServiceError): service.transcribe(tmp_path / "nonexistent.wav") def test_default_mock_text_produces_timeline(self, service, tmp_path): """不传 mock_text 时生成默认测试字幕.""" audio = tmp_path / "test.wav" audio.write_bytes(b"fake audio") timeline = service.transcribe(audio) assert timeline is not None assert timeline.language == "zh" assert timeline.total_duration > 0 assert len(timeline.segments) > 0 def test_custom_mock_text(self, tmp_path): """使用自定义 mock_text.""" text = "你好世界。今天天气真好!" service = MockASRService(mock_text=text) audio = tmp_path / "test.wav" audio.write_bytes(b"fake audio") timeline = service.transcribe(audio) assert timeline.total_duration > 0 # 两句话,应该有2个segment assert len(timeline.segments) == 2 assert "你好世界" in timeline.segments[0].text assert "今天天气真好" in timeline.segments[1].text def test_reads_txt_file(self, tmp_path): """音频同目录同名 txt 文件存在时,读取其内容.""" audio = tmp_path / "voice.wav" audio.write_bytes(b"fake audio") txt = tmp_path / "voice.txt" txt.write_text("这是从文件读取的。内容。", encoding="utf-8") service = MockASRService() timeline = service.transcribe(audio) assert len(timeline.segments) == 2 assert "从文件读取" in timeline.segments[0].text def test_mock_text_takes_priority_over_txt(self, tmp_path): """mock_text 参数优先于 txt 文件.""" audio = tmp_path / "test.wav" audio.write_bytes(b"fake audio") txt = tmp_path / "test.txt" txt.write_text("文件里的文字。", encoding="utf-8") service = MockASRService(mock_text="自定义的。优先!") timeline = service.transcribe(audio) assert "自定义的" in timeline.segments[0].text assert "文件" not in timeline.segments[0].text def test_custom_language(self, service, tmp_path): """可以指定语言.""" audio = tmp_path / "test.wav" audio.write_bytes(b"fake audio") timeline = service.transcribe(audio, language="en") assert timeline.language == "en" # ── _text_to_segments 切分逻辑 ────────────────────────────────────────── class TestTextToSegments: """_text_to_segments 文本切分逻辑.""" def setup_method(self): self.service = MockASRService() def test_split_by_chinese_period(self): """按中文句号切分.""" text = "第一句。第二句。第三句。" segments = self.service._text_to_segments(text, 10.0, with_word_timestamps=False) assert len(segments) == 3 def test_split_by_exclamation(self): """按感叹号切分.""" text = "你好!世界!" segments = self.service._text_to_segments(text, 5.0, with_word_timestamps=False) assert len(segments) == 2 def test_split_by_question_mark(self): """按问号切分.""" text = "你好吗?我很好。" segments = self.service._text_to_segments(text, 5.0, with_word_timestamps=False) assert len(segments) == 2 def test_mixed_punctuation(self): """混合标点.""" text = "你好!你是谁?我是测试。再见!" segments = self.service._text_to_segments(text, 10.0, with_word_timestamps=False) assert len(segments) == 4 def test_empty_text_returns_empty(self): """空文本返回空列表.""" segments = self.service._text_to_segments("", 5.0, with_word_timestamps=False) assert segments == [] def test_no_punctuation_single_segment(self): """没有标点时整段作为一个segment.""" text = "这是一段没有标点的文字" segments = self.service._text_to_segments(text, 5.0, with_word_timestamps=False) assert len(segments) == 1 assert segments[0].text == text def test_segments_duration_adds_up(self): """所有segment时长加起来约等于总时长.""" text = "第一句。第二句。" total = 10.0 segments = self.service._text_to_segments(text, total, with_word_timestamps=False) sum_duration = sum(s.end - s.start for s in segments) assert abs(sum_duration - total) < 0.01 def test_segments_sequential(self): """segment按顺序排列,首尾相接.""" text = "第一句。第二句。第三句。" segments = self.service._text_to_segments(text, 9.0, with_word_timestamps=False) assert segments[0].start == 0.0 for i in range(1, len(segments)): assert abs(segments[i].start - segments[i - 1].end) < 0.001 def test_with_word_timestamps(self): """带词级时间戳时,每个字一个word.""" text = "你好世界!" segments = self.service._text_to_segments(text, 2.0, with_word_timestamps=True) assert len(segments) == 1 # 4个汉字 + 1个感叹号 = 5个字符 assert len(segments[0].words) == 5 def test_word_timestamps_sequential(self): """词级时间戳按顺序排列.""" text = "你好!" segments = self.service._text_to_segments(text, 3.0, with_word_timestamps=True) words = segments[0].words assert len(words) == 3 assert words[0].start == 0.0 # 最后一个词的结束时间约等于 segment 结束时间 assert abs(words[-1].end - segments[0].end) < 0.01 def test_without_word_timestamps(self): """不带词级时间戳时,words为空.""" text = "你好世界。" segments = self.service._text_to_segments(text, 2.0, with_word_timestamps=False) assert len(segments) == 1 assert segments[0].words == [] def test_duration_proportional_to_length(self): """长句子占时长,短句子占时短.""" text = "短。很长很长很长很长的句子。" segments = self.service._text_to_segments(text, 10.0, with_word_timestamps=False) # 第一句1个字,第二句9个字(包括标点) # 第一句时长应该比第二句短 dur0 = segments[0].end - segments[0].start dur1 = segments[1].end - segments[1].start assert dur1 > dur0 # 第二句更长