claudeandClaude Opus 5 7e2cee02e5 Survive a ComfyUI crash, retry clips the model cuts short
Crash recovery
  ComfyUI's queue and history do not persist across a restart, so a prompt
  submitted before a crash exists nowhere afterwards. The old poll loop waited
  on that dead prompt for ever, and its error branch skipped the backoff, so it
  hammered the closed port four times a second printing "Poll error" on every
  attempt.

  run_workflow() replaces wait_for_completion(). It reports once when the
  backend goes away and once when it returns, polls every 3s in between, and on
  reconnect checks both /history and /queue: if the prompt is in neither, the
  queue did not survive and the clip is submitted again. When it cannot tell, it
  assumes the job is still alive — resubmitting a running job would waste a
  generation.

  It also starts ComfyUI itself, via the script named by [comfy] start_command,
  in a new terminal window. Guarded by a 90s cooldown and 3 attempts per run so
  a backend that dies during startup cannot spawn an endless stream of windows;
  the window pauses on exit so a failed start leaves its error on screen. Falls
  back to a background launch with a log file when there is no display. Set
  [comfy] autostart = no, or pass --no-comfy-autostart, to turn it off.

Clips cut short mid-word
  About one clip in twelve comes back truncated: the model stops before the line
  is finished. Measured on raw ComfyUI output, 6 of 80 recent clips, and 15 of
  472 in one episode. It is invisible in the text but obvious in the audio — a
  finished clip decays to about -65 dB, a cut one stops at about -16 dB.

  Truncated clips are now regenerated with a distant seed, up to
  [tts] truncation_retries extra attempts, and a run that still has bad clips
  ends by printing them as a ready-made --regen list. --check-clips reports the
  same for a job already on disk.

  Worth recording what this is NOT: the trailing-silence trimming added earlier
  was suspected, and cleared. It only cuts silence that runs to end of file, so
  a pause between words never triggers it — verified against six synthetic cases
  including a 1.2s mid-clip pause, and against real clips containing pauses. The
  truncation is present in ComfyUI's own output before this script touches it.

settings.ini
  New: [tts] truncation_retries, [comfy] autostart and start_command, all
  documented in place. The editor renders yes/no keys as a dropdown.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:12:33 +02:00

lektor_ai_comfy — Polish AI Dubbing Tool
=========================================

FULL DOCUMENTATION: see docs/
  docs/README.md           start here — overview and quick start
  docs/requirements.md     hardware, software, dependencies
  docs/installation.md     installing ComfyUI + the TTS engine
  docs/usage.md            every CLI flag, working modes, recipes
  docs/architecture.md     how the pipeline works internally
  docs/troubleshooting.md  diagnosed failure modes and their fixes

Installer: ./install_comfyui.sh   (see docs/installation.md)

This file remains as a condensed quick reference.


Converts SRT subtitles to AI voiceover (lektor style) using ComfyUI + Higgs Audio V3 TTS,
then mixes the result with the original video audio into a two-track MKV.
All subtitle tracks in the source video are carried over to the output.

Requirements
------------
- ComfyUI running at http://127.0.0.1:8189 with TTS-Audio-Suite custom node
- Model: higgs-audio-v3-tts-4b (bosonai/higgs-audio-v3)
- ffmpeg and ffprobe available on PATH
- Voice reference: a WAV file + matching .txt transcript in the voice directory
  (default: voices/gudowski_short.wav + voices/gudowski_short.txt)

Settings
--------
Defaults live in settings.ini next to the script:
  voice, speed, temperature, max_tokens, strip_leading_ms, trailing_fade_ms,
  orig_volume, tts_volume, output_dir, comfy_url

Precedence: command-line flag > settings.ini > built-in default.
Use --settings FILE to select a different profile.
See docs/usage.md for the full description.


Basic Usage
-----------

Full run with an external SRT file:

    python3 lektor_ai_comfy.py input/film.srt input/film.mkv

Use subtitles embedded in the video (lists tracks, prompts for a choice):

    python3 lektor_ai_comfy.py input/film.mkv --embedded-subs

Process only a time range (extracts a video fragment automatically):

    python3 lektor_ai_comfy.py input/film.srt input/film.mkv --start 01:20 --end 05:40


Subtitle Handling
-----------------
Both external SRT files and embedded subtitle tracks go through the same cleaning:

