Files
DarkRoom/tools/manual/record.sh
T
dtourolle 757133d2a8 Picture a panorama too large for one texture being navigated
A 22 927 × 8966 Lightroom panorama of a glacier, opened in develop:
the wheel in from fit to the peaks, a slow pan along the ridge at that
zoom, back out, then 1:1 with a double-click and fit with another. The
scene borrows the file from outside the demo library, copies it into a
panorama folder, restarts so the scan finds it, and deletes it and the
sidecar the app writes beside it before restarting again; it is
registered last so no other scene sees it.

The film is fine ice and rock in every frame and was 22 MB at the
usual 960 px and ten frames a second, so a scene can now name its own
GIF size (GIF_SIZE, read by record.sh through `scenes.py gif`); this
one is 640 px at eight, 8.7 MB.
2026-09-27 19:43:09 -04:00

158 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Re-record the manual: docs/manual/README.md's pictures, from the app itself.
#
# tools/manual/record.sh LIBRARY [SCENE...] every scene, or those named
# tools/manual/record.sh --changed LIBRARY the scenes whose sources
# changed since their pictures
# tools/manual/record.sh --check the manual and the scenes
# agree (no app, no display)
# tools/manual/record.sh --list the scenes and their pictures
#
# Needs Xvfb, xdotool, ImageMagick's `import`, ffmpeg and optipng. Builds the
# desktop binary with the `automation` feature — the hook scenes.py finds
# controls by name through — unless DR_BIN names one. Starts a private X
# server, opens LIBRARY in a fresh throwaway profile under DR_HOME, runs the
# scenes and writes PNGs and GIFs into docs/manual/media/.
#
# LIBRARY is the author's demo set — seventy face-free frames, the
# twelve-frame panorama from fixtures/pano among them — and the scenes open
# its photographs by file name. Recording changes it (ratings, collections,
# the merged DNG), so DR_LIBRARY_SNAPSHOT, if set, is copied over it first.
#
# Environment: DR_HOME (/var/tmp/dr-manual), DR_DISPLAY (:7), DR_BIN,
# DR_LIBRARY_SNAPSHOT, DR_EXPORT_DIR (DR_HOME/Exports), DR_INFERENCE
# (`cpu`, the default, pins inference to the CPU so a recording does not
# fight a training run for the GPU; `probe` lets the app choose), CARGO
# (cargo; a wrapper taking cargo's arguments works too).
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
repo="$(cd "$here/../.." && pwd)"
scenes="$here/scenes.py"
mode=record
case "${1:-}" in
--check) exec python3 "$scenes" check ;;
--list) exec python3 "$scenes" list ;;
--changed) mode=changed; shift ;;
""|-h|--help) sed -n '2,/^set -e/p' "$0" | sed '$d; s/^# \{0,1\}//'; exit 0 ;;
esac
library="$(cd "${1:?library folder}" && pwd)"
shift
if [ "$mode" = changed ]; then
python3 "$scenes" changed | tee /dev/stderr > /dev/null
mapfile -t chosen < <(python3 "$scenes" changed | cut -f1)
if [ ${#chosen[@]} -eq 0 ]; then
echo "nothing to re-record: every picture is newer than what it depends on"
exit 0
fi
set -- "${chosen[@]}"
fi
export DR_HOME="${DR_HOME:-/var/tmp/dr-manual}"
export DR_DISPLAY="${DR_DISPLAY:-:7}"
export DR_LIBRARY="$library"
export DR_AUTOMATION="$DR_HOME/automation.sock"
media="$repo/docs/manual/media"
if [ -z "${DR_BIN:-}" ]; then
(cd "$repo" && ${CARGO:-cargo} build --release -p darkroom-desktop --features automation)
DR_BIN="$repo/target/release/darkroom-desktop"
fi
export DR_BIN
if [ -n "${DR_LIBRARY_SNAPSHOT:-}" ]; then
rsync -a --delete "$DR_LIBRARY_SNAPSHOT/" "$library/"
fi
# A fresh profile every time, so a scene sees the library and not whatever
# the last run left in the catalog. The inference cache is kept — it is
# minutes of compiling — and so are the links to the models. The marker is
# what makes the wipe safe: nothing without it is deleted.
profile="$DR_HOME/xdg"
if [ -d "$profile" ] && [ ! -e "$DR_HOME/.manual-profile" ]; then
echo "$profile exists and was not made by record.sh; set DR_HOME elsewhere" >&2
exit 1
fi
mkdir -p "$DR_HOME" && touch "$DR_HOME/.manual-profile"
rm -rf "$profile/config" "$profile/state"
if [ -d "$profile/data/darkroom" ]; then
find "$profile/data/darkroom" -mindepth 1 -maxdepth 1 \
! -name inference ! -name models ! -name runtime -exec rm -rf {} +
fi
mkdir -p "$profile/config/darkroom" "$profile/data/darkroom"
# The models and the runtime are shared with the real profile: a segmenter
# that is not there makes "Find subjects" a picture of nothing.
for d in models runtime; do
src="${XDG_DATA_HOME:-$HOME/.local/share}/darkroom/$d"
[ -e "$src" ] && ln -sfn "$src" "$profile/data/darkroom/$d"
done
# Keep the inference choice the app last made, but on the CPU unless asked.
backend="$profile/data/darkroom/inference/backend.json"
if [ "${DR_INFERENCE:-cpu}" = cpu ] && [ -e "$backend" ]; then
python3 - "$backend" <<'PY'
import json, sys
p = sys.argv[1]
d = json.load(open(p))
d['rung'], d['reason'] = 'Cpu', 'forced for the demo'
json.dump(d, open(p, 'w'), indent=2)
PY
fi
# A folder library, chosen: no launch screen on the way in.
cat > "$profile/config/darkroom/sessions.json" <<JSON
{ "version": 0, "sessions": [ { "backend": "folder", "server": "$library",
"login": "", "user_id": "", "root": "", "root_chosen": true,
"formats": [], "last_scan": null } ] }
JSON
# An export folder set before albums becomes the album "Exports" on first
# open (albums_ui::adopt_old_destination), which is how the albums scene has
# an album without the portal's dialogue. In the throwaway profile's folder,
# outside the library, and the scene deletes what it exported.
export DR_EXPORT_DIR="${DR_EXPORT_DIR:-$DR_HOME/Exports}"
export_dir="$DR_EXPORT_DIR"
mkdir -p "$export_dir"
cat > "$profile/config/darkroom/settings.json" <<JSON
{ "export": { "destination": "$export_dir" } }
JSON
started_x=""
if ! DISPLAY="$DR_DISPLAY" xdpyinfo >/dev/null 2>&1; then
Xvfb "$DR_DISPLAY" -screen 0 1920x1200x24 +extension GLX +render -noreset \
> "$DR_HOME/xvfb.log" 2>&1 &
echo $! > "$DR_HOME/xvfb.pid"
started_x=1
sleep 2
fi
cleanup() {
python3 "$here/drive.py" stop || true
if [ -n "$started_x" ]; then kill "$(cat "$DR_HOME/xvfb.pid")" || true; fi
}
trap cleanup EXIT
stamp="$DR_HOME/.recording-started"
touch "$stamp"
python3 "$here/drive.py" launch
DR_TOOLS="$here" python3 -c '
import os, sys, time
sys.path.insert(0, os.environ["DR_TOOLS"])
import drive as dr
dr.wait_ready()
dr.wait_for("id:grid-scroll", 120)
time.sleep(10) # the first thumbnails, and the scan settling
'
python3 "$scenes" record "$media" "${@:-all}"
# GIFs for the page; the MP4s are working files and are not kept.
for mp4 in "$media"/*.mp4; do
[ -e "$mp4" ] || continue
read -r width fps < <(python3 "$scenes" gif "$(basename "${mp4%.mp4}")")
"$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" "$width" "$fps"
rm "$mp4"
done
find "$media" -name '*.png' -newer "$stamp" -exec optipng -quiet -o2 {} + || true