NarratoAI/app/services/test_short_drama_narration_validation_unittest.py
viccy e6e39d2dcd feat(short-drama): 完整实现短剧解说剪辑全流程并新增LLM流式生成支持
- 新增短剧解说全流程四类提示词模板:解说文案生成、片段规划、文案画面匹配、脚本修复
- 重构原有脚本生成提示词至v2.1,改为基于上游规划片段生成合规解说脚本
- 为LLM基础服务层新增流式文本生成接口,完善OpenAI兼容提供商的流式实现,支持流式回调与推理内容提取
- 重构OpenAI兼容文本提供商的生成逻辑,提取公共参数构建方法
- 新增多语言国际化文案,覆盖解说语言、短剧类型、原片占比等配置项与交互提示
- 新增多套单元测试,覆盖脚本校验、适配器流程、工具函数等模块
- 封装SubtitleAnalyzerAdapter,统一短剧解说脚本生成的整套业务接口
- 新增前端交互所需的解说文案审核相关提示文案
2026-06-07 17:10:48 +08:00

291 lines
9.5 KiB
Python

import unittest
from app.services.short_drama_narration_validation import (
build_subtitle_index,
normalize_script_video_sources,
validate_narration_script_items,
)
SUBTITLE_CONTENT = """# 视频 1: first.mp4
字幕文件: first.srt
1
00:00:01,000 --> 00:00:04,000
女主被众人误会。
2
00:00:04,000 --> 00:00:08,000
男主冷眼看着她。
# 视频 2: second.mp4
字幕文件: second.srt
1
00:00:02,000 --> 00:00:05,000
女主终于拿出证据。
2
00:00:05,000 --> 00:00:09,000
众人震惊,反派慌了。
"""
class ShortDramaNarrationValidationTests(unittest.TestCase):
def setUp(self):
self.video_paths = ["/tmp/first.mp4", "/tmp/second.mp4"]
self.subtitle_index = build_subtitle_index(SUBTITLE_CONTENT, self.video_paths)
def test_build_subtitle_index_preserves_multi_video_sources(self):
self.assertEqual(4, len(self.subtitle_index))
self.assertEqual({1, 2}, {cue.video_id for cue in self.subtitle_index})
self.assertEqual("first.mp4", self.subtitle_index[0].video_name)
self.assertEqual("second.mp4", self.subtitle_index[2].video_name)
self.assertEqual("00:00:02,000-00:00:05,000", self.subtitle_index[2].timestamp)
def test_valid_script_passes_and_normalizes_video_name(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "wrong-name.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "她被当众误会。",
"OST": 0,
},
{
"_id": 2,
"video_name": "second.mp4",
"timestamp": "00:00:02,000-00:00:05,000",
"picture": "女主拿出证据",
"narration": "播放原片2",
"OST": 1,
},
]
normalized = normalize_script_video_sources(items, self.video_paths)
result = validate_narration_script_items(normalized, self.subtitle_index, self.video_paths)
self.assertTrue(result.valid, result.errors)
self.assertEqual(2, result.items[1]["video_id"])
self.assertEqual("second.mp4", result.items[1]["video_name"])
def test_invalid_timestamp_and_overlap_fail(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:05,000",
"picture": "画面",
"narration": "解说",
"OST": 0,
},
{
"_id": 2,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:04,500-00:00:08,000",
"picture": "画面",
"narration": "解说",
"OST": 0,
},
{
"_id": 3,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "bad",
"picture": "画面",
"narration": "解说",
"OST": 0,
},
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("重叠" in error for error in result.errors))
self.assertTrue(any("时间戳格式" in error for error in result.errors))
def test_invalid_video_id_does_not_default_to_first_video(self):
items = [
{
"_id": 1,
"video_id": 99,
"video_name": "missing.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "画面",
"narration": "解说",
"OST": 0,
}
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("video_id=99" in error for error in result.errors))
def test_out_of_range_timestamp_fails(self):
items = [
{
"_id": 1,
"video_id": 2,
"video_name": "second.mp4",
"timestamp": "00:00:20,000-00:00:25,000",
"picture": "画面",
"narration": "解说",
"OST": 0,
}
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("不在视频 2 的字幕范围内" in error for error in result.errors))
def test_three_consecutive_original_audio_segments_fail(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "她被当众误会。",
"OST": 0,
},
{
"_id": 2,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:04,000-00:00:05,000",
"picture": "男主看着她",
"narration": "播放原片2",
"OST": 1,
},
{
"_id": 3,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:05,000-00:00:06,000",
"picture": "男主看着她",
"narration": "播放原片3",
"OST": 1,
},
{
"_id": 4,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:06,000-00:00:08,000",
"picture": "男主继续观察",
"narration": "播放原片4",
"OST": 1,
},
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("连续原声过多" in error for error in result.errors))
def test_cross_video_original_audio_requires_narration_bridge(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "她被当众误会。",
"OST": 0,
},
{
"_id": 2,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:04,000-00:00:08,000",
"picture": "男主看着她",
"narration": "播放原片2",
"OST": 1,
},
{
"_id": 3,
"video_id": 2,
"video_name": "second.mp4",
"timestamp": "00:00:02,000-00:00:05,000",
"picture": "女主拿出证据",
"narration": "播放原片3",
"OST": 1,
},
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("跨视频切换缺少 OST=0 解说桥段" in error for error in result.errors))
def test_cross_video_switch_with_narration_bridge_passes(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "她被当众误会。",
"OST": 0,
},
{
"_id": 2,
"video_id": 2,
"video_name": "second.mp4",
"timestamp": "00:00:02,000-00:00:05,000",
"picture": "女主拿出证据",
"narration": "播放原片2",
"OST": 1,
},
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertTrue(result.valid, result.errors)
def test_first_segment_must_be_narration_hook(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "播放原片1",
"OST": 1,
}
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("解说开场钩子" in error for error in result.errors))
def test_dense_narration_fails_when_video_duration_is_too_short(self):
items = [
{
"_id": 1,
"video_id": 1,
"video_name": "first.mp4",
"timestamp": "00:00:01,000-00:00:04,000",
"picture": "女主被误会",
"narration": "她明明什么都没做却被所有人推到风口浪尖只能独自承受委屈",
"OST": 0,
}
]
result = validate_narration_script_items(items, self.subtitle_index, self.video_paths)
self.assertFalse(result.valid)
self.assertTrue(any("解说过密" in error for error in result.errors))
if __name__ == "__main__":
unittest.main()