Commit Graph

230 Commits

Author SHA1 Message Date
Vance Ingalls 2d504763da refactor(core): simplify lint rules and assembler per code review
- Extract getSceneElements() into lint/utils.ts (was copy-pasted 4x)
- Consolidate triple zero-detection in late_init_set to single parseFloat
- Fix double regex in scene_position_override (single .match() call)
- Remove dead stripWrapperTags from assembler (validation rejects first)
- Remove WHAT comments, keep WHY comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:36:21 -07:00
Vance Ingalls 03fdc880da fix(core): add style tag to prohibited patterns and strip wrapper tags in assembler
Scene fragments with <style>/<script> wrappers around their CSS/GSAP
sections caused nested tags in the assembled output, rendering raw code
visibly on screen. The assembler now:
- Rejects fragments with <style>/<script> tags (prohibited pattern)
- Strips wrapper tags as safety net during split (defense in depth)

Also adds <style> to the fragment spec's prohibited list in multi-scene.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:21:36 -07:00
Vance Ingalls 39f363bad5 fix(core): allow extra attributes on scene divs in assembler regex
The scaffold's scene divs include data-start, data-duration, and
data-track-index attributes. The assembler regex only matched
<div id="sceneN" class="scene"> exactly. Changed to [^>]* to allow
any additional attributes before the closing >.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:18:01 -07:00
Vance Ingalls d794b0088e feat(core): add deterministic scene assembler
Adds @hyperframes/core/assemble with assembleScenes() — a deterministic
function that validates scene fragments against the spec, splits on
markers, and injects into the scaffold. No AI in the assembly loop.

The scaffold must include three markers:
- <!-- SCENE N CONTENT --> in each empty scene div
- /* SCENE STYLES */ in the <style> block
- // SCENE TWEENS in the <script> block

Fragment validation catches: duplicate/missing markers, wrong order,
prohibited patterns (script tags, DOCTYPE, tl.from, etc). Div balance
is verified post-assembly. Errors abort with specific file + message.

Also updates multi-scene.md Phase 1 (scaffold markers) and Phase 3
(use assembleScenes instead of manual stitching).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:43:28 -07:00
Vance Ingalls 4e17c2ad44 fix(core): exclude .hyperframes from studio lint file walk
The studio API's walkDir scans all HTML files in a project directory.
Scene fragments in .hyperframes/scenes/ and the design picker in
.hyperframes/pick-design.html are build artifacts, not compositions.
Linting them produces false errors (missing data-composition-id, etc).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:34:34 -07:00
Vance Ingalls e3a7ebdcf6 merge: resolve conflicts in SKILL.md references section
Keep our categorized reference structure (workflow/authoring/media)
while incorporating items from main (captions, css-patterns, transitions).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:43:32 -07:00
Vance Ingalls 317d322ace test(lint): add 11 test cases for 3 new multi-scene lint rules
All 33 tests pass (18 existing + 15 new).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:30:24 -07:00
James Russo 237847e5c6 docs: add prompt cookbook + prompting guide for AI agents (#286)
* docs: add prompt cookbook + prompting guide for AI agents

Addresses user feedback that there's no guidance on how to actually
prompt Claude Code (or other agents) once the hyperframes skills are
installed. Adds copy-pasteable example prompts in the README and
quickstart, a new prompting guide page, and a starter-prompt nudge in
the `hyperframes init` output.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(prompting): add vocabulary tables, rules, and TTS voice guide

Merges the best content from the internal prompt guide into
prompting.mdx: easing vocabulary, caption tone table, transition
energy matrix, audio-reactive frequency mapping, marker highlight
modes, TTS voice recommendations, rendering quality presets, and
framework rules (technical requirements vs best practices).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(prompting): rename page title to "Prompt Guide"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: remove greensock/gsap-skills dependency, fix Math.random nuance

The bundled skills/gsap/ already covers the GSAP surface needed for
HyperFrames compositions. Installing greensock/gsap-skills on top adds
a competing full-ecosystem skill that's mostly irrelevant (ScrollTrigger,
Draggable, SplitText, etc.) and can confuse agents about which GSAP
context to load.

Also adds seeded-PRNG nuance to the Math.random() rule in the prompt
guide (matching the skill's actual guidance).

Removed from: skills.ts, README, AGENTS.md, shared AGENTS.md/CLAUDE.md,
and prompting.mdx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: require minimal reproduction link in bug report template

Adds a required "Link to reproduction" input field asking users to push
a minimal repro to a public GitHub repo (scaffolded via
`hyperframes init repro --non-interactive --example blank`).

Also consolidates the OS/Node/FFmpeg/version fields into a single
"Environment" field using `npx hyperframes info` output — fewer fields
to fill, more consistent data.

Follows the same pattern as Next.js and Gatsby issue templates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(issue-template): use hyperframes doctor for environment info

`hyperframes info` only prints project metadata (resolution, duration,
elements). `hyperframes doctor` prints the full environment: version,
Node.js, FFmpeg, Chrome, memory, disk, Docker — everything needed to
diagnose bugs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(prompting): mention validate alongside lint in anti-patterns

Per Vance's review comment — validate catches runtime errors (JS
exceptions, missing assets, contrast) that lint doesn't.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: replace libretto example URL with hyperframes repo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:22:46 -07:00
Vance Ingalls bba8d7a7b6 fix: address PR review — lint bugs, picker crashes, pipeline gaps
Blockers:
- #2: late_init_set false positive on fractional opacity (0.5 matched as 0)
  Fixed: /opacity\s*:\s*0(?![.\d])/ negative lookahead
- #3: scene-1 prefix skip matches scene 10+ (s1- matches s10-)
  Fixed: extract full number and compare exactly

High severity:
- #4: autoAlpha not covered by late_init_set
  Fixed: checks both opacity and autoAlpha
- #5: al() crashes on non-hex colors (#fff shorthand, rgb(), null)
  Fixed: guard + shorthand expansion + NaN fallback
- #6: "Full palette" with null bg crashes isDark
  Fixed: null guard defaults to dark
- #7: template literals missed by tl_from_in_multiscene
  Fixed: regex includes backtick quotes

Medium:
- #9: no retry limit on eval failures → infinite loop
  Fixed: max 2 retries, then escalate to user
- #10: vague ID convention
  Fixed: explicit s{N}- prefix rule in multi-scene.md
- #11: visual-style.md backward compat
  Fixed: Step 0b checks both filenames
- #13: preview_html script injection
  Fixed: documented prohibition in design-picker.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:22:20 -07:00
Miguel Ángel a6ff9e2d9f fix(player): preserve iframe media attributes for runtime sync (#291)
## Summary

- `_setupParentMedia()` (added in #266) was stripping `data-start`, `data-duration`, and `src` from audio/video elements inside the composition iframe
- The runtime's `syncRuntimeMedia` queries `audio[data-start]` to find media clips — removing these attributes made the runtime unable to find, sync, or play audio
- Result: silent audio in studio preview and any context where `__player.play()` is called directly (not through the web component)

## Fix

- Keep all iframe media attributes intact so the runtime can track time position and manage playback
- When parent-frame media `play()` succeeds (mobile use case), mute the iframe copies via `volume = 0` to prevent double audio
- On desktop and in the studio (which calls `__player.play()` directly), the runtime's own media sync handles playback normally

## Test plan

- [x] 21 player unit tests pass
- [x] Verified with John Wu's slideshow project: audio element preserves `data-start`, `data-duration`, `src` after runtime init
- [x] Verified runtime `syncRuntimeMedia` finds and plays audio (currentTime advances in sync with timeline)
- [x] Build passes (lint, format, typecheck)
2026-04-16 06:20:28 +02:00
Vance Ingalls edc38bc5fd feat(lint): add 3 multi-scene lint rules, replace immediateRender rule
Three new rules that catch the bugs we hit building a 9-scene composition:

1. tl_from_in_multiscene (warning) — flags tl.from() in multi-scene
   compositions. Should use tl.set()+tl.to() instead. tl.from() causes
   elements to flash visible before their entrance animation.

2. late_init_set (warning) — flags tl.set({ opacity: 0 }) at time > 0.
   Init sets must fire at time 0 so elements are hidden before any
   transition reveals the scene.

3. scene_position_override (error) — flags #sceneN { position: relative }
   which overrides the scaffold's position: absolute and pushes scenes
   off-screen. This was the root cause of blank scenes.

Replaces missing_immediate_render_false which is now obsolete — the
correct pattern avoids tl.from() entirely rather than patching it with
immediateRender: false.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:48:43 -07:00
Vance Ingalls 5176aea065 feat(lint): add missing_immediate_render_false rule for multi-scene compositions
In multi-scene compositions sharing a single GSAP timeline, tl.from()
and tl.fromTo() default to immediateRender: true — rendering their FROM
state at time 0. This makes elements in hidden scenes (opacity: 0)
invisible when the transition reveals them.

New lint rule detects tl.from/tl.fromTo calls targeting elements inside
hidden scene divs without immediateRender: false, and emits a warning
with a fix hint.

Also updates SKILL.md multi-scene build instructions to include the
immediateRender requirement in subagent instructions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 19:31:32 -07:00
James 0a3ca498ea chore: release v0.3.1 2026-04-15 18:42:14 +00:00
Vance Ingalls a262ad59f3 chore(skills): remove 1,685 lines of redundant skill content (#283)
* chore(skills): remove 1,685 lines of redundant and irrelevant skill content

- Remove 5 GSAP references irrelevant to HyperFrames (scrolltrigger,
  plugins, react, frameworks, utils) — no scroll, no frameworks, no
  interactive plugins in video compositions
- Remove shader-setup.md and shader-transitions.md — duplicated by
  @hyperframes/shader-transitions package (packages/shader-transitions/)
- Remove marker-highlight.md and examples.md — JS library docs superseded
  by css-patterns.md (deterministic, GSAP-driven, fully seekable)
- Trim CLAUDE.md to dev-only instructions — move product docs (transcription,
  TTS, player) to skills where they belong
- Deduplicate house-style.md typography/motion sections — point to
  dedicated references instead of repeating rules
- Clean up stale references to deleted files across SKILL.md and catalog.md
- Update gsap skill description to reflect HyperFrames-only scope

Skills: 5,230 → 3,714 lines (29% reduction)
CLAUDE.md: 204 → 50 lines (75% reduction)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): update broken marker-highlight.md references in captions.md

Point to css-patterns.md instead of deleted marker-highlight.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): update stale shader CSS rule to reference package API

BG_COLOR was from the old manual setup. Now it's bgColor in the
@hyperframes/shader-transitions init() config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): address 6 doc gaps surfaced by eval agents

