fix(media-use): explain missing bundled SFX (#2460)

This commit is contained in:
Miguel Ángel
2026-07-14 21:59:44 -04:00
committed by GitHub
parent 7382fabab9
commit da19f9a692
5 changed files with 197 additions and 21 deletions
@@ -1,7 +1,63 @@
import { existsSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";
const LIB_DIR = join(import.meta.dirname, "..", "..", "audio", "assets", "sfx");
const LIB_DIR =
process.env.HYPERFRAMES_MEDIA_USE_SFX_DIR ||
join(import.meta.dirname, "..", "..", "audio", "assets", "sfx");
export const BUNDLED_SFX_RECOVERY_COMMAND = "npx hyperframes skills update media-use";
export class BundledSfxAssetsError extends Error {
constructor(health) {
super(
`bundled SFX assets are missing or incomplete (${health.detail}). Repair the installed media-use skill: ${health.fix}`,
);
this.name = "BundledSfxAssetsError";
this.code = health.code;
this.fix = health.fix;
}
}
function unhealthy(detail) {
return {
ok: false,
code: "bundled_sfx_assets_missing",
detail,
fix: BUNDLED_SFX_RECOVERY_COMMAND,
};
}
export function inspectBundledSfxAssets(libraryDir = LIB_DIR) {
const manifestPath = join(libraryDir, "manifest.json");
if (!existsSync(manifestPath)) return unhealthy(`manifest not found: ${manifestPath}`);
let manifest;
try {
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
} catch {
return unhealthy(`manifest is not valid JSON: ${manifestPath}`);
}
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
return unhealthy(`manifest must contain an object: ${manifestPath}`);
}
const entries = Object.entries(manifest);
if (entries.length === 0) return unhealthy(`manifest contains no SFX entries: ${manifestPath}`);
for (const [key, entry] of entries) {
if (!entry?.file || typeof entry.file !== "string") {
return unhealthy(`manifest entry "${key}" has no file`);
}
const assetPath = join(libraryDir, entry.file);
if (!existsSync(assetPath)) return unhealthy(`asset not found: ${assetPath}`);
}
return {
ok: true,
count: entries.length,
detail: `${entries.length} bundled SFX asset${entries.length === 1 ? "" : "s"} available`,
fix: "",
};
}
const normalize = (value) =>
String(value)
@@ -23,16 +79,11 @@ function score(intent, key, entry) {
}
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;
}
async search(intent, ctx = {}) {
const libraryDir = ctx.libraryDir || LIB_DIR;
const health = inspectBundledSfxAssets(libraryDir);
if (!health.ok) throw new BundledSfxAssetsError(health);
const manifest = JSON.parse(readFileSync(join(libraryDir, "manifest.json"), "utf8"));
const ranked = Object.entries(manifest)
.map(([key, entry]) => ({ key, entry, score: score(intent, key, entry) }))
@@ -41,8 +92,7 @@ export const bundledSfxProvider = {
const best = ranked[0];
if (!best) return null;
const localPath = join(LIB_DIR, best.entry.file);
if (!existsSync(localPath)) return null;
const localPath = join(libraryDir, best.entry.file);
return {
localPath,
ext: extensionForBundledSfxFile(best.entry.file),
@@ -1,9 +1,85 @@
import { strict as assert } from "node:assert";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { extensionForBundledSfxFile } from "./bundled-sfx-provider.mjs";
import {
BUNDLED_SFX_RECOVERY_COMMAND,
BundledSfxAssetsError,
bundledSfxProvider,
extensionForBundledSfxFile,
inspectBundledSfxAssets,
} 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");
});
test("reports an agent-friendly recovery when the bundled SFX manifest is absent", () => {
const libraryDir = mkdtempSync(join(tmpdir(), "media-use-sfx-missing-"));
try {
const health = inspectBundledSfxAssets(libraryDir);
assert.equal(health.ok, false);
assert.equal(health.code, "bundled_sfx_assets_missing");
assert.match(health.detail, /manifest\.json/);
assert.match(health.fix, /hyperframes skills update media-use/);
assert.equal(health.fix, BUNDLED_SFX_RECOVERY_COMMAND);
} finally {
rmSync(libraryDir, { recursive: true, force: true });
}
});
test("reports the exact missing file from an incomplete bundled SFX install", () => {
const libraryDir = mkdtempSync(join(tmpdir(), "media-use-sfx-incomplete-"));
try {
writeFileSync(
join(libraryDir, "manifest.json"),
JSON.stringify({ whoosh: { file: "whoosh.mp3", description: "transition" } }),
);
const health = inspectBundledSfxAssets(libraryDir);
assert.equal(health.ok, false);
assert.equal(health.code, "bundled_sfx_assets_missing");
assert.match(health.detail, /whoosh\.mp3/);
} finally {
rmSync(libraryDir, { recursive: true, force: true });
}
});
test("bundled provider raises a typed install error instead of a generic catalog miss", async () => {
const libraryDir = mkdtempSync(join(tmpdir(), "media-use-sfx-provider-"));
try {
await assert.rejects(
() => bundledSfxProvider.search("whoosh", { libraryDir }),
(error) => {
assert.ok(error instanceof BundledSfxAssetsError);
assert.equal(error.code, "bundled_sfx_assets_missing");
assert.match(error.message, /hyperframes skills update media-use/);
return true;
},
);
} finally {
rmSync(libraryDir, { recursive: true, force: true });
}
});
test("accepts a complete bundled SFX library", () => {
const libraryDir = mkdtempSync(join(tmpdir(), "media-use-sfx-complete-"));
try {
mkdirSync(libraryDir, { recursive: true });
writeFileSync(
join(libraryDir, "manifest.json"),
JSON.stringify({ whoosh: { file: "whoosh.mp3", description: "transition" } }),
);
writeFileSync(join(libraryDir, "whoosh.mp3"), "audio");
assert.deepEqual(inspectBundledSfxAssets(libraryDir), {
ok: true,
count: 1,
detail: "1 bundled SFX asset available",
fix: "",
});
} finally {
rmSync(libraryDir, { recursive: true, force: true });
}
});
+29 -5
View File
@@ -35,6 +35,10 @@ import {
flushHeygenFailureTracking,
versionLessThan,
} from "./lib/heygen-cli.mjs";
import {
BundledSfxAssetsError,
inspectBundledSfxAssets,
} from "./lib/bundled-sfx-provider.mjs";
const INGEST_TYPES = [...listTypes(), "video"];
@@ -339,9 +343,11 @@ async function run() {
// 3. provider search — registry tries providers in order (heygen-CLI first)
let searchResult = null;
let providerFailure = null;
try {
searchResult = await runCapability(type, "search", intent, ctx);
} catch {
} catch (error) {
providerFailure = error;
// search failed, try generate
}
@@ -349,7 +355,8 @@ async function run() {
if (!searchResult) {
try {
searchResult = await runCapability(type, "generate", intent, ctx);
} catch {
} catch (error) {
providerFailure ??= error;
// generate failed too
}
}
@@ -377,13 +384,23 @@ async function run() {
// brand stays local: no frame.md/design.md -> upsell the HyperFrames design
// flow rather than reporting a generic miss (B5).
const msg =
type === "brand"
providerFailure instanceof BundledSfxAssetsError
? providerFailure.message
: type === "brand"
? "no brand spec found — add a frame.md or design.md (colors/font/logo) to this project. Run the HyperFrames design flow to create one; brand tokens are read locally for deterministic rendering."
: args.provider
? `provider "${args.provider}" could not resolve ${type}: "${intent}"${localOnly ? " (--local-only skips network providers; drop it or the --provider override)" : ""}`
: `no provider could resolve ${type}: "${intent}"`;
if (args.json) {
console.log(JSON.stringify({ ok: false, error: msg }));
console.log(
JSON.stringify({
ok: false,
...(providerFailure instanceof BundledSfxAssetsError
? { code: providerFailure.code, fix: providerFailure.fix }
: {}),
error: msg,
}),
);
} else {
console.error(`error: ${msg}`);
}
@@ -852,6 +869,13 @@ function heygenAuthCheck() {
function runDoctor() {
const checks = [];
const bundledSfx = inspectBundledSfxAssets();
checks.push({
name: "bundled SFX assets",
ok: bundledSfx.ok,
detail: bundledSfx.detail,
fix: bundledSfx.fix,
});
const heygenVersionProbe = runCommand("heygen", ["--version"]);
const heygenOnPath = heygenVersionProbe.status === 0;
const heygenVersionText = commandText(heygenVersionProbe);
@@ -952,7 +976,7 @@ function runDoctor() {
// missing and then break at the first probe call.
const ffmpeg = checks.find((check) => check.name === "ffmpeg on PATH");
const ffprobe = checks.find((check) => check.name === "ffprobe on PATH");
return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks };
return { ok: bundledSfx.ok && !!ffmpeg?.ok && !!ffprobe?.ok, checks };
}
function printDoctor(checks) {
+27 -1
View File
@@ -145,6 +145,29 @@ test("bundled SFX resolve without HeyGen on PATH", () => {
cleanup();
});
test("missing bundled SFX install returns a typed recovery command", () => {
setup();
const missingLibrary = join(tmp, "missing-sfx-library");
const result = spawnResolve(
["--type", "sfx", "--intent", "whoosh", "--project", tmp, "--local-only", "--json"],
{
env: {
HOME: tmp,
PATH: tmp,
HYPERFRAMES_MEDIA_USE_SFX_DIR: missingLibrary,
},
},
);
assert.equal(result.status, 1, result.stderr);
const parsed = JSON.parse(result.stdout);
assert.equal(parsed.ok, false);
assert.equal(parsed.code, "bundled_sfx_assets_missing");
assert.equal(parsed.fix, "npx hyperframes skills update media-use");
assert.match(parsed.error, /bundled SFX assets are missing or incomplete/);
assert.match(parsed.error, /manifest not found/);
cleanup();
});
function writeFakeHeygen(body, exitCode = 0) {
const binDir = join(tmp, "bin");
mkdirSync(binDir, { recursive: true });
@@ -514,6 +537,7 @@ test("--doctor --json reports dependency checks and top-level ok requires ffmpeg
assert.ok(Array.isArray(parsed.checks));
const expected = [
"bundled SFX assets",
"heygen on PATH",
"heygen version",
"heygen authenticated",
@@ -532,7 +556,9 @@ test("--doctor --json reports dependency checks and top-level ok requires ffmpeg
const ffmpeg = byName.get("ffmpeg on PATH");
const ffprobe = byName.get("ffprobe on PATH");
const strictOk = ffmpeg.ok && ffprobe.ok;
const bundledSfx = byName.get("bundled SFX assets");
assert.match(bundledSfx.detail, /bundled SFX assets available/);
const strictOk = bundledSfx.ok && ffmpeg.ok && ffprobe.ok;
assert.equal(parsed.ok, strictOk);
assert.equal(result.status, strictOk ? 0 : 1);
});