Commit Graph

312 Commits

Author SHA1 Message Date
Miguel Ángel 0eb4d079dc fix(renderer): remove noopener from turbo popup (returns null per spec)
window.open with noopener always returns null to the opener per spec,
which caused the POPUP_BLOCKED fallback to always trigger. Removed
noopener so the opener gets a window reference for popup-blocked detection.

Note: without noopener, popup windows share the opener's event loop
in same-origin contexts, limiting parallelism. True multi-process
parallelism requires cross-origin isolation or future APIs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:29:16 +02:00
Miguel Ángel 4992555c27 fix(renderer): force reflow on sub-composition iframes to prevent position jumps
After seek, force offsetHeight reflow on all nested iframes (not just
the main document). Sub-compositions have their own GSAP timelines in
separate documents — without reflowing them, transform values (x, y)
captured by SnapDOM can be stale between frames, causing text/element
position jumps.

Also reorders the yield-before-reflow (let runtime propagate seek to
sub-compositions first, then reflow everything at once).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:16:02 +02:00
Miguel Ángel d4774bedf2 fix(renderer): improve warmup for sub-composition fonts and GSAP settle
- Seek to 0.1s first to trigger sub-composition iframe loading
- Wait for fonts.ready in all sub-composition iframes (not just main)
- Increase font timeout to 5s for CDN fonts
- Add more settle ticks (10x10ms) for complex GSAP nested timelines
- Double-seek to 0 after warmup for clean first frame
- Increase per-frame yield from 0ms to 4ms for better GSAP sync

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:08:41 +02:00
Miguel Ángel 18a4a2dd9c fix(studio): read POST body as Buffer to preserve binary video uploads
The Vite middleware was reading request bodies as UTF-8 strings,
corrupting binary data from the browser renderer's blob upload.
Changed to Buffer.concat() for binary-safe body reading.

Also adds error handling for failed upload responses.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:49:16 +02:00
Miguel Ángel 04b4eee462 feat(renderer): add turbo render mode — multi-tab parallel capture
Opens N browser tabs (via window.open with noopener) for true
multi-process parallelism. Each tab gets its own Chromium renderer
process with independent main thread, bypassing the single-thread
SnapDOM bottleneck.

Architecture:
- TurboPool coordinator opens N minimized offscreen tabs
- Each tab loads composition via SnapdomFrameSource independently
- Frames captured as PNG, sent via BroadcastChannel to coordinator
- Coordinator decodes to ImageBitmap, feeds reorder buffer + encoder
- Concurrency: min(cores/2, 6), capped at 6 tabs
- Falls back to normal IframePool if popups blocked

Also:
- Add turbo toggle (lightning bolt) to Studio render UI
- Add /api/projects/:id/renders/upload endpoint for saving
  browser-rendered videos to project renders directory
