Skip to content

feat(studio): consolidate into single OSS-ready NLE editor#7

Closed
miguel-heygen wants to merge 1 commit intofeat/core-compilerfrom
feat/studio-consolidate
Closed

feat(studio): consolidate into single OSS-ready NLE editor#7
miguel-heygen wants to merge 1 commit intofeat/core-compilerfrom
feat/studio-consolidate

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Mar 21, 2026

What

Consolidate @hyperframes/studio from a split frontend/backend architecture into a single Vite-powered package. Absorb @hyperframes/ui-player (Player, Timeline, PlayerControls, Zustand store) directly into studio/src/player/. Remove all agent-specific and infinite canvas code — the package is now a clean, embeddable NLE editor.

Why

The old studio had separate studio/frontend and studio/backend packages with a Hono server, making it heavy to run and impossible to use standalone. The hyperframes dev CLI command needs a single self-contained editor that starts instantly. For OSS, the studio must work without archive, producer, or any HeyGen infrastructure.

How

  • Single Vite dev server with an inline API plugin (devProjectApi) that serves project files, bundled HTML previews, and handles file writes — no separate backend process needed
  • HTML bundling via @hyperframes/core/compiler (bundleToSingleHtml) injects the HyperFrames runtime and inlines sub-compositions at serve time
  • Player merged from ui-player: Player.tsx, Timeline.tsx, PlayerControls.tsx, useTimelinePlayer hook, and playerStore moved into studio/src/player/
  • NLE layout (NLELayout.tsx): resizable preview + timeline split with composition drill-down up to 3 levels deep (Premiere-style breadcrumb navigation)
  • Extensible via React slots: previewOverlay and timelineFooter props allow archive to inject agent cursors, highlights, and activity tracks without any agent code in studio itself
  • File watcher: Vite's built-in watcher monitors project directories (with symlink resolution) and sends custom HMR events to auto-refresh the preview on external file edits
  • Speed control: 0.25x–2x playback via GSAP timeScale() + runtime bridge set-playback-rate
  • Security: isSafePath() guard on all endpoints that resolve user-controlled paths, 403 on write attempts that escape the project directory
  • PostMessage compatibility: sends/listens for both hf-parent/hf-preview and magic-edit-parent/magic-edit-preview to support the CDN runtime
  • DOM fallback: when __clipManifest isn't available (no runtime), parses data-start attributes directly from the iframe DOM to populate the timeline
  • Sub-composition clips filtered from root timeline — only shown when drilling down into the composition

Notable decisions:

  • useState(() => { fetch(...) }) anti-pattern replaced with useEffect
  • Regex injection in sourcePatcher.ts fixed with escapeRegex() helper
  • className used only for clip label (human-readable), not id (must be unique)
  • Dead dev-server.ts removed (Vite plugin replaces it entirely)

Test plan

  • cd packages/studio && pnpm dev starts the studio on localhost
  • Project picker lists all projects in the data directory
  • Open a project — preview renders with bundled HTML + runtime injection
  • Play/pause (spacebar), seek (click/drag), arrow key scrub works
  • Timeline shows all root-level clips with correct names and durations
  • Double-click composition clip → drill-down with breadcrumb navigation
  • Nested drill-down works (Master → outro → credits, 3 levels)
  • Escape or breadcrumb click navigates back
  • Source editor (code button) opens sidebar, edits auto-refresh preview
  • External file edit (sed -i) triggers hot reload via file watcher
  • Speed control: select 2x, play — animation runs at double speed
  • Lint button checks compositions for common errors
  • __node__index clips from sub-compositions don't leak into root timeline
  • Path traversal attempts on API endpoints return 404/403

Replace legacy frontend/backend split with single Vite-powered package.
Absorb @hyperframes/ui-player into studio/src/player/.

Features:
- NLE layout: preview + timeline + composition drill-down (3 levels)
- Source editor with CodeMirror (HTML/CSS/JS)
- Bundled preview via @hyperframes/core/compiler
- File watcher for hot reload on external edits
- Playback speed control (0.25x-2x)
- Extensible via slots (previewOverlay, timelineFooter)
- Path traversal protection on all API endpoints
- Support both hf-*/magic-edit-* postMessage sources
Copy link
Copy Markdown
Collaborator Author

