Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"files": 10
},
"media-use": {
"hash": "a842eab0c9d3a0c0",
"hash": "7a51f53cf615fe97",
"files": 124
},
"motion-graphics": {
Expand Down
76 changes: 13 additions & 63 deletions skills/media-use/scripts/lib/bundled-sfx-provider.mjs
Original file line number Diff line number Diff line change
@@ -1,63 +1,7 @@
import { existsSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";

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 LIB_DIR = join(import.meta.dirname, "..", "..", "audio", "assets", "sfx");

const normalize = (value) =>
String(value)
Expand All @@ -79,11 +23,16 @@ function score(intent, key, entry) {
}

export const bundledSfxProvider = {
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"));
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) }))
Expand All @@ -92,7 +41,8 @@ export const bundledSfxProvider = {
const best = ranked[0];
if (!best) return null;

const localPath = join(libraryDir, best.entry.file);
const localPath = join(LIB_DIR, best.entry.file);
if (!existsSync(localPath)) return null;
return {
localPath,
ext: extensionForBundledSfxFile(best.entry.file),
Expand Down
78 changes: 1 addition & 77 deletions skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs
Original file line number Diff line number Diff line change
@@ -1,85 +1,9 @@
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 {
BUNDLED_SFX_RECOVERY_COMMAND,
BundledSfxAssetsError,
bundledSfxProvider,
extensionForBundledSfxFile,
inspectBundledSfxAssets,
} from "./bundled-sfx-provider.mjs";
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");
});

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 });
}
});
34 changes: 5 additions & 29 deletions skills/media-use/scripts/resolve.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@ import {
flushHeygenFailureTracking,
versionLessThan,
} from "./lib/heygen-cli.mjs";
import {
BundledSfxAssetsError,
inspectBundledSfxAssets,
} from "./lib/bundled-sfx-provider.mjs";

const INGEST_TYPES = [...listTypes(), "video"];

Expand Down Expand Up @@ -343,20 +339,17 @@ 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 (error) {
providerFailure = error;
} catch {
// search failed, try generate
}

// 4. generate fallback — same ordered cascade for the generate capability
if (!searchResult) {
try {
searchResult = await runCapability(type, "generate", intent, ctx);
} catch (error) {
providerFailure ??= error;
} catch {
// generate failed too
}
}
Expand Down Expand Up @@ -384,23 +377,13 @@ 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 =
providerFailure instanceof BundledSfxAssetsError
? providerFailure.message
: type === "brand"
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,
...(providerFailure instanceof BundledSfxAssetsError
? { code: providerFailure.code, fix: providerFailure.fix }
: {}),
error: msg,
}),
);
console.log(JSON.stringify({ ok: false, error: msg }));
} else {
console.error(`error: ${msg}`);
}
Expand Down Expand Up @@ -869,13 +852,6 @@ 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);
Expand Down Expand Up @@ -976,7 +952,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: bundledSfx.ok && !!ffmpeg?.ok && !!ffprobe?.ok, checks };
return { ok: !!ffmpeg?.ok && !!ffprobe?.ok, checks };
}

function printDoctor(checks) {
Expand Down
28 changes: 1 addition & 27 deletions skills/media-use/scripts/resolve.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -145,29 +145,6 @@ 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 });
Expand Down Expand Up @@ -537,7 +514,6 @@ 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",
Expand All @@ -556,9 +532,7 @@ 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 bundledSfx = byName.get("bundled SFX assets");
assert.match(bundledSfx.detail, /bundled SFX assets available/);
const strictOk = bundledSfx.ok && ffmpeg.ok && ffprobe.ok;
const strictOk = ffmpeg.ok && ffprobe.ok;
assert.equal(parsed.ok, strictOk);
assert.equal(result.status, strictOk ? 0 : 1);
});
Expand Down
Loading