From 99f0c4b135f2a21d52c8a0f5659e09b35ba3c119 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 13 Aug 2026 00:38:19 -0700 Subject: [PATCH 1/3] 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. --- README.md | 22 ++++----- skills-manifest.json | 6 ++- skills/hyperframes-audio/SKILL.md | 6 +-- .../references/attributes.md | 32 +++++++++++-- .../references/fx-registry.md | 46 +++++++++---------- 5 files changed, 69 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 6af8bd153b..796cb85f3f 100644 --- a/README.md +++ b/README.md @@ -94,17 +94,17 @@ This writes `dist/hyperframes-plugin.zip` with a `hyperframes/` root folder and Atomic capabilities the creation workflows compose against — pull one when you need that specific layer. -| Skill | Covers | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `/hyperframes-core` | The composition contract — `data-*` timing attributes, `class="clip"`, tracks, sub-compositions, variables, framework-owned media playback, determinism rules. | -| `/hyperframes-animation` | All animation knowledge — atomic motion rules, scene blueprints, transitions, runtime adapters (GSAP / Lottie / Three.js / Anime.js / CSS / WAAPI / TypeGPU). | -| `/hyperframes-keyframes` | Seek-safe keyframe authoring across runtimes — GSAP timelines, CSS keyframes, Anime.js, WAAPI, FLIP, paths, masks, SVG morph/draw, 3D depth — plus `hyperframes keyframes` diagnostics for rendered motion. | -| `/hyperframes-creative` | Non-animation creative direction — `frame.md` / `design.md`, palettes, typography, narration, beat planning, audio-reactive visuals, composition patterns. | -| `/media-use` | The media OS — resolve any media need (BGM, SFX, image, icon, logo, voice, color grade, LUT) into a frozen local file or paste-ready block + ledger record, generate via TTS/music/image models when the catalog misses, transcribe, caption, remove backgrounds, and reuse assets across projects. One shared audio engine + manifest tracking. | -| `/hyperframes-cli` | CLI dev loop — `init`, `lint`, `check`, `snapshot`, `preview`, `render`, `publish`, `doctor`, plus HeyGen-hosted cloud rendering (`cloud render`) and AWS Lambda rendering (`lambda deploy / render / progress`). | -| `/hyperframes-audio` | Mix the audio already placed in a composition — voiceover carve (dip a music bed only in the bands the voice occupies, static or dynamic, level match included), the effect chain (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), and automation envelopes on volume or any effect parameter. Sourcing the audio is `/media-use`. | -| `/hyperframes-registry` | Install and wire registry blocks and components into compositions via `hyperframes add`. Authoring a new block or component to contribute upstream. | -| `/figma` | Import Figma assets, tokens, components, and storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI) plus Motion animations (MCP) and shaders (MCP source / native export) into a composition. | +| Skill | Covers | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `/hyperframes-core` | The composition contract — `data-*` timing attributes, `class="clip"`, tracks, sub-compositions, variables, framework-owned media playback, determinism rules. | +| `/hyperframes-animation` | All animation knowledge — atomic motion rules, scene blueprints, transitions, runtime adapters (GSAP / Lottie / Three.js / Anime.js / CSS / WAAPI / TypeGPU). | +| `/hyperframes-keyframes` | Seek-safe keyframe authoring across runtimes — GSAP timelines, CSS keyframes, Anime.js, WAAPI, FLIP, paths, masks, SVG morph/draw, 3D depth — plus `hyperframes keyframes` diagnostics for rendered motion. | +| `/hyperframes-creative` | Non-animation creative direction — `frame.md` / `design.md`, palettes, typography, narration, beat planning, audio-reactive visuals, composition patterns. | +| `/media-use` | The media OS — resolve any media need (BGM, SFX, image, icon, logo, voice, color grade, LUT) into a frozen local file or paste-ready block + ledger record, generate via TTS/music/image models when the catalog misses, transcribe, caption, remove backgrounds, and reuse assets across projects. One shared audio engine + manifest tracking. | +| `/hyperframes-cli` | CLI dev loop — `init`, `lint`, `check`, `snapshot`, `preview`, `render`, `publish`, `doctor`, plus HeyGen-hosted cloud rendering (`cloud render`) and AWS Lambda rendering (`lambda deploy / render / progress`). | +| `/hyperframes-audio` | Mix the audio already placed in a composition — voiceover carve (dip a music bed only in the bands the voice occupies, static or dynamic, level match included), the effect chain (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), and automation envelopes on volume or any effect parameter. Sourcing the audio is `/media-use`. | +| `/hyperframes-registry` | Install and wire registry blocks and components into compositions via `hyperframes add`. Authoring a new block or component to contribute upstream. | +| `/figma` | Import Figma assets, tokens, components, and storyboard sections → reconstructed motion (frames read as states, not slides) (REST/CLI) plus Motion animations (MCP) and shaders (MCP source / native export) into a composition. | For visual design handoff workflows, see the [Claude Design guide](https://hyperframes.heygen.com/guides/claude-design) and [Open Design guide](https://hyperframes.heygen.com/guides/open-design). diff --git a/skills-manifest.json b/skills-manifest.json index a8c0565d69..889c7553ca 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -18,13 +18,17 @@ "files": 4 }, "hyperframes": { - "hash": "3a3ea01fe52e1600", + "hash": "756a72f58fa3739b", "files": 17 }, "hyperframes-animation": { "hash": "5bc2ce098387a547", "files": 121 }, + "hyperframes-audio": { + "hash": "08f593a7c159ce04", + "files": 4 + }, "hyperframes-cli": { "hash": "e042fcaaa3f9767f", "files": 11 diff --git a/skills/hyperframes-audio/SKILL.md b/skills/hyperframes-audio/SKILL.md index 9bae288c13..6449c3ad94 100644 --- a/skills/hyperframes-audio/SKILL.md +++ b/skills/hyperframes-audio/SKILL.md @@ -28,7 +28,7 @@ Three attributes carry everything, all on the audio/video element itself: | Attribute | Holds | | ----------------- | --------------------------------------------------------- | | `data-fx-chain` | the effects, in signal order | -| `data-automation` | envelopes on this track's volume or its effect parameters | +| `data-automation` | envelopes on this track's volume or its effect parameters | | `data-fx-carve` | the carve's own settings, so it can be re-derived | Exact JSON for each, and the rules a lane must satisfy: `references/attributes.md`. @@ -133,7 +133,7 @@ source denser, not cleaner. **Time** (`delay`, `reverb`, `chorus`, `phaser`) puts a track in a space or gives it width. These are the ones that most easily wreck a mix, because a tail or a detuned copy occupies the same room a voice needs. Use them on the thing that -should sit *behind* something else, and keep the wet amount lower than sounds +should sit _behind_ something else, and keep the wet amount lower than sounds right in isolation. The chain is serial: each effect processes what the one before it produced. So @@ -149,7 +149,7 @@ not need the whole spectrum. It needs the few bands it actually occupies. Carve takes only those, and the bed keeps its low end and its top, so it is still music while the voice is still intelligible. -**It is a relationship, not an effect.** The settings live on the *bed* — the +**It is a relationship, not an effect.** The settings live on the _bed_ — the track that gets processed — and they name the voice to listen to, exactly as a sidechain compressor does: you select the track that gets quieter and pick what makes it quieter. **Never put a carve on the voice track.** A voice carved diff --git a/skills/hyperframes-audio/references/attributes.md b/skills/hyperframes-audio/references/attributes.md index cb44a1bfd3..83400c3c41 100644 --- a/skills/hyperframes-audio/references/attributes.md +++ b/skills/hyperframes-audio/references/attributes.md @@ -13,8 +13,18 @@ that sounds plausible and is wrong. "version": 1, "nodes": [ { "type": "highpass", "id": "n1", "params": { "frequency": 120, "q": 0.707, "poles": "2" } }, - { "type": "peaking", "id": "n2", "fromCarve": true, "params": { "frequency": 1600, "gain": -6, "q": 1.4 } }, - { "type": "limiter", "id": "n3", "enabled": false, "params": { "limit": -1, "attack": 5, "release": 50, "level_out": 0 } } + { + "type": "peaking", + "id": "n2", + "fromCarve": true, + "params": { "frequency": 1600, "gain": -6, "q": 1.4 } + }, + { + "type": "limiter", + "id": "n3", + "enabled": false, + "params": { "limit": -1, "attack": 5, "release": 50, "level_out": 0 } + } ] } ``` @@ -40,8 +50,20 @@ that sounds plausible and is wrong. { "version": 1, "lanes": [ - { "target": "volume", "points": [{ "t": 0, "v": 1 }, { "t": 2.5, "v": 0.4 }] }, - { "target": "fx.n2.gain", "points": [{ "t": 0, "v": 0 }, { "t": 1, "v": -6, "curve": 0.4 }] } + { + "target": "volume", + "points": [ + { "t": 0, "v": 1 }, + { "t": 2.5, "v": 0.4 } + ] + }, + { + "target": "fx.n2.gain", + "points": [ + { "t": 0, "v": 0 }, + { "t": 1, "v": -6, "curve": 0.4 } + ] + } ] } ``` @@ -54,7 +76,7 @@ that sounds plausible and is wrong. - A lane holds its first value backwards to the start of its clip and its last value forward to the end. So a bed that begins before the voice needs an explicit "no cut" point at `t: 0`, or it starts out already ducked. -- `curve` (-1..1) bends the segment *leaving* a point: positive holds low then +- `curve` (-1..1) bends the segment _leaving_ a point: positive holds low then rises late. `viaX`/`viaY` name an interior point the segment passes through (progress 0..1, value travelled 0..1) and supersede `curve` when both are present — that is what the timeline writes when a bend is dragged. diff --git a/skills/hyperframes-audio/references/fx-registry.md b/skills/hyperframes-audio/references/fx-registry.md index 0835fa1d63..5bfb37f1b6 100644 --- a/skills/hyperframes-audio/references/fx-registry.md +++ b/skills/hyperframes-audio/references/fx-registry.md @@ -10,13 +10,13 @@ of truth — if this table and the code disagree, the code is right. ## Filter — which frequencies a track may occupy -| Effect | Parameter | -| ----------- | -------------------------------------------------------------------------------------- | -| `highpass` | `frequency` 20–20000 Hz (300, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\|`2` (2) | -| `lowpass` | `frequency` 100–20000 Hz (8000, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\|`2` (2) | +| Effect | Parameter | +| ----------- | ----------------------------------------------------------------------------------------------------------- | +| `highpass` | `frequency` 20–20000 Hz (300, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\|`2` (2) | +| `lowpass` | `frequency` 100–20000 Hz (8000, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\|`2` (2) | | `peaking` | `frequency` 20–20000 Hz (1000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** · `q` 0.1–20 (1, log) **AUTO** | -| `lowshelf` | `frequency` 20–2000 Hz (200, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** | -| `highshelf` | `frequency` 500–20000 Hz (4000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** | +| `lowshelf` | `frequency` 20–2000 Hz (200, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** | +| `highshelf` | `frequency` 500–20000 Hz (4000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** | `q` is bandwidth — higher is narrower. `poles` is the slope: `2` is the usual biquad (12 dB/oct), `1` is gentler (6 dB/oct). Shelving filters have no `q`: the @@ -24,12 +24,12 @@ Web Audio spec leaves it unused for them, so a control would have moved nothing. ## Dynamics — how level behaves over time -| Effect | Parameter | -| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `gain` | `gain` −60–12 dB (0) **AUTO** | -| `compressor` | `threshold` −60–0 dB (−24) · `ratio` 1–20 (4) · `attack` 0.01–2000 ms (20, log) · `release` 0.01–9000 ms (250, log) · `knee` 1–8 (2.83) · `makeup` 0–36 dB (0) · `mix` 0–1 (1) | -| `limiter` | `limit` −24–0 dB (−1) · `attack` 0.1–80 ms (5) · `release` 1–8000 ms (50, log) · `level_out` −24–24 dB (0) | -| `gate` | `threshold` −80–0 dB (−35) · `range` −80–0 dB (−24) · `ratio` 1–20 (10) · `attack` 0.01–9000 ms (1, log) · `release` 0.01–9000 ms (100, log) · `knee` 1–8 (2.83) | +| Effect | Parameter | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `gain` | `gain` −60–12 dB (0) **AUTO** | +| `compressor` | `threshold` −60–0 dB (−24) · `ratio` 1–20 (4) · `attack` 0.01–2000 ms (20, log) · `release` 0.01–9000 ms (250, log) · `knee` 1–8 (2.83) · `makeup` 0–36 dB (0) · `mix` 0–1 (1) | +| `limiter` | `limit` −24–0 dB (−1) · `attack` 0.1–80 ms (5) · `release` 1–8000 ms (50, log) · `level_out` −24–24 dB (0) | +| `gate` | `threshold` −80–0 dB (−35) · `range` −80–0 dB (−24) · `ratio` 1–20 (10) · `attack` 0.01–9000 ms (1, log) · `release` 0.01–9000 ms (100, log) · `knee` 1–8 (2.83) | Cuts on `gain` go to −60 dB, boosts stop at +12: it is a level stage for making room, and a chain that could add 40 dB would clip long before that was useful. @@ -39,10 +39,10 @@ when closed — a gate that pulls all the way to silence sounds like a switch. ## Nonlinear — changes the waveform's shape -| Effect | Parameter | -| ----------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `saturate` | `type` `tanh`\|`atan`\|`cubic`\|`exp`\|`alg`\|`quintic`\|`sin`\|`erf`\|`hard` (tanh) · `threshold` −40–0 dB (−6) · `output` −24–24 dB (0) **AUTO** · `oversample` 1–8× (4) | -| `bitcrush` | `bits` 1–32 (8) · `samples` 1–250× (1) · `mix` 0–1 (1) | +| Effect | Parameter | +| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `saturate` | `type` `tanh`\|`atan`\|`cubic`\|`exp`\|`alg`\|`quintic`\|`sin`\|`erf`\|`hard` (tanh) · `threshold` −40–0 dB (−6) · `output` −24–24 dB (0) **AUTO** · `oversample` 1–8× (4) | +| `bitcrush` | `bits` 1–32 (8) · `samples` 1–250× (1) · `mix` 0–1 (1) | `tanh` is the gentlest curve and `hard` is outright clipping. Higher `oversample` costs more CPU and keeps aliasing down. `samples` repeats each sample N times — a @@ -50,14 +50,14 @@ crude downsample, which is where the lo-fi character comes from. ## Time — space and width -| Effect | Parameter | -| --------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `delay` | `time` 1–5000 ms (250, log) **AUTO** · `feedback` 0.01–0.95 (0.35) **AUTO** · `mix` 0–1 (0.4) **AUTO** | -| `reverb` | `size` 0.05–1 (0.7) · `damping` 0–1 (0.5) · `wet` 0–1 (0.35) **AUTO** · `dry` 0–1 (0.7) **AUTO** | -| `chorus` | `delay` 1–100 ms (7) **AUTO** · `depth` 0–10 ms (2) **AUTO** · `speed` 0.01–10 Hz (1) **AUTO** · `mix` 0–1 (0.5) **AUTO** | -| `phaser` | `in_gain` 0–1 (0.4) **AUTO** · `out_gain` 0–2 (0.74) **AUTO** · `delay` 0.1–5 ms (3) · `decay` 0–0.99 (0.4) · `speed` 0.1–2 Hz (0.5) **AUTO** · `type` `0`\|`1` (0) | +| Effect | Parameter | +| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `delay` | `time` 1–5000 ms (250, log) **AUTO** · `feedback` 0.01–0.95 (0.35) **AUTO** · `mix` 0–1 (0.4) **AUTO** | +| `reverb` | `size` 0.05–1 (0.7) · `damping` 0–1 (0.5) · `wet` 0–1 (0.35) **AUTO** · `dry` 0–1 (0.7) **AUTO** | +| `chorus` | `delay` 1–100 ms (7) **AUTO** · `depth` 0–10 ms (2) **AUTO** · `speed` 0.01–10 Hz (1) **AUTO** · `mix` 0–1 (0.5) **AUTO** | +| `phaser` | `in_gain` 0–1 (0.4) **AUTO** · `out_gain` 0–2 (0.74) **AUTO** · `delay` 0.1–5 ms (3) · `decay` 0–0.99 (0.4) · `speed` 0.1–2 Hz (0.5) **AUTO** · `type` `0`\|`1` (0) | -Reverb convolves a *generated* impulse, and both preview and render generate the +Reverb convolves a _generated_ impulse, and both preview and render generate the same one — so a room is reproducible without shipping an impulse file. Higher `damping` rolls the top off the tail faster, which is what makes a large room sound like a soft one. `feedback` near the top of its range is a very long tail; From 009fb88239bc5e4c8dfd004ba0ab0b4f30f29bd6 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 13 Aug 2026 00:42:18 -0700 Subject: [PATCH 2/3] fix(core): stop \b from missing underscore-separated names, guard clipsOverlap's negative duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \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). --- packages/core/src/audioCarve.test.ts | 54 ++++++++++++++++++++++++++++ packages/core/src/audioCarve.ts | 27 +++++++++++--- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/core/src/audioCarve.test.ts b/packages/core/src/audioCarve.test.ts index 519afc875e..c2cb260198 100644 --- a/packages/core/src/audioCarve.test.ts +++ b/packages/core/src/audioCarve.test.ts @@ -7,6 +7,7 @@ import { carveBandsToChain, carveProfile, classifyAudioName, + clipsOverlap, mixCarveSources, couldBeCarveSource, DEFAULT_CARVE, @@ -494,6 +495,59 @@ describe("classifyAudioName", () => { expect(couldBeCarveSource("music-bed")).toBe(false); expect(couldBeCarveSource("sfx-explosion")).toBe(false); }); + + it("treats underscores as separators, not word characters, for short hints", () => { + // `\b` treats `_` as a word character, so `\bbed\b` used to miss `bed_01` — + // an underscore-separated bed classified as "unknown" and could end up + // offered as its own carve source. + expect(classifyAudioName("bed_01")).toBe("music"); + expect(classifyAudioName("my_bed")).toBe("music"); + expect(classifyAudioName("music_bed_loop")).toBe("music"); + expect(classifyAudioName("theme_song")).toBe("music"); + expect(classifyAudioName("vo_take3")).toBe("voice"); + expect(classifyAudioName("main_vox")).toBe("voice"); + expect(couldBeCarveSource("bed_01")).toBe(false); + }); +}); + +describe("clipsOverlap", () => { + it("overlaps when the spans genuinely share time", () => { + expect(clipsOverlap({ start: 0, duration: 5 }, { start: 3, duration: 5 })).toBe(true); + }); + + it("does not overlap when one span ends before the other starts", () => { + expect(clipsOverlap({ start: 0, duration: 5 }, { start: 5, duration: 5 })).toBe(false); + expect(clipsOverlap({ start: 10, duration: 5 }, { start: 0, duration: 5 })).toBe(false); + }); + + it("does not overlap two clips that only touch at an edge", () => { + // Half-open: an end exactly at the other's start shares no time to carve. + expect(clipsOverlap({ start: 0, duration: 5 }, { start: 5, duration: 5 })).toBe(false); + }); + + it("treats a null or undefined duration as unbounded", () => { + expect(clipsOverlap({ start: 0, duration: null }, { start: 100, duration: 1 })).toBe(true); + expect(clipsOverlap({ start: 0 }, { start: 100, duration: 1 })).toBe(true); + // Symmetric: the unbounded span can be on either side. + expect(clipsOverlap({ start: 100, duration: 1 }, { start: 0, duration: undefined })).toBe(true); + }); + + it("gives a zero-duration clip a single instant, not a span", () => { + expect(clipsOverlap({ start: 5, duration: 0 }, { start: 5, duration: 5 })).toBe(false); + expect(clipsOverlap({ start: 5, duration: 0 }, { start: 4, duration: 5 })).toBe(true); + }); + + it("clamps a negative duration to zero rather than inverting the interval", () => { + // The regression: end = start + duration puts a negative-duration clip's + // end BEFORE its start, and end(a) is what the other clip's start gets + // compared against — so a smaller (earlier) broken end silently rejects + // real overlaps too. {start:10, duration:-5} clamped is a zero-length + // clip AT t=10, which genuinely sits inside {start:6, duration:20}'s + // [6, 26) span; the unclamped math missed it (end(a) came out to 5). + expect(clipsOverlap({ start: 10, duration: -5 }, { start: 6, duration: 20 })).toBe(true); + // And it stays correct where it isn't inside anything. + expect(clipsOverlap({ start: 10, duration: -5 }, { start: 20, duration: 5 })).toBe(false); + }); }); describe("mixCarveSources", () => { diff --git a/packages/core/src/audioCarve.ts b/packages/core/src/audioCarve.ts index 7ee5a6c029..080638c4cd 100644 --- a/packages/core/src/audioCarve.ts +++ b/packages/core/src/audioCarve.ts @@ -105,11 +105,26 @@ export type HfAudioNameKind = "voice" | "music" | "sfx" | "unknown"; /** Short, deliberately dull effects. Nothing here is ever a voiceover. */ const SFX_NAME = /sfx|foley|whoosh|impact|riser|stinger|swoosh|thud|boom|click|ding|beep|ambien|room[-_ ]?tone/i; +// `\b` treats `_` as a word character, so `\bbed\b` does not match `bed_01` +// or `music_bed_loop` — exactly the separator an asset name is likely to use. +// These short words need a boundary that actually excludes letters and +// digits on both sides; everything else here is long enough that a +// substring match is already the intent (`music` inside `bgmusic` is fine). +const NOT_WORD = "(? `${NOT_WORD}${term}${NOT_WORD_END}`; + /** A bed, which is the thing being carved rather than the thing carving it. */ -const MUSIC_NAME = /music|bgm|\bbed\b|soundtrack|score|\bsong\b|theme|instrumental|track\d/i; +const MUSIC_NAME = new RegExp( + `music|bgm|${wordish("bed")}|soundtrack|score|${wordish("song")}|theme|instrumental|track\\d`, + "i", +); /** Speech. */ -const VOICE_NAME = - /voice|\bvo\b|\bvox\b|narrat|speech|dialog|monolog|announce|\btts\b|talk|interview|podcast|recap|script/i; +const VOICE_NAME = new RegExp( + `voice|${wordish("vo")}|${wordish("vox")}|narrat|speech|dialog|monolog|announce|` + + `${wordish("tts")}|talk|interview|podcast|recap|script`, + "i", +); /** * Classify a track from its id and filename together. @@ -152,7 +167,11 @@ export interface HfClipSpan { export function clipsOverlap(a: HfClipSpan, b: HfClipSpan): boolean { const end = (clip: HfClipSpan): number => typeof clip.duration === "number" && Number.isFinite(clip.duration) - ? clip.start + clip.duration + ? // Negative is clamped to zero-length rather than passed through: a + // clip cannot un-play time, and letting it through inverts the + // interval (end before start), which reads as overlapping everything + // it is nowhere near. + clip.start + Math.max(0, clip.duration) : Number.POSITIVE_INFINITY; return a.start < end(b) && b.start < end(a); } From 06aff52cea0d67e4a2379163afa9cf092654a588 Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Thu, 13 Aug 2026 00:49:31 -0700 Subject: [PATCH 3/3] fix(studio): widen PropertyPanel's resetModules render timeout again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../studio/src/components/editor/PropertyPanel.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/studio/src/components/editor/PropertyPanel.test.tsx b/packages/studio/src/components/editor/PropertyPanel.test.tsx index 82098f3774..a1a3c7a01a 100644 --- a/packages/studio/src/components/editor/PropertyPanel.test.tsx +++ b/packages/studio/src/components/editor/PropertyPanel.test.tsx @@ -238,7 +238,11 @@ async function renderPanel( // renderPanel resetModules()+dynamic-imports PropertyPanel (needed for a fresh // flag read); transforming the full section graph uncached can exceed the 5s // default under heavy parallel full-suite load, so give these a wider margin. -const RENDER_TIMEOUT_MS = 20_000; +// 20s itself has now been observed timing out in CI's full-monorepo run (the +// same suite passes in well under 2s standalone) — widened again rather than +// re-tuned down to a number that will just need doing again next time CI adds +// load. +const RENDER_TIMEOUT_MS = 45_000; // Find the collapsed accordion row whose title matches and click it open. function openFlatGroup(host: HTMLElement, title: string) {