feat(cli): route media search through media-use

This commit is contained in:
Miguel Angel Simon Sierra
2026-09-21 00:14:29 -04:00
parent 1c3b7d2b08
commit 7c79aba950
8 changed files with 88 additions and 129 deletions
+2
View File
@@ -102,6 +102,8 @@ async function main() {
join(mediaEngine, "resolve.mjs"),
join(DIST, "skills", "media-use", "scripts", "resolve.mjs"),
);
mkdirSync(join(DIST, "skills", "registry"), { recursive: true });
copyDirContents(join(DIST, "registry"), join(DIST, "skills", "registry"));
const dockerfile = join(CLI_ROOT, "src", "docker", "Dockerfile.render");
if (existsSync(dockerfile)) {
+1 -37
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { countUnindexed, pickByName, rankMediaRows, searchMissCommand } from "./catalog.js";
import { countUnindexed, pickByName, searchMissCommand } from "./catalog.js";
/** The whole registry, which is what "in this registry" has to be measured against. */
const registryNames = new Set(["fade-through", "whip-pan", "count-up"]);
@@ -63,42 +63,6 @@ describe("countUnindexed", () => {
});
});
describe("rankMediaRows", () => {
it("ranks the click SFX first for a click query", () => {
const rows = [
{
id: "whoosh",
kind: "sfx",
title: "whoosh",
description: "Fast scene transition accent",
tags: ["sfx"],
file: "skills/media-use/audio/assets/sfx/whoosh.mp3",
},
{
id: "click",
kind: "sfx",
title: "click",
description: "Crisp UI click for a button press or selection",
tags: ["sfx"],
file: "skills/media-use/audio/assets/sfx/click.mp3",
},
];
expect(rankMediaRows("click", rows).map((row) => row.id)).toEqual(["click"]);
});
});
describe("catalog --media", () => {
it("returns JSON media rows without entering the installable registry path", async () => {
const output = await runCatalog({ media: true, json: true, query: "click" });
expect(JSON.parse(output)).toMatchObject({
tier: "words",
total: 1,
results: [{ id: "click", file: "skills/media-use/audio/assets/sfx/click.mp3" }],
});
});
});
// ── The command envelope ────────────────────────────────────────────────────
// The JSON envelope is the surface an agent reads, so under-coverage and the
// score are pinned where they are actually published rather than only at the
-78
View File
@@ -30,70 +30,12 @@ import {
import { ensureLocalRuntime } from "../registry/localEmbedder.js";
import {
cachedLocalVectorRevision,
fetchMediaVectors,
fetchLocalVectors,
hasLocalVectors,
localSemanticRanking,
localVectorNames,
mediaSemanticRanking,
mediaVectorRows,
} from "../registry/localSemantic.js";
export interface MediaCatalogRow {
id: string;
kind: string;
title: string;
description: string;
tags: string[];
file: string;
duration?: number;
dimensions?: { width: number; height: number };
}
export function rankMediaRows(query: string, rows: MediaCatalogRow[]): MediaCatalogRow[] {
return searchByWords(query, rows, (row) => ({
strong: `${row.id} ${row.title}`,
weak: `${row.description} ${row.tags.join(" ")} ${row.kind}`,
}));
}
async function runMediaCatalog(query: string, registry: string): Promise<void> {
const fetched = await fetchMediaVectors(registry);
const rows = mediaVectorRows() as MediaCatalogRow[];
const warnings: string[] = [];
if (!fetched && rows.length === 0) {
warnings.push("media search unavailable: the media index could not be fetched");
}
let matching = rankMediaRows(query, rows);
let tier: "words" | "on-device" = "words";
let tierDetail = "local word match";
if (matching.length === 0 && localModelStatus().status === "ready") {
const ranking = await mediaSemanticRanking(query);
if (ranking) {
matching = ranking.map(({ row }) => row as MediaCatalogRow);
tier = "on-device";
tierDetail = "on-device meaning search";
}
}
console.log(
JSON.stringify(
{
query,
tier,
tier_detail: tierDetail,
shown: matching.length,
total: rows.length,
...(warnings.length ? { warnings } : {}),
results: matching,
},
null,
2,
),
);
}
/**
* Get the offline tier ready, and report every reason it could not be.
*
@@ -213,10 +155,6 @@ export default defineCommand({
description:
"Search by meaning when the on-device model is on, otherwise by name, title, description and tags",
},
media: {
type: "boolean",
description: "Search the media-use asset corpus instead of installable registry items",
},
yes: {
type: "boolean",
alias: "y",
@@ -239,22 +177,6 @@ export default defineCommand({
const dir = resolve(process.cwd());
const config = loadProjectConfig(dir) ?? DEFAULT_PROJECT_CONFIG;
if (args.media === true) {
if (!json) {
console.error(
"--media requires --json because media rows are not installable registry items.",
);
finishCommand(1);
}
const query = typeof args.query === "string" ? args.query.trim() : "";
if (!query) {
console.error("--media requires --query.");
finishCommand(1);
}
await runMediaCatalog(query, config.registry);
return;
}
let typeFilter: ItemType | undefined;
if (args.type === "block") typeFilter = "hyperframes:block";
else if (args.type === "component") typeFilter = "hyperframes:component";
@@ -1,5 +1,6 @@
import { existsSync, readFileSync } from "node:fs";
import { extname, join } from "node:path";
import { rankMediaRows } from "../../registry/mediaSearch.js";
const LIB_DIR =
process.env.HYPERFRAMES_MEDIA_USE_SFX_DIR ||
@@ -101,11 +102,20 @@ export const bundledSfxProvider = {
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) }))
.filter(({ entry, score }) => entry?.file && score > 0)
.sort((a, b) => b.score - a.score || a.key.localeCompare(b.key));
const best = ranked[0];
const rows = Object.entries(manifest).map(([key, entry]) => ({
id: key,
kind: "sfx",
title: key,
description: entry?.description || key,
tags: ["sfx"],
file: entry?.file || "",
duration: entry?.duration,
}));
const bestRow = rankMediaRows(intent, rows)[0];
const best =
bestRow && manifest[bestRow.id]?.file
? { key: bestRow.id, entry: manifest[bestRow.id] }
: null;
if (!best) return null;
const localPath = join(libraryDir, best.entry.file);
+1 -1
View File
@@ -51,8 +51,8 @@ const P = (name, caps) => ({ name, network: true, paid: true, ...caps }); // rem
const REGISTRY = {
bgm: [N("heygen.audio.sounds", { search: bgmProvider.search })],
sfx: [
N("heygen.audio.sounds", { search: sfxProvider.search }),
A("bundled.sfx", { search: bundledSfxProvider.search }),
N("heygen.audio.sounds", { search: sfxProvider.search }),
],
image: [
N("heygen.asset.search", { search: imageProvider.search }),
@@ -84,13 +84,13 @@ test("video cascade: HeyGen first, LTX local fallback, generate-only", async ()
assert.equal(await runCapability("video", "search", "x", {}), null);
});
test("sfx cascade: HeyGen catalog first, bundled library remains the local fallback", () => {
test("sfx cascade: bundled library first, HeyGen catalog remains the network 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");
assert.equal(ps[0].name, "bundled.sfx");
assert.equal(typeof ps[0].search, "function");
assert.ok(!ps[0].network, "bundled SFX remain available offline");
assert.equal(ps[1].name, "heygen.audio.sounds");
assert.ok(ps[1].network, "HeyGen SFX catalog is network-only");
});
test("ctx.provider forces one generator (e.g. 'make an image WITH codex')", async () => {
+40 -2
View File
@@ -49,6 +49,8 @@ import {
versionLessThan,
} from "./lib/heygen-cli.mjs";
import { BundledSfxAssetsError, inspectBundledSfxAssets } from "./lib/bundled-sfx-provider.mjs";
import { rankMediaRowsWithVectors } from "../registry/mediaSearch.js";
import { fetchMediaVectors, mediaVectorRows } from "../registry/localSemantic.js";
const INGEST_TYPES = listTypes();
const DEFAULT_EXT = {
@@ -408,11 +410,47 @@ async function run() {
return resolveColor(type, intent, { projectDir });
}
// 3. provider search — registry tries providers in order (heygen-CLI first)
// 3. SFX is local-first: bundled library, committed local index, then HeyGen.
let searchResult = null;
let providerFailure = null;
try {
searchResult = await runCapability(type, "search", intent, ctx);
if (type === "sfx" && !args.provider) {
searchResult = await runCapability(type, "search", intent, {
...ctx,
provider: "bundled.sfx",
});
if (!searchResult) {
const registry =
process.env.HYPERFRAMES_REGISTRY ||
"https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry";
await fetchMediaVectors(registry);
const ranked = await rankMediaRowsWithVectors(
intent,
mediaVectorRows().filter((row) => row.kind === "sfx"),
);
const row = ranked.rows[0];
const candidates = row
? [resolve(row.file), join(import.meta.dirname, "..", "..", "..", row.file)]
: [];
const localPath = candidates.find((candidate) => existsSync(candidate));
if (row && localPath) {
searchResult = {
localPath,
ext: extname(localPath),
source: "local-index",
metadata: {
description: row.description,
duration: row.duration ?? null,
provider: "catalog.local",
provenance: { library_key: row.id, tier: ranked.tier },
},
};
}
}
if (!searchResult) searchResult = await runCapability(type, "search", intent, ctx);
} else {
searchResult = await runCapability(type, "search", intent, ctx);
}
} catch (error) {
providerFailure = error;
// search failed, try generate
+23
View File
@@ -0,0 +1,23 @@
import { searchByWords } from "./localSearch.js";
import { mediaSemanticRanking, type MediaVectorRow } from "./localSemantic.js";
export type MediaSearchRow = MediaVectorRow;
export function rankMediaRows(query: string, rows: MediaSearchRow[]): MediaSearchRow[] {
return searchByWords(query, rows, (row) => ({
strong: `${row.id} ${row.title}`,
weak: `${row.description} ${row.tags.join(" ")} ${row.kind}`,
}));
}
export async function rankMediaRowsWithVectors(
query: string,
rows: MediaSearchRow[],
): Promise<{ rows: MediaSearchRow[]; tier: "words" | "on-device" }> {
const words = rankMediaRows(query, rows);
if (words.length > 0) return { rows: words, tier: "words" };
const semantic = await mediaSemanticRanking(query);
return semantic
? { rows: semantic.map(({ row }) => row), tier: "on-device" }
: { rows: [], tier: "words" };
}