From 5c778c599a0bae4232709b8a0b919edf7c22aa3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Sat, 11 Jul 2026 20:22:23 +0000 Subject: [PATCH 1/6] fix(media-use): fall back to bundled SFX --- skills-manifest.json | 4 +- .../scripts/lib/bundled-sfx-provider.mjs | 54 +++++++++++++++++++ skills/media-use/scripts/lib/registry.mjs | 8 ++- .../media-use/scripts/lib/registry.test.mjs | 11 +++- skills/media-use/scripts/resolve.test.mjs | 14 +++++ 5 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 skills/media-use/scripts/lib/bundled-sfx-provider.mjs diff --git a/skills-manifest.json b/skills-manifest.json index 9321089cb1..d60708921e 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,8 +46,8 @@ "files": 10 }, "media-use": { - "hash": "49aa5adae0800403", - "files": 122 + "hash": "facf3a0c03fb1bc5", + "files": 123 }, "motion-graphics": { "hash": "dafcdce07d221aa4", diff --git a/skills/media-use/scripts/lib/bundled-sfx-provider.mjs b/skills/media-use/scripts/lib/bundled-sfx-provider.mjs new file mode 100644 index 0000000000..e109808115 --- /dev/null +++ b/skills/media-use/scripts/lib/bundled-sfx-provider.mjs @@ -0,0 +1,54 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +const LIB_DIR = join(import.meta.dirname, "..", "..", "audio", "assets", "sfx"); + +const normalize = (value) => + String(value) + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); + +function score(intent, key, entry) { + const query = normalize(intent); + const name = normalize(key); + if (query === name) return 100; + if (query.includes(name) || name.includes(query)) return 50; + const haystack = new Set(normalize(`${key} ${entry.description || ""}`).split(/\s+/)); + return query.split(/\s+/).filter((token) => token && haystack.has(token)).length; +} + +export const bundledSfxProvider = { + async search(intent) { + const manifestPath = join(LIB_DIR, "manifest.json"); + if (!existsSync(manifestPath)) return null; + + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch { + return null; + } + + const ranked = Object.entries(manifest) + .map(([key, entry]) => ({ key, entry, score: score(intent, key, entry) })) + .filter(({ entry, score }) => entry?.file && score > 0) + .sort((a, b) => b.score - a.score || a.key.localeCompare(b.key)); + const best = ranked[0]; + if (!best) return null; + + const localPath = join(LIB_DIR, best.entry.file); + if (!existsSync(localPath)) return null; + return { + localPath, + ext: ".mp3", + source: "bundled", + metadata: { + description: best.entry.description || best.key, + duration: best.entry.duration ?? null, + provider: "bundled.sfx", + provenance: { library_key: best.key }, + }, + }; + }, +}; diff --git a/skills/media-use/scripts/lib/registry.mjs b/skills/media-use/scripts/lib/registry.mjs index 84df07ccf9..ef406fb2bc 100644 --- a/skills/media-use/scripts/lib/registry.mjs +++ b/skills/media-use/scripts/lib/registry.mjs @@ -22,6 +22,7 @@ import { bgmProvider } from "./bgm-provider.mjs"; import { sfxProvider } from "./sfx-provider.mjs"; +import { bundledSfxProvider } from "./bundled-sfx-provider.mjs"; import { imageProvider, iconProvider } from "./image-provider.mjs"; import { brandProvider } from "./brand-provider.mjs"; import { @@ -44,10 +45,13 @@ const A = (name, caps) => ({ name, ...caps }); // local, free const N = (name, caps) => ({ name, network: true, ...caps }); // remote, free const P = (name, caps) => ({ name, network: true, paid: true, ...caps }); // remote, paid -// heygen-CLI first (and currently only). All remote providers are skipped by --local-only. +// heygen-CLI first. All remote providers are skipped by --local-only. const REGISTRY = { bgm: [N("heygen.audio.sounds", { search: bgmProvider.search })], - sfx: [N("heygen.audio.sounds", { search: sfxProvider.search })], + sfx: [ + N("heygen.audio.sounds", { search: sfxProvider.search }), + A("bundled.sfx", { search: bundledSfxProvider.search }), + ], image: [ N("heygen.asset.search", { search: imageProvider.search }), // Catalog miss -> generate. Local first (best FLUX-class model the machine's diff --git a/skills/media-use/scripts/lib/registry.test.mjs b/skills/media-use/scripts/lib/registry.test.mjs index 6cd51ac430..4d570e5e76 100644 --- a/skills/media-use/scripts/lib/registry.test.mjs +++ b/skills/media-use/scripts/lib/registry.test.mjs @@ -21,7 +21,7 @@ test("heygen provider is first for every type it serves", () => { test("sanctioned providers only: heygen, local mflux/kokoro, codex, design spec, logo tiers", () => { const allowed = - /^heygen|^mflux\.local$|^kokoro\.local$|^codex\.image_gen$|^design_spec$|^svgl$|^simple-icons$|^github\.avatar$|^favicon\.ddg$|^color_grade\.local$|^cube_lut\.local$/; + /^heygen|^bundled\.sfx$|^mflux\.local$|^kokoro\.local$|^codex\.image_gen$|^design_spec$|^svgl$|^simple-icons$|^github\.avatar$|^favicon\.ddg$|^color_grade\.local$|^cube_lut\.local$/; for (const t of listTypes()) { for (const p of getProviders(t)) { assert.ok(allowed.test(p.name), `${t} lists unsanctioned provider: ${p.name}`); @@ -52,6 +52,15 @@ test("voice cascade: HeyGen TTS first, Kokoro remains the local fallback", () => assert.ok(!ps[1].paid, "local Kokoro is free"); }); +test("sfx cascade: HeyGen catalog first, bundled library remains the local fallback", () => { + const ps = getProviders("sfx"); + assert.equal(ps[0].name, "heygen.audio.sounds"); + assert.ok(ps[0].network, "HeyGen SFX catalog is network-only"); + assert.equal(ps[1].name, "bundled.sfx"); + assert.equal(typeof ps[1].search, "function"); + assert.ok(!ps[1].network, "bundled SFX remain available offline"); +}); + test("ctx.provider forces one generator (e.g. 'make an image WITH codex')", async () => { const providers = [ { name: "heygen.asset.search", network: true, search: async () => null }, diff --git a/skills/media-use/scripts/resolve.test.mjs b/skills/media-use/scripts/resolve.test.mjs index fea0e3be8c..81e746af5e 100644 --- a/skills/media-use/scripts/resolve.test.mjs +++ b/skills/media-use/scripts/resolve.test.mjs @@ -130,6 +130,20 @@ function test(name, fn) { // --- manifest cache hit --- +test("bundled SFX resolve without HeyGen on PATH", () => { + setup(); + const result = spawnResolve( + ["--type", "sfx", "--intent", "whoosh", "--project", tmp, "--json"], + { env: { HOME: tmp, PATH: tmp } }, + ); + assert.equal(result.status, 0, result.stderr); + const parsed = JSON.parse(result.stdout); + assert.equal(parsed.ok, true); + assert.equal(parsed.provenance.provider, "bundled.sfx"); + assert.ok(existsSync(join(tmp, parsed.path))); + cleanup(); +}); + test("project manifest hit skips providers", () => { setup(); const record = makeRecord({ provenance: { prompt: "cached query", provider: "test" } }); From d3185e3e1aff8a58b0b5e4b0574f7fc4aa6b53ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 01:12:46 +0000 Subject: [PATCH 2/6] chore(skills): refresh media-use manifest --- skills-manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 256a8dbd8f..9ef40f4903 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "3546dbbd14afe560", + "hash": "91f0ab45f6ac1d13", "files": 123 }, "motion-graphics": { From 62259246a33ef761c809e7923e5420b1cbac40fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 01:25:01 +0000 Subject: [PATCH 3/6] docs(media-use): design CLI fallback advisory --- ...07-13-media-use-sfx-cli-advisory-design.md | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-13-media-use-sfx-cli-advisory-design.md diff --git a/docs/superpowers/specs/2026-07-13-media-use-sfx-cli-advisory-design.md b/docs/superpowers/specs/2026-07-13-media-use-sfx-cli-advisory-design.md new file mode 100644 index 0000000000..39fcb756b7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-13-media-use-sfx-cli-advisory-design.md @@ -0,0 +1,34 @@ +# Media-use SFX CLI advisory design + +## Goal + +When SFX resolution succeeds through the bundled fallback because the HeyGen CLI is missing or outdated, tell the agent how to restore the broader HeyGen catalog path. Keep bundled fallback successful and never install software automatically. + +## Behavior + +- Reuse the existing HeyGen CLI error classification produced by the failed catalog provider call. +- Retain only the latest actionable `not_found` or `outdated` remediation for the current resolver process. +- When `bundled.sfx` wins after that failure, include a structured advisory in JSON output and print the same concise hint in human output. +- Use the existing canonical commands: + - Missing: `curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --oauth` + - Outdated: `heygen update` +- Do not emit this advisory for authentication, quota, network, or legitimate empty-catalog results; `--local-only`; or an explicitly forced bundled provider. +- Do not execute the install or update command. + +## Components + +1. `heygen-cli.mjs` records and exposes one process-local actionable remediation alongside the existing logging and telemetry behavior. +2. `resolve.mjs` consumes that remediation only when the winning provider is `bundled.sfx`, then attaches it to the result. +3. The result formatter exposes the advisory consistently to JSON and human consumers. + +## Tests + +- Missing CLI followed by bundled fallback returns the install advisory. +- Outdated CLI followed by bundled fallback returns the update advisory. +- A healthy CLI with a legitimate catalog miss returns bundled SFX without an install/update advisory. +- `--local-only` and explicit bundled-provider resolution do not suggest installation. +- Existing fallback, registry, skill, format, and manifest checks remain green. + +## Safety + +This is recommendation-only. It does not cross the software-install consent boundary, run a shell, or turn a successful local fallback into an error. From 2032d355378e9887c166bc694a4a346278586509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 01:26:15 +0000 Subject: [PATCH 4/6] docs(media-use): plan CLI fallback advisory --- .../2026-07-13-media-use-sfx-cli-advisory.md | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-13-media-use-sfx-cli-advisory.md diff --git a/docs/superpowers/plans/2026-07-13-media-use-sfx-cli-advisory.md b/docs/superpowers/plans/2026-07-13-media-use-sfx-cli-advisory.md new file mode 100644 index 0000000000..0742817b6f --- /dev/null +++ b/docs/superpowers/plans/2026-07-13-media-use-sfx-cli-advisory.md @@ -0,0 +1,88 @@ +# Media-use SFX CLI Advisory Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Surface an actionable install/update advisory when bundled SFX succeeds specifically because the HeyGen CLI is missing or outdated. + +**Architecture:** Extend the existing HeyGen error classifier with process-local remediation state, consume it only after `bundled.sfx` wins, and expose it through the existing result formatter. Preserve successful fallback, existing stderr diagnostics, telemetry, and provider ordering. + +**Tech Stack:** Node.js ESM, `node:test`, media-use resolver CLI, generated skills manifest. + +--- + +### Task 1: Pin remediation state behavior + +**Files:** +- Modify: `skills/media-use/scripts/lib/heygen-cli.test.mjs` +- Modify: `skills/media-use/scripts/lib/heygen-cli.mjs` + +- [ ] **Step 1: Write failing tests** + +Add tests that call `reportHeygenFailure` with `ENOENT`, an outdated-version error, and a non-actionable error, then assert a new `consumeHeygenRemediation()` returns `{ code, message }` once for only `not_found` and `outdated`. + +- [ ] **Step 2: Verify RED** + +Run: `node --test skills/media-use/scripts/lib/heygen-cli.test.mjs` +Expected: FAIL because `consumeHeygenRemediation` is not exported. + +- [ ] **Step 3: Implement minimal state** + +Store the latest `not_found` or `outdated` result inside `reportHeygenFailure`; export a consume-once function that returns and clears it. Do not store auth, quota, network, or generic failures. + +- [ ] **Step 4: Verify GREEN** + +Run the same test; expected: PASS. + +### Task 2: Attach the advisory only to implicit bundled fallback + +**Files:** +- Modify: `skills/media-use/scripts/resolve.test.mjs` +- Modify: `skills/media-use/scripts/resolve.mjs` + +- [ ] **Step 1: Write failing resolver tests** + +Add subprocess cases for missing CLI and outdated CLI that resolve bundled SFX and assert `advisory.message` contains the canonical install/update command. Add negative cases for `--local-only`, `--provider bundled.sfx`, and a healthy catalog miss. + +- [ ] **Step 2: Verify RED** + +Run: `node --test skills/media-use/scripts/resolve.test.mjs` +Expected: FAIL because successful JSON results do not contain `advisory`. + +- [ ] **Step 3: Implement result plumbing** + +After provider resolution, consume remediation only when the winner is `bundled.sfx`, the run is not local-only, and no provider override was supplied. Attach it to the record/result; print a concise human hint and include the structured object in JSON. + +- [ ] **Step 4: Verify GREEN** + +Run the resolver test; expected: all cases PASS. + +### Task 3: Regenerate and verify + +**Files:** +- Modify: `skills-manifest.json` + +- [ ] **Step 1: Format changed files** + +Run `bunx oxfmt` on the four changed source/test files. + +- [ ] **Step 2: Regenerate manifest** + +Run: `bun packages/cli/scripts/gen-skills-manifest.ts` + +- [ ] **Step 3: Run required checks** + +Run: + +```bash +node --test skills/media-use/scripts/lib/heygen-cli.test.mjs skills/media-use/scripts/lib/registry.test.mjs skills/media-use/scripts/resolve.test.mjs +bun run lint:skills +bun run test:skills +bun run format:check +bun packages/cli/scripts/gen-skills-manifest.ts --check +``` + +Expected: zero failures and an in-sync manifest. + +- [ ] **Step 4: Commit and push** + +Commit the implementation and generated manifest, push to `fix/media-use-bundled-sfx`, then wait for PR #2257 CI to become terminal and green. From c288c0bf285e034ebad287ae805fdf6f1b4bce23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 01:30:37 +0000 Subject: [PATCH 5/6] fix(media-use): surface HeyGen CLI fallback guidance --- skills-manifest.json | 2 +- skills/media-use/scripts/lib/heygen-cli.mjs | 10 +++ .../media-use/scripts/lib/heygen-cli.test.mjs | 34 ++++++++++ skills/media-use/scripts/resolve.mjs | 11 ++++ skills/media-use/scripts/resolve.test.mjs | 63 +++++++++++++++++++ 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/skills-manifest.json b/skills-manifest.json index 9ef40f4903..decc72fd98 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,7 +46,7 @@ "files": 10 }, "media-use": { - "hash": "91f0ab45f6ac1d13", + "hash": "c6e7380cfe3c40ca", "files": 123 }, "motion-graphics": { diff --git a/skills/media-use/scripts/lib/heygen-cli.mjs b/skills/media-use/scripts/lib/heygen-cli.mjs index d5208c9503..f1c9a6c152 100644 --- a/skills/media-use/scripts/lib/heygen-cli.mjs +++ b/skills/media-use/scripts/lib/heygen-cli.mjs @@ -89,9 +89,19 @@ function classifyHeygenErrorResult(err) { // same "awaited so a short-lived run flushes it" discipline telemetry.mjs's // track() already documents, just reachable from a sync call site. const pendingFailureTracking = new Set(); +let pendingRemediation = null; + +export function consumeHeygenRemediation() { + const remediation = pendingRemediation; + pendingRemediation = null; + return remediation; +} export function reportHeygenFailure(err, context, trackEvent = track) { const { code, message } = classifyHeygenErrorResult(err); + if (code === "not_found" || code === "outdated") { + pendingRemediation = { code, message }; + } if (ACTIONABLE_MESSAGES.has(message)) { console.error(message); } else { diff --git a/skills/media-use/scripts/lib/heygen-cli.test.mjs b/skills/media-use/scripts/lib/heygen-cli.test.mjs index 04886c14dd..56594ac0d0 100644 --- a/skills/media-use/scripts/lib/heygen-cli.test.mjs +++ b/skills/media-use/scripts/lib/heygen-cli.test.mjs @@ -4,6 +4,7 @@ import { test } from "node:test"; import { classifyHeygenError, classifyHeygenErrorCode, + consumeHeygenRemediation, flushHeygenFailureTracking, HEYGEN_NOT_AUTHENTICATED_MESSAGE, HEYGEN_NOT_FOUND_MESSAGE, @@ -149,6 +150,39 @@ test("tracks not-found failures without changing actionable output", () => { ]); }); +test("records missing and outdated CLI remediation once", () => { + consumeHeygenRemediation(); + captureFailureReport({ code: "ENOENT" }, "heygen audio sounds list", () => {}); + assert.deepEqual(consumeHeygenRemediation(), { + code: "not_found", + message: HEYGEN_NOT_FOUND_MESSAGE, + }); + assert.equal(consumeHeygenRemediation(), null); + + captureFailureReport( + { stderr: Buffer.from("heygen v0.1.5 does not support --headers") }, + "heygen audio sounds list", + () => {}, + ); + assert.deepEqual(consumeHeygenRemediation(), { + code: "outdated", + message: HEYGEN_OUTDATED_MESSAGE, + }); + assert.equal(consumeHeygenRemediation(), null); +}); + +test("does not record non-install remediation", () => { + consumeHeygenRemediation(); + for (const error of [ + { stderr: Buffer.from("HTTP 401 Unauthorized") }, + { stderr: Buffer.from("quota exhausted") }, + { stderr: Buffer.from("provider unavailable") }, + ]) { + captureFailureReport(error, "heygen audio sounds list", () => {}); + assert.equal(consumeHeygenRemediation(), null); + } +}); + test("tracks generic failures without including raw detail", () => { const trackingCalls = []; const stderrCalls = captureFailureReport( diff --git a/skills/media-use/scripts/resolve.mjs b/skills/media-use/scripts/resolve.mjs index 617443ac70..7d17560ffe 100644 --- a/skills/media-use/scripts/resolve.mjs +++ b/skills/media-use/scripts/resolve.mjs @@ -30,6 +30,7 @@ import { HEYGEN_INSTALL_COMMAND, HEYGEN_MIN_VERSION, HEYGEN_UPDATE_COMMAND, + consumeHeygenRemediation, firstSemver, flushHeygenFailureTracking, versionLessThan, @@ -429,6 +430,16 @@ async function run() { }, }; + const heygenRemediation = consumeHeygenRemediation(); + if ( + searchResult.metadata?.provider === "bundled.sfx" && + !localOnly && + !args.provider && + heygenRemediation + ) { + record.advisory = heygenRemediation; + } + appendRecord(projectDir, record); regenerateIndex(projectDir); // Auto-promote: surface every fetched asset in the global cache so it's diff --git a/skills/media-use/scripts/resolve.test.mjs b/skills/media-use/scripts/resolve.test.mjs index 0136de37be..1e50ad2412 100644 --- a/skills/media-use/scripts/resolve.test.mjs +++ b/skills/media-use/scripts/resolve.test.mjs @@ -7,6 +7,7 @@ import { mkdirSync, existsSync, readdirSync, + chmodSync, } from "node:fs"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -139,10 +140,72 @@ test("bundled SFX resolve without HeyGen on PATH", () => { const parsed = JSON.parse(result.stdout); assert.equal(parsed.ok, true); assert.equal(parsed.provenance.provider, "bundled.sfx"); + assert.match(parsed.advisory?.message ?? "", /Install: curl -fsSL/); assert.ok(existsSync(join(tmp, parsed.path))); cleanup(); }); +function writeFakeHeygen(body, exitCode = 0) { + const binDir = join(tmp, "bin"); + mkdirSync(binDir, { recursive: true }); + const command = join(binDir, "heygen"); + writeFileSync(command, `#!/bin/sh\n${body}\nexit ${exitCode}\n`); + chmodSync(command, 0o755); + return binDir; +} + +test("bundled SFX advises update when the HeyGen CLI is outdated", () => { + setup(); + const binDir = writeFakeHeygen('echo "heygen v0.1.5 does not support --headers" >&2', 1); + const result = spawnResolve(["--type", "sfx", "--intent", "whoosh", "--project", tmp, "--json"], { + env: { HOME: tmp, PATH: binDir }, + }); + assert.equal(result.status, 0, result.stderr); + const parsed = JSON.parse(result.stdout); + assert.equal(parsed.provenance.provider, "bundled.sfx"); + assert.match(parsed.advisory?.message ?? "", /heygen update/); + cleanup(); +}); + +test("bundled SFX does not advise installation after a healthy catalog miss", () => { + setup(); + const binDir = writeFakeHeygen(`echo '{"data":[]}'`); + const result = spawnResolve(["--type", "sfx", "--intent", "whoosh", "--project", tmp, "--json"], { + env: { HOME: tmp, PATH: binDir }, + }); + assert.equal(result.status, 0, result.stderr); + const parsed = JSON.parse(result.stdout); + assert.equal(parsed.provenance.provider, "bundled.sfx"); + assert.equal(parsed.advisory, undefined); + cleanup(); +}); + +test("explicit local bundled SFX resolution does not advise installation", () => { + for (const extraArgs of [["--local-only"], ["--provider", "bundled.sfx"]]) { + setup(); + const result = spawnResolve( + ["--type", "sfx", "--intent", "whoosh", "--project", tmp, "--json", ...extraArgs], + { env: { HOME: tmp, PATH: tmp } }, + ); + assert.equal(result.status, 0, result.stderr); + const parsed = JSON.parse(result.stdout); + assert.equal(parsed.provenance.provider, "bundled.sfx"); + assert.equal(parsed.advisory, undefined); + cleanup(); + } +}); + +test("human bundled fallback prints the install hint once", () => { + setup(); + const result = spawnResolve(["--type", "sfx", "--intent", "whoosh", "--project", tmp], { + env: { HOME: tmp, PATH: tmp }, + }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stderr.match(/Install: curl -fsSL/g)?.length, 1); + assert.match(result.stdout, /resolved sfx_001/); + cleanup(); +}); + test("project manifest hit skips providers", () => { setup(); const record = makeRecord({ provenance: { prompt: "cached query", provider: "test" } }); From fcc6ecb1707642d30d842cbfd07860a392f4c6c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Mon, 13 Jul 2026 01:54:58 +0000 Subject: [PATCH 6/6] fix(media-use): derive bundled SFX extension --- skills-manifest.json | 4 ++-- skills/media-use/scripts/lib/bundled-sfx-provider.mjs | 8 ++++++-- .../media-use/scripts/lib/bundled-sfx-provider.test.mjs | 9 +++++++++ skills/media-use/scripts/lib/heygen-cli.mjs | 3 +++ 4 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs diff --git a/skills-manifest.json b/skills-manifest.json index decc72fd98..84f572ace7 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -46,8 +46,8 @@ "files": 10 }, "media-use": { - "hash": "c6e7380cfe3c40ca", - "files": 123 + "hash": "8fd94ced0d1daee6", + "files": 124 }, "motion-graphics": { "hash": "dafcdce07d221aa4", diff --git a/skills/media-use/scripts/lib/bundled-sfx-provider.mjs b/skills/media-use/scripts/lib/bundled-sfx-provider.mjs index e109808115..7f11f420de 100644 --- a/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +++ b/skills/media-use/scripts/lib/bundled-sfx-provider.mjs @@ -1,5 +1,5 @@ import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; +import { extname, join } from "node:path"; const LIB_DIR = join(import.meta.dirname, "..", "..", "audio", "assets", "sfx"); @@ -9,6 +9,10 @@ const normalize = (value) => .replace(/[^a-z0-9]+/g, " ") .trim(); +export function extensionForBundledSfxFile(filename) { + return extname(filename) || ".mp3"; +} + function score(intent, key, entry) { const query = normalize(intent); const name = normalize(key); @@ -41,7 +45,7 @@ export const bundledSfxProvider = { if (!existsSync(localPath)) return null; return { localPath, - ext: ".mp3", + ext: extensionForBundledSfxFile(best.entry.file), source: "bundled", metadata: { description: best.entry.description || best.key, diff --git a/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs b/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs new file mode 100644 index 0000000000..66b7f44f64 --- /dev/null +++ b/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs @@ -0,0 +1,9 @@ +import { strict as assert } from "node:assert"; +import { test } from "node:test"; +import { extensionForBundledSfxFile } from "./bundled-sfx-provider.mjs"; + +test("derives bundled SFX extension from the manifest filename", () => { + assert.equal(extensionForBundledSfxFile("impact.wav"), ".wav"); + assert.equal(extensionForBundledSfxFile("whoosh.ogg"), ".ogg"); + assert.equal(extensionForBundledSfxFile("extensionless"), ".mp3"); +}); diff --git a/skills/media-use/scripts/lib/heygen-cli.mjs b/skills/media-use/scripts/lib/heygen-cli.mjs index f1c9a6c152..f54e3224a4 100644 --- a/skills/media-use/scripts/lib/heygen-cli.mjs +++ b/skills/media-use/scripts/lib/heygen-cli.mjs @@ -89,6 +89,9 @@ function classifyHeygenErrorResult(err) { // same "awaited so a short-lived run flushes it" discipline telemetry.mjs's // track() already documents, just reachable from a sync call site. const pendingFailureTracking = new Set(); +// resolve.mjs is a single-shot CLI (one resolve per process), so one shared +// consume-once slot is sufficient. If resolve becomes an in-process/concurrent +// API, move this state into a per-resolve context before reusing that path. let pendingRemediation = null; export function consumeHeygenRemediation() {