From f6bda521b21d455b05d475af6f36b497b281cb46 Mon Sep 17 00:00:00 2001 From: viccy Date: Mon, 8 Jun 2026 16:23:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui,=20jianying):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=AD=97=E5=B9=95=E5=8C=B9=E9=85=8D=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BF=AE=E5=A4=8Dwebui=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为剪映任务模块新增自动根据视频文件名匹配对应字幕文件的逻辑,当未传入原始字幕路径时自动查找并选择最新的匹配字幕 - 修复webui脚本设置页的selectbox状态同步问题,改用session_state作为唯一状态源,避免同时传递index和key导致的冲突 - 更新webui脚本路径的特殊路径判断列表,新增MODE_FILE的特殊情况处理 - 新增两个单元测试用例验证自动字幕匹配和原片字幕导入功能 --- app/services/jianying_task.py | 58 +++++++++++++++++++++ app/services/test_jianying_task_unittest.py | 57 ++++++++++++++++++++ webui/components/script_settings.py | 13 +++-- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/app/services/jianying_task.py b/app/services/jianying_task.py index 21e2c01..941a2c8 100644 --- a/app/services/jianying_task.py +++ b/app/services/jianying_task.py @@ -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], diff --git a/app/services/test_jianying_task_unittest.py b/app/services/test_jianying_task_unittest.py index e977242..d66afb9 100644 --- a/app/services/test_jianying_task_unittest.py +++ b/app/services/test_jianying_task_unittest.py @@ -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" diff --git a/webui/components/script_settings.py b/webui/components/script_settings.py index 5d68dd6..42b68d9 100644 --- a/webui/components/script_settings.py +++ b/webui/components/script_settings.py @@ -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"