127 Commits

Author SHA1 Message Date
Miguel Ángel f059a6eb53 feat(cli): default BeginFrame, agent blank, and looks quality (#3927)
* feat(cli): render summary names the capture path, gpu mode and stage timings

* fix(lint): drop gsap_exit_missing_hard_kill, its hazard does not reproduce on cold workers

* fix(cli): render slow-path hint fires only when the gpu was auto-probed

* test(cli): isolate agent hint suite from ambient agent env keys

The suite runs inside agent sessions. Ambient env keys fill the 16-key
hint cap and fail assertions that a plain shell would pass.

* docs(music-to-video): drop leftover hard-kill mandate after lint rule removal

Frame workers still required a tl.set hard-kill that the dropped lint
rule no longer enforces. Align the self-check with montage.md.

* refactor(cli): share hint-key pattern and collapse render summary args

Export HINT_KEY_PATTERN for the test isolation strip. Read duration and
frame count from the perf summary instead of passing them beside it.

* test(cli): lock capture-mode preference and 16-key hint isolation

Session mode, including drawelement, must print over observability.
The hint suite now fails if ambient keys already fill the 16-key cap.

* feat(cli): default BeginFrame, agent blank, and looks quality

Local auto render opts out of the software-GPU screenshot clamp so
BeginFrame can run. init --agent scaffolds a centered Inter blank with
no prompts. --quality looks (now the default) is CRF 16; delivery is high.

* fix(cli): honor screenshot env and teach agent pitfalls on the happy path

Local auto still requests BeginFrame unless PRODUCER_FORCE_SCREENSHOT=true.
The agent blank fills its canvas so --resolution can resize it. Skills now
name the four pitfalls and tell agents to read the render summary line.

* feat(cli): default init to the centered blank

Bare init scaffolds the centered Inter stage. --agent is a hidden alias.

* fix(cli): pack from-file and use it for video init

TTY --video with no --example selects from-file. Pack copies that template.

* fix(cli): wire --video onto from-file even with --example blank

One resolver picks the scaffold. Spawned init --video asserts a-roll src.

* fix(cli): narrow looks CRF on the quality alias

Only looks carries crf. Typecheck failed on the union without a guard.

* style(cli): shorten render-pipeline summary comment
2026-09-13 21:44:19 -04:00
miga-heygen bafc7b4e00 fix(cli): stop promising a universal per-time filmstrip for --layout strip (#3867)
## What

`keyframes --shot --layout strip`'s help text, its type's inline doc comment, and the CLI reference docs all described `strip` as an unqualified "filmstrip by time." The tool doesn't actually do that for the overwhelmingly common case.

## Why

A real per-time pixel filmstrip is only produced when the sampled selector is an SVG element (gated by an internal shape check — `typeof element.getBBox === "function" && typeof element.getScreenCTM === "function"`). Any other selector — including every nested sub-composition host, which is always a `<div data-composition-src>` — silently falls back to one live screenshot plus vector position markers instead.

This isn't a capture bug: for a non-SVG selector, real per-time pixel compositing was never implemented, only 3D bbox/marker sampling. But the documented behavior over-promised what the tool does, so a user following the docs on the common case (a DOM/sub-composition selector) sees root captions and empty image boxes where they expected the nested composition's actual content to move across frames — the diagnostic strip is misleading, even though the real render is correct.

## How

Reworded all three descriptions (CLI help text, `ShotOptions.layout` TSDoc, and the reference docs table) to state the SVG-only condition and the DOM/sub-composition fallback explicitly. No behavior changed — this is a documentation-accuracy fix, per the ticket's own framing that a doc-only fix fully resolves the reported symptom (a silent, misleading omission) for a P3.

## Testing

Added a test asserting the CLI help text no longer makes the unqualified "filmstrip by time" claim and does disclose the SVG-only condition — guards against a future regression back to the misleading wording. Verified RED (fails against the pre-fix string) and GREEN (passes after the fix) via a local before/after comparison.

- `bunx vitest run src/commands/keyframes.test.ts src/commands/motionShotLayout.test.ts` — 46/46 passing
- `bunx tsc --noEmit` in `packages/cli` — clean
- `bunx oxlint` / `bunx oxfmt --write` on changed files — clean
- Full CLI suite (excluding known-broken-in-sandbox browser-launch tests, unrelated to this change): 217 test files / 3025 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-09-10 23:22:14 -04:00
James Russo 4233b5c6bf fix(core): contain generated HTML CSS and script contexts (#3800)
* fix(core): escape generator metadata attributes

* fix(parsers): retain decoded metadata while assigning ids

* fix(parsers): preserve runtime html parser semantics

* fix(parsers): canonicalize HTML attribute names for stable IDs

* fix(parsers): normalize SVG attribute hashes across HTML parsers

* fix(core): escape public resolution attribute values

* fix(core): contain generated HTML CSS and script contexts

* fix(core): preserve empty captions and document authored code trust
2026-09-09 03:56:10 -04:00
Miguel Ángel 2f7b7a9529 fix(render): reject unsupported ProRes rate controls (#3702) 2026-09-08 18:09:19 +00:00
James Russo 29efd5dfa8 docs(registry): document installer metadata (#3658)
Co-authored-by: Kiyeon Jeon <kiyeon.jeon.21@gmail.com>
2026-09-04 18:39:41 -04:00
WaterrrForever c5224ac7e3 feat(cli): snapshot --against pairs each frame with a reference video (#3637)
Rebuilding footage means checking the render against the source at the same timestamps. `--against <video>` saves the reference frame at every captured time (frame-exact seek: -ss after -i) plus a labelled render|reference pair sheet built with the existing contact-sheet helper. Reference frames and pair sheets are derived artifacts, kept out of the snapshot list like contact-sheet.jpg. No new dependencies.
2026-09-04 23:25:36 +08:00
Miguel Ángel ae7e530f2d feat(cli): capture reports why a referenced asset is not in the folder (#3598)
Capture drops assets for four reasons and reported none of them, so a folder
with thirty images and a folder truncated to thirty images were the same
object. Every drop site was a bare `continue`, `break`, `return null` or an
empty `catch`, and the only signals downstream were two hand-written warning
strings that fired when the budget was already gone before a download pass
started, which is the one case where the pass could not say how much it lost.

`downloadAssets` and `downloadAndRewriteFonts` now return an `AssetDropCounts`
tally beside their result, incremented at the single line that performs each
drop. `capture --json` carries it as `dropped`; the human summary prints a
`Dropped:` line when it is non-zero.

Four reasons, three decisions and one failure:

  size-floor        fetched, then judged too small to be a real asset
  budget-exhausted  the post-navigation clock ran out before this one
  cap-reached       30 inline SVGs, 30 fonts, or 6 faces per family
  unavailable       the request or the write failed

A break now counts everything it did not reach rather than the one it stopped
on, because "how many did we lose" is the question and one is never the answer.

The two budget warnings are gone. Both existed only to cover the case where
the budget ran out before a pass was called, so both passes are now called
unconditionally: a zero budget makes each loop break on its first item and
record `budget-exhausted` for the rest, which costs no network and produces a
real number instead of the word "some". The single remaining warning is derived
from the tally, so the prose and the count cannot disagree.

Measured on a live capture of a large marketing site:

  default budget    232 kept, 91 dropped (39 size-floor, 20 cap-reached,
                    32 unavailable)
  15s budget         30 kept, 299 dropped (279 budget-exhausted, 20 cap-reached)

Same page, same command, and until now both runs described themselves the same
way.
2026-09-02 23:24:59 -04:00
Xuanru Li 45cc343525 fix(cli): flag caption-zone by DOM box overlap (#3580)
A card centered at y=.860 can still cover the painted V2A pill. Intersect the element's getBoundingClientRect with the keepout instead of testing whether its center sits inside the band.
2026-09-01 04:41:57 +00:00
Miguel Ángel e5a5e6b151 fix(cli): keep overlap waivers local to marked text (#3464)
* fix(cli): scope overlap waiver to marked text

* fix(skills): guard changelog caption rail

* fix(skills): densify changelog caption checks

* test(skills): satisfy strict seek typing
2026-08-24 14:22:03 -04:00
Miguel Ángel 9ec75a485f docs: drop --full-depth from skills install commands (#3399)
* Update skills.mdx

* docs: drop --full-depth from skills install commands
2026-08-21 14:45:01 -04:00
Miguel Ángel b3c43e2480 feat(cli): add normalize-audio to match one clip's loudness to another (#3306)
* feat(cli): add normalize-audio to match one clip's loudness to another

Measures two authored `<audio>` clips with FFmpeg's integrated EBU R128
loudness and writes the target's matching `data-volume`, leaving the
reference untouched.

The measurement is bounded to the window the composition actually plays.
`data-end` bounds a clip's timeline window just as `data-duration` does, and
`-ss`/`-t` belong before `-i`: after it they bound the OUTPUT, and with
`-f null` there is none, so ebur128 keeps integrating past the clip. On a
fixture whose played window is -61.8 LUFS inside a file that measures -27.9
whole, either mistake reports a loudness the composition never plays and
"corrects" an already-matched clip by tens of dB.

Two EBU R128 passes run between reading the composition and writing it, each
bounded only by a two-minute timeout, and the skill docs tell agents to keep
Studio open meanwhile — so the attribute patch is re-applied to a fresh read
and written through a temp file and a rename.

Under `--json` the failures are documents too: an agent doing
`JSON.parse(stdout)` on a bare error line throws. A pair needing more than the
+12 dB ceiling has a source-file problem rather than a mixer one — mixer gain
raises the noise floor with the signal — so the refusal names the remedy.

* fix(cli): validate --tolerance before paying for the measurement

Each EBU R128 pass is bounded at 120s and normalize-audio runs two, so
parsing the argument afterwards made a typo'd --tolerance cost both of them
before failing on something that was wrong from the start.

Not pinned by a test: the ordering is internal to the command and neither it
nor the parser is exported, so covering it would mean restructuring for a spy
rather than asserting the behaviour.

* docs(cli): restore the blank line between the preview and normalize-audio sections

Lost when I resolved the rebase conflict against the background-preview docs
by hand instead of letting the formatter near it. oxfmt --check failed on the
one file, which fails Preflight — and because preview-parity needs Preflight it
skipped, and the preview-regression gate fails closed on a skip, so a missing
newline read as a preview defect.

The quieter half: the same needs chain meant the required Test context was
never created at that head. Not failing — absent, so there was no test signal
at all on the PR.
2026-08-19 17:36:10 -04:00
Miguel Ángel 9da422fd7f feat(cli): run a managed background preview in every launch mode (#3310)
`--background` was rejected outside the embedded server. It now re-execs the
CLI in foreground, which makes it mode-agnostic by construction: whichever
server the child resolves to serves the config endpoint the readiness probe
looks for. `--foreground` is its counterpart, for a non-interactive shell that
wants to stay attached, and a bare launch keeps the same promise — attached in
an interactive terminal, managed in an agent session.

That generalization exposed an existing hole. Local-studio mode runs Vite with
the studio package as its cwd and needs that package's own Vite config, which
the published tarball does not carry, but resolving the package was treated as
proof the mode was usable. An npm-installed studio therefore took a path that
can never come up — previously a clear error, now a ten-second silent timeout.
The predicate becomes "can this studio actually be served", so a published
install falls back to embedded mode, which works.

Over the 1k line budget at ~1.3k. The overage is one command file and its
tests carrying one invariant, and the seam that would split it further is
inside a single request-handling function — a split there would produce two
PRs neither of which starts a preview on its own.
2026-08-19 17:02:44 -04:00
James Russo 17a2a00ed5 feat(cloud): default distributed plans to v2 (#3311)
* feat(cloud): default distributed plans to v2

* fix(cloud): address plan v2 review feedback

* fix(examples): document explicit v2 samples
2026-08-17 17:24:31 -04:00
Miguel Ángel 0bda6b55b8 feat(cli): track which registry items add installs (#3099)
* feat(cli): track which registry items `add` installs

`cli_command` records that `add` ran and nothing about what it installed, and
the registry is served from raw.githubusercontent.com, which gives no per-item
counter either — so there is no way to tell which block or component people
actually pull, and no way to know what is worth building more of.

Emit one `registry_item_added` event per item written into a project, from
`runAdd` after the install succeeds. That is the single choke point: the bulk
`add <tag>` path re-enters it per item, and a failed or compatibility-refused
install throws before it, so a refused install is never counted as a download.

`requested` separates the item the user named from the transitive
`registryDependencies` pulled in behind it; without it a popular dependency
outranks everything that depends on it.

Item names are public registry identifiers, never user content or project data,
and the event goes through `trackEvent` — an install that opted out via
`hyperframes telemetry disable`, `HYPERFRAMES_NO_TELEMETRY` or `DO_NOT_TRACK`
sends nothing.

* test(cli): cover `add` telemetry end to end against the built CLI

The unit tests assert the emit seam and nothing past it. `shouldTrack()`
short-circuits whenever `isDevMode()` is true, and that is true for any `.ts`
entry, so under vitest a real event and no event are indistinguishable and the
transport is never exercised at all.

Drive the built CLI instead and assert on the HTTP body it actually produces:
one event per installed item, the dependency reported with `requested: false`,
an opted-out install sending no request at all (not merely one without this
event), and a refused install counting nothing.

Two fixtures, because neither case is reachable through the real registry. The
registry origin is a first-class project setting, so a local one supplies the
`registryDependencies` edge that no shipped catalog item declares today; and
`globalThis.fetch` is wrapped to capture the batch rather than send it. The
faked 200 is load-bearing: only a failed flush leaves events queued, and only a
non-empty queue spawns the detached `flushSync` child that would bypass the
hook and reach production analytics.

Verified the check can fail — forcing `requested: true` for every item turns it
red on exactly the dependency assertion.
2026-08-07 16:00:23 -07:00
ukimsanov 592301248e fix(scripts): drop the catalog poster instead of guarding it, and cut the encode pass down
**The poster guard I added twice was unworkable and I never ran it.** It called
existsSync on docs/images/catalog/<name>.png. That directory is gitignored —
previews are generated locally, uploaded to the CDN and never committed — so the
check is false on every clean checkout and in CI. It would have stripped the
poster from all 168 pages, not the 13 with a missing file. It also referenced
REPO_ROOT, which does not exist in that file, so the script crashed on the first
item. I described this guard in two commit messages without once executing the
generator.

The poster is now gone entirely, which is the smaller and more honest fix. These
previews are autoPlay muted loop, so the poster is visible for a few hundred
milliseconds; 13 of the 168 files do not exist and the browser fetches the poster
before the video. Removing the attribute kills 13 x 403 and 168 needless image
requests, and there is nothing to keep in sync.

Also applied a complexity pass to the delivery encode:
- hasAudio() deleted. 17 lines and a spawnSync per item to choose between
  "-c:a aac" and "-an". ffmpeg ignores -c:a when the input has no audio stream;
  checked, exit 0, output carries no audio track.
- The 40-line spawn + Promise wrapper is execFileSync. Everything around it in
  that script is already synchronous.
- The duplicated poster lookup is gone with the poster itself.

Net 76 lines lighter. Generator runs clean, emits 168 pages, carry-forward intact.
2026-08-04 12:52:26 -07:00
ukimsanov 7a91b93dd6 docs: correct four developer-reference claims the source contradicts
Miguel's three P2s and Rames' one finding on #2974, all verified in source
before changing anything.

**`render --json` is not a progress stream.** It prints exactly one
`batch-complete` document at the end (`batchRender.ts:408-418`), asserted as a
single `console.log` in `batchRender.test.ts`. Described as a final result now.

**The iframe drag example never captured the pointer.** `event.target` comes
from `iframe.contentDocument`, so `instanceof Element` against this window's
constructor is always false for a cross-realm node and `setPointerCapture()`
never ran — a pointer leaving the frame then loses `pointerup` and drag state
sticks. Structural feature detection instead, with the reason in a comment so it
does not get "simplified" back.

**The preview adapter example did not compile under strict TypeScript.** `comp`
was captured by the callback before definite assignment (TS2454). Optional, with
`comp?.dispatch(op)`.

**`ORIGIN_APPLY_PATCHES` was imported in a fence that did not use it and used in
fences that did not import it.** Imports do not cross fences, so both examples
were wrong in opposite directions. Rames found the pair in
`open-composition.mdx`; the same shape is in `composition.mdx:630`, which he did
not name. All three fences are self-contained now.

**And `types.mdx` claimed coverage it does not have.** It promised "every type
exported from `@hyperframes/sdk`" while omitting 13 of 42. Eleven are documented
on sibling pages, so the sentence now points at those instead of overclaiming.
The two with no home anywhere — `CompositionVariableType` and
`VariableUsageScan`, both re-exported from the barrel — have entries. The second
is worth having written down: `scanIncomplete` means `usedIds` is a lower bound,
so an id missing from it is unknown rather than unused.
2026-08-04 02:45:21 -07:00
ukimsanov bebaf679d9 docs: rebuild developer and rendering reference 2026-08-04 02:45:21 -07:00
Vance Ingalls 71fd96bbf1 Merge pull request #2854 from heygen-com/feat/canary-rollouts
feat(core): percentage-based canary rollouts + calibration experiment
2026-08-03 22:35:37 -07:00
James Russo 1d636f603c refactor(producer): share plan execution builder (#2906)
## What

Refactor distributed planning around one shared local execution-plan builder:

- `buildLocalExecutionPlan()` now owns compile/probe/extract/audio/freeze.
- Legacy `plan()` remains a deprecated v1 transport wrapper.
- Plan v2 calls the shared builder directly and publishes through the existing manifest/CAS contract.
- Add neutral `createPlanV2FromExecutionPlan()`, `publishPlanV2FromExecutionPlan()`, `getPlanV2ExecutionPlanHash()`, and `PLAN_PROTOCOL_V1` names.
- Retain deprecated v1-named exports and wire aliases.
- Recommend explicit Plan v2 opt-in for new producer, Lambda, and Cloud Run integrations.

## Why

Plan v2 previously looked like it invoked a v1 planner even though v1 and v2 share the same frozen local execution representation. This removes that migration-era coupling while preserving the public minor-version compatibility contract.

## How

The shared builder returns neutral internal execution-plan fields. The v1 wrapper maps those fields back to the existing `PlanResult`; the v2 publisher consumes them directly.

Compatibility is intentional and covered by exact shape tests:

- omitted `planProtocol` still serializes/selects `"v1"`;
- v1 layouts, descriptor-less decoding, event unions, workflow branches, and exports remain;
- the v1 descriptor JSON is byte-identical and `CURRENT_PLAN_PROTOCOL` is an identity-preserving alias;
- v2 manifest bytes, key order, hash framing, and `sourcePlanV1Hash` wire key remain unchanged;
- no enumerable neutral hash field was added to manifests or returned result objects;
- v1/v2 result objects, cloud event payloads, and SDK handle key sets remain unchanged.

## Test plan

- Focused Plan v1/v2/protocol/export/size compatibility: 141 passed
- `@hyperframes/core`: 1,419 passed
- `@hyperframes/producer` unit lane: 990 passed
- `@hyperframes/aws-lambda`: 140 passed
- `@hyperframes/gcp-cloud-run`: 101 passed
- Producer, Lambda, and Cloud Run typechecks
- Repository-wide lint, format check, workspace/package-subpath checks
- Full workspace build
- `git diff --check`

- [x] Unit tests added/updated
- [ ] Manual testing performed
- [x] Documentation updated (if applicable)
2026-07-30 17:41:02 -07:00
Vance Ingalls 54534d53c2 docs(canary): connect canary rollouts to the telemetry docs
Second half of a review comment I had only half-addressed: the opt-out
behaviour shipped in 4f464dc4, but "should we add this into/connected to
the existing telemetry doc?" did not.

There is no standalone telemetry page — the canonical disclosure is the
`telemetry` command section in packages/cli. Links now run both ways so
neither page describes canaries as a separate system:

- packages/cli #telemetry: telemetry state also controls canary
  enrolment, with every opt-out route named.
- contributing/canary-rollouts: a Note at the top, before any of the
  how-to, saying canaries sit behind the telemetry switch and why (a
  canary is a measured rollout; an install that reports nothing cannot be
  compared, so enrolling it buys no signal).
- guides/feedback "Opting Out": disabling telemetry also ends canary
  enrolment, alongside the feedback prompt and usage tracking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:50:43 -07:00
Santhi Prakash a52dd9c308 docs(studio): document monorepo dev server port (#2902)
- Problem: @hyperframes/studio package docs showed bun run dev without the
  localhost URL contributors need after #2901 fixed contributing.mdx only.
- Fix: note that the studio dev server listens on localhost:5190 per
  packages/studio/vite.config.ts server.port.
- Verification: preflight_ship.py + read vite.config.ts; bun run format:check.
2026-07-30 21:14:42 +02:00
Miguel Ángel fdc5932897 fix(cli): honor check navigation timeout (#2860)
* fix(cli): honor check navigation timeout

* test(cli): clarify diagnostic timeout precedence
2026-07-29 20:50:20 +02:00
Xuanru Li 3a7950fd63 feat(check): add data-layout-allow-caption-zone waiver (#2853)
* feat(check): add data-layout-allow-caption-zone waiver

Opt intentional lower-third copy out of caption_zone_collision.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(check): address caption-zone waiver review nits

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(skills): document caption-zone waiver on CLI agent path

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(cli): document caption-zone waiver under check, not inspect

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 15:56:43 -07:00
ukimsanov 1828627731 docs: refine media treatment guides 2026-07-27 18:27:20 -07:00
James e73304fb0e feat(cli): make cloud archives size-aware 2026-07-17 18:44:58 -04:00
Vance Ingalls a7c0fa4fe5 Merge pull request #2564 from heygen-com/via/win-workdir-env
feat(cli): surface extract-cache dir in doctor + add --frames-cache-dir sugar
2026-07-17 00:43:06 -07:00
Xuanru Li 4ad582606b feat(lint): flag relative-value second writers and tl.set initial hides (#2612)
## What

Part 2 of the GSAP seek-safety rules (stacks on #2611): the two rules that touch existing catalog content and required reconciliation with an existing rule.

- `gsap_relative_value_second_writer` (error) — a relative var value (`y: "-=15"`) on a property whose target has another writer **active at the relative tween's start**. The relative base is captured at tween init, which reads a different partial state per seek path: sequential seek inits it mid-entrance, a cold render worker inits it at the entrance's end state, and the element teleports at chunk boundaries (production case: all scene nodes jumping ~20px mid-scene). Writers that complete strictly before the start are safe (children render in start-time order within a seek pass — verified against gsap 3.15.0) and are not flagged; neither are single-writer relatives, `from()`/`fromTo()`, build-time `gsap.set`, or relative position parameters (`"+=0.5"`). Selector resolution bails on combinators and cross-composition scoping rather than guessing. Findings aggregate per tween pair and report the overlap window.
- `gsap_timeline_set_initial_hide` (warning) — initial-state hiding via `tl.set(target, vars, 0)` on a paused timeline is not rendered while the playhead sits at exactly 0, so frame 0 shows the unhidden state (verified against gsap 3.15.0: opacity stays 1 after `tl.time(0)`, applies only past 0). Exempt when the target is already hidden by authored CSS/inline styles or a standalone `gsap.set()`, and only sets preceding every tween in source order qualify (mutated position variables resolve to their initial binding in the parser — outro hard-kills don't masquerade as position-0 sets).
- Reconciliation: `gsap_fullscreen_overlay_starts_visible`'s fixHint previously recommended exactly the flagged `tl.set(sel, {opacity:0}, 0)` pattern; it now recommends authored CSS hiding or immediate `gsap.set()`.
- Docs for the full rule family in `docs/packages/lint.mdx`.

## Corpus impact (the reason this is its own PR)

These two rules are the ones that fire on repo-shipped content:

- `gsap_relative_value_second_writer`: 4 errors in `gooey-metaball`, all genuine overlaps. Measured with gsap 3.15.0: ballD diverges **3.31 xPercent / 1.99 yPercent (~8px/5px at 240px ball size)** between sequential and cold seek — a permanent base offset that appears as a teleport at a chunk boundary. Real but modest; happy to fix the block in a follow-up (start the drift at the entrance's end, or use absolute `fromTo`).
- `gsap_timeline_set_initial_hide`: 10 warnings across the catalog after the CSS-hidden exemption (down from 54 pre-narrowing); spot-checked as genuine frame-0 pops with no authored hide (e.g. `vfx-text-cursor` `#phrase-b`).

Adversarially reviewed the same way as #2611 (393-composition corpus + gsap semantics experiments); FP classes fixed and locked as negative tests: precede-only second writers, descendant/cross-composition selector mis-joins, CSS-hidden re-assertions, mutated position variables.

## Tests

Full `packages/lint` suite green at 440 tests including multi-composition roots; `tsc`, oxlint, fallow audit clean.
2026-07-16 23:49:45 -07:00
Via ca35227506 feat(cli): surface extract-cache dir in doctor + add --frames-cache-dir sugar
Windows users with the OS temp dir on a small system drive have hit
C: exhaustion mid-render (Slack ts=1784219488 · CLI v0.7.58 · win32
15 GB / 8-core, ~5500 frames). The engine already honors
HYPERFRAMES_EXTRACT_CACHE_DIR for relocation, but the knob was
undocumented and invisible in diagnostics — the reporter had to piece
together a 4-flag compound workaround including EXTRACT_CACHE_DIR=off.

Changes:
- Extract the env-var resolver into a public engine API
  (resolveExtractCacheDir, defaultExtractCacheDir,
  EXTRACT_CACHE_DIR_DISABLED_ALIASES) with a typed resolution shape
  distinguishing "disabled by user" vs "default" vs "env override".
- Add a Frames-cache check to `hyperframes doctor` that reports the
  effective directory, its free space, source (env or default), and
  fails with a relocation hint when <2 GB free at that mount.
- Add `hyperframes render --frames-cache-dir <path>` as discoverable
  CLI sugar for the env var, including the opt-out aliases
  (off/none/false/0) and CWD-safe absolute-path resolution.
- Document the flag in docs/packages/cli.mdx with the field-signal
  citation, and add a render example row for the Windows workflow.
- Cover both surfaces with unit tests (6 doctor cases + 4 engine
  cases including all disabled-alias variants).

Refs Slack #hyperframes-cli-feedback ts=1784219488 (win32 v0.7.58).

Co-authored-by: Via <via-heygen[bot]@users.noreply.github.com>
2026-07-16 18:24:43 +00:00
Via f8210d96da feat(cli): configurable transcribe timeout with duration-scaled default
Adds a `--timeout <ms>` CLI flag (and `HYPERFRAMES_TRANSCRIBE_TIMEOUT_MS`
env var) plus a model-slowdown factor in the auto-scaled default so
`hyperframes transcribe` doesn't hard-fail with `spawnSync ETIMEDOUT`
on slow CPUs running heavier whisper models.

Field-signal ts=1784165471 (win32/arm64 emulating x64 on Snapdragon,
CLI 0.7.59) reported the failure on a 63s wav with `-m medium` at ~13x
realtime — the historical 10x-realtime scale (PR #2463) gave 10.5 min
while the machine needed ~13.7 min. Splitting audio and merging offsets
was the manual workaround.

- Add `--timeout <ms>` and `HYPERFRAMES_TRANSCRIBE_TIMEOUT_MS` (min 5000).
  Explicit override bypasses auto-scaling; still capped at 12h.
- Add per-model slowdown factor (tiny 0.5, base 0.7, small 1, medium 2,
  large 4, large-v3-turbo 2). Multiplied into the 10s/audio-second
  baseline so medium/large get proportional headroom while `small.en`
  (the default) preserves the historical safety window.
- Wrap whisper's spawn error with a discoverability hint naming
  `--timeout`, the env var, and the effective timeout when the child
  was killed by SIGTERM/ETIMEDOUT (mirrors PR #2504 protocol-timeout).
- Docs: new `--timeout` row in `docs/packages/cli.mdx` Flags table.

Regression coverage in `packages/cli/src/whisper/transcribe.test.ts`
(56 tests) and `packages/cli/src/commands/transcribe.test.ts` (5 tests):
- Model factor per known name + case-insensitive + safe unknown fallback.
- 63s field-signal case on medium.en → 1_260_000ms (was 630_000ms).
- Explicit override honored below the auto floor + capped at 12h.
- Model factor ignored when overrideMs is set.
- SIGTERM/ETIMEDOUT detection + augmented message contract.
- CLI rejects below-minimum `--timeout` with error naming both the flag
  and the 5000ms floor.

— Via

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-16 05:49:46 +00:00
Via 6944a1c2d0 feat(engine): surface protocolTimeout env + flag in Puppeteer timeout errors
Field signal ts=1784047847 (darwin/arm64, 8GB M1, 9 videos + 22 images):
reporter hit Runtime.callFunctionOn timeout and switched to FFmpeg
because the error didn't surface HyperFrames' existing knobs
(PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS env, --protocol-timeout CLI).

Wraps main-render Puppeteer errors matching /Runtime\.callFunctionOn
timed out|Target closed|protocolTimeout/i with an augmented message that
names the effective timeout, the env var, the CLI flag, and the
field-signal shape. Non-matching errors pass through unchanged
(returned as the same instance). Original error preserved via err.cause.

Also adds a dedicated --protocol-timeout row to the CLI docs Flags table
so PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS is discoverable via search.

Signed-off-by: Via <noreply@heygen.com>
2026-07-15 22:11:44 +00:00
WaterrrForever b9be0b2625 feat(skills,studio,media-use): the intent layer, review loop, and user memory — BRIEF.md, companion mode, recipes; /website-to-video folds into /product-launch-video (#2133)
* feat(studio,cli): per-frame board comments, self-refreshing storyboard, status-aware preview landing

Per-frame comment boxes on the storyboard board batch into
.hyperframes/frame-comments.json (a resubmit wins per frame; unconsumed
comments on other frames are kept). Submitted-but-unconsumed comments
stay visible — a toolbar banner plus a per-tile echo — until the agent
consumes the file; the banner also says what to do next (reply anything
in the agent chat).

The board keeps itself current: GET /projects/:id/signature exposes the
watcher-cached project signature, the storyboard payload carries the
signature it was derived from, and the view polls at 2s (hidden tabs
skipped, re-checked on visibility), refetching in place with no loading
flash. Posters bake the signature into their URL so tiles fill in as
sketches land and a poster that failed mid-write retries on the next
version; the empty state upgrades itself when STORYBOARD.md appears,
and its handoff prompt now points the agent at the review loop and uses
the parser's real status vocabulary (outline, not planned).

preview lands the browser on the storyboard view while the board is the
review surface — any frame built, or pure planning (srcs declared, none
on disk yet) — and on the timeline once the video is assembled.

* feat(skills): the review loop — plan, sketch, build as one shared process

hyperframes-core/references/review-loop.md is the single source for the
three-pass collaborative review: the plan proposed on a live board
(§ 1), wireframe sketches marked built with one layout question (§ 2 —
real words on plain blocks, run no CLI; a confirmed board is itself a
valid deliverable when the user asked for a storyboard, not a video),
the build dressing confirmed layouts (§ 3, worker or inline), and the
final look (§ 4). Autonomous runs skip every gate and keep one question
before render.

The three narrative workflows' Steps 3/4/6 collapse to references plus
their sketch stand-ins (captured-asset blocks for product-launch-video,
plain code panels for pr-to-video); the confirmed-sketch handoff stays
in each frame-worker prompt. general-video plans on a board for
multi-scene narrative pieces in collaborative mode — its sketch pass is
layout-before-animation with the user watching. The router treats
"I want a storyboard" as a process request rather than a route, and
closes exploratory intake by recommending a route plus how the run will
review.

The supporting contracts land next door: the comments channel (silent
submit, one reply picks it up, check the file before the words) in
brief-contract § 1; the sidecar schema and the built status rung in
storyboard-format; the mode question asked first and alone in the three
workflows' Step 0.

* feat(media-use): user memory — remembered preferences and frozen recipes

Two tiers of memory on media-use's existing two-tier storage split.

Preferences (lightweight): confirmed brief answers — destination, aspect,
language, mode, voice, style preset — recorded to the project's
.media/preferences.json (committed, the team inherits it) and promoted
to the personal ~/.media/preferences.json once the same value is
confirmed in two different projects (a sightings ledger accumulates the
cross-project evidence user-side, since project files can't see each
other). prefs.mjs get/record; merge reads project-over-user; a changed
value restarts its provenance.

Recipes (heavyweight): one approved run frozen as a named, versioned
bundle — frame.md, the storyboard skeleton (structure kept: durations,
transitions, srcs, Video direction; statuses reset to outline; content
blanked to per-frame fill-ins naming the beat's role), and the confirmed
brief values. Named folders, not content hashes: re-freezing bumps
version and archives <name>@v<N>; a freeze is already confirmed, so it
promotes to the user tier immediately. recipe.mjs freeze/list/use, plus
resolve --type recipe --entity <name> delegating like grade/lut.

16 new node --test cases; the media-use lib suite is 168/168.

* feat(skills): wire user memory into the brief and the review loop

brief-contract § 2 gains Remembered defaults: read the merged
preferences before Round 2 and let a remembered value become the
recommended option with a receipt naming its source project. Memory
changes the default, never the question — every ask-marked field still
gets asked, and what the request says this time beats what was picked
last time. Record only what the user actually confirmed (a defaulted
voice nobody chose is not an answer; a "go" that accepts the
recommended defaults is). The first record announces itself once;
after that the receipts carry the reminder. In autonomous mode a
remembered value becomes the decided value, receipt included.

The three narrative workflows read the remembered defaults before
Round 2, record the confirmed answers at the Step 0 gate, record the
chosen preset at the Step 2 gate (pr-to-video excepted — its preset is
fixed), and fall back to the remembered voice when the request names
none. general-video's discovery reads the same defaults.

Recipes wire in at both ends: Step 0 checks for a matching recipe
before the mode question — one question, plural-aware, and adopting
one fills the brief, skips the design step, and drafts the storyboard
from the frozen skeleton while every review gate still runs. The
review loop's final look (§ 4) offers the freeze once after approval,
and the confirmation teaches the recall phrase — the name is something
the system reminds the user of, never something they must remember.
The router recognizes a named recipe or "like last time" as a route.

* docs(skills): the sketch pass names check, not the deprecated validate

* feat(skills): intent-layer references — process, route briefs, capability menu, BRIEF.md format

* feat(media-use): brief skeleton as the recipe's fourth artifact; flow/storyboard preference keys

* feat(skills): the intent layer conducts every brief — workflows execute BRIEF.md

* feat(skills): retire the mode preference key; sync catalog surfaces for intent layer

* refactor(skills): dedupe router vs intent-layer guidance — one owner per rule

* feat(skills): the design ask — own spec, pick by eye from showcases, or defer

* docs(skills): the design ask says the honest line on capture routes

* feat(skills): product-launch-video absorbs website-to-video as the tour angle

* refactor(skills): keep product-launch-video pristine — a tour is brief intent, not a pipeline branch

* feat(skills): production loop + genre lenses; general-video goes freeform (route yours, laws hold)

* refactor(skills): /hyperframes is the front door - route tables and scope lists leave the workflows

* docs(skills): review-loop pass across skill catalog

* fix(cli): pass project dir to openStudioBrowser in background-server path

* feat(skills): add pitch-round reference - verbalized sampling concept gate

* feat(skills): wire pitch round into intent layer - completeness triage + route eligibility

* feat(skills): editorial capability recommendations, handoff disciplines, menu-probe split

* feat(skills): pitches carry their machinery; source-only-formed requests pitch the telling

* feat(skills): companion goes director - ceiling treatment plus blueprint/rule citation discipline

* fix(scripts): sandbox npx-leak guard - private npm global prefix keeps npx on the branch CLI

* chore(skills): resync manifest hash after formatter pass reflowed general-video tables

* fix(skills): recipe freeze reads workflow from BRIEF.md; style_preset records require workflow scope

Two holes found by a live companion-run freeze: the agent-supplied --workflow
contradicted the run's actual workflow (recipe.json said faceless-explainer,
brief-skeleton said general-video), and the style_preset lookup missed because
the preference had been recorded under the bare key.

- freezeRecipe resolves the workflow from BRIEF.md frontmatter; the flag is a
  fallback for briefless projects and a contradicting flag is ignored (noted).
- recordPreference refuses a bare style_preset — the scoped key is the only
  writable shape; freeze tolerates legacy bare records via read fallback.
- review-loop § 4 / media-use SKILL / brief-format wording follow the machinery.
2026-07-15 21:19:14 +08:00
Miguel Ángel 990f5c3145 feat(feedback): adopt 0–10 recommendation scale (#2438)
* feat(feedback): adopt 10-point recommendation scale

* docs(feedback): keep OSS scale contract self-contained
2026-07-14 15:46:47 -04:00
Xuanru Li 7f4eaeb568 feat(cli): coordinate-frame layout findings in check (#2354)
* feat(cli): coordinate-frame layout findings in check

Four production compositions shipped with 100-600px layout drift, each a
different coordinate-frame confusion the check graded info or missed
entirely: viewport pixels written as container left/top, gsap x/y
treated as absolute position, a -350px margin fighting flex centering,
and stage-relative path coords drawn into a nested SVG.

Three new layout findings close the class:
- positioned_out_of_parent: an absolute/fixed element rendering mostly
  outside its positioning ancestor (warning) — the parent needs no
  overflow clipping, which is what let container_overflow miss it.
- box_out_of_canvas: a painted panel breaching the canvas (warning) —
  text is canvas_overflow's, media is frame_out_of_frame's, painted
  boxes were nobody's.
- connector_detached: a connector path whose endpoints land far from
  every anchorable element (warning) — measured coordinates drawn into
  an SVG with a different origin.

canvas_overflow additionally promotes from info to warning when held
across samples AND the breach exceeds 5% of the canvas.

All three are persistence-tiered and respect data-layout-allow-overflow.
Verified against the four incident compositions: every one now surfaces
its drift as held warnings (previously: info or silence).

* fix(cli): harden coordinate-frame findings against review false positives

Reworks all three findings after two-lens review (adversarial FP hunt in
real Chrome + maintainer pass):

- escaped_container (was positioned_out_of_parent): uses offsetParent
  (transform-aware, skips fixed-as-canvas), exempts fully-detached
  callouts within an attachment allowance while still flagging
  touching-but-mostly-outside drift.
- panel_out_of_canvas (was box_out_of_canvas): paint alone qualifies
  (flat solid panels were a false negative), fully off-canvas rects are
  parked entrances and stay silent, pointer-events:none marks decorative
  layers, hero-sized breaches warn while small bleeds stay info.
- connector_detached: endpoints via getPointAtLength + getScreenCTM
  (viewBox, preserveAspectRatio, group transforms, every command type),
  defs/marker/clipPath subtrees skipped, word-boundary connector naming,
  containment tier limited to opaque non-ancestor targets (a text-bearing
  wrapper contains its own diagram's endpoints).
- canvas_overflow promotion requires partial visibility — a fully
  off-canvas rect is a parked entrance, not drift.

Verified: the four incident compositions still surface their drift as
held warnings; the review's false-positive repros (fixed HUD, callout,
parked entrance, corner bleed, marker arrowheads, g-transform and
viewBox-scaled connectors) are clean at warning level. Docs and the CLI
skill reference now describe the coordinate-frame findings.

* fix(cli): panel ownership is geometric — direct-text panels were a silent false negative

A painted panel whose direct text stays in-bounds while its box breaches
the canvas produced neither finding: canvas_overflow measures the text
range and panel_out_of_canvas skipped every own-text element. Skip the
panel finding only when the element's own text ALSO breaches (that
geometry belongs to canvas_overflow); pin the message/fixHint wording of
all three findings with positive assertions; document the SVG-internal
anchor blind spot.

* fix(cli): classify panel decoration by paint kind, not pointer-events

pointer-events:none exempted the framed-painting incident's gold frame
layers — hero content that happens to disable hit-testing. Decoration is
now gradient-only paint (spotlights, textures, vignettes); url() images,
solid fills and borders are content regardless of pointer-events.

* fix(cli): add fixHint to the test-local AuditIssue shape

* fix(cli): gradient stops decide content vs decoration; ownership matches canvas_overflow's tolerance

A gradient with any solid stop (alpha >= 0.6) is content — heroes and
cards painted with linear-gradient were invisible under the blanket
gradient exemption; all-translucent stops (spotlights, vignettes) stay
decoration. The text-ownership check now uses the audit tolerance that
canvas_overflow itself fires at, making the contract strict-mutex: any
text breach past that tolerance cedes the element, so a shallow 20px
text breach no longer double-reports.
2026-07-13 15:35:03 -07:00
Miguel Angel Simon Sierra cf7c1d7609 docs(cli,skills): teach check as the canonical verification gate
Scaffolded projects' npm run check now invokes the single check command
instead of chaining lint, validate, and inspect (three Chrome boots
become one). The CLI skill, its correctness reference, the entry skill's
capability map, README/docs catalog rows, the Mintlify CLI page (new
check section, deprecation banner on inspect), template CLAUDE/AGENTS
(byte-identical), root CLAUDE/AGENTS, and every creation-workflow skill
that taught the old sequence all point at check. snapshot keeps its
standalone sections; validate/inspect stay documented as deprecated
aliases with their check equivalents.
2026-07-10 13:30:09 -04:00
Miguel Ángel 3d8372f880 feat(cli): associate signed-in HeyGen account with telemetry (#2020)
* feat(cli): associate signed-in HeyGen account with telemetry

Sign-in telemetry currently attributes everything to the anonymous
install id, so the sign-in funnel can be counted but a completed sign-in
can't be tied to the account it produced. This associates the two.

- On a completed sign-in, emit a PostHog `$identify` alias whose
  `$anon_distinct_id` is the install's anonymousId, so events recorded
  before sign-in stitch to the same person, and tag `auth_login_completed`
  with the account identity (the pre-plumbed `distinctId`).
- `/v3/users/me` exposes no opaque user_id, so the identity key is the
  account email, falling back to username (single `identityKey` helper).
- Both no-op under the `telemetry disable` opt-out and only fire after
  the user chooses to sign in.

Privacy disclosure updated in lockstep, since this is the first PII the
CLI attaches: the first-run telemetry notice and the telemetry section
of docs/packages/cli.mdx now state that signing in links your account
email to your usage.

Tests: identifyUser payload + no-op, completion attribution incl.
username fallback and no-identity-on-reject/empty. Verified end-to-end
against the built CLI: pre-auth events anonymous, $identify carries
$anon_distinct_id, completion carries the account email.

* docs(cli): disclose the username identity fallback

Review gating item: identityKey is `email ?? username`, but the
first-run notice and cli.mdx said only "email", so an emailless
account's username would reach PostHog undisclosed. `/v3/users/me`
treats email as optional (pickString), so the fallback is live code,
not dead — disclose it rather than assert an unverifiable email
guarantee. Both surfaces now say "email, or username if the account
has no email".

Also soften the identityKey comment: it implied username is "less
identifying", but HeyGen usernames are often email-shaped, so the note
now states username is a fallback, not a privacy win.
2026-07-07 02:23:54 -04:00
Miguel Angel Simon Sierra 5fe957363d feat(media-use): v2 media OS core (resolve cascade, providers, local generation, telemetry) + retire hyperframes-media 2026-07-06 23:41:05 -04:00
ukimsanov 52d586dd31 docs: fix 53 inaccuracies across all documentation
Exhaustive audit of every MDX file in docs/ against skill references
and package source code. Every API signature, default value, flag,
and technical claim verified against ground truth.
2026-07-05 21:26:41 -07:00
Vance Ingalls 26e9283f6b docs(sdk): comprehensive SDK reference + guides (#1817)
* docs(sdk): comprehensive SDK reference + guides

Adds a dedicated SDK tab to the Mintlify docs documenting the entire
@hyperframes/sdk surface, verified against source:

Reference (6 pages):
- openComposition + OpenCompositionOptions
- Composition (every typed method, query, selection, dispatch/batch/can,
  events, serialize, override mode, lifecycle)
- Edit Operations (all 33 EditOp variants for dispatch/can/batch)
- Types (every exported type + constants)
- Adapters (PersistAdapter/PreviewAdapter + memory/fs/headless/iframe factories)
- Utilities & Constants (history, persist-queue, document utils, origins, errors)

Guides (7) + Overview + Quickstart:
- querying-and-editing, timing-and-animation, undo-redo-and-patches,
  persistence, embedded-override-mode, canvas-integration, editing-affordances

The existing packages/sdk.mdx stays as the package card and now links the
new SDK tab. editing-affordances documents the @hyperframes/sdk/editing
subpath shipping in #1814 (flagged with a version Note).

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

* docs(sdk): address PR review feedback

Correctness fixes from PR #1817 review (Miga + Rames):
- types.mdx: FindQuery.text is a substring match (String.includes), not exact
- persistence.mdx: import PersistAdapter/PersistVersionEntry/PersistErrorEvent
  from @hyperframes/sdk (no @hyperframes/sdk/adapters/types export exists)
- open-composition.mdx: createHeadlessAdapter is a PreviewAdapter, not a persist
  adapter; PersistAdapter is exported from @hyperframes/sdk (no /adapters subpath)
- types.mdx / adapters.mdx: note KeyframeSpec, ElementAtPointResult, DraftProps
  are structural shapes, not barrel exports (no import to copy)
- overview.mdx: drop leaked authoring meta-comment
- timing-and-animation.mdx: getElementTimings is keyed by scopedId
- embedded-override-mode.mdx: history is already off by default in embedded mode
- editing-affordances.mdx: /editing subpath is merged; soften the version note
- querying-and-editing.mdx: bare id only resolves top-level; use find() for
  sub-composition leaves
- canvas-integration.mdx + persistence.mdx: explain the comp closure forward-ref
  and the fs-adapter subpath (tree-shaking) asymmetry

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-30 14:45:54 -07:00
Miguel Ángel 010df6a0a4 feat(cli): file a GitHub issue with a published repro from feedback (#1816)
Add an opt-in --file-issue flag to hyperframes feedback. When set, after
sending the usual feedback the CLI publishes a minimal repro of the project
to a public URL (consent-gated, mirroring publish --yes) and opens a
pre-filled GitHub bug issue draft containing the rating, comment, public
repro link, and environment summary. The user reviews and submits the issue
under their own account; there is no token, backend, or gh invocation. New
--dir selects the project to publish; --yes skips the consent prompt for
scripts. URL/body building is extracted into pure, unit-tested helpers.
2026-06-30 11:48:22 -07:00
Miguel Ángel 6aaab32ccb refactor: make @hyperframes/lint depend only on parsers (#1773)
* refactor: make @hyperframes/lint depend only on parsers, not core

Relocates the leaf utilities lint pulled from core — URL/asset-path helpers,
font aliases, and the slideshow manifest parser — into the standalone
@hyperframes/parsers base, and drops @hyperframes/core from lint's
dependencies. Core keeps back-compat re-export stubs at the old paths, so
producer/studio/cli are unchanged.

Why: lint was the lightweight validator from #1749, but depending on core
transitively pulled studio-server (hono) and bpm-detective — irrelevant to
linting. Now installing @hyperframes/lint pulls only parsers + postcss, and
the core<->lint dependency cycle is gone.

- parsers main entry stays browser-safe (pure utils only); the node:path
  asset helpers live behind the new @hyperframes/parsers/asset-paths subpath
- slideshow parser exposed via @hyperframes/parsers/slideshow

* feat(lint): add browser entry; harden CSS url() regex (ReDoS)

@hyperframes/lint/browser — a fully client-side rule engine (lintHyperframeHtml,
lintMediaUrls, shouldBlockRender) with zero node: builtins, so browser-only
editors can validate compositions with no Node.js and no server round-trip.
Closes the browser-validation ask on #1749.

- shouldBlockRender extracted from the fs-bound project.ts into its own pure
  module so the browser entry stays node-free
- pure composition primitives (data types, font aliases, URL helper) exposed via
  a new recast-free @hyperframes/parsers/composition subpath, so the browser
  bundle tree-shakes out the GSAP/recast machinery (verified: esbuild
  platform=browser bundles with 0 node builtins)
- lint built with a platform:browser tsup pass — compile-time guarantee the
  browser entry never pulls a node builtin
- harden CSS_URL_RE against polynomial ReDoS (CodeQL js/polynomial-redos);
  behavior-preserving, verified against existing tests + an old/new parity check
- parsers/lint marked sideEffects:false
2026-06-27 13:51:21 -04:00
Miguel Ángel 7e0a4cd02e docs: add package pages for parsers, lint, and studio-server (#1772)
New docs pages for the three packages extracted from core (#1755, #1756,
#1757), wired into the Packages nav after @hyperframes/core. Each covers
when-to-use, exports, and API with cross-links. Core's parser/lint sections
now point at the dedicated packages and its Related Packages lists all three.
2026-06-27 12:18:03 -04:00
Matt 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
WaterrrForever 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
Miguel Ángel e4058e8bbf docs: add missing package pages (#1660)
* docs: add sdk package page

* docs: add remaining package pages
2026-06-22 20:09:54 -04:00
ukimsanov 969d6a334b fix(cli): default capture output to ./capture/ (auto-suffix capture-2/, capture-3/ on re-run)
`hyperframes capture <url>` (no -o) used to dump into `./captures/<hostname>/`,
which buries the project two levels deep and silently merges re-runs into the
previous dir — file-by-file, so leftover screenshots / assets from the prior
run stay mixed in and any later `glob` sees both.

Switch the default to `./capture/`. When it already exists, auto-suffix to
`./capture-2/`, `./capture-3/`, … (up to -99). Each capture is its own clean
directory — no crud, no friction, no clobber. The CLI prints a one-line note
when the suffix kicks in so the user sees which dir actually got written.
Explicit `-o <name>` is unaffected (still overwrite-tolerant).
2026-06-16 22:26:31 -07:00
James Russo 0ba52fc130 docs(cloud): add managed cloud rendering guide + fix flag reference (#1518)
* docs(cloud): add managed cloud rendering guide + fix flag reference

Add a dedicated guide for the managed `hyperframes cloud render` path
(HeyGen-hosted, zero-infra) at docs/deploy/cloud.mdx, covering auth/setup,
the zip→upload→render→download flow, templates via --variables, webhooks /
fire-and-forget, render management, and idempotent retries. Register it at
the top of the Deploy nav group and link it from the local Rendering guide.

Also fix a stale flag reference in the CLI docs: the `cloud render`
`--resolution` row listed the local-render presets (landscape/portrait/...)
but the cloud command only accepts `1080p`/`4k`, and `--aspect-ratio` was
missing. Verified against `hyperframes cloud render --help`.

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

* docs(cloud): correct aspect-ratio wording and flow-diagram status

Two accuracy fixes from review:
- `--aspect-ratio` is only auto-detected for a local project dir; for
  `--asset-id`/`--url` there is no local composition, so detection is
  skipped and the server defaults to 16:9. Reword both the guide and the
  CLI-reference rows to say so.
- The flow diagram showed status `done`, which is not a real value
  (HyperframesRenderStatus is queued | rendering | completed | failed).
  Use `completed` and re-align the box.

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-16 17:07:27 -07:00
James Russo 36b24acf20 feat: add video frame format render option (#1481)
* feat: add video frame format render option

* refactor: single source of truth for video-frame-format allow-list

Addresses PR review (Via) on #1481: the ["auto","jpg","png"] set was
declared three times — render.ts (VIDEO_FRAME_FORMATS), server.ts
(inline includes), and renderConfigValidation.ts
(ALLOWED_VIDEO_FRAME_FORMATS) — three boundaries to update when a new
extraction format lands.

Hoist the constant + a reusable `isVideoFrameFormat` type guard into
@hyperframes/engine (where VideoFrameFormat is defined) and route all
three call sites through them. Behavior unchanged; also drops two
`as RenderConfig[...]` casts in favor of the guard (narrowing over
assertion, per repo TS conventions).

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

---------

Co-authored-by: Xuelong Mu <xuelongmu@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 22:05:20 -07:00
James Russo 646ffff927 feat(cli): support OpenRouter as an alternative vision provider for capture captioning (#1478)
* feat(cli): support OpenRouter as an alternative vision provider for capture captioning

`hyperframes capture` could only enrich asset descriptions with Gemini vision,
which requires a Google API key. Add OpenRouter as an alternative so users
without Google access can caption via any vision-capable model through one
unified key.

Provider is selected by which key is present: OPENROUTER_API_KEY → OpenRouter
(OpenAI-style /chat/completions with an image_url data URI), else
GEMINI_API_KEY/GOOGLE_API_KEY → Gemini (unchanged), else DOM-only as before.
OpenRouter wins if both are set. Default model is google/gemini-3.1-flash-lite
(the OpenRouter analog of the Gemini path's existing 3.1-flash-lite tier),
overridable via HYPERFRAMES_OPENROUTER_MODEL.

Both vision call sites — the image loop and the rasterized-SVG loop — route
through a single `captionOne` dispatcher, so the new provider works for SVGs too
(the original PR #840 only patched the image loop, which would have left
OpenRouter-only users with crashing SVG captioning). The OpenRouter path checks
res.ok and surfaces the status/body on failure.

Reimplements #840 (which was unmergeable: saved with a UTF-8 BOM + CRLF so
GitHub rendered it as a binary diff, used `any`, reused the Gemini model env
var, and had a hallucinated default model id).

- Adds unit tests for the OpenRouter path (happy path, graceful degradation on
  non-OK status, no-key skip).
- Documents OPENROUTER_API_KEY in the website-to-video guide and the CLI capture
  reference.

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

* test(cli): fix typecheck in OpenRouter caption test — capture request without `as`

The test cast `fetchMock.mock.calls[0]` to a tuple (TS2352: `[] | undefined`
doesn't overlap `[string, RequestInit]`), which failed the Typecheck CI job.
Capture the url/init inside the typed mock and assert via `new Headers()` +
`typeof` narrowing instead — no `as` assertions (which the repo bans anyway).

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-15 20:14:45 -07:00
Miguel Ángel 9175eced45 feat(cli): declarative motion verification in inspect (#1437) (#1459)
Extend `inspect` to verify motion intent against the same seeked timeline
the renderer uses, catching render-≠-preview bugs that layout sampling can't:
entrance reveals the seek skips, broken stagger order, off-frame drift, and
frozen shots.

A `*.motion.json` sidecar next to the composition opts in (auto-discovered,
no flag, no authoring-framework changes); without one, inspect is unchanged.
inspect seeks a dense grid over the asserted selectors, builds an
element × time matrix of {rect, opacity, visible} plus per-scope liveness
signatures, and evaluates four assertions in Node:

  appearsBy    -> motion_appears_late
  before       -> motion_out_of_order
  staysInFrame -> motion_off_frame
  keepsMoving  -> motion_frozen

A selector matching nothing is reported as motion_selector_missing rather
than silently passing. Findings reuse the LayoutIssue shape and flow through
the existing dedupe/collapse/limit/format pipeline and JSON envelope; they
are errors by default, so a failed assertion fails the run.

The motion pass runs in the same Chrome session as the layout audit (no extra
launch) and only when a sidecar is present.
2026-06-15 16:29:11 -04:00
WaterrrForever 3b3ece81d1 docs: reconcile skills surface; rename read-first entry skill to /hyperframes (#1461)
Make /hyperframes the single entry skill and bring the docs back in sync with
the #1349 skills refactor.

Skills:
- Rename hyperframes-read-first -> hyperframes so the leaderboard-tracked
  /hyperframes is the entry/router skill; description leads with "READ THIS
  FIRST" to preserve the read-first intent. Update all references across
  CLAUDE.md, AGENTS.md, CLI templates, test script, and workflow SKILLs.

Docs (closes the quickstart confusion in #1428):
- quickstart + prompting: replace the dead standalone runtime slash commands
  (/gsap /lottie /three /waapi /animejs /css-animations /tailwind) with the
  real surface; document the picker as required core skills (8) vs optional
  workflows, with --all as the install-everything shortcut.
- frame-adapters: map every runtime to /hyperframes-animation.
- packages/cli: /tailwind -> /hyperframes-core; rewrite the skills-include
  blurb around the current domain skills.
- copilot-cli/pipeline/migrating-to-lambda: /hyperframes is the router; the
  composition contract lives in /hyperframes-core. Fix a dead /gsap example.
- antigravity: stop listing gsap/ and tailwind/ as separate skill dirs.
- contributing/catalog: /contribute-catalog -> /hyperframes-registry.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 01:33:07 +08:00