feat(webui, jianying): 添加自动字幕匹配功能并修复webui状态问题

- 为剪映任务模块新增自动根据视频文件名匹配对应字幕文件的逻辑,当未传入原始字幕路径时自动查找并选择最新的匹配字幕
- 修复webui脚本设置页的selectbox状态同步问题,改用session_state作为唯一状态源,避免同时传递index和key导致的冲突
- 更新webui脚本路径的特殊路径判断列表,新增MODE_FILE的特殊情况处理
- 新增两个单元测试用例验证自动字幕匹配和原片字幕导入功能
This commit is contained in:
viccy 2026-06-08 16:23:10 +08:00
parent 7d4bd45f69
commit f6bda521b2
3 changed files with 124 additions and 4 deletions

View File

@ -1,5 +1,6 @@
import json
import os
import re
import subprocess
import time
from os import path
@ -253,9 +254,66 @@ def _get_original_subtitle_paths(params: VideoClipParams) -> list[str]:
if single_subtitle_path and single_subtitle_path not in seen:
normalized_paths.insert(0, single_subtitle_path)
if not normalized_paths:
normalized_paths = _find_original_subtitle_paths_for_videos(_get_video_source_paths(params))
return normalized_paths
def _video_stem_candidates(video_path: str) -> list[str]:
stem = path.splitext(path.basename(str(video_path or "").strip()))[0]
if not stem:
return []
candidates = [stem]
timestamp_stripped = re.sub(r"_[0-9]{14}$", "", stem)
if timestamp_stripped and timestamp_stripped not in candidates:
candidates.append(timestamp_stripped)
return candidates
def _find_original_subtitle_paths_for_videos(video_paths: list[str]) -> list[str]:
subtitle_dir = utils.subtitle_dir()
if not path.isdir(subtitle_dir):
return []
subtitle_files = [
path.join(subtitle_dir, filename)
for filename in os.listdir(subtitle_dir)
if filename.lower().endswith(".srt")
]
if not subtitle_files:
return []
resolved_paths = []
seen = set()
for video_path in video_paths:
candidates = _video_stem_candidates(video_path)
if not candidates:
continue
matches = []
for subtitle_path in subtitle_files:
subtitle_stem = path.splitext(path.basename(subtitle_path))[0]
for candidate in candidates:
if subtitle_stem == candidate or subtitle_stem.startswith(f"{candidate}_"):
matches.append(subtitle_path)
break
if not matches:
continue
matches.sort(key=lambda item: path.getmtime(item), reverse=True)
selected_path = matches[0]
if selected_path not in seen:
resolved_paths.append(selected_path)
seen.add(selected_path)
if resolved_paths:
logger.info(f"剪映导出未从参数获取原片字幕,已按视频文件名自动匹配: {resolved_paths}")
return resolved_paths
def _create_jianying_subtitle_file(
task_id: str,
draft_script: list[Dict],

View File

@ -305,6 +305,63 @@ class JianyingTaskTests(unittest.TestCase):
self.assertEqual(1.25, draft_script[0]["duration"])
self.assertTrue(draft_script[0]["use_source_timerange"])
def test_get_original_subtitle_paths_falls_back_to_matching_video_name(self):
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
video_path = temp_path / "episode_20260608010240.mp4"
older_subtitle = temp_path / "episode_fun_asr_20260608000100.srt"
newer_subtitle = temp_path / "episode_fun_asr_20260608010100.srt"
video_path.write_bytes(b"video")
older_subtitle.write_text("old", encoding="utf-8")
newer_subtitle.write_text("new", encoding="utf-8")
params = VideoClipParams(video_origin_path=str(video_path))
with patch.object(jianying_task.utils, "subtitle_dir", return_value=str(temp_path)):
subtitle_paths = jianying_task._get_original_subtitle_paths(params)
self.assertEqual([str(newer_subtitle)], subtitle_paths)
def test_create_jianying_subtitle_file_includes_original_audio_subtitles(self):
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
task_dir = temp_path / "task"
task_dir.mkdir()
video_path = temp_path / "episode.mp4"
subtitle_path = temp_path / "episode.srt"
video_path.write_bytes(b"video")
subtitle_path.write_text(
"1\n00:00:05,000 --> 00:00:06,500\n原片对白\n",
encoding="utf-8",
)
params = VideoClipParams(video_origin_path=str(video_path), subtitle_enabled=True)
draft_script = jianying_task._build_jianying_draft_script(
[
{
"_id": 1,
"timestamp": "00:00:05,000-00:00:07,000",
"narration": "播放原片1",
"OST": 1,
}
],
params,
[],
)
with (
patch.object(jianying_task.utils, "subtitle_dir", return_value=str(temp_path)),
patch.object(jianying_task.utils, "task_dir", return_value=str(task_dir)),
):
output_path = jianying_task._create_jianying_subtitle_file(
"task-id",
draft_script,
params,
)
self.assertTrue(output_path)
self.assertIn("原片对白", Path(output_path).read_text(encoding="utf-8"))
def test_start_export_jianying_draft_does_not_clip_video(self):
with tempfile.TemporaryDirectory() as temp_dir:
root_path = Path(temp_dir) / "drafts"

View File

@ -448,7 +448,7 @@ def render_script_file(tr, params):
# 如果当前path是特殊值(auto/short/summary/film_summary),则重置为空
saved_script_path = (
current_path
if current_path not in [MODE_AUTO, MODE_SHORT, MODE_SHORT_SUMMARY, MODE_FILM_SUMMARY]
if current_path not in [MODE_FILE, MODE_AUTO, MODE_SHORT, MODE_SHORT_SUMMARY, MODE_FILM_SUMMARY]
else ""
)
@ -458,13 +458,18 @@ def render_script_file(tr, params):
selected_index = i
break
# 如果找到了保存的脚本,同步更新 selectbox 的 key 状态
if saved_script_path and selected_index > 0:
# 用 session_state 作为 selectbox 的唯一来源,避免同时传默认 index 和设置 key 状态。
if (
"script_file_selection" not in st.session_state
or st.session_state["script_file_selection"] >= len(script_list)
):
st.session_state["script_file_selection"] = selected_index
elif saved_script_path and selected_index > 0:
st.session_state['script_file_selection'] = selected_index
selected_script_index = st.selectbox(
tr("Script Files"),
index=selected_index,
index=None,
options=range(len(script_list)),
format_func=lambda x: script_list[x][0],
key="script_file_selection"