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

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

* chore(skills): refresh media-use manifest

* docs(media-use): design CLI fallback advisory

* docs(media-use): plan CLI fallback advisory

* fix(media-use): surface HeyGen CLI fallback guidance

* fix(media-use): derive bundled SFX extension
This commit is contained in:
Miguel Ángel
2026-07-12 22:17:52 -04:00
committed by GitHub
parent 44653b31da
commit 2e34a2d5a0
11 changed files with 341 additions and 5 deletions
@@ -0,0 +1,58 @@
import { existsSync, readFileSync } from "node:fs";
import { extname, 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();
export function extensionForBundledSfxFile(filename) {
return extname(filename) || ".mp3";
}
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: extensionForBundledSfxFile(best.entry.file),
source: "bundled",
metadata: {
description: best.entry.description || best.key,
duration: best.entry.duration ?? null,
provider: "bundled.sfx",
provenance: { library_key: best.key },
},
};
},
};
@@ -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");
});
@@ -89,9 +89,22 @@ 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() {
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 {
@@ -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(
+6 -2
View File
@@ -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
+10 -1
View File
@@ -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 },
+11
View File
@@ -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
+76
View File
@@ -7,6 +7,7 @@ import {
mkdirSync,
existsSync,
readdirSync,
chmodSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
@@ -130,6 +131,81 @@ 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.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" } });