diff --git a/packages/cli/demo/.gitignore b/packages/cli/demo/.gitignore new file mode 100644 index 00000000000..c18dd8d83ce --- /dev/null +++ b/packages/cli/demo/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/packages/cli/demo/add-sound.sh b/packages/cli/demo/add-sound.sh new file mode 100755 index 00000000000..6db901a5dad --- /dev/null +++ b/packages/cli/demo/add-sound.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Adds a soundtrack to the VHS-rendered demo: a lo-fi music bed (demo/music.mp3) +# plus mechanical-keyboard clicks timed off the .tape script. +# +# Input: demo/superset-cli.mp4 (produced by `vhs demo/superset-cli.tape`) +# demo/music.mp3 (the music bed) +# demo/keyboard.mp3 (a continuous mechanical-keyboard recording) +# Output: demo/superset-cli-sound.mp4 +# +# Credits: music — "Lofi Production" by Pulsebox (Pixabay, royalty-free); +# keyboard — "Mechanical Keyboard Typing HD" by VirtualZero (Pixabay). +# Individual keystrokes are sliced out of keyboard.mp3 and dropped onto the +# .tape timeline (one random sample per key, with slight pitch/level jitter). +# If keyboard.mp3 is missing, the clicks fall back to a numpy synth. +set -euo pipefail +cd "$(dirname "$0")/.." # -> packages/cli +SRC=demo/superset-cli.mp4 +TAPE=demo/superset-cli.tape +MUSIC=${1:-demo/music.mp3} +KB=demo/keyboard.mp3 +OUT=demo/superset-cli-sound.mp4 +TMP=$(mktemp -d -t demo-sound) +CLICKS="$TMP/clicks.wav" + +[ -f "$SRC" ] || { echo "missing $SRC — run: vhs demo/superset-cli.tape" >&2; exit 1; } +[ -f "$MUSIC" ] || { echo "missing music bed: $MUSIC" >&2; exit 1; } +DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$SRC") +FADE_AT=$(awk "BEGIN{print $DUR-3}") + +KEYS_ARG="" +if [ -f "$KB" ]; then + echo "slicing keystroke samples from $KB ..." + ffmpeg -y -loglevel error -i "$KB" -ac 1 -ar 44100 "$TMP/kb.wav" + python3 demo/extract_keys.py "$TMP/kb.wav" "$TMP/keys" + KEYS_ARG="$TMP/keys" +fi + +echo "placing clicks on the timeline..." +python3 demo/gen_audio.py "$TAPE" "$CLICKS" "$DUR" "$KEYS_ARG" + +# [music] -> trim to video length, fade in/out, light low-pass, drop the level +# [clicks] -> as-is (already left headroom); mix, keep under the ceiling +ffmpeg -y -i "$SRC" -i "$MUSIC" -i "$CLICKS" \ + -filter_complex "\ + [1:a]atrim=0:${DUR},asetpts=PTS-STARTPTS,lowpass=f=12000,volume=0.5,afade=t=in:st=0:d=2,afade=t=out:st=${FADE_AT}:d=3[mus];\ + [2:a]volume=0.4[clk];\ + [mus][clk]amix=inputs=2:normalize=0,alimiter=limit=0.95:level=disabled,aresample=44100[a]" \ + -map 0:v -map "[a]" -c:v copy -c:a aac -b:a 192k -shortest "$OUT" +rm -rf "$TMP" +echo "wrote $OUT" diff --git a/packages/cli/demo/extract_keys.py b/packages/cli/demo/extract_keys.py new file mode 100644 index 00000000000..39298b7bfbd --- /dev/null +++ b/packages/cli/demo/extract_keys.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Slice individual keystroke samples out of a continuous typing recording. + +Detects transients in and writes one short WAV per keystroke into +/keyNN.wav (1 ms fade-in, ~20 ms fade-out so the edges don't click). + +Usage: python3 extract_keys.py +""" +import os +import sys +import wave + +import numpy as np + +SR = 44100 + + +def load_mono(path): + with wave.open(path, "rb") as w: + n, sw, ch, fr = w.getnframes(), w.getsampwidth(), w.getnchannels(), w.getframerate() + raw = w.readframes(n) + dt = {1: np.int8, 2: np.int16, 4: np.int32}[sw] + a = np.frombuffer(raw, dtype=dt).astype(np.float32) + a = (a - 128) / 128.0 if sw == 1 else a / float(np.iinfo(dt).max) + if ch > 1: + a = a.reshape(-1, ch).mean(axis=1) + if fr != SR: + idx = np.linspace(0, len(a) - 1, int(len(a) * SR / fr)) + a = np.interp(idx, np.arange(len(a)), a) + return a + + +def detect_onsets(a, min_gap=0.06, thr_frac=0.16): + win = int(SR * 0.003) + sm = np.convolve(np.abs(a), np.ones(win) / win, "same") + above = sm > thr_frac * sm.max() + rising = np.where(above[1:] & ~above[:-1])[0] + out, last = [], -10 * SR + for i in rising: + if i - last > int(SR * min_gap): + out.append(i) + last = i + return out + + +def main(): + src, out_dir = sys.argv[1], sys.argv[2] + os.makedirs(out_dir, exist_ok=True) + a = load_mono(src) + onsets = detect_onsets(a) + + pre, length = int(SR * 0.004), int(SR * 0.14) + fi, fo = int(SR * 0.001), int(SR * 0.02) + kept = 0 + peak_global = np.max(np.abs(a)) or 1.0 + for k, on in enumerate(onsets): + s = max(0, on - pre) + seg = a[s:s + length].copy() + if len(seg) < length // 2: + continue + if np.max(np.abs(seg)) < 0.06 * peak_global: # too quiet — probably a tail, skip + continue + # mellow it a touch: gentle low-pass + a softer/longer fade-out + a_lp = np.exp(-2 * np.pi * 3800 / SR) + prev = 0.0 + for j in range(len(seg)): + prev = (1 - a_lp) * seg[j] + a_lp * prev + seg[j] = prev + fo = int(SR * 0.045) + if len(seg) >= fi + fo: + seg[:fi] *= np.linspace(0, 1, fi) + seg[-fo:] *= np.linspace(1, 0, fo) ** 1.5 + seg = seg / (np.max(np.abs(seg)) or 1.0) * 0.95 + kept += 1 + with wave.open(os.path.join(out_dir, f"key{kept:02d}.wav"), "wb") as w: + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(SR) + w.writeframes((seg * 32767).astype(np.int16).tobytes()) + print(f" extracted {kept} keystroke samples from {os.path.basename(src)} -> {out_dir}/") + + +if __name__ == "__main__": + main() diff --git a/packages/cli/demo/gen_audio.py b/packages/cli/demo/gen_audio.py new file mode 100644 index 00000000000..9f9ebd172f3 --- /dev/null +++ b/packages/cli/demo/gen_audio.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Generate a keyboard-click track for the VHS demo, timed off the .tape script. + +Clicks are placed by replaying the .tape timeline: every `Type` character costs +`TypingSpeed`, every `Enter` is a keystroke, every `Sleep` advances the clock, +and the `Hide`..`Show` block is skipped (VHS doesn't render it). + +The click sound is synthesized with numpy by default. Pass a WAV (a single +keystroke) or a directory of WAVs (a pool of keystrokes — picked at random per +key) to use real recordings instead; each hit gets slight pitch/level jitter. + +Usage: python3 gen_audio.py [keys.wav|keys_dir] [keyreturn.wav] +""" +import os +import re +import sys +import wave + +import numpy as np + +SR = 44100 +_rng = np.random.default_rng(7) + + +# ---------------------------------------------------------------- tape timeline +def parse_events(tape_path): + typing_speed = 0.05 # VHS default; tape overrides via `Set TypingSpeed` + t = 0.0 + in_hidden = False + keys, returns = [], [] + + for raw in open(tape_path, encoding="utf-8"): + line = raw.strip() + if not line or line.startswith("#"): + continue + head = line.split(None, 1)[0] + + if head == "Hide": + in_hidden = True + continue + if head == "Show": + in_hidden = False + t = 0.0 + continue + + m = re.match(r"Set\s+TypingSpeed\s+([\d.]+)(ms|s)?", line) + if m: + typing_speed = float(m.group(1)) / (1000 if m.group(2) == "ms" else 1) + continue + if head in ("Set", "Output", "Require", "Env"): + continue + + m = re.match(r"Sleep\s+([\d.]+)(ms|s)?", line) + if m: + dt = float(m.group(1)) / (1000 if (m.group(2) or "s") == "ms" else 1) + if not in_hidden: + t += dt + continue + + if head == "Type": + body = line[len("Type"):].strip() + if len(body) >= 2 and body[0] in "\"'`" and body[-1] == body[0]: + body = body[1:-1] + for _ch in body: + if not in_hidden: + keys.append(t) + t += typing_speed + continue + + if head == "Enter": + if not in_hidden: + returns.append(t) + t += typing_speed + continue + + if re.match(r"(Ctrl\+|Alt\+|Shift\+|Backspace|Tab|Space|Escape|Up|Down|Left|Right|PageUp|PageDown)", head): + if not in_hidden: + keys.append(t) + t += typing_speed + continue + + return keys, returns + + +# --------------------------------------------------------------------- samples +def _load_wav_mono(path): + with wave.open(path, "rb") as w: + n, sw, ch, fr = w.getnframes(), w.getsampwidth(), w.getnchannels(), w.getframerate() + raw = w.readframes(n) + dt = {1: np.int8, 2: np.int16, 4: np.int32}[sw] + a = np.frombuffer(raw, dtype=dt).astype(np.float32) + if sw == 1: + a = (a - 128) / 128.0 + else: + a /= float(np.iinfo(dt).max) + if ch > 1: + a = a.reshape(-1, ch).mean(axis=1) + if fr != SR: # cheap linear resample + idx = np.linspace(0, len(a) - 1, int(len(a) * SR / fr)) + a = np.interp(idx, np.arange(len(a)), a) + # trim leading silence so the transient lands on the timestamp + thr = 0.02 * (np.max(np.abs(a)) or 1.0) + nz = np.argmax(np.abs(a) > thr) + return a[nz:] + + +def _jitter(sample, semitones=1.5, gain_db=2.5): + sp = 2 ** (_rng.uniform(-semitones, semitones) / 12) + idx = np.arange(0, len(sample), sp) + s = np.interp(idx, np.arange(len(sample)), sample) + return s * (10 ** (_rng.uniform(-gain_db, gain_db) / 20)) + + +# ---------------------------------------------------------- synthesized clicks +def _synth_click(kind="key"): + if kind == "return": + body_f, dur, amp = _rng.uniform(95, 120), 0.075, 0.95 + click_amp, noise_amp = 0.5, 0.35 + else: + body_f, dur, amp = _rng.uniform(150, 235), 0.045, _rng.uniform(0.6, 0.85) + click_amp, noise_amp = 0.45, 0.30 + n = int(SR * dur) + tt = np.arange(n) / SR + nlen = int(SR * 0.006) + noise = np.zeros(n) + noise[:nlen] = _rng.standard_normal(nlen) * np.exp(-np.arange(nlen) / (nlen * 0.4)) + noise *= noise_amp + tick = np.sin(2 * np.pi * _rng.uniform(2600, 3400) * tt) * np.exp(-tt / 0.004) * click_amp + body = np.sin(2 * np.pi * body_f * tt) * np.exp(-tt / (dur * 0.5)) + sig = (noise + tick + body) * amp + a = int(SR * 0.0008) + sig[:a] *= np.linspace(0, 1, a) + return sig.astype(np.float32) + + +def _load_pool(path): + """path may be a single WAV or a directory of WAVs. Returns a list of arrays.""" + if not path or not os.path.exists(path): + return [] + if os.path.isdir(path): + files = sorted(f for f in os.listdir(path) if f.lower().endswith(".wav")) + return [_load_wav_mono(os.path.join(path, f)) for f in files] + return [_load_wav_mono(path)] + + +# Don't fire a key click within this gap of the previous one — keeps fast +# on-screen typing from sounding like a machine gun (the audio "types" calmer). +CLICK_MIN_GAP = 0.09 + + +def _thin(times, min_gap): + out, last = [], -1e9 + for t in times: + if t - last >= min_gap: + out.append(t) + last = t + return out + + +def build_track(keys, returns, total_len, key_pool, ret_pool): + buf = np.zeros(int(SR * total_len) + SR, dtype=np.float32) + + def place(times, pool, kind, gain=1.0): + for t in times: + if pool: + c = _jitter(pool[_rng.integers(len(pool))]) * gain + else: + c = _synth_click(kind) + i = int(t * SR) + buf[i:i + len(c)] += c + + place(_thin(keys, CLICK_MIN_GAP), key_pool, "key") + # Enter: prefer a dedicated return sample; else reuse the keypress pool a touch louder + place(returns, ret_pool or key_pool, "return", gain=1.15 if not ret_pool else 1.0) + return buf[:int(SR * total_len)] + + +def main(): + tape, out_wav, dur = sys.argv[1], sys.argv[2], float(sys.argv[3]) + key_path = sys.argv[4] if len(sys.argv) > 4 else None + ret_path = sys.argv[5] if len(sys.argv) > 5 else None + + key_pool = _load_pool(key_path) + ret_pool = _load_pool(ret_path) + src = f"sample pool x{len(key_pool)}" if key_pool else "synth" + + keys, returns = parse_events(tape) + print(f" {len(keys)} keystrokes + {len(returns)} returns over {dur:.1f}s ({src} clicks)") + + track = build_track(keys, returns, dur, key_pool, ret_pool) + peak = float(np.max(np.abs(track))) or 1.0 + track = (track / peak) * 0.9 # leave headroom for the music mix downstream + pcm = (track * 32767).astype(np.int16) + + with wave.open(out_wav, "wb") as w: + w.setnchannels(1) + w.setsampwidth(2) + w.setframerate(SR) + w.writeframes(pcm.tobytes()) + print(f" wrote {out_wav}") + + +if __name__ == "__main__": + main() diff --git a/packages/cli/demo/keyboard.mp3 b/packages/cli/demo/keyboard.mp3 new file mode 100644 index 00000000000..2572b1fdde4 Binary files /dev/null and b/packages/cli/demo/keyboard.mp3 differ diff --git a/packages/cli/demo/music.mp3 b/packages/cli/demo/music.mp3 new file mode 100644 index 00000000000..82c66026455 Binary files /dev/null and b/packages/cli/demo/music.mp3 differ diff --git a/packages/cli/demo/superset-cli-sound.mp4 b/packages/cli/demo/superset-cli-sound.mp4 new file mode 100644 index 00000000000..1406dd49a4f Binary files /dev/null and b/packages/cli/demo/superset-cli-sound.mp4 differ diff --git a/packages/cli/demo/superset-cli.gif b/packages/cli/demo/superset-cli.gif new file mode 100644 index 00000000000..2118eb0ef63 Binary files /dev/null and b/packages/cli/demo/superset-cli.gif differ diff --git a/packages/cli/demo/superset-cli.mp4 b/packages/cli/demo/superset-cli.mp4 new file mode 100644 index 00000000000..fc7d82929b8 Binary files /dev/null and b/packages/cli/demo/superset-cli.mp4 differ diff --git a/packages/cli/demo/superset-cli.tape b/packages/cli/demo/superset-cli.tape new file mode 100644 index 00000000000..6011569ba62 --- /dev/null +++ b/packages/cli/demo/superset-cli.tape @@ -0,0 +1,146 @@ +# ───────────────────────────────────────────────────────────── +# Superset CLI — punchy end-to-end walkthrough (demo render) +# +# Render with: vhs demo/superset-cli.tape (run from packages/cli) +# Then add the soundtrack: ./demo/add-sound.sh (pad + keyboard clicks → superset-cli-sound.mp4) +# +# Note: this is a *demo* — `superset` is shimmed in the hidden setup block +# to print clean, canned output so the commands stay short and readable. +# Nothing real runs; no auth, host, or network needed to render. +# ───────────────────────────────────────────────────────────── + +Output demo/superset-cli.gif +Output demo/superset-cli.mp4 + +Set Shell "bash" +Set FontSize 33 +Set Width 1980 +Set Height 900 +Set Padding 22 +Set Margin 28 +Set MarginFill "#0a0a0b" +Set BorderRadius 12 +# Superset brand palette — burnt-orange accent on near-black (matches superset.sh) +Set Theme { "name": "Superset", "background": "#101012", "foreground": "#ECECEE", "cursor": "#E8804A", "selection": "#D2561140", "black": "#1C1C1F", "red": "#D25611", "green": "#6BB37E", "yellow": "#D2A24A", "blue": "#5B9BD5", "magenta": "#C97FD6", "cyan": "#56B6C2", "white": "#C8C8CC", "brightBlack": "#6B6B72", "brightRed": "#E8804A", "brightGreen": "#86D49B", "brightYellow": "#E8C07A", "brightBlue": "#7FB3E8", "brightMagenta": "#D896E4", "brightCyan": "#86D0DB", "brightWhite": "#FFFFFF" } +Set TypingSpeed 28ms +Set PlaybackSpeed 1.0 + +# --- setup (hidden from the final video) --- +Set TypingSpeed 2ms +Hide +Type `export PS1="\[\033[1;91m\]superset\[\033[0m\] \[\033[90m\]demo\[\033[0m\] \[\033[91m\]❯\[\033[0m\] "` +Enter +Type `superset() { case "$1.$2" in status.*) echo "Superset · Kiets-Spaceship running · healthy · up 3h 12m"; printf ' endpoint http://127.0.0.1:58968\n org Superset\n';; tasks.create) echo; echo " ✓ Created ENG-204 · Dark mode flickers on the dashboard"; echo " · synced — already live in your Superset app";; tasks.list) printf 'ID STATUS PRIORITY TITLE\nENG-204 todo — Dark mode flickers on the dashboard\nENG-201 in progress medium Stripe webhook retries on a 500\nENG-198 todo low Tidy the settings page copy\n';; tasks.update) echo; echo " ✓ ENG-204 · priority is now urgent";; tasks.delete) echo " ✓ Deleted ENG-204";; agents.list) printf 'LABEL PRESET COMMAND\nClaude claude claude\nCodex codex codex\nCursor cursor cursor-agent\n';; workspaces.create) printf '\n ✓ Created workspace dark-mode-fix\n branch fix/dark-mode\n agent Claude — running, streaming live to your Superset app\n';; workspaces.open) echo; echo " ✓ dark-mode-fix is now open in your Superset app — Claude is live there";; workspaces.delete) echo " ✓ Deleted workspace dark-mode-fix";; *) echo "superset: $*";; esac; }` +Enter +Type "clear" +Enter +Show +Set TypingSpeed 28ms + +# --- the walkthrough --- +Sleep 600ms +Type "# 👋 The Superset CLI — drive your dev loop from the terminal." +Sleep 1.4s +Enter +Sleep 500ms +Type "# ...or have your agents do it ;)" +Sleep 1.2s +Enter +Sleep 800ms +Type "# This machine's a host — workspaces run right here:" +Sleep 400ms +Enter +Sleep 250ms +Type "superset status" +Enter +Sleep 2.4s + +Type "clear" +Enter +Sleep 300ms +Type "# A bug just landed — file it:" +Sleep 600ms +Enter +Sleep 300ms +Type `superset tasks create --title "Dark mode flickers on the dashboard"` +Enter +Sleep 2s +Type "superset tasks list" +Enter +Sleep 3s + +Type "# This one's not waiting — bump it to urgent:" +Sleep 600ms +Enter +Sleep 300ms +Type "superset tasks update ENG-204 --priority urgent" +Enter +Sleep 2.4s + +Type "clear" +Enter +Sleep 300ms +Type "# Which agent can fix it?" +Sleep 500ms +Enter +Sleep 300ms +Type "superset agents list" +Enter +Sleep 2.6s +Type "# Spin up a workspace running Claude on it — one command:" +Sleep 800ms +Enter +Sleep 350ms +Type `superset workspaces create --name dark-mode-fix --agent claude --prompt "fix the dark-mode flicker"` +Enter +Sleep 3s + +Type "clear" +Enter +Sleep 300ms +Type "# Same workspace the desktop app shows — jump straight into it:" +Sleep 700ms +Enter +Sleep 350ms +Type "superset workspaces open dark-mode-fix" +Enter +Sleep 2.6s + +Type "# Demo's done — clean up:" +Sleep 500ms +Enter +Sleep 300ms +Type "superset workspaces delete dark-mode-fix" +Enter +Sleep 1.6s +Type "superset tasks delete ENG-204" +Enter +Sleep 2.4s + +Type "clear" +Enter +Sleep 300ms +Type "# Your agent runs this whole loop too." +Sleep 600ms +Enter +Sleep 300ms +Type "# Drop it into any coding agent:" +Sleep 400ms +Enter +Sleep 300ms +Type "# npx skills add https://superset.sh" +Sleep 700ms +Enter +Sleep 250ms +Type "# ...or in Claude Code:" +Sleep 400ms +Enter +Sleep 250ms +Type "# /plugin install superset@superset" +Sleep 1.4s +Enter +Sleep 700ms +Type "# File it, fix it, ship it — from your terminal. superset.sh" +Sleep 1.2s +Enter +Sleep 3s