Skip to content
Open
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
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@
"files": 10
},
"media-use": {
"hash": "49aa5adae0800403",
"files": 122
"hash": "facf3a0c03fb1bc5",
"files": 123
},
"motion-graphics": {
"hash": "dafcdce07d221aa4",
Expand Down
54 changes: 54 additions & 0 deletions skills/media-use/scripts/lib/bundled-sfx-provider.mjs
Original file line number Diff line number Diff line change
@@ -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 },
},
};
},
};
8 changes: 6 additions & 2 deletions skills/media-use/scripts/lib/registry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion skills/media-use/scripts/lib/registry.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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 },
Expand Down
14 changes: 14 additions & 0 deletions skills/media-use/scripts/resolve.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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" } });
Expand Down
Loading