""" pyVideoTrans WebUI — Gradio-based web interface for video translation. Usage: uv run webui.py # or uv run python webui.py Requires: uv sync --extra webui """ import os import sys import json import time import asyncio import traceback from pathlib import Path from typing import List if sys.platform == "win32": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # --------------------------------------------------------------------------- # 语言常量 # --------------------------------------------------------------------------- CLI_LANG = "zh" os.environ['PYVIDEOTRANS_LANG'] = CLI_LANG # --------------------------------------------------------------------------- # 初始化 videotrans 环境 # --------------------------------------------------------------------------- from videotrans.configure import config config.init_run() from videotrans.configure.config import ROOT_DIR, TEMP_DIR, app_cfg, params, settings from videotrans.configure.contants import FASTER_MODELS_DICT from videotrans import recognition, translator, tts from videotrans.util import tools from videotrans.util.gpus import getset_gpu from videotrans.util.help_role import role_menu # --------------------------------------------------------------------------- # params / settings 持久化路径 # --------------------------------------------------------------------------- PARAMS_JSON = Path(ROOT_DIR) / "videotrans" / "params.json" SETTINGS_JSON = Path(ROOT_DIR) / "videotrans" / "cfg.json" def _load_params() -> dict: """从 params.json 加载""" try: if PARAMS_JSON.exists(): return json.loads(PARAMS_JSON.read_text(encoding="utf-8")) except Exception: pass return {} def _save_params(data: dict): """保存到 params.json""" PARAMS_JSON.parent.mkdir(parents=True, exist_ok=True) PARAMS_JSON.write_text(json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8") # 同步更新内存中的 params params.getset_params(data) def _load_settings() -> dict: try: if SETTINGS_JSON.exists(): return json.loads(SETTINGS_JSON.read_text(encoding="utf-8")) except Exception: pass return {} def _save_settings(data: dict): SETTINGS_JSON.parent.mkdir(parents=True, exist_ok=True) SETTINGS_JSON.write_text(json.dumps(data, indent=4, ensure_ascii=False), encoding="utf-8") settings.parse_init(data) # 加载当前配置 _user_params = _load_params() _user_settings = _load_settings() # --------------------------------------------------------------------------- # 渠道名称列表 # --------------------------------------------------------------------------- RECOGN_NAMES: List[str] = recognition.RECOGN_NAME_LIST TRANSLATE_NAMES: List[str] = translator.TRANSLASTE_NAME_LIST TTS_NAMES: List[str] = tts.TTS_NAME_LIST LANGNAME_DICT: dict = translator.LANGNAME_DICT # --------------------------------------------------------------------------- # 可选渠道索引 # --------------------------------------------------------------------------- SELECTABLE_RECOGN = {0, 1, 2, 3, 4} DEFAULT_RECOGN = 0 SELECTABLE_TRANSLATE = {0, 1, 2} DEFAULT_TRANSLATE = 0 SELECTABLE_TTS = {0, 1, 3, 4, 5, 6, 7, 31} DEFAULT_TTS = 0 FASTER_MODEL_NAMES = list(FASTER_MODELS_DICT.keys()) DEFAULT_MODEL = "large-v3-turbo" if "large-v3-turbo" in FASTER_MODEL_NAMES else FASTER_MODEL_NAMES[0] LANG_DISPLAY_NAMES = list(LANGNAME_DICT.values()) DEFAULT_SOURCE_LANG = LANG_DISPLAY_NAMES[0] DEFAULT_TARGET_LANG = '-' SUBTITLE_TYPES = {"不嵌入字幕": 0, "嵌入硬字幕": 1, "嵌入软字幕": 2, "嵌入硬字幕(双语)": 3, "嵌入软字幕(双语)": 4} DEFAULT_SUBTITLE_TYPE = "嵌入硬字幕" PUNC_OPTIONS = {"默认标点": 0, "恢复标点": 1, "删除标点": 2} LOOP_BGM_OPTIONS = {"背景音截断": 0, "背景音循环": 1} # --------------------------------------------------------------------------- # ASS 字幕样式 # --------------------------------------------------------------------------- ASS_JSON_FILE = f'{ROOT_DIR}/videotrans/ass.json' DEFAULT_ASS_STYLE = { 'Name': 'Default', 'Fontname': 'Arial', 'Bottom_Fontname': 'Arial', 'Fontsize': 16, 'Bottom_Fontsize': 16, 'PrimaryColour': '&H00FFFFFF&', 'Bottom_PrimaryColour': '&H00FFFFFF&', 'SecondaryColour': '&H00FFFFFF&', 'OutlineColour': '&H00000000&', 'BackColour': '&H00000000&', 'Bold': 0, 'Italic': 0, 'Bottom_SecondaryColour': '&H00FFFFFF&', 'Bottom_OutlineColour': '&H00000000&', 'Bottom_BackColour': '&H00000000&', 'Bottom_Bold': 0, 'Bottom_Italic': 0, 'Underline': 0, 'StrikeOut': 0, 'ScaleX': 100, 'ScaleY': 100, 'Spacing': 0, 'Angle': 0, 'BorderStyle': 1, 'Outline': 0.5, 'Shadow': 0.5, 'Alignment': 2, 'MarginL': 10, 'MarginR': 10, 'MarginV': 10, 'Encoding': 1, } def _parse_ass_color(c): if not c.startswith('&H') or not c.endswith('&'): return '#ffffff' h = c[2:-1].upper() if len(h) == 6: return f'#{int(h[4:6],16):02x}{int(h[2:4],16):02x}{int(h[0:2],16):02x}' elif len(h) == 8: return f'#{int(h[6:8],16):02x}{int(h[4:6],16):02x}{int(h[2:4],16):02x}' return '#ffffff' def _to_ass_color(h): h = h.lstrip('#') if len(h) == 6: r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) return f'&H00{b:02X}{g:02X}{r:02X}&' return '&H00FFFFFF&' def _load_ass_style(): try: if Path(ASS_JSON_FILE).exists(): return json.loads(Path(ASS_JSON_FILE).read_text(encoding='utf-8')) except Exception: pass return DEFAULT_ASS_STYLE.copy() def _save_ass_style(s): Path(ASS_JSON_FILE).parent.mkdir(parents=True, exist_ok=True) Path(ASS_JSON_FILE).write_text(json.dumps(s, indent=4, ensure_ascii=False), encoding='utf-8') # --------------------------------------------------------------------------- # 辅助函数 # --------------------------------------------------------------------------- def _lang_code_from_display(d): for code, name in LANGNAME_DICT.items(): if name == d: return code return d def _tts_index_from_display(d): for i, name in enumerate(TTS_NAMES): if name == d: return i return 0 def _recogn_index_from_display(d): for i, name in enumerate(RECOGN_NAMES): if name == d: return i return 0 def _translate_index_from_display(d): for i, name in enumerate(TRANSLATE_NAMES): if name == d: return i return 0 def _format_rate(v): return f"+{v}%" if v >= 0 else f"{v}%" def _format_pitch(v): return f"+{v}Hz" if v >= 0 else f"{v}Hz" def _safe_get(key, default=""): """从 _user_params 读取值,支持 str/int/float/bool""" v = _user_params.get(key, default) if v is None: return default return v # --------------------------------------------------------------------------- # 渠道设置面板定义 # --------------------------------------------------------------------------- CHANNEL_SETTINGS = { # === 翻译渠道 === "ChatGPT 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "chatgpt_api", "label": "API URL", "type": "text", "default": "", "placeholder": "留空使用官方API"}, {"key": "chatgpt_key", "label": "SK 密钥", "type": "text", "default": "", "placeholder": "API Key"}, {"key": "chatgpt_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, {"key": "chatgpt_model", "label": "模型", "type": "text", "default": "gpt-4o-mini", "placeholder": "输入模型名称"}, ], }, "DeepSeek 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "deepseek_key", "label": "SK 密钥", "type": "text", "default": "", "placeholder": "API Key"}, {"key": "deepseek_model", "label": "模型", "type": "text", "default": "deepseek-chat", "placeholder": "输入模型名称"}, {"key": "deepseek_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, ], }, "Gemini 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "gemini_key", "label": "Gemini Key", "type": "text", "default": ""}, {"key": "gemini_model", "label": "模型", "type": "text", "default": "gemini-2.5-flash", "placeholder": "输入模型名称"}, {"key": "gemini_maxtoken", "label": "最大 Token", "type": "text", "default": "8192"}, ], }, "AzureGPT 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "azure_api", "label": "API URL", "type": "text", "default": ""}, {"key": "azure_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "azure_model", "label": "模型", "type": "text", "default": "gpt-4o-mini", "placeholder": "输入模型名称"}, ], }, "本地大模型 (LocalLLM)": { "category": "字幕翻译渠道", "fields": [ {"key": "localllm_api", "label": "API URL", "type": "text", "default": "http://127.0.0.1:11434/v1", "placeholder": "如 http://127.0.0.1:11434/v1"}, {"key": "localllm_key", "label": "SK 密钥", "type": "text", "default": "no-key", "placeholder": "通常填 no-key"}, {"key": "localllm_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, {"key": "localllm_model", "label": "模型", "type": "text", "default": "", "placeholder": "输入模型名称"}, ], }, "DeepL 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "deepl_authkey", "label": "AUTH KEY", "type": "text", "default": ""}, {"key": "deepl_api", "label": "API URL (第三方)", "type": "text", "default": "", "placeholder": "留空使用官方API"}, {"key": "deepl_gid", "label": "术语表 ID", "type": "text", "default": ""}, ], }, "百度翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "baidu_appid", "label": "App ID", "type": "text", "default": ""}, {"key": "baidu_miyue", "label": "密钥", "type": "text", "default": ""}, ], }, "腾讯翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "tencent_SecretId", "label": "SecretId", "type": "text", "default": ""}, {"key": "tencent_SecretKey", "label": "SecretKey", "type": "text", "default": ""}, ], }, "阿里百炼 (QwenMT)": { "category": "字幕翻译渠道", "fields": [ {"key": "qwenmt_key", "label": "百炼 SK", "type": "text", "default": ""}, {"key": "qwenmt_model", "label": "翻译模型", "type": "text", "default": "qwen-mt-plus", "placeholder": "需以 qwen-mt 开头"}, {"key": "qwenmt_asr_model", "label": "语音识别模型", "type": "text", "default": "qwen3-asr-flash", "placeholder": "需以 qwen3-asr 开头"}, ], }, "字节火山 (VolcEngine)": { "category": "字幕翻译渠道", "fields": [ {"key": "zijiehuoshan_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "zijiehuoshan_model", "label": "推理接入点", "type": "text", "default": "", "placeholder": "输入接入点名称"}, ], }, "MiniMax 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "minimax_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "minimax_api", "label": "API URL", "type": "text", "default": "api.minimax.io"}, {"key": "minimax_model", "label": "模型", "type": "text", "default": "MiniMax-M3", "placeholder": "输入模型名称"}, {"key": "minimax_max_tokens", "label": "最大输出 Token", "type": "text", "default": "8192"}, ], }, "智谱 AI 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "zhipu_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "zhipu_model", "label": "模型", "type": "text", "default": "glm-4-flash", "placeholder": "输入模型名称"}, {"key": "zhipu_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, ], }, "硅基流动 (SiliconFlow)": { "category": "字幕翻译渠道", "fields": [ {"key": "guiji_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "guiji_model", "label": "模型", "type": "text", "default": "Qwen/Qwen3-32B", "placeholder": "输入模型名称"}, {"key": "guiji_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, ], }, "OpenRouter 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "openrouter_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "openrouter_model", "label": "模型", "type": "text", "default": "", "placeholder": "输入模型名称"}, {"key": "openrouter_max_token", "label": "最大输出 Token", "type": "text", "default": "8192"}, ], }, "小米 AI 翻译": { "category": "字幕翻译渠道", "fields": [ {"key": "xiaomi_key", "label": "小米 Key", "type": "text", "default": ""}, {"key": "xiaomi_model", "label": "模型", "type": "text", "default": "mimo-v2.5-pro", "placeholder": "输入模型名称"}, {"key": "xiaomi_maxtoken", "label": "最大 Token", "type": "text", "default": "8192"}, ], }, # === 语音识别渠道 === "OpenAI ASR": { "category": "语音识别渠道", "fields": [ {"key": "openairecognapi_url", "label": "API URL", "type": "text", "default": "", "placeholder": "留空使用官方API"}, {"key": "openairecognapi_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "openairecognapi_model", "label": "模型", "type": "text", "default": "whisper-1", "placeholder": "输入模型名称"}, ], }, "Deepgram ASR": { "category": "语音识别渠道", "fields": [ {"key": "deepgram_apikey", "label": "API Key", "type": "text", "default": ""}, ], }, "Parakeet ASR": { "category": "语音识别渠道", "fields": [ {"key": "parakeet_address", "label": "API URL", "type": "text", "default": "http://127.0.0.1:8080"}, ], }, "字节语音识别": { "category": "语音识别渠道", "fields": [ {"key": "zijierecognmodel_appid", "label": "AppID", "type": "text", "default": ""}, {"key": "zijierecognmodel_token", "label": "Access Token", "type": "text", "default": ""}, ], }, # === 配音渠道 === "OpenAI TTS": { "category": "配音渠道", "fields": [ {"key": "openaitts_api", "label": "API URL", "type": "text", "default": "", "placeholder": "留空使用官方API"}, {"key": "openaitts_key", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "openaitts_model", "label": "模型", "type": "text", "default": "tts-1", "placeholder": "输入模型名称"}, ], }, "Azure TTS": { "category": "配音渠道", "fields": [ {"key": "azure_speech_key", "label": "SPEECH KEY", "type": "text", "default": ""}, {"key": "azure_speech_region", "label": "Region / URL", "type": "text", "default": "eastasia", "placeholder": "如 eastasia 或完整URL"}, ], }, "ElevenLabs TTS": { "category": "配音渠道", "fields": [ {"key": "elevenlabstts_key", "label": "API Key", "type": "text", "default": ""}, ], }, "GPT-SoVITS": { "category": "配音渠道", "fields": [ {"key": "gptsovits_url", "label": "API URL", "type": "text", "default": "http://127.0.0.1:9880"}, ], }, "F5-TTS / Spark / Index / Dia / VoxCPM": { "category": "配音渠道", "fields": [ {"key": "f5tts_url", "label": "F5-TTS URL", "type": "text", "default": "http://127.0.0.1:7860"}, {"key": "sparktts_url", "label": "Spark-TTS URL", "type": "text", "default": "http://127.0.0.1:7860"}, {"key": "indextts_url", "label": "Index-TTS URL", "type": "text", "default": "http://127.0.0.1:7860"}, {"key": "diatts_url", "label": "Dia-TTS URL", "type": "text", "default": "http://127.0.0.1:7860"}, {"key": "voxcpmtts_url", "label": "VoxCPM URL", "type": "text", "default": "http://127.0.0.1:7860"}, {"key": "confuciustts_url", "label": "Confucius URL", "type": "text", "default": "http://127.0.0.1:7860"}, ], }, "CosyVoice TTS": { "category": "配音渠道", "fields": [ {"key": "cosyvoice_url", "label": "WebUI URL", "type": "text", "default": "http://127.0.0.1:8000"}, {"key": "cosyvoice_instruct_text", "label": "Prompt 提示词", "type": "text", "default": ""}, ], }, "OmniVoice TTS": { "category": "配音渠道", "fields": [ {"key": "omnivoice_url", "label": "URL", "type": "text", "default": "http://127.0.0.1:8081"}, ], }, "阿里百炼 TTS (Qwen-TTS)": { "category": "配音渠道", "fields": [ {"key": "qwentts_key", "label": "百炼 SK", "type": "text", "default": ""}, {"key": "qwentts_model", "label": "模型", "type": "text", "default": "qwen3-tts-flash", "placeholder": "输入模型名称"}, ], }, "Qwen-TTS 本地": { "category": "配音渠道", "fields": [ {"key": "qwenttslocal_prompt", "label": "自定义语音提示词", "type": "text", "default": ""}, ], }, "豆包语音合成 2.0": { "category": "配音渠道", "fields": [ {"key": "doubao2_appid", "label": "App ID", "type": "text", "default": ""}, {"key": "doubao2_access", "label": "Access Token", "type": "text", "default": ""}, ], }, "Minimaxi TTS": { "category": "配音渠道", "fields": [ {"key": "minimaxi_apikey", "label": "SK 密钥", "type": "text", "default": ""}, {"key": "minimaxi_apiurl", "label": "API URL", "type": "text", "default": "api.minimaxi.com"}, ], }, "X.AI TTS": { "category": "配音渠道", "fields": [ {"key": "xaitts_key", "label": "SK 密钥", "type": "text", "default": ""}, ], }, "小米 TTS": { "category": "配音渠道", "fields": [ {"key": "xiaomi_key", "label": "小米 Key", "type": "text", "default": ""}, ], }, } # --------------------------------------------------------------------------- # ASS 样式编辑器(纯 Gradio) # --------------------------------------------------------------------------- def build_ass_editor(): import gradio as gr style = _load_ass_style() with gr.Accordion("🎨 硬字幕样式编辑", open=False): gr.Markdown("修改后点击「保存样式」,样式将应用于所有嵌入硬字幕的任务。") with gr.Tabs(): with gr.Tab("主字幕"): with gr.Row(): ass_fontname = gr.Textbox(label="字体名称", value=style.get('Fontname', 'Arial')) ass_fontsize = gr.Slider(label="字体大小", minimum=1, maximum=200, value=style.get('Fontsize', 16), step=1) with gr.Row(): ass_primary_color = gr.ColorPicker(label="主颜色", value=_parse_ass_color(style.get('PrimaryColour', '&H00FFFFFF&'))) ass_outline_color = gr.ColorPicker(label="描边颜色", value=_parse_ass_color(style.get('OutlineColour', '&H00000000&'))) ass_back_color = gr.ColorPicker(label="背景颜色", value=_parse_ass_color(style.get('BackColour', '&H00000000&'))) with gr.Row(): ass_bold = gr.Checkbox(label="粗体", value=bool(style.get('Bold', 0))) ass_italic = gr.Checkbox(label="斜体", value=bool(style.get('Italic', 0))) ass_underline = gr.Checkbox(label="下划线", value=bool(style.get('Underline', 0))) ass_strikeout = gr.Checkbox(label="删除线", value=bool(style.get('StrikeOut', 0))) with gr.Tab("底部字幕(双语时)"): with gr.Row(): ass_bottom_fontname = gr.Textbox(label="字体名称", value=style.get('Bottom_Fontname', 'Arial')) ass_bottom_fontsize = gr.Slider(label="字体大小", minimum=1, maximum=200, value=style.get('Bottom_Fontsize', 16), step=1) with gr.Row(): ass_bottom_primary_color = gr.ColorPicker(label="主颜色", value=_parse_ass_color(style.get('Bottom_PrimaryColour', '&H00FFFFFF&'))) ass_bottom_outline_color = gr.ColorPicker(label="描边颜色", value=_parse_ass_color(style.get('Bottom_OutlineColour', '&H00000000&'))) ass_bottom_back_color = gr.ColorPicker(label="背景颜色", value=_parse_ass_color(style.get('Bottom_BackColour', '&H00000000&'))) with gr.Row(): ass_bottom_bold = gr.Checkbox(label="粗体", value=bool(style.get('Bottom_Bold', 0))) ass_bottom_italic = gr.Checkbox(label="斜体", value=bool(style.get('Bottom_Italic', 0))) with gr.Tab("全局样式"): with gr.Row(): ass_border_style = gr.Dropdown(label="边框样式", choices=["描边", "不透明背景"], value="描边" if style.get('BorderStyle', 1) == 1 else "不透明背景") ass_outline = gr.Slider(label="描边粗细", minimum=0.0, maximum=10.0, value=style.get('Outline', 0.5), step=0.1) ass_shadow = gr.Slider(label="阴影", minimum=0.0, maximum=10.0, value=style.get('Shadow', 0.5), step=0.1) with gr.Row(): ass_scale_x = gr.Slider(label="水平缩放 %", minimum=1, maximum=1000, value=style.get('ScaleX', 100), step=1) ass_scale_y = gr.Slider(label="垂直缩放 %", minimum=1, maximum=1000, value=style.get('ScaleY', 100), step=1) ass_spacing = gr.Slider(label="字间距", minimum=-100, maximum=100, value=style.get('Spacing', 0), step=1) ass_angle = gr.Slider(label="旋转角度", minimum=-360, maximum=360, value=style.get('Angle', 0), step=1) with gr.Row(): ass_margin_l = gr.Slider(label="左边距", minimum=0, maximum=1000, value=style.get('MarginL', 10), step=1) ass_margin_r = gr.Slider(label="右边距", minimum=0, maximum=1000, value=style.get('MarginR', 10), step=1) ass_margin_v = gr.Slider(label="垂直边距", minimum=0, maximum=1000, value=style.get('MarginV', 10), step=1) ass_alignment = gr.Dropdown(label="对齐位置", choices=["左下", "中下", "右下", "左中", "正中", "右中", "左上", "中上", "右上"], value={1: "左下", 2: "中下", 3: "右下", 4: "左中", 5: "正中", 6: "右中", 7: "左上", 8: "中上", 9: "右上"}.get(style.get('Alignment', 2), "中下")) with gr.Row(): ass_save_btn = gr.Button("💾 保存样式", variant="primary") ass_reset_btn = gr.Button("🔄 恢复默认") ass_status = gr.Textbox(label="状态", interactive=False, visible=True) def save_ass_style(fontname, fontsize, primary_color, outline_color, back_color, bold, italic, underline, strikeout, bottom_fontname, bottom_fontsize, bottom_primary_color, bottom_outline_color, bottom_back_color, bottom_bold, bottom_italic, border_style, outline, shadow, scale_x, scale_y, spacing, angle, margin_l, margin_r, margin_v, alignment): am = {"左下": 1, "中下": 2, "右下": 3, "左中": 4, "正中": 5, "右中": 6, "左上": 7, "中上": 8, "右上": 9} _save_ass_style({ 'Name': 'Default', 'Fontname': fontname, 'Bottom_Fontname': bottom_fontname, 'Fontsize': int(fontsize), 'Bottom_Fontsize': int(bottom_fontsize), 'PrimaryColour': _to_ass_color(primary_color), 'Bottom_PrimaryColour': _to_ass_color(bottom_primary_color), 'SecondaryColour': '&H00FFFFFF&', 'OutlineColour': _to_ass_color(outline_color), 'BackColour': _to_ass_color(back_color), 'Bold': 1 if bold else 0, 'Italic': 1 if italic else 0, 'Bottom_SecondaryColour': '&H00FFFFFF&', 'Bottom_OutlineColour': _to_ass_color(bottom_outline_color), 'Bottom_BackColour': _to_ass_color(bottom_back_color), 'Bottom_Bold': 1 if bottom_bold else 0, 'Bottom_Italic': 1 if bottom_italic else 0, 'Underline': 1 if underline else 0, 'StrikeOut': 1 if strikeout else 0, 'ScaleX': int(scale_x), 'ScaleY': int(scale_y), 'Spacing': int(spacing), 'Angle': int(angle), 'BorderStyle': 1 if border_style == "描边" else 3, 'Outline': float(outline), 'Shadow': float(shadow), 'Alignment': am.get(alignment, 2), 'MarginL': int(margin_l), 'MarginR': int(margin_r), 'MarginV': int(margin_v), 'Encoding': 1, }) return "✅ 样式已保存" def reset_ass_style(): _save_ass_style(DEFAULT_ASS_STYLE.copy()) s = DEFAULT_ASS_STYLE return (s['Fontname'], s['Fontsize'], _parse_ass_color(s['PrimaryColour']), _parse_ass_color(s['OutlineColour']), _parse_ass_color(s['BackColour']), bool(s['Bold']), bool(s['Italic']), bool(s['Underline']), bool(s['StrikeOut']), s['Bottom_Fontname'], s['Bottom_Fontsize'], _parse_ass_color(s['Bottom_PrimaryColour']), _parse_ass_color(s['Bottom_OutlineColour']), _parse_ass_color(s['Bottom_BackColour']), bool(s['Bottom_Bold']), bool(s['Bottom_Italic']), "描边" if s['BorderStyle'] == 1 else "不透明背景", s['Outline'], s['Shadow'], s['ScaleX'], s['ScaleY'], s['Spacing'], s['Angle'], s['MarginL'], s['MarginR'], s['MarginV'], {1: "左下", 2: "中下", 3: "右下", 4: "左中", 5: "正中", 6: "右中", 7: "左上", 8: "中上", 9: "右上"}.get(s['Alignment'], "中下"), "✅ 已恢复默认样式") ass_save_btn.click(fn=save_ass_style, inputs=[ass_fontname, ass_fontsize, ass_primary_color, ass_outline_color, ass_back_color, ass_bold, ass_italic, ass_underline, ass_strikeout, ass_bottom_fontname, ass_bottom_fontsize, ass_bottom_primary_color, ass_bottom_outline_color, ass_bottom_back_color, ass_bottom_bold, ass_bottom_italic, ass_border_style, ass_outline, ass_shadow, ass_scale_x, ass_scale_y, ass_spacing, ass_angle, ass_margin_l, ass_margin_r, ass_margin_v, ass_alignment], outputs=[ass_status]) ass_reset_btn.click(fn=reset_ass_style, inputs=[], outputs=[ass_fontname, ass_fontsize, ass_primary_color, ass_outline_color, ass_back_color, ass_bold, ass_italic, ass_underline, ass_strikeout, ass_bottom_fontname, ass_bottom_fontsize, ass_bottom_primary_color, ass_bottom_outline_color, ass_bottom_back_color, ass_bottom_bold, ass_bottom_italic, ass_border_style, ass_outline, ass_shadow, ass_scale_x, ass_scale_y, ass_spacing, ass_angle, ass_margin_l, ass_margin_r, ass_margin_v, ass_alignment, ass_status]) # --------------------------------------------------------------------------- # 渠道设置面板构建 # --------------------------------------------------------------------------- def build_channel_settings(): """构建所有渠道设置面板""" import gradio as gr # 按 category 分组 categories = {} for name, cfg in CHANNEL_SETTINGS.items(): cat = cfg["category"] if cat not in categories: categories[cat] = [] categories[cat].append((name, cfg)) gr.Markdown("### 渠道设置") gr.Markdown("配置各渠道的 API 地址、SK 密钥等信息。**保存后与桌面版 (sp.exe) 通用**,配置文件存储在 `videotrans/params.json` 中。") with gr.Tabs(): for cat_name, channels in categories.items(): with gr.Tab(cat_name): for ch_name, ch_cfg in channels: with gr.Accordion(ch_name, open=False): fields = [] for f in ch_cfg["fields"]: val = str(_safe_get(f["key"], f.get("default", ""))) tb = gr.Textbox( label=f["label"], value=val, placeholder=f.get("placeholder", ""), interactive=True, ) fields.append((f["key"], tb)) save_btn = gr.Button("💾 保存", size="sm") status = gr.Textbox(label="", interactive=False, visible=True,show_label=False) # 使用闭包捕获当前值 def make_save_handler(field_keys, field_widgets): def handler(*values): data = {} for k, v in zip(field_keys, values): data[k] = v _save_params(data) return "✅ 已保存" return handler save_btn.click( fn=make_save_handler([f[0] for f in fields], [f[1] for f in fields]), inputs=[f[1] for f in fields], outputs=[status], ) # === 参考音频 Tab === with gr.Tab("设置参考音频"): gr.Markdown("### 声音克隆参考音频设置") gr.Markdown( "配置声音克隆(clone)使用的参考音频。每行一条,格式为:`文件名.wav#音频中的说话文本`\n" f"- 音频文件需放在 `{ROOT_DIR}/f5-tts/` 目录下\n" "- 文件格式必须为 wav\n" "- 每行用 `#` 分隔文件名和对应文本" ) ref_audio_text = gr.Textbox( label="参考音频列表", value=str(_safe_get("f5tts_role", "")), placeholder="myaudio1.wav#你说四大皆空,却为何紧闭双眼\nmyaudio2.wav#Hello, this is a test audio", lines=8, interactive=True, ) ref_audio_save = gr.Button("💾 保存参考音频", variant="primary") ref_audio_status = gr.Markdown("", visible=False) def save_ref_audio(text): text = text.strip() if not text: return gr.Markdown("⚠️ 请输入参考音频信息", visible=True) lines = text.split("\n") errors = [] for i, line in enumerate(lines): line = line.strip() if not line: continue parts = line.split("#") if len(parts) != 2: errors.append(f"第 {i+1} 行格式错误,需用 # 分隔文件名和文本") continue filename = parts[0].strip() f5tts_dir = Path(ROOT_DIR) / "f5-tts" # 检查文件是否存在(支持带/不带 .wav 后缀) if not (f5tts_dir / filename).exists() and not (f5tts_dir / f"{filename}.wav").exists(): errors.append(f"第 {i+1} 行:文件 `{filename}` 在 f5-tts/ 目录下不存在") continue # 自动补全 .wav 后缀 if not filename.endswith(".wav") and (f5tts_dir / f"{filename}.wav").exists(): lines[i] = f"{filename}.wav#{parts[1].strip()}" if errors: return gr.Markdown("⚠️ 保存失败:\n" + "\n".join(errors), visible=True) role_text = "\n".join(line for line in lines if line.strip()) _save_params({"f5tts_role": role_text}) return gr.Markdown("✅ 参考音频已保存", visible=True) ref_audio_save.click( fn=save_ref_audio, inputs=[ref_audio_text], outputs=[ref_audio_status], ) # --------------------------------------------------------------------------- # 高级选项设置面板 # --------------------------------------------------------------------------- COMBO_BOX_KEYS = { 'cuda_com_type', 'llm_ai_type', 'vad_type', 'speaker_type', 'video_codec', 'preset', 'lang', 'uvr_models', 'out_video_ext', 'fps_mode', } COMBO_BOX_OPTIONS = { "cuda_com_type": ['default', 'auto', 'int8', 'int16', 'float16', 'float32', 'bfloat16', 'int8_float16', 'int8_float32', 'int8_bfloat16'], "fps_mode": ["vfr", "cfr"], "llm_ai_type": ['chatgpt', 'deepseek'], "vad_type": ['tenvad', 'silero'], "speaker_type": ['built', 'ali_CAM', 'pyannote', 'reverb'], "video_codec": ['264', '265'], "preset": ['ultrafast', 'superfast', 'veryfast', 'faster', 'fast', 'medium', 'slow', 'slower', 'veryslow'], "uvr_models": ['spleeter', 'UVR-MDX-NET-Inst_HQ_4', 'UVR-MDX-NET-Inst_HQ_1', 'UVR-MDX-NET-Inst_HQ_2', 'UVR-MDX-NET-Inst_HQ_3', 'UVR-MDX-NET-Inst_HQ_5', 'UVR-MDX-NET-Inst_Main', 'UVR-MDX-NET-Inst_1', 'UVR-MDX-NET-Inst_2', 'UVR-MDX-NET-Inst_3'], "out_video_ext": ['.mp4', '.mkv'], } # Whisper 提示词 keys 和中文标签 _prompt_keys_list = [ "initial_prompt_zh-cn", "initial_prompt_zh-tw", "initial_prompt_en", "initial_prompt_ja", "initial_prompt_ko", "initial_prompt_fr", "initial_prompt_de", "initial_prompt_ru", "initial_prompt_es", "initial_prompt_pt", "initial_prompt_it", "initial_prompt_ar", "initial_prompt_vi", "initial_prompt_th", "initial_prompt_tr", "initial_prompt_hi", ] _prompt_labels = {k: f"whisper {k.replace('initial_prompt_', '')} 提示词" for k in _prompt_keys_list} # 全局 widget 注册表 _all_widgets = {} def _w(key, label, tip="", area=False): """创建一个设置项:标题在上,组件在下""" import gradio as gr val = str(_user_settings.get(key, "")) with gr.Column(): label_text = f"**{label}**" + (f"\n{tip}" if tip else "") gr.Markdown(label_text) if key in COMBO_BOX_KEYS: options = COMBO_BOX_OPTIONS.get(key, [val]) w = gr.Dropdown(choices=options, value=val if val in options else options[0], label="", interactive=True,show_label=False) elif val.lower() in ('true', 'false'): w = gr.Checkbox(value=val.lower() == 'true', label="", show_label=False,interactive=True) else: w = gr.Textbox(value=val, label=None,show_label=False, lines=3 if area else 1, interactive=True) _all_widgets[key] = w def _save_section(section_key, keys): """为指定分区创建保存按钮和状态显示""" import gradio as gr with gr.Row(): save_btn = gr.Button(f"💾 保存 {ADVANCED_SECTION_TITLES.get(section_key, section_key)}", variant="primary", size="sm") status = gr.Markdown("", visible=False) def _make_handler(k_list): def handler(*values): data = {} for k, v in zip(k_list, values): data[k] = str(v) _save_settings(data) return gr.Markdown(f"✅ 已保存", visible=True) return handler save_btn.click(fn=_make_handler(keys), inputs=[_all_widgets[k] for k in keys], outputs=[status]) # --------------------------------------------------------------------------- # 高级选项设置面板(紧凑网格布局) # --------------------------------------------------------------------------- ADVANCED_SECTION_TITLES = { "common": "通用设置", "video": "视频输出控制", "whisper": "语音识别参数", "trans": "字幕翻译调整", "dubbing": "字幕配音调整", "justify": "字幕声音画面对齐", "prompt_init": "Whisper模型提示词", } def build_advanced_settings(): import gradio as gr gr.Markdown("配置全局高级参数。**保存后与桌面版 (sp.exe) 通用**,配置文件存储在 `videotrans/cfg.json` 中。\n⚠️ 部分参数修改后需要**重启软件**才能生效。") # ---- 通用设置 ---- with gr.Accordion("📋 通用设置", open=True): with gr.Row(): _w("lang", "软件界面语言", "设置后需重启") _w("countdown_sec", "单视频暂停倒计时", "设为0跳过编辑窗口") _w("retry_nums", "失败后重试次数", "") with gr.Row(): _w("llm_chunk_size", "LLM断句每批字幕行数", "默认20") _w("llm_ai_type", "LLM断句AI渠道", "chatgpt/deepseek") _w("batch_nums", "批量每批数量", "0=不限制") with gr.Row(): _w("dont_notify", "禁用桌面通知", "") _w("show_more_settings", "主界面显示所有参数?", "") _w("homedir", "独立功能输出目录", "") with gr.Row(): _w("process_max", "CPU任务数[重启]", "不超过cpu核数") _w("process_max_gpu", "GPU任务数[重启]", "多卡或显存>24G才>1") _w("multi_gpus", "多显卡模式[重启]", "") _save_section("common", ["lang", "countdown_sec", "retry_nums", "llm_chunk_size", "llm_ai_type", "batch_nums", "dont_notify", "show_more_settings", "homedir", "process_max", "process_max_gpu", "multi_gpus"]) # ---- 视频输出控制 ---- with gr.Accordion("📋 视频输出控制", open=False): with gr.Row(): _w("crf", "视频质量(0=无损,51=差)", "") _w("preset", "压缩率", "ultrafast→veryslow") _w("video_codec", "264/265编码", "") with gr.Row(): _w("out_video_ext", "输出格式", "mp4/mkv") _w("fps_mode", "帧率模式", "vfr/cfr") _w("force_lib", "强制软编码?", "") with gr.Row(): _w("hw_decode", "cuda硬解码", "") _w("ffmpeg_cmd", "自定义ffmpeg参数", "") _save_section("video", ["crf", "preset", "video_codec", "out_video_ext", "fps_mode", "force_lib", "hw_decode", "ffmpeg_cmd"]) # ---- 语音识别参数 ---- with gr.Accordion("📋 语音识别参数", open=False): with gr.Row(): _w("vad_type", "选择VAD", "tenvad/silero") _w("threshold", "语音阈值", "") _w("no_speech_threshold", "非语音阈值", "") with gr.Row(): _w("max_speech_duration_s", "最长语音(秒)", "") _w("min_speech_duration_ms", "最短语音(毫秒)", "") _w("min_silence_duration_ms", "静音分割(毫秒)", "") with gr.Row(): _w("max_speech_duration_s2", "二次识别最长(秒)", "") _w("min_speech_duration_ms2", "二次识别最短(毫秒)", "") _w("merge_short_sub", "合并过短字幕", "") with gr.Row(): _w("whisper_prepare", "Whisper预分割?", "clone配音时选中") _w("speaker_type", "说话人分离模型", "内置/pyannote") _w("hf_token", "Huggingface token", "pyannote需要") with gr.Row(): _w("cuda_com_type", "计算数据类型", "int8/float16/float32") _w("beam_size", "beam_size", "1-5") _w("best_of", "best_of", "1-5") with gr.Row(): _w("condition_on_previous_text", "上下文感知", "") _w("repetition_penalty", "重复惩罚", "") _w("compression_ratio_threshold", "文本压缩率", "") with gr.Row(): _w("temperature", "采样温度", "") _w("hotwords", "热词", "逗号分隔") _w("gemini_recogn_chunk", "Gemini切片数", "") with gr.Row(): _w("zh_hant_s", "繁体转简体", "") _w("del_end_punc", "删除末尾标点", "") with gr.Row(): _w("model_list", "faster-whisper模型", "逗号分隔", area=True) with gr.Row(): _w("Whisper_cpp_models", "whisper.cpp模型", "逗号分隔", area=True) _save_section("whisper", ["vad_type", "threshold", "no_speech_threshold", "max_speech_duration_s", "min_speech_duration_ms", "max_speech_duration_s2", "min_speech_duration_ms2", "min_silence_duration_ms", "merge_short_sub", "whisper_prepare", "speaker_type", "hf_token", "cuda_com_type", "beam_size", "best_of", "condition_on_previous_text", "repetition_penalty", "compression_ratio_threshold", "temperature", "hotwords", "gemini_recogn_chunk", "zh_hant_s", "del_end_punc", "model_list", "Whisper_cpp_models"]) # ---- 字幕翻译调整 ---- with gr.Accordion("📋 字幕翻译调整", open=False): with gr.Row(): _w("trans_thread", "传统翻译每批行数", "") _w("aitrans_thread", "AI翻译每批行数", "") _w("aitrans_temperature", "AI温度值", "默认1.0") with gr.Row(): _w("translation_wait", "翻译后暂停秒", "") _w("aisendsrt", "发送完整字幕", "") _w("aitrans_context", "一次性翻译所有行", "需超长上下文模型") _save_section("trans", ["trans_thread", "aitrans_thread", "aitrans_temperature", "translation_wait", "aisendsrt", "aitrans_context"]) # ---- 字幕配音调整 ---- with gr.Accordion("📋 字幕配音调整", open=False): with gr.Row(): _w("dubbing_thread", "并发配音线程数", "") _w("dubbing_wait", "配音后暂停秒", "") _w("remove_dubb_silence", "移除配音前后静音", "") with gr.Row(): _w("save_segment_audio", "保留每行配音文件", "") _w("normal_text", "文本规范化", "") _w("chattts_voice", "ChatTTS音色值", "") with gr.Row(): _w("edgetts_max_concurrent_tasks", "EdgeTTS并发数", "越大越快但可能限流") _w("edgetts_retry_nums", "EdgeTTS重试次数", "") _w("noise_separate_nums", "人声分离线程数", "") with gr.Row(): _w("uvr_models", "分离背景声模型", "") _save_section("dubbing", ["dubbing_thread", "dubbing_wait", "remove_dubb_silence", "save_segment_audio", "normal_text", "chattts_voice", "edgetts_max_concurrent_tasks", "edgetts_retry_nums", "noise_separate_nums", "uvr_models"]) # ---- 字幕声音画面对齐 ---- with gr.Accordion("📋 字幕声音画面对齐", open=False): with gr.Row(): _w("max_audio_speed_rate", "音频加速最大倍数", "默认100") _w("max_video_pts_rate", "视频慢放最大倍数", "默认10,≤10") with gr.Row(): _w("cjk_len", "中日韩字幕单行字符数", "") _w("other_len", "其他语言字幕单行字符数", "") _save_section("justify", ["max_audio_speed_rate", "max_video_pts_rate", "cjk_len", "other_len"]) # ---- Whisper模型提示词 ---- with gr.Accordion("📋 Whisper模型提示词", open=False): for i in range(0, len(_prompt_keys_list), 3): with gr.Row(): for k in _prompt_keys_list[i:i+3]: _w(k, _prompt_labels.get(k, k), "") _save_section("prompt_init", _prompt_keys_list) # --------------------------------------------------------------------------- # UI 构建 # --------------------------------------------------------------------------- def build_ui(): import gradio as gr with gr.Blocks(title="pyVideoTrans WebUI") as app: gr.Markdown(""" # pyVideoTrans 视频翻译 WebUI > [该界面仅实现部分功能,完整功能请使用桌面软件版(sp.exe 或 sp.py)](https://pyvideotrans.com) > > [使用文档](https://pyvideotrans.com) | > [开源地址](https://github.com/jianchang512/pyvideotrans) | > [遇到问题](https://bbs.pyvideotrans.com) ---- """) with gr.Tabs(): # === Tab 1: 视频翻译 === with gr.Tab("🎬 视频翻译", id="translate"): prev_recogn = gr.State(value=RECOGN_NAMES[DEFAULT_RECOGN]) prev_translate = gr.State(value=TRANSLATE_NAMES[DEFAULT_TRANSLATE]) prev_tts = gr.State(value=TTS_NAMES[DEFAULT_TTS]) with gr.Row(): with gr.Column(scale=3): input_file = gr.Video(label="选择视频文件", interactive=True) recogn_choice = gr.Dropdown(choices=RECOGN_NAMES, value=RECOGN_NAMES[int(_user_params.get('recogn_type', DEFAULT_RECOGN)) if str(_user_params.get('recogn_type', '')).isdigit() else DEFAULT_RECOGN], label="识别渠道", interactive=True) model_choice = gr.Dropdown(choices=FASTER_MODEL_NAMES, value=_user_params.get('model_name', DEFAULT_MODEL), label="模型", interactive=True) translate_choice = gr.Dropdown(choices=TRANSLATE_NAMES, value=TRANSLATE_NAMES[int(_user_params.get('translate_type', DEFAULT_TRANSLATE)) if str(_user_params.get('translate_type', '')).isdigit() else DEFAULT_TRANSLATE], label="翻译渠道", interactive=True) source_lang = gr.Dropdown(choices=LANG_DISPLAY_NAMES, value=_user_params.get('source_language', DEFAULT_SOURCE_LANG), label="发音语言(源语言)", interactive=True) target_lang = gr.Dropdown(choices=['-']+LANG_DISPLAY_NAMES, value=_user_params.get('target_language', DEFAULT_TARGET_LANG), label="目标语言", interactive=True) tts_choice = gr.Dropdown(choices=TTS_NAMES, value=TTS_NAMES[int(_user_params.get('tts_type', DEFAULT_TTS)) if str(_user_params.get('tts_type', '')).isdigit() else DEFAULT_TTS], label="配音渠道", interactive=True) # 根据已加载的TTS渠道和目标语言预填充角色列表 _init_tts_idx = int(_user_params.get('tts_type', DEFAULT_TTS)) if str(_user_params.get('tts_type', '')).isdigit() else DEFAULT_TTS _init_target = _user_params.get('target_language', DEFAULT_TARGET_LANG) _init_langcode = _lang_code_from_display(_init_target) if _init_target and _init_target != '-' else None try: _init_roles = role_menu(_init_tts_idx, langcode=_init_langcode) if not _init_roles: _init_roles = ["No"] except Exception: _init_roles = ["No"] _saved_role = _user_params.get('voice_role', 'No') _init_role_val = _saved_role if _saved_role in _init_roles else _init_roles[0] voice_role = gr.Dropdown(choices=_init_roles, value=_init_role_val, label="配音角色", interactive=True) with gr.Row(): voice_autorate = gr.Checkbox(label="配音加速", value=True) video_autorate = gr.Checkbox(label="视频慢速", value=False) with gr.Row(): voice_rate = gr.Slider(minimum=-50, maximum=50, value=int(str(_user_params.get("voice_rate", "0")).replace("%","")), step=1, label="配音语速 (%)") volume_rate = gr.Slider(minimum=-95, maximum=100, value=int(str(_user_params.get("volume", "0")).replace("%","")), step=1, label="音量调整 (%)") pitch_rate = gr.Slider(minimum=-100, maximum=100, value=int(str(_user_params.get("pitch", "0")).replace("Hz","")), step=1, label="音调 (Hz)") subtitle_type = gr.Dropdown(choices=list(SUBTITLE_TYPES.keys()), value=list(SUBTITLE_TYPES.keys())[int(_user_params.get('subtitle_type', 1)) if str(_user_params.get('subtitle_type', '')).isdigit() and int(_user_params.get('subtitle_type', 1)) < len(SUBTITLE_TYPES) else 1], label="字幕嵌入类型", interactive=True) build_ass_editor() with gr.Accordion("📋 更多设置", open=False): with gr.Row(): remove_noise = gr.Checkbox(label="降噪", value=False) fix_punc = gr.Dropdown(choices=list(PUNC_OPTIONS.keys()), value="默认标点", label="标点处理", interactive=True) with gr.Row(): is_separate = gr.Checkbox(label="分离人声背景声", value=False) embed_bgm = gr.Checkbox(label="重新嵌入背景声", value=True) with gr.Row(): loop_bgm = gr.Dropdown(choices=list(LOOP_BGM_OPTIONS.keys()), value="背景音截断", label="背景音处理", interactive=True) backaudio_volume = gr.Slider(minimum=0.0, maximum=2.0, value=float(_user_params.get("backaudio_volume", settings.get("backaudio_volume", 0.8))), step=0.1, label="背景音量") cuda_accel = gr.Checkbox(label="启用 CUDA 加速", value=False) channel_warning = gr.Markdown("", visible=False) start_btn = gr.Button("🚀 开始执行", variant="primary", size="lg") with gr.Column(scale=2): log_output = gr.Textbox(label="执行日志", lines=20, interactive=False) video_preview = gr.Video(label="视频预览", interactive=False) result_files = gr.File(label="输出文件(点击下载)", interactive=False) # 渠道验证 def validate_recogn(choice, prev): idx = _recogn_index_from_display(choice) _rs=recognition.is_input_api(recogn_type=idx, return_str=True) if _rs is not True: msg = "渠道「{}」暂不可用,已自动回退".format(choice) gr.Warning(msg) return prev, f"⚠️ {msg}" return choice, "" def validate_translate(choice, prev): idx = _translate_index_from_display(choice) _rs=translator.is_allow_translate(translate_type=idx, return_str=True) if _rs is not True: msg = "渠道「{}」暂不可用,已自动回退".format(choice) gr.Warning(msg) return prev, f"⚠️ {msg}" return choice, "" def tts_change_handler(choice, prev, target_display): idx = _tts_index_from_display(choice) warning = "" _rs=tts.is_input_api(tts_type=idx, return_str=True) if _rs is not True: msg = "渠道「{}」暂不可用,已自动回退".format(choice) gr.Warning(msg) choice = prev warning = f"⚠️ {msg}" tts_idx = _tts_index_from_display(choice) lang_code = _lang_code_from_display(target_display) try: roles = role_menu(tts_idx, langcode=lang_code) if not roles: roles = ["No"] except Exception: roles = ["No"] return choice, gr.update(choices=roles, value=roles[0] if roles else "No"), warning recogn_choice.change(fn=validate_recogn, inputs=[recogn_choice, prev_recogn], outputs=[recogn_choice, channel_warning]) translate_choice.change(fn=validate_translate, inputs=[translate_choice, prev_translate], outputs=[translate_choice, channel_warning]) tts_choice.change(fn=tts_change_handler, inputs=[tts_choice, prev_tts, target_lang], outputs=[tts_choice, voice_role, channel_warning]) def update_voice_roles(tts_display, target_display): tts_idx = _tts_index_from_display(tts_display) lang_code = _lang_code_from_display(target_display) try: roles = role_menu(tts_idx, langcode=lang_code) if not roles: roles = ["No"] except Exception: roles = ["No"] return gr.update(choices=roles, value=roles[0] if roles else "No") target_lang.change(fn=update_voice_roles, inputs=[tts_choice, target_lang], outputs=[voice_role]) # 执行翻译 _BTN_RUNNING = gr.update(value="⏳ 执行中...", interactive=False) _BTN_IDLE = gr.update(value="🚀 开始执行", interactive=True) def run_translation(file_path, recogn_display, model_name, translate_display, source_display, target_display, tts_display, voice_role_name, voice_autorate_val, video_autorate_val, voice_rate_val, volume_rate_val, pitch_rate_val, subtitle_type_name, remove_noise_val, fix_punc_name, is_separate_val, embed_bgm_val, loop_bgm_name, backaudio_volume_val, cuda_val): print(f'{file_path=}') if not file_path: yield "❌ 请先选择一个视频或音频文件", None, [], _BTN_IDLE return app_cfg.current_status = 'ing' # 清空上次的日志、预览和输出,显示执行中状态 yield "", None, [], _BTN_RUNNING log_lines = [] def log(msg): log_lines.append(f"[{time.strftime('%H:%M:%S')}] {msg}") return "\n".join(log_lines) recogn_idx = _recogn_index_from_display(recogn_display) translate_idx = _translate_index_from_display(translate_display) tts_idx = _tts_index_from_display(tts_display) source_code = _lang_code_from_display(source_display) target_code = _lang_code_from_display(target_display) subtitle_val = SUBTITLE_TYPES.get(subtitle_type_name, 1) fix_punc_val = PUNC_OPTIONS.get(fix_punc_name, 0) loop_bgm_val = LOOP_BGM_OPTIONS.get(loop_bgm_name, 0) try: app_cfg.exit_soft = False app_cfg.exec_mode = 'cli' getset_gpu() _file_obj = tools.format_video(Path(file_path).absolute().as_posix()) _nospacebasename = _file_obj["basename"].replace(" ", "-").replace(".", "-") _cache_folder = f'{TEMP_DIR}/{_file_obj["uuid"]}' app_cfg.rm_uuid(_file_obj['uuid']) _target_dir = f'{ROOT_DIR}/output/{_nospacebasename}' _file_obj['target_dir'] = _target_dir Path(_cache_folder).mkdir(parents=True, exist_ok=True) target_path = Path(_target_dir) if target_path.exists(): for f in sorted(target_path.rglob("*")): if f.is_file(): if f.suffix.lower() in ['.mp4','.mkv']: f.unlink(missing_ok=True) Path(_target_dir).mkdir(parents=True, exist_ok=True) from dataclasses import asdict common_params = {'name': file_path, "cache_folder": _cache_folder} common_params.update(asdict(_file_obj)) yield log(f"源文件: {Path(file_path).name}"), None, [], _BTN_RUNNING vtv_params = { "source_language_code": source_code, "target_language_code": target_code, "recogn_type": recogn_idx, "model_name": model_name, "is_cuda": cuda_val, "remove_noise": remove_noise_val, "enable_diariz": False, "nums_diariz": -1, "detect_language": source_code, "rephrase": 0, "fix_punc": fix_punc_val, "tts_type": tts_idx, "voice_role": voice_role_name, "voice_rate": _format_rate(int(voice_rate_val)), "volume": _format_rate(int(volume_rate_val)), "pitch": _format_pitch(int(pitch_rate_val)), "voice_autorate": voice_autorate_val, "video_autorate": video_autorate_val, "align_sub_audio": True, "translate_type": translate_idx, "is_separate": is_separate_val, "recogn2pass": False, "subtitle_type": subtitle_val, "clear_cache": True, "embed_bgm": embed_bgm_val, "loop_backaudio": loop_bgm_val, "backaudio_volume": backaudio_volume_val, "background_music": "", } params_dict = {**common_params, **vtv_params} yield log(f"识别: {RECOGN_NAMES[recogn_idx]} 翻译: {TRANSLATE_NAMES[translate_idx]} 配音: {TTS_NAMES[tts_idx]}"), None, [], _BTN_RUNNING yield log(f"语言: {source_code} → {target_code} 角色: {voice_role_name}"), None, [], _BTN_RUNNING yield log(""), None, [], _BTN_RUNNING yield log("▶ 开始执行视频翻译..."), None, [], _BTN_RUNNING from videotrans.task.trans_create import TransCreate from videotrans.task.taskcfg import TaskCfgVTT trk = TransCreate(cfg=TaskCfgVTT(**params_dict)) stages = [ ("阶段 1/8: 预处理...", "prepare", "预处理完成"), ("阶段 2/8: 语音识别...", "recogn", "语音识别完成"), ("阶段 3/8: 说话人分离...", "diariz", "说话人分离完成"), ("阶段 4/8: 字幕翻译...", "trans", "字幕翻译完成"), ("阶段 5/8: 配音生成...", "dubbing", "配音生成完成"), ("阶段 6/8: 音画对齐...", "align", "音画对齐完成"), ("阶段 7/8: 二次识别...", "recogn2pass", "二次识别完成"), ("阶段 8/8: 最终合成...", "assembling", "最终合成完成"), ] for stage_name, method, done_msg in stages: yield log(stage_name), None, [], _BTN_RUNNING getattr(trk, method)() if method != "assembling": yield log(f"✓ {done_msg}"), None, [], _BTN_RUNNING trk.task_done() yield log("✓ 视频合成完成"), None, [], _BTN_RUNNING yield log("✅ 全部任务执行完毕!"), None, [], _BTN_RUNNING output_files, video_preview_path = [], None if target_path.exists(): for f in sorted(target_path.rglob("*")): if f.is_file(): if f.suffix.lower() == '.mp4' and video_preview_path is None: video_preview_path = str(f) else: output_files.append(str(f)) if not output_files and video_preview_path is None: for f in sorted(Path(_cache_folder).rglob("*")): if f.is_file(): if f.suffix.lower() == '.mp4' and video_preview_path is None: video_preview_path = str(f) elif f.suffix.lower() in ('.mkv', '.wav', '.srt', '.txt', '.mp3'): output_files.append(str(f)) # 添加当天日志文件到输出列表 import datetime log_file = Path(ROOT_DIR) / "logs" / f"{datetime.datetime.now().strftime('%Y%m%d')}.log" if log_file.exists(): output_files.append(str(log_file)) yield log(f"输出目录: {_target_dir}"), video_preview_path, output_files, _BTN_IDLE except Exception as e: tb = traceback.format_exc() yield log(f"❌ 执行出错: {str(e)}\n\n{tb}"), None, [], _BTN_IDLE start_btn.click(fn=run_translation, inputs=[input_file, recogn_choice, model_choice, translate_choice, source_lang, target_lang, tts_choice, voice_role, voice_autorate, video_autorate, voice_rate, volume_rate, pitch_rate, subtitle_type, remove_noise, fix_punc, is_separate, embed_bgm, loop_bgm, backaudio_volume, cuda_accel], outputs=[log_output, video_preview, result_files, start_btn]) # === Tab 2: 渠道设置 === with gr.Tab("⚙️ 渠道设置", id="settings"): build_channel_settings() # === Tab 3: 高级选项 === with gr.Tab("🔧 高级选项", id="advanced"): build_advanced_settings() return app if __name__ == "__main__": try: import argparse import gradio as gr parser = argparse.ArgumentParser(description="pyVideoTrans WebUI") parser.add_argument("--host", type=str, default="0.0.0.0", help="Host address") parser.add_argument("--port", type=int, default=7860, help="Port number") parser.add_argument("--share", action="store_true", help="Create a public Gradio link") args = parser.parse_args() app = build_ui() app.launch(server_name=args.host, server_port=args.port, share=args.share, inbrowser=True, theme=gr.themes.Soft(),css=""" /* 默认字体:微软雅黑 > 苹果方黑 > 系统无衬线字体 */ *, *::before, *::after { font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", "Noto Sans CJK SC", "Source Han Sans SC", "SimHei", sans-serif !important; } h1{text-align:center} /* 输入框和按钮的字体也统一 */ input, textarea, select, button, label, .gr-textbox, .gr-dropdown, .gr-checkbox { font-family: "Microsoft YaHei", "PingFang SC", "Hiragino Sans GB", "WenQuanYi Micro Hei", "Noto Sans CJK SC", "Source Han Sans SC", "SimHei", sans-serif !important; } """) except Exception as e: import traceback traceback.print_exc() print(f"\n❌ 启动失败: {e}")