mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-07-23 06:28:25 +00:00
feat(webui, jianying): 添加自动字幕匹配功能并修复webui状态问题
- 为剪映任务模块新增自动根据视频文件名匹配对应字幕文件的逻辑,当未传入原始字幕路径时自动查找并选择最新的匹配字幕 - 修复webui脚本设置页的selectbox状态同步问题,改用session_state作为唯一状态源,避免同时传递index和key导致的冲突 - 更新webui脚本路径的特殊路径判断列表,新增MODE_FILE的特殊情况处理 - 新增两个单元测试用例验证自动字幕匹配和原片字幕导入功能
This commit is contained in:
parent
7d4bd45f69
commit
f6bda521b2
@ -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],
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user