#!/usr/bin/env bash # Regenerate the website capture assets: search.webm, manage-indexing.webm, # duplicates.png, query-highlight.png — all landing in packaging/captures/ # (gitignored). Needs a graphical session (X11 or Wayland — screenshots and # video frames are read back from the app's own framebuffer, so the display # server does not matter) and ffmpeg with libx264rgb, libvpx-vp9 and libwebp. # # Alongside those masters it emits the derivatives the website actually ships: # -700.webp thumbnails, -1400.webp for the lightbox, and a -poster.webp first # frame per clip. The website only needs the .webm and .webp files; the PNG # masters stay here. # # The app is built with the `capture` feature and drives itself through # packaging/capture-scenario.txt (see crates/quicksearch-gui/src/capture.rs # for the command grammar). It runs against a throwaway index of this # repository plus ~/.cargo/registry/src, under scratch XDG dirs — the real # ~/.config/quicksearch and index are never touched. # # The scratch dirs live OUTSIDE both index roots on purpose: the demo config # has an empty ignore list, so a scratch index database inside an indexed # tree would be indexed and watched by the very run that writes it. set -euo pipefail here="$(cd "$(dirname "$0")" && pwd)" repo="$(dirname "$here")" out="$here/captures" work="${TMPDIR:-/tmp}/quicksearch-capture" # --- preflight -------------------------------------------------------------- if [ -z "${DISPLAY:-}" ] && [ -z "${WAYLAND_DISPLAY:-}" ]; then echo "capture needs a graphical session (neither DISPLAY nor" \ "WAYLAND_DISPLAY is set)" >&2 exit 1 fi for tool in ffmpeg ffprobe cargo; do command -v "$tool" >/dev/null || { echo "missing tool: $tool" >&2; exit 1; } done # The encoder list is captured first: `grep -q` closing the pipe early would # make ffmpeg exit on SIGPIPE, which pipefail reports as failure. encoders="$(ffmpeg -hide_banner -encoders 2>/dev/null)" for enc in libx264rgb libvpx-vp9 libwebp; do grep -q "$enc" <<< "$encoders" \ || { echo "ffmpeg lacks the $enc encoder" >&2; exit 1; } done # A config.toml beside the binary would silently override the scratch XDG # config entirely (portable mode) — refuse to run with one present. if [ -e "$repo/target/release/config.toml" ]; then echo "remove $repo/target/release/config.toml first: portable mode would" \ "override the capture config" >&2 exit 1 fi registry="$HOME/.cargo/registry/src" [ -d "$registry" ] || { echo "missing demo index root: $registry" >&2; exit 1; } # --- build + parser tests --------------------------------------------------- cargo test --manifest-path "$repo/Cargo.toml" --release --locked \ -p quicksearch-gui --features capture capture:: cargo build --manifest-path "$repo/Cargo.toml" --release --locked \ -p quicksearch-gui --features capture # --- scratch environment ---------------------------------------------------- # Fresh every run: no stale index, no schema-mismatch prompt, and a fresh # app.ron so the window opens at its default 1000x700 geometry. rm -rf "$work" mkdir -p "$work/config/quicksearch" "$work/data" "$work/tmp" "$out" cat > "$work/config/quicksearch/config.toml" <&2; exit 1; } # --- transcode the lossless intermediates to VP9 webm ----------------------- # The recording itself is lossless, so webm quality is decided entirely here. # QS_WEBM_CRF is the knob: constant-quality factor, 0 (best) to 63; UI text # stays crisp around 24-32, and each step of ~6 roughly halves/doubles the # file size. QS_WEBM_CPU trades encode time for compression efficiency # (0 slowest/best to 5 fastest). crf="${QS_WEBM_CRF:-26}" cpu="${QS_WEBM_CPU:-0}" for clip in manage-indexing search; do [ -s "$work/tmp/$clip.cap.mkv" ] \ || { echo "missing recording: $clip.cap.mkv" >&2; exit 1; } # The crop drops at most one row/column: yuv420p needs even dimensions, # and fractional display scaling can make the window an odd size. ffmpeg -y -hide_banner -loglevel warning -i "$work/tmp/$clip.cap.mkv" \ -vf "crop=trunc(iw/2)*2:trunc(ih/2)*2" \ -c:v libvpx-vp9 -b:v 0 -crf "$crf" -deadline good -cpu-used "$cpu" \ -row-mt 1 -pix_fmt yuv420p -an "$out/$clip.webm" done mv "$work/tmp/query-highlight.png" "$work/tmp/duplicates.png" "$out/" # --- web derivatives -------------------------------------------------------- # The PNG masters are ~1400x980 and 350-700 KB each, but the website renders # them in a ~320 CSS px column (~350 px full-width on a phone). Shipping the # masters was costing about a megabyte for two thumbnails, so the site loads # -700.webp inline and only fetches -1400.webp when the lightbox opens. # QS_WEBP_Q is the inline-thumbnail quality; the lightbox copy is encoded # higher because it is the one people zoom into to read UI text. webp_q="${QS_WEBP_Q:-82}" webp_q_full="${QS_WEBP_Q_FULL:-90}" for shot in duplicates query-highlight; do ffmpeg -y -hide_banner -loglevel warning -i "$out/$shot.png" \ -vf "scale=700:-2:flags=lanczos" \ -c:v libwebp -quality "$webp_q" -compression_level 6 \ "$out/$shot-700.webp" ffmpeg -y -hide_banner -loglevel warning -i "$out/$shot.png" \ -c:v libwebp -quality "$webp_q_full" -compression_level 6 \ "$out/$shot-1400.webp" done # Poster frames: with the videos deferred behind an IntersectionObserver these # are what the page paints on first load, so each is sized to the slot it # actually renders in — search is the full-width hero, manage-indexing is one # column of a three-up grid. The frame is taken from 80% through rather than # frame 0: every clip opens on an empty results list, which is the worst # possible still to hold on the hero while the video is still downloading. for spec in "search:1200" "manage-indexing:700"; do clip="${spec%:*}"; pw="${spec#*:}" dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 \ "$out/$clip.webm") at=$(awk -v d="$dur" 'BEGIN{printf "%.2f", d*0.8}') ffmpeg -y -hide_banner -loglevel warning -ss "$at" -i "$out/$clip.webm" \ -frames:v 1 -vf "scale=$pw:-2:flags=lanczos" \ -c:v libwebp -quality "$webp_q" -compression_level 6 \ "$out/$clip-poster.webp" done # --- verify ----------------------------------------------------------------- fail=0 for f in "$out/search.webm" "$out/manage-indexing.webm"; do codec=$(ffprobe -v error -select_streams v:0 \ -show_entries stream=codec_name -of csv=p=0 "$f") dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$f") [ "$codec" = vp9 ] || { echo "FAIL: $f codec=$codec" >&2; fail=1; } awk -v d="$dur" 'BEGIN{exit !(d>=4 && d<=60)}' \ || { echo "FAIL: $f duration=${dur}s (expected 4-60s)" >&2; fail=1; } echo "OK: $f (vp9, ${dur}s)" done for f in "$out/duplicates.png" "$out/query-highlight.png"; do dims=$(ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height -of csv=p=0 "$f") echo "OK: $f (${dims})" done # The derivatives are what the website ships, so a silent encode failure here # would ship broken tags rather than merely large ones. for f in "$out/duplicates-700.webp" "$out/duplicates-1400.webp" \ "$out/query-highlight-700.webp" "$out/query-highlight-1400.webp" \ "$out/search-poster.webp" "$out/manage-indexing-poster.webp"; do [ -s "$f" ] || { echo "FAIL: missing or empty $f" >&2; fail=1; continue; } dims=$(ffprobe -v error -select_streams v:0 \ -show_entries stream=width,height -of csv=p=0 "$f") echo "OK: $f (${dims}, $(stat -c%s "$f") bytes)" done [ "$fail" -eq 0 ] # $work is kept for post-mortems (app stderr is on this terminal; the scratch # index and raw .cap.mkv files live there) and recreated fresh next run. echo echo "Assets (the website needs the .webm and .webp files; PNGs stay here):" ls -l "$out"/*.webm "$out"/*.png "$out"/*.webp