"""TTS配音引擎纯逻辑测试 — 数据结构 + 边界情况(mock TTS服务).""" from __future__ import annotations from pathlib import Path from unittest.mock import MagicMock import pytest from video_processing.tts_engine import ( TtsEngine, VoiceoverResult, VoiceoverSegment, ) from packages.domain.tts_config import TtsConfig from packages.ports.tts_service import TtsError class TestVoiceoverSegment: """VoiceoverSegment 数据结构测试.""" def test_default_values(self): """默认值正确.""" seg = VoiceoverSegment(text="你好") assert seg.text == "你好" assert seg.start_time == 0.0 assert seg.end_time == 0.0 assert seg.audio_path is None assert seg.duration == 0.0 def test_full_values(self): """完整字段.""" seg = VoiceoverSegment( text="测试", start_time=1.5, end_time=3.5, audio_path=Path("/tmp/test.wav"), duration=2.0, ) assert seg.text == "测试" assert seg.start_time == 1.5 assert seg.end_time == 3.5 assert seg.audio_path == Path("/tmp/test.wav") assert seg.duration == 2.0 class TestVoiceoverResult: """VoiceoverResult 数据结构测试.""" def test_failure_default(self): """失败结果默认值.""" result = VoiceoverResult(success=False) assert result.success is False assert result.segments == [] assert result.total_duration == 0.0 assert result.error_message == "" def test_success_with_segments(self): """成功结果带片段.""" seg = VoiceoverSegment(text="hi", duration=5.0) result = VoiceoverResult( success=True, segments=[seg], total_duration=5.0, ) assert result.success is True assert len(result.segments) == 1 assert result.total_duration == 5.0 def test_failure_with_message(self): """失败带错误信息.""" result = VoiceoverResult(success=False, error_message="TTS出错") assert result.success is False assert result.error_message == "TTS出错" class TestTtsEngineFullVoiceover: """TtsEngine.generate_full_voiceover 整段配音测试(mock TTS).""" def _make_engine(self, tmp_path: Path, tts: MagicMock | None = None) -> TtsEngine: """创建测试用TtsEngine.""" if tts is None: tts = MagicMock() tts.synthesize.return_value = str(tmp_path / "out.wav") return TtsEngine(tts_service=tts, work_dir=tmp_path) def test_disabled_returns_failure(self, tmp_path): """配音未启用→失败.""" engine = self._make_engine(tmp_path) config = TtsConfig(enabled=False, text="测试") result = engine.generate_full_voiceover(config) assert result.success is False assert "未启用" in result.error_message assert len(result.segments) == 0 def test_empty_text_returns_failure(self, tmp_path): """文本为空→失败.""" engine = self._make_engine(tmp_path) config = TtsConfig(enabled=True, text=" ") result = engine.generate_full_voiceover(config) assert result.success is False assert "文本为空" in result.error_message def test_success_creates_segment(self, tmp_path): """成功合成返回正确结构.""" mock_tts = MagicMock() output_file = tmp_path / "voiceover_full.wav" output_file.write_bytes(b"fake audio") mock_tts.synthesize.return_value = str(output_file) engine = self._make_engine(tmp_path, mock_tts) config = TtsConfig(enabled=True, text="测试文本", voice_id="female_warm", speed=1.0) result = engine.generate_full_voiceover(config) assert result.success is True assert len(result.segments) == 1 assert result.segments[0].text == "测试文本" assert result.segments[0].start_time == 0.0 assert result.total_duration > 0 mock_tts.synthesize.assert_called_once() def test_tts_error_returns_failure_gracefully(self, tmp_path): """TTS抛错→优雅降级返回失败.""" mock_tts = MagicMock() mock_tts.synthesize.side_effect = TtsError("合成失败") engine = self._make_engine(tmp_path, mock_tts) config = TtsConfig(enabled=True, text="测试") result = engine.generate_full_voiceover(config) assert result.success is False assert "合成失败" in result.error_message def test_generic_exception_returns_failure(self, tmp_path): """其他异常也降级返回失败.""" mock_tts = MagicMock() mock_tts.synthesize.side_effect = RuntimeError("未知错误") engine = self._make_engine(tmp_path, mock_tts) config = TtsConfig(enabled=True, text="测试") result = engine.generate_full_voiceover(config) assert result.success is False assert "未知错误" in result.error_message def test_work_dir_created(self, tmp_path): """工作目录自动创建.""" new_dir = tmp_path / "nested" / "tts" mock_tts = MagicMock() TtsEngine(tts_service=mock_tts, work_dir=new_dir) assert new_dir.exists() class TestTtsEngineSubtitleVoiceover: """TtsEngine.generate_subtitle_voiceover 字幕配音测试(mock TTS).""" def _make_engine(self, tmp_path: Path, tts: MagicMock | None = None) -> TtsEngine: if tts is None: tts = MagicMock() return TtsEngine(tts_service=tts, work_dir=tmp_path) def test_disabled_returns_failure(self, tmp_path): """配音未启用→失败.""" engine = self._make_engine(tmp_path) config = TtsConfig(enabled=False, text="") result = engine.generate_subtitle_voiceover(config, [{"text": "hi", "start_time": 0, "end_time": 1}]) assert result.success is False assert "未启用" in result.error_message def test_empty_subtitles_returns_failure(self, tmp_path): """字幕列表为空→失败.""" engine = self._make_engine(tmp_path) config = TtsConfig(enabled=True, text="") result = engine.generate_subtitle_voiceover(config, []) assert result.success is False assert "字幕为空" in result.error_message def test_none_subtitles_returns_failure(self, tmp_path): """None字幕也失败.""" engine = self._make_engine(tmp_path) config = TtsConfig(enabled=True, text="") result = engine.generate_subtitle_voiceover(config, None) # type: ignore assert result.success is False