← back to Rentv Tour
auto-save: 2026-07-28T14:31:09 (3 files) — batch-pitch.sh pitch-set.mjs videos/pitch/
865336280feeb5837c9dff82049c4d50955a6914 · 2026-07-28 14:31:11 -0700 · Steve Abrams
Files touched
A batch-pitch.shA pitch-set.mjsA videos/pitch/AGENTS.mdA videos/pitch/CLAUDE.mdA videos/pitch/hyperframes.jsonA videos/pitch/index.htmlA videos/pitch/meta.jsonA videos/pitch/package.json
Diff
commit 865336280feeb5837c9dff82049c4d50955a6914
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Jul 28 14:31:11 2026 -0700
auto-save: 2026-07-28T14:31:09 (3 files) — batch-pitch.sh pitch-set.mjs videos/pitch/
---
batch-pitch.sh | 39 ++++++++++++
pitch-set.mjs | 140 ++++++++++++++++++++++++++++++++++++++++++
videos/pitch/AGENTS.md | 95 ++++++++++++++++++++++++++++
videos/pitch/CLAUDE.md | 95 ++++++++++++++++++++++++++++
videos/pitch/hyperframes.json | 13 ++++
videos/pitch/index.html | 56 +++++++++++++++++
videos/pitch/meta.json | 5 ++
videos/pitch/package.json | 11 ++++
8 files changed, 454 insertions(+)
diff --git a/batch-pitch.sh b/batch-pitch.sh
new file mode 100755
index 0000000..122b512
--- /dev/null
+++ b/batch-pitch.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+cd /Users/macstudio3/Projects/rentv-tour
+SAY=~/.claude/skills/clone-voice/scripts/say.sh
+AV_LOOK=b2105c3cc1fd4464b11cd24c417b5751
+for id in $(node pitch-set.mjs ids); do
+ echo "=== $id ==="
+ txt=$(node pitch-set.mjs vo $id)
+ bash $SAY "$txt" /tmp/pv-$id.wav >/dev/null 2>&1
+ ffmpeg -v error -y -i /tmp/pv-$id.wav -c:a aac -b:a 160k videos/pitch/assets/vo.m4a >/dev/null 2>&1
+ dur=$(ffprobe -v error -show_entries format=duration -of default=nk=1:nw=1 videos/pitch/assets/vo.m4a)
+ echo " VO ${dur}s"
+ node pitch-set.mjs emit $id $dur >/dev/null 2>&1
+ (cd videos/pitch && npx hyperframes render --output renders/$id-base.mp4 >/dev/null 2>&1)
+ echo " base rendered"
+ # 15s avatar intro
+ ffmpeg -v error -y -t 15 -i /tmp/pv-$id.wav -c:a libmp3lame -b:a 192k /tmp/pv15-$id.mp3 >/dev/null 2>&1
+ AID=$(heygen asset create --file /tmp/pv15-$id.mp3 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["asset_id"])' 2>/dev/null)
+ VID=$(heygen video create -d "{\"type\":\"avatar\",\"avatar_id\":\"$AV_LOOK\",\"audio_asset_id\":\"$AID\",\"aspect_ratio\":\"1:1\",\"title\":\"$id intro\"}" 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["video_id"])' 2>/dev/null)
+ echo " avatar VID=$VID"
+ for i in $(seq 1 24); do
+ S=$(heygen video get $VID 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"].get("status",""))' 2>/dev/null)
+ if [ "$S" = "completed" ]; then
+ URL=$(heygen video get $VID 2>/dev/null | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"].get("video_url",""))' 2>/dev/null)
+ curl -s -o /tmp/pav-$id.mp4 "$URL"; echo " avatar downloaded"; break
+ fi
+ [ "$S" = "failed" ] && { echo " avatar FAILED"; break; }
+ node -e 'setTimeout(()=>{},10000)' 2>/dev/null
+ done
+ # composite: circular PIP over first 15s, base audio (full VO)
+ if [ -f /tmp/pav-$id.mp4 ]; then
+ ffmpeg -v error -y -i videos/pitch/renders/$id-base.mp4 -i /tmp/pav-$id.mp4 -filter_complex \
+ "[1:v]scale=340:340,format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='if(gt((X-170)*(X-170)+(Y-170)*(Y-170),165*165),0,255)'[pip];[0:v][pip]overlay=W-w-56:H-h-56:enable='lte(t,15)':format=auto[v]" \
+ -map "[v]" -map 0:a -c:v libx264 -preset medium -crf 20 -c:a aac -b:a 192k -pix_fmt yuv420p videos/pitch/renders/$id.mp4 >/dev/null 2>&1
+ echo " FINAL $id.mp4 ($(ls -la videos/pitch/renders/$id.mp4|awk '{printf "%.1fMB",$5/1048576}'))"
+ else
+ cp videos/pitch/renders/$id-base.mp4 videos/pitch/renders/$id.mp4; echo " (no avatar — used base)"
+ fi
+done
+echo "PITCH BATCH DONE"
diff --git a/pitch-set.mjs b/pitch-set.mjs
new file mode 100644
index 0000000..ec63f95
--- /dev/null
+++ b/pitch-set.mjs
@@ -0,0 +1,140 @@
+import fs from 'fs';
+const W = 1920, H = 1080;
+const esc = (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
+
+// Each film: id, title, full VO (Steve voice), scenes[]. Scene types:
+// {t:'title', h, sub} {t:'stat', n, label} {t:'bul', h, items[]} {t:'shot', img, label} {t:'outro', h}
+export const FILMS = {
+ growth: {
+ title: 'Growth Strategy',
+ vo: "RENTV's growth plan is simple — own Western U.S. commercial real estate news. Step one: modernize the legacy site into a fast, mobile-first experience readers return to. Step two: grow the newsletter and subscriber base around daily deal coverage. Step three: an integrated social engine that pushes every deal to LinkedIn, X, and Instagram. And step four: search optimization that ranks RENTV above The Real Deal, Bisnow, and GlobeSt. News, audience, distribution, search — the flywheel that compounds.",
+ scenes: [
+ { t: 'title', h: 'Growth Strategy', sub: 'Own Western U.S. commercial real estate news' },
+ { t: 'bul', h: 'The Plan', items: ['Modernize the site — fast & mobile-first', 'Grow the newsletter & subscriber base', 'Integrated social engine (LinkedIn · X · Instagram)', 'Search optimization to own the rank'] },
+ { t: 'bul', h: 'Outrank', items: ['The Real Deal', 'Bisnow', 'GlobeSt', 'CoStar News'] },
+ { t: 'shot', img: 'home', label: 'The rebuilt front page' },
+ { t: 'outro', h: 'The flywheel that compounds' },
+ ],
+ },
+ deepdive: {
+ title: 'Sublease Deep-Dive',
+ vo: "Sublease dot com is RENTV's biggest opportunity. Commercial sublease is a six hundred fifty billion dollar market — fragmented, opaque, and underserved. RENTV already owns the registry: three hundred eighty-two thousand brokers, thirty-four thousand firms, and a live sublease desk. The play — turn that data into the definitive sublease marketplace for the Western U.S., with a clear path to nearly six million in year-two revenue. The audience is here. The data is here. Sublease is the wedge.",
+ scenes: [
+ { t: 'title', h: 'Sublease Deep-Dive', sub: "RENTV's biggest opportunity" },
+ { t: 'stat', n: '$650B', label: 'U.S. commercial sublease market — fragmented & underserved' },
+ { t: 'stat', n: '382K / 34K', label: 'brokers / firms already in the RENTV registry' },
+ { t: 'stat', n: '~$5.9M', label: 'projected year-two revenue' },
+ { t: 'shot', img: 'desk', label: 'The live Broker & Owner Desk' },
+ { t: 'outro', h: 'Sublease is the wedge' },
+ ],
+ },
+ swot: {
+ title: 'SWOT Brief',
+ vo: "A quick, honest read on RENTV. Strengths — a real audience, decades of deal coverage, and a live intelligence registry competitors don't have. Weaknesses — a dated site and social channels that were underused. Opportunities — the sublease marketplace, integrated social distribution, and conference growth. Threats — well-funded rivals like CoStar, Bisnow, and The Real Deal moving faster. The verdict — RENTV's data moat is the edge. Modernize, distribute, and press it.",
+ scenes: [
+ { t: 'title', h: 'SWOT Brief', sub: 'An honest read on RENTV' },
+ { t: 'bul', h: 'Strengths', items: ['A real, established audience', 'Decades of deal coverage', 'A live intelligence registry rivals lack'] },
+ { t: 'bul', h: 'Weaknesses', items: ['A dated legacy site', 'Underused social channels'] },
+ { t: 'bul', h: 'Opportunities', items: ['The sublease marketplace', 'Integrated social distribution', 'Conference growth'] },
+ { t: 'bul', h: 'Threats', items: ['CoStar', 'Bisnow', 'The Real Deal — moving faster'] },
+ { t: 'outro', h: 'Press the data moat' },
+ ],
+ },
+ adplan: {
+ title: 'Advertising & Media Plan',
+ vo: "How RENTV wins attention. The channels are already connected — LinkedIn, X, and Instagram — feeding every deal and headline into an integrated social engine. Paid amplifies the winners: sponsored deal alerts, conference promotion, and retargeting the newsletter list. The content engine runs on what RENTV already produces — daily deals, market pulse, and The REview video platform. Measure, double down on what converts, and turn readers into subscribers, and subscribers into conference registrations.",
+ scenes: [
+ { t: 'title', h: 'Advertising & Media Plan', sub: 'How RENTV wins attention' },
+ { t: 'bul', h: 'Channels — already connected', items: ['LinkedIn', 'X (Twitter)', 'Instagram'] },
+ { t: 'bul', h: 'Paid amplifies the winners', items: ['Sponsored deal alerts', 'Conference promotion', 'Newsletter retargeting'] },
+ { t: 'bul', h: 'Content engine (already produced)', items: ['Daily deals', 'Market Pulse', 'The REview video platform'] },
+ { t: 'shot', img: 'social', label: 'The social command center' },
+ { t: 'outro', h: 'Readers → subscribers → registrations' },
+ ],
+ },
+};
+
+function emit(id, voDur) {
+ const film = FILMS[id];
+ const TOTAL = +Math.max(voDur + 0.5, 12).toFixed(2);
+ const TITLE_D = 2.4, OUTRO_D = 2.9;
+ const mids = film.scenes.filter(s => s.t !== 'title' && s.t !== 'outro');
+ const midTotal = TOTAL - TITLE_D - OUTRO_D;
+ const midD = midTotal / Math.max(mids.length, 1);
+ const accent = '#57b0ff';
+ const clips = [], tw = [];
+ let t = 0, i = 0, mi = 0;
+ for (const s of film.scenes) {
+ const id2 = 's' + i;
+ let dur;
+ if (s.t === 'title') dur = TITLE_D;
+ else if (s.t === 'outro') dur = OUTRO_D;
+ else { dur = (mi === mids.length - 1) ? (TOTAL - OUTRO_D - t) : midD; mi++; }
+ const st = t.toFixed(3), du = dur.toFixed(3);
+ if (s.t === 'title' || s.t === 'outro') {
+ clips.push(`<section id="${id2}" class="clip card" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci">${s.t === 'title' ? `<div class="tag" id="${id2}t">RENTV · GROWTH DECK</div>` : ''}<h1 id="${id2}h">${esc(s.h)}</h1>${s.sub ? `<p id="${id2}p">${esc(s.sub)}</p>` : ''}</div></section>`);
+ tw.push(`tl.from('#${id2}h',{y:34,autoAlpha:0,duration:0.6,ease:'power3.out'},${(t + 0.15).toFixed(3)});`);
+ if (s.sub) tw.push(`tl.from('#${id2}p',{y:20,autoAlpha:0,duration:0.5},${(t + 0.35).toFixed(3)});`);
+ } else if (s.t === 'stat') {
+ clips.push(`<section id="${id2}" class="clip card" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci"><div class="stat" id="${id2}n">${esc(s.n)}</div><p id="${id2}l" class="statl">${esc(s.label)}</p></div></section>`);
+ tw.push(`tl.from('#${id2}n',{scale:0.8,autoAlpha:0,duration:0.6,ease:'back.out(1.5)'},${(t + 0.1).toFixed(3)});`);
+ tw.push(`tl.from('#${id2}l',{y:18,autoAlpha:0,duration:0.5},${(t + 0.4).toFixed(3)});`);
+ } else if (s.t === 'bul') {
+ const lis = s.items.map((it, k) => `<li id="${id2}i${k}">${esc(it)}</li>`).join('');
+ clips.push(`<section id="${id2}" class="clip card left" data-start="${st}" data-duration="${du}" data-track-index="1"><div class="ci wide"><h2 id="${id2}h" style="color:${accent}">${esc(s.h)}</h2><ul>${lis}</ul></div></section>`);
+ tw.push(`tl.from('#${id2}h',{x:-24,autoAlpha:0,duration:0.5,ease:'power3.out'},${(t + 0.1).toFixed(3)});`);
+ s.items.forEach((it, k) => tw.push(`tl.from('#${id2}i${k}',{x:-20,autoAlpha:0,duration:0.4},${(t + 0.35 + k * 0.28).toFixed(3)});`));
+ } else if (s.t === 'shot') {
+ clips.push(`<div id="${id2}" class="clip shot" data-start="${st}" data-duration="${du}" data-track-index="2"><img id="${id2}i" class="simg" src="assets/${s.img}.png" alt="${esc(s.label)}"></div>`);
+ clips.push(`<div id="${id2}l" class="clip label" data-start="${st}" data-duration="${du}" data-track-index="3"><div class="li"><span class="txt">${esc(s.label)}</span></div></div>`);
+ tw.push(`tl.from('#${id2}i',{opacity:0,duration:0.35},${st});`);
+ tw.push(`tl.fromTo('#${id2}i',{scale:1.06},{scale:1.0,duration:${du},ease:'none'},${st});`);
+ tw.push(`tl.from('#${id2}l',{y:22,autoAlpha:0,duration:0.4},${(t + 0.1).toFixed(3)});`);
+ }
+ t += dur; i++;
+ }
+ const html = `<!doctype html><html lang="en"><head>
+<meta charset="UTF-8" /><meta name="viewport" content="width=1920, height=1080" />
+<title>RENTV — ${esc(film.title)}</title>
+<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+<style>
+ body{margin:0;background:#070b10;font-family:system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif}
+ #root{position:relative;width:${W}px;height:${H}px;overflow:hidden}
+ .clip{position:absolute;inset:0}
+ #bg{background:radial-gradient(120% 120% at 50% 0%,#0d1725 0%,#070b10 60%)}
+ .shot{overflow:hidden}.simg{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;will-change:transform,opacity}
+ .card{display:grid;place-items:center}.card.left{place-items:center start}
+ .ci{text-align:center;padding:0 130px}.ci.wide{text-align:left;padding:0 160px;max-width:1400px}
+ .tag{font-size:19px;letter-spacing:.36em;font-weight:600;color:#57b0ff;margin-bottom:18px}
+ .card h1{margin:0;color:#f3f7fc;font-size:82px;font-weight:500;letter-spacing:-.01em;line-height:1.05}
+ .card p{margin:20px 0 0;color:#8ea3b8;font-size:30px;font-weight:300}
+ .stat{color:#fff;font-size:150px;font-weight:700;letter-spacing:-.02em;line-height:1}
+ .statl{color:#8ea3b8;font-size:30px;font-weight:300;margin-top:20px;max-width:900px}
+ .ci.wide h2{font-size:30px;text-transform:uppercase;letter-spacing:.12em;margin:0 0 26px;font-weight:700}
+ .ci.wide ul{margin:0;padding:0;list-style:none}
+ .ci.wide li{color:#eef3f8;font-size:44px;font-weight:400;padding:12px 0 12px 40px;position:relative}
+ .ci.wide li:before{content:'';position:absolute;left:0;top:26px;width:16px;height:16px;border-radius:4px;background:#57b0ff}
+ .label{left:56px;bottom:60px;right:auto;top:auto;width:auto;height:auto}
+ .li{display:flex}.li .txt{color:#fff;font-size:30px;font-weight:500;background:rgba(9,15,24,.9);padding:8px 18px;border-radius:10px}
+</style></head><body>
+ <div id="root" data-composition-id="main" data-start="0" data-width="${W}" data-height="${H}" data-duration="${TOTAL}" data-fps="30">
+ <div id="bg" class="clip" data-start="0" data-duration="${TOTAL}" data-track-index="0"></div>
+ <audio id="vo" src="assets/vo.m4a" data-start="0" data-volume="1"></audio>
+${clips.map(c => ' ' + c).join('\n')}
+ </div>
+ <script>
+ window.__timelines = window.__timelines || {};
+ const tl = gsap.timeline({ paused: true });
+${tw.map(x => ' ' + x).join('\n')}
+ window.__timelines['main'] = tl;
+ </script>
+</body></html>`;
+ fs.writeFileSync('videos/pitch/index.html', html);
+ return TOTAL;
+}
+
+const [cmd, a, b] = process.argv.slice(2);
+if (cmd === 'ids') console.log(Object.keys(FILMS).join(' '));
+else if (cmd === 'vo') process.stdout.write(FILMS[a].vo);
+else if (cmd === 'title') process.stdout.write(FILMS[a].title);
+else if (cmd === 'emit') console.log('TOTAL=' + emit(a, parseFloat(b)));
diff --git a/videos/pitch/AGENTS.md b/videos/pitch/AGENTS.md
new file mode 100644
index 0000000..c4ff0ae
--- /dev/null
+++ b/videos/pitch/AGENTS.md
@@ -0,0 +1,95 @@
+# HyperFrames Composition Project
+
+## Skills — USE THESE FIRST
+
+**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
+
+**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
+
+- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
+- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
+- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
+- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
+- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
+- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
+- `/music-to-video` — a **music track** (audio file, or video to pull audio from) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
+- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
+- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
+
+**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
+
+The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
+
+**Changing how real footage or images look or reveal?** Load `/media-use` and its media-treatment policy before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
+
+> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
+
+> **Skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
+> the specific skill you need (the `/hyperframes` router does this automatically before
+> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
+> everything already installed — neither pulls the full set. Restart the agent session so
+> newly installed skills load.
+
+## Commands
+
+```bash
+npm run dev # start the preview server (long-running — keep it alive in background)
+npm run check # lint + runtime + layout + motion + contrast (one command)
+npm run render # render to MP4
+npm run publish # publish and get a shareable link
+npx hyperframes lint --verbose # include info-level findings
+npx hyperframes lint --json # machine-readable output for CI
+npx hyperframes docs <topic> # reference docs in terminal
+```
+
+> **`npm run dev` is a long-running server, not a one-shot command.** It blocks until stopped.
+> In Claude Code, always run it with `run_in_background: true`. Never run it as a foreground
+> command — it will time out and the server will die, breaking the browser preview.
+
+> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
+
+## Documentation
+
+**For quick reference**, use the local CLI docs command (no network required):
+
+```bash
+npx hyperframes docs <topic>
+```
+
+Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
+
+**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
+
+```
+https://hyperframes.heygen.com/llms.txt
+```
+
+## Project Structure
+
+- `index.html` — main composition (root timeline)
+- `compositions/` — sub-compositions referenced via `data-composition-src`
+- `meta.json` — project metadata (id, name)
+- `transcript.json` — whisper word-level transcript (if generated)
+
+## Linting — ALWAYS RUN AFTER CHANGES
+
+After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
+
+```bash
+npm run check
+```
+
+Fix all errors before presenting the result. Warnings should be reviewed before rendering.
+
+## Key Rules
+
+1. Every timed element needs `data-start`, `data-duration`, and `data-track-index`
+2. Elements with timing **MUST** have `class="clip"` — the framework uses this for visibility control
+3. Timelines must be paused and registered on `window.__timelines`:
+ ```js
+ window.__timelines = window.__timelines || {};
+ window.__timelines["composition-id"] = gsap.timeline({ paused: true });
+ ```
+4. Videos use `muted` with a separate `<audio>` element for the audio track
+5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
+6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
diff --git a/videos/pitch/CLAUDE.md b/videos/pitch/CLAUDE.md
new file mode 100644
index 0000000..c4ff0ae
--- /dev/null
+++ b/videos/pitch/CLAUDE.md
@@ -0,0 +1,95 @@
+# HyperFrames Composition Project
+
+## Skills — USE THESE FIRST
+
+**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
+
+**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
+
+- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
+- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
+- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
+- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
+- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
+- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
+- `/music-to-video` — a **music track** (audio file, or video to pull audio from) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
+- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
+- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
+
+**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
+
+The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
+
+**Changing how real footage or images look or reveal?** Load `/media-use` and its media-treatment policy before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
+
+> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
+
+> **Skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
+> the specific skill you need (the `/hyperframes` router does this automatically before
+> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
+> everything already installed — neither pulls the full set. Restart the agent session so
+> newly installed skills load.
+
+## Commands
+
+```bash
+npm run dev # start the preview server (long-running — keep it alive in background)
+npm run check # lint + runtime + layout + motion + contrast (one command)
+npm run render # render to MP4
+npm run publish # publish and get a shareable link
+npx hyperframes lint --verbose # include info-level findings
+npx hyperframes lint --json # machine-readable output for CI
+npx hyperframes docs <topic> # reference docs in terminal
+```
+
+> **`npm run dev` is a long-running server, not a one-shot command.** It blocks until stopped.
+> In Claude Code, always run it with `run_in_background: true`. Never run it as a foreground
+> command — it will time out and the server will die, breaking the browser preview.
+
+> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
+
+## Documentation
+
+**For quick reference**, use the local CLI docs command (no network required):
+
+```bash
+npx hyperframes docs <topic>
+```
+
+Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
+
+**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
+
+```
+https://hyperframes.heygen.com/llms.txt
+```
+
+## Project Structure
+
+- `index.html` — main composition (root timeline)
+- `compositions/` — sub-compositions referenced via `data-composition-src`
+- `meta.json` — project metadata (id, name)
+- `transcript.json` — whisper word-level transcript (if generated)
+
+## Linting — ALWAYS RUN AFTER CHANGES
+
+After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
+
+```bash
+npm run check
+```
+
+Fix all errors before presenting the result. Warnings should be reviewed before rendering.
+
+## Key Rules
+
+1. Every timed element needs `data-start`, `data-duration`, and `data-track-index`
+2. Elements with timing **MUST** have `class="clip"` — the framework uses this for visibility control
+3. Timelines must be paused and registered on `window.__timelines`:
+ ```js
+ window.__timelines = window.__timelines || {};
+ window.__timelines["composition-id"] = gsap.timeline({ paused: true });
+ ```
+4. Videos use `muted` with a separate `<audio>` element for the audio track
+5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
+6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
diff --git a/videos/pitch/hyperframes.json b/videos/pitch/hyperframes.json
new file mode 100644
index 0000000..ec2e3c2
--- /dev/null
+++ b/videos/pitch/hyperframes.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
+ "registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
+ "paths": {
+ "blocks": "compositions",
+ "components": "compositions/components",
+ "assets": "assets"
+ },
+ "media": {
+ "autoProxy": true
+ },
+ "authoringSkill": "product-launch-video"
+}
diff --git a/videos/pitch/index.html b/videos/pitch/index.html
new file mode 100644
index 0000000..b9d3fd2
--- /dev/null
+++ b/videos/pitch/index.html
@@ -0,0 +1,56 @@
+<!doctype html><html lang="en"><head>
+<meta charset="UTF-8" /><meta name="viewport" content="width=1920, height=1080" />
+<title>RENTV — Growth Strategy</title>
+<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+<style>
+ body{margin:0;background:#070b10;font-family:system-ui,-apple-system,"Helvetica Neue",Arial,sans-serif}
+ #root{position:relative;width:1920px;height:1080px;overflow:hidden}
+ .clip{position:absolute;inset:0}
+ #bg{background:radial-gradient(120% 120% at 50% 0%,#0d1725 0%,#070b10 60%)}
+ .shot{overflow:hidden}.simg{width:100%;height:100%;object-fit:cover;object-position:top center;display:block;will-change:transform,opacity}
+ .card{display:grid;place-items:center}.card.left{place-items:center start}
+ .ci{text-align:center;padding:0 130px}.ci.wide{text-align:left;padding:0 160px;max-width:1400px}
+ .tag{font-size:19px;letter-spacing:.36em;font-weight:600;color:#57b0ff;margin-bottom:18px}
+ .card h1{margin:0;color:#f3f7fc;font-size:82px;font-weight:500;letter-spacing:-.01em;line-height:1.05}
+ .card p{margin:20px 0 0;color:#8ea3b8;font-size:30px;font-weight:300}
+ .stat{color:#fff;font-size:150px;font-weight:700;letter-spacing:-.02em;line-height:1}
+ .statl{color:#8ea3b8;font-size:30px;font-weight:300;margin-top:20px;max-width:900px}
+ .ci.wide h2{font-size:30px;text-transform:uppercase;letter-spacing:.12em;margin:0 0 26px;font-weight:700}
+ .ci.wide ul{margin:0;padding:0;list-style:none}
+ .ci.wide li{color:#eef3f8;font-size:44px;font-weight:400;padding:12px 0 12px 40px;position:relative}
+ .ci.wide li:before{content:'';position:absolute;left:0;top:26px;width:16px;height:16px;border-radius:4px;background:#57b0ff}
+ .label{left:56px;bottom:60px;right:auto;top:auto;width:auto;height:auto}
+ .li{display:flex}.li .txt{color:#fff;font-size:30px;font-weight:500;background:rgba(9,15,24,.9);padding:8px 18px;border-radius:10px}
+</style></head><body>
+ <div id="root" data-composition-id="main" data-start="0" data-width="1920" data-height="1080" data-duration="33.05" data-fps="30">
+ <div id="bg" class="clip" data-start="0" data-duration="33.05" data-track-index="0"></div>
+ <audio id="vo" src="assets/vo.m4a" data-start="0" data-volume="1"></audio>
+ <section id="s0" class="clip card" data-start="0.000" data-duration="2.400" data-track-index="1"><div class="ci"><div class="tag" id="s0t">RENTV · GROWTH DECK</div><h1 id="s0h">Growth Strategy</h1><p id="s0p">Own Western U.S. commercial real estate news</p></div></section>
+ <section id="s1" class="clip card left" data-start="2.400" data-duration="9.250" data-track-index="1"><div class="ci wide"><h2 id="s1h" style="color:#57b0ff">The Plan</h2><ul><li id="s1i0">Modernize the site — fast & mobile-first</li><li id="s1i1">Grow the newsletter & subscriber base</li><li id="s1i2">Integrated social engine (LinkedIn · X · Instagram)</li><li id="s1i3">Search optimization to own the rank</li></ul></div></section>
+ <section id="s2" class="clip card left" data-start="11.650" data-duration="9.250" data-track-index="1"><div class="ci wide"><h2 id="s2h" style="color:#57b0ff">Outrank</h2><ul><li id="s2i0">The Real Deal</li><li id="s2i1">Bisnow</li><li id="s2i2">GlobeSt</li><li id="s2i3">CoStar News</li></ul></div></section>
+ <div id="s3" class="clip shot" data-start="20.900" data-duration="9.250" data-track-index="2"><img id="s3i" class="simg" src="assets/home.png" alt="The rebuilt front page"></div>
+ <div id="s3l" class="clip label" data-start="20.900" data-duration="9.250" data-track-index="3"><div class="li"><span class="txt">The rebuilt front page</span></div></div>
+ <section id="s4" class="clip card" data-start="30.150" data-duration="2.900" data-track-index="1"><div class="ci"><h1 id="s4h">The flywheel that compounds</h1></div></section>
+ </div>
+ <script>
+ window.__timelines = window.__timelines || {};
+ const tl = gsap.timeline({ paused: true });
+ tl.from('#s0h',{y:34,autoAlpha:0,duration:0.6,ease:'power3.out'},0.150);
+ tl.from('#s0p',{y:20,autoAlpha:0,duration:0.5},0.350);
+ tl.from('#s1h',{x:-24,autoAlpha:0,duration:0.5,ease:'power3.out'},2.500);
+ tl.from('#s1i0',{x:-20,autoAlpha:0,duration:0.4},2.750);
+ tl.from('#s1i1',{x:-20,autoAlpha:0,duration:0.4},3.030);
+ tl.from('#s1i2',{x:-20,autoAlpha:0,duration:0.4},3.310);
+ tl.from('#s1i3',{x:-20,autoAlpha:0,duration:0.4},3.590);
+ tl.from('#s2h',{x:-24,autoAlpha:0,duration:0.5,ease:'power3.out'},11.750);
+ tl.from('#s2i0',{x:-20,autoAlpha:0,duration:0.4},12.000);
+ tl.from('#s2i1',{x:-20,autoAlpha:0,duration:0.4},12.280);
+ tl.from('#s2i2',{x:-20,autoAlpha:0,duration:0.4},12.560);
+ tl.from('#s2i3',{x:-20,autoAlpha:0,duration:0.4},12.840);
+ tl.from('#s3i',{opacity:0,duration:0.35},20.900);
+ tl.fromTo('#s3i',{scale:1.06},{scale:1.0,duration:9.250,ease:'none'},20.900);
+ tl.from('#s3l',{y:22,autoAlpha:0,duration:0.4},21.000);
+ tl.from('#s4h',{y:34,autoAlpha:0,duration:0.6,ease:'power3.out'},30.300);
+ window.__timelines['main'] = tl;
+ </script>
+</body></html>
\ No newline at end of file
diff --git a/videos/pitch/meta.json b/videos/pitch/meta.json
new file mode 100644
index 0000000..a689c3c
--- /dev/null
+++ b/videos/pitch/meta.json
@@ -0,0 +1,5 @@
+{
+ "id": "pitch",
+ "name": "pitch",
+ "createdAt": "2026-07-28T21:29:29.247Z"
+}
\ No newline at end of file
diff --git a/videos/pitch/package.json b/videos/pitch/package.json
new file mode 100644
index 0000000..420f9e4
--- /dev/null
+++ b/videos/pitch/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "pitch",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "npx --yes hyperframes@0.7.78 preview",
+ "check": "npx --yes hyperframes@0.7.78 check",
+ "render": "npx --yes hyperframes@0.7.78 render",
+ "publish": "npx --yes hyperframes@0.7.78 publish"
+ }
+}
← 1a67a3b Component video batch (18, Steve voice) + real bearded-Steve
·
back to Rentv Tour
·
auto-save: 2026-07-28T15:01:16 (7 files) — videos/pitch/inde 6cceb3f →