Skip to content

fix(media-use): fall back to bundled SFX#2257

Merged
miguel-heygen merged 7 commits into
mainfrom
fix/media-use-bundled-sfx
Jul 13, 2026
Merged

fix(media-use): fall back to bundled SFX#2257
miguel-heygen merged 7 commits into
mainfrom
fix/media-use-bundled-sfx

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Make media-use resolve --type sfx fall back to the shipped SFX library when the HeyGen CLI/catalog is unavailable. The remote catalog remains the first choice.

Why

CLI 0.7.53 documents a bundled 19-file SFX fallback, but the resolver registry exposed only heygen.audio.sounds. On a machine without the HeyGen CLI, even --intent whoosh failed with no provider could resolve although whoosh.mp3 was installed with the skill.

How

  • Add a deterministic bundled-SFX provider backed by audio/assets/sfx/manifest.json.
  • Register it after the HeyGen catalog as a local/free fallback.
  • Match exact effect names first, then deterministic intent-token overlap.
  • Add registry and end-to-end coverage that removes HeyGen from PATH and verifies the frozen local asset.
  • Regenerate the skills manifest.

Test plan

  • bun run test:skills (219 passed)
  • node --test skills/media-use/scripts/lib/registry.test.mjs skills/media-use/scripts/resolve.test.mjs
  • npx oxfmt --check skills/media-use/scripts/lib/bundled-sfx-provider.mjs skills/media-use/scripts/lib/registry.mjs skills/media-use/scripts/lib/registry.test.mjs skills/media-use/scripts/resolve.test.mjs
  • npx oxlint skills/media-use/scripts/lib/bundled-sfx-provider.mjs skills/media-use/scripts/lib/registry.mjs skills/media-use/scripts/lib/registry.test.mjs skills/media-use/scripts/resolve.test.mjs
  • bun packages/cli/scripts/gen-skills-manifest.ts --check

@mintlify

mintlify Bot commented Jul 13, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
hyperframes 🟢 Ready View Preview Jul 13, 2026, 1:31 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at c288c0bf.

Well-scoped for +325. Bulk of the diff is docs (plan + spec = ~120 LOC) and test infra (~140 LOC); the core code change (bundled provider + remediation state + advisory plumbing) is ~65 LOC.