P0: Document HyperShader as IIFE global name in shader-transitions README
P1: Replace async fetch() with sync XHR in effects.md audio data loading
    (fetch violates synchronous timeline construction rule in SKILL.md)
P1: Change <div> to <span> in css-patterns.md marker highlight patterns
    (<div> inside <p> is invalid HTML, breaks layout in inline contexts)
P2: Clarify bgColor as fallback color in shader-transitions README
P2: Add data-start to Composition Clips table in SKILL.md
    (root composition element needs data-start="0", linter enforces it)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(templates): update init templates to match trimmed skill scope

- Remove ScrollTrigger/plugins/React/Vue/Svelte from gsap skill description
- Replace class="clip" with accurate pattern examples in skill intro text
  (class="clip" is still in Key Rules where it belongs)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): remove contradictory 5:1 contrast threshold from house-style

house-style.md said 5:1 minimum, but hyperframes validate enforces
WCAG AA (4.5:1 normal text, 3:1 large text). Now defers to validate
instead of stating a conflicting number.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 10:51:10 -07:00
James Russo acf8223171 fix(skills): correct README skills table and move orphaned scripts into hyperframes skill (#282)
## What

Fixes the README skills table to match actual skill names, and moves two orphaned script directories into the `hyperframes` skill where they belong.

## Why

**README**: Listed `hyperframes-compose` and `hyperframes-captions` as separate skills — these don't exist. Captions/compose are part of the `hyperframes` skill. Also listed `gsap-core, gsap-timeline, gsap-plugins, ...` but the actual skill is just `gsap`. Missing `hyperframes-cli` entirely.

**Orphaned scripts**: `skills/hyperframes-animation-map/` and `skills/hyperframes-contrast/` had scripts but no `SKILL.md` — they looked like broken skills and wouldn't be installed by `npx skills add`. They're helper scripts invoked by the main `hyperframes` skill (SKILL.md already references them in the "Quality Checks" section). Moving them under `skills/hyperframes/scripts/` makes them part of the skill they belong to.

## How

**README skills table** — corrected to match the 4 actual skills:
- `hyperframes` (was `hyperframes-compose` + `hyperframes-captions`)
- `hyperframes-cli` (was missing)
- `hyperframes-registry` (unchanged)
- `gsap` (was `gsap-core, gsap-timeline, gsap-plugins, ...`)

**Script moves:**
- `skills/hyperframes-animation-map/scripts/animation-map.mjs` → `skills/hyperframes/scripts/`
- `skills/hyperframes-contrast/scripts/contrast-report.mjs` → `skills/hyperframes/scripts/`
- Removed empty `skills/hyperframes-animation-map/` and `skills/hyperframes-contrast/`
- Updated path references in SKILL.md, both script headers, and `contrast-audit.browser.js`

## Test plan

- [ ] `grep -r "hyperframes-animation-map\|hyperframes-contrast" --include="*.md" --include="*.mjs" --include="*.js" --include="*.ts" .` returns no results
- [ ] `ls skills/` shows only `gsap`, `hyperframes`, `hyperframes-cli`, `hyperframes-registry`
- [ ] README skills table matches `skills/*/SKILL.md` names
- [x] Documentation updated (if applicable)
2026-04-14 20:41:54 -07:00
James Russo 8551cffddd docs: add AGENTS.md for universal AI assistant configuration (#278)
## What

Adds `AGENTS.md` at two levels:
1. **Repo-level** (`/AGENTS.md`) — for contributors working on HyperFrames itself
2. **Project-level** (`packages/cli/src/templates/_shared/AGENTS.md`) — scaffolded into user projects by `hyperframes init`

Also replaces the previous `AGENTS.md → CLAUDE.md` symlink with a standalone file.

## Why

AGENTS.md is the emerging universal standard for AI coding tool configuration, supported by Claude Code, Cursor, GitHub Copilot, Gemini CLI, and Codex (60K+ repos). Remotion already has one. For a project that positions itself as AI-native, this is a gap.

The project-level file (scaffolded by `init`) ensures every AI tool — not just Claude — gets framework context when working on a user's composition project. The symlink was replaced because symlinks are fragile on Windows and the content should differ (repo-level covers build/test, project-level covers composition rules).

## How

- **Repo-level AGENTS.md**: Build/test/lint commands, project structure, key conventions, skills install, doc links
- **Project-level AGENTS.md**: Composition-specific — skills, CLI commands, project structure, linting workflow, key rules, doc links
- **CLAUDE.md** remains for Claude-specific skill invocation syntax (slash commands)

## Test plan

- [ ] Verify AGENTS.md renders correctly on GitHub
- [ ] Verify no duplication with CLAUDE.md (AGENTS.md = universal basics, CLAUDE.md = Claude-specific slash commands)
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
2026-04-14 19:13:26 -07:00
Miguel Ángel d3f2295d80 feat(skills): add contrast audit + animation map quality skills (#267)
## Summary

Two new quality skills + CLI integration that give agents feedback loops they currently lack — pixel-level contrast auditing and structured animation analysis.

### What this unlocks for agents

**Agents can now catch accessibility failures that humans and LLMs consistently miss.** The contrast audit runs automatically on every `hyperframes validate` and reports WCAG AA violations as warnings. In the eval, 4 out of 5 palettes had failing contrast — every baseline composition shipped broken, every treatment composition caught and fixed it.

**Agents can now reason about animation choreography.** The animation map produces a structured JSON report with:
- Per-tween natural language summaries ("card1 slides 23px up over 0.5s, fades in, ends at (120, 200)")
- ASCII timeline showing the full choreography as a Gantt chart
- Stagger detection with actual intervals ("3 elements stagger at 120ms" — validates against brief specs)
- Dead zone detection (periods >1s with no animation — missing entrance or intentional hold?)
- Element lifecycles (first/last animation, final visibility — catches elements that enter but never exit)
- Scene snapshots at 5 timestamps (what's on screen at any moment)

### Changes

**Skills (new)**
- \`skills/hyperframes-contrast/\` — WCAG contrast audit skill + script
- \`skills/hyperframes-animation-map/\` — animation analysis skill + script

**CLI**
- \`hyperframes validate\` now runs contrast audit by default (warnings, not errors)
- \`hyperframes validate --no-contrast\` to skip
- \`hyperframes render --html-only\` compiles HTML without video encoding
- Browser-side WCAG code in \`contrast-audit.browser.js\`, inlined at build time via esbuild text loader

**Producer**
- Exported \`compileForRender\` for the \`--html-only\` flag

### Eval results

5 prompts x 2 arms = 10 compositions. Arm A = baseline skills. Arm B = +contrast +animation-map.

| Prompt | Failing color | Before | After |
|--------|--------------|--------|-------|
| Halflife | Cement on Ink | 2.98:1 | 5.33:1 |
| Meridian | Ash on Midnight | 2.08:1 | 5.44:1 |
| Typesmith | Pencil on Paper | 3.19:1 | 5.50:1 |
| Lattice | Gray-600 on Terminal | 2.59:1 | 7.50:1 |

Animation map correctly enumerated 142 tweens across 5 compositions, detected stagger groups, flagged pacing issues, and produced scene snapshots.

### Pitch video

https://itnjfahrnzqvcluhrtif.supabase.co/storage/v1/object/public/assets/uploads/8f043e1c-6882-4fa9-98fd-efb6b3583afa.mp4

### Dedicated evals

https://www.heygenverse.com/a/2cac956b-3d14-47bf-90e8-3c1f50e671f3

## Test plan

- [x] Eval: 10 compositions (5 baseline, 5 treatment), all rendered
- [x] Contrast audit caught 4/4 failing palettes, 0 missed
- [x] \`hyperframes validate\` shows contrast warnings by default (exit 0)
- [x] \`hyperframes validate --no-contrast\` skips audit
- [x] \`hyperframes validate --json\` includes contrast data
- [x] Animation map tested on 3 compositions (17, 27, 51 tweens)
- [x] Stagger detection, dead zones, snapshots, timeline all verified
- [x] \`bun run build\` passes
- [x] \`bun run lint\` passes (0 errors, 0 warnings, 0 skill lint issues)
2026-04-15 03:09:56 +02:00
James Russo 13ab1932ad feat(cli): catalog browser command (#271)
Adds `hyperframes catalog` for browsing the registry:

- Default: non-interactive table output (agent-friendly)
- --type block/component and --tag filters
- --json for machine-readable output
- --human-friendly for interactive picker that installs on select

Registered in cli.ts, help.ts, documented in docs/packages/cli.mdx.
2026-04-14 16:46:42 -07:00
James Russo 4bde66f532 feat(skills): hyperframes-registry skill (#261)
## What

New skill `hyperframes-registry` that teaches AI coding agents how to install and wire registry blocks and components into HyperFrames compositions.

### Skill structure
```
skills/hyperframes-registry/
  SKILL.md                          — triggers, overview, quick reference
  references/
    install-locations.md            — default paths, hyperframes.json config
    wiring-blocks.md                — iframe inclusion, data attributes, positioning
    wiring-components.md            — snippet merging (HTML, CSS, JS, timeline)
    discovery.md                    — manifest reading, item fields, available items table
    demo-html-pattern.md            — why components ship demo.html, structure conventions
  examples/
    add-block.md                    — worked example: data-chart block install + wiring
    add-component.md                — worked example: shimmer-sweep component install + wiring
```

## Why

Phase B of the catalog plan (PR 10). Without this skill, agents using `hyperframes add` have to guess how to wire installed items into compositions. The skill encodes the iframe/snippet patterns so agents get it right on the first attempt.

## How

- SKILL.md frontmatter triggers on: `hyperframes add`, "block", "component", `hyperframes.json`
- References cover every step: discovery, install, wiring blocks (iframe), wiring components (snippet merge), and the demo.html convention
- Two worked examples walk through complete install-to-preview workflows
- Updated CLAUDE.md skills table + trigger rules, README.md skills table, docs/packages/cli.mdx

## Test plan

- [x] `scripts/lint-skills.ts` passes (checked 4 skill files, no issues)
- [x] `oxfmt --check` passes on all markdown files
- [x] SKILL.md frontmatter has valid `name` and `description`
- [x] All reference links in SKILL.md resolve to existing files
- [x] CLAUDE.md, README.md, and docs CLI page updated with new skill
2026-04-14 16:27:24 -07:00
James Russo b23b0751da fix(player): parent-frame media playback for mobile (#266)
* fix(player): parent-frame media playback for mobile

Mobile browsers block media.play() inside iframes when the user
gesture happened in the parent frame — postMessage doesn't transfer
user activation (per the User Activation v2 spec).

## Problem

The player renders compositions in a sandboxed iframe. When a user
taps play in the parent frame, the player sends a postMessage to the
iframe's runtime, which calls audio.play(). On mobile, this fails
silently because the iframe has no user activation context.

## Solution

The player now extracts ALL timed media elements (audio/video with
data-start) from the iframe's DOM (same-origin access), creates
parent-frame copies, and disables the iframe originals. On play(),
parentMedia.play() runs synchronously in the gesture call stack,
satisfying mobile autoplay policy.

### Generic media handling

- Finds all `audio[data-start], video[data-start]` in the iframe
- Creates a parent-frame copy for each (Audio or Video element)
- Preserves data-start offsets for correct seek positioning
- Strips data-start from iframe elements so the runtime ignores them
- Falls back to iframe media for cross-origin iframes

### `audio-src` attribute

Convenience for the common single-narration case. When set, the
player starts preloading audio immediately — before the iframe loads.
This eliminates the loading delay that caused jittery playback.

### No active sync

Both parent media and the GSAP timeline are real-time systems. When
started simultaneously, they naturally stay within ~10ms — no drift
correction needed. Active sync with coarse granularity (50ms polling)
caused MORE jitter than it prevented via repeated audio seeks.

## CI

- Added unified `test` job replacing separate per-package test jobs
- Added root `test` script: `bun run --filter '*' test`
- New packages with test scripts are automatically included
- Added happy-dom for player DOM tests

## Tests

- 10 new tests for parent-frame media: preloading, play, pause,
  seek, muted/rate sync, cleanup, attribute changes
- All 21 player tests pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(shader-transitions): pass CI when no test files exist

Add --passWithNoTests to vitest run so the unified test job
doesn't fail on packages that have a test script but no test
files yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): update tests for new id field and GSAP lint rule

- normalize.test.ts: loadTranscript now assigns id fields (w0, w1, etc.)
  to SRT/VTT results and empty string for words-json passthrough
- lintProject.test.ts: add GSAP CDN script to validHtml() fixture to
  satisfy the missing_gsap_script lint rule added in core

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(cli): add missing data-start/data-duration to validHtml fixture

The validHtml() test fixture was missing data-start and data-duration
attributes, triggering the root_composition_missing_data_start and
root_composition_missing_data_duration lint warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): fetch LFS objects for producer test job

Producer regression tests compare rendered output against reference MP4
files stored in git LFS. Without lfs: true, checkout fetches pointer
files instead of actual videos, causing "moov atom not found" errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ci: remove redundant test-producer job

The regression workflow already runs the same 28 producer fixtures
in a Docker container with prod-matching Chrome/fonts/ffmpeg, sharded
across 8 parallel matrix jobs with 40-min timeouts. The CI test-producer
job was a duplicate that ran on bare runners with worse determinism
and a 15-min timeout too short for all fixtures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:20:07 -07:00
James Russo 9bf4956fae chore(shader-transitions): add to CI publish pipeline and README (#264)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 22:07:19 -07:00
James Russo 08fb1de61f feat(cli): add command + hyperframes.json (#256)
## What

PR 5/17 of the catalog system rollout. Adds the `hyperframes add` verb for installing blocks and components from the registry into an existing project, plus the `hyperframes.json` project config that tells `add` which registry to use and where to drop files. Stacks on #255.

- **`packages/cli/src/commands/add.ts`** — new `hyperframes add <name>` command. Resolves an item, validates target paths, installs files in parallel, builds an include snippet, copies it to the clipboard. Exposes a testable `runAdd(opts)` function; the citty default wraps it with console output + exit handling
- **`packages/cli/src/utils/projectConfig.ts`** — read/write/normalize `hyperframes.json`. Tolerant to missing and partial configs
- **`packages/cli/src/utils/clipboard.ts`** — minimal cross-platform clipboard (pbcopy / clip.exe / wl-copy / xclip / xsel). Zero deps. Gracefully no-ops in headless environments
- **`packages/cli/src/commands/init.ts`** — write `hyperframes.json` during scaffold if not already present
- **`packages/cli/src/cli.ts`** + **`help.ts`** — register `add` under Getting Started (directly below `init`)

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a).

## UX

```bash
# Scaffold a project (now writes hyperframes.json too)
npx hyperframes init my-video --example blank
cd my-video

# Add a block — files land, snippet copied to clipboard
npx hyperframes add claude-code-window
#  ✓ Added claude-code-window (hyperframes:block)
#    compositions/claude-code-window.html
#
#  Include snippet:
#    <iframe src="compositions/claude-code-window.html" data-start="0" data-duration="6"></iframe>
#
#  Copied to clipboard — paste into your host composition.

# Add a component effect
npx hyperframes add shader-wipe

# Headless / CI — no clipboard, JSON output for tooling
npx hyperframes add shader-wipe --no-clipboard --json
```

Running `hyperframes add warm-grain` (an example) errors clearly pointing to `init --example`.

## Docs (bundled in this PR per the tracker principle)

- `docs/packages/cli.mdx` — new `add` subsection under Commands (flags, examples, trigger rules) + new `hyperframes.json` section describing the config file shape

## Tests

- **`packages/cli/src/commands/add.test.ts`** — 11 tests:
  - `remapTarget` / `buildSnippet` pure helpers (5 tests)
  - `runAdd` integration against a mocked `fetch` registry: block install lands files + returns snippet, component install respects `paths.components` remap, example-typed names throw `AddError` with code `example-type`, unknown names throw `AddError` with code `unknown-item` (4 tests plus 2 covering block default path and non-default path preservation)
- **`packages/cli/src/utils/projectConfig.test.ts`** — 9 tests:
  - Write/read round-trip, partial-config normalization, corrupt-file handling, absent-file fallback to defaults, custom paths preserved
- **CLI suite:** 92 passed (was 72 on #255, **+20**). Same 4 pre-existing failures unchanged

## Scope decisions

- **`init.ts` full port to new resolver deferred.** The original plan bundled a removal of the `packages/cli/src/templates/` compat shim. That's ~300 more lines and isn't required for `add` to work. The compat shim from #254 still functions; a separate cleanup PR handles it
- **No ajv runtime schema validation.** Manifests are trusted as schema-valid. Full validation lands when third-party registries arrive (PR 14/15). Path safety is still enforced by the installer's `assertSafeTarget` guard
- **Default project paths stay under `compositions/`.** Blocks → `compositions/<name>.html`; components → `compositions/components/<name>/<file>`. Users override via `hyperframes.json#paths`

## Breaking / migration

**None.** Pure additive — new command, new file types, no existing commands or flags change. `init.ts` now writes `hyperframes.json` but that's a new additional file, not a modification of existing output.

## Stacks on

#255 — base branch. When #255 merges, this rebases onto `main`.

## Next in stack

PR 6 — `feat(registry): seed block — claude-code-window`. First real registry item. Exercises the full `hyperframes add <name>` flow end-to-end against a committed item on `main`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 21:04:59 -07:00
James Russo c8acd8abd8 feat(cli)!: rename --template to --example (#255)
## What

PR 4/17 of the catalog system rollout. **Single clean cut** — the old flag is gone, replaced by `--example`. Alias changes from `-t` to `-e`. Stacks on #254.

- Rename `--template` → `--example` (alias `-e`) on `hyperframes init`
- Accept `--template` as a recognized-but-errored flag so users get a clear rename hint instead of citty silently ignoring the flag and producing a blank project
- Update all user-visible strings that referenced "template" as a user-facing concept in the init flow (picker prompt, step comments, offline-fallback suggestion)
- New `init.test.ts` covering both the success case (`--example` scaffolds) and the error case (`--template` exits 1 with rename hint)

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a).

## ⚠️ Breaking change

`--template` is no longer accepted. Example:

```bash
# before
npx hyperframes init my-video --template warm-grain

# after
npx hyperframes init my-video --example warm-grain
```

Users who still type the old flag will see:

```
The --template flag was renamed to --example. Example:
  npx hyperframes init my-video --example warm-grain
```

and the command exits with code 1. This is **user guidance, not backwards compat** — the old flag's behavior is fully gone.

## Docs (bundled per the tracker principle)

- `docs/templates.mdx` — every `--template` reference
- `docs/quickstart.mdx` — agent-mode and video-mode examples
- `docs/packages/cli.mdx` — prose, `--help` flag table, `-e` alias
- `packages/cli/src/docs/templates.md` — CLI-embedded help topic
- `README.md` and `CONTRIBUTING.md` — not affected (no flag references)

User-facing renames of the `templates.mdx` page title, nav entry, and URL route are deferred to PR 11 (catalog discoverability UX) as planned.

## Why

1. **"examples"** matches shadcn + Remotion convention for full-project scaffolds and frees the word "template" for future parameterization work (string templating, placeholder substitution)
2. Once `hyperframes add` lands in PR 5, "template" vs "block" vs "component" would be three subtly different concepts sharing one word — renaming the old one to "example" makes the taxonomy self-explaining

## How

- **citty silently ignores unknown flags.** Naively removing `--template` would cause `hyperframes init my-video --template warm-grain` to silently fall through and scaffold a blank project. So `--template` stays declared in the args schema, but its run handler immediately errors with a rename hint and exits 1
- **Internal names unchanged** — `templateId` local variables, `getStaticTemplateDir` function, `BUNDLED_TEMPLATES` constant. They're implementation details; their rename is scheduled for PR 5 when the compat shims in `packages/cli/src/templates/` are fully removed alongside the `init` refactor

## Test plan

- [x] `bun run test` in `packages/cli`: **72 passed** (was 70 on #254, +2 new `init.test.ts` cases). Same 4 pre-existing failures unchanged
- [x] **New unit tests** in `init.test.ts`:
  - `--example blank` non-interactive: exits 0, writes `index.html` to the target dir
  - `--template blank` non-interactive: exits non-zero, stderr contains the rename hint + corrected command line, target dir is **not** created
- [x] **Manual smoke:**
  - `npx hyperframes init /tmp/x --example blank` → "Created /tmp/x/"
  - `npx hyperframes init /tmp/y --template blank` → "The --template flag was renamed to --example..." exit=1
- [x] `bunx oxfmt --check` + `bunx oxlint` on changed files: clean
- [x] Pre-commit typecheck (core + studio): clean

## Incidental fix

Resolver test regression from PR 3's simplify follow-up: `loadAllItems`' warning-path test was still spying on `console.warn` after the `onWarn` callback refactor. Now uses the callback directly.

## Stacks on

#254 — base branch. When #254 merges, this rebases onto `main`.

## Next in stack

PR 5 — `feat(cli): add command + hyperframes.json`. The big UX PR where:
- `init.ts` gets fully ported to the new registry resolver
- Compat shims in `packages/cli/src/templates/` are removed
- Users gain the `add` verb for installing blocks and components into existing projects
- `hyperframes.json` project-config file lands

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 20:44:23 -07:00
James Russo 969474e843 feat(cli): registry resolver + installer (#254)
## What

PR 3/17 of the catalog system rollout. Introduces the registry resolver/installer abstraction. No UX change — `init --template` still works identically. Stacks on #253.

**New module: `packages/cli/src/registry/`**
- `remote.ts` — fetches manifests (`registry.json`, `registry-item.json`) and item files from a GitHub-hosted registry. 24h cache on manifests; item files stream straight to `destDir`
- `resolver.ts` — `listRegistryItems`, `loadAllItems` (parallel fetch for picker UX), `resolveItem` (single-item fetch with `Available:` error)
- `installer.ts` — `assertSafeTarget` (runtime path-traversal guard) + `installItem` (parallel file download with up-front validation; all-or-nothing semantics)
- `index.ts` — barrel

**Registry content:**
- `registry/registry.json` — top-level manifest in PR 1's `RegistryManifest` shape. 8 examples
- `registry/examples/<id>/registry-item.json` — per-item manifest for each existing example, generated from legacy `templates.json` + HTML data-attribute probing
- `registry/examples/templates.json` — **deleted**, replaced by the above

**Compat layer:**
- `packages/cli/src/templates/{remote,generators}.ts` — thin shims that delegate to `../registry/`, keeping `init.ts`'s existing imports stable. `init.ts` doesn't move to the new API until PR 5 where it's part of a larger UX pass

**Tooling:**
- `scripts/generate-registry-items.ts` — idempotent one-off generator for this PR, kept in-repo for future example additions (`--only <name>` flag)

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a). Tracker entry in local `hyperframes-catalog-plan.md`.

## Why

Every future PR (`hyperframes add`, seed blocks, seed components, custom registries) otherwise has to keep piling onto the ad-hoc fetch + `cpSync` pattern in the old `fetchRemoteTemplate`. The new module is the single place that understands the registry wire format and file layout. **This is also where PR 1's schema comes alive.**

## How

### Scope-trimmed from the plan

- **No transitive dependency resolution yet.** Examples have no deps today. `resolveItem` doesn't walk `registryDependencies`; PR 5 adds that when blocks/components need it.
- **No ajv schema validation yet.** TS types + runtime path-traversal guard are the only safety nets. Full JSON-Schema validation lands when the registry starts accepting third-party content (PR 14 / custom registries).
- **init.ts refactor deferred to PR 5.** Compat shims keep this PR small and reviewable. PR 5 rewrites init alongside adding the `add` command.

### Safety

- `assertSafeTarget` rejects absolute paths, `..` segments, Windows drive letters, and any target that `path.resolve` shows to escape `destDir`. Mirrors the PR 1 schema `pattern`/`not.anyOf` on `target`, but runs at install-time so a registry that bypasses schema validation still can't write outside the project
- Up-front validation in `installItem` means a malformed item fails **before** any file is written. Atomic-ish semantics: all files land or none do

### Caching

- 24h manifest cache lives at `~/.hyperframes/cache/` per existing convention, but now keyed by `<baseUrl>__<kind>__<name>.json` so PR 14 custom registries can coexist

## Test plan

- [x] `bun run test` in `packages/cli`: **70 passed** (was 57 on #253, +13). Same 4 pre-existing failures (SRT/VTT whisper normalizer + `lintProject` clean-project test) — identical to main. No regressions
- [x] **Resolver unit tests (8):** filter by type, parallel load with fail-safe, resolve-by-name with `Available:` error message, unreachable-registry handling
- [x] **Installer unit tests (5):** accepts simple relative paths, rejects `..` segments, rejects Unix absolute paths, rejects Windows drive letters, permits `.` and dotfile-like names
- [x] **Smoke test**: `hyperframes init /tmp/x --template blank` (bundled code path, unchanged) works end-to-end
- [x] `bunx oxfmt --check` + `bunx oxlint`: clean
- [x] Pre-commit typecheck (core + studio): clean. CLI typecheck has 2 pre-existing errors (`render.ts`, `studioServer.ts` — unrelated `"mov"` format issue on main)
- [ ] **Smoke test remote fetch (`--template warm-grain`)** — verifiable only post-merge; registry paths live on `main` after this PR lands

## Breaking / migration

**No end-user-visible UX change.** `init --template <name>` still works the same way. Internally, `templates.json` is gone and the CLI now reads `registry.json` + `registry-item.json` per example.

Installed CLIs on old versions (`hyperframes@0.1.0`–`0.3.0`) already broke at PR 2 merge (see #253 rollout note). The next CLI release after this lands (`0.3.1`+) is the full fix.

## Commits

1. `generate-registry-items.ts` + generated manifests + deleted `templates.json`
2. Resolver + installer + compat shims
3. Unit tests

(All squashed into one commit on this branch; see `git log feat/registry-resolver ^refactor/registry-examples-dir`.)

## Stacks on

#253 — base branch. When #253 merges, this rebases onto `main`.

## Next in stack

PR 4 — `feat(cli)!: rename --template to --example`. Single clean cut, no alias. Tiny PR (~150 lines) that mostly updates `init.ts`'s argument schema, help text, and docs. Depends on this PR so the new flag name can be applied against the refactored code path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 20:41:23 -07:00
James Russo 69d9f08061 refactor: migrate templates/ → registry/examples/ (#253)
## What

PR 2/17 of the catalog system rollout. **Physical directory rename.** Stacks on #252.

- `git mv templates/ registry/examples/` — all 8 example directories (`decision-tree`, `kinetic-type`, `nyt-graph`, `play-mode`, `product-promo`, `swiss-grid`, `vignelli`, `warm-grain`) plus `templates.json`
- `packages/cli/src/templates/remote.ts` — `TEMPLATES_DIR` constant from `"templates"` → `"registry/examples"`, exported for regression testing
- `scripts/generate-template-previews.ts` — `remoteTemplatesDir` resolved to the new path
- Comment updates in `packages/cli/src/templates/generators.ts` and `packages/cli/src/commands/init.ts`
- New regression test `packages/cli/src/templates/remote.test.ts` pinning the path constants so future reverts fail a test instead of silently breaking installed CLIs

Design doc: [Hyperframes Catalog System](https://www.notion.so/heygen/Hyperframes-Catalog-System-Design-Plan-341449792c69813f899dcd53b4c0383a).

## Why

The current `templates/` directory is a flat "things that scaffold projects" bucket. The catalog model splits content into three tiers: **examples** (full projects — what today's templates are), **blocks** (sub-compositions), and **components** (effect snippets). `registry/examples/` is the canonical home for what was previously at `templates/`, and this PR makes room for `registry/blocks/` and `registry/components/` in future PRs without top-level clutter.

## How

- `git mv` preserves file history — GitHub renders these as renames, not deletions + additions.
- Remote template fetch via giget reads `TEMPLATES_DIR`, so updating that one constant is sufficient for the CLI's remote code path.
- The CLI's **internal** `packages/cli/src/templates/` directory (which holds the `blank` and `_shared` bundled assets plus `generators.ts`/`remote.ts`) is a separate concept and is **not** touched here. Renaming that module belongs to PR 3 where the abstraction changes to a registry resolver.
- `templates.json` keeps its existing shape and location (now at `registry/examples/templates.json`). **PR 3 will transform it** to the new `registry.json` shape introduced in PR 1 and generate a per-item `registry-item.json` for each example. Leaving the shape change to PR 3 keeps this PR a pure physical move.

## ⚠️ Breaking change for previously-installed CLIs (`hyperframes@0.1.0` – `0.3.0`)

**What happens:** every published CLI version has `TEMPLATES_DIR = "templates"` baked in. After this PR lands on `main`, those CLIs will 404 on:

- `raw.githubusercontent.com/heygen-com/hyperframes/main/templates/templates.json` (manifest list) — caught silently in `listRemoteTemplates`, so the template picker falls back to showing only `blank`
- `github:heygen-com/hyperframes/templates/<id>#main` (giget download) — raises "Template downloaded but missing index.html"

**Decision: accept the break.** Hyperframes is pre-1.0 OSS with a small installed base; complex mitigations (dual-path fetch, redirect stubs, manifest-at-old-path with empty array) add permanent maintenance cost for a one-time rename.

**Rollout plan:**

1. Merge #252 (PR 1 — types & schemas) first
2. Merge this PR (#253)
3. Ship a patched CLI release (`hyperframes@0.3.1`) in the same work-day. Already-pinned old CLIs break on remote examples, but upgrading restores full functionality
4. Note the break in release notes + `CHANGELOG.md` under the `0.3.1` entry

Users still on an older CLI will see the failure only if they invoke `hyperframes init` with `--template <non-blank>`; `--template blank` (bundled) continues to work offline on every version.

## Test plan

- [x] `bun run test` in `packages/cli`: **57 passed** (was 55 on main, +2 regression tests for the path constants). Same 4 pre-existing failures (SRT/VTT whisper normalizer + `lintProject` clean-project test) — unchanged from main. No regressions
- [x] **Manual smoke test**: `hyperframes init /tmp/x --template blank` works (bundled code path, unchanged)
- [x] `bunx oxfmt --check` + `bunx oxlint`: clean
- [x] `bun run typecheck` (core + studio, pre-commit hook): clean
- [ ] **Manual smoke test for remote fetch (`--template warm-grain`)** — not verifiable locally before merge. Remote fetch resolves `github:heygen-com/hyperframes/registry/examples/<id>#main`, which doesn't exist until this PR lands. Will work on `main` immediately after merge.

## Breaking / migration

- Internal repo path changes only. `--template` CLI flag continues to accept the same template names.
- See "Breaking change for previously-installed CLIs" above — decision is to ship a simultaneous CLI release rather than add a compat shim.

## Commits

1. `d691bd1` — initial rename + CLI path constant update
2. `fc0c642` — review feedback: docstring fix, regression tests, clarifying comment in `init.ts`, export constants for testing

## Stacks on

#252 — base branch. When #252 merges, this rebases onto `main`.

## Next in stack

PR 3 — `feat(cli): registry resolver + installer`. Transforms `templates.json` to the new `registry.json` shape (from PR 1's schema), generates `registry-item.json` for every existing example, introduces `packages/cli/src/registry/{resolver,installer,remote}.ts`, renames the `packages/cli/src/templates/` CLI module, and refactors `init` to call through the new abstraction.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 20:21:23 -07:00
James Russo eb338ae859 feat(core): add registry schema + TS types (#252)
* feat(core): add registry schema + TS types

PR 1/17 of the catalog system rollout. Foundation for a shadcn-style
registry with three item tiers: examples (full projects), blocks
(sub-compositions), components (effect snippets).

## What

- TS types: RegistryItem (discriminated union of ExampleItem/BlockItem/
  ComponentItem), RegistryManifest, FileTarget, ItemType, FileType
- JSON Schemas: schemas/registry.json, schemas/registry-item.json
- Compile-time exhaustiveness asserts on ITEM_TYPES/FILE_TYPES so adding
  to the TS union without updating the constant stops compiling
- Drift-guard test: schema enums must equal ITEM_TYPES/FILE_TYPES by
  set-equality; exactly 2 distinct type enums in registry-item.json
- Public API via new ./registry export path plus re-exports from root;
  schemas exposed via ./schemas/registry.json export for external tooling

## Why

- Every downstream PR (resolver, installer, hyperframes add, docs
  codegen, CI previews, skill, catalog command) builds on these types
- Getting the shape right now avoids painful migrations later

## How

- Discriminated union enforces that components do not have dimensions
  or duration and examples/blocks must have them (schema mirrors via
  if/then/else on the type discriminant)
- target path pattern rejects .. segments, Unix absolute paths, and
  Windows drive letters (defense-in-depth; CLI validates at runtime in
  PR 3)
- name pattern requires alphanumeric start and end (no trailing hyphens)
- Optional metadata: version, author, license, deprecated, minCliVersion
- additionalProperties: false on nested objects (catches typos on
  critical fields) but relaxed on top-level RegistryItem (allows
  third-party custom metadata in PR 15 custom registries)

## Test plan

- [x] Unit tests: 11 new tests covering type guards, discriminant
      narrowing, schema/TS drift guards, schema \$id sanity, optional
      metadata acceptance, and compile-time checks (via @ts-expect-error)
- [x] bun run test in packages/core: 445 passed (was 434 on main,
      +11 from this PR)
- [x] bunx oxfmt and bunx oxlint: clean
- [x] bun run typecheck: clean
- [ ] Manual testing: N/A (types + schemas only)
- [ ] Documentation updated: per-item doc pages land in PR 9 (codegen
      from these manifests); guide updates in PR 10+

## Breaking / migration

None. Pure additive — new module, new export paths, no existing
surface touched.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): remove version field + hyperframes:demo file type

Address review feedback from Miguel:

- Remove `version` from RegistryItemBase + schema. Per shadcn model,
  the registry is versioned by git tags, not per-item. The adversarial
  review added it; the original design doc was correct.
- Remove `hyperframes:demo` from FileType union + FILE_TYPES constant
  + schema. Demo files exist on disk for the CI preview pipeline but
  are NOT installed to user projects and should not appear in
  registry-item.json files[]. Neither shadcn nor Remotion has a
  dedicated demo file type — demos are just compositions.
- Add `required: ["type"]` to the if-condition in the schema's
  allOf discriminant (Miguel's nit — makes the condition self-
  contained)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ci): add shader-transitions to Dockerfile.test

PR #251 added packages/shader-transitions/ to the workspace but didn't
update Dockerfile.test to COPY its package.json. This caused
`bun install --frozen-lockfile` to fail in the regression Docker build:
bun saw a lockfile referencing @hyperframes/shader-transitions but the
package.json wasn't present in the container, so it wanted to remove
the entry — triggering "lockfile had changes."

Verified: Docker build passes with `--no-cache` after this fix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:18:37 -07:00
Vance Ingalls cb3d94c2a5 feat: add @hyperframes/shader-transitions package (#251)
## Summary

New `@hyperframes/shader-transitions` package that encapsulates WebGL shader transitions into a single `HyperShader.init()` call. Replaces ~200 lines of per-composition boilerplate that LLMs failed to wire correctly 60% of the time.

### API

```js
var tl = HyperShader.init({
  bgColor: "#0a0a1a",
  accentColor: "#6366f1",
  scenes: ["scene1", "scene2", "scene3", "scene4", "scene5"],
  transitions: [
    { time: 7.2, shader: "cross-warp-morph", duration: 0.7 },
    { time: 15.2, shader: "domain-warp", duration: 0.7 },
  ]
});
tl.from("#s1-title", { y: 50, opacity: 0, duration: 0.7 }, 0.3);
```

### What the library handles

- **13 shader programs**: domain-warp, ridged-burn, whip-pan, sdf-iris, ripple-waves, gravitational-lens, cinematic-zoom, chromatic-split, glitch, swirl-vortex, thermal-distortion, cross-warp-morph, light-leak
- **html2canvas** bundled as dependency (not CDN) — single script tag for CLI users
- **DOM-during-holds**: canvas hidden between transitions, GSAP animations play on live DOM
- **Async capture with pause/resume**: timeline pauses during capture, resumes after textures uploaded — prevents progress tween from running ahead
- **Accent color theming**: `accentColor` derives dark/mid/bright uniforms. Burns, glows, leaks match the composition palette
- **Graceful degradation**: falls back silently when WebGL unavailable

### Code quality (from 3 review agents)

- No `!` non-null assertions — all WebGL creation calls throw on failure
- Vertex shader compiled once, cached across all programs
- Uniform/attribute locations cached per program via WeakMap (not looked up every frame)
- Captured canvases freed after texture upload (8MB each)
- Single timeline creation (was creating two, discarding one)
- Shared `tickShader()` render callback (was copy-pasted)
- `.finally()` for DOM restore in capture (was duplicated in `.then`/`.catch`)
- `parseHex` validates input (was silently producing NaN on invalid hex)
- Dead `ND`/`CP` shader library exports removed

### Shader-compatible CSS rules (transitions.md)

6 rules for compositions using shader transitions:
1. No `transparent` in gradients (canvas interpolates through black)
2. No gradient backgrounds on elements < 4px
3. No CSS variables on captured elements
4. `data-no-capture` for uncapturable decoratives
5. No gradient opacity < 0.15
6. Every `.scene` must have explicit `background-color` matching `bgColor`

### Build output

- IIFE (~214KB with html2canvas bundled, ~65KB gzipped) — `window.HyperShader`
- ESM + CJS + TypeScript declarations
- tsup build following `@hyperframes/player` conventions

## Test plan
- [ ] `bun run build` succeeds (includes shader-transitions)
- [ ] `bunx oxlint packages/shader-transitions/src/` — 0 errors
- [ ] Create a composition using `HyperShader.init()` — verify transitions fire, DOM animations play, accent colors match
- [ ] Test graceful degradation: composition works without WebGL (no transitions, no crash)
- [ ] Verify pause/resume: scrub to transition boundary — no jump in progress

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 18:44:00 -07:00
Vance Ingalls 5de2af5bde feat(skills): improve hyperframes composition quality rules (#250)
## Summary

Overhaul the hyperframes composition skill based on 26 eval rounds (~100 generated compositions). The goal: prevent known AI design tells and composition bugs while giving the LLM maximum creative freedom.

### Typography (`fonts.md` → `typography.md`)
- Two-tier banned font list (32 fonts): tier 1 bans training-data defaults, tier 2 bans the reflex replacements
- Font discovery script: queries Google Fonts API, 5 dynamic categories, top 5 randomized per run
- Selection philosophy: register-first thinking, cross-check assumptions

### Google Fonts on-demand (`deterministicFonts.ts`)
- Any Google Font works without pre-bundling — compiler fetches woff2 at compile time
- Cached to `~/.cache/hyperframes/fonts/<slug>/<weight>-<style>.woff2`
- Parallel woff2 fetches via `Promise.allSettled` (was sequential)
- Single `mkdirSync({ recursive: true })` per family (was `existsSync` x11)
- Skip redundant `readFileSync` when buffer is already in memory from fetch

### Layout rules (`SKILL.md`)
- Flexbox with gap for content text — prevents overlap from absolute positioning
- `position: absolute` reserved for decoratives only
- Cards/containers explicitly banned

### Background layer (`house-style.md`)
- 3-5 persistent decorative elements per scene (glows, ghost text, accent lines)
- All decoratives MUST have ambient GSAP animation — static decoratives banned
- WRONG/RIGHT code examples

### Transition rules (`SKILL.md`)
- Always use transitions, always entrance animations, exit animations banned except final scene
- WRONG/RIGHT code examples showing banned exit patterns

### Other
- Flash cut transition removed
- CLAUDE.md: `bun install` / `bun run build` / `bun run test` (was pnpm)
- house-style.md trimmed from 184 to ~80 lines
- SKILL.md trimmed from 364 to ~230 lines

## Test plan
- [ ] `bun install` succeeds, workspace links resolve
- [ ] `bun run build` succeeds
- [ ] `npx hyperframes lint` passes on existing compositions
- [ ] Generate a composition with `/hyperframes` skill — verify flexbox, background decoratives with animation, entrance-only animations, no banned fonts
- [ ] Verify Google Fonts on-demand: use a non-bundled font, run `npx hyperframes preview`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-13 18:40:57 -07:00
Miguel Ángel a9428e0b02 feat(studio): format info tooltip on export selector (#257)
## Summary

- Adds a hover tooltip (?) next to the format dropdown in the render queue export bar
- Shows the selected format's details (codec, use case) plus a comparison with the other two formats
- Helps users pick between MP4 (general), MOV/ProRes 4444 (transparent video for editors), and WebM/VP9 (transparent for web)

## Test plan

- [x] Open studio, go to the render queue panel
- [x] Hover over the (?) icon next to the format dropdown — tooltip appears above
- [x] Switch format in the dropdown — tooltip content updates to show the selected format first
- [x] Move pointer away — tooltip dismisses
- [x] Verify tooltip doesn't clip or overflow the panel

<img width="420" height="263" alt="image" src="https://github.com/user-attachments/assets/f4bd8bf7-65ed-45ab-ac53-577b4985fa34" />
2026-04-14 03:37:40 +02:00
Miguel Ángel 58ddb11bc5 chore: release v0.3.0 (#249)
## Summary

Coordinated minor bump across all published packages. No source changes in this PR itself; it is the version stamp for everything that landed on main since v0.2.5.

## Version bumps

| Package | from | to |
|---|---|---|
| `@hyperframes/cli` | 0.2.5 | **0.3.0** |
| `@hyperframes/core` | 0.2.5 | **0.3.0** |
| `@hyperframes/engine` | 0.2.5 | **0.3.0** |
| `@hyperframes/player` | 0.2.7 | **0.3.0** |
| `@hyperframes/producer` | 0.2.5 | **0.3.0** |
| `@hyperframes/studio` | 0.2.9 | **0.3.0** |

Between 0.2.5 and this release, `player` and `studio` received several patch versions on npm as we iterated on the bundler, entry point, and SSR issues. 0.3.0 collapses that into a single coordinated minor so the ecosystem is aligned again.

## What is in v0.3.0

### `@hyperframes/player`

**Restored package entry points to the compiled `dist/` output.** 0.2.5 shipped with `"main": "./src/hyperframes-player.ts"` but the published tarball only included `dist/` via the `"files"` field. Every consumer trying to import the package failed with `Module not found: Can't resolve '@hyperframes/player'`. Entry points now point at the built JS/`.d.ts` files inside `dist/`.

**DOM-based root timeline resolution in the ready probe.** In a bundled preview, `window.__timelines` contains the master composition alongside its sub-compositions, for example:

```js
{
  main: GSAPTimeline(14s),
  intro: GSAPTimeline(1.5s),
  'scene2-4-canvas': GSAPTimeline(12.6s),
  'scene5-logo-outro': GSAPTimeline(3.2s),
}
```

The probe used to select the adapter with `keys[keys.length - 1]`. Object key ordering meant the last-registered sub-composition would win, so the `ready` event reported a sub-composition's duration (e.g. 3.2s) instead of the master's 14s. The probe now looks up the root composition id from the outermost `[data-composition-id]` element in the iframe DOM and uses its key. Falls back to the last key when no element is present, so standalone sub-composition previews keep working.

### `@hyperframes/studio`

**`useTimelinePlayer.getAdapter()` uses the same DOM-based root id lookup** as the player. Previously play, pause, seek, and duration readout were all driven by whichever sub-composition happened to register its timeline last.

**`Player.tsx` loads `@hyperframes/player` lazily.** The component used to call `import "@hyperframes/player"` at module scope, which runs the package's `customElements.define(...)` side effect during module evaluation. `HTMLElement` does not exist in the Node runtime, so any consumer page that transitively imported the studio during server rendering threw:

```
ReferenceError: HTMLElement is not defined
  at module evaluation (@hyperframes/studio/src/player/components/Player.tsx)
```

The import now runs inside the mount effect via `import(...)` so it only evaluates in the browser. Added a cancellation flag and deferred cleanup so a fast unmount before the dynamic import resolves does not leak listeners or DOM nodes.

**Captions module imports stripped of `.js` extensions.** Files under `src/captions/` imported siblings as `./types.js` and `./parser.js`. That is legal ESM TypeScript but Turbopack and several other bundlers refuse to resolve those specifiers against `.ts` files inside `node_modules`, breaking any consumer build that transitively pulled in the captions module. Captions now uses extensionless imports, matching the rest of the studio codebase.

### `@hyperframes/core`, `@hyperframes/cli`, `@hyperframes/engine`, `@hyperframes/producer`

Version bump only, no source changes since 0.2.5. Kept on the same version so the ecosystem is easier to reason about.

## Impact for consumers

If you use `@hyperframes/studio` in a Next.js app:
- The play button in a bundled preview reports the correct composition duration and drives the master timeline.
- The session page no longer 500s in dev mode when the studio barrel is imported (the SSR fix).
- Turbopack builds that transitively load the captions module no longer fail on `Cannot resolve './types.js'`.

If you use `@hyperframes/player` directly:
- Consumer bundlers can resolve the package again (dist entry points restored).
- The `ready` event duration reports the master, not a sub-composition.

## After merge

Publish each package to npm with `pnpm publish` (workspace deps auto-resolve).
2026-04-14 01:16:06 +02:00
Miguel Ángel bf0d698858 fix(studio): SSR-safe player load, captions import cleanup (#248)
* fix(studio): load @hyperframes/player lazily to support SSR

Player.tsx had a bare `import "@hyperframes/player"` at module scope. The
player package registers a class that extends HTMLElement as a side effect,
and HTMLElement doesn't exist in a Node server runtime. Any consumer that
imported from @hyperframes/studio during server-side rendering (e.g. the
Next.js App Router evaluating a client component for SSR) threw
`HTMLElement is not defined`.

Move the import inside the mount effect via dynamic `import(...)` so it
only runs in the browser, and wire up a cancellation flag and deferred
cleanup so a fast unmount doesn't leak listeners or DOM nodes.

* fix(studio): remove .js extensions from captions-internal imports

The captions module imported sibling files as `./types.js` and
`./parser.js`. That's legal ESM TypeScript, but Turbopack (and other
bundlers) refuse to resolve those specifiers against .ts files when the
package is consumed from node_modules — the rest of @hyperframes/studio
uses extensionless imports for that reason.

Align captions with the rest of the codebase so the package builds
without bundler-specific configuration in consumers.

* chore: release @hyperframes/player@0.2.7 and @hyperframes/studio@0.2.9

Ships the root-timeline resolution fix (#247), the SSR-safe player load,
and the captions import cleanup.
2026-04-14 00:15:16 +02:00
Miguel Ángel f40447f2e8 fix(player,studio): resolve root timeline from DOM instead of last key (#247)
Bundled previews register a master composition alongside its sub-compositions
in `window.__timelines`, e.g. { main, intro, scene2, scene5 }. Both the
player's probe and studio's getAdapter() were using `keys[keys.length - 1]`
to pick the adapter, which returned whichever timeline was registered last.

That made the player report the final sub-composition's duration as the
video length (e.g. 3.2s instead of the master's 14s) and play/pause/seek
targeted that sub-composition instead of the full composition.

Look up the outermost `[data-composition-id]` element in the iframe DOM
and use its id to select the right timeline. Falls back to last-key when
no element is present (standalone sub-composition previews) so drill-down
views keep working.

Also restores `main`/`import` entry points on @hyperframes/player to
point at compiled dist output (the src/ paths broke workspace consumers
that only receive the published tarball).
2026-04-13 23:15:08 +02:00
Miguel Ángel 1dd898786c chore: release v0.2.5 (#246) 2026-04-13 17:55:40 +02:00
Miguel Ángel 1149602bc9 fix(studio): support web-component refs in useTimelinePlayer (#245)
* fix(studio): support web-component refs in useTimelinePlayer

The studio's `useTimelinePlayer` hook returns an `iframeRef` that
consumers attach to an `<iframe>` element. When consumers wrap the
iframe in a custom element (e.g. `<hyperframes-player>`) that puts
the iframe inside its shadow DOM, every `iframeRef.current.contentWindow`
access returned `null` and `getAdapter()` silently failed — meaning
timeline seek, play, pause, and `refreshPlayer` all became no-ops.

Changes:
- Add `resolveIframe(el)` helper that returns the underlying iframe
  whether the host is the iframe itself, a custom element with a
  shadow-DOM iframe, or a wrapper with a descendant iframe.
- Export `resolveIframe` from the studio so consumers can pre-resolve
  the iframe before assigning it to `iframeRef`.
- Internal `useTimelinePlayer` keeps the strict `HTMLIFrameElement`
  ref type, so existing consumers attaching directly to an `<iframe>`
  are unaffected.

Also adds:
- JSDoc on the player's `iframeElement` getter.
- "Advanced: iframe access" docs section in `packages/player/README.md`
  and `docs/packages/player.mdx`.
- Type-safety lint rules in `.oxlintrc.json` and a "Type-safety
  conventions" section in `CONTRIBUTING.md`.

Backward compatible — App.tsx and NLELayout.tsx continue to work
unchanged.

* chore(lint): defer no-explicit-any rule; it broke existing codebase

The new rules added 37 errors across 32 existing files — mostly
legitimate `window as any` casts at browser-global and test-mock
boundaries. Enabling them without fixing all violations breaks CI.

Revert the `.oxlintrc.json` additions and soften the CONTRIBUTING.md
wording to describe the convention without claiming lint enforcement
(that enforcement will come in a follow-up PR that fixes all sites).
2026-04-13 17:53:01 +02:00
James Russo 18de86e4bd fix(player): handle Infinity duration; add lint rules for data-duration and Math.ceil overshoot (#243)
* fix(player): handle Infinity duration from runtime gracefully

When compositions have repeating animations without data-duration, the
runtime sends durationInFrames: Infinity. The player now ignores
non-finite duration values instead of displaying "Infinity:NaN" in the
controls. formatTime also returns "0:00" for non-finite inputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(lint): add data-duration and Math.ceil overshoot rules

- Add root_composition_missing_data_duration warning when the root
  composition element is missing data-duration, which causes the runtime
  to infer Infinity for loop-inflated timelines.
- Add gsap_repeat_ceil_overshoot warning that catches
  repeat: Math.ceil(d/c)-1 patterns which overshoot the intended
  duration. Recommends Math.floor instead.
- Fix gsap_infinite_repeat fixHint to suggest Math.floor (not Math.ceil).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(player): wait for injected runtime before declaring ready

When the player auto-injects the runtime script (because the
composition has GSAP timelines but no runtime), it would immediately
declare ready on the next probe cycle — before the runtime script
finished loading from CDN. This caused play() to send a postMessage
that nobody received, making autoplay silently fail.

Now the probe waits for the runtime bridge (__hf or __player) to
appear before proceeding to the ready state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 10:41:13 -07:00
Miguel Ángel 794b02153d fix(lint): upgrade bare composition HTML to error (#242)
## Summary

- Upgrades `root_composition_missing_html_wrapper` from **warning** to **error** — a bare `<div data-composition-id>` as `index.html` without `<!DOCTYPE html>/<html>/<body>` causes browsers to quirks-mode, the preview server to fail, and the bundler to silently skip runtime injection
- Improves the error message to explain _why_ this is bad, and includes a snippet of the offending root element
- Skips `<template>`\-wrapped compositions (already caught by the separate `standalone_composition_wrapped_in_template` rule)
- Adds 8 tests covering the exact screenshot scenario, proper HTML, sub-compositions, plain HTML, and template wrappers

## Test plan

- [x] All 441 existing tests pass (`vitest run`)
- [x] 8 new tests for `root_composition_missing_html_wrapper` and `standalone_composition_wrapped_in_template`
- [x] TypeScript build clean (`tsc --noEmit`)
- [x] oxlint + oxfmt pass
- [x] Run `npx hyperframes lint` on a bare composition `index.html` and verify it now reports an error
2026-04-11 05:00:47 +02:00
Miguel Ángel 0da93cea3d feat(player): add speed control with popup menu and CSS theming (#241)
Add playback speed control to the player controls bar:
- Popup menu with logarithmic presets (0.25x-4x)
- Custom presets via speed-presets attribute
- Full CSS custom property theming (--hfp-accent, --hfp-controls-bg, etc.)
- ratechange event dispatch
- Exports: SPEED_PRESETS, formatSpeed, ControlsOptions
- Fix package.json export condition ordering
2026-04-10 20:47:12 +02:00
James Russo 9a3ed569a0 docs(cli): add tts command to --help groups, CLI docs, and CLAUDE.md checklist (#240)
The tts command was implemented (PR #201) but never added to the root-level
help display or documentation. This adds it to:

- help.ts GROUPS (AI & Integrations) so it appears in `hyperframes --help`
- docs/packages/cli.mdx with usage examples and flag reference
- CLAUDE.md "Adding CLI Commands" checklist: new steps 4-5 require adding
  commands to help.ts groups and docs, preventing future omissions

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 09:12:56 -07:00
Miguel Ángel 078ed7d5cd fix(deps): patch security vulnerabilities in pretext and vite (#237)
## Summary

- Bump `@chenglou/pretext` ^0.0.3 → ^0.0.5 in `packages/core` — fixes **high-severity** algorithmic complexity DoS ([Dependabot #3](https://github.com/heygen-com/hyperframes/security/dependabot/3))
- Bump `vite` ^5.0.0 → ^6.4.2 in `packages/studio` — fixes **medium-severity** path traversal in optimized deps `.map` handling ([Dependabot #2](https://github.com/heygen-com/hyperframes/security/dependabot/2))

## Test plan

- [x] `pnpm --filter @hyperframes/core build` — passes
- [x] `pnpm --filter @hyperframes/studio build` — passes (vite 6.4.2, 4631 modules, 3.85s)
- [x] `@vitejs/plugin-react@^4.0.0` supports vite 6 (`peerDependencies: vite ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0`)
2026-04-10 03:09:17 +02:00
Miguel Ángel 3482441c9f feat(studio): use @hyperframes/player web component for preview (#238)
## Summary

- **Replaces the studio's hand-rolled iframe + scaling in** **`Player.tsx`** with the `<hyperframes-player>` web component, eliminating duplicated ResizeObserver, dimension detection, and stage-size message handling
- **Adds a public** **`iframeElement`** **getter** to the player web component so the studio's `useTimelinePlayer` can still access the inner iframe for clip manifest parsing, timeline probing, and DOM inspection
- **Updates player package exports** to resolve from source for workspace consumers (matching `@hyperframes/core` pattern), while npm-published consumers still get built `dist/` files

### Why a separate player package?

1. **Zero dependencies, any framework** — 12KB vanilla web component vs 940KB React+Zustand+CodeMirror studio
2. **CDN-ready** — single `<script>` tag, no build pipeline needed
3. **Embeddable by third parties** — users embed compositions in their own sites without the studio
4. **Single source of truth** — studio now uses the player instead of duplicating its scaling/detection logic

## Test plan

- [x] `pnpm --filter @hyperframes/player typecheck` passes
- [x] `pnpm --filter @hyperframes/studio typecheck` passes
- [x] `pnpm --filter @hyperframes/studio build` passes
- [x] `pnpm --filter @hyperframes/studio test` passes (2 pre-existing failures, unrelated)
- [x] E2E: Standalone player loads composition, detects 4s GSAP timeline, controls work, play/pause works
- [x] E2E: Studio preview renders via `<hyperframes-player>`, `iframeElement` bridge works, playback controls sync correctly
2026-04-10 03:00:54 +02:00
Miguel Ángel 7e7d41f833 docs(player): add README, bump to v0.2.4 (#236)
## Summary

- Add comprehensive README for `@hyperframes/player` covering installation, usage, full API reference (attributes, properties, methods, events), sizing, and distribution formats
- Bump version from 0.2.2 to 0.2.4 to align with monorepo release

## Test plan

- [x] Verify README renders correctly on GitHub
- [x] Confirm package.json version matches monorepo (0.2.4)
2026-04-10 01:57:52 +02:00
Miguel Ángel dce2c6ee14 chore: release v0.2.4
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 23:44:08 +00:00
Miguel Ángel 78de791392 fix(studio): render in-process, remove producer server dependency (#235)
## Summary
- The Vite dev server proxied studio renders to a separate producer server (port 9847) that needed to be started manually
- When the producer wasn't running, renders silently failed — red dot, no error message, no way to know what went wrong
- Replaced the proxy with direct in-process rendering via `@hyperframes/producer` — same code path as the CLI and embedded preview mode
- Removed ~70 lines of SSE proxy streaming code, replaced with the same ~20-line in-process pattern used everywhere else

## DX improvement
**Before:** `pnpm dev` + `npx tsx packages/producer/src/public-server.ts` (two terminals, easy to forget)
**After:** `pnpm dev` (renders work immediately)

## Testing
Verified manually: open studio via `pnpm dev`, navigate to a project, click Export — renders complete with live progress updates, no separate server needed.
2026-04-10 01:41:18 +02:00
Miguel Ángel ceb54811c6 fix(runtime): preload media on init to prevent broken first-play audio (#234)
## Summary
- Audio (and video) sounds broken/choppy on first play in the studio preview, but works fine on second play
- Root cause: `<audio>` elements default to `preload="metadata"`, which only fetches enough data to determine duration — not enough for smooth playback. When `el.play()` fires, the browser hasn't buffered the audio data yet
- The runtime now eagerly sets `preload="auto"` and calls `load()` during init, ensuring media is fully buffered before the user clicks play
- `syncRuntimeMedia` now defers `play()` on unbuffered media by registering a `canplay` listener, instead of silently swallowing the failure

## Testing
Verified with agent-browser against a 26s narration composition (soulscape-film):

```
# After fix — audio element state at init:
preload: "auto"
readyState: 4 (HAVE_ENOUGH_DATA)
buffered: 26.07s (entire file)
duration: 26.07s
```

Audio is fully buffered before any play attempt, so first-play works identically to subsequent plays.

## Files changed
- `packages/core/src/runtime/init.ts` — set `preload="auto"` + `load()` in `bindMediaMetadataListeners`
- `packages/core/src/runtime/media.ts` — defer `play()` on unbuffered media via `canplay` listener
- `packages/core/src/runtime/media.test.ts` — updated test + added unbuffered media test case
2026-04-10 00:05:32 +02:00
James Russo 9115d7364c feat(producer): add request-level render concurrency semaphore (#232)
Add a FIFO semaphore to limit concurrent renders in the producer server,
preventing Chrome CPU contention that causes beginFrame failures.

- New Semaphore utility class (packages/producer/src/utils/semaphore.ts)
- Both blocking render and SSE renderStream handlers acquire/release the semaphore
- SSE stream sends a "queued" event when request must wait
- New GET /render/queue endpoint exposes active/queued render counts
- Configurable via HandlerOptions.maxConcurrentRenders or PRODUCER_MAX_CONCURRENT_RENDERS env var (default: 2)
- New --max-concurrent-renders CLI flag (1-10)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:32 -07:00
Miguel Ángel 6f04983e20 fix(engine): retry beginFrame on parallel render contention (#230)
## Summary
- When 2-3 renders run in parallel on Linux (beginFrame mode), Chrome's `HeadlessExperimental.beginFrame` fails with "Another frame is pending" due to CPU contention
- Extracts `sendBeginFrame` helper with exponential backoff retry (50ms–800ms, 5 attempts) — used by both the main capture path and the hasDamage=false fallback
- After retries exhaust, throws an actionable error instead of a raw protocol error

## Testing

### Environment
- Linux (Ubuntu 20.04), 8 cores
- `chrome-headless-shell` 146.0.7680.153 (beginFrame mode active)
- Test composition: 1920×1080, 5s duration, 30fps, 150 frames, 3 GSAP-animated elements

### Before fix (main)
Ran 3 parallel renders of the same composition simultaneously:

| Render | Result | Details |
|--------|--------|---------|
| R1 | Completed | 304 KB, 6.6s |
| R2 | **FAILED** | `Protocol error (HeadlessExperimental.beginFrame): Another frame is pending` at frame 120/150 |
| R3 | Completed | 304 KB, 6.7s |

The error is non-deterministic — it hits whichever worker loses the CDP frame contention race under CPU pressure.

### After fix (this branch)
Same 3 parallel renders:

| Render | Result | Details |
|--------|--------|---------|
| R1 | Completed | 304 KB, 7.3s |
| R2 | Completed | 304 KB, 7.3s |
| R3 | Completed | 304 KB, 7.3s |

All 3 succeeded. The slight increase in wall time (6.6s → 7.3s) is consistent with occasional retries absorbing transient contention without failing.

### Code review
- Both `beginFrame` call sites in `beginFrameCapture` (main capture path + hasDamage=false fallback) use the shared `sendBeginFrame` helper
- Backoff ceiling is 1.55s per frame (50+100+200+400+800ms), acceptable for transient contention
- beginFrame mode is Linux-only (`chrome-headless-shell` + `--enable-begin-frame-control`); macOS uses screenshot mode so the retry code path isn't exercised there
2026-04-09 19:46:39 +02:00
Miguel Ángel 0cf03016b2 fix(engine): resolve external asset paths from compiled dir (#231)
## Summary
- Parent-relative paths (e.g. `src="../file.wav"`) silently drop media from rendered MP4
- The compiler rewrites external paths to `hf-ext/` and copies files to the compiled directory, but both the audio mixer and video frame extractor only resolved against `projectDir` — never finding them
- Now checks `compiledDir` first (matching the file server's resolution order), then falls back to `projectDir`
- Fixes both `<audio>` and `<video>` elements with external paths

## Real-world context
Reported in Slack by Abhai — a TTS comparison video using `<audio src="../tts-voxcpm2.wav">` (audio file in parent directory, composition in subdirectory) rendered successfully but the output MP4 had no audio stream. The render completed without any error, silently dropping the audio.

## Testing

### Environment
- Linux (Ubuntu 20.04), ffmpeg 4.2
- Test composition: `subdir/index.html` with `<audio id="bg-audio" src="../test-audio.wav">`, WAV file at parent directory

### Before fix (main)
```
[AUDIO-DEBUG] element.src=hf-ext/tmp/hf-test-231/test-audio.wav
              baseDir=/tmp/hf-test-231/subdir
[AUDIO-DEBUG] resolved srcPath=/tmp/hf-test-231/subdir/hf-ext/tmp/hf-test-231/test-audio.wav
              exists=false
```

- Audio mixer tries `join(projectDir, "hf-ext/...")` → file doesn't exist at that path
- Output: **9.8 KB, video stream only** (confirmed via ffprobe)
- No error logged — audio silently dropped

### After fix (this branch)
```
[AUDIO-DEBUG] element.src=hf-ext/tmp/hf-test-231/test-audio.wav
              baseDir=/tmp/hf-test-231/subdir
              compiledDir=/tmp/.../compiled
[AUDIO-DEBUG] fromCompiled=/tmp/.../compiled/hf-ext/tmp/hf-test-231/test-audio.wav
              exists=true
[AUDIO-DEBUG] resolved srcPath=/tmp/.../compiled/hf-ext/tmp/hf-test-231/test-audio.wav
              exists=true
[AUDIO-RESULT] success=true, hasAudio=true
```

- Audio mixer checks `join(compiledDir, "hf-ext/...")` first → file found
- Output: **44.6 KB, video + audio streams** (confirmed via ffprobe)

### ffprobe comparison

| Branch | File size | Streams |
|--------|-----------|---------|
| `main` | 9.8 KB | `video (h264)` only |
| `fix` | 44.6 KB | `video (h264)` + `audio (aac)` |

### Path resolution flow
1. Compiler sees `<audio src="../test-audio.wav">`
2. Compiler resolves to absolute path, maps it to `hf-ext/tmp/.../test-audio.wav`
3. Compiler copies file to `compiled/hf-ext/tmp/.../test-audio.wav`
4. Audio mixer gets `element.src = "hf-ext/tmp/.../test-audio.wav"`
5. **main**: tries `join(projectDir, src)` → not found → silent drop
6. **fix**: tries `join(compiledDir, src)` first → found → audio mixed in

### Repro
```bash
mkdir -p /tmp/test/subdir
ffmpeg -f lavfi -i "sine=frequency=440:duration=2" /tmp/test/test-audio.wav -y
# Create subdir/index.html with <audio src="../test-audio.wav" ...>
cd /tmp/test/subdir && npx hyperframes render
ffprobe -v error -show_streams output.mp4  # video only on main, video+audio on fix
```
2026-04-09 19:05:28 +02:00
Vance Ingalls 4c5b8e38a1 feat(skills): add typography and motion principles, fix validate $& bug (#228)
Add two new skill reference files that address measured LLM composition failures:

- fonts.md: Typography principles — banned fonts, guardrails for violations
  (pairing two sans-serifs, defaulting to 400/700 weight), and guidance the LLM
  genuinely doesn't apply without being told (register switching, tension as
  meaning, easing direction as emotion). Includes Google Fonts API discovery
  script with 7-category multi-strategy query.

- motion-principles.md: Motion design principles — guardrails for same-ease and
  same-speed defaults, y-axis entrance monotony, and guidance for build/breathe/
  resolve scene structure, hard cuts as intentional transitions, visual
  composition rules for video-not-web density.

Both files validated against baseline evals: 3 compositions created without
guidance confirmed the LLM reaches for banned fonts (Inter, Cormorant Garamond,
Playfair Display, Roboto Condensed), uses power2.out on 45-72% of tweens,
enters 80%+ of elements from y-axis, and pairs multiple sans-serifs.

Also:
- Fix validate.ts $& replacement bug (runtime source containing $& caused
  String.prototype.replace to re-insert the matched <script src=""> tag)
- Clean up font loading guidance across skills (compiler embeds automatically)
- Update house-style.md to reference fonts.md

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:49:46 -07:00
Vance Ingalls fc973ee2e8 feat(lint): add rules for missing data-start, template wrapper, and DOCTYPE
Three new lint rules that catch structural issues causing compositions
to fail silently in preview:

- root_composition_missing_data_start: Root composition needs data-start="0"
  for the runtime to begin playback
- standalone_composition_wrapped_in_template: index.html should not be
  wrapped in <template> (only sub-compositions use that)
- root_composition_missing_html_wrapper: index.html needs <!DOCTYPE html>
  and <html> wrapper for the bundler

Also adds rawSource to LintContext so rules can inspect pre-template-stripped
HTML, and isSubComposition to linter options so rules can distinguish root
from sub-composition files.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:45:25 -07:00
James b33bbfa0f9 chore: release v0.2.3 2026-04-08 18:19:07 +00:00