Commit Graph
4089 Commits
Author SHA1 Message Date
Miguel Ángel 7f4e4333e5 fix(cli): read actual composition dimensions in info and fix semver update check (#194)
## Summary

- **`info`** **command** now reads actual `data-width`/`data-height` from the root composition element instead of hardcoding 1920x1080 or 1080x1920 based on a parser heuristic that defaults to "portrait"
- **Update check** now uses proper semver comparison instead of string inequality (`!== VERSION`). Previously reported "update available" when installed `0.2.1` and npm had `0.2.0`

**Part 2 of 5** in a stacked PR series fixing E2E test findings.

## Test plan

- [x] `npx hyperframes info` on a 1920x1080 project shows "1920x1080" (not "1080x1920")
- [x] `npx hyperframes info --json` returns correct width/height fields
- [x] Version check no longer reports downgrade as available update
2026-04-02 14:12:48 -07:00
James RussoandClaude Opus 4.6 5e8ff36675 refactor(cli): colocate --help examples in command files (#202)
Move per-command examples from the centralized `help.ts` record into
each command file as `export const examples: Example[]`. help.ts now
dynamically imports them at --help time.

This means adding a new command and its examples happens in one file
instead of two, reducing the chance of forgetting examples.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 14:11:38 -07:00
Miguel Ángel 4bb01fd1b6 feat(lint): add rules for non-deterministic code, missing clip class, overlapping tracks, and rAF detection (#193)
## Summary

Adds 4 new lint rules found missing during comprehensive E2E testing:

- **`non_deterministic_code`** (error) — detects `Math.random()`, `Date.now()`, `new Date()`, `performance.now()`, `crypto.getRandomValues()` in scripts. Confirmed: two renders with Math.random() produced different checksums.
- **`timed_element_missing_clip_class`** (warning) — flags elements with `data-start`/`data-duration` but no `class="clip"`. Without it, elements are visible forever instead of only during their time window.
- **`overlapping_clips_same_track`** (error) — detects clips on the same `data-track-index` with overlapping time ranges.
- **`requestanimationframe_in_composition`** (warning) — warns that rAF-based animations don't sync with frame capture. Discovered when Vivus.js (rAF-based) produced incorrect output.

12 new tests, all passing.

**Part 1 of 5** in a stacked PR series fixing E2E test findings.

## Test plan

- [x] 12 new tests (3 per rule: positive, negative, edge case)
- [x] Full suite: 56 pass in rule tests
- [x] `npx tsx scripts/lint-skills.ts` — no issues
2026-04-02 14:09:50 -07:00
James RussoandClaude Opus 4.6 7389c0c89b feat(cli): add tts command for local text-to-speech via Kokoro-82M (#201)
* feat(cli): add `tts` command for local text-to-speech via Kokoro-82M

Adds `hyperframes tts` — generate speech audio locally using Kokoro-82M
(ONNX), no API key needed. Mirrors the transcribe command architecture.

- New command: `hyperframes tts "text" --voice af_heart --output speech.wav`
- 54 voices across 8 languages, ~5x realtime on CPU
- Auto-downloads model (~311 MB) + voices (~27 MB) to ~/.cache/hyperframes/tts/
- Requires Python 3.8+ with kokoro-onnx installed
- Extracted shared `downloadFile` utility from whisper/manager.ts with
  atomic .tmp→rename to prevent partial download corruption
- Added hyperframes-tts skill with voice selection guide
- Updated CLAUDE.md with TTS docs, voice table, and skill reference

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

* docs(tts): improve skill per skill-creator guidelines

- Move trigger info from body to frontmatter description
- Remove `trigger` field (not a valid frontmatter field)
- Remove CLI flag docs Claude can derive from --help
- Remove redundant voice tables (keep content-to-voice mapping)
- Fix composition audio example to use actual <audio> element pattern
- Keep non-obvious workflows: TTS+transcribe for captions, long scripts

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

* docs(tts): add guidance for using external TTS sources

Help users understand when to use cloud TTS (voice cloning, broader
languages, higher quality) vs the built-in Kokoro model, and how
external audio integrates into the same composition workflow.

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

* docs(tts): prioritize HeyGen API as recommended cloud TTS

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

* docs(tts): remove external TTS section for now

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

* fix(tts): set required: false on input arg so --list works standalone

Citty treats positional args as required by default unless explicitly
set to required: false. Without this, `hyperframes tts --list` fails
with "Missing required positional argument".

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

* feat(tts): add --help examples and fix required:false for --list

Add examples section to `tts --help` matching the pattern from other
commands (transcribe, render, etc.). Fix citty positional arg requiring
explicit `required: false` for --list to work standalone.

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

* docs: add CLI command checklist to CLAUDE.md

Ensure new commands always get --help examples in help.ts.

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-02 14:09:25 -07:00
Vance Ingalls cb0b17062a feat(skills): add marker-highlight skill for animated text highlighting (#190)
## Summary

- **New skill:** **`marker-highlight`** — integrates [MarkerHighlight.js](https://github.com/Robincodes-Sandbox/marker-highlight) into HyperFrames compositions. Canvas-based animated text highlighting with 5 drawing modes: marker pen, circle, burst, scribble, and sketchout.
- **Studio fix:** added missing `captionSync` to useEffect dependency array (oxlint exhaustive-deps)
- **Studio fix:** `loadOverrides` now checks `res.ok` before parsing, preventing 404 console noise on projects without captions

## Skill details

The skill documents the non-obvious GSAP integration pattern discovered during development:

1. **One highlighter per container** — the library clears ALL `.highlight` divs from the shared parent on init, so multiple instances on sibling marks conflict
2. **`data-color`** **\+** **`data-original-bgcolor`** — prevents the CSS background-color flash that occurs when the library reads and clears the mark's background
3. **Canvas pre-draw + clear + reanimate** — `animate: false` pre-draws statically, canvases are hidden, then cleared and shown with `reanimateMark()` at trigger time for clean animated reveals
4. **`onReverseComplete`** **for rewind** — hides highlight divs when the timeline seeks backward past the trigger point

## Test plan

- [ ] `npx hyperframes lint` passes on test-composition
- [ ] Studio preview shows marker highlight on "something" at 1s, circle on "love" at 2.2s
- [ ] Rewind past trigger points hides highlights
- [ ] No 404 console errors for caption-overrides.json on non-caption projects



[Screen Recording 2026-04-02 at 1.56.30 AM.mov <span class="graphite__hidden">(uploaded via Graphite)</span> <img class="graphite__hidden" src="https://app.graphite.com/user-attachments/thumbnails/53b03f4e-538e-477a-b738-7a033b99a84e.mov" />](https://app.graphite.com/user-attachments/video/53b03f4e-538e-477a-b738-7a033b99a84e.mov)



🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-02 13:48:24 -07:00
Vance IngallsandClaude Opus 4.6 5e2781b459 fix(studio): address caption designer PR feedback (#200)
* fix(studio): address caption designer PR feedback

Fixes from review comments on feature/caption-designer (#180):

- fix(generator): guard named colors in hexToRgba — "red", "transparent"
  no longer produce NaN rgba values
- fix(sync): log auto-save failures instead of silently swallowing them
- fix(sync): check res.ok before parsing caption-overrides response
- refactor(components): extract Section, Row, inputCls into shared.tsx
  to eliminate duplication between CaptionPropertyPanel and
  CaptionAnimationPanel
- fix(store): replace non-deterministic Date.now()+Math.random() ID with
  counter-based group IDs
- fix(store): read selectedGroupId from state param instead of get() to
  avoid stale reads in batched set() calls
- fix(overlay): remove cssScale multiplier from getBoundingClientRect
  coords — the browser already accounts for CSS transforms
- docs(parser): add comment explaining the lazy ];  regex assumption

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

* fix(studio): address remaining caption designer feedback

Overlay: handle both per-word spans (generator output) and grouped text
nodes (existing templates). Wraps text nodes into individual spans on
demand so the overlay can target words in any caption format.

Property panel: add Typography (font, size, weight, spacing) and Color
(color, active, dim, opacity) sections alongside existing Position and
Transform controls.

Timeline: move caption timeline into a dedicated flex-shrink-0 section
below the main timeline tracks instead of inside the scrollable area.
Gives it fixed 60px height that's always visible.

Caption overrides: classify color tweens by comparing target color to
the dim baseline instead of relying on timeline position order. This
handles compositions with custom color tweens correctly.

App.tsx: remove polling interval, rely on runtime postMessage events
for caption detection. Add clarifying comment on why useEffect is
appropriate (external event subscription).

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

* fix(studio): restore cssScale in overlay coordinate conversion

getBoundingClientRect() on iframe-internal elements returns coordinates
in the iframe's native resolution (1920x1080), not the CSS-scaled
display size. The cssScale multiplier is needed to convert to parent
window coordinates. The earlier removal was incorrect — it only worked
at 1:1 scale.

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

* fix(studio): fix reversed scaling on left-side corner handles

Scale interaction used horizontal dx from start position, which goes
negative when dragging left handles outward. Now uses distance from box
center — dragging away from center increases scale regardless of which
corner handle is used.

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

* fix(studio): make rotation respond to horizontal drag only

Rotation handle sits directly above the word, so atan2-based rotation
barely responds to left/right movement. Replace with linear horizontal
mapping: drag right = clockwise, drag left = counter-clockwise,
200px = 90 degrees. Vertical movement is ignored.

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

* fix(studio): remove animation tab and typography/color from property panel

Keep only Position (X, Y) and Transform (Scale, Rotation) controls.
Remove tab switcher UI since there's only one view now.

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

* style: fix oxfmt formatting in CLAUDE.md and captions skill docs

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-02 11:38:35 -07:00
Vance Ingalls d36c1785b9 feat(captions): energy-based technique selection and mandatory quality checks (#176)
## Summary

- Rewrite script-to-style mapping as an energy detection table (high → low) with mandatory animation requirements: karaoke baseline, 2+ highlight techniques, kinetic exits
- Replace `tl.call()` per-frame audio-reactive pattern with group-level GSAP tweens — read peak bass/treble for each group's time range and modulate entrance intensity at build time, no per-frame callbacks needed
- Add transcript quality check with automatic retry rules (>20% music tokens = retry with larger model)
- Add caption word structure lint rule (`.caption-group` + `<span>`) for studio editor compatibility
- Add multilingual model guidance and decision tree for model selection

## Test plan

- [ ] Skill files render correctly as markdown
- [ ] Cross-references between SKILL.md, dynamic-techniques.md, and transcript-guide.md resolve correctly
- [ ] `dynamic-techniques.md` audio-reactive section uses `tl.to()`/`tl.set()` only, no `tl.call()` loops

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-02 00:47:43 -07:00
Vance Ingalls ad2d63db32 feat(cli): skill install targets + remove custom install in favor of vercel-labs/skills (#177)
## Summary

**Skill install targets (original):**
- Add project-level skill install targets: Windsurf, Cline, Roo Code, Trae (opt-in via flag)
- Split install logic into global vs project-level
- Fix lint false positive: timed tags with `data-composition-id` no longer flagged by media rule

**Skill system cleanup (folded from #189):**
- Delete `install-skills.ts` (~485 lines) — remove custom installation wrapper entirely
- Strip skill logic from `init` — no more project-level `.claude/skills/` copies, no `--skip-skills` flag; replaced with post-scaffold message: `npx skills add heygen-com/hyperframes`
- Front-load SKILL.md trigger words — all 5 skill descriptions rewritten so activation language comes first (~150 chars)
- Update CLAUDE.md — install instructions now point to [vercel-labs/skills](https://github.com/vercel-labs/skills)
- Fix `.claude/settings.json` — pre-commit hook changed from `pnpm` to `bun`

## Test plan

- [ ] `npx hyperframes skills` → "Unknown command skills"
- [ ] `npx hyperframes init test --template blank --non-interactive --skip-transcribe` → prints `npx skills add heygen-com/hyperframes`
- [ ] `grep -r "install-skills" packages/cli/src/` → no results
- [ ] All 5 `skills/*/SKILL.md` have front-loaded descriptions

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-02 00:47:25 -07:00
Vance Ingalls 37404f23da feat(whisper+captions): language detection, audio-reactive captions, multilingual defaults (#175)
## Summary

**Whisper improvements:**
- Auto-detect language and switch from `.en` to multilingual model when needed
- Detect speech onset in WAV to strip hallucinated words before speech begins
- Merge whisper-cpp token fragments: contractions (`didn` + `'t` → `didn't`), split capitals (`C` + `aught` → `Caught`), dropped-g (`shin` + `in'` → `shinin'`)
- Interpolate zero-duration word clusters for reliable karaoke timing

**Captions skill updates (folded from #176):**
- Rewrite script-to-style mapping as an energy detection table (high → low) with mandatory animation requirements: karaoke baseline, 2+ highlight techniques, kinetic exits
- Replace `tl.call()` per-frame audio-reactive pattern with group-level GSAP tweens — read peak bass/treble for each group's time range and modulate entrance intensity at build time
- Add transcript quality check with automatic retry rules (>20% music tokens = retry with larger model)
- Add caption word structure lint rule (`.caption-group` + `<span>`) for studio editor compatibility

**Multilingual defaults (folded from #186):**
- Default whisper model changed from `small.en` to `small` to prevent silent translation of non-English audio
- Added non-negotiable language rule to captions skill

## Test plan

- [ ] `pnpm test` passes (contraction merging, fragment merging, zero-duration interpolation, speech onset)
- [ ] Transcribe non-English audio — verify it transcribes in original language, not translates
- [ ] Skill files render correctly, cross-references resolve
- [ ] `dynamic-techniques.md` audio-reactive section uses `tl.to()`/`tl.set()` only, no `tl.call()` loops

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-02 00:08:41 -07:00
Vance Ingalls 159a2e7113 feat(skills): add dynamic caption techniques and split captions skill into references (#173)
## Summary

- Split the captions skill from a single 611-line file into focused references: `SKILL.md` (core rules), `transcript-guide.md` (whisper/transcription), `dynamic-techniques.md` (animation patterns)
- Add `audio-reactive` skill with "Content, Not Medium" constraint — steers away from generic visualizations (equalizer bars, spectrum analyzers, waveforms) toward content-grounded animation where audio drives *when* and *how much*, not *what to show*
- Add initial dynamic caption technique selection by energy level

## Test plan

- [ ] All skill files render correctly as markdown
- [ ] Cross-references between files use correct relative paths
- [ ] `audio-reactive/SKILL.md` contains the anti-pattern list and content-grounded examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-01 23:24:25 -07:00
James 870ca76c8b chore: release v0.2.1 v0.2.1 2026-04-01 21:36:24 +00:00
Miguel Ángel 1efb23dfae fix(producer): inline CDN scripts for offline render and detect black video (#182)
## Context

`npx hyperframes render` fails to load assets and produces all-black video. Root causes:

1. CDN scripts (GSAP, Lottie) are left as `<script src="https://...">` in compiled HTML — the headless browser must fetch them over the network, which fails in Docker, CI, and firewalled environments
2. Assets referenced from outside the project directory (e.g. `../shared-assets/hero.png`) 404 because the file server only serves from `projectDir`
3. When GSAP fails to load, the timeline never registers, all `.clip` elements stay `visibility: hidden`, and every frame is black — with no diagnostic output
4. The linter reports `missing_gsap_script` when GSAP is bundled inline (no `<script src>` tag), which blocks users from working around the CDN issue

## What changed

### 1\. CDN script inlining (`htmlCompiler.ts`)

`compileForRender` now downloads all external `<script src="https://...">` tags at compile time and inlines their content into the HTML. Rendering no longer needs network access.

| Before | After |
| --- | --- |
| `<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>` stays in HTML, browser must fetch at runtime | Script downloaded during compilation, embedded as `<script>/* inlined: https://... */\n...code...</script>` |
| Docker/CI render → `net::ERR_NAME_NOT_RESOLVED` → black video | Render works fully offline |
| No feedback when CDN fails | `[Compiler] WARNING: Failed to download CDN script: ... Consider bundling it locally` |

### 2\. External asset copying (`htmlCompiler.ts`, `renderOrchestrator.ts`)

After compilation, the HTML is scanned for `src`, `href`, and CSS `url()` references that resolve outside `projectDir`. These files are copied into the compiled output directory so the file server can serve them.

| Before | After |
| --- | --- |
| `background-image: url(../shared-assets/hero.png)` → 404 (file server can't serve outside `projectDir`) | Asset detected, copied to compiled dir, path rewritten → serves correctly |
| `<img src="../shared-assets/logo.png">` → 404 | Same fix — works for all `src`/`href` attributes and CSS `url()` |

### 3\. Black video diagnostics (`renderOrchestrator.ts`)

When composition duration is 0 (which would produce a black video), the error now probes the browser for diagnostics instead of a generic message.

| Before | After |
| --- | --- |
| `Invalid composition duration: 0. Check that GSAP timelines are registered.` | `Composition duration is 0 — this would produce a black video.\n\nDiagnostics:\n  - GSAP is not loaded — CDN script may have failed to download. Bundle GSAP locally...\n  - Browser: [Browser:PAGEERROR] gsap is not defined` |
| Asset 404s during page load silently logged | `[Render] Asset load failure: ...` + `[WARN] Browser encountered network failures during page load` |

### 4\. Linter: recognize inline GSAP (`core/lint/rules/gsap.ts`)

The `missing_gsap_script` rule now recognizes GSAP bundled inline — matching the producer's inlining comment (`/* inlined: ...gsap... */`), GSAP library internals (`_gsScope`, `GreenSock`), and large inline scripts (>5KB) referencing gsap.

| Before | After |
| --- | --- |
| User inlines GSAP → linter errors with `missing_gsap_script` | Inline GSAP detected, no false error |
| Producer inlines CDN → linter errors on the compiled HTML | Producer's `/* inlined: ... */` comment recognized |

## Test plan

- [x] `pnpm build` passes
- [x] Core tests pass (410/410, +2 new)
- [x] **Reproduced baseline failures on** **`main`**: CDN scripts not inlined, external assets 404, no diagnostics
- [x] **Verified fixes**: CDN script inlined, external assets copied and served, diagnostics printed
- [x] Render with working CDN → `[Compiler] Inlined CDN script: ...` → render succeeds
- [x] Render with broken CDN → `[Compiler] WARNING: Failed to download CDN script` + browser errors surfaced
- [x] Render with assets outside project dir → `[Compiler] Found 1 asset(s) outside project directory` → assets served correctly
- [x] Linter with inline GSAP → no `missing_gsap_script` false positive
2026-04-01 23:15:00 +02:00
James RussoandClaude Opus 4.6 2d30654632 feat(cli): improve --help with grouped commands and per-command examples (#184)
* feat(cli): improve --help with grouped commands and per-command examples

Replaces citty's flat COMMANDS list with kubectl-style grouped categories
and adds examples to every subcommand. Root help now groups 14 commands
into 5 categories (Getting Started, Project, Tooling, AI & Integrations,
Settings). Per-command --help now appends a formatted Examples section
with practical usage patterns.

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

* refactor(cli): use shared colors, lazy-load help, fix description drift

- Replace hand-rolled ANSI helpers with existing ui/colors.ts (fixes
  non-conformant NO_COLOR handling)
- Add cyan and gray to shared color module
- Lazy-load help.ts via dynamic import to avoid allocating help data
  on non-help invocations
- Fix description drift: benchmark and transcribe descriptions now
  match their command meta.description
- Unify tuple order: ROOT_EXAMPLES now uses [comment, command] to
  match COMMAND_EXAMPLES
- Remove redundant comment restating type annotation

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

* fix(cli): resolve typecheck error in showUsage generic signature

The lazy-load wrapper needs to cast CommandDef<T> to CommandDef when
forwarding to the help module, since TypeScript's generic variance
makes the direct assignment incompatible.

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

* chore(cli): remove kubectl references from help comments

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-01 12:55:26 -07:00
James RussoandClaude Opus 4.6 eb94e6cf77 chore: update license from MIT to Apache 2.0 (#183)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 12:01:08 -07:00
Miguel Ángel 2213e9f7a4 refactor(lint): split monolithic test file into per-rule-module test files (#181)
## Summary

- Splits the 872-line `hyperframeLinter.test.ts` into focused per-domain test files matching the rule module structure from #170
- Tests still use the public API (`lintHyperframeHtml`) — no internals exposed
- Fixes misplaced tests: `gsap_css_transform_conflict` tests were inside `describe("lintScriptUrls")`, caption/adapter tests were inside `describe("template_literal_selector rule")`

## New structure

```
src/lint/
  hyperframeLinter.test.ts   — orchestrator integration + lintScriptUrls (8 tests)
  rules/
    core.test.ts             — root attrs, timeline registry, host id (6 tests)
    media.test.ts            — duplicate ids, missing id/src, preload (6 tests)
    gsap.test.ts             — clip element, transform conflict, missing script (12 tests)
    captions.test.ts         — caption exit, overflow, relative position (5 tests)
    composition.test.ts      — external script, template literal selector (5 tests)
    adapters.test.ts         — missing Lottie/Three.js scripts (8 tests)
```

## Test plan

- [x] 404 tests pass (402 before — 2 extra from previously misplaced adapter tests now correctly counted)
- [x] Lint + format clean
2026-04-01 18:30:13 +02:00
Miguel Ángel 1681350ac4 fix(preview): rewrite sub-composition asset urls in styles (#174)
## Summary
- rewrite CSS `url(...)` asset paths from sub-compositions before styles are hoisted into bundled/master preview output
- rewrite standalone sub-composition preview HTML so `src`/`href` paths keep resolving correctly under the preview root `<base>`
- add regression tests for bundled CSS asset rewriting and standalone sub-composition preview rewriting

## Root cause
Standalone composition previews reused the project `<head>` with a preview-root `<base href="/api/projects/:id/preview/">`, but the sub-composition body still contained `../...` asset references. Those escaped the preview route and 404ed. Separately, bundled preview already rewrote `<img src="../...">` paths but left hoisted CSS asset references like `@font-face src: url("../font.woff2")` untouched.

## Validation
- `pnpm --filter @hyperframes/core exec vitest run src/compiler/htmlBundler.test.ts src/studio-api/helpers/subComposition.test.ts`
- `pnpm --filter @hyperframes/core exec tsc --noEmit`
- `pnpm --filter @hyperframes/producer exec tsc --noEmit` *(blocked by pre-existing `packages/producer/src/services/deterministicFonts.ts` importing missing generated file `./fontData.generated.js` in the worktree install)*
- browser verification with `agent-browser` against the local preview server using `/Users/miguel07code/dev/test-hyperframes/heygen-promo`

## Browser proof
Verified the fixed preview routes in-browser after seeking to visible frames:
- standalone composition preview
- bundled master preview
2026-04-01 18:13:38 +02:00
James 1a5badc803 chore: release v0.2.0 v0.2.0 2026-04-01 03:28:05 +00:00
Miguel Ángel 110ea12597 fix(core,engine,producer): handle id-less media in sub-composition renders (#96)
## What

Move the id-less media fix into the shared timing compiler so producer can resolve durations for sub-composition videos before inlining, then carry the merged result through engine parsing, regression coverage, and the regression Docker image used in CI.

This PR now does five concrete things:

- assigns stable ids to id-less media in core `compileTimingAttrs()` so unresolved duration injection can target them
- keeps the engine-side `parseVideoElements()` support for `video[src]` plus the newer `data-duration` / natural-duration fallback from `main`
- makes producer prefer sub-composition media metadata over the later inlined-document parse when the same media id appears in both places
- makes `sub-composition-video` a runnable regression test by fixing its metadata and checking in the missing `output/compiled.html` snapshot
- removes the stale `pnpm-workspace.yaml` copy step from `Dockerfile.test`, so regression CI builds the Bun-based test image from the current workspace layout

## Why

- media without an explicit `id` could not participate in unresolved-duration resolution early enough
- producer could lose the resolved sub-composition timing by overwriting it with the later inlined parse
- the regression fixture intended to cover this case was not actually running in CI because its `meta.json` was incomplete and the required compiled snapshot was missing
- the regression image definition still expected a deleted `pnpm-workspace.yaml`, so GitHub Actions failed before the test shard could start

Putting the id-generation step in core makes the behavior reusable instead of relying on producer-only HTML patching.

## How

### Shared compiler

- core `compileTimingAttrs()` now auto-assigns stable ids to id-less `video` / `audio` tags
- those generated ids are returned in `unresolved`, so `injectDurations()` can add `data-duration` and `data-end` to the same media element later in the pipeline
- added core tests that cover auto-id assignment and duration injection for generated ids

### Producer

- when producer combines `subVideos` / `subAudios` with the media re-parsed from the final inlined HTML, it now lets the sub-composition metadata win
- this preserves the resolved/clamped timing already computed for nested media instead of overwriting it with the later parse
- `sub-composition-video` now has valid regression metadata and a checked-in `output/compiled.html` snapshot so CI actually executes it

### Engine

- resolved the merge conflict in `videoFrameExtractor` by keeping the broader `video[src]` parsing from this branch and the `data-duration` / natural-duration fallback that landed on `main`
- added a focused engine unit test for videos without ids

### CI image

- `Dockerfile.test` now copies only `package.json` and `bun.lock` at the workspace root before `bun install --frozen-lockfile`
- this matches the current monorepo layout and removes the obsolete pnpm-era dependency on `pnpm-workspace.yaml`

## Test plan

- [x] `bun run --filter @hyperframes/core test`
- [x] `bun run --filter @hyperframes/engine test`
- [x] `bun run --filter @hyperframes/producer test --update --sequential sub-composition-video`
- [x] `bun run --filter @hyperframes/producer test --sequential sub-composition-video`
- [x] Browser check with `agent-browser` against the compiled fixture page (`http://127.0.0.1:8123/compiled.html`)
- [x] Clean tracked-only Docker build of `Dockerfile.test` with the PR version of the file applied

## Notes

- Latest regression workflow is green on `main`, but before this PR the `sub-composition-video` fixture was being skipped by the harness rather than exercised end to end.
- The CI Docker fix was validated from a tracked-only export to avoid local untracked worktree artifacts affecting the result.
2026-04-01 02:59:22 +02:00
Vance Ingalls 1e4c101fb4 feat: lint for audio tag existingon found project audio (#169) 2026-03-31 16:16:56 -07:00
Vance Ingalls 0dcf73d62a feat: async skills install (#172)
## What

Added progress reporting to the skills installation process by converting synchronous operations to asynchronous ones and implementing progress callbacks.

## Why

The skills installation process can take a significant amount of time, especially when cloning repositories or running npm operations. Users need feedback about what's happening during the installation to understand progress and know the system hasn't frozen.

## How

- Converted `execFileSync` calls to a new `execFileAsync` function using promises
- Made all installation functions (`runSkillsAdd`, `gitClone`, `fetchRepo`, `fallbackInstall`) asynchronous
- Added an optional `onProgress` callback parameter to `installAllSkills` that accepts progress messages
- Integrated progress reporting in the `init` command by passing spinner message updates to the progress callback
- Added progress messages for key installation steps like "Installing {source} skills..." and "Cloning skill repositories..."
- Added "giget" to the external dependencies list in the build configuration

## Test plan

- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
2026-03-31 16:06:44 -07:00
Vance Ingalls 9d54192e83 chore: remove pnpm lock (#171) 2026-03-31 16:02:59 -07:00
Miguel Ángel 5421c23fff refactor(lint): break 1,314-line monolith into focused rule modules with plugin system (#170)
## Summary

- Breaks `hyperframeLinter.ts` from 1,314 lines (one massive function) into a plugin system of focused rule modules
- Introduces `LintContext` — HTML is parsed once and shared across all rules
- Adds `LintRule<TContext>` type as the formal contract for rules
- Public API unchanged: `lintHyperframeHtml`, `lintMediaUrls`, `lintScriptUrls` signatures identical

## New file structure

```
src/lint/
  utils.ts          — shared types (OpenTag, ExtractedBlock), regex constants, helpers
  context.ts        — LintContext type + buildLintContext() factory
  rules/
    core.ts         — structural rules (root attrs, timeline registry, script syntax)
    media.ts        — media element rules (duplicate ids, video pitfalls, placeholder URLs, etc.)
    gsap.ts         — GSAP rules + GSAP-specific parsing utils
    captions.ts     — caption rules
    composition.ts  — timed element, deprecated attrs, template literal selector, external scripts
    adapters.ts     — Lottie + Three.js missing-script rules (from PR #149)
  hyperframeLinter.ts — orchestrator only (~200 lines, down from 1,314)
```

## Adding a new adapter rule going forward

1. Create `src/lint/rules/my-adapter.ts` exporting `myAdapterRules: LintRule[]`
2. Import and spread into `ALL_RULES` in `hyperframeLinter.ts`

## Test plan

- [x] All 402 core tests pass unchanged
- [x] Full workspace build clean (`pnpm build`)
- [x] TypeScript strict mode clean (`pnpm tsc --noEmit`)
2026-04-01 00:50:13 +02:00
Vance IngallsandClaude Opus 4.6 116e6aa8e0 feat(skills): add audio visualizer effect with extraction script (#168)
* feat(skills): add audio visualizer effect with extraction script

Adds reactive audio visualization patterns for HyperFrames:

Script: extract-audio-data.py pre-extracts per-frame RMS amplitude and
frequency band data via ffmpeg. Uses a 4096-sample FFT window for clean
frequency resolution and per-band normalization across the full track
so treble activity is visible alongside louder bass.

Patterns: spectrum bars, mirrored waveform, pulsing circle, circular
visualizer, background glow — all Canvas 2D driven from the GSAP
timeline via tl.call() at each frame.

Includes smoothing helper, band count guide, band ordering rules
(horizontal: low-left high-right), and combining patterns section.

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

* refactor(skills): replace prescriptive examples with data model + motion principles

Removes five hardcoded draw functions that would get copy-pasted verbatim.
Replaces with:
- Clear data model docs (what rms and bands mean, how to index)
- Rendering approach setup for Canvas 2D, WebGL/Three.js, and DOM
- Motion principles (smoothing, value mapping, what makes it feel good)
- Spatial mapping conventions (low-left/high-right, etc)

The LLM invents the visualization; the skill teaches the data contract
and motion constraints.

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

* fix(skills): off-by-one in band slicing, add data loading, fix trigger

- Fix exclusive slice end: high_bin clamped to n_bins (not n_bins-1)
  so the last FFT bin in each band is included
- Add data loading section to skill doc (inline and fetch patterns)
- Fix example JSON to show frame 0 at time 0.0
- Update description to trigger when audio is analyzed and music detected

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

* refactor(skills): require numpy, fix bugs, clean up skill doc

Script rewrite:
- numpy is now required (pure-Python DFT was unusable for real files)
- Use np.frombuffer instead of struct.unpack (~10x less memory)
- Precompute Hann window and band edges (were recalculated every frame)
- Extract SAMPLE_RATE as module-level constant
- Clamp band bins to prevent max() on empty slice
- Validate --fps and --bands inputs

Skill doc fixes:
- Fix fetch loading example (was null ref on sync for-loop)
- Remove redundant Canvas 2D section (was duplicate of Step 3)
- Fix opening line (said "Canvas 2D" but doc covers 3 approaches)
- Fix undefined W/H in example

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-03-31 15:33:00 -07:00
Miguel Ángel 5f3488e996 feat(studio): drag-drop assetfile/folder and asset import anywhere in the studio (#155)
## Summary

- Add `/api/projects/:id/upload` endpoint for multipart file uploads with automatic dedup naming
- Wire `onImportFiles` from Assets tab "Import media" button through to the upload API
- Add global drag-drop overlay — drop media files **anywhere** in the studio, not just the Assets panel
- Files that already exist get `(2)`, `(3)` suffixes instead of overwriting
- Support uploading into subdirectories via `?dir=` param — dropping on a folder imports there
- Make folders draggable in the file tree + support drop-to-root
- Add `bodyLimit` middleware for early rejection of oversized payloads
- Surface skipped/failed uploads via toast notification instead of console-only

Addresses feedback: _"Wish I could upload/drag-drop assets directly in the Studio (music, images, video) like a CapCut media panel"_

## Test plan

- [x] Open studio, drag an image/video/audio file onto any part of the UI
- [x] Verify the drop overlay appears with "Drop files to import" message
- [x] Drop the file — verify it appears in the Assets tab and file tree
- [x] Drop a file with the same name — verify it gets a `(2)` suffix
- [x] Click "Import media" button in Assets tab — verify file picker works
- [x] Import multiple files at once via drag-drop
- [x] Drop a file onto a nested folder in the file tree — verify it lands in that folder
- [x] Drag a folder in the file tree and drop it on another folder or root — verify it moves
- [x] Drop a file >500MB — verify toast notification appears
- [x] Verify drag overlay doesn't get stuck when dragging over nested UI elements
2026-04-01 00:21:34 +02:00
Miguel Ángel 7265d0adfd fix(producer): rewrite relative asset paths when inlining sub-compositions (#166)
## Summary

- **Fixes**: `<img src="../icon.svg">` and similar `../` relative asset references in sub-compositions resolve to 404 after inlining into the root document
- **Unifies**: Both `hyperframes preview` (core bundler) and `hyperframes render` (producer) now use the same shared logic — no duplication

## Root cause

When inlining a sub-composition at `compositions/scene.html` into root `index.html`, a relative path like `../icon.svg` is correct from `compositions/` (it points to project root) but after inlining, `../` escapes the project directory.

## Fix

Extracts path rewriting into a shared `rewriteSubCompPaths.ts` utility in `@hyperframes/core`, used by both the bundler and the producer. **Only rewrites paths starting with** **`../`** — plain relative paths like `assets/foo.svg` are already correct from the root perspective and must not be rewritten (this was the regression cause in `overlay-montage-prod`: sub-composition asset refs like `assets/notch.svg` were incorrectly being rewritten to `compositions/assets/notch.svg`).

## Regression fix

The earlier version of this fix (now in history) rewrote ALL relative paths including `assets/foo.svg`, breaking the `overlay-montage-prod` regression test. This PR fixes that by scoping rewrites to `../`\-prefixed paths only.

## Test plan

- [x] `../icon.svg` in sub-composition renders correctly in both preview and render
- [x] `assets/foo.svg` (no `../`) in sub-composition still resolves correctly — not rewritten
- [x] `overlay-montage-prod` regression test passes
- [x] All other regression shards pass
2026-04-01 00:13:55 +02:00
James Russo e6a38c9e0f feat(ci): add alpha/beta/rc pre-release support to publish workflow (#167) 2026-03-31 14:16:57 -07:00
James Russo b866a28545 feat(cli): add remote template fetching via giget (#162)
* feat(cli): add remote template fetching via giget

* fix: update remote.ts to use templates/ instead of examples/

* fix(cli): validate template ID against manifest before downloading

Fails fast with available template list instead of downloading
an empty directory for nonexistent templates.

* refactor(cli): simplify to single --template flag with dynamic validation

- Remove --example flag (--template handles bundled + remote)
- Remove static ALL_TEMPLATE_IDS list (validates against GitHub manifest)
- No CLI release needed to add new templates — just add to templates/ and templates.json
- scaffoldProject auto-detects bundled vs remote

* chore: update lockfiles for giget dependency

* fix(cli): remove undefined isAudioOnly reference
2026-03-31 13:41:01 -07:00
Miguel Ángel b43b6fb1dc fix(producer): hoist external CDN scripts from sub-compositions (#164)
## Summary

- **Fixes**: External `<script src="...">` tags in sub-compositions (e.g. GSAP TextPlugin, ScrollTrigger) were silently discarded during `hyperframes render`, causing `ReferenceError` at runtime
- **Root cause**: The producer's `inlineSubCompositions` skipped external scripts with `if (src) continue` but then removed them via the blanket `querySelectorAll("style, script").forEach(remove)` — they were never hoisted to the parent document
- **Fix**: Mirror the core bundler's (`htmlBundler.ts`) approach — collect external script `src` URLs, deduplicate against existing scripts in the parent, and inject as `<script>` tags before inline composition scripts

## How to reproduce

1. Create a sub-composition that uses GSAP TextPlugin:

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/TextPlugin.min.js"></script>
<script>
  gsap.registerPlugin(TextPlugin);
  tl.to("#text", { text: { value: "Hello" }, duration: 1 });
</script>
```

1. Run `hyperframes render`
2. Browser console shows: `[Compiler] Composition script failed scene-hook ReferenceError: TextPlugin is not defined`

## Test plan

- [x] Verify `hyperframes render` on a project using TextPlugin in a sub-composition no longer errors
- [x] Verify external scripts are deduped (same CDN URL in 2 sub-compositions → only 1 `<script>` tag)
- [x] Verify external scripts already in `index.html` are not duplicated
- [x] Verify script order: external CDN scripts load before inline composition scripts
2026-03-31 22:31:56 +02:00
James Russo 6b1463fd5b refactor(cli): move templates to templates/ for remote fetching (#161)
* refactor(cli): move templates to examples/ for remote fetching

* refactor(cli): rename examples/ to templates/ for clarity

Follows Remix/Vite convention — these are init scaffolds, not general examples.
2026-03-31 13:10:04 -07:00
James Russo fee51f7a65 feat(docs): add template gallery page with visual previews (#160)
* feat(docs): add template gallery page with visual previews

* fix(docs): remove invalid MDX heading anchors

* chore: retrigger CI

* feat(docs): merge gallery into templates page with hover-to-play video previews

- Consolidated gallery.mdx and templates.mdx into single templates.mdx
- Moved templates page to Getting Started section
- Added MP4 video previews rendered by hyperframes (hover to play)
- Custom JS for hover-to-play behavior (Mintlify strips JSX event handlers)
- 2-column grid for landscape, 3-column for portrait
- Remotion-style cards with gradient overlay labels

* fix(docs): update broken links after templates page move

* ci(regression): remove scripts/ from regression trigger paths

scripts/ contains dev utilities (lint, versioning, preview generation)
that don't affect the rendering engine.
2026-03-31 13:04:04 -07:00
Miguel Ángel 5bb74cc0fc feat(cli): simplify init flow (#163)
## Summary

- **Streamline init from 7 steps to 3**: name → template → auto-opens studio preview
- **Remove** interactive "Got a video or audio file?" prompt (`--video`/`--audio` flags still work)
- **Remove** "What do you want to do?" post-scaffold menu (auto-launches preview)
- **Fast startup**: `--version` exits in ~10ms (lazy-load telemetry + update checker)
- **Brand teal**: CLI accent color is now `#3CE6AC` instead of generic cyan

### Before

```
name → overwrite? → media file? → [drag file] → transcribe? → pick template (9) → what next?
```

### After

```
name → pick template → auto-opens studio
```

## Test plan

- [x] `hyperframes init my-project` — should prompt name + template, then auto-open preview
- [x] `hyperframes --version` — should print version instantly (~10ms)
- [x] `hyperframes init --video path.mp4 my-project` — video flag still works
- [x] `hyperframes init --template blank my-project` — template flag still works
- [x] Verify teal accent color in terminal output
2026-03-31 21:16:18 +02:00
Vance IngallsandClaude Opus 4.6 e7283e5ce3 feat(skills): add gsap-effects skill with typewriter pattern (#158)
* feat(skills): add gsap-effects skill with typewriter pattern

Distills typewriter text animation into a reusable reference:
basic typewriter, blinking cursor, word rotation, appending words,
and a characters-per-second timing guide. Uses GSAP TextPlugin.

Also references the new skill from compose-video.

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

* fix(skills): emphasize cursor must always blink when idle and sit flush

Two key rules added to the typewriter skill:
1. Cursor must blink in every idle state (after typing, after clearing,
   during hold pauses) — a solid idle cursor looks broken.
2. No whitespace between text and cursor elements in HTML — any gap
   between the last character and the caret looks wrong.

Also adds cursor-hide state for multi-line handoffs and updates word
rotation example to include cursor state management.

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

* fix(skills): backspace must delete from end, not front

TextPlugin's text:{value:""} removes characters from the front,
which looks wrong. Added a backspace helper that steps through
substrings from right to left using tl.call(). Updated word
rotation example to use it.

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

* fix(skills): handoffs must blink before typing, use margin for spacing

Two lessons from testing:
1. Cursor handoffs need a blink pause — going hide→solid directly
   skips the idle state. Pattern: hide→blink→pause→solid→type→blink.
2. Use margin-left on a wrapper span for spacing between static and
   dynamic text. Flex gap spaces the cursor away, trailing spaces
   collapse.

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

* fix(skills): enforce single visible cursor as a hard rule

Only one cursor may be visible at a time. Multiple cursors on
screen looks broken. Every other cursor must be cursor-hide.
Promoted to rule #1 in the cursor section.

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-03-31 11:35:19 -07:00
James Russo 05aceedd30 feat(docs): add template preview generation script (#159)
## What

Add a script that uses `@hyperframes/producer` to render PNG thumbnails of each built-in template. Output goes to `docs/images/templates/`.

## Why

User feedback: "I would have loved more visual examples of what's actually possible. I had to scaffold every template just to see what they look like." This is the foundation for a visual template gallery in the docs.

## How

- New `scripts/generate-template-previews.ts` uses the producer's `createFileServer` + `createCaptureSession` + `captureFrame` APIs — hyperframes renders its own templates
- Patches out `__VIDEO_SRC__` placeholders (same logic as `init.ts`) so templates render without a video file
- Captures a frame at t=2s for each template (skips `blank` — it's just empty scaffolding)
- Handles varying dimensions (vignelli is 1080x1920 portrait)
- Adds `pnpm generate:previews` npm script

## Test plan

- [x] `pnpm generate:previews` generates 8 PNGs in `docs/images/templates/`
- [x] Each PNG is visually correct (verified by viewing)
- [x] `--only <template>` flag works for single template generation
2026-03-31 14:23:11 -04:00
Miguel Ángel ecb590d444 feat(studio): full IDE-like file management (#147)
## Summary

- **API**: POST (create), DELETE (delete), PATCH (rename/move), POST duplicate endpoints with null-byte sanitization
- **FileTree**: right-click context menu with New File, New Folder, Rename, Duplicate, Delete
- **Drag-and-drop**: move files between folders with visual feedback and subtree guard
- **Inline editing**: rename/create inputs with filename validation
- **Header actions**: quick New File / New Folder buttons in the FILES header

Stacks on top of `feat/studio-code-quality`.

## Test plan

- [x] Right-click file → Rename, Delete, Duplicate all work
- [x] Right-click folder → New File, New Folder, Delete work
- [x] Drag file from one folder to another
- [x] Create file with invalid name (`../foo`, `a/b`) → rejected client-side
- [x] Delete currently-edited file → editor clears
- [x] Studio build succeeds
2026-03-31 20:03:05 +02:00
Vance IngallsandClaude Opus 4.6 256c7a74fe feat(cli): add gradient ASCII banner to init command (#156)
Displays a white → #74E1B9 → #6ADCFF gradient HYPERFRAMES banner
using ANSI Shadow figlet font when running hyperframes init.
Gracefully skips in non-TTY or no-color environments.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 10:04:48 -07:00
Vance IngallsandClaude Opus 4.6 9cbfec1eca feat(skills): add hyperframes-cli skill (#154)
* feat(skills): add hyperframes-cli skill for CLI workflow guidance

Adds a new skill that teaches AI agents how to use the HyperFrames CLI
(init, lint, dev, render, doctor). Previously, agents had no way to
discover the CLI — the compose-video skill only covered HTML authoring.
This led to agents searching for binaries, finding the monorepo, and
running bun run studio manually instead of using npx hyperframes dev.

Also registers the skill in init.ts so new projects get it bundled
alongside hyperframes-compose and hyperframes-captions.

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

* refactor(cli): rename dev command to preview

The command starts a preview server — "preview" describes what users
are doing more accurately than "dev". Updates the command name, file
name, all CLI references, docs, skills, and template CLAUDE.md.

22 files updated across CLI source, docs, skills, and templates.

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

* fix(skills): replace stale dev reference with preview in CLI skill

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

* fix(docs): catch remaining dev references missed in rename

- testing-local-changes.mdx: two inline command examples
- troubleshooting.mdx: anchor link #dev → #preview, "dev server" → "preview server"
- cli.mdx: "dev server" → "preview server"

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-03-31 00:30:55 -07:00
James bc956bb6da chore: release v0.1.15 v0.1.15 2026-03-31 03:34:27 +00:00
James RussoandClaude Opus 4.6 a9d49cd528 fix(cli): auto-copy all templates to dist and add skill lint (#153)
- Replace hardcoded template list in build:copy with `cp -r src/templates/*`
  so new templates are included automatically (kinetic-type, decision-tree,
  product-promo, nyt-graph were missing from published package)
- Fix captions SKILL.md: reword `!` and `>` in inline backticks that
  triggered Claude Code's bash permission checker
- Add scripts/lint-skills.ts to catch shell-unsafe patterns in SKILL.md
  files (runs as part of `bun run lint` in CI)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:33:54 -07:00
James 25767fd8dd chore: release v0.1.14 v0.1.14 2026-03-31 03:15:36 +00:00
ef6225da1d feat(core): add fitTextFontSize utility for pixel-accurate text measurement (#152)
Add @chenglou/pretext dependency and fitTextFontSize() utility that uses
canvas measureText to compute the largest font size that fits text within
a given width. Replaces character-count heuristics with actual font-aware
measurement.

- New fitTextFontSize() in @hyperframes/core/text, exposed on window.__hyperframes
- Generalized for all text elements (captions, titles, etc.), not just captions
- Unit tests (mocked pretext) + browser integration test (real Chromium canvas)
- Updated captions skill docs with usage, exit guarantee, and self-lint patterns

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 20:14:13 -07:00
Miguel Ángel dac304ed9f refactor(studio): code quality — 22 findings, dead code removal, App.tsx split (#144)
## Summary

Full code quality review of the studio package, fixing 22 of 25 findings. Removes dead code, extracts modules from App.tsx, fixes accessibility and performance issues.

## Critical fixes (3)

- **`aria-valuenow`** on seek bar now updates imperatively via `liveTime.subscribe` — screen readers previously always reported position 0
- **Speed menu** closes on outside click (was permanently stuck open)
- **RenderQueue auto-scroll** moved from render phase to `useEffect` (was violating React render purity via `queueMicrotask` during render)

## Dead code removed (-331 lines)

| File | Lines | Why dead |
|---|---|---|
| `PreviewPanel.tsx` | 180 | Replaced by NLELayout + NLEPreview |
| `useCodeEditor.ts` | 80 | Exported but never imported |
| `formatTick` alias | 2 | Deprecated, unused |
| `onClipChange` prop | 5 | Declared, never used |
| `trackH` prop | 5 | Declared, never used |
| `editRange*` + updaters in store | 60 | Never read or written |

## App.tsx extraction

| Extracted to | Lines | What |
|---|---|---|
| `components/LintModal.tsx` | 130 | Lint results modal + LintFinding type |
| `components/MediaPreview.tsx` | 75 | Image/video/audio/font file previewer |
| `utils/mediaTypes.ts` | 15 | Shared regex constants (App.tsx and AssetsTab.tsx had diverged copies) |

## Performance fixes

- `useMemo` for `compositions`/`assets` derivation from `fileTree`
- `useMemo` for `buildTree(files)` in FileTree
- Debounced `handleContentChange` PUT (600ms — was firing on every keystroke)
- CompositionsTab iframe hover debounced (300ms — was mounting immediately)
- `VideoFrameThumbnail` re-extracts frame when `src` prop changes

## Not addressed (3 — low priority)

- #6: SystemIcons consolidation (large refactor across many files)
- #16-17: Overlay dismiss pattern standardization
- #18: Inline SVG → Phosphor replacement (gradual, per-PR)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-31 04:21:12 +02:00
Miguel Ángel 1bc83c62a5 feat(studio): timeline hidden by default with toggle in header + player (#142)
## Summary

The timeline is now hidden by default. A toggle button appears in both the header (alongside panel toggles) and the player controls bar. Both sync the same state and turn teal when the timeline is visible.

## Changes

- **`timelineVisible` state** in `App.tsx`, defaults to `false`
- **Header toggle**: icon button between sidebar toggle and Renders button
- **Player controls toggle**: icon button at the right end of the controls bar
- **NLELayout**: `timelineVisible` and `onToggleTimeline` props gate the timeline + resize divider
- **Player controls stay visible**: moved from inside the timeline section to inside the preview area, so hiding the timeline doesn't hide play/seek/timecode

## Behavior

| State | Preview | Player controls | Timeline |
|---|---|---|---|
| Timeline hidden (default) | Full height |  Visible | Hidden |
| Timeline visible | Shorter |  Visible | Shown with resize handle |

Both toggle buttons show identical teal active state (`#3CE6AC/10` bg + `#3CE6AC/30` border).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-31 04:01:01 +02:00
James RussoandClaude Opus 4.6 2f99e33bbe feat(cli,core): standalone transcribe command, transcript normalization, caption lint rules (#151)
* feat(cli,core): add standalone transcribe command, transcript normalization, and caption lint rules

- Add `hyperframes transcribe` command for transcribing audio/video and importing
  existing transcripts (SRT, VTT, OpenAI Whisper API JSON, whisper.cpp JSON)
- Add transcript format normalizer (normalize.ts) with auto-detection and
  conversion to standard [{text, start, end}] word arrays
- Upgrade default whisper model from base.en to small.en for better accuracy
- Add --model and --language flags to both `transcribe` and `init` commands
- Extract shared patchCaptionHtml() to eliminate duplication between init.ts
  and transcribe.ts (init.ts reduced by ~55 lines)
- Add 3 caption lint rules: caption_exit_missing_hard_kill,
  caption_text_overflow_risk, caption_container_relative_position
- Update captions skill with model guide, format docs, music guidance,
  text overflow prevention, caption exit guarantee pattern
- Expand captions skill trigger to cover lyrics, karaoke, lyric videos

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

* docs(cli): add transcribe command and --model/--language flags to CLI docs

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

* fix(cli): fix blank template lint issues

- blank/index.html: remove data-start from video (was nested in timed parent),
  add class="clip" for initial hidden state
- blank/captions.html: add max-width + overflow:hidden to prevent text clipping,
  add tl.set hard kill after exit tween to prevent stuck captions

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

* docs: add lint-after-edit rule to repo and project CLAUDE.md

Agents must run `npx hyperframes lint` after editing compositions.
Also expand captions skill description in project template.

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

* style: format _shared/CLAUDE.md

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-03-30 18:58:06 -07:00
Miguel Ángel 7b18c0352e feat(studio): redesign left panel — Code tab, Renders header, Lint bottom (#141)
## Summary

Major layout redesign matching the Paper mockup. The Code editor moves to the left panel, Renders moves to the header, and interactions are simplified.

## Layout changes

| Before | After |
|---|---|
| Left: Compositions \| Assets | Left: **Code** \| Compositions \| Assets |
| Right: Code \| Renders tabs | Right: Renders-only (hidden by default) |
| Header: Projects ← / Lint / panel toggles | Header: title / sidebar toggle / **Renders button** |
| Lint: header button | Lint: **pinned to bottom** of left panel |

## Interaction changes

- **Renders panel** opens via a dedicated header button (teal when active), hidden by default
- **ExpandOnHover removed** — replaced with inline autoplay on thumbnails:
  - Compositions: hover shows a tiny iframe (1920px scaled to 80px, 300ms debounce)
  - Assets: hover shows `<video autoPlay muted loop>` in the thumbnail cell
- **Deleted** `ExpandOnHover.tsx` (194 lines) and `ExpandedVideoPreview.tsx` (35 lines)
- **Left panel max width**: 50% viewport, auto-expands to 50% when opening a file in Code tab

## Visual polish

- Equal-width tabs (Code \| Compositions \| Assets)
- Sidebar toggle button highlighted when panel is open
- Phosphor duotone file-type icons (`FileHtml`, `FileCss`, `FileJs`, `FileTs`, etc.)
- "FILES" header removed from file tree
- Redundant "RENDERS (N)" title removed from renders panel

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-31 03:57:32 +02:00
Miguel Ángel 05a7b03841 feat(studio): remove home page, auto-select project (#145)
## Summary

Removes the `ProjectPicker` home screen entirely. The studio now boots directly into the editor by auto-selecting the first available project from `/api/projects`.

## Changes

- **Auto-select on load**: When no `#project/` hash is present, fetches the project list and navigates to the first one
- **Type narrowing**: Added `if (resolving || !projectId)` early return so TypeScript narrows `projectId` to `string` for all downstream props
- **Removed**: `ProjectPicker`, `ProjectCard`, `ExpandedPreviewIframe` components (~280 lines), `handleSelectProject` callback, `ProjectEntry` interface

## Why

The CLI studio always has exactly one project. The home page was an extra click with no value.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-03-31 03:51:55 +02:00
Miguel Ángel 6d54217e74 feat(core): support inline template compositions (#146)
## Summary

- Adds `loadInlineTemplateCompositions` to the runtime to handle compositions defined inline via `<template id="X-template">` paired with empty host elements that have `data-composition-id="X"` but no `data-composition-src`
- Updates the HTML bundler to inline template content into matching hosts during compilation
- Users can now define sub-compositions inline instead of requiring separate files in `compositions/`

### Before

```html
<!-- This showed nothing — the template was inert and the host was empty -->
<template id="logo-reveal-template">
  <div data-composition-id="logo-reveal" data-width="1920" data-height="1080">
    <style>...</style>
    <script>/* animation */</script>
  </div>
</template>

<div data-composition-id="logo-reveal" data-start="0" data-duration="10"
     data-width="1920" data-height="1080"></div>
```

### After

The runtime detects the matching `<template>` and injects its content into the host element — styles hoisted to `<head>`, scripts executed, dimensions copied. Works in both preview and render.

## Test plan

- [x] 9 new unit tests for `loadInlineTemplateCompositions` (basic mount, no-op cases, style/script injection, dimensions)
- [x] 3 new unit tests for bundler inline template handling
- [x] All 384 existing tests pass
2026-03-31 03:46:23 +02:00
Miguel Ángel 1aca29a414 fix(core,cli): improve lint output - JSON flag, info/warning counts, severity display (#134)
## Summary

- Respect `--json` flag on all lint exit paths so agents always get machine-readable output
- Separate `infoCount` from `warningCount` in linter results (was conflated)
- Display `info` vs `warning` severity distinctly in lint output
2026-03-31 02:45:19 +02:00
Miguel Ángel 0d51fb751c docs: add guide for testing local CLI changes outside the monorepo (#137)
## Summary

Adds `docs/guides/testing-local-changes.mdx` — a contributor guide explaining how to test unreleased CLI changes against real projects outside the monorepo.

**Covers:**

- `pnpm link --global` (recommended — makes `hyperframes` in `$PATH` point at your local build)
- `node` alias (no PATH changes)
- `npm pack` (test the exact artifact that would be published)
- Troubleshooting (`which hyperframes`, port conflicts, stale builds)
- Table of test scenarios for each bug category

Also registers the page in `docs/docs.json` so it appears in the Guides nav.
2026-03-31 00:47:35 +02:00
Vance IngallsandClaude Opus 4.6 a97dc75702 fix(lint): detect GSAP animations targeting clip elements (tab crash) (#114)
* fix(lint): detect GSAP animations targeting clip elements (tab crash)

The runtime manages clip visibility via inline styles. When GSAP also
writes inline styles on the same element, both systems trigger style
recalculations every frame, creating a runaway loop that crashes the
browser tab.

New rule gsap_animates_clip_element (error severity):
- Builds map of all elements with class="clip" (by id and class)
- Checks if any GSAP selector resolves to a clip element
- Nested selectors like "#overlay .title" are correctly ignored
- Merged into existing GSAP script loop (no redundant parsing)

* fix: remove non-null assertions and add missing test coverage

- Replace `!` assertions with optional chaining in lint.ts and tests
- Add shouldBlockRender tests for --strict-all without --strict
- Add clip element test for class-only detection (no id)

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

* fix: use optional chaining for array access in lintProject tests

TypeScript's strict mode flags array indexing as possibly undefined.
Use optional chaining and fallbacks instead of non-null assertions.

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-03-30 15:32:41 -07:00
James cf0ec5d27d chore: release v0.1.13 v0.1.13 2026-03-30 22:21:18 +00:00