Initial commit: Polish AI dubbing pipeline (ComfyUI + Higgs Audio V3)
Converts SRT subtitles into a Polish lektor-style voiceover using a local
ComfyUI instance running Higgs Audio V3, then muxes the result back into the
source video as a second audio track. Video and original audio are always
stream-copied; only the new voiceover track is encoded.
Contents:
lektor_ai_comfy.py main pipeline (standard library only, needs ffmpeg)
lektor_ai_comfy_v2.py batched-context experiment (timing unresolved)
lektor_ai.py earlier version using the hosted Boson AI API
install_comfyui.sh reproduces the ComfyUI environment, versions pinned
docs/ requirements, installation, usage, architecture,
troubleshooting
gudowski/ voice references (.wav) with their transcripts (.txt)
Source media and generated video are gitignored.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+328
@@ -0,0 +1,328 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# install_comfyui.sh — reproduce the ComfyUI + Higgs Audio V3 TTS environment
|
||||
# used by lektor_ai_comfy.py, on this or any other Linux machine.
|
||||
#
|
||||
# Everything lands in two removable directories and nothing touches the system:
|
||||
# ~/miniforge3 — conda distribution (its own Python)
|
||||
# ~/comfy-tts-app — ComfyUI, the TTS-Audio-Suite node, and the models
|
||||
#
|
||||
# No sudo, no apt, no changes to system Python. `conda init` is never run, so
|
||||
# your .bashrc is left alone.
|
||||
#
|
||||
# Usage:
|
||||
# ./install_comfyui.sh # pinned versions (recommended)
|
||||
# ./install_comfyui.sh --latest # newest ComfyUI + suite instead
|
||||
# ./install_comfyui.sh --skip-model # don't pre-download the 8.7 GB model
|
||||
# ./install_comfyui.sh --lock FILE # restore exact package set from a lockfile
|
||||
# ./install_comfyui.sh --help
|
||||
#
|
||||
# Override locations with environment variables:
|
||||
# CONDA_ROOT=/opt/miniforge3 COMFY_ROOT=/data/comfy ./install_comfyui.sh
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
# ── Configuration ────────────────────────────────────────────────────────────
|
||||
CONDA_ROOT="${CONDA_ROOT:-$HOME/miniforge3}"
|
||||
COMFY_ROOT="${COMFY_ROOT:-$HOME/comfy-tts-app}"
|
||||
ENV_NAME="${ENV_NAME:-comfy-tts}"
|
||||
COMFY_PORT="${COMFY_PORT:-8189}"
|
||||
PYTHON_VERSION="${PYTHON_VERSION:-3.12}"
|
||||
|
||||
# Exact versions of the working installation this script reproduces.
|
||||
COMFYUI_REPO="https://github.com/comfyanonymous/ComfyUI.git"
|
||||
COMFYUI_COMMIT="7cb784e0f48784bb6ed588912e186e5ee1e9ee68" # v0.26.0-13-g7cb784e0
|
||||
TTS_SUITE_REPO="https://github.com/diodiogod/TTS-Audio-Suite.git"
|
||||
TTS_SUITE_TAG="v5.3.0" # 09df1681
|
||||
MINIFORGE_URL="https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh"
|
||||
|
||||
# PyTorch: CUDA 12.8 wheels. These carry their own CUDA runtime, so the
|
||||
# system CUDA toolkit version is irrelevant — only the NVIDIA driver matters.
|
||||
TORCH_INDEX="https://download.pytorch.org/whl/cu128"
|
||||
TORCH_PINS=(torch==2.11.0 torchvision==0.26.0 torchaudio==2.11.0)
|
||||
|
||||
# The TTS model. The suite downloads this automatically on first use; doing it
|
||||
# here just means the first generation isn't a surprise 8.7 GB wait.
|
||||
HF_MODEL_REPO="bosonai/higgs-audio-v3-tts-4b"
|
||||
MODEL_SUBDIR="models/TTS/higgs_audio_v3/higgs-audio-v3-tts-4b"
|
||||
|
||||
REQUIRED_DISK_GB=35 # ~10 env + ~3 ComfyUI + ~9 model + headroom
|
||||
|
||||
# ── Options ──────────────────────────────────────────────────────────────────
|
||||
USE_LATEST=0
|
||||
SKIP_MODEL=0
|
||||
LOCKFILE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--latest) USE_LATEST=1; shift ;;
|
||||
--skip-model) SKIP_MODEL=1; shift ;;
|
||||
--lock) LOCKFILE="${2:-}"; shift 2 ;;
|
||||
-h|--help) awk 'NR>1 {if (/^#/) {sub(/^# ?/,""); print} else exit}' "$0"; exit 0 ;;
|
||||
*) echo "Unknown option: $1 (try --help)" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Output helpers ───────────────────────────────────────────────────────────
|
||||
if [[ -t 1 ]]; then
|
||||
B=$'\033[1m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; N=$'\033[0m'
|
||||
else
|
||||
B=""; G=""; Y=""; R=""; N=""
|
||||
fi
|
||||
step() { echo; echo "${B}==> $*${N}"; }
|
||||
ok() { echo " ${G}[+]${N} $*"; }
|
||||
warn() { echo " ${Y}[!]${N} $*"; }
|
||||
die() { echo " ${R}[X]${N} $*" >&2; exit 1; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# Activate the conda env. conda's shell functions dislike `set -u`, so it is
|
||||
# lifted for the duration of the call only.
|
||||
activate_env() {
|
||||
# shellcheck disable=SC1091
|
||||
source "$CONDA_ROOT/etc/profile.d/conda.sh"
|
||||
set +u
|
||||
conda activate "$ENV_NAME"
|
||||
set -u
|
||||
}
|
||||
|
||||
ENV_PY="$CONDA_ROOT/envs/$ENV_NAME/bin/python"
|
||||
pip_install() { "$ENV_PY" -m pip install --no-input "$@"; }
|
||||
|
||||
# ── 0. Preflight ─────────────────────────────────────────────────────────────
|
||||
step "Preflight checks"
|
||||
|
||||
[[ "$(uname -s)" == "Linux" ]] || die "This script targets Linux (found $(uname -s))."
|
||||
|
||||
have git || die "git is required. Install it first (e.g. sudo apt install git)."
|
||||
if have curl; then DL=(curl -fL -o); elif have wget; then DL=(wget -O); else
|
||||
die "Need curl or wget to download Miniforge."
|
||||
fi
|
||||
ok "git and a downloader are present"
|
||||
|
||||
if have nvidia-smi && nvidia-smi >/dev/null 2>&1; then
|
||||
ok "GPU: $(nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader | head -1)"
|
||||
else
|
||||
warn "No usable nvidia-smi. Install will proceed but TTS will run on CPU (very slow)."
|
||||
warn "The cu128 wheels need an NVIDIA driver >= 525; the CUDA toolkit is NOT required."
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$COMFY_ROOT")"
|
||||
avail_gb=$(df -BG --output=avail "$(dirname "$COMFY_ROOT")" | tail -1 | tr -dc '0-9')
|
||||
if (( avail_gb < REQUIRED_DISK_GB )); then
|
||||
warn "Only ${avail_gb} GB free where $COMFY_ROOT will live; ~${REQUIRED_DISK_GB} GB recommended."
|
||||
warn "Use --skip-model, or set COMFY_ROOT to a roomier disk."
|
||||
else
|
||||
ok "Disk space: ${avail_gb} GB available"
|
||||
fi
|
||||
|
||||
if have ffmpeg; then
|
||||
ok "ffmpeg found at $(command -v ffmpeg) (used by lektor_ai_comfy.py, not by ComfyUI)"
|
||||
else
|
||||
warn "ffmpeg not found. ComfyUI does not need it, but lektor_ai_comfy.py does."
|
||||
warn "Install it with: conda install -n $ENV_NAME -c conda-forge ffmpeg (no sudo)"
|
||||
fi
|
||||
|
||||
if (( USE_LATEST )); then
|
||||
warn "--latest: installing newest ComfyUI and TTS-Audio-Suite rather than the pinned"
|
||||
warn "versions this project was validated against. Expect to debug breakage yourself."
|
||||
else
|
||||
ok "Pinned: ComfyUI ${COMFYUI_COMMIT:0:8} (v0.26.0), TTS-Audio-Suite $TTS_SUITE_TAG"
|
||||
fi
|
||||
|
||||
# ── 1. Miniforge ─────────────────────────────────────────────────────────────
|
||||
step "Miniforge (self-contained conda in $CONDA_ROOT)"
|
||||
|
||||
if [[ -x "$CONDA_ROOT/bin/conda" ]]; then
|
||||
ok "Already installed: $("$CONDA_ROOT/bin/conda" --version)"
|
||||
else
|
||||
tmp_installer="$(mktemp -t miniforge-XXXXXX.sh)"
|
||||
trap 'rm -f "$tmp_installer"' EXIT
|
||||
echo " Downloading Miniforge…"
|
||||
"${DL[@]}" "$tmp_installer" "$MINIFORGE_URL"
|
||||
echo " Installing to $CONDA_ROOT (batch mode, no shell integration)…"
|
||||
bash "$tmp_installer" -b -p "$CONDA_ROOT"
|
||||
rm -f "$tmp_installer"; trap - EXIT
|
||||
ok "Installed $("$CONDA_ROOT/bin/conda" --version)"
|
||||
fi
|
||||
|
||||
# ── 2. Conda environment ─────────────────────────────────────────────────────
|
||||
step "Conda environment '$ENV_NAME' (Python $PYTHON_VERSION)"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source "$CONDA_ROOT/etc/profile.d/conda.sh"
|
||||
if conda env list | awk '{print $1}' | grep -qx "$ENV_NAME"; then
|
||||
ok "Environment already exists — reusing it"
|
||||
else
|
||||
conda create -n "$ENV_NAME" "python=$PYTHON_VERSION" -y
|
||||
ok "Created"
|
||||
fi
|
||||
|
||||
activate_env
|
||||
ok "Active: $(python -V) at $(command -v python)"
|
||||
|
||||
[[ "$(command -v python)" == "$CONDA_ROOT/envs/$ENV_NAME/"* ]] \
|
||||
|| die "Wrong Python active — refusing to install into the wrong environment."
|
||||
|
||||
pip_install --upgrade pip setuptools wheel >/dev/null
|
||||
ok "pip toolchain up to date"
|
||||
|
||||
# ── Exact restore path ───────────────────────────────────────────────────────
|
||||
if [[ -n "$LOCKFILE" ]]; then
|
||||
step "Restoring exact package set from $LOCKFILE"
|
||||
[[ -f "$LOCKFILE" ]] || die "Lockfile not found: $LOCKFILE"
|
||||
pip_install --extra-index-url "$TORCH_INDEX" -r "$LOCKFILE"
|
||||
ok "Lockfile applied — skipping individual package steps"
|
||||
fi
|
||||
|
||||
# ── 3. PyTorch ───────────────────────────────────────────────────────────────
|
||||
step "PyTorch (CUDA 12.8 wheels)"
|
||||
|
||||
if [[ -z "$LOCKFILE" ]]; then
|
||||
if (( USE_LATEST )); then
|
||||
pip_install --index-url "$TORCH_INDEX" torch torchvision torchaudio
|
||||
else
|
||||
pip_install --index-url "$TORCH_INDEX" "${TORCH_PINS[@]}"
|
||||
fi
|
||||
fi
|
||||
python - <<'PY'
|
||||
import torch
|
||||
print(f" torch {torch.__version__} cuda_available={torch.cuda.is_available()}")
|
||||
if torch.cuda.is_available():
|
||||
print(f" device: {torch.cuda.get_device_name(0)}")
|
||||
PY
|
||||
ok "PyTorch ready"
|
||||
|
||||
# ── 4. ComfyUI ───────────────────────────────────────────────────────────────
|
||||
step "ComfyUI"
|
||||
|
||||
mkdir -p "$COMFY_ROOT"
|
||||
COMFY_DIR="$COMFY_ROOT/ComfyUI"
|
||||
|
||||
if [[ -d "$COMFY_DIR/.git" ]]; then
|
||||
ok "Repository already present at $COMFY_DIR"
|
||||
else
|
||||
git clone "$COMFYUI_REPO" "$COMFY_DIR"
|
||||
fi
|
||||
|
||||
if (( ! USE_LATEST )); then
|
||||
git -C "$COMFY_DIR" fetch --quiet origin
|
||||
git -C "$COMFY_DIR" checkout --quiet "$COMFYUI_COMMIT"
|
||||
ok "Checked out $COMFYUI_COMMIT (detached HEAD — expected)"
|
||||
else
|
||||
git -C "$COMFY_DIR" pull --quiet --ff-only origin master || warn "Could not fast-forward; leaving as-is"
|
||||
fi
|
||||
|
||||
[[ -z "$LOCKFILE" ]] && pip_install -r "$COMFY_DIR/requirements.txt"
|
||||
ok "ComfyUI dependencies installed"
|
||||
|
||||
# ── 5. TTS-Audio-Suite ───────────────────────────────────────────────────────
|
||||
step "TTS-Audio-Suite custom node"
|
||||
|
||||
SUITE_DIR="$COMFY_DIR/custom_nodes/TTS-Audio-Suite"
|
||||
mkdir -p "$COMFY_DIR/custom_nodes"
|
||||
|
||||
if [[ -d "$SUITE_DIR/.git" ]]; then
|
||||
ok "Repository already present"
|
||||
else
|
||||
git clone "$TTS_SUITE_REPO" "$SUITE_DIR"
|
||||
fi
|
||||
|
||||
if (( ! USE_LATEST )); then
|
||||
git -C "$SUITE_DIR" fetch --quiet --tags origin
|
||||
git -C "$SUITE_DIR" checkout --quiet "tags/$TTS_SUITE_TAG"
|
||||
ok "Checked out $TTS_SUITE_TAG"
|
||||
else
|
||||
git -C "$SUITE_DIR" pull --quiet --ff-only || warn "Could not fast-forward; leaving as-is"
|
||||
fi
|
||||
|
||||
if [[ -z "$LOCKFILE" ]]; then
|
||||
# install.py resolves this suite's many conflicting dependencies itself
|
||||
# (selective --no-deps installs, numpy pinning). Do not replace it with a
|
||||
# plain `pip install -r requirements.txt` — that file deliberately omits
|
||||
# the packages that need special handling.
|
||||
echo " Running the suite's own installer (this pulls a lot and takes a while)…"
|
||||
( cd "$SUITE_DIR" && python install.py )
|
||||
fi
|
||||
ok "TTS-Audio-Suite installed"
|
||||
|
||||
# ── 6. Model ─────────────────────────────────────────────────────────────────
|
||||
step "Higgs Audio V3 model ($HF_MODEL_REPO)"
|
||||
|
||||
MODEL_DIR="$COMFY_DIR/$MODEL_SUBDIR"
|
||||
if (( SKIP_MODEL )); then
|
||||
warn "Skipped (--skip-model). It downloads automatically on the first generation."
|
||||
elif [[ -f "$MODEL_DIR/model.safetensors" ]]; then
|
||||
ok "Already present ($(du -h "$MODEL_DIR/model.safetensors" | cut -f1))"
|
||||
else
|
||||
echo " Downloading ~8.7 GB into $MODEL_DIR …"
|
||||
mkdir -p "$MODEL_DIR"
|
||||
if ! HF_MODEL_REPO="$HF_MODEL_REPO" MODEL_DIR="$MODEL_DIR" python - <<'PY'
|
||||
import os, sys
|
||||
from huggingface_hub import snapshot_download
|
||||
try:
|
||||
snapshot_download(repo_id=os.environ["HF_MODEL_REPO"],
|
||||
local_dir=os.environ["MODEL_DIR"])
|
||||
except Exception as exc:
|
||||
print(f"download failed: {exc}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
PY
|
||||
then
|
||||
warn "Model download failed."
|
||||
warn "This model is licensed for research / non-commercial use and may require"
|
||||
warn "accepting its terms on huggingface.co first, then: hf auth login"
|
||||
warn "The suite will retry automatically on the first generation."
|
||||
else
|
||||
ok "Model downloaded"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 7. Launcher ──────────────────────────────────────────────────────────────
|
||||
step "Launcher script"
|
||||
|
||||
LAUNCHER="$COMFY_ROOT/start_comfyui.sh"
|
||||
cat > "$LAUNCHER" <<EOF
|
||||
#!/bin/bash
|
||||
# Generated by install_comfyui.sh — starts ComfyUI on port $COMFY_PORT.
|
||||
source "$CONDA_ROOT/etc/profile.d/conda.sh"
|
||||
conda activate $ENV_NAME
|
||||
cd "$COMFY_DIR" && python main.py --port $COMFY_PORT
|
||||
EOF
|
||||
chmod +x "$LAUNCHER"
|
||||
ok "Wrote $LAUNCHER"
|
||||
|
||||
# ── 8. Verification ──────────────────────────────────────────────────────────
|
||||
step "Verifying installation"
|
||||
|
||||
python - <<'PY'
|
||||
import importlib, sys
|
||||
failed = []
|
||||
for mod in ("torch", "torchaudio", "transformers", "safetensors", "soundfile", "librosa"):
|
||||
try:
|
||||
m = importlib.import_module(mod)
|
||||
print(f" [+] {mod:<13} {getattr(m, '__version__', '?')}")
|
||||
except Exception as exc:
|
||||
print(f" [X] {mod:<13} {exc}")
|
||||
failed.append(mod)
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
ok "Core imports succeed"
|
||||
|
||||
echo
|
||||
echo "${B}${G}Installation complete.${N}"
|
||||
echo
|
||||
echo " Start ComfyUI: $LAUNCHER"
|
||||
echo " Web UI: http://127.0.0.1:$COMFY_PORT"
|
||||
echo " Then run: python3 lektor_ai_comfy.py <video.mkv> --embedded-subs"
|
||||
echo
|
||||
echo " lektor_ai_comfy.py expects these paths — edit its constants if you changed them:"
|
||||
echo " COMFY_URL = \"http://127.0.0.1:$COMFY_PORT\""
|
||||
echo " COMFY_OUTPUT_DIR = \"$COMFY_DIR/output\""
|
||||
echo " COMFY_INPUT_DIR = \"$COMFY_DIR/input\""
|
||||
echo
|
||||
echo " Record this environment for later:"
|
||||
echo " source $CONDA_ROOT/etc/profile.d/conda.sh && conda activate $ENV_NAME"
|
||||
echo " pip freeze > comfyui-requirements.lock.txt"
|
||||
echo
|
||||
echo " Remove everything:"
|
||||
echo " conda env remove -n $ENV_NAME && rm -rf $COMFY_ROOT"
|
||||
echo " rm -rf $CONDA_ROOT # only if nothing else uses it"
|
||||
Reference in New Issue
Block a user