install.sh
#!/usr/bin/env bash
# video-editor-tools installer: idempotent, CPU-only, Ubuntu/Debian. Safe to re-run (skips what is already there).
# Installs: ffmpeg, HandBrakeCLI, Node 22 + HyperFrames (+ gsap, lottie-web for offline renders), a Python venv with
# audio-separator (MelBand-Roformer), onnxruntime and DeepFilterNet3 (official deep-filter binary), yt-dlp,
# the Robust Video Matting model, OFL fonts and generated placeholder media.
# Usage: bash install.sh (from a clone)
# VET_HOME=/some/dir bash install.sh
set -euo pipefail
REPO_URL="${VET_REPO_URL:-https://git.cafe/done/video-editor-tools}"
VET_HOME="${VET_HOME:-/workspace/video-editor-tools}"
NODE_MAJOR=22
RVM_URL="https://github.com/PeterL1n/RobustVideoMatting/releases/download/v1.0.0/rvm_mobilenetv3_fp32.onnx"
RVM_SHA="88d4531297118f595bf2fd60f6f566aec2e559393802d1f436c380f0cbbd2828"
DF_URL="https://github.com/Rikorose/DeepFilterNet/releases/download/v0.5.6/deep-filter-0.5.6-x86_64-unknown-linux-musl"
DF_SHA="70775e251eee44c0f2451a1e833326cf8bcbbe304d3e7cd12851e6fce72ef7da"
FONT_BASE="https://raw.githubusercontent.com/google/fonts/main/ofl"
say(){ printf '\n\033[1m==> %s\033[0m\n' "$*"; }
warn(){ printf '\033[33mWARN: %s\033[0m\n' "$*" >&2; }
SUDO=""; [ "$(id -u)" -eq 0 ] || { command -v sudo >/dev/null && SUDO="sudo"; }
fetch(){ # url dest [sha256]
local u="$1" d="$2" s="${3:-}"
if [ -s "$d" ] && { [ -z "$s" ] || echo "$s $d" | sha256sum -c --quiet - 2>/dev/null; }; then return 0; fi
curl -fsSL --retry 3 -o "$d.part" "$u" && mv "$d.part" "$d"
[ -z "$s" ] || echo "$s $d" | sha256sum -c --quiet - || { rm -f "$d"; echo "checksum mismatch: $u" >&2; return 1; }
}
# 0. get the repo (when run via curl | bash, or from outside a clone)
if [ ! -f "$VET_HOME/tools/audio/voice_master.py" ]; then
HERE="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd || true)"
if [ -n "$HERE" ] && [ -f "$HERE/tools/audio/voice_master.py" ]; then VET_HOME="$HERE"
else say "cloning $REPO_URL -> $VET_HOME"; command -v git >/dev/null || { $SUDO apt-get update -qq; $SUDO apt-get install -y -qq git; }
mkdir -p "$(dirname "$VET_HOME")"; git clone --depth 1 "$REPO_URL" "$VET_HOME"; fi
fi
cd "$VET_HOME"; mkdir -p bin models fonts vendor placeholders sfx
say "1/8 system packages (ffmpeg, HandBrakeCLI, python venv, build basics)"
PKGS=(ffmpeg handbrake-cli python3-venv python3-pip python3-dev build-essential curl ca-certificates xz-utils git fonts-dejavu-core fonts-noto-color-emoji)
MISSING=(); for p in "${PKGS[@]}"; do dpkg -s "$p" >/dev/null 2>&1 || MISSING+=("$p"); done
if [ ${#MISSING[@]} -gt 0 ]; then
if [ -n "$SUDO" ] || [ "$(id -u)" -eq 0 ]; then $SUDO apt-get update -qq && DEBIAN_FRONTEND=noninteractive $SUDO apt-get install -y -qq "${MISSING[@]}"
else warn "no sudo: install these yourself: ${MISSING[*]}"; fi
fi
command -v HandBrakeCLI >/dev/null || warn "HandBrakeCLI not found (package handbrake-cli)"
say "2/8 Node $NODE_MAJOR"
NODE_DIR="$HOME/.local/node$NODE_MAJOR"
node_ok(){ "$1" -v 2>/dev/null | grep -qE "^v(2[2-9]|[3-9][0-9])\."; }
if command -v node >/dev/null && node_ok node; then NODE_BIN="$(dirname "$(command -v node)")"
elif [ -x "$NODE_DIR/bin/node" ] && node_ok "$NODE_DIR/bin/node"; then NODE_BIN="$NODE_DIR/bin"
else
ARCH=$(uname -m); case "$ARCH" in x86_64) NA=x64;; aarch64|arm64) NA=arm64;; *) echo "unsupported arch $ARCH"; exit 1;; esac
TGZ=$(curl -fsSL "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/SHASUMS256.txt" | awk "/linux-$NA.tar.xz/{print \$2}")
mkdir -p "$NODE_DIR"; curl -fsSL "https://nodejs.org/dist/latest-v$NODE_MAJOR.x/$TGZ" | tar -xJ -C "$NODE_DIR" --strip-components=1
NODE_BIN="$NODE_DIR/bin"
fi
echo "node $("$NODE_BIN/node" -v) at $NODE_BIN"
say "3/8 HyperFrames + gsap + lottie-web (local to \$VET_HOME/node)"
mkdir -p node
if [ ! -x node/node_modules/.bin/hyperframes ]; then
PATH="$NODE_BIN:$PATH" npm --prefix "$VET_HOME/node" install --no-fund --no-audit --silent hyperframes gsap lottie-web
fi
cat > bin/hyperframes <<WRAP
#!/usr/bin/env bash
# HyperFrames on Node $NODE_MAJOR; telemetry off.
export PATH="$NODE_BIN:\$PATH" DO_NOT_TRACK=1
exec "$VET_HOME/node/node_modules/.bin/hyperframes" "\$@"
WRAP
chmod +x bin/hyperframes
cp -f node/node_modules/gsap/dist/gsap.min.js vendor/gsap.min.js
cp -f node/node_modules/lottie-web/build/player/lottie.min.js vendor/lottie.min.js
bin/hyperframes telemetry disable >/dev/null 2>&1 || true
say "4/8 Python venv (audio-separator, onnxruntime, torch CPU, yt-dlp, numpy, pillow) - CPU only"
[ -x .venv/bin/python ] || python3 -m venv .venv
PIP=".venv/bin/pip"; $PIP install -q --upgrade pip
.venv/bin/python -c "import torch" 2>/dev/null || $PIP install -q torch torchaudio --index-url https://download.pytorch.org/whl/cpu
.venv/bin/python -c "import audio_separator.separator, onnxruntime, numpy, PIL, requests, yt_dlp, audioread" 2>/dev/null || \
$PIP install -q "audio-separator[cpu]" onnxruntime numpy pillow requests yt-dlp audioread soundfile
# DeepFilterNet3 runs from the official prebuilt binary (step 5). The PyPI "deepfilternet" package is NOT installed here:
# it pins numpy<2 and imports torchaudio.backend (removed in current torchaudio), which breaks audio-separator.
say "5/8 DeepFilterNet3 binary (deep-filter 0.5.6, MIT/Apache-2.0)"
if [ "$(uname -m)" = x86_64 ]; then fetch "$DF_URL" bin/deep-filter "$DF_SHA" && chmod +x bin/deep-filter
else warn "no prebuilt deep-filter for $(uname -m); use the python package: .venv/bin/deepFilter"; fi
say "6/8 Robust Video Matting model (GPL-3.0, downloaded from the official release, not redistributed here)"
fetch "$RVM_URL" models/rvm_mobilenetv3_fp32.onnx "$RVM_SHA"
say "7/8 fonts (SIL OFL 1.1, Google Fonts repo)"
while read -r src dst; do fetch "$FONT_BASE/$src" "fonts/$dst"; done <<'FONTS'
poppins/Poppins-Regular.ttf Poppins-Regular.ttf
poppins/Poppins-Bold.ttf Poppins-Bold.ttf
poppins/Poppins-ExtraBold.ttf Poppins-ExtraBold.ttf
poppins/Poppins-Black.ttf Poppins-Black.ttf
poppins/OFL.txt LICENSE-Poppins-OFL.txt
dmsans/DMSans%5Bopsz,wght%5D.ttf DMSans-Variable.ttf
dmsans/OFL.txt LICENSE-DMSans-OFL.txt
montserrat/Montserrat%5Bwght%5D.ttf Montserrat-Variable.ttf
montserrat/OFL.txt LICENSE-Montserrat-OFL.txt
anton/Anton-Regular.ttf Anton-Regular.ttf
anton/OFL.txt LICENSE-Anton-OFL.txt
gabarito/Gabarito%5Bwght%5D.ttf Gabarito-Variable.ttf
gabarito/OFL.txt LICENSE-Gabarito-OFL.txt
inter/Inter%5Bopsz,wght%5D.ttf Inter-Variable.ttf
inter/OFL.txt LICENSE-Inter-OFL.txt
playfairdisplay/PlayfairDisplay%5Bwght%5D.ttf PlayfairDisplay-Variable.ttf
playfairdisplay/PlayfairDisplay-Italic%5Bwght%5D.ttf PlayfairDisplay-Italic-Variable.ttf
playfairdisplay/OFL.txt LICENSE-PlayfairDisplay-OFL.txt
archivoblack/ArchivoBlack-Regular.ttf ArchivoBlack-Regular.ttf
archivoblack/OFL.txt LICENSE-ArchivoBlack-OFL.txt
FONTS
say "8/8 placeholders + SFX library (generated locally)"
[ -s placeholders/grain-512.png ] || .venv/bin/python - <<'PY'
import numpy as np; from PIL import Image
r = np.random.default_rng(7); g = (128 + r.normal(0, 40, (512, 512))).clip(0, 255).astype('uint8'); Image.fromarray(g, 'L').save('placeholders/grain-512.png')
PY
[ -s placeholders/avatar.jpg ] || ffmpeg -v error -y -f lavfi -i "gradients=s=400x400:c0=0x3a3a5a:c1=0xb0b0d0:n=2,format=yuv420p" -frames:v 1 placeholders/avatar.jpg
[ -s placeholders/placeholder-gradient-16x9-90s.mp4 ] || ffmpeg -v error -y -f lavfi -i "gradients=s=1920x1080:r=30:d=90:speed=0.01:c0=0x1d2b53:c1=0x7e2553:c2=0xff004d:n=3" -c:v libx264 -crf 28 -pix_fmt yuv420p placeholders/placeholder-gradient-16x9-90s.mp4
[ -s placeholders/placeholder-gradient-9x16-60s.mp4 ] || ffmpeg -v error -y -f lavfi -i "gradients=s=1080x1920:r=30:d=60:speed=0.01:c0=0x0b3d2e:c1=0x2a6f97:c2=0xf2c14e:n=3" -c:v libx264 -crf 28 -pix_fmt yuv420p placeholders/placeholder-gradient-9x16-60s.mp4
[ -s sfx/SOURCES.md ] || .venv/bin/python tools/audio/sfx_synth.py gen sfx >/dev/null
say "extra: LottieFiles starter set (Lottie Simple License: free incl. commercial, not redistributable on its own)"
mkdir -p lottie
tail -n +2 lottie/SOURCES.tsv | while IFS=$'\t' read -r f u page; do fetch "$u" "lottie/$f" || warn "lottie $f failed"; done
# PATH for future shells (idempotent)
LINE="export VET_HOME=\"$VET_HOME\"; export PATH=\"\$VET_HOME/bin:\$VET_HOME/.venv/bin:\$PATH\""
grep -qsF "$LINE" "$HOME/.bashrc" || echo "$LINE" >> "$HOME/.bashrc"
say "done"
cat <<NEXT
Installed to $VET_HOME
open a new shell (or run: $LINE )
Next steps:
1. hyperframes doctor # checks Chrome/ffmpeg for renders
2. Smoke test (5 s HyperFrames render + audio chain):
$VET_HOME/tools/smoke_test.sh [optional_talking_clip.mp4]
3. Read README.md for the full stack (CloudConvert, VideoDatabase, Context.dev, Google Drive, Notion) and keys.
NEXT