mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-08-31 02:41:44 +00:00
sync/hyperframes-codegen-0975ac7d
4079
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
634df5a5af |
fix(producer): give inlined media a document-unique render id (#3342)
* fix(producer): give inlined media a document-unique render id
Element ids are unique per composition file, but the render document is
the inlined union of every file. The producer merged the per-file media
lists and deduplicated by id, so clips that shared an id collapsed into a
single entry, and every id-keyed stage (extract, inject, visibility,
bounds) resolved to whichever element came first in the document. The
surviving clip's frames landed on the wrong element and the visible scene
rendered without footage.
Two shapes hit this, and neither is author error:
- Two scenes that each declare `<video id="clip">`. Legal per file, and
unavoidable when a scene is duplicated into a copy with inner ids
kept, or when one file is mounted twice.
- Two scenes that each declare a bare `<video>`. The timing compiler
numbers auto-ids per file, so both arrive as `hf-video-0` with no
authored id involved at all.
Stamp a document-unique `data-hf-render-id` while inlining, and read the
media list off the inlined document instead of merging per-file lists.
The render id equals the element id whenever that id is already unique,
so documents without a collision keep identical pipeline keys.
Author `id` attributes are left alone: 158 of the 161 registry blocks
reference their own ids from `#id` CSS or getElementById, so renaming
would trade broken footage for broken styling. The engine resolves media
elements through the render id instead, falling back to getElementById
for documents the producer never compiled.
Collecting from the inlined document also retires the per-file media
extraction in parseSubCompositions along with its offset bookkeeping;
host offsets are recovered from the composition hosts the clip sits in.
* fix(core): resolve render-frame siblings by render id in the runtime
The injector creates each `__render_frame_<id>__` sibling from the media
element's render id, but four runtime readers still built that id from the
plain `el.id`. On a document where two compositions share a media id, all
of them resolved the first collider's frame.
colorGrading is the one that changes pixels: findRenderFrameImage returns
the image the grading pass samples, with no class check to catch the
mismatch, so the second video was graded from the first one's frame.
media, mediaProxy and video-texture-compat use it as a render-mode or
substitute-source signal, where both colliders happen to agree during
render, but none of them should rest on that.
Add renderFrameSibling as the single owner of "which frame belongs to
this element" and route all four through it. It reads the stamped render
id and falls back to the author id, so a collision-free document resolves
exactly as before and an uncompiled one (preview, snapshot, check) is
unchanged.
The engine's in-page bridge keeps its own copy of the rule because code
serialized into page.evaluate cannot import; it now names core as the
definition, and a test pins the sibling-id format both sides build so
they cannot drift apart silently.
* refactor(engine): build render-frame sibling ids from core's definition
The drift guard named both sides but pinned one. renderFrameSibling.test
asserts core's format, while the engine rebuilt the same id from a literal
template at six independent sites. Changing the format on either side left
the test green and every runtime reader silently unable to find its frame —
this PR's own failure mode, one level up.
Export the affixes and renderFrameIdForRenderId from core, and take the id
from there at all six. Four sites resolve it on the Node side, where the
engine can import; the two that iterate the DOM in-page receive the affixes
as evaluate arguments, which avoids depending on bridge install order.
Also switch two `__hfMediaId?.(el) ?? el.id` reads to `||`. The bridge
returns "" for an element with neither id, so `??` kept the empty string
and built `__render_frame___`, which no reader looks for. Inert today
because the compiler assigns positional ids to id-less timed media, but it
made the two sides disagree in the one case they could.
|
||
|
|
ec0b23f3ce |
fix(studio): make Delete remove the whole canvas selection (#3339)
* fix(studio): delete every clip in the selection, not just the first Select all in the timeline, press Delete, and one clip disappeared while the rest stayed — still drawn as selected. The Delete hotkey built the selection set correctly and then called `elements.find(...)`, which stops at the first match, and handed that single element to a handler that deletes exactly one. The comment above it claimed the handler "expands a clip that is part of the multi-selection into an atomic delete of the whole selection (single undo)" — no such expansion existed anywhere; `useTimelineEditing` never read `selectedElementIds`. `handleTimelineElementsDelete` takes the whole selection and removes every element before saving once, so the delete is a single history entry and a single undo — what the comment already promised. The hotkey layer now takes only that plural handler, since it never deletes one element in isolation; the singular entry point stays for the context menu and clip chrome. The store drops every deleted key and clears the marquee set, rather than leaving a selection drawn around clips that no longer exist. Elements whose `sourceFile` is not the composition being edited are dropped from the pass rather than written to the wrong file. Also removes the preview's double-click-to-reset-zoom. It was a document-level capture listener, so any double-click anywhere over the viewport snapped the zoom back to fit — including double-clicks meant for the content under it. The explicit reset control beside the zoom HUD stays. Reproduced by test: restoring `elements.find` reds the new marquee case. * fix(studio): delete every canvas element in the selection, not just the primary Selecting several elements on the canvas and pressing Delete removed one of them and left the rest — still drawn as selected. The delete path only ever took the primary selection; the marquee group it belongs to was ignored. Expand the session-level delete through the group ref, the same way the other group commits already do, and let the lifecycle op remove every member under a single save so one Undo restores the whole selection. * fix(studio): let the canvas selection own Delete instead of its timeline mirror Marquee-selecting elements on the canvas and pressing Delete removed a fraction of them. The hotkey routed to the timeline delete whenever the timeline store held anything, and the timeline's copy of a canvas selection is derived and lossy by construction — a member with no timeline row of its own is dropped from it. Selecting 73 elements published 14 ids, so 14 went and 59 stayed, still drawn as selected. The canvas selection is what the user drew the marquee around, so it owns Delete whenever it holds something; the timeline path stays as the fallback for rows with no canvas node to select. Both paths already remove through the same endpoint, so this is one addressing scheme replacing two. That makes the canvas delete the path a Delete press normally takes, so it picks up the same mid-recording refusal the timeline delete has. * fix(studio): let the marquee see the whole document, not the first 80 elements Dragging a marquee over the entire canvas selected a fraction of what it covered, so Delete left most of the page behind. The hit test sourced its candidates from the layers-panel collector, which stops after 80 items — a budget for how many rows that panel is willing to render, silently reused as if it described the document. Everything past the 80th element in document order was unselectable no matter where the user dragged. The off-canvas indicators were reading the same truncated list. The cap now belongs to the panel that wants it; the collector returns everything. To pay for that, the marquee measures its candidates once when the drag passes the threshold instead of re-reading layout for every element on every pointer-move: unbounded plus per-move stalled the tab outright, and the iframe DOM does not mutate mid-drag, so one pass stays true for the gesture. On a captured page: one marquee, one Delete, 734 elements down to 81. * fix(studio): report a no-op delete instead of claiming the elements went A target the file no longer holds answers `changed: false`, which is normal for a member nested inside another member already removed. Every target answering that is not — it means the preview is describing a document the file does not have, so each removal misses and the file is written back untouched. The toast still said "Deleted 503 elements. Use Undo to restore them." That is how a delete that did nothing at all looked from the outside: press Delete, the page stays, nothing on screen explains it. Say the preview is out of date and reload it instead. * fix(studio): keep the canvas hotkeys alive across preview reloads Pressing Delete with a canvas selection did nothing at all — no removal, no toast, nothing on screen to explain it. A keypress goes to whichever document has focus, and clicking the canvas puts focus inside the preview iframe, so the app's hotkeys have to be forwarded there. They were, but only from the iframe element's ref callback, which fires when the element mounts. A preview reload keeps the same element, so the callback never runs again, and keeps the same WindowProxy, so the forwarder's identity check saw no change and skipped re-attaching — while the inner window holding the listeners had been replaced. After the first reload the canvas had no app hotkeys left. Undo and redo kept working because their forwarder re-attaches on every load, which is why this read as "only Delete is broken". Fold the app handler into that per-load forwarder so both attach in the same place, on every load, and drop the mount-only one. Window only: the history pair also listens on the document, and capture listeners on both would run the app handler twice per press. * perf(studio): stop re-probing every restored selection member on load The hash carries the whole canvas selection, and restoring it asked the server whether each member still exists in the source — one request per member, awaited one after another. A marquee over a captured page puts hundreds of members in the URL, so every later load of that URL spent hundreds of serial round trips rebuilding the selection before the canvas answered anything, keypresses included. The marquee that produced those members already skips the probe. Restoring them skips it too; only the primary, whose panel reads the flag, still pays for one. * fix(studio): delete a canvas selection in one pass and say the key landed Reproduced with a real, focus-routed keypress instead of a synthetic one: the press does reach the handler and the delete does run to completion, but at hundreds of members it takes seconds during which the canvas is unchanged and nothing acknowledges the key. Silence for that long is indistinguishable from Delete being broken, and pressing it again or reloading mid-flight lands in a worse state. Two things, one per cause. The removal now sends the whole selection in a single request against a new remove-elements route, which reads the file once, drops every member and writes once — it was a round trip AND a full rewrite of the file per element. And a multi-element delete announces itself before the work starts, so the press is visibly acknowledged instead of leaving the canvas looking untouched until it finishes. Measured on a captured page, 84 members: 933ms of serial round trips against 84 rewrites, down to 583ms and one. * refactor(studio): narrow the SDK delete targets instead of asserting them The batch SDK path guarded on every member having an hfId and then asserted it away per member. Narrow once into a string list so the guard and the values come from the same place, and drop a threaded content variable that never changed — the SDK owns the document it edits, so every member is removed against the same starting content. Also mounts the new forwarding test through the existing harness rather than repeating its setup. * fix(studio): stop Delete acting on a canvas selection the user replaced Two things the reordered Delete arbitration got wrong, both found in review. A clip with no canvas node left the canvas selection pointing at whatever was picked before it, and the canvas branch wins whenever that ref is non-null — so selecting an audio clip and pressing Delete removed the previously selected canvas element and left the clip, right after the toast said the clip was not in the preview. The timeline fallback the comment described could not be reached. Clearing that selection has to stay quiet: the clear is announced to the timeline, so echoing it would deselect the clip that was just picked. Expanding the primary to the marquee group also moved out of the delete handler and up to the Delete key. Cut copies the primary alone, so expanding for every caller put one element on the clipboard and removed every other member with it — undo brought them back, paste restored one. The rule is a named function now, so the two callers can differ without either guessing. Also throttles the off-canvas indicator rebuild, which the cap had been hiding. It walks every element in the preview and reads layout for each — measured at 6.5ms on an 825-element captured page against a 16.7ms frame — and what marks it dirty is a MutationObserver on inline style, which is how animation writes. * fix(studio): hold the canvas selection inside the timeline selection The stale-canvas-selection defect survived at the second writer. The store-driven sync bails when a member has not resolved yet and returned without touching the canvas, so a pick with no canvas node at all left the previous selection in place — and Delete acts on the canvas first, so it deleted that. Reachable from the sidebar audio and asset reveals and from an asset drop, none of which go through the handler already fixed. Clearing on every bail would be wrong: the bail exists for a member whose node is not ready, which a later run resolves, and clearing there would flicker. Only a canvas anchor that resolves OUTSIDE the current selection goes, which is the state that is dangerous rather than merely unfinished. Quietly, for the same reason as the first writer: announcing would deselect the clip just picked. The invariant is named now, since Delete depends on it: the canvas selection never points outside the current timeline selection. Also drops the x-hf-removed header, which nothing read and whose comment promised a partial-vs-no-op distinction the response cannot make, and pins the indicator throttle that was measured but uncovered. |
||
|
|
0e3c5f6bef |
feat(cli): give every preview lifecycle op one JSON document (#3309)
`--status`, `--stop`, `--list` and `--kill-all` emit a schema-versioned envelope with an `ok` discriminant under `--json`, from one writer and one failure-payload builder. Human output is unchanged; the JSON path is additive. The value is in the failure paths. An agent that gets a bare error line on stderr and an empty stdout cannot tell a crash from a "not running", so every failure is a document too — including a missing project, which under `--json` resolves through the throwing resolver rather than the human-shaped nudge. |
||
|
|
e282ff15cc |
fix(audio): raise the authoring gain ceiling and carry it through the probes (#3333)
* fix(audio): raise the authoring gain ceiling and carry it through the probes Builds on #3328, which made the preview graph apply author gain and user volume exactly once each. That ownership is now correct but everything is still clamped to 1.0, so a clip authored above unity cannot be heard or rendered. `HTMLMediaElement.volume` is spec-clamped to [0,1], so both timeline probes lost a clip's authored gain the moment it also carried a fade: the probe seeded the element at the clamped value and every sample read back at or below 0 dB, and the mixer prefers probed keyframes over the static volume. Both probes now shadow the accessor for their own duration and forward the clamped value to the native setter, so the authored gain survives while nothing outside the probe ever sees an illegal volume. Measured on one 6 s composition, first 4 s: unity -32.8 LUFS, boosted-with-fade -32.8 before and -27.0 after — +5.8 dB, exactly the gain the clip was authored at. One ceiling, defined once in `audioGain.ts` and reachable from both sides: the render mixer imports it, and the page-serialized probe takes it as a parameter rather than re-literalling it. User volume stays spec-clamped — it is a fader, not a gain. Also holds the percent volume slider above unity in both property panels. That control tops out at 100%, so one touch would cap a boosted clip and drop up to 12 dB that now genuinely renders; the dB fader that can represent these levels replaces it in the next PR. * fix(audio): carry a static above-unity gain onto the preview gain node Review follow-up. `setElementVolume` receives the clip's author gain and clamped it to [0,1], so a static `data-volume` above unity was capped on the WebAudio preview path while the render honoured it — the exact preview/render divergence this ceiling exists to close. Automation lanes hid it: they schedule ramps onto the param directly and never pass through here. The master volume beside it stays spec-clamped, because a user fader is not a gain. Verified by mutation: restoring the [0,1] clamp reds the new case. Also scope the leveller's rationale to this rung — `VOLUME_RANGE` still stops at unity until the dB fader lands, so "both now span the same range" was premature — and say why the GSAP-tracking fallback is unity-capped: it reads back through `el.volume`, which the spec pins to [0,1], so it cannot observe an above-unity value however wide the clamp gets. * fix(audio): restore the live test files this branch overwrote, and uncap preview Review blocker: three files were wholesale copies from the abandoned #3304 branch laid over a two-day-newer base, so they silently reverted work that had landed in between. CI could not see it — deleted tests do not fail. - `audioMixer.test.ts` was byte-identical to #3304's head: 1186 lines against a base of 1353. Gone with it were the `data-playback-start` fallthrough cases from #3322 — merged 54 minutes before this branch's own merge base — and all retiming coverage (`playbackRate` 7 to 0, `atempo` 5 to 0), the strict literal-timing table, and the zero-window cases. - `mediaVolumeEnvelope.test.ts` dropped the trailing-garbage duration case and the plateau-retention case. - `packages/core/package.json` rolled the package version back 0.8.3 to 0.7.109. All three are restored from `main` with only this PR's additions re-applied on top, and the subpath export is regenerated by the repo's own script rather than hand-edited. Also closes the preview/render split the same review raised. Two clamps had to go, not one: `setElementVolume` capped the author gain at the transport, and the first-tick branch in `syncRuntimeMedia` trusted `el.volume` — which is spec-bound to [0,1] and so cannot represent a boost, opening a boosted clip at 0 dB for one tick before the steady-state branch took over. Both pinned by tests, both verified by mutation. |
||
|
|
74149e249a |
fix(cli): keep a live preview's ownership record and stop past a bad one (#3308)
* fix(cli): keep a live preview's ownership record and stop past a bad one A missed liveness probe is not proof the preview is gone — a server blocked on a Puppeteer capture answers nothing for a second or two — but any miss retired the session record, and the record carries the only PID-reuse guard `--stop` has. Reproduced by SIGSTOPping a managed preview and running `--status`: the record was deleted and never came back, leaving every later stop to fall through to an unauthenticated port scan with no ownership proof at all. Only a wrapper process that is provably gone now retires a record. That record gains a process-birth token so a recycled PID reads as a different process, and it is written through a temp file and renamed — every reader deletes it when it fails to parse, so a torn read would otherwise destroy a live server's proof of ownership. Two failure-propagation bugs in the stop path: `--kill-all` collected the first unprovable record's exception and abandoned every server after it, so they were left running AND unreported; and a replacement refused to launch when the server it was replacing had already exited on its own, which is the goal state rather than a failure. `--list` now shows managed sessions ahead of whatever else answers the scan. * fix(cli): keep a record whose identity lookup gave no answer, not a different one Review blocker. The keep-alive path this PR adds could still retire a LIVE record — through a different door than the one it closed. `processIdentity` catches every failure into `null`, and on two of three platforms that failure is a subprocess timeout on a live process: the win32 `Win32_Process` CIM query and the POSIX `ps -o lstart=` both run on a 2 s budget, under exactly the load that made the HTTP probe miss in the first place. A `null` compared unequal to the saved token, so the record was deleted and `wrapperIdentity` — the only PID-reuse guard `--stop` has — was gone for good. Only Linux, reading /proc directly, was reliable. No answer is now distinguished from a different answer: the PID is checked with `kill(pid, 0)` first, which asks the kernel without signalling and treats EPERM as alive. A PID nothing can signal is gone and retires the record with no subprocess at all; a signalable PID whose token cannot be read keeps it. Only a token that comes back and differs retires it. That ordering also answers the `--list` note: the identity subprocess no longer runs for the stale records that made it slow, so the N x 2 s worst case is gone along with the timeouts that fed the bug. Verified by mutation: restoring the old "no answer means gone" behaviour reds the new case. Also clean up the temp file when a rename fails, rather than orphaning it in the session directory. * test(cli): assert only what the birth-token lookup actually guarantees `captures a stable birth token for the current process` made two assertions that a lookup allowed to fail cannot support. `processIdentity` returns null whenever the lookup cannot be completed — not only when the process is absent — and on Windows and macOS it shells out to PowerShell or `ps` on a 2 s budget that a cold CI runner routinely outruns. Both failed on windows-latest, in sequence: first `.toMatch()` received null, and once that was guarded, `expect(second).toBe(first)` compared a null from the cold first spawn against a token from the warm second one. Two lookups can disagree for exactly one reason — one of them failed — so stability is only assertable across two successful ones. The token itself cannot change between calls; it is a birth timestamp and the process did not restart. `processIdentity(-1)` stays unconditional: the guard rejects it before any subprocess runs. The strict shape assertion moves to a Linux-only case, where /proc is read directly with no subprocess and null is genuinely not allowed — keeping the guarantee on the one platform that can honour it rather than dropping it everywhere. Callers already depend on this contract: `wrapperProcessIsAlive` treats null as "no answer" rather than "gone" precisely because it is reachable. |
||
|
|
b31dde35b1 |
fix(producer): clamp embedded video/audio windows to the scene (#3332)
Browser probe end reflects full source duration, not the scene slot. Never extend a compile-time end — only fill missing or shrink — so long recordings sliced across short scenes don't inflate extraction windows and time out black on Cloud Run (ARC-13403). |
||
|
|
c1c70f44bd |
fix(cli): signal only processes the OS says own the port (#3307)
* fix(cli): signal only processes the OS says own the port `/__hyperframes_config` is unauthenticated and the PID it reports is what `--stop` and `--kill-all` send signals to, so any local process answering on a scanned port could name an arbitrary PID and have the CLI kill it. Reproduced with a twenty-line HTTP server on a scanned port self-reporting an unrelated PID: before this, `--kill-all` killed that process; after it, the process survives and only the real listener is stopped. The listening PID now comes from the OS — `lsof`, and `netstat` on Windows, where the lookup was previously unavailable and the self-reported value was taken on trust. The response's own PID is used only where the OS lookup fails, which is also the only case where it is unfalsifiable. Orphan cleanup moves to the last step before a launch. It reaches outside the process and kills other people's PIDs, so it must not run for an invocation that turns out to be a validation error and never starts anything. * fix(cli): fail closed when the OS cannot confirm who owns a port Review follow-up. The two halves of this change picked opposite directions for the same condition. `isProcessDescendant` fails closed by design; `activeServerOnPort` fell back to the self-reported PID whenever the OS lookup came back empty — and that is not only "unsupported platform". `lsof` may be absent (the default on many slim images), may time out, or may not see a socket owned by another user. On such a machine every scanned port silently reverted to pre-change behaviour, with nothing said. Provenance is now part of the type rather than a convention: `ActiveServer` carries `pidSource`, so a caller cannot mistake a self-report for the kernel's answer. `--kill-all` requires `"os"` and skips the rest, naming the ports it left alone and why. That is the deliberate trade — a blind sweep of a port range has no evidence beyond an unauthenticated response, so an unconfirmed PID must not be signalled. Managed previews are unaffected: they stop through their session record, which proves ownership by process birth identity. The fallback branch — the one with the security consequence — now has the coverage it lacked, via an injected lookup matching the seam `testPortOnAllHosts` and `isProcessDescendant` already use, including a live process that survives because nothing confirmed it owns the socket. Also state that `killProcessTree` honours `signal` on POSIX only: Windows always passes `/F`, deliberately, since taskkill without it posts WM_CLOSE that a console process may ignore. The caller-side comment claiming Windows cleanup is a no-op described the code before this change and now says the opposite. |
||
|
|
3e4b08cdc1 | chore: release v0.8.3 (#3327) v0.8.3 | ||
|
|
995c9e346e | fix(core): separate author and user audio gain (#3328) | ||
|
|
afafca4b96 |
feat: make creator media edits render-safe (#3322)
* feat: make creator media edits render-safe * fix: align media playback timing * docs: add creator editing recipes * docs: expand creator editing guidance * fix: unify media source offsets * fix: scale natural media duration * fix: preserve natural media zero spans * fix: align compiled natural media timing * test: classify compiler media test as integration * fix: drop inactive media windows * fix: unify literal timing parsing * fix: keep browser media parsing serializable * fix: keep page timing readers strict * fix: close remaining preview timing gaps * fix(core): preserve Studio voice pitch at playback speed * chore: keep creator contract source-neutral |
||
|
|
049f5618d7 | chore: release v0.8.2 (#3324) v0.8.2 | ||
|
|
406bf316a3 |
fix(catalog): make component previews answer their variables panel (#3323)
* fix(catalog): make component previews answer their variables panel Every control on a component's catalog page did nothing. Asking caption-camera-follow for a violet accent rendered gold, and so did green and blue, on 166 of the 168 components that declare variables. A component ships a snippet, which is what the page hands you to paste and which carries the declaration plus the script that turns a chosen value into a CSS custom property, and a demo.html which stages and animates it. The preview is built from the demo, and the demo was authored as a copy of the snippet rather than a reference to it. The copies drifted until almost none of them carried the declaration or the reader, so the payload for that page never contained the word violet at all. Components come in two shapes, so the repair does too. 123 ship a snippet that registers its own paused timeline. That snippet is a whole piece, so their preview is now built from it and carries markup, variables and motion together. 45 are markup plus a commented recipe, where the demo owns the motion. Those demos now carry the snippet's declaration, reader and var-driven CSS in the registry itself, written by scripts/catalog/sync-demo-variables.ts. Nothing is patched in at build time. A test runs that tool in dry mode and fails when a demo has drifted again, naming the command that repairs it. It also asserts it inspected more than a hundred components, because a check that silently matches nothing is how this rotted in the first place. Measured by rendering every payload in a real player rather than by reading markup: payloads declaring their variables go from 2 of 168 to 168 of 168, previews that animate go from 166 to 167, and nothing that moved stopped moving. ascii-render-pass and star-rating-fill render a still frame when built from their snippet, so they keep the demo path as a recorded exception and stay in the state they were already in. * refactor(catalog): give the preview pipeline one lookup and one entrypoint guard Follow-up on the same branch, no behaviour change: 42 tests still pass and `sync-demo-variables --check` still reports all 168 components clean. The payload generator and the demo sync had each grown their own copy of "given a component directory, find the snippet and the demo". Both now call `componentFiles`, which is the same duplication-by-copying that broke the previews in the first place. Both catalog generators also carried a byte-identical 12-line guard for "only run main() when this file is the entrypoint". That clone was already in the tree, but nothing had touched both files at once before, so it had never surfaced. It is now `runAsCommand`, and the sync script's variant of the same condition is `isEntrypoint`. The rest is flattening: the layering guards read as a table of conditions instead of a chain, the reporting splits by what it reports, and the entry resolution comes out of `buildPayload` rather than being spliced into it. Also runs the formatter over the demos this branch rewrote. Whitespace only, and `notes-typing` is the only component demo that renders pre-formatted text, which this does not touch. |
||
|
|
0d874adc68 |
fix(skills): count proxy-driver tweens in the animation map (#3301)
enumerateTweens dropped every tween whose targets() held no Element:
if (!targets.length) return;
That silently deleted the proxy-driver idiom — tween a plain object, apply the
motion inside onUpdate — which is real, visible animation. The consequence was
not just a missing row: computeDensity counted zero active tweens across the
tween's span, so findDeadZones reported animating time as DEAD, telling agents
to add motion to a stretch that already had it.
A target-less tween is now kept when a driver reaches it, marked
driver:"onUpdate". The driver can be the tween's own onUpdate, or the
TIMELINE's — the WebGL/uniform idiom is gsap.timeline({ onUpdate: renderFrame })
over children that tween plain uniform objects and carry no onUpdate of their
own, so walk() threads a `driven` flag beside parentOffset.
The discriminator is what keeps this from trading one false reading for
another. A bare `tl.to({}, { duration: D })` spacer produces nothing, and every
preset caption skin ends with exactly such a full-span anchor; counting those
would mask genuine dead zones. So a tween's own onUpdate is proof of work by
itself (a repaint loop need not animate a property), while an inherited driver
additionally requires the tween to change something.
There is no element to select or measure for a driver tween:
* selectorHint is null rather than a placeholder — it feeds
document.querySelector, so it must be absent, not unmatchable;
* bbox sampling is skipped; the report shows "(onUpdate driver)";
* computeFlags guards its geometry flags on bboxes.length, since [].every()
is vacuously true and would report an unmeasured tween as both degenerate
and invisible;
* describeTween says the motion is applied in JS and no geometry was measured;
* the per-element analyses (buildElementLifecycles, detectStaggers) run over
element-backed tweens only, so drivers cannot collapse into one pseudo-
element or invent a stagger. Density, dead zones and the timeline still
count them — those are per-span, which is what a driver has.
Verified end to end: a 4s composition with an element tween over 0-1s and a
proxy driver over 2-4s went from "1/1 tweens, dead zones: 1.5-4s" to "2/2
tweens" with no dead zone.
|
||
|
|
a41da86517 |
fix(skills): declare the caption brand font's style axis, not just its weight (#3300)
A font filename encodes more than a weight, but only the weight was ever read out of it, so two faces of one family collapsed onto a single slot. Google Fonts ships Newsreader as Newsreader-Italic-VariableFont_opsz,wght.ttf and Newsreader-VariableFont_opsz,wght.ttf. The italic sorts first, both scored 400, so the italic claimed the family's only 400 slot, the upright was dropped as a duplicate, and the surviving face was declared with no font-style at all. @font-face is deliberately global — the composition CSS scoper exempts it, since a face declaration cannot be scoped — so mounting captions re-pointed the whole document's Newsreader at the italic file and every sibling composition rendered in italics. Faces now carry a font-style descriptor and dedupe on weight AND style. The same fix had to land in build-frame.mjs, which renames captured fonts BEFORE captions.mjs sees them. It dropped the style token while renaming, so an italic file arrived as "Newsreader-Regular.ttf" and was then asserted upright — leaving the global normal slot pointing at italic bytes even once brandFontFaces understood styles. The staged filename is a contract: it must carry every axis that distinguishes one face from another, and the dedup key must be the whole face. Second axis, same misparse: weight parsing matched WORDS only, so a Fontsource capture (inter-latin-500-normal.woff2) scored a whole family 400 and shipped one of its faces. A numeric axis in the filename now wins over the word heuristic, anchored so it is not read out of the middle of a hash-named capture file — a non-digit before it and no alphanumeric after, which keeps both the 4-digit guard and separator-free names like Roboto900.ttf. Tests pin both ends of the contract: a round-trip asserting the names build-frame stages map back to the right weight+style through the real brandFontFaces, plus a source check that no copy reverts to a weight-only name or a hardcoded font-style:normal. captions.mjs also gains a parity pin across the three workflows that ship it. Not addressed: a VariableFont file is still declared at a single font-weight rather than its range, so weights it could interpolate are still synthesized. |
||
|
|
f8a1e2d315 |
fix(skills): pin UTF-8 in Python scripts instead of the platform code page (#3298)
Windows sizes Python's stdio and text-mode file IO to the ANSI code page
(cp1252), not UTF-8. Every skill Python script relied on that default:
* analyze-beatgrid.py --print writes the glyphs cp1252 has no slot for
(delta, arrow), so the brief died with UnicodeEncodeError on every Windows
run — the reported crash;
* its audiomap write_text() pairs ensure_ascii=False with the default file
encoding, so a non-ASCII payload is unwritable there too;
* lint_source.py read_text() raises UnicodeDecodeError before any rule runs
when a Remotion source carries an em dash or a curly quote;
* gen-stroke-path.py reads an SVG font whose glyph keys ARE literal
characters, so a mis-decoded key stops matching the requested text.
Stdio is reconfigured to UTF-8 at import and every text-mode IO call names its
encoding. `errors` is carried across the reconfigure: it resets to "strict",
and CPython gives stderr "backslashreplace" on purpose so the diagnostic path
can never itself raise.
extract-audio-data.py also decoded ffmpeg's stderr strictly while reporting a
failure, which would bury the very error being reported on a Windows ffmpeg.
skills/python-encoding.test.mjs guards the class: it fails if any skill Python
script drops the stdio block or omits encoding= on a text-mode IO call. The
mode is read as a whole comma-delimited argument of mode characters only, so a
payload key like {"bpm": 120} cannot spell the check away.
Verified with a cp1252 stdio stream installed before module load, matching how
Windows starts the interpreter: pre-fix UnicodeEncodeError, post-fix both
glyphs present in the UTF-8 bytes. Not run on real Windows hardware.
|
||
|
|
ad84b00c90 | chore: release v0.8.1 (#3319) v0.8.1 | ||
|
|
d7688f9943 |
fix(docs): load the player from latest, not a pinned minor (#3320)
* fix(docs): load the player from latest, not a pinned minor The catalog pages pinned the player CDN URL to a minor line, and that pin sat one line behind after the last release. Every page kept rendering, on the older build, so nothing surfaced it: the only symptom was that a fix published to npm never appeared on the docs. The generator derived its pin from the player's package.json, which is correct only if every page is regenerated on the release that moves it. That is the step that did not happen, and it has to happen across 175 generated pages plus three hand-written files for the pin to be true. A version carried in step across 178 places will be stale, and stale here is silent. Ask for latest instead and there is nothing to carry. This costs the ability to hold the docs back from a bad player release. Paid deliberately: the pin did not buy that either, it only delayed the good releases too. A test asserts no pinned version comes back, and fails if it stops finding the references at all, so it cannot pass by matching nothing. * refactor(scripts): list tracked files instead of walking the tree The pin guard hand-rolled a recursive directory walk with its own skip list and size cap, which the audit flagged: helpers living in a test file earn no coverage, so their complexity lands straight on the CRAP score. git already knows which files to read, and ignores node_modules and build output for us, so one call replaces the walker and both findings go away. |
||
|
|
5058236eda |
feat(studio): prompt to install FFmpeg before Export, not after (#3314)
Exporting without FFmpeg installed used to show "Server error (503). Check
the terminal for details." The server already knew the exact cause and sent
a per-platform install command in the response body; Studio discarded that
body and printed the status code. The user found out only after the
composition was finished.
Studio now asks the dev server on load whether this machine can encode, and
the Renders panel shows the cause plus a copyable install command when it
cannot, with a Recheck that avoids restarting Studio.
- New GET /api/environment/ffmpeg calls runEnvironmentChecks() with every
optional check off, which is exactly the FFmpeg and ffprobe pair `doctor`
runs, so Studio and the CLI cannot disagree. Only a passing result is
cached.
- The refusal lives in startRender, not in a button. Studio renders from
three places (the panel's Export, the header's, and each composition card
in the sidebar), so a per-button check would leave the others free to
queue a render that cannot finish. The header and sidebar controls reveal
the prompt rather than going dead.
- A null probe result means "no answer", not "missing", so an older or
unreachable dev server cannot lock a working setup.
- Failed render responses now surface the server's { error, hint }.
- getFFmpegInstallCommand() is the single owner of platform-to-command, with
the prose hint derived from it. Windows gains a winget command and keeps
the manual download route.
Accessibility: the prompt's explanatory line measured 2.2:1 on the card's
amber background against a 4.5:1 minimum, because the panel's usual grey for
secondary text does not survive the tint. Now 6.6:1. Keyboard focus was
invisible on all three controls and now matches the panel's focus ring.
Also folds in cleanups the repo's gates required: the Renders tab moves out
of StudioRightPanel (it was at the 600-line cap and every field it needed was
already on the shell context), StudioContextInput stops keeping a second copy
of the renderQueue shape, and the server tests share one temp-project helper.
|
||
|
|
ea7c48f372 |
fix(add): make chosen variables actually take effect, in the CLI and the preview (#3316)
* fix(add): apply --vars to components, and explain a failed download
Customising an item on the catalog page, copying the printed command and
running it did nothing for a component. `--vars` was accepted, documented
and then dropped: buildSnippet put the values on a block's mount element
and returned a bare "paste from ..." comment for a component, so 221 of
the 375 catalog items silently ignored every value the page produced.
A component has no mount element to hang values on. It is markup pasted
into a host, and it resolves values through __hyperframes.getVariables(),
which merges the declared defaults of every [data-composition-variables]
element in the document with render-time overrides. So the component's
own declaration is the only place a chosen value can live and still be
there after the paste. `add --vars` now rewrites those defaults.
Blocks keep the mount attribute. Per-mount values are strictly better
where a mount exists: the file on disk stays byte-identical to the
registry's, so a later reinstall can still tell an edit from an update,
and two mounts of the same block can differ.
A value the item cannot accept is now refused rather than written. An
out-of-range number or an unlisted enum value falls back at runtime and
warns, so writing one would produce a file that renders exactly as if the
value had been ignored -- the failure this change exists to remove. Ids
the item never declared are reported too, instead of vanishing. Only the
requested item is rewritten; a dependency dragged in behind it never
declared these variables.
Separately, `Install failed: fetch failed` is now a sentence. Item FILES
are not cached (only manifests are), so a network blip surfaces as node's
bare message with no URL and no cause, immediately after the user copied
a command off a web page -- which reads as "the command was wrong" rather
than "the network was". It now names what failed, says it is usually
connectivity or a proxy rather than a bad command, and mentions
HTTPS_PROXY.
Also fixes the two transcribe tests that were failing before this branch.
They assert the whisper soft-skip path but never pinned the engine, and
`auto` picks Parakeet whenever parakeet-mlx is installed -- so on those
machines the test shelled out to a real ASR binary, failed with "Parakeet
did not produce output", and landed in the generic failure branch it
claims is never taken. Pinned to `engine: "whisper"`, plus an assertion
that the mocked transcribe actually ran, which is what stops the test
passing on a machine without Parakeet while testing nothing on one with
it. The file now runs in 18ms rather than 3.7s, because it no longer
launches a subprocess.
Test plan: 10 new tests for the rewrite (enum and range refusal, the
numeric-string coercion the catalog URL depends on since every query
value is a string, delimiter escaping, unparseable declarations) and 3
for the failure message. Full CLI suite: 2661 passed, ZERO failures.
Verified as a user, not just in unit tests: installed blur-in with the
exact reported command, confirmed the declaration carried 76 / accent /
center, pasted it into a composition and ran `check` -- which reported
canvas_overflow at 76px, which only happens if the baked size is really
in effect. Bad values warn and are refused; blocks still emit
data-variable-values.
* fix(player): load the runtime before the body, not after
Customising a component on a catalog page did nothing to the preview.
badge-pop with count 10 and a green accent rendered 3, in red.
The probe injects the runtime by appending a script to an already loaded
document, and only once it has a reason to: a nested composition, or five
polls with a timeline present. A component has neither. It is markup
pasted into a composition, and it reads its values in an inline IIFE that
runs while the body is parsing:
var vars = window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables() : {};
With the runtime arriving afterwards that guard always took the empty
branch, so the component used the defaults hardcoded in its own script
and every chosen value was dropped. The values were never the problem:
the preview sets window.__hfVariables correctly, and nothing was there to
read it.
prepareSrcdocForElement now puts the same runtime URL in the document's
head before the srcdoc is set. A classic external script in head is
parser-blocking, so it runs before body scripts without changing what
gets loaded or adding a dependency the player did not already have. A CLI
render never had this bug because the engine already orders it this way.
Skipped when the page carries the runtime already, so a CLI-rendered page
(which inlines it) does not get a second copy re-initialising the runtime
underneath a live composition. The probe's late injection stays for the
src= path, where there is no srcdoc to prepare. The runtime URL moved to
its own module so the two injection points cannot drift apart.
Test plan: 8 new tests for the injection (ordering against the reading
script, head placement, both no-op guards, missing head/body, attributes
on the head tag). Three srcdoc tests asserted byte-identical forwarding
and now assert what they were actually protecting -- that the composition
arrives intact -- plus the new runtime guarantee. player 338 passed,
studio 4249 passed.
Verified end to end against the real runtime and a real registry
component, asking for size 96 / accent / right:
before 52px, rgb(243,243,243), flex-start, runtime absent
after 96px, rgb(60,230,172), flex-end, runtime present
rgb(60,230,172) is #3ce6ac, the accent green. That is the reported bug
before, and the chosen values after.
* fix(add): name the registry and the real reason an install failed, and retry
`Install failed: fetch failed` was two words that describe every network
problem equally badly. Three things were missing, and each of them was
the whole answer in a different case.
The URL. undici throws with no URL attached, so a project that points
`registry` at a private host in hyperframes.json got a message that
looked like the public registry had failed. Naming the URL is the entire
diagnosis there.
The cause. undici buries the real reason one or two levels down in
`cause`, and it was being dropped. The reported failure turned out to be
`self-signed certificate in certificate chain`: a private registry whose
certificate node refuses and curl accepts, which is why the host looked
healthy from a terminal. That sentence tells the reader which knob to
turn; `fetch failed` sends them to check a connection that is working.
The retry. Item files are the one uncached path -- manifests fall back to
a stale copy, but every install downloads its files fresh -- so a single
blip killed the whole command. Now two extra attempts with short backoff,
and deliberately NOT for TLS failures: a self-signed certificate fails
identically every time, so retrying it only makes the user wait three
times as long for the same message.
Also retypes the declaration reader. It modelled variables as a local
interface of six `unknown` fields and re-checked each one at every use.
Core already owns this shape as a discriminated union and exports
`isCompositionVariable`, the same predicate `parseCompositionVariables`
filters with, so the union is used directly and the duplicate type is
gone. A declaration the schema rejects now leaves the file untouched
rather than being partially rewritten from guesses.
Test plan: 4 retry and URL tests, 5 cause-chain tests, and the add-side
tests now cover the custom-registry hint and its absence on the default
registry. The variableDefaults fixtures gained the `label` the schema
actually requires; without it they were not valid declarations, which the
stricter reader caught. CLI suite 2671 passed, zero failures.
Verified with the BUILT dist rather than the source, in the reporter's
own project directory. The failure now reads:
File fetch failed: https://<host>/registry/components/blur-in/blur-in.html
- fetch failed (self-signed certificate in certificate chain
[SELF_SIGNED_CERT_IN_CHAIN])
and once the project points back at the public registry the original
command succeeds with `variables applied: size, tone, align`.
* fix(registry): name the registry on the not-found path too
The item-file failure now names the host it could not reach, but the
sibling path did not. A project whose registry is unreachable at the
MANIFEST stage got `Item "blur-in" not found - registry unreachable or
empty`, which reads as the public catalog having lost the item and sends
the reader to search a registry that never saw the request.
Same fix, same reason, applied where the other three call sites live so
one of them cannot stay behind: the message names the host and says it
came from this project's hyperframes.json, and only when it is not the
public registry, so the common case stays short.
Test plan: 3 tests covering the private-registry hint and its absence on
the default registry and on no registry at all. CLI suite 2674 passed,
zero failures. Verified with the built dist against a host with a bad
certificate:
Item "blur-in" not found - registry unreachable or empty. Contacted
https://self-signed.badssl.com/registry, set by this project's
hyperframes.json, not the public registry.
* fix(catalog): reconcile the two spellings of a compound word
`countdown` returned exactly one item, the only thing tagged with that
spelling. `count down timer` returned sixteen, and that one was in none
of them. The tokenizer splits on word boundaries, so the two spellings of
a single idea produced disjoint sets, and whichever phrasing an author
happened to type decided which half of the answer they saw. Neither half
was the whole answer: the one-word spelling hid count-up and
decline-chart, which are the two things you would actually build with.
Both directions now, each gated on the catalog's own vocabulary so this
can only add signal. A query token is split when both halves are words
the catalog uses, and adjacent tokens are joined when the compound is.
A word in neither form, like `timer` which appears in no item, is left
alone: this widens phrasing, it does not invent matches.
Everything inferred this way carries a fraction of a real token's weight.
That is the part worth keeping honest, because the first version relied
on the halves being statistically common in a 375-item catalog, which is
not the same as making them count for less. In a small corpus that
version let `type` matching the name of `type-match-cut` outrank
`typewriter` matching the name of `typewriter`: searching a word returned
something that merely contained half of it. Two tests written against
that real failure caught it.
All spellings now return the same 17 items, and each still ranks its own
exact match first: `countdown` leads with yt-circle-pointer, `count down`
leads with the two-word items, and count-up and decline-chart appear in
both.
Test plan: 6 new tests covering both directions, the identical-set
property that was the actual defect, exact-match precedence, an unknown
word left alone, and the typewriter case. Eval set unchanged at 33/39
top-1 and 39/39 top-3, so no query regressed. CLI suite 2680 passed.
|
||
|
|
232686f7e0 | chore: release v0.8.0 (#3318) v0.8.0 | ||
|
|
ed18f1ea9a | chore(catalog): remove two AI UI items (#3317) | ||
|
|
4403b8beef | chore: release v0.7.111 (#3315) v0.7.111 | ||
|
|
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 |
||
|
|
6b17c24f98 |
fix(catalog): rank on where a word appears and how rare it is (#3312)
* fix(catalog): rank on where a word appears and how rare it is Word search returned the right move in the top three for 87% of a 39-query eval set built from real catalog intents. Three defects, all in the same 75-line scorer, and all found by running the queries rather than by reading the code. A token matching an item's NAME counted exactly as much as one buried in a description. Searching "typewriter effect on a title" ranked the item literally called `typewriter` seventh, behind entries that merely mention typing. Name and title now carry three times the weight: an author who types a move's name is giving the strongest signal available and it was being averaged away. Plurals shared no vocabulary with the singular. "a stat that counts up and then pulses once" matched nothing in a description reading "lands with a restrained scale pulse", because `counts` is not `count`. Adding detail to a query made results strictly worse, which is the opposite of what a search should do. Plurals now fold, and only plurals: Porter would fold `counter` to `count` and `values` to `valu`, merging moves that mean different things. Field weighting alone made one case worse, which is why inverse document frequency is here too. "reveal a headline one line at a time" put every item merely NAMED `*-reveal` on top, because one strong hit on the catalog's most common word outscored several weak hits on the words that actually narrowed it down. Rarity now scales each term. Separately: a query in a script this ranker cannot index no longer reports itself as an empty catalog. Tokenising on [a-z]+ leaves nothing of a Japanese query, and returning "no items match" told the author the catalog lacked a move it may well have, then invited them to file a gap report about it. That case now says what actually happened and withholds the gap prompt, since nothing was searched. Measured on the same 39 queries, before and after: top-1 31/39 (79%) -> 33/39 (85%) top-3 34/39 (87%) -> 39/39 (100%) Test plan: 13 new tests, each a real failing query reduced to the smallest fixture that still reproduces it. Existing tests migrated to the fields API (two callers total). Full CLI suite 2643 passed, 2 pre-existing transcribe failures unchanged. Verified against the real CLI: "typewriter effect on a title" now returns typewriter first, and "chat conversation between a user and an assistant" returns chat-message, chat-thread, ai-chat-reveal instead of transitions-blur. * docs(skills): say to query the catalog in English The runtime message added alongside this explains an unsearchable query after the fact. Saying it up front is cheaper: an agent that never writes the query in Japanese never sees the error, never wastes the turn, and never files a gap report about a component that exists. Worth stating rather than assuming, because the mistake is a reasonable one. On a Japanese or Chinese project the brief, the narration and the captions are all in that language and the query naturally follows. The rule is that the query language and the video language are unrelated: describe the move in English, write the on-screen copy in whatever the video needs. Both skills that own `catalog --query` carry it, and those are the only two that mention the command at all. * fix(catalog): fail a non-English query instead of returning nothing The message explaining an unsearchable query went to stdout and the command exited 0. An agent that checks the exit code, which is most of them, read that as "searched successfully, the catalog has nothing" and went off to hand-author a move that is sitting in the registry. The explanation only helped a human who happened to be reading the terminal. It is bad input, not an empty shelf, so it now behaves like one: the guidance goes to stderr and the command exits 1, matching what an invalid --type already does. A genuine empty result, where the query parsed fine and the catalog simply has nothing, still exits 0 -- that distinction is the whole point, and both halves are pinned by tests. The wording now also says what to do rather than only what happened: search in English, and let the on-screen copy of the video stay in whatever language it needs. That was the part agents were getting wrong, since a Japanese project makes a Japanese query feel natural. Test plan: 3 new tests covering the exit code, the wording, and the genuine-empty case that must stay at 0. Also asserts the gap-report line is absent, since nothing was searched and a report there is noise in the one signal that tells us what to build. catalog.test.ts 32 passed; commands + registry suites 887 passed with the 2 pre-existing transcribe failures unchanged. Verified against the real CLI: a CJK query exits 1, a genuine miss exits 0. |
||
|
|
35eb6d2906 |
refactor(studio): split the two files over the 600-line cap (#3313)
The file size check has been failing on main. It is diff-scoped on a PR but full-scans packages/studio on push, so two files that crept over the cap were only ever caught after merge, and every release since has been red: TimelineAutomationLane.tsx 674 lines StudioRightPanel.tsx 609 lines Both are pure code moves. No behavior change. TimelineAutomationLane.tsx keeps the single-lane editor and gives up the track-level layer: ClipLaneRow, ClipAutomationLanes and TimelineAutomationLaneSlot move to TimelineAutomationLaneSlot.tsx, which is the name its test file already used. The dependency runs one way, slot -> lane, so there is no cycle. 674 -> 499. StudioRightPanel.tsx gives up its props interface to a sibling .types.ts. That block is the part that changes least, so moving it keeps the component's own diffs small; two in-flight branches touch this file and both are based on a 556-line copy of it, so keeping the cut away from the body matters. 609 -> 568. Verified by running the CI rule's full-scan branch over the 679 tracked packages/studio source files: no file over 600, exit 0. Studio suite green at 2790 passed across 226 files, and typecheck clean. |
||
|
|
0285a711e9 |
feat(core): expose pretext text measurement on window.__hyperframes (#3302)
## What
Exposes the `pretext` text-measurement API on `window.__hyperframes`, so the API our agent-facing docs already describe actually exists.
Adds `pretext.prepare`, `.layout`, `.prepareWithSegments`, `.measureLineStats`, `.measureNaturalWidth`.
## Why
`skills/hyperframes-core/references/determinism-rules.md` is required reading for any agent authoring a composition. Line 59 tells them to call `window.__hyperframes.pretext.prepare(text, font)` then `pretext.layout(prepared, maxWidth, lineHeight)` for text measurement without a DOM reflow.
That object did not exist. The runtime exposed exactly `fitTextFontSize` and `getVariables`. Any composition following the documented recipe threw at runtime.
Deleting the doc line was the smaller change, but reflow-free measurement is genuinely the right tool for sizing text per frame, and `fitTextFontSize` is already built on it. Making the docs true is the better fix.
## How
- New `packages/core/src/text/pretext.ts` assembles the exposed surface in one place, with the include/exclude rationale next to it.
- `entry.ts` attaches it alongside the existing helpers. Sub-compositions inherit it for free: the scoping shim builds its scoped variant with `Object.assign({}, base, { getVariables })`, so anything added to the base object is carried through.
Two deliberate decisions:
**Wider than the doc named.** `layout()` returns only `{ lineCount, height }`. The doc's own "shrinkwrap containers" use case needs a width, which is impossible with just `prepare` + `layout`. `measureNaturalWidth` and `measureLineStats` make that claim achievable; `prepareWithSegments` is their required input.
**`clearCache` and `setLocale` withheld.** Both mutate state shared across compositions. Exposing them would let one composition change how a later one measures, making a render depend on what ran before it.
**Doc correction.** The reference called this "pure arithmetic, ~0.0002 ms per call". Not quite: `prepare` measures fonts through a canvas and throws outside a browser. Only the steps after a prepared string are arithmetic. Reworded, and documented the width helpers and the omissions.
## Trade-off
The runtime bundle grows **4,903 bytes (+1.30%)**, from 377,865 to 382,768. That ships inline in every composition. Measured by building the artifact with and without the change.
## Test plan
- [x] Unit tests added/updated
- [x] Manual testing performed
- [x] Documentation updated (if applicable)
`packages/core/src/text/pretext.test.ts` guards the shape of the published surface: the two documented names exist, the width helpers exist, and the two stateful functions are absent. Behaviour is deliberately not asserted there. `prepare` needs a canvas, and mocking it (as `fitTextFontSize.test.ts` must) would assert nothing real.
Real behaviour was verified by rendering a composition that calls the documented API:
```
lines=2 height=216 naturalWidth=1785
```
Self-consistent: natural width 1785 exceeds the 1600 container so it wraps to 2 lines, and 2 x 108 line-height is exactly the reported 216. The frame was inspected visually.
Also run:
- `packages/core` full suite from the package root: **903 passed, 46 files**
- `tsc --noEmit` and `tsc --noEmit -p tsconfig.runtime.json`: clean
- `oxlint` / `oxfmt`: clean
## Follow-ups (not in this PR)
An audit of the wider attribute surface found several more doc/runtime mismatches, including `data-gpu-mode` documented as an HTML attribute when it is a config field, and `data-no-timeline` being real, load-bearing, and absent from the table agents read. Those are separate changes.
|
||
|
|
5e36f7ac54 | chore: release v0.7.110 (#3303) v0.7.110 | ||
|
|
3c712d9ebd |
docs(changelog): weekly digest 2026-08-10–2026-08-17 (#3297)
* docs(changelog): weekly digest 2026-08-10–2026-08-17 Rewritten from `git log --no-merges` grouped by type and scope rather than polished from the generator draft. The window overlaps last week's digest. 106 non-merge commits land inside 2026-08-10T00:00:00Z..2026-08-18T00:00:00Z, but five were already published in the Aug 3-10 entry (#3148, #3149, #3150, #3089, #3151, all dated Aug 10). Those are excluded by set-subtracting the 109 SHAs that entry cites, so this covers 101 commits and cites 89 PRs. The release range is corrected the same way: last week claimed v0.7.90 through v0.7.105, and both the v0.7.104 and v0.7.105 release commits fall inside this window, so this week is v0.7.106 through v0.7.109. Verified at HEAD rather than from commit bodies: - The audio FX rack is behind the `audio-fx-rack` canary at percentage 0 in canaryRegistry.ts, and the gate site is real: PropertyPanelFlat.tsx reads isCanaryEnabled("audio-fx-rack"). It is described as staged, not shipped. The flag gates the authoring surface only, so a composition that already carries data-fx-chain still plays and renders, and that is stated. - The video primitive moves were added, reverted twenty minutes later because their previews did not deploy, then relanded with the workflow fixed. Only the end state is announced. - The CLI canary route was added and reverted inside the window, so the revert is what gets reported. - The preview volume control landed after v0.7.109, so it is called out as shipping in the next release rather than as available now. Every SHA is emitted by a script that refuses to write unless it resolves to 40 chars, prefix-matches its abbreviation, is an ancestor of origin/main, falls inside the window, and was not cited by the previous digest. Six negative controls confirm each check rejects, and a positive control confirms valid input still writes. No SHA was hand-typed. * docs(changelog): embed the weekly video in the Aug 10-17 digest 45.1s square film built from the changelog-video skill. Uploaded to static.heygen.ai and the CloudFront path invalidated and verified serving. |
||
|
|
37f8c48449 |
fix(catalog): survive an unreachable registry, and ask for the gap (#3299)
Serve an expired registry cache when revalidation fails, so one timeout against the registry host no longer reports the whole catalog as unreachable while a usable copy sits on disk. Hand back the gap-report command at the moment a search comes back wrong: catalog --query prints it pre-filled on both tiers, and every --json search envelope carries it as report_gap. Report on either tier, since the on-device tier needs a consented download and every gap reported to date came from the word tier. Document the gap channel in the registry skill, which owns hyperframes catalog and never mentioned it, and name the CLI commands no skill did. |
||
|
|
67edb01bf4 |
docs(skills): fix anime.js v3 syntax in v4 adapter guidance (#3064)
* docs(skills): fix anime.js v3 syntax in v4 adapter guidance
The animejs adapter docs teach v3 syntax against a v4 build, so the examples
cannot run as written: every v4 bundle assigns a *namespace object* to the
global `anime`, making v3's `anime({ targets })` call form a TypeError. `easing:`
is now `ease:`, ease names lost their `ease` prefix, and `timeline.add()` takes
`(targets, parameters, position)`.
- Rewrite `skills/hyperframes-animation/adapters/animejs.md` for v4:
`anime.animate()` / `anime.createTimeline()`, `ease:` names, targets-first
`add()`, position shorthands, and a note that the producer fixtures pin
4.0.2 (`lib/`) while 4.1+ moved bundles to `dist/bundles/`.
- Fix the same v3 `anime.timeline({ targets })` snippet in
`skills/hyperframes-keyframes/references/keyframe-patterns.md`.
- Stop advertising `anime.running` auto-discovery as a safety net. No v4 build
exports `running` (checked 4.0.2 and 4.5.0), so `discover()` returns
immediately and any instance a composition forgets to push onto
`window.__hfAnime` is silently never seeked. Marked v3-only/inert in the
skill page and the adapter docstring; explicit registration is now stated
as mandatory.
- Add render-safety notes the page lacked: `createSeededRandom()` as the
deterministic replacement for `Math.random()`, and why
`autoplay: onScroll(...)`, `createDraggable`, and pointer-driven
`createAnimatable` cannot work under headless seek rendering.
- Regenerate skills-manifest.json.
Runtime behaviour is unchanged: the `packages/core` edit is comment-only, and
the seek path already works on v4 (registered instances expose
seek/pause/play). The now-dead `anime.running` branch in `discover()` is left
in place — it is guarded and try/caught, and removing it is a behaviour change
that belongs in its own PR.
* fix(core): align Anime.js globals with v4
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
|
||
|
|
fecaf72d1f | feat(studio): add a preview volume control (#3282) | ||
|
|
b9a4dfcbe5 |
feat(registry): add avatar promo and Slack notification templates (#3279)
* feat(registry): add avatar promo and Slack notification templates * fix(registry): remove local emoji font dependency * fix(docs): regenerate Slack template catalog source |
||
|
|
de4062a933 |
fix: create temp dirs with mkdtemp, not a name built from Date.now() (#3241)
* fix: create temp dirs with mkdtemp, not a name built from Date.now() Closes nine open `js/insecure-temporary-file` alerts — the technically correct ones. An audit of all 29 open alerts for that rule split them three ways: - 19 false positives: the write lands inside a directory the caller already made with `mkdtempSync`, and CodeQL's dataflow reaches `tmpdir()` without seeing the mkdtemp in between. - 1 mitigated: `fontCompression.ts` writes with `flag: "wx"` and only takes the tmpdir branch inside Lambda, where /tmp is single-tenant. - 9 real, and these are them. A name built from `Date.now()` under the shared temp dir, followed by `mkdirSync`, is guessable to the millisecond AND leaves a window between choosing the name and creating it, so on a shared machine another user can pre-create or symlink the path first. `mkdtempSync` closes both halves: it picks the random suffix and creates the directory 0700 in one syscall. Same shape, one line shorter, and the alerts go away rather than being dismissed. Six sites in `normalize.test.ts` (its `mkdirSync` import goes with them), one in `generate-catalog-previews.ts` — that single construction accounted for three alerts, since the other two were writes into the directory it made. No shared helper. `mkdtempSync` is already the stdlib primitive for exactly this, and the two callers live in different packages, so a wrapper would need a home in core to serve one CLI test and one build script — more indirection than the line it saves. Deliberately not touching the other 20: excluding the rule repo-wide would hide this class of bug from future code, which is the reason these are fixed rather than silenced. * fix: track the wav temp dir for cleanup and finish the mkdtemp sweep The wav helper pushed the file path into `dirs`, so `afterEach` removed `tone.wav` and left the directory it had just made — four per suite run. Push the directory and derive the file path from it. Measured: the old code leaks 4 directories per run, the new code leaks 0. Three sites still built a predictable name and then created it. CodeQL never flagged them — its dataflow reaches the template preview writes through a `readdir` walk and does not connect them back to the `tmpdir()` root — so the alert list was narrower than the pattern, and closing only the alerts would turn the rule green while the shape survived where nothing would re-flag it. `generate-template-previews.ts` is the near-twin of the file this change started from, and the other two are producer dev entry points. All three use the path only through the variable, so the random suffix changes nothing. Catalog previews now call the existing `createCatalogPreviewTempDir` instead of repeating its body. That test was in no runner, so it pinned uniqueness and mode 0700 on a function nothing called; adding it to `test:scripts` alongside a real caller makes it load-bearing. The rationale for the primitive moves to the helper, which is now the only place it lives. * ci: re-run catalog previews when the temp-dir module changes Routing the renderer through `createCatalogPreviewTempDir` made that module part of its runtime path, and the workflow already states the rule for the sibling case: a module the renderer imports has to appear in the trigger, or a change to it alone never re-runs the job that exercises it. Add it to the `paths:` filter and to the renderer canary, so a PR touching only the temp-dir allocation still renders both shape canaries. Verified against this branch's own range: the previous argument list does not report the file, so a helper-only PR was invisible to both checks. |
||
|
|
12fd6d9087 | chore: release v0.7.109 (#3273) v0.7.109 | ||
|
|
c32b8041db |
fix(producer): propagate audio mixer config (#3239)
* fix(producer): forward ffmpeg timeout to audio mixer * fix(producer): propagate audio gain with timeout |
||
|
|
b1b368d0f0 | fix(studio): preserve media offsets when splitting clips (#3272) | ||
|
|
532caf7aa2 | chore(catalog): remove internal source markers | ||
|
|
f7d2260f9d |
feat(engine): stamp rendered files with hidden renderer provenance (#3264)
* feat(engine): stamp rendered files with hidden renderer provenance * fix(engine,producer): re-assert provenance at every container writer Review found that a no-audio MOV render still shipped untagged. The concat step is the last container write on that path (mux is skipped without audio, and applyFaststart only copies mov/webm), and the concat demuxer does not carry the chunks' container metadata through. The same hole applies to no-audio WebM, and to the in-process chunked encode in chunkEncoder, not just the distributed assemble path. mp4 was masked throughout because applyFaststart re-runs ffmpeg for that format and re-tagged the output. Tags the four remaining writers: the chunked-encode concat, and assemble's single-chunk remux, concat and cfr re-encode. Also corrects the trust claim. These are unsigned, freely writable keys, so a present tag means the file claims to be HyperFrames output, not that HyperFrames wrote it. Documented as an unauthenticated diagnostic hint rather than an authenticity or attribution boundary. Tests assert on the assembled file through the real assemble() path for both mov and webm; both fail without the concat fix. * test(engine): pin provenance through the in-process chunked concat Review noted the distributed writers are mutation-pinned but the encodeFramesChunkedConcat fix had no real-file regression of its own. Encodes 70 frames at a 30-frame chunk size so the concat step actually runs, then asserts the tags on the resulting no-audio mov. Fails without the concat fix, passes with it. |
||
|
|
9ba528914d | chore: release v0.7.108 (#3265) v0.7.108 | ||
|
|
d6c4774ef4 |
feat(studio): instrument the audio FX rack, including work an agent did (#3229)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. * refactor(studio): break up the FX rack's largest functions and files Fallow flagged 9 complexity findings and 2 file-size violations after the telemetry stack landed. Extracts FxPresetRun, FxAddMenu, FxRackChain, FxNodeOpenBody, FxNodeParams, and useFxAudition/useFxCarve/useFxLevelling/ useFxChainObserved out of propertyPanelFxSection.tsx and propertyPanelAudioFxGroup.tsx, splits propertyPanelFxNodeRow.tsx's open-face rendering into its own component, and dedupes a clone in studioTelemetry.ts. Pure structural move — no behavior change; full test suite still green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
e3ec48adce |
feat(studio): fold a preset shut, and give each one its own title design (#3191)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. * chore: fix markdown formatting --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6bcf739390 |
fix(studio): audition from the playhead while paused, and let an author out of a menu (#3190)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
acdb11250c |
feat: switch a preset off, or ramp it, as one thing (#3189)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
29f516d490 |
feat: one knob for the five effects that cannot honestly have one (#3188)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
2020b82c9b |
feat(studio): family lettering, tints, and the rack as a signal path (#3187)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
96fd4d061e |
feat(studio): two faces, and the shared frequency ruler (#3186)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
31fa523b7e |
feat: offer the job, not the machine — the range IS the module (#3185)
* fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0e4da52c82 |
feat(studio): make the FX rack speak the author's language (#3192)
* fix(studio): drop the unused DomEditSelection import in PropertyPanelFlat CI caught it on PR #3026 (wa-12-panel-params); a later refactor in the stack removed the last use of the type here without removing the import. * fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
27cdd4d5b1 |
feat(core): land the plain-language layer, and test that it covers the rack (#3184)
* fix(studio): drop the unused DomEditSelection import in PropertyPanelFlat CI caught it on PR #3026 (wa-12-panel-params); a later refactor in the stack removed the last use of the type here without removing the import. * fix(studio): close the typecheck and fallow gaps wa-18b-reschedule opened useAutomationLanes.ts's write() assumed gesture-scoped coalescing and a preview-only commit that useDomEditAttributeCommits.ts never grew — backported that option support from its own later commit so the two sides of the API agree. The paste path and its tests were missing the box selection's v0/v1 bounds a sibling commit added to AutomationSelection. The FX panel's carve controls still edited the six mechanism numbers (maxCutDb, bands, intelligibilityBias) after carveProfile() collapsed authoring to one Strength knob, so those fields no longer existed on HfCarveSettings; UI now edits strength, and analyseCarveBands is called with carveProfile(strength). Also closes fallow's complexity, dead-code and duplication findings on this PR's diff: extracted automationLaneDragMath.ts (pure group/point-move math) and useAutomationRangeDrag.ts (the marquee-select gesture) out of useAutomationLaneGestures.ts, pulled a couple of render-loop ternaries and a resolver into named functions, dropped an export nothing outside its file used, and shared a step-simplifier between audioCarve's two envelope builders. The edge-stretch vs. box-select priority test in TimelineAutomationLane.test was still pinning the pre-box-select rule (edge wins over a point sitting on it) that a sibling commit deliberately reversed — a point inside the box is now selected content, so grabbing it drags the group instead. Updated the test to the shipped rule instead of the old one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): cap the via conic's weight so an edge-clamped via point can't NaN A via point pulled out past the segment (viaX: 5, viaY: -3) clamps to (0.999, 0.001) — exactly on the steady region's edge, where edge - viaX is 0. viaConic divided by that zero to get an infinite weight, and shapeVia turned Infinity into NaN a few steps later (Infinity - Infinity in the quadratic coefficient). NaN reaching setValueCurveAtTime silences the automated parameter for the rest of the render. Capped the weight at 1e6 instead of leaving it unbounded — past that point the arc already reads as touching the via point, so nothing visible is lost. Also hardened shapeVia's existing denominator guard (`<= 0`) to `!(> 0)`, since NaN fails the original comparison and fell through it. Review by Miga (PR #3208). * fix(studio-server): fingerprint the proactive waveform cache key too The route already keys the waveform cache on the asset's size and mtime as well as its path, so a rebuilt-in-place file gets fresh peaks instead of stale ones. generateWaveformCache — the proactive path that runs on upload — still called buildWaveformCacheKey with the path alone, so it wrote to a different key than the route reads from (making the pre-generated cache never found) and kept the exact collision bug this fingerprint exists to fix on its own path. Review by Miga (PR #3211). * style(docs): run oxfmt on the /hyperframes-audio skill docs Table column widths had drifted out of alignment with oxfmt's own rules, failing format:check and blocking the Preflight gate every downstream branch inherits. Whitespace only, no content change. * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). * fix(studio): widen PropertyPanel's resetModules render timeout again The 20s margin (already once widened for the same reason) is timing out in CI's full-monorepo Test run — the resetModules()+fresh-import render this test needs is uncached and competes with every other package's test suite for the same worker pool, and the same test passes in well under 2s standalone. Went to 45s rather than re-tuning to whatever number happens to clear the current CI load, since that number moves every time CI gains a package. * fix(studio): stop the single-candidate auto-apply carve firing twice Two auto-apply effects both fire when sourceOptions.length === 1: the multi-candidate effect only guards length === 0, so a single candidate passes it too, and the single-candidate effect passes its own guard right after — both compute the same sources list and both call setCarve, so the common case (one narrator, one bed) triggered two decodes, two FFT runs, and two concurrent attribute writes for one decision. The multi-candidate effect now defers to its sibling for exactly one candidate, which already has its own detailed handling for that case. Review by Miga (PR #3213). * feat(core): carve against every voice over a bed, always (#3212) * feat(core): carve against every voice over a bed, always dynamically A bed usually runs under a whole sequence — a narrator, an interview answer, a second presenter — and carving against one of them left the others fighting it. `source` becomes `sources`, and `mixCarveSources` sums every voice onto the BED's clock before anything is measured. That is what keeps one analysis sufficient: the chain is fixed, so there is no per-voice filter to switch between, and bands drawn from all the speech there is with envelopes that rise wherever any of it happens answer the actual question — where and when is speech masking this bed. Summed rather than averaged: two people talking at once mask more than either alone. Audio before the bed starts is dropped rather than folded in at zero, since it plays over nothing and shifting it would put a cut where there is no voice. `dynamic` is gone. A fixed depth thins the bed through every pause, and once both have been heard there is no reason to want it, so every carve follows the speech. Two helpers the panel and the headless script now share instead of each carrying a copy — two definitions of "what does this name suggest" drift, and then the two disagree about which track is the voice: - `classifyAudioName` reads a track's kind from its id and filename together. `unknown` is deliberately common: treating an unrecognised name as "not a voice" would hide the one track somebody needs to pick. - `clipsOverlap` keeps out a voice that never plays while the bed does. An unwritten duration counts as unbounded, not zero — refusing a clip whose length the composition leaves to the media would drop the commonest case there is. Files written before this still load: a single `source` reads as a one-voice list, a stored `dynamic` is ignored, and an absent attribute means the defaults whole. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration \b treats `_` as a word character, so \bbed\b never matched bed_01, music_bed_loop, or theme_song, and \bvo\b/\bvox\b/\btts\b had the same gap — an underscore-separated bed classified as "unknown" and could end up offered as its own carve source. Replaced the short hints with a boundary that actually excludes letters and digits on both sides. clipsOverlap computed end = start + duration without guarding sign, so a negative duration put end before start — an interval that does not describe anything, and one specific case showed it silently dropping a real overlap (a shorter, earlier broken end rejected a clip that genuinely contained the point). Duration clamps to zero instead: a clip cannot un-play time, and a zero-length clip at its start is the sane reading of "duration nobody wrote down as positive." Review by Miga (PR #3212). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(studio): port the carve UI off the removed source/dynamic fields #3212 (accidentally squash-merged into this branch instead of main) changed HfCarveSettings from a single `source` + `dynamic` toggle to a `sources` list with dynamic mode removed outright — the multi-voice UI consumer that goes with that shape lands in the very next PR, so this branch was left with a type that no longer matched its own code. Minimal port, not the multi-voice redesign that PR does properly: the "Listen to" picker and analyse() treat sources[0] as the one voice this UI still understands, and every dynamic-mode branch (the automated envelope lanes, the toggle, the checkbox) is gone along with the field — a carve is now always the static value the analysis computes, matching what the type change made permanent. Test suite trimmed the same way: the automation-lane and toggle tests covered behavior that no longer exists. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |