install_comfyui.sh becomes install_backend.sh and now builds everything the app needs, not just the speech half: - Step 7 runs normalizer/setup.sh --model, which fetches both models: the open inflection model first, so the install gets as far as it can before anything can ask for credentials, then the gated fine-tune. - A missing Hugging Face account is no longer fatal. Accepting a licence is the one thing an installer cannot do, so the rest installs, the verification reports "fallback only (gated model missing)", and the summary prints the three commands that finish it. - --skip-normalizer, disk requirement 35 -> 55 GB, removal instructions and the step list updated to match. Queue logging — queue.log in the first queued video's job folder, which for a single-video run is its own: - Records what went wrong rather than what happened: warnings, tracebacks, ffmpeg errors, guard rejections, clips cut off mid-word or that crashed the backend, and the --regen line carrying their numbers. - ComfyUI failures *and* restarts, so the log shows the recovery rather than reading as though the run simply stopped. GUI-initiated backend actions go in too; they never pass through a job's pipe. - A failure also dumps the job's last 15 lines. Keyword matching alone is not enough — ffmpeg says "Invalid data found when processing input" and nothing that looks like an error. UI: - The output pane follows new output only while already at the bottom. Scroll up and it stays there; scroll back down and it resumes. Reading an earlier warning during a run was impossible while every ffmpeg progress frame yanked the view to the end. - The ComfyUI button's status dot is drawn as its own canvas item, so it can be green (answering) or amber (holds the port, does not answer). A "." inside the label could only ever be the same grey as the words. comfyui.log is capped at 120,000 lines, trimmed on backend start and every five minutes. Rewritten in place, never replaced: tee holds it open in append mode, so swapping the inode would leave the backend writing to an unlinked file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
391 lines
17 KiB
Bash
Executable File
391 lines
17 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# install_backend.sh — build everything lektor_ai_comfy.py needs, on this or any
|
|
# other Linux machine: the ComfyUI + Higgs Audio V3 speech backend, and the
|
|
# Polish number normalizer with its own environment and models.
|
|
#
|
|
# After this finishes there is nothing left to install by hand, with one
|
|
# exception the script cannot do for you: one normalizer model is gated behind
|
|
# a licence you have to accept with your own Hugging Face account. The script
|
|
# says so and installs everything else regardless.
|
|
#
|
|
# Everything lands in three removable directories and nothing touches the system:
|
|
# ~/miniforge3 — conda distribution (its own Python)
|
|
# ~/comfy-tts-app — ComfyUI, the TTS-Audio-Suite node, and the TTS model
|
|
# ./normalizer/ — the normalizer venv and its models, beside this script
|
|
#
|
|
# No sudo, no apt, no changes to system Python. `conda init` is never run, so
|
|
# your .bashrc is left alone.
|
|
#
|
|
# Usage:
|
|
# ./install_backend.sh # pinned versions (recommended)
|
|
# ./install_backend.sh --latest # newest ComfyUI + suite instead
|
|
# ./install_backend.sh --skip-model # don't pre-download the 8.7 GB TTS model
|
|
# ./install_backend.sh --skip-normalizer # ComfyUI only, no number normalizer
|
|
# ./install_backend.sh --lock FILE # restore exact package set from a lockfile
|
|
# ./install_backend.sh --help
|
|
#
|
|
# Hugging Face credentials, if you have them, are picked up automatically from
|
|
# `hf auth login` or from HF_TOKEN in the environment.
|
|
#
|
|
# Override locations with environment variables:
|
|
# CONDA_ROOT=/opt/miniforge3 COMFY_ROOT=/data/comfy ./install_backend.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"
|
|
|
|
# ~10 conda env + ~3 ComfyUI + ~9 TTS model, then the normalizer: ~7 venv
|
|
# (CUDA wheels again, in its own environment) + ~8 inflection model + ~1 gated.
|
|
REQUIRED_DISK_GB=55
|
|
|
|
# ── Options ──────────────────────────────────────────────────────────────────
|
|
USE_LATEST=0
|
|
SKIP_MODEL=0
|
|
SKIP_NORMALIZER=0
|
|
LOCKFILE=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--latest) USE_LATEST=1; shift ;;
|
|
--skip-model) SKIP_MODEL=1; shift ;;
|
|
--skip-normalizer) SKIP_NORMALIZER=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. Number normalizer ─────────────────────────────────────────────────────
|
|
# Its own venv, deliberately: the normalizer needs transformers>=5 and the TTS
|
|
# environment above is pinned to versions that must not move. They talk over a
|
|
# pipe instead, so the conflict never has to be resolved.
|
|
NORMALIZER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/normalizer"
|
|
|
|
if [[ $SKIP_NORMALIZER -eq 1 ]]; then
|
|
step "Number normalizer (skipped)"
|
|
warn "Digits will reach the TTS as digits, which it often misreads."
|
|
elif [[ ! -x "$NORMALIZER_DIR/setup.sh" ]]; then
|
|
step "Number normalizer"
|
|
warn "$NORMALIZER_DIR/setup.sh not found — skipping."
|
|
else
|
|
step "Number normalizer (own venv + models)"
|
|
if "$NORMALIZER_DIR/setup.sh" --model; then
|
|
ok "Normalizer environment ready"
|
|
else
|
|
warn "Normalizer setup reported a problem — see above."
|
|
warn "The pipeline still runs; numbers fall back to num2words."
|
|
fi
|
|
fi
|
|
|
|
# ── 8. Launcher ──────────────────────────────────────────────────────────────
|
|
step "Launcher script"
|
|
|
|
LAUNCHER="$COMFY_ROOT/start_comfyui.sh"
|
|
cat > "$LAUNCHER" <<EOF
|
|
#!/bin/bash
|
|
# Generated by install_backend.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"
|
|
|
|
# ── 9. 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"
|
|
|
|
NORMALIZER_STATE="not installed"
|
|
if [[ -x "$NORMALIZER_DIR/venv/bin/python" ]]; then
|
|
if "$NORMALIZER_DIR/venv/bin/python" "$NORMALIZER_DIR/normalizer.py" --status \
|
|
2>/dev/null | grep -q '"mode": "model"'; then
|
|
NORMALIZER_STATE="ready (neural)"
|
|
ok "Normalizer ready"
|
|
else
|
|
NORMALIZER_STATE="fallback only (gated model missing)"
|
|
warn "Normalizer installed but running on the num2words fallback"
|
|
fi
|
|
fi
|
|
|
|
echo
|
|
echo "${B}${G}Installation complete.${N}"
|
|
echo
|
|
echo " Normalizer: $NORMALIZER_STATE"
|
|
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 $NORMALIZER_DIR/venv $NORMALIZER_DIR/model $NORMALIZER_DIR/models"
|
|
echo " rm -rf $CONDA_ROOT # only if nothing else uses it"
|
|
|
|
if [[ "$NORMALIZER_STATE" == fallback* ]]; then
|
|
echo
|
|
echo " ${Y}One model still needs you:${N} it is gated behind a licence."
|
|
echo " 1. Open https://huggingface.co/Folx/qwen3-0.6b-pl-text-normalization"
|
|
echo " while signed in, and accept the terms"
|
|
echo " 2. $NORMALIZER_DIR/venv/bin/hf auth login"
|
|
echo " 3. $NORMALIZER_DIR/setup.sh --model"
|
|
echo " Until then times, dates and identifiers fall back to num2words."
|
|
fi
|