Design correctness checks all pass:

  • Provider cascade orderbundledSfxProvider registered as second in the sfx array at registry.mjs:52; heygen tried first, bundled only fires on catalog failure. Sibling test sfx cascade: HeyGen catalog first, bundled library remains the local fallback locks this in and matches the voice cascade shape (HeyGen TTS + Kokoro) already sanctioned.

  • Advisory gating — the four-condition guard at resolve.mjs:432-442 (provider === "bundled.sfx" && !localOnly && !args.provider && heygenRemediation) correctly closes all three false-fire holes: --local-only (user didn't want catalog anyway), --provider bundled.sfx (user explicitly forced local), healthy-catalog-miss (heygen returned empty, no remediation set). Test matrix at resolve.test.mjs:184-227 covers each of those negative paths with advisory === undefined assertions — good ratchet.

  • pendingRemediation state discipline — module-scoped singleton, consume-once via consumeHeygenRemediation() (returns and clears). Only not_found/outdated classifications ever populate it (heygen-cli.mjs:100). The negative test does not record non-install remediation covers 401 / quota / generic — those legitimate-but-not-actionable failures don't leak into the advisory. Test hygiene calls consumeHeygenRemediation() at the top of each test to drain residual state.

  • Bundled search algorithm — tiered scoring (exact 100 / substring 50 / token-overlap count), deterministic alphabetical tie-break. For the reported --intent whoosh case, whooshwhoosh.mp3 at score 100 (exact), whoosh-cinematic / whoosh-short at 50. First result is stable. Score-zero filter (score > 0) correctly excludes non-overlap entries.

  • Node versionimport.meta.dirname is Node 20.11+; CLI package's engines.node: ">=22" at packages/cli/package.json:71 is safely above. Sibling pattern already used across scripts/, so no new precedent.

Nits

  • 🟡 ext: ".mp3" hardcoded at bundled-sfx-provider.mjs:41. All 19 bundled files are .mp3 today (verified against audio/assets/sfx/), so this is correct as-shipped. But if the library ever grows a .wav or .ogg variant, the extension label diverges from the actual file. Trivial to derive:

    ext: extname(best.entry.file) || ".mp3",

    Won't fire today; defensive against future manifest edits.

  • 🟡 pendingRemediation is a single process-global slot. Under the current CLI single-shot invocation (one resolve per process), that's fine — one heygen failure records → one bundled win consumes. But if the resolver is ever embedded (in-process batch mode, tests running multiple resolves without spawning) two concurrent resolves could cross-consume each other's state. The consume-once discipline mitigates but doesn't eliminate. Worth a comment on the export declaring the "one resolve per process" assumption, or scoping the remediation state to a passed-in context if that assumption ever weakens.

  • 🟡 Provider metadata provenance shape divergence — heygen.audio.sounds returns { track_id, score, query }, bundled.sfx returns { library_key }. Both are useful for their sources but downstream consumers reading record.provenance.* need to accommodate the union. Not a bug, just an interface-shape observation; a shared discriminated union type would surface this in the type system if the codebase ever moves to TS for these scripts.

  • 🟡 The two docs (plans/2026-07-13-media-use-sfx-cli-advisory.md + specs/2026-07-13-media-use-sfx-cli-advisory-design.md) match the implementation shape exactly. No drift. If the team's convention is to keep these post-implementation, great; if they're intended as scaffolding to delete after landing, they'll accumulate. Non-blocking either way.

LGTM. Nice discipline — plan + spec + tests + tight code.

Review by Rames D Jusso

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

derive the bundled SFX extension from the manifest filename and document the remediation-state assumption

Addressed in fcc6ecb17: extension metadata now uses extname(best.entry.file) || ".mp3", with .wav, .ogg, and extensionless regression coverage. The remediation slot now documents the single-shot/one-resolve-per-process contract and the required move to per-resolve context if this becomes concurrent or embedded. Evidence: full skills suite 229/229; skills lint; generated manifest check; targeted format check.

The provenance observation is intentionally unchanged: provider-specific provenance is already a source-shaped union (library_key versus HeyGen track/query fields), and no current consumer assumes one common shape. The plan/spec docs also match the shipped behavior and are retained team artifacts. A broader type refactor or deleting aligned docs would add unrelated churn without fixing a current defect.

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R2 reviewed at fcc6ecb1, delta from R1 head c288c0bf.

Both R1 nits addressed:

  • Manifest-derived extensionextensionForBundledSfxFile(best.entry.file) at bundled-sfx-provider.mjs:48 now derives the ext from the actual filename (extname(filename) || ".mp3"). All 19 bundled entries are currently .mp3 so behavior is unchanged today, but a future .wav/.ogg addition to manifest.json no longer requires touching the provider. Test asserts all three shapes (.wav, .ogg, extensionless fallback).
  • Remediation singleton assumption documentedpendingRemediation at heygen-cli.mjs:92-94 now carries a comment explicitly stating this is fine because resolve.mjs is a single-shot CLI, and calling out the migration path (per-resolve context) if resolve ever becomes an in-process/concurrent API.

LGTM from my side.

Review by Rames D Jusso

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving on Rames-D's go (R2 delta clean, nit round addressed) + Magi's CI-green confirmation. Verified at final head fcc6ecb1: CI green, no changes-requested, mergeable.

@miguel-heygen miguel-heygen merged commit 2e34a2d into main Jul 13, 2026
41 checks passed
@miguel-heygen miguel-heygen deleted the fix/media-use-bundled-sfx branch July 13, 2026 02:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants