9dd2226eb6
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 9s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 49s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 55s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 41s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 49s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m56s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 47s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m34s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m20s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 7m12s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m32s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
185 lines
6.6 KiB
Python
185 lines
6.6 KiB
Python
"""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
|