{"id":"4baa3049-d3fa-4daf-b8fd-2045ea97078a","entityType":"agent","slug":"clawhub-heygen-com-hyperframes-audio","name":"hyperframes-audio","canonicalUrl":"https://www.xpersona.co/agent/clawhub-heygen-com-hyperframes-audio","canonicalPath":"/agent/clawhub-heygen-com-hyperframes-audio","generatedAt":"2026-10-11T01:45:00.362Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":null},"description":"Use when audio already placed in a HyperFrames composition needs to be mixed: fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking, a music bed that fights a voiceover (voiceover carve), effects on a track (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), automation envelopes drawn on a track's volume or any effect parameter, or one submix bus carrying a chain, a fader and an automation clock for several tracks at once (`<hf-audio-group>`). Don't use for sourcing or generating audio — finding BGM, SFX, or making a voiceover is `/media-use`. Don't use for clip timing or track layout, which is `/hyperframes-core`.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.2K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:hyperframes-audio","sourceUrl":"https://clawhub.ai/heygen-com/hyperframes-audio","homepage":"https://clawhub.ai/heygen-com/skills/hyperframes-audio","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/heygen-com/hyperframes-audio","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/heygen-com/skills/hyperframes-audio","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":62,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"hyperframes-audio technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":null},"stars":null,"forks":null,"downloads":1238,"packageName":null,"latestVersion":"1.0.15","tractionLabel":"1.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:47:30.031Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T22:47:30.096Z","lastCrawledAt":"2026-10-10T22:47:30.031Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T22:47:30.031Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.15","createdAt":"2026-10-10T18:44:55.961Z","changelog":"Synced from 3746703 (main)","fileCount":10,"zipByteSize":43042},{"version":"1.0.14","createdAt":"2026-10-10T05:07:18.487Z","changelog":"Synced from f46033d (main)","fileCount":10,"zipByteSize":42500},{"version":"1.0.13","createdAt":"2026-10-04T19:17:39.739Z","changelog":"Synced from 173103d (main)","fileCount":9,"zipByteSize":42232},{"version":"1.0.12","createdAt":"2026-09-27T21:19:32.845Z","changelog":"Synced from ff6e210 (main)","fileCount":9,"zipByteSize":42304},{"version":"1.0.11","createdAt":"2026-09-20T16:22:31.001Z","changelog":"Synced from 0c158be (main)","fileCount":9,"zipByteSize":42374},{"version":"1.0.10","createdAt":"2026-09-11T21:20:43.820Z","changelog":"Synced from b848d81 (main)","fileCount":9,"zipByteSize":42349},{"version":"1.0.9","createdAt":"2026-08-24T02:20:11.430Z","changelog":"Synced from 2685c8f (main)","fileCount":9,"zipByteSize":41855},{"version":"1.0.8","createdAt":"2026-08-22T12:17:25.678Z","changelog":"Synced from f0cc9b1 (main)","fileCount":9,"zipByteSize":40472}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:hyperframes-audio","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fpgb0p797dzkbtbrxw5x1hh89qs64:hyperframes-audio` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/heygen-com/hyperframes-audio before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T01:45:00.355Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-heygen-com-hyperframes-audio/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":null},"readme":"Skill: hyperframes-audio\n\nOwner: heygen-com\n\nSummary: Use when audio already placed in a HyperFrames composition needs to be mixed: fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking, a music bed that fights a voiceover (voiceover carve), effects on a track (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), automation envelopes drawn on a track's volume or any effect parameter, or one submix bus carrying a chain, a fader and an automation clock for several tracks at once (`<hf-audio-group>`). Don't use for sourcing or generating audio — finding BGM, SFX, or making a voiceover is `/media-use`. Don't use for clip timing or track layout, which is `/hyperframes-core`.\n\nTags: latest:1.0.15\n\nVersion history:\n\nv1.0.15 | 2026-10-10T18:44:55.961Z | user\n\nSynced from 3746703 (main)\n\nv1.0.14 | 2026-10-10T05:07:18.487Z | user\n\nSynced from f46033d (main)\n\nv1.0.13 | 2026-10-04T19:17:39.739Z | user\n\nSynced from 173103d (main)\n\nv1.0.12 | 2026-09-27T21:19:32.845Z | user\n\nSynced from ff6e210 (main)\n\nv1.0.11 | 2026-09-20T16:22:31.001Z | user\n\nSynced from 0c158be (main)\n\nv1.0.10 | 2026-09-11T21:20:43.820Z | user\n\nSynced from b848d81 (main)\n\nv1.0.9 | 2026-08-24T02:20:11.430Z | user\n\nSynced from 2685c8f (main)\n\nv1.0.8 | 2026-08-22T12:17:25.678Z | user\n\nSynced from f0cc9b1 (main)\n\nv1.0.7 | 2026-08-21T16:42:56.932Z | user\n\nSynced from 485c037 (main)\n\nv1.0.6 | 2026-08-19T22:08:59.891Z | user\n\nSynced from 228eabd (main)\n\nv1.0.5 | 2026-08-19T21:36:50.129Z | user\n\nSynced from b3c43e2 (main)\n\nv1.0.4 | 2026-08-18T14:17:55.492Z | user\n\nSynced from afafca4 (main)\n\nv1.0.3 | 2026-08-16T18:02:56.852Z | user\n\nSynced from 67edb01 (main)\n\nv1.0.2 | 2026-08-13T20:54:04.394Z | user\n\nSynced from e3ec48a (main)\n\nv1.0.1 | 2026-08-13T09:36:44.027Z | user\n\nSynced from d18cbcb (main)\n\nv1.0.0 | 2026-08-13T09:17:22.698Z | auto\n\nInitial release of hyperframes-audio.\n\n- Adds tools for mixing pre-placed audio in HyperFrames compositions.\n- Supports voiceover carves (frequency/level ducking using dynamic sidechain).\n- Implements effect chains, including EQ, compression, saturation, time effects, and automation envelopes, attached to audio/video elements.\n- Provides deterministic preview/render pipeline using Web Audio API; ensures WYSIWYG mixing experience across live and offline contexts.\n- Documents exact attribute specifications and effect parameters for integration and authoring tools.\n\nArchive index:\n\nArchive v1.0.15: 10 files, 43042 bytes\n\nFiles: references/attributes.md (6223b), references/diagnosis.md (12080b), references/fx-registry.md (7981b), references/presets.md (13155b), scripts/carve.mjs (23848b), scripts/carve.test.mjs (11818b), scripts/lib/main-module.mjs (479b), skill-card.md (1888b), SKILL.md (25976b), _meta.json (137b)\n\nFile v1.0.15:SKILL.md\n\n---\nname: hyperframes-audio\ndescription: >\n  Use when audio already placed in a HyperFrames composition needs to be mixed:\n  fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking,\n  a music bed that fights a voiceover (voiceover carve), effects on a track\n  (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser,\n  bitcrush), automation envelopes drawn on a track's volume or any effect\n  parameter, or one submix bus carrying a chain, a fader and an automation clock\n  for several tracks at once (`<hf-audio-group>`).\n  Don't use for sourcing or generating audio — finding BGM, SFX, or making a\n  voiceover is `/media-use`. Don't use for clip timing or track layout, which is\n  `/hyperframes-core`.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Audio\n\nA mix is a set of relationships, not a stack of processors. Two tracks that each\nsound right alone can be unlistenable together, and the fix is almost never \"turn\none down\" — it is finding what they are fighting over and giving it to whichever\none needs it. Every tool here exists to express one of those relationships.\n\nEffects live on the element as `data-fx-chain`, and preview and render run the\nsame Web Audio graph — the studio in a live context, the engine in an offline one\ninside the browser it already drives. There is one implementation of each effect,\nso what you hear while scrubbing is what gets written. You never tune twice.\n\nClip timing remains `/hyperframes-core`: audio/video trims and source ranges use\n`data-start`, `data-duration`, and `data-media-start`, and crossfades overlap\nclips on different tracks. This skill owns placed-track fade-in/fade-out,\ncrossfade envelopes, track gain/track volume, volume and effect automation,\nducking/voiceover carve, and the effect chain. `/media-use` owns sourcing,\ngeneration, and preprocessing.\n\nConstant `data-playback-rate` (`0.1..10`) is render-safe for picture and\npitch-preserved sound when matching audio/video elements use the same timing,\nsource offset, and rate. A speed ramp is a `rate` lane in `data-automation`\n(see `docs/reference/speed-ramps`); it wins over the constant and keeps pitch\nin preview and render. HyperFrames does not\nprovide automatic waveform sync or drift correction.\nFor copyable cut/crossfade/retime recipes, use `/hyperframes-core` → `references/creator-editing-recipes.md`.\n\nThree attributes carry everything, on the audio/video element itself — or, for\nthe first two, on an `<hf-audio-group>` bus (see \"One bus for many tracks\"):\n\n| Attribute         | Holds                                                     |\n| ----------------- | --------------------------------------------------------- |\n| `data-fx-chain`   | the effects, in signal order                              |\n| `data-automation` | envelopes on this track's volume or its effect parameters |\n| `data-fx-carve`   | the carve's own settings, so it can be re-derived         |\n\nThe shipped effect families are gain, EQ (highpass, lowpass, peaking, shelves),\ncompressor, limiter, truepeak (lookahead true-peak limiter), gate, saturate, delay, reverb, chorus, phaser, and bitcrush.\n\nExact JSON for each, and the rules a lane must satisfy: `references/attributes.md`.\nEvery effect with its parameters, ranges and units: `references/fx-registry.md`.\nHow to work out what is wrong with a file you cannot hear:\n`references/diagnosis.md`.\n**Presets, named jobs and one-knob profiles, plus a symptom-to-fix table:\n`references/presets.md`** — read that before hand-building a chain, because one\nof the presets or named jobs usually already names the problem.\n\n## How it fits together\n\nTwo authoring surfaces write those attributes; two runtimes read them through the\nsame builders. That shared middle is why preview predicts the render.\n\n```mermaid\nflowchart TB\n  voice[\"voice track<br/>media file\"]\n  bed[\"music bed<br/>media file\"]\n\n  subgraph AUTHOR[\"Authoring — the only things that write attributes\"]\n    panel[\"Studio<br/>Voiceover carve control\"]\n    script[\"scripts/carve.mjs<br/>detects the pair\"]\n    analysis[\"core/audioCarve.ts<br/>carveProfile · analyseCarveBands<br/>analyseCarveDuck · analyseCarveDynamics\"]\n    panel --> analysis\n    script --> analysis\n  end\n\n  voice --> analysis\n  bed --> analysis\n\n  subgraph ATTRS[\"Written onto the bed element\"]\n    carveAttr[\"data-fx-carve<br/>sources · strength\"]\n    chainAttr[\"data-fx-chain<br/>peaking xN + gain, tagged fromCarve\"]\n    autoAttr[\"data-automation<br/>a lane per carved parameter\"]\n  end\n\n  analysis --> carveAttr\n  analysis --> chainAttr\n  analysis --> autoAttr\n\n  subgraph SHARED[\"One implementation, read by both\"]\n    build[\"audioFxGraph.ts · buildFxChain\"]\n    sched[\"audioFxAutomation.ts · scheduleChainAutomation\"]\n  end\n\n  chainAttr --> build\n  autoAttr --> sched\n\n  build --> preview[\"Preview<br/>live AudioContext<br/>attachElementFxChain\"]\n  sched --> preview\n  build --> render[\"Render<br/>OfflineAudioContext in the headless browser<br/>applyAudioFxChain\"]\n  sched --> render\n\n  preview --> heard[\"what you hear while scrubbing\"]\n  render --> wav[\"processed WAV<br/>+ chainTailSeconds so the mix lets the tail through\"]\n  wav --> mix[\"engine · audioMixer<br/>volume lane baked into the PCM here, not in the graph\"]\n  mix --> out[\"the rendered mix\"]\n\n  edit[\"editing the attribute mid-playback\"] -.->|MutationObserver| preview\n```\n\nThe carve's own settings are never read at playback — the chain and lanes it\nproduced are what play. `data-fx-carve` exists so strength can be changed on an\nexisting carve instead of guessed back out of the filters.\n\nInside a carved bed the signal runs through the dips first, then the level match,\nthen anything you built yourself — which is why a limiter you add still acts as\nthe last ceiling:\n\n```mermaid\nflowchart LR\n  src[\"decoded bed\"] --> p1[\"peaking<br/>400 Hz\"]\n  p1 --> p2[\"peaking<br/>1 kHz\"]\n  p2 --> p3[\"peaking<br/>1.6 kHz\"]\n  p3 --> g[\"gain<br/>level match\"]\n  g --> hand[\"your own effects<br/>e.g. limiter\"]\n  hand --> dest[\"track gain, then out\"]\n\n  l1[\"lane fx.n1.gain\"] -.->|\"envelope of the voice's<br/>level in that band\"| p1\n  l4[\"lane fx.n4.gain\"] -.->|\"how far the bed<br/>ducks overall\"| g\n```\n\n## First, work out what is wrong\n\nThe table below starts from \"it sounds boomy\" — which presumes somebody already\nlistened and said so. Handed a file and \"fix this\", you have no such sentence\nand you cannot listen, so you have to measure. One rule governs all of it:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n> Formants are ±10 dB, fundamentals run 85–255 Hz, and sentences decline 5–6 dB\n> as they end. Every one of those reads as a defect on its own, and every one of\n> them is the speaker.\n\nSo compare, and compare against something **inside the same file**: the clean\noriginal if it exists, otherwise the pauses — whatever is audible in a gap is\nadditive, and the gap's spectrum is the channel rather than the voice. Comparing\nagainst a published average spectrum or a synthesised control voice does not\nwork: two speakers differ by more than most defects, and both wrong answers in\nthe evaluation behind this guidance came from exactly that.\n\nWhen there is no original and no usable silence, a static tonal defect is\ngenuinely under-determined. Say so and offer the readings that fit, rather than\npicking one and building a chain on it.\n\nCommands, traps and worked recipes: **`references/diagnosis.md`**. Read it\nbefore diagnosing a file nobody has described.\n\n## Start from the symptom\n\nOnce you know the band and the kind, name what is wrong with the audio. Most bad audio is\none or two of these, and each has a shipped answer:\n\n| It sounds like                     | Reach for                                          |\n| ---------------------------------- | -------------------------------------------------- |\n| Hum or thump underneath            | `rumble-cut`, or a `highpass` at 80 Hz             |\n| Boomy, chesty                      | **Tame Boominess** job (200 Hz)                    |\n| Muffled, behind cardboard          | **Reduce Mud** job (250 Hz)                        |\n| Words hard to make out             | **Add Clarity** job (3 kHz), or carve the bed      |\n| Harsh and tiring                   | **Soften Harshness** job (3.2 kHz)                 |\n| Some words much louder than others | **Evenness** on a compressor, or Even Out Levels   |\n| Room tone between sentences        | `room-gate`                                        |\n| Voice and music fighting           | **Voiceover carve** — not an EQ on either          |\n| Dry, recorded nowhere              | `room-tight` or `room-natural`                     |\n| Just \"amateur\"                     | `voice-clean`, which is four of the above in order |\n\nFull catalogue, what each preset contains, the band vocabulary, and what is\ndeliberately NOT covered (de-essing, noise removal, tone match):\n`references/presets.md`.\n\nSubtract before you add, level after you filter, relationships after level,\ncharacter and ceiling last. Each step changes what the next one hears — a\ncompressor set before a high-pass spends its time chasing rumble.\n\n## Reach for a family by the problem, not the name\n\n**Filters** (`highpass`, `lowpass`, `peaking`, `lowshelf`, `highshelf`) decide\nwhich frequencies a track is allowed to occupy. This is the first tool for two\nsources colliding, because collisions happen in bands: a bed and a voice both\nwant 1–3 kHz, and taking that from the bed costs the bed far less than turning\nthe whole thing down costs the mix. A high-pass on a voice is the standard fix\nfor rumble; a low-pass darkens or muffles deliberately.\n\n**Dynamics** (`gain`, `compressor`, `limiter`, `truepeak`, `gate`) decide how a track's level\nbehaves over time. Compression narrows the distance between loud and quiet so the\nquiet parts can come up. A limiter is a ceiling — it does not shape anything, it\nguarantees nothing gets past. `limiter` follows the level, so for a delivery\nceiling use `truepeak`, which looks ahead and measures the inter-sample peak. It\nholds a 4x estimate at the ceiling, not the waveform itself, so full-band noise can\nend up to 1.7 dB over: under a hard delivery limit set the ceiling at least 2 dB\nbelow it. A gate removes what is below a threshold, which is\nhow you silence room tone between phrases. `gain` is a plain level stage, and it\nis what an automation lane rides when a track has to move out of the way.\n\n**Nonlinear** (`saturate`, `bitcrush`) changes the waveform's shape, which adds\nharmonics that were not there. Reach for it when a track needs character or\ngrit rather than correction — and remember it is generative: it makes a thin\nsource denser, not cleaner.\n\n**Time** (`delay`, `reverb`, `chorus`, `phaser`) puts a track in a space or gives\nit width. These are the ones that most easily wreck a mix, because a tail or a\ndetuned copy occupies the same room a voice needs. Use them on the thing that\nshould sit _behind_ something else, and keep the wet amount lower than sounds\nright in isolation.\n\nThe chain is serial: each effect processes what the one before it produced. So\ncorrective filtering goes early, character in the middle, and a limiter last\nwhere it can actually act as a ceiling.\n\n## Voiceover carve\n\n**The problem it solves.** A music bed under a voice makes the voice hard to\nfollow. The reflex is to duck the whole bed, which works and costs the bed all of\nits presence — the music goes limp for the entire voiceover. But the voice does\nnot need the whole spectrum. It needs the few bands it actually occupies. Carve\ntakes only those, and the bed keeps its low end and its top, so it is still music\nwhile the voice is still intelligible.\n\n**It is a relationship, not an effect.** The settings live on the _bed_ — the\ntrack that gets processed — and they name the voices to listen to, exactly as a\nsidechain compressor does: you select the track that gets quieter and pick what\nmakes it quieter. **Never put a carve on a voice track.** A voice carved against\nitself is a bug, not a subtle mix choice.\n\n**Every voice, not one of them.** `sources` is a list, because a bed usually runs\nunder a whole sequence — a narrator, an interview answer, a second presenter. They\nare summed onto the bed's own clock before anything is measured (`mixCarveSources`),\nso one analysis covers all of them: the bands come from all the speech there is, and\nthe envelopes rise wherever any of it is happening. Voices that never play while the\nbed does are left out; they cannot mask it.\n\n**A carve against more than one clip id is wrong. Group the clips and carve\nagainst the group.** This is an invariant, not a tip. Naming clips one by one has\nto be exhaustively right and stays right only until the next edit — a fourth\nnarration clip added later plays outside the carve's awareness, and the bed\nfails to duck under it silently. Naming the group instead resolves membership at\nanalysis time, so a clip added to the group later is covered without touching\n`sources` at all:\n\n```html\n<!-- group the narration, then carve the bed against the group -->\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-outro\" data-audio-group=\"voiceover\" …></audio>\n\n<audio id=\"music\" data-fx-carve='{\"enabled\":true,\"sources\":[\"voiceover\"],\"strength\":0.8}' …></audio>\n```\n\nA `sources` list naming two or more plain clip ids instead of a group is caught\nby the `audio_carve_ungrouped_sources` lint rule — it still works, but it is the\nversion that silently rots when a clip is added.\n\n**Keep the carve group a voice group: no bed, no SFX, no music.** A group id in\n`sources` resolves to every _current_ member on _every_ analysis, so the group\nyou name is the group you get later — not the tracks that were measured when it\nwas written. Two ways that bites:\n\n- **The bed in its own source group.** It is handed to itself as a voice and\n  carved against its own content — the \"never carve a track against itself\" rule\n  arriving one re-analysis later.\n- **An SFX or music clip in the voice group.** It enters the sidechain on the\n  next analysis and the bed starts ducking under a whoosh, even though the run\n  that wrote the attribute never measured it.\n\nBoth are invisible at the moment the carve is written: the analysis sums the\nvoices it detected and never round-trips through group resolution, so the first\npass is genuinely correct and only the next one is wrong. So give each role its\nown group — `music` for the bed, `voiceover` for the narration, `sfx` for the\nhits — and keep the group named in `sources` holding nothing but voices.\n\n`carve.mjs` refuses to write the group form when it sees either case, records\nclip ids, and says on stderr which member blocked it. The\n`audio_carve_ungrouped_sources` rule then points at the arrangement instead of\nthe CLI quietly persisting a wider carve than it measured.\n\nA voice that this run left out is **not** one of these cases and does not block\nthe group form: `carve.mjs` only analyses voices that overlap the bed, and\npicking up a clip that plays later without an edit to `sources` is the whole\nreason to name the group.\n\n### One bus for many tracks\n\nMembership alone is enough to carve against, as above — but add an\n`<hf-audio-group>` element with that id and the group becomes a real submix bus:\none chain, one fader, one automation clock for every member.\n\n```html\n<hf-audio-group\n  id=\"voiceover\"\n  data-label=\"Voiceover\"\n  data-volume=\"0.9\"\n  data-fx-chain='{\"version\":1,\"nodes\":[\n    {\"type\":\"compressor\",\"id\":\"g1\",\"params\":{\"threshold\":-18,\"ratio\":3}},\n    {\"type\":\"peaking\",\"id\":\"g2\",\"params\":{\"frequency\":3000,\"gain\":2,\"q\":1}}]}'\n></hf-audio-group>\n\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>\n```\n\n**Reach for the bus when the same treatment belongs on several tracks.** Four\nnarration clips that each want the same compressor is four chains to keep in\nstep, and they drift the moment one is edited; on the bus it is one chain, and\nthe compressor sees the whole voice rather than each clip in isolation — which is\nthe point, since a compressor cannot ride a sequence it only hears a third of.\nPer-clip chains remain right for what is genuinely per-clip: one noisy take that\nneeds its own de-esser.\n\n| On the bus        | Does                                      |\n| ----------------- | ----------------------------------------- |\n| `data-fx-chain`   | one chain over the summed members         |\n| `data-automation` | envelopes on the bus, in COMPOSITION time |\n| `data-volume`     | one fader for every member (default 1)    |\n| `data-label`      | the display name; falls back to the id    |\n| `data-hidden`     | drops every member from the mix           |\n\n**Group automation is composition time, not clip time.** A bus has no\n`data-start` — members are already at their composition positions when they\nreach it — so `t: 0` in a group lane is the start of the composition, not of any\nclip. A lane on a clip is clip-local; the same numbers mean different instants on\nthe two, which is the one thing to get right when moving an envelope from a clip\nup onto its bus.\n\n**A carve stays on the clip.** `data-fx-carve` is not a group attribute. The bed\nbeing carved is a single track, and it is that track which carries\n`data-fx-carve` — pointed AT a group, per the rule above. Group and carve meet in\n`sources`, not on one element. A carve written onto a bus is half an effect\napplied twice: the level half measures the bed's own audio, which a bus has none\nof, so only the filters survive — and a bus and its members are one signal path,\nso the bed then runs through the bus's filters AND its own. The\n`audio_group_carve_attr` lint rule catches it.\n\n**One clip is not a bus.** A group exists to give several tracks one chain, one\nfader and one clock. Wrapping a single clip in a bus buys nothing the clip's own\n`data-fx-chain` does not already do, and it doubles the places a later edit has\nto land. The one reason to do it anyway: a bus's automation clock is composition\ntime, so a single-member bus is how a lane on that clip gets composition-time\ntiming.\n\n**One knob.** `strength` is 0..1 and derives everything: how deep to cut, how\nmany bands, how wide, how far to favour intelligibility over raw voice energy,\nhow far the level may drop, how far under the voice to aim. Those six move\ntogether in any real mix — a gentle carve is a shallow cut in few bands with\nlittle ducking, a hard one is deeper in more bands with more — so they are one\nrelationship written once, in `carveProfile`. `carve.mjs` defaults to `0.8` —\nsix bands from 250 Hz to 2.5 kHz cut about 7 dB each and 15 dB at 1.6 kHz, with\n19 dB of level room — because a bed under narration has to get out of the way\nfirst and be music second; `0.25` (a 6 dB dip in three bands, 6 dB of room) kept\nthe bed present but still let it fight the voice, and was judged too weak in\npractice. At `0.5` the dip reaches 10 dB, which is where a carve starts being\nheard as an effect rather than as room for the voice. Drop the strength when the\nbed is the point and the voice is sparse. `0` is spectral only — one band, no\nlevel match at all.\n\n**Carve by default — required whenever music plays under a voice.** A bed\nunder any voice track (narration, avatar speech, interview, voiceover) gets a\ncarve as part of finishing the mix, not as a polish step to get to if there is\ntime. Place both tracks, run the command below (default strength `0.8`; add\n`--bed` / `--voice` when detection picks wrong), confirm the written\n`data-fx-carve`, `data-fx-chain` and `data-automation` with `npx hyperframes check`,\nand only then render. A volume duck on its own is not a finished mix: it leaves\nthe voice and the bed fighting in the 1–3 kHz band and costs the bed all of its\npresence for the whole voiceover. Skip the carve only when there is no voice for\nthe music to sit under — a music video, a title card, a montage cut to the track.\n\n**It always follows the voice.** There is no static mode: a fixed depth thins the\nbed through every pause, and once you have heard both there is no reason to want it.\nEvery value becomes an envelope of the speech's own level — silence leaves the bed\nalone, a loud passage pushes the carve to full depth — written as ordinary automation,\nwhich is why the lanes show up in the timeline and can be edited afterwards.\n\n**Level matching is part of it.** Spectral carving cannot fix a bed that is\nsimply louder than the voice. So the carve also measures how far over the voice\nthe bed sits and writes a `gain` stage driven by an envelope. That envelope releases slowly on\npurpose — music that snaps back to full the instant a word ends sounds like a\nmachine doing it.\n\n**Running it.** In Studio the carve is one module at the top of a track's effect\nrack — voice, strength, and the analysis it produced, in one card. It is\nthere whenever another track could be the voice, and a bed with exactly **one**\ncandidate above it is carved by default, at the default strength:\nthat is what a bed under narration wants, and the module is where you change or\nswitch it off. Several candidates leaves the picker waiting rather than guessing.\nHeadless —\nwhich is the path when you are authoring a composition rather than editing one:\n\n```bash\nnode <SKILL_DIR>/scripts/carve.mjs --comp index.html\n```\n\nThat is the whole command. It finds the voice and the bed itself, carves\nat the default strength, and prints what it decided:\n\n```\nbed    music-bed (name looks like music)\nvoice  narration (only track left)\ncarve  strength 0.8, 1 voice\nbands  250Hz -7.4dB q2.06, 400Hz -7.4dB q2.06, 630Hz -7.4dB q2.06, 1000Hz -7.4dB q2.06, 1600Hz -14.8dB q2.06, 2500Hz -7.4dB q2.06\nlevel  273-point envelope, floor -19.2 dB\n```\n\nName the tracks with `--bed` / `--voice` (repeatable) when the automatic choice is\nwrong, `--strength` to push it, `--dry-run` to see that report and write nothing.\n\n**How it picks the tracks.** Names first, because that is what you already told it\nand the answer is explainable — `classifyAudioName` in core, the same classifier\nStudio's own picker uses, so the two cannot disagree. A track whose id or filename\nlooks like music (`music`, `bgm`, `bed`, `score`…) is the bed; everything else that\nplays over it and is not SFX-shaped is a voice. Audio elements are preferred: video\ncounts only when no audio track is left to be the voice, or every B-roll clip in the\ncomposition would read as somebody talking. **It refuses when it cannot tell which\ntrack is the bed** rather than carving the wrong one — typing one id is cheap.\n\nSame analysis functions as the panel, so the result is identical. Needs `ffmpeg`\non PATH and `@hyperframes/core` installed in the project (`npm i -D\n@hyperframes/core`) — the CLI inlines core rather than shipping it, so it cannot\nbe borrowed from there.\n\n**What it writes** is an ordinary chain of peaking filters plus a gain stage,\ntagged `fromCarve`. That tagging is the whole trick: a re-run replaces the\nprevious carve and leaves every effect you built by hand — and every lane you\ndrew by hand — exactly where it was. So re-carving at a new strength is safe and\nrepeatable, and `data-fx-carve` exists so the settings can be read back rather\nthan guessed from the filters.\n\n## Automation\n\nA lane is a set of breakpoints on one parameter: `{t, v}` in clip-local seconds\nand the parameter's own units. Targets are `volume` for the track's level, or\n`fx.<nodeId>.<param>` for an effect's knob.\n\n**Only some parameters can be automated, and a lane on the others is silently\ninert.** A knob is automatable when a Web Audio `AudioParam` backs it. The\nworklet-based effects — `compressor`, `limiter`, `truepeak`, `gate`, `bitcrush` — expose\nnone at all, so no lane on any of their parameters will ever move: to make a\ncompressor's behaviour change over time, automate a `gain` stage before it\ninstead. `references/fx-registry.md` marks every parameter.\n\n## Verify\n\nAlmost no static gate covers the mix. The linter reads `data-automation` for\nexactly one conflict — `audio_volume_double_automation`, a volume lane on a track\nthat also has a GSAP tween on `volume`, where the lane wins and the tween is\nignored — plus `audio_volume_tween_overrides_gain`, an authored `data-volume`\non a track whose `volume` is tweened, where the tween's values are absolute and\nreplace that gain instead of scaling it. Nothing validates the\nchain or the effect lanes at all. What\nenforces those is the render: a chain it cannot parse fails the whole mix rather\nthan quietly writing the dry signal, because a mix that sounds plausible and is\nwrong is worse than a refusal. Preview is the opposite by design: an unreadable\nchain plays dry so the composition stays workable.\n\nA lane pointing at a node the chain does not have is pruned on read, not an\nerror — so a typo'd `nodeId` costs you the envelope silently. Read the ids back\nout of the chain rather than assuming what was minted.\n\nEffects with a tail (`reverb`, `delay`) make the rendered track **longer** than\nits source, and the mix is told how much by the chain. So a bed with reverb no\nlonger ends exactly at its `data-duration`; that is expected, not a bug.\n\nBeyond that, a mix is verified by rendering and listening. For a carve: the voice\nshould be legible without the bed sounding hollowed, and the bed\nshould come back up between phrases rather than staying flat. If the bed sounds\nnotched rather than simply quieter under the voice, the strength is too high —\nthat is the one failure mode with an obvious sound.\n\nFile v1.0.15:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-audio\",\n  \"version\": \"1.0.15\",\n  \"publishedAt\": 1791657895961\n}\n\nFile v1.0.15:references/attributes.md\n\n# The three audio attributes\n\nAll three go on the `<audio>` / `<video>` element itself, JSON-encoded, so a\ncomposition carries its whole mix in the HTML with nothing to load beside it.\n`data-fx-chain` and `data-automation` also go on an `<hf-audio-group>` bus,\nwhere they mean the same thing over the summed members — with one difference\nworth knowing: a group's automation runs on COMPOSITION time, since a bus has no\n`data-start` of its own. `data-fx-carve` is clip-only; a bus has no carve.\nNothing static validates them: preview plays an unreadable chain dry to stay\nworkable, and the render refuses the whole mix rather than shipping a dry track\nthat sounds plausible and is wrong.\n\n## `data-fx-chain` — the effects\n\n```json\n{\n  \"version\": 1,\n  \"nodes\": [\n    {\n      \"type\": \"highpass\",\n      \"id\": \"n1\",\n      \"label\": \"Remove Rumble\",\n      \"params\": { \"frequency\": 120, \"q\": 0.707, \"poles\": \"2\" }\n    },\n    {\n      \"type\": \"peaking\",\n      \"id\": \"n2\",\n      \"fromCarve\": true,\n      \"params\": { \"frequency\": 1600, \"gain\": -6, \"q\": 1.4 }\n    },\n    {\n      \"type\": \"limiter\",\n      \"id\": \"n3\",\n      \"enabled\": false,\n      \"params\": { \"limit\": -1, \"attack\": 5, \"release\": 50, \"level_out\": 0 }\n    }\n  ]\n}\n```\n\n**Write these attributes double-quoted, with the JSON's own quotes as `&quot;`.**\nThe browser reads them through `getAttribute` and does not care, but\n`scripts/carve.mjs` finds them with a `name=\"...\"` regex, so a single-quoted\nattribute is invisible to it — the carve reports no existing chain and quietly\noverwrites work it could not see. `&` becomes `&amp;`; nothing else needs\nescaping.\n\n- **Order is signal order.** Each node processes what the one before produced.\n- `type` is an effect id from the registry. `params` are in the units a person\n  thinks in — dB, ms, Hz — and out-of-range values are clamped on read, so a\n  chain that parses is always safe to realise.\n- `id` is a stable handle. Automation addresses nodes by id, never by position,\n  so reordering the chain cannot re-point a lane at a different effect. A node\n  with no id loads fine but cannot be automated. Writing a chain by hand, any\n  unique string works; Studio hands out the first free `n1`, `n2`, … so matching\n  that convention keeps a hand-written chain and an edited one looking alike.\n- `label` is what the rack calls this node, replacing the effect's own name.\n  Write one whenever the node is doing a named job — a chain with two `peaking`\n  nodes otherwise shows the same row twice and the author cannot tell which is\n  the mud cut and which is the clarity lift. Presets and jobs always set it; a\n  hand-written node should too. See `presets.md` for the names they use.\n- `enabled: false` is bypass — the node stays in the chain, out of the signal\n  path. Absent means enabled.\n- `fromCarve: true` marks a node the carve analysis generated. Re-running the\n  carve replaces exactly these and leaves hand-built effects alone. **Do not set\n  it by hand**: a node tagged this way will be deleted by the next carve.\n\n## `data-automation` — the envelopes\n\n```json\n{\n  \"version\": 1,\n  \"lanes\": [\n    {\n      \"target\": \"volume\",\n      \"points\": [\n        { \"t\": 0, \"v\": 1 },\n        { \"t\": 2.5, \"v\": 0.4 }\n      ]\n    },\n    {\n      \"target\": \"fx.n2.gain\",\n      \"points\": [\n        { \"t\": 0, \"v\": 0 },\n        { \"t\": 1, \"v\": -6, \"curve\": 0.4 }\n      ]\n    }\n  ]\n}\n```\n\n- `target` is `volume` for the track's own level, or `fx.<nodeId>.<param>`.\n- `t` is **seconds from the start of the clip**, not of the composition. A bed\n  starting at `data-start=\"8\"` has `t: 0` at composition time 8.\n- `v` is in the parameter's own unit: dB for a gain, Hz for a frequency, 0..1 for\n  volume.\n- A lane holds its first value backwards to the start of its clip and its last\n  value forward to the end. So a bed that begins before the voice needs an\n  explicit \"no cut\" point at `t: 0`, or it starts out already ducked.\n- `curve` (-1..1) bends the segment _leaving_ a point: positive holds low then\n  rises late. `viaX`/`viaY` name an interior point the segment passes through\n  (progress 0..1, value travelled 0..1) and supersede `curve` when both are\n  present — that is what the timeline writes when a bend is dragged.\n- 512 points per lane, maximum.\n- A lane whose node is gone is pruned on read rather than erroring.\n\n**A lane on a non-automatable parameter is silently inert.** Automation is\ndelivered as native `AudioParam` scheduling, so a knob that no `AudioParam` backs\ncannot move: worklet processor options, a WaveShaper curve and a convolution\nimpulse are all set wholesale. `fx-registry.md` marks each parameter; the\nworklet effects (`compressor`, `limiter`, `truepeak`, `gate`, `bitcrush`) have none at all.\n\n## `data-fx-carve` — the carve's settings\n\n```json\n{ \"enabled\": true, \"sources\": [\"narration\", \"interview-guest\"], \"strength\": 0.35 }\n```\n\n- `sources` are the **element ids of every voice this bed makes room for**. They live\n  on the bed being processed, not on the voices. Summed onto the bed's clock before\n  the analysis, so one set of filters and envelopes covers all of them.\n- `strength` 0..1 derives the whole mechanism (see `carveProfile`).\n- There is no `dynamic`: a carve always follows the speech.\n- `enabled` is whether the carve applies. It exists because a bed with exactly one\n  candidate voice is carved by default: with \"off\" represented by an absent\n  attribute, switching it off would read as never-configured and the default would\n  put it back. `enabled: false` keeps the settings and stops the carve.\n\nThis attribute is not read at playback — the chain and lanes it produced are what\nplay. It exists so the settings can be read back and re-derived rather than\nguessed from the filters, which is what makes changing strength on an existing\ncarve possible.\n\nOlder projects may carry the six mechanism numbers (`maxCutDb`, `bands`, `q`,\n`intelligibilityBias`, `duckDb`, `headroomDb`) instead of `strength`. They still\nload: the depth maps back onto a strength and everything else is re-derived. A\nstored carve with no `enabled` reads as on, and a single `source` reads as a one-voice\n`sources` list. A stored `dynamic` is ignored — every carve follows the speech now.\n\nFile v1.0.15:references/diagnosis.md\n\n# Diagnosing audio you cannot hear\n\nThe symptom table in `SKILL.md` starts from \"it sounds boomy\". That presumes\nsomebody already listened and said so. Handed a file and \"fix this\", you have\nno such sentence — and you cannot listen. This is how to get one.\n\nIt is worth being blunt about the difficulty first, because the failure mode is\nnot \"no answer\", it is **a confident wrong answer**:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n\nEvery voice has peaks and dips of exactly the size an injected filter has.\nFormants are ±10 dB. A speaker's fundamental sits anywhere from 85 to 255 Hz.\nSentences decline 5–6 dB from start to end as a matter of ordinary prosody. Look\nat one spectrum on its own and you will find \"defects\" in all of it, and the\nones you find will be the speaker.\n\nSo diagnosis is always **comparison**. The whole method is choosing the right\nthing to compare against.\n\n---\n\n## Compare against something inside the same file\n\nRanked by how much they can tell you. Prefer the highest one available.\n\n### 1. The clean original, if it exists\n\nIf the undamaged take is on disk, this is the whole job — measure both, subtract,\nand the difference _is_ the defect. Nothing below is as good. Look for it before\nanything else.\n\n### 2. The pauses\n\nThe strongest reference that lives inside a single file. Speech stops; whatever\nis still there in the gap is not the voice.\n\n**What it answers: \"was something added?\"**\n\nAnything audible in the pauses is additive — hum, rumble, hiss, room tone. It was\nlaid on top, so it can be subtracted, and this is a reliable positive finding.\n\n**What it does NOT answer: \"was something filtered?\"** — and getting this\nbackwards is how the method produces a confident wrong answer.\n\nA filter multiplies. Applied to a file whose gaps already sit at the\nquantisation floor, it leaves them at the quantisation floor: near-silence times\nanything is still near-silence. So the pause carries no trace of it. Measured on\none take with a −9 dB shelf above 2.5 kHz applied to the whole file:\n\n|                   | 1 kHz | 5 kHz | tilt      |\n| ----------------- | ----- | ----- | --------- |\n| pause, undamaged  | −91.0 | −91.0 | +0.0      |\n| pause, shelved    | −91.0 | −91.0 | **+0.0**  |\n| speech, undamaged | −34.7 | −42.8 | −8.1      |\n| speech, shelved   | −35.4 | −48.5 | **−13.1** |\n\nThe defect is a clear 5 dB in the speech and **exactly zero** in the pause.\n\nSo: **never use a null result from the pause spectrum to rule out EQ.** A run\nthat did exactly that — measured the pause, found it smooth, and concluded\n\"static EQ of any type or Q is ruled out\" — went on to treat an inaudible\n−72 dBFS rumble as the defect and shipped a high-pass for a file whose actual\nproblem was that it had no top end.\n\nThe pause spectrum _is_ a transfer function only when the gaps carry a real\nrecorded noise floor that passed through the same filter. A room-tone bed does;\na digitally clean take does not. Check which you have before trusting it: if the\ngaps are within a few dB of the quantisation floor, this reference can find\nadditive content and nothing else.\n\n### 3. The speech's own tilt, for a suspected filter\n\nWhen the pause cannot see a filter (above), the only thing left carrying it is\nthe speech. Read the tilt across a few 1/3-octave bands rather than any single\none — `1k / 3.2k / 5k / 7k` is enough to see a shelf:\n\n```bash\nfor f in 1000 3200 5000 7000; do third voice.wav $f; done\n```\n\nSpeech falls away steadily above about 1 kHz, so a downward slope is expected;\nwhat you are looking for is a slope that keeps steepening, or a step. In the\ntable above, −8.1 dB from 1 k to 5 k is an ordinary voice and −13.1 dB is the\nsame voice with 9 dB taken off the top.\n\n**This is a candidate, not a verdict.** Where the ordinary slope ends and a\ndefect begins is speaker-dependent, and you have no baseline for this speaker.\nSay what you measured and what it would mean, and let somebody hear it.\n\n### 4. The file against itself over time\n\nFor anything level-related, compare each passage to the track's own median rather\nthan to a target. That is what `levellingResult` does, and it is why an already\neven track comes back untouched.\n\n---\n\n## Do not compare against a different voice\n\nBoth wrong answers in the evaluation that produced this page came from an\nexternal reference, and both were argued rigorously from bad ground:\n\n- **A published average spectrum** (LTASS and friends). One run concluded\n  \"+10 dB above 7 kHz, split-half stable, gating-independent\" on a file whose\n  actual defect was +6.6 dB at 200 Hz. Its supporting claim — 10 kHz sitting\n  6.2 dB above 6.3 kHz — measured 0.6 dB on re-check, and measured the same in\n  the clean original. Published curves are mixed-sex, mixed-corpus, and\n  mixed-microphone; the gap between them and any one speaker is larger than most\n  defects.\n- **A synthesised control voice** (`say`, a TTS take, another narrator). One run\n  generated a control this way, found the spectrum \"normal\", and missed a −6.9 dB\n  shelf. Two speakers differ by more than 7 dB across the top octaves as a matter\n  of course, so a cross-voice comparison cannot resolve a defect that size.\n\nIf neither the original nor usable pauses exist — continuous speech, or gaps that\nare digital silence and so carry no channel — then a static tonal defect is\n**genuinely under-determined**.\n\nReport that. It is a finding, not a failure to find one, and it is the correct\nanswer rather than the fallback when the better methods are unavailable. Give\nthe author the two or three readings that fit and ask which they hear; they can\nlisten, and that one sentence from them collapses the whole problem.\n\n**This is the point where a capable agent goes wrong.** Told a thing is\nunder-determined, the instinct is to invent a cleverer measurement and escape\nit — and something will always be found, because a single voice's spectrum is\nfull of peaks and valleys that survive any amount of statistical rigour. An\nelaborate novel method reaching a confident conclusion, on a file where the two\nreliable references were both unavailable, is the _signature_ of this failure,\nnot evidence against it. If you notice yourself building one, stop and report\nthe ambiguity instead.\n\n---\n\n## Recipes\n\n### Compare loudness from the bytes the listener actually hears\n\nDo not call two clips equally loud because their Studio faders, waveform peaks,\nor cached asset metadata match. Those are controls and proxies, not a loudness\nmeasurement. Resolve the exact URLs used by preview/render, download or inspect\nthose exact served bytes, and measure each decoded stream with FFmpeg's\n`ebur128` filter. Compare the integrated LUFS values.\n\nFor a target loudness, the required move is:\n\n```text\ngain_db = target_lufs - measured_lufs\nlinear_gain = 10 ** (gain_db / 20)\n```\n\nWhen both clips are local authored `<audio>` elements with stable ids, use the\nCLI instead of transcribing that arithmetic by hand:\n\n```bash\nnpx hyperframes normalize-audio --reference target-audio --target user-audio\nnpx hyperframes normalize-audio --reference target-audio --target user-audio --write\n```\n\nThe first command is a dry run. The second writes only the target's\n`data-volume`, after accounting for both existing gains and refusing a boost\nthat would clip or exceed Studio's ceiling. Always choose the reference from the\nauthor's stated intent; the command does not guess which clip should define the\nmix.\n\nStudio's clip-gain fader uses `0 dB` / linear gain `1` at its physical midpoint\nand provides up to `+12 dB` on the upper half. After changing gain, measure the\nserved preview/render bytes again. If a listener still hears a mismatch, trust\nthe report and first verify the asset URL and bytes are current; do not explain\nit away with matching peaks or a stale proxy measurement.\n\nAll verified with ffmpeg 8.1.1. `-hide_banner` keeps the output readable;\n`volumedetect` prints to stderr, so do not silence it with `-v error`.\n\n### Band energy, in proportional bands\n\n**Use proportional bandwidths or the numbers lie.** A fixed 2000 Hz-wide band at\n10 kHz collects more energy than a 1200 Hz-wide band at 6.3 kHz for no reason but\nits width, which manufactures a high-frequency excess that is not there. One\nthird of an octave is `f × 0.2316`.\n\n```bash\nthird() {\n  w=$(python3 -c \"print(round($2*0.2316))\")\n  ffmpeg -hide_banner -i \"$1\" -af \"bandpass=f=$2:width_type=h:w=$w,volumedetect\" \\\n    -f null - 2>&1 | grep -m1 mean_volume\n}\nthird voice.wav 200     # weight / boom\nthird voice.wav 3200    # presence / harshness\n```\n\nRead them as a shape across 100 / 200 / 400 / 1k / 3.2k / 7k, and read the shape\nagainst a reference from the list above — never on its own.\n\n### The noise floor, and what is in it\n\n```bash\nffmpeg -hide_banner -i voice.wav -af astats=metadata=1 -f null - 2>&1 | grep -i 'noise floor'\n```\n\n`-inf` means digital silence in the gaps: no additive noise, so rumble, hiss and\nroom tone are all ruled out in one command. A real number is the level of\nwhatever is sitting under the voice. To see its _shape_, cut a pause out with\n`-ss`/`-t` and run the band recipe on that slice alone.\n\n### Level over time\n\n```bash\nffmpeg -hide_banner -i voice.wav -af ebur128=framelog=quiet -f null - 2>&1 | tail -6\n```\n\nLRA under ~3 LU is even. Then window it, because LRA hides a single sagging\npassage:\n\n```bash\nfor s in 0 1.2 2.4 3.6 4.8 6.0; do\n  ffmpeg -hide_banner -ss $s -t 1.2 -i voice.wav -af volumedetect -f null - 2>&1 |\n    grep -m1 mean_volume\ndone\n```\n\n**A 4–6 dB spread across windows is normal speech**, not a defect — sentences\ndecline as they end. Injected unevenness looks like 12 dB or more. Levelling a\ntrack that only has declination flattens the prosody and is heard as robotic.\n\n### Pitch, before blaming the low end\n\n```bash\nffmpeg -hide_banner -i voice.wav -af \"lowpass=f=400,astats=metadata=1\" -f null - 2>&1 | grep -i 'peak level'\n```\n\nA voice has no energy below its own fundamental, so a \"missing\" 100 Hz on a\nspeaker whose F0 is 210 Hz is the speaker, not a rolloff.\n\nThe same fact runs the other way, and that direction is the trap: **a boost near\nthe fundamental is indistinguishable from that voice being naturally chesty.**\nBoth look like energy at F0, because both are.\n\nSo the rule is symmetric, and the dangerous half is the second one:\n\n- Do not call a peak at F0 a defect on its own evidence.\n- **Do not dismiss one either.** \"The peak is at 200 Hz, F0 is 185 Hz, therefore\n  it is the fundamental\" is not a diagnosis — it is the same observation\n  restated, and it discards the one candidate most likely to be real. Boominess\n  _is_ excess energy at the bottom of a voice; that is what the word means.\n\nWhat you can do is measure how much, against the same file's midrange:\n\n```bash\nthird voice.wav 200      # or the nearest 1/3-octave band to F0\nthird voice.wav 1000\n```\n\nIn an ordinary take these land within a couple of dB of each other. A low band\nsitting **more than about 4 dB above the 1 kHz band** is a strong boom or mud\ncandidate. Measured across one voice damaged several ways: undamaged +0.9,\nharsh +0.6, dull +2.0; boomy +6.7, muddy +5.8. Treat the figure as indicative\nrather than a threshold — it is one speaker — but the separation is wide, and a\nreading up at +6 is worth raising even when you cannot explain it.\n\nIt still cannot tell you whether a filter did that or the speaker did, so report\nit as a candidate. That is the whole answer here: measure it, name it, hand the\nchoice to somebody who can hear it.\n\n---\n\n## Then, and only then, the symptom table\n\nMeasurement gives you the band and the kind. `SKILL.md`'s table and\n`presets.md`'s fuller one turn that into a fix. Going the other way round —\npicking a plausible fix and finding evidence for it — is how both wrong answers\nin the evaluation happened, and both were long, careful and confident.\n\nOne habit that catches it: before applying anything, state what you would expect\nto measure **if you are wrong**, and check that too.\n\nFile v1.0.15:references/fx-registry.md\n\n# Effect registry\n\nEvery effect, its parameters and the usable range of each. Values outside a range\nare clamped on read, so anything that parses is safe to realise. **AUTO** marks a\nparameter an automation lane can drive; anything unmarked cannot move over time\n(see the note at the bottom).\n\nGenerated from `HF_AUDIO_FX` in `@hyperframes/core/audio-fx`, which is the source\nof truth — if this table and the code disagree, the code is right.\n\n## Filter — which frequencies a track may occupy\n\n| Effect      | Parameter                                                                                                   |\n| ----------- | ----------------------------------------------------------------------------------------------------------- |\n| `highpass`  | `frequency` 20–20000 Hz (300, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)       |\n| `lowpass`   | `frequency` 100–20000 Hz (8000, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)     |\n| `peaking`   | `frequency` 20–20000 Hz (1000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** · `q` 0.1–20 (1, log) **AUTO** |\n| `lowshelf`  | `frequency` 20–2000 Hz (200, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                                  |\n| `highshelf` | `frequency` 500–20000 Hz (4000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                               |\n\n`q` is bandwidth — higher is narrower. `poles` is the slope: `2` is the usual\nbiquad (12 dB/oct), `1` is gentler (6 dB/oct). Shelving filters have no `q`: the\nWeb Audio spec leaves it unused for them, so a control would have moved nothing.\n\n## Dynamics — how level behaves over time\n\n| Effect       | Parameter                                                                                                                                                                      |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `gain`       | `gain` −60–12 dB (0) **AUTO**                                                                                                                                                  |\n| `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) |\n| `limiter`    | `limit` −24–0 dB (−1) · `attack` 0.1–80 ms (5) · `release` 1–8000 ms (50, log) · `level_out` −24–24 dB (0)                                                                     |\n| `truepeak`   | `ceiling` −24–0 dBTP (−1) · `lookahead` 0.5–10 ms (3) · `release` 10–2000 ms (80, log)                                                                                         |\n| `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)               |\n\nCuts on `gain` go to −60 dB, boosts stop at +12: it is a level stage for making\nroom, and a chain that could add 40 dB would clip long before that was useful.\n`knee` of 1 is a hard corner, higher eases into it. `mix` below 1 blends the dry\nsignal back in (parallel compression). `range` is how far down the gate pulls\nwhen closed — a gate that pulls all the way to silence sounds like a switch.\n\n`limiter` follows the signal's level and has no lookahead, so it does not\nguarantee a peak ceiling. For a delivery ceiling use `truepeak`: it estimates the\ninter-sample (true) peak at 4x, looks `lookahead` ms ahead, and holds that estimate\nat `ceiling` dBTP. It delays its output by `lookahead` plus about 0.3 ms; the\nrender trims the delay, live preview plays it.\n\nThe ceiling is a 4x estimate, so the real true peak can end above it. Against an\nideal 16x interpolation, tones, pink noise and a dense mix ended at most 0.6 dB\nover; full-band white noise driven 8 dB or more into the limiter ended 1.2 to 1.7\ndB over (median 1.4). A 4x meter such as ffmpeg `ebur128=peak=true` reads that\nnoise about 0.1 dB over and will not show it. Under a hard delivery limit set\n`ceiling` at least 2 dB below it (−3 dBTP for a −1 dBTP limit).\n\n## Nonlinear — changes the waveform's shape\n\n| Effect     | Parameter                                                                                                                                                                  |\n| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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) |\n| `bitcrush` | `bits` 1–32 (8) · `samples` 1–250× (1) · `mix` 0–1 (1)                                                                                                                     |\n\n`tanh` is the gentlest curve and `hard` is outright clipping. Higher `oversample`\ncosts more CPU and keeps aliasing down. `samples` repeats each sample N times — a\ncrude downsample, which is where the lo-fi character comes from.\n\n## Time — space and width\n\n| Effect   | Parameter                                                                                                                                                           |\n| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `delay`  | `time` 1–5000 ms (250, log) **AUTO** · `feedback` 0.01–0.95 (0.35) **AUTO** · `mix` 0–1 (0.4) **AUTO**                                                              |\n| `reverb` | `size` 0.05–1 (0.7) · `damping` 0–1 (0.5) · `wet` 0–1 (0.35) **AUTO** · `dry` 0–1 (0.7) **AUTO**                                                                    |\n| `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**                                           |\n| `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) |\n\nReverb convolves a _generated_ impulse, and both preview and render generate the\nsame one — so a room is reproducible without shipping an impulse file. Higher\n`damping` rolls the top off the tail faster, which is what makes a large room\nsound like a soft one. `feedback` near the top of its range is a very long tail;\nit is bounded below 1 because at 1 it never decays.\n\n## Why some parameters cannot be automated\n\nAutomation is handed to the audio thread once, as native `AudioParam` ramps and\ncurves, which is what keeps it sample-accurate and identical between preview and\nrender. A parameter can therefore only be automated if an `AudioParam` backs it.\nThree kinds do not:\n\n- **worklet processor options** — `compressor`, `limiter`, `truepeak`, `gate` and `bitcrush`\n  are AudioWorklets configured wholesale, so **none of their parameters are\n  automatable at all**.\n- **a WaveShaper curve** — `saturate`'s `type`, `threshold` and `oversample`\n  rebuild the curve; only its `output` stage is a real param.\n- **a convolution impulse** — `reverb`'s `size` and `damping` regenerate the\n  impulse; `wet`/`dry` are gain stages and automate fine.\n\nTo make one of those behave differently over time, automate a `gain` stage\naround it instead: a lane on a `gain` before a compressor changes how hard the\ncompressor is driven, which is most of what automating its threshold would have\ndone.\n\nFile v1.0.15:references/presets.md\n\n# Presets, jobs and one-knob profiles\n\nEverything here is a shortcut to a chain you could have built by hand. A preset\nwrites ordinary nodes tagged with `fromPreset`, a job writes one ordinary node\nwith a name, and a profile is one control over several parameters of one effect.\nNothing is opaque: open any of them and you find effects from\n[`fx-registry.md`](./fx-registry.md) with their parameters showing.\n\nReach for one when it names the problem you actually have. Build by hand when\nnone of them does — a preset applied because it was nearby is worse than three\ndeliberate nodes.\n\n---\n\n## Diagnose first: what to listen for, and what fixes it\n\nWork from the symptom, not from the effect list. Most bad audio is one or two of\nthese, and the fix is usually a job rather than a whole preset.\n\n| It sounds like                                | Where it lives | Reach for                                                    |\n| --------------------------------------------- | -------------- | ------------------------------------------------------------ |\n| Hum, rumble, traffic, footsteps, handling     | 20–80 Hz       | `rumble-cut` preset, or a `highpass` at 80 Hz                |\n| Boomy, chesty, too close to the mic           | 80–250 Hz      | **Tame Boominess** job (200 Hz, −4 dB)                       |\n| Muffled, like it is behind cardboard          | 250–600 Hz     | **Reduce Mud** job (250 Hz, −3 dB)                           |\n| Boxy, like a small room                       | ~400 Hz        | **Reduce Boxiness** job (400 Hz, −3 dB)                      |\n| Words hard to make out, sits behind the music | 2–5 kHz        | **Add Clarity** job (3 kHz, +2.5 dB), or carve the bed       |\n| Harsh, brittle, tiring over a whole listen    | 3–5 kHz        | **Soften Harshness** job (3.2 kHz, −3 dB)                    |\n| Sibilant — `s` sounds spitting                | 5–10 kHz       | Nothing shipped does this properly; see \"Not covered\" below  |\n| Dull, closed-in, lifeless                     | 10–20 kHz      | `highshelf` lift, or `voice-broadcast` which includes one    |\n| Some words much louder than others            | not a band     | **Evenness** profile on a `compressor`, or `levellingResult` |\n| Room tone audible between sentences           | not a band     | `room-gate` preset (**Tightness** profile)                   |\n| Peaks clipping or spiking                     | not a band     | `limiter` last in the chain — every voice preset ends in one |\n| Voice and music fighting each other           | 1–3 kHz mostly | **Voiceover carve**, not an EQ on either track               |\n| Dry, stuck to the speaker, recorded nowhere   | not a band     | `room-tight` or `room-natural`                               |\n\n**The band vocabulary** these map onto — the same names the rack shows:\n\n| Range          | Name     | What lives there             |\n| -------------- | -------- | ---------------------------- |\n| 20–80 Hz       | Rumble   | traffic, footsteps, handling |\n| 80–250 Hz      | Weight   | chest, body, warmth          |\n| 250–600 Hz     | Mud      | boxy, muffled, cardboard     |\n| 600–2000 Hz    | Middle   | the body of a voice          |\n| 2000–5000 Hz   | Presence | consonants, intelligibility  |\n| 5000–10000 Hz  | Edge     | sibilance, harshness         |\n| 10000–20000 Hz | Air      | sparkle, openness            |\n\n### Order of operations\n\nDiagnose in this order, because each step changes what the next one hears:\n\n1. **Subtract before you add.** Cut rumble and mud first. A voice that sounds\n   dull often has too much low-mid, not too little top — lifting the top of a\n   muddy voice makes it muddy _and_ harsh.\n2. **Level after you filter.** A compressor reacts to whatever is loudest, and\n   a rumble it can no longer see is a rumble it stops chasing.\n3. **Relationships after level.** Carve a bed against a voice once the voice\n   itself is settled, or the analysis measures a problem you are about to fix.\n4. **Character, then ceiling.** Saturation and space go late; a `limiter` goes\n   last, where it can actually act as a ceiling. Anything after it is not\n   bounded by it.\n\n---\n\n## Presets\n\nFour families, listed in full below. Apply one and it **appends** — stacking a character preset\nonto an already-cleaned voice is a real thing to want. Re-applying one that is\nalready present replaces its own nodes in place, because position in the chain\nis signal order.\n\n### Voice — make a real voice sound like its better self\n\n| Preset            | Answers                         | Chain                                                                                               |\n| ----------------- | ------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `voice-clean`     | \"My voice sounds amateur\"       | Remove Rumble → Reduce Mud → Even Out Loudness → Add Clarity → Peak Ceiling                         |\n| `voice-broadcast` | \"I want it to sound like radio\" | Remove Rumble → Reduce Boxiness → Even Out Loudness → Add Clarity → Add Air → Warmth → Peak Ceiling |\n| `voice-warm`      | \"I want it intimate and close\"  | Remove Rumble → Add Weight → Even Out Loudness → Add Clarity → Peak Ceiling                         |\n\n`voice-clean` is the default answer to \"fix this voiceover\". The other two are\nthe same idea pushed in one direction: broadcast is denser and more forward,\nwarm has body added rather than cut.\n\n### Repair — one problem, one node\n\n| Preset       | Answers                                 | Does                                                                        |\n| ------------ | --------------------------------------- | --------------------------------------------------------------------------- |\n| `rumble-cut` | \"There's a hum or thump underneath\"     | High-pass under the voice                                                   |\n| `room-gate`  | \"I can hear the room between sentences\" | Closes the pauses. **Does not remove noise** — room tone under speech stays |\n| `boom-tame`  | \"My voice sounds boomy\"                 | Cuts the chestiness of a too-close mic                                      |\n| `harsh-tame` | \"It's harsh and tiring to listen to\"    | Rounds a brittle upper-mid, broad and always-on                             |\n\n### Character — deliberate, not corrective\n\n`telephone`, `radio-am`, `megaphone`, `lofi-tape`, `pa-system` (Tannoy),\n`intercom`, `doofus-worble`.\n\nThese are costumes. Each is a band restriction plus a resonance plus its own kind\nof dirt, and they are tuned to be distinguishable from one another — measured on\na log sweep, no two sit closer than the signal itself. Do not stack two.\n\n### Space — put it somewhere\n\n`room-tight` (presence without wash), `room-natural` (recorded somewhere rather\nthan nowhere), `hall` (far back and big), `slap-echo` (one quick repeat),\n`dub-throw` (repeats trailing well behind).\n\nUse these on whatever should sit _behind_ something else, and keep the wet amount\nlower than sounds right in isolation — a tail occupies the room a voice needs.\n\n### The whole preset as one control\n\nA preset's nodes are wrapped in a wet/dry blend, so `presetAmount` (0..1) fades\nthe entire thing in or out, and `fx.preset.<id>` is an automation target that\nramps it over time. This is the only way to automate a preset as a unit: its\nnodes share no common parameter, and worklet effects (compressor, limiter, gate,\nbitcrush) expose no automatable parameters at all.\n\n---\n\n## Jobs — the range IS the module\n\nFive named peaking filters with the frequency already chosen. Picking the job is\npicking the range, which is what makes a single \"how much\" knob honest.\n\n| Job              | Symptom                              | Sets                  |\n| ---------------- | ------------------------------------ | --------------------- |\n| Tame Boominess   | Too much chest — it booms            | 200 Hz, −4 dB, Q 1.4  |\n| Reduce Mud       | Muffled, like it is behind cardboard | 250 Hz, −3 dB, Q 1.2  |\n| Reduce Boxiness  | Sounds like a small room, or a box   | 400 Hz, −3 dB, Q 1.4  |\n| Add Clarity      | Words are hard to make out           | 3 kHz, +2.5 dB, Q 1   |\n| Soften Harshness | Harsh and tiring to listen to        | 3.2 kHz, −3 dB, Q 1.6 |\n\nEach is an ordinary `peaking` node underneath — the frequency is a starting\npoint, not a cage. Prefer a job to a bare `peaking` when one matches: it arrives\nalready aimed, and the rack names it for the work rather than the mechanism.\n\nWriting one by hand, **carry the name in `label`** — `{\"type\":\"peaking\",\"id\":\"n2\",\n\"label\":\"Reduce Mud\",\"params\":{\"frequency\":250,\"gain\":-3,\"q\":1.2}}`. The\nparameters alone are not the job. A chain with three unlabelled `peaking` nodes\nshows the author three identical rows, which is the exact problem jobs exist to\ndissolve.\n\n**Every job also ships inside a preset, at identical settings** — that is where\nthe five came from. `boom-tame` _is_ Tame Boominess; `harsh-tame` _is_ Soften\nHarshness; `voice-clean` contains Reduce Mud and Add Clarity; `voice-broadcast`\ncontains Reduce Boxiness. So check what a preset already contains before adding\na job on top of it, or the cut lands twice — `voice-clean` plus a Reduce Mud job\nis −6 dB at 250 Hz where −3 was meant. The rack shows the contained nodes by\nname once the preset is expanded, which is the fastest way to see it.\n\n---\n\n## One-knob profiles\n\nFive effects have no single parameter that can honestly be their face — a\ncompressor's threshold means nothing without its ratio. They get a derived\ncontrol instead, 0..1, which sets several parameters together.\n\n| Effect       | Knob      | 0 → 1                                      | Sets                                      |\n| ------------ | --------- | ------------------------------------------ | ----------------------------------------- |\n| `compressor` | Evenness  | Barely touched → Very even, quite squashed | threshold, ratio, attack, release, makeup |\n| `gate`       | Tightness | Only true silence → Cuts quiet words too   | threshold, range, release                 |\n| `saturate`   | Warmth    | Just a sheen → Openly distorted            | threshold, output                         |\n| `reverb`     | Space     | A small tight room → A big open hall       | size, wet, dry                            |\n| `bitcrush`   | Crush     | Slightly gritty → Destroyed                | bits, samples, mix                        |\n\n**Evenness, Warmth and Space are level-matched** — the make-up gain, the output\ntrim and the dry leg move with the drive, so turning the knob up does not also\nturn the track up or down. Those figures were solved by measurement, not chosen:\nthe compressor originally left a track 2.5 dB _quieter_ at full evenness, and\nsaturation's trim ran the wrong way entirely.\n\nTightness and Crush are not level-matched, because neither has a trim to move —\na gate only removes, and Crush's `mix` is the effect itself rather than a\nmake-up.\n\nThe chain stores the mechanism values, not the knob position; the knob is read\nback by inverting the curve. So hand-editing a parameter under a profile is\nallowed and will simply move the knob.\n\n---\n\n## Measuring scripts, not presets\n\nTwo things measure the audio before they act, so they cannot be a fixed chain:\n\n- **Voiceover carve** — analyses the voice and cuts the bed in the bands the\n  voice occupies. The answer to \"the music is fighting the voice\". See the\n  carve section in `SKILL.md`.\n- **Even Out Levels** (`levellingResult`) — measures the track's own speaking\n  windows and writes a gain envelope. Its target is the 80th percentile of that\n  track, not an absolute level, so an already-even track is left alone. Use it\n  over a compressor when the problem is passages drifting over a whole take\n  rather than word-to-word dynamics.\n\n---\n\n## Not covered by anything shipped\n\nName the gap rather than reaching for the nearest preset and calling it the\nthing — but then **ship the honest fallback anyway**, with its cost stated. An\nauthor who asked for a fix and got only an explanation has been told something\ntrue and handed nothing. Say what it is, say what it costs, apply it.\n\n- **De-essing.** `harsh-tame` is a broad always-on cut centred a band too low,\n  not a de-esser. A real one needs a detector faster than the analysis hop\n  available here. _Fallback:_ a narrow `peaking` cut in the Edge band — sweep\n  5–9 kHz to find where this voice actually spits, Q 3–4, −3 to −5 dB. It is\n  always on, so it costs a little air on every word; that trade is usually worth\n  it and is the author's to reject.\n- **Tone matching** one track to another. _Fallback:_ the Tone EQ by hand, which\n  is predictable in a way a match curve derived from two takes would not be.\n- **Noise removal.** `room-gate` closes the gaps; the noise under speech is\n  untouched. There is no fallback for hiss beneath the words — a source with\n  audible hiss needs a better source, and saying so is the whole answer.\n\nFile v1.0.15:skill-card.md\n\n## Description:\n\nHelps creators mix audio already placed in a HyperFrames composition with fades, automation, effects, submix buses, and voiceover carving.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[heygen-com](https://clawhub.ai/user/heygen-com)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nCreators and developers use this skill to balance music, voice, and effects in existing HyperFrames compositions, including automating levels and carving space for narration.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Audio-mixing commands can change composition HTML attributes.\n\nMitigation: Review the selected composition and proposed track changes; use dry-run before writing when appropriate.\n\nRisk: Unpinned project tooling may change behavior between runs.\n\nMitigation: Prefer project-local, lockfile-backed HyperFrames tooling or pinned npx versions.\n\n## Reference(s):\n\n- [HyperFrames Audio on ClawHub](https://clawhub.ai/heygen-com/skills/hyperframes-audio)\n- [Audio attributes](artifact/references/attributes.md)\n- [Effect registry](artifact/references/fx-registry.md)\n- [Audio diagnosis](artifact/references/diagnosis.md)\n- [Mixing presets](artifact/references/presets.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Code, Configuration]\n\n**Output Format:** [Markdown guidance and HTML audio attributes]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Can update a specified composition HTML file; does not source or generate audio.]\n\n## Skill Version(s):\n\n1.0.15 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.14: 10 files, 42500 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (23848b), scripts/carve.test.mjs (11818b), scripts/lib/main-module.mjs (479b), skill-card.md (2011b), SKILL.md (25614b), _meta.json (137b)\n\nFile v1.0.14:SKILL.md\n\n---\nname: hyperframes-audio\ndescription: >\n  Use when audio already placed in a HyperFrames composition needs to be mixed:\n  fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking,\n  a music bed that fights a voiceover (voiceover carve), effects on a track\n  (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser,\n  bitcrush), automation envelopes drawn on a track's volume or any effect\n  parameter, or one submix bus carrying a chain, a fader and an automation clock\n  for several tracks at once (`<hf-audio-group>`).\n  Don't use for sourcing or generating audio — finding BGM, SFX, or making a\n  voiceover is `/media-use`. Don't use for clip timing or track layout, which is\n  `/hyperframes-core`.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Audio\n\nA mix is a set of relationships, not a stack of processors. Two tracks that each\nsound right alone can be unlistenable together, and the fix is almost never \"turn\none down\" — it is finding what they are fighting over and giving it to whichever\none needs it. Every tool here exists to express one of those relationships.\n\nEffects live on the element as `data-fx-chain`, and preview and render run the\nsame Web Audio graph — the studio in a live context, the engine in an offline one\ninside the browser it already drives. There is one implementation of each effect,\nso what you hear while scrubbing is what gets written. You never tune twice.\n\nClip timing remains `/hyperframes-core`: audio/video trims and source ranges use\n`data-start`, `data-duration`, and `data-media-start`, and crossfades overlap\nclips on different tracks. This skill owns placed-track fade-in/fade-out,\ncrossfade envelopes, track gain/track volume, volume and effect automation,\nducking/voiceover carve, and the effect chain. `/media-use` owns sourcing,\ngeneration, and preprocessing.\n\nConstant `data-playback-rate` (`0.1..10`) is render-safe for picture and\npitch-preserved sound when matching audio/video elements use the same timing,\nsource offset, and rate. A speed ramp is a `rate` lane in `data-automation`\n(see `docs/reference/speed-ramps`); it wins over the constant and keeps pitch\nin preview and render. HyperFrames does not\nprovide automatic waveform sync or drift correction.\nFor copyable cut/crossfade/retime recipes, use `/hyperframes-core` → `references/creator-editing-recipes.md`.\n\nThree attributes carry everything, on the audio/video element itself — or, for\nthe first two, on an `<hf-audio-group>` bus (see \"One bus for many tracks\"):\n\n| Attribute         | Holds                                                     |\n| ----------------- | --------------------------------------------------------- |\n| `data-fx-chain`   | the effects, in signal order                              |\n| `data-automation` | envelopes on this track's volume or its effect parameters |\n| `data-fx-carve`   | the carve's own settings, so it can be re-derived         |\n\nThe shipped effect families are gain, EQ (highpass, lowpass, peaking, shelves),\ncompressor, limiter, gate, saturate, delay, reverb, chorus, phaser, and bitcrush.\n\nExact JSON for each, and the rules a lane must satisfy: `references/attributes.md`.\nEvery effect with its parameters, ranges and units: `references/fx-registry.md`.\nHow to work out what is wrong with a file you cannot hear:\n`references/diagnosis.md`.\n**Presets, named jobs and one-knob profiles, plus a symptom-to-fix table:\n`references/presets.md`** — read that before hand-building a chain, because one\nof the presets or named jobs usually already names the problem.\n\n## How it fits together\n\nTwo authoring surfaces write those attributes; two runtimes read them through the\nsame builders. That shared middle is why preview predicts the render.\n\n```mermaid\nflowchart TB\n  voice[\"voice track<br/>media file\"]\n  bed[\"music bed<br/>media file\"]\n\n  subgraph AUTHOR[\"Authoring — the only things that write attributes\"]\n    panel[\"Studio<br/>Voiceover carve control\"]\n    script[\"scripts/carve.mjs<br/>detects the pair\"]\n    analysis[\"core/audioCarve.ts<br/>carveProfile · analyseCarveBands<br/>analyseCarveDuck · analyseCarveDynamics\"]\n    panel --> analysis\n    script --> analysis\n  end\n\n  voice --> analysis\n  bed --> analysis\n\n  subgraph ATTRS[\"Written onto the bed element\"]\n    carveAttr[\"data-fx-carve<br/>sources · strength\"]\n    chainAttr[\"data-fx-chain<br/>peaking xN + gain, tagged fromCarve\"]\n    autoAttr[\"data-automation<br/>a lane per carved parameter\"]\n  end\n\n  analysis --> carveAttr\n  analysis --> chainAttr\n  analysis --> autoAttr\n\n  subgraph SHARED[\"One implementation, read by both\"]\n    build[\"audioFxGraph.ts · buildFxChain\"]\n    sched[\"audioFxAutomation.ts · scheduleChainAutomation\"]\n  end\n\n  chainAttr --> build\n  autoAttr --> sched\n\n  build --> preview[\"Preview<br/>live AudioContext<br/>attachElementFxChain\"]\n  sched --> preview\n  build --> render[\"Render<br/>OfflineAudioContext in the headless browser<br/>applyAudioFxChain\"]\n  sched --> render\n\n  preview --> heard[\"what you hear while scrubbing\"]\n  render --> wav[\"processed WAV<br/>+ chainTailSeconds so the mix lets the tail through\"]\n  wav --> mix[\"engine · audioMixer<br/>volume lane baked into the PCM here, not in the graph\"]\n  mix --> out[\"the rendered mix\"]\n\n  edit[\"editing the attribute mid-playback\"] -.->|MutationObserver| preview\n```\n\nThe carve's own settings are never read at playback — the chain and lanes it\nproduced are what play. `data-fx-carve` exists so strength can be changed on an\nexisting carve instead of guessed back out of the filters.\n\nInside a carved bed the signal runs through the dips first, then the level match,\nthen anything you built yourself — which is why a limiter you add still acts as\nthe last ceiling:\n\n```mermaid\nflowchart LR\n  src[\"decoded bed\"] --> p1[\"peaking<br/>400 Hz\"]\n  p1 --> p2[\"peaking<br/>1 kHz\"]\n  p2 --> p3[\"peaking<br/>1.6 kHz\"]\n  p3 --> g[\"gain<br/>level match\"]\n  g --> hand[\"your own effects<br/>e.g. limiter\"]\n  hand --> dest[\"track gain, then out\"]\n\n  l1[\"lane fx.n1.gain\"] -.->|\"envelope of the voice's<br/>level in that band\"| p1\n  l4[\"lane fx.n4.gain\"] -.->|\"how far the bed<br/>ducks overall\"| g\n```\n\n## First, work out what is wrong\n\nThe table below starts from \"it sounds boomy\" — which presumes somebody already\nlistened and said so. Handed a file and \"fix this\", you have no such sentence\nand you cannot listen, so you have to measure. One rule governs all of it:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n> Formants are ±10 dB, fundamentals run 85–255 Hz, and sentences decline 5–6 dB\n> as they end. Every one of those reads as a defect on its own, and every one of\n> them is the speaker.\n\nSo compare, and compare against something **inside the same file**: the clean\noriginal if it exists, otherwise the pauses — whatever is audible in a gap is\nadditive, and the gap's spectrum is the channel rather than the voice. Comparing\nagainst a published average spectrum or a synthesised control voice does not\nwork: two speakers differ by more than most defects, and both wrong answers in\nthe evaluation behind this guidance came from exactly that.\n\nWhen there is no original and no usable silence, a static tonal defect is\ngenuinely under-determined. Say so and offer the readings that fit, rather than\npicking one and building a chain on it.\n\nCommands, traps and worked recipes: **`references/diagnosis.md`**. Read it\nbefore diagnosing a file nobody has described.\n\n## Start from the symptom\n\nOnce you know the band and the kind, name what is wrong with the audio. Most bad audio is\none or two of these, and each has a shipped answer:\n\n| It sounds like                     | Reach for                                          |\n| ---------------------------------- | -------------------------------------------------- |\n| Hum or thump underneath            | `rumble-cut`, or a `highpass` at 80 Hz             |\n| Boomy, chesty                      | **Tame Boominess** job (200 Hz)                    |\n| Muffled, behind cardboard          | **Reduce Mud** job (250 Hz)                        |\n| Words hard to make out             | **Add Clarity** job (3 kHz), or carve the bed      |\n| Harsh and tiring                   | **Soften Harshness** job (3.2 kHz)                 |\n| Some words much louder than others | **Evenness** on a compressor, or Even Out Levels   |\n| Room tone between sentences        | `room-gate`                                        |\n| Voice and music fighting           | **Voiceover carve** — not an EQ on either          |\n| Dry, recorded nowhere              | `room-tight` or `room-natural`                     |\n| Just \"amateur\"                     | `voice-clean`, which is four of the above in order |\n\nFull catalogue, what each preset contains, the band vocabulary, and what is\ndeliberately NOT covered (de-essing, noise removal, tone match):\n`references/presets.md`.\n\nSubtract before you add, level after you filter, relationships after level,\ncharacter and ceiling last. Each step changes what the next one hears — a\ncompressor set before a high-pass spends its time chasing rumble.\n\n## Reach for a family by the problem, not the name\n\n**Filters** (`highpass`, `lowpass`, `peaking`, `lowshelf`, `highshelf`) decide\nwhich frequencies a track is allowed to occupy. This is the first tool for two\nsources colliding, because collisions happen in bands: a bed and a voice both\nwant 1–3 kHz, and taking that from the bed costs the bed far less than turning\nthe whole thing down costs the mix. A high-pass on a voice is the standard fix\nfor rumble; a low-pass darkens or muffles deliberately.\n\n**Dynamics** (`gain`, `compressor`, `limiter`, `gate`) decide how a track's level\nbehaves over time. Compression narrows the distance between loud and quiet so the\nquiet parts can come up. A limiter is a ceiling — it does not shape anything, it\nguarantees nothing gets past. A gate removes what is below a threshold, which is\nhow you silence room tone between phrases. `gain` is a plain level stage, and it\nis what an automation lane rides when a track has to move out of the way.\n\n**Nonlinear** (`saturate`, `bitcrush`) changes the waveform's shape, which adds\nharmonics that were not there. Reach for it when a track needs character or\ngrit rather than correction — and remember it is generative: it makes a thin\nsource denser, not cleaner.\n\n**Time** (`delay`, `reverb`, `chorus`, `phaser`) puts a track in a space or gives\nit width. These are the ones that most easily wreck a mix, because a tail or a\ndetuned copy occupies the same room a voice needs. Use them on the thing that\nshould sit _behind_ something else, and keep the wet amount lower than sounds\nright in isolation.\n\nThe chain is serial: each effect processes what the one before it produced. So\ncorrective filtering goes early, character in the middle, and a limiter last\nwhere it can actually act as a ceiling.\n\n## Voiceover carve\n\n**The problem it solves.** A music bed under a voice makes the voice hard to\nfollow. The reflex is to duck the whole bed, which works and costs the bed all of\nits presence — the music goes limp for the entire voiceover. But the voice does\nnot need the whole spectrum. It needs the few bands it actually occupies. Carve\ntakes only those, and the bed keeps its low end and its top, so it is still music\nwhile the voice is still intelligible.\n\n**It is a relationship, not an effect.** The settings live on the _bed_ — the\ntrack that gets processed — and they name the voices to listen to, exactly as a\nsidechain compressor does: you select the track that gets quieter and pick what\nmakes it quieter. **Never put a carve on a voice track.** A voice carved against\nitself is a bug, not a subtle mix choice.\n\n**Every voice, not one of them.** `sources` is a list, because a bed usually runs\nunder a whole sequence — a narrator, an interview answer, a second presenter. They\nare summed onto the bed's own clock before anything is measured (`mixCarveSources`),\nso one analysis covers all of them: the bands come from all the speech there is, and\nthe envelopes rise wherever any of it is happening. Voices that never play while the\nbed does are left out; they cannot mask it.\n\n**A carve against more than one clip id is wrong. Group the clips and carve\nagainst the group.** This is an invariant, not a tip. Naming clips one by one has\nto be exhaustively right and stays right only until the next edit — a fourth\nnarration clip added later plays outside the carve's awareness, and the bed\nfails to duck under it silently. Naming the group instead resolves membership at\nanalysis time, so a clip added to the group later is covered without touching\n`sources` at all:\n\n```html\n<!-- group the narration, then carve the bed against the group -->\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-outro\" data-audio-group=\"voiceover\" …></audio>\n\n<audio id=\"music\" data-fx-carve='{\"enabled\":true,\"sources\":[\"voiceover\"],\"strength\":0.8}' …></audio>\n```\n\nA `sources` list naming two or more plain clip ids instead of a group is caught\nby the `audio_carve_ungrouped_sources` lint rule — it still works, but it is the\nversion that silently rots when a clip is added.\n\n**Keep the carve group a voice group: no bed, no SFX, no music.** A group id in\n`sources` resolves to every _current_ member on _every_ analysis, so the group\nyou name is the group you get later — not the tracks that were measured when it\nwas written. Two ways that bites:\n\n- **The bed in its own source group.** It is handed to itself as a voice and\n  carved against its own content — the \"never carve a track against itself\" rule\n  arriving one re-analysis later.\n- **An SFX or music clip in the voice group.** It enters the sidechain on the\n  next analysis and the bed starts ducking under a whoosh, even though the run\n  that wrote the attribute never measured it.\n\nBoth are invisible at the moment the carve is written: the analysis sums the\nvoices it detected and never round-trips through group resolution, so the first\npass is genuinely correct and only the next one is wrong. So give each role its\nown group — `music` for the bed, `voiceover` for the narration, `sfx` for the\nhits — and keep the group named in `sources` holding nothing but voices.\n\n`carve.mjs` refuses to write the group form when it sees either case, records\nclip ids, and says on stderr which member blocked it. The\n`audio_carve_ungrouped_sources` rule then points at the arrangement instead of\nthe CLI quietly persisting a wider carve than it measured.\n\nA voice that this run left out is **not** one of these cases and does not block\nthe group form: `carve.mjs` only analyses voices that overlap the bed, and\npicking up a clip that plays later without an edit to `sources` is the whole\nreason to name the group.\n\n### One bus for many tracks\n\nMembership alone is enough to carve against, as above — but add an\n`<hf-audio-group>` element with that id and the group becomes a real submix bus:\none chain, one fader, one automation clock for every member.\n\n```html\n<hf-audio-group\n  id=\"voiceover\"\n  data-label=\"Voiceover\"\n  data-volume=\"0.9\"\n  data-fx-chain='{\"version\":1,\"nodes\":[\n    {\"type\":\"compressor\",\"id\":\"g1\",\"params\":{\"threshold\":-18,\"ratio\":3}},\n    {\"type\":\"peaking\",\"id\":\"g2\",\"params\":{\"frequency\":3000,\"gain\":2,\"q\":1}}]}'\n></hf-audio-group>\n\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>\n```\n\n**Reach for the bus when the same treatment belongs on several tracks.** Four\nnarration clips that each want the same compressor is four chains to keep in\nstep, and they drift the moment one is edited; on the bus it is one chain, and\nthe compressor sees the whole voice rather than each clip in isolation — which is\nthe point, since a compressor cannot ride a sequence it only hears a third of.\nPer-clip chains remain right for what is genuinely per-clip: one noisy take that\nneeds its own de-esser.\n\n| On the bus        | Does                                      |\n| ----------------- | ----------------------------------------- |\n| `data-fx-chain`   | one chain over the summed members         |\n| `data-automation` | envelopes on the bus, in COMPOSITION time |\n| `data-volume`     | one fader for every member (default 1)    |\n| `data-label`      | the display name; falls back to the id    |\n| `data-hidden`     | drops every member from the mix           |\n\n**Group automation is composition time, not clip time.** A bus has no\n`data-start` — members are already at their composition positions when they\nreach it — so `t: 0` in a group lane is the start of the composition, not of any\nclip. A lane on a clip is clip-local; the same numbers mean different instants on\nthe two, which is the one thing to get right when moving an envelope from a clip\nup onto its bus.\n\n**A carve stays on the clip.** `data-fx-carve` is not a group attribute. The bed\nbeing carved is a single track, and it is that track which carries\n`data-fx-carve` — pointed AT a group, per the rule above. Group and carve meet in\n`sources`, not on one element. A carve written onto a bus is half an effect\napplied twice: the level half measures the bed's own audio, which a bus has none\nof, so only the filters survive — and a bus and its members are one signal path,\nso the bed then runs through the bus's filters AND its own. The\n`audio_group_carve_attr` lint rule catches it.\n\n**One clip is not a bus.** A group exists to give several tracks one chain, one\nfader and one clock. Wrapping a single clip in a bus buys nothing the clip's own\n`data-fx-chain` does not already do, and it doubles the places a later edit has\nto land. The one reason to do it anyway: a bus's automation clock is composition\ntime, so a single-member bus is how a lane on that clip gets composition-time\ntiming.\n\n**One knob.** `strength` is 0..1 and derives everything: how deep to cut, how\nmany bands, how wide, how far to favour intelligibility over raw voice energy,\nhow far the level may drop, how far under the voice to aim. Those six move\ntogether in any real mix — a gentle carve is a shallow cut in few bands with\nlittle ducking, a hard one is deeper in more bands with more — so they are one\nrelationship written once, in `carveProfile`. `carve.mjs` defaults to `0.8` —\nsix bands from 250 Hz to 2.5 kHz cut about 7 dB each and 15 dB at 1.6 kHz, with\n19 dB of level room — because a bed under narration has to get out of the way\nfirst and be music second; `0.25` (a 6 dB dip in three bands, 6 dB of room) kept\nthe bed present but still let it fight the voice, and was judged too weak in\npractice. At `0.5` the dip reaches 10 dB, which is where a carve starts being\nheard as an effect rather than as room for the voice. Drop the strength when the\nbed is the point and the voice is sparse. `0` is spectral only — one band, no\nlevel match at all.\n\n**Carve by default — required whenever music plays under a voice.** A bed\nunder any voice track (narration, avatar speech, interview, voiceover) gets a\ncarve as part of finishing the mix, not as a polish step to get to if there is\ntime. Place both tracks, run the command below (default strength `0.8`; add\n`--bed` / `--voice` when detection picks wrong), confirm the written\n`data-fx-carve`, `data-fx-chain` and `data-automation` with `npx hyperframes check`,\nand only then render. A volume duck on its own is not a finished mix: it leaves\nthe voice and the bed fighting in the 1–3 kHz band and costs the bed all of its\npresence for the whole voiceover. Skip the carve only when there is no voice for\nthe music to sit under — a music video, a title card, a montage cut to the track.\n\n**It always follows the voice.** There is no static mode: a fixed depth thins the\nbed through every pause, and once you have heard both there is no reason to want it.\nEvery value becomes an envelope of the speech's own level — silence leaves the bed\nalone, a loud passage pushes the carve to full depth — written as ordinary automation,\nwhich is why the lanes show up in the timeline and can be edited afterwards.\n\n**Level matching is part of it.** Spectral carving cannot fix a bed that is\nsimply louder than the voice. So the carve also measures how far over the voice\nthe bed sits and writes a `gain` stage driven by an envelope. That envelope releases slowly on\npurpose — music that snaps back to full the instant a word ends sounds like a\nmachine doing it.\n\n**Running it.** In Studio the carve is one module at the top of a track's effect\nrack — voice, strength, and the analysis it produced, in one card. It is\nthere whenever another track could be the voice, and a bed with exactly **one**\ncandidate above it is carved by default, at the default strength:\nthat is what a bed under narration wants, and the module is where you change or\nswitch it off. Several candidates leaves the picker waiting rather than guessing.\nHeadless —\nwhich is the path when you are authoring a composition rather than editing one:\n\n```bash\nnode <SKILL_DIR>/scripts/carve.mjs --comp index.html\n```\n\nThat is the whole command. It finds the voice and the bed itself, carves\nat the default strength, and prints what it decided:\n\n```\nbed    music-bed (name looks like music)\nvoice  narration (only track left)\ncarve  strength 0.8, 1 voice\nbands  250Hz -7.4dB q2.06, 400Hz -7.4dB q2.06, 630Hz -7.4dB q2.06, 1000Hz -7.4dB q2.06, 1600Hz -14.8dB q2.06, 2500Hz -7.4dB q2.06\nlevel  273-point envelope, floor -19.2 dB\n```\n\nName the tracks with `--bed` / `--voice` (repeatable) when the automatic choice is\nwrong, `--strength` to push it, `--dry-run` to see that report and write nothing.\n\n**How it picks the tracks.** Names first, because that is what you already told it\nand the answer is explainable — `classifyAudioName` in core, the same classifier\nStudio's own picker uses, so the two cannot disagree. A track whose id or filename\nlooks like music (`music`, `bgm`, `bed`, `score`…) is the bed; everything else that\nplays over it and is not SFX-shaped is a voice. Audio elements are preferred: video\ncounts only when no audio track is left to be the voice, or every B-roll clip in the\ncomposition would read as somebody talking. **It refuses when it cannot tell which\ntrack is the bed** rather than carving the wrong one — typing one id is cheap.\n\nSame analysis functions as the panel, so the result is identical. Needs `ffmpeg`\non PATH and `@hyperframes/core` installed in the project (`npm i -D\n@hyperframes/core`) — the CLI inlines core rather than shipping it, so it cannot\nbe borrowed from there.\n\n**What it writes** is an ordinary chain of peaking filters plus a gain stage,\ntagged `fromCarve`. That tagging is the whole trick: a re-run replaces the\nprevious carve and leaves every effect you built by hand — and every lane you\ndrew by hand — exactly where it was. So re-carving at a new strength is safe and\nrepeatable, and `data-fx-carve` exists so the settings can be read back rather\nthan guessed from the filters.\n\n## Automation\n\nA lane is a set of breakpoints on one parameter: `{t, v}` in clip-local seconds\nand the parameter's own units. Targets are `volume` for the track's level, or\n`fx.<nodeId>.<param>` for an effect's knob.\n\n**Only some parameters can be automated, and a lane on the others is silently\ninert.** A knob is automatable when a Web Audio `AudioParam` backs it. The four\nworklet-based effects — `compressor`, `limiter`, `gate`, `bitcrush` — expose\nnone at all, so no lane on any of their parameters will ever move: to make a\ncompressor's behaviour change over time, automate a `gain` stage before it\ninstead. `references/fx-registry.md` marks every parameter.\n\n## Verify\n\nAlmost no static gate covers the mix. The linter reads `data-automation` for\nexactly one conflict — `audio_volume_double_automation`, a volume lane on a track\nthat also has a GSAP tween on `volume`, where the lane wins and the tween is\nignored — plus `audio_volume_tween_overrides_gain`, an authored `data-volume`\non a track whose `volume` is tweened, where the tween's values are absolute and\nreplace that gain instead of scaling it. Nothing validates the\nchain or the effect lanes at all. What\nenforces those is the render: a chain it cannot parse fails the whole mix rather\nthan quietly writing the dry signal, because a mix that sounds plausible and is\nwrong is worse than a refusal. Preview is the opposite by design: an unreadable\nchain plays dry so the composition stays workable.\n\nA lane pointing at a node the chain does not have is pruned on read, not an\nerror — so a typo'd `nodeId` costs you the envelope silently. Read the ids back\nout of the chain rather than assuming what was minted.\n\nEffects with a tail (`reverb`, `delay`) make the rendered track **longer** than\nits source, and the mix is told how much by the chain. So a bed with reverb no\nlonger ends exactly at its `data-duration`; that is expected, not a bug.\n\nBeyond that, a mix is verified by rendering and listening. For a carve: the voice\nshould be legible without the bed sounding hollowed, and the bed\nshould come back up between phrases rather than staying flat. If the bed sounds\nnotched rather than simply quieter under the voice, the strength is too high —\nthat is the one failure mode with an obvious sound.\n\nFile v1.0.14:_meta.json\n\n{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-audio\",\n  \"version\": \"1.0.14\",\n  \"publishedAt\": 1791608838487\n}\n\nFile v1.0.14:references/attributes.md\n\n# The three audio attributes\n\nAll three go on the `<audio>` / `<video>` element itself, JSON-encoded, so a\ncomposition carries its whole mix in the HTML with nothing to load beside it.\n`data-fx-chain` and `data-automation` also go on an `<hf-audio-group>` bus,\nwhere they mean the same thing over the summed members — with one difference\nworth knowing: a group's automation runs on COMPOSITION time, since a bus has no\n`data-start` of its own. `data-fx-carve` is clip-only; a bus has no carve.\nNothing static validates them: preview plays an unreadable chain dry to stay\nworkable, and the render refuses the whole mix rather than shipping a dry track\nthat sounds plausible and is wrong.\n\n## `data-fx-chain` — the effects\n\n```json\n{\n  \"version\": 1,\n  \"nodes\": [\n    {\n      \"type\": \"highpass\",\n      \"id\": \"n1\",\n      \"label\": \"Remove Rumble\",\n      \"params\": { \"frequency\": 120, \"q\": 0.707, \"poles\": \"2\" }\n    },\n    {\n      \"type\": \"peaking\",\n      \"id\": \"n2\",\n      \"fromCarve\": true,\n      \"params\": { \"frequency\": 1600, \"gain\": -6, \"q\": 1.4 }\n    },\n    {\n      \"type\": \"limiter\",\n      \"id\": \"n3\",\n      \"enabled\": false,\n      \"params\": { \"limit\": -1, \"attack\": 5, \"release\": 50, \"level_out\": 0 }\n    }\n  ]\n}\n```\n\n**Write these attributes double-quoted, with the JSON's own quotes as `&quot;`.**\nThe browser reads them through `getAttribute` and does not care, but\n`scripts/carve.mjs` finds them with a `name=\"...\"` regex, so a single-quoted\nattribute is invisible to it — the carve reports no existing chain and quietly\noverwrites work it could not see. `&` becomes `&amp;`; nothing else needs\nescaping.\n\n- **Order is signal order.** Each node processes what the one before produced.\n- `type` is an effect id from the registry. `params` are in the units a person\n  thinks in — dB, ms, Hz — and out-of-range values are clamped on read, so a\n  chain that parses is always safe to realise.\n- `id` is a stable handle. Automation addresses nodes by id, never by position,\n  so reordering the chain cannot re-point a lane at a different effect. A node\n  with no id loads fine but cannot be automated. Writing a chain by hand, any\n  unique string works; Studio hands out the first free `n1`, `n2`, … so matching\n  that convention keeps a hand-written chain and an edited one looking alike.\n- `label` is what the rack calls this node, replacing the effect's own name.\n  Write one whenever the node is doing a named job — a chain with two `peaking`\n  nodes otherwise shows the same row twice and the author cannot tell which is\n  the mud cut and which is the clarity lift. Presets and jobs always set it; a\n  hand-written node should too. See `presets.md` for the names they use.\n- `enabled: false` is bypass — the node stays in the chain, out of the signal\n  path. Absent means enabled.\n- `fromCarve: true` marks a node the carve analysis generated. Re-running the\n  carve replaces exactly these and leaves hand-built effects alone. **Do not set\n  it by hand**: a node tagged this way will be deleted by the next carve.\n\n## `data-automation` — the envelopes\n\n```json\n{\n  \"version\": 1,\n  \"lanes\": [\n    {\n      \"target\": \"volume\",\n      \"points\": [\n        { \"t\": 0, \"v\": 1 },\n        { \"t\": 2.5, \"v\": 0.4 }\n      ]\n    },\n    {\n      \"target\": \"fx.n2.gain\",\n      \"points\": [\n        { \"t\": 0, \"v\": 0 },\n        { \"t\": 1, \"v\": -6, \"curve\": 0.4 }\n      ]\n    }\n  ]\n}\n```\n\n- `target` is `volume` for the track's own level, or `fx.<nodeId>.<param>`.\n- `t` is **seconds from the start of the clip**, not of the composition. A bed\n  starting at `data-start=\"8\"` has `t: 0` at composition time 8.\n- `v` is in the parameter's own unit: dB for a gain, Hz for a frequency, 0..1 for\n  volume.\n- A lane holds its first value backwards to the start of its clip and its last\n  value forward to the end. So a bed that begins before the voice needs an\n  explicit \"no cut\" point at `t: 0`, or it starts out already ducked.\n- `curve` (-1..1) bends the segment _leaving_ a point: positive holds low then\n  rises late. `viaX`/`viaY` name an interior point the segment passes through\n  (progress 0..1, value travelled 0..1) and supersede `curve` when both are\n  present — that is what the timeline writes when a bend is dragged.\n- 512 points per lane, maximum.\n- A lane whose node is gone is pruned on read rather than erroring.\n\n**A lane on a non-automatable parameter is silently inert.** Automation is\ndelivered as native `AudioParam` scheduling, so a knob that no `AudioParam` backs\ncannot move: worklet processor options, a WaveShaper curve and a convolution\nimpulse are all set wholesale. `fx-registry.md` marks each parameter; the four\nworklet effects (`compressor`, `limiter`, `gate`, `bitcrush`) have none at all.\n\n## `data-fx-carve` — the carve's settings\n\n```json\n{ \"enabled\": true, \"sources\": [\"narration\", \"interview-guest\"], \"strength\": 0.35 }\n```\n\n- `sources` are the **element ids of every voice this bed makes room for**. They live\n  on the bed being processed, not on the voices. Summed onto the bed's clock before\n  the analysis, so one set of filters and envelopes covers all of them.\n- `strength` 0..1 derives the whole mechanism (see `carveProfile`).\n- There is no `dynamic`: a carve always follows the speech.\n- `enabled` is whether the carve applies. It exists because a bed with exactly one\n  candidate voice is carved by default: with \"off\" represented by an absent\n  attribute, switching it off would read as never-configured and the default would\n  put it back. `enabled: false` keeps the settings and stops the carve.\n\nThis attribute is not read at playback — the chain and lanes it produced are what\nplay. It exists so the settings can be read back and re-derived rather than\nguessed from the filters, which is what makes changing strength on an existing\ncarve possible.\n\nOlder projects may carry the six mechanism numbers (`maxCutDb`, `bands`, `q`,\n`intelligibilityBias`, `duckDb`, `headroomDb`) instead of `strength`. They still\nload: the depth maps back onto a strength and everything else is re-derived. A\nstored carve with no `enabled` reads as on, and a single `source` reads as a one-voice\n`sources` list. A stored `dynamic` is ignored — every carve follows the speech now.\n\nFile v1.0.14:references/diagnosis.md\n\n# Diagnosing audio you cannot hear\n\nThe symptom table in `SKILL.md` starts from \"it sounds boomy\". That presumes\nsomebody already listened and said so. Handed a file and \"fix this\", you have\nno such sentence — and you cannot listen. This is how to get one.\n\nIt is worth being blunt about the difficulty first, because the failure mode is\nnot \"no answer\", it is **a confident wrong answer**:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n\nEvery voice has peaks and dips of exactly the size an injected filter has.\nFormants are ±10 dB. A speaker's fundamental sits anywhere from 85 to 255 Hz.\nSentences decline 5–6 dB from start to end as a matter of ordinary prosody. Look\nat one spectrum on its own and you will find \"defects\" in all of it, and the\nones you find will be the speaker.\n\nSo diagnosis is always **comparison**. The whole method is choosing the right\nthing to compare against.\n\n---\n\n## Compare against something inside the same file\n\nRanked by how much they can tell you. Prefer the highest one available.\n\n### 1. The clean original, if it exists\n\nIf the undamaged take is on disk, this is the whole job — measure both, subtract,\nand the difference _is_ the defect. Nothing below is as good. Look for it before\nanything else.\n\n### 2. The pauses\n\nThe strongest reference that lives inside a single file. Speech stops; whatever\nis still there in the gap is not the voice.\n\n**What it answers: \"was something added?\"**\n\nAnything audible in the pauses is additive — hum, rumble, hiss, room tone. It was\nlaid on top, so it can be subtracted, and this is a reliable positive finding.\n\n**What it does NOT answer: \"was something filtered?\"** — and getting this\nbackwards is how the method produces a confident wrong answer.\n\nA filter multiplies. Applied to a file whose gaps already sit at the\nquantisation floor, it leaves them at the quantisation floor: near-silence times\nanything is still near-silence. So the pause carries no trace of it. Measured on\none take with a −9 dB shelf above 2.5 kHz applied to the whole file:\n\n|                   | 1 kHz | 5 kHz | tilt      |\n| ----------------- | ----- | ----- | --------- |\n| pause, undamaged  | −91.0 | −91.0 | +0.0      |\n| pause, shelved    | −91.0 | −91.0 | **+0.0**  |\n| speech, undamaged | −34.7 | −42.8 | −8.1      |\n| speech, shelved   | −35.4 | −48.5 | **−13.1** |\n\nThe defect is a clear 5 dB in the speech and **exactly zero** in the pause.\n\nSo: **never use a null result from the pause spectrum to rule out EQ.** A run\nthat did exactly that — measured the pause, found it smooth, and concluded\n\"static EQ of any type or Q is ruled out\" — went on to treat an inaudible\n−72 dBFS rumble as the defect and shipped a high-pass for a file whose actual\nproblem was that it had no top end.\n\nThe pause spectrum _is_ a transfer function only when the gaps carry a real\nrecorded noise floor that passed through the same filter. A room-tone bed does;\na digitally clean take does not. Check which you have before trusting it: if the\ngaps are within a few dB of the quantisation floor, this reference can find\nadditive content and nothing else.\n\n### 3. The speech's own tilt, for a suspected filter\n\nWhen the pause cannot see a filter (above), the only thing left carrying it is\nthe speech. Read the tilt across a few 1/3-octave bands rather than any single\none — `1k / 3.2k / 5k / 7k` is enough to see a shelf:\n\n```bash\nfor f in 1000 3200 5000 7000; do third voice.wav $f; done\n```\n\nSpeech falls away steadily above about 1 kHz, so a downward slope is expected;\nwhat you are looking for is a slope that keeps steepening, or a step. In the\ntable above, −8.1 dB from 1 k to 5 k is an ordinary voice and −13.1 dB is the\nsame voice with 9 dB taken off the top.\n\n**This is a candidate, not a verdict.** Where the ordinary slope ends and a\ndefect begins is speaker-dependent, and you have no baseline for this speaker.\nSay what you measured and what it would mean, and let somebody hear it.\n\n### 4. The file against itself over time\n\nFor anything level-related, compare each passage to the track's own median rather\nthan to a target. That is what `levellingResult` does, and it is why an already\neven track comes back untouched.\n\n---\n\n## Do not compare against a different voice\n\nBoth wrong answers in the evaluation that produced this page came from an\nexternal reference, and both were argued rigorously from bad ground:\n\n- **A published average spectrum** (LTASS and friends). One run concluded\n  \"+10 dB above 7 kHz, split-half stable, gating-independent\" on a file whose\n  actual defect was +6.6 dB at 200 Hz. Its supporting claim — 10 kHz sitting\n  6.2 dB above 6.3 kHz — measured 0.6 dB on re-check, and measured the same in\n  the clean original. Published curves are mixed-sex, mixed-corpus, and\n  mixed-microphone; the gap between them and any one speaker is larger than most\n  defects.\n- **A synthesised control voice** (`say`, a TTS take, another narrator). One run\n  generated a control this way, found the spectrum \"normal\", and missed a −6.9 dB\n  shelf. Two speakers differ by more than 7 dB across the top octaves as a matter\n  of course, so a cross-voice comparison cannot resolve a defect that size.\n\nIf neither the original nor usable pauses exist — continuous speech, or gaps that\nare digital silence and so carry no channel — then a static tonal defect is\n**genuinely under-determined**.\n\nReport that. It is a finding, not a failure to find one, and it is the correct\nanswer rather than the fallback when the better methods are unavailable. Give\nthe author the two or three readings that fit and ask which they hear; they can\nlisten, and that one sentence from them collapses the whole problem.\n\n**This is the point where a capable agent goes wrong.** Told a thing is\nunder-determined, the instinct is to invent a cleverer measurement and escape\nit — and something will always be found, because a single voice's spectrum is\nfull of peaks and valleys that survive any amount of statistical rigour. An\nelaborate novel method reaching a confident conclusion, on a file where the two\nreliable references were both unavailable, is the _signature_ of this failure,\nnot evidence against it. If you notice yourself building one, stop and report\nthe ambiguity instead.\n\n---\n\n## Recipes\n\n### Compare loudness from the bytes the listener actually hears\n\nDo not call two clips equally loud because their Studio faders, waveform peaks,\nor cached asset metadata match. Those are controls and proxies, not a loudness\nmeasurement. Resolve the exact URLs used by preview/render, download or inspect\nthose exact served bytes, and measure each decoded stream with FFmpeg's\n`ebur128` filter. Compare the integrated LUFS values.\n\nFor a target loudness, the required move is:\n\n```text\ngain_db = target_lufs - measured_lufs\nlinear_gain = 10 ** (gain_db / 20)\n```\n\nWhen both clips are local authored `<audio>` elements with stable ids, use the\nCLI instead of transcribing that arithmetic by hand:\n\n```bash\nnpx hyperframes normalize-audio --reference target-audio --target user-audio\nnpx hyperframes normalize-audio --reference target-audio --target user-audio --write\n```\n\nThe first command is a dry run. The second writes only the target's\n`data-volume`, after accounting for both existing gains and refusing a boost\nthat would clip or exceed Studio's ceiling. Always choose the reference from the\nauthor's stated intent; the command does not guess which clip should define the\nmix.\n\nStudio's clip-gain fader uses `0 dB` / linear gain `1` at its physical midpoint\nand provides up to `+12 dB` on the upper half. After changing gain, measure the\nserved preview/render bytes again. If a listener still hears a mismatch, trust\nthe report and first verify the asset URL and bytes are current; do not explain\nit away with matching peaks or a stale proxy measurement.\n\nAll verified with ffmpeg 8.1.1. `-hide_banner` keeps the output readable;\n`volumedetect` prints to stderr, so do not silence it with `-v error`.\n\n### Band energy, in proportional bands\n\n**Use proportional bandwidths or the numbers lie.** A fixed 2000 Hz-wide band at\n10 kHz collects more energy than a 1200 Hz-wide band at 6.3 kHz for no reason but\nits width, which manufactures a high-frequency excess that is not there. One\nthird of an octave is `f × 0.2316`.\n\n```bash\nthird() {\n  w=$(python3 -c \"print(round($2*0.2316))\")\n  ffmpeg -hide_banner -i \"$1\" -af \"bandpass=f=$2:width_type=h:w=$w,volumedetect\" \\\n    -f null - 2>&1 | grep -m1 mean_volume\n}\nthird voice.wav 200     # weight / boom\nthird voice.wav 3200    # presence / harshness\n```\n\nRead them as a shape across 100 / 200 / 400 / 1k / 3.2k / 7k, and read the shape\nagainst a reference from the list above — never on its own.\n\n### The noise floor, and what is in it\n\n```bash\nffmpeg -hide_banner -i voice.wav -af astats=metadata=1 -f null - 2>&1 | grep -i 'noise floor'\n```\n\n`-inf` means digital silence in the gaps: no additive noise, so rumble, hiss and\nroom tone are all ruled out in one command. A real number is the level of\nwhatever is sitting under the voice. To see its _shape_, cut a pause out with\n`-ss`/`-t` and run the band recipe on that slice alone.\n\n### Level over time\n\n```bash\nffmpeg -hide_banner -i voice.wav -af ebur128=framelog=quiet -f null - 2>&1 | tail -6\n```\n\nLRA under ~3 LU is even. Then window it, because LRA hides a single sagging\npassage:\n\n```bash\nfor s in 0 1.2 2.4 3.6 4.8 6.0; do\n  ffmpeg -hide_banner -ss $s -t 1.2 -i voice.wav -af volumedetect -f null - 2>&1 |\n    grep -m1 mean_volume\ndone\n```\n\n**A 4–6 dB spread across windows is normal speech**, not a defect — sentences\ndecline as they end. Injected unevenness looks like 12 dB or more. Levelling a\ntrack that only has declination flattens the prosody and is heard as robotic.\n\n### Pitch, before blaming the low end\n\n```bash\nffmpeg -hide_banner -i voice.wav -af \"lowpass=f=400,astats=metadata=1\" -f null - 2>&1 | grep -i 'peak level'\n```\n\nA voice has no energy below its own fundamental, so a \"missing\" 100 Hz on a\nspeaker whose F0 is 210 Hz is the speaker, not a rolloff.\n\nThe same fact runs the other way, and that direction is the trap: **a boost near\nthe fundamental is indistinguishable from that voice being naturally chesty.**\nBoth look like energy at F0, because both are.\n\nSo the rule is symmetric, and the dangerous half is the second one:\n\n- Do not call a peak at F0 a defect on its own evidence.\n- **Do not dismiss one either.** \"The peak is at 200 Hz, F0 is 185 Hz, therefore\n  it is the fundamental\" is not a diagnosis — it is the same observation\n  restated, and it discards the one candidate most likely to be real. Boominess\n  _is_ excess energy at the bottom of a voice; that is what the word means.\n\nWhat you can do is measure how much, against the same file's midrange:\n\n```bash\nthird voice.wav 200      # or the nearest 1/3-octave band to F0\nthird voice.wav 1000\n```\n\nIn an ordinary take these land within a couple of dB of each other. A low band\nsitting **more than about 4 dB above the 1 kHz band** is a strong boom or mud\ncandidate. Measured across one voice damaged several ways: undamaged +0.9,\nharsh +0.6, dull +2.0; boomy +6.7, muddy +5.8. Treat the figure as indicative\nrather than a threshold — it is one speaker — but the separation is wide, and a\nreading up at +6 is worth raising even when you cannot explain it.\n\nIt still cannot tell you whether a filter did that or the speaker did, so report\nit as a candidate. That is the whole answer here: measure it, name it, hand the\nchoice to somebody who can hear it.\n\n---\n\n## Then, and only then, the symptom table\n\nMeasurement gives you the band and the kind. `SKILL.md`'s table and\n`presets.md`'s fuller one turn that into a fix. Going the other way round —\npicking a plausible fix and finding evidence for it — is how both wrong answers\nin the evaluation happened, and both were long, careful and confident.\n\nOne habit that catches it: before applying anything, state what you would expect\nto measure **if you are wrong**, and check that too.\n\nFile v1.0.14:references/fx-registry.md\n\n# Effect registry\n\nEvery effect, its parameters and the usable range of each. Values outside a range\nare clamped on read, so anything that parses is safe to realise. **AUTO** marks a\nparameter an automation lane can drive; anything unmarked cannot move over time\n(see the note at the bottom).\n\nGenerated from `HF_AUDIO_FX` in `@hyperframes/core/audio-fx`, which is the source\nof truth — if this table and the code disagree, the code is right.\n\n## Filter — which frequencies a track may occupy\n\n| Effect      | Parameter                                                                                                   |\n| ----------- | ----------------------------------------------------------------------------------------------------------- |\n| `highpass`  | `frequency` 20–20000 Hz (300, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)       |\n| `lowpass`   | `frequency` 100–20000 Hz (8000, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)     |\n| `peaking`   | `frequency` 20–20000 Hz (1000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** · `q` 0.1–20 (1, log) **AUTO** |\n| `lowshelf`  | `frequency` 20–2000 Hz (200, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                                  |\n| `highshelf` | `frequency` 500–20000 Hz (4000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                               |\n\n`q` is bandwidth — higher is narrower. `poles` is the slope: `2` is the usual\nbiquad (12 dB/oct), `1` is gentler (6 dB/oct). Shelving filters have no `q`: the\nWeb Audio spec leaves it unused for them, so a control would have moved nothing.\n\n## Dynamics — how level behaves over time\n\n| Effect       | Parameter                                                                                                                                                                      |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `gain`       | `gain` −60–12 dB (0) **AUTO**                                                                                                                                                  |\n| `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) |\n| `limiter`    | `limit` −24–0 dB (−1) · `attack` 0.1–80 ms (5) · `release` 1–8000 ms (50, log) · `level_out` −24–24 dB (0)                                                                     |\n| `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)               |\n\nCuts on `gain` go to −60 dB, boosts stop at +12: it is a level stage for making\nroom, and a chain that could add 40 dB would clip long before that was useful.\n`knee` of 1 is a hard corner, higher eases into it. `mix` below 1 blends the dry\nsignal back in (parallel compression). `range` is how far down the gate pulls\nwhen closed — a gate that pulls all the way to silence sounds like a switch.\n\n## Nonlinear — changes the waveform's shape\n\n| Effect     | Parameter                                                                                                                                                                  |\n| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `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) |\n| `bitcrush` | `bits` 1–32 (8) · `samples` 1–250× (1) · `mix` 0–1 (1)                                                                                                                     |\n\n`tanh` is the gentlest curve and `hard` is outright clipping. Higher `oversample`\ncosts more CPU and keeps aliasing down. `samples` repeats each sample N times — a\ncrude downsample, which is where the lo-fi character comes from.\n\n## Time — space and width\n\n| Effect   | Parameter                                                                                                                                                           |\n| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `delay`  | `time` 1–5000 ms (250, log) **AUTO** · `feedback` 0.01–0.95 (0.35) **AUTO** · `mix` 0–1 (0.4) **AUTO**                                                              |\n| `reverb` | `size` 0.05–1 (0.7) · `damping` 0–1 (0.5) · `wet` 0–1 (0.35) **AUTO** · `dry` 0–1 (0.7) **AUTO**                                                                    |\n| `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**                                           |\n| `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) |\n\nReverb convolves a _generated_ impulse, and both preview and render generate the\nsame one — so a room is reproducible without shipping an impulse file. Higher\n`damping` rolls the top off the tail faster, which is what makes a large room\nsound like a soft one. `feedback` near the top of its range is a very long tail;\nit is bounded below 1 because at 1 it never decays.\n\n## Why some parameters cannot be automated\n\nAutomation is handed to the audio thread once, as native `AudioParam` ramps and\ncurves, which is what keeps it sample-accurate and identical between preview and\nrender. A parameter can therefore only be automated if an `AudioParam` backs it.\nThree kinds do not:\n\n- **worklet processor options** — `compressor`, `limiter`, `gate` and `bitcrush`\n  are AudioWorklets configured wholesale, so **none of their parameters are\n  automatable at all**.\n- **a WaveShaper curve** — `saturate`'s `type`, `threshold` and `oversample`\n  rebuild the curve; only its `output` stage is a real param.\n- **a convolution impulse** — `reverb`'s `size` and `damping` regenerate the\n  impulse; `wet`/`dry` are gain stages and automate fine.\n\nTo make one of those behave differently over time, automate a `gain` stage\naround it instead: a lane on a `gain` before a compressor changes how hard the\ncompressor is driven, which is most of what automating its threshold would have\ndone.\n\nFile v1.0.14:references/presets.md\n\n# Presets, jobs and one-knob profiles\n\nEverything here is a shortcut to a chain you could have built by hand. A preset\nwrites ordinary nodes tagged with `fromPreset`, a job writes one ordinary node\nwith a name, and a profile is one control over several parameters of one effect.\nNothing is opaque: open any of them and you find effects from\n[`fx-registry.md`](./fx-registry.md) with their parameters showing.\n\nReach for one when it names the problem you actually have. Build by hand when\nnone of them does — a preset applied because it was nearby is worse than three\ndeliberate nodes.\n\n---\n\n## Diagnose first: what to listen for, and what fixes it\n\nWork from the symptom, not from the effect list. Most bad audio is one or two of\nthese, and the fix is usually a job rather than a whole preset.\n\n| It sounds like                                | Where it lives | Reach for                                                    |\n| --------------------------------------------- | -------------- | ------------------------------------------------------------ |\n| Hum, rumble, traffic, footsteps, handling     | 20–80 Hz       | `rumble-cut` preset, or a `highpass` at 80 Hz                |\n| Boomy, chesty, too close to the mic           | 80–250 Hz      | **Tame Boominess** job (200 Hz, −4 dB)                       |\n| Muffled, like it is behind cardboard          | 250–600 Hz     | **Reduce Mud** job (250 Hz, −3 dB)                           |\n| Boxy, like a small room                       | ~400 Hz        | **Reduce Boxiness** job (400 Hz, −3 dB)                      |\n| Words hard to make out, sits behind the music | 2–5 kHz        | **Add Clarity** job (3 kHz, +2.5 dB), or carve the bed       |\n| Harsh, brittle, tiring over a whole listen    | 3–5 kHz        | **Soften Harshness** job (3.2 kHz, −3 dB)                    |\n| Sibilant — `s` sounds spitting                | 5–10 kHz       | Nothing shipped does this properly; see \"Not covered\" below  |\n| Dull, closed-in, lifeless                     | 10–20 kHz      | `highshelf` lift, or `voice-broadcast` which includes one    |\n| Some words much louder than others            | not a band     | **Evenness** profile on a `compressor`, or `levellingResult` |\n| Room tone audible between sentences           | not a band     | `room-gate` preset (**Tightness** profile)                   |\n| Peaks clipping or spiking                     | not a band     | `limiter` last in the chain — every voice preset ends in one |\n| Voice and music fighting each other           | 1–3 kHz mostly | **Voiceover carve**, not an EQ on either track               |\n| Dry, stuck to the speaker, recorded nowhere   | not a band     | `room-tight` or `room-natural`                               |\n\n**The band vocabulary** these map onto — the same names the rack shows:\n\n| Range          | Name     | What lives there             |\n| -------------- | -------- | ---------------------------- |\n| 20–80 Hz       | Rumble   | traffic, footsteps, handling |\n| 80–250 Hz      | Weight   | chest, body, warmth          |\n| 250–600 Hz     | Mud      | boxy, muffled, cardboard     |\n| 600–2000 Hz    | Middle   | the body of a voice          |\n| 2000–5000 Hz   | Presence | consonants, intelligibility  |\n| 5000–10000 Hz  | Edge     | sibilance, harshness         |\n| 10000–20000 Hz | Air      | sparkle, openness            |\n\n### Order of operations\n\nDiagnose in this order, because each step changes what the next one hears:\n\n1. **Subtract before you add.** Cut rumble and mud first. A voice that sounds\n   dull often has too much low-mid, not too little top — lifting the top of a\n   muddy voice makes it muddy _and_ harsh.\n2. **Level after you filter.** A compressor reacts to whatever is loudest, and\n   a rumble it can no longer see is a rumble it stops chasing.\n3. **Relationships after level.** Carve a bed against a voice once the voice\n   itself is settled, or the analysis measures a problem you are about to fix.\n4. **Character, then ceiling.** Saturation and space go late; a `limiter` goes\n   last, where it can actually act as a ceiling. Anything after it is not\n   bounded by it.\n\n---\n\n## Presets\n\nFour families, listed in full below. Apply one and it **appends** — stacking a character preset\nonto an already-cleaned voice is a real thing to want. Re-applying one that is\nalready present replaces its own nodes in place, because position in the chain\nis signal order.\n\n### Voice — make a real voice sound like its better self\n\n| Preset            | Answers                         | Chain                                                                                               |\n| ----------------- | ------------------------------- | --------------------------------------------------------------------------------------------------- |\n| `voice-clean`     | \"My voice sounds amateur\"       | Remove Rumble → Reduce Mud → Even Out Loudness → Add Clarity → Peak Ceiling                         |\n| `voice-broadcast` | \"I want it to sound like radio\" | Remove Rumble → Reduce Boxiness → Even Out Loudness → Add Clarity → Add Air → Warmth → Peak Ceiling |\n| `voice-warm`      | \"I want it intimate and close\"  | Remove Rumble → Add Weight → Even Out Loudness → Add Clarity → Peak Ceiling                         |\n\n`voice-clean` is the default answer to \"fix this voiceover\". The other two are\nthe same idea pushed in one direction: broadcast is denser and more forward,\nwarm has body added rather than cut.\n\n### Repair — one problem, one node\n\n| Preset       | Answers                                 | Does                                                                        |\n| ------------ | --------------------------------------- | --------------------------------------------------------------------------- |\n| `rumble-cut` | \"There's a hum or thump underneath\"     | High-pass under the voice                                                   |\n| `room-gate`  | \"I can hear the room between sentences\" | Closes the pauses. **Does not remove noise** — room tone under speech stays |\n| `boom-tame`  | \"My voice sounds boomy\"                 | Cuts the chestiness of a too-close mic                                      |\n| `harsh-tame` | \"It's harsh and tiring to listen to\"    | Rounds a brittle upper-mid, broad and always-on                             |\n\n### Character — deliberate, not corrective\n\n`telephone`, `radio-am`, `megaphone`, `lofi-tape`, `pa-system` (Tannoy),\n`intercom`, `doofus-worble`.\n\nThese are costumes. Each is a band restriction plus a resonance plus its own kind\nof dirt, and they are tuned to be distinguishable from one another — measured on\na log sweep, no two sit closer than the signal itself. Do not stack two.\n\n### Space — put it somewhere\n\n`room-tight` (presence without wash), `room-natural` (recorded somewhere rather\nthan nowhere), `hall` (far back and big), `slap-echo` (one quick repeat),\n`dub-throw` (repeats trailing well behind).\n\nUse these on whatever should sit _behind_ something else, and keep the wet amount\nlower than sounds right in isolation — a tail occupies the room a voice needs.\n\n### The whole preset as one control\n\nA preset's nodes are wrapped in a wet/dry blend, so `presetAmount` (0..1) fades\nthe entire thing in or out, and `fx.preset.<id>` is an automation target that\nramps it over time. This is the only way to automate a preset as a unit: its\nnodes share no common parameter, and worklet effects (compressor, limiter, gate,\nbitcrush) expose no automatable parameters at all.\n\n---\n\n## Jobs — the range IS the module\n\nFive named peaking filters with the frequency already chosen. Picking the job is\npicking the range, which is what makes a single \"how much\" knob honest.\n\n| Job              | Symptom                              | Sets                  |\n| ---------------- | ------------------------------------ | --------------------- |\n| Tame Boominess   | Too much chest — it booms            | 200 Hz, −4 dB, Q 1.4  |\n| Reduce Mud       | Muffled, like it is behind cardboard | 250 Hz, −3 dB, Q 1.2  |\n| Reduce Boxiness  | Sounds like a small room, or a box   | 400 Hz, −3 dB, Q 1.4  |\n| Add Clarity      | Words are hard to make out           | 3 kHz, +2.5 dB, Q 1   |\n| Soften Harshness | Harsh and tiring to listen to        | 3.2 kHz, −3 dB, Q 1.6 |\n\nEach is an ordinary `peaking` node underneath — the frequency is a starting\npoint, not a cage. Prefer a job to a bare `peaking` when one matches: it arrives\nalready aimed, and the rack names it for the work rather than the mechanism.\n\nWriting one by hand, **carry the name in `label`** — `{\"type\":\"peaking\",\"id\":\"n2\",\n\"label\":\"Reduce Mud\",\"params\":{\"frequency\":250,\"gain\":-3,\"q\":1.2}}`. The\nparameters alone are not the job. A chain with three unlabelled `peaking` nodes\nshows the author three identical rows, which is the exact problem jobs exist to\ndissolve.\n\n**Every job also ships inside a preset, at identical settings** — that is where\nthe five came from. `boom-tame` _is_ Tame Boominess; `harsh-tame` _is_ Soften\nHarshness; `voice-clean` contains Reduce Mud and Add Clarity; `voice-broadcast`\ncontains Reduce Boxiness. So check what a preset already contains before adding\na job on top of it, or the cut lands twice — `voice-clean` plus a Reduce Mud job\nis −6 dB at 250 Hz where −3 was meant. The rack shows the contained nodes by\nname once the preset is expanded, which is the fastest way to see it.\n\n---\n\n## One-knob profiles\n\nFive effects have no single parameter that can honestly be their face — a\ncompressor's threshold means nothing without its ratio. They get a derived\ncontrol instead, 0..1, which sets several parameters together.\n\n| Effect       | Knob      | 0 → 1                                      | Sets                                      |\n| ------------ | --------- | ------------------------------------------ | ----------------------------------------- |\n| `compressor` | Evenness  | Barely touched → Very even, quite squashed | threshold, ratio, attack, release, makeup |\n| `gate`       | Tightness | Only true silence → Cuts quiet words too   | threshold, range, release                 |\n| `saturate`   | Warmth    | Just a sheen → Openly distorted            | threshold, output                         |\n| `reverb`     | Space     | A small tight room → A big open hall       | size, wet, dry                            |\n| `bitcrush`   | Crush     | Slightly gritty → Destroyed                | bits, samples, mix                        |\n\n**Evenness, Warmth and Space are level-matched** — the make-up gain, the output\ntrim and the dry leg move with the drive, so turning the knob up does not also\nturn the track up or down. Those figures were solved by measurement, not chosen:\nthe compressor originally left a track 2.5 dB _quieter_ at full evenness, and\nsaturation's trim ran the wrong way entirely.\n\nTightness and Crush are not level-matched, because neither has a trim to move —\na gate only removes, and Crush's `mix` is the effect itself rather than a\nmake-up.\n\nThe chain stores the mechanism values, not the knob position; the knob is read\nback by inverting the curve. So hand-editing a parameter under a profile is\nallowed and will simply move the knob.\n\n---\n\n## Measuring scripts, not presets\n\nTwo things measure the audio before they act, so they cannot be a fixed chain:\n\n- **Voiceover carve** — analyses the voice and cuts the bed in the bands the\n  voice occupies. The answer to \"the music is fighting the voice\". See the\n  carve section in `SKILL.md`.\n- **Even Out Levels** (`levellingResult`) — measures the track's own speaking\n  windows and writes a gain envelope. Its target is the 80th percentile of that\n  track, not an absolute level, so an already-even track is left alone. Use it\n  over a compressor when the problem is passages drifting over a whole take\n  rather than word-to-word dynamics.\n\n---\n\n## Not covered by anything shipped\n\nName the gap rather than reaching for the nearest preset and calling it the\nthing — but then **ship the honest fallback anyway**, with its cost stated. An\nauthor who asked for a fix and got only an explanation has been told something\ntrue and handed nothing. Say what it is, say what it costs, apply it.\n\n- **De-essing.** `harsh-tame` is a broad always-on cut centred a band too low,\n  not a de-esser. A real one needs a detector faster than the analysis hop\n  available here. _Fallback:_ a narrow `peaking` cut in the Edge band — sweep\n  5–9 kHz to find where this voice actually spits, Q 3–4, −3 to −5 dB. It is\n  always on, so it costs a little air on every word; that trade is usually worth\n  it and is the author's to reject.\n- **Tone matching** one track to another. _Fallback:_ the Tone EQ by hand, which\n  is predictable in a way a match curve derived from two takes would not be.\n- **Noise removal.** `room-gate` closes the gaps; the noise under speech is\n  untouched. There is no fallback for hiss beneath the words — a source with\n  audible hiss needs a better source, and saying so is the whole answer.\n\nFile v1.0.14:skill-card.md\n\n## Description:\n\nHelps agents mix audio already placed in HyperFrames compositions using fades, track levels, effects, automation, submix buses, and voiceover carving.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[heygen-com](https://clawhub.ai/user/heygen-com)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nCreators and developers use this skill to diagnose and improve the mix of audio tracks in existing HyperFrames compositions, including voiceover clarity, fades, effects, and automation. It does not source audio or arrange clips.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The carve command can modify composition files.\n\nMitigation: Run it only on compositions you intend to edit, and use --dry-run to review proposed changes before writing.\n\nRisk: Installing an unpinned HyperFrames package can introduce unreviewed dependency changes.\n\nMitigation: Prefer pinned package versions or a reviewed project lockfile before running setup commands.\n\n## Reference(s):\n\n- [HyperFrames Audio on ClawHub](https://clawhub.ai/heygen-com/skills/hyperframes-audio)\n- [Audio attributes](references/attributes.md)\n- [Audio diagnosis](references/diagnosis.md)\n- [Effects registry](references/fx-registry.md)\n- [Presets and mixing recipes](references/presets.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Code, Shell commands, Configuration instructions]\n\n**Output Format:** [Markdown with HTML attribute examples, JSON configurations, and shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Can propose or apply changes to composition files using the bundled carve command.]\n\n## Skill Version(s):\n\n1.0.14 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.13: 9 files, 42232 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24261b), scripts/carve.test.mjs (11818b), skill-card.md (2084b), SKILL.md (25614b), _meta.json (137b)\n\nFile v1.0.13:SKILL.md\n\n---\nname: hyperframes-audio\ndescription: >\n  Use when audio already placed in a HyperFrames composition needs to be mixed:\n  fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking,\n  a music bed that fights a voiceover (voiceover carve), effects on a track\n  (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser,\n  bitcrush), automation envelopes drawn on a track's volume or any effect\n  parameter, or one submix bus carrying a chain, a fader and an automation clock\n  for several tracks at once (`<hf-audio-group>`).\n  Don't use for sourcing or generating audio — finding BGM, SFX, or making a\n  voiceover is `/media-use`. Don't use for clip timing or track layout, which is\n  `/hyperframes-core`.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Audio\n\nA mix is a set of relationships, not a stack of processors. Two tracks that each\nsound right alone can be unlistenable together, and the fix is almost never \"turn\none down\" — it is finding what they are fighting over and giving it to whichever\none needs it. Every tool here exists to express one of those relationships.\n\nEffects live on the element as `data-fx-chain`, and preview and render run the\nsame Web Audio graph — the studio in a live context, the engine in an offline one\ninside the browser it already drives. There is one implementation of each effect,\nso what you hear while scrubbing is what gets written. You never tune twice.\n\nClip timing remains `/hyperframes-core`: audio/video trims and source ranges use\n`data-start`, `data-duration`, and `data-media-start`, and crossfades overlap\nclips on different tracks. This skill owns placed-track fade-in/fade-out,\ncrossfade envelopes, track gain/track volume, volume and effect automation,\nducking/voiceover carve, and the effect chain. `/media-use` owns sourcing,\ngeneration, and preprocessing.\n\nConstant `data-playback-rate` (`0.1..10`) is render-safe for picture and\npitch-preserved sound when matching audio/video elements use the same timing,\nsource offset, and rate. A speed ramp is a `rate` lane in `data-automation`\n(see `docs/reference/speed-ramps`); it wins over the constant and keeps pitch\nin preview and render. HyperFrames does not\nprovide automatic waveform sync or drift correction.\nFor copyable cut/crossfade/retime recipes, use `/hyperframes-core` → `references/creator-editing-recipes.md`.\n\nThree attributes carry everything, on the audio/video element itself — or, for\nthe first two, on an `<hf-audio-group>` bus (see \"One bus for many tracks\"):\n\n| Attribute         | Holds                                                     |\n| ----------------- | --------------------------------------------------------- |\n| `data-fx-chain`   | the effects, in signal order                              |\n| `data-automation` | envelopes on this track's volume or its effect parameters |\n| `data-fx-carve`   | the carve's own settings, so it can be re-derived         |\n\nThe shipped effect families are gain, EQ (highpass, lowpass, peaking, shelves),\ncompressor, limiter, gate, saturate, delay, reverb, chorus, phaser, and bitcrush.\n\nExact JSON for each, and the rules a lane must satisfy: `references/attributes.md`.\nEvery effect with its parameters, ranges and units: `references/fx-registry.md`.\nHow to work out what is wrong with a file you cannot hear:\n`references/diagnosis.md`.\n**Presets, named jobs and one-knob profiles, plus a symptom-to-fix table:\n`references/presets.md`** — read that before hand-building a chain, because one\nof the presets or named jobs usually already names the problem.\n\n## How it fits together\n\nTwo authoring surfaces write those attributes; two runtimes read them through the\nsame builders. That shared middle is why preview predicts the render.\n\n```mermaid\nflowchart TB\n  voice[\"voice track<br/>media file\"]\n  bed[\"music bed<br/>media file\"]\n\n  subgraph AUTHOR[\"Authoring — the only things that write attributes\"]\n    panel[\"Studio<br/>Voiceover carve control\"]\n    script[\"scripts/carve.mjs<br/>detects the pair\"]\n    analysis[\"core/audioCarve.ts<br/>carveProfile · analyseCarveBands<br/>analyseCarveDuck · analyseCarveDynamics\"]\n    panel --> analysis\n    script --> analysis\n  end\n\n  voice --> analysis\n  bed --> analysis\n\n  subgraph ATTRS[\"Written onto the bed element\"]\n    carveAttr[\"data-fx-carve<br/>sources · strength\"]\n    chainAttr[\"data-fx-chain<br/>peaking xN + gain, tagged fromCarve\"]\n    autoAttr[\"data-automation<br/>a lane per carved parameter\"]\n  end\n\n  analysis --> carveAttr\n  analysis --> chainAttr\n  analysis --> autoAttr\n\n  subgraph SHARED[\"One implementation, read by both\"]\n    build[\"audioFxGraph.ts · buildFxChain\"]\n    sched[\"audioFxAutomation.ts · scheduleChainAutomation\"]\n  end\n\n  chainAttr --> build\n  autoAttr --> sched\n\n  build --> preview[\"Preview<br/>live AudioContext<br/>attachElementFxChain\"]\n  sched --> preview\n  build --> render[\"Render<br/>OfflineAudioContext in the headless browser<br/>applyAudioFxChain\"]\n  sched --> render\n\n  preview --> heard[\"what you hear while scrubbing\"]\n  render --> wav[\"processed WAV<br/>+ chainTailSeconds so the mix lets the tail through\"]\n  wav --> mix[\"engine · audioMixer<br/>volume lane baked into the PCM here, not in the graph\"]\n  mix --> out[\"the rendered mix\"]\n\n  edit[\"editing the attribute mid-playback\"] -.->|MutationObserver| preview\n```\n\nThe carve's own settings are never read at playback — the chain and lanes it\nproduced are what play. `data-fx-carve` exists so strength can be changed on an\nexisting carve instead of guessed back out of the filters.\n\nInside a carved bed the signal runs through the dips first, then the level match,\nthen anything you built yourself — which is why a limiter you add still acts as\nthe last ceiling:\n\n```mermaid\nflowchart LR\n  src[\"decoded bed\"] --> p1[\"peaking<br/>400 Hz\"]\n  p1 --> p2[\"peaking<br/>1 kHz\"]\n  p2 --> p3[\"peaking<br/>1.6 kHz\"]\n  p3 --> g[\"gain<br/>level match\"]\n  g --> hand[\"your own effects<br/>e.g. limiter\"]\n  hand --> dest[\"track gain, then out\"]\n\n  l1[\"lane fx.n1.gain\"] -.->|\"envelope of the voice's<br/>level in that band\"| p1\n  l4[\"lane fx.n4.gain\"] -.->|\"how far the bed<br/>ducks overall\"| g\n```\n\n## First, work out what is wrong\n\nThe table below starts from \"it sounds boomy\" — which presumes somebody already\nlistened and said so. Handed a file and \"fix this\", you have no such sentence\nand you cannot listen, so you have to measure. One rule governs all of it:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n> Formants are ±10 dB, fundamentals run 85–255 Hz, and sentences decline 5–6 dB\n> as they end. Every one of those reads as a defect on its own, and every one of\n> them is the speaker.\n\nSo compare, and compare against something **inside the same file**: the clean\noriginal if it exists, otherwise the pauses — whatever is audible in a gap is\nadditive, and the gap's spectrum is the channel rather than the voice. Comparing\nagainst a published average spectrum or a synthesised control voice does not\nwork: two speakers differ by more than most defects, and both wrong answers in\nthe evaluation behind this guidance came from exactly that.\n\nWhen there is no original and no usable silence, a static tonal defect is\ngenuinely under-determined. Say so and offer the readings that fit, rather than\npicking one and building a chain on it.\n\nCommands, traps and worked recipes: **`references/diagnosis.md`**. Read it\nbefore diagn\n\nArchive v1.0.12: 9 files, 42304 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24261b), scripts/carve.test.mjs (11818b), skill-card.md (1954b), SKILL.md (25822b), _meta.json (137b)\n\nArchive v1.0.11: 9 files, 42374 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24261b), scripts/carve.test.mjs (11818b), skill-card.md (2369b), SKILL.md (25578b), _meta.json (137b)\n\nArchive v1.0.10: 9 files, 42349 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24261b), scripts/carve.test.mjs (11818b), skill-card.md (2378b), SKILL.md (25538b), _meta.json (137b)\n\nArchive v1.0.9: 9 files, 41855 bytes\n\nFiles: references/attributes.md (6216b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24102b), scripts/carve.test.mjs (11278b), skill-card.md (2556b), SKILL.md (24734b), _meta.json (136b)\n\nArchive v1.0.8: 9 files, 40472 bytes\n\nFiles: references/attributes.md (5905b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (24102b), scripts/carve.test.mjs (11278b), skill-card.md (2438b), SKILL.md (21491b), _meta.json (136b)\n\nArchive v1.0.7: 8 files, 32899 bytes\n\nFiles: references/attributes.md (5905b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (15319b), skill-card.md (2473b), SKILL.md (19890b), _meta.json (136b)\n\nArchive v1.0.6: 8 files, 32291 bytes\n\nFiles: references/attributes.md (5905b), references/diagnosis.md (12080b), references/fx-registry.md (6928b), references/presets.md (13155b), scripts/carve.mjs (15319b), skill-card.md (2067b), SKILL.md (18804b), _meta.json (136b)","readmeExcerpt":"Skill: hyperframes-audio Owner: heygen-com Summary: Use when audio already placed in a HyperFrames composition needs to be mixed: fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking, a music bed that fights a voiceover (voiceover carve), effects on a track (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser, bitcrush), automation envelopes drawn on a track's volume or a","codeSnippets":[],"executableExamples":[{"language":"mermaid","snippet":"flowchart TB\n  voice[\"voice track<br/>media file\"]\n  bed[\"music bed<br/>media file\"]\n\n  subgraph AUTHOR[\"Authoring — the only things that write attributes\"]\n    panel[\"Studio<br/>Voiceover carve control\"]\n    script[\"scripts/carve.mjs<br/>detects the pair\"]\n    analysis[\"core/audioCarve.ts<br/>carveProfile · analyseCarveBands<br/>analyseCarveDuck · analyseCarveDynamics\"]\n    panel --> analysis\n    script --> analysis\n  end\n\n  voice --> analysis\n  bed --> analysis\n\n  subgraph ATTRS[\"Written onto the bed element\"]\n    carveAttr[\"data-fx-carve<br/>sources · strength\"]\n    chainAttr[\"data-fx-chain<br/>peaking xN + gain, tagged fromCarve\"]\n    autoAttr[\"data-automation<br/>a lane per carved parameter\"]\n  end\n\n  analysis --> carveAttr\n  analysis --> chainAttr\n  analysis --> autoAttr\n\n  subgraph SHARED[\"One implementation, read by both\"]\n    build[\"audioFxGraph.ts · buildFxChain\"]\n    sched[\"audioFxAutomation.ts · scheduleChainAutomation\"]\n  end\n\n  chainAttr --> build\n  autoAttr --> sched\n\n  build --> preview[\"Preview<br/>live AudioContext<br/>attachElementFxChain\"]\n  sched --> preview\n  build --> render[\"Render<br/>OfflineAudioContext in the headless browser<br/>applyAudioFxChain\"]\n  sched --> render\n\n  preview --> heard[\"what you hear while scrubbing\"]\n  render --> wav[\"processed WAV<br/>+ chainTailSeconds so the mix lets the tail through\"]\n  wav --> mix[\"engine · audioMixer<br/>volume lane baked into the PCM here, not in the graph\"]\n  mix --> out[\"the rendered mix\"]\n\n  edit[\"editing the attribute mid-playback\"] -.->|MutationObserver| preview"},{"language":"mermaid","snippet":"flowchart LR\n  src[\"decoded bed\"] --> p1[\"peaking<br/>400 Hz\"]\n  p1 --> p2[\"peaking<br/>1 kHz\"]\n  p2 --> p3[\"peaking<br/>1.6 kHz\"]\n  p3 --> g[\"gain<br/>level match\"]\n  g --> hand[\"your own effects<br/>e.g. limiter\"]\n  hand --> dest[\"track gain, then out\"]\n\n  l1[\"lane fx.n1.gain\"] -.->|\"envelope of the voice's<br/>level in that band\"| p1\n  l4[\"lane fx.n4.gain\"] -.->|\"how far the bed<br/>ducks overall\"| g"},{"language":"html","snippet":"<!-- group the narration, then carve the bed against the group -->\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-outro\" data-audio-group=\"voiceover\" …></audio>\n\n<audio id=\"music\" data-fx-carve='{\"enabled\":true,\"sources\":[\"voiceover\"],\"strength\":0.8}' …></audio>"},{"language":"html","snippet":"<hf-audio-group\n  id=\"voiceover\"\n  data-label=\"Voiceover\"\n  data-volume=\"0.9\"\n  data-fx-chain='{\"version\":1,\"nodes\":[\n    {\"type\":\"compressor\",\"id\":\"g1\",\"params\":{\"threshold\":-18,\"ratio\":3}},\n    {\"type\":\"peaking\",\"id\":\"g2\",\"params\":{\"frequency\":3000,\"gain\":2,\"q\":1}}]}'\n></hf-audio-group>\n\n<audio id=\"vo-intro\" data-audio-group=\"voiceover\" …></audio>\n<audio id=\"vo-middle\" data-audio-group=\"voiceover\" …></audio>"},{"language":"bash","snippet":"node <SKILL_DIR>/scripts/carve.mjs --comp index.html"},{"language":"text","snippet":"bed    music-bed (name looks like music)\nvoice  narration (only track left)\ncarve  strength 0.8, 1 voice\nbands  250Hz -7.4dB q2.06, 400Hz -7.4dB q2.06, 630Hz -7.4dB q2.06, 1000Hz -7.4dB q2.06, 1600Hz -14.8dB q2.06, 2500Hz -7.4dB q2.06\nlevel  273-point envelope, floor -19.2 dB"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: hyperframes-audio\ndescription: >\n  Use when audio already placed in a HyperFrames composition needs to be mixed:\n  fade-in/fade-out, crossfade, track gain or volume, volume automation, ducking,\n  a music bed that fights a voiceover (voiceover carve), effects on a track\n  (EQ, compressor, limiter, gate, saturation, delay, reverb, chorus, phaser,\n  bitcrush), automation envelopes drawn on a track's volume or any effect\n  parameter, or one submix bus carrying a chain, a fader and an automation clock\n  for several tracks at once (`<hf-audio-group>`).\n  Don't use for sourcing or generating audio — finding BGM, SFX, or making a\n  voiceover is `/media-use`. Don't use for clip timing or track layout, which is\n  `/hyperframes-core`.\n---\n\n**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin. Standalone installs keep the update instructions below.\n\n# HyperFrames Audio\n\nA mix is a set of relationships, not a stack of processors. Two tracks that each\nsound right alone can be unlistenable together, and the fix is almost never \"turn\none down\" — it is finding what they are fighting over and giving it to whichever\none needs it. Every tool here exists to express one of those relationships.\n\nEffects live on the element as `data-fx-chain`, and preview and render run the\nsame Web Audio graph — the studio in a live context, the engine in an offline one\ninside the browser it already drives. There is one implementation of each effect,\nso what you hear while scrubbing is what gets written. You never tune twice.\n\nClip timing remains `/hyperframes-core`: audio/video trims and source ranges use\n`data-start`, `data-duration`, and `data-media-start`, and crossfades overlap\nclips on different tracks. This skill owns placed-track fade-in/fade-out,\ncrossfade envelopes, track gain/track volume, volume and effect automation,\nducking/voiceover carve, and the effect chain. `/media-use` owns sourcing,\ngeneration, and preprocessing.\n\nConstant `data-playback-rate` (`0.1..10`) is render-safe for picture and\npitch-preserved sound when matching audio/video elements use the same timing,\nsource offset, and rate. A speed ramp is a `rate` lane in `data-automation`\n(see `docs/reference/speed-ramps`); it wins over the constant and keeps pitch\nin preview and render. HyperFrames does not\nprovide automatic waveform sync or drift correction.\nFor copyable cut/crossfade/retime recipes, use `/hyperframes-core` → `references/creator-editing-recipes.md`.\n\nThree attributes carry everything, on the audio/video element itself — or, for\nthe first two, on an `<hf-audio-group>` bus (see \"One bus for many tracks\"):\n\n| Attribute         | Holds                                                     |\n| ----------------- | --------------------------------------------------------- |\n| `data-fx-chain`   | the effects, in signal order                              |\n| `data-auto"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn77d06grj6xqp3dqwkk4bavhn89pegt\",\n  \"slug\": \"hyperframes-audio\",\n  \"version\": \"1.0.15\",\n  \"publishedAt\": 1791657895961\n}"},{"path":"references/attributes.md","content":"# The three audio attributes\n\nAll three go on the `<audio>` / `<video>` element itself, JSON-encoded, so a\ncomposition carries its whole mix in the HTML with nothing to load beside it.\n`data-fx-chain` and `data-automation` also go on an `<hf-audio-group>` bus,\nwhere they mean the same thing over the summed members — with one difference\nworth knowing: a group's automation runs on COMPOSITION time, since a bus has no\n`data-start` of its own. `data-fx-carve` is clip-only; a bus has no carve.\nNothing static validates them: preview plays an unreadable chain dry to stay\nworkable, and the render refuses the whole mix rather than shipping a dry track\nthat sounds plausible and is wrong.\n\n## `data-fx-chain` — the effects\n\n```json\n{\n  \"version\": 1,\n  \"nodes\": [\n    {\n      \"type\": \"highpass\",\n      \"id\": \"n1\",\n      \"label\": \"Remove Rumble\",\n      \"params\": { \"frequency\": 120, \"q\": 0.707, \"poles\": \"2\" }\n    },\n    {\n      \"type\": \"peaking\",\n      \"id\": \"n2\",\n      \"fromCarve\": true,\n      \"params\": { \"frequency\": 1600, \"gain\": -6, \"q\": 1.4 }\n    },\n    {\n      \"type\": \"limiter\",\n      \"id\": \"n3\",\n      \"enabled\": false,\n      \"params\": { \"limit\": -1, \"attack\": 5, \"release\": 50, \"level_out\": 0 }\n    }\n  ]\n}\n```\n\n**Write these attributes double-quoted, with the JSON's own quotes as `&quot;`.**\nThe browser reads them through `getAttribute` and does not care, but\n`scripts/carve.mjs` finds them with a `name=\"...\"` regex, so a single-quoted\nattribute is invisible to it — the carve reports no existing chain and quietly\noverwrites work it could not see. `&` becomes `&amp;`; nothing else needs\nescaping.\n\n- **Order is signal order.** Each node processes what the one before produced.\n- `type` is an effect id from the registry. `params` are in the units a person\n  thinks in — dB, ms, Hz — and out-of-range values are clamped on read, so a\n  chain that parses is always safe to realise.\n- `id` is a stable handle. Automation addresses nodes by id, never by position,\n  so reordering the chain cannot re-point a lane at a different effect. A node\n  with no id loads fine but cannot be automated. Writing a chain by hand, any\n  unique string works; Studio hands out the first free `n1`, `n2`, … so matching\n  that convention keeps a hand-written chain and an edited one looking alike.\n- `label` is what the rack calls this node, replacing the effect's own name.\n  Write one whenever the node is doing a named job — a chain with two `peaking`\n  nodes otherwise shows the same row twice and the author cannot tell which is\n  the mud cut and which is the clarity lift. Presets and jobs always set it; a\n  hand-written node should too. See `presets.md` for the names they use.\n- `enabled: false` is bypass — the node stays in the chain, out of the signal\n  path. Absent means enabled.\n- `fromCarve: true` marks a node the carve analysis generated. Re-running the\n  carve replaces exactly these and leaves hand-built effects alone. **Do not set\n  it by hand**: a node tagged this way will be deleted "},{"path":"references/diagnosis.md","content":"# Diagnosing audio you cannot hear\n\nThe symptom table in `SKILL.md` starts from \"it sounds boomy\". That presumes\nsomebody already listened and said so. Handed a file and \"fix this\", you have\nno such sentence — and you cannot listen. This is how to get one.\n\nIt is worth being blunt about the difficulty first, because the failure mode is\nnot \"no answer\", it is **a confident wrong answer**:\n\n> **The absolute spectrum of a single unknown voice cannot be diagnosed.**\n\nEvery voice has peaks and dips of exactly the size an injected filter has.\nFormants are ±10 dB. A speaker's fundamental sits anywhere from 85 to 255 Hz.\nSentences decline 5–6 dB from start to end as a matter of ordinary prosody. Look\nat one spectrum on its own and you will find \"defects\" in all of it, and the\nones you find will be the speaker.\n\nSo diagnosis is always **comparison**. The whole method is choosing the right\nthing to compare against.\n\n---\n\n## Compare against something inside the same file\n\nRanked by how much they can tell you. Prefer the highest one available.\n\n### 1. The clean original, if it exists\n\nIf the undamaged take is on disk, this is the whole job — measure both, subtract,\nand the difference _is_ the defect. Nothing below is as good. Look for it before\nanything else.\n\n### 2. The pauses\n\nThe strongest reference that lives inside a single file. Speech stops; whatever\nis still there in the gap is not the voice.\n\n**What it answers: \"was something added?\"**\n\nAnything audible in the pauses is additive — hum, rumble, hiss, room tone. It was\nlaid on top, so it can be subtracted, and this is a reliable positive finding.\n\n**What it does NOT answer: \"was something filtered?\"** — and getting this\nbackwards is how the method produces a confident wrong answer.\n\nA filter multiplies. Applied to a file whose gaps already sit at the\nquantisation floor, it leaves them at the quantisation floor: near-silence times\nanything is still near-silence. So the pause carries no trace of it. Measured on\none take with a −9 dB shelf above 2.5 kHz applied to the whole file:\n\n|                   | 1 kHz | 5 kHz | tilt      |\n| ----------------- | ----- | ----- | --------- |\n| pause, undamaged  | −91.0 | −91.0 | +0.0      |\n| pause, shelved    | −91.0 | −91.0 | **+0.0**  |\n| speech, undamaged | −34.7 | −42.8 | −8.1      |\n| speech, shelved   | −35.4 | −48.5 | **−13.1** |\n\nThe defect is a clear 5 dB in the speech and **exactly zero** in the pause.\n\nSo: **never use a null result from the pause spectrum to rule out EQ.** A run\nthat did exactly that — measured the pause, found it smooth, and concluded\n\"static EQ of any type or Q is ruled out\" — went on to treat an inaudible\n−72 dBFS rumble as the defect and shipped a high-pass for a file whose actual\nproblem was that it had no top end.\n\nThe pause spectrum _is_ a transfer function only when the gaps carry a real\nrecorded noise floor that passed through the same filter. A room-tone bed does;\na digitally clean take does not. Check which you have before trus"},{"path":"references/fx-registry.md","content":"# Effect registry\n\nEvery effect, its parameters and the usable range of each. Values outside a range\nare clamped on read, so anything that parses is safe to realise. **AUTO** marks a\nparameter an automation lane can drive; anything unmarked cannot move over time\n(see the note at the bottom).\n\nGenerated from `HF_AUDIO_FX` in `@hyperframes/core/audio-fx`, which is the source\nof truth — if this table and the code disagree, the code is right.\n\n## Filter — which frequencies a track may occupy\n\n| Effect      | Parameter                                                                                                   |\n| ----------- | ----------------------------------------------------------------------------------------------------------- |\n| `highpass`  | `frequency` 20–20000 Hz (300, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)       |\n| `lowpass`   | `frequency` 100–20000 Hz (8000, log) **AUTO** · `q` 0.1–20 (0.707, log) **AUTO** · `poles` `1`\\|`2` (2)     |\n| `peaking`   | `frequency` 20–20000 Hz (1000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO** · `q` 0.1–20 (1, log) **AUTO** |\n| `lowshelf`  | `frequency` 20–2000 Hz (200, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                                  |\n| `highshelf` | `frequency` 500–20000 Hz (4000, log) **AUTO** · `gain` −40–40 dB (0) **AUTO**                               |\n\n`q` is bandwidth — higher is narrower. `poles` is the slope: `2` is the usual\nbiquad (12 dB/oct), `1` is gentler (6 dB/oct). Shelving filters have no `q`: the\nWeb Audio spec leaves it unused for them, so a control would have moved nothing.\n\n## Dynamics — how level behaves over time\n\n| Effect       | Parameter                                                                                                                                                                      |\n| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `gain`       | `gain` −60–12 dB (0) **AUTO**                                                                                                                                                  |\n| `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) |\n| `limiter`    | `limit` −24–0 dB (−1) · `attack` 0.1–80 ms (5) · `release` 1–8000 ms (50, log) · `level_out` −24–24 dB (0)                                                                     |\n| `truepeak`   | `ceiling` −24–0 dBTP (−1) · `lookahead` 0.5–10 ms (3) · `release` 10–2000 ms (80, log)                                                                                         |\n| `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.8"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2104,"uniquenessScore":43,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T22:47:30.096Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T01:45:00.362Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}