miguel-heygen commented Mar 21, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@miguel-heygen miguel-heygen deleted the feat/studio-consolidate branch April 6, 2026 23:25
miguel-heygen added a commit that referenced this pull request Apr 7, 2026
## Summary

Adds critical rendering constraints to the `hyperframes` skill discovered from eval analysis of 27 agent-generated compositions. These guardrails prevent agents from producing compositions that technically work but render poorly.

## What it fixes

| Rule Added | Eval Prompts Affected | Issue |
| --- | --- | --- |
| Ban `repeat: -1` | #20 loading-spinner (2.0/5) | Infinite timeline broke capture engine |
| Ban async timeline construction | #16 particle-logo (2.6/5) | Timeline empty at capture time |
| Min font size 16px (labels), 20px (body) | #7, #8, #13, #14, #15, #19 | Illegible text after encoding |
| Ban full-screen dark linear gradients | #3, #5, #10, #14 | H.264 color banding |
| `<link>` fonts over CSS `@import` | #7, #24 | Font loading race conditions |

## Changes

- **Rules section**: Added `repeat: -1` ban, async timeline ban, items 8-9 to "Never do" list
- **Typography section**: Expanded font size guidance with specific minimums per text role (headlines, body, labels)
- **New "Backgrounds and Color" section**: Guidance on avoiding gradient banding
- **Output Checklist**: 5 new items covering all new constraints

## Test plan

- [ ] Run eval with updated skill and compare avg quality scores
- [x] Skill renders correctly in `/hyperframes` invocation
vanceingalls added a commit that referenced this pull request Apr 16, 2026
Blockers:
- #2: late_init_set false positive on fractional opacity (0.5 matched as 0)
  Fixed: /opacity\s*:\s*0(?![.\d])/ negative lookahead
- #3: scene-1 prefix skip matches scene 10+ (s1- matches s10-)
  Fixed: extract full number and compare exactly

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

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
miguel-heygen added a commit that referenced this pull request Apr 17, 2026
Addresses James Russo's review on PR #298.

- Drop drift threshold 150ms → 50ms. ITU-R BT.1359 puts A/V offset
  perceptibility at ±45ms; 150ms risks audible lip-sync drift on
  talking-head content. Steady-state offset under parent ownership
  on the factory-series-c-video repro measures 27–37ms.
- Add MutationObserver for dynamic sub-composition media so late-
  attached audio[data-start] / video[data-start] elements get a
  parent proxy. Without it, parent ownership silences the new
  element in the iframe via sticky outputMuted but has no parent
  counterpart to play — silent hole.
- Make bridgeMuted sticky in syncRuntimeMedia. Symmetric with
  mediaOutputMuted so a sub-composition activating after user mute
  doesn't briefly play at author volume before the next bridge
  message. Both flags feed a single shouldMute gate per tick.
- Reset _audioOwner on iframe load. Latch previously never cleared,
  leaving stale parent-ownership against a fresh runtime after
  composition switch — a brief double-voice window until the next
  NotAllowedError re-promoted.
- Dispatch audioownershipchange CustomEvent on every owner change,
  with reason: 'autoplay-blocked' | 'iframe-reload'. Gives host apps
  an SLO-ready signal for '% of sessions in parent ownership'.
- Surface parent proxy play() rejection as a playbackerror event
  instead of swallowing silently. Covers the rare case where the
  parent also lacks activation.
- Test gaps filled: userMuted stickiness, OR invariant between flags,
  contract pin (media.ts fires onAutoplayBlocked every rejection),
  caller-side latch pattern dedupe, audioownershipchange dispatch,
  mid-playback promotion path, playbackerror surface.
- One-line comment in _promoteToParentProxy on the postMessage
  async race — defensible because the autoplay gate keeps rejecting
  until the mute lands.

Not addressed here:
- Manual verification on a physical iOS / Android device (review
  item #7). Simulated autoplay block via agent-browser + mobile
  emulation was done on the original PR; real-device validation
  is still pending before the next release.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant