jianchang512/pyvideotrans/main 605k tokens More Tools
```
├── .gitattributes (omitted)
├── .github/
   ├── FUNDING.yml (100 tokens)
   ├── workflows/
      ├── auto-close-old-issues.yml (300 tokens)
      ├── main.yml (100 tokens)
├── .gitignore (100 tokens)
├── .python-version
├── Dockerfile (400 tokens)
├── LICENSE (omitted)
├── README.md (1800 tokens)
├── cli.py (5k tokens)
├── docs/
   ├── README_CN.md (1200 tokens)
   ├── Synchronize.md (4.7k tokens)
   ├── about.md (2.9k tokens)
   ├── architecture.md (7k tokens)
   ├── cli.md (3.2k tokens)
   ├── faq.md (2.9k tokens)
   ├── googlecloud_tts.md (500 tokens)
   ├── language.md (1200 tokens)
   ├── webui.md (600 tokens)
   ├── whisper_net_setup.md (1800 tokens)
├── f5-tts/
   ├── cosy.wav
   ├── nverguo.wav
├── ffmpeg/
   ├── .gitignore
├── law.txt (500 tokens)
├── pyproject.toml (1700 tokens)
├── sp.py (1400 tokens)
├── test.py (200 tokens)
├── tests/
   ├── __init__.py
   ├── conftest.py (1100 tokens)
   ├── omnivoice-qwentts.py (500 tokens)
   ├── test_actions_split.py (1100 tokens)
   ├── test_base_recogn.py (1000 tokens)
   ├── test_base_trans.py (600 tokens)
   ├── test_base_tts.py (800 tokens)
   ├── test_cli.py (5.3k tokens)
   ├── test_config_split.py (1200 tokens)
   ├── test_contants.py (800 tokens)
   ├── test_cuda.py (100 tokens)
   ├── test_excepts.py (600 tokens)
   ├── test_help_ffmpeg_split.py (1500 tokens)
   ├── test_help_srt_split.py (1600 tokens)
   ├── test_job_helpers.py (300 tokens)
   ├── test_main_win_split.py (300 tokens)
   ├── test_mainwin_actions.py (500 tokens)
   ├── test_prepare_audio_split.py (1200 tokens)
   ├── test_stt_fun_split.py (2.1k tokens)
   ├── test_taskcfg.py (2.3k tokens)
   ├── test_trans_split.py (400 tokens)
   ├── test_translator_split.py (2.6k tokens)
   ├── test_ui_en_split.py (1100 tokens)
   ├── test_winform_split.py (300 tokens)
├── update_ffmpeg.bat
├── uv.lock (omitted)
├── videotrans/
   ├── __init__.py (200 tokens)
   ├── codes/
      ├── model.py (5.7k tokens)
   ├── component/
      ├── __init__.py
      ├── clip_video.py (3.1k tokens)
      ├── component.py (1000 tokens)
      ├── controlobj.py (100 tokens)
      ├── format_srtfiles_folders.py (1700 tokens)
      ├── onlyone_set_editdubb.py (4.4k tokens)
      ├── onlyone_set_recogn.py (2.3k tokens)
      ├── onlyone_set_recogn2.py (2.3k tokens)
      ├── onlyone_set_role.py (4.7k tokens)
      ├── progressbar.py (700 tokens)
      ├── realtime_stt.py (3.3k tokens)
      ├── set_ass.py (5.7k tokens)
      ├── set_cpp.py (400 tokens)
      ├── set_form.py (4.9k tokens)
      ├── set_proxy.py (500 tokens)
      ├── set_subtitles_length.py (400 tokens)
      ├── set_threads.py (700 tokens)
      ├── set_xxl.py (400 tokens)
      ├── textmatching.py (5.1k tokens)
   ├── configure/
      ├── __init__.py
      ├── _app_cfg.py (400 tokens)
      ├── _app_params.py (2.1k tokens)
      ├── _app_settings.py (2.5k tokens)
      ├── _helpers.py (100 tokens)
      ├── _i18n.py (400 tokens)
      ├── _logging.py (400 tokens)
      ├── _paths.py (600 tokens)
      ├── base.py (2.2k tokens)
      ├── config.py (300 tokens)
      ├── contants.py (1900 tokens)
      ├── excepts.py (2.3k tokens)
      ├── signal_hub.py (200 tokens)
      ├── whispernet_config.py (600 tokens)
   ├── language/
      ├── en.json (11.1k tokens)
      ├── zh.json (8.8k tokens)
   ├── mainwin/
      ├── __init__.py
      ├── _actions.py (400 tokens)
      ├── _actions_base.py (300 tokens)
      ├── _actions_base_file.py (1000 tokens)
      ├── _actions_base_misc.py (1100 tokens)
      ├── _actions_base_mode.py (1600 tokens)
      ├── _actions_check.py (2.1k tokens)
      ├── _actions_config.py (1300 tokens)
      ├── _actions_task.py (2.4k tokens)
      ├── _bind_signals.py (2.1k tokens)
      ├── _lifecycle.py (700 tokens)
      ├── _winform.py (400 tokens)
      ├── main_win.py (1800 tokens)
   ├── mosstts/
      ├── __init__.py
      ├── args.py (200 tokens)
      ├── moss_tts_nano/
         ├── __init__.py
         ├── defaults.py (100 tokens)
      ├── onnx_tts_runtime.py (5.8k tokens)
      ├── ort_cpu_runtime.py (8.1k tokens)
      ├── text_normalization_pipeline.py (2.4k tokens)
      ├── tts_robust_normalizer_single_script.py (2.7k tokens)
   ├── process/
      ├── __init__.py
      ├── _audio_noise.py (600 tokens)
      ├── _audio_separate.py (1000 tokens)
      ├── _audio_speakers.py (3.1k tokens)
      ├── _audio_utils.py (100 tokens)
      ├── _stt_faster.py (1800 tokens)
      ├── _stt_funasr.py (700 tokens)
      ├── _stt_glmasr.py (700 tokens)
      ├── _stt_openai.py (1200 tokens)
      ├── _stt_paraformer.py (700 tokens)
      ├── _stt_pipe.py (900 tokens)
      ├── _stt_qwen.py (500 tokens)
      ├── _stt_utils.py (1200 tokens)
      ├── prepare_audio.py (100 tokens)
      ├── signelobj.py (1100 tokens)
      ├── stt_fun.py (100 tokens)
      ├── tts_fun.py (1000 tokens)
      ├── vad.py (3.6k tokens)
   ├── prompts/
      ├── help.txt (100 tokens)
      ├── recogn/
         ├── gemini_recogn.txt (500 tokens)
      ├── resegment/
         ├── llm.txt (400 tokens)
         ├── llm2.txt (400 tokens)
      ├── srt/
         ├── ai302.txt (1200 tokens)
         ├── azure.txt (1200 tokens)
         ├── bailian.txt (1200 tokens)
         ├── chatgpt.txt (1200 tokens)
         ├── deepseek.txt (1200 tokens)
         ├── gemini.txt (1200 tokens)
         ├── localllm.txt (500 tokens)
         ├── minimax.txt (1200 tokens)
         ├── openrouter.txt (1200 tokens)
         ├── siliconflow.txt (1200 tokens)
         ├── xiaomi.txt (1200 tokens)
         ├── zhipuai.txt (1200 tokens)
         ├── zijie.txt (1200 tokens)
      ├── text/
         ├── ai302.txt (600 tokens)
         ├── azure.txt (600 tokens)
         ├── bailian.txt (600 tokens)
         ├── chatgpt.txt (600 tokens)
         ├── deepseek.txt (600 tokens)
         ├── gemini.txt (600 tokens)
         ├── localllm.txt (600 tokens)
         ├── minimax.txt (600 tokens)
         ├── openrouter.txt (600 tokens)
         ├── siliconflow.txt (600 tokens)
         ├── xiaomi.txt (600 tokens)
         ├── zhipuai.txt (600 tokens)
         ├── zijie.txt (600 tokens)
   ├── recognition/
      ├── __init__.py (1700 tokens)
      ├── _ai302.py (1200 tokens)
      ├── _base.py (3.6k tokens)
      ├── _camb.py (1200 tokens)
      ├── _cpp.py (400 tokens)
      ├── _deepgram.py (800 tokens)
      ├── _dolphin.py (500 tokens)
      ├── _elevenlabs.py (1000 tokens)
      ├── _fireredasr.py (500 tokens)
      ├── _funasr.py (700 tokens)
      ├── _gemini.py (1200 tokens)
      ├── _glmasr.py (500 tokens)
      ├── _google.py (900 tokens)
      ├── _huggingface.py (500 tokens)
      ├── _omnilingual.py (500 tokens)
      ├── _openairecognapi.py (1300 tokens)
      ├── _parakeet.py (300 tokens)
      ├── _parakeetja.py (500 tokens)
      ├── _qwen3asr.py (600 tokens)
      ├── _qwenasrlocal.py (400 tokens)
      ├── _recognapi.py (2.3k tokens)
      ├── _stt.py (600 tokens)
      ├── _whisper.py (1200 tokens)
      ├── _whispernet.py (2.1k tokens)
      ├── _whisperx.py (700 tokens)
      ├── _xiaomiasr.py (700 tokens)
      ├── _xxl.py (400 tokens)
      ├── _zijiemodel.py (700 tokens)
   ├── styles/
      ├── icon.ico
      ├── logo.png
      ├── no-remove.mp4
      ├── no-remove.wav
      ├── preview.png
      ├── simhei.ttf
      ├── style.qss (4.6k tokens)
   ├── task/
      ├── __init__.py
      ├── _base.py (1100 tokens)
      ├── _rate.py (6.1k tokens)
      ├── _stage_align.py (700 tokens)
      ├── _stage_assemble.py (3.7k tokens)
      ├── _stage_audio.py (900 tokens)
      ├── _stage_diariz.py (1000 tokens)
      ├── _stage_dubbing.py (1000 tokens)
      ├── _stage_prepare.py (1700 tokens)
      ├── _stage_recogn.py (2.5k tokens)
      ├── _stage_subtitle.py (900 tokens)
      ├── _stage_translate.py (600 tokens)
      ├── child_win_sign.py (200 tokens)
      ├── dubbing.py (2.3k tokens)
      ├── job.py (1500 tokens)
      ├── mult_video.py (400 tokens)
      ├── only_one.py (1200 tokens)
      ├── separate_worker.py (700 tokens)
      ├── simple_runnable_qt.py (100 tokens)
      ├── speech2text.py (2.8k tokens)
      ├── taskcfg.py (1900 tokens)
      ├── trans_create.py (1100 tokens)
      ├── translate_srt.py (700 tokens)
      ├── update_ffmpeg.py (600 tokens)
   ├── translator/
      ├── __init__.py (200 tokens)
      ├── _ai302.py (100 tokens)
      ├── _ali.py (400 tokens)
      ├── _azure.py (100 tokens)
      ├── _baidu.py (400 tokens)
      ├── _base.py (1900 tokens)
      ├── _camb.py (1000 tokens)
      ├── _chatgpt.py (200 tokens)
      ├── _constants.py (200 tokens)
      ├── _deepl.py (400 tokens)
      ├── _deeplx.py (500 tokens)
      ├── _deepseek.py (100 tokens)
      ├── _gemini.py (1000 tokens)
      ├── _google.py (300 tokens)
      ├── _huoshan.py (100 tokens)
      ├── _hymt2.py (300 tokens)
      ├── _lang_codes.py (2.2k tokens)
      ├── _lang_utils.py (1400 tokens)
      ├── _libre.py (400 tokens)
      ├── _localllm.py (100 tokens)
      ├── _m2m100.py (800 tokens)
      ├── _microsoft.py (400 tokens)
      ├── _minimax.py (100 tokens)
      ├── _openaicompat.py (1900 tokens)
      ├── _openrouter.py (100 tokens)
      ├── _qwenmt.py (1200 tokens)
      ├── _registry.py (700 tokens)
      ├── _runner.py (400 tokens)
      ├── _siliconflow.py (100 tokens)
      ├── _tencent.py (400 tokens)
      ├── _transapi.py (300 tokens)
      ├── _xiaomi.py (100 tokens)
      ├── _zhipuai.py (100 tokens)
   ├── tts/
      ├── __init__.py (1800 tokens)
      ├── _ai302tts.py (700 tokens)
      ├── _azuretts.py (700 tokens)
      ├── _base.py (2.4k tokens)
      ├── _cambtts.py (900 tokens)
      ├── _chatterbox.py (700 tokens)
      ├── _chattts.py (500 tokens)
      ├── _clone.py (600 tokens)
      ├── _confuciustts.py (200 tokens)
      ├── _cosyvoice.py (300 tokens)
      ├── _doubao2.py (900 tokens)
      ├── _edgetts.py (1700 tokens)
      ├── _elevenlabs.py (400 tokens)
      ├── _f5tts.py (1200 tokens)
      ├── _f5ttsapi.py (300 tokens)
      ├── _fishtts.py (400 tokens)
      ├── _geminitts.py (1300 tokens)
      ├── _glmtts.py (400 tokens)
      ├── _gptsovits.py (800 tokens)
      ├── _gradio.py (800 tokens)
      ├── _gtts.py (300 tokens)
      ├── _index.py (300 tokens)
      ├── _kokoro.py (400 tokens)
      ├── _minimaxi.py (600 tokens)
      ├── _mosstts.py (1000 tokens)
      ├── _omnivoice.py (1000 tokens)
      ├── _openaitts.py (500 tokens)
      ├── _piper.py (700 tokens)
      ├── _qwentts.py (600 tokens)
      ├── _qwenttslocal.py (600 tokens)
      ├── _spark.py (100 tokens)
      ├── _supertonic.py (400 tokens)
      ├── _ttsapi.py (600 tokens)
      ├── _vits.py (1400 tokens)
      ├── _voxcpm.py (300 tokens)
      ├── _xaitts.py (400 tokens)
      ├── _xiaomi.py (500 tokens)
      ├── _zipvoice.py (800 tokens)
   ├── ui/
      ├── __init__.py
      ├── _setup_menus.py (1700 tokens)
      ├── _setup_rows.py (2.1k tokens)
      ├── ai302.py (1100 tokens)
      ├── ali.py (1000 tokens)
      ├── azure.py (1200 tokens)
      ├── azuretts.py (1600 tokens)
      ├── baidu.py (1100 tokens)
      ├── cambasr.py (500 tokens)
      ├── cambtrans.py (500 tokens)
      ├── cambtts.py (700 tokens)
      ├── chatgpt.py (1400 tokens)
      ├── chatterbox.py (600 tokens)
      ├── chattts.py (1100 tokens)
      ├── clone.py (800 tokens)
      ├── cosyvoice.py (600 tokens)
      ├── dark/
         ├── __init__.py
         ├── darkstyle_rc.py (29.6k tokens)
         ├── palette.py (200 tokens)
      ├── deepgram.py (1000 tokens)
      ├── deepl.py (1000 tokens)
      ├── deeplx.py (800 tokens)
      ├── deepseek.py (1200 tokens)
      ├── doubao2.py (900 tokens)
      ├── elevenlabs.py (700 tokens)
      ├── en.py (4.8k tokens)
      ├── fanyi.py (1900 tokens)
      ├── fishtts.py (700 tokens)
      ├── formatcover.py (800 tokens)
      ├── gemini.py (1300 tokens)
      ├── getaudio.py (600 tokens)
      ├── googlecloud.py (1400 tokens)
      ├── gptsovits.py (700 tokens)
      ├── gradiowin.py (1100 tokens)
      ├── hunliu.py (800 tokens)
      ├── info.py (1300 tokens)
      ├── kokoro.py (800 tokens)
      ├── lawalert.py (1100 tokens)
      ├── libretranslate.py (800 tokens)
      ├── localllm.py (1300 tokens)
      ├── minimax.py (1200 tokens)
      ├── minimaxi.py (700 tokens)
      ├── mosstts.py (700 tokens)
      ├── openairecognapi.py (1300 tokens)
      ├── openaitts.py (1400 tokens)
      ├── openrouter.py (1200 tokens)
      ├── parakeet.py (800 tokens)
      ├── peiyin.py (2.1k tokens)
      ├── peiyinrole.py (2.9k tokens)
      ├── qwenmt.py (1400 tokens)
      ├── qwentts.py (900 tokens)
      ├── qwenttslocal.py (500 tokens)
      ├── recogn.py (2.5k tokens)
      ├── recognapi.py (1200 tokens)
      ├── refaudio.py (600 tokens)
      ├── separate.py (1100 tokens)
      ├── setini.py (7.8k tokens)
      ├── siliconflow.py (1100 tokens)
      ├── srthebing.py (900 tokens)
      ├── stt.py (1000 tokens)
      ├── subtitlescover.py (700 tokens)
      ├── tencent.py (1000 tokens)
      ├── transapi.py (900 tokens)
      ├── ttsapi.py (1400 tokens)
      ├── vasrt.py (1900 tokens)
      ├── videoandaudio.py (800 tokens)
      ├── videoandsrt.py (1200 tokens)
      ├── volcenginetts.py (1500 tokens)
      ├── watermark.py (1200 tokens)
      ├── whisperx.py (600 tokens)
      ├── xaitts.py (600 tokens)
      ├── xiaomi.py (1200 tokens)
      ├── zhipuai.py (1100 tokens)
      ├── zijiehuoshan.py (1100 tokens)
      ├── zijierecognmodel.py (1100 tokens)
   ├── util/
      ├── ListenVoice.py (200 tokens)
      ├── TestSTT.py (200 tokens)
      ├── TestSrtTrans.py (200 tokens)
      ├── __init__.py
      ├── _ffmpeg_audio.py (1100 tokens)
      ├── _ffmpeg_hwcodec.py (1000 tokens)
      ├── _ffmpeg_misc.py (300 tokens)
      ├── _ffmpeg_runner.py (700 tokens)
      ├── _ffprobe.py (1300 tokens)
      ├── _srt_ass.py (1300 tokens)
      ├── _srt_parse.py (1600 tokens)
      ├── _srt_wrap.py (400 tokens)
      ├── checkgpu.py (100 tokens)
      ├── cn_tn.py (8.3k tokens)
      ├── en_tn.py (2.1k tokens)
      ├── gpus.py (500 tokens)
      ├── help_down.py (3k tokens)
      ├── help_ffmpeg.py (200 tokens)
      ├── help_misc.py (3.4k tokens)
      ├── help_role.py (2.9k tokens)
      ├── help_srt.py (100 tokens)
      ├── helper_supertonic.py (3k tokens)
      ├── req_fac.py (100 tokens)
      ├── tools.py
   ├── voicejson/
      ├── 302.json (1900 tokens)
      ├── azure_voice_list.json (6.9k tokens)
      ├── camb.json (1100 tokens)
      ├── camb_languages.json (600 tokens)
      ├── doubao2.json (2.2k tokens)
      ├── edge_tts.json (3.6k tokens)
      ├── elevenlabs.json (500 tokens)
      ├── f5ttscfg.json (300 tokens)
      ├── f5ttscfg/
         ├── ar.yaml (200 tokens)
         ├── de.yaml (500 tokens)
         ├── en.yaml (500 tokens)
         ├── es.yaml (500 tokens)
         ├── fr.yaml (500 tokens)
         ├── hi.yaml (500 tokens)
         ├── it.yaml (500 tokens)
         ├── ja.yaml (500 tokens)
         ├── ru.yaml (500 tokens)
         ├── zh.yaml (500 tokens)
      ├── glmtts.json
      ├── minimaxi.json (2.8k tokens)
      ├── minimaxiio.json (3.5k tokens)
      ├── piper.json (1600 tokens)
      ├── qwen3tts.json (300 tokens)
      ├── supertonic.json
   ├── winform/
      ├── __init__.py (500 tokens)
      ├── _helpers.py (300 tokens)
      ├── ai302.py (300 tokens)
      ├── ali.py (300 tokens)
      ├── azure.py (200 tokens)
      ├── azuretts.py (500 tokens)
      ├── baidu.py (300 tokens)
      ├── cambtts.py (500 tokens)
      ├── chatgpt.py (400 tokens)
      ├── chatterbox.py (600 tokens)
      ├── chattts.py (400 tokens)
      ├── clone.py (400 tokens)
      ├── cosyvoice.py (500 tokens)
      ├── deepL.py (300 tokens)
      ├── deepLX.py (300 tokens)
      ├── deepgram.py (300 tokens)
      ├── deepseek.py (300 tokens)
      ├── doubao2.py (300 tokens)
      ├── elevenlabs.py (500 tokens)
      ├── fishtts.py (400 tokens)
      ├── fn_audiofromvideo.py (900 tokens)
      ├── fn_fanyisrt.py (2.8k tokens)
      ├── fn_formatcover.py (800 tokens)
      ├── fn_hebingsrt.py (800 tokens)
      ├── fn_hunliu.py (800 tokens)
      ├── fn_peiyin.py (4.1k tokens)
      ├── fn_peiyinrole.py (3.5k tokens)
      ├── fn_recogn.py (3k tokens)
      ├── fn_separate.py (500 tokens)
      ├── fn_subtitlescover.py (1100 tokens)
      ├── fn_vas.py (3.2k tokens)
      ├── fn_videoandaudio.py (1500 tokens)
      ├── fn_videoandsrt.py (1700 tokens)
      ├── fn_watermark.py (1600 tokens)
      ├── gemini.py (300 tokens)
      ├── gptsovits.py (500 tokens)
      ├── gradiowin.py (700 tokens)
      ├── kokoro.py (300 tokens)
      ├── libre.py (300 tokens)
      ├── localllm.py (300 tokens)
      ├── minimax.py (300 tokens)
      ├── minimaxi.py (500 tokens)
      ├── openairecognapi.py (400 tokens)
      ├── openaitts.py (500 tokens)
      ├── openrouter.py (300 tokens)
      ├── parakeet.py (300 tokens)
      ├── qwenmt.py (300 tokens)
      ├── qwentts.py (400 tokens)
      ├── qwenttslocal.py (400 tokens)
      ├── recognapi.py (300 tokens)
      ├── setini.py (600 tokens)
      ├── siliconflow.py (300 tokens)
      ├── sttapi.py (300 tokens)
      ├── tencent.py (400 tokens)
      ├── transapi.py (300 tokens)
      ├── ttsapi.py (600 tokens)
      ├── whisperxapi.py (200 tokens)
      ├── xaitts.py (300 tokens)
      ├── xiaomi.py (300 tokens)
      ├── zhipuai.py (300 tokens)
      ├── zijiehuoshan.py (300 tokens)
      ├── zijierecognmodel.py (300 tokens)
├── webui.py (12.5k tokens)
```


## /.github/FUNDING.yml

```yml path="/.github/FUNDING.yml" 
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: jianchang512
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: https://www.paypal.me/wangbobo866

```

## /.github/workflows/auto-close-old-issues.yml

```yml path="/.github/workflows/auto-close-old-issues.yml" 
name: Auto Close Inactive Issues

on:
  schedule:
    - cron: "0 2 * * *"
  workflow_dispatch:

permissions:
  issues: write

jobs:
  close-inactive-issues:
    runs-on: ubuntu-latest

    steps:
      - name: Close issues inactive for 30 days
        uses: actions/github-script@v7
        with:
          script: |
            const owner = 'jianchang512';
            const repo = 'pyvideotrans';
            const commentBody = 'Automatically closed after 30 days.';
            const now = new Date();
            const cutoff = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

            console.log(`Cutoff date: ${cutoff.toISOString()}`);

            const issues = await github.paginate(
              github.rest.issues.listForRepo,
              {
                owner,
                repo,
                state: 'open',
                per_page: 100
              }
            );

            const inactiveIssues = issues.filter(issue => {
              if (issue.pull_request) return false;
              return new Date(issue.updated_at) < cutoff;
            });

            console.log(`Found ${inactiveIssues.length} inactive open issues older than 30 days.`);

            for (const issue of inactiveIssues) {
              console.log(`Commenting on and closing #${issue.number}: ${issue.title}`);

              await github.rest.issues.createComment({
                owner,
                repo,
                issue_number: issue.number,
                body: commentBody
              });

              await github.rest.issues.update({
                owner,
                repo,
                issue_number: issue.number,
                state: 'closed'
              });
            }

```

## /.github/workflows/main.yml

```yml path="/.github/workflows/main.yml" 
name: Compile Python
on: [push]
jobs:
  pyinstaller-build:
    runs-on: windows-latest
    steps:
      - name: Versatile PyInstaller
        uses: sayyid5416/pyinstaller@v1.6.0    
        with: 
         python_ver: '3.10'
         pyinstaller_ver: '==6.3.0'
         spec: 'sp.spec'
         requirements: 'requirements-win-gpu.txt'
         upload_exe_with_name: 'sp1.5'
         options: --name "sp"--windowed

```

## /.gitignore

```gitignore path="/.gitignore" 
*.log
*.srt
*.7z
*.wav
*.spec
*.ui
*.bak
*.bat
*.aac
*.pyi
*.mp4
.agree.txt
.git
.github
.idea
.venv
.pytest_cache
.mimocode
.ruff_cache
AGENTS.md
err.txt
nolang.txt
edgetts.txt
debug*.py
ceshi*
ffmpeg/ffmpeg.exe
ffmpeg/ffprobe.exe
ffmpeg/ffplay.exe
ffmpeg/ytwin32.exe
videotrans/ass.json
videotrans/codec.json
videotrans/cfg.json
videotrans/params.json
videotrans/webui_state.json
videotrans/glossary.txt
videotrans/newlang.txt

g2pW/
output/
hooks
tmp
images
logs
models
dev
venv
chatterbox
!f5-tts/nverguo.wav
!f5-tts/cosy.wav
apidata
dist
source
build
ytlinux
ytdarwin
__pycache__
pretrained_models
runtime
guide/

# Arquivos do sistema
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
test.py
```

## /.python-version

```python-version path="/.python-version" 
3.10.19

```

## /Dockerfile

``` path="/Dockerfile" 
# ============================================================
# pyVideoTrans WebUI Dockerfile
#
# CPU:  docker build -t pyvideotrans-webui .
# GPU:  docker build --build-arg USE_CUDA=true -t pyvideotrans-webui:gpu .
# ============================================================

# 定义全局 ARG 变量
ARG USE_CUDA=false

# 巧妙地将阶段命名为 base-false 和 base-true
FROM python:3.10-slim AS base-false
FROM nvidia/cuda:12.8.0-cudnn-runtime-ubuntu22.04 AS base-true

# 根据 USE_CUDA 变量的值,动态继承上文对应的基础镜像
FROM base-${USE_CUDA} AS final-base

# 【关键】在新的 FROM 阶段之后,必须重新声明一次 ARG 才能在 RUN 等指令中使用该变量
ARG USE_CUDA

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

ENV GRADIO_SERVER_NAME="0.0.0.0"
ENV GRADIO_SERVER_PORT=7860
ENV DEBIAN_FRONTEND=noninteractive
ENV PYTHONUNBUFFERED=1
ENV FONTCONFIG_PATH=/etc/fonts

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
    fontconfig fonts-noto-cjk fonts-liberation fonts-dejavu wget \
    xz-utils git libglib2.0-0 libgl1 libsm6 libxext6 libxrender-dev \
    libxkbcommon-x11-0 libdbus-1-3 libsndfile1 python3-dev rubberband-cli libsndfile1-dev \
    && wget -q https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz \
    && tar -Jxf ffmpeg-release-amd64-static.tar.xz \
    && cp ffmpeg-*-static/ffmpeg /usr/local/bin/ \
    && cp ffmpeg-*-static/ffprobe /usr/local/bin/ \
    && rm -rf ffmpeg-* \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

RUN git clone -b dev https://github.com/jianchang512/pyvideotrans.git .

# 修复丢失了变量的 if 语句,正确引用 "${USE_CUDA}"
RUN if [ "${USE_CUDA}" = "true" ]; then \
        echo ">>> CUDA" && \
        uv pip install --system -r pyproject.toml --all-extras && \
        uv pip install --system torch==2.7.1 torchaudio==2.7.1 --index-url https://download.pytorch.org/whl/cu128 && \
        uv pip install --system nvidia-cublas-cu12 nvidia-cudnn-cu12; \
    else \
        echo ">>> CPU" && \
        uv pip install --system -r pyproject.toml --all-extras; \
    fi

RUN rm -rf /root/.cache/uv /tmp/*

EXPOSE 7860

CMD ["python", "webui.py"]
```

## /README.md

> Sponsors: **[Recall.ai](https://www.recall.ai/product/meeting-transcription-api?utm_source=github&utm_medium=sponsorship&utm_campaign=jianchang512-pyvideotrans) - Meeting Transcription API**
>
> If you’re looking for a transcription API for meetings, consider checking out **[Recall.ai](https://www.recall.ai/product/meeting-transcription-api?utm_source=github&utm_medium=sponsorship&utm_campaign=jianchang512-pyvideotrans)** , an API that works with Zoom, Google Meet, Microsoft Teams, and more


---

# pyVideoTrans

<div align="center">

**A Powerful Open Source Video Translation / Audio Transcription / AI Dubbing / Subtitle Translation Tool**

[中文](docs/README_CN.md) | [**Documentation**](https://pyvideotrans.com) | [**Online Q&A**](https://bbs.pyvideotrans.com)

[![License](https://img.shields.io/badge/License-GPL_v3-blue.svg)](LICENSE) [![Python](https://img.shields.io/badge/Python-3.10%2B-green.svg)](https://www.python.org/) [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)]()

</div>

**pyVideoTrans** is dedicated to seamlessly converting videos from one language to another, offering a complete workflow that includes speech recognition, subtitle translation, multi-role dubbing, and audio-video synchronization. It supports both local offline deployment and a wide variety of mainstream online APIs.

<img width="1566" height="912" alt="image" src="https://github.com/user-attachments/assets/7410b17d-9903-4919-954a-31764e246c15" />

---

## ✨ Core Features

> [Technical Architecture and Principles](docs/architecture.md)

- **🎥 Fully Automatic Video Translation**: One-click workflow: Speech Recognition (ASR) → Subtitle Translation → Speech Synthesis (TTS) → Video Synthesis.
- **🎙️ Audio Transcription / Subtitle Generation**: Batch convert audio/video to SRT subtitles, supporting **Speaker Diarization** to distinguish between different roles.
- **🗣️ Multi-Role AI Dubbing**: Assign different AI dubbing voices to different speakers.
- **🧬 Voice Cloning**: Integrates models like **F5-TTS, CosyVoice, GPT-SoVITS** for zero-shot voice cloning.
- **🧠 Powerful Model Support**:
  - **ASR**: Faster-Whisper (Local), OpenAI Whisper, Alibaba Qwen, ByteDance Volcano, Azure, Google, etc.
  - **LLM Translation**: DeepSeek, ChatGPT, Claude, Gemini, MiniMax, Ollama (Local), Alibaba Bailian, etc.
  - **TTS**: Edge-TTS (Free), OpenAI, Azure, Minimaxi, ChatTTS, ChatterBox, etc.
- **🖥️ Interactive Editing**: Supports pausing and manual proofreading at each stage (recognition, translation, dubbing) to ensure accuracy.
- **🛠️ Utility Toolkit**: Includes auxiliary tools such as vocal separation, video/subtitle merging, audio-video alignment, and transcript matching.
- **💻 Command Line Interface (CLI)**: Supports headless operation, convenient for server deployment or batch processing.
- **🌐 Web Interface (WebUI)**: Browser-based interface for remote access or internal network deployment.


---

## 🚀 Quick Start (Windows Users)

We provide a pre-packaged `.exe` version for Windows 10/11 users, requiring no Python environment configuration.

1. **Download**: [Click to download the latest pre-packaged version](https://github.com/jianchang512/pyvideotrans/releases)
2. **Unzip**: Extract the compressed file to a path without Chinese characters or spaces (e.g., `D:\pyVideoTrans`).
3. **Run**: Double-click `sp.exe` inside the folder to launch.

> **Note**:
> * Do not run directly from within the compressed archive.
> * To use GPU acceleration, ensure **CUDA 12.8** and **cuDNN 9.11** are installed.

---

## 🛠️ Source Deployment (macOS / Linux / Windows Developers)

We recommend using **[`uv`](https://docs.astral.sh/uv/)** for package management for faster speed and better environment isolation.

### 1. Prerequisites

* **Python**: Recommended version 3.10
* **FFmpeg**: Must be installed and configured in the environment variables.
  * **macOS**: `brew install ffmpeg libsndfile git`
  * **Linux (Ubuntu/Debian)**: `sudo apt-get install ffmpeg libsndfile1-dev`
  * **Windows**: [Download FFmpeg](https://ffmpeg.org/download.html) and configure Path, or place `ffmpeg.exe` and `ffprobe.exe` directly in the project directory.

### 2. Install uv (If not installed)

```bash
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

### 3. Clone and Install

```bash
git clone https://github.com/jianchang512/pyvideotrans.git
cd pyvideotrans
uv sync
```

> By default, `whisper.net` and `WebUI` are not installed locally.
> - To install all optional channels: `uv sync --all-extras`
> - To install whisper.net: `uv sync --extra dotnet` 
> - To install WebUI: `uv sync --extra webui` 

### 4. Launch Software

**GUI**:
```bash
uv run sp.py
```

**CLI**:
```bash
# Video Translation
uv run cli.py --task vtv --name "./video.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural"

# Audio to Subtitle
uv run cli.py --task stt --name "./audio.wav" --model_name large-v3

# Subtitle Translation
uv run cli.py --task sts --name "./subs.srt" --target_language_code en

# Text to Speech
uv run cli.py --task tts --name "./subs.srt" --voice_role "zh-CN-YunyangNeural"
```

> [CLI documentation with all parameters](docs/cli.md)

**WebUI** (for remote/internal network access):
```bash
uv sync --extra webui
uv run webui.py
```


**Docker** (containerized deployment):
```bash
# Build
docker build -t pyvideotrans-webui .

# Run
docker run -d -p 7860:7860 --name pyvideotrans pyvideotrans-webui

# With persistent config and output
docker run -d -p 7860:7860 \
  -v ./data/output:/app/output \
  -v ./data/config:/app/videotrans \
  --name pyvideotrans pyvideotrans-webui
```

> [WebUI documentation](docs/webui.md)

### 5. (Optional) GPU Acceleration Configuration

If you have an NVIDIA graphics card, execute the following commands to install the CUDA-supported PyTorch version:

```bash
# Uninstall CPU version
uv remove torch torchaudio

# Install CUDA version (Example for CUDA 12.x)
uv add torch==2.7 torchaudio==2.7 --index-url https://download.pytorch.org/whl/cu128
uv add nvidia-cublas-cu12 nvidia-cudnn-cu12
```

> [AMD GPU acceleration via Whisper.NET](docs/whisper_net_setup.md)

---

## 🧩 Supported Channels & Models (Partial)

| Category | Channel/Model | Description |
| :--- | :--- | :--- |
| **ASR (Speech Recognition)** | **Faster-Whisper** (Local) | Recommended, fast speed, high accuracy |
| | WhisperX / Parakeet | Supports timestamp alignment & speaker diarization |
| | Alibaba Qwen3-ASR / ByteDance Volcano | Online API, excellent for Chinese |
| **Translation (LLM/MT)** | **DeepSeek** / ChatGPT | Supports context understanding, more natural translation |
| | MiniMax AI | MiniMax M3 LLM, latest flagship model, OpenAI-compatible |
| | Google / Microsoft | Traditional machine translation, fast speed |
| | Ollama / M2M100 | Fully local offline translation |
| **TTS (Speech Synthesis)** | **Edge-TTS** | Microsoft free interface, natural effect |
| | **F5-TTS / CosyVoice** | Supports **Voice Cloning**, requires local deployment |
| | GPT-SoVITS / ChatTTS | High-quality open-source TTS |
| | 302.AI / OpenAI / Azure | High-quality commercial API |

---

## 📚 Documentation & Support

* **Official Documentation**: [https://pyvideotrans.com](https://pyvideotrans.com) (Includes detailed tutorials, API configuration guides, FAQ)
* **Online Q&A Community**: [https://bbs.pyvideotrans.com](https://bbs.pyvideotrans.com) (Submit error logs for automated AI analysis and answers)
* **GitHub Wiki**: [architecture.md](docs/architecture.md) | [cli.md](docs/cli.md) | [webui.md](docs/webui.md) | [Synchronize.md](docs/Synchronize.md) | [faq.md](docs/faq.md)

## ⚠️ Disclaimer

This software is an open-source, free, non-commercial project. Users are solely responsible for any legal consequences arising from the use of this software (including but not limited to calling third-party APIs or processing copyrighted video content). Please comply with local laws and regulations and the terms of use of relevant service providers.

## 🙏 Acknowledgements

This project mainly relies on the following open-source projects (partial):

* [FFmpeg](https://github.com/FFmpeg/FFmpeg)
* [PySide6](https://pypi.org/project/PySide6/)
* [faster-whisper](https://github.com/SYSTRAN/faster-whisper)
* [openai-whisper](https://github.com/openai/whisper)
* [edge-tts](https://github.com/rany2/edge-tts)
* [F5-TTS](https://github.com/SWivid/F5-TTS)
* [CosyVoice](https://github.com/FunAudioLLM/CosyVoice)
* [Gradio](https://www.gradio.app/) (WebUI)

---

*Created by [jianchang512](https://github.com/jianchang512)*




## /cli.py

```py path="/cli.py" 
"""
pyVideoTrans CLI — command-line interface for video translation, dubbing, and transcription.

Usage examples:
  # Speech to text
  uv run cli.py --task stt --name "D:/videos/demo.mp4" --recogn_type 0 --model_name large-v3

  # Subtitle translation
  uv run cli.py --task sts --name "D:/subs/source.srt" --target_language_code en

  # Text to speech
  uv run cli.py --task tts --name "C:/subs/movie.srt" --tts_type 0 --voice_role "zh-CN-YunyangNeural"

  # Full video translation
  uv run cli.py --task vtv --name "E:/movies/clip.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda
"""

import asyncio
import multiprocessing
import sys
import re
import argparse
from dataclasses import asdict
from multiprocessing import freeze_support
from pathlib import Path
from typing import Dict, List, Optional

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

# ---------------------------------------------------------------------------
# TEXT_DB — bilingual (zh/en) strings for all CLI output
# ---------------------------------------------------------------------------
TEXT_DB: Dict[str, Dict[str, str]] = {
    # --- Log messages ---
    "exec_stt_task": {"zh": "[执行任务] 语音转录 (STT)", "en": "[Task] Speech Transcription (STT)"},
    "exec_tts_task": {"zh": "[执行任务] 语音合成 (TTS)", "en": "[Task] Text-to-Speech (TTS)"},
    "exec_sts_task": {"zh": "[执行任务] 字幕翻译 (STS)", "en": "[Task] Subtitle Translation (STS)"},
    "exec_vtv_task": {"zh": "[执行任务] 视频翻译 (VTV)", "en": "[Task] Video Translation (VTV)"},
    "process_file":  {"zh": "[处理文件] {}", "en": "[File] {}"},
    "param_list":    {"zh": "[参数列表] {}", "en": "[Params] {}"},
    "output_dir":    {"zh": "[输出目录] {}", "en": "[Output Dir] {}"},
    "done":          {"zh": "[完成] 任务执行完毕", "en": "[Done] Task completed successfully"},
    "failed":        {"zh": "[失败] 任务执行出错: {}", "en": "[Failed] Task error: {}"},

    # --- Argparse descriptions ---
    "cli_desc": {
        "zh": "pyVideoTrans 命令行模式\n文档: https://pyvideotrans.com/cli",
        "en": "pyVideoTrans CLI Mode\nDocs: https://pyvideotrans.com/cli"
    },
    "cli_epilog": {
        "zh": "示例:\n"
              "  %(prog)s --task stt --name \"D:/demo.mp4\" --recogn_type 0 --model_name large-v3\n"
              "  %(prog)s --task tts --name \"D:/demo.srt\" --tts_type 0 --voice_role \"zh-CN-YunyangNeural\"\n"
              "  %(prog)s --task sts --name \"D:/demo.srt\" --target_language_code en\n"
              "  %(prog)s --task vtv --name \"D:/demo.mp4\" --source_language_code zh-cn --target_language_code en --voice_role \"en-US-GuyNeural\"\n"
              "  %(prog)s --list providers\n"
              "  %(prog)s --list languages",
        "en": "Examples:\n"
              "  %(prog)s --task stt --name \"D:/demo.mp4\" --recogn_type 0 --model_name large-v3\n"
              "  %(prog)s --task tts --name \"D:/demo.srt\" --tts_type 0 --voice_role \"zh-CN-YunyangNeural\"\n"
              "  %(prog)s --task sts --name \"D:/demo.srt\" --target_language_code en\n"
              "  %(prog)s --task vtv --name \"D:/demo.mp4\" --source_language_code zh-cn --target_language_code en --voice_role \"en-US-GuyNeural\"\n"
              "  %(prog)s --list providers\n"
              "  %(prog)s --list languages"
    },
    "help_task": {
        "zh": "任务类型: stt(语音转录), tts(文字配音), sts(字幕翻译), vtv(视频翻译)",
        "en": "Task type: stt(Speech to Text), tts(Text to Speech), sts(Subtitle Trans), vtv(Video Trans)"
    },
    "help_name": {
        "zh": "待处理文件的绝对路径 (请使用双引号包裹含空格的路径)",
        "en": "Absolute path of the file to process (wrap in quotes if path contains spaces)"
    },
    "help_list": {
        "zh": "列出可用选项: providers(渠道), languages(语言), models(模型)",
        "en": "List available options: providers, languages, models"
    },
    "help_output_dir": {
        "zh": "输出目录 (默认: 与输入文件同级的 _video_out 目录)",
        "en": "Output directory (default: _video_out alongside input file)"
    },
    "help_log_level": {
        "zh": "日志级别: DEBUG, INFO, WARNING, ERROR (默认: WARNING)",
        "en": "Log level: DEBUG, INFO, WARNING, ERROR (default: WARNING)"
    },
    "help_verbose": {
        "zh": "显示详细输出 (等同于 --log-level INFO)",
        "en": "Show verbose output (equivalent to --log-level INFO)"
    },
    "help_quiet": {
        "zh": "静默模式,仅输出错误",
        "en": "Quiet mode, only output errors"
    },

    # --- STT params ---
    "group_stt":       {"zh": "STT (语音转录) 参数", "en": "STT (Speech Transcription) Parameters"},
    "help_recogn_type": {"zh": "语音识别渠道编号", "en": "Speech recognition provider index"},
    "help_detect_lang":  {"zh": "音频视频发音语言", "en": "Source language of audio/video"},
    "help_model_name": {
        "zh": "语音识别模型名称\nfaster-whisper(0) 和 openai-whisper(1) 可选: {}\n其他渠道请在软件界面中查看",
        "en": "ASR model name\nfaster-whisper(0) & openai-whisper(1) options: {}\nOthers: check GUI"
    },
    "help_cuda":           {"zh": "启用CUDA加速", "en": "Enable CUDA acceleration"},
    "help_remove_noise":   {"zh": "启用降噪", "en": "Enable noise reduction"},
    "help_enable_diariz":  {"zh": "启用说话人识别", "en": "Enable speaker diarization"},
    "help_nums_diariz":    {"zh": "指定说话人数量", "en": "Number of speakers"},
    "help_rephrase":       {"zh": "重新断句 (0=默认, 1=LLM断句)", "en": "Rephrase (0=default, 1=LLM)"},
    "help_fix_punc":       {"zh": "恢复标点符号", "en": "Restore punctuation"},

    # --- TTS params ---
    "group_tts":         {"zh": "TTS (文字配音) 参数", "en": "TTS (Text-to-Speech) Parameters"},
    "help_tts_type":     {"zh": "配音渠道编号", "en": "TTS provider index"},
    "help_voice_role":   {"zh": "音色名称 (TTS模式必选)", "en": "Voice role name (required for TTS)"},
    "help_voice_rate":   {"zh": "语速 (如 +20%%, -10%%)", "en": "Speech rate (e.g. +20%%, -10%%)"},
    "help_volume":       {"zh": "音量 (如 +50%%, -30%%)", "en": "Volume (e.g. +50%%, -30%%)"},
    "help_pitch":        {"zh": "音调 (如 +10Hz, -5Hz)", "en": "Pitch (e.g. +10Hz, -5Hz)"},
    "help_voice_autorate": {"zh": "自动加速音频以对齐字幕", "en": "Auto-speed audio to match subtitles"},
    "help_align_sub_audio": {"zh": "强制修改字幕以对齐音频", "en": "Force subtitle adjustment to align with audio"},

    # --- Translation params ---
    "group_trans":        {"zh": "Translation (翻译) 参数", "en": "Translation Parameters"},
    "help_translate_type": {"zh": "翻译渠道编号", "en": "Translation provider index"},
    "help_source_lang":   {"zh": "源语言代码 (STS默认auto, VTV必选)", "en": "Source language (auto for STS, required for VTV)"},
    "help_target_lang":   {"zh": "目标语言代码 (必选)", "en": "Target language (required)"},

    # --- VTV extra params ---
    "group_vtv":           {"zh": "VTV (视频翻译) 额外参数", "en": "VTV Extra Parameters"},
    "help_video_autorate": {"zh": "自动慢速视频以对齐字幕", "en": "Auto-slow video to match subtitles"},
    "help_is_separate":    {"zh": "分离人声背景声", "en": "Separate vocals and background"},
    "help_recogn2pass":    {"zh": "二次语音识别", "en": "Enable 2-pass recognition"},
    "help_subtitle_type":  {"zh": "字幕类型 (0=无, 1=硬, 2=软, 3=硬双, 4=软双)", "en": "Subtitle type (0=None, 1=Hard, 2=Soft, 3=Hard Dual, 4=Soft Dual)"},
    "help_clear_cache":    {"zh": "完成后清理缓存 (默认)", "en": "Clear cache after finish (default)"},
    "help_no_clear_cache": {"zh": "不清理缓存", "en": "Do not clear cache"},

    # --- Error messages ---
    "err_missing_task": {
        "zh": "缺少 --task 参数,可选值: stt, tts, sts, vtv\n使用 --help 查看详细帮助",
        "en": "Missing --task parameter. Choose: stt, tts, sts, vtv\nUse --help for details"
    },
    "err_file_not_found": {
        "zh": "文件不存在: {}\n请检查路径是否正确,含空格的路径请用双引号包裹",
        "en": "File not found: {}\nCheck path, wrap space-containing paths in quotes"
    },
    "err_tts_role_required": {
        "zh": "TTS 模式下 --voice_role 是必选参数\n使用 --list providers 查看可用渠道和音色",
        "en": "--voice_role is required for TTS mode\nUse --list providers to see available options"
    },
    "err_sts_target_required": {
        "zh": "--target_language_code 是必选参数\n使用 --list languages 查看可用语言",
        "en": "--target_language_code is required\nUse --list languages to see available options"
    },
    "err_vtv_missing": {
        "zh": "VTV 模式缺少必选参数: {}",
        "en": "VTV mode missing required params: {}"
    },
    "miss_source_lang": {"zh": "--source_language_code", "en": "--source_language_code"},
    "miss_target_lang": {"zh": "--target_language_code", "en": "--target_language_code"},

    # --- List output ---
    "list_providers_header": {
        "zh": "\n=== 可用渠道 ===\n\n--- 语音识别 (STT) ---",
        "en": "\n=== Available Providers ===\n\n--- Speech Recognition (STT) ---"
    },
    "list_trans_header":     {"zh": "\n--- 翻译 (Translation) ---", "en": "\n--- Translation ---"},
    "list_tts_header":       {"zh": "\n--- 配音 (TTS) ---", "en": "\n--- Text-to-Speech (TTS) ---"},
    "list_languages_header": {"zh": "\n=== 可用语言代码 ===", "en": "\n=== Available Language Codes ==="},
    "list_models_header":    {"zh": "\n=== faster-whisper 可用模型 ===", "en": "\n=== faster-whisper Models ==="},
}


# ---------------------------------------------------------------------------
# tr() — translation helper
# ---------------------------------------------------------------------------
_lang: str = "en"


def set_lang(lang: str) -> None:
    """Set the global language for CLI output."""
    global _lang
    _lang = lang


def tr(key: str, *args) -> str:
    """Translate a TEXT_DB key to the current language."""
    lang_dict = TEXT_DB.get(key, {})
    text = lang_dict.get(_lang, lang_dict.get("en", key))
    if args:
        return text.format(*args)
    return text


# ---------------------------------------------------------------------------
# Task execution functions
# ---------------------------------------------------------------------------
def stt_fun(params: dict) -> None:
    """Execute speech-to-text task."""
    from videotrans.configure.config import app_cfg
    from videotrans.task.speech2text import SpeechToText
    from videotrans.task.taskcfg import TaskCfgSTT

    print(f"\n{tr('exec_stt_task')}")
    print(tr('process_file', params.get('name')))
    try:
        trk = SpeechToText(cfg=TaskCfgSTT(**params), out_format='srt')
        trk.prepare()
        trk.recogn()
        trk.diariz()
        trk.task_done()
        print(tr('done'))
    except Exception as e:
        print(tr('failed', str(e)), file=sys.stderr)
        raise


def tts_fun(params: dict) -> None:
    """Execute text-to-speech task."""
    from videotrans.task.dubbing import DubbingSrt
    from videotrans.task.taskcfg import TaskCfgTTS

    print(f"\n{tr('exec_tts_task')}")
    print(tr('process_file', params.get('name')))
    try:
        trk = DubbingSrt(cfg=TaskCfgTTS(**params), out_ext='wav')
        trk.prepare()
        trk.dubbing()
        trk.align()
        trk.task_done()
        print(tr('done'))
    except Exception as e:
        print(tr('failed', str(e)), file=sys.stderr)
        raise


def sts_fun(params: dict) -> None:
    """Execute subtitle translation task."""
    from videotrans.task.translate_srt import TranslateSrt
    from videotrans.task.taskcfg import TaskCfgSTS

    print(f"\n{tr('exec_sts_task')}")
    print(tr('process_file', params.get('name')))
    try:
        trk = TranslateSrt(cfg=TaskCfgSTS(**params), out_format=0)
        trk.prepare()
        trk.trans()
        trk.task_done()
        print(tr('done'))
    except Exception as e:
        print(tr('failed', str(e)), file=sys.stderr)
        raise


def vtv_fun(params: dict) -> None:
    """Execute full video translation task."""
    from videotrans.configure.config import app_cfg
    from videotrans.task.trans_create import TransCreate
    from videotrans.task.taskcfg import TaskCfgVTT

    app_cfg.current_status = 'ing'
    print(f"\n{tr('exec_vtv_task')}")
    print(tr('process_file', params.get('name')))
    try:
        trk = TransCreate(cfg=TaskCfgVTT(**params))
        trk.prepare()
        trk.recogn()
        trk.diariz()
        trk.trans()
        trk.dubbing()
        trk.align()
        trk.recogn2pass()
        trk.assembling()
        trk.task_done()
        print(tr('done'))
    except Exception as e:
        print(tr('failed', str(e)), file=sys.stderr)
        raise


# ---------------------------------------------------------------------------
# List functions
# ---------------------------------------------------------------------------
def list_providers() -> None:
    """Print available providers for all categories."""
    from videotrans import recognition, translator, tts

    print(tr('list_providers_header'))
    for i, name in enumerate(recognition.RECOGN_NAME_LIST):
        print(f"  {i:2d} = {name}")

    print(tr('list_trans_header'))
    for i, name in enumerate(translator.TRANSLASTE_NAME_LIST):
        print(f"  {i:2d} = {name}")

    print(tr('list_tts_header'))
    for i, name in enumerate(tts.TTS_NAME_LIST):
        print(f"  {i:2d} = {name}")


def list_languages() -> None:
    """Print available language codes."""
    from videotrans import translator

    print(tr('list_languages_header'))
    for code, name in translator.LANGNAME_DICT.items():
        print(f"  {code:10s}  {name}")


def list_models() -> None:
    """Print available faster-whisper models."""
    from videotrans.configure.contants import FASTER_MODELS_DICT

    print(tr('list_models_header'))
    for name, repo in FASTER_MODELS_DICT.items():
        print(f"  {name:25s}  {repo}")


# ---------------------------------------------------------------------------
# Argument parser construction
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
    """Build and return the argument parser."""
    parser = argparse.ArgumentParser(
        description=tr("cli_desc"),
        epilog=tr("cli_epilog"),
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )

    parser.add_argument('--version', action='version', version='%(prog)s 4.03')

    parser.add_argument('--task', type=str, choices=['stt', 'tts', 'sts', 'vtv'],
                        help=tr("help_task"))
    parser.add_argument('--name', type=str, help=tr("help_name"))

    parser.add_argument('--list', type=str, choices=['providers', 'languages', 'models'],
                        help=tr("help_list"))
    parser.add_argument('--output-dir', type=str, default=None,
                        help=tr("help_output_dir"))
    parser.add_argument('--log-level', type=str, default='WARNING',
                        choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
                        help=tr("help_log_level"))
    parser.add_argument('--verbose', '-v', action='store_true', help=tr("help_verbose"))
    parser.add_argument('--quiet', '-q', action='store_true', help=tr("help_quiet"))

    # --- STT ---
    stt_group = parser.add_argument_group(tr("group_stt"))
    stt_group.add_argument('--recogn_type', type=int, default=0, help=tr("help_recogn_type"))
    stt_group.add_argument('--detect_language', type=str, default='auto', help=tr("help_detect_lang"))
    stt_group.add_argument('--model_name', type=str, default='tiny', help=tr("help_model_name", 'tiny, base, small, medium, large-v3'))
    stt_group.add_argument('--cuda', action='store_true', help=tr("help_cuda"))
    stt_group.add_argument('--remove_noise', action='store_true', help=tr("help_remove_noise"))
    stt_group.add_argument('--enable_diariz', action='store_true', help=tr("help_enable_diariz"))
    stt_group.add_argument('--nums_diariz', type=int, default=-1, help=tr("help_nums_diariz"))
    stt_group.add_argument('--rephrase', type=int, default=0, help=tr("help_rephrase"))
    stt_group.add_argument('--fix_punc', action='store_true', help=tr("help_fix_punc"))

    # --- TTS ---
    tts_group = parser.add_argument_group(tr("group_tts"))
    tts_group.add_argument('--tts_type', type=int, default=0, help=tr("help_tts_type"))
    tts_group.add_argument('--voice_role', type=str, default=None, help=tr("help_voice_role"))
    tts_group.add_argument('--voice_rate', type=str, default='+0%', help=tr("help_voice_rate"))
    tts_group.add_argument('--volume', type=str, default='+0%', help=tr("help_volume"))
    tts_group.add_argument('--pitch', type=str, default='+0Hz', help=tr("help_pitch"))
    tts_group.add_argument('--voice_autorate', action='store_true', help=tr("help_voice_autorate"))
    tts_group.add_argument('--align_sub_audio', action='store_true', help=tr("help_align_sub_audio"))

    # --- Translation ---
    trans_group = parser.add_argument_group(tr("group_trans"))
    trans_group.add_argument('--translate_type', type=int, default=0, help=tr("help_translate_type"))
    trans_group.add_argument('--source_language_code', type=str, default=None, help=tr("help_source_lang"))
    trans_group.add_argument('--target_language_code', type=str, default=None, help=tr("help_target_lang"))

    # --- VTV extra ---
    vtv_group = parser.add_argument_group(tr("group_vtv"))
    vtv_group.add_argument('--video_autorate', action='store_true', help=tr("help_video_autorate"))
    vtv_group.add_argument('--is_separate', action='store_true', help=tr("help_is_separate"))
    vtv_group.add_argument('--recogn2pass', action='store_true', help=tr("help_recogn2pass"))
    vtv_group.add_argument('--subtitle_type', type=int, default=1, help=tr("help_subtitle_type"))
    vtv_group.add_argument('--clear_cache', action='store_true', default=True, help=tr("help_clear_cache"))
    vtv_group.add_argument('--no-clear-cache', dest='clear_cache', action='store_false', help=tr("help_no_clear_cache"))

    return parser


# ---------------------------------------------------------------------------
# Parameter validation
# ---------------------------------------------------------------------------
def validate_task_params(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
    """Validate required parameters for the given task type."""
    if not args.name:
        parser.error("--name is required")

    if not Path(args.name).exists():
        parser.error(tr("err_file_not_found", args.name))

    if args.task == 'tts' and not args.voice_role:
        parser.error(tr("err_tts_role_required"))

    if args.task == 'sts' and not args.target_language_code:
        parser.error(tr("err_sts_target_required"))

    if args.task == 'vtv':
        missing = []
        if not args.source_language_code:
            missing.append(tr("miss_source_lang"))
        if not args.target_language_code:
            missing.append(tr("miss_target_lang"))
        if missing:
            parser.error(tr("err_vtv_missing", ', '.join(missing)))


# ---------------------------------------------------------------------------
# Common parameter building
# ---------------------------------------------------------------------------
def build_common_params(args: argparse.Namespace, output_dir: Optional[str] = None) -> dict:
    """Build common parameters dict from parsed args."""
    from videotrans.configure.config import ROOT_DIR, TEMP_DIR
    from videotrans.util import tools
    from videotrans.util.gpus import getset_gpu

    _file_obj = tools.format_video(Path(args.name).absolute().as_posix())
    _nospacebasename = re.sub(r'[\s. #*?!:"]', '-', _file_obj["basename"])
    _cache_folder = f'{TEMP_DIR}/{_file_obj["uuid"]}'

    if output_dir:
        _target_dir = str(Path(output_dir).absolute())
    else:
        _target_dir = f'{ROOT_DIR}/output/{_nospacebasename}'

    _file_obj['target_dir'] = _target_dir

    common_params = {'name': args.name, "cache_folder": _cache_folder}
    common_params.update(asdict(_file_obj))

    Path(_cache_folder).mkdir(parents=True, exist_ok=True)
    Path(_target_dir).mkdir(parents=True, exist_ok=True)

    return common_params


def build_stt_params(args: argparse.Namespace) -> dict:
    """Build STT-specific parameters."""
    return {
        "recogn_type": args.recogn_type,
        "detect_language": args.detect_language,
        "model_name": args.model_name,
        "is_cuda": args.cuda,
        "remove_noise": args.remove_noise,
        "enable_diariz": args.enable_diariz,
        "nums_diariz": args.nums_diariz,
        "rephrase": args.rephrase,
        "fix_punc": args.fix_punc,
    }


def build_tts_params(args: argparse.Namespace) -> dict:
    """Build TTS-specific parameters."""
    return {
        "tts_type": args.tts_type,
        "voice_role": args.voice_role,
        "voice_rate": args.voice_rate,
        "volume": args.volume,
        "pitch": args.pitch,
        "is_cuda": args.cuda,
        "voice_autorate": args.voice_autorate,
        "align_sub_audio": args.align_sub_audio,
        "target_language_code": args.target_language_code,
    }


def build_sts_params(args: argparse.Namespace) -> dict:
    """Build STS-specific parameters."""
    return {
        "translate_type": args.translate_type,
        "source_language_code": args.source_language_code or "auto",
        "target_language_code": args.target_language_code,
    }


def build_vtv_params(args: argparse.Namespace) -> dict:
    """Build VTV-specific parameters."""
    return {
        "source_language_code": args.source_language_code,
        "target_language_code": args.target_language_code,
        **build_stt_params(args),
        **{k: v for k, v in build_tts_params(args).items()
           if k not in ('target_language_code', 'is_cuda')},
        "is_cuda": args.cuda,
        "translate_type": args.translate_type,
        "is_separate": args.is_separate,
        "recogn2pass": args.recogn2pass,
        "subtitle_type": args.subtitle_type,
        "clear_cache": args.clear_cache,
    }


# ---------------------------------------------------------------------------
# Logging setup
# ---------------------------------------------------------------------------
def setup_logging(log_level: str, verbose: bool = False, quiet: bool = False) -> None:
    """Configure logging level for the application."""
    import logging

    if quiet:
        level = logging.ERROR
    elif verbose:
        level = logging.INFO
    else:
        level = getattr(logging, log_level.upper(), logging.WARNING)

    logging.basicConfig(
        level=level,
        format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
        datefmt='%H:%M:%S',
        force=True,
    )


# ---------------------------------------------------------------------------
# Main entry
# ---------------------------------------------------------------------------
def main() -> int:
    """Main CLI entry point. Returns exit code (0=success, 1=error)."""
    # Parse language from system before anything else
    from videotrans.configure import config
    config.init_run()
    from videotrans.configure.config import defaulelang, app_cfg

    # Set language for CLI output
    set_lang(defaulelang if defaulelang in ('zh', 'en') else 'en')

    # Build parser and parse args
    parser = build_parser()
    args = parser.parse_args()

    # Handle --list before other validation
    if args.list:
        if args.list == 'providers':
            list_providers()
        elif args.list == 'languages':
            list_languages()
        elif args.list == 'models':
            list_models()
        return 0

    # Require --task when not using --list
    if not args.task:
        parser.error(tr("err_missing_task"))

    # Setup logging
    setup_logging(args.log_level, verbose=args.verbose, quiet=args.quiet)

    # Validate parameters
    validate_task_params(args, parser)

    # Set runtime flags
    app_cfg.exit_soft = False
    app_cfg.exec_mode = 'cli'

    # Get GPU info
    from videotrans.util.gpus import getset_gpu
    getset_gpu()

    # Build common params
    common_params = build_common_params(args, output_dir=args.output_dir)

    # Dispatch to task function
    task_map = {
        'stt': lambda: stt_fun({**common_params, **build_stt_params(args)}),
        'tts': lambda: tts_fun({**common_params, **build_tts_params(args)}),
        'sts': lambda: sts_fun({**common_params, **build_sts_params(args)}),
        'vtv': lambda: vtv_fun({**common_params, **build_vtv_params(args)}),
    }

    try:
        task_map[args.task]()
        print(tr('output_dir', common_params.get('target_dir', '')))
        return 0
    except SystemExit as e:
        return e.code if isinstance(e.code, int) else 1
    except KeyboardInterrupt:
        print("\nInterrupted.", file=sys.stderr)
        return 130
    except Exception as e:
        print(tr('failed', str(e)), file=sys.stderr)
        return 1


if __name__ == "__main__":
    freeze_support()
    try:
        multiprocessing.set_start_method('spawn', force=True)
    except RuntimeError:
        pass
    sys.exit(main())

```

## /docs/README_CN.md

> Sponsors: **[Recall.ai](https://www.recall.ai/product/meeting-transcription-api?utm_source=github&utm_medium=sponsorship&utm_campaign=jianchang512-pyvideotrans) - Meeting Transcription API**
>
> If you’re looking for a transcription API for meetings, consider checking out **[Recall.ai](https://www.recall.ai/product/meeting-transcription-api?utm_source=github&utm_medium=sponsorship&utm_campaign=jianchang512-pyvideotrans)** , an API that works with Zoom, Google Meet, Microsoft Teams, and more


---

# pyVideoTrans

<div align="center">

**一款强大的开源视频翻译 / 语音转录 / AI配音 / 字幕翻译工具**

[English](../README.md) | [**文档**](https://pyvideotrans.com) | [**在线问答**](https://bbs.pyvideotrans.com) 

[![License](https://img.shields.io/badge/License-GPL_v3-blue.svg)](../LICENSE) [![Python](https://img.shields.io/badge/Python-3.10%2B-green.svg)](https://www.python.org/) [![Platform](https://img.shields.io/badge/Platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)]()

</div>

**pyVideoTrans** 致力于无缝地将视频从一种语言转换为另一种语言,包含语音识别、字幕翻译、多角色配音及音画同步等全套流程。支持本地离线部署与多种主流在线 API。

<img width="1566" height="912" alt="image" src="https://github.com/user-attachments/assets/2d5bd178-3dc0-45ee-bc1c-dbb5f6705cf4" />

---

## ✨ 核心功能

> [技术架构与原理](architecture.md)

- **🎥 全自动视频翻译**: 一键完成:语音识别(ASR) → 字幕翻译 → 语音合成(TTS) → 视频合成。
- **🎙️ 语音转录 / 字幕生成**: 批量将音视频转为 SRT 字幕,支持 **说话人分离**,区分不同角色。
- **🗣️ 多角色 AI 配音**: 支持根据不同说话人分配不同的 AI 配音角色。
- **🧬 声音克隆**: 集成 **F5-TTS, CosyVoice, GPT-SoVITS** 等模型,支持零样本声音克隆。
- **🧠 强大的模型支持**:
  - **ASR**: Faster-Whisper (本地), OpenAI Whisper, 阿里 Qwen, 字节火山, Azure, Google 等。
  - **LLM 翻译**: DeepSeek, ChatGPT, Claude, Gemini, MiniMax, Ollama (本地), 阿里百炼等。
  - **TTS**: Edge-TTS (免费), OpenAI, Azure, Minimaxi, ChatTTS, ChatterBox 等。
- **🖥️ 交互式编辑**: 支持在识别、翻译、配音的每个阶段暂停并人工校对,确保精准度。
- **🛠️ 实用工具集**: 包含人声分离、视频/字幕合并、音画对齐、文稿匹配等辅助工具。
- **💻 命令行模式 (CLI)**: 支持无头模式运行,方便服务器部署或批处理。
- **🌐 Web 界面 (WebUI)**: 基于浏览器的界面,适合远程访问或局域网部署。


---

## 🚀 快速开始 (Windows 用户)

我们为 Windows 10/11 用户提供了预打包的 `.exe` 版本,无需配置 Python 环境。

1. **下载**: [点击下载最新预打包版本](https://github.com/jianchang512/pyvideotrans/releases)
2. **解压**: 将压缩包解压到一个**不包含中文、空格**的路径下 (例如 `D:\pyVideoTrans`)。
3. **运行**: 双击文件夹内的 `sp.exe` 启动。

> **注意**:
> * 请勿直接在压缩包内运行。
> * 如需使用 GPU 加速,请确保安装 **CUDA 12.8** 和 **cuDNN 9.11**。

---

## 🛠️ 源码部署 (macOS / Linux / Windows 开发者)

推荐使用 **[`uv`](https://docs.astral.sh/uv/)** 进行包管理,速度更快且环境隔离更好。

### 1. 前置准备

* **Python**: 建议版本 3.10
* **FFmpeg**: 必须安装并配置到环境变量。
  * **macOS**: `brew install ffmpeg libsndfile git`
  * **Linux (Ubuntu/Debian)**: `sudo apt-get install ffmpeg libsndfile1-dev`
  * **Windows**: [下载 FFmpeg](https://ffmpeg.org/download.html) 并配置 Path,或者直接将 ffmpeg.exe 和 ffprobe.exe 放在项目目录下

### 2. 安装 uv (如果尚未安装)

```bash
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

### 3. 克隆与安装

```bash
git clone https://github.com/jianchang512/pyvideotrans.git
cd pyvideotrans
uv sync
```

> 默认不安装 `whisper.net` 本地渠道,若需要全部安装请执行 `uv sync --all-extras`
> - 单独安装 `whisper.net`:`uv sync --extra dotnet`

### 4. 启动软件

**启动 GUI 界面**:
```bash
uv run sp.py
```

**使用 CLI 命令行**:

```bash
# 视频翻译示例
uv run cli.py --task vtv --name "./video.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural"

# 语音转字幕示例
uv run cli.py --task stt --name "./audio.wav" --model_name large-v3

# 字幕翻译示例
uv run cli.py --task sts --name "./subs.srt" --target_language_code en

# 文字配音示例
uv run cli.py --task tts --name "./subs.srt" --voice_role "zh-CN-YunyangNeural"
```

> [CLI 详细参数说明](cli.md)

**启动 WebUI** (适合远程访问或局域网部署):
```bash
uv sync --extra webui
uv run webui.py
```


**Docker 部署** (容器化部署):
```bash
# 构建镜像
docker build -t pyvideotrans-webui .

# 运行
docker run -d -p 7860:7860 --name pyvideotrans pyvideotrans-webui

# 持久化配置和输出
docker run -d -p 7860:7860 \
  -v ./data/output:/app/output \
  -v ./data/config:/app/videotrans \
  --name pyvideotrans pyvideotrans-webui
```

> [WebUI 使用说明](webui.md)


### 5. (可选) GPU 加速配置

1. 如果您拥有 NVIDIA 显卡,请执行以下命令以安装支持 CUDA 的 PyTorch 版本:

```bash
# 卸载 CPU 版本
uv remove torch torchaudio

# 安装 CUDA 版本 (以 CUDA 12.x 为例)
uv add torch==2.7 torchaudio==2.7 --index-url https://download.pytorch.org/whl/cu128
uv add nvidia-cublas-cu12 nvidia-cudnn-cu12
```

2. [如果你使用 AMD 显卡,可查看该文档尝试加速](whisper_net_setup.md)

---

## 🧩 支持的渠道与模型 (部分)

| 类别 | 渠道/模型 | 说明 |
| :--- | :--- | :--- |
| **语音识别 (ASR)** | **Faster-Whisper** (本地) | 推荐,速度快,精度高 |
| | WhisperX / Parakeet | 支持时间轴对齐与说话人分离 |
| | 阿里 Qwen3-ASR / 字节火山 | 在线 API,中文效果极佳 |
| **翻译 (LLM/MT)** | **DeepSeek** / ChatGPT | 支持上下文理解,翻译更自然 |
| | MiniMax AI | MiniMax M3 大模型,最新旗舰模型,OpenAI兼容接口 |
| | Google / Microsoft | 传统机器翻译,速度快 |
| | Ollama / M2M100 | 完全本地离线翻译 |
| **语音合成 (TTS)** | **Edge-TTS** | 微软免费接口,效果自然 |
| | **F5-TTS / CosyVoice** | 支持 **声音克隆**,需本地部署 |
| | GPT-SoVITS / ChatTTS | 高质量开源 TTS |
| | 302.AI / OpenAI / Azure | 高质量商业 API |

---

## 📚 文档与支持

* **官方文档**: [https://pyvideotrans.com](https://pyvideotrans.com) (包含详细教程、API配置指南、常见问题)
* **在线问答社区**: [https://bbs.pyvideotrans.com](https://bbs.pyvideotrans.com) (提交报错日志,AI 自动分析回答)
* **GitHub Wiki**: [架构说明](architecture.md) | [CLI 文档](cli.md) | [WebUI 说明](webui.md) | [音画对齐原理](Synchronize.md) | [常见问题](faq.md)

## ⚠️ 免责声明

本软件为开源免费非商业项目,使用者需自行承担因使用本软件(包括但不限于调用第三方 API、处理受版权保护的视频内容)所产生的一切法律后果。请遵守当地法律法规及相关服务商的使用协议。

## 🙏 致谢

本项目主要依赖以下开源项目 (部分):

* [FFmpeg](https://github.com/FFmpeg/FFmpeg)
* [PySide6](https://pypi.org/project/PySide6/)
* [faster-whisper](https://github.com/SYSTRAN/faster-whisper)
* [openai-whisper](https://github.com/openai/whisper)
* [edge-tts](https://github.com/rany2/edge-tts)
* [F5-TTS](https://github.com/SWivid/F5-TTS)
* [CosyVoice](https://github.com/FunAudioLLM/CosyVoice)
* [Gradio](https://www.gradio.app/) (WebUI)

---

*Created by [jianchang512](https://github.com/jianchang512)*




## /docs/Synchronize.md

# 音频视频时间轴对齐原理说明

本文档详细说明 pyVideoTrans 中「配音、字幕、视频对齐」模块(`videotrans/task/_rate.py`)的实现原理。该模块负责将翻译后的配音音频与原始无声视频在时间轴上精确对齐,最终合并为流畅的新视频。

---

## 目录

- [一、问题背景](#一问题背景)
- [二、核心挑战](#二核心挑战)
- [三、对齐策略总览](#三对齐策略总览)
- [四、数据预处理:时间轴扩展](#四数据预处理时间轴扩展)
- [五、模式一:仅音频加速](#五模式一仅音频加速)
- [六、模式二:仅视频慢速](#六模式二仅视频慢速)
- [七、模式三:音频+视频协同](#七模式三音频视频协同)
- [八、模式四:无变速拼接](#八模式四无变速拼接)
- [九、音频变速实现细节](#九音频变速实现细节)
- [十、视频变速实现细节](#十视频变速实现细节)
- [十一、最终音频拼接对齐](#十一最终音频拼接对齐)
- [十二、视频片段拼接](#十二视频片段拼接)
- [十三、TtsSpeedRate:纯配音场景](#十三ttsspeedrate纯配音场景)
- [十四、跨平台兼容性](#十四跨平台兼容性)
- [十五、已知限制与注意事项](#十五已知限制与注意事项)

---

## 一、问题背景

pyVideoTrans 将视频从 A 语言翻译为 B 语言的完整流程:

```text
原始视频(A语言)
    │
    ├─→ 分离无声视频流 (novoice.mp4)
    ├─→ 提取音频 → 语音识别(ASR) → A语言字幕
    ├─→ 翻译 → B语言字幕
    ├─→ 配音(TTS) → 逐条B语言配音音频(wav)
    │
    └─→ 【本模块】将 B语言配音 + B语言字幕 + 无声视频 → 对齐合并 → 新视频
```

**核心矛盾**:不同语言表达同一意思时,音节数和语法结构不同,导致配音时长与原始字幕时长不一致。

**示例**:
- 原始中文字幕片段:`0:03.000 ~ 0:06.000`(时长 3 秒)
- 翻译后英文配音:实际生成 4.2 秒的音频
- 差值:`4.2 - 3.0 = 1.2` 秒的溢出

如果不处理,会导致:
1. 配音与视频画面错位(嘴巴动了但声音还没到)
2. 字幕与声音不同步
3. 多条字幕的时间轴累积漂移

---

## 二、核心挑战

### 2.1 FFmpeg 的精度限制

FFmpeg 处理视频无法精确到毫秒级。使用 PTS(Presentation Time Stamp)进行变速时,最终输出的视频可能比期望时长略短或略长。这种误差在单个片段中很小(几毫秒),但在数百个片段拼接后会累积。

### 2.2 帧率不固定

视频帧率可能是 25fps、29.97fps、30fps 等。某些片段时长可能小于 1 帧,FFmpeg 对这类极短片段进行变速处理大概率会失败。

### 2.3 语言差异的不可预测性

配音时长的变化取决于:
- 源语言和目标语言的音节密度差异
- TTS 引擎的语速特性
- 句子的语法结构差异
- 是否使用了声音克隆(克隆模式下时长变化更不可控)

---

## 三、对齐策略总览

pyVideoTrans 提供四种对齐模式,由两个布尔标志位控制:

| 模式 | `should_audiorate` | `should_videorate` | 说明 |
|------|:---:|:---:|------|
| **仅音频加速** | ✅ | ✗ | 加速配音以匹配字幕时长 |
| **仅视频慢速** | ✗ | ✅ | 慢放视频以匹配配音时长 |
| **音频+视频协同** | ✅ | ✅ | 两者各负担一半时间差 |
| **无变速拼接** | ✗ | ✗ | 直接拼接,用静音填充间隙 |

```text
                    ┌─────────────────────┐
                    │  配音时长 > 字幕时长?  │
                    └──────────┬──────────┘
                               │
                    ┌──────────┴──────────┐
                    │                      │
                   否                      是
                    │                      │
            ┌───────┴───────┐    ┌────────┴────────┐
            │  无需处理      │    │  计算加速倍率     │
            │  直接拼接      │    │  ratio = 配音/字幕 │
            └───────────────┘    └────────┬────────┘
                                          │
                              ┌───────────┴───────────┐
                              │                       │
                     ratio ≤ 1.2               ratio > 1.2
                              │                       │
                     ┌────────┴────────┐    ┌────────┴────────┐
                     │ 仅加速音频      │    │ 音频+视频各半    │
                     │ 无需视频慢速    │    │ 分担时间差       │
                     └─────────────────┘    └─────────────────┘
```

---

## 四、数据预处理:时间轴扩展

### 4.1 问题:字幕间的静音间隙

原始字幕的时间轴通常包含间隙:

```text
字幕1: 0:00.000 ~ 0:03.000  (3s)
       ─────── 静音 0.5s ───────
字幕2: 0:03.500 ~ 0:07.000  (3.5s)
```

如果直接对字幕1的配音加速到 3s,而实际可用空间是 3.5s(到下条字幕开始),就会浪费 0.5s 的缓冲空间,导致不必要的加速。

### 4.2 解决方案:扩展每条字幕的结束时间

在预处理阶段,将每条字幕的 `end_time` 修改为下一条字幕的 `start_time`,从而将静音间隙纳入当前字幕的可用时间范围:

```text
处理前:
字幕1: start=0ms,    end=3000ms   (3s)
字幕2: start=3500ms, end=7000ms   (3.5s)

处理后:
字幕1: start=0ms,    end=3500ms   (3.5s) ← 扩展到下条开始
字幕2: start=3500ms, end=7000ms   (3.5s) ← 最后一条扩展到视频末尾
```

### 4.3 关键代码

```python
def _prepare_data(self):
    """数据清洗与预处理"""
    for i in range(len(self.queue_tts)):
        current = self.queue_tts[i]

        # 保存原始开始时间
        current['start_time_source'] = current['start_time']

        # 有视频慢速且第一条字幕开始时间 < 100ms,从0开始
        if self.should_videorate and i == 0 and current['start_time'] < 100:
            current['start_time_source'] = 0

        # 关键:将结束时间扩展到下一条字幕的开始时间
        if i < len(self.queue_tts) - 1:
            next_sub = self.queue_tts[i + 1]
            current['end_time_source'] = next_sub['start_time']
            current['end_time'] = next_sub['start_time']
        else:
            # 最后一条:扩展到视频末尾
            current['end_time_source'] = self.raw_total_time
            current['end_time'] = self.raw_total_time

        # 计算扩展后的可用时长
        current['source_duration'] = current['end_time_source'] - current['start_time_source']
```

### 4.4 效果对比

```text
假设原始数据:
字幕1: start=1000ms, end=3000ms (2s), 配音=3.5s
字幕2: start=3500ms, end=6000ms (2.5s), 配音=2.0s

处理后:
字幕1: source_duration = 3500 - 1000 = 2500ms (扩展了500ms静音间隙)
字幕2: source_duration = 6000 - 3500 = 2500ms

加速倍率:
字幕1: 3.5 / 2.5 = 1.4x (原本需要 3.5/2.0 = 1.75x)
字幕2: 无需加速 (2.0 < 2.5)
```

**结论**:时间轴扩展将字幕1的加速倍率从 1.75x 降低到 1.4x,显著减少了音频加速的幅度,提升了音质。

---

## 五、模式一:仅音频加速

### 5.1 策略

当配音时长 > 字幕可用时长时,将音频加速到匹配字幕时长。加速倍率不得超过 `max_audio_speed_rate`(默认 100)。

```text
配音: ═══════════════════════  (3500ms)
字幕: ══════════════           (2500ms)
                    ↓ 加速 1.4x
结果: ══════════════           (2500ms) + 静音填充
```

### 5.2 关键代码

```python
# 仅音频加速
if self.should_audiorate and not self.should_videorate:
    if dubb_dur > source_dur:
        ratio = dubb_dur / source_dur
        if ratio > self.max_audio_speed_rate:
            # 超过最大加速倍率,限制加速幅度
            audio_target = int(dubb_dur / self.max_audio_speed_rate)
        else:
            # 加速到匹配字幕时长
            audio_target = source_dur
```

### 5.3 注册加速任务

```python
if self.should_audiorate and audio_target < dubb_dur:
    self.audio_data.append({
        "filename": it['filename'],       # 配音文件路径
        "dubb_time": dubb_dur,            # 原始配音时长
        "target_time": audio_target        # 目标时长(加速后)
    })
```

---

## 六、模式二:仅视频慢速

### 6.1 策略

当配音时长 > 字幕可用时长时,将对应视频片段慢速播放,延长视频时长以匹配配音。PTS 倍率不得超过 `max_video_pts_rate`(默认 10)。

```text
视频片段: ══════════════       (2500ms)
配音:     ═══════════════════  (3500ms)
                      ↓ 慢速 PTS=1.4
结果:     ═══════════════════  (3500ms)
```

### 6.2 PTS 原理

PTS(Presentation Time Stamp)控制视频帧的显示时间。FFmpeg 的 `setpts` 滤镜可以改变 PTS:

```text
setpts=1.0*PTS  → 正常速度
setpts=2.0*PTS  → 慢速 2 倍(每帧显示时间翻倍)
setpts=0.5*PTS  → 加速 2 倍(每帧显示时间减半)
```

### 6.3 关键代码

```python
# 仅视频慢速
elif not self.should_audiorate and self.should_videorate:
    if dubb_dur > source_dur:
        video_target = dubb_dur  # 视频目标时长 = 配音时长
        pts = video_target / source_dur
        if pts > self.max_video_pts_rate:
            # 超过最大慢速倍率,限制慢速幅度
            video_target = int(source_dur * self.max_video_pts_rate)
```

### 6.4 注册视频片段

```python
if self.should_videorate:
    pts = video_target / source_dur if source_dur > 0 else 1.0
    self.video_for_clips.append({
        "start": it['start_time_source'],   # 视频裁切起点
        "end": it['end_time_source'],        # 视频裁切终点
        "target_time": video_target,         # 目标输出时长
        "pts": pts,                          # PTS 倍率
        "tts_index": i,                      # 对应字幕索引
        "line": it['line']                   # 字幕行号
    })
```

---

## 七、模式三:音频+视频协同

### 7.1 策略

当音频加速和视频慢速同时启用时,根据配音/字幕倍率选择不同的协同策略:

| 倍率 (ratio) | 策略 | 说明 |
|:---:|------|------|
| ≤ 1.2 | 仅加速音频 | 倍率较小,音频加速对音质影响小,无需慢速视频 |
| > 1.2 | 各负担一半 | 音频加速和视频慢速各自分担一半时间差 |

```text
示例:字幕 2500ms,配音 6000ms,ratio = 2.4

策略 A(ratio ≤ 1.2):
  音频加速到 2500ms (2.4x) → 音质损失大
  视频不变 → 2500ms

策略 B(ratio > 1.2,实际使用):
  diff = 6000 - 2500 = 3500ms
  joint_target = 2500 + 3500/2 = 4250ms
  音频加速到 4250ms (1.41x) → 音质损失小
  视频慢速到 4250ms (PTS=1.7) → 画面略慢但可接受
```

### 7.2 关键代码

```python
elif self.should_audiorate and self.should_videorate:
    if dubb_dur > source_dur:
        ratio = dubb_dur / source_dur
        if ratio <= self.BOTH_MODE_AUDIO_ONLY_THRESHOLD:  # 1.2
            # 倍率较小,仅加速音频即可,无需视频慢速
            audio_target = source_dur
            video_target = source_dur
        else:
            # 倍率较大,音频加速和视频慢速各自负担一半时间差
            diff = dubb_dur - source_dur
            joint_target = int(source_dur + (diff / 2))
            audio_target = joint_target
            video_target = joint_target
```

### 7.3 为什么选择 1.2 作为阈值?

- **音频加速 ≤ 1.2x**:人耳几乎察觉不到音质变化
- **超过 1.2x**:单一手段的副作用开始明显,需要协同分担

---

## 八、模式四:无变速拼接

### 8.1 策略

当音频加速和视频慢速都未启用时,直接按字幕时间轴拼接配音音频,用静音填充间隙,或者当选择了移除静音时直接移除。
如果选择了对齐字幕时间轴,则根据实际音频时长,修改字幕时间轴,以便实现声音开始时字幕显示,声音结束时字幕消失

### 8.2 拼接规则

```text
字幕时间轴:
├── 0ms ──── 1000ms ──── 3500ms ──── 6000ms ──── 8000ms
│   静音      字幕1        字幕2        字幕3
│  (1000ms)  (2500ms)     (2500ms)     (2000ms)

拼接结果:
├── [静音1000ms] + [配音1] + [配音2] + [配音3] + [尾部静音]
```

### 8.3 关键代码

```python
def _run_no_rate_change_mode(self):
    audio_concat_list = []
    total_audio_duration = 0

    for i, it in enumerate(self.queue_tts):
        prev_end = 0 if i == 0 else self.queue_tts[i-1].get('end_pos_for_concat', 0)
        start_time = it['start_time']

        # 计算与前一条的间隙
        gap = start_time - prev_end

        # 如果不移除静音间隙,填充静音
        if not self.remove_silent_mid and gap > 0:
            audio_concat_list.append(self._create_silen_file(f"gap_{i}", gap))
            total_audio_duration += gap

        # 拼接配音文件
        if it.get('filename') and Path(it['filename']).exists():
            audio_concat_list.append(it['filename'])
            dubb_len = len(AudioSegment.from_file(it['filename']))
        # ...

        total_audio_duration += dubb_len
        it['end_pos_for_concat'] = total_audio_duration

        # 对齐字幕时间轴
        if self.align_sub_audio:
            it['start_time'] = total_audio_duration - dubb_len
            it['end_time'] = total_audio_duration

    # 尾部静音:如果音频总时长 < 视频总时长
    if self.raw_total_time > total_audio_duration:
        audio_concat_list.append(
            self._create_silen_file("tail_end", self.raw_total_time - total_audio_duration)
        )
```

---

## 九、音频变速实现细节

### 9.1 两种变速引擎

pyVideoTrans 支持两种音频变速方式,按优先级自动选择:

| 引擎 | 优先级 | 依赖 | 特点 |
|------|:---:|------|------|
| **Rubber Band** | 高 | `pyrubberband` + `rubberband` CLI | 音质最佳,保留音高不变 |
| **FFmpeg atempo** | 低 | FFmpeg(内置) | 无需额外依赖,音质略差 |

### 9.2 Rubber Band 变速

```python
def _change_speed_rubberband(input_path, target_duration):
    # 读取音频
    y, sr = sf.read(input_path)
    current_duration = round((len(y) / sr) * 1000)

    # 计算变速倍率
    time_stretch_rate = current_duration / target_duration
    time_stretch_rate = max(0.2, min(time_stretch_rate, 50.0))

    # 执行变速(保留音高)
    y_stretched = pyrb.time_stretch(y, sr, time_stretch_rate)

    # 单声道转双声道
    if y_stretched.ndim == 1:
        y_stretched = np.column_stack((y_stretched, y_stretched))

    # 写回文件
    sf.write(input_path, y_stretched, sr)
```

**Rubber Band 的优势**:
- 使用 Phase Vocoder 算法,变速时保持音高不变
- 支持大倍率变速(最高 50x)而不会产生明显的音质损失
- 处理速度快,支持多线程

### 9.3 FFmpeg atempo 变速(回退方案)

```python
def _precise_speed_up_audio(input_path, target_duration):
    current_duration_ms = len(AudioSegment.from_file(input_path, format='wav'))

    # atempo 限制:参数必须在 [0.5, 2.0] 之间
    # 超出范围时,链式串联多个 atempo
    atempo_list = []
    speed_factor = current_duration_ms / target_duration

    while speed_factor > 2.0:
        atempo_list.append("atempo=2.0")
        speed_factor /= 2.0

    atempo_list.append(f"atempo={speed_factor}")
    filter_str = ",".join(atempo_list)

    # 示例:8x 加速 → "atempo=2.0,atempo=2.0,atempo=2.0"
    cmd = [
        '-y', '-i', input_path,
        '-filter:a', filter_str,
        '-t', f"{target_duration/1000.0}",  # 强制裁剪到目标时长
        '-ar', "48000", '-ac', "2",
        '-c:a', 'pcm_s16le',
        f'{input_path}-after.wav'
    ]
    tools.runffmpeg(cmd)
    shutil.copy2(f'{input_path}-after.wav', input_path)
```

**atempo 链式串联原理**:

```text
atempo 范围: [0.5, 2.0]

需要 8x 加速:
  8.0 = 2.0 × 2.0 × 2.0
  → "atempo=2.0,atempo=2.0,atempo=2.0"

需要 3x 加速:
  3.0 = 2.0 × 1.5
  → "atempo=2.0,atempo=1.5"

需要 1.3x 加速:
  1.3 < 2.0,无需拆分
  → "atempo=1.3"
```

### 9.4 多进程并行加速

音频变速任务通过 `ProcessPoolExecutor` 并行执行:

```python
def _execute_audio_speedup_rubberband(self):
    _wok = min(12, len(self.audio_data), max(os.cpu_count() - 1, 1))

    with ProcessPoolExecutor(max_workers=int(_wok)) as pool:
        for i, d in enumerate(self.audio_data):
            pool.submit(
                _change_speed_rubberband if HAS_RUBBERBAND else _precise_speed_up_audio,
                d['filename'],
                d['target_time']
            )
```

---

## 十、视频变速实现细节

### 10.1 PTS 变速原理

FFmpeg 的 `setpts` 滤镜通过修改 PTS 实现变速:

```text
原始帧序列:
  帧1(0ms) → 帧2(33ms) → 帧3(66ms) → 帧4(100ms)  [30fps]

setpts=2.0*PTS (慢速 2x):
  帧1(0ms) → 帧2(66ms) → 帧3(132ms) → 帧4(200ms)

setpts=0.5*PTS (加速 2x):
  帧1(0ms) → 帧2(16ms) → 帧3(33ms) → 帧4(50ms)
```

### 10.2 FFmpeg 命令构建

```python
def _cut_video_get_duration(i, task, novoice_mp4_original, preset, crf, fps_mode):
    # 裁切参数
    ss_time = tools.ms_to_time_string(ms=task['start'], sepflag='.')
    source_duration_s = (task['end'] - task['start']) / 1000.0
    target_duration_s = task.get('target_time', source_duration_ms) / 1000.0
    pts_factor = task.get('pts', 1.0)

    cmd = [
        '-y',
        '-ss', ss_time,                    # 起始时间
        '-t', f'{source_duration_s:.6f}',  # 裁切时长
        '-i', input_video_path,
        '-an',                             # 去除音频
        '-c:v', 'libx264',                # 视频编码器
        '-g', '1',                         # GOP=1,确保精确裁切
        '-preset', preset,                 # 编码速度
        '-crf', crf,                       # 质量
        '-pix_fmt', 'yuv420p'              # 像素格式
    ]

    # PTS 变速滤镜
    if abs(pts_factor - 1.0) >= 0.001:
        cmd.extend(['-vf', f'setpts={pts_factor}*PTS'])
    else:
        cmd.extend(['-vf', 'setpts=PTS'])

    cmd.extend(fps_mode)  # VFR 或 CFR 模式
    cmd.extend(['-t', f'{target_duration_s:.6f}'])  # 强制限制输出时长
    cmd.append(os.path.basename(task['filename']))
```

### 10.3 帧率模式选择

```python
self.fps_mode = ["-fps_mode", "vfr"]  # 默认可变帧率

if settings.get('fps_mode') == 'cfr':
    video_fps = tools.get_video_info(novoice_mp4, video_fps=True)
    self.fps_mode = ["-r", f"{video_fps}", "-fps_mode", "cfr"]
```

| 模式 | 说明 | 适用场景 |
|------|------|---------|
| **VFR** (可变帧率) | 允许帧率变化,变速效果更好 | 默认推荐 |
| **CFR** (固定帧率) | 强制固定帧率,兼容性更好 | 某些播放器兼容性问题时使用 |

### 10.4 兜底机制

如果变速处理失败(输出文件 < 1024B),自动回退到无变速裁切:

```python
if not file_path.exists() or file_path.stat().st_size < 1024:
    # 兜底:无变速裁切
    cmd_backup = [
        '-y', '-ss', ss_time,
        '-t', f'{source_duration_s:.6f}',
        '-i', input_video_path,
        '-an', '-c:v', 'libx264',
        '-g', '1', '-preset', preset, '-crf', crf,
        '-pix_fmt', 'yuv420p',
        '-vf', 'setpts=PTS',  # 显式保持原始 PTS
    ] + fps_mode
    cmd_backup.append(os.path.basename(task['filename']))
    tools.runffmpeg(cmd_backup, force_cpu=True, cmd_dir=work_dir)
```

### 10.5 多进程并行处理

```python
def _video_speeddown(self):
    _wok = min(12, len(data), max(os.cpu_count() - 1, 1))

    with ProcessPoolExecutor(max_workers=int(_wok)) as pool:
        for i, d in enumerate(data):
            pool.submit(_cut_video_get_duration, i, d,
                       self.novoice_mp4_original,
                       self.preset, self.crf, self.fps_mode)
```

---

## 十一、最终音频拼接对齐

### 11.1 对齐原则

无论使用哪种变速模式,最终的音频拼接都遵循相同的原则:

1. **每条配音占据一个"槽位"**,槽位时长由变速策略决定
2. **配音短于槽位**:末尾填充静音
3. **配音长于槽位**:截断音频以匹配槽位
4. **配音等于槽位**:直接放入

```text
时间轴:
├── [槽位1: 3500ms] ├── [槽位2: 2500ms] ├── [槽位3: 2000ms] ──→

槽位1 内容:
├── [配音1: 3200ms] + [静音: 300ms]

槽位2 内容:
├── [配音2: 2500ms]  (精确匹配)

槽位3 内容:
├── [配音3: 2800ms] → 截断为 2000ms
```

### 11.2 关键代码

```python
def _concat_audio_aligned(self):
    audio_list = []
    current_timeline = self.queue_tts[0]['start_time']

    # 首部静音
    if current_timeline > 0:
        audio_list.append(self._create_silen_file("head_0", current_timeline))

    for i, it in enumerate(self.queue_tts):
        # 槽位时长:有视频慢速时用视频实际时长,否则用字幕区间时长
        slot_duration = it.get('final_duration', it['source_duration'])

        # 兜底:槽位时长为0时回退
        if slot_duration <= 0:
            slot_duration = max(1, it['source_duration'])

        # 读取配音文件
        seg = AudioSegment.from_file(audio_file)
        current_slot_audio_len = len(seg)

        # 三种情况
        if current_slot_audio_len > slot_duration:
            # 溢出:截断
            cut_seg = seg[:slot_duration]
            cut_seg.export(final_slot_path, format='wav')
            audio_list.append(final_slot_path)

        elif current_slot_audio_len < slot_duration:
            # 不足:补静音
            diff = slot_duration - current_slot_audio_len
            audio_list.append(audio_file)
            audio_list.append(self._create_silen_file(f"tail_{i}", diff))

        else:
            # 精确匹配
            audio_list.append(audio_file)

        # 更新字幕时间轴
        it['start_time'] = current_timeline
        it['end_time'] = current_timeline + slot_duration
        current_timeline += slot_duration

    self._exec_concat_audio(audio_list)
```

### 11.3 静音文件生成

```python
def _create_silen_file(self, name, duration_ms):
    path = Path(self.cache_folder, f"silence_{name}.wav").as_posix()
    duration_ms = max(1, int(duration_ms))
    AudioSegment.silent(duration=duration_ms, frame_rate=48000) \
                .set_channels(2) \
                .export(path, format="wav")
    return path
```

### 11.4 FFmpeg 拼接

```python
def _exec_concat_audio(self, file_list):
    # 生成拼接列表文件
    concat_txt = Path(self.cache_folder, 'final_audio_concat.txt').as_posix()
    tools.create_concat_txt(file_list, concat_txt=concat_txt)

    # FFmpeg concat 拼接
    cmd = [
        '-y', '-f', 'concat', '-safe', '0',
        '-i', concat_txt,
        '-c:a', 'copy',  # 直接复制,不重新编码
        temp_wav
    ]
    tools.runffmpeg(cmd, force_cpu=True, cmd_dir=self.cache_folder)
```

---

## 十二、视频片段拼接

### 12.1 流程

```text
原始无声视频 (novoice.mp4)
    │
    ├─→ 裁切片段1 (clip_0_1.400.mp4)  ← PTS=1.4 慢速
    ├─→ 裁切片段2 (clip_1_1.000.mp4)  ← PTS=1.0 不变
    ├─→ 裁切片段3 (clip_2_1.700.mp4)  ← PTS=1.7 慢速
    │
    └─→ FFmpeg concat 合并 → 新的 novoice.mp4
```

### 12.2 拼接命令

```python
def _concat_video(self, processed_clips):
    # 生成拼接列表
    txt_content = []
    for clip in processed_clips:
        if clip.get('actual_duration', 0) > 0 and Path(clip['filename']).exists():
            txt_content.append(f"file '{clip['filename']}'")

    # FFmpeg concat(直接复制,不重新编码)
    cmd = [
        '-y', '-f', 'concat', '-safe', '0',
        '-i', concat_list,
        '-c', 'copy',  # 无损拼接
        output_path
    ]
    tools.runffmpeg(cmd, force_cpu=True, cmd_dir=self.cache_folder)

    # 替换原始视频
    shutil.move(output_path, self.novoice_mp4)
```

---

## 十三、TtsSpeedRate:纯配音场景

### 13.1 与 SpeedRate 的区别

`TtsSpeedRate` 继承自 `SpeedRate`,专门用于「批量为字幕配音」场景:

| 特性 | SpeedRate | TtsSpeedRate |
|------|-----------|-------------|
| 视频慢速 | 支持 | **禁用**(`should_videorate=False`) |
| 最大加速倍率 | 可配置(默认 100) | 固定 100 |
| 时间轴扩展 | 完整(保存 `start_time_source`) | 简化(仅移动 `end_time`) |
| 输出 | 视频 + 音频 | 仅音频 |

### 13.2 简化的预处理

```python
class TtsSpeedRate(SpeedRate):
    def _prepare_data(self):
        _len = len(self.queue_tts)
        for i in range(_len):
            current = self.queue_tts[i]
            if i < _len - 1:
                # 仅移动结束时间,不保存原始开始时间
                current['end_time'] = self.queue_tts[i + 1]['start_time']

            current['source_duration'] = current['end_time'] - current['start_time']
            # ...
```

### 13.3 简化的计算策略

```python
def _calculate_adjustments(self):
    for i, it in enumerate(self.queue_tts):
        source_dur = it['source_duration']
        dubb_dur = it['dubb_time']

        if dubb_dur > source_dur:
            # 无限制,强制加速到对齐
            self.audio_data.append({
                "filename": it['filename'],
                "dubb_time": dubb_dur,
                "target_time": source_dur
            })
```

---

## 十四、跨平台兼容性

### 14.1 路径处理

所有文件路径使用 `Path.as_posix()` 转换为正斜杠格式,确保 FFmpeg 在 Windows/Linux/macOS 上都能正确解析:

```python
input_video_path = Path(novoice_mp4_original).resolve().as_posix()
work_dir = Path(task['filename']).parent.as_posix()
```

### 14.2 FFmpeg 调用

通过 `tools.runffmpeg()` 统一调用 FFmpeg,自动处理:
- Windows 上的路径空格问题
- FFmpeg 可执行文件的查找(系统 PATH 或内置 `ffmpeg/` 目录)
- 命令参数的正确拼接

### 14.3 进程池

使用 `ProcessPoolExecutor` 而非 `multiprocessing.Pool`,提供更好的跨平台兼容性和资源管理。

### 14.4 文件清理

使用 `Path.glob()` + `Path.unlink()` 替代 `os.scandir()` + `os.remove()`,保持 API 一致性。

---

## 十五、已知限制与注意事项

### 15.1 FFmpeg 精度限制

- FFmpeg 无法精确到毫秒级,PTS 变速后的视频可能比期望时长略短或略长
- 单个片段误差约 10-50ms,数百个片段拼接后可能累积到秒级
- **缓解措施**:最终音频拼接时统一截断或补静音,确保总时长一致

### 15.2 极短片段处理

- 时长 < 1 帧的片段(如 30fps 下 < 33ms)FFmpeg 变速大概率失败
- **缓解措施**:预处理阶段将间隙合并到当前字幕,确保每个片段至少有数百毫秒

### 15.3 音频加速的音质损失

- Rubber Band:加速 ≤ 3x 时音质损失极小,> 5x 时开始出现机械感
- FFmpeg atempo:加速 > 2x 时可能出现轻微的音色变化
- **建议**:对于需要大幅加速的场景(> 3x),考虑同时启用视频慢速协同处理

### 15.4 视频慢速的画面卡顿

- PTS 慢速不会生成新的帧,只是延长每帧的显示时间
- 低帧率视频(如 24fps)慢速 2x 后,每帧显示 83ms,可能出现轻微卡顿感
- **建议**:视频慢速倍率尽量控制在 2x 以内

### 15.5 无效片段过滤

小于 1024 字节的视频片段视为无效(仅包含容器头和元数据),在拼接时自动跳过:

```python
if clip.get('actual_duration', 0) > 0 and Path(clip['filename']).exists():
    # 有效片段,加入拼接列表
    txt_content.append(f"file '{path}'")
else:
    logger.warning(f"[Video-Concat] 忽略无效片段: {clip.get('filename')}")
```

---

## 附录:完整处理流程图

```text
                    ┌──────────────────────────┐
                    │   输入: queue_tts 列表     │
                    │   (每条字幕 + 配音文件)     │
                    └────────────┬─────────────┘
                                 │
                    ┌────────────┴─────────────┐
                    │  should_audiorate 或       │
                    │  should_videorate 启用?   │
                    └────────────┬─────────────┘
                                 │
                  ┌──────────────┴──────────────┐
                  │                             │
                 是                             否
                  │                             │
         ┌────────┴────────┐          ┌─────────┴─────────┐
         │ _prepare_data() │          │ _run_no_rate_      │
         │ 时间轴扩展       │          │ change_mode()      │
         └────────┬────────┘          │ 无变速直接拼接      │
                  │                   └─────────┬─────────┘
         ┌────────┴────────┐                     │
         │ _calculate_     │                     │
         │ adjustments()   │                     │
         │ 计算变速策略     │                     │
         └────────┬────────┘                     │
                  │                              │
    ┌─────────────┴─────────────┐                │
    │                           │                │
 音频变速                    视频变速             │
    │                           │                │
 ┌──┴──┐                  ┌─────┴─────┐          │
 │RB/  │                  │_cut_video │          │
 │atempo│                 │_get_dur-  │          │
 │加速  │                  │ation()   │          │
 └──┬──┘                  │PTS变速    │          │
    │                     └─────┬─────┘          │
    │                           │                │
    │                     ┌─────┴─────┐          │
    │                     │_concat_   │          │
    │                     │video()    │          │
    │                     │拼接视频   │          │
    │                     └─────┬─────┘          │
    │                           │                │
    └─────────────┬─────────────┘                │
                  │                              │
         ┌────────┴────────┐                     │
         │ _concat_audio_  │◄────────────────────┘
         │ aligned()       │
         │ 音频对齐拼接     │
         └────────┬────────┘
                  │
         ┌────────┴────────┐
         │ _exec_concat_   │
         │ audio()         │
         │ FFmpeg 合并      │
         └────────┬────────┘
                  │
         ┌────────┴────────┐
         │ 输出: 最终音频   │
         │ + 更新字幕时间轴 │
         └─────────────────┘
```


## /docs/about.md

# 👑捐助该项目,助力项目持续维护


----

本开源项目基于兴趣创建,没有商业计划,也就是你可以一直免费使用,或者fork后自己修改(必须遵守GPL-v3开源协议),不用担心免费版功能受限或闭源安全问题。

至于维护呢,开源嘛都是用爱发电,闲时就多花些精力在这上面,忙时可能就一段时间顾不上了。

当然了,如果觉得该项目对你有价值,希望该项目能一直稳定持续维护和优化,也欢迎各位小额捐助。

如果不愿捐助或者没有能力捐助,也无妨欢迎继续使用,点个star、提交个pr或关注下公众号(内容均是本项目教程,搜一搜公众号`pyvideotrans` ),也是对开发者的一种帮助。

----


## 如何捐助 Donate Methods

你可以向微信或支付宝二维码付款,备注你的github名称

> **Donate URL at ko-fo.com:** https://ko-fi.com/jianchang512

<img src="https://pyvideotrans.com/images/wx.png" width="200">

<img src="https://pyvideotrans.com/images/alipay.png" width="200">

<img src="https://pyvideotrans.com/images/biancn.jpg" width="200">


# [pyVideoTrans项目](https://github.com/jianchang512/pyvideotrans)捐助者列表

**感谢所有捐助者,您的支持是我坚持维护的动力**


| 微信支付宝昵称/GitHub用户名 | 日期  | 金额 |
| --- | --- | --- |
| yuppiesnotzhuhao  |  2023-12-01  | 捐助10元 |
| 9*9  |  2023-12-02  | 捐助10元 |
| *s  |  2023-12-7  | 捐助 10 元 |
| 汪*X  |  2023-12-10  | 捐助 0.6 元 |
| *兄  |  2023-12-12  | 捐助 1 元 |
| *辰  |  2023-12-13  | 捐助 5 元 |
| 喵の左爪  |  2023-12-18  | 捐助5元 |
| ID:BigD-a-yi | 2023-12-20   | 捐助10元 |
| *D  |  2023-12-20  | 捐助 10 元 |
| *溪  |  2023-12-22  | 捐助 5 元 |
| *星  |  2023-12-23  | 捐助 66 元 |
| F*f  |  2023-12-24  | 捐助 10 元 |
| *玲  |  2023-12-25  | 捐助 10 元 |
| laowangtou-888  |  2023-12-25 | 捐助 100 元 |
| 铁*  |  2023-12-29  | 捐助 5 元 |
| dingsmart  |  2024-1-1  | 捐助 100 元 |
| *王  |  2024-1-1  | 捐助 50 元 |
| *想  |  2024-1-2  | 捐助 2.88 元 |
| *)  |  2024-1-6  | 捐助 50 元 |
| *道  |  2024-1-6  | 捐助 20 元 |
| *剑  |  2024-1-6  | 捐助 8 元 |
| o*u  |  2024-1-9  | 捐助 100 元 |
| o*u  |  2024-1-9  | 捐助 100 元 |
| qxk2005  |  2024-1-9  | 捐助 100 元 |
| 喵の左爪  |  2024-1-11  | 捐助10元 |
| *泉  |  2024-1-11  | 捐助 6.6 元 |
| *匐  |  2024-1-11  | 捐助 20 元 |
| m*o  |  2024-1-11  | 捐助 1.0 元 |
| *工  |  2024-1-12  | 捐助 10 元 |
| maotouying0102  |  2024-1-12  | 捐助 10 元 |
| super5hunz1  |  2024-1-13  | 捐助 16.66 元 |
| zhulinzhao  |  2024-1-14  | 捐助 5 元 |
| darksiderlyd  |  2024-1-14  | 捐助 20 元 |
| againstthewindtofly   |  2024-1-15  | 捐助 50 元 |
| wxxvc  |  2024-1-16  | 捐助 10 元 |
| 喵の左爪  |  2024-1-16  | 捐助5元 |
| *眼  |  2024-1-20  | 捐助 8.88 元 |
| *工  |  2024-1-21  | 捐助 10 元 |
| D* t  |  2024-1-21  | 捐助 10 元 |
| L* N  |  2024-1-23  | 捐助 5 元 |
| M*  |  2024-1-24  | 捐助 10 元 |
| m*l  |  2024-1-24  | 捐助 10 元 |
| *生  |  2024-1-25  | 捐助 18.88 元 |
| w*d  |  2024-1-26  | 捐助 6.66 元 |
| *.  |  2024-1-27  | 捐助 0.15 元 |
| vedanthkadam555(ko-fi.com)  |  2024-1-28  | 捐助 10 美元 |
| *明  |  2024-1-29  | 捐助 20 元 |
| *骁(支付宝)  |  2024-1-29  | 捐助 2 元 |
| 5*)  |  2024-1-30  | 捐助 10 元 |
| rqi14(U*d)  |  2024-1-30  | 捐助 200 元 |
| *正  |  2024-1-30  | 捐助 10 元 |
| i*8  |  2024-1-31  | 捐助 18 元 |
| *.  |  2024-2-1  | 捐助 10 元 |
| *途  |  2024-2-1  | 捐助 30 元 |
| *甜(bingsunny0730)  |  2024-2-2  | 捐助 1.68 元 |
| k*v  |  2024-2-2  | 捐助 10 元 |
| *林(xjsszl)  |  2024-2-2  | 捐助 10 元 |
| **宇(支付宝)  |  2024-2-2  | 捐助 10 元 |
| 创新科技(QQ)  |  2024-2-2  | 捐助 1.26 元 |
| *彦  |  2024-2-3  | 捐助 10 元 |
| *遡  |  2024-2-6  | 捐助 20 元 |
| *程(支付宝)  |  2024-2-6  | 捐助 50 元 |
| *豪(支付宝)  |  2024-2-7  | 捐助 8.8 元 |
| *u  |  2024-2-8  | 捐助 20 元 |
| *哦  |  2024-2-8  | 捐助 10 元 |
| *u  |  2024-2-9  | 捐助 30 元 |
| *许  |  2024-2-10  | 捐助 10 元 |
| *伟(支付宝)  |  2024-2-10  | 捐助 88.88 元 |
| *明  |  2024-2-12  | 捐助 1 元 |
| 好*_  |  2024-2-14  | 捐助 22.8 元 |
| t*e  |  2024-2-14  | 捐助 28 元 |
| *栎(xigongyue)  |  2024-2-14  | 捐助 10 元 |
| a*X  |  2024-2-14  | 捐助 8 元 |
| *狗  |  2024-2-14  | 捐助 20 元 |
| *种  |  2024-2-15  | 捐助 20 元 |
| R*y  |  2024-2-15  | 捐助 20 元 |
| R*s  |  2024-2-15  | 捐助 20 元 |
| *人  |  2024-2-16  | 捐助 18.88 元 |
| *.  |  2024-2-16  | 捐助 0.01 元 |
| *喜  |  2024-2-16  | 捐助 10 元 |
| *哦  |  2024-2-17  | 捐助 10 元 |
| *林  |  2024-2-19  | 捐助 100 元 |
| H*p  |  2024-2-19  | 捐助 10 元 |
| M*c  |  2024-2-19  | 捐助 8.88 元 |
| *飞(支付宝)  |  2024-2-19  | 捐助 30 元 |
| *伦(支付宝)  |  2024-2-19  | 捐助 100 元 |
| W*l(longwan8888)  |  2024-2-20  | 捐助 10 元 |
| D*N  |  2024-2-20  | 捐助 11.11 元 |
| *涛  |  2024-2-20  | 捐助 50 元 |
| *月  |  2024-2-21  | 捐助 2 元 |
| J*y  |  2024-2-21  | 捐助 4 元 |
| *军(支付宝)  |  2024-2-21  | 捐助 18.88 元 |
| r*r  |  2024-2-23  | 捐助 10 元 |
| M*e  |  2024-2-23  | 捐助 20 元 |
| *、  |  2024-2-24  | 捐助 1 元 |
| *理  |  2024-2-25  | 捐助 111 元 |
| *德  |  2024-2-26  | 捐助 20 元 |
| *。  |  2024-2-26  | 捐助 1 元 |
| l*i  |  2024-2-26  | 捐助 10 元 |
| 雕个锤子(QQ)  |  2024-2-26  | 捐助 15 元 |
| *勇  |  2024-2-27  | 捐助 10 元 |
| P*n  |  2024-2-28  | 捐助 30 元 |
| *口  |  2024-2-28  | 捐助 99 元 |
| 0..0(QQ)  |  2024-2-28  | 捐助 5 元 |
| *匐  |  2024-2-29  | 捐助 28 元 |
| *平  |  2024-2-29  | 捐助 10 元 |
| *匐  |  2024-2-29  | 捐助 20 元 |
| *吧  |  2024-2-29  | 捐助 100 元 |
| Orkun(ko-fi.com)  |  2024-2-29  | 捐助 150 美元 |
| *章  |  2024-3-1  | 捐助 2 元 |
| *军(支付宝)  |  2024-3-1  | 捐助 20 元 |
| 孙*9  |  2024-3-2  | 捐助 2 元 |
| *叔  |  2024-3-2  | 捐助 20 元 |
| d*n(xiongxiong2023)  |  2024-3-2  | 捐助 10 元 |
| *匐  |  2024-3-3  | 捐助 18 元 |
| *手  |  2024-3-3  | 捐助 2 元 |
| *化(huawei250)  |  2024-3-4  | 捐助 50 元 |
| B*B  |  2024-3-4  | 捐助 15 元 |
| *人  |  2024-3-4  | 捐助 18.88 元 |
| 煙圈仔(QQ)  |  2024-3-4  | 捐助 8.88 元 |
| NNT Music EDM Trending(ko-fi.com)  |  2024-3-5  | 捐助 5 美元 |
| h*e  |  2024-3-5  | 捐助 1 元 |
| *帅(k576026608)  |  2024-3-5  | 捐助 100 元 |
| *华  |  2024-3-6  | 捐助 100 元 |
| *菜(badboy-tian)  |  2024-3-6  | 捐助 50 元 |
| *鑫  |  2024-3-6  | 捐助 20 元 |
| DataGO(QQ)  |  2024-3-6  | 捐助 10 元 |
| 兔宝宝(QQ)  |  2024-3-6  | 捐助 6.66 元 |
| 你De世界、只允许有(QQ)  |  2024-3-6  | 捐助 10 元 |
| *雨(solielune)  |  2024-3-7  | 捐助 199 元 |
| *式  |  2024-3-8  | 捐助 10 元 |
| *)  |  2024-3-8  | 捐助 10 元 |
| *~  |  2024-3-10  | 捐助 10 元 |
| *雀  |  2024-3-10  | 捐助 3 元 |
| JIN LONG(支付宝)  |  2024-3-12  | 捐助 99 元 |
| qq碎泪无常  |  2024-3-13  | 捐助 10 元 |
| *凯(支付宝)  |  2024-3-14  | 捐助 58 元 |
| *杨  |  2024-3-15  | 捐助 20 元 |
| *光(Utterlyuseless000)  |  2024-3-16  | 捐助 20 元 |
| *端  |  2024-3-19  | 捐助 5 元 |
| *彩虹图标  |  2024-3-20  | 捐助 88.88 元 |
| D*o	 | 	2024-3-20	 | 捐助 20 元 |
| *水	 | 	2024-3-20	 | 捐助 5 元 |
| H*o	 | 	2024-3-20	 | 捐助 10 元 |
| K*(kevinqingqinga)	 | 	2024-3-21	 | 捐助 10 元 |
| M*u	 | 	2024-3-22	 | 捐助 10 元 |
| *(dahonghong520)	 | 	2024-3-22	 | 捐助 9.99 元 |
| *式(zhangsun972)	 | 	2024-3-23	 | 捐助 3 元 |
| *龙(支付宝)	 | 	2024-3-23	 | 捐助 10 元 |
| E*c	 | 	2024-3-24	 | 捐助 10 元 |
| *鱼(Zonda)	 | 	2024-3-24	 | 捐助 23.33 元 |
| C*n	 | 	2024-3-24	 | 捐助 100 元 |
| .*.(xincheng213618)	 | 	2024-3-24	 | 捐助 10 元 |
| L*G(lrglzm)	 | 	2024-3-26	 | 捐助 19.9 元 |
| s*e	 | 	2024-3-26	 | 捐助 6.6元 |
| *腻	 | 	2024-3-31	 | 捐助 50 元 |
| *哥	 | 	2024-3-31	 | 捐助 30 元 |
| *(TigaGUTS)	 | 	2024-4-2	 | 捐助 8 元 |
| *郎(machenme)	 | 	2024-4-2	 | 捐助 6.66 元 |
| *达	 | 	2024-4-2	 | 捐助 6.0 元 |
| **军(支付宝)	 | 	2024-4-2	 | 捐助 1 元 |
| l*r	 | 	2024-4-3	 | 捐助 5.0 元 |
| *立	 | 	2024-4-3	 | 捐助 6.66 元 |
| *晴	 | 	2024-4-3	 | 捐助 10 元 |
| a*c	 | 	2024-4-3	 | 捐助 2.33 元 |
| T*m	 | 	2024-4-4	 | 捐助 100 元 |
| Nguyen Ngoc Thien(kifo.com)	 | 	2024-4-4	 | 捐助 $5 美元 |
| *贤	 | 	2024-4-5	 | 捐助 20 元 |
| *军	 | 	2024-4-5	 | 捐助 50 元 |
| *樱    |    2024-4-6     | 捐助 1 元 |
| *赵    |    2024-4-6     | 捐助 1 元 |
| *乐    |    2024-4-6     | 捐助 10 元 |
| *子    |    2024-4-11     | 捐助 10 元 |
| *家    |    2024-4-20     | 捐助 9.9 元 |
| *哦    |    2024-4-21     | 捐助 10 元 |
| *笔    |    2024-4-23     | 捐助 30 元 |
| M*u(LiuVfx)    |    2024-4-23     | 捐助 6.66 元 |
| *波(支付宝)    |    2024-4-23     | 捐助 88.88 元 |
| *华    |    2024-4-24     | 捐助 30 元 |
| *OBJ    |    2024-4-26     | 捐助 1 元 |
| M*i    |    2024-4-28     | 捐助 28 元 |
| **彬(支付宝)    |    2024-4-28     | 捐助 20 元 |
| *籽    |    2024-4-29     | 捐助 1 元 |
| *笑    |    2024-5-1     | 捐助 2 元 |
| **豹(支付宝)    |    2024-5-1     | 捐助 11 元 |
| **豹(支付宝)    |    2024-5-1     | 捐助 10 元 |
| *磊    |    2024-5-4     | 捐助 2 元 |
| Anthony Tran(ko-fi)    |    2024-5-4     | 捐助 $20 美元 |
| *蟹    |    2024-5-6     | 捐助 20 元 |
| x*y    |    2024-5-6     | 捐助 0.5 元 |
| *曹    |    2024-5-7     | 捐助 6 元 |
| *维    |    2024-5-8     | 捐助 50 元 |
| *林    |    2024-5-9     | 捐助 20 元 |
| **璨(支付宝)    |    2024-5-9     | 捐助 5 元 |
| *易    |    2024-5-10     | 捐助 5 元 |
| **文(支付宝)    |    2024-5-10     | 捐助 100 元 |
| T*n    |    2024-5-13     | 捐助 10 元 |
| *娜    |    2024-5-14     | 捐助 5 元 |
| *徐    |    2024-5-14     | 捐助 9.5 元 |
| *阳(支付宝)    |    2024-5-15     | 捐助 30 元 |
| *柯(支付宝)    |    2024-5-18     | 捐助 30 元 |
| Q*Q    |    2024-5-18     | 捐助 2 元 |
| *旧    |    2024-5-21     | 捐助 10 元 |
| 小*0    |    2024-5-22     | 捐助 10 元 |
| *l    |    2024-5-22     | 捐助 10 元 |
| *猪    |    2024-5-22     | 捐助 15 元 |
| *成    |    2024-5-25     | 捐助 66.6 元 |
| *强    |    2024-5-26     | 捐助 20 元 |
| 灯光设计吴江南    |    2024-5-26     | 捐助 6.66 元 |
| m*g    |    2024-5-27     | 捐助 50 元 |
| *亮    |    2024-5-29     | 捐助 10 元 |
| *+    |    2024-5-30     | 捐助 0.99 元 |
| *轩(支付宝)    |    2024-5-31     | 捐助 20 元 |
| 民*5    |    2024-6-3     | 捐助 18.8 元 |
| *远    |    2024-6-3     | 捐助 6.66 元 |
| *声   |    2024-6-4     | 捐助 10 元 |
| *帅(支付宝)   |    2024-6-4     | 捐助 1 元 |
| *梁(支付宝)   |    2024-6-5     | 捐助 1 元 |
| *刚   |    2024-6-5     | 捐助 50 元 |
| z*e   |    2024-6-5     | 捐助 5 元 |
| *心   |    2024-6-5     | 捐助 20 元 |
| *哥   |    2024-6-5     | 捐助 0.1 元 |
| T*y   |    2024-6-6     | 捐助 1 元 |
| *怡(支付宝)   |    2024-6-6     | 捐助 8 元 |
| *+   |    2024-6-6     | 捐助 19.99 元 |
| *行   |    2024-6-7     | 捐助 50 元 |
| *+   |    2024-6-7     | 捐助 19.99 元 |
| *呆   |    2024-6-8     | 捐助 5 元 |
| 崔*9   |    2024-6-8     | 捐助 8.88 元 |
| *聊   |    2024-6-10     | 捐助 6.66 元 |
| Q*J   |    2024-6-11     | 捐助 20 元 |
| *龙(支付宝)   |    2024-6-11     | 捐助 19.9 元 |
| g*g   |    2024-6-12     | 捐助 10 元 |
| derbyoen   |    2024-6-12     | 捐助 20 元 |
| *海   |    2024-6-12     | 捐助 50 元 |
| *哥   |    2024-6-14     | 捐助 12 元 |
| T*m(ccynet) |    2024-6-14     | 捐助 20 元 |
| *就   |    2024-6-15     | 捐助 88.88 元 |
| *布   |    2024-6-16     | 捐助 3 元 |
| *昊   |    2024-6-17     | 捐助 6 元 |
| *健(支付宝)   |    2024-6-17     | 捐助 10 元 |
| l*G(njzlrjkj)   |    2024-6-18     | 捐助 10 元 |
| *、   |    2024-6-18     | 捐助 0.88 元 |
| *迷   |    2024-6-18     | 捐助 6 元 |
| *唱   |    2024-6-19     | 捐助 18 元 |
| *哦(喵の左爪)   |    2024-6-19     | 捐助 10 元 |
| *维(支付宝)   |    2024-6-21     | 捐助 0.6 元 |
| *齐   |    2024-6-23     | 捐助 10 元 |
| *贤   |    2024-6-24     | 捐助 9.74 元 |
| L*n   |    2024-6-27     | 捐助 1.00 元 |
| *王   |    2024-6-28     | 捐助 10.00 元 |
| *钰   |    2024-6-30     | 捐助 10.00 元 |
| *㯖   |    2024-6-30     | 捐助 1.00 元 |
| 张*)(zhangzhiqun)   |    2024-7-3     | 捐助 19.99 元 |
| *意   |    2024-7-5     | 捐助 10.00 元 |
| J*n(0xouzm)   |    2024-7-5     | 捐助 10.00 元 |
| *瑄   |    2024-7-6     | 捐助 10.00 元 |
| *子   |    2024-7-6     | 捐助 10.00 元 |
| 0*8   |    2024-7-6     | 捐助 20.00 元 |
| *草(支付宝)   |    2024-7-6     | 捐助 50.00 元 |
| *浩(支付宝)   |    2024-7-6     | 捐助 20.00 元 |
| *子   |    2024-7-8     | 捐助 10.00 元 |
| *哦(喵の左爪)   |    2024-7-10     | 捐助 10.00 元 |
| *柳   |    2024-7-11     | 捐助 2.00 元 |
| *淳(dcyy-l)   |    2024-7-11     | 捐助 10.00 元 |
| *枫   |    2024-7-11     | 捐助 10.00 元 |
| *娴   |    2024-7-11     | 捐助 10.00 元 |
| s*n   |    2024-7-11     | 捐助 5.00 元 |
| *剑   |    2024-7-13     | 捐助 6.00 元 |
| *砀(支付宝)   |    2024-7-14     | 捐助 14.00 元 |
| *俊   |    2024-7-15     | 捐助 10.00 元 |
| J*g   |    2024-7-15     | 捐助 10.00 元 |
| *羽(fanvfx2022)   |    2024-7-16     | 捐助 18.00 元 |
| *.   |    2024-7-16     | 捐助 1.00 元 |
| *生   |    2024-7-16     | 捐助 20.00 元 |
| *虎(thinsir)   |    2024-7-18    | 捐助 50.00 元 |
| *x(rich360)   |    2024-7-21    | 捐助 10.00 元 |
| *开(支付宝)   |    2024-7-21    | 捐助 10.00 元 |
| *号(19903110997)   |    2024-7-21    | 捐助 10.00 元 |
| *亮(支付宝)   |    2024-7-24    | 捐助 10.00 元 |
| *发   |    2024-7-25    | 捐助 10.00 元 |
| *轩(支付宝)   |    2024-7-27    | 捐助 10.00 元 |
| l*n   |    2024-7-28    | 捐助 3.00 元 |
| *丁  |    2024-7-29    | 捐助 50.00 元 |
| s*r   |    2024-7-29    | 捐助 1.00 元 |
| *璞   |    2024-7-29    | 捐助 5.00 元 |
| *繁   |    2024-7-29    | 捐助 100.00 元 |
| *言   |    2024-7-30    | 捐助 10.00 元 |
| 喵の左爪   |    2024-7-31    | 捐助 2.5 元 |
| *猫   |    2024-8-2    | 捐助 20.00 元 |
| *峰(支付宝)   |    2024-8-4    | 捐助 10.00 元 |
| 鲁*U   |    2024-8-9    | 捐助 10.00 元 |
| *龙(支付宝)   |    2024-8-12    | 捐助 1.00 元 |
| *郎   |    2024-8-12    | 捐助 20.00 元 |
| n*o   |    2024-8-12    | 捐助 5.00 元 |
| m*s(wjcbigwjc)   |    2024-8-15    | 捐助 50.00 元 |
| *勇(支付宝)  |    2024-8-16    | 捐助 0.01 元 |
| *枝  |    2024-8-17    | 捐助 20.00 元 |
| *成  |    2024-8-19    | 捐助 1.00 元 |
| *飞(支付宝)  |    2024-8-19    | 捐助 10.00 元 |
| *D  |    2024-8-20    | 捐助 5.00 元 |
| *根(zhangjiangen11)  |    2024-8-21    | 捐助 100.00 元 |
| *人  |    2024-8-25    | 捐助 2.00 元 |
| *鹏  |    2024-8-25    | 捐助 5.00 元 |
| *博(支付宝)  |    2024-8-26    | 捐助 20.00 元 |
| *堂  |    2024-8-29    | 捐助 8.8 元 |
| *腾  |    2024-9-2    | 捐助 20 元 |
| 尤*)  |    2024-9-3    | 捐助 20 元 |
|*宇(支付宝)|2024-9-4|捐助 49.9 元|
|*魂|2024-9-4|捐助 3 元|
|*鲤(lichuanbin2011)|2024-9-9|捐助 10 元|
|*源(支付宝)|2024-9-8| 捐助 50 元|
|W*Y|2024-9-9|捐助 6.66 元|
|F*n|2024-9-10|捐助 10 元|
|*章|2024-9-10|捐助 2 元|
|*平|2024-9-11|捐助 10 元|
|*哦|2024-9-11|捐助 10 元|
|*国|2024-9-12|捐助 0.55 元|
|*红|2024-9-12|捐助 0.52 元|
|*工|2024-9-15|捐助 10 元|
|*云|2024-9-22|捐助 10 元|
|H*n|2024-9-22|捐助 20 元|
|杨*g(Liming)|2024-9-24|捐助 66.6 元|
|*佳|2024-9-24|捐助 10 元|
|*℃|2024-9-25|捐助 20 元|
|*单|2024-9-29|捐助 10 元|
|p*k|2024-9-29|捐助 7 元|
|A*n|2024-9-29|捐助 0.1 元|
|wilson araujo|2024-9-29|捐助 $5 美元|
|*。|2024-10-1|捐助 5 元|
|*钱(支付宝)|2024-10-1|捐助 66 元|
|*溪|2024-10-3 |捐助  33 元|
|*EXSP|2024-10-4 |捐助  18.8 元|
|*凡|2024-10-4 |捐助  10 元|
|*秋|2024-10-4 |捐助  5 元|
|*焦|2024-10-4 |捐助  6 元|
|*鹏|2024-10-5 |捐助  20 元|
|*骨|2024-10-5 |捐助  20 元|
|*|2024-10-6 |捐助  10 元|
|*仔|2024-10-8 |捐助  5 元|
|*|2024-10-8 |捐助  20 元|
|*璠(支付宝)|2024-10-8 |捐助  20 元|
|*喆(支付宝)|2024-10-9 |捐助  50 元|
|*垠|2024-10-11|捐助 30  元|
|*仔|2024-10-11 |捐助  5 元|
|*F|2024-10-12 |捐助  10 元|
|Y*S|2024-10-13 |捐助  66 元|
|*淀|2024-10-15 |捐助  6.6 元|
|*章|2024-10-15 |捐助  5 元|
|*宁(支付宝)|2024-10-15 |捐助  20 元|
|*姚|2024-10-16 |捐助  15 元|
|*军(支付宝)|2024-10-16 |捐助  1 元|
|*哦|2024-10-17 |捐助  10 元|
|*兴(支付宝)|2024-10-17 |捐助  10 元|
|*猫|2024-10-18 |捐助  6.66 元|
|*爱|2024-10-18 |捐助  10 元|
|*叶|2024-10-19 |捐助  1 元|
|*磊|2024-10-19 |捐助  20 元|
|*见|2024-10-20 |捐助 9.9  元|
|*山|2024-10-20 |捐助  1 元|
|M*4|2024-10-21 |捐助  30 元|
|月夜|2024-10-21 |捐助 100  元|
|*意|2024-10-22 |捐助  10 元|
|*空|2024-10-22 |捐助 5  元|
|n*n|2024-10-23 |捐助  20 元|
|s*l|2024-10-23 |捐助  0.2 元|
|*中(支付宝)|2024-10-23 |捐助  66 元|
|pyirun*|2024-10-24 |捐助  20 元|
|*元|2024-10-30 |捐助 100  元|
|*哦|2024-10-30 |捐助  10 元|
|*彪(支付宝)|2024-10-31 |捐助  1.88 元|
|*z|2024-11-1 |捐助  1 元|
|*|2024-11-2  |捐助 10 元|
|*瞳(支付宝)|2024-11-3  |捐助 10 元|
|*山|2024-11-4  |捐助 10 元|
|F*n|2024-11-5  |捐助 5 元|
|深蓝广告|2024-11-6  |捐助 5 元|
|a*l|2024-11-7  |捐助 10 元|
|*!|2024-11-7  |捐助 10 元|
|*观2024-11-8  |捐助 10 元|






# [ChatTTS-ui项目](https://github.com/jianchang512/chattts-ui)捐助者列表

| 微信支付宝昵称/GitHub用户名 | 日期  | 金额 |
| --- | --- | --- |
| T*m(ccynet) |    2024-6-14     | 捐助 10 元 |

**未标注付款方式的即为微信支付,括号内标注为GitHub用户名,感谢所有支持者,软件的每一点进步都离不开您的支持和帮助。**





## /docs/architecture.md

# pyVideoTrans 技术架构与实现原理

`pyvideotrans` 是一款功能强大的开源视频翻译配音工具(v4.03),能够将视频自动翻译并配上目标语言的语音。其核心设计理念是模块化、多线程流水线,通过灵活的标志位组合支持多种工作模式。

![](https://pvtr2.pyvideotrans.com/1760167240539_image.png)

---

## 一、核心处理流程

![](https://pvtr2.pyvideotrans.com/1760165489380_image.png)

软件将视频翻译配音过程分解为 **9 个独立阶段**,形成一条自动化的处理流水线。每个任务通过 5 个布尔标志位(`should_recogn`、`should_trans`、`should_dubbing`、`should_hebing`、`should_separate`)控制哪些阶段被跳过,从而支持不同的工作模式。

### 1.1 九个处理阶段

| 阶段 | 方法 | 职责 |
|------|------|------|
| **① 预处理** | `prepare()` | 从视频中分离无声视频流和原始音频流;可选人声/背景分离(UVR/Spleeter);可选降噪;创建缓存目录和输出目录 |
| **② 语音识别** | `recogn()` | 调用 ASR 引擎(默认 Faster-Whisper,支持 22 种渠道)将音频转录为带时间戳的 SRT 字幕;可选标点恢复、LLM 重新断句 |
| **③ 说话人分离** | `diariz()` | 调用说话人分离模型(built、ali_CAM、pyannote、reverb 四种后端),将字幕按说话人归类标注 |
| **④ 字幕翻译** | `trans()` | 将原始语言 SRT 字幕通过翻译渠道(24 种渠道)翻译为目标语言字幕;支持双语字幕输出 |
| **⑤ 配音** | `dubbing()` | 根据目标语言字幕内容和时间戳,调用 TTS 引擎(34 种渠道)逐条生成配音音频;支持声音克隆(从原始音频截取参考片段) |
| **⑥ 音画对齐** | `align()` | 通过 `SpeedRate` 类处理:配音加速、视频慢放、去除字幕间隙静音、字幕音频强制对齐;完成后可选调节音量 |
| **⑦ 二次识别** | `recogn2pass()` | 对配音音频再次进行 ASR,生成时间轴精确且短小的字幕(仅在启用配音且非双字幕嵌入时执行) |
| **⑧ 最终合成** | `assembling()` | 将无声视频流、配音音频、背景音乐、目标语言字幕合并为最终视频文件(ffmpeg) |
| **⑨ 收尾** | `task_done()` | 将输出文件从临时目录移动到指定输出目录,清理临时文件,发送完成通知 |

### 1.2 流程控制标志位

定义在 `videotrans/task/_base.py:20-29`,五个标志位在 `TransCreate.__post_init__()` 中根据配置自动计算:

```python
should_recogn: bool    # 是否需要语音识别(无已有字幕则为 True)
should_trans: bool     # 是否需要翻译(源语言 ≠ 目标语言则为 True)
should_dubbing: bool   # 是否需要配音(选择了配音角色且非 'No' 则为 True)
should_hebing: bool    # 是否需要嵌入合并(非 'tiqu' 模式且有配音或字幕嵌入则为 True)
should_separate: bool  # 是否需要人声背景分离
```

### 1.3 模式切换示例

不同功能通过标志位组合实现:

| 功能 | should_recogn | should_trans | should_dubbing | should_hebing |
|------|:---:|:---:|:---:|:---:|
| 视频翻译配音(标准模式) | ✓ | ✓ | ✓ | ✓ |
| 视频/音频转字幕(tiqu) | ✓ | 可选 | ✗ | ✗ |
| 字幕配音 | ✗ | ✗ | ✓ | ✓ |
| 仅翻译字幕文件 | ✗ | ✓ | ✗ | ✗ |

### 1.4 任务子类体系

`BaseTask` 有四个具体子类,各自对应不同的使用场景:

| 子类 | 文件 | 继承的 TaskCfg | 使用场景 |
|------|------|----------------|---------|
| `TransCreate` | `task/trans_create.py` | `TaskCfgVTT` | 完整视频翻译配音(标准模式 / tiqu 提取模式) |
| `SpeechToText` | `task/speech2text.py` | `TaskCfgSTT` | 批量语音转字幕 |
| `DubbingSrt` | `task/dubbing.py` | `TaskCfgTTS` | 批量为字幕配音 |
| `TranslateSrt` | `task/translate_srt.py` | `TaskCfgSTS` | 批量翻译 SRT 字幕 |

---

## 二、任务配置数据类体系

v4.03 重构了任务配置为分层继承的 `@dataclass` 体系(`videotrans/task/taskcfg.py`,261 行):

```
@dataclass TaskCfgBase              ← 通用字段(路径、语言代码、缓存目录等)
    ├── @dataclass TaskCfgSTT       ← 语音识别相关字段(recogn_type, model_name, rephrase 等)
    ├── @dataclass TaskCfgTTS       ← 配音相关字段(tts_type, voice_role, voice_autorate 等)
    ├── @dataclass TaskCfgSTS       ← 翻译相关字段(translate_type)
    └── @dataclass TaskCfgVTT       ← 视频翻译全量字段(继承 STT + TTS + STS,新增视频特有字段)
```

辅助数据类:

| 数据类 | 文件 | 用途 |
|--------|------|------|
| `InputFile` | `task/taskcfg.py` | 输入文件元信息(name, dirname, noextname, basename, ext, uuid, target_dir),支持 dict 式访问 |
| `SignMsg` | `task/taskcfg.py` | 信号消息体(type, uuid, text),提供 `is_stop()` 和 `is_error()` 方法判断状态,在 Worker 线程与主线程间传递 |
| `SrtItem` | `task/taskcfg.py` | 单条字幕数据(text, start_time, end_time, startraw, endraw, line, time, spk, filename) |

`SrtItem` 支持同时用属性访问(`item.text`)和字典访问(`item['text']`),并可通过 `items()` 迭代。

---

## 三、多线程异步任务处理架构

软件采用基于 **"生产者-消费者"模式** 的多线程多队列架构。`MultVideo` 线程充当生产者,将任务对象推入流水线的第一个队列;9 种专用 `BaseWorker` 子类作为消费者,各自监听专属队列。

### 3.1 队列流水线

```
                     MultVideo (生产者)
                           │
                    app_cfg.prepare_queue
                           ▼
                   WorkerPrepare (×N)
                    ┌────────┼────────┐
                    │ should_recogn ?  │
                    ▼        ▼        ▼
           regcon_queue  trans_queue  dubb_queue / assemb_queue / taskdone_queue
                │
                ▼
          WorkerRegcon (×N)
                │
         diariz_queue
                │
                ▼
          WorkerDiariz (×N)
           ┌────┼────┐
           ▼    ▼    ▼
      trans_queue  dubb_queue  assemb_queue / taskdone_queue
           │
           ▼
     WorkerTrans (×1)
      ┌────┼────┐
      ▼    ▼    ▼
 dubb_queue  assemb_queue  taskdone_queue
      │
      ▼
WorkerDubb (×1)
      │
 align_queue
      │
      ▼
WorkerAlign (×1)
 ┌────┼────┐
 ▼    ▼    ▼
regcon2_queue  assemb_queue  taskdone_queue
 │
 ▼
WorkerRegcon2Pass (×1)
 ┌────┼────┐
 ▼    ▼
assemb_queue  taskdone_queue
 │
 ▼
WorkerAssemb (×N)
 │
taskdone_queue
 │
 ▼
WorkerTaskDone (×1)
 │
(end)
```

### 3.2 Worker 基类设计

所有工作线程继承自 `BaseWorker(QThread)`(`videotrans/task/job.py:13-66`):

```python
class BaseWorker(QThread):
    def __init__(self, name, queue):
        self.name = name
        self.queue = queue

    def run(self):
        while True:
            if app_cfg.exit_soft:          # 全局软退出标志
                return
            try:
                trk = self.queue.get(timeout=1)  # 阻塞1秒取任务
            except Empty:
                continue
            if trk.uuid in app_cfg.stoped_uuid_set:  # 任务已停止
                continue
            try:
                self.process_task(trk)       # 子类实现具体逻辑
            except Exception as e:
                self.handle_error(e, trk)    # 统一错误处理
```

每个子类重写以下方法:

| 方法 | 说明 |
|------|------|
| `process_task(trk)` | **必须** — 执行阶段逻辑,并将 trk 路由到下一个队列 |
| `get_error_prefix(trk)` | 可选 — 返回错误前缀字符串(如 `"识别出错[Faster-Whisper]"`) |
| `cleanup_on_error(trk)` | 可选 — 出错时的清理逻辑 |

`handle_error()` 统一调用 `get_msg_from_except()` 解析异常为用户可读信息,然后通过 `trk.signal()` 发送错误消息。

### 3.3 Worker 路由决策逻辑

每个 Worker 在执行完 `process_task(trk)` 后,根据 `trk` 的标志位决定下一跳队列:

```
WorkerPrepare    →  regcon_queue | trans_queue | dubb_queue | assemb_queue | taskdone_queue
WorkerRegcon     →  diariz_queue  (无条件)
WorkerDiariz     →  trans_queue | dubb_queue | assemb_queue | taskdone_queue  (diariz 异常不阻断流程)
WorkerTrans      →  dubb_queue | assemb_queue | taskdone_queue
WorkerDubb       →  align_queue  (无条件)
WorkerAlign      →  regcon2_queue | assemb_queue | taskdone_queue  (regcon2 仅当有 recogn2pass 属性时)
WorkerRegcon2Pass → assemb_queue | taskdone_queue
WorkerAssemb     →  taskdone_queue  (无条件)
WorkerTaskDone   →  (终止)
```

### 3.4 线程数量动态计算

`start_thread()`(`videotrans/task/job.py:206-245`)根据 GPU 配置动态决定各 Worker 的实例数:

| Worker | 实例数 | 原因 |
|--------|--------|------|
| `WorkerPrepare` | 1 ~ 4 | GPU 密集型操作(视频编解码) |
| `WorkerRegcon` | 1 ~ 4 | GPU 密集型(ASR 推理) |
| `WorkerDiariz` | 1 ~ 4 | GPU 密集型(说话人分离) |
| `WorkerTrans` | **固定 1** | API 调用,避免并发限流 |
| `WorkerDubb` | **固定 1** | TTS API 调用,避免并发限流 |
| `WorkerRegcon2Pass` | **固定 1** | 辅助阶段 |
| `WorkerAlign` | **固定 1** | 音画对齐为单线程 |
| `WorkerAssemb` | 1 ~ 4 | GPU 密集型(ffmpeg 编码) |
| `WorkerTaskDone` | **固定 1** | 文件移动/清理 |

`task_nums` 计算逻辑:优先使用 `settings.process_max_gpu` 手动指定值;否则根据 `multi_gpus` + `NVIDIA_GPU_NUMS` 自动检测(1 GPU = 1,2-3 GPU = 2,≥4 GPU = 4,无 GPU = 1)。

### 3.5 批量任务提交:MultVideo

`MultVideo(QThread)`(`videotrans/task/mult_video.py`,54 行)负责将用户选择的多个视频文件逐个创建 `TransCreate` 对象并推入 `prepare_queue`。支持通过 `batch_nums` 参数控制每批并发数量:

- `batch_nums == 0`:全部任务一次性推入队列(最大并发)
- `batch_nums == 1`:逐次推入,每个任务完成后再推下一个
- `batch_nums > 1`:每批推入 N 个,等待该批全部完成后再推下一批

### 3.6 软退出机制

全局标志 `app_cfg.exit_soft` 设为 `True` 时,所有 Worker 在下一轮循环中检测并安全退出。`app_cfg.stoped_uuid_set` 用于标记被手动停止的特定任务 UUID,Worker 在取出任务后跳过这些任务。

---

## 四、核心类的设计与继承关系

### 4.1 类继承体系

```
@dataclass BaseCon                    ← videotrans/configure/base.py
    │                                  基础属性和工具方法
    ├── @dataclass BaseTask           ← videotrans/task/_base.py
    │       │                          定义 8 个阶段空方法和 5 个标志位
    │       ├── @dataclass TransCreate ← videotrans/task/trans_create.py (~1678 行核心)
    │       ├── @dataclass SpeechToText ← videotrans/task/speech2text.py (批量语音识别)
    │       ├── @dataclass DubbingSrt  ← videotrans/task/dubbing.py (批量字幕配音)
    │       └── @dataclass TranslateSrt ← videotrans/task/translate_srt.py (批量字幕翻译)
    │
    ├── @dataclass BaseRecogn         ← videotrans/recognition/_base.py
    │       │                          VAD 音频切分、字幕合并、CJK 处理
    │       └── 22 个子类(懒加载)    各 ASR 渠道具体实现
    │
    ├── @dataclass BaseTrans          ← videotrans/translator/_base.py
    │       │                          MD5 缓存、逐行/全文翻译调度
    │       └── 24 个子类(懒加载)    各翻译渠道具体实现
    │
    └── @dataclass BaseTTS            ← videotrans/tts/_base.py
            │                          异步/多线程并发调度
            └── 34 个子类(懒加载)    各 TTS 渠道具体实现
```

所有通道类均为 `@dataclass`,使用 `__post_init__` 初始化而非传统构造函数 `__init__`。

### 4.2 BaseCon——顶层基类

`videotrans/configure/base.py`(296 行)定义了所有类共用的核心能力:

| 方法 | 职责 |
|------|------|
| `_exit()` | 检查是否应停止(`exit_soft` 或 UUID 在 `stoped_uuid_set` 中) |
| `signal(**kwargs)` | 向 UI 发送消息(通过 `push_queue()` → `SignalHub`。CLI 模式下直接 print) |
| `_set_proxy(type)` | 设置/清除 HTTP 代理(操作 `app_cfg.proxy` 和环境变量) |
| `_new_process(callback, title, is_cuda, kwargs)` | **在子进程中执行耗时任务**(返回 `(data, error)` 元组) |
| `_signal_of_process(logs_file)` | 通过轮询 JSON 日志文件的 mtime 实时读取子进程进度 |
| `convert_to_wav()` | 音频统一转为 48kHz 立体声 WAV(可选去静音) |
| `_base64_to_audio()` / `_audio_to_base64()` | Base64 音频编解码 |
| `_process_callback(data)` | 下载进度回调(转发到 `signal()`) |

`BaseCon.__post_init__()` 在初始化时自动调用 `_set_proxy(type='set')` 获取代理配置。

### 4.3 BaseTask——任务基类

`videotrans/task/_base.py:10-167` 定义了所有任务子类的阶段空方法和共享工具:

**阶段方法**(均为空实现,由子类重写):
`prepare()`、`recogn()`、`diariz()`、`trans()`、`dubbing()`、`align()`、`assembling()`、`task_done()`

> 注意:`recogn2pass()` 方法定义在 `TransCreate` 中,不在 `BaseTask` 基类中。

**共享方法**:
| 方法 | 职责 |
|------|------|
| `_unlink_size0(file)` | 删除尺寸为 0 的无效文件 |
| `_save_srt_target(srtstr, file)` | 将 SrtItem 列表格式化为 SRT 字符串并写入文件,发送 `replace_subtitle` 信号 |
| `check_target_sub(source, target)` | 校验翻译前后字幕行数一致性;不一致时按时间轴匹配对齐 |
| `set_end(succeed=False)` | 标记任务结束,成功时发送通知并清理临时文件夹 |
| `_edgetts_single(target_audio, kwargs)` | Edge-TTS 一次性异步配音(带代理回退) |

### 4.4 TransCreate——视频翻译核心实现

`videotrans/task/trans_create.py`(约 1678 行)是完整 9 阶段处理逻辑的实现类。关键内部方法:

| 方法 | 职责 |
|------|------|
| `__post_init__()` | 初始化所有文件路径、计算标志位、启动进度计时线程 |
| `_split_novoice_byraw()` | 从原始视频分离无声视频(优先硬件解码 h264_cuvid,回退 libx264) |
| `_split_audio_byraw()` | 从原始视频提取 16kHz 单声道 PCM 音频 + 可选人声/背景分离 |
| `_tts()` | 构建 `queue_tts` 列表(含 clone 参考音频片段),调用 `tts.run()` |
| `_create_ref_from_vocal()` | 多线程(ThreadPoolExecutor)裁剪原始音频对应片段作为声音克隆参考 |
| `_recogn_succeed()` | 识别完成后的处理(tiqu 模式下复制文件) |
| `_back_music()` | 将用户上传的背景音乐与配音音频混合 |
| `_separate()` | 将分离出的背景音乐重新嵌入配音音频 |
| `_process_subtitles()` | 处理软/硬字幕嵌入逻辑(单/双字幕、样式设置) |

### 4.5 子进程通道

为防止 `faster-whisper` 崩溃导致整个软件退出,`Faster-Whisper`、`Faster-Whisper-XXL` 和 `Whisper.cpp`(以及部分 TTS 引擎如 `QWEN3LOCAL_TTS`)通过 `BaseCon._new_process()` 委托给 `GlobalProcessManager` 在独立子进程中执行。

子进程通过写入 JSON 日志文件来报告进度。`BaseCon._signal_of_process()` 在守护线程中轮询该日志文件,检测到 mtime 变化时解析 JSON 并通过 `signal()` 上报。

---

## 五、配置系统

软件将配置分为三个层次(`videotrans/configure/config.py`,902 行),均为 `@dataclass`:

| 配置类 | 持久化 | 用途 | 示例字段 |
|--------|--------|------|---------|
| `AppCfg` | 纯内存 | 队列、状态、线程控制、运行时上下文 | `prepare_queue`, `exit_soft`, `stoped_uuid_set`, `current_status`, `line_roles`, `exec_mode`, `video_codec`, `onlyone_source_sub`, `onlyone_target_sub`, `proxy`, `SUPPORT_LANG` |
| `AppSettings` | `videotrans/cfg.json` | 全局默认设置、模型列表 | `homedir`, `model_list`, `vad_type`, `cuda_com_type` |
| `AppParams` | `videotrans/params.json` | 用户偏好、API 密钥 | `source_language`, `recogn_type`, `chatgpt_key`, `voice_role`, `app_mode` |

关键单例变量在模块加载时自动初始化:

```python
app_cfg: AppCfg = AppCfg()        # 运行时状态(含 9 个 Queue 实例)
settings: AppSettings = AppSettings()  # 从 cfg.json 加载
params: AppParams = AppParams()    # 从 params.json 加载
```

### 5.1 AppSettings 特性

- 支持 `settings['key']` 字典式访问和 `settings.get('key', default)` 方法
- `get()` 自动对数字类型字段进行类型强制转换(`int_type` 和 `float_type` 白名单)
- `_get_defaults()` 定义了 ~100 个配置项的默认值
- 支持连字符字段名映射(如 `"initial_prompt_zh-cn"` → `"initial_prompt_zh_cn"`)

### 5.2 AppParams 特性

- `_get_defaults()` 定义了 ~100 个用户参数默认值
- `getset_params(update_data)` 支持批量更新(如 `check_start()` 中收集所有 UI 控件值)
- API key 类字段统一在此管理,供 `is_input_api()` 校验

### 5.3 AppCfg 运行时状态

- 9 个 `Queue(maxsize=0)`(无限容量):`prepare_queue` ~ `taskdone_queue`
- `queue_novice: Dict` — 跟踪无声视频分离进度(key=uuid, value='ing'|'end')
- `line_roles: Dict` — 存储单视频模式下用户逐行分配的字幕角色
- `child_forms: Dict` — 缓存已打开的窗口实例,避免重复创建
- `exec_mode` — 执行模式('gui' 或 'cli')
- `video_codec` / `codec_cache` — 视频编解码器缓存
- `onlyone_source_sub` / `onlyone_target_sub` / `onlyone_trans` — 单视频模式字幕状态
- `SUPPORT_LANG` — 支持的语言列表

### 5.4 环境变量初始化

`_set_env()` 在模块加载时自动执行(`videotrans/configure/config.py:44-72`),设置:
- `MODELSCOPE_CACHE` / `HF_HOME` / `HF_HUB_CACHE` → `ROOT_DIR/models`
- `QT_API = 'pyside6'`
- `PATH` 追加 ffmpeg/sox 目录
- `OMP_NUM_THREADS = 1`
- `HF_HUB_DOWNLOAD_TIMEOUT = 3600`
- `HF_HUB_DISABLE_XET = 1`

---

## 六、GlobalProcessManager——子进程池管理

`videotrans/process/signelobj.py`(167 行)实现了一个类级别单例的 `GlobalProcessManager`:

```
GlobalProcessManager (类级别单例)
    ├── _executor_cpu: multiprocessing.Pool
    │       workers = max(min(available_ram/4GB, 8, cpu_count), 1)  ← 基于剩余内存量计算
    │       maxtasksperchild = 1  ← 每个子进程执行一个任务后重启,防内存泄漏
    │
    └── _executor_gpu: multiprocessing.Pool
            workers = GPU 数量(优先 settings.process_max_gpu 手动设置)
            maxtasksperchild = 1
```

### 6.1 CPU 进程池规模

不再使用固定公式,而是通过 `psutil.virtual_memory().available` 获取当前系统剩余内存,按每 4GB 一个进程计算,限制在 **1~8** 之间,且不超过 `os.cpu_count()`。可通过 `settings.process_max` 手动覆盖。

### 6.2 GPU 进程池规模

优先使用 `settings.process_max_gpu` 手动设置值;否则根据 `multi_gpus` 和 `NVIDIA_GPU_NUMS` 自动确定(无显卡 = 1,有显卡但未启用多显卡 = 1,启用多显卡 = min(GPU 数量, 8, cpu_count))。

### 6.3 任务提交接口

```python
GlobalProcessManager.submit_task_cpu(func, **kwargs)   → AsyncResultFutureWrapper
GlobalProcessManager.submit_task_gpu(func, **kwargs)   → AsyncResultFutureWrapper
```

`AsyncResultFutureWrapper` 将 `Pool.apply_async` 的 `AsyncResult` 包装为 `Future` 兼容接口(`.result()`, `.done()`)。

### 6.4 使用场景

通过 `BaseCon._new_process()` 统一调用,用于执行:ASR 推理、TTS 合成、噪声去除、人声分离、说话人分离、标点恢复(均在独立子进程中运行,崩溃不影响主进程)。

---

## 七、SignalHub——跨线程消息中心

`videotrans/configure/signal_hub.py`(33 行)实现了基于 Qt 信号的单例消息传递:

```python
class SignalHub(QObject):
    _instance = None
    new_message = Signal(str, object)  # (uuid, SignMsg)

    @classmethod
    def instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    @Slot(str, object)
    def post(self, uuid=None, data=None):
        self.new_message.emit(uuid, data)  # 跨线程自动使用 QueuedConnection
```

### 消息流

```
BaseCon.signal(**kwargs)
    → push_queue(uuid, SignMsg(**kwargs))     [configure/config.py]
        → SignalHub.instance().post(uuid, data)
            → new_message Signal (QueuedConnection)
                → WinAction.update_data(uuid, data)   [mainwin/_actions.py]
                    → 按 type 分发:
                        'logs'|'error'|'succeed'|'set_precent' → set_process_btn_text()
                        'edit_subtitle_source' → 弹出 EditRecognResultDialog
                        'edit_subtitle_target' → 弹出 SpeakerAssignmentDialog
                        'edit_dubbing' → 弹出 EditDubbingResultDialog
                        'replace_subtitle' → 更新字幕编辑区
                        'end' → update_status('end')
```

### 消息类型枚举

| type | 含义 | 处理逻辑 |
|------|------|---------|
| `logs` | 普通日志 | 更新进度条文本 |
| `error` | 错误 | 进度条变红,加入重试队列 |
| `succeed` | 成功 | 进度条变绿,标记完成 |
| `set_precent` | 进度百分比 | `text="耗时???百分比"` 格式 |
| `edit_subtitle_source` | 弹出原始字幕编辑框 | 单视频模式暂停点① |
| `edit_subtitle_target` | 弹出翻译字幕编辑框 | 单视频模式暂停点② |
| `edit_dubbing` | 弹出配音结果编辑框 | 单视频模式暂停点③ |
| `replace_subtitle` | 替换字幕区域内容 | 批量/单视频共用 |
| `subtitle` | 追加字幕行 | 逐行输出到编辑器 |
| `end` | 任务完成 | 触发 update_status('end') |
| `disabled_edit` | 禁止编辑字幕 | 批量模式下锁定编辑器 |
| `refreshtts` | 刷新 TTS 选择 | 重新设置 TTS 下拉框 |
| `shitingerror` | 试听错误 | 弹出错误提示 |
| `ffmpeg` | ffmpeg 状态 | 更新开始按钮文本 |

---

## 八、动态通道加载

`videotrans/__init__.py`(35 行)提供了通用的懒加载机制:

```python
@dataclass
class ChannelProvider:
    name: str           # 界面显示名称
    imp: str            # 模块导入后缀(如 "._whisper" → "videotrans.recognition._whisper")
    key_name: str|None  # 对应 params.json 中的 API key 字段(用于 is_input_api 校验)
    win: str|None       # 对应 winform 中的设置窗口名称

def get_class(channel_id=0, provider_type=None, _ID_NAME_DICT=None):
    _key = f'{provider_type}-{channel_id}'
    if _key in _loaded_modules:
        return _loaded_modules[_key]
    module = importlib.import_module(f'videotrans.{provider_type}{_module_map.imp}')
    for _, obj in inspect.getmembers(module, inspect.isclass):
        if obj.__module__ == module.__name__:
            _loaded_modules[_key] = obj
            return obj
```

三大模块各自的 `_ID_NAME_DICT`:

| 模块 | 渠道数 | 定义位置 |
|------|--------|---------|
| 识别 (recognition) | 22 | `videotrans/recognition/__init__.py:48-79` |
| 翻译 (translator) | 24 | `videotrans/translator/__init__.py:60-90` |
| 配音 (tts) | **34** | `videotrans/tts/__init__.py:75-116` |

### 8.1 统一入口函数

每个模块提供 `run()` 统一入口,内部通过 `get_class()` 获取对应渠道类并实例化调用:

```python
# recognition/__init__.py
def run(*, recogn_type, detect_language, audio_file, ...) -> List[SrtItem]:
    _cls = get_class(recogn_type, "recognition", _ID_NAME_DICT)
    return _cls(**kwargs).run()

# translator/__init__.py
def run(*, translate_type, text_list, source_code, target_code, ...) -> List[SrtItem]:
    _cls = get_class(translate_type, "translator", _ID_NAME_DICT)
    return _cls(**kwargs).run()

# tts/__init__.py
def run(*, queue_tts, language, tts_type, ...) -> None:
    _cls = get_class(tts_type, "tts", _ID_NAME_DICT)
    return _cls(**kwargs).run()
```

### 8.2 API Key 校验

每个模块提供 `is_input_api(recogn_type/translate_type/tts_type)` 函数,检查对应渠道的 `key_name` 在 `params` 中是否已填写。未填写时自动弹出对应的 winform 设置窗口。

### 8.3 翻译缓存

`BaseTrans`(`videotrans/translator/_base.py`)实现了基于 MD5 的翻译缓存:
- 缓存 key = `md5(channel_name + api_url + model + source_lang + target_lang + text)`
- 缓存文件存储在 `{TEMP_ROOT}/translate_cache/`
- 写入接口 `_set_cache()`,读取接口 `_get_cache()`

### 8.4 CJK 特殊处理

`BaseRecogn`(`videotrans/recognition/_base.py:58-80`)在 `__post_init__` 中对中日韩等语言进行特殊处理:
- `join_word_flag`:CJK 语言(zh, ja, ko, yu, th, km, yue)字幕词间不加空格(其他语言加空格)
- `maxlen`:CJK 语言每行最大字符数为 `settings.cjk_len`(默认 15),其他语言为 `settings.other_len`(默认 40)
- `jianfan`:中文语言且 `settings.zh_hant_s=True` 时启用繁简转换

### 8.5 翻译调度策略

`BaseTrans.run()` 根据 `aisendsrt` 标志选择不同策略:

| 模式 | 条件 | 方法 | 并发数 |
|------|------|------|--------|
| 逐行翻译 | 非 AI 渠道 | `_run_text()` → `_item_task()` | `settings.trans_thread`(默认 10) |
| 全文翻译 | AI 渠道 + `aisendsrt=True` | `_run_srt()` | `settings.aitrans_thread`(默认 50) |

### 8.6 TTS 调度策略

`BaseTTS.run()` 根据渠道类型选择执行方式:

| 渠道类型 | 调度方式 | 说明 |
|---------|---------|------|
| Edge-TTS | `asyncio` 异步 | 单线程内 async 并发 |
| 其他渠道 | `ThreadPoolExecutor` | 由 `dubbing_thread` 控制并发数(默认 1) |

渠道子类可重写 `_exec()` 方法实现自定义调度。`BaseTTS` 默认调用 `__local_mul_thread()` → `_item_task()`。

---

## 九、交互式单视频处理模式

当用户选择 **1 个视频** 且在**标准模式(biaozhun)**下时,程序采用不同于批量流水线的处理模型。

### 9.1 实现:Worker(QThread)

`videotrans/task/only_one.py`(148 行)中的 `Worker` 类在**单个 QThread 内串行执行**全部 9 个阶段,通过 `uito = Signal(str, SignMsg)` 与主线程通信:

```
Worker.run()
    ├── trk = TransCreate(cfg=TaskCfgVTT(**self.cfg | obj))
    ├── trk.prepare()
    ├── trk.recogn()
    ├── trk.diariz()
    ├── [暂停点 ①] → _post(type='edit_subtitle_source')
    │    用户校对原始字幕 → 点击"确定"或等待倒计时
    ├── trk.trans() (if should_trans)
    ├── [暂停点 ②] → _post(type='edit_subtitle_target')
    │    用户校对翻译字幕 + 分配说话人角色 → 点击"确定"
    ├── trk.dubbing() (if should_dubbing)
    ├── [暂停点 ③] → _post(type='edit_dubbing')
    │    用户修改配音结果 → 点击"确定"
    ├── trk.align()
    ├── trk.recogn2pass()
    ├── trk.assembling()
    └── trk.task_done()
```

### 9.2 与批量模式的关键差异

| 维度 | 单视频模式 | 批量模式 |
|------|-----------|---------|
| 执行线程 | `Worker(QThread)` 直接执行,不使用队列管道 | `TransCreate` 推入 `prepare_queue`,经 9 个 Worker 队列流动 |
| 消息通道 | `uito` 信号直接连接到 `WinAction.update_data()` | `BaseCon.signal()` → `push_queue()` → `SignalHub` |
| 暂停机制 | 三段暂停点,用户可中间编辑 | 不支持暂停编辑 |
| 进度显示 | 字幕编辑区实时显示 | 进度条 + 按钮文本 |

### 9.3 倒计时与暂停机制

1. **自动倒计时**:`app_cfg.set_countdown(86400)` 设置初始值。Worker 线程每 `sleep(1)` 递减一次。默认倒计时由 `settings.countdown_sec` 控制。
2. **无限期暂停**:用户点击"停止"按钮将 `app_cfg.current_status` 设为 `'stop'`,Worker 的 `_exit()` 检测后退出;或 `set_countdown(-1)` 让倒计时消失。
3. **手动继续**:用户在校对对话框中点击"确定"后,`WinAction.set_djs_timeout()` 调用 `app_cfg.set_countdown(-1)` 使倒计时立即归零。

### 9.4 校对对话框

| 对话框 | 文件 | 功能 |
|--------|------|------|
| `EditRecognResultDialog` | `component/onlyone_set_recogn.py` | 原始字幕编辑(文本 + 时间轴) |
| `SpeakerAssignmentDialog` | `component/onlyone_set_role.py` | 翻译字幕编辑 + 逐行分配配音角色 |
| `EditDubbingResultDialog` | `component/onlyone_set_editdubb.py` | 配音结果试听 + 单独重新配音 |

![](https://pvtr2.pyvideotrans.com/1760192881455_image.png)
![](https://pvtr2.pyvideotrans.com/1760192930833_image.png)


---

## 十、音画对齐引擎(SpeedRate)

`videotrans/task/_rate.py`(877 行)实现了 `SpeedRate` 和 `TtsSpeedRate` 两个对齐引擎:

### 10.1 SpeedRate(视频翻译场景)

处理策略(按优先级):

| 条件 | 策略 |
|------|------|
| 启用音频加速 + 视频慢速 | 各负担一半时间差(忽略倍率限制) |
| 仅启用音频加速 | 加速配音到匹配字幕时长(最高不超过 `max_audio_speed_rate`) |
| 仅启用视频慢速 | 慢放视频片段到匹配配音时长(最高不超过 `max_video_pts_rate`) |
| 两者均未启用 | 按字幕时间轴拼接音频片段,填充静音/定格处理时长差异 |

额外处理:
- `remove_silent_mid`:去除字幕之间的静音区间
- `align_sub_audio`:强制对齐字幕时间轴到实际配音位置
- 末尾静音移除

### 10.2 TtsSpeedRate(纯配音场景)

简化版对齐引擎,仅负责音频拼接与加速,无视频慢放逻辑。

---

## 十一、软件启动与 UI 实现

### 11.1 启动流程

`sp.py` 是唯一入口(221 行),启动过程如下:

```
sp.py (if __name__ == "__main__")
  │
  ├── 1. multiprocessing.freeze_support() / set_start_method('spawn')
  ├── 2. qInstallMessageHandler() 抑制 Qt 警告
  ├── 3. atexit.register(cleanup) 注册退出清理
  ├── 4. QApplication.setHighDpiScaleFactorRoundingPolicy(PassThrough)
  ├── 5. 创建 QApplication
  ├── 6. 检测是否在压缩包内运行(PyInstaller 打包版)
  ├── 7. 创建 StartWindow (splash screen, 无边框半透明)
  │       └── QTimer.singleShot(100ms) → initialize_full_app()
  │           ├── 重定向 sys.stdout/stderr 到日志文件
  │           ├── 设置全局异常钩子 show_global_error_dialog
  │           ├── 解析 --lang CLI 参数
  │           ├── 导入 darkstyle_rc(编译后的 QRC 资源)
  │           ├── 加载 QSS 样式表 (videotrans/styles/style.qss)
  │           ├── 恢复上次窗口大小 (QSettings)
  │           └── 实例化 MainWindow → uito 连接 splash.update_lable
  │               └── MainWindow.__init__()
  │                   ├── setupUi() → 填充下拉列表(翻译/识别/TTS 渠道、语言列表)
  │                   ├── AiLoaderThread 启动 → 检测 GPU → 回调 _start_workers()
  │                   ├── _start_workers() → start_thread() 启动 9 种 Worker 线程
  │                   ├── _set_default() → 恢复上次用户选择
  │                   ├── _bind_signal() → 绑定 ~60 个控件事件
  │                   ├── SignalHub.new_message.connect(win_action.update_data)
  │                   └── uito.emit('end') → splash 关闭
  └── 8. app.exec() → Qt 事件循环
```

### 11.2 退出机制

用户点击关闭按钮时:
1. 设置 `app_cfg.exit_soft = True`, `app_cfg.current_status = 'stop'`
2. 主窗口立即隐藏(`hide()`)
3. 保存窗口尺寸到 `QSettings`
4. 隐藏/关闭所有子窗口
5. 等待 ~4 秒让所有 Worker 完成当前工作并安全退出
6. 清理临时目录 `TEMP_ROOT`
7. `atexit` cleanup 回调执行 → 程序终止
8. 若为重启模式,启动新进程后 `os._exit(0)`

### 11.3 UI 架构分层

```
UI 定义层         videotrans/ui/         ← PySide6 UI 布局文件(~75 个),dark/ 资源文件
    ↓
UI 逻辑层         videotrans/component/   ← 通用组件:进度条、设置表单、字幕编辑器、实时语音识别、视频裁剪、文本比对
    ↓
窗口管理层        videotrans/winform/     ← 懒加载的 ~65 个设置/功能窗口模块
    ↓
主窗口层          videotrans/mainwin/
    ├── main_win.py                      ← MainWindow(QMainWindow): UI 初始化、信号绑定、Worker 启动、窗口生命周期(528 行)
    ├── _actions.py                       ← WinAction: 核心业务逻辑 → 参数收集 → 任务启动 → 状态分发(798 行)
    └── _actions_base.py                 ← WinActionBase: 代理管理、模式切换、文件选择、CUDA 检测、试听(590 行)
    ↓
任务层            videotrans/task/        ← TransCreate、SpeechToText、DubbingSrt、TranslateSrt、Worker 线程、SpeedRate
```

### 11.4 MainWindow——主窗口

`videotrans/mainwin/main_win.py`(528 行)职责:
- `setupUi()`:加载 UI 布局,填充下拉列表(翻译渠道、识别渠道、TTS 渠道、语言、字幕类型)
- `_bind_signal()`:绑定约 60 个控件事件到 `WinAction` 方法
- `_start_workers(status)`:GPU 检测完成后启动 9 种 Worker 后台线程
- `open_winform(name)`:统一窗口打开入口(优先复用已缓存的 `app_cfg.child_forms`,否则调用 `winform.get_win(name).openwin()`)
- `closeEvent()`:安全关闭流程(标记退出 → 隐藏窗口 → 停止线程 → 清理临时文件)
- `restart_app()`:询问确认后触发 `closeEvent()` 并启动新进程

### 11.5 WinAction——核心控制器

`WinAction` 继承自 `WinActionBase`(两者均为 `@dataclass`),是连接 UI 和后台任务的关键枢纽:

**WinActionBase**(`mainwin/_actions_base.py`,590 行)提供:
- 文件选择(`get_mp4()`)—— 单文件/文件夹模式
- 输出目录设置(`get_save_dir()`)
- 代理配置(`change_proxy()`, `check_proxy()`, `proxy_alert()`)
- 模式切换(`set_biaozhun()`, `set_tiquzimu()`)—— 控制 UI 元素显隐
- CUDA 检测(`check_cuda()`, `cuda_isok()`)
- 试听功能(`listen_voice_fun()`)—— 创建 `ListenVoice` 线程
- 角色列表更新(`tts_type_change()`, `set_voice_role()`)
- 高级选项折叠(`toggle_adv()`)
- UI 启用/禁用控制(`disabled_widget()`, `_disabled_button()`)

**WinAction**(`mainwin/_actions.py`,798 行)提供:
- `check_start()`:收集所有 UI 控件值 → 构建 `cfg` 字典 → 参数校验 → 调用 `create_btns()`
- `create_btns()`:格式化输入文件路径 → 创建进度条 → 单视频启动 `Worker`,批量启动 `MultVideo`
- `update_data(uuid, SignMsg)`:连接 `SignalHub.new_message` 信号 → 按消息类型分发
- `update_status(type)`:切换 `ing`/`stop`/`end` 状态,控制按钮和进度条
- `set_process_btn_text(d)`:更新进度条文本/百分比/颜色
- `retry()`:重新处理失败的任务
- `_check_all_done()`:检测是否所有任务完成

---

## 十二、异常体系

`videotrans/configure/excepts.py`(376 行)定义了分层异常:

```
VideoTransError (基类)
    ├── TranslateSrtError       # 翻译相关错误
    ├── DubbingSrtError         # 配音相关错误
    ├── SpeechToTextError       # 语音识别相关错误
    ├── LLMSegmentError         # LLM 重新断句错误
    ├── FFmpegError             # FFmpeg 操作错误
    ├── DownloadModelsError     # 模型下载错误
    ├── SttTimeoutError         # STT 子进程超时
    ├── StopTask                # 需立即停止的任务异常
    └── StopRetry               # 不可重试的错误
```

`get_msg_from_except(e)` 函数映射数十种第三方库异常为用户可读的中/英文错误消息(覆盖 `httpx`、`openai`、`requests`、`deepgram`、`elevenlabs`、`tenacity` 等)。

`NO_RETRY_EXCEPT` 元组定义了不可恢复的异常类型,翻译/配音模块在重试循环中遇到这些异常时直接放弃。

---

## 十三、代码结构概览

```
/
├── sp.py                       # ★ 主程序入口(221 行)
├── cli.py                      # ★ CLI 命令行入口
├── models/                     # 存放本地 AI 模型文件(ONNX 等)
├── logs/                       # 日志文件目录(YYYYMMDD.log)
├── ffmpeg/                     # ffmpeg 及 sox 二进制文件
├── f5-tts/                     # 声音克隆参考音频存放目录
├── docs/                       # 文档
├── tmp/                        # 临时文件根目录
│   ├── _temp/                  # 进程级临时目录
│   └── translate_cache/        # 翻译 MD5 缓存目录
│
└── videotrans/                 # 核心业务逻辑代码
    │   __init__.py             # ★ VERSION, ChannelProvider 定义, get_class() 懒加载
    │   cfg.json                # settings 持久化文件
    │   params.json             # params 持久化文件
    │   codec.json              # 视频编解码器缓存
    │
    ├── codes/
    │   └── model.py            # 模型相关定义
    │
    ├── configure/              # 全局配置、队列定义、顶层基类
    │   ├── config.py           # ★ AppCfg / AppSettings / AppParams / logger / 队列定义 / tr() / push_queue()(902 行)
    │   ├── base.py             # ★ BaseCon 基类(_new_process, signal, _exit, convert_to_wav 等)(296 行)
    │   ├── contants.py         # ★ 全局常量(模型列表、语言测试文本、标点符号、代理白名单等)
    │   ├── excepts.py          # ★ 异常体系 + get_msg_from_except()(376 行)
    │   ├── signal_hub.py       # ★ SignalHub 单例(跨线程 Qt 信号)(33 行)
    │   └── whispernet_config.py # Whisper.NET 配置
    │
    ├── task/                   # 任务处理逻辑与后台线程
    │   ├── _base.py            # ★ BaseTask 基类(8 阶段空方法 + 5 标志位 + 共享工具方法)(167 行)
    │   ├── taskcfg.py          # ★ TaskCfgBase/VTT/STT/TTS/STS + InputFile + SignMsg + SrtItem(261 行)
    │   ├── trans_create.py     # ★ TransCreate 完整实现(~1678 行,视频翻译核心)
    │   ├── speech2text.py      # ★ SpeechToText(批量语音转字幕)
    │   ├── dubbing.py          # ★ DubbingSrt(批量字幕配音)
    │   ├── translate_srt.py    # ★ TranslateSrt(批量翻译 SRT 字幕)
    │   ├── job.py              # ★ 9 种 BaseWorker 子类 + start_thread() 入口(245 行)
    │   ├── only_one.py         # ★ 单视频交互式 Worker(QThread) + uito 信号(148 行)
    │   ├── mult_video.py       # ★ 多视频批量提交 MultVideo(QThread)(54 行)
    │   ├── _rate.py            # SpeedRate / TtsSpeedRate 音画对齐引擎(877 行)
    │   ├── separate_worker.py  # SeparateWorker 独立人声分离 QThread
    │   ├── simple_runnable_qt.py # QRunnable 线程池工具
    │   ├── child_win_sign.py   # 子窗口信号处理
    │   └── update_ffmpeg.py    # ffmpeg 更新管理
    │
    ├── recognition/            # 语音识别 (ASR) 模块(22 个渠道)
    │   ├── __init__.py         # ★ 渠道常量 ID、_ID_NAME_DICT、run()、is_allow_lang()、is_input_api()
    │   ├── _base.py            # ★ BaseRecogn(VAD 分割、CJK 处理、字幕合并,400 行)
    │   └── _*.py               # 22 个渠道实现(_whisper, _whisperx, _whispernet, _qwenasrlocal, _qwen3asr, _funasr 等)
    │
    ├── translator/             # 字幕翻译模块(24 个渠道)
    │   ├── __init__.py         # ★ 渠道常量、_ID_NAME_DICT、LANG_CODE、run()、is_allow_translate()(860 行)
    │   ├── _base.py            # ★ BaseTrans(MD5 缓存、逐行/全文翻译调度,176 行)
    │   └── _*.py               # 24 个渠道实现(_google, _chatgpt, _deepseek, _gemini, _deepl, _baidu 等)
    │
    ├── tts/                    # 文本转语音 (TTS) 模块(**34** 个渠道)
    │   ├── __init__.py         # ★ 渠道常量 ID、_ID_NAME_DICT、SUPPORT_CLONE、CHANGE_BY_LANGUAGE、run()(192 行)
    │   ├── _base.py            # ★ BaseTTS(异步/多线程并发调度,304 行)
    │   └── _*.py               # 34 个渠道实现(_edgetts, _openaitts, _azuretts, _gptsovits, _cosyvoice 等)
    │
    ├── process/                # 独立子进程实现
    │   ├── __init__.py         # 子进程函数导出
    │   ├── signelobj.py        # ★ GlobalProcessManager(CPU/GPU 双进程池,167 行)
    │   ├── prepare_audio.py    # 人声分离、降噪、标点恢复、说话人分离(4 种后端)
    │   ├── stt_fun.py          # ASR 子进程入口(openai_whisper, faster_whisper, paraformer, funasr_mlt, qwen3asr_fun 等)
    │   ├── tts_fun.py          # TTS 子进程入口(qwen3tts_fun)
    │   └── vad.py              # VAD 语音活动检测(Silero VAD)
    │
    ├── mainwin/                # 主窗口界面与业务逻辑
    │   ├── main_win.py         # ★ MainWindow(QMainWindow) 初始化、信号绑定、线程启动(528 行)
    │   ├── _actions.py         # ★ WinAction 核心控制器(检查、启动、状态更新,798 行)
    │   └── _actions_base.py    # ★ WinActionBase 基类(代理、模式切换、CUDA、文件选择,590 行)
    │
    ├── component/              # UI 通用组件
    │   ├── progressbar.py      # 可点击进度条
    │   ├── set_form.py         # 通用设置表单 / 关于页面
    │   ├── onlyone_set_recogn.py    # 单视频模式:原始字幕编辑对话框
    │   ├── onlyone_set_role.py      # 单视频模式:说话人角色分配对话框
    │   ├── onlyone_set_editdubb.py  # 单视频模式:配音结果编辑对话框
    │   ├── clip_video.py       # 视频裁剪组件
    │   ├── realtime_stt.py     # 实时语音识别窗口
    │   ├── textmatching.py     # 文本比对窗口
    │   ├── set_proxy.py        # 代理设置弹窗
    │   ├── set_ass.py          # ASS 字幕样式设置
    │   ├── set_cpp.py          # Whisper.cpp 路径设置
    │   ├── set_xxl.py          # Faster-Whisper-XXL 路径设置
    │   ├── set_subtitles_length.py # 字幕长度设置
    │   ├── set_threads.py      # 线程数设置
    │   └── controlobj.py       # 控件对象管理
    │
    ├── ui/                     # PySide6 UI 定义文件(~75 个.py 文件)
    │   ├── en.py               # ★ 主窗口 UI 布局定义
    │   ├── chatgpt.py, deepseek.py, gemini.py, ...    # 各渠道设置对话框布局
    │   ├── videoandaudio.py, separate.py, peiyin.py, ... # 功能窗口布局
    │   └── dark/               # 暗色主题资源(darkstyle_rc.py, palette.py)
    │
    ├── winform/                # 各渠道设置窗口懒加载管理(~65 个模块)
    │   ├── __init__.py         # ★ get_win() 懒加载入口 + _module_map(91 行)
    │   ├── chatgpt.py, azure.py, baidu.py, ...  # ~50 个渠道设置窗口(openwin())
    │   └── fn_*.py             # ~10 个独立功能窗口(批量语音转字幕、批量为字幕配音、批量翻译srt字幕等)
    │
    ├── styles/                 # UI 样式与媒体资源
    │   ├── style.qss           # Qt 样式表
    │   ├── logo.png            # 启动画面 logo
    │   ├── icon.ico            # 应用图标
    │   ├── simhei.ttf          # 黑体中文字体
    │   ├── preview.png         # 预览图
    │   ├── no-remove.mp4       # 防清理的占位视频
    │   └── no-remove.wav       # 防清理的占位音频
    │
    ├── util/                   # 通用工具函数(18 个文件)
    │   ├── tools.py            # ★ 核心工具函数(ffmpeg 封装、字幕解析/格式化、文件操作、系统通知、模型下载)
    │   ├── gpus.py             # GPU 检测与分配(get_cudaX 获取可用 GPU 索引)
    │   ├── checkgpu.py         # GPU 检测线程(AiLoaderThread)
    │   ├── ListenVoice.py      # 声音试听功能(ListenVioce QThread)
    │   ├── req_fac.py          # HuggingFace 自定义 session 工厂
    │   ├── cn_tn.py            # 中文文本规范化
    │   ├── en_tn.py            # 英文文本规范化
    │   ├── help_down.py        # 下载工具函数
    │   ├── help_ffmpeg.py      # ffmpeg 视频编解码器检测
    │   ├── help_misc.py        # 杂项工具
    │   ├── help_role.py        # 配音角色工具
    │   ├── help_srt.py         # 字幕文件工具
    │   ├── helper_supertonic.py # Supertonic TTS 辅助
    │   ├── TestSrtTrans.py     # 翻译测试工具
    │   └── TestSTT.py          # STT 测试工具
    │
    ├── language/               # 界面多语言 JSON 文件
    │   ├── en.json
    │   ├── zh.json
    │   └── ...                 # 30+ 语言
    │
    ├── prompts/                # AI 翻译提示词模板(31 个文件)
    │   ├── srt/                # SRT 格式翻译 prompt(chatgpt.txt, deepseek.txt 等 13 个)
    │   ├── text/               # 纯文本翻译 prompt(同 13 个)
    │   ├── recogn/             # 语音识别 prompt(gemini_recogn.txt)
    │   └── recharge/           # LLM重新断句 prompt(recharge-llm.txt)
    │
    └── voicejson/              # TTS 音色配置文件(14 个 JSON)
        ├── edge_tts.json       # Edge-TTS 各语言音色列表
        ├── azure_voice_list.json # Azure TTS 音色列表
        ├── qwen3tts.json       # Qwen3-TTS 音色
        └── ...                 # 其他渠道音色配置
```

---

## 十四、扩展开发指南

### 14.1 新增一个翻译通道

假设要新增翻译通道 `MyTranslator`:

#### Step 1: 创建通道实现文件

在 `videotrans/translator/` 下创建 `_mytranslator.py`:

```python
from dataclasses import dataclass
from videotrans.translator._base import BaseTrans

@dataclass
class MyTranslator(BaseTrans):
    def __post_init__(self):
        super().__post_init__()
        self.api_url = 'https://api.example.com/translate'

    def _item_task(self, data: dict) -> str:
        text = data['text']
        source = data['source_code']
        target = data['target_code']
        result = call_my_api(text, source, target)
        return result
```

#### Step 2: 分配渠道 ID 并注册

在 `videotrans/translator/__init__.py` 中:

```python
MYTRANSLATOR_INDEX = 24   # 分配不重复的整数 ID

# 在 _ID_NAME_DICT 末尾添加:
_ID_NAME_DICT[MYTRANSLATOR_INDEX] = ChannelProvider(
    "My Translator",
    imp="._mytranslator",
    key_name="mytranslator_key",
    win="mytranslator"
)
```

#### Step 3: 添加用户配置字段

在 `videotrans/configure/config.py` 的 `AppParams._get_defaults()` 中添加:

```python
"mytranslator_key": "",
"mytranslator_model": "model-v1",
```

#### Step 4: 创建设置窗口

在 `videotrans/winform/` 下创建 `mytranslator.py`,实现 `openwin()` 函数。在 `videotrans/winform/__init__.py` 的 `_module_map` 中注册:

```python
"mytranslator": ".mytranslator",
```

#### Step 5: 可选扩展

- 在 `is_allow_translate()` 中添加语言兼容性检测
- 在 `ui/` 目录下新增界面文件
- 在菜单 `ui/en.py` 中添加对应 Action

---

### 14.2 新增一个 TTS 通道

步骤与翻译通道类似:

1. 创建 `videotrans/tts/_mytts.py`,继承 `BaseTTS`
2. 在 `videotrans/tts/__init__.py` 中分配 ID 并注册 `_ID_NAME_DICT`
3. 如需声音克隆支持,将 ID 加入 `SUPPORT_CLONE` 列表
4. 如需语言跟随角色变化,将 ID 加入 `CHANGE_BY_LANGUAGE` 列表
5. 在 `AppParams._get_defaults()` 中添加对应的 API Key / URL 配置字段
6. 在 `videotrans/winform/` 和 `_module_map` 中注册设置窗口

### 14.3 新增一个识别通道

步骤同翻译/TTS,渠道实现类继承 `BaseRecogn`,必须实现 `.run()` 方法返回 `List[SrtItem]`。

### 14.4 常规约定

- 所有渠道类使用 `@dataclass` + `__post_init__`
- 通过 `get_class(channel_id, "recognition/translator/tts", _ID_NAME_DICT)` 懒加载
- API key 校验依赖 `is_input_api()` 函数 + `_ID_NAME_DICT` 中的 `key_name` / `win` 字段
- 翻译/配音引擎内部并发数由 `settings` 中的对应字段控制

---

> **版本**: v4.03 (VERSION_NUM=403)
> **主页**: https://github.com/jianchang512/pyvideotrans
> **文档**: https://pyvideotrans.com
> **BBS**: https://bbs.pyvideotrans.com


## /docs/cli.md

# pyVideoTrans 命令行(CLI)使用指南

pyVideoTrans 支持通过命令行进行无界面操作,适合服务器部署、批量处理、自动化流水线等场景。

---

## 目录

- [环境要求](#环境要求)
- [基本用法](#基本用法)
- [全局选项](#全局选项)
- [任务类型总览](#任务类型总览)
- [STT — 语音转录](#stt--语音转录)
- [TTS — 文字配音](#tts--文字配音)
- [STS — 字幕翻译](#sts--字幕翻译)
- [VTV — 视频翻译](#vtv--视频翻译)
- [查询工具](#查询工具)
- [完整示例](#完整示例)
- [常见问题](#常见问题)

---

## 环境要求

| 项目 | 要求 |
|------|------|
| Python | 3.10 |
| 包管理 | [uv](https://docs.astral.sh/uv/) |
| FFmpeg | 必须安装并配置环境变量(Windows 打包版已内置) |
| GPU 加速(可选) | NVIDIA 显卡 + CUDA 12.8 + cuDNN 9.11 |

### 启动方式

```bash
# 源码部署
uv run cli.py [参数...]

# Windows 打包版
cli.exe [参数...]
```

> **注意**:Windows 打包版(`cli.exe`)无需安装 Python,直接运行即可。

---

## 基本用法

```bash
uv run cli.py --task <任务类型> --name "<文件路径>" [其他参数]
```

**四种任务类型:**

| 任务 | 说明 | 流水线 |
|------|------|--------|
| `stt` | 语音转录 — 将音频/视频中的人声转为 SRT 字幕 | 预处理 → 语音识别 → 说话人分离 → 输出字幕 |
| `tts` | 文字配音 — 将 SRT 字幕或文本转为语音音频 | 预处理 → 配音 → 音画对齐 → 输出音频 |
| `sts` | 字幕翻译 — 将 SRT 字幕翻译为目标语言 | 预处理 → 翻译 → 输出字幕 |
| `vtv` | 视频翻译 — 全流程:识别 → 翻译 → 配音 → 合成视频 | 预处理 → 识别 → 说话人分离 → 翻译 → 配音 → 对齐 → 二次识别 → 合成视频 |

---

## 全局选项

| 选项 | 说明 | 默认值 |
|------|------|--------|
| `--task {stt,tts,sts,vtv}` | **必选** — 任务类型 | — |
| `--name FILE` | **必选** — 输入文件的绝对路径 | — |
| `--output-dir DIR` | 输出目录 | `<软件目录>/output/<文件名>/` |
| `--list {providers,languages,models}` | 查询可用渠道/语言/模型列表 | — |
| `--log-level {DEBUG,INFO,WARNING,ERROR}` | 日志级别 | `WARNING` |
| `-v, --verbose` | 详细输出(等同 `--log-level INFO`) | 否 |
| `-q, --quiet` | 静默模式,仅输出错误 | 否 |
| `--version` | 显示版本号 | — |
| `-h, --help` | 显示帮助信息 | — |

---

## 任务类型总览

### 各任务必选参数

| 任务 | `--name` | `--voice_role` | `--source_language_code` | `--target_language_code` |
|------|:---:|:---:|:---:|:---:|
| `stt` | ✅ | — | — | — |
| `tts` | ✅ | ✅ | — | — |
| `sts` | ✅ | — | 可选(默认 auto) | ✅ |
| `vtv` | ✅ | 可选(默认 No) | ✅ | ✅ |

### 各任务参数范围

| 参数 | stt | tts | sts | vtv |
|------|:---:|:---:|:---:|:---:|
| `--recogn_type` | ✅ | — | — | ✅ |
| `--detect_language` | ✅ | — | — | ✅ |
| `--model_name` | ✅ | — | — | ✅ |
| `--cuda` | ✅ | — | — | ✅ |
| `--remove_noise` | ✅ | — | — | ✅ |
| `--enable_diariz` | ✅ | — | — | ✅ |
| `--nums_diariz` | ✅ | — | — | ✅ |
| `--rephrase` | ✅ | — | — | ✅ |
| `--fix_punc` | ✅ | — | — | ✅ |
| `--tts_type` | — | ✅ | — | ✅ |
| `--voice_role` | — | ✅ | — | ✅ |
| `--voice_rate` | — | ✅ | — | ✅ |
| `--volume` | — | ✅ | — | ✅ |
| `--pitch` | — | ✅ | — | ✅ |
| `--voice_autorate` | — | ✅ | — | ✅ |
| `--align_sub_audio` | — | ✅ | — | ✅ |
| `--translate_type` | — | — | ✅ | ✅ |
| `--source_language_code` | — | — | ✅ | ✅ |
| `--target_language_code` | — | — | ✅ | ✅ |
| `--video_autorate` | — | — | — | ✅ |
| `--is_separate` | — | — | — | ✅ |
| `--recogn2pass` | — | — | — | ✅ |
| `--subtitle_type` | — | — | — | ✅ |
| `--clear_cache` | — | — | — | ✅ |

---

## STT — 语音转录

将音频或视频中的人声转录为带时间轴的 SRT 字幕文件。

### 参数说明

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--recogn_type` | int | `0` | 语音识别渠道编号(0=faster-whisper, 1=openai-whisper, ...) |
| `--detect_language` | str | `auto` | 音频发音语言(auto=自动检测, zh-cn, en, ja, ...) |
| `--model_name` | str | `tiny` | 模型名称(仅 faster-whisper/openai-whisper 有效) |
| `--cuda` | flag | 否 | 启用 CUDA GPU 加速 |
| `--remove_noise` | flag | 否 | 启用降噪 |
| `--enable_diariz` | flag | 否 | 启用说话人识别 |
| `--nums_diariz` | int | `-1` | 说话人数量(-1=自动检测) |
| `--rephrase` | int | `0` | 重新断句(0=默认, 1=LLM 断句) |
| `--fix_punc` | flag | 否 | 恢复标点符号 |

### 示例

**最简用法 — 使用 faster-whisper 转录中文视频:**

```bash
uv run cli.py --task stt --name "60.mp4"
```

> 默认使用 faster-whisper + tiny 模型,输出 SRT 字幕到 `output/60-mp4/` 目录。

**指定 large-v3 模型 + GPU 加速:**

```bash
uv run cli.py --task stt --name "60.mp4" --recogn_type 0 --model_name large-v3 --cuda
```

**指定源语言为中文 + 降噪:**

```bash
uv run cli.py --task stt --name "60.mp4" --detect_language zh-cn --remove_noise --cuda
```

**使用 openai-whisper 渠道:**

```bash
uv run cli.py --task stt --name "60.mp4" --recogn_type 1 --model_name large-v3 --cuda
```

**启用说话人识别(指定 2 人):**

```bash
uv run cli.py --task stt --name "60.mp4" --enable_diariz --nums_diariz 2 --cuda
```

**启用 LLM 重新断句 + 恢复标点:**

```bash
uv run cli.py --task stt --name "60.mp4" --rephrase 1 --fix_punc --cuda
```

**自定义输出目录:**

```bash
uv run cli.py --task stt --name "60.mp4" --output-dir "D:/my_output" --cuda
```

---

## TTS — 文字配音

将 SRT 字幕文件或纯文本文件转换为语音音频。

### 参数说明

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--tts_type` | int | `0` | 配音渠道编号(0=Edge-TTS, ...) |
| `--voice_role` | str | **必选** | 音色名称 |
| `--voice_rate` | str | `+0%` | 语速(如 `+20%` 加速, `-10%` 减速) |
| `--volume` | str | `+0%` | 音量(如 `+50%` 增大, `-30%` 减小) |
| `--pitch` | str | `+0Hz` | 音调(如 `+10Hz` 变尖锐, `-5Hz` 变低沉) |
| `--voice_autorate` | flag | 否 | 自动加速音频以对齐字幕时间轴 |
| `--align_sub_audio` | flag | 否 | 强制修改字幕时间轴以对齐音频 |
| `--target_language_code` | str | `None` | 目标语言代码 |

### 示例

**最简用法 — 使用 Edge-TTS 为中文字幕配音:**

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "zh-CN-YunyangNeural"
```

> 使用微软免费 Edge-TTS 的云扬(男声)为中文字幕生成配音音频。

**英文配音(从中文翻译后配音):**

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "en-US-GuyNeural" --target_language_code en
```

**调整语速和音量:**

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "zh-CN-YunyangNeural" --voice_rate=+20% --volume=+10%
```

**调整音调(变低沉):**

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "zh-CN-YunyangNeural" --pitch=-5Hz
```

**启用自动加速对齐:**

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "zh-CN-YunyangNeural" --voice_autorate
```

**使用其他 TTS 渠道(如 OpenAI TTS,渠道编号需通过 `--list providers` 查看):**

```bash
uv run cli.py --task tts --name "zw.srt" --tts_type <渠道编号> --voice_role "alloy"
```

### 常用 Edge-TTS 音色

| 音色名称 | 性别 | 语言 | 说明 |
|----------|------|------|------|
| `zh-CN-YunyangNeural` | 男 | 中文 | 云扬 — 新闻播报风格 |
| `zh-CN-XiaoxiaoNeural` | 女 | 中文 | 晓晓 — 自然对话 |
| `zh-CN-YunxiNeural` | 男 | 中文 | 云希 — 年轻活泼 |
| `en-US-GuyNeural` | 男 | 英文 | Guy — 自然男声 |
| `en-US-JennyNeural` | 女 | 英文 | Jenny — 自然女声 |
| `en-US-AriaNeural` | 女 | 英文 | Aria — 专业女声 |
| `en-US-EmmaNeural` | 女 | 英文 | Emma — 温暖女声 |
| `en-US-BrianNeural` | 男 | 英文 | Brian — 沉稳男声 |

> 完整音色列表请运行 `uv run cli.py --list providers` 或在软件 GUI 的 TTS 设置中查看。

---

## STS — 字幕翻译

将 SRT 字幕文件从一种语言翻译为另一种语言。

### 参数说明

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--translate_type` | int | `0` | 翻译渠道编号(0=Google, ...) |
| `--source_language_code` | str | `auto` | 源语言代码(auto=自动检测) |
| `--target_language_code` | str | **必选** | 目标语言代码 |

### 示例

**最简用法 — 将中文字幕翻译为英文:**

```bash
uv run cli.py --task sts --name "zw.srt" --target_language_code en
```

> 默认使用 Google 翻译,源语言自动检测。

**指定源语言为中文:**

```bash
uv run cli.py --task sts --name "zw.srt" --source_language_code zh-cn --target_language_code en
```

**使用其他翻译渠道(如 DeepSeek,渠道编号需通过 `--list providers` 查看):**

```bash
uv run cli.py --task sts --name "zw.srt" --translate_type <渠道编号> --target_language_code en
```

**翻译为日文:**

```bash
uv run cli.py --task sts --name "zw.srt" --target_language_code ja
```

**翻译为韩文:**

```bash
uv run cli.py --task sts --name "zw.srt" --target_language_code ko
```

---

## VTV — 视频翻译

全流程视频翻译:语音识别 → 字幕翻译 → 配音 → 音画合成。这是最常用也是最复杂的任务类型。

### 参数说明

VTV 模式包含 STT + TTS + STS 的所有参数,加上以下额外参数:

| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `--source_language_code` | str | **必选** | 源语言代码(不可为 auto) |
| `--target_language_code` | str | **必选** | 目标语言代码 |
| `--voice_role` | str | `No` | 配音角色(`No`=不配音) |
| `--video_autorate` | flag | 否 | 自动慢速视频以对齐配音 |
| `--is_separate` | flag | 否 | 分离人声背景声 |
| `--recogn2pass` | flag | 否 | 二次语音识别(生成更精准字幕) |
| `--subtitle_type` | int | `1` | 字幕类型(0=无, 1=硬字幕, 2=软字幕, 3=硬字幕双语, 4=软字幕双语) |
| `--clear_cache` | flag | 是 | 完成后清理缓存 |
| `--no-clear-cache` | flag | — | 不清理缓存 |

### 示例

**最简用法 — 中文视频翻译为英文(不配音,仅替换字幕):**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en
```

> 默认使用 faster-whisper 识别 + Google 翻译 + 不配音(voice_role=No),嵌入硬字幕。

**完整流程 — 中文视频翻译为英文并配音:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural"
```

> 使用 Edge-TTS 的 Guy 男声为翻译后的英文字幕配音。

**GPU 加速 + 高精度模型:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda --recogn_type 0 --model_name large-v3
```

**分离人声背景声(提高识别和配音质量):**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --is_separate --cuda
```

**双语硬字幕 + 二次识别:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --subtitle_type 3 --recogn2pass --cuda
```

**软字幕(播放器可开关)+ 音频自动加速:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --subtitle_type 2 --voice_autorate --cuda
```

**视频慢速对齐(配音比视频长时放慢视频):**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --video_autorate --cuda
```

**自定义输出目录 + 保留缓存:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --output-dir "D:/translated" --no-clear-cache
```

**翻译为日文并配音:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code ja --voice_role "ja-JP-KeitaNeural" --cuda
```

**翻译为韩文并配音:**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code ko --voice_role "ko-KR-InJoonNeural" --cuda
```

**静默模式运行(仅输出错误):**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" -q
```

**详细日志模式(调试用):**

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" -v
```

---

## 查询工具

### 列出所有可用渠道

```bash
uv run cli.py --list providers
```

输出示例:

```
=== Available Providers ===

--- Speech Recognition (STT) ---
  0 = faster-whisper(本地)
  1 = openai-whisper(本地)
  2 = 字节语音识别大模型极速版
  ...

--- Translation ---
  0 = Google翻译
  1 = 微软翻译
  2 = 百度翻译
  ...

--- Text-to-Speech (TTS) ---
  0 = Edge-TTS
  1 = Azure TTS
  2 = OpenAI TTS
  ...
```

### 列出所有支持的语言

```bash
uv run cli.py --list languages
```

输出示例:

```
=== Available Language Codes ===
  en         English
  zh-cn      简体中文
  zh-tw      繁體中文
  ja         日本語
  ko         한국어
  fr         Français
  de         Deutsch
  es         Español
  ...
```

### 列出 faster-whisper 可用模型

```bash
uv run cli.py --list models
```

输出示例:

```
=== faster-whisper Models ===
  tiny                      Systran/faster-whisper-tiny
  base                      Systran/faster-whisper-base
  small                     Systran/faster-whisper-small
  medium                    Systran/faster-whisper-medium
  large-v3                  Systran/faster-whisper-large-v3
  large-v3-turbo            mobiuslabsgmbh/faster-whisper-large-v3-turbo
  ...
```

---

## 完整示例

以下示例均假设:
- 中文原始视频文件为 `60.mp4`
- 中文字幕文件为 `zw.srt`
- 翻译目标语言为英文
- 配音使用 Edge-TTS 的 `en-US-GuyNeural` 音色
- 其他非必须参数保持默认

### 场景 1:仅语音转录(中文字幕生成)

```bash
uv run cli.py --task stt --name "60.mp4" --detect_language zh-cn --cuda
```

**说明**:将 `60.mp4` 中的中文语音转录为 `zh-cn.srt` 字幕文件,输出到 `output/60-mp4/`。

### 场景 2:仅字幕翻译(中文字幕 → 英文字幕)

```bash
uv run cli.py --task sts --name "zw.srt" --source_language_code zh-cn --target_language_code en
```

**说明**:将 `zw.srt` 翻译为 `en.srt`,输出到 `output/zw-srt/`。

### 场景 3:仅文字配音(为中文字幕生成英文配音)

```bash
uv run cli.py --task tts --name "zw.srt" --voice_role "en-US-GuyNeural" --target_language_code en
```

**说明**:为 `zw.srt` 中的文本生成英文配音 WAV 文件,输出到 `output/zw-srt/`。

### 场景 4:完整视频翻译(中文 → 英文,带配音)

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda
```

**说明**:全流程处理:
1. 识别 `60.mp4` 中的中文语音 → 生成中文字幕
2. 将中文字幕翻译为英文字幕
3. 使用 Edge-TTS Guy 男声生成英文配音
4. 将英文字幕和配音合成到视频中

输出:`output/60-mp4/60.mp4`(翻译后的视频)

### 场景 5:高质量视频翻译(分离人声 + GPU 加速 + 二次识别)

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda --is_separate --recogn2pass --model_name large-v3
```

**说明**:
- `--is_separate`:分离人声和背景声,提高识别和配音质量
- `--recogn2pass`:配音完成后再次识别,生成更精准的字幕时间轴
- `--model_name large-v3`:使用最高精度的识别模型
- `--cuda`:GPU 加速

### 场景 6:双语字幕视频

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --subtitle_type 3 --cuda
```

**说明**:`--subtitle_type 3` 生成硬字幕双语(中英同时显示)。

### 场景 7:视频翻译到日文

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code ja --voice_role "ja-JP-KeitaNeural" --cuda
```

### 场景 8:批量处理多个文件(Shell 循环)

```bash
# Bash / Git Bash
for f in *.mp4; do
  uv run cli.py --task vtv --name "$f" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda
done
```

```powershell
# PowerShell
Get-ChildItem *.mp4 | ForEach-Object {
  uv run cli.py --task vtv --name $_.FullName --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda
}
```

---

## 常见问题

### Q: 如何查看所有可用的配音渠道和音色?

```bash
uv run cli.py --list providers
```

或者在软件 GUI 中,选择配音渠道后查看音色下拉列表。

### Q: 如何查看所有支持的语言代码?

```bash
uv run cli.py --list languages
```

### Q: 路径中包含空格怎么办?

使用英文双引号包裹路径:

```bash
uv run cli.py --task vtv --name "D:/my videos/60.mp4" --source_language_code zh-cn --target_language_code en
```

### Q: 如何启用 GPU 加速?

添加 `--cuda` 参数:

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --cuda
```

> 前提:已安装 NVIDIA 显卡驱动、CUDA 12.8+、cuDNN 9.11+。

### Q: 如何使用本地大模型翻译?

需要先在本地部署兼容 OpenAI 接口的大模型(如 Ollama),然后:

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --translate_type <兼容AI渠道编号> --cuda
```

> 翻译渠道的 API 地址需要在软件 GUI 的翻译设置中预先配置。

### Q: 处理速度太慢怎么办?

1. **启用 GPU 加速**:添加 `--cuda`
2. **使用小模型**:`--model_name tiny`(速度快但精度低)
3. **跳过人声分离**:不加 `--is_separate`
4. **跳过二次识别**:不加 `--recogn2pass`

### Q: 翻译后的字幕和声音不同步怎么办?

添加 `--voice_autorate`(自动加速音频)或 `--video_autorate`(自动慢速视频):

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --voice_autorate --cuda
```

### Q: 如何只翻译不配音?

不指定 `--voice_role` 或指定为 `No`:

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en
```

### Q: 如何查看详细的处理日志?

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" -v
```

或者指定日志级别:

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --log-level DEBUG
```

### Q: 如何使用软字幕(播放器可开关)?

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --subtitle_type 2 --cuda
```

> `--subtitle_type 2` = 软字幕,`--subtitle_type 1` = 硬字幕(默认)。

### Q: 如何保留处理缓存以便调试?

```bash
uv run cli.py --task vtv --name "60.mp4" --source_language_code zh-cn --target_language_code en --voice_role "en-US-GuyNeural" --no-clear-cache
```

---

## 退出码

| 退出码 | 含义 |
|--------|------|
| `0` | 任务成功完成 |
| `1` | 任务执行出错 |
| `130` | 用户中断(Ctrl+C) |
| `2` | 参数错误(argparse 自动退出) |

---

## 相关文档

- [使用入门](https://pyvideotrans.com/getstart)
- [CLI 命令行模式文档](https://pyvideotrans.com/cli)
- [语音识别渠道说明](https://pyvideotrans.com/yuyinshibiequdao)
- [翻译渠道说明](https://pyvideotrans.com/fanyiqudao)
- [配音渠道说明](https://pyvideotrans.com/peiyinqudao)
- [常见问题 FAQ](https://pyvideotrans.com/faq)
- [技术架构](https://pyvideotrans.com/yuanli)


## /docs/faq.md

---
title: 常见错误与解决方法
date: 2024-01-22 14:33:00
description: 菜单栏--帮助/关于 中有很多链接,比如模型下载地址、CUDA配置等,遇到问题时可尝试点开使用
---

# pyVideoTrans 常见问题与解决方案

为了帮助您更好地使用 `pyVideoTrans`,我们整理了以下常见问题及其解决方案。

在 `菜单栏--帮助/关于` 中有很多链接,比如模型下载地址、CUDA配置等,遇到问题时可尝试点开使用。

![image.png](https://pvtr2.pyvideotrans.com/images/c02de778f70546ba9a1eb2772ee9f18b~tplv-73owjymdk6-jj-mark_0_0_0_0_q75.webp)

> **如何查看日志**:软件根目录下的 `logs/` 文件夹有按日期命名的 `.log` 日志文件。报错时可复制日志底部约 30 行内容寻求帮助。
>
> **如何恢复出厂设置**:删除 `videotrans/` 目录下的 `cfg.json`、`params.json`、`codec.json`、`ass.json` 四个文件,重启软件即可。

---

## 第一部分:安装与启动问题

### 1. 双击 `sp.exe` 后,软件无法打开或长时间没有反应?

这通常是正常现象,请不要着急。

*   **原因**:本软件基于 `PySide6` 开发,主界面包含较多组件,首次加载时需要初始化,这会消耗一些时间。根据您的电脑性能,启动时间可能在 **5秒到2分钟** 不等。
*   **解决方案**:
    1.  **耐心等待**:双击后请耐心等待一段时间。
    2.  **检查安全软件**:部分杀毒软件或安全卫士可能会阻止程序启动,请尝试暂时关闭它们,或将本软件添加到信任/白名单中。
    3.  **检查文件路径**:确保软件存放的路径**只包含英文和数字**,不应有中文、空格或特殊符号。例如,`D:\pyVideoTrans` 是一个好的路径,而 `D:\program file\视频 工具` 则可能导致问题。
    4.  **升级包问题**:如果您是覆盖了升级包后无法启动,说明操作有误。请重新下载完整的软件包,解压后再覆盖新版升级包。

### 2. 启动时提示缺少 `python310.dll` 文件怎么办?

这个问题说明您只下载了升级补丁包,而没有下载主程序。

*   **解决方案**:
    1.  请先前往官网下载 **完整软件包**。
    2.  解压完整包到指定目录。
    3.  之后再下载最新的升级补丁包,覆盖到完整包的目录中即可。

### 3. 软件需要安装吗?

本软件是绿色版,**无需安装**。下载完整包后解压,双击 `sp.exe` 即可直接运行。

### 4. 为什么杀毒软件会报病毒或拦截?

*   **原因**:本软件使用 `PyInstaller` 工具打包,并且没有进行商业数字签名认证。一些安全软件会基于此启动风险预警,这属于**常见误报**。
*   **解决方案**:
    1.  **添加信任**:将本软件添加到您杀毒软件的信任区或白名单中。
    2.  **源码运行**:如果您是开发者,也可以选择从源代码直接部署运行,以完全避免此问题。

### 5. 软件支持 Windows 7 系统吗?

**不支持**。软件依赖的许多核心组件(如 PyTorch、PySide6)已不再支持 Windows 7 系统。请使用 Windows 10 或 Windows 11。

### 6. macOS / Linux 如何部署源码?

*   **前置依赖**:
    *   Python 3.10
    *   FFmpeg(`brew install ffmpeg` / `apt install ffmpeg`)
    *   uv 包管理器
    *   libsndfile
*   **部署步骤**:
    ```bash
    git clone https://github.com/jianchang512/pyvideotrans
    cd pyvideotrans
    uv sync
    uv run sp.py
    ```
*   **可选依赖**:`uv sync --all-extra` 安装所有可选渠道(qwen-tts, qwen-asr, moss-tts, chatterbox)

### 7. 源码部署后启动报错怎么办?

常见原因及解决方案:
*   **FFmpeg 未安装**:确保系统已安装 FFmpeg 且配置了环境变量
*   **依赖缺失**:运行 `uv sync` 重新安装依赖
*   **Python 版本不对**:必须使用 Python 3.10(`.python-version` 文件已指定)

---

## 第二部分:核心功能与设置

### 8. 如何提升语音识别的准确率?

识别准确率主要取决于您选择的模型大小和设置。

*   **模型选择**:在 "faster" 或 "openai" 模式下,模型越大,准确率越高,但处理速度越慢、资源消耗也越大。
    *   `tiny`: 体积最小,速度最快,但准确率较低。
    *   `base` / `small` / `medium`: 效果与资源消耗居中,是常用的选项。
    *   `large-v3`: 体积最大,效果最好,对硬件要求也最高(需要 8GB+ 显存)。
*   **优化设置**:点击 `菜单--工具--高级选项`

找到 `faster/openai语音识别调整` 部分,进行如下修改:

- **语音阈值** 设为 `0.5`
- **最短持续时间/毫秒** 设为 `3000`
- **最大语音持续时间/秒** 设为 `6`
- **静音分隔毫秒** 设为 `140`
- **热词**:如果视频中有专有名词,可以在此填写,以逗号分隔

*   **降噪处理**:如果视频有背景音乐或噪声,点击 `设置更多参数` 选中 `分离人声背景声`,可以显著提升识别效果。

### 9. 为什么处理后的视频清晰度/质量降低了?

任何涉及**重新编码**的操作都会不可避免地导致视频质量损失。如果您希望最大程度地保持原始画质,请确保满足以下所有条件:

1.  **原始视频格式**:使用兼容性最好的 **H.264 (libx264) 编码的 MP4 文件**。
2.  **禁用慢速处理**:在功能选项中,不要勾选"视频自动慢速"。
3.  **不嵌入硬字幕**:可以选择不嵌入字幕,或只嵌入**软字幕**。硬字幕会强制重新编码整个视频。
4.  **高级选项-视频输出质量控制**:数字默认23,可以降低到18或更低(最低0),越低输出视频质量越高,但尺寸也越大
5.  **高级选项-输出视频压缩率**:默认是`fast`,可用选择slow或slower,质量会更高,但输出耗时将增加
6.  **高级选项-264/265编码**:默认是`264`,可选265,输出视频质量更高

### 10. 为什么输出视频超级大?

1. 修改**高级选项-视频输出质量控制** 为 25-51 越大输出视频尺寸越小,但质量也随之降低
2. 高级选项-264/265编码:选择265,同质量下 265 尺寸更小

### 11. 如何配置网络代理?

部分翻译或配音服务(如 Google、OpenAI、Gemini)在国内无法直接访问,需要通过网络代理。

*   **设置方法**:在主界面的"网络代理地址"文本框中,填入您的代理服务地址。
*   **格式要求**:通常是 `http://127.0.0.1:10808` 这样的格式(端口号需根据您的代理客户端设置填写)。
*   **重要提示**:如果您不了解代理或没有可用的代理服务,**请将此项留空**。错误的设置将导致报错。
*   **国内 API 不需要代理**:百度翻译、腾讯翻译、阿里翻译、DeepSeek、智谱AI、字节火山等国内 API 默认不走代理。
*   **本地服务不需要代理**:GPT-SoVITS、ChatTTS、F5-TTS 等本地服务自动绕过代理。

### 12. 如何自定义字幕的字体、颜色和样式?

点击主界面中 -> 设置更多参数 -> **修改硬字幕**

---

## 第三部分:语音识别问题

### 13. 识别结果为空或乱码

*   **原因**:可能语言选择错误、视频无有效人声、或显存不足
*   **解决方案**:
    1.  检查"原始语言"是否选择正确(不要过度依赖 Auto)
    2.  检查视频是否有背景音乐干扰(尝试开启降噪)
    3.  显存不足:降低 `beam_size`,改用 `int8` 量化,或使用 `small` 模型
    4.  尝试更换识别渠道(如从 faster-whisper 换成 openai-whisper)

### 14. 识别速度非常慢

*   **原因**:使用了大型模型但未启用 GPU 加速
*   **解决方案**:
    1.  **启用 CUDA 加速**:确保已安装 CUDA 12.8+ 和 cuDNN 9.x,勾选 `CUDA加速`
    2.  **使用小模型**:将 `large-v3` 换成 `medium` 或 `small`
    3.  **CPU 模式优化**:在高级选项中将 `计算数据类型` 改为 `int8`

### 15. 提示显存或内存不足(`Unable to allocate`、`CUDA out of memory`)

*   **原因**:模型太大或显存被其他程序占用
*   **解决方案(按推荐顺序尝试)**:
    1.  **使用更小的模型**:将识别模型从 `large-v3` 更换为 `medium`、`small` 或 `base`。`large-v3` 模型最低需要 8GB 显存。
    2.  **调整高级设置**:在菜单栏 **`工具/选项` -> `高级选项`** 中进行如下修改:
        *   `CUDA数据类型`: 将 `float32` 改为 `float16` 或 `int8`
        *   `beam_size`: 将 `5` 改为 `1`
        *   `best_of`: 将 `5` 改为 `1`
        *   `上下文`: 将 `true` 改为 `false`
    3.  **检查多显卡**:如果有多个可用显卡,检查第一块显卡可用显存是否过小。软件默认使用第一块显卡,升级到 v3.98-317 以上版本会自动选择显存最大的显卡。

### 16. 说话人识别不准确

*   **原因**:说话人分离模型对某些场景(如多人同时说话、背景噪声大)效果有限
*   **解决方案**:
    1.  在 `设置更多参数` 中勾选 `识别说话人` 并指定人数
    2.  在高级选项中切换说话人模型(内置、阿里CAM++、pyannote)
    3.  使用 pyannote 模型需要在 HuggingFace 上申请 token 并同意授权协议

### 17. LLM 重新断句后结果更差

*   **原因**:本地小模型(如 7B)智能不足,或提示词过于复杂
*   **解决方案**:
    1.  使用更强的在线模型(DeepSeek-V3、GPT-4o 等)
    2.  精简提示词(在 `videotrans/prompts/recharge/recharge-llm.txt` 中修改)
    3.  使用 `clone` 角色克隆原音色时,**不建议**使用 LLM 重新断句

### 18. 配音后字幕和声音不同步

这是翻译配音中的常见现象,源于语言间的时长差异。

*   **原因**:不同语言表达同一意思时,音节数和语法结构不同,导致配音时长与原始字幕时长不一致。例如,一句2秒的中文,翻译成英文后配音时长可能变为3-4秒。
*   **解决方案**:
    1.  **启用音频加速**:勾选 `音频加速`,自动将过长的配音加速到匹配字幕时长
    2.  **启用视频慢速**:勾选 `视频慢速`,放慢视频画面以匹配配音时长
    3.  **两者同时启用**:当倍率 > 1.2x 时,音频加速和视频慢速各负担一半时间差
    4.  **调整语速**:设置 `配音语速` 值(如 `+10%`)加快整体配音速度
    5.  **使用二次识别**:勾选 `二次识别`,在配音完成后再次识别生成更精准的字幕时间轴

> 详细原理请参考 [音频视频时间轴对齐原理说明](Synchronize.md)

### 19. 二次识别是什么?什么时候需要?

二次识别是在配音完成后,对生成的配音音频再次进行语音识别,生成时间轴更精准、字数更简短的字幕。

*   **适用场景**:选择了 `嵌入单字幕`(硬字幕或软字幕),且需要字幕和配音精确对齐
*   **设置方法**:勾选 `二次识别`,在高级选项中设置二次识别的最长/最短语音持续时间
*   **注意**:二次识别需要额外的处理时间

---

## 第四部分:翻译问题

### 20. 翻译结果有空白行或包含提示词

*   **原因**:本地小模型智能不足,或 AI 合并了字幕行
*   **解决方案**:
    1.  本地小模型(如 7B)智能不足,建议改用 DeepSeek/GPT-4 等在线模型
    2.  取消"发送完整字幕"选项,改为按行翻译
    3.  设置 `trans_thread=1` 降低并发
    4.  [具体原理和解决方法点击查看](/faq17)

### 21. AI 翻译触发安全限制被过滤

*   **错误信息**:`内容触发AI风控被过滤`
*   **原因**:翻译内容被 AI 服务的安全系统拦截
*   **解决方案**:
    1.  手动编辑字幕,移除可能触发风控的内容
    2.  更换翻译渠道(如从 OpenAI 换成 DeepSeek)

### 22. 翻译结果与原文不对应(字幕行错位)

*   **原因**:AI 翻译时合并了字幕行,导致行号错位
*   **解决方案**:
    1.  在高级选项中取消勾选"发送完整字幕"
    2.  将翻译并发数设为 1
    3.  使用支持大上下文的在线 AI 模型

### 23. 翻译缓存导致结果异常

*   **原因**:翻译结果被缓存,修改提示词或翻译渠道后未生效
*   **解决方案**:
    1.  勾选主界面的 `清理已生成` 选项
    2.  或手动删除 `tmp/translate_cache/` 目录下的缓存文件

---

## 第五部分:配音问题

### 24. Edge-TTS 报错 403 或生成静音

*   **原因**:微软限流,短时间内请求过多
*   **解决方案**:
    1.  在"高级选项"中将"同时配音线程数"设为 1
    2.  将"配音后暂停秒数"设为 5-10 秒
    3.  如果使用了代理,Edge-TTS 可能因代理问题失败。在软件根目录创建 `edgetts-noproxy.txt` 空文件可强制绕过代理

### 25. F5-TTS / CosyVoice / GPT-SoVITS 无法连接

*   **原因**:本地 TTS 服务未启动或地址配置错误
*   **解决方案**:
    1.  确保外部 TTS 服务的终端窗口未关闭
    2.  检查 API 地址是否正确(注意端口号)
    3.  GPT-SoVITS 需启动 `api.py` 或 `api_v2.py`,不能使用网页版 7860 端口
    4.  如果填写了 `0.0.0.0` 作为地址,改为 `127.0.0.1`

### 26. GPT-SoVITS 报错 `{"detail":"Not Found"}`

*   **原因**:API 版本不匹配或端口错误
*   **解决方案**:
    1.  检查启动的是 `api.py` 还是 `api_v2.py`,在软件中勾选对应的 `api_v2?` 选项
    2.  确保填写的是 API 地址(默认 9880),而非网页版地址(7860)

### 27. Index-TTS 报错 `Value: 'Same as the voice reference' is not in the list`

*   **原因**:Index-TTS 内部多语言翻译不一致的 Bug
*   **解决方案**:打开 Index-TTS 项目根目录的 `webui.py`,将 `i18n("与音色参考音频相同")` 替换为 `Same as the voice reference`

### 28. Azure-TTS 报错 `Could not find module Microsoft.CognitiveServices.Speech.core.dll`

*   **原因**:缺少微软 VC++ 运行库
*   **解决方案**:
    1.  如果是下载的补丁包,请重新下载完整包
    2.  如果已是完整包,安装 [微软 VC++ 运行时集合包](https://aka.ms/vs/17/release/vc_redist.x64.exe) 后重启电脑

### 29. 配音后声音有机械感或杂音

*   **原因**:音频加速倍率过高(> 3x),或参考音频质量差
*   **解决方案**:
    1.  启用视频慢速,与音频加速协同分担时间差
    2.  提升参考音频质量:使用清晰的 5-10 秒单人声 WAV 文件
    3.  勾选 `分离人声背景声`,去除背景噪声

---

## 第六部分:声音克隆问题

### 30. 使用 `clone` 角色配音失败或音质差

*   **原因**:参考音频时长不在 3-10 秒范围内,或字幕时间轴被 LLM 重新断句打乱
*   **解决方案**:
    1.  **禁止使用 LLM 重新断句**:LLM 重新断句会打乱时间轴,导致参考音频截取错位
    2.  **强制控制字幕时长**:在 `高级选项 -> 语音识别参数` 中,将 `最长语音持续秒数` 设为 6-10,`最短语音持续毫秒` 设为 3000-4000
    3.  勾选 `合并过短字幕到邻近` 和 `Whisper预分割音频`
    4.  使用 `OmniVoice-TTS` 渠道,对短参考音频兼容性更好
    5.  勾选 `分离人声背景声`,提升参考音频质量

### 31. 如何使用自定义参考音频?

1.  录制或截取一段 5-10 秒的 WAV 格式音频(单人声、无背景噪声)
2.  将音频复制到软件目录下的 `f5-tts` 文件夹
3.  打开 `菜单 -> TTS 设置 -> 设置参考音频`,填写 `文件名.wav#音频中的说话文本`
4.  在主界面配音角色下拉框中选择该文件名

> **注意**:GPT-SoVITS 的参考音频需要放在 GPT-SoVITS 软件的根目录下,而非 `f5-tts` 文件夹。

---

## 第七部分:视频合成与输出问题

### 32. 执行过程中报错 `ffprobe exec error` 或 `ffmpeg` 相关异常

*   **原因**:文件路径过长或含有特殊符号
*   **解决方案**:
    1.  将视频文件移动到更浅的目录(如 `D:\videos`)
    2.  重命名为简短的英文或数字名称
    3.  删除文件名中的特殊符号(`?*`、表情符号等)

### 33. 软件提示视频"不含音轨"

*   **可能原因 1**:视频确实没有声音(从某些网站下载时画面和声音分离)
*   **可能原因 2**:视频编码格式不支持(如 AV1)
*   **可能原因 3**:背景噪音过大,人声被掩盖
*   **解决方案**:
    1.  用播放器本地播放确认是否有声音
    2.  尝试先将视频转换为标准 H.264/MP4 格式
    3.  开启降噪或人声分离功能

### 34. 如何输出无损视频?

当满足以下所有条件时,视频将无损输出(不重新编码):
1.  原始视频编码为 `mp4/h.264/yuv420p`
2.  高级选项中 `264/265编码` 选择 `264`
3.  未启用 `视频慢速`
4.  未嵌入 `硬字幕`(软字幕不影响)

> 注意:若配音后时长大于视频原时长,超出部分会被截断。

### 35. 处理后出现声音、字幕、画面不同步

这是语言翻译中的正常现象。

*   **原因**:不同语言表达同一个意思时,句子的长度和音节数均不同,发音时长必然发生变化。
*   **解决方案**:
    1.  启用 `音频加速` 和/或 `视频慢速`
    2.  设置 `配音语速`(如 `+10%`)加快整体速度
    3.  启用 `二次识别` 生成更精准的字幕时间轴
    4.  详细原理请参考 [音频视频时间轴对齐原理说明](Synchronize.md)

### 36. 总是提示显存不足 (例如 `Unable to allocate` 错误)

这个错误意味着您的显卡没有足够的显存或内存来执行当前任务。

*   **解决方案(按推荐顺序尝试)**:
    1.  **使用更小的模型**:将识别模型从 `large-v3` 更换为 `medium`、`small` 或 `base`
    2.  **调整高级设置**:
        *   `CUDA数据类型`: 将 `float32` 改为 `float16` 或 `int8`
        *   `beam_size`: 将 `5` 改为 `1`
        *   `best_of`: 将 `5` 改为 `1`
        *   `上下文`: 将 `true` 改为 `false`

### 37. 已经安装了 CUDA,为什么软件还是无法使用 GPU 加速?

请检查以下可能的原因:

*   **CUDA 版本不兼容**:本软件要求 CUDA 12.8 及以上版本
*   **显卡驱动过旧**:请更新您的 NVIDIA 显卡驱动到最新版本
*   **缺少 cuDNN**:确保已安装 cuDNN 9.x 并配置了环境变量
*   **硬件不兼容**:GPU 加速仅支持 NVIDIA 显卡(N卡)。AMD 或 Intel 显卡无法使用 CUDA
*   **环境变量未配置**:检查系统环境变量中是否包含 CUDA 的 `bin` 和 `lib` 目录

### 38. GPU 使用率很低,正常吗?

**正常**。软件的工作流程是:`语音识别 -> 文字翻译 -> 文本配音 -> 视频合成`。

只有在第一步 **"语音识别"** 阶段,才会大量使用 GPU 进行运算。其他阶段(如翻译、合成)主要依赖 CPU,因此 GPU 在大部分时间处于低负载状态是符合预期的。

### 39. 处理几个视频后,发现硬盘空间被占满?

这通常是由于启用了"视频慢速"功能并产生了大量临时文件。

*   **原因**:该功能会将视频按字幕切割成许多小片段,并对每个片段进行处理,这会产生远超原视频体积的缓存文件。
*   **解决方案**:
    1.  **手动清理**:处理完成后,手动删除软件根目录下的 **`tmp/` 文件夹**内的所有内容
    2.  **自动清理**:正常关闭软件时,程序会自动清理这些缓存

### 40. 反复处理同一个视频,为什么识别结果和字幕总是不变?

*   **原因**:软件默认启用了缓存机制,如果检测到某个视频已经生成过字幕文件,会直接使用缓存结果
*   **解决方案**:在软件主界面的左上角,勾选 **`清理已生成`** 复选框

![](https://pvtr2.pyvideotrans.com/1760281358634_image.png)

---

## 第八部分:批量处理问题

### 41. 批量翻译视频时总是会卡住

默认批量任务时,会将每个任务分为多个阶段,同时交叉并行处理,太多任务时可能导致资源耗尽。

*   **解决方案**:选中 **高级选项--批量翻译时强制串行**,将执行方式改为串行处理

### 42. 批量处理时如何控制并发数量

在 `高级选项 -> 通用设置` 中:
*   `CPU同时任务数`:最大 CPU 同时任务数,不超过 CPU 核数
*   `GPU同时任务数`:GPU 任务同时执行数量,除非多卡或单卡显存 > 24G,否则设为 1
*   `批量翻译视频时每批数量`:设为 1 可逐个处理,设为 0 则全部同时处理

---

## 第九部分:高级选项详解

### 43. 音频加速和视频慢速的区别?

| 选项 | 效果 | 适用场景 |
|------|------|---------|
| **音频加速** | 加速配音以匹配字幕时长,音质可能略有损失 | 配音比字幕长 1-2 倍 |
| **视频慢速** | 慢放视频以匹配配音时长,画面可能略卡 | 配音比字幕长 2 倍以上 |
| **两者同时** | 各负担一半时间差,效果最佳 | 配音远长于字幕 |

### 44. `发送完整字幕` 有什么作用?

选中后,AI 翻译时会附带行号和时间轴发给 AI,翻译质量更好但可能合并行。建议:
*   使用在线大模型(DeepSeek、GPT-4o)时**选中**
*   使用本地小模型时**取消选中**

### 45. `二次识别` 与 `LLM重新断句` 的区别?

| 选项 | 时机 | 作用 |
|------|------|------|
| **LLM重新断句** | 语音识别后 | AI 修正错别字、重新切分长文本 |
| **二次识别** | 配音完成后 | 对配音音频再次识别,生成更精准的时间轴 |

> 使用 `clone` 角色时,**不建议**使用 LLM 重新断句。

### 46. 嵌入字幕类型如何选择?

| 类型 | 说明 | 适用场景 |
|------|------|---------|
| 不嵌入字幕 | 只替换声音,不添加字幕 | 仅需配音 |
| 嵌入硬字幕 | 字幕永久烧录到画面,无法关闭 | 任何播放器都能显示 |
| 嵌入软字幕 | 字幕作为独立轨道,播放器可开关 | 需要灵活控制字幕显示 |
| 嵌入硬字幕(双) | 中英双语硬字幕 | 需要双语对照 |
| 嵌入软字幕(双) | 中英双语软字幕 | 需要双语对照且可关闭 |

---

## 第十部分:文件与路径问题

### 47. 输入文件路径有什么要求?

1.  **路径长度**:Windows 命令行有 260 字符限制,文件路径应尽量简短
2.  **特殊符号**:文件名中不应包含 `?*`、表情符号等特殊符号
3.  **中文路径**:虽然支持,但建议使用英文路径以避免兼容性问题
4.  **空格**:路径中可以有空格,但建议避免

### 48. 输出文件保存在哪里?

*   **默认位置**:原视频目录下的 `_video_out/` 文件夹
*   **独立功能输出**:批量转录、配音、翻译 SRT 等功能输出到 `output/` 目录
*   **自定义输出**:可在主界面设置输出目录

### 49. 如何导入已有的 SRT 字幕?

1.  在视频文件同级目录下创建 `_video_out/` 文件夹
2.  在其中创建视频同名子文件夹(如 `myvideo-mp4`,必须带格式后缀)
3.  将字幕文件复制到子文件夹,重命名为 `zh-cn.srt`(源语言)和 `en.srt`(目标语言)
4.  导入视频执行翻译,软件会自动跳过 ASR 和翻译阶段

---

## 第十一部分:CLI 命令行问题

### 50. CLI 基本用法

```bash
uv run cli.py --task <任务类型> --name "<文件路径>" [其他参数]
```

任务类型:`stt`(语音转录)、`tts`(文字配音)、`sts`(字幕翻译)、`vtv`(视频翻译)

### 51. 如何查看可用的渠道和语言?

```bash
uv run cli.py --list providers    # 查看所有渠道
uv run cli.py --list languages    # 查看所有语言代码
uv run cli.py --list models       # 查看 faster-whisper 模型
```

### 52. CLI 常见报错

*   **`--name is required`**:未指定输入文件
*   **`File not found`**:文件路径错误或文件不存在
*   **`--voice_role is required`**:TTS 模式下必须指定配音角色
*   **`--target_language_code is required`**:STS/VTV 模式下必须指定目标语言

---

## 第十二部分:综合信息

### 53. 软件是否支持 Docker 部署?

目前**不支持**。

### 54. 能否识别视频画面中的硬字幕(OCR 功能)?

**不能**。本软件的原理是分析视频中的**音频轨道**,识别出人类的语音并转换为文字。它不具备图像文字识别(OCR)功能。
[若有需要,可以点击查看另一个项目,提取视频中硬字幕](https://pyvideotrans.com/ocrsp)

### 55. 我可以添加新的语言支持吗?

**[可以新增目标语言,具体查看](https://pyvideotrans.com/newlanguage)**

### 56. 软件是否收费?可以商用吗?

*   **费用**:本项目是一个**免费且开源**的软件,您可以免费使用所有功能。请注意,如果您使用第三方的翻译或TTS或语音转录接口,这些服务商可能会收取费用,但这与本软件无关。
*   **商用**:个人和公司均可**自由使用**本软件。但如果您希望将本项目的代码集成到您自己的商业产品中,则必须遵守 **GPL-v3 开源协议**。此外某些渠道使用的模型或在线API可能有他们自己的协议要求,是否允许商用,请咨询所使用的渠道对应的平台。

### 57. 是否提供人工客服?

没有。本项目为个人开发的免费开源软件,没有盈利,因此无法配备专门的人工客服团队。如果您遇到问题,请先仔细阅读本 FAQ。
或你也可以选择软件右下角微信二维码打赏,留言你的微信号,获取有偿技术支持。

### 58. 从哪里下载软件和模型?

*   **软件下载地址**:[pyvideotrans.com/downpackage](https://pyvideotrans.com/downpackage)
*   **源码仓库地址**:[github.com/jianchang512/pyvideotrans](https://github.com/jianchang512/pyvideotrans)

### 59. 报错与日志

*   **日志位置**:软件根目录下的 `logs` 文件夹有当前年月日命名的 log 格式日志文件
*   **反馈方式**:报错时点击弹窗的"报告错误"可自动提交至官方论坛;或复制日志底部 30 行内容询问 AI

### 60. 新版本为什么在发音语言列表中没有了"自动检测"?

在 "批量语音转字幕" 功能面板中可以选择"自动检测",在"翻译视频或音频"功能中去掉了自动检测。因为视频翻译后续工作如字幕翻译、配音(涉及参考音频)等某些渠道需要明确指定原始语言,否则会报错。如果你仅仅想转录语音为字幕,可单独使用左侧面板中的"批量语音转字幕"功能。

---

## 快速问题排查表

| 问题 | 可能原因 | 解决方案 |
|------|---------|---------|
| 软件无法启动 | 杀毒软件拦截 / 路径问题 | 添加信任白名单 / 移至英文路径 |
| 缺少 python310.dll | 只下载了补丁包 | 下载完整包再覆盖补丁 |
| 识别结果为空 | 语言选择错误 / 无有效人声 | 正确选择语言 / 开启降噪 |
| 显存不足 | 模型太大 | 换小模型 / 改 int8 / 降 beam_size |
| GPU 未启用 | CUDA 未安装 / 驱动过旧 | 安装 CUDA 12.8+ / 更新驱动 |
| 翻译有空白行 | AI 合并了字幕行 | 取消"发送完整字幕" / 用在线模型 |
| Edge-TTS 403 | 微软限流 | 降并发 / 加暂停秒数 |
| 声音字幕不同步 | 语言时长差异 | 启用音频加速 / 视频慢速 |
| ffprobe 报错 | 路径过长或特殊符号 | 简化文件名 / 移至浅层目录 |
| 硬盘空间占满 | 视频慢速产生大量临时文件 | 清理 tmp/ 文件夹 |
| clone 配音差 | 参考音频时长不当 | 控制 3-10 秒 / 禁用 LLM 断句 |
| GPT-SoVITS 404 | API 版本不匹配 | 检查 api.py vs api_v2.py |


## /docs/googlecloud_tts.md

# Google Cloud Text-to-Speech Integration

## Overview
This integration adds Google Cloud Text-to-Speech as a new TTS provider in VideoTrans. It offers high-quality voice synthesis with support for multiple languages and voices.

## Features
- Support for 16+ languages including:
  - Portuguese (Brazil)
  - English (US/GB)
  - Spanish
  - French
  - German
  - Italian
  - Japanese
  - Korean
  - Chinese
  - Russian
  - Hindi
  - Arabic
  - Turkish
  - Thai
  - Vietnamese
  - Indonesian
- Multiple voice options per language
- Adjustable speaking rate and pitch
- Support for multiple audio formats (MP3, LINEAR16, OGG_OPUS)
- User-friendly configuration interface

## Requirements
1. Python package:
   ```bash
   pip install google-cloud-texttospeech>=2.14.0
   ```

2. Google Cloud Project:
   - Create a project in [Google Cloud Console](https://console.cloud.google.com)
   - Enable the Cloud Text-to-Speech API
   - Create a service account and download the credentials JSON file

## Configuration
1. In VideoTrans, go to Settings > Google Cloud TTS
2. Configure the following settings:
   - **Credential JSON**: Path to your Google Cloud service account credentials file
   - **Language**: Select the target language (e.g., "pt-BR" for Brazilian Portuguese)
   - **Voice**: Choose from available voices for the selected language
   - **Audio Encoding**: Select output format (MP3, LINEAR16, or OGG_OPUS)

## Usage
1. Select "Google Cloud TTS" as your TTS provider
2. Choose your target language
3. Select a voice from the available options
4. Adjust speaking rate and pitch if needed
5. Proceed with your video translation as usual

## Troubleshooting
Common issues and solutions:

1. **"Credentials not found"**
   - Verify the path to your credentials JSON file
   - Ensure the file has proper read permissions

2. **"No voices available"**
   - Check if your credentials have Text-to-Speech API access
   - Verify if the selected language is supported
   - Check the logs for detailed error messages

3. **"Invalid speaking rate"**
   - Speaking rate should be a percentage (e.g., "+10%", "-5%")
   - Default is "+0%"

4. **"Invalid pitch"**
   - Pitch should be in Hz (e.g., "+2Hz", "-1Hz")
   - Default is "+0Hz"

## Contributing
Feel free to:
- Report bugs
- Suggest improvements
- Add support for more languages
- Enhance the configuration interface

## License
This integration follows the same license as the main VideoTrans project.

## Credits
- Google Cloud Text-to-Speech API
- VideoTrans team for the base project
- Contributors who helped with this integration 

## /docs/language.md

[English](./language.md#adding-language-packs)

# 添加语言包

1. 首先在控制台执行下面代码,查看系统当前语言代码

```
    import locale
    locale.getdefaultlocale()[0]
```
将输出内容的前2个字符小写,拼接上`.json`作为文件名创建json文件,比如输出的是`en_US`,就创建 `en.json` 到 videotrans/language 目录下,这个`en.json`就是语言文件。


> 
> 在软件启动时,会以该方式locale.getdefaultlocale()[0]的前2个字符小写,然后拼接`.json`,组成文件名,到 videotrans/language目录下搜寻,如果存在则使用,不存在则显示英文界面。
> 如果在 `videotrans/set.ini` 文件中  `lang=` 设置了值,则以该值为默认语言代码,否则以 `locale.getdefaultlocale()` 结果为准。
>  


已存在`en.json` `zh.json` 2种语言文件,可直接复制后修改名称,在此基础上制作新的语言文件

每个语言文件都是一个json对象,最外层有4个字段,分别是

```
{
"translate_language":{},
"ui_lang":{},
"toolbox_lang":{}, 
"language_code_list":{}
}
```

其中 `translate_language` 是用于进度显示、错误提示、各种交互状态的文本,`ui_lang` 软件界面各个部件的显示名称,`toolbox_lang` 是视频工具箱界面各个部件的显示名称, `language_code_list` 是支持的语言显示名称

## translate_language 修改

```
"translate_language": {
    "qianyiwenjian": "The video path or name contains non ASCII spaces. To avoid errors, it has been migrated to ",
    "mansuchucuo": "Video automatic slow error, please try to cancel the 'Video auto down' option",
}
```

如上,translate_language 是 `字段名:字段值` 组成的json对象,字段名不要动,字段值改为相应语言的文本即可。


## ui_lang 修改

"ui_lang": {
    "SP-video Translate Dubbing": "SP-video Translate Dubbing",
    "Multiple MP4 videos can be selected and automatically queued for processing": "Multiple MP4 videos can be selected and automatically queued for processing",
    "Select video..": "Select video..",
}
同 `translate_language` 的修改一样,字段名不要动,将字段值改为相应语言的文本即可。

## toolbox_lang 修改

"toolbox_lang": {
    "No voice video":"无声视频",
    "Open dir":"打开目录",
    "Audio Wav":"音频文件",
}
同 `translate_language` 的修改一样,字段名不要动,将字段值改为相应语言的文本即可。

## language_code_list 的修改

```
"language_code_list": {
    "zh-cn":"Simplified Chinese",
    "zh-tw":"Traditional Chinese",
    "en":"English",
    "fr":"French",
    "de":"German",
    "ja":"Japanese",
    "ko":"Korean",
    "ru":"Russian",
    "es":"Spanish",
    "th":"Thai",
    "it":"Italian",
    "pt":"Portuguese",
    "vi":"Vietnamese",
    "ar":"Arabic",
    "tr":"Turkish",
    "hi":"Hindi"
  }
```

和其他一样,该内容字段名不要动,字段值改为要显示的名称

**制作完成后,确认符合正确的 json 格式,然后放到 videotrans/language 目录下,重启软件就会自动应用该语言,如何你制作的语言包和默认语言不同,可通过设置 `set.ini`中 lang=语言代码和强制使用,比如 `lang=zh`将强制显示 zh.json 内容**



----


----



# Adding Language Packs



1. First, execute the following code in the console to check the system's current language code

```
    import locale
    locale.getdefaultlocale()[0]
```
Lowercase the first 2 characters of the output content and append `.json` to create a json file as the filename. For example, if the output is `en_US`, create `en.json` in the videotrans/language directory, where `en.json` is the language file.


> 
> When the software starts, the system will take the first 2 characters lowercase from locale.getdefaultlocale()[0] and append `.json` to form the filename, and then look for it under the videotrans/language directory. If it exists, it will be used; otherwise, the English interface will be displayed.
> If the `lang=` in the `videotrans/set.ini` file has a value set, then this value will be taken as the default language code, otherwise the result of `locale.getdefaultlocale()` will be used.
> 


There are already `en.json` and `zh.json` 2 language files. You can copy and modify the name directly to create new language files.

Each language file is a json object. The outermost layer has 4 fields, which are

```
{
"translate_language":{},
"ui_lang":{},
"toolbox_lang":{}, 
"language_code_list":{}
}
```

Here `translate_language` is used for progress display, error prompts, various interaction states of text, `ui_lang` software interface display name of each component, `toolbox_lang` video toolbox interface display name of each component, `language_code_list` is the supported language display name

## Modification of translate_language 

```
"translate_language": {
    "qianyiwenjian": "The video path or name contains non ASCII spaces. To avoid errors, it has been migrated to ",
    "mansuchucuo": "Video automatic slow error, please try to cancel the 'Video auto down' option",
}
```

As mentioned above, translate_language is a json object composed of `field name: field value`. Do not move the field name and change the field value to the corresponding language text.


## Modification of ui_lang 

"ui_lang": {
    "SP-video Translate Dubbing": "SP-video Translate Dubbing",
    "Multiple MP4 videos can be selected and automatically queued for processing": "Multiple MP4 videos can be selected and automatically queued for processing",
    "Select video..": "Select video..",
}
The same as the modification of `translate_language`, do not move the field name and change the field value to the corresponding language text.

## Modification of toolbox_lang 

"toolbox_lang": {
    "No voice video":"Silent video",
    "Open dir":"Open directory",
    "Audio Wav":"Audio file",
}
The same as the modification of `translate_language`, do not move the field name, and change the field value to the relevant language text.

## Modification of language_code_list 

```
"language_code_list": {
    "zh-cn":"Simplified Chinese",
    "zh-tw":"Traditional Chinese",
    "en":"English",
    "fr":"French",
    "de":"German",
    "ja":"Japanese",
    "ko":"Korean",
    "ru":"Russian",
    "es":"Spanish",
    "th":"Thai",
    "it":"Italian",
    "pt":"Portuguese",
    "vi":"Vietnamese",
    "ar":"Arabic",
    "tr":"Turkish",
    "hi":"Hindi"
  }
```

Like the others, do not modify the field name of this content, change the field value to the display name

**After the production is completed, make sure it meets the correct json format, put it into the videotrans/language directory, and the software will automatically apply the language when restarted. If the language pack you made is different from the default language, you can set `set.ini` in lang= language code and use it forcibly, such as `lang=zh` will forcibly display the content of zh.json**

## /docs/webui.md

# pyVideoTrans WebUI 使用指南

## ⚠️ 重要提示

> **WebUI 版本仅实现了部分功能**,主要用于以下场景:
> - 云服务器部署(远程访问翻译服务)
> - 局域网内部署(服务器与使用机分离)
> - Docker 容器化部署
>
> **如需完整功能**,请使用桌面客户端(`sp.exe`)或源码运行(`sp.py`)。
> 桌面版支持更多 API 渠道配置、实时交互编辑、批量处理等高级功能。

---

## 一、部署方式

### 1.1 源码部署(推荐)

```bash
git clone https://github.com/jianchang512/pyvideotrans.git
cd pyvideotrans
uv sync --extra webui
```

启动服务:

```bash
uv run webui.py                    # 默认 0.0.0.0:7860
uv run webui.py --port 8080        # 指定端口
uv run webui.py --host 127.0.0.1   # 仅本机访问
uv run webui.py --share            # 创建 Gradio 公网链接
```

访问:`http://127.0.0.1:7860` 或 `http://<服务器IP>:7860`

### 1.2 Docker 部署

```bash
# 构建镜像
git clone https://github.com/jianchang512/pyvideotrans.git
cd pyvideotrans
docker build -t pyvideotrans-webui .

# 运行
docker run -d -p 7860:7860 --name pyvideotrans pyvideotrans-webui

# 持久化配置和输出
docker run -d -p 7860:7860 \
  -v ./data/output:/app/output \
  -v ./data/config:/app/videotrans \
  --name pyvideotrans pyvideotrans-webui

# GPU 加速
docker run -d -p 7860:7860 --gpus all \
  -v ./data/output:/app/output \
  -v ./data/config:/app/videotrans \
  --name pyvideotrans pyvideotrans-webui
```

### 1.3 Google Colab

1. 打开 https://colab.research.google.com/drive/1kPTeAMz3LnWRnGmabcz4AWW42hiehmfm?usp=sharing
2. 登录 Google 账号 → 点击 **全部运行**
3. 等待 `*.gradio.live` 链接出现,点击使用

> ⚠️ Colab 免费版有 4-6 小时使用时长限制。

---

## 二、界面说明

WebUI 分为三个标签页:

### 2.1 🎬 视频翻译(主界面)

**文件选择**:支持 mp4/mkv/avi/mov/webm/wav/mp3/m4a/flac 等格式

**语音识别**:可选 faster-whisper/openai-whisper/Qwen-ASR/FunASR/Huggingface_ASR(均为本地内置免费渠道)

**字幕翻译**:可选 Google/Microsoft/M2M100(免费渠道)

**字幕配音**:可选 Edge-TTS/Qwen3-TTS/MOSS-TTS/Piper/VITS/Supertonic/ChatterBox/gTTS(免费/本地内置渠道)

**对齐与字幕**:配音加速、视频慢速、语速/音量/音调调节、字幕嵌入类型

**更多设置**:降噪、标点处理、人声分离、背景声嵌入、CUDA 加速

**硬字幕样式编辑**:字体、颜色、描边、阴影、对齐等全面自定义

### 2.2 ⚙️ 渠道设置

配置各渠道的 API 地址、SK 密钥、模型等。**与桌面版通用**,配置保存在 `videotrans/params.json` 中。

包含:翻译渠道、语音识别渠道、配音渠道、参考音频设置

> 使用 API 渠道前,需先用桌面版(sp.exe)配置好 API 地址和 SK 密钥。

### 2.3 🔧 高级选项

配置全局高级参数,与桌面版 `菜单 → 工具 → 高级选项` 完全通用。

包含:通用设置、视频输出控制、语音识别参数、字幕翻译调整、字幕配音调整、字幕声音画面对齐、Whisper模型提示词

---

## 三、执行翻译

1. 选择视频/音频文件
2. 配置识别/翻译/配音参数
3. 点击「🚀 开始执行」

执行过程:
- 按钮变为「⏳ 执行中...」并禁用
- 右侧日志实时显示 8 个阶段进度
- 完成后按钮恢复,视频预览区可在线播放,文件区可下载

---

## 四、与桌面版对比

| 功能 | WebUI | 桌面版 |
|------|:-----:|:-----:|
| 视频翻译完整流程 | ✅ | ✅ |
| API 渠道(需先用桌面版配置) | ✅ | ✅ |
| 高级选项配置 | ✅ | ✅ |
| 实时交互编辑字幕 | ❌ | ✅ |
| 批量处理 | ❌ | ✅ |
| 视频预览播放 | ✅ | ❌ |
| 远程访问 / Docker | ✅ | ❌ |

---

## 五、常见问题

**Q: 启动报错 No module named gradio**
`uv sync --extra webui`

**Q: Docker 如何持久化配置**
`-v ./data/output:/app/output -v ./data/config:/app/videotrans`

**Q: Docker 如何使用 GPU**
安装 nvidia-container-toolkit 后:`docker run --gpus all ...`

**Q: 如何使用 API 渠道**
先用桌面版配置好 API 地址和 SK,WebUI 自动读取 `params.json`

**Q: 如何创建公网链接**
`uv run webui.py --share`,控制台输出临时 `*.gradio.live` 链接


## /docs/whisper_net_setup.md

# Whisper.NET 安装指南

## 这是什么?

Whisper.NET 是一个语音识别引擎,可以让你的 AMD 显卡通过 Vulkan 加速来识别语音(不用 NVIDIA 的 CUDA)。

## 适用场景

适合 **Windows + AMD 显卡** 用户使用。

**背景说明**:我在使用 Whisper.cpp 时发现,最新版本已经不再提供 Windows 环境下 AMD 显卡的 GPU 支持,导致 Windows + AMD 显卡只能使用 CPU 进行语音识别,速度很慢。经过与 AI 的咨询和讨论,最终选择了 Whisper.NET 这条技术路线,并借助 AI 的编程能力得以实现。

**测试环境**:目前仅在 **Windows 11 23H2 + RX 6650 XT** 环境下测试通过。其他环境可能需要用户自行测试,欢迎反馈结果。

---

## 第一步:下载 DLL 文件

### 需要下载的文件列表

**托管 DLL**(下载后放到 `deps/` 文件夹):

| 文件名 | 版本 | 下载链接 |
|--------|------|----------|
| Whisper.net.dll | 1.9.0 | [点击下载](https://www.nuget.org/packages/Whisper.net/1.9.0) |
| Microsoft.Extensions.AI.Abstractions.dll | 10.0.0 | [点击下载](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/10.0.0) |
| Microsoft.Bcl.AsyncInterfaces.dll | 10.0.0 | [点击下载](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces/10.0.0) |
| System.Memory.dll | 4.6.3 | [点击下载](https://www.nuget.org/packages/System.Memory/4.6.3) |
| System.Buffers.dll | 4.6.1 | [点击下载](https://www.nuget.org/packages/System.Buffers/4.6.1) |
| System.Runtime.CompilerServices.Unsafe.dll | 6.1.2 | [点击下载](https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/6.1.2) |
| System.Numerics.Vectors.dll | 4.6.1 | [点击下载](https://www.nuget.org/packages/System.Numerics.Vectors/4.6.1) |

**Native DLL**(下载后放到 `deps/native/` 文件夹):

从 [Whisper.net.Runtime.Vulkan 1.9.0](https://www.nuget.org/packages/Whisper.net.Runtime.Vulkan/1.9.0) 下载,解压后把 `build/win-x64/` 文件夹里的**所有 DLL 文件**都复制到 `deps/native/`。

NuGet 包里包含这些文件(全部需要):

| 文件名 | 大小 | 用途 |
|--------|------|------|
| whisper.dll | 473KB | 语音识别核心 |
| libwhisper.dll | 473KB | whisper.dll 的别名(必需) |
| ggml-whisper.dll | 66KB | 计算库 |
| libggml-whisper.dll | 66KB | ggml-whisper.dll 的别名 |
| ggml-base-whisper.dll | 528KB | 基础库(必需依赖) |
| libggml-base-whisper.dll | 528KB | ggml-base-whisper.dll 的别名 |
| ggml-cpu-whisper.dll | 590KB | CPU 后备 |
| libggml-cpu-whisper.dll | 590KB | ggml-cpu-whisper.dll 的别名 |
| ggml-vulkan-whisper.dll | 45MB | GPU 加速(Vulkan) |
| libggml-vulkan-whisper.dll | 45MB | ggml-vulkan-whisper.dll 的别名 |

### 如何从 NuGet 下载?

1. 点击上面的链接打开 NuGet 页面
2. 点击 **"Download package"** 下载 `.nupkg` 文件
3. 把 `.nupkg` 文件后缀改成 `.zip`,用解压软件打开
4. 找到里面的 DLL 文件:
   - 托管 DLL 在 `lib/netstandard2.0/` 文件夹里
   - Native DLL 在 `build/win-x64/` 文件夹里

---

## 第二步:下载语音模型

从 [ggerganov/whisper.cpp models](https://github.com/ggerganov/whisper.cpp/tree/master/models) 下载 `.bin` 格式的模型文件,放到 `models/` 文件夹。

比如下载:`ggml-large-v3-turbo.bin`(效果好、速度快)

---

## 第三步:检查文件结构

确保你的目录结构是这样的:

```
pyvideotrans/
├─ models/
│  └─ ggml-large-v3-turbo.bin    ← 语音模型
└─ deps/
   ├─ Whisper.net.dll            ← 下面 7 个是托管 DLL
   ├─ Microsoft.Extensions.AI.Abstractions.dll
   ├─ Microsoft.Bcl.AsyncInterfaces.dll
   ├─ System.Memory.dll
   ├─ System.Buffers.dll
   ├─ System.Runtime.CompilerServices.Unsafe.dll
   ├─ System.Numerics.Vectors.dll
   └─ native/                     ← 把 NuGet 包里 build/win-x64/ 的所有 DLL 复制到这里
      ├─ whisper.dll
      ├─ libwhisper.dll
      ├─ ggml-whisper.dll
      ├─ libggml-whisper.dll
      ├─ ggml-base-whisper.dll
      ├─ libggml-base-whisper.dll
      ├─ ggml-cpu-whisper.dll
      ├─ libggml-cpu-whisper.dll
      ├─ ggml-vulkan-whisper.dll
      └─ libggml-vulkan-whisper.dll
```

---

## 第四步:开始使用

0. 源码部署本项目,运行`uv sync --all-extras`,如果已安装,请单独执行`uv sync --extra dotnet` 安装 `pythonnet` 模块
1. 执行`uv run sp.py` 打开软件
2. 在"语音识别"下拉框选择 **"Whisper.NET"**
3. 选择你下载的模型文件
4. 点击开始

---

## 遇到问题?

### 提示 "Native Library not found" 或错误代码 `0x8007007E`

- 检查 `deps/native/` 文件夹里是否有 10 个 DLL 文件
- 检查文件名是否正确

### GPU 加速不工作

- 更新显卡驱动
- AMD 显卡需要支持 Vulkan(RX 400 系列及以上)
- NVIDIA 显卡需要 GTX 600 系列及以上

### 提示 pythonnet 初始化失败

- 安装 [.NET Runtime](https://dotnet.microsoft.com/download/dotnet)(选最新的 .NET 8 或 .NET 9)

### 想确认显卡是否支持 Vulkan

打开命令行,输入:
```
vulkaninfo
```
如果显示显卡信息就说明支持。

---

# Whisper.NET Setup Guide

## What is this?

Whisper.NET is a speech recognition engine that uses Vulkan acceleration for AMD GPUs (no NVIDIA CUDA required).

## Use Case

Designed for **Windows + AMD GPU** users.

**Background**: While using Whisper.cpp, I discovered that the latest version no longer provides AMD GPU support on Windows. This means Windows + AMD GPU users can only use CPU for speech recognition, which is very slow. After consulting and discussing with AI, I chose the Whisper.NET approach and implemented it with AI assistance.

**Tested Environment**: Currently only tested on **Windows 11 23H2 + RX 6650 XT**. Other environments may need further testing. Feedback is welcome.

---

## Step 1: Download DLL Files

### Required Files

**Managed DLLs** (place in `deps/` folder):

| File | Version | Download Link |
|------|---------|---------------|
| Whisper.net.dll | 1.9.0 | [Download](https://www.nuget.org/packages/Whisper.net/1.9.0) |
| Microsoft.Extensions.AI.Abstractions.dll | 10.0.0 | [Download](https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/10.0.0) |
| Microsoft.Bcl.AsyncInterfaces.dll | 10.0.0 | [Download](https://www.nuget.org/packages/Microsoft.Bcl.AsyncInterfaces/10.0.0) |
| System.Memory.dll | 4.6.3 | [Download](https://www.nuget.org/packages/System.Memory/4.6.3) |
| System.Buffers.dll | 4.6.1 | [Download](https://www.nuget.org/packages/System.Buffers/4.6.1) |
| System.Runtime.CompilerServices.Unsafe.dll | 6.1.2 | [Download](https://www.nuget.org/packages/System.Runtime.CompilerServices.Unsafe/6.1.2) |
| System.Numerics.Vectors.dll | 4.6.1 | [Download](https://www.nuget.org/packages/System.Numerics.Vectors/4.6.1) |

**Native DLLs** (place in `deps/native/` folder):

Download from [Whisper.net.Runtime.Vulkan 1.9.0](https://www.nuget.org/packages/Whisper.net.Runtime.Vulkan/1.9.0), extract and copy **all DLL files** from `build/win-x64/` folder to `deps/native/`.

The NuGet package contains these files (all required):

| File | Size | Purpose |
|------|------|---------|
| whisper.dll | 473KB | Speech recognition core |
| libwhisper.dll | 473KB | Alias for whisper.dll (required) |
| ggml-whisper.dll | 66KB | Compute library |
| libggml-whisper.dll | 66KB | Alias for ggml-whisper.dll |
| ggml-base-whisper.dll | 528KB | Base library (required dependency) |
| libggml-base-whisper.dll | 528KB | Alias for ggml-base-whisper.dll |
| ggml-cpu-whisper.dll | 590KB | CPU backend |
| libggml-cpu-whisper.dll | 590KB | Alias for ggml-cpu-whisper.dll |
| ggml-vulkan-whisper.dll | 45MB | GPU acceleration (Vulkan) |
| libggml-vulkan-whisper.dll | 45MB | Alias for ggml-vulkan-whisper.dll |

### How to download from NuGet?

1. Click the download link above to open the NuGet page
2. Click **"Download package"** to download the `.nupkg` file
3. Rename the `.nupkg` file extension to `.zip` and open with any archive tool
4. Find the DLL files inside:
   - Managed DLLs are in `lib/netstandard2.0/` folder
   - Native DLLs are in `build/win-x64/` folder

---

## Step 2: Download Speech Model

Download `.bin` format model files from [ggerganov/whisper.cpp models](https://github.com/ggerganov/whisper.cpp/tree/master/models) and place them in the `models/` folder.

Recommended: `ggml-large-v3-turbo.bin` (good quality, fast)

---

## Step 3: Verify File Structure

Make sure your directory structure looks like this:

```
pyvideotrans/
├─ models/
│  └─ ggml-large-v3-turbo.bin    ← Speech model
└─ deps/
   ├─ Whisper.net.dll            ← Managed DLLs (7 files)
   ├─ Microsoft.Extensions.AI.Abstractions.dll
   ├─ Microsoft.Bcl.AsyncInterfaces.dll
   ├─ System.Memory.dll
   ├─ System.Buffers.dll
   ├─ System.Runtime.CompilerServices.Unsafe.dll
   ├─ System.Numerics.Vectors.dll
   └─ native/                     ← Copy all DLLs from build/win-x64/ here
      ├─ whisper.dll
      ├─ libwhisper.dll
      ├─ ggml-whisper.dll
      ├─ libggml-whisper.dll
      ├─ ggml-base-whisper.dll
      ├─ libggml-base-whisper.dll
      ├─ ggml-cpu-whisper.dll
      ├─ libggml-cpu-whisper.dll
      ├─ ggml-vulkan-whisper.dll
      └─ libggml-vulkan-whisper.dll
```

---

## Step 4: Start Using

1. Open pyVideoTrans
2. Select **"Whisper.NET"** from the speech recognition dropdown
3. Choose your downloaded model file
4. Click Start

---

## Troubleshooting

### "Native Library not found" or error code `0x8007007E`

- Check if `deps/native/` folder contains all 10 DLL files
- Verify file names are correct

### GPU acceleration not working

- Update your graphics driver
- AMD GPUs require Vulkan support (RX 400 series or newer)
- NVIDIA GPUs require GTX 600 series or newer

### pythonnet initialization failed

- Install [.NET Runtime](https://dotnet.microsoft.com/download/dotnet) (choose the latest .NET 8 or .NET 9)

### Check if your GPU supports Vulkan

Open command line and run:
```
vulkaninfo
```
If it displays your GPU information, Vulkan is supported.

## /f5-tts/cosy.wav

Binary file available at https://raw.githubusercontent.com/jianchang512/pyvideotrans/refs/heads/main/f5-tts/cosy.wav

## /f5-tts/nverguo.wav

Binary file available at https://raw.githubusercontent.com/jianchang512/pyvideotrans/refs/heads/main/f5-tts/nverguo.wav

## /ffmpeg/.gitignore

```gitignore path="/ffmpeg/.gitignore" 
*.exe
*.dll
sox
```

## /law.txt

<h1>pyVideoTrans 软件许可与服务协议</h1>
<p>更新日期:2025年10月21日</p>

<p>欢迎使用 pyVideoTrans(以下简称“本软件”)!本软件是一款免费、开源的本地视频翻译和语音转录工具。在安装、复制或以任何方式使用本软件前,请您务必仔细阅读并充分理解本协议中的所有条款。</p>

<p class="warning"><strong>您的安装、复制、下载或任何形式的使用行为,即表示您已阅读、理解并无条件接受本协议所有条款的约束。如果您不同意本协议的任何内容,请立即停止使用并从您的设备中彻底删除本软件。</strong></p>

<h2>1. 许可授予与软件性质</h2>
<ul>
    <li><strong>开源免费</strong>:本软件是一款基于 GPL-v3 开源协议发布的免费软件。您可以从官方渠道(<code>https://github.com/jianchang512/pyvideotrans</code>)或文档站(<code>https://pyvideotrans.com</code>)获取本软件的源代码和Windows预打包版。</li>
    <li><strong>禁止商业销售</strong>:开发者未授权任何实体或个人销售本软件。任何通过付费渠道获取本软件的行为均与开发者无关,开发者对此不承担任何责任。</li>
    <li><strong>重要提醒:</strong>第三方 API 需您自行提供账户和密钥(仅本地存储),产生的费用由第三方收取,与开发者无关,开发者仅在软件中提供API对接技术规范。请查阅各 API 协议以确认商用许可及费用标准。
    </li>
</ul>

<h2>2. 数据隐私</h2>
<ul>
    <li><strong>本地运行</strong>:本软件的核心功能完全在您的本地计算机上运行,不会收集或上传您的任何个人信息、视频文件或操作数据至开发者服务器。</li>
    <li><strong>第三方服务</strong>:当您选择使用集成的第三方API服务(如 Microsoft Azure, OpenAI, Edge TTS等)时,相关数据将直接由您的计算机发送至相应的第三方服务提供商。您的数据处理将受限于该第三方服务商的隐私政策和使用条款。开发者不参与此过程,也不对第三方服务的数据安全和隐私泄露承担任何责任。</li>
    <li><strong>版本更新与报错信息</strong>:软件通过 <code>https://pyvideotrans.com/version.json</code> 这个静态文件获取最新版本号; <br>当你在软件中点击“报告错误”按钮时,会打开 <code>https://bbs.pyvideotrans.com/post</code> 报错提交页面并显示错误信息,在该页面你仍需要再次点击“发布”按钮,才会向开发者提交错误信息,否则错误信息只会保留在本地和你的浏览器缓存中,不会提交。</li>
</ul>

<h2>3. 免责声明与责任限制</h2>
<p class="warning">
    <strong>本软件按“原样” 提供,不附带任何形式的明示或暗示的保证,包括但不限于对适销性、特定用途适用性及非侵权性的保证。</strong>
</p>
<ul>
    <li><strong>无保证</strong>:开发者不保证本软件能够满足您的所有需求,也不保证软件运行不会中断或出现错误。您将承担使用本软件所带来的一切风险。</li>
    <li><strong>责任限制</strong>:在任何情况下,无论基于何种法律理论(无论是合同、侵权或其他),<strong>开发者均不对任何因使用或无法使用本软件而导致的任何形式的直接、间接、特殊、偶然或后果性损害承担责任</strong>。这包括但不限于:数据丢失、文件损坏、利润损失、业务中断、计算机故障或任何其他商业损害或损失,即便开发者已被告知存在此类损害的可能性。</li>
    <li><strong>用户责任</strong>:您对通过本软件处理的所有内容负全部责任。您必须确保拥有处理这些内容的合法权利,并遵守您所在地区及中华人民共和国的所有适用法律法规,包括但不限于版权法和知识产权法。任何因非法使用本软件而导致的法律后果,均由您自行承担。</li>
</ul>

<h2>4. 您的义务</h2>
<ul>
    <li><strong>数据备份</strong>:软件缺陷或不当操作可能导致数据丢失或文件损坏。<strong>在使用本软件处理任何重要文件之前,您有绝对责任对您的原始文件和重要数据进行充分备份。</strong></li>
    <li><strong>合法使用</strong>:您承诺不使用本软件进行任何非法活动,包括但不限于侵犯他人版权、传播非法信息等。</li>
</ul>

<h2>5. 其他条款</h2>
<ul>
    <li><strong>协议修改</strong>:开发者保留随时修改本协议条款的权利。修改后的协议将在官方渠道公布,恕不另行通知。您继续使用本软件将被视为接受修改后的协议。</li>
    <li><strong>最终解释权</strong>:在法律允许的最大范围内,本协议的最终解释权归本软件开发者所有。</li>
</ul>

<p>如果您已阅读并同意上述所有条款,请开始使用本软件。否则,请删除本软件。</p>


## /pyproject.toml

```toml path="/pyproject.toml" 
[project]
name = "pyVideoTrans"
version = "4.05"
requires-python = ">=3.10, <3.11"
dependencies = [
    "absl-py==2.0.0",
    "accelerate==1.12.0",
    "addict==2.4.0",
    "aenum==3.1.15",
    "aiofiles==24.1.0",
    "aiohappyeyeballs==2.6.1",
    "aiohttp==3.12.13",
    "aiosignal==1.3.2",
    "alibabacloud-alimt20181012==1.1.0",
    "alibabacloud-credentials==0.3.6",
    "alibabacloud-endpoint-util==0.0.3",
    "alibabacloud-gateway-spi==0.0.2",
    "alibabacloud-openapi-util==0.2.2",
    "alibabacloud-openplatform20191219==2.0.0",
    "alibabacloud-oss-sdk==0.1.0",
    "alibabacloud-oss-util==0.0.6",
    "alibabacloud-tea==0.4.0",
    "alibabacloud-tea-fileform==0.0.5",
    "alibabacloud-tea-openapi==0.3.12",
    "alibabacloud-tea-util==0.3.13",
    "alibabacloud-tea-xml==0.0.2",
    "aliyun-python-sdk-core==2.16.0",
    "aliyun-python-sdk-kms==2.16.5",
    "altgraph==0.17.4",
    "annotated-types==0.6.0",
    "antlr4-python3-runtime==4.9.3",
    "anyio==4.9.0",
    "asttokens==2.4.1",
    "astunparse==1.6.3",
    "async-timeout==5.0.1",
    "attrs==25.3.0",
    "audioread==3.0.1",
    "av==16.0.1",
    "azure-cognitiveservices-speech",
    "blinker==1.8.2",
    "cachetools==5.3.2",
    "certifi==2025.10.5",
    "cffi==1.17.1",
    "chardet==3.0.4",
    "charset-normalizer==3.4.4",
    "click==8.1.7",
    "colorama==0.4.6",
    "coloredlogs==15.0.1",
    "contourpy==1.1.1",
    "crcmod==1.7",
    "cryptography==45.0.4",
    "ctranslate2==4.8",
    "cycler==0.12.1",
    "dashscope",
    "dataclasses-json==0.6.7",
    "datasets==3.0.0",
    "decorator==5.2.1",
    "deepgram-captions==1.2.0",
    "deepgram-sdk==4.1.0",
    "deepl==1.18.0",
    "deprecation==2.1.0",
    "diffusers==0.33.1",
    "dill==0.3.8",
    "distro==1.8.0",
    "editdistance==0.8.1",
    "exceptiongroup==1.2.2",
    "executing==2.0.1",
    "ffmpeg-python==0.2.0",
    "filelock==3.13.1",
    "flatbuffers==25.9.23",
    "fonttools==4.43.1",
    "frozenlist==1.7.0",
    "fsspec==2024.6.1",
    "future==0.18.3",
    "gast==0.4.0",
    "google-api-core==2.28.1",
    "google-api-python-client==2.128.0",
    "google-auth==2.29.0",
    "google-auth-httplib2==0.2.0",
    "google-auth-oauthlib==0.4.6",
    "google-cloud-texttospeech==2.27.0",
    "google-pasta==0.2.0",
    "googleapis-common-protos==1.63.0",
    "grpcio==1.60.0",
    "grpcio-status==1.60.0",
    "h11==0.14.0",
    "h2==3.2.0",
    "h5py==3.10.0",
    "hdbscan==0.8.40",
    "hf-xet",
    "hpack==3.0.0",
    "hstspreload==2023.1.1",
    "httpcore==1.0.6",
    "httplib2==0.22.0",
    "httpx[socks]==0.28.1",
    "huggingface",
    #"huggingface==0.0.1",
    #"huggingface-hub==0.35.3",
    "huggingface-hub",
    "humanfriendly==10.0",
    "hydra-core==1.3.2",
    "hyperframe==5.2.0",
    "idna==3.11",
    "imageio==2.31.4",
    "imageio-ffmpeg==0.4.9",
    "importlib-metadata==8.7.0",
    "inflate64==1.0.0",
    "ipython==8.23.0",
    "iso639-lang==2.2.3",
    "itsdangerous==2.2.0",
    "jaconv==0.4.0",
    "jamo==0.4.1",
    "jedi==0.19.1",
    "jieba==0.42.1",
    "jinja2==3.1.2",
    "jiter==0.11.1",
    "jmespath==0.10.0",
    "joblib==1.5.1",
    "kaldiio==2.18.1",
    "keras==2.9.0",
    "keras-preprocessing==1.1.2",
    "kiwisolver==1.4.5",
    "lazy-loader==0.4",
    "libclang==16.0.6",
    "librosa==0.11.0",
    "llvmlite==0.44.0",
    "markdown==3.5",
    "markdown-it-py==4.0.0",
    "markupsafe==2.1.3",
    "marshmallow==3.23.0",
    "matplotlib-inline==0.1.6",
    "mdurl==0.1.2",
    "more-itertools==10.1.0",
    "mpmath==1.3.0",
    "msgpack==1.1.1",
    "mypy-extensions==1.0.0",
    "networkx==3.2",
    "norbert==0.2.1",
    "numba==0.61.2",
    "numpy==1.26.0",
    "oauthlib==3.2.2",
    "omegaconf==2.3.0",
    "opt-einsum==3.3.0",
    "ordered-set==4.1.0",
    "oss2==2.19.1",
    "packaging==25.0",
    "pandas==2.3.0",
    "parso==0.8.3",
    "pefile==2023.2.7",
    "peft==0.15.2",
    "pip==25.1.1",
    "platformdirs==4.3.8",
    "plyer==2.1.0",
    "pooch==1.8.2",
    "proglog==0.1.10",
    "prompt-toolkit==3.0.43",
    "propcache==0.3.2",
    "proto-plus==1.23.0",
    "protobuf==4.21.6",
    "psutil==6.0.0",
    "pure-eval==0.2.2",
    "py7zr==0.22.0",
    "pyarrow==20.0.0",
    "pyasn1==0.5.0",
    "pyasn1-modules==0.3.0",
    "pybcj==1.0.2",
    "pycparser==2.22",
    "pycryptodome==3.23.0",
    "pycryptodomex==3.21.0",
    "pydantic==2.10.2",
    "pydantic-core==2.27.1",
    "pydub==0.25.1",
    "pygments==2.17.2",
    "pyinstaller==6.16.0",
    "pyinstaller-hooks-contrib==2025.8",
    "pynndescent==0.5.13",
    "pyparsing==3.1.1",
    "pyppmd==1.1.0",
    "pyside6==6.9.2",
    "pyside6-addons==6.9.2",
    "pyside6-essentials==6.9.2",
    "pysoundfile==0.9.0.post1",
    "python-dateutil==2.9.0.post0",
    "pytorch-wpe==0.0.1",
    "pytz==2025.2",
    "pyyaml==6.0.3",
    "pyzstd==0.16.1",
    "qdarkstyle==3.2.3",
    "regex",
    "requests[socks]==2.32.5",
    "requests-oauthlib==1.3.1",
    "resampy==0.4.2",
    "rfc3986==1.5.0",
    "rich==14.2.0",
    "rsa==4.9",
    "safetensors==0.4.5",
    "scikit-learn==1.7.0",
    "scipy==1.15.3",
    "sentencepiece==0.2.0",
    "setuptools==80.9.0",
    "shellingham==1.5.4",
    "shiboken6==6.9.2",
    "simplejson==3.19.3",
    "six==1.17.0",
    "snakeviz==2.2.2",
    "sniffio==1.3.1",
    "sortedcontainers==2.4.0",
    "soundfile==0.13.1",
    "soxr==0.5.0.post1",
    "speechrecognition==3.10.0",
    "srt==3.5.2",
    "stack-data==0.6.3",
    "sympy==1.14.0",
    "tabulate==0.9.0",
    "tenacity==9.1.2",
    "tencentcloud-sdk-python==3.0.1223",
    "tencentcloud-sdk-python-common==3.0.1032",
    "tencentcloud-sdk-python-tmt==3.0.1032",
    "tensorboardx==2.6.4",
    "tiktoken==0.6.0",
    "torch==2.7.1",
    "torch-complex==0.4.4",
    "torchaudio==2.7.1",
    "tqdm==4.67.1",
    "typer==0.19.2",
    "typing-extensions==4.15.0",
    "typing-inspect==0.9.0",
    "tzdata==2025.2",
    "urllib3==2.5.0",
    "websocket-client==1.8.0",
    "websockets==13.1",
    "wheel==0.45.1",
    "wrapt==1.15.0",
    "xxhash==3.5.0",
    "yarl==1.20.1",
    "zhconv==1.4.3",
    "sherpa-onnx>=1.13.0",
    "sherpa-onnx-core>=1.13.0",
    "sounddevice>=0.5.3",
    "openai-whisper>=20250625",
    "multidict>=6.7",
    "pytsmod>=0.3.8",
    "pyrubberband>=0.4.0",
    "piper-tts>=1.3.0",
    "pillow>=12.0.0",
    "gradio-client>=2.0.1",
    "ten-vad>=1.0.6.8",
    "pyannote-audio",
    "modelscope>=1.34.0",
    "funasr>=1.3.1",
    "edge-tts",
    "onnxruntime",
    "openai",
    "faster-whisper",
    "google-genai",
    "elevenlabs",
    "camb-sdk",
    "brotli>=1.2.0",
    "gtts>=2.5.4",
    "importlib-resources",
    "g2pw>=0.1.1",
    "unicode-rbnf>=2.4.0",
    "sentence-stream>=1.3.0",
    "google-cloud-storage>=3.12.0",
    "chatterbox-tts>=0.1.7",
    "pynini",
    "WeTextProcessing",
    "transformers4576>=4.57.6",
    "omnivoice",
    "qwen-asr-pvt",
    "qwen-tts-pvt",
    "f5-tts",
    "gradio>=6.8.0",
]
# "omnivoice",
# "qwen-asr-pvt",
# "qwen-tts-pvt",
# "f5-tts",

[project.optional-dependencies]
dotnet = [
    "pythonnet>=3.0.1",
]
webui = [
	"gradio"
]

[dependency-groups]
dev = [
  "pytest",
]
lint = [
  "ruff",
]

# 忽略版本冲突错误
[tool.uv]
override-dependencies = [
	"torch==2.7.1",
    "transformers==5.3.0",
    "torchaudio==2.7.1",
    "diffusers==0.33.1",
	"safetensors==0.4.5",
	"f5-tts==1.1.20",
	"huggingface-hub==1.10.0",
	"chatterbox-tts==0.1.7",
	"google-cloud-storage==3.12.0",
	"torchcodec ; sys_platform == 'never'"
]

index-strategy = "unsafe-best-match"



[[tool.uv.index]]
name = "pytorch-cu128"
url = "https://download.pytorch.org/whl/cu128"
explicit = true

[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true



[tool.uv.sources]
torch = [
    { index = "pytorch-cu128", marker = "sys_platform == 'win32' or sys_platform == 'linux'" },
    { index = "pytorch-cpu", marker = "sys_platform == 'darwin'" },  # darwin 为 macOS
]
torchaudio = [
    { index = "pytorch-cu128", marker = "sys_platform == 'win32' or sys_platform == 'linux'" },
    { index = "pytorch-cpu", marker = "sys_platform == 'darwin'" },
]
# 仅针对 Windows (win32) 平台指定从 URL 下载 wheel 文件
# https://github.com/OpenMOSS/MOSS-TTS-Nano/issues/6
#  https://github.com/billwuhao/pynini-windows-wheels/releases/download/v2.1.6.post1/pynini-2.1.6.post1-cp310-cp310-win_amd64.whl

pynini = [
    { url = "https://github.com/billwuhao/pynini-windows-wheels/releases/download/v2.1.6.post1/pynini-2.1.6.post1-cp310-cp310-win_amd64.whl", marker = "sys_platform == 'win32'" }
]


#faster-whisper = { url = "https://github.com/SYSTRAN/faster-whisper/archive/refs/heads/master.tar.gz" }



```

## /sp.py

```py path="/sp.py" 
"""
pyVideoTrans: Translate the video from one language to another and add dubbing

Home-page: https://github.com/jianchang512/pyvideotrans
Author: jianchang512@gmail.com
Documents: https://pyvideotrans.com
Discuss: https://bbs.pyvideotrans.com
License: GPL-V3

码不在雅,能跑则灵。
型不在秀,兼容就行。
斯是烂码,自得其乐。
全局变量乱如麻,if分支叠成塔。
线程队列八九个,传参全靠大字典。
可以塞硬件,怼系统。
无单元之测试,无类型之规整。
启动加载三百秒,界面UI丑到爆。
前有Whisper卡进程,后有FF猛报错。
三大平台皆可跑,上万星友亦成行。
AI嘲: 码之烂平生仅见
作者云:又不是不能跑。

"""

import os
import atexit, sys, time
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout, QMessageBox
from PySide6.QtCore import Qt, qInstallMessageHandler, QTimer
from PySide6.QtGui import QPixmap, QGuiApplication, QIcon
import argparse
import tempfile
from pathlib import Path
from PySide6.QtCore import QSize, QSettings
import traceback
from videotrans import VERSION
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


# 抑制警告
def suppress_qt_warnings(msg_type, context, message):
    if "QThreadStorage" in message:
        return


def cleanup():
    """强制清理函数"""
    try:
        if 'app' in globals():
            app.quit()
            app.deleteLater()
    except:
        pass


def show_global_error_dialog(exctype, value, tb):
    tb_str = "".join(traceback.format_exception(exctype, value, tb))
    QMessageBox.critical(None, 'Error', tb_str)


# 启动画面
class StartWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.main_window = None
        self.LoadNotif = None
        self.start_time = time.time()
        self.loader = None
        self.setWindowTitle('pyVideoTrans')

        self.resize(560, 350)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)  # 窗口背景透明

        self.background_label = QLabel(self)
        self.pixmap = QPixmap("./videotrans/styles/logo.png")
        self.background_label.setPixmap(self.pixmap)
        self.background_label.setScaledContents(True)
        self.background_label.setGeometry(self.rect())

        # 背景上叠加文字
        v_layout = QVBoxLayout(self)
        v_layout.addStretch(1)
        self.status_label = QLabel(f"pyVideoTrans {VERSION} Loading...")
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignRight)
        self.status_label.setStyleSheet("font-size:16px; color:white; background-color:transparent;")

        v_layout.addWidget(self.status_label)
        v_layout.setContentsMargins(0, 0, 0, 20)

    def closeEvent(self, event):
        # 释放启动画面的资源
        if hasattr(self, 'pixmap') and self.pixmap:
            self.pixmap = None

        # 如果主窗口不存在,则退出应用程序
        if self.main_window is None:
            QApplication.instance().quit()

        super().closeEvent(event)

    def update_lable(self, t):
        print(f'{int(time.time())}:{t}')
        if t == 'end':
            self.status_label.setText(f'Total time {int(time.time() - self.start_time)}s')
            QTimer.singleShot(1000, lambda: self.close())
        else:
            self.status_label.setText(f'{t}  {int(time.time() - self.start_time)}s')
        QApplication.processEvents()

    def center(self):
        screen = QGuiApplication.primaryScreen()
        if screen:
            center_point = screen.geometry().center()
            self.move(center_point.x() - self.width() // 2, center_point.y() - self.height() // 2)


# 启动主窗口
def initialize_full_app(start_window, app_instance):
    if sys.stdout is None or sys.stderr is None:
        try:
            log_dir = os.path.join(os.getcwd(), "logs")
            os.makedirs(log_dir, exist_ok=True)
            log_file_path = os.path.join(log_dir, f"{time.strftime('%Y%m%d')}.log")
            log_file = open(log_file_path, 'a', encoding='utf-8', buffering=1)
            sys.stdout = log_file
            sys.stderr = log_file
            print(f"\n\n--- Application started at {time.strftime('%Y-%m-%d %H:%M:%S')} ---")
        except Exception as e:
            print(e)

    sys.excepthook = show_global_error_dialog

    # 命令行参数
    parser = argparse.ArgumentParser()
    parser.add_argument('--lang', type=str, help='Set the application language (e.g., en, zh)')
    cli_args, unknown = parser.parse_known_args()
    if cli_args.lang:
        os.environ['PYVIDEOTRANS_LANG'] = cli_args.lang.lower()
    start_window.update_lable('Loading resources...')
    QApplication.processEvents()
    # 导入qss image 资源
    import videotrans.ui.dark.darkstyle_rc
    with open('./videotrans/styles/style.qss', 'r', encoding='utf-8') as f:
        app_instance.setStyleSheet(f.read())
    start_window.update_lable('Loading main window...')
    QApplication.processEvents()

    from videotrans.mainwin.main_win import MainWindow
    try:
        screen = QGuiApplication.primaryScreen().geometry()
        sets = QSettings("pyvideotrans", "settings")
        w, h = int(screen.width() * 0.85), int(screen.height() * 0.85)
        size = sets.value("windowSize", QSize(w, h))
        w, h = size.width(), size.height()
        start_window.update_lable('Initializing UI...')
        QApplication.processEvents()
        start_window.main_window = MainWindow(width=w, height=h,callback=start_window.update_lable)
    except Exception as e:
        show_global_error_dialog(type(e), e, e.__traceback__)
        app_instance.quit()
        return


if __name__ == "__main__":
    # Windows 打包需要
    import multiprocessing

    multiprocessing.freeze_support()
    multiprocessing.set_start_method('spawn', force=True)
    qInstallMessageHandler(suppress_qt_warnings)
    atexit.register(cleanup)
    if sys.platform != "win32":
        import signal


        def handle_exit(signum, frame):
            cleanup()
            sys.exit(0)


        signal.signal(signal.SIGINT, handle_exit)
        signal.signal(signal.SIGTERM, handle_exit)

    # 设置 HighDpi
    try:
        QApplication.setHighDpiScaleFactorRoundingPolicy(Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
    except AttributeError:
        pass

    app = QApplication(sys.argv)
    res = 0
    if getattr(sys, 'frozen', False) and (Path(sys.executable).parent.as_posix()).startswith(
            Path(tempfile.gettempdir()).as_posix()):
        msg_box = QMessageBox()
        msg_box.setIcon(QMessageBox.Critical)
        msg_box.setWindowTitle('Error')
        msg_box.setText('请解压后再双击 sp.exe,不可直接压缩包内使用')
        msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint)
        msg_box.exec()
        app.quit()
    else:
        splash = StartWindow()
        splash.setWindowIcon(QIcon("./videotrans/styles/icon.ico"))
        splash.center()
        splash.show()

        QTimer.singleShot(100, lambda: initialize_full_app(splash, app))
        try:
            res = app.exec()
            res = 0 if res is None else res
        finally:
            try:
                cleanup()
                import gc

                gc.collect()
            except Exception as e:
                print(e)
    sys.exit(res if isinstance(res, int) else 0)

```

## /test.py

```py path="/test.py" 
from videotrans.configure import config
from transformers import AutoModelForRNNT, AutoProcessor
from transformers.audio_utils import load_audio

model_id = "nvidia/nemotron-3.5-asr-streaming-0.6b"
processor = AutoProcessor.from_pretrained(model_id)
model = AutoModelForRNNT.from_pretrained(model_id, device_map="auto")

audio = load_audio(
    "10.wav",
    sampling_rate=processor.feature_extractor.sampling_rate,
)

# Condition on a known language ...
inputs = processor(audio, sampling_rate=processor.feature_extractor.sampling_rate, language="zh-CN")
inputs.to(model.device, dtype=model.dtype)
output = model.generate(**inputs, return_dict_in_generate=True)
print(processor.decode(output.sequences, skip_special_tokens=True))

# ... or let the model detect it and keep the emitted <xx-XX> language tag.
inputs = processor(audio, sampling_rate=processor.feature_extractor.sampling_rate) # equiv to ..., language="auto"
inputs.to(model.device, dtype=model.dtype)
output = model.generate(**inputs, return_dict_in_generate=True)
print(processor.decode(output.sequences, skip_special_tokens=False))
```

## /tests/__init__.py

```py path="/tests/__init__.py" 

```

## /tests/conftest.py

```py path="/tests/conftest.py" 
"""
conftest.py — sets up mocks for heavy dependencies so videotrans
modules can be imported without a full PySide6 / torch installation.

Only mocks packages that are genuinely NOT installed.
"""

import importlib
import sys
from unittest.mock import MagicMock


def _is_installed(name):
    """Check if a package is actually importable (not just in sys.modules)."""
    try:
        spec = importlib.util.find_spec(name)
        return spec is not None
    except (ImportError, ValueError, ModuleNotFoundError):
        return False


_HAS_PYSIDE6 = _is_installed("PySide6")
_HAS_TORCH = _is_installed("torch")
_HAS_REQUESTS = _is_installed("requests")
_HAS_TENACITY = _is_installed("tenacity")
_HAS_OPENAI = _is_installed("openai")
_HAS_DEEPGRAM = _is_installed("deepgram")
_HAS_ELEVENLABS = _is_installed("elevenlabs")
_HAS_AIOHTTP = _is_installed("aiohttp")
_HAS_HTTPCORE = _is_installed("httpcore")
_HAS_HTTPX = _is_installed("httpx")
_HAS_HF_HUB = _is_installed("huggingface_hub")
_HAS_TENVAD = _is_installed("ten_vad")
_HAS_PYDUB = _is_installed("pydub")

if not _HAS_PYSIDE6:
    _pyside_mock = MagicMock()
    _pyside_mock.QtCore = MagicMock()
    _pyside_mock.QtCore.QObject = type("QObject", (), {})
    _pyside_mock.QtCore.QThread = type("QThread", (), {})
    _pyside_mock.QtCore.Signal = MagicMock(return_value=MagicMock())
    _pyside_mock.QtCore.QLocale = MagicMock()
    _pyside_mock.QtCore.Qt = MagicMock()
    _pyside_mock.QtCore.Slot = lambda *a, **kw: lambda f: f
    _pyside_mock.QtCore.QSettings = MagicMock()
    _pyside_mock.QtCore.QTimer = MagicMock()
    _pyside_mock.QtCore.QEvent = MagicMock()
    _pyside_mock.QtCore.QCoreApplication = MagicMock()
    _pyside_mock.QtCore.QThreadPool = MagicMock()
    _pyside_mock.QtCore.QThreadPool.globalInstance = MagicMock()
    _pyside_mock.QtGui.QIcon = MagicMock()
    _pyside_mock.QtGui.QPixmap = MagicMock()
    _pyside_mock.QtGui.QGuiApplication = MagicMock()
    _pyside_mock.QtGui.QTextCursor = MagicMock()
    _pyside_mock.QtWidgets.QApplication = MagicMock()
    _pyside_mock.QtWidgets.QMainWindow = type("QMainWindow", (), {})
    _pyside_mock.QtWidgets.QWidget = type("QWidget", (), {})
    _pyside_mock.QtWidgets.QFileDialog = MagicMock()
    _pyside_mock.QtWidgets.QMessageBox = MagicMock()
    _pyside_mock.QtWidgets.QLabel = MagicMock()
    _pyside_mock.QtWidgets.QVBoxLayout = MagicMock()
    _pyside_mock.QtWidgets.QPushButton = MagicMock()

    sys.modules["PySide6"] = _pyside_mock
    sys.modules["PySide6.QtCore"] = _pyside_mock.QtCore
    sys.modules["PySide6.QtGui"] = _pyside_mock.QtGui
    sys.modules["PySide6.QtWidgets"] = _pyside_mock.QtWidgets

# Exception base classes for isinstance() checks in excepts.py
if not _HAS_TENACITY:
    _m = MagicMock()
    _m.RetryError = type("RetryError", (Exception,), {})
    sys.modules["tenacity"] = _m

if not _HAS_OPENAI:
    _m = MagicMock()
    for _n in ("AuthenticationError", "PermissionDeniedError", "NotFoundError",
               "BadRequestError", "RateLimitError", "APIConnectionError",
               "APIError", "ContentFilterFinishReasonError", "InternalServerError",
               "LengthFinishReasonError", "UnprocessableEntityError"):
        setattr(_m, _n, type(_n, (Exception,), {}))
    sys.modules["openai"] = _m

def _make_pkg(name, attrs=None):
    """Create a mock package that supports subpackage imports."""
    import types
    m = types.ModuleType(name)
    if attrs:
        for k, v in attrs.items():
            setattr(m, k, v)
    return m


if not _HAS_DEEPGRAM:
    # Full subpackage chain for: from deepgram.clients.common.v1.errors import DeepgramApiError
    sys.modules["deepgram.clients.common.v1.errors"] = _make_pkg(
        "deepgram.clients.common.v1.errors",
        {"DeepgramApiError": type("DeepgramApiError", (Exception,), {})}
    )
    for _pkg in ("deepgram.clients.common.v1", "deepgram.clients.common", "deepgram.clients"):
        sys.modules[_pkg] = _make_pkg(_pkg)
    sys.modules["deepgram"] = _make_pkg("deepgram")

if not _HAS_ELEVENLABS:
    sys.modules["elevenlabs.core"] = _make_pkg(
        "elevenlabs.core",
        {"ApiError": type("ApiError_11", (Exception,), {})}
    )
    sys.modules["elevenlabs"] = _make_pkg("elevenlabs")

if not _HAS_AIOHTTP:
    _ce = MagicMock()
    _ce.ClientProxyConnectionError = type("ClientProxyConnectionError", (Exception,), {})
    sys.modules["aiohttp.client_exceptions"] = _ce
    _a = MagicMock()
    _a.client_exceptions = _ce
    sys.modules["aiohttp"] = _a

if not _HAS_HTTPCORE:
    _m = MagicMock()
    for _n in ("ConnectTimeout", "ConnectError", "ReadError"):
        setattr(_m, _n, type(_n, (Exception,), {}))
    sys.modules["httpcore"] = _m

if not _HAS_HTTPX:
    _m = MagicMock()
    for _n in ("ProxyError", "ConnectError", "ConnectTimeout", "ReadError",
               "InvalidURL", "LocalProtocolError", "ProtocolError",
               "TooManyRedirects", "UnsupportedProtocol"):
        setattr(_m, _n, type(_n, (Exception,), {}))
    sys.modules["httpx"] = _m

if not _HAS_HF_HUB:
    sys.modules["huggingface_hub"] = MagicMock()

if not _HAS_TENVAD:
    _m = MagicMock()
    _m.TenVad = MagicMock()
    _m.VadOptions = MagicMock()
    sys.modules["ten_vad"] = _m

if not _HAS_PYDUB:
    _m = MagicMock()
    sys.modules["pydub"] = _m
    _ms = MagicMock()
    sys.modules["pydub.playback"] = _ms

```

## /tests/omnivoice-qwentts.py

```py path="/tests/omnivoice-qwentts.py" 
"""
from omnivoice import OmniVoice
import soundfile as sf
import torch

model = OmniVoice.from_pretrained(
    "./models/models--k2-fsa--OmniVoice",
    #device_map="cuda:0",
    #dtype=torch.float16
)
# Apple Silicon users: use device_map="mps" instead
# Intel Arc GPU users: use device_map="xpu" instead

audio = model.generate(
    text="Hello, this is a test of zero-shot voice cloning.",
    ref_audio="cosy.wav",
    ref_text="希望你以后,能够做的比我还好哟!",
) # audio is a list of `np.ndarray` with shape (T,) at 24 kHz.

# If you don't want to input `ref_text` manually, you can directly omit the `ref_text`.
# The model will use Whisper ASR to auto-transcribe it.

sf.write("out0.wav", audio[0], 24000)


import sys
sys.exit(1)
"""
from videotrans.util.help_role import get_qwenttslocal_rolelist
import torch
from pathlib import Path
import traceback, json
from typing import Tuple, Union
from videotrans.configure.config import logger,ROOT_DIR
import soundfile as sf


from qwen_tts import Qwen3TTSModel
CUSTOM_VOICE= {"Vivian", "Serena", "Uncle_fu", "Dylan", "Eric", "Ryan", "Aiden", "Ono_anna", "Sohee"}



atten=None
device_map = 'cpu'
dtype=torch.float32

BASE_OBJ=None
CUSTOM_OBJ=None

   
BASE_OBJ=Qwen3TTSModel.from_pretrained(
    f"{ROOT_DIR}/models/models--Qwen--Qwen3-TTS-12Hz-0.6B-Base",
    device_map=device_map,
    dtype=dtype,
    attn_implementation=atten
)
kw={
    "text":"你好啊朋友们,要天天开心哦!",
    "language":"Chinese",
    "ref_audio":"./f5-tts/cosy.wav",
}
kw['ref_text']="希望你以后,能够做的比我还好哟!"
wavs, sr = BASE_OBJ.generate_voice_clone(**kw)
sf.write("ceshi2.wav", wavs[0], sr)


from qwen_asr import Qwen3ASRModel
model = Qwen3ASRModel.from_pretrained(
            f"./models/models--Qwen--Qwen3-ASR-0.6B",
            max_inference_batch_size=8,
            # Batch size limit for inference. -1 means unlimited. Smaller values can help avoid OOM.
            max_new_tokens=2048,  # Maximum number of tokens to generate. Set a larger value for long audio input.
        )
results = model.transcribe(
                audio=["10.wav"],
                language=[None],  # can also be set to None for automatic language detection
                return_time_stamps=False,
                #context=hotword.split(',') if hotword else []
            )
print(results)

```

## /tests/test_actions_split.py

```py path="/tests/test_actions_split.py" 
import importlib
import inspect

import pytest


class TestActionsSplitImports:
    def test_actions_check_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_check')
        assert hasattr(mod, 'WinActionCheckMixin')

    def test_actions_config_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_config')
        assert hasattr(mod, 'WinActionConfigMixin')

    def test_actions_task_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_task')
        assert hasattr(mod, 'WinActionTaskMixin')

    def test_actions_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions')
        assert hasattr(mod, 'WinAction')

    def test_actions_base_mode_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_base_mode')
        assert hasattr(mod, 'WinActionBaseModeMixin')

    def test_actions_base_file_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_base_file')
        assert hasattr(mod, 'WinActionBaseFileMixin')

    def test_actions_base_misc_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_base_misc')
        assert hasattr(mod, 'WinActionBaseMiscMixin')

    def test_actions_base_importable(self):
        mod = importlib.import_module('videotrans.mainwin._actions_base')
        assert hasattr(mod, 'WinActionBase')


class TestActionsClassHierarchy:
    def test_winaction_inherits_winactionbase(self):
        from videotrans.mainwin._actions import WinAction
        from videotrans.mainwin._actions_base import WinActionBase
        assert issubclass(WinAction, WinActionBase)

    def test_winaction_inherits_mixins(self):
        from videotrans.mainwin._actions import WinAction
        from videotrans.mainwin._actions_check import WinActionCheckMixin
        from videotrans.mainwin._actions_config import WinActionConfigMixin
        from videotrans.mainwin._actions_task import WinActionTaskMixin
        assert issubclass(WinAction, WinActionCheckMixin)
        assert issubclass(WinAction, WinActionConfigMixin)
        assert issubclass(WinAction, WinActionTaskMixin)

    def test_winactionbase_inherits_mixins(self):
        from videotrans.mainwin._actions_base import WinActionBase
        from videotrans.mainwin._actions_base_mode import WinActionBaseModeMixin
        from videotrans.mainwin._actions_base_file import WinActionBaseFileMixin
        from videotrans.mainwin._actions_base_misc import WinActionBaseMiscMixin
        assert issubclass(WinActionBase, WinActionBaseModeMixin)
        assert issubclass(WinActionBase, WinActionBaseFileMixin)
        assert issubclass(WinActionBase, WinActionBaseMiscMixin)


class TestActionsMethods:
    EXPECTED_WINACTION_METHODS = {
        '_reset', 'set_djs_timeout', 'delete_process', 'import_sub_fun',
        'set_translate_type', 'set_subtitle_type', 'shound_translate', 'check_tts',
        'check_reccogn', 'check_output', 'check_name_length', 'check_start',
        'show_xxl_select', 'show_cpp_select', 'recogn_type_change', 'model_type_change',
        'tts_type_change', 'set_voice_role',
        'create_btns', 'retry', 'add_process_btn', 'set_process_btn_text',
        'update_status', 'update_data', '_check_all_done',
    }

    EXPECTED_WINACTIONBASE_METHODS = {
        'set_biaozhun', 'set_tiquzimu', 'toggle_adv', 'hide_show_element',
        'set_mode', '_disabled_button', 'disabled_widget',
        'get_mp4', 'get_save_dir', 'get_background', 'change_proxy',
        '_test_proxy', 'proxy_alert', 'clearcache', '_clean_dir',
        'about', 'check_cuda', 'check_voice_autorate', 'check_video_autorate',
        'check_txt', 'cuda_isok', 'listen_voice_fun', 'show_listen_btn',
        'check_name', 'lawalert', 'open_url',
    }

    def test_winaction_has_expected_methods(self):
        from videotrans.mainwin._actions import WinAction
        actual = {m for m in dir(WinAction) if not m.startswith('__')}
        missing = self.EXPECTED_WINACTION_METHODS - actual
        assert not missing, f"WinAction missing methods: {missing}"

    def test_winactionbase_has_expected_methods(self):
        from videotrans.mainwin._actions_base import WinActionBase
        actual = {m for m in dir(WinActionBase) if not m.startswith('__')}
        missing = self.EXPECTED_WINACTIONBASE_METHODS - actual
        assert not missing, f"WinActionBase missing methods: {missing}"

    def test_winaction_method_count(self):
        from videotrans.mainwin._actions import WinAction
        user_methods = [
            m for m in dir(WinAction)
            if not m.startswith('_') or m in ('_reset', '_check_all_done')
        ]
        user_methods = [
            m for m in user_methods
            if callable(getattr(WinAction, m, None))
        ]
        assert len(user_methods) >= 23, f"Expected >= 23 user methods, got {len(user_methods)}"

    def test_winactionbase_method_count(self):
        from videotrans.mainwin._actions_base import WinActionBase
        user_methods = [
            m for m in dir(WinActionBase)
            if not m.startswith('__')
            and callable(getattr(WinActionBase, m, None))
        ]
        assert len(user_methods) >= 24, f"Expected >= 24 user methods, got {len(user_methods)}"

```

## /tests/test_base_recogn.py

```py path="/tests/test_base_recogn.py" 
from videotrans.recognition._base import BaseRecogn
from videotrans.task.taskcfg import SrtItem
from videotrans.configure.config import settings


def _make_srt(text, start, end, line=1):
    return SrtItem(
        text=text, start_time=start, end_time=end,
        startraw=f"00:00:{start // 1000:02d},{start % 1000:03d}",
        endraw=f"00:00:{end // 1000:02d},{end % 1000:03d}",
        time=f"00:00:{start // 1000:02d},{start % 1000:03d} --> 00:00:{end // 1000:02d},{end % 1000:03d}",
        line=line,
    )


class TestBaseRecognPostInit:
    def test_cjk_language_join_word_flag(self):
        rec = BaseRecogn(detect_language="zh-cn")
        assert rec.join_word_flag == ""
        assert rec.is_cjk is True
        assert rec.maxlen > 0

    def test_japanese_sets_cjk(self):
        rec = BaseRecogn(detect_language="ja")
        assert rec.is_cjk is True

    def test_english_not_cjk(self):
        rec = BaseRecogn(detect_language="en")
        assert rec.is_cjk is False
        assert rec.join_word_flag == " "

    def test_device_from_cuda(self):
        rec1 = BaseRecogn(is_cuda=True)
        assert rec1.device == "cuda"
        rec2 = BaseRecogn(is_cuda=False)
        assert rec2.device == "cpu"

    def test_flag_initialization(self):
        rec = BaseRecogn(detect_language="en")
        assert isinstance(rec.flag, list)
        assert len(rec.flag) > 0

    def test_defaults(self):
        rec = BaseRecogn()
        assert rec.recogn_type == 0
        assert rec.is_cuda is False
        assert rec.subtitle_type == 0
        assert rec.max_speakers == -1
        assert rec.llm_post is False
        assert rec.recogn2pass is False


class TestPostFix:
    def test_removes_punctuation_only_lines(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [
            _make_srt("Hello", 0, 1000, 1),
            _make_srt("...", 1000, 2000, 2),
            _make_srt("World", 2000, 3000, 3),
        ]
        result = rec._post_fix(subs)
        texts = [it["text"] for it in result]
        assert "Hello" in texts
        assert "World" in texts

    def test_renumbers_lines(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [
            _make_srt("A", 0, 1000, 5),
            _make_srt("", 1000, 2000, 6),
            _make_srt("B", 2000, 3000, 7),
        ]
        result = rec._post_fix(subs)
        assert len(result) == 2
        assert result[0]["line"] == 1
        assert result[1]["line"] == 2

    def test_fixes_overlapping_timestamps(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [
            _make_srt("First", 0, 2000, 1),
            _make_srt("Second", 1500, 3000, 2),
        ]
        result = rec._post_fix(subs)
        assert result[0]["end_time"] == result[1]["start_time"]

    def test_recogn2pass_skips_merge(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        rec.recogn2pass = True
        subs = [_make_srt("Hello", 0, 1000, 1)]
        result = rec._post_fix(subs)
        assert len(result) == 1


class TestMergeSubPipeline:
    """Test merge pipeline with explicit settings to make tests deterministic."""

    def test_single_element_passes_through(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [_make_srt("Hello world", 0, 3000, 1)]
        result = rec._merge_sub(subs)
        # _merge_sub should return at least the input
        assert len(result) >= 1
        assert "Hello" in result[0]["text"]

    def test_phase1_keeps_long_items(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [
            _make_srt("This is a long enough sentence", 0, 3000, 1),
            _make_srt("Another long sentence here", 4000, 7000, 2),
        ]
        result = rec._phase1_merge_short(subs, min_speech=500, post_srt_raws=[])
        assert len(result) == 2

    def test_phase1_merges_short_to_neighbor(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        subs = [
            _make_srt("Long enough text here.", 0, 3000, 1),
            _make_srt("Tiny", 3100, 3200, 2),
            _make_srt("Some more content.", 3400, 6000, 3),
        ]
        result = rec._phase1_merge_short(subs, min_speech=1000, post_srt_raws=[])
        # The tiny segment should be merged (removed from result)
        assert len(result) <= 2

    def test_phase2_merges_short_first(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        post = [
            _make_srt("Hi", 0, 200, 1),
            _make_srt("How are you today?", 500, 3000, 2),
        ]
        result = rec._phase2_merge_first(post, min_speech=1000)
        assert len(result) == 1

    def test_phase3_merges_short_last(self):
        rec = BaseRecogn(detect_language="en", recogn_type=0)
        post = [
            _make_srt("Long sentence here.", 0, 2000, 1),
            _make_srt("Bye", 2100, 2200, 2),
        ]
        result = rec._phase3_merge_last(post, min_speech=1000)
        assert len(result) == 1

```

## /tests/test_base_trans.py

```py path="/tests/test_base_trans.py" 
import hashlib

from videotrans.translator._base import BaseTrans
from videotrans.task.taskcfg import SrtItem


def _make_srt_item(text, line=1, start=0, end=1000):
    return SrtItem(
        text=text, line=line, start_time=start, end_time=end,
        startraw="00:00:00,000", endraw="00:00:01,000",
        time="00:00:00,000 --> 00:00:01,000",
    )


class TestBaseTransPostInit:
    def test_default_trans_thread(self):
        bt = BaseTrans(text_list=[_make_srt_item("hello")])
        assert bt.trans_thread > 0

    def test_translate_type_default(self):
        bt = BaseTrans(text_list=[_make_srt_item("hello")])
        assert bt.translate_type == 0

    def test_source_target_code(self):
        bt = BaseTrans(
            text_list=[_make_srt_item("bonjour")],
            source_code="fr",
            target_code="zh-cn",
        )
        assert bt.source_code == "fr"
        assert bt.target_code == "zh-cn"

    def test_uuid_set(self):
        bt = BaseTrans(text_list=[_make_srt_item("test")], uuid="test-123")
        assert bt.uuid == "test-123"


class TestBaseTransGetKey:
    def test_key_is_md5(self):
        bt = BaseTrans(
            text_list=[_make_srt_item("hello world")],
            translate_type=0,
            source_code="en",
            target_code="zh-cn",
        )
        key = bt._get_key("hello world")
        assert isinstance(key, str)
        assert len(key) == 32  # MD5 hex length

    def test_different_text_different_keys(self):
        bt = BaseTrans(text_list=[_make_srt_item("test")], source_code="en", target_code="fr")
        k1 = bt._get_key("hello")
        k2 = bt._get_key("world")
        assert k1 != k2

    def test_different_languages_different_keys(self):
        bt1 = BaseTrans(text_list=[_make_srt_item("test")], source_code="en", target_code="fr")
        bt2 = BaseTrans(text_list=[_make_srt_item("test")], source_code="en", target_code="zh-cn")
        assert bt1._get_key("same") != bt2._get_key("same")

    def test_same_key_for_identical_input(self):
        bt = BaseTrans(text_list=[_make_srt_item("test")], source_code="en", target_code="zh-cn")
        k1 = bt._get_key("repeat me")
        k2 = bt._get_key("repeat me")
        assert k1 == k2


class TestBaseTransRunTextChunking:
    def test_chunking_with_thread_count(self):
        items = [_make_srt_item(f"line {i}", line=i + 1) for i in range(10)]
        bt = BaseTrans(text_list=items, source_code="en", target_code="zh-cn")
        # trans_thread controls chunk size
        bt.trans_thread = 3
        chunks = [items[i:i + bt.trans_thread] for i in range(0, len(items), bt.trans_thread)]
        assert len(chunks) == 4  # 10 items, 3 per chunk = 4 chunks
        assert len(chunks[0]) == 3
        assert len(chunks[-1]) == 1

```

## /tests/test_base_tts.py

```py path="/tests/test_base_tts.py" 
from videotrans.tts._base import BaseTTS


class TestBaseTTSCleantts:
    def test_volume_default_zero(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "+0%"}])
        assert btts.volume == "+0%"

    def test_volume_positive(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "+20%"}])
        assert btts.volume == "+20%"

    def test_volume_negative(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "-10%"}])
        assert btts.volume == "-10%"

    def test_volume_plain_number_fixed(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "20%"}])
        assert btts.volume == "+20%"

    def test_volume_decimal(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "+5.5%"}])
        assert btts.volume == "+5.5%"

    def test_rate_default_zero(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "+0%"}])
        assert btts.rate == "+0%"

    def test_rate_positive(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "+30%"}])
        assert btts.rate == "+30%"

    def test_rate_plain_number_fixed(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "15%"}])
        assert btts.rate == "+15%"

    def test_pitch_default_zero(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "+0Hz"}])
        assert btts.pitch == "+0Hz"

    def test_pitch_lowercase_hz(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "+5hz"}])
        assert btts.pitch == "+5Hz"

    def test_pitch_negative(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "-3Hz"}])
        assert btts.pitch == "-3Hz"


class TestBaseTTSGetters:
    def test_get_speed_zero(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "+0%"}])
        assert btts.get_speed() == 1.0

    def test_get_speed_positive(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "+50%"}])
        assert btts.get_speed() == 1.5

    def test_get_speed_negative(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "-20%"}])
        assert btts.get_speed() == 0.8

    def test_get_volume_zero(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "+0%"}])
        assert btts.get_volume() == 1.0

    def test_get_volume_positive(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "+100%"}])
        assert btts.get_volume() == 2.0

    def test_get_volume_negative(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "volume": "-50%"}])
        assert btts.get_volume() == 0.5

    def test_get_pitch_zero_hz_returns_default(self):
        # _cleantts normalizes 'hz' to 'Hz'; get_pitch regex [hz%] misses 'H'
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "+0Hz"}])
        assert btts.get_pitch() == 1.0

    def test_get_pitch_positive_hz_returns_default(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "+12Hz"}])
        assert btts.get_pitch() == 1.0

    def test_get_pitch_negative_hz_returns_default(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "pitch": "-6Hz"}])
        assert btts.get_pitch() == 1.0


class TestBaseTTSInitFields:
    def test_default_values(self):
        btts = BaseTTS(queue_tts=[{"text": "hello", "rate": "+0%", "volume": "+0%", "pitch": "+0Hz"}])
        assert btts.tts_type == 0
        assert btts.play is False
        assert btts.is_test is False
        assert btts.is_cuda is False
        assert btts.len == 1

    def test_queue_tts_deep_copy(self):
        data = [{"text": "hello", "rate": "+0%", "volume": "+0%", "pitch": "+0Hz"}]
        btts = BaseTTS(queue_tts=data)
        # queue_tts is deepcopy'd, modifying original should not affect
        data[0]["text"] = "modified"
        assert btts.queue_tts[0]["text"] == "hello"

    def test_len_matches_queue(self):
        data = [
            {"text": "a", "rate": "+0%", "volume": "+0%", "pitch": "+0Hz"},
            {"text": "b", "rate": "+0%", "volume": "+0%", "pitch": "+0Hz"},
            {"text": "c", "rate": "+0%", "volume": "+0%", "pitch": "+0Hz"},
        ]
        btts = BaseTTS(queue_tts=data)
        assert btts.len == 3

```

## /tests/test_cli.py

```py path="/tests/test_cli.py" 
"""
Comprehensive tests for cli.py — tests all public functions and argument handling.

Uses conftest.py mocks for heavy dependencies (PySide6, torch, etc.)
"""

import logging
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest


# ---------------------------------------------------------------------------
# Import the module under test (only the pure functions, not main())
# ---------------------------------------------------------------------------
from cli import (
    TEXT_DB,
    tr,
    set_lang,
    build_parser,
    validate_task_params,
    build_common_params,
    build_stt_params,
    build_tts_params,
    build_sts_params,
    build_vtv_params,
    setup_logging,
    list_providers,
    list_languages,
    list_models,
    stt_fun,
    tts_fun,
    sts_fun,
    vtv_fun,
)


# ===========================================================================
# Tests for TEXT_DB
# ===========================================================================
class TestTEXTDB:
    def test_text_db_is_dict(self):
        assert isinstance(TEXT_DB, dict)

    def test_all_task_types_have_entries(self):
        for key in ("exec_stt_task", "exec_tts_task", "exec_sts_task", "exec_vtv_task"):
            assert key in TEXT_DB, f"Missing TEXT_DB key: {key}"

    def test_error_messages_exist(self):
        for key in ("err_missing_task", "err_file_not_found",
                     "err_tts_role_required", "err_sts_target_required", "err_vtv_missing"):
            assert key in TEXT_DB, f"Missing error key: {key}"

    def test_zh_and_en_present_in_all_entries(self):
        for key, val in TEXT_DB.items():
            assert "zh" in val, f"TEXT_DB[{key}] missing 'zh'"
            assert "en" in val, f"TEXT_DB[{key}] missing 'en'"

    def test_help_keys_exist(self):
        for key in ("help_task", "help_name", "help_recogn_type",
                     "help_tts_type", "help_translate_type"):
            assert key in TEXT_DB, f"Missing help key: {key}"

    def test_format_placeholders_consistent(self):
        """All entries with {} in zh must also have {} in en."""
        for key, val in TEXT_DB.items():
            zh_count = val.get("zh", "").count("{}")
            en_count = val.get("en", "").count("{}")
            assert zh_count == en_count, (
                f"TEXT_DB[{key}]: zh has {zh_count} placeholders, en has {en_count}"
            )


# ===========================================================================
# Tests for tr() and set_lang()
# ===========================================================================
class TestTrFunction:
    def test_tr_returns_en_by_default(self):
        set_lang("en")
        result = tr("exec_stt_task")
        assert "Speech Transcription" in result

    def test_tr_returns_zh_when_set(self):
        set_lang("zh")
        result = tr("exec_stt_task")
        assert "语音转录" in result
        set_lang("en")  # restore

    def test_tr_with_format_args(self):
        set_lang("en")
        result = tr("process_file", "test.mp4")
        assert "test.mp4" in result

    def test_tr_with_multiple_format_args(self):
        set_lang("en")
        result = tr("err_vtv_missing", "source, target")
        assert "source, target" in result

    def test_tr_unknown_key_returns_key(self):
        set_lang("en")
        result = tr("nonexistent_key")
        assert result == "nonexistent_key"

    def test_tr_fallback_to_en(self):
        """If current lang entry is missing, fall back to 'en'."""
        set_lang("zh")
        # All keys have zh, so test with a hypothetical missing one
        # We can test the fallback logic by checking that en is used as default
        set_lang("en")
        result = tr("exec_stt_task")
        assert "Speech Transcription" in result
        set_lang("en")  # restore


class TestSetLang:
    def test_set_lang_updates_global(self):
        import cli
        original = cli._lang
        set_lang("zh")
        assert cli._lang == "zh"
        set_lang(original)

    def test_set_lang_rejects_invalid(self):
        """set_lang should still accept any string (no validation in current impl)."""
        set_lang("fr")
        import cli
        assert cli._lang == "fr"
        set_lang("en")  # restore


# ===========================================================================
# Tests for build_parser()
# ===========================================================================
class TestBuildParser:
    def test_returns_parser(self):
        parser = build_parser()
        assert isinstance(parser, type(sys.modules["argparse"].ArgumentParser())) or hasattr(parser, 'parse_args')

    def test_task_choices(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4'])
        assert args.task == 'stt'

    def test_task_invalid_choice(self):
        parser = build_parser()
        with pytest.raises(SystemExit):
            parser.parse_args(['--task', 'invalid', '--name', 'test.mp4'])

    def test_name_argument(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', '/path/to/video.mp4'])
        assert args.name == '/path/to/video.mp4'

    def test_list_argument(self):
        parser = build_parser()
        args = parser.parse_args(['--list', 'providers'])
        assert args.list == 'providers'

    def test_list_invalid_choice(self):
        parser = build_parser()
        with pytest.raises(SystemExit):
            parser.parse_args(['--list', 'invalid'])

    def test_version_flag(self, capsys):
        parser = build_parser()
        with pytest.raises(SystemExit) as exc_info:
            parser.parse_args(['--version'])
        assert exc_info.value.code == 0

    def test_stt_defaults(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4'])
        assert args.recogn_type == 0
        assert args.detect_language == 'auto'
        assert args.model_name == 'tiny'
        assert args.cuda is False
        assert args.remove_noise is False
        assert args.enable_diariz is False
        assert args.nums_diariz == -1
        assert args.rephrase == 0
        assert args.fix_punc is False

    def test_tts_defaults(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'tts', '--name', 'test.srt', '--voice_role', 'test'])
        assert args.tts_type == 0
        assert args.voice_rate == '+0%'
        assert args.volume == '+0%'
        assert args.pitch == '+0Hz'
        assert args.voice_autorate is False
        assert args.align_sub_audio is False

    def test_sts_defaults(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'sts', '--name', 'test.srt', '--target_language_code', 'en'])
        assert args.translate_type == 0
        assert args.source_language_code is None

    def test_vtv_defaults(self):
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'vtv', '--name', 'test.mp4',
            '--source_language_code', 'zh-cn', '--target_language_code', 'en'
        ])
        assert args.video_autorate is False
        assert args.is_separate is False
        assert args.recogn2pass is False
        assert args.subtitle_type == 1
        assert args.clear_cache is True

    def test_no_clear_cache_flag(self):
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'vtv', '--name', 'test.mp4',
            '--source_language_code', 'zh-cn', '--target_language_code', 'en',
            '--no-clear-cache'
        ])
        assert args.clear_cache is False

    def test_verbose_flag(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '--verbose'])
        assert args.verbose is True

    def test_quiet_flag(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '-q'])
        assert args.quiet is True

    def test_output_dir(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '--output-dir', '/tmp/out'])
        assert args.output_dir == '/tmp/out'

    def test_log_level(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '--log-level', 'DEBUG'])
        assert args.log_level == 'DEBUG'

    def test_cuda_flag(self):
        parser = build_parser()
        args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '--cuda'])
        assert args.cuda is True

    def test_custom_params(self):
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'stt', '--name', 'test.mp4',
            '--recogn_type', '5',
            '--model_name', 'large-v3',
            '--detect_language', 'ja',
            '--remove_noise',
            '--enable_diariz',
            '--nums_diariz', '3',
            '--rephrase', '1',
            '--fix_punc',
        ])
        assert args.recogn_type == 5
        assert args.model_name == 'large-v3'
        assert args.detect_language == 'ja'
        assert args.remove_noise is True
        assert args.enable_diariz is True
        assert args.nums_diariz == 3
        assert args.rephrase == 1
        assert args.fix_punc is True


# ===========================================================================
# Tests for validate_task_params()
# ===========================================================================
class TestValidateTaskParams:
    def _make_args(self, **overrides):
        """Create a simple namespace for testing (not MagicMock, to avoid attribute issues)."""
        from argparse import Namespace
        defaults = {
            'task': 'stt',
            'name': None,
            'voice_role': None,
            'target_language_code': None,
            'source_language_code': None,
        }
        defaults.update(overrides)
        return Namespace(**defaults)

    def _make_parser(self):
        return build_parser()

    def test_missing_name_raises(self):
        args = self._make_args(task='stt', name=None)
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_nonexistent_file_raises(self):
        args = self._make_args(task='stt', name='/nonexistent/file.mp4')
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_stt_valid(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        args = self._make_args(task='stt', name=str(f))
        parser = self._make_parser()
        # Should not raise
        validate_task_params(args, parser)

    def test_tts_requires_voice_role(self, tmp_path):
        f = tmp_path / "test.srt"
        f.touch()
        args = self._make_args(task='tts', name=str(f), voice_role=None)
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_tts_valid_with_role(self, tmp_path):
        f = tmp_path / "test.srt"
        f.touch()
        args = self._make_args(task='tts', name=str(f), voice_role='test-role')
        parser = self._make_parser()
        validate_task_params(args, parser)

    def test_sts_requires_target_lang(self, tmp_path):
        f = tmp_path / "test.srt"
        f.touch()
        args = self._make_args(task='sts', name=str(f), target_language_code=None)
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_sts_valid_with_target(self, tmp_path):
        f = tmp_path / "test.srt"
        f.touch()
        args = self._make_args(task='sts', name=str(f), target_language_code='en')
        parser = self._make_parser()
        validate_task_params(args, parser)

    def test_vtv_requires_source_and_target(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        args = self._make_args(
            task='vtv', name=str(f),
            source_language_code=None, target_language_code=None
        )
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_vtv_requires_source(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        args = self._make_args(
            task='vtv', name=str(f),
            source_language_code=None, target_language_code='en'
        )
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_vtv_requires_target(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        args = self._make_args(
            task='vtv', name=str(f),
            source_language_code='zh-cn', target_language_code=None
        )
        parser = self._make_parser()
        with pytest.raises(SystemExit):
            validate_task_params(args, parser)

    def test_vtv_valid(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        args = self._make_args(
            task='vtv', name=str(f),
            source_language_code='zh-cn', target_language_code='en'
        )
        parser = self._make_parser()
        validate_task_params(args, parser)


# ===========================================================================
# Tests for parameter builders
# ===========================================================================
class TestBuildSttParams:
    def test_build_stt_params(self):
        args = MagicMock(
            recogn_type=2, detect_language='ja', model_name='large-v3',
            cuda=True, remove_noise=True, enable_diariz=True,
            nums_diariz=3, rephrase=1, fix_punc=True,
        )
        result = build_stt_params(args)
        assert result == {
            "recogn_type": 2,
            "detect_language": "ja",
            "model_name": "large-v3",
            "is_cuda": True,
            "remove_noise": True,
            "enable_diariz": True,
            "nums_diariz": 3,
            "rephrase": 1,
            "fix_punc": True,
        }

    def test_build_stt_params_defaults(self):
        args = MagicMock(
            recogn_type=0, detect_language='auto', model_name='tiny',
            cuda=False, remove_noise=False, enable_diariz=False,
            nums_diariz=-1, rephrase=0, fix_punc=False,
        )
        result = build_stt_params(args)
        assert result["is_cuda"] is False
        assert result["remove_noise"] is False


class TestBuildTTSParams:
    def test_build_tts_params(self):
        args = MagicMock(
            tts_type=3, voice_role='test-role', voice_rate='+20%',
            volume='-10%', pitch='+5Hz', cuda=True,
            voice_autorate=True, align_sub_audio=False,
            target_language_code='en',
        )
        result = build_tts_params(args)
        assert result == {
            "tts_type": 3,
            "voice_role": "test-role",
            "voice_rate": "+20%",
            "volume": "-10%",
            "pitch": "+5Hz",
            "is_cuda": True,
            "voice_autorate": True,
            "align_sub_audio": False,
            "target_language_code": "en",
        }


class TestBuildSTSParams:
    def test_build_sts_params(self):
        args = MagicMock(translate_type=1, source_language_code='zh-cn', target_language_code='en')
        result = build_sts_params(args)
        assert result == {
            "translate_type": 1,
            "source_language_code": "zh-cn",
            "target_language_code": "en",
        }

    def test_build_sts_params_defaults_source_to_auto(self):
        args = MagicMock(translate_type=0, source_language_code=None, target_language_code='ja')
        result = build_sts_params(args)
        assert result["source_language_code"] == "auto"


class TestBuildVTVParams:
    def test_build_vtv_params(self):
        args = MagicMock(
            source_language_code='zh-cn', target_language_code='en',
            recogn_type=0, model_name='large-v3', cuda=True,
            remove_noise=False, enable_diariz=False,
            nums_diariz=-1, rephrase=0, fix_punc=False,
            tts_type=0, voice_role='en-US-GuyNeural',
            voice_rate='+0%', volume='+0%', pitch='+0Hz',
            voice_autorate=True, video_autorate=False,
            align_sub_audio=True,
            translate_type=0,
            is_separate=True, recogn2pass=True,
            subtitle_type=1, clear_cache=True,
        )
        result = build_vtv_params(args)
        assert result["source_language_code"] == "zh-cn"
        assert result["target_language_code"] == "en"
        assert result["is_separate"] is True
        assert result["recogn2pass"] is True
        assert result["subtitle_type"] == 1
        assert result["clear_cache"] is True
        assert result["voice_role"] == "en-US-GuyNeural"
        assert result["recogn_type"] == 0
        assert result["is_cuda"] is True


class TestBuildCommonParams:
    def test_build_common_params_returns_dict_keys(self, tmp_path):
        """Test that build_common_params returns a dict with expected keys."""
        from argparse import Namespace
        video_file = tmp_path / "test_video.mp4"
        video_file.write_bytes(b'\x00' * 100)

        args = Namespace(name=str(video_file))

        # We can't easily test this without mocking the full config system,
        # so we just verify the function signature accepts the right args
        # and that it calls the right dependencies
        with patch('cli.Path') as mock_path, \
             patch('cli.re') as mock_re:
            mock_path.return_value.return_value.exists.return_value = True
            mock_path.return_value.return_value.absolute.return_value.as_posix.return_value = str(video_file)
            mock_path.return_value.return_value.parent.resolve.return_value.as_posix.return_value = str(tmp_path)
            mock_path.return_value.return_value.suffix.lower.return_value = '.mp4'
            mock_path.return_value.return_value.name = 'test_video.mp4'
            mock_path.return_value.return_value.stem = 'test_video'
            mock_re.sub.return_value = 'test_video-mp4'

            # Just verify the function is callable and has the right signature
            assert callable(build_common_params)


# ===========================================================================
# Tests for setup_logging()
# ===========================================================================
class TestSetupLogging:
    def test_setup_logging_default(self):
        # Reset root logger to NOTSET first
        root = logging.getLogger()
        old_level = root.level
        root.setLevel(logging.NOTSET)
        try:
            setup_logging("WARNING")
            assert root.level == logging.WARNING
        finally:
            root.setLevel(old_level)

    def test_setup_logging_debug(self):
        root = logging.getLogger()
        old_level = root.level
        root.setLevel(logging.NOTSET)
        try:
            setup_logging("DEBUG")
            assert root.level == logging.DEBUG
        finally:
            root.setLevel(old_level)

    def test_setup_logging_verbose_overrides(self):
        root = logging.getLogger()
        old_level = root.level
        root.setLevel(logging.NOTSET)
        try:
            setup_logging("WARNING", verbose=True)
            assert root.level == logging.INFO
        finally:
            root.setLevel(old_level)

    def test_setup_logging_quiet_overrides(self):
        root = logging.getLogger()
        old_level = root.level
        root.setLevel(logging.NOTSET)
        try:
            setup_logging("DEBUG", quiet=True)
            assert root.level == logging.ERROR
        finally:
            root.setLevel(old_level)


# ===========================================================================
# Tests for list functions
# ===========================================================================
class TestListProviders:
    def test_list_providers_runs(self, capsys):
        list_providers()
        captured = capsys.readouterr()
        assert "Speech Recognition" in captured.out or "语音识别" in captured.out

    def test_list_providers_shows_indices(self, capsys):
        list_providers()
        captured = capsys.readouterr()
        assert "0 =" in captured.out


class TestListLanguages:
    def test_list_languages_runs(self, capsys):
        list_languages()
        captured = capsys.readouterr()
        assert "Language" in captured.out or "语言" in captured.out

    def test_list_languages_shows_codes(self, capsys):
        list_languages()
        captured = capsys.readouterr()
        assert "en" in captured.out


class TestListModels:
    def test_list_models_runs(self, capsys):
        list_models()
        captured = capsys.readouterr()
        assert "faster-whisper" in captured.out or "Faster" in captured.out

    def test_list_models_shows_tiny(self, capsys):
        list_models()
        captured = capsys.readouterr()
        assert "tiny" in captured.out


# ===========================================================================
# Tests for task execution functions (with mocks)
# ===========================================================================
class TestSttFun:
    def test_stt_fun_is_callable(self):
        """Test that stt_fun is a callable function."""
        assert callable(stt_fun)

    def test_stt_fun_imports(self):
        """Test that stt_fun can be imported and has correct signature."""
        import inspect
        sig = inspect.signature(stt_fun)
        params = list(sig.parameters.keys())
        assert 'params' in params


class TestTtsFun:
    def test_tts_fun_is_callable(self):
        assert callable(tts_fun)

    def test_tts_fun_imports(self):
        import inspect
        sig = inspect.signature(tts_fun)
        params = list(sig.parameters.keys())
        assert 'params' in params


class TestStsFun:
    def test_sts_fun_is_callable(self):
        assert callable(sts_fun)

    def test_sts_fun_imports(self):
        import inspect
        sig = inspect.signature(sts_fun)
        params = list(sig.parameters.keys())
        assert 'params' in params


class TestVtvFun:
    def test_vtv_fun_is_callable(self):
        assert callable(vtv_fun)

    def test_vtv_fun_imports(self):
        import inspect
        sig = inspect.signature(vtv_fun)
        params = list(sig.parameters.keys())
        assert 'params' in params


# ===========================================================================
# Integration tests: argument parsing + validation
# ===========================================================================
class TestArgumentParsingIntegration:
    def test_stt_full_args(self, tmp_path):
        f = tmp_path / "demo.mp4"
        f.touch()
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'stt', '--name', str(f),
            '--recogn_type', '2', '--model_name', 'large-v3',
            '--detect_language', 'ja', '--cuda',
            '--remove_noise', '--enable_diariz', '--nums_diariz', '3',
            '--rephrase', '1', '--fix_punc',
        ])
        assert args.task == 'stt'
        assert args.recogn_type == 2
        assert args.model_name == 'large-v3'
        assert args.cuda is True
        validate_task_params(args, parser)

    def test_tts_full_args(self, tmp_path):
        f = tmp_path / "movie.srt"
        f.touch()
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'tts', '--name', str(f),
            '--tts_type', '3', '--voice_role', 'zh-CN-YunyangNeural',
            '--voice_rate=+20%', '--volume=-10%', '--pitch=+5Hz',
            '--voice_autorate',
        ])
        assert args.task == 'tts'
        assert args.voice_role == 'zh-CN-YunyangNeural'
        assert args.voice_rate == '+20%'
        assert args.volume == '-10%'
        validate_task_params(args, parser)

    def test_sts_full_args(self, tmp_path):
        f = tmp_path / "subs.srt"
        f.touch()
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'sts', '--name', str(f),
            '--target_language_code', 'en',
            '--source_language_code', 'zh-cn',
            '--translate_type', '1',
        ])
        assert args.task == 'sts'
        assert args.target_language_code == 'en'
        validate_task_params(args, parser)

    def test_vtv_full_args(self, tmp_path):
        f = tmp_path / "clip.mp4"
        f.touch()
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'vtv', '--name', str(f),
            '--source_language_code', 'zh-cn', '--target_language_code', 'en',
            '--voice_role', 'en-US-GuyNeural', '--cuda',
            '--is_separate', '--recogn2pass',
            '--subtitle_type', '3', '--no-clear-cache',
        ])
        assert args.task == 'vtv'
        assert args.source_language_code == 'zh-cn'
        assert args.target_language_code == 'en'
        assert args.clear_cache is False
        validate_task_params(args, parser)

    def test_list_providers(self):
        parser = build_parser()
        args = parser.parse_args(['--list', 'providers'])
        assert args.list == 'providers'
        assert args.task is None

    def test_list_languages(self):
        parser = build_parser()
        args = parser.parse_args(['--list', 'languages'])
        assert args.list == 'languages'

    def test_list_models(self):
        parser = build_parser()
        args = parser.parse_args(['--list', 'models'])
        assert args.list == 'models'

    def test_output_dir(self, tmp_path):
        f = tmp_path / "test.mp4"
        f.touch()
        parser = build_parser()
        args = parser.parse_args([
            '--task', 'stt', '--name', str(f),
            '--output-dir', str(tmp_path / 'custom_output'),
        ])
        assert args.output_dir == str(tmp_path / 'custom_output')

    def test_log_levels(self):
        parser = build_parser()
        for level in ('DEBUG', 'INFO', 'WARNING', 'ERROR'):
            args = parser.parse_args(['--task', 'stt', '--name', 'test.mp4', '--log-level', level])
            assert args.log_level == level

```

## /tests/test_config_split.py

```py path="/tests/test_config_split.py" 
# -*- coding: utf-8 -*-
"""Tests that the config.py split preserves all public imports and behaviour."""
from pathlib import Path


class TestConfigSplitImports:
    """Verify every commonly-imported name is reachable from config.py."""

    def test_app_cfg_importable(self):
        from videotrans.configure.config import app_cfg
        assert app_cfg is not None

    def test_settings_importable(self):
        from videotrans.configure.config import settings
        assert settings is not None

    def test_params_importable(self):
        from videotrans.configure.config import params
        assert params is not None

    def test_tr_importable(self):
        from videotrans.configure.config import tr
        assert callable(tr)

    def test_root_dir_importable(self):
        from videotrans.configure.config import ROOT_DIR
        assert isinstance(ROOT_DIR, str)
        assert ROOT_DIR  # non-empty

    def test_logger_importable(self):
        from videotrans.configure.config import logger
        import logging
        assert isinstance(logger, logging.Logger)

    def test_home_dir_importable(self):
        from videotrans.configure.config import HOME_DIR
        assert isinstance(HOME_DIR, str)
        assert HOME_DIR

    def test_temp_root_importable(self):
        from videotrans.configure.config import TEMP_ROOT
        assert isinstance(TEMP_ROOT, str)
        assert TEMP_ROOT.endswith('/tmp')

    def test_temp_dir_importable(self):
        from videotrans.configure.config import TEMP_DIR
        assert isinstance(TEMP_DIR, str)

    def test_translate_cache_importable(self):
        from videotrans.configure.config import TRANSLATE_CACHE
        assert isinstance(TRANSLATE_CACHE, str)
        assert 'translate_cache' in TRANSLATE_CACHE


class TestTrFunction:
    """Verify tr() behaves correctly."""

    def test_tr_returns_key_on_missing(self):
        from videotrans.configure.config import tr
        result = tr("totally_nonexistent_key_xyz_12345")
        assert result == "totally_nonexistent_key_xyz_12345"

    def test_tr_accepts_list(self):
        from videotrans.configure.config import tr
        result = tr(["nonexistent_a", "nonexistent_b"])
        assert isinstance(result, str)


class TestPushQueue:
    """Verify push_queue is accessible."""

    def test_push_queue_exists(self):
        from videotrans.configure.config import push_queue
        assert callable(push_queue)


class TestAppCfgClass:
    """Verify AppCfg dataclass works."""

    def test_app_cfg_has_expected_attrs(self):
        from videotrans.configure.config import app_cfg
        assert hasattr(app_cfg, 'exit_soft')
        assert hasattr(app_cfg, 'stoped_uuid_set')
        assert hasattr(app_cfg, 'current_status')
        assert hasattr(app_cfg, 'prepare_queue')

    def test_app_cfg_rm_uuid(self):
        from videotrans.configure.config import app_cfg
        app_cfg.stoped_uuid_set.add("test-uuid-123")
        app_cfg.rm_uuid("test-uuid-123")
        assert "test-uuid-123" not in app_cfg.stoped_uuid_set

    def test_app_cfg_rm_uuid_none(self):
        from videotrans.configure.config import app_cfg
        app_cfg.rm_uuid(None)  # should not raise


class TestAppSettingsClass:
    """Verify AppSettings dataclass works."""

    def test_settings_has_expected_attrs(self):
        from videotrans.configure.config import settings
        assert hasattr(settings, 'homedir')
        assert hasattr(settings, 'lang')
        assert hasattr(settings, 'proxy')

    def test_settings_to_dict(self):
        from videotrans.configure.config import settings
        d = settings.to_dict()
        assert isinstance(d, dict)
        assert 'homedir' in d


class TestAppParamsClass:
    """Verify AppParams dataclass works."""

    def test_params_has_expected_attrs(self):
        from videotrans.configure.config import params
        assert hasattr(params, 'chatgpt_api')
        assert hasattr(params, 'chatgpt_key')
        assert hasattr(params, 'deepl_authkey')

    def test_params_to_dict(self):
        from videotrans.configure.config import params
        d = params.to_dict()
        assert isinstance(d, dict)
        assert 'chatgpt_api' in d


class TestInitRun:
    """Verify init_run is callable."""

    def test_init_run_exists(self):
        from videotrans.configure.config import init_run
        assert callable(init_run)


class TestAccessibleFunctions:
    """Verify internal functions are accessible through config module."""

    def test_push_queue_accessible(self):
        from videotrans.configure.config import push_queue
        assert callable(push_queue)

    def test_update_logging_level_accessible(self):
        from videotrans.configure.config import update_logging_level
        assert callable(update_logging_level)

    def test_set_env_accessible(self):
        from videotrans.configure.config import _set_env
        assert callable(_set_env)

    def test_set_logs_accessible(self):
        from videotrans.configure.config import _set_logs
        assert callable(_set_logs)


class TestModuleLevelConstants:
    """Verify constants match expected values."""

    def test_root_dir_not_empty(self):
        from videotrans.configure.config import ROOT_DIR
        assert len(ROOT_DIR) > 0

    def test_temp_root_format(self):
        from videotrans.configure.config import ROOT_DIR, TEMP_ROOT
        assert TEMP_ROOT == f"{ROOT_DIR}/tmp"

    def test_logs_dir_format(self):
        from videotrans.configure.config import ROOT_DIR, LOGS_DIR
        assert LOGS_DIR == f"{ROOT_DIR}/logs"

    def test_is_frozen_is_bool(self):
        from videotrans.configure.config import IS_FROZEN
        assert isinstance(IS_FROZEN, bool)

    def test_sys_tmp_is_string(self):
        from videotrans.configure.config import SYS_TMP
        assert isinstance(SYS_TMP, str)

    def test_log_dir_exists(self):
        from videotrans.configure.config import LOGS_DIR
        assert Path(LOGS_DIR).exists()

    def test_models_dir_exists(self):
        from videotrans.configure.config import ROOT_DIR
        assert Path(f"{ROOT_DIR}/models").exists()

```

## /tests/test_cuda.py

```py path="/tests/test_cuda.py" 
import pytest

torch = pytest.importorskip("torch", reason="torch not installed")

# 检查 CUDA 是否可用
print(f"\nCUDA是否可用: {'是 Yes' if torch.cuda.is_available() else '否 No'}")

# 如果 CUDA 可用,再检查 CUDNN
if torch.cuda.is_available():
    print(f"\ncuDNN 是否可用: {'是 Yes' if torch.backends.cudnn.is_available() else '否 No'}")
    print(f"\ncuDNN 版本号: {torch.backends.cudnn.version()}\n\n")


def test_cuda_available():
    # 不强制要求 CUDA,仅打印信息
    has_cuda = torch.cuda.is_available()
    print(f"CUDA available: {has_cuda}")

```

## /tests/test_job_helpers.py

```py path="/tests/test_job_helpers.py" 
"""
Tests for videotrans/task/job.py helper functions.
These are pure functions that don't require Qt or heavy deps.
"""

from videotrans.task.job import _get_type_name, get_recogn_type, get_tanslate_type, get_tts_type


class TestGetTypeName:
    def test_valid_index(self):
        name_list = ["A", "B", "C"]
        assert _get_type_name(0, name_list) == "A"
        assert _get_type_name(1, name_list) == "B"
        assert _get_type_name(2, name_list) == "C"

    def test_none_index_returns_dash(self):
        assert _get_type_name(None, ["A", "B"]) == "-"

    def test_out_of_range_returns_dash(self):
        assert _get_type_name(5, ["A", "B"]) == "-"

    def test_negative_index_returns_last_element(self):
        # Python list[-1] returns the last element, not IndexError
        assert _get_type_name(-1, ["A", "B"]) == "B"


class TestGetRecognType:
    def test_returns_dash_for_none(self):
        assert get_recogn_type(None) == "-"

    def test_returns_name_for_valid_index(self):
        result = get_recogn_type(0)
        assert result != "-"
        assert isinstance(result, str)

    def test_index_too_large(self):
        assert get_recogn_type(999) == "-"


class TestGetTranslateType:
    def test_returns_dash_for_none(self):
        assert get_tanslate_type(None) == "-"

    def test_returns_name_for_valid_index(self):
        result = get_tanslate_type(0)
        assert result != "-"
        assert isinstance(result, str)


class TestGetTTSType:
    def test_returns_dash_for_none(self):
        assert get_tts_type(None) == "-"

    def test_returns_name_for_valid_index(self):
        result = get_tts_type(0)
        assert result != "-"
        assert isinstance(result, str)

```

## /videotrans/component/__init__.py

```py path="/videotrans/component/__init__.py" 

```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!