From 65d5a681ac8b265cbfa7876d3861f9558beb8ad1 Mon Sep 17 00:00:00 2001 From: linyq Date: Fri, 6 Dec 2024 18:01:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(webui):=20=E8=A7=86=E9=A2=91=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E6=B7=BB=E5=8A=A0=E4=B8=80=E9=94=AE=E8=BD=AC=E5=BD=95?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -改进文件上传和预览逻辑,支持视频和字幕文件的独立上传 - 添加字幕预览功能,可显示已上传字幕文件的内容 - 实现一键转录功能,为没有字幕的视频生成字幕 -优化合并文件的流程,提高合并效率 - 增加合并结果预览,方便用户查看合并后的视频和字幕 -重构代码,提高可维护性和可扩展性 --- .gitignore | 1 + app/services/subtitle.py | 6 +- webui/components/merge_settings.py | 291 +++++++++++++++++++---------- webui/i18n/zh.json | 13 +- 4 files changed, 204 insertions(+), 107 deletions(-) diff --git a/.gitignore b/.gitignore index 3b6f801..d0a8e81 100644 --- a/.gitignore +++ b/.gitignore @@ -29,5 +29,6 @@ resource/songs/*.mp3 resource/songs/*.flac resource/fonts/*.ttc resource/fonts/*.ttf +resource/fonts/*.otf resource/srt/*.srt app/models/faster-whisper-large-v2/* \ No newline at end of file diff --git a/app/services/subtitle.py b/app/services/subtitle.py index 7b18e8d..e7f037d 100644 --- a/app/services/subtitle.py +++ b/app/services/subtitle.py @@ -419,11 +419,11 @@ def extract_audio_and_create_subtitle(video_file: str, subtitle_file: str = "") if __name__ == "__main__": - task_id = "12121" + task_id = "123456" task_dir = utils.task_dir(task_id) - subtitle_file = f"{task_dir}/subtitle.srt" + subtitle_file = f"{task_dir}/subtitle_123456.srt" audio_file = f"{task_dir}/audio.wav" - video_file = f"{task_dir}/duanju_demo.mp4" + video_file = "/Users/apple/Desktop/home/NarratoAI/resource/videos/merged_video_1702.mp4" extract_audio_and_create_subtitle(video_file, subtitle_file) diff --git a/webui/components/merge_settings.py b/webui/components/merge_settings.py index 6fbfbae..99b8b43 100644 --- a/webui/components/merge_settings.py +++ b/webui/components/merge_settings.py @@ -3,48 +3,87 @@ import time import math import sys import tempfile +import traceback +import shutil + import streamlit as st -from pathlib import Path +from loguru import logger from typing import List, Dict, Tuple from dataclasses import dataclass from streamlit.runtime.uploaded_file_manager import UploadedFile from webui.utils.merge_video import merge_videos_and_subtitles from app.utils.utils import video_dir, srt_dir +from app.services.subtitle import extract_audio_and_create_subtitle + +# 定义临时目录路径 +TEMP_MERGE_DIR = os.path.join("storage", "temp", "merge") + +# 确保临时目录存在 +os.makedirs(TEMP_MERGE_DIR, exist_ok=True) @dataclass class VideoSubtitlePair: video_file: UploadedFile | None - subtitle_file: UploadedFile | None + subtitle_file: str | None base_name: str order: int = 0 -def save_uploaded_file(uploaded_file: UploadedFile, temp_dir: str) -> str: - """Save uploaded file to temporary directory and return the file path""" - file_path = os.path.join(temp_dir, uploaded_file.name) +def save_uploaded_file(uploaded_file: UploadedFile, target_dir: str) -> str: + """Save uploaded file to target directory and return the file path""" + file_path = os.path.join(target_dir, uploaded_file.name) + # 如果文件已存在,先删除它 + if os.path.exists(file_path): + os.remove(file_path) with open(file_path, "wb") as f: f.write(uploaded_file.getvalue()) return file_path +def clean_temp_dir(): + """清空临时目录""" + if os.path.exists(TEMP_MERGE_DIR): + for file in os.listdir(TEMP_MERGE_DIR): + file_path = os.path.join(TEMP_MERGE_DIR, file) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception as e: + logger.error(f"清理临时文件失败: {str(e)}") + + def group_files(files: List[UploadedFile]) -> Dict[str, VideoSubtitlePair]: """Group uploaded files by their base names""" pairs = {} order_counter = 0 + + # 首先处理所有视频文件 for file in files: base_name = os.path.splitext(file.name)[0] ext = os.path.splitext(file.name)[1].lower() - if base_name not in pairs: - pairs[base_name] = VideoSubtitlePair(None, None, base_name, order_counter) - order_counter += 1 - if ext == ".mp4": + if base_name not in pairs: + pairs[base_name] = VideoSubtitlePair(None, None, base_name, order_counter) + order_counter += 1 pairs[base_name].video_file = file - elif ext == ".srt": - pairs[base_name].subtitle_file = file + # 保存视频文件到临时目录 + video_path = save_uploaded_file(file, TEMP_MERGE_DIR) + + # 然后处理所有字幕文件 + for file in files: + base_name = os.path.splitext(file.name)[0] + ext = os.path.splitext(file.name)[1].lower() + + if ext == ".srt": + # 即使没有对应视频也保存字幕文件 + subtitle_path = os.path.join(TEMP_MERGE_DIR, f"{base_name}.srt") + save_uploaded_file(file, TEMP_MERGE_DIR) + + if base_name in pairs: # 如果有对应的视频 + pairs[base_name].subtitle_file = subtitle_path return pairs @@ -92,7 +131,7 @@ def render_merge_settings(tr): # 计算需要多少行来显示所有视频(每行5个) num_pairs = len(sorted_pairs) - num_rows = (num_pairs + 4) // 5 # 向上取整 + num_rows = (num_pairs + 4) // 5 # 向上取整,每行5个 # 遍历每一行 for row in range(num_rows): @@ -107,13 +146,63 @@ def render_merge_settings(tr): with cols[col_idx]: st.caption(base_name) - # 显示视频(如果存在) - if pair.video_file: - st.video(pair.video_file) + # 显示视频预览(如果存在) + video_path = os.path.join(TEMP_MERGE_DIR, f"{base_name}.mp4") + if os.path.exists(video_path): + st.video(video_path) else: st.warning(tr("Missing Video")) - # 添加排序输入框 + # 显示字幕预览(如果存在) + subtitle_path = os.path.join(TEMP_MERGE_DIR, f"{base_name}.srt") + if os.path.exists(subtitle_path): + with open(subtitle_path, 'r', encoding='utf-8') as f: + subtitle_content = f.read() + st.markdown(tr("Subtitle Preview")) + st.text_area( + "Subtitle Content", + value=subtitle_content, + height=100, # 减高度以适应5列布局 + label_visibility="collapsed", + key=f"subtitle_preview_{base_name}" + ) + else: + st.warning(tr("Missing Subtitle")) + # 如果有视频但没有字幕,显示一键转录按钮 + if os.path.exists(video_path): + if st.button(tr("One-Click Transcribe"), key=f"transcribe_{base_name}"): + with st.spinner(tr("Transcribing...")): + try: + # 生成字幕文件 + result = extract_audio_and_create_subtitle(video_path, subtitle_path) + if result: + # 读取生成的字幕文件内容并显示预览 + with open(subtitle_path, 'r', encoding='utf-8') as f: + subtitle_content = f.read() + st.markdown(tr("Subtitle Preview")) + st.text_area( + "Subtitle Content", + value=subtitle_content, + height=150, + label_visibility="collapsed", + key=f"subtitle_preview_transcribed_{base_name}" + ) + st.success(tr("Transcription Complete!")) + # 更新pair的字幕文件路径 + pair.subtitle_file = subtitle_path + else: + st.error(tr("Transcription Failed. Please try again.")) + except Exception as e: + error_message = str(e) + logger.error(traceback.format_exc()) + if "rate limit exceeded" in error_message.lower(): + st.error(tr("API rate limit exceeded. Please wait about an hour and try again.")) + elif "resource_exhausted" in error_message.lower(): + st.error(tr("Resources exhausted. Please try again later.")) + else: + st.error(f"{tr('Transcription Failed')}: {str(e)}") + + # 排序输入框 order = st.number_input( tr("Order"), min_value=0, @@ -124,95 +213,91 @@ def render_merge_settings(tr): if order != st.session_state.file_orders[base_name]: st.session_state.file_orders[base_name] = order st.session_state.needs_reorder = True - - # 显示字幕(如果存在) - if pair.subtitle_file: - subtitle_content = pair.subtitle_file.getvalue().decode('utf-8') - st.text_area( - "字幕预览", - value=subtitle_content, - height=150, - label_visibility="collapsed" - ) - else: - st.warning(tr("Missing Subtitle")) - # 合并后的视频预览 + + # 如果需要重新排序,重新加载页面 + if st.session_state.needs_reorder: + st.session_state.needs_reorder = False + st.rerun() + + # 找出有完整视频和字幕的文件对 + complete_pairs = { + k: v for k, v in all_pairs.items() + if os.path.exists(os.path.join(TEMP_MERGE_DIR, f"{k}.mp4")) and + os.path.exists(os.path.join(TEMP_MERGE_DIR, f"{k}.srt")) + } + + # 合并按钮和结果显示 + cols = st.columns([1, 2, 1]) + with cols[0]: + st.write(f"{tr('Mergeable Files')}: {len(complete_pairs)}") + merge_videos_result = None - # 只有当存在完整的配对时才显示按钮 - complete_pairs = {k: v for k, v in all_pairs.items() if v.video_file and v.subtitle_file} - if complete_pairs: - # 创建按钮 - cols = st.columns([1, 1, 3, 3, 3]) - with cols[0]: - if st.button(tr("Reorder"), disabled=not st.session_state.needs_reorder, use_container_width=True): - st.session_state.needs_reorder = False - st.rerun() - - with cols[1]: - if st.button(tr("Merge All Files"), type="primary", use_container_width=True): - try: - # 获取排序后的完整文件对 - sorted_complete_pairs = sorted( - [(k, v) for k, v in complete_pairs.items()], - key=lambda x: st.session_state.file_orders[x[0]] + + with cols[1]: + if st.button(tr("Merge All Files"), type="primary", use_container_width=True): + try: + # 获取排序后的完整文件对 + sorted_complete_pairs = sorted( + [(k, v) for k, v in complete_pairs.items()], + key=lambda x: st.session_state.file_orders[x[0]] + ) + + video_paths = [] + subtitle_paths = [] + for base_name, _ in sorted_complete_pairs: + video_paths.append(os.path.join(TEMP_MERGE_DIR, f"{base_name}.mp4")) + subtitle_paths.append(os.path.join(TEMP_MERGE_DIR, f"{base_name}.srt")) + + # 获取输出文件路径 + output_video = os.path.join(video_dir(), f"merged_video_{time.strftime('%M%S')}.mp4") + output_subtitle = os.path.join(srt_dir(), f"merged_subtitle_{time.strftime('%M%S')}.srt") + + with st.spinner(tr("Merging files...")): + # 合并文件 + merge_videos_and_subtitles( + video_paths, + subtitle_paths, + output_video, + output_subtitle ) - # 创建临时目录保存文件 - with tempfile.TemporaryDirectory() as temp_dir: - # 保存上传的文件到临时目录 - video_paths = [] - subtitle_paths = [] - for _, pair in sorted_complete_pairs: - video_path = save_uploaded_file(pair.video_file, temp_dir) - subtitle_path = save_uploaded_file(pair.subtitle_file, temp_dir) - video_paths.append(video_path) - subtitle_paths.append(subtitle_path) - - # 获取输出目录, 文件名添加 MMSS 时间戳 - output_video = os.path.join(video_dir(), f"merged_video_{time.strftime('%M%S')}.mp4") - output_subtitle = os.path.join(srt_dir(), f"merged_subtitle_{time.strftime('%M%S')}.srt") - - with st.spinner(tr("Merging files...")): - # 合并文件 - merge_videos_and_subtitles( - video_paths, - subtitle_paths, - output_video, - output_subtitle - ) - - success = True - error_msg = "" - - # 检查输出文件是否成功生成 - if not os.path.exists(output_video): - success = False - error_msg += tr("Failed to generate merged video. ") - if not os.path.exists(output_subtitle): - success = False - error_msg += tr("Failed to generate merged subtitle. ") - - if success: - # 显示成功消息 - st.success(tr("Merge completed!")) - merge_videos_result = (output_video, output_subtitle) - else: - st.error(error_msg) - - except Exception as e: - error_message = str(e) - if "moviepy" in error_message.lower(): - st.error(tr("Error processing video files. Please check if the videos are valid MP4 files.")) - elif "pysrt" in error_message.lower(): - st.error(tr("Error processing subtitle files. Please check if the subtitles are valid SRT files.")) + success = True + error_msg = "" + + # 检查输出文件是否成功生成 + if not os.path.exists(output_video): + success = False + error_msg += tr("Failed to generate merged video. ") + if not os.path.exists(output_subtitle): + success = False + error_msg += tr("Failed to generate merged subtitle. ") + + if success: + # 显示成功消息 + st.success(tr("Merge completed!")) + merge_videos_result = (output_video, output_subtitle) + # 清理临时目录 + clean_temp_dir() else: - st.error(f"{tr('Error during merge')}: {error_message}") - with cols[2]: - # 提供视频和字幕的预览 - if merge_videos_result: - with st.popover(tr("Preview Merged Video")): - st.video(merge_videos_result[0], subtitles=merge_videos_result[1]) - st.code(f"{tr('Video Path')}: {merge_videos_result[0]}") - st.code(f"{tr('Subtitle Path')}: {merge_videos_result[1]}") + st.error(error_msg) + + except Exception as e: + error_message = str(e) + if "moviepy" in error_message.lower(): + st.error(tr("Error processing video files. Please check if the videos are valid MP4 files.")) + elif "pysrt" in error_message.lower(): + st.error(tr("Error processing subtitle files. Please check if the subtitles are valid SRT files.")) + else: + st.error(f"{tr('Error during merge')}: {error_message}") + + # 合并结果预览放在合并按钮下方 + if merge_videos_result: + st.markdown(f"

{tr('Merge Result Preview')}

", unsafe_allow_html=True) + # 使用列布局使视频居中 + col1, col2, col3 = st.columns([1,2,1]) + with col2: + st.video(merge_videos_result[0]) + st.code(f"{tr('Video Path')}: {merge_videos_result[0]}") + st.code(f"{tr('Subtitle Path')}: {merge_videos_result[1]}") else: - st.warning(tr("No Matched Pairs Found")) + st.warning(tr("No Files Found")) diff --git a/webui/i18n/zh.json b/webui/i18n/zh.json index 1f42d1d..428fa8c 100644 --- a/webui/i18n/zh.json +++ b/webui/i18n/zh.json @@ -177,6 +177,17 @@ "Clear tasks": "清理任务", "Directory cleared": "目录清理完成", "Directory does not exist": "目录不存在", - "Failed to clear directory": "清理目录失败" + "Failed to clear directory": "清理目录失败", + "Subtitle Preview": "字幕预览", + "One-Click Transcribe": "一键转录", + "Transcribing...": "正在转录中...", + "Transcription Complete!": "转录完成!", + "Transcription Failed. Please try again.": "转录失败,请重试。", + "API rate limit exceeded. Please wait about an hour and try again.": "API 调用次数已达到限制,请等待约一小时后再试。", + "Resources exhausted. Please try again later.": "资源已耗尽,请稍后再试。", + "Transcription Failed": "转录失败", + "Mergeable Files": "可合并文件数", + "Subtitle Content": "字幕内容", + "Merge Result Preview": "合并结果预览" } }