Files
thunderheronightandClaude Opus 5.5 3cf0e81aef Fixes from the review; release 0.3.0
- The watcher reports the next part (or putting the parts together) as running in the tick that
  starts it: no false standby notice between parts.
- Deleting an unfinished part deletes its piece of video in runs/.parts too.
- combine.sh keeps the floor plan's labels (plan.json).
- Tests: the chain through the watcher's tick (twice), one standby notice per chain, the own lock
  released before suspending, rooms of the same name, view-dependent colour matched, the preview plan
  of a home named by rooms; notifications are awaited instead of checked too early.
- 0.3.0: version, changelog, READMEs without the preview notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:41:49 +02:00

123 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# Put the scenes of one recording session together into one big scene ("tour"):
# all splats for the PC, a smaller version for the Steam Frame.
#
# Usage: scripts/combine.sh <folder name> "<display name>" <run folder>=<label> <run folder>=<label> ...
#
# Environment:
# ALIGN=frame|images|shift|rigid how the rooms are fitted together (default: images, see combine.py)
# PARTITION=view|nearest where the borders between the recordings run (default: view)
# COLOUR=match|keep bring all recordings to one brightness (default: match)
# ROOMS='["Kitchen","Kitchen","Hallway"]' the room of every recording, in order: a room can be made of
# several recordings (default: every recording is a room of its own)
# FRAME_SPLATS=1000000 splats in the Frame version (the full version keeps all)
# FULL=0 skip the full SOG for the PC (the step that needs the most memory)
# FULL_NAME="..." name plate of the full version (default: "<name> (full)")
# PLAN_COMMA=1 decimal comma on the floor plan used as preview
# NO_UPLOAD=1 only compute, don't copy to the Frame
# WORLD_SCALE=87 show a model in Gaussian XR in the size of the original (1:87)
# A previous result of the same name stays until the new one is done.
#
# Lines starting with "@@" are machine-readable progress markers for the web interface.
set -euo pipefail
# Everything in one block: bash reads it completely before it starts, so updating this file
# (git pull, editing) does not affect a run that is already going.
{
HERE=$(cd "$(dirname "$(readlink -f "$0")")" && pwd)
ROOT=$(dirname "$HERE")
eval "$(cd "$ROOT" && python3 -m splat_workshop.config --shell)"
slug=${1:?Give a folder name}
name=${2:?Give a display name}
shift 2
[ $# -ge 2 ] || { echo "Give at least two scenes (run folder=label)" >&2; exit 1; }
case "$slug" in ''|.*|*/*) echo "Unusable folder name: $slug" >&2; exit 1 ;; esac
[ -n "$SPLAT_TRANSFORM" ] || { echo "splat-transform not found (npm install -g @playcanvas/splat-transform)" >&2; exit 1; }
out=$RUNS_DIR/$slug
log=$RUNS_DIR/$slug.log
work=$RUNS_DIR/.building
tmp=$work/$slug
stem=$(printf '%s' "$slug" | tr 'A-Z' 'a-z')
frame_splats=${FRAME_SPLATS:-1000000}
# Gaussian XR's worldScale in a scene's transform file, when WORLD_SCALE is set (a model shown in the
# size of the original, e.g. 87 for H0)
world_scale() {
[ -n "${WORLD_SCALE:-}" ] || return 0
python3 - "$1" "$WORLD_SCALE" <<'PY'
import json, sys
path, scale = sys.argv[1], float(sys.argv[2])
data = json.load(open(path))
data["worldScale"] = scale
json.dump(data, open(path, "w"), indent=2)
PY
}
main() {
rm -rf "$tmp"
mkdir -p "$tmp"
trap 'rm -rf "$tmp" "$tmp.old"; rmdir "$work" 2>/dev/null || true' EXIT
trap 'exit 143' TERM; trap 'exit 130' INT; trap 'exit 129' HUP
echo "Putting $# scenes together as \"$name\" (alignment ${ALIGN:-images}, borders ${PARTITION:-view})"
uv run -q "$HERE/combine.py" "$tmp" "$@" --align "${ALIGN:-images}" --partition "${PARTITION:-view}" --colour "${COLOUR:-match}" \
${ROOMS:+--rooms "$ROOMS"}
total=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["splats"])' "$tmp/combine.json")
# Frame version: fewer splats
echo "@@ cphase 2"
src=$tmp/spirula/scene.ply
if [ "$total" -gt "$frame_splats" ]; then
echo "Reducing $total splats to $frame_splats for the Frame ..."
"$SPLAT_TRANSFORM" -w "$src" --decimate "$frame_splats" "$tmp/frame.ply"
src=$tmp/frame.ply
fi
echo "@@ cphase 3"
"$SPLAT_TRANSFORM" -w "$src" "$tmp/scene.sog"
rm -f "$tmp/frame.ply"
if [ "${FULL:-1}" = 1 ]; then
echo "Full version ($total splats) ..."
mkdir -p "$tmp/full"
"$SPLAT_TRANSFORM" -w "$tmp/spirula/scene.ply" "$tmp/full/scene.sog"
cp "$tmp/scene.xrSceneTransform.json" "$tmp/full/scene.xrSceneTransform.json"
fi
# Preview for the gallery: the floor plan of all rooms (labelled by room, not by recording)
plan_args=("$@")
if [ -n "${ROOMS:-}" ]; then
mapfile -t plan_args < <(python3 -c '
import json, sys
for arg, room in zip(sys.argv[2:], json.loads(sys.argv[1])):
print(arg.partition("=")[0] + "=" + room)' "$ROOMS" "$@")
fi
uv run -q "$HERE/floorplan.py" "$tmp/plan.png" "${plan_args[@]}" ${PLAN_COMMA:+--comma} > "$tmp/plan.json"
uv run -q "$HERE/make_gallery.py" --plan "$tmp/plan.png" "$tmp" "$name" "$stem"
if [ -d "$tmp/full" ]; then
uv run -q "$HERE/make_gallery.py" --plan "$tmp/plan.png" "$tmp/full" "${FULL_NAME:-$name (full)}" "$stem-full"
cp "$tmp/full/scene.sog" "$tmp/full/$stem-full.sog"
cp "$tmp/full/scene.xrSceneTransform.json" "$tmp/full/$stem-full.xrSceneTransform.json"
world_scale "$tmp/full/$stem-full.xrSceneTransform.json"
fi
# Replace the previous result only now
if [ -e "$out" ]; then mv "$out" "$tmp.old"; fi
mv "$tmp" "$out"
rm -rf "$tmp.old"
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: $out"
else
LOG="$log" "$HERE/to-frame.sh" "$out" "$name"
fi
}
mkdir -p "$RUNS_DIR"
main "$@" 2>&1 | tee -a "$log"
exit
}