diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 16b0e6753a..6b3edda896 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "hyperframes", "source": "./", - "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.", "homepage": "https://hyperframes.heygen.com", "license": "Apache-2.0" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index fb60a8e73d..10c96d78bf 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "hyperframes", - "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", + "description": "HyperFrames by HeyGen. Write HTML, render video. Compositions, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.", "version": "0.7.57", "author": { "name": "HeyGen", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 060662d2a7..ccf97d7d2b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "hyperframes", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.", "version": "0.7.57", - "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", "email": "hyperframes@heygen.com", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 36ec91c4b8..d9f3bea354 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -2,8 +2,8 @@ "$schema": "https://cursor.com/schemas/cursor-plugin/plugin.json", "name": "hyperframes", "displayName": "HyperFrames by HeyGen", + "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website capture for HyperFrames.", "version": "0.7.57", - "description": "Write HTML, render video. Compositions, Tailwind v4 styles, GSAP and runtime adapter animations, captions, voiceovers, audio-reactive visuals, and website-to-video capture for HyperFrames.", "author": { "name": "HeyGen", "email": "hyperframes@heygen.com" diff --git a/AGENTS.md b/AGENTS.md index 1c1cd9eff3..64374bf022 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,10 +10,9 @@ This repo ships AI agent skills via [vercel-labs/skills](https://github.com/verc npx skills add heygen-com/hyperframes ``` -**Creation workflows** route through one entry skill — read `/hyperframes` first: it orients you to the whole surface and maps "make me a…" intent — usually a video, but also a navigable deck (`/slideshow`) or a composition port (`/remotion-to-hyperframes`) — to a concrete workflow. Consult it before invoking a specific workflow: +**Creation workflows** route through one entry skill — read `/hyperframes` first: it orients you to the whole surface, confirms the brief up front (the intent layer), and maps "make me a…" intent — usually a video, but also a navigable deck (`/slideshow`) or a composition port (`/remotion-to-hyperframes`) — to a concrete workflow. Consult it before invoking a specific workflow: -- `/product-launch-video` — a **product** URL (or a pre-written script / text brief in no-capture mode) → product launch / promo video, up to ~3 min (sweet spot ~30-90s). -- `/website-to-video` — a **general** website / URL → a video _of_ the site (tour / showcase / social clip from captured screenshots + assets); for a product **launch / promo**, use `/product-launch-video`. +- `/product-launch-video` — any **website** URL (or a pre-written script / text brief in no-capture mode) → a product launch / promo video, or a site tour / showcase featuring the site's own captured screens; up to ~3 min (sweet spot ~30-90s). - `/faceless-explainer` — arbitrary text, **no URL and no website capture** → faceless explainer, up to ~3 min (sweet spot ~30-90s); every visual is LLM-invented (typography / abstract graphics / diagram / data-viz). - `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (verbatim rail + embedded climax, or pure-cinematic embed); the footage itself is untouched (no NLE-style editing). - `/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, footage untouched. For plain captions/subtitles → `/embedded-captions`. @@ -21,7 +20,7 @@ npx skills add heygen-com/hyperframes - `/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 creation (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition); the original hyperframes flow — design → plan → layout → build → validate, any length. +- `/general-video` — fallback for any other video creation (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 flow — design → plan → layout → build → validate, any length. **Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) video composition into HyperFrames HTML — a source migration, separate from the creation workflows above. diff --git a/CLAUDE.md b/CLAUDE.md index abd8a019f2..83a6887ce9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,22 +4,21 @@ Open-source video rendering framework: write HTML, render video. ## Skills -This repo ships 20 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. +This repo ships 19 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. ```bash npx skills add heygen-com/hyperframes --full-depth # interactive picker -npx skills add heygen-com/hyperframes --all --full-depth # install all 20 (skips picker) +npx skills add heygen-com/hyperframes --all --full-depth # install all 19 (skips picker) npx skills add heygen-com/hyperframes --skill --full-depth # just one (bare name, no leading slash) ``` Keep `--full-depth`: it installs the current `main`. Without it, `skills add` fetches the skills.sh registry blob, which lags `main` by hours (you'd get a stale skill). `hyperframes skills update` already uses full-depth. -**`/hyperframes` is the entry skill — read it first.** It's the capability map for the domain skills below AND the intent router for the creation workflows. The full README skills section mirrors this list; keep them in sync (see "Skill catalog maintenance" below). +**`/hyperframes` is the entry skill — read it first.** It's the capability map for the domain skills below, the intent layer that confirms every creation brief up front, AND the intent router for the creation workflows. The full README skills section mirrors this list; keep them in sync (see "Skill catalog maintenance" below). ### Creation workflows -- `/product-launch-video` — a **product** URL (or a pre-written script / text brief in no-capture mode) → product launch / promo video, up to ~3 min (sweet spot ~30-90s). -- `/website-to-video` — a **general** website / URL → a video _of_ the site (tour / showcase / social clip from captured screenshots + assets); for a product **launch / promo**, use `/product-launch-video`. +- `/product-launch-video` — any **website** URL (or a pre-written script / text brief in no-capture mode) → a product launch / promo video, or a site tour / showcase featuring the site's own captured screens; up to ~3 min (sweet spot ~30-90s). - `/faceless-explainer` — arbitrary text, **no URL and no website capture** → faceless explainer, up to ~3 min (sweet spot ~30-90s); every visual is LLM-invented (typography / abstract graphics / diagram / data-viz). - `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → code-change explainer, up to ~3 min (changelog / feature reveal / fix / refactor). A PR link, not a product website. - `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (verbatim rail + embedded climax, or pure-cinematic embed); the footage itself is untouched (no NLE-style editing). @@ -27,7 +26,7 @@ Keep `--full-depth`: it installs the current `main`. Without it, `skills add` fe - `/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 creation (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition); the original hyperframes flow — design → plan → layout → build → validate, any length. +- `/general-video` — fallback for any other video creation (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 flow — design → plan → layout → build → validate, any length. - `/remotion-to-hyperframes` — port an existing Remotion (React) composition to HyperFrames HTML. One-way migration, not creation. ### Domain skills (loaded on demand) @@ -51,7 +50,7 @@ When adding a new skill, or substantially renaming / repurposing an existing one 2. The scaffolded project template `packages/cli/src/templates/_shared/CLAUDE.md` + `AGENTS.md` — written into every `hyperframes init` project, so a stale entry there ships to users. The two template files must stay byte-identical. 3. If the skill changes the routing surface for "make a video" requests, also update the capability map and intent router in `skills/hyperframes/SKILL.md` — that's the canonical router agents read first. 4. Mirror the Router / Creation workflows / Domain skills grouping across all surfaces so a skill always lives in the same column. -5. Skill count appears in the README and CLAUDE.md intro lines ("20 AI agent skills…") — update on add/remove. The `docs/guides/skills.mdx` page and the CLI templates deliberately omit a count to avoid drift; keep them count-free. +5. Skill count appears in the README and CLAUDE.md intro lines ("19 AI agent skills…") — update on add/remove. The `docs/guides/skills.mdx` page and the CLI templates deliberately omit a count to avoid drift; keep them count-free. The skill's own `SKILL.md` frontmatter `description:` is the source of truth for the one-line "use when" blurb; copy from there into the catalog rather than paraphrasing. diff --git a/README.md b/README.md index 8d0009b4d1..18d208acb2 100644 --- a/README.md +++ b/README.md @@ -51,33 +51,32 @@ The skills teach agents the HyperFrames production loop: plan the video, write v ## Skills -HyperFrames ships 20 skills agents load on demand. Read `/hyperframes` first — it's the router and capability map; it picks a workflow for any "make me a…" request — video, deck, or composition port — and points to the domain skills below. +HyperFrames ships 19 skills agents load on demand. Read `/hyperframes` first — it's the router and capability map; it picks a workflow for any "make me a…" request — video, deck, or composition port — and points to the domain skills below. -Run `npx skills add heygen-com/hyperframes --full-depth` for the interactive picker, `npx skills add heygen-com/hyperframes --all --full-depth` to install all 20 at once (skips the picker), or `npx skills add heygen-com/hyperframes --skill --full-depth` for just one (bare name, no leading `/`). Keep `--full-depth` — it installs the current `main`; without it `skills add` fetches the skills.sh blob, which lags by hours. +Run `npx skills add heygen-com/hyperframes --full-depth` for the interactive picker, `npx skills add heygen-com/hyperframes --all --full-depth` to install all 19 at once (skips the picker), or `npx skills add heygen-com/hyperframes --skill --full-depth` for just one (bare name, no leading `/`). Keep `--full-depth` — it installs the current `main`; without it `skills add` fetches the skills.sh blob, which lags by hours. Installs stay lean after that: `npx hyperframes init` keeps the **core set** fresh (the router, the `hyperframes-*` domain skills, and `media-use` — plus whatever is already installed; `/figma` stays on demand) and never expands a partial install; the creation workflows install **on demand** — the router runs `npx hyperframes skills update ` before entering one. Nothing re-pulls the full set behind your back. ### Router -| Skill | Use when | -| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/hyperframes` | **Read first** for any request to make / create / edit / animate / render a video, animation, or motion graphic. Capability map for the domain skills and intent router for the creation workflows below. | +| Skill | Use when | +| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `/hyperframes` | **Read first** for any request to make / create / edit / animate / render a video, animation, or motion graphic. Capability map for the domain skills, the intent layer that confirms every creation brief up front, and intent router for the creation workflows below. | ### Creation workflows -| Skill | Use when | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/product-launch-video` | Marketing / launching / promoting a **product** — from its URL, a brief, or a script (even if the site is only named). Up to ~3 min (sweet spot 30-90s). | -| `/website-to-video` | Turning a **general website** into a video — site tour, portfolio / landing-page showcase, social clip from the site's own visuals. | -| `/faceless-explainer` | **Explaining a topic / concept** from arbitrary text — no product, no URL, no website capture; every visual is LLM-invented (typography / abstract / diagram / data-viz). | -| `/pr-to-video` | A **GitHub pull request** (PR URL, `owner/repo#N` ref, or "this PR") → changelog / feature-reveal / fix / refactor explainer, read via the `gh` CLI. | -| `/embedded-captions` | Adding **captions / subtitles** to an existing talking-head video (footage untouched) — verbatim rail, embedded climax behind the subject, or pure-cinematic embed. | -| `/talking-head-recut` | Packaging an existing talking-head / interview / podcast video with **designed graphic overlays** — lower-thirds, data callouts, kinetic titles, pull-quotes, side panels, PiP. | -| `/motion-graphics` | A short, **unnarrated, design-led motion graphic** (~under 10s) — kinetic type, stat / chart hit, logo sting, lower-third, animated tweet / headline. MP4 or transparent overlay. | -| `/music-to-video` | A **music track** (audio file, or video to pull audio from) → a **beat-synced** video — lyric, slideshow, or kinetic promo; music drives pacing. | -| `/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` | **Anything else** — longer or multi-scene pieces, brand / sizzle reel, title card, static loop, freeform composition. Input- and length-agnostic fallback. | -| `/remotion-to-hyperframes` | **Porting an existing Remotion** (React) composition's source to HyperFrames HTML. One-way migration, not creation. | +| Skill | Use when | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/product-launch-video` | Any **website** — marketing / launching / promoting a product (from its URL, a brief, or a script), or a site tour / showcase / social clip featuring the site's own visuals. Up to ~3 min (sweet spot 30-90s). | +| `/faceless-explainer` | **Explaining a topic / concept** from arbitrary text — no product, no URL, no website capture; every visual is LLM-invented (typography / abstract / diagram / data-viz). | +| `/pr-to-video` | A **GitHub pull request** (PR URL, `owner/repo#N` ref, or "this PR") → changelog / feature-reveal / fix / refactor explainer, read via the `gh` CLI. | +| `/embedded-captions` | Adding **captions / subtitles** to an existing talking-head video (footage untouched) — verbatim rail, embedded climax behind the subject, or pure-cinematic embed. | +| `/talking-head-recut` | Packaging an existing talking-head / interview / podcast video with **designed graphic overlays** — lower-thirds, data callouts, kinetic titles, pull-quotes, side panels, PiP. | +| `/motion-graphics` | A short, **unnarrated, design-led motion graphic** (~under 10s) — kinetic type, stat / chart hit, logo sting, lower-third, animated tweet / headline. MP4 or transparent overlay. | +| `/music-to-video` | A **music track** (audio file, or video to pull audio from) → a **beat-synced** video — lyric, slideshow, or kinetic promo; music drives pacing. | +| `/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` | **Anything else** — longer or multi-scene pieces, brand / sizzle reel, title card, static loop, freeform composition. Input- and length-agnostic fallback, and the home of companion mode (co-create with the full toolbox). | +| `/remotion-to-hyperframes` | **Porting an existing Remotion** (React) composition's source to HyperFrames HTML. One-way migration, not creation. | ### Domain skills (loaded on demand) @@ -115,7 +114,7 @@ Need ideas? Browse the [Showcase](https://hyperframes.heygen.com/showcase) for f - PR walkthroughs with animated code diffs, narration, and captions - Data visualizations, chart races, and map animations - Social videos with kinetic captions, overlays, and music -- Docs-to-video, PDF-to-video, and website-to-video explainers +- Docs-to-video, PDF-to-video, and site-tour explainers - Reusable motion graphics for automated content pipelines ## Frame.md diff --git a/docs/guides/authentication.mdx b/docs/guides/authentication.mdx index a16a42ca8e..797b79e679 100644 --- a/docs/guides/authentication.mdx +++ b/docs/guides/authentication.mdx @@ -59,7 +59,7 @@ Each capability picks the **first available provider** in order; the last is alw | **Voice (TTS)** | HeyGen → ElevenLabs → Kokoro | `HEYGEN_API_KEY` → `HYPERFRAMES_API_KEY` → `~/.heygen` · then `ELEVENLABS_API_KEY` | Kokoro: `pip install kokoro-onnx soundfile` | | **Music (BGM)** | HeyGen library → Lyria → MusicGen | HeyGen credential (above) · then `GEMINI_API_KEY` → `GOOGLE_API_KEY` | MusicGen: `pip install transformers torch soundfile numpy` | | **Sound effects** | HeyGen library → bundled library | HeyGen credential (above) | bundled — no deps | -| **Capture descriptions** | OpenRouter → Gemini | `OPENROUTER_API_KEY` → `GEMINI_API_KEY` | — (optional; for [website-to-video](/guides/website-to-video)) | +| **Capture descriptions** | OpenRouter → Gemini | `OPENROUTER_API_KEY` → `GEMINI_API_KEY` | — (optional; for [website capture](/guides/website-to-video)) | Run `npx hyperframes doctor` to check which local dependencies are installed. The media skills also run `hyperframes auth status` as a preflight before generating, so you always know whether a run will use HeyGen or a local engine before it starts. diff --git a/docs/guides/pipeline.mdx b/docs/guides/pipeline.mdx index 8298adf986..71b273ccd3 100644 --- a/docs/guides/pipeline.mdx +++ b/docs/guides/pipeline.mdx @@ -5,7 +5,7 @@ description: "The 7-step pipeline for producing any Hyperframes video: capture, Every well-structured Hyperframes video flows through the same 7 steps, whether it starts from a website, a PDF, a CSV, or a blank page. Each step produces a named artifact that the next step depends on, so your AI agent (and you) always know what's done, what's next, and where the creative decisions live on disk. -This pipeline is the backbone of the [website-to-video workflow](/guides/website-to-video), but it's just as useful when you're scripting a brand reel from scratch, turning research notes into a launch teaser, or learning Hyperframes for the first time. Most of the production-grade [launch videos](/launch-videos) HeyGen ships are organized this way. +This pipeline is the backbone of the [website-to-video guide](/guides/website-to-video), but it's just as useful when you're scripting a brand reel from scratch, turning research notes into a launch teaser, or learning Hyperframes for the first time. Most of the production-grade [launch videos](/launch-videos) HeyGen ships are organized this way. ## The seven steps @@ -196,7 +196,7 @@ Each artifact is a checkpoint, so you can stop, hand off to a human reviewer, or The pipeline is the recommended structure for: -- Capturing a website with the [/website-to-video](/guides/website-to-video) skill, which follows it end-to-end. +- Capturing a website with the [/product-launch-video](/guides/website-to-video) workflow, which follows it end-to-end. - Shipping a product launch. Most of the [HeyGen launch videos](/launch-videos) use this artifact layout. - Any narrative video with three or more beats, where a storyboard pays for itself. - Learning Hyperframes, because the artifacts leave every creative decision inspectable on disk. @@ -207,7 +207,7 @@ For a 5-second one-shot animation, a single hand-authored composition is fine; t - The full website-to-video workflow built on this pipeline. + The full website-to-video guide built on this pipeline. How to invoke the pipeline through your AI agent. diff --git a/docs/guides/prompting.mdx b/docs/guides/prompting.mdx index fe78e69047..e74688ef35 100644 --- a/docs/guides/prompting.mdx +++ b/docs/guides/prompting.mdx @@ -32,8 +32,7 @@ The installer shows a picker. Select the **core skills** below — every project | Slash command | Input → output | | ------------------------ | --------------------------------------------------------------------------- | -| `/product-launch-video` | A product URL / brief / script → launch or promo video | -| `/website-to-video` | A general website / URL → a video of the site (tour / showcase / social clip) | +| `/product-launch-video` | Any website URL / brief / script → launch or promo video, or a site tour / showcase | | `/faceless-explainer` | Arbitrary text (no URL) → faceless explainer with its own TTS narration | | `/pr-to-video` | A GitHub PR → code-change explainer | | `/embedded-captions` | An existing talking-head video → the same footage with captions / subtitles | diff --git a/docs/guides/skills.mdx b/docs/guides/skills.mdx index 33aee727eb..f9b2f1199e 100644 --- a/docs/guides/skills.mdx +++ b/docs/guides/skills.mdx @@ -7,7 +7,7 @@ HyperFrames ships AI agent skills via [vercel-labs/skills](https://github.com/ve The skills split into three groups: -- **Router** — the entry skill that picks a workflow for any "make me a…" request — usually a video, but also a navigable deck (`/slideshow`) or a composition port (`/remotion-to-hyperframes`). +- **Router** — the entry skill that confirms the brief up front (the intent layer) and picks a workflow for any "make me a…" request — usually a video, but also a navigable deck (`/slideshow`) or a composition port (`/remotion-to-hyperframes`). - **Creation workflows** — one per input shape (URL, PR, music track, captions, etc.). Each owns its task end-to-end: project setup, gated steps, and the final deliverable. Read by the router; you can also invoke directly if you already know which one fits. - **Domain skills** — atomic capabilities (animation, media, CLI, registry) the workflows compose against; they never own the end-to-end task. Load one when you need that specific layer. @@ -68,7 +68,7 @@ The single entry point. Read `/hyperframes` before invoking anything else — it | Skill | Use when | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/hyperframes` | **Read first** for any request to make / create / edit / animate / render a video, animation, or motion graphic. Capability map for the domain skills and intent router for the creation workflows below. | +| `/hyperframes` | **Read first** for any request to make / create / edit / animate / render a video, animation, or motion graphic. Capability map for the domain skills, the intent layer that confirms every creation brief up front, and intent router for the creation workflows below. | ## Creation workflows @@ -76,8 +76,7 @@ One workflow per input shape. The router (`/hyperframes`) picks one of these for | Skill | Use when | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `/product-launch-video` | Marketing / launching / promoting a **product** — from its URL, a brief, or a script (even if the site is only named). Up to ~3 min (sweet spot 30-90s). | -| `/website-to-video` | Turning a **general website** into a video — site tour, portfolio / landing-page showcase, social clip from the site's own visuals. | +| `/product-launch-video` | Any **website** — marketing / launching / promoting a product (from its URL, a brief, or a script), or a site tour / showcase / social clip featuring the site's own visuals. Up to ~3 min (sweet spot 30-90s). | | `/faceless-explainer` | **Explaining a topic / concept** from arbitrary text — no product, no URL, no website capture; every visual is LLM-invented (typography / abstract / diagram / data-viz). | | `/pr-to-video` | A **GitHub pull request** (PR URL, `owner/repo#N` ref, or "this PR") → changelog / feature-reveal / fix / refactor explainer, read via the `gh` CLI. | | `/embedded-captions` | Adding **captions / subtitles** to an existing talking-head video (footage untouched) — verbatim rail, embedded climax behind the subject, or pure-cinematic embed. | @@ -85,7 +84,7 @@ One workflow per input shape. The router (`/hyperframes`) picks one of these for | `/motion-graphics` | A short, **unnarrated, design-led motion graphic** (~under 10s) — kinetic type, stat / chart hit, logo sting, lower-third, animated tweet / headline. MP4 or transparent overlay. | | `/music-to-video` | A **music track** (audio file, or video to pull audio from) → a **beat-synced** video — lyric, slideshow, or kinetic promo; music drives pacing. | | `/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` | **Anything else** — longer or multi-scene pieces, brand / sizzle reel, title card, static loop, freeform composition. Input- and length-agnostic fallback. | +| `/general-video` | **Anything else** — longer or multi-scene pieces, brand / sizzle reel, title card, static loop, freeform composition. Input- and length-agnostic fallback, and the home of companion mode (co-create with the full toolbox). | | `/remotion-to-hyperframes` | **Porting an existing Remotion** (React) composition's source to HyperFrames HTML. One-way migration, not creation. | ## Domain skills (loaded on demand) diff --git a/docs/guides/website-to-video.mdx b/docs/guides/website-to-video.mdx index fd530f9fbf..b2f2215af3 100644 --- a/docs/guides/website-to-video.mdx +++ b/docs/guides/website-to-video.mdx @@ -216,7 +216,7 @@ See the [pipeline guide](/guides/pipeline#iterating) for more re-entry patterns. npx skills add heygen-com/hyperframes ``` - Lead your prompt with _"Use the /website-to-video skill"_ for the most reliable results. Agents also discover it automatically when they see a URL and a video request. + Lead your prompt with _"Use the /product-launch-video skill"_ for the most reliable results — site tours route there too. Agents also discover it automatically when they see a URL and a video request. diff --git a/docs/launch-videos.mdx b/docs/launch-videos.mdx index 0c7e6fc9a1..c4dcdd1b03 100644 --- a/docs/launch-videos.mdx +++ b/docs/launch-videos.mdx @@ -19,7 +19,7 @@ Each subdirectory is a standalone HyperFrames project — `index.html`, `composi | Project | What it shows | Source | | --- | --- | --- | | **HyperFrames launch** | The original announcement video — 49.7 s, glass-frame intro, CSS / GSAP / Lottie / shader / Three.js flex section, full narrative arc. | [`hyperframes-launch/`](https://github.com/heygen-com/hyperframes-launches/tree/main/hyperframes-launch) | -| **Website → HyperFrames** | Companion to the [website-to-video](/guides/website-to-video) workflow — captures a site and animates it. | [`website-to-hyperframes/`](https://github.com/heygen-com/hyperframes-launches/tree/main/website-to-hyperframes) | +| **Website → HyperFrames** | Companion to the [website-to-video](/guides/website-to-video) flow — now handled by `/product-launch-video` — captures a site and animates it. | [`website-to-hyperframes/`](https://github.com/heygen-com/hyperframes-launches/tree/main/website-to-hyperframes) | | **Timeline Editor launch** | 60 fps reveal video for the HyperFrames Timeline Editor: late-SFX cold open, chat-spiral of fine-tune prompts, editor reveal. | [`timeline-launch/`](https://github.com/heygen-com/hyperframes-launches/tree/main/timeline-launch) | | **Texture launch** | Texture-mask text + shader-driven background, used as a launch teaser. | [`texture-launch-video/`](https://github.com/heygen-com/hyperframes-launches/tree/main/texture-launch-video) | | **VFX × HeyGen combined** | Multi-act video chaining a VFX text-cursor scene with the HeyGen iPhone canvas test — useful as a reference for combining two existing projects into one render. | [`vfx-heygen-combined/`](https://github.com/heygen-com/hyperframes-launches/tree/main/vfx-heygen-combined) | diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 9204e1a0c1..6003b9c409 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -438,7 +438,7 @@ Word-level transcripts (whisper output) are grouped into readable caption cues o The capture command extracts everything an AI agent needs to understand a website's visual identity: viewport screenshots at every scroll depth, color palette (pixel-sampled + DOM computed), font files, images with semantic names, SVGs, Lottie animations, video previews, WebGL shaders, visible text, and page structure. - Output is a self-contained directory with a `CLAUDE.md` file that any AI agent can read to understand the captured site. Used by the `/website-to-video` skill as step 1 of the video production pipeline. + Output is a self-contained directory with a `CLAUDE.md` file that any AI agent can read to understand the captured site. Used by the `/product-launch-video` workflow as step 1 of the video production pipeline. Set `GEMINI_API_KEY` in a `.env` file for AI-powered image descriptions via Gemini vision (~$0.001/image), or set `OPENROUTER_API_KEY` to use any vision model through [OpenRouter](https://openrouter.ai) instead (takes priority if both are set; override the model with `HYPERFRAMES_OPENROUTER_MODEL`). See the [Website to Video](/guides/website-to-video#enriching-captures-with-gemini-vision) guide for details. diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx index af6fb3e448..971c8c6a6b 100644 --- a/docs/quickstart.mdx +++ b/docs/quickstart.mdx @@ -26,7 +26,7 @@ The installer shows a picker. Select the **core set** every project needs — th | `/hyperframes-registry` | Install catalog blocks and components | | `/general-video` | The general authoring workflow — the fallback for any video that doesn't match a specific workflow | -The **workflow skills** are optional — add the ones that match your inputs, and `/hyperframes` routes to whichever you've installed: `/product-launch-video`, `/website-to-video`, `/faceless-explainer`, `/pr-to-video`, `/embedded-captions`, `/talking-head-recut`, `/motion-graphics`, `/music-to-video`, `/slideshow`, `/general-video`, `/remotion-to-hyperframes`. See the [skills catalog](/guides/skills) for the full list with one-line "use when" descriptions. +The **workflow skills** are optional — add the ones that match your inputs, and `/hyperframes` routes to whichever you've installed: `/product-launch-video`, `/faceless-explainer`, `/pr-to-video`, `/embedded-captions`, `/talking-head-recut`, `/motion-graphics`, `/music-to-video`, `/slideshow`, `/general-video`, `/remotion-to-hyperframes`. See the [skills catalog](/guides/skills) for the full list with one-line "use when" descriptions. To skip the picker and install everything (core + every workflow) in one shot, run `npx skills add heygen-com/hyperframes --all`. diff --git a/packages/cli/src/capture/agentPromptGenerator.ts b/packages/cli/src/capture/agentPromptGenerator.ts index 9b6f337450..1f590f8de7 100644 --- a/packages/cli/src/capture/agentPromptGenerator.ts +++ b/packages/cli/src/capture/agentPromptGenerator.ts @@ -7,7 +7,7 @@ * * This file generates a DATA INVENTORY that tells the AI agent what files * exist and what they contain. The actual workflow lives in the - * website-to-video skill — this file points agents there. + * product-launch-video skill — this file points agents there. */ import { writeFileSync, readdirSync, existsSync } from "node:fs"; @@ -198,7 +198,7 @@ function buildPrompt( Source: ${url} -To create a video from this capture, use the \`website-to-video\` skill. +To create a video from this capture, use the \`product-launch-video\` skill. ## What's in This Capture diff --git a/packages/cli/src/commands/preview.test.ts b/packages/cli/src/commands/preview.test.ts new file mode 100644 index 0000000000..38b02f58e6 --- /dev/null +++ b/packages/cli/src/commands/preview.test.ts @@ -0,0 +1,53 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { studioLandingSearch } from "./preview.js"; + +const tempDirs: string[] = []; + +afterEach(() => { + for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true }); +}); + +function projectWith(storyboard: string | null, frameFiles: string[] = []): string { + const dir = mkdtempSync(join(tmpdir(), "hf-preview-landing-")); + tempDirs.push(dir); + if (storyboard !== null) writeFileSync(join(dir, "STORYBOARD.md"), storyboard); + for (const file of frameFiles) { + mkdirSync(join(dir, file, ".."), { recursive: true }); + writeFileSync(join(dir, file), "
"); + } + return dir; +} + +const FRAME = (n: number, status: string) => + `## Frame ${n} — F${n}\n- status: ${status}\n- src: compositions/frames/0${n}.html\n\nBeat.\n`; + +describe("studioLandingSearch", () => { + it("returns no search without a storyboard", () => { + expect(studioLandingSearch(projectWith(null))).toBe(""); + }); + + it("lands on the board while sketches are under review (any built frame)", () => { + const dir = projectWith(`${FRAME(1, "built")}${FRAME(2, "outline")}`, [ + "compositions/frames/01.html", + ]); + expect(studioLandingSearch(dir)).toBe("?view=storyboard"); + }); + + it("lands on the board during pure planning (srcs declared, none exist)", () => { + const dir = projectWith(`${FRAME(1, "outline")}${FRAME(2, "outline")}`); + expect(studioLandingSearch(dir)).toBe("?view=storyboard"); + }); + + it("lands on the timeline once frames exist without a built status", () => { + const dir = projectWith(`${FRAME(1, "outline")}`, ["compositions/frames/01.html"]); + expect(studioLandingSearch(dir)).toBe(""); + }); + + it("lands on the timeline for fully animated boards", () => { + const dir = projectWith(`${FRAME(1, "animated")}`, ["compositions/frames/01.html"]); + expect(studioLandingSearch(dir)).toBe(""); + }); +}); diff --git a/packages/cli/src/commands/preview.ts b/packages/cli/src/commands/preview.ts index 9e1ad75598..57eed97ae5 100644 --- a/packages/cli/src/commands/preview.ts +++ b/packages/cli/src/commands/preview.ts @@ -22,7 +22,16 @@ export const examples: Example[] = [ ["List all active preview servers", "hyperframes preview --list"], ["Kill all active preview servers", "hyperframes preview --kill-all"], ]; -import { existsSync, lstatSync, symlinkSync, unlinkSync, readlinkSync, mkdirSync } from "node:fs"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + readlinkSync, + symlinkSync, + unlinkSync, +} from "node:fs"; +import { parseStoryboard, STORYBOARD_FILENAME } from "@hyperframes/core/storyboard"; import { resolve, dirname, basename, join } from "node:path"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; @@ -742,9 +751,47 @@ function compactSelectionPayload(selection: StudioSelectionSnapshot): CompactSel }; } -function openStudioBrowser(url: string, projectName: string, options?: BrowserLaunchOptions): void { +// Land the browser on the Storyboard view while the project is still planning +// or sketching — the timeline only becomes the right landing once frames are +// animated (or the storyboard never tracked statuses at all, e.g. beat plans). +export function studioLandingSearch(projectDir: string): string { + const storyboardPath = join(projectDir, STORYBOARD_FILENAME); + if (!existsSync(storyboardPath)) return ""; + let frames; + try { + frames = parseStoryboard(readFileSync(storyboardPath, "utf8")).frames; + } catch { + return ""; + } + // Sketch review in progress — the board is the review surface. + if (frames.some((f) => f.status === "built")) return "?view=storyboard"; + // Pure planning stage: frames declare src paths but none are built yet. + const srcs = frames + .map((f) => f.src) + .filter((s): s is string => typeof s === "string" && s.length > 0); + const planning = + frames.length > 0 && + frames.every((f) => f.status === "outline") && + srcs.length > 0 && + !srcs.some((s) => existsSync(join(projectDir, s))); + return planning ? "?view=storyboard" : ""; +} + +// The full Studio URL to open or hand to the user: status-aware landing view +// plus the project hash route. `url` never carries a trailing slash (both the +// embedded server and the Vite `Local:` match strip it). +function studioDeepLink(url: string, projectName: string, projectDir: string): string { + return `${url}/${studioLandingSearch(projectDir)}#project/${projectName}`; +} + +function openStudioBrowser( + url: string, + projectName: string, + projectDir: string, + options?: BrowserLaunchOptions, +): void { if (options?.noOpen) return; - openBrowser(`${url}#project/${projectName}`, { + openBrowser(studioDeepLink(url, projectName, projectDir), { browserPath: options?.browserPath, userDataDir: options?.userDataDir, remoteDebuggingPort: options?.remoteDebuggingPort, @@ -829,6 +876,7 @@ function attachStudioReadyHandler( child: StudioChildProcess, spinner: ReturnType, projectName: string, + projectDir: string, options?: BrowserLaunchOptions, ): void { let detected = false; @@ -840,7 +888,7 @@ function attachStudioReadyHandler( detected = true; spinner.stop(c.success("Studio running")); printStudioSummary(projectName, url, { footer: "Press Ctrl+C to stop" }); - openStudioBrowser(url, projectName, options); + openStudioBrowser(url, projectName, projectDir, options); child.stdout.removeListener("data", handleOutput); child.stderr.removeListener("data", handleOutput); } @@ -878,7 +926,7 @@ async function runDevMode(dir: string, options?: StudioLaunchOptions): Promise { + const project = await adapter.resolveProject(projectId); + if (!project) return null; + return { project, signature: resolveProjectSignature(adapter, project.dir) }; +} + /** * Creates a stable preview cache-busting signature for project source plus Studio manifests. */ diff --git a/packages/studio-server/src/routes/preview.ts b/packages/studio-server/src/routes/preview.ts index ebb522dcd6..f9d399f34b 100644 --- a/packages/studio-server/src/routes/preview.ts +++ b/packages/studio-server/src/routes/preview.ts @@ -7,7 +7,10 @@ import type { StudioApiAdapter } from "../types.js"; import { resolveWithinProject } from "../helpers/safePath.js"; import { getMimeType } from "../helpers/mime.js"; import { buildSubCompositionHtml } from "../helpers/subComposition.js"; -import { createProjectSignature } from "../helpers/projectSignature.js"; +import { + resolveProjectAndSignature, + resolveProjectSignature, +} from "../helpers/projectSignature.js"; import { createStudioMotionRenderBodyScript, STUDIO_MOTION_PATH, @@ -22,10 +25,6 @@ const GSAP_CDN_SCRIPT = ``; const GSAP_MOTION_PATH_CDN_SCRIPT = ``; -function resolveProjectSignature(adapter: StudioApiAdapter, projectDir: string): string { - return adapter.getProjectSignature?.(projectDir) ?? createProjectSignature(projectDir); -} - function injectProjectSignature(html: string, signature: string): string { const tag = ``; if (html.includes(`name="${PROJECT_SIGNATURE_META}"`)) { @@ -309,15 +308,15 @@ export function registerPreviewRoutes(api: Hono, adapter: StudioApiAdapter): voi // Bundled composition preview // fallow-ignore-next-line complexity api.get("/projects/:id/preview", async (c) => { - const project = await adapter.resolveProject(c.req.param("id")); - if (!project) return c.json({ error: "not found" }, 404); + const resolved = await resolveProjectAndSignature(adapter, c.req.param("id")); + if (!resolved) return c.json({ error: "not found" }, 404); + const { project, signature } = resolved; // fallow-ignore-next-line code-duplication const vars = previewVariablesFromRequest(c.req.query("variables")); if (vars.error !== undefined) return c.json({ error: vars.error }, 400); const previewVariables = vars.values; - const signature = resolveProjectSignature(adapter, project.dir); const etag = `"preview:${signature}${variablesEtagSalt(vars.raw)}"`; const ifNoneMatch = c.req.header("If-None-Match"); if (ifNoneMatch === etag) { @@ -422,15 +421,14 @@ export function registerPreviewRoutes(api: Hono, adapter: StudioApiAdapter): voi // Sub-composition preview // fallow-ignore-next-line complexity api.get("/projects/:id/preview/comp/*", async (c) => { - const project = await adapter.resolveProject(c.req.param("id")); - if (!project) return c.json({ error: "not found" }, 404); + const resolved = await resolveProjectAndSignature(adapter, c.req.param("id")); + if (!resolved) return c.json({ error: "not found" }, 404); + const { project, signature } = resolved; // fallow-ignore-next-line code-duplication const vars = previewVariablesFromRequest(c.req.query("variables")); if (vars.error !== undefined) return c.json({ error: vars.error }, 400); const previewVariables = vars.values; - - const signature = resolveProjectSignature(adapter, project.dir); const compPath = decodeURIComponent( c.req.path.replace(`/projects/${project.id}/preview/comp/`, "").split("?")[0] ?? "", ); diff --git a/packages/studio-server/src/routes/projects.test.ts b/packages/studio-server/src/routes/projects.test.ts index 7414048bdc..e6a6fcfe32 100644 --- a/packages/studio-server/src/routes/projects.test.ts +++ b/packages/studio-server/src/routes/projects.test.ts @@ -58,6 +58,50 @@ function createAdapter(projectDir: string): StudioApiAdapter { }; } +describe("GET /projects/:id/signature", () => { + it("returns the adapter's cached signature when provided", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerProjectRoutes(app, { + ...createAdapter(projectDir), + getProjectSignature: () => "cached-sig", + }); + + const response = await app.request("http://localhost/projects/demo/signature"); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ signature: "cached-sig" }); + }); + + it("computes a signature that moves when project files change", async () => { + const projectDir = createProjectDir(); + const app = new Hono(); + registerProjectRoutes(app, createAdapter(projectDir)); + + const first = (await ( + await app.request("http://localhost/projects/demo/signature") + ).json()) as { + signature: string; + }; + expect(first.signature).toMatch(/^[0-9a-f]{24}$/); + + writeFileSync(join(projectDir, "compositions", "scene.html"), "new"); + const second = (await ( + await app.request("http://localhost/projects/demo/signature") + ).json()) as { signature: string }; + expect(second.signature).not.toBe(first.signature); + }); + + it("404s for an unknown project", async () => { + const app = new Hono(); + registerProjectRoutes(app, { + ...createAdapter(createProjectDir()), + resolveProject: async () => null, + }); + const response = await app.request("http://localhost/projects/nope/signature"); + expect(response.status).toBe(404); + }); +}); + describe("registerProjectRoutes — composition discovery (#1384)", () => { it("excludes HTML inside dot-directories from compositions", async () => { const projectDir = createProjectDir(); diff --git a/packages/studio-server/src/routes/projects.ts b/packages/studio-server/src/routes/projects.ts index 76fb0079ec..b0ca6ad2a5 100644 --- a/packages/studio-server/src/routes/projects.ts +++ b/packages/studio-server/src/routes/projects.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import type { Hono } from "hono"; import type { StudioApiAdapter } from "../types.js"; import { isInHiddenOrVendorDir, walkDir } from "../helpers/safePath.js"; +import { resolveProjectSignature } from "../helpers/projectSignature.js"; const COMPOSITION_ID_RE = /data-composition-id\s*=/; @@ -39,6 +40,15 @@ export function registerProjectRoutes(api: Hono, adapter: StudioApiAdapter): voi return c.json(result); }); + // Current content signature for a project — a cheap poll target for clients + // that refresh themselves when files change on disk (the storyboard board + // re-fetches when this differs from the signature its data was loaded with). + api.get("/projects/:id/signature", async (c) => { + const project = await adapter.resolveProject(c.req.param("id")); + if (!project) return c.json({ error: "not found" }, 404); + return c.json({ signature: resolveProjectSignature(adapter, project.dir) }); + }); + // Project file tree api.get("/projects/:id", async (c) => { const project = await adapter.resolveProject(c.req.param("id")); diff --git a/packages/studio-server/src/routes/storyboard.test.ts b/packages/studio-server/src/routes/storyboard.test.ts index 94f040c375..a2fd535235 100644 --- a/packages/studio-server/src/routes/storyboard.test.ts +++ b/packages/studio-server/src/routes/storyboard.test.ts @@ -106,4 +106,17 @@ Not built yet. const { body } = await getStoryboard(dir); expect(body.frames[0].srcExists).toBe(false); }); + + it("carries a project signature in both the absent and present branches", async () => { + const dir = makeProject(); + const absent = await getStoryboard(dir); + expect(absent.body.exists).toBe(false); + expect(absent.body.signature).toMatch(/^[0-9a-f]{24}$/); + + writeFileSync(join(dir, "STORYBOARD.md"), "## Frame 1\n\nHi.\n"); + const present = await getStoryboard(dir); + expect(present.body.exists).toBe(true); + expect(present.body.signature).toMatch(/^[0-9a-f]{24}$/); + expect(present.body.signature).not.toBe(absent.body.signature); + }); }); diff --git a/packages/studio-server/src/routes/storyboard.ts b/packages/studio-server/src/routes/storyboard.ts index 874204208b..519463b295 100644 --- a/packages/studio-server/src/routes/storyboard.ts +++ b/packages/studio-server/src/routes/storyboard.ts @@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs"; import type { Hono } from "hono"; import type { StudioApiAdapter } from "../types.js"; import { resolveWithinProject } from "../helpers/safePath.js"; +import { resolveProjectAndSignature } from "../helpers/projectSignature.js"; import { parseStoryboard, SCRIPT_FILENAME, @@ -45,8 +46,11 @@ export function registerStoryboardRoutes(api: Hono, adapter: StudioApiAdapter): // file is absent we return `exists: false` with empty frames rather than 404, // so the Studio can render an opt-in empty state. api.get("/projects/:id/storyboard", async (c) => { - const project = await adapter.resolveProject(c.req.param("id")); - if (!project) return c.json({ error: "not found" }, 404); + // The signature lets the board bust poster caches and lets the client tell + // whether this payload is already current (see /projects/:id/signature). + const resolved = await resolveProjectAndSignature(adapter, c.req.param("id")); + if (!resolved) return c.json({ error: "not found" }, 404); + const { project, signature } = resolved; const abs = resolveWithinProject(project.dir, STORYBOARD_FILENAME); if (!abs || !existsSync(abs)) { @@ -57,6 +61,7 @@ export function registerStoryboardRoutes(api: Hono, adapter: StudioApiAdapter): frames: [], warnings: [], script: readScript(project.dir), + signature, }); } @@ -75,6 +80,7 @@ export function registerStoryboardRoutes(api: Hono, adapter: StudioApiAdapter): frames: resolveFrames(project.dir, manifest.frames), warnings: manifest.warnings, script: readScript(project.dir), + signature, }); }); } diff --git a/packages/studio/src/components/storyboard/FramePoster.tsx b/packages/studio/src/components/storyboard/FramePoster.tsx index 722f3dbd65..60d41e044b 100644 --- a/packages/studio/src/components/storyboard/FramePoster.tsx +++ b/packages/studio/src/components/storyboard/FramePoster.tsx @@ -10,6 +10,13 @@ export interface FramePosterProps { title: string; /** `cover` fills+crops (contact-sheet tile); `contain` letterboxes (focus hero). */ fit?: "cover" | "contain"; + /** + * Project content signature to key the poster URL on. The thumbnail route + * regenerates when the frame's source changes, but the browser only refetches + * when the URL does — so bake the signature in to pick up fresh posters after + * a background board refresh. + */ + posterVersion?: string; } /** @@ -18,11 +25,19 @@ export interface FramePosterProps { * no postMessage seek, and no client-side fps assumption. Shared by the * contact-sheet tile and the frame-focus view. */ -export function FramePoster({ projectId, src, seconds, title, fit = "cover" }: FramePosterProps) { +export function FramePoster({ + projectId, + src, + seconds, + title, + fit = "cover", + posterVersion, +}: FramePosterProps) { const [failed, setFailed] = useState(false); // The is reused (no key) when a tile/hero swaps to a different frame, so a - // prior load error would stick. Reset when the poster target changes. - useEffect(() => setFailed(false), [src, seconds]); + // prior load error would stick. Reset when the poster target changes — including + // a new posterVersion, so a frame that failed mid-write retries once it settles. + useEffect(() => setFailed(false), [src, seconds, posterVersion]); if (failed) { return (
@@ -30,12 +45,17 @@ export function FramePoster({ projectId, src, seconds, title, fit = "cover" }: F
); } - const url = buildCompositionThumbnailUrl({ + let url = buildCompositionThumbnailUrl({ previewUrl: `/api/projects/${projectId}/preview/comp/${src}`, seekTime: seconds, duration: 0, origin: window.location.origin, }); + if (posterVersion) { + const withVersion = new URL(url, window.location.origin); + withVersion.searchParams.set("sig", posterVersion); + url = withVersion.toString(); + } return ( void; /** Select a composition in the timeline (sets active comp + editing file + sidebar highlight). */ onSelectComposition: (path: string) => void; + /** Project signature the board was loaded with (busts the poster cache). */ + posterVersion?: string; } /** @@ -39,6 +41,7 @@ export function StoryboardFrameFocus({ onNavigate, onSaved, onSelectComposition, + posterVersion, }: StoryboardFrameFocusProps) { const { readProjectFile, writeProjectFile } = useFileManagerContext(); const { setViewMode } = useViewMode(); @@ -151,6 +154,7 @@ export function StoryboardFrameFocus({ seconds={posterTime(frame)} title={title} fit="contain" + posterVersion={posterVersion} /> ) : (
diff --git a/packages/studio/src/components/storyboard/StoryboardFrameTile.tsx b/packages/studio/src/components/storyboard/StoryboardFrameTile.tsx index 62b0173bfb..a128a6fbe8 100644 --- a/packages/studio/src/components/storyboard/StoryboardFrameTile.tsx +++ b/packages/studio/src/components/storyboard/StoryboardFrameTile.tsx @@ -7,6 +7,13 @@ export interface StoryboardFrameTileProps { frame: StoryboardFrameView; /** Open this frame in the full-area focus view. */ onOpen: (index: number) => void; + /** This frame's pending comment draft ("" when none). */ + commentDraft: string; + onCommentDraftChange: (index: number, text: string) => void; + /** A submitted comment the agent has not consumed yet (null when none). */ + pendingComment: string | null; + /** Project signature the board was loaded with (busts the poster cache). */ + posterVersion?: string; } function firstLine(text: string): string { @@ -26,7 +33,15 @@ function placeholderMessage(frame: StoryboardFrameView): string { /** A single contact-sheet tile: poster preview + its metadata. Click to focus. */ // fallow-ignore-next-line complexity -export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFrameTileProps) { +export function StoryboardFrameTile({ + projectId, + frame, + onOpen, + commentDraft, + onCommentDraftChange, + pendingComment, + posterVersion, +}: StoryboardFrameTileProps) { const meta = FRAME_STATUS_META[frame.status]; const renderable = frame.srcExists && frame.status !== "outline"; const title = frame.title ?? `Frame ${frame.index}`; @@ -48,6 +63,7 @@ export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFram src={frame.src} seconds={posterTime(frame)} title={title} + posterVersion={posterVersion} /> ) : ( @@ -74,6 +90,19 @@ export function StoryboardFrameTile({ projectId, frame, onOpen }: StoryboardFram {frame.duration && {frame.duration}} {frame.transitionIn && ↘ {frame.transitionIn}}
+