#!/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 (LIBRARY/../export), 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" < "$profile/config/darkroom/settings.json" </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 "$here/gif.sh" "$mp4" "${mp4%.mp4}.gif" 960 10 rm "$mp4" done find "$media" -name '*.png' -newer "$stamp" -exec optipng -quiet -o2 {} + || true