From 42009efd09998677b8b5fcccf0402e81f7629489 Mon Sep 17 00:00:00 2001 From: hacka0wi <124970567+hacka0wi@users.noreply.github.com> Date: Fri, 8 May 2026 00:54:13 +0700 Subject: [PATCH] =?UTF-8?q?feat(scripts):=20make-demo-gif.sh=20=E2=80=94?= =?UTF-8?q?=20produce=20optimized=20README=20hero=20GIF=20(Closes=20#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turn a screen recording (.mov from QuickTime/OBS) into a README-ready optimized GIF. Two-stage pipeline so input format/codec doesn't matter: 1. ffmpeg trim + scale + fps + drop audio → intermediate mp4 2. gifski (preferred, perceptual quantizer) or ffmpeg palettegen+ paletteuse fallback → final gif Defaults tuned for UI demos: DURATION=15, WIDTH=900, FPS=15, TARGET_MAX_MB=10 All overridable via env vars. The script warns (doesn't fail) when the output exceeds the soft limit and suggests three knobs to turn. Usage: scripts/make-demo-gif.sh ~/Desktop/runloop-demo.mov # → docs/screenshots/flow-editor.gif --- scripts/make-demo-gif.sh | 106 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100755 scripts/make-demo-gif.sh diff --git a/scripts/make-demo-gif.sh b/scripts/make-demo-gif.sh new file mode 100755 index 0000000..26665c6 --- /dev/null +++ b/scripts/make-demo-gif.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# make-demo-gif.sh — turn a screen recording into a README-ready GIF. +# +# Usage: +# scripts/make-demo-gif.sh [output.gif] +# +# Defaults: +# output docs/screenshots/flow-editor.gif +# trim first 15 s of the recording +# width 900 px (preserves aspect ratio) +# fps 15 (good motion-to-size tradeoff for UI demos) +# palette gifski's perceptual quantizer (preferred), or +# ffmpeg's two-pass palettegen+paletteuse (fallback) +# +# Why this script: we want the README hero GIF to look sharp without +# blowing past the 10 MiB GitHub asset soft-limit. Gifski is markedly +# better than ffmpeg's GIF encoder for UI screen captures, but isn't +# always installed; ffmpeg almost always is. Both branches go through +# the same trim+scale ffmpeg first stage so the input format/codec +# doesn't matter. + +set -euo pipefail + +if [[ $# -lt 1 ]]; then + echo "usage: $0 [output.gif]" >&2 + echo " default output: docs/screenshots/flow-editor.gif" >&2 + exit 1 +fi + +INPUT="$1" +OUTPUT="${2:-docs/screenshots/flow-editor.gif}" +DURATION="${DURATION:-15}" +WIDTH="${WIDTH:-900}" +FPS="${FPS:-15}" +TARGET_MAX_MB="${TARGET_MAX_MB:-10}" + +if [[ ! -f "$INPUT" ]]; then + echo "❌ input not found: $INPUT" >&2 + exit 1 +fi + +if ! command -v ffmpeg >/dev/null 2>&1; then + echo "❌ ffmpeg not found. Install it:" >&2 + echo " macOS: brew install ffmpeg" >&2 + echo " debian: apt install ffmpeg" >&2 + exit 1 +fi + +mkdir -p "$(dirname "$OUTPUT")" + +WORK_DIR="$(mktemp -d -t make-demo-gif.XXXXXX)" +trap 'rm -rf "$WORK_DIR"' EXIT + +INTERMEDIATE_MP4="$WORK_DIR/trimmed.mp4" + +say() { printf "\n\033[1;36m==> %s\033[0m\n" "$*"; } + +# ── 1. Trim + scale + drop audio ───────────────────────────────────── +say "Trim to ${DURATION}s, scale width to ${WIDTH}, fps=${FPS}" +ffmpeg -hide_banner -loglevel warning -y \ + -i "$INPUT" \ + -t "$DURATION" \ + -vf "scale=${WIDTH}:-2:flags=lanczos,fps=${FPS}" \ + -an \ + -c:v libx264 -preset slow -crf 18 \ + "$INTERMEDIATE_MP4" + +# ── 2. Encode GIF — gifski (preferred) or ffmpeg palettegen ────────── +if command -v gifski >/dev/null 2>&1; then + say "Encoding GIF with gifski (high quality)" + # gifski needs frames; pull them out first. + FRAMES_DIR="$WORK_DIR/frames" + mkdir -p "$FRAMES_DIR" + ffmpeg -hide_banner -loglevel warning -y \ + -i "$INTERMEDIATE_MP4" \ + "$FRAMES_DIR/frame-%05d.png" + gifski --fps "$FPS" --width "$WIDTH" --quality 90 \ + -o "$OUTPUT" "$FRAMES_DIR"/frame-*.png +else + say "Encoding GIF with ffmpeg palettegen+paletteuse (gifski not installed; brew install gifski for better quality)" + PALETTE="$WORK_DIR/palette.png" + ffmpeg -hide_banner -loglevel warning -y \ + -i "$INTERMEDIATE_MP4" \ + -vf "palettegen=stats_mode=diff" \ + "$PALETTE" + ffmpeg -hide_banner -loglevel warning -y \ + -i "$INTERMEDIATE_MP4" -i "$PALETTE" \ + -filter_complex "paletteuse=dither=bayer:bayer_scale=5:diff_mode=rectangle" \ + "$OUTPUT" +fi + +# ── 3. Report size + warn if over target ───────────────────────────── +SIZE_BYTES=$(stat -f%z "$OUTPUT" 2>/dev/null || stat -c%s "$OUTPUT") +SIZE_MB=$(awk "BEGIN {printf \"%.2f\", $SIZE_BYTES / 1024 / 1024}") +say "Wrote $OUTPUT (${SIZE_MB} MiB)" + +if (( $(awk "BEGIN {print ($SIZE_MB > $TARGET_MAX_MB)}") )); then + cat >&2 <