aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/companion/gen-frames
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-23 11:57:21 +0200
committerDanilo M. <danix@danix.xyz>2026-10-23 11:57:21 +0200
commit14a2c755d6b4488c8572dcfa5d64f673fae9cedc (patch)
treee7c67b58243bd8c836779ec3f8e42a11c7eb6291 /desktop/companion/gen-frames
parent96752d5c09a08991ef6efe80dafbeab98ca32321 (diff)
downloadquickshell-14a2c755d6b4488c8572dcfa5d64f673fae9cedc.tar.gz
quickshell-14a2c755d6b4488c8572dcfa5d64f673fae9cedc.zip
companion: frames.txt, gen-frames and prep-frames
The frames are a list now, not a session in the web UI: frames.txt holds one line per frame (output, the image klein edits, the prompt, with shared sentences as variables), and gen-frames makes the missing ones, or the ones named, through the running sd-server's job API (/sdcpp/v1/img_gen, polled at /sdcpp/v1/jobs/<id>). sd-cli would reload the model for every frame. Adding or changing a frame is an edit and a rerun; --seed rerolls one. Then prep-frames cuts each out of its white background at one common scale. Checked on dress/smile: about 2 minutes a frame on klein, pose and outfit kept. The art stays outside the repo, in ~/.local/share/keystone/companion. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/companion/gen-frames')
-rwxr-xr-xdesktop/companion/gen-frames142
1 files changed, 142 insertions, 0 deletions
diff --git a/desktop/companion/gen-frames b/desktop/companion/gen-frames
new file mode 100755
index 0000000..75e6655
--- /dev/null
+++ b/desktop/companion/gen-frames
@@ -0,0 +1,142 @@
+#!/bin/bash
+#
+# Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# gen-frames: make the companion's frames listed in frames.txt.
+#
+# gen-frames [--seed N] [--list] [OUTPUT...]
+#
+# With no OUTPUT, makes every frame whose image does not exist yet. Naming
+# outputs (dress/mad, leotard/mad) remakes those, existing or not; a dress
+# frame remade leaves its leotard frame stale, so name both. --seed changes
+# the seed (default 42) to reroll a result; --list prints what would be made.
+# Then runs prep-frames.
+#
+# It drives a running sd-server rather than sd-cli, which would reload the
+# model for every frame: start sd-webui and pick FLUX.2 klein first. sd-webui
+# also unloads llama-server's model, which otherwise leaves too little VRAM.
+# The model is checked from sd-server's command line, since its API does not
+# name it.
+
+set -euo pipefail
+
+here=$(cd "$(dirname "$0")" && pwd)
+art=${ART_DIR:-$HOME/.local/share/keystone/companion/2b}
+url=${SD_URL:-http://127.0.0.1:7860}
+seed=42
+list=0
+want=()
+while (($#)); do
+ case $1 in
+ --seed) seed=$2; shift 2 ;;
+ --list) list=1; shift ;;
+ -h|--help) sed -n '14,28p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
+ *) want+=("$1"); shift ;;
+ esac
+done
+
+# frames.txt to "output<TAB>reference<TAB>prompt" lines, variables expanded.
+parse() {
+ local line name out ref prompt
+ local frame='^[[:space:]]*([a-z0-9/_-]+)[[:space:]]*<-[[:space:]]*([a-z0-9/_-]+)[[:space:]]*:[[:space:]]*(.*)$'
+ declare -A vars=()
+ while IFS= read -r line || [[ -n $line ]]; do
+ [[ $line =~ ^[[:space:]]*(#|$) ]] && continue
+ if [[ $line =~ ^([A-Z_]+)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
+ vars[${BASH_REMATCH[1]}]=${BASH_REMATCH[2]}
+ continue
+ fi
+ if [[ ! $line =~ $frame ]]; then
+ echo "gen-frames: frames.txt: cannot read: $line" >&2
+ exit 1
+ fi
+ out=${BASH_REMATCH[1]} ref=${BASH_REMATCH[2]} prompt=${BASH_REMATCH[3]}
+ for name in "${!vars[@]}"; do prompt=${prompt//\$\{$name\}/${vars[$name]}}; done
+ if [[ $prompt == *'${'* ]]; then
+ echo "gen-frames: frames.txt: $out: undefined variable in: $prompt" >&2
+ exit 1
+ fi
+ printf '%s\t%s\t%s\n' "$out" "$ref" "$prompt"
+ done <"$here/frames.txt"
+}
+
+# What to make: the named outputs, else every missing one, in file order.
+# (No grep -q in a pipe: under pipefail its early exit fails the printf.)
+todo=()
+rows=$(parse) || exit 1
+mapfile -t rows <<<"$rows"
+declare -A known=() wanted=()
+for row in "${rows[@]}"; do known[${row%%$'\t'*}]=1; done
+for name in "${want[@]}"; do
+ [[ -n ${known[$name]:-} ]] || { echo "gen-frames: $name is not in frames.txt" >&2; exit 1; }
+ wanted[$name]=1
+done
+for row in "${rows[@]}"; do
+ out=${row%%$'\t'*}
+ if ((${#want[@]})); then
+ [[ -n ${wanted[$out]:-} ]] && todo+=("$row")
+ elif [[ ! -e $art/$out.png ]]; then
+ todo+=("$row")
+ fi
+done
+if ((list)); then
+ for row in "${todo[@]}"; do printf '%s <- %s\n' "${row%%$'\t'*}" "$(cut -f2 <<<"$row")"; done
+ exit 0
+fi
+if ((${#todo[@]} == 0)); then
+ echo "gen-frames: nothing to make"
+ exec "$here/prep-frames" "$art"
+fi
+
+pgrep -ax sd-server | grep -q klein ||
+ { echo "gen-frames: sd-server is not running FLUX.2 klein; start sd-webui and pick it" >&2; exit 1; }
+
+tmp=$(mktemp -d)
+trap 'rm -rf "$tmp"' EXIT
+
+made=0
+for row in "${todo[@]}"; do
+ IFS=$'\t' read -r out ref prompt <<<"$row"
+ if [[ ! -e $art/$ref.png ]]; then
+ echo "gen-frames: $out: its reference $ref.png does not exist yet, skipped" >&2
+ continue
+ fi
+ echo "gen-frames: $out <- $ref"
+ { printf 'data:image/png;base64,'; base64 -w0 "$art/$ref.png"; } >"$tmp/ref"
+ # The settings the frames were tuned with in sd-webui: recipe.txt.
+ jq -n --arg prompt "$prompt" --rawfile ref "$tmp/ref" --argjson seed "$seed" '{
+ prompt: $prompt, negative_prompt: "", width: 1024, height: 1024, seed: $seed,
+ batch_count: 1, ref_images: [$ref], auto_resize_ref_image: true,
+ increase_ref_index: false, lora: [], output_format: "png", output_compression: 100,
+ sample_params: { sample_steps: 4, sample_method: "euler", scheduler: "default",
+ guidance: { txt_cfg: 1.0 } }
+ }' >"$tmp/body"
+ id=$(curl -sf -m 30 -X POST "$url/sdcpp/v1/img_gen" -H 'Content-Type: application/json' \
+ --data-binary @"$tmp/body" | jq -r '.id // empty') || id=""
+ [[ -n $id ]] || { echo "gen-frames: $out: sd-server refused the job" >&2; exit 1; }
+ while :; do
+ curl -sf -m 30 "$url/sdcpp/v1/jobs/$id" >"$tmp/job" || { echo "gen-frames: $out: lost the job" >&2; exit 1; }
+ status=$(jq -r '.status' "$tmp/job")
+ case $status in
+ queued|generating) sleep 2 ;;
+ completed) break ;;
+ *) echo "gen-frames: $out: $status: $(jq -r '.error.message // "no message"' "$tmp/job")" >&2; exit 1 ;;
+ esac
+ done
+ mkdir -p "$art/$(dirname "$out")"
+ jq -r '.result.images[0].b64_json' "$tmp/job" | base64 -d >"$tmp/out.png"
+ mv "$tmp/out.png" "$art/$out.png"
+ printf '%s seed %s %s <- %s\n' "$(date +%F)" "$seed" "$out" "$ref" >>"$art/generated.log"
+ made=$((made + 1))
+done
+echo "gen-frames: made $made"
+exec "$here/prep-frames" "$art"