Commit Graph
2006 Commits
Author SHA1 Message Date
Miguel Ángel c9e8dd3862 fix(runtime): honor render fps when seeking (#1739) 2026-06-26 12:28:41 -04:00
James RussoandClaude Opus 4.8 88fffb04d1 feat(cli): surface and prune skills removed upstream (#1740)
* feat(cli): surface and prune skills removed upstream

`skills add` / `init` / `hyperframes skills update` only ever add or
refresh — none of them delete a skill that was renamed or dropped
upstream (e.g. graphic-overlays → talking-head-recut). `skills check`
also ignored any installed skill not in the manifest, so a stale bundle
lingered forever with no signal and no cleanup path.

- skills check: detect "removed" skills by cross-referencing the
  vercel-labs/skills lock — a skill the lock attributes to our manifest
  `source` that the manifest no longer lists. Surface them in the human
  and --json output and count them toward the non-zero exit so the
  `check || update` contract gates on them.
- skills update: after `skills add --all`, prune those skills via
  `skills remove -g --yes` so the install fully reconciles with the
  manifest. Best-effort — a cleanup failure doesn't fail the update.

Attribution is via the lock's source field, never the bare directory
name: `.../skills` is shared across sources, so skills from other
sources (e.g. greensock/gsap-skills) are never touched.

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

* fix(cli): prune removed skills only in the scope they were attributed from

Make the cleanup in `skills update` impossible to misfire onto a user's
own skills. The prune already only targets names the lock attributes to
our source, but it hardcoded `skills remove -g` (global) while `skills
add` defaults to project scope — so detection could attribute from one
scope's lock while removal hit another, potentially deleting a global
skill of the same name from a different source.

- checkSkills now returns the located install's `scope`.
- skills update removes in that exact scope (`-g` only when global), so
  attribution scope and removal scope always match.

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

* fix(cli): validate skill names before passing them to skills remove

Addresses review feedback: skill names fed to `skills remove` originate
as lock-file JSON keys, so a corrupt or crafted lock entry could smuggle
a flag-like (`--config=…`) or shell-special token into the spawn — which
matters most on the Windows cmd.exe path where arg escaping is fragile.

Filter the names through a strict kebab-case pattern and warn on any that
are rejected, rather than relying on a `--` separator (the upstream
`skills` arg parser silently ignores unknown `-`-prefixed tokens and has
no `--` end-of-options handling, so `--` would be a no-op there).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 09:23:37 -07:00
James RussoandJerrai ca9e1316be perf(producer): stream binary file responses, async-read HTML (#1735)
* perf(producer): stream binary file responses, async-read HTML

Replaces the per-request readFileSync in fileServer's static file handler
with a createReadStream pipe (binary) and an async readFile (HTML). Static
asset serving no longer blocks the Node event loop.

Why
---

The pre-fix handler called readFileSync(filePath) on every binary asset.
On video-heavy compositions Chrome requests several 32MB video files
back-to-back; each readFileSync(32MB) blocked the main event loop long
enough to wedge concurrent /health responses and other timers.

Scope clarification — this addresses the event-loop block documented at
renderOrchestrator.ts:1277-1306 (the video-heavy regression class). It is
NOT the fix for today's infinite-duration incident; Miguel is shipping
that upstream as a plan()-time duration guard. The two are complementary:

  - Miguel's guard kills the impossible-work input shape before chunk
    planning so the producer doesn't try to enumerate 300B frames.
  - This streaming fix removes the next-largest known main-thread block
    (large binary I/O during video-heavy renders), so future wedge
    classes don't kill otherwise-healthy probes either.

The companion worker_thread /health PR + the heygen-com/app probe-timeout
bump round out the defense-in-depth: even if some future code path
introduces another main-thread stall, the probe lives off-thread and the
budget is 30s anyway.

What changed
------------

fileServer.ts: switched both file branches off the sync I/O path.

  - Binary (the hot path for video-heavy renders): readFileSync(filePath)
    -> createReadStream + Readable.toWeb -> Response stream body.
    Content-Length is set via statSync so Chrome's range-aware media
    stack sees the size up front. The handler is now async because the
    HTML branch awaits.

  - HTML (small files; injected with pre/head/body scripts):
    readFileSync(filePath, "utf-8") -> readFile(filePath, "utf-8").
    The injection is still sync — pure string ops — only the disk read
    moved off-thread. Index HTMLs are tiny (~200KB max for AI-generated
    compositions) but a ms of stall per render-start adds up across a
    fleet.

Test
----

fileServer.test.ts: added a streaming regression that pins three
properties on a 5MB synthetic binary asset (chunk-boundary spanning):

  1. Correctness — served bytes match the file across multiple
     createReadStream chunks (default 64KB highWaterMark).
  2. Content-Length header is set from statSync.
  3. Four parallel fetches all return identical content; the streaming
     path doesn't serialize them.

All 31 fileServer tests pass locally (bun test).

TODO: link Miguel's upstream plan() duration guard PR once known.

— Jerrai

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(producer): implement Accept-Ranges + 206 Partial Content for fileServer

Delivers the range-request semantics the original PR body promised but
the diff did not implement. Without range support, Chrome's <video>
element issues full-file GETs on seek; with this commit it can issue
`Range: bytes=...` and get a sliced 206 back, so seek + partial-load
work without re-pulling the whole file.

- Add `parseRangeHeader` (exported for unit tests) covering the three
  RFC 7233 single-range forms: bytes=START-END (closed), bytes=START-
  (open-ended), bytes=-SUFFIX (last N bytes). Multi-range falls back to
  `absent` (full 200) so we never reassemble multipart/byteranges.
- Binary path now returns 206 Partial Content with Content-Range +
  sliced Content-Length on satisfiable ranges, 416 Range Not Satisfiable
  with `Content-Range: bytes (asterisk)/<size>` on unsatisfiable ranges,
  and 200 with `Accept-Ranges: bytes` on full-body GETs so clients know
  ranges are supported.
- Add unit tests for parseRangeHeader (10 cases: 3 forms, clamping,
  unsatisfiable edges, malformed inputs, multi-range fallback).
- Add integration test covering 200 + Accept-Ranges, all 3 range forms
  with byte-correct slices, 416 on out-of-bounds, and multi-range -> 200
  fallback.

Addresses Miga's review finding on #1735.

Co-Authored-By: Jerrai <noreply@anthropic.com>

— Jerrai

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 08:47:53 -07:00
James RussoandJerrai 01a10cdc53 fix(producer): serve /health from a worker_thread so probes survive main-thread stalls (#1733)
* fix(producer): serve /health from a worker_thread so probes survive main-thread stalls

Adds an off-main-thread /health endpoint that listens on its own port
(default 9848, env PRODUCER_HEALTH_PORT). The endpoint binds inside a
Node worker_thread with a minimal node:http server — separate event loop,
separate isolate — so probe responses don't depend on whatever the
producer's main thread is doing.

Why now
-------

Today's hyperframes-producer crashloop traced to an infinite GSAP
timeline -> distributed planner trying to enumerate ~300,000,000,000
frames -> sidecar /health stops landing within k8s's 5s window ->
otherwise-healthy pods killed.

Miguel is shipping the root-cause fix at plan() time (impossible /
non-finite / sentinel durations get rejected before chunk planning).
That removes today's wedge.

This change is defense-in-depth for the kill mechanism. Even with the
plan() guard, future wedge classes can stall the main event loop for
seconds at a time: large synchronous file I/O (see the companion
fileServer streaming PR), GC pauses on long-running renders, tight
loops in user-authored GSAP / Three.js / canvas code, future
activity / pool changes whose runtime cost we haven't yet characterized.

Probe responsiveness should reflect process liveness, not main-thread
event-loop responsiveness. If the entire Node process is dead the OS
tears down both threads' sockets simultaneously and k8s correctly kills
the pod. Anything short of that and the worker thread's listener keeps
answering.

Backwards-compatible: the main-thread /health on PRODUCER_PORT (9847)
keeps working exactly as before. The k8s sidecar probe config in
heygen-com/app can migrate to the worker port at its own pace. A
companion heygen-com/app PR in this batch raises the probe timeout
from 5s -> 30s as a last-resort backstop.

TODO: link Miguel's upstream plan() duration guard PR once known.

Test: healthWorker.test.ts (vitest) — 3 tests pass locally, including
the load-bearing one: stays responsive while the main thread is blocked
on a 500ms sync busy-spin.

— Jerrai

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(producer): tighten healthWorker startup race + shutdown semantics

Addresses Miga's review on #1733.

- server.ts: store the worker as a Promise<HealthWorkerHandle | null>
  instead of mutating a `let` from inside `.then`. A SIGTERM landing
  before the `.then` callback fired would previously see `healthWorker
  === null` and skip cleanup. shutdown() now `await`s the promise with
  a bounded 1.5s timeout so a hung-startup worker can't keep SIGTERM
  waiting (worker.terminate() from process exit still kills it).
- healthWorkerThread.ts: replace `process.exit()` inside the worker
  with `parentPort.close()` + natural event-loop drain. Node-version
  semantics for `process.exit()` from a worker have been historically
  inconsistent; the documented clean path is to close the channel and
  let the worker exit naturally. Also drops the redundant 2s force-exit
  on shutdown — the parent already owns the authoritative deadline via
  Promise.race + worker.terminate(), so the worker-side timer was
  belt-and-suspenders noise.

Co-Authored-By: Jerrai <noreply@anthropic.com>

— Jerrai

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-26 08:47:49 -07:00
WaterrrForeverandClaude Opus 4.8 d70ee134cc feat(cli): add skills version check, update, and freshness manifest (#1738)
* feat(cli): add skills version check, update, and freshness manifest

Give the HyperFrames skill bundle a content fingerprint so agents and
users can tell whether installed skills are the latest version, on any
platform that can run the CLI.

- skills-manifest.json (repo root): per-skill sha256 over the whole skill
  directory; minimal {source, skills}, no version/timestamp so it is fully
  deterministic. Generated by scripts/gen-skills-manifest.ts.
- `hyperframes skills check` [--json]: compares installed skills to the
  manifest; exits non-zero when something is outdated (agent/CI gate).
- `hyperframes skills update`: thin wrapper over `npx skills update`.
- Passive nudge on render/lint/validate when skills are stale (24h cache,
  same opt-out as the CLI self-update notice).
- "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge
  GitHub raw-CDN lag, falling back to the main branch URL.
- CI job + lefthook hook keep skills-manifest.json in sync with skills/.

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

* fix(cli): add execFile to child_process mock in skills test

skills.test.ts mocks node:child_process but only declared execFileSync
and spawn. Loading skills.js transitively loads skillsManifest.ts, which
runs promisify(execFile) at module load, so vitest threw on the missing
execFile named export. Add a bare stub — these tests never invoke it.

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

* feat(cli): init installs all skills; skills update pulls the full set

Make `hyperframes init` the single place skills are pulled in full, and
make "update" mean "get everything" rather than "refresh what's there".

- init now always installs/refreshes ALL skills (incl. ones not yet
  present) instead of prompting "Install AI coding skills?" — opt out
  with `init --skip-skills`. Both the interactive and non-interactive
  paths pass `--all --yes` so the complete set is fetched.
- `hyperframes skills update` switches from `npx skills update` (which
  only refreshes already-installed skills) to `skills add --all`, so it
  installs missing skills too — the same install step init runs.
- SKILL.md documents init-installs-all and the new update semantics.

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

* feat(cli): skills check treats missing skills as needing an update

The full skill set is now the goal (init and `skills update` both pull
all, including ones not installed), so a partial install is no longer
"a choice" — it's something to fix.

- diffSkills: updateAvailable is now true when anything is outdated OR
  missing (local-only still doesn't count). So `skills check` exits
  non-zero — and renders "Update:" instead of "up to date" — whenever a
  skill is missing, not just when one is stale.
- The passive render/lint/validate nudge follows suit: it now counts
  missing alongside outdated ("N skills out of date or missing"),
  tracked via a new skillsMissingCount cache field.
- SKILL.md documents the stricter check.

Note: platforms that intentionally vendor only a subset of skills (e.g.
a Codex snapshot) will now see check report non-zero.

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

* fix(cli): install/update skills straight from the GitHub repo

`skills add owner/repo` can resolve through the skills.sh registry, which
lags behind the repo — so `update` could install a stale version while
`check` (which resolves latest directly from GitHub) keeps reporting
"outdated", an endless loop.

Switch the install source to the full GitHub URL
(https://github.com/heygen-com/hyperframes), which makes `skills add`
git-clone the repo directly at latest main, bypassing the registry. This
covers `hyperframes skills`, `hyperframes skills update`, and `init`'s
skill install — all of which go through SOURCES. Now install/update and
check agree on what "latest" means.

The init "install skills" hint now points at `npx hyperframes skills
update` so the manual path uses the same GitHub-direct fetch.

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

* feat(cli): init checks skills against GitHub, installs only when stale

`hyperframes init` now runs the skills version check first and only
(re)installs when something is outdated or missing — instead of
unconditionally re-pulling every time. Re-running init on an
already-current project is now a no-op ("skills are already up to date").

- New ensureSkillsCurrent() helper, shared by both the interactive and
  non-interactive init paths (no duplicated install logic).
- The check resolves "latest" straight from GitHub (same source the
  install uses); best-effort — if it can't reach GitHub it installs anyway.
- SKILL.md updated to describe the check-then-install behavior.

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

* refactor(cli): address skills manifest review feedback

From the PR review (points 1, 2, 4, 5):

1. Remove the `local-only` skill status. checkSkills only ever hashes
   manifest-listed skills, so a local-only status could never appear in
   the end-to-end output — and making it appear would wrongly flag
   unrelated skills (the `.../skills` dir is shared across sources).
   diffSkills now reports only on manifest skills; skills on disk that
   aren't in the manifest are ignored.
2. Drop the redundant per-directory sort in listFilesSorted — the single
   final out.sort() is what guarantees a deterministic hash (verified:
   manifest unchanged).
4. resolveLatestManifest local-path detection now uses path.isAbsolute,
   so Windows absolute paths (C:\...) are treated as local instead of
   falling through to a remote fetch.
5. fetchManifest validates the response shape (asSkillsManifest) instead
   of a blind `as` cast, so a CDN error page served as 200 fails with a
   clear error rather than a cryptic crash later in diffSkills.

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

* fix(cli): strict skills update + auto-discover any agent host

Address PR review (Magi blocker + James/Rames robustness):

- Blocker (Magi): `skills update` is the documented recovery path for
  `skills check || skills update`, but it delegated to installAllSkills()
  which swallowed missing-npx and failed `skills add` as "skipped",
  exiting 0 even when nothing changed. Add a strict mode that throws on
  failure; update sets a non-zero exit (init stays best-effort). New tests
  simulate a non-zero `skills add` (exit 1) and the success path.

- Robustness (James/Rames #2): the upstream `skills` CLI installs into
  ~72 agent conventions; a hard-coded list (4, or even 11) can't track
  that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd +
  $HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG
  `.config/<host>/skills`), so detection is structural and future-proof,
  no closed list. agentFromDir infers the host from the path.

- Tests (Rames #3): temp-fixture detection tests for every convention ×
  {project, global}, scope priority, claude-code preference, the
  no-install case, the --dir override, and an unknown/new host (proving
  the no-closed-list property).

- Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip;
  findRepoManifest climbs 16 levels (was 8) for deep monorepos.

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

* fix(cli): resolve CodeQL file-system race + de-flake Windows npx test

Two CI fixes:

- CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the
  existsSync(outPath) precheck followed by writeFileSync(outPath) is a
  check-then-write race. Read the committed manifest directly in a
  try/catch instead (missing/unreadable ⇒ "no committed manifest"), so
  there's no precheck to race against. Behavior is unchanged.

- Windows Tests: npxCommand.test.ts's real `npx --version` smoke test
  cold-starts slower than vitest's 5s default on Windows runners and
  timed out. Give the test 60s headroom (and a 30s exec timeout). Kept
  as a real execution check — mocking would reduce it to a tautology.

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

* fix(cli): repair garbled npx smoke-test timeout comment

The explanatory comment for the 60s timeout was scrambled across the
callback/timeout arguments, failing oxfmt --check (and thus preflight,
which in turn skipped preview-parity and failed the regression gate).
Move it above the it() call so it no longer sits between call arguments.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:52:42 +08:00
Miguel Ángel 0c1e236dcd chore: release v0.7.10 (#1734) v0.7.10 2026-06-26 00:51:34 -04:00
Miguel Ángel dca5fa90cb fix(producer): reject impossible distributed durations (#1732) 2026-06-26 00:47:27 -04:00
TzuhanyandJames 7db84fc0ad fix(producer): rebuildExtractedFramesFromPlanDir off-by-one in framePaths key indexing (#1730)
* fix(producer): rebuildExtractedFramesFromPlanDir off-by-one in framePaths key indexing

In distributed chunk-lambda render mode, every <video>'s first-paint
frame (the moment a vid first becomes visible on the composition
timeline) renders as PRISTINE Y=16 black. For a 3-vid back-to-back
composition (v1: 0-4s, v2: 4-8s, v3: 8-12s at 30fps), frames 0, 121,
242 are all PRISTINE black; the render then either stays black for 1
frame, or shows body bg + persistent overlays only (Y~22 with sparse
highlights from text/logo). The symptom only reproduces in distributed
mode — local single-process renders are unaffected.

Root cause: rebuildExtractedFramesFromPlanDir builds the framePaths Map
with 1-based keys, but the consumer (getFrameAtTime at
engine/videoFrameExtractor.ts:958) computes a 0-based frame index via
Math.floor(localTime * fps + 1e-9). For each vid's first-paint frame
(localTime === 0 → frameIndex === 0), framePaths.get(0) returns
undefined; the vid is silently dropped from activePayloads,
videoFrameInjector doesn't fire, syncVideoFrameVisibility hides
everything, and BeginFrame screenshots an empty composition.

Every other site in the codebase builds/consumes framePaths with
0-based keys:

  - engine/videoFrameExtractor.ts:317     framePaths.set(index, ...)
  - engine/extractionCache.ts:204          framePaths.set(idx, ...)
  - engine/videoFrameExtractor.test.ts:264/1066  framePaths.set(i, ...)
  - engine/videoFrameExtractor.ts:958 (consumer) Math.floor 0-based
  - producer/renderOrchestrator.test.ts:287/315/349  framePaths.get(0)

Only producer/distributed/renderChunk.ts:198 was 1-based, with a
stale comment claiming FrameLookupTable indexes frames 1-based —
which the surrounding evidence contradicts. This is why local tests
pass while distributed-lambda renders always had cold black at each
vid first paint.

Verified locally against a 3-vid composition and a single-vid 4-worker
case in a Lambda render fleet. Before fix: every vid first-paint frame
is PRISTINE Y=16 black. After fix: all frames are valid source content,
blackdetect reports zero black regions outside legitimate source video
content (intentional fade-ins / hard cuts in source mp4).

* test(producer): pin rebuildExtractedFramesFromPlanDir 0-based framePaths contract

Regression guard for the off-by-one fix in HF#1730. The pre-fix code
indexed framePaths 1-based while the consumer (getFrameAtTime in
engine/videoFrameExtractor.ts:958) reads 0-based, dropping every
<video>'s first-paint frame in distributed chunk-lambda renders.

Asserts framePaths.get(0) resolves to the first extracted frame, and
framePaths.get(N-1) resolves to the last — pre-fix the keys were
shifted to [1..N], so get(0) returned undefined and get(N) resolved.
Verified to fail against the previous i+1 indexing.

Also exports rebuildExtractedFramesFromPlanDir (was module-local) so the
test can call it directly. Pure logic worth testing in isolation — the
bug only reproduces under distributed mode and the existing
renderChunk.test.ts already pays a multi-second Chrome smoke probe in
its module-level beforeAll, so the regression check lives in its own
file (rebuildExtractedFrames.test.ts) and runs Chrome-free in ~10ms.

The function's doc comment said "1-based framePaths" — updated to
"0-based" with a pointer to the consumer site and the bug context.

Per Miguel's REQUEST_CHANGES on HF#1730.

— Jerrai (https://claude.com/claude-code)

---------

Co-authored-by: James <james.russo@heygen.com>
2026-06-26 00:44:17 -04:00
Miguel Angel Simon Sierra 8db190dd43 chore: release v0.7.9 v0.7.9 2026-06-25 21:32:51 -04:00
Miguel Ángel 226a741c9d fix(studio): Enable keyframes works (and auto-tracks) on elements with a global gsap.set (#1728)
* fix(core): a global gsap.set is off-timeline, so resolvedStart is 0 not the comp end

resolveTimelinePositions walks anims in document order advancing a cursor; a
global `gsap.set(...)` carries no position arg, so it fell through to the
cursor fallback and inherited the comp-end time (every prior tween's duration
summed) as its resolvedStart. A global set is a load-time hold — its start is 0.

This silently broke 'Enable keyframes' on any element whose only animation is a
global gsap.set (e.g. a statically-positioned card): promoteSetToKeyframes bails
when `playhead <= setStart`, and setStart was the comp end, so any playhead
before the end was a no-op. Pin a global set to resolvedStart 0 in both the
recast and acorn parsers; don't let it advance the cursor/prevStart.

* fix(studio): Enable-keyframes marks the generated 0% endpoint as auto-tracking

When 'Enable keyframes' promotes a static set to a two-stop tween, the 0% (the
held start the user didn't choose) is now marked `auto: true` → serialized as the
`_auto: 1` marker. The parser's endpoint-sync then keeps it tracking the nearest
keyframe until the user edits it directly; the 100% (the real keyframe placed at
the playhead) stays fixed.

This re-wires the auto-endpoint behavior that was silently dropped in #1605 (the
sync logic stayed, but no flow produced an `_auto` endpoint anymore, so an
untouched 0% never tracked). Adds a guard test so it can't be lost again.

* chore: suppress fallow complexity findings surfaced in the touched files

The resolveTimelinePositions guard added a branch (pushes it over threshold), and
changed-file scope re-surfaces pre-existing complex functions (readElementPosition,
applyArcWaypointAtPlayhead, the useEnableKeyframes callback). Bare directives only.

* fix(studio): dragging a --hf-studio-offset element no longer flies

Dragging a static element positioned via the legacy --hf-studio-offset CSS var
(e.g. dot-a) flew off-screen — three independent failure modes, all fixed:

1. Live drag integrated: the per-move draft read its base from the live transform
   it set last frame (gsap.getProperty), so base+delta accumulated frame-over-frame.
   Fix: carry a stable baseGsap on the in-memory drag member (immune to mid-drag
   re-renders that wipe the data-hf-drag-* attrs) and use it as the fallback.

2. Commit re-added the delta: the source commit re-read the wiped attrs / live
   transform. Fix: re-stamp the stable base/initial attrs in applyManualOffsetDragCommit
   before the commit reads them.

3. Drop left it offset: the committed source was correct, but the LIVE element kept
   its --hf-studio-offset var + translate:var(...), which composed with the GSAP
   transform (rendered at dropped + offset) until a full reload. Fix: on cleanup,
   when GSAP owns the position, clearStudioPathOffset() migrates the element off the
   legacy CSS channel (leaving transform untouched) — matching the stripped source.

Adds a regression suite covering all three layers.
2026-06-25 21:31:23 -04:00
Miguel Ángel bafed1fc9a fix(core): lint leaked head text in compositions (#1727)
* fix(core): lint leaked head text in compositions

* fix(core): harden leaked head text lint

* chore: refresh code scanning status

* fix(core): cover parser-error head close tags
2026-06-25 21:25:37 -04:00
Miguel Ángel 92385711dc fix(engine): hold last frame when a clip's media is shorter than its slot (#1726)
Renders showed the page background (a one-frame black flash) right before a cut
when a video clip's source media was a hair shorter than its data-duration slot
— the common case, since `ffmpeg -t 1.45` emits 43 frames = 1.433s at 30fps.
The frame lookup only held the last frame at the exact clip end, so the
sub-frame remainder rendered blank.

- Hold the last extracted frame for the rest of the slot once the source is
  exhausted, within a tolerance floored at the compiler's 0.05s clamp epsilon so
  the seam is covered at any fps (2 frames alone is < 0.05s above 40fps). Clips
  deliberately much shorter than their slot still blank for the tail (unchanged).
- Warn when the compiler clamps a video's data-duration down to its media length
  (slot longer than source by more than the clamp epsilon): a render-time
  `[compile]` warning in the producer, plus a matching `validate` warning that
  reads each <video>'s live duration in headless Chrome (static HTML lint can't
  see media durations). A shared `analyzeClipMediaFit` keeps both on one
  threshold.

Adds engine unit tests for the hold behavior and the analyzer.
2026-06-25 19:16:27 -04:00
Miguel Angel Simon Sierra 764aa02a3a chore: release v0.7.8 v0.7.8 2026-06-25 18:59:53 -04:00
Miguel Ángel 37ac138041 feat(studio): draggable 3D-transform cube in the design panel (#1710)
* feat(studio): draggable 3D-transform cube in the design panel

Add a Figma-style draggable cube to the 3D Transform section so users can set an
element's 3D orientation by dragging instead of typing degrees. Drag tilts the
element (rotationX/Y); Shift-drag rolls it (rotationZ); a recenter button resets
the 3D transform to identity. The cube previews the orientation live and commits
on release.

It's an input affordance over the existing keyframe-aware commit path
(commitAnimatedProperty) — a drag at the playhead writes/updates keyframes just
like the numeric fields, no new mutation infra.

- transform3dProjection.ts: pure unit-cube projection with back-face culling and
  painter ordering (no 3D dependency), unit-tested.
- Transform3DCube.tsx: the SVG drag widget (pointer-capture, draft→commit).
- Surface the two missing numeric fields (RotZ, Perspective). Perspective drives
  the new editable `transformPerspective` prop (per-element depth) rather than
  CSS `perspective` (which only affects children).

* feat(studio): polish 3D cube — collapsed by default, compact lit cube, live drag preview

Address review of the first cut:
- 3D Transform section is now collapsible and collapsed by default (it was tall
  and ate panel space).
- Redesign the cube: compact and centered (was full-width), resting isometric
  camera so it reads as a 3D cube at identity instead of a flat square,
  directional per-face lighting, gradient backdrop + grounding shadow.
- Live element preview while dragging: onLivePreviewProps gsap.sets the live
  transform on the preview element so it moves WITH the cube; release still
  commits via the keyframe-aware path.
- Extract Cube3dControl to keep the panel component under the complexity gate.

* fix(studio): persist static 3D transform + refine cube edges

The cube (and the RotX/RotY numeric fields) didn't stick on an element whose
only tween is a position 'set' — commitAnimatedProperty tried to convert the
zero-duration hold into keyframes, so the rotation was never written and the
cube snapped back. Handle the static-set case: merge the property into the set
(update-property) so a static 3D rotation/perspective persists, and the cube
reads it back from runtime.

Also refine the cube rendering: muted teal lit faces with edges that brighten
with how front-facing each face is (crisp bevels, not flat neon outlines), a
soft halo glow, and a stronger grounding shadow.

* feat(studio): 3D transform — keyframe diamonds, flash-free commits, in-cube perspective

- Keyframe diamonds: RotX/RotY/RotZ + Perspective (and Z/Scale) now each carry a
  KeyframeNavigation diamond, so 3D transforms can be keyframed like Layout X/Y.
  Refactored the six fields onto a shared Transform3dField.
- Flash-free: static-set 3D commits now use instantPatch (in-place runtime patch,
  no soft reload), and the set fast-path was widened to the 3D channels
  (rotationX/Y/Z, z, transformPerspective) — dragging the cube / scrubbing a 3D
  field no longer flashes.
- In-cube perspective: a Persp slider lives in the cube widget and the cube's
  foreshortening reflects transformPerspective live.

* feat(studio): 3D cube X/Y/Z axis gizmo + gated flash-diagnostic logs

- Axis gizmo: render the rotated X (red) / Y (green) / Z (blue) vectors from the
  cube center — away-facing axes dimmed behind the cube, toward-facing on top
  with a tip dot + label — so orientation is readable at a glance.
- Flash diagnostics: add a gated, JSON-stringified [hf-3d:*] logger (on in dev or
  via window.__hfDebug). Instruments the commit path (which branch + picked
  tween), the cube pose/axis commits, and — the key signal — applyPreviewSync's
  instant-patch-vs-soft-reload decision (a soft reload IS the flash). Reproduce
  with the console open to pinpoint any remaining flash to a specific commit.

* fix(studio): make the 3D cube mirror the element's orientation 1:1

The resting isometric camera made the cube always look tilted, so at rotation
0/0/0 the cube showed a 3D pose while the element was flat — the cube didn't
represent the element. Drop the decorative camera (VIEW_RX/RY = 0): the cube now
faces front at identity, exactly matching the un-rotated element, and tilts to
match as the element rotates. The X/Y/Z axis gizmo keeps the flat-at-rest state
readable.

Flash status (from the gated [hf-3d:*] logs): every commit now reports
'instant (no flash)' via instantPatch — the soft-reload flashes are resolved.

* fix(studio): stop design-panel flicker — read transform channels live

Each 3D commit bumps the gsap cache; the panel then re-read runtime values, but
readGsapRuntimeValuesForPanel only included props already present in the parsed
gsapAnimations. A just-set rotationX isn't in the parse yet, so for that window
the cube + fields dropped it and flickered to 0. Always read the core transform
channels (x/y/rotation/rotationX/Y/Z/z/scale/transformPerspective/opacity)
directly via gsap.getProperty — which reflects the in-place instant patch — so
the panel shows the true current value with no flicker.

* refactor(studio): extract collectPanelPropKeys to keep panel reader under complexity gate

* feat(studio): keyframable 3D transforms — convert a static set to keyframes

The cube/3D fields stored rotation as a static 'set', and convert-to-keyframes
flatly refused to convert a set (gsapParser.ts) — so two 3D 'keyframes' just
overwrote the same static value with no interpolation.

Now a set converts to an animatable to(): resolveConversionProps emits both
endpoints from the set's value (visual unchanged until edited), and both writers
flip set→to, drop the immediateRender hold, and add a duration. The element's
clip duration is threaded through the convert chain (3D field → handler →
convertToKeyframes → route → parser) so the keyframes span the whole clip and
land in range at any playhead. Click a 3D field's diamond to convert, then edit
at different playheads to animate. Acorn writer mirrored; recast round-trip test
added.

* feat(studio): keyframe toggle on the 3D cube

The cube had no keyframe affordance, so dragging it only ever wrote the static
set (logs showed every rotation commit as path:static-set) and nothing
interpolated — converting required clicking a numeric field's diamond, which
isn't discoverable while driving the cube.

Add a keyframe diamond button to the cube widget: it converts the 3D
('other'-group) static set to keyframes spanning the element's clip, and lights
up when the transform is already keyframed. Once keyframed, cube drags + numeric
edits add keyframes at the playhead and the 3D rotation interpolates.

* feat(studio): auto-keyframe 3D transforms on animated elements + stop AssetsTab 404 loop

3D transforms now auto-keyframe like drag/resize/rotate: when the element is
already animated (its clip has keyframes), editing a 3D prop converts the static
set to keyframes so edits at other playheads interpolate — no manual keyframe
toggle needed. Purely static elements still write a static set (and the cube's
keyframe button remains a manual opt-in for them).

Also fix the AssetsTab media-manifest fetch: it was keyed on the assets array
reference (new each render) so it re-fetched the (usually missing) manifest on
every re-render — spamming 404s and churning the left sidebar during cube drags.
Key on a stable join and cache the 404 so a missing manifest is fetched once.

* fix(studio): cube writes one keyframe per drag (no duplicate keyframes)

The cube committed rotationX/Y/Z as separate add-keyframe mutations; the first
axis's auto-keyframe convert shifted the tween so the second axis computed a
slightly different percentage → two adjacent keyframes instead of one.

Add a batched commitAnimatedProperties that writes all changed props into ONE
keyframe, and route the cube through it (commitAnimatedProperty is now a thin
single-prop wrapper). Threaded through the panel chain; numeric fields keep the
single-prop path. Set-path and keyframe-path extracted to helpers to stay under
the complexity gate.

* refactor(studio): extract AudioRow from AssetsTab to satisfy file-size check

The manifest-404 fix touched AssetsTab.tsx, which was already over the 600-line
cap (702). Move the self-contained AudioRow sub-component to its own file,
bringing AssetsTab to 493 lines.

* fix(studio): self-heal stale animationId on 3D property commit

A 3D property edit (cube drag / field) picks its target from the panel's
selectedGsapAnimations cache. When keyframes were just removed or the script
changed underneath, that id is gone server-side and the commit POST 404s
('animation not found'). The raw commitMutation already toasts but rethrows,
so the rejection escaped as an uncaught promise. Catch it in
commitAnimatedProperties and bump the cache so the panel re-syncs and the
next edit self-heals.

* fix(studio): batch the 3D reset into one commit (was six flashes)

Reset 3D orientation looped six props (rotationX/Y/Z, z, scale,
transformPerspective) through the single-property commit, so one click
triggered six separate soft-reloads — six preview flashes. Batch them into
one onCommitAnimatedProperties call (one keyframe, one reload), matching the
cube-drag path.

* fix(studio): 3D-edit a static element writes a set, not keyframes

Editing the 3D transform of an element with no keyframes created a keyframed
tween (Case 3 made a tl.to() + convert, a flat tween converted to keyframes).
A static element should stay static — same as manual drag / resize / rotate,
which tl.set() it. Route no-keyframe elements to a set: update an existing one
in place, or create a dedicated tl.set carrying all axes in ONE add mutation.
The single mutation also avoids the per-axis id race (a flat tween's
group-derived id shifts after the first prop, 404-ing the next and polluting
an unrelated tween).

* feat(studio): instant 3D keyframe edits via in-place tween rebuild

Dragging the cube on an animated element soft-reloaded the iframe on every
edit (a flash). GSAP compiles object-form keyframes ({ "0%": {...} }) into
sub-tweens at creation and ignores later vars.keyframes mutations, so the value
can't be patched the way a tl.set can. Instead REBUILD the tween in place: kill
it and recreate it on the same parent timeline at the same position with the
edited keyframe merged and all other vars preserved, then re-seek — no iframe
reload, no flash. Resolution is now channel-aware for keyframe tweens too, so a
rotation edit lands on the rotation tween, never a co-located position tween.
Declines (→ soft reload) for array-form, motionPath, or dynamic values.

* feat(studio): static 3D transform persists as off-timeline gsap.set (no 0% keyframe)

Adjusting a 3D transform on an element with no keyframes created a
tl.set(...,0), which the timeline renders as a 0% keyframe diamond — even
though it's a static hold, not animated. Persist a newly-created static 3D
hold as a base gsap.set(...) instead: it runs immediately, sits OFF the
timeline, and shows no keyframe marker (matching the manual-drag UX).

- Model: GsapAnimation.global marks a base gsap.set vs an on-timeline tl.set.
- Parser (recast + acorn): parse a STRING-LITERAL gsap.set("#sel", {...}) as an
  editable global set so it round-trips and re-edits in place; variable-target
  gsap.set(el, ...) holds stay opaque surrounding source (unchanged).
- Serializer + writers: emit gsap.set(sel, props) (no timeline var, no position)
  when global; in-place updates keep it a gsap.set.
- add mutation gains global; commitStaticSet sends it when creating a holder.

* fix(studio): static manual drag persists as off-timeline gsap.set, instant (no flash/diamond)

After keyframes are removed, manually dragging a now-static element wrote a
tl.set(...,0) — an on-timeline hold that shows a 0% keyframe diamond and
soft-reloaded on the first nudge (a flash/teleport between the overlay and the
committed position). Make the static position/rotation drag persist as a base
gsap.set (off-timeline, no marker), like the 3D path.

A gsap.set has no runtime tween to patch, so add a 'global-set' instant-patch
that applies the value straight to the element (gsap.set(el, props)) — the
element is static on these channels, so it reflects instantly with no soft
reload. Existing tl.set holds keep the tween 'set' patch; only global sets use
global-set. Create now carries the instant patch too, so the first nudge is
flash-free.

* fix(studio): a base gsap.set shows no keyframe diamond (timeline + panel)

A base gsap.set is parsed as an editable set (for idempotent re-edits), but
synthesizeFlatTweenKeyframes turned it into a synthetic 0% keyframe, so the
timeline track and the panel field showed a phantom keyframe diamond for a
static, non-animated value. Return null for a global set so it contributes no
keyframes — it's an off-timeline static hold, not a keyframe.

* fix(studio): a static set never shows a keyframe diamond (timeline + panel)

A set (gsap.set OR tl.set) is a static hold — a value applied at one point,
not an animated keyframe — so it must not synthesize a phantom keyframe. The
prior fix only skipped GLOBAL gsap.set; on-timeline tl.set holds (and ones a
split/conversion produced) still showed a diamond. Skip every set, which also
aligns the AST keyframe cache with the runtime scan (it already drops every
zero-duration set).

* fix(studio): batch set-property edits (reset 3D no longer 404s)

Reset 3D fires 6 props (rotationX/Y/Z, z, scale, perspective) at a set;
commitSetProps updated them one at a time. A set's id is GROUP-derived, so the
moment scale lands on a rotation set its id shifts (-other -> mixed), 404-ing
the next prop (perspective never got set). Add an update-properties mutation
(merge many props in one call) and have commitSetProps/commitStaticSet use it —
one round-trip, no mid-loop id shift.

* style(studio): fix format + trim 3D-patch helper complexity

oxfmt the runtime-patch file (the failing Format/Preflight check) and reduce
the complexity of the new helpers: flatten keyframeVarsCarryChannel with .some,
extract finiteNumericProps from applyGlobalSet, suppress the inherently-defensive
rebuildKeyframeTween guard chain.

* chore(studio): remove [hf-3d:*] debug logs (3D transform verified working)

Strip the log3d call sites + the debug3d util now that the 3D transform /
static-set / keyframe-rebuild paths are confirmed working.

* chore(studio): strategic [hf-pos:*] logs for position-commit path audit

Temporary DEV-gated logs to confirm which path each drag takes: single drag →
GSAP code path (single-gsap), multi-select/group drag → DEPRECATED CSS-var path
(group-css, applyStudioPathOffset → --hf-studio-offset), and the single CSS
fallback (single-css). To be removed once group drag is routed through GSAP.

* fix(studio): route multi-select group drag through GSAP code path

Group drag committed positions via the deprecated --hf-studio-offset CSS
var (applyStudioPathOffset) and outright blocked GSAP-animated elements.
Single drag already routes through tryGsapDragIntercept (tl.set /
keyframes / gsap.set); group drag now does the same per element, so a
multi-select move writes real GSAP code with no CSS-var fallback. Removed
the now-dead CSS group commit.

* feat(studio): live candidate highlight while marquee-selecting

The marquee only revealed what it selected on mouse-up, so it was easy to
grab too much or too little. Now each element the marquee box currently
intersects is outlined live (studio-accent) as you drag, before release —
so you can see the selection forming. Shares one synchronous OBB/SAT
intersection pass between the live highlight and the commit; the async
source-probe still runs only once, on mouse-up.

* chore(studio): remove temporary [hf-pos:*] position-path debug logs

Investigation done — group drag now routes through the GSAP code path, so
the CSS-vs-GSAP path-audit scaffolding (logPos / debugPos) is no longer
needed. Removes the util and its imports/calls.

* fix(studio): marquee selects/highlights elements at their real positions

The marquee derived element boxes from elementObbCorners, whose
non-identity-transform branch reconstructed the box from offsetLeft/offsetTop
plus the element's own transform matrix — ignoring the matrix translate
(m.e/m.f) and any ancestor transforms. Mid-GSAP-animation (elements carry a
translate() transform), that put boxes at their pre-translate layout
position, so the marquee highlighted/selected the wrong elements vs. the
box shown when you click an element directly.

Route the marquee through the same toOverlayRect basis the selection and
group boxes use (a getBoundingClientRect-based AABB). Now highlight ==
selection-commit == the click-selection box, at the element's real on-screen
position. Drops the buggy OBB/SAT path (elementObbCorners,
marqueeIntersectsObb); AABB matches the selection box, which never rotated.

Adds dev-only [hf-marquee:*] tracing (per-element rect + intersect + skip
reason, JSON) to debug what the marquee sees; stripped from prod builds.

* fix(studio): off-canvas elements no longer render a selection-style border

OffCanvasIndicators drew two layers per partly-off-screen element: a dashed
sliver on the protruding part, plus a solid studio-accent border (with the
selection box-shadow) over the on-canvas portion. That solid border only
ever draws for UNSELECTED elements (selected ones get a real selection box
via the filter), so an unselected off-canvas element looked selected.
Removed the solid inside layer — the dashed protruding sliver stays as the
off-canvas hint.

* chore(studio): remove [hf-marquee:*] debug logs

Marquee position fix is verified; strip the dev-only tracing scaffolding
(logMarquee/debugLabel/debug param) back to the lean intersection loop.

* fix(studio): convert a global gsap.set to a seekable timeline tween + review cleanups

Primary fix: converting a global `gsap.set` to keyframes flipped only the
method (set->to), leaving the callee object `gsap` — emitting `gsap.to(...)`,
an off-timeline tween that fires once at load and isn't on the paused master
`window.__timelines` (the engine can't seek/render it). Reachable from the
cube's keyframe toggle + maybeAutoKeyframeSet on the global sets commitStaticSet
creates. Now re-roots onto the timeline var and adds the position arg, in both
the recast and acorn writers; covered by a convert test seeded from gsap.set in
each path.

Review cleanups: drop dead confirmDelete/<DeleteConfirm> in AudioRow; drop the
always-zero viewRx/viewRy camera params from the 3D projection; un-export four
internal-only symbols (clears fallow unused-exports); re-add the collectMarqueeHits
complexity suppression dropped with the debug scaffolding.

* chore(studio): green the CI gate + 3D panel expanded by default

- File-size: extract the marquee/candidate render into MarqueeOverlay so
  DomEditOverlay drops back under the 600-line cap.
- Fallow complexity: suppress the 8 accepted-complexity findings from the 3D/
  runtime work (resolveRuntimeTween, readRuntimeKeyframes, hasNonHoldTweenForElement,
  commitKeyframeProps, scored, ImageCard, selectionShapeStyles, off-canvas effect)
  with the bare directive the linter recognizes.
- 3D transform panel now defaults to expanded (the cube gizmo is the headline).
2026-06-25 18:54:06 -04:00
Miguel Ángel 690cf1b7a5 fix(producer): stop retrying capture attempts that made zero progress (#1725)
* fix(producer): stop retrying capture attempts that made zero progress

A structurally broken composition (never-ready page, zero duration, or
unparseable HTML) captures no frames, so the adaptive retry loop kept
re-running it at halved parallelism — 16->8->4->2->1 workers — each attempt
burning a full readiness/protocol timeout per worker. That multiplied
wall-clock to ~46min on broken renders and was the driver of the render
P95 blowup (~370k -> 2.79M ms) seen Jun 20-22.

Add captureAttemptMadeProgress(): when an attempt leaves at least as many
frames missing as it set out to capture, it made no forward progress, so the
composition is broken rather than the workers being flaky. Bail immediately
instead of retrying. A partially-captured attempt still retries, so genuine
flaky-worker gaps are unaffected.

* fix(producer): log the zero-progress bail + cover it with an integration test

Address review feedback on the no-progress capture guard:

- Warn before bailing so an oncall can tell a structurally-broken render that
  bailed fast apart from one that exhausted worker-halving retries (both
  previously threw the same "frame(s) are missing" message).
- Add an integration test that drives executeDiskCaptureWithAdaptiveRetry
  through the bail (capture functions mocked to write nothing) and asserts a
  single attempt runs — the gate would otherwise walk 4->2->1 workers. Guards
  the placement of the gate, not just the predicate.
- Reword the helper docstring (drop stray prefix and internal incident detail).
2026-06-25 17:50:57 -04:00
ab818a2f1d feat(registry): add lower thirds catalog blocks (#1134)
Adds news ticker from #1134, the podcast/interview lower-thirds pack from #1689,
and the BILD-style lower third from #1276.

Also adds generated catalog pages for flowchart-vertical and vfx-liquid-glass
from #1525, plus a Lower Thirds catalog group for discovery.

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Kiyeon Jeon <kiyeonjeon21@users.noreply.github.com>
Co-authored-by: sunlesshalo <198846711+sunlesshalo@users.noreply.github.com>
Co-authored-by: Moritz <moritz.wedel@axelspringer.com>
Co-authored-by: Claude Sonnet <noreply@anthropic.com>
Co-authored-by: Dashsoap <42135402+Dashsoap@users.noreply.github.com>
2026-06-25 16:53:44 -04:00
James Russo 0c05025c04 ci(skills): run skills/**/*.test.mjs in CI (#1724)
skills/**/*.test.mjs files (e.g. skills/media-use/scripts/resolve.test.mjs
and skills/media-use/scripts/lib/manifest.test.mjs) are bare `node --test`
files with only `node:` built-in imports. They aren't part of any workspace
package, and the existing `Test` job's path filter (the `code` filter in
the `changes` job) excludes `skills/**`, so even on PRs that touch only
skills/ those tests never run.

This matters for regression guards. The shell-injection probe test added in
HF#1723 feeds probe() a filename containing `clip"; touch INJECTED; echo
".mp4` and asserts no marker file is created. The test passes locally but
under the current job graph it would never run in CI on a follow-up skills/
change that re-introduces the bug.

Closing the gap with a dedicated `Test: skills` job rather than relaxing
the `code` filter. The existing `Test` job's steps run `bun run test:scripts`
(hardcoded file list) and `bun run --filter '*' test` (workspace packages
only), neither of which would actually execute skills tests even if the
filter let `skills/**` through. The dedicated job needs no `bun install`,
just node 22, since the tests only import from `node:` and relative paths.

The discovery step shells out to `find` and fails loudly when zero test
files match, so a future rename or layout change can't silently turn this
into a no-op pass.

Spotted by Via in HF#1723 review thread, confirmed by James as a separate
follow-up rather than a blocker for HF#1723.

--
Jerrai (https://claude.com/claude-code)
2026-06-25 12:50:43 -07:00
James RussoandClaude Opus 4.8 7517f6ac86 feat(slideshow): per-slide autoplay (manual-advance, opt-in) (#1708)
* feat(slideshow): per-slide autoplay (manual-advance, opt-in)

Adds an opt-in `autoplay` flag to slideshow slides: when the presenter lands
on a video slide, its `<video>` plays from the start. The slideshow still
holds and never auto-advances — the presenter clicks Next when ready. This
covers compositions whose own controls can't be clicked (the player renders
the composition pointer-events:none).

Plumbing (done, tested):
- core: `SlideRef.autoplay?: boolean`, parsed + validated in parseSlideshow
  (a non-boolean autoplay rejects the manifest); carried through resolve.
- controller: optional `PlayerPort.playSceneMedia(sceneId)`, fired only on
  forward `enterSlide` for autoplay slides (not resume/back/sync, so the
  audience — which mirrors the presenter's media events — isn't double-driven).
- component: `playSceneDocumentMedia` reaches the same-origin composition
  iframe, finds the scene's `<video>`, and asserts playback; `stopMedia`
  (already wired on slide change) resets it. An autoplay token cancels a
  pending start when the slide changes.
- tests: controller autoplay behavior + parser flag round-trip/validation
  (131 player + 22 core slideshow tests pass).

KNOWN LIMITATION — runtime media-start needs the player media model (@vance):
On current main the clip<->timeline binding from #1601 keeps every clip synced
and *paused* to the held timeline frame, which wins against playSceneMedia's
play() — so the clip does not actually start on main yet (it does on the
pre-#1601 player). The correct fix is a sanctioned "let this clip free-run
while the timeline holds" path in the player/runtime media controller. Flagging
for Vance to wire the start into the #1601 media model (or rebase onto it) when
back. The plumbing above is the stable surface that hook plugs into.

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

* refactor(slideshow): address code-review on per-slide autoplay

- guard playSceneDocumentMedia behind resolveMode() !== "audience": the
  audience mirrors the presenter's media events, so it must not independently
  drive its own copy of the clip.
- drop the per-enter window pointerdown/keydown "gesture retry" listeners,
  which leaked when muted autoplay succeeded without a gesture. The poll already
  re-asserts play(), so a gesture within the window is picked up next tick.
- stop polling once the clip is advancing across two ticks (was re-asserting
  play() for the full window even after playback was confirmed).
- cancel any in-flight autoplay loop on disconnectedCallback (bump the token).
- split the poll into findSceneVideo + stepAutoplay helpers (keeps each small).
- fix the enterSlide comment: autoplay fires from enterSlide (next/prev/
  goToSlide), not resumeSlide (back/backToMain/syncTo).
- parser: isOptionalBoolean type guard instead of a one-off helper; drop `as`
  assertions in the new controller test.

131 player + 22 core slideshow tests pass; lint/format/typecheck/fallow clean.

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

* docs(slideshow): autoplay skill guidance + address review nits

Addresses review feedback on #1708:
- skill: document per-slide `autoplay` in the slideshow standalone-harness
  reference — when to use it (video is the slide's primary content, its end is
  the advance cue) vs not (background/ambient loops, footage talked over), per
  Vance's guidance, before merge.
- play() rejection is no longer blanket-swallowed: AbortError (timeline-sync
  seek interrupt) and NotAllowedError (gesture-gated autoplay) are expected and
  ignored; any other rejection is surfaced once via console.warn (Via nit 1).
- clarify in the SlideRef.autoplay doc that it plays the scene's FIRST <video>
  (Via nit 2).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 12:47:54 -07:00
Miguel Ángel 041f2fa196 fix(media-use): kill shell command injection in probe/heygen-search/eval
Swap execSync(<shell-string>) → execFileSync(file, [argv]) in probe.mjs, heygen-search.mjs, and eval.mjs so hostile filenames / queries / manifest metadata can't inject shell. Adds probe.test.mjs regression guard and a CI Test (skills) job so it actually runs. Closes the media-use High/Critical scanner alert.
2026-06-25 15:16:52 -04:00
James Russo f7bc0384f0 docs: add 19-skills catalog to README, CLAUDE.md, and Mintlify docs (#1722)
* docs: list all 19 skills in README + add CLAUDE.md maintenance reminder

Agents discover skills via the README, so silently-out-of-date entries
kill discovery. This change:

- Adds a `## Skills` section to the README listing all 19 skills,
  grouped Router / Creation workflows / Domain skills, with a one-line
  "use when" blurb for each (sourced from each skill's SKILL.md
  frontmatter `description:`).
- Updates the existing CLAUDE.md `## Skills` section to cover all 19
  skills (was missing the domain skills, `/media-use`, `/slideshow`,
  and `/music-to-video`), mirroring the README's Router / Creation /
  Domain grouping.
- Adds a "Skill catalog maintenance" section to CLAUDE.md so future
  skill additions / renames update both surfaces and the
  `/hyperframes` router skill in lockstep.

Docs-only — no source or test changes.

— Jerrai (https://claude.com/claude-code)

* docs(mintlify): add skills catalog page + extend maintenance reminder

Per follow-up on HF#1722: the Mintlify docs at
hyperframes.heygen.com also need the skills catalog so agent
discoverability is consistent across README and docs site.

- New: docs/guides/skills.mdx (3-group catalog — router / creation
  workflows / domain skills — mirrors README structure, sourced from
  the same SKILL.md frontmatter)
- Update: docs/quickstart.mdx — completes the workflow-skills list
  (was missing /music-to-video, /slideshow, /general-video) and
  cross-links the new page
- Update: docs/introduction.mdx — adds a skills-catalog card to the
  hero CardGroup and the Next Steps section
- Update: docs/docs.json — adds /guides/skills to the Guides nav
- Update: CLAUDE.md "Skill catalog maintenance" — adds
  docs/guides/skills.mdx as the third sync target alongside README
  and skills/hyperframes/SKILL.md, and notes the count drift surface
  (README + CLAUDE.md mention "19 AI agent skills" in their intros;
  the new docs page deliberately omits a count to avoid drift)

Docs-only — no source, packages, or test changes.

— Jerrai (https://claude.com/claude-code)

* docs(readme): oxfmt table column-alignment fix

Pure whitespace — oxfmt's table-column alignment caught README.md
after the previous commit. No content change.

— Jerrai (https://claude.com/claude-code)

* docs(skills): reconcile install-command contract across README/CLAUDE/Mintlify

Per Magi's review on HF#1722: the new README/CLAUDE/skills.mdx pages
described bare `npx skills add heygen-com/hyperframes` as installing all
19 skills, while existing quickstart/prompting docs said the bare command
opens a picker and `--all` installs everything.

Verified actual CLI behavior with `npx skills add --help` and a clean-dir
run: bare command opens an interactive picker for human users (the CLI
help documents `--all` as "Shorthand for --skill '*' --agent '*' -y" —
the picker-skipping form). Inside an agent the bare command auto-installs
all non-interactively, but that's an agent-detection UX shortcut, not the
public contract — documenting the picker is correct for human readers.

All touched docs now use the consistent contract:
  - `npx skills add heygen-com/hyperframes`               -> interactive picker
  - `npx skills add heygen-com/hyperframes --all`         -> install all 19 (skips picker)
  - `npx skills add heygen-com/hyperframes --skill <name>` -> install just one

Files updated: README.md, CLAUDE.md, docs/guides/skills.mdx. Existing
docs/quickstart.mdx and docs/guides/prompting.mdx already used this
contract and are unchanged.

— Jerrai (https://claude.com/claude-code)
2026-06-25 12:12:44 -07:00
56859b618f refactor(skills): rename graphic-overlays skill to talking-head-recut (#1720)
Rename the `graphic-overlays` workflow skill to `talking-head-recut`:

- move skills/graphic-overlays/ -> skills/talking-head-recut/
- update SKILL.md frontmatter name, H1, and self-references
- update all /graphic-overlays route references (hyperframes router,
  general-video, root + cli-template AGENTS.md/CLAUDE.md, docs, quickstart)
- update telemetry --skill flag, example composition id, timeline key
- update .prettierignore path and scripts/test-skills-fresh.sh

Identifier-only rename: the graphic-overlay card mechanism, design
references, and trigger wording are unchanged.

Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 00:32:51 +08:00
Miguel Ángel 1494f715fb chore: release v0.7.7 (#1719) v0.7.7 2026-06-25 11:54:10 -04:00
Miguel Ángel 96ab4b18a4 fix(plugin): avoid high compression silence fixture (#1717) 2026-06-25 11:20:11 -04:00
Miguel Ángel 0558b8761e fix(producer): retry probe navigation timeouts (#1713) 2026-06-25 10:52:38 -04:00
Vance Ingalls 64eaad7d69 feat(slideshow): auto-set interactive on inner player (#1712)
* feat(slideshow): auto-set interactive on inner player

The slideshow now sets the `interactive` attribute on its inner
<hyperframes-player> instances at mount time, so pointer events
reach the composition iframe automatically. Removes the
agent-compliance burden of having to remember to add `interactive`
on every player tag inside a slideshow.

Idempotent: an author-supplied `interactive` attribute (any value,
including `interactive="false"`) is preserved. A MutationObserver
also picks up players inserted dynamically after the initial mount.

Standalone player usage outside a slideshow still requires the
explicit attribute — that surface is unchanged.

Skill guidance at skills/slideshow/SKILL.md updated to reflect the
automatic behavior.

* docs(slideshow): clarify interactive attribute semantics

Per Rames R1 review feedback: the test comment implied
`interactive="false"` is an author opt-out, but `:host([interactive])`
is presence-matching per HTML boolean-attribute convention — so any
value (including "false") enables pointer events at runtime. The
slideshow's mechanical wire-up preserves any author-supplied value
verbatim for DOM hygiene, not as a runtime opt-out.
2026-06-24 21:31:39 -07:00
Miguel Ángel 364992203e feat(studio): motion editing — speed-curve editor, class-tween attribution, per-keyframe size & ease (#1705)
Speed-curve editor: a fixed-square cubic-bezier graph (grid, linear reference,
draggable handles, live preview) for editing eases; conventional preset grid.

Class/selector tweens: attribute `gsap.from(".dot", …)`-style tweens to every
matching element so they surface in the inspector and keep their timeline
keyframe diamonds when the clip is selected.

Apply-to-all easing: a "Set all…" control sets easeEach and strips every
per-keyframe ease override in one mutation (AE select-all + F9). Implemented in
BOTH gsap writers — the acorn writer and the recast writer (the default server
path); the recast side was missing resetKeyframeEases, so "Set all" set easeEach
but left per-keyframe eases in place.

Per-keyframe size: resizing an animated element writes a width/height keyframe
at the playhead — other keyframes keep their size — instead of a global
gsap.set hold; static elements keep the simple global resize. The extra size
tween exposed a motion-path bug (the overlay read whichever tween contained the
playhead), fixed with an opt-in requireChannels filter so the path only reads
the positional tween.

Inferred Timing: derive Start/End/Duration from an element's animations when it
has no authored clip range, instead of showing 0.00s.

Ease labels now surface the raw GSAP token (power2.out, back.out, …) instead of
invented names ("Smooth slowdown") that confused authors.

Also pass the preview iframe to the inspector's animation hook so element
resolution runs, and remove the unused editDebugLog facility.
2026-06-24 23:38:13 -04:00
Miguel ÁngelandClaude Opus 4.8 814c96cefa fix(skills): make media-use frontmatter valid YAML so skills add works (#1709)
The `media-use` SKILL.md `description:` was an unquoted YAML scalar containing
a mid-value `: ` (`...the full cascade: project cache...`). YAML 1.2 reads
that as a nested mapping and the parse fails with "Nested mappings are not
allowed in compact mappings". `skills add` aborts the entire install when any
one skill fails to parse, so this single file blocked installing all 19
skills for everyone following the README's `npx skills add heygen-com/hyperframes`.

- Replace the offending `: ` with ` — ` (keeps the plain-scalar style used by
  the other 18 skills; the description already uses `—` as a separator).
- Add a frontmatter guard to scripts/lint-skills.ts that flags unquoted
  top-level scalars containing `: ` — the exact ambiguity — so this can't
  regress. No new dependency.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 22:33:28 -04:00
Miguel ÁngelandClaude Opus 4.8 34bb496566 test(media-use): resolve tests + eval harness (#1685)
* feat(media-use): core infrastructure — manifest, cache, adopt, probe

Foundation for media-use — the media resolution layer for HyperFrames.

- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)

* fix(media-use): oxfmt formatting + cap freeze download size

Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).

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

* feat(media-use): resolve engine + all providers + brand from frame.md

- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references

* fix(media-use): oxfmt formatting for resolve + providers

Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).

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

* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)

Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:

- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
  (the allowlisted flag the CLI added for media-use in v0.1.6). The old
  `--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
  `asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
  `asset search` backend rejects it and returns no score field, so only the
  audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
  are .png (not .svg), some BGM is .wav (not .mp3).

Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.

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

* feat(studio): redesign Asset tab + fix beat analysis auto-trigger

Asset tab: categorized sections, filter chips, text search, audio
spectrum visualizer, "in use" badge, manifest metadata, panel tokens.

Beat fix: only run analysis when a beats file exists on disk.

* fix(studio): oxfmt formatting for AssetsTab

CI oxfmt --check gate.

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

* test(media-use): resolve tests + eval harness

12 resolve engine tests + eval against 7 real registry blocks.

* fix(media-use): oxfmt formatting for eval + resolve tests

CI oxfmt --check gate.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:29:40 -04:00
Miguel ÁngelandClaude Opus 4.8 92befea305 feat(studio): redesign Asset tab + fix beat analysis auto-trigger (#1684)
* feat(media-use): core infrastructure — manifest, cache, adopt, probe

Foundation for media-use — the media resolution layer for HyperFrames.

- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)

* fix(media-use): oxfmt formatting + cap freeze download size

Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).

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

* feat(media-use): resolve engine + all providers + brand from frame.md

- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references

* fix(media-use): oxfmt formatting for resolve + providers

Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).

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

* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)

Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:

- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
  (the allowlisted flag the CLI added for media-use in v0.1.6). The old
  `--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
  `asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
  `asset search` backend rejects it and returns no score field, so only the
  audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
  are .png (not .svg), some BGM is .wav (not .mp3).

Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.

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

* feat(studio): redesign Asset tab + fix beat analysis auto-trigger

Asset tab: categorized sections, filter chips, text search, audio
spectrum visualizer, "in use" badge, manifest metadata, panel tokens.

Beat fix: only run analysis when a beats file exists on disk.

* fix(studio): oxfmt formatting for AssetsTab

CI oxfmt --check gate.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:29:22 -04:00
Miguel ÁngelandClaude Opus 4.8 b2dc353725 feat(media-use): resolve engine + BGM/SFX/image/icon providers (#1683)
* feat(media-use): core infrastructure — manifest, cache, adopt, probe

Foundation for media-use — the media resolution layer for HyperFrames.

- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)

* fix(media-use): oxfmt formatting + cap freeze download size

Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).

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

* feat(media-use): resolve engine + all providers + brand from frame.md

- resolve.mjs: cheapest-first cascade
- BGM/SFX via heygen --headers, Image/Icon via heygen asset search
- Brand tokens from frame.md / design.md (local, no API)
- SKILL.md: full agent docs + hyperframes.dev/design redirect
- Router skill + workflow skill references

* fix(media-use): oxfmt formatting for resolve + providers

Format brand/heygen-search/providers/sfx providers + resolve.mjs
(CI oxfmt --check gate).

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

* fix(media-use): align providers with the real heygen CLI surface (v0.1.6)

Verified live against the official Go `heygen` CLI v0.1.6 with a valid key:

- Caller attribution: pass `--headers 'X-HeyGen-Client-Source: media-use'`
  (the allowlisted flag the CLI added for media-use in v0.1.6). The old
  `--x-source media-use` was never a real flag and broke every call.
- Command is `asset search` (the `list` leaf was dropped in v0.1.6), not
  `asset search list`.
- `--min-score` is sent server-side: honored by `audio sounds list`, but the
  `asset search` backend rejects it and returns no score field, so only the
  audio providers pass it (image/icon don't).
- Drop hardcoded `ext` so resolve.mjs derives it from the URL: catalog icons
  are .png (not .svg), some BGM is .wav (not .mp3).

Also: surface CLI/auth failures on stderr instead of swallowing them as
'no results', carry icon width/height through, and document the heygen CLI
install + >= v0.1.6 requirement.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:29:04 -04:00
Miguel ÁngelandClaude Opus 4.8 73f6d3e5be feat(media-use): core infrastructure — manifest, cache, adopt, probe (#1682)
* feat(media-use): core infrastructure — manifest, cache, adopt, probe

Foundation for media-use — the media resolution layer for HyperFrames.

- manifest.mjs: JSONL read/write/find for .media/manifest.jsonl
- index-gen.mjs: regenerate agent-readable index.md from manifest
- cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel)
- freeze.mjs: download URL or copy local file to .media/
- probe.mjs: extract duration/dimensions via ffprobe
- adopt.mjs: scan assets/ directory, register existing files with metadata
- 19 passing tests (manifest round-trip, cache, promote, index generation)

* fix(media-use): oxfmt formatting + cap freeze download size

Format adopt/cache/probe/manifest.test (CI oxfmt --check gate).
Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill
the disk (addresses CodeQL #670: network data written to file).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:28:38 -04:00
Miguel Ángel eee376cb5e chore: release v0.7.6 (#1707) v0.7.6 2026-06-24 20:17:20 -04:00
Miguel Ángel 9bf1e1c298 fix(runtime): keep stamped flow children in document flow in preview (#1702)
Studio/preview stamps `data-start` onto ID'd and GSAP-targeted flow
children (eg. a <header>/<footer> in a flex column) so the design panel
can discover them. applyClipLayout then force-absolutized those stamped
elements as if they were authored overlay clips, collapsing the layout:
the footer shrink-wrapped and its `justify-content: space-between`
clustered into the top-left, while the rendered video — which never
stamps (production renders run as the top-level page, not in an iframe) —
stayed correct.

Mark runtime-stamped clips with `data-hf-autostamped` and skip them in
applyClipLayout so they remain in document flow. The preview now matches
the rendered video (true WYSIWYG). Authored overlay clips are unchanged,
so the golden regression suite is unaffected.
2026-06-24 20:12:02 -04:00
James RussoandClaude Opus 4.8 1c389983de fix(cli): ship player + slideshow bundles so present/play work from npm (#1706)
`present` and `play` render compositions in the standalone browser player,
resolving the player/slideshow IIFE bundles via resolvePlayerPath /
resolveSlideshowPath. Those resolvers look for the bundles alongside the built
CLI (dist/hyperframes-player.global.js, dist/hyperframes-slideshow.global.js),
but build-copy.mjs never staged them into dist/. The remaining candidate paths
are monorepo-dev only, so an npm install has nothing to resolve.

Result: `npx hyperframes present` always failed with
"@hyperframes/player not found", forcing users to run the presenter from a
monorepo checkout.

Copy both player globals from packages/player/dist into the CLI dist during
build:copy (existsSync-guarded + warn, matching the surrounding pattern). The
runtime bundle is already handled by build:runtime. Verified: the globals now
appear in `npm pack`, and `node dist/cli.js present` starts without the
player-not-found error.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:57:46 -07:00
Miguel Ángel 89ff299a11 fix(engine): defend macOS regular Chrome screenshots
Fixes #1699.
2026-06-24 19:21:46 -04:00
Matt Van HornandMatt Van Horn 821bf2921a feat(cli): export .srt/.vtt caption sidecars from a transcript (#1704)
Add formatSrt/formatVtt/wordsToCues to normalize.ts (the inverse of the
existing parseSrt/parseVtt) and a 'hyperframes transcribe <transcript> --to
srt|vtt' export mode. Word-level whisper transcripts group into cues on
sentence boundaries with maxChars/maxGap guards; imported phrase-level cues
pass through unchanged. Default transcribe behavior is unchanged and no new
dependencies are added.

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-24 19:03:16 -04:00
Miguel Ángel 97db811a2f feat(studio): per-keyframe ease presets, velocity fitting, gesture smoothing (#1694)
Per-keyframe speed-curve editing, velocity-based ease fitting, and Gaussian gesture smoothing. Easy Ease presets, per-segment KeyframeEaseList with a bezier editor, AE-convention ease fitting, position-only set-tween rows, and AnimationCard extraction.
2026-06-24 18:43:37 -04:00
Miguel ÁngelandClaude Opus 4.8 8ae010bf51 feat(studio): marquee multi-selection + off-canvas indicators (#1693)
* chore(studio): remove all console.* calls from studio package

* chore(studio): address review — remove dead stubs, restore consent notice

- Delete empty if-blocks left after console removal (snapTargetCollection,
  Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording
  dev guard + now-unused isDevBuild) and the stale "surface in dev" comment.
- Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher.
- Restore the one-time telemetry consent disclosure in showNoticeOnce (kept
  behind a pragma — it is a user-facing notice, not debug noise).
- Remove the missed timelineIcons console.warn while preserving the
  `tag || "div"` null-safety fallback.
- Route caption auto-save failures (a data-loss path) through telemetry
  instead of swallowing silently.
- Restore the accidentally-clobbered css-var-fonts output.mp4 fixture.

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

* fix(runtime): immediateRender for set tweens + array timeline normalization

- Set tweens now emit immediateRender:true so they render on page load
  without requiring the runtime to seek past position 0
- Runtime IIFE normalizes array timelines (window.__timelines = [tl])
  to keyed objects, and auto-adds data-start on root elements
- Drag teardown clears translate:none to prevent #1673 fly-off
- Position-only set tweens hidden from timeline diamonds (3 cache paths)
- Parser: ease-only keyframe update preserves existing properties

* fix(runtime): address review — restore perf gate, debug surface, scrub restore

- Restore the #1651 skipForInjectedVideo gate in media.ts that was dropped on
  restack — avoids ~2400 wasted per-tick seeks on video-heavy renders.
- Restore the console.debug body + docstring bullet of swallow() in
  diagnostics.ts: the __hfDebug opt-in debug surface had been gutted to an
  empty if-block.
- Rebind: after the progress-cycle set() kick, seek to state.currentTime via
  totalTime() instead of snapping to 0, so a rebind after scrub / soft-reload
  restore keeps the playhead.
- Array __timelines normalization + data-start default now resolve the root
  via a shared findRootCompositionEl() that honors data-root="true" first
  (matches resolveRootCompositionElement, which now delegates to it).
- Ease-only keyframe update leaves a primitive (non-object) keyframe value
  untouched instead of wiping it to {}; add a preservation unit test.
- Document the boundDuration<=0 progress(1) kick + restore the STATIC-case
  comment in gsapRuntimeBridge.

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

* feat(studio): marquee multi-selection + off-canvas indicators

- Click+drag on empty canvas draws dashed selection rectangle
- SAT/OBB intersection handles rotated/scaled/skewed elements
- Shift+marquee adds to existing selection
- Click on empty canvas deselects
- Off-canvas elements show dashed outline indicators (clickable)
- Dashed border only shows outside canvas, solid inside (clip-path)
- 12 geometry unit tests

* feat(studio): address review — group-aware off-canvas indicators + fixes

- Off-canvas indicator suppression now skips every selected element (primary
  AND marquee group members), not just the primary, so group members no longer
  render a doubled overlay (group rect + dashed indicator).
- Drop selection from the off-canvas layout effect deps; the selected-element
  filter runs at render time. Avoids re-walking geometry on each selection change.
- applyMarqueeSelection now honors STUDIO_INSPECTOR_PANELS_ENABLED.
- Restore the stale-selection clear in useDomEditPreviewSync when the selected
  element no longer resolves after a re-sync. Drag-release stays handled by
  suppressNextBoxClickRef.
- Off-canvas indicator is keyboard-accessible; canvas cursor driven by marquee
  rect state, not a render-time ref read.
- Rename partiallyOutside -> extendsOutsideComp + comment the clip-path hit-test.
- Extract OffCanvasIndicators into its own component (DomEditOverlay was already
  over the 600-LOC cap on this branch; extraction brings it under).
- Declare onUpdateKeyframeEase on PropertyPanelProps so this branch typechecks
  standalone (handler + wiring already here; only the type had leaked upstack).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:53:18 -04:00
Miguel ÁngelandClaude Opus 4.8 6987447a75 fix(runtime): immediateRender for set tweens + array timeline normalization (#1692)
* chore(studio): remove all console.* calls from studio package

* chore(studio): address review — remove dead stubs, restore consent notice

- Delete empty if-blocks left after console removal (snapTargetCollection,
  Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording
  dev guard + now-unused isDevBuild) and the stale "surface in dev" comment.
- Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher.
- Restore the one-time telemetry consent disclosure in showNoticeOnce (kept
  behind a pragma — it is a user-facing notice, not debug noise).
- Remove the missed timelineIcons console.warn while preserving the
  `tag || "div"` null-safety fallback.
- Route caption auto-save failures (a data-loss path) through telemetry
  instead of swallowing silently.
- Restore the accidentally-clobbered css-var-fonts output.mp4 fixture.

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

* fix(runtime): immediateRender for set tweens + array timeline normalization

- Set tweens now emit immediateRender:true so they render on page load
  without requiring the runtime to seek past position 0
- Runtime IIFE normalizes array timelines (window.__timelines = [tl])
  to keyed objects, and auto-adds data-start on root elements
- Drag teardown clears translate:none to prevent #1673 fly-off
- Position-only set tweens hidden from timeline diamonds (3 cache paths)
- Parser: ease-only keyframe update preserves existing properties

* fix(runtime): address review — restore perf gate, debug surface, scrub restore

- Restore the #1651 skipForInjectedVideo gate in media.ts that was dropped on
  restack — avoids ~2400 wasted per-tick seeks on video-heavy renders.
- Restore the console.debug body + docstring bullet of swallow() in
  diagnostics.ts: the __hfDebug opt-in debug surface had been gutted to an
  empty if-block.
- Rebind: after the progress-cycle set() kick, seek to state.currentTime via
  totalTime() instead of snapping to 0, so a rebind after scrub / soft-reload
  restore keeps the playhead.
- Array __timelines normalization + data-start default now resolve the root
  via a shared findRootCompositionEl() that honors data-root="true" first
  (matches resolveRootCompositionElement, which now delegates to it).
- Ease-only keyframe update leaves a primitive (non-object) keyframe value
  untouched instead of wiping it to {}; add a preservation unit test.
- Document the boundDuration<=0 progress(1) kick + restore the STATIC-case
  comment in gsapRuntimeBridge.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:53:02 -04:00
Miguel ÁngelandClaude Opus 4.8 adb40321d6 chore(studio): remove all console.* calls from studio package (#1691)
* chore(studio): remove all console.* calls from studio package

* chore(studio): address review — remove dead stubs, restore consent notice

- Delete empty if-blocks left after console removal (snapTargetCollection,
  Player asset-poll, useTimelineSyncCallbacks 5s probe, useGestureRecording
  dev guard + now-unused isDevBuild) and the stale "surface in dev" comment.
- Drop the dangling no-console pragma + dead duplicate-id branch in sourcePatcher.
- Restore the one-time telemetry consent disclosure in showNoticeOnce (kept
  behind a pragma — it is a user-facing notice, not debug noise).
- Remove the missed timelineIcons console.warn while preserving the
  `tag || "div"` null-safety fallback.
- Route caption auto-save failures (a data-loss path) through telemetry
  instead of swallowing silently.
- Restore the accidentally-clobbered css-var-fonts output.mp4 fixture.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:49:36 -04:00
WaterrrForeverandClaude Opus 4.8 ae8b94c518 docs(skills): route /slideshow as a workflow, not a domain capability (#1701)
* docs(hyperframes): route /slideshow as a workflow, not a capability

Move /slideshow out of the domain-skill capability map and into the
intent router as a top-level workflow. It is an intent-gated orchestration
that produces a navigable deck, not an atomic capability loaded on demand.

Also clarify that workflows need not output a video: /slideshow builds a
deck and /remotion-to-hyperframes ports a composition. Adds the cheat-sheet
row, a disambiguation bullet, and the per-workflow detail block.

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

* docs(hyperframes): sharpen /slideshow disambiguation to match its intent gate

Reword the slideshow disambiguation bullet to match the skill's actual
intent-confirmation behavior: an explicit "slideshow" request proceeds
directly; an adjacent trigger ("deck / slides / presentation / convert this
page") makes /slideshow confirm before authoring and switch to the
appropriate non-slideshow workflow if not. Drops the inaccurate "may
actually want a video" narrowing.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:04:24 +08:00
WaterrrForeverandClaude Opus 4.8 54cab331d0 feat(cli): shared TTS/BGM auth preflight + caption and skill-workflow fixes (#1697)
* fix: handle caption skin workflow

* docs(skills): simplify the finalize step across video workflows

- Drop --strict-layout; all skills use plain `hyperframes inspect`
- Add the caption text_box_overflow false-positive note to faceless-explainer
- On a failed check, the orchestrator makes the cheapest safe edit itself
  (no worker re-dispatch / Step 3 backtrack language)
- Snapshot: glance at the stitched contact-sheet.jpg and move on

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

* feat(auth): onboarding-first `auth status` + shared TTS/BGM preflight

When no HeyGen credential is configured, `hyperframes auth status` now
prints registration-first guidance instead of a terse error:

- Interactive / agent-driven sessions get sign-in guidance led by
  `hyperframes auth login` (the OAuth step that also creates an account
  and is shared with heygen-cli), and never steer users to a per-repo
  `.env`. CI / non-interactive runs get a terse note. Exit 1 is kept so
  the "am I logged in?" `$?` contract still holds.
- It probes which local engine voice/music will fall back to (Kokoro /
  MusicGen, mirroring the skill resolution order) and whether their
  Python deps are installed, with a pip hint when missing. `--json`
  exposes `recommended_action` + `offline_engines` for skills to branch.
- `doctor` gains matching "TTS (Kokoro)" / "BGM (MusicGen)" checks via
  the same shared probe (findPython/hasPythonModules extracted to
  tts/python.ts; provider resolution in audio/providers.ts).

Every TTS/BGM workflow now relays this at Step 0 (setup) instead of
improvising its own "missing key" prompt: pr-to-video, product-launch-
video, faceless-explainer, website-to-video, music-to-video. The
canonical behavior + key-priority table live once in hyperframes-media.

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

* feat(pr-to-video): scale recommended video length to PR change size

Step 0 led with a fixed ~60-90s length default. Now the recommended
length is derived from the PR's diff stat (lines added+deleted, nudged
by file count) on a tier scale (trivial ~20-40s → large ~110-180s, hard
cap ~3 min), reusing the same PR peek already done to infer the angle.
The agent states the basis when proposing it, and a huge PR with one
headline change still stays tight. User can always override.

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

* fix(captions): embed brand fonts whose files use separators

brandFontFaces() matched font files by stripping only whitespace, so an
underscore/hyphen-named file (TT_Norms_Pro_Bold.woff2) never matched the
family key "ttnormspro" — captions shipped with no @font-face, the
font_family_without_font_face bug. Now both family and filename normalize
away all non-alphanumerics; families match longest-key-first so a parent
family can't swallow a more specific one's files (TT Norms Pro vs Mono);
each file is claimed once; "demibold" ranks before "bold"; and when
nothing matches it warns loudly at build time instead of returning "".

Also: parseFonts() falls back to h1/h2/title/hero display roles, and the
frame-worker + caption authoring docs spell out that only shipped font
files render — no system CJK/Devanagari families on the headless renderer.

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

* fix(hyperframes-media): enforce sign-in preflight on standalone BGM/TTS

A one-off "generate me a BGM" request went straight to local MusicGen
without recommending sign-in: bgm.md/tts.md framed the no-credential path
as an automatic fallback, so the generation path bypassed the Preflight
stop, and the preflight used a bare `hyperframes auth status` that isn't
on PATH in a fresh `npx skills` project.

- Preflight now applies to one-off generation as well as workflows, uses
  `npx hyperframes auth status`, and says: if the CLI can't run, still
  recommend signing in and STOP — never treat "no credential" as a silent
  green light for local generation.
- bgm.md and tts.md point at the Preflight before generating, reframing
  local generation as the fallback the user opts into, not a default.

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

* docs(auth): add Authentication & API keys guide

Document signing in, the keys each capability (voice, music, capture)
uses, their resolution priority, and the fully local fallback. Add the
guide to the nav and cross-link it from the cloud deploy note and the
CLI env-var reference.

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

* fix(lint): strip HTML comments in a fixpoint loop (CodeQL)

Single-pass <!-- --> removal can re-form a complete comment from
adjacent markers (e.g. `<<!-- -->!-- ... -->`), letting a decoy
<template> survive and hijack the template-boundary match. Loop to a
fixpoint, mirroring the captions.mjs precedent; add a regression test
that fails on single-pass (2 root findings) and passes on the loop.

Also wrap the build-frame.mjs node:fs imports to satisfy oxfmt — the
new copyFileSync import pushed the line past the width limit, which
was the sole cause of the Format / Preflight CI failures.

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

* fix(lint): strip HTML comments with a linear scan (CodeQL ReDoS)

The fixpoint loop still ran a /<!--[\s\S]*?-->/ regex per pass, which
backtracks O(n^2) on inputs with many unterminated "<!--" — CodeQL
js/polynomial-redos (high). Looping the same regex (the prescribed
fix) never addressed this; only the regex itself does.

Replace it with an indexOf-based linear strip in utils.ts
(stripHtmlComments), kept in a fixpoint loop so markers that re-form
when a comment is removed are still stripped. 200k unterminated
"<!--" now strips in ~3ms instead of quadratic time; behavior is
otherwise unchanged — unterminated comments are kept verbatim, as the
old regex left them. The re-forming regression test still guards it.

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

* fix(auth): make TTS/BGM sign-in guidance accurate and runnable

From team review of the not-signed-in onboarding:

- OAuth is a `hyperframes auth login` feature only. The separate `heygen`
  CLI is API-key-only — `heygen auth login` stores a pasted key, it is not
  OAuth and does not create an account. Stop presenting the two CLIs as the
  same OAuth/sign-up step.
- Use `npx hyperframes` in every imperative and runtime hint. Bare
  `hyperframes` is not on PATH on a fresh machine (command not found); only
  `npx hyperframes` is guaranteed. Also updates the JSON recommended_action.
- Drop `heygen auth login` from the terminal/skill onboarding: it needs its
  own install and there is no `npx heygen`, so it was a command-not-found
  trap. The shared-credential fact stays in the reference docs.

Covers the `auth status` guidance + tests, the Authentication docs, the
shared hyperframes-media preflight (SKILL, requirements, tts, error hints),
and the `npx hyperframes auth status` preflight in every TTS/BGM workflow
(pr-to-video, product-launch-video, faceless-explainer, website-to-video,
music-to-video).

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:20:39 +08:00
James Russo c7b9bf3386 fix(studio): resolve project-root-relative asset URLs in preview iframe (#1698)
## What

Studio preview now resolves `<video src="../../assets/x.mp4">` (and the
same shape for `<img>`, `<audio>`, inline `style` `url()`, and `<style>`
CSS `url()`) against the sub-composition's URL — matching what the
server-side bundler already does for the render path.

## Why

Authored compositions live at `compositions/frames/*.html` and reference
project-root assets either as plain `assets/x.mp4` (already correct
because the main document's `<base href>` points at the project preview
root) or as `../../assets/x.mp4` (the explicit project-root-relative
form). The server-side `inlineSubCompositions` flattens sub-comps into
`index.html` and rewrites the `../`-form against the sub-comp's source
path so it resolves against the project root in the baked render.

The browser-side runtime that mounts external sub-compositions via
`fetch` did no such rewriting. So `<video src="../../assets/x.mp4">`
authored inside a `compositions/frames/scene.html` resolved against the
main document's base href, climbed above the project root, and 404'd in
Studio preview — even though the same path rendered correctly in the
final video. An OSS user (Miao Yang) hit this in a real project.

## How

Added `rewriteSubCompositionAssetPaths` to the runtime
`compositionLoader`. After parsing the fetched sub-composition HTML and
before extracting any nodes, walk the parsed document and rewrite the
same surface the server-side path touches:

- `[src]` and `[href]` attributes on every element
- `[style]` attribute `url(...)` references
- `<style>` element CSS `url(...)` references

The rewrite mirrors the producer's semantics exactly: only values that
start with `../` (or are literal `..`) are rewritten — against the
sub-composition's URL via `new URL(value, compositionUrl)`. Absolute
URLs, root-relative paths, `data:`, hash refs, and plain
`assets/x.mp4` are left untouched. **Plain relative paths must not be
rewritten** because the main document's `<base href>` already covers
them; rewriting would double-prefix the URL.

The walk recurses into `<template>` content because authored
compositions typically wrap their rendered body in a `<template>` and
`querySelectorAll` does not enter template content (it lives in a
detached `DocumentFragment`).

## Test plan

- [x] Unit tests added (6 new tests in
  `compositionLoader.test.ts`): rewrites `../`-traversing src on
  template-wrapped sub-comps; leaves plain relative paths untouched (no
  double-prefix); leaves absolute / data / hash / root-relative URLs
  untouched; rewrites CSS `url()` in `<style>` blocks and inline
  `style` attributes; rewrites for non-template (full-HTML-doc)
  sub-comps.
- [x] Full core test suite green (2065 tests).
- [x] Full studio test suite green (1148 tests).
- [x] Manual verification with the reporter's actual project:
  before the fix one `<video>` with a `../../assets/...` src returned
  `MEDIA_ELEMENT_ERROR: Format error`; after the fix all 7 `<video>`
  elements load (`readyState=4`, correct `currentSrc`). The 6 plain
  `assets/...` paths are *unchanged* (no double-prefix) and continue
  to resolve via `<base href>` as before.
- [x] `bun run lint`, `bun run format:check`, `bun run typecheck`,
  `fallow audit` all green.

Reported by Miao Yang.

— Jerrai (https://claude.com/claude-code)
2026-06-24 07:48:38 -07:00
kiritowooandkiritowoo 5242dde2dc feat(telemetry): attribute renders to the authoring workflow skill (#1695)
* feat(telemetry): attribute renders to the authoring workflow skill

Add an optional `--skill` flag to `hyperframes render` and tag the
`render_complete` / `render_error` events with `authoring_skill`, so render
usage can be broken down per authoring workflow. The value is slug-gated (a
malformed value is ignored) and the existing anonymous / opt-out telemetry
pipeline is otherwise unchanged.

Each end-user workflow that renders now passes `--skill=<name>` on its render
command: embedded-captions, faceless-explainer, graphic-overlays,
motion-graphics, music-to-video, pr-to-video, product-launch-video,
remotion-to-hyperframes, website-to-video.

Not instrumented, by design: general-video renders freeform with no canonical
render command to attach to, and slideshow produces an interactive deck rather
than a rendered video. Both can follow up if per-skill numbers are wanted.

* fix(telemetry): address review — shared slug util, equals-form flag, invalid-value warning

- Extract the SKILL_SLUG regex + a normalizeSkillSlug() helper into
  telemetry/skill.ts, shared by the `events` and `render` commands (the regex
  was duplicated). `render` adopts normalizeSkillSlug (so it now trims the value,
  matching `events`); `events` references the shared SKILL_SLUG. + unit test.
- `render` warns on a non-empty but invalid --skill value (e.g. a camelCase
  typo) so attribution isn't silently lost — stderr only, never fails the render.
- embedded-captions render script: `--skill embedded-captions` -> `--skill=embedded-captions`.
  On an older CLI that does not declare --skill, the space form leaks the value
  as a positional and clobbers the project dir (resolveProject fails); the equals
  form is parsed as a self-delimiting flag and safely ignored. Verified via Node
  parseArgs(strict:false).

Addresses review feedback on the PR (shared util + .trim drift, version-skew
safety, invalid-value visibility).

---------

Co-authored-by: kiritowoo <295860553+kiritowoo@users.noreply.github.com>
2026-06-24 06:16:36 -04:00
Miguel Ángel 649c216394 chore: release v0.7.5 v0.7.5 2026-06-24 04:41:56 +00:00
Miguel Ángel ea23e6309f fix(player): treat runtime timeline as cross-origin ready (#1690) 2026-06-24 00:39:53 -04:00
miga-heygenandClaude Opus 4.6 546b2d770b fix(producer): retry probe stage on transient browser errors (#1688)
* fix(producer): retry probe stage on transient browser errors (#1687)

The distributed render plan stage crashes when headless Chrome encounters
a transient frame detachment ("Navigating frame was detached") during
browser probe, with no retry logic. The plan tarball is never uploaded,
and all downstream chunk workers fail with S3 404.

Add a retry-with-fresh-session mechanism to the probe stage:

- `isTransientBrowserError()` classifier in the engine identifies 9
  known transient Puppeteer/Chrome errors (frame detached, target closed,
  session closed, protocol error, page crashed, execution context
  destroyed, etc.).

- `runProbeStage()` wraps browser session creation + initialization in a
  retry loop (max 2 attempts). On transient error: logs structured
  diagnostics (attempt, isTransient, error message, elapsed time), closes
  the crashed session cleanly, creates a fresh browser, and retries. Non-
  transient errors throw immediately without consuming retry budget.

- 17 unit tests for the error classifier, 3 integration tests for retry
  behavior (successful retry, immediate throw on non-transient, exhaust
  retry budget on persistent transient).

Closes #1687

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

* fix: address review feedback — widen retry scope, deduplicate patterns

- Move createCaptureSession inside the retry try/catch so browser launch
  failures (Failed to launch the browser process, ECONNREFUSED) are also
  retried — not just initializeSession errors.
- Deduplicate transient error patterns: remove "Protocol error.*Target
  closed" (subsumed by "Target closed") and "Navigation failed because
  browser has disconnected" (subsumed by "browser has disconnected").
- Add browser launch failure patterns: "Failed to launch the browser
  process" and "ECONNREFUSED".
- Add test for createCaptureSession transient throw (browser launch retry).
- Update test mock comment to document sync requirement with engine
  pattern list.

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-06-24 00:07:14 -04:00
Miguel Ángel 899b8faa12 fix: flag visible GSAP transition overlays (#1686)
* fix: flag visible GSAP transition overlays

* fix: cover GSAP overlay lint review cases
2026-06-23 21:13:32 -04:00
Miguel Ángel ba77a0bd72 chore: release v0.7.4 v0.7.4 2026-06-24 00:10:04 +00:00
Miguel Ángel 45a9440c61 fix(producer): derive duration from sub-composition timing when root has no data-duration (#1680)
When the root element lacks an explicit data-duration attribute and
there is no GSAP timeline, getDeclaredDuration now computes
max(data-start + data-duration) across all sub-compositions instead
of returning zero.
2026-06-23 20:06:01 -04:00