- HTML tags stripped:          <i>, </i>, <b>, <font color="…">, etc.
- ASS override codes stripped: {\an8}, {\pos(x,y)}, {\c&H…&}, etc.
- ALL-CAPS blocks lowercased and converted to sentence case
- Sentence fragments starting with a lowercase letter are capitalised
- Missing terminal punctuation added (… or . depending on context)
- Dialogue lines (prefixed with -) are split into separate clips with proportional timing

With --embedded-subs the extracted subtitles are saved to the job folder as embedded_sub.srt
so you can inspect them before or after the run.

The final MKV preserves all subtitle tracks from the source video — text-based
(SRT, ASS, SSA, WebVTT) and image-based (PGS Blu-ray, VOBSUB) alike — using
stream copy with no conversion.


Output Files
------------
Everything belonging to one video is kept together in a job folder named after
the video file. Only the finished video is written to the top level.

output/
  clips/
    <video name>/      — job folder for one video:
      clip_NNNN.flac   — individual TTS clips, one per block (lossless)
      tts_input.srt    — processed subtitle text sent to TTS (inspect to verify)
      tts_assembled.flac — all clips assembled with correct timing (lossless)
      embedded_sub.srt — extracted embedded subtitle (only with --embedded-subs)
      video_fragment.mkv — extracted video segment (only when --end is used)
  {stem}_lektor.mkv    — final video with two audio tracks + all original subtitles:
                           Track 0 — Original (source audio, stream-copied as-is)
                           Track 1 — Polish lektor (TTS + original mixed,
                                     AAC 192k stereo)

Each run prints the job folder it uses. tts_input.srt and the clips are a
matched pair (block N is clip_NNNN.flac), which is why they live together —
previously each new job overwrote the last one's, so --rebuild on an older
film silently paired its clips with the newest film's text.


All Options
-----------

  srt                   Input SRT subtitle file (omit when using --embedded-subs)
  video                 Input video file (MKV, MP4, ...)

  --embedded-subs       Use a subtitle track embedded in the video instead of an
                        external SRT file. The script lists all subtitle tracks
                        found in the video and prompts you to choose one.
                        The video path can be given as the only positional argument:
                            python3 lektor_ai_comfy.py film.mkv --embedded-subs

  --start MM:SS         Start time of the range to process (default: 00:00)
  --end MM:SS           End time of the range to process (default: end of file)

  --voice NAME|PATH     Voice prompt: name of a subdirectory under the voice prompts
                        directory, or a direct path to a WAV file.
                        The directory must contain a .wav and matching .txt transcript.
                        (default: gudowski_short)

  --speed FACTOR        Per-clip playback speed via atempo (default: 1.2)
                        Values above 1.0 speed up speech; 1.0 = no change.

  --seed N              Base generation seed. 0 = random each run. (default: 0)
                        A fixed seed produces deterministic output.

  --temperature T       TTS sampling temperature (default: 0.9)
                        Lower = more deterministic; higher = more expressive but
                        risks hallucination (1.0 can cause the model to recite
                        the reference text instead of the subtitle).

  --max-tokens N        Max audio tokens the model may generate per clip (default: 4096)
                        Raise this if long clips are cut off mid-word.

  --orig-volume 0-1     Volume of the original audio in the Polish lektor mix (default: 0.60)
  --tts-volume 0-2      Volume of the TTS voiceover in the Polish lektor mix (default: 1.8)
                        Note: these affect Track 1 only. Track 0 is always the raw original.

  --strip-leading-ms MS Strip N milliseconds from the start of each clip to remove
                        leading breath or noise artifacts (default: 0, try 80-150)

  --trailing-fade-ms MS Fade out the last N milliseconds of each clip to suppress
                        tail pops or noise (default: 80, set 0 to disable)

  --output-dir DIR      Output directory (default: output/)

  --comfy-url URL       ComfyUI server URL (default: http://127.0.0.1:8189)

  --skip-tts AUDIO      Skip TTS generation entirely and use this pre-assembled
                        audio file instead. Goes straight to video merge.


Recovery and Fixing Bad Clips
------------------------------

Resume a crashed run from block N (reuses existing clips 1..N-1):

    python3 lektor_ai_comfy.py input/film.srt input/film.mkv --resume-from 47

Regenerate specific clips by block number (same numbering as tts_input.srt):

    python3 lektor_ai_comfy.py input/film.mkv --regen 12 22 41

    Only replaces the selected clip_NNNN.flac files in the job folder — does not reassemble
    or rebuild the video. Run --rebuild afterwards to apply the changes.
    Useful for fixing hallucinations, mumbling, or cut-off clips.

Reassemble audio from all existing clips and rebuild the final video
(no TTS generation — use this after --regen, or to adjust volumes):

    python3 lektor_ai_comfy.py input/film.mkv --rebuild

    python3 lektor_ai_comfy.py input/film.mkv --rebuild --orig-volume 0.5 --tts-volume 2.0

    For --rebuild and --regen the SRT file is not needed — just pass the video path.
    When --end is given, rebuilds from the same video fragment as the original run
    (re-extracts it if needed). Without --end, uses the full video file.


Tips
----

- Check output/clips/<video name>/tts_input.srt to see the exact text sent to TTS.
  Block numbers in that file match the clip_NNNN.flac filenames.

- If a clip sounds wrong (hallucination, wrong language, cut off), note its block
  number from tts_input.srt and use --regen N to replace just that clip,
  then run --rebuild to reassemble the video.

- If ComfyUI crashes mid-run, note the last completed block number and use
  --resume-from N to continue without regenerating finished clips.

- The final MP4 has two audio tracks. Most players let you switch between them:
    Track 0 — Original soundtrack
    Track 1 — Polish lektor (AI voiceover mixed with quiet original)

- Track 0 (Original) is stream-copied without re-encoding, so source quality is preserved.
  The MKV container supports all common audio codecs (AAC, AC3, EAC3, DTS, TrueHD),
  so no workarounds are needed for the original audio track.

- Videos with audio starting at a non-zero PTS offset (common in MKV rips) are handled
  automatically — the original audio background in the lektor mix is normalised so it
  stays in sync with the video.

- The lektor track (Track 1) is stereo by design, while Track 0 keeps the original
  5.1 untouched. ffmpeg's AAC encoder does not signal a 5.1 channel layout — it ends up
  tagged "unknown" in every container, and players that cannot route the channels play
  the track as silence. Stereo is tagged correctly and plays everywhere. It also spends
  the 192k on two channels instead of spreading it thinly across six. The 5.1 original
  is downmixed with ffmpeg's standard coefficients and the mono TTS is centred.

- The final video is built in two passes: first the lektor audio mix is rendered to a
  temporary file, then everything is stream-copied into the MKV. This is deliberate.
  Doing both at once fails on sources with many subtitle tracks (some releases carry
  40+) — the sparse subtitle streams stall the muxer while the audio filter keeps
  producing, and ffmpeg aborts with "Failed to inject frame into filter network".
  The two-pass approach also makes the final mux about 10x faster.

- The final mux passes -max_interleave_delta 0 as an OUTPUT option (it must come after
  the maps and before the output path; placed before -i it configures the demuxer and
  silently does nothing). Without it, sparse subtitle tracks make the muxer abandon
  interleaving after its default 10-second window, and the lektor audio ends up written
  hundreds of megabytes away from the video it belongs with. The file measures perfectly
  fine with ffmpeg, but players read MKV sequentially and never reach that audio, so the
  Polish track plays as complete silence. Setting it to 0 buffers until every stream has
  a packet, costing a couple of GB of transient RAM for a few seconds.

- If a lektor track ever plays silent again, check interleaving before suspecting the
  audio. Compare byte positions at the same timestamp:
    ffprobe -read_intervals 00:20:00%+1 -select_streams v   -show_entries packet=pos file.mkv
    ffprobe -read_intervals 00:20:00%+1 -select_streams a:1 -show_entries packet=pos file.mkv
  They should be within a few MB of each other. An existing file can be repaired without
  regenerating anything:
    ffmpeg -i in.mkv -map 0 -c copy -max_interleave_delta 0 out.mkv

- ComfyUI crash recovery:
    sudo fuser -k /dev/nvidia*
    sudo nvidia-smi --gpu-reset
S
Description
Converts SRT subtitles into a Polish AI voiceover in the *lektor* style (a single narrator read over the quietened original soundtrack) using a local ComfyUI instance running the Higgs Audio V3 TTS model, then muxes the result back into the video as a second audio track.
Readme
94 MiB
Languages
Python 93.1%
Shell 6.9%