Files
thunderheronight--splat-wor…/scripts/splat.sh
T
thunderheronightandClaude Opus 5.5 ea8e8dca8e Resume: restart an interrupted training instead of failing
The pipeline only reuses completed stages and refuses an incomplete training
directory. Remove it, so images and camera positions are reused and only the
training runs again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:21:16 +02:00

106 lines
5.2 KiB
Bash
Executable File

#!/bin/bash
# Arcturus Vision recording -> Gaussian splat -> scene in the Gaussian XR gallery on the Steam Frame.
#
# Usage: scripts/splat.sh <recording> "<display name>" [extra av1_to_3dgs.py options]
# <recording> a local .mp4, or part of a file name on the Frame
# (e.g. "19-00" or "2026-10-03_19-00-53"); it is downloaded first
# <display name> shown on the name plate in the gallery, e.g. "Living room"
#
# Environment:
# QUALITY=fast|high|max preset (default: high)
# fast 100 image pairs, half resolution, up to 1.5 M splats (~10 min, for trying things out)
# high ~5 image pairs per second of video (100-500), full resolution, up to 2 M splats
# max ~8 image pairs per second of video (100-500), full resolution, up to 3 M splats, 50k steps
# FRAMES, SCALE, MAXG, ITER override single preset values
# NO_UPLOAD=1 only compute, don't copy to the Frame
# FRESH=1 discard an existing run of the same name first
# Interrupted? Run the same command again with --resume.
# Paths and the Frame address come from ~/.config/splat-workshop/config.toml
# (or PIPELINE_DIR, SPIRULA_BIN, SPLAT_TRANSFORM, RECORDINGS_DIR, RUNS_DIR, FRAME_HOST).
#
# Lines starting with "@@" are machine-readable progress markers for the web interface.
set -euo pipefail
HERE=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
ROOT=$(dirname "$HERE")
eval "$(cd "$ROOT" && python3 -m splat_workshop.config --shell)"
FRAME_VIDEOS=Videos/ArcturusVisionCamera
src=${1:?Give a recording (.mp4 or part of a file name on the Frame)}
name=${2:?Give a display name, e.g. "Living room"}
shift 2
# Folder name from the display name: transliterate, keep only safe characters
slug=$(cd "$ROOT" && python3 -m splat_workshop.config --slug "$name")
case "$slug" in ''|.*) echo "The display name does not give a usable folder name" >&2; exit 1 ;; esac
[ -f "$PIPELINE_DIR/av1_to_3dgs.py" ] || { echo "Arcturus pipeline not found in $PIPELINE_DIR (see README)" >&2; exit 1; }
[ -x "$SPIRULA_BIN" ] || { echo "Spirula Studio not found at $SPIRULA_BIN (see README)" >&2; exit 1; }
[ -n "$SPLAT_TRANSFORM" ] || { echo "splat-transform not found (npm install -g @playcanvas/splat-transform)" >&2; exit 1; }
# 1. Recording
if [ -f "$src" ]; then
in=$(realpath "$src")
else
[ -n "$FRAME_HOST" ] || { echo "No such file, and no Frame configured: $src" >&2; exit 1; }
remote=$(ssh -o ConnectTimeout=8 "$FRAME_HOST" "ls $FRAME_VIDEOS/*${src}*_stereo.mp4 2>/dev/null" || true)
count=$(printf '%s\n' "$remote" | grep -c . || true)
[ "$count" = 1 ] || { echo "$count recordings on the Frame match \"$src\":" >&2; printf '%s\n' "$remote" >&2; exit 1; }
mkdir -p "$RECORDINGS_DIR"
in=$RECORDINGS_DIR/$(basename "$remote")
if [ ! -f "$in" ]; then
echo "Downloading $(basename "$remote") from the Frame ..."
scp -q "$FRAME_HOST:$remote" "$in.part" && mv "$in.part" "$in"
fi
fi
out=$RUNS_DIR/$slug
log=$RUNS_DIR/$slug.log
mkdir -p "$RUNS_DIR"
if [ "${FRESH:-0}" = 1 ]; then rm -rf "$out" "$log"; fi
# The pipeline resumes finished stages only: an interrupted training run blocks --resume.
# Then start the training over; images and camera positions are reused.
manifest=$out/spirula/run.json
if [[ " $* " == *" --resume "* ]] && [ -f "$manifest" ] && ! grep -q '"status": *"complete"' "$manifest"; then
ckpt=$(ls -d "$out"/spirula/training/step-*.ckpt 2>/dev/null | tail -1)
echo "Training was interrupted${ckpt:+ (last checkpoint: ${ckpt##*/step-})}; the pipeline cannot continue it." | tee -a "$log"
echo "Restarting the training, images and camera positions are reused." | tee -a "$log"
rm -rf "$out/spirula"
fi
# Image pairs from the duration: <per second>, clamped to the pipeline limits 100..500
pairs() {
local secs
secs=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$in")
awk -v s="$secs" -v r="$1" 'BEGIN { f = int(s * r + 0.5); if (f < 100) f = 100; if (f > 500) f = 500; print f }'
}
case "${QUALITY:-high}" in
fast) frames=100; scale=0.5; maxg=1500000; iter=30000 ;;
high) frames=$(pairs 5); scale=1; maxg=2000000; iter=30000 ;;
max) frames=$(pairs 8); scale=1; maxg=3000000; iter=50000 ;;
*) echo "QUALITY must be fast, high or max" >&2; exit 1 ;;
esac
quality=(--frames "${FRAMES:-$frames}" --training-image-scale "${SCALE:-$scale}"
--max-gaussians "${MAXG:-$maxg}" --iterations "${ITER:-$iter}")
echo "Quality ${QUALITY:-high}: ${quality[*]}" | tee -a "$log"
# 2. Compute the Gaussian splat. Spirula in English, so the progress lines can be parsed.
cd "$PIPELINE_DIR"
SS_LANG=en uv run av1_to_3dgs.py "$in" "$out" \
--backend spirula --sfm-backend spirula --no-undistort \
--spirula "$SPIRULA_BIN" \
--splat-transform "$SPLAT_TRANSFORM" \
"${quality[@]}" "$@" 2>&1 | tee -a "$log"
[ -f "$out/scene.sog" ] || { echo "No scene.sog was produced, see $log" >&2; exit 1; }
# 3. Gallery files, then copy to the Frame (to-frame.sh also works on its own)
if [ "${NO_UPLOAD:-0}" = 1 ] || [ -z "$FRAME_HOST" ]; then
GALLERY_ONLY=1 LOG="$log" "$HERE/to-frame.sh" "$out" "$name"
echo "@@ done local"
echo "Done (not copied to the Frame): $out"
exit 0
fi
LOG="$log" "$HERE/to-frame.sh" "$out" "$name"