- Export isTurboSupported() from @hyperframes/renderer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:42:23 +02:00
Miguel Ángel e00055f821 docs: add turbo render design spec
Multi-tab parallel capture using noopener windows + BroadcastChannel
for true multi-process SnapDOM parallelism.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:35:40 +02:00
Miguel Ángel f077c47731 chore: merge main — resolve conflict in studio package.json
Keep both @hyperframes/player (from main) and @hyperframes/renderer deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:14:08 +02:00
Miguel Ángel ef63bb9cde chore(renderer): remove brainstorming docs, QA pages, and lockfile from PR
- Remove docs/superpowers/plans and specs (brainstorming artifacts)
- Remove packages/renderer/qa/ (dev testing pages)
- Remove packages/renderer/pnpm-lock.yaml (monorepo uses root lockfile)
- Remove run-parity-test.sh (superseded by regression-parity.ts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 14:03:49 +02:00
Miguel Ángel 83436f3d36 fix(renderer): fix regression harness — async wait, upload server, asset copying
- Use async spawn for agent-browser wait so Node event loop stays active
  for the upload HTTP server to process browser POST requests
- Add CORS headers to upload server responses
- Add frameSource: 'snapdom' to test page (headless Chrome auto-detects
  tab-capture which hangs waiting for screen share permission)
- Copy source assets to output dir (CSS, fonts not inlined by compiler)
- Use compile-test.ts for compilation (injects runtime + __hf bridge)
- Write serve.json with cleanUrls:false to prevent .html→extensionless redirects
- Increase upload timeout from 10s to 300s for long compositions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:56:41 +02:00
Miguel Ángel 6d4ad2ecce fix(renderer): fix video frame drops, audio, animation jitter, and background rendering
- Discover media elements from DOM when __player bridge doesn't expose hf.media
  (scans both <video> and <audio> elements with data-start/data-duration)
- Resolve media src to absolute URLs so audio mixer fetch() works cross-origin
- Seek original video elements directly instead of clone+canvas overlay approach
  (SnapDOM natively captures <video> via drawImage)
- Force synchronous reflow after GSAP seek to prevent stale computed styles
- Add warmup phase: wait for fonts.ready + settle ticks before first capture
- Replace all requestAnimationFrame with setTimeout for background tab support
- Fix interleaved capture/encoding progress by deferring encoding reports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:58:32 +02: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 0b15403593 fix(studio): set concurrency=1 for browser render to prevent UI lockup
Single-iframe capture keeps Chrome responsive during long renders.
With 8 parallel iframes, SnapDOM overwhelms the main thread and locks
the browser. Concurrency=1 trades speed for reliability.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 04:23:24 +02:00
Miguel Ángel 7d36ab38e3 fix(renderer): add yields between frames to prevent main thread lockup
- iframe-pool: setTimeout(0) yield after each captured frame
- SnapDOM source: requestAnimationFrame after seek() before capture
  to ensure layout/paint is processed

This prevents Chrome from becoming unresponsive during long renders,
keeping CDP, DevTools, and UI interactions functional.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 04:01:26 +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 09363f1898 fix(renderer): bridge __player→__hf in SnapDOM source, fix studio worker URL
- SnapDOM frame source now detects window.__player (studio runtime) in
  addition to window.__hf and creates a bridge automatically. This lets
  the renderer work with studio preview URLs directly.
- Studio useBrowserRender hook: force snapdom frame source and provide
  explicit worker URL for Vite dev compatibility.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:43:56 +02:00
Miguel Ángel e924c76c10 feat(studio): add browser-side export button using @hyperframes/renderer
Adds a "Browser" export button next to the existing server-side Export
button in the RenderQueue panel. Uses the @hyperframes/renderer package
to render compositions entirely client-side via WebCodecs, with progress
tracked in the existing job list and automatic file download on completion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:46 +02:00
Miguel Ángel f9489afb98 feat(renderer): add drawElementImage + tab-capture frame sources, regression harness
drawElementImage: pixel-perfect capture via html-in-canvas API using
<canvas layoutsubtree> with iframe child. Achieves 42 dB PSNR vs 30 dB
for SnapDOM. Requires Chrome --enable-blink-features=CanvasDrawElement.

Tab-capture: experimental getDisplayMedia-based capture.

Also adds regression parity scripts for compiling test compositions and
comparing PSNR against golden reference renders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel f10cd056f7 fix(renderer): fix viewport capture, empty audio, cancel propagation, progress ETA
- Capture doc.body instead of doc.documentElement in SnapDOM to fix 75% viewport mismatch
- Make audio track conditional (hasAudio flag) to prevent corrupt empty audio tracks
- Add publishConfig for npm publishing
- Wire ProgressTracker for estimatedTimeRemaining and captureRate in progress callbacks
- Recompute video overlay canvas position per-frame for GSAP-animated elements
- Add AbortSignal to captureAll for cancel propagation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel 76727306c9 fix(renderer): fix iframe polling, add workerUrl config, add QA test page
- Replace requestAnimationFrame with setTimeout in __hf protocol polling
  (rAF doesn't fire for off-screen iframes)
- Add workerUrl option to EncoderOptions and RenderConfig for configuring
  the encoding worker URL (needed for bundled deployments)
- Add QA test page with self-contained CSS-animated composition

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel 23b5d834a1 fix(renderer): address code review issues
- Fix audio format: replace interleaveChannels with concatPlanarChannels
- Fix MIME type: derive video/mp4 vs video/webm from outputFormat in finalize
- Add isSupported() guard at top of render() for early failure
- Fix durationMs to report wall-clock render time (totalMs)
- Fix audio filter to use strict equality (hasAudio === true)
- Remove unused progress type from WorkerOutMessage union

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel 18d6a3e9e2 feat(renderer): add progress tracking and ETA estimation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel 725d9e71b1 feat(renderer): integrate video frame injector into SnapDOM frame source
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:45 +02:00
Miguel Ángel 826179e44f test(renderer): add end-to-end integration test with basic composition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel e79f6b02b0 feat(renderer): add main orchestrator and public API (render, createRenderer)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel 5019fc09b4 feat(renderer): add parallel iframe pool for frame capture
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel fc823c572f feat(renderer): add video frame injector for <video> element capture
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel 05ee085c36 feat(renderer): add OfflineAudioContext-based audio mixer
Implements mixAudio() with TDD: decodes audio sources, applies volume/timing
via GainNode, and renders a mixed PCM AudioBuffer using OfflineAudioContext.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel 4b1e46294e feat(renderer): add WebCodecs encoding worker and main-thread wrapper
Implements the encoding pipeline for @hyperframes/renderer: worker message
protocol types, Web Worker entry point (VideoEncoder + MediaBunny muxer),
and the main-thread Encoder class that manages the worker lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:44 +02:00
Miguel Ángel 3112bc8035 feat(renderer): add SnapDOM frame source implementation
Implements SnapdomFrameSource class that loads compositions in hidden iframes,
seeks via window.__hf.seek(), and captures DOM frames using @zumer/snapdom.
Adds jsdom dev dependency for the test environment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:43 +02:00
Miguel Ángel 408d69b1a0 feat(renderer): add frame timing and distribution utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:43 +02:00
Miguel Ángel a00930d952 feat(renderer): scaffold package with types and compat detection
Adds the @hyperframes/renderer package skeleton: package.json, tsconfig.json,
src/types.ts (all pipeline interfaces), src/compat.ts (WebCodecs feature
detection), and src/index.ts (re-exports). Typecheck passes with zero errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 05:24:43 +02:00
Miguel Ángel b8c56195c0 docs: add client-side renderer implementation plan
13-task plan covering package scaffold, SnapDOM frame source, WebCodecs
encoding worker, MediaBunny muxer, OfflineAudioContext audio mixer,
parallel iframe capture, video frame injector, and main orchestrator.
56 total steps with TDD approach.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:43 +02:00
Miguel Ángel e58bff5756 docs: add client-side renderer design spec
Design spec for @hyperframes/renderer — a new browser-only package that
renders compositions to MP4 using WebCodecs + MediaBunny + OffscreenCanvas
workers, with SnapDOM as interim frame source and html-in-canvas as the
future pixel-perfect path. Replaces the need for Puppeteer + FFmpeg.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 05:24:43 +02: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>
v0.2.4
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
Vance Ingalls d86e4cb3c3 feat(skills): add layout-before-animation approach to hyperframes skill (#233)
Adds structured approach (what, structure, timing, layout, animate) and
layout-before-animation guidance — build end-state CSS first, then add
gsap.from() entrances and gsap.to() exits. Prevents unintentional overlap
by making layout problems visible before adding motion.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:20: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 v0.2.3 2026-04-08 18:19:07 +00:00
James Russo aea128b606 feat(cli): smart port selection with instance reuse (#226)
* feat(cli): smart port selection with instance reuse

Replace the simple 10-port retry loop with best-in-class port handling:

- Multi-host port testing (127.0.0.1, 0.0.0.0, ::1, ::) catches ports
  occupied by SSH forwarding or other interfaces invisible to localhost
- HTTP probe (/__hyperframes_config) detects existing HyperFrames
  preview servers — reuses same-project instances instead of spawning
  duplicates, skips different-project instances
- PID detection via lsof for actionable "Port N in use by PID X" logs
- Expanded scan range from 10 to 100 ports
- Added --force-new flag to bypass instance detection
- Async PID detection (execFile, no shell) and parallel host testing

Fixes the "10 ports are all in use" error that occurs when zombie
preview servers accumulate or devbox port forwarding occupies ports.

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

* feat(cli): add --list and --kill-all flags to preview command

- `hyperframes preview --list` scans the port range and displays all
  active HyperFrames preview servers with their project name, directory,
  and PID
- `hyperframes preview --kill-all` kills all active preview servers
- Port scanning uses parallel batched probes (20 at a time) for speed

Gives users visibility into zombie preview servers and a one-command
way to clean them up.

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-08 10:58:57 -07:00
James Russo fe9cd301ec docs: apply HyperFrames design system to Mintlify theme (#225)
* docs: apply HyperFrames design system to Mintlify theme

Update docs config and add custom CSS to match the HyperFrames brand:
- Switch theme from mint to maple, replace cyan palette with warm neutrals
- Add Inter (body/headings) and IBM Plex Mono (code) fonts
- Add custom.css with full light/dark mode CSS variables
- Default to light mode appearance
- Replace box-shadow hover effects with border-color (flat aesthetic)
- Add DESIGN.md to repo root as design system reference
- Fix docs CI to also trigger on DOCS_GUIDELINES.md pushes to main

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

* docs: replace HeyGen logo with HyperFrames text wordmark

Replace 41KB HeyGen SVG logos with lightweight (~400B) text-based SVGs
rendering "HyperFrames" in Inter semibold with tight tracking, matching
the wordmark style on hyperframes.heygen.com.

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

* docs: use ABC Solar Display font for logo wordmark

Match the exact font rendering from hyperframes.heygen.com:
- Load ABC Solar Display Bold from HeyGen static assets CDN
- SVGs use 15.2px/600w/-0.15 letter-spacing (matches computed styles)
- Dark mode fill matches rgb(240,240,240) from the website
- Add @font-face in custom.css for site-wide availability
- Fix lefthook: remove css from oxfmt glob (oxfmt doesn't support CSS)

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

* docs: convert logo SVGs to outlined paths

SVG <text> elements don't render custom fonts when loaded as <img>
(browser security restriction). Convert the ABC Solar Display glyphs
to SVG paths extracted from the font outlines — renders identically
everywhere with zero font dependency. Remove @font-face for the
display font from custom.css since it's no longer needed.

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

* docs: constrain logo height to match website sizing

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

* Revert "docs: constrain logo height to match website sizing"

This reverts commit 89e9cb598e.

* docs: switch body font from Inter to TT Norms Pro

Use TT Norms Pro (from HeyGen static assets CDN) to match
hyperframes.heygen.com. Loads weights 400-700 via @font-face
with Inter as fallback.

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-07 20:24:35 -07:00
Miguel Ángel 1c61a0b25a chore: release v0.2.3-alpha.2 v0.2.3-alpha.2 2026-04-08 03:19:08 +02:00