[teamai] Push 87 resource(s) from XingfenD

This commit is contained in:
2026-09-10 16:10:45 +08:00
parent 425c9c078a
commit 65c04def51
1314 changed files with 211681 additions and 0 deletions
@@ -0,0 +1,128 @@
import { readdirSync, statSync, existsSync } from "node:fs";
import { join, extname, basename } from "node:path";
import { readManifest, appendRecord, nextId } from "./manifest.mjs";
import { regenerateIndex } from "./index-gen.mjs";
import { probe } from "./probe.mjs";
import { matchTokens } from "./match.mjs";
const AUDIO_EXT = new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac"]);
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"]);
const VIDEO_EXT = new Set([".mp4", ".webm", ".mov"]);
function inferType(filePath) {
const ext = extname(filePath).toLowerCase();
if (AUDIO_EXT.has(ext)) {
const lower = filePath.toLowerCase();
if (lower.includes("/bgm/") || lower.includes("/music/") || lower.startsWith("bgm/"))
return "bgm";
if (lower.includes("/sfx/") || lower.includes("/sound") || lower.startsWith("sfx/"))
return "sfx";
if (lower.includes("/voice/") || lower.includes("/narrat") || lower.startsWith("voice/"))
return "voice";
return "bgm";
}
if (IMAGE_EXT.has(ext)) {
if (ext === ".svg" || ext === ".ico") return "icon";
return "image";
}
if (VIDEO_EXT.has(ext)) return "video";
return null;
}
function walkDir(dir, base = "") {
const files = [];
if (!existsSync(dir)) return files;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const rel = base ? `${base}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
files.push(...walkDir(join(dir, entry.name), rel));
} else {
files.push(rel);
}
}
return files;
}
export function scanExistingAssets(projectDir) {
const assetsDir = join(projectDir, "assets");
if (!existsSync(assetsDir)) return [];
const files = walkDir(assetsDir);
const found = [];
for (const rel of files) {
const type = inferType(rel);
if (!type) continue;
const fullPath = join(assetsDir, rel);
const stat = statSync(fullPath);
if (stat.size === 0) {
// A 0-byte asset would register clean but fail at render — skip it loudly
// rather than adopt a broken file.
console.error(`media-use: skipping 0-byte asset assets/${rel}`);
continue;
}
const meta = probe(fullPath);
found.push({
relativePath: `assets/${rel}`,
type,
size: stat.size,
name: basename(rel, extname(rel)),
...meta,
});
}
return found;
}
export function adoptExistingAssets(projectDir) {
const existing = scanExistingAssets(projectDir);
if (existing.length === 0) return [];
const manifest = readManifest(projectDir);
const knownPaths = new Set(manifest.map((r) => r.path));
const adopted = [];
for (const asset of existing) {
if (knownPaths.has(asset.relativePath)) continue;
const id = nextId(projectDir, asset.type);
const record = {
id,
type: asset.type,
path: asset.relativePath,
source: "existing",
description: asset.name.replace(/[-_]/g, " "),
...(asset.duration != null && { duration: asset.duration }),
...(asset.width != null && { width: asset.width }),
...(asset.height != null && { height: asset.height }),
provenance: { provider: "local", adopted: true },
};
appendRecord(projectDir, record);
adopted.push(record);
}
if (adopted.length > 0) regenerateIndex(projectDir);
return adopted;
}
// Adopt a pre-existing assets/ file only when it shares a meaningful word with
// the intent. The old test — `name.includes(intent) || intent.includes(name)` —
// silently returned the WRONG file: "whoosh" grabbed a stray who.mp3, and a
// one-letter filename matched every intent. A false negative just falls through
// to a catalog search (safe); a false positive ships the wrong asset. So bias to
// precision: require a shared token, don't guess from substrings.
export function findExistingAsset(projectDir, intent, type) {
const assetsDir = join(projectDir, "assets");
if (!existsSync(assetsDir)) return null;
const intentTokens = matchTokens(intent);
if (intentTokens.size === 0) return null;
for (const rel of walkDir(assetsDir)) {
const t = inferType(rel);
if (!t || (type && t !== type)) continue;
const stem = basename(rel, extname(rel));
for (const tok of matchTokens(stem)) {
if (intentTokens.has(tok)) {
return { relativePath: `assets/${rel}`, type: t, name: stem };
}
}
}
return null;
}
@@ -0,0 +1,95 @@
import { strict as assert } from "node:assert";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { findExistingAsset, adoptExistingAssets } from "./adopt.mjs";
let tmp;
function setup() {
tmp = mkdtempSync(join(tmpdir(), "mu-adopt-test-"));
}
function cleanup() {
if (tmp) rmSync(tmp, { recursive: true, force: true });
}
function drop(rel) {
const full = join(tmp, "assets", rel);
mkdirSync(join(full, ".."), { recursive: true });
writeFileSync(full, "x");
}
function runTests() {
const tests = [];
const test = (name, fn) => tests.push({ name, fn });
test("does NOT false-match a short filename stem (who.mp3 vs 'whoosh')", () => {
setup();
drop("sfx/who.mp3");
assert.equal(findExistingAsset(tmp, "whoosh", "sfx"), null);
cleanup();
});
test("does NOT match a one-letter filename against any intent", () => {
setup();
drop("images/a.jpg");
assert.equal(findExistingAsset(tmp, "gradient tech background", "image"), null);
cleanup();
});
test("matches on a shared meaningful word", () => {
setup();
drop("images/hero-shot.jpg");
const hit = findExistingAsset(tmp, "hero image", "image");
assert.ok(hit);
assert.equal(hit.relativePath, "assets/images/hero-shot.jpg");
cleanup();
});
test("matches multi-word overlap", () => {
setup();
drop("images/gradient-tech-bg.jpg");
assert.ok(findExistingAsset(tmp, "gradient tech background", "image"));
cleanup();
});
test("a shared stopword alone does not match", () => {
setup();
drop("video/the-video.mp4");
assert.equal(findExistingAsset(tmp, "the rocket", null), null);
cleanup();
});
test("respects the type filter", () => {
setup();
drop("sfx/rocket.mp3");
assert.equal(findExistingAsset(tmp, "rocket", "image"), null, "wrong type is skipped");
assert.ok(findExistingAsset(tmp, "rocket", "sfx"), "right type matches");
cleanup();
});
test("adoptExistingAssets still imports every typed file (unaffected by match rule)", () => {
setup();
drop("sfx/who.mp3");
drop("images/a.jpg");
assert.equal(adoptExistingAssets(tmp).length, 2);
cleanup();
});
let passed = 0;
let failed = 0;
for (const { name, fn } of tests) {
try {
fn();
passed++;
console.log(` \x1b[32m✓\x1b[0m ${name}`);
} catch (err) {
failed++;
console.log(` \x1b[31m✗\x1b[0m ${name}`);
console.log(` ${err.message}`);
}
}
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
console.log("media-use · adopt / findExistingAsset tests\n");
runTests();
@@ -0,0 +1,20 @@
import { heygenSearch } from "./heygen-search.mjs";
export const bgmProvider = {
async search(intent) {
const results = heygenSearch("audio sounds list", intent, { type: "music" });
if (!results) return null;
const best = results[0];
return {
url: best.audio_url,
source: "search",
// ext derived from audio_url by resolve.mjs — catalog tracks are .mp3 or .wav
metadata: {
description: best.description || intent,
duration: best.duration || null,
provider: "heygen.audio.sounds",
provenance: { track_id: best.id, score: best.score, query: intent },
},
};
},
};
@@ -0,0 +1,59 @@
import { readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
function findDesignSpec(projectDir) {
for (const name of ["frame.md", "design.md", "DESIGN.md"]) {
const p = join(projectDir, name);
if (existsSync(p)) return { path: p, name };
}
return null;
}
function parseFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
const yaml = match[1];
const tokens = {};
for (const line of yaml.split("\n")) {
const m = line.match(/^\s*(\w[\w-]*):\s*(.+)/);
if (m) tokens[m[1]] = m[2].trim().replace(/^["']|["']$/g, "");
}
return tokens;
}
function extractColors(tokens) {
const colors = [];
for (const [k, v] of Object.entries(tokens)) {
if (typeof v === "string" && /^#[0-9a-fA-F]{3,8}$/.test(v)) {
colors.push({ name: k, hex: v });
}
}
return colors;
}
export const brandProvider = {
async search(intent, { projectDir } = {}) {
if (!projectDir) return null;
const spec = findDesignSpec(projectDir);
if (!spec) return null;
const content = readFileSync(spec.path, "utf8");
const tokens = parseFrontmatter(content);
if (!tokens) return null;
const colors = extractColors(tokens);
return {
localPath: spec.path,
source: "local",
ext: ".md",
metadata: {
description: "Brand tokens from " + spec.name,
provider: "design_spec",
provenance: {
file: spec.name,
colors,
font: tokens.font || tokens.typography || null,
logo: tokens.logo || null,
},
},
};
},
};
@@ -0,0 +1,108 @@
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 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, 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) }))
.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(libraryDir, best.entry.file);
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,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 {
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 });
}
});
@@ -0,0 +1,150 @@
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
import { join, basename } from "node:path";
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { readManifest, appendRecord, normalizePrompt } from "./manifest.mjs";
const SCHEMA_PREFIX = "mu-v1-";
const KEY_HEX_CHARS = 16;
const COMPLETE_SENTINEL = ".hf-complete";
export function globalMediaDir() {
return join(homedir(), ".media");
}
export function contentHash(filePath) {
const bytes = readFileSync(filePath);
return createHash("sha256").update(bytes).digest("hex");
}
function cacheEntryDir(rootDir, sha) {
return join(rootDir, SCHEMA_PREFIX + sha.slice(0, KEY_HEX_CHARS));
}
function isComplete(entryDir) {
return existsSync(join(entryDir, COMPLETE_SENTINEL));
}
function markComplete(entryDir) {
writeFileSync(join(entryDir, COMPLETE_SENTINEL), "", "utf8");
}
// The manifest helpers append their own ".media" to the dir they get, so the
// global manifest must be addressed by HOME, not by globalMediaDir() — passing
// the latter nested it at ~/.media/.media/manifest.jsonl, invisible to the
// Studio /api/assets/global route (which reads the documented flat path).
export function readGlobalManifest() {
return readManifest(homedir());
}
// Resolve a content-sha (full or unambiguous prefix) to a reusable global-cache
// record, for `resolve --reuse <sha>`. Returns null on no match, or
// { ambiguous: true, count } when a prefix matches multiple distinct entries.
// Completeness (the .hf-complete sentinel) is left to importFromCache so the
// caller can surface an "incomplete cache entry" error distinctly from a miss.
export function findGlobalBySha(shaPrefix) {
const p = String(shaPrefix || "")
.toLowerCase()
.trim();
if (!p) return null;
const matches = readGlobalManifest().filter(
(r) => r.reusable && typeof r.sha === "string" && r.sha.startsWith(p),
);
if (matches.length === 0) return null;
if (matches.length > 1) {
const exact = matches.find((r) => r.sha === p);
if (exact) return exact;
return { ambiguous: true, count: matches.length };
}
return matches[0];
}
function validateCacheHit(match) {
if (!match?.sha) return null;
return isComplete(cacheEntryDir(globalMediaDir(), match.sha)) ? match : null;
}
export function cacheGet(prompt, type) {
const key = normalizePrompt(prompt);
if (!key) return null;
return validateCacheHit(
readGlobalManifest().find(
(r) =>
r.reusable &&
normalizePrompt(r.provenance?.prompt) === key &&
(type == null || r.type === type),
),
);
}
export function cacheGetByEntity(entity) {
const lower = entity.toLowerCase();
return validateCacheHit(
readGlobalManifest().find((r) => r.reusable && r.entity && r.entity.toLowerCase() === lower),
);
}
export function cachePut(filePath, record) {
const sha = contentHash(filePath);
// Idempotent: same content already promoted -> don't duplicate the global
// record. ponytail: skips usage_count bump; add it when the metric is needed.
const existing = readGlobalManifest().find((r) => r.sha === sha);
if (existing) return { sha, cached_path: existing.cached_path, deduped: true };
const dir = globalMediaDir();
const entryDir = cacheEntryDir(dir, sha);
mkdirSync(entryDir, { recursive: true });
const dest = join(entryDir, basename(filePath));
copyFileSync(filePath, dest);
markComplete(entryDir);
const globalRecord = {
...record,
sha,
reusable: true,
cached_path: dest,
};
appendRecord(homedir(), globalRecord);
return { sha, cached_path: dest };
}
export function importFromCache(cacheRecord, projectDir, localId, localPath) {
const sha = cacheRecord.sha;
const entryDir = cacheEntryDir(globalMediaDir(), sha);
if (!isComplete(entryDir)) return null;
const cachedFile = cacheRecord.cached_path;
if (!cachedFile || !existsSync(cachedFile)) return null;
mkdirSync(join(projectDir, ".media"), { recursive: true });
const fullDest = join(projectDir, localPath);
mkdirSync(join(fullDest, ".."), { recursive: true });
copyFileSync(cachedFile, fullDest);
const projectRecord = {
...cacheRecord,
id: localId,
path: localPath,
provenance: {
...cacheRecord.provenance,
imported_from: sha,
},
};
delete projectRecord.sha;
delete projectRecord.reusable;
delete projectRecord.cached_path;
return projectRecord;
}
export function promote(projectDir, id) {
const records = readManifest(projectDir);
const record = records.find((r) => r.id === id);
if (!record) throw new Error(`asset not found in project manifest: ${id}`);
const filePath = join(projectDir, record.path);
if (!existsSync(filePath)) throw new Error(`asset file not found: ${filePath}`);
return cachePut(filePath, record);
}
@@ -0,0 +1,91 @@
// Reuse candidates: a side-effect-free view of assets already available to this
// project (its own manifest) and across every project (the global ~/.media
// cache), so the calling agent can judge semantic fit itself. No download, no
// provider, no mutation. The ranker only *surfaces* — it orders by lexical
// overlap but never filters a candidate out on zero overlap (that would
// pre-empt the agent's judgment); the agent does the semantic call.
import { readManifest } from "./manifest.mjs";
import { readGlobalManifest } from "./cache.mjs";
import { tokenOverlap, typesMatch } from "./match.mjs";
export const CANDIDATE_CAP = 8;
function shape(record, scope, intent) {
const description = record.description || record.provenance?.prompt || "";
const prompt = record.provenance?.prompt || null;
return {
id: record.id,
type: record.type,
scope,
description,
prompt,
provider: record.provenance?.provider || null,
duration: record.duration ?? null,
width: record.width ?? null,
height: record.height ?? null,
// Only global records carry a content sha — it is the stable reuse handle
// for `resolve --reuse <sha>`. Project assets are reused by referencing
// their path directly, so they need no handle.
sha: scope === "global" ? record.sha || null : null,
path: scope === "project" ? record.path : null,
score: intent ? tokenOverlap(intent, `${description} ${prompt || ""}`) : 0,
};
}
// Rank one scope: type-matched (icon<->image aware), newest-first within equal
// overlap, ordered by overlap desc. Returns the full ranked list (uncapped).
function rankScope(records, scope, type, intent) {
return records
.filter((r) => typesMatch(r.type, type))
.reverse() // manifest is append-order (oldest first); newest-first at equal score
.map((r) => shape(r, scope, intent))
.sort((a, b) => b.score - a.score); // Array.sort is stable → recency preserved
}
// List reuse candidates for `type`, capped per scope. Returns:
// candidates: capped project candidates followed by capped global candidates
// truncated: true if either scope had more than `cap`
// total: { project, global } counts before the cap (machine-readable)
// similar: count of candidates with lexical overlap > 0 (drives the nudge)
export function listCandidates({ projectDir, type, intent = "", cap = CANDIDATE_CAP }) {
const project = rankScope(readManifest(projectDir), "project", type, intent);
const global = rankScope(readGlobalManifest(), "global", type, intent);
const candidates = [...project.slice(0, cap), ...global.slice(0, cap)];
return {
candidates,
truncated: project.length > cap || global.length > cap,
total: { project: project.length, global: global.length },
similar: [...project, ...global].filter((c) => c.score > 0).length,
};
}
function meta(c) {
const parts = [];
if (c.duration != null) parts.push(`${c.duration}s`);
if (c.width && c.height) parts.push(`${c.width}x${c.height}`);
if (c.provider) parts.push(c.provider);
return parts.join(", ");
}
// Human-readable listing. The agent can read this directly; --json is for
// programmatic use. Reuse handle differs by scope: path for project, sha for
// global.
export function formatCandidates(candidates, { truncated, total } = {}) {
if (candidates.length === 0) return "no reuse candidates found (project or global cache)";
const lines = [`${candidates.length} reuse candidate${candidates.length === 1 ? "" : "s"}:`, ""];
for (const c of candidates) {
const handle =
c.scope === "global" ? `--reuse ${String(c.sha).slice(0, 16)}` : c.path || `manifest:${c.id}`;
const m = meta(c);
lines.push(` [${c.scope}] ${c.description}${m ? ` (${m})` : ""}`);
lines.push(` ${handle}`);
}
if (truncated && total) {
lines.push("");
lines.push(
` (showing top ${CANDIDATE_CAP} per scope; ${total.project} project / ${total.global} global total — refine --intent to narrow)`,
);
}
return lines.join("\n");
}
@@ -0,0 +1,155 @@
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { listCandidates, formatCandidates, CANDIDATE_CAP } from "./candidates.mjs";
import { findGlobalBySha } from "./cache.mjs";
// candidates + findGlobalBySha are offline (no heygen), so we can override HOME
// to a temp dir and seed a fake global ~/.media manifest deterministically.
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-cand-"));
const project = join(root, "proj");
const home = join(root, "home");
process.env.HOME = home;
return { root, project, home };
}
function seedManifest(dir, records) {
const md = join(dir, ".media");
mkdirSync(md, { recursive: true });
writeFileSync(md + "/manifest.jsonl", records.map((r) => JSON.stringify(r)).join("\n") + "\n");
}
function proj(id, type, description, prompt) {
return { id, type, path: `.media/audio/bgm/${id}.wav`, description, provenance: { prompt } };
}
function glob(id, type, description, prompt, sha) {
return {
id,
type,
sha,
reusable: true,
cached_path: `/x/${sha}/${id}.wav`,
description,
provenance: { prompt, provider: "heygen.audio.sounds" },
};
}
test("ranks project + global by overlap, tags scope", () => {
const { root, project, home } = sandbox();
try {
seedManifest(project, [proj("bgm_001", "bgm", "calm ambient piano", "calm ambient piano")]);
seedManifest(home, [
glob("bgm_009", "bgm", "energetic tech launch", "energetic tech launch", "a".repeat(64)),
glob("bgm_010", "bgm", "sad corporate piano", "sad corporate piano", "b".repeat(64)),
]);
const { candidates } = listCandidates({
projectDir: project,
type: "bgm",
intent: "tech launch",
});
assert.equal(candidates[0].scope, "project"); // project listed first
const g = candidates.filter((c) => c.scope === "global");
assert.equal(g[0].description, "energetic tech launch"); // higher overlap ranks first
assert.ok(g[0].score >= g[1].score);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("zero-overlap intent still lists candidates (no hard filter)", () => {
const { root, project, home } = sandbox();
try {
seedManifest(project, []);
seedManifest(home, [glob("bgm_009", "bgm", "driving synth", "driving synth", "c".repeat(64))]);
const { candidates, similar } = listCandidates({
projectDir: project,
type: "bgm",
intent: "totally unrelated words xyz",
});
assert.equal(candidates.length, 1, "listed despite zero overlap");
assert.equal(candidates[0].score, 0);
assert.equal(similar, 0, "similar counts only overlap>0");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("caps per scope and reports truncation + totals", () => {
const { root, project, home } = sandbox();
try {
const many = Array.from({ length: CANDIDATE_CAP + 3 }, (_, i) =>
glob(`bgm_${i}`, "bgm", `track ${i}`, `track ${i}`, String(i).padStart(64, "0")),
);
seedManifest(project, []);
seedManifest(home, many);
const { candidates, truncated, total } = listCandidates({ projectDir: project, type: "bgm" });
assert.equal(candidates.length, CANDIDATE_CAP);
assert.equal(truncated, true);
assert.equal(total.global, CANDIDATE_CAP + 3);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("honors icon<->image adjacency", () => {
const { root, project, home } = sandbox();
try {
seedManifest(project, []);
seedManifest(home, [glob("image_1", "image", "rocket logo", "rocket logo", "d".repeat(64))]);
const { candidates } = listCandidates({ projectDir: project, type: "icon", intent: "rocket" });
assert.equal(candidates.length, 1, "image asset surfaces for icon request");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("sha only on global, path only on project", () => {
const { root, project, home } = sandbox();
try {
seedManifest(project, [proj("bgm_001", "bgm", "x", "x")]);
seedManifest(home, [glob("bgm_009", "bgm", "y", "y", "e".repeat(64))]);
const { candidates } = listCandidates({ projectDir: project, type: "bgm" });
const p = candidates.find((c) => c.scope === "project");
const g = candidates.find((c) => c.scope === "global");
assert.ok(p.path && !p.sha);
assert.ok(g.sha && !g.path);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("findGlobalBySha resolves unique prefix, flags ambiguity, misses cleanly", () => {
const { root, project, home } = sandbox();
try {
seedManifest(project, []);
seedManifest(home, [
glob("bgm_1", "bgm", "a", "a", "abc" + "0".repeat(61)),
glob("bgm_2", "bgm", "b", "b", "abd" + "0".repeat(61)),
glob("bgm_3", "bgm", "c", "c", "fff" + "0".repeat(61)),
]);
assert.equal(findGlobalBySha("fff").id, "bgm_3", "unique prefix resolves");
assert.deepEqual(
{ ambiguous: findGlobalBySha("ab").ambiguous, count: findGlobalBySha("ab").count },
{ ambiguous: true, count: 2 },
"ambiguous prefix flagged",
);
assert.equal(findGlobalBySha("zzz"), null, "miss returns null");
assert.equal(findGlobalBySha(""), null, "empty returns null");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("formatCandidates shows reuse handles by scope; empty message", () => {
const { candidates } = {
candidates: [
{ scope: "project", description: "p", path: ".media/audio/bgm/bgm_001.wav" },
{ scope: "global", description: "g", sha: "f".repeat(64) },
],
};
const out = formatCandidates(candidates, {});
assert.match(out, /\.media\/audio\/bgm\/bgm_001\.wav/);
assert.match(out, /--reuse ffffffffffffffff/);
assert.match(formatCandidates([], {}), /no reuse candidates/);
});
@@ -0,0 +1,161 @@
import { execFileSync } from "node:child_process";
import { copyFileSync, existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
// Image generation via the OpenAI Codex CLI's built-in image tool (gpt-image-2)
// on the user's ChatGPT subscription: the codex CLI owns auth, media-use holds
// no key (CLI-only). The image UPSELL behind local mflux; skipped by --local-only.
//
// Retrieval mirrors illo-skill rather than trusting the model to save a file:
// `--enable image_generation` makes the built-in tool drop the rendered artifact
// into $CODEX_HOME/generated_images/, and we fetch the freshest file that
// postdates this run. The save-to-path instruction is only a best-effort
// verify-first.
//
// The flag was called `imagegenext` until Codex CLI ~0.145, which renamed it to
// `image_generation` and dropped the old row from `codex features list`. Both
// the gate below and the --enable arg track the new name, with a fallback to
// the old one so older CLIs keep working.
const TIMEOUT_MS = 600000; // codex exec round-trips the sub; first-run tool spin-up is slow
const MTIME_SKEW_MS = 2000; // tolerate mtime granularity / clock skew (illo uses 2s)
function codexGeneratedDir() {
// Codex relocates CODEX_HOME on some hosts, so resolve it at run time.
return join(process.env.CODEX_HOME || join(homedir(), ".codex"), "generated_images");
}
// Newest artifact that postdates `sinceMs` (minus skew), so a stale prior render
// or a concurrent session's file can't be mistaken for this run's output.
function freshestGeneratedImage(sinceMs) {
const dir = codexGeneratedDir();
if (!existsSync(dir)) return null;
const floor = sinceMs - MTIME_SKEW_MS;
let best = null;
for (const name of readdirSync(dir)) {
let st;
try {
st = statSync(join(dir, name));
} catch {
continue;
}
if (!st.isFile() || st.mtimeMs < floor) continue;
if (!best || st.mtimeMs > best.mtimeMs) best = { path: join(dir, name), mtimeMs: st.mtimeMs };
}
return best?.path ?? null;
}
// Short `codex` subcommand → combined stdout+stderr, or null if it can't run.
function codexRun(args) {
try {
return execFileSync("codex", args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 10000,
});
} catch (err) {
return `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}` || null;
}
}
// Name of the feature flag this CLI uses for the extended image tool, or null if
// it exposes neither. Newer CLIs list only `image_generation`; older ones list
// `imagegenext` alongside it and reject the new name on --enable.
export function imageGenFlag(feats) {
if (/\bimagegenext\b/.test(feats)) return "imagegenext";
if (/\bimage_generation\b/.test(feats)) return "image_generation";
return null;
}
// Fail-fast host check (mirrors illo): don't burn a minutes-long exec when Codex
// isn't usable. Returns { flag } when ready, else { reason }. The flag ships
// default-disabled, so we check the ROW is present (the capability signal) — the
// exec enables it per-render with --enable.
function codexUnavailableReason() {
try {
const which = process.platform === "win32" ? "where" : "which";
execFileSync(which, ["codex"], { stdio: ["ignore", "ignore", "ignore"], timeout: 5000 });
} catch {
// A shell alias (e.g. `codex → /Applications/Codex.app/...`) is NOT enough:
// aliases live only in the interactive shell, so a spawned subprocess's PATH
// lookup can't see them. Symlink the real binary onto PATH.
return {
reason:
'codex CLI not reachable on PATH (a shell alias won\'t work — spawned processes can\'t see aliases; symlink the real binary onto PATH, e.g. ln -s "$(readlink -f "$(command -v codex)")" ~/.local/bin/codex)',
};
}
// Auth marker: presence of the credentials file, NOT `codex login status`.
// That command prints "Logged in using ChatGPT" only to a human stream
// (stderr / TTY) and exits 0, so its piped stdout — how media-use spawns it —
// is empty, and the gate falsely reported "not logged in", blocking codex
// image gen in every headless / CI / agent run even when fully authed.
// auth.json is the durable, TTY-independent signal; token validity is proven
// by the exec itself, which fails cleanly if the login is stale.
const authPath = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json");
if (!existsSync(authPath)) return { reason: "codex not logged in (run: codex login)" };
const feats = codexRun(["features", "list"]);
if (feats == null) return { reason: "could not read `codex features list`" };
const flag = imageGenFlag(feats);
if (!flag) return { reason: "codex image generation feature unavailable (upgrade Codex CLI)" };
return { flag };
}
export async function codexImageGenerate(intent) {
const { reason, flag } = codexUnavailableReason();
if (reason) {
console.error(`media-use: codex image upsell unavailable: ${reason}`);
return null;
}
const outPath = join(tmpdir(), `media-use-codex-${process.pid}-${Date.now()}.png`);
const prompt =
`${intent}\n\n` +
`Use your built-in image generation tool to render this, then save the image ` +
`to ${outPath} (overwrite if it exists). Do not ask for confirmation. ` +
`If you have no built-in image tool, do nothing (no PIL/matplotlib/SVG substitute).`;
try {
unlinkSync(outPath); // clear any prior file so verify-first can't accept a stale render
} catch {
/* no prior file */
}
const started = Date.now();
try {
execFileSync(
"codex",
[
"exec",
"--cd",
tmpdir(),
"-s",
"workspace-write",
"--skip-git-repo-check",
"--enable",
flag,
"-",
],
{ input: prompt, encoding: "utf8", timeout: TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"] },
);
} catch (err) {
console.error(
`media-use: \`codex exec\` image generation failed: ${err.stderr?.toString().trim().slice(-200) || err.message}`,
);
return null;
}
// Verify-first (save-to-path may have worked), else fetch the image-tool artifact.
const produced =
existsSync(outPath) && statSync(outPath).size > 0 ? outPath : freshestGeneratedImage(started);
if (!produced) return null;
if (produced !== outPath) {
try {
copyFileSync(produced, outPath);
} catch {
return null;
}
}
return {
localPath: outPath,
ext: ".png",
source: "generated",
metadata: { description: intent, provider: "codex.image_gen", provenance: { prompt: intent } },
};
}
@@ -0,0 +1,34 @@
import test from "node:test";
import assert from "node:assert";
import { imageGenFlag } from "./codex-provider.mjs";
// Real `codex features list` output is three space-padded columns:
// <name> <stage> <enabled>
const OLD_CLI = [
"image_detail_original removed false",
"image_generation stable true",
"imagegenext under development false",
].join("\n");
const NEW_CLI = [
"image_detail_original removed false",
"image_generation stable true",
].join("\n");
test("older CLIs keep the old flag — they reject --enable image_generation", () => {
assert.equal(imageGenFlag(OLD_CLI), "imagegenext");
});
test("Codex CLI >=0.145 dropped the imagegenext row; fall back to its new name", () => {
// The regression this guards: gating on `imagegenext` alone made the provider
// report "unavailable" on every up-to-date CLI, without attempting a render.
assert.equal(imageGenFlag(NEW_CLI), "image_generation");
});
test("no image feature at all is unavailable, not a silent default", () => {
assert.equal(imageGenFlag("apps stable true\nhooks stable true"), null);
});
test("substring lookalikes do not count as the feature row", () => {
assert.equal(imageGenFlag("image_generation_v2 stable true"), null);
});
@@ -0,0 +1,117 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { existsSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { listTypes, getProviders } from "./registry.mjs";
import { CAPABILITIES, listModels } from "./local-models.mjs";
// Capstone: media-use must actually OWN each hyperframes media weakness. This
// test enforces the weakness→owner matrix in references/meta.md so a claim can't rot — if
// a capability's entrypoint disappears, this fails.
const SKILL = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
test("weakness: audio-only → media-use resolves image + icon", () => {
for (const t of ["image", "icon"]) {
assert.ok(getProviders(t).length > 0, `no provider for ${t}`);
}
});
test("weakness: no third-party brand logos → media-use resolves logo", () => {
assert.ok(listTypes().includes("logo"), "logo type missing");
assert.ok(getProviders("logo").length >= 4, "logo cascade incomplete");
});
test("weakness: no voice/audio gen → media-use exposes voice + the audio engine", () => {
assert.ok(listTypes().includes("voice"), "voice type missing");
assert.ok(getProviders("voice").length > 0, "no enabled voice provider (Bin approved)");
assert.ok(existsSync(join(SKILL, "audio", "scripts", "audio.mjs")), "audio engine missing");
});
test("weakness: scattered audio engine → consolidated under media-use (hyperframes-media gone)", () => {
assert.ok(existsSync(join(SKILL, "audio", "scripts", "lib", "tts.mjs")), "tts engine missing");
assert.ok(
existsSync(join(SKILL, "audio", "assets", "sfx", "manifest.json")),
"bundled SFX missing",
);
});
test("weakness: no media-ops → ops guidance reference exists", () => {
assert.ok(existsSync(join(SKILL, "references", "operations.md")), "operations.md missing");
});
test("weakness: no transcript-driven cutting → cut compiler entrypoints exist", async () => {
assert.ok(existsSync(join(SKILL, "scripts", "transcript-cut.mjs")), "transcript-cut missing");
assert.ok(existsSync(join(SKILL, "scripts", "lib", "cutlist.mjs")), "cutlist lib missing");
const cutlist = await import("./cutlist.mjs");
assert.equal(typeof cutlist.compileCutList, "function");
});
test("weakness: whisper.cpp is weak → better local ASR (Parakeet) entrypoint exists", async () => {
assert.ok(existsSync(join(SKILL, "scripts", "transcribe.mjs")), "transcribe.mjs missing");
const pw = await import("./parakeet-words.mjs");
assert.equal(typeof pw.mergeTokensToWords, "function", "token->word merge missing");
const lm = await import("./local-models.mjs");
const asr = lm.listModels("asr");
const parakeet = asr.find((m) => m.id === "parakeet-mlx");
assert.ok(parakeet && parakeet.rank === 0, "Parakeet must be the rank-0 preferred ASR");
});
test("weakness: no auto-duck/loudness → duck compiler and recipes exist", async () => {
assert.ok(existsSync(join(SKILL, "scripts", "audio-duck.mjs")), "audio-duck missing");
assert.ok(existsSync(join(SKILL, "scripts", "lib", "duck.mjs")), "duck lib missing");
assert.ok(existsSync(join(SKILL, "references", "operations.md")), "operations.md missing");
const duck = await import("./duck.mjs");
assert.equal(typeof duck.speechSpans, "function");
assert.equal(typeof duck.duckKeyframes, "function");
});
test("weakness: no cross-project memory → global cache + ingest entrypoints exist", async () => {
const cache = await import("./cache.mjs");
assert.equal(typeof cache.cachePut, "function");
assert.equal(typeof cache.promote, "function");
assert.equal(typeof cache.globalMediaDir, "function");
const freeze = await import("./freeze.mjs");
assert.equal(typeof freeze.isDirectMediaUrl, "function", "ingest URL guard missing");
});
// Wenbo (06-29): heygen free-usage is the default; local models are the opt-out
// fallback ("if user no, then local"). We still assert the fallback table is
// populated so the opt-out path stays real.
test("weakness: weak local defaults → local models exist as the opt-out fallback (tts/asr/upscale)", () => {
for (const cap of ["tts", "asr", "upscale"]) {
assert.ok(CAPABILITIES.includes(cap), `capability ${cap} missing`);
assert.ok(listModels(cap).length > 0, `no local models for ${cap}`);
}
});
test("weakness: no image generation → local mflux (RAM-graded) + codex upsell", async () => {
const ps = getProviders("image");
assert.ok(
ps.some((p) => p.name === "mflux.local" && typeof p.generate === "function"),
"local image gen missing",
);
assert.ok(
ps.some((p) => p.name === "codex.image_gen" && typeof p.generate === "function"),
"codex image upsell missing",
);
const lm = await import("./local-models.mjs");
assert.ok(lm.CAPABILITIES.includes("imagegen"), "imagegen capability missing");
assert.ok(lm.listModels("imagegen").length >= 3, "imagegen RAM ladder too small");
assert.equal(typeof lm.describeModelLadder, "function", "agent-facing ladder missing");
});
test("weakness: no video generation → local videogen ladder + heygen avatar upsell", async () => {
const lm = await import("./local-models.mjs");
assert.ok(lm.CAPABILITIES.includes("videogen"), "videogen capability missing");
assert.ok(lm.listModels("videogen").length >= 2, "videogen ladder too small");
const ops = existsSync(join(SKILL, "references", "operations.md"));
assert.ok(ops, "operations.md (avatar-upsell recipe) missing");
});
test("every resolve type has at least one enabled provider", () => {
for (const t of listTypes()) {
assert.ok(getProviders(t).length > 0, `type ${t} has no enabled provider`);
}
});
@@ -0,0 +1,181 @@
const DEFAULT_SIZE = 33;
const MAX_SIZE = 64;
function clamp(value, min, max) {
if (!Number.isFinite(value)) return 0;
return Math.min(max, Math.max(min, value));
}
function clampUnit(value) {
return clamp(value, 0, 1);
}
function readParam(params, key, min, max) {
return clamp(Number(params?.[key] ?? 0), min, max);
}
function luma([r, g, b]) {
// Rec.709 luma weightings (matches the color space the grading runtime uses).
return r * 0.2126 + g * 0.7152 + b * 0.0722;
}
function smoothstep(edge0, edge1, value) {
const t = clampUnit((value - edge0) / (edge1 - edge0));
return t * t * (3 - 2 * t);
}
function applyLiftGain(color, params) {
const y = luma(color);
const blacks = readParam(params, "blacks", -1, 1);
const shadows = readParam(params, "shadows", -1, 1);
const highlights = readParam(params, "highlights", -1, 1);
const whites = readParam(params, "whites", -1, 1);
const shadowMask = 1 - smoothstep(0.18, 0.62, y);
const highlightMask = smoothstep(0.38, 0.82, y);
const offset =
blacks * 0.08 + shadows * 0.12 * shadowMask + highlights * 0.12 * highlightMask + whites * 0.08;
return color.map((channel) => clampUnit(channel + offset));
}
function applyExposure(color, params) {
const exposure = readParam(params, "exposure", -2, 2);
const gain = 2 ** exposure;
const lift = Math.max(0, exposure) * 0.015;
return color.map((channel) => clampUnit(channel * gain + lift));
}
function applyContrast(color, params) {
const contrast = readParam(params, "contrast", -1, 1);
if (contrast === 0) return color;
const factor = 1 + contrast * 1.2;
return color.map((channel) => clampUnit(0.5 + (channel - 0.5) * factor));
}
function applyWhiteBalance(color, params) {
const temperature = readParam(params, "temperature", -1, 1);
const tint = readParam(params, "tint", -1, 1);
const redScale = 1 + temperature * 0.28 + tint * 0.08;
const greenScale = 1 - Math.abs(tint) * 0.1 - tint * 0.08;
const blueScale = 1 - temperature * 0.28 + tint * 0.08;
return [
clampUnit(color[0] * redScale),
clampUnit(color[1] * greenScale),
clampUnit(color[2] * blueScale),
];
}
function applySplitTone(color, params) {
const split = params?.splitTone;
if (!split) return color;
const intensity = clampUnit(Number(split.intensity ?? 0));
if (intensity === 0) return color;
const balance = clampUnit(Number(split.balance ?? 0.5));
const y = luma(color);
const shadowMask = 1 - smoothstep(balance - 0.25, balance + 0.2, y);
const highlightMask = smoothstep(balance - 0.2, balance + 0.25, y);
const shadows = Array.isArray(split.shadows) ? split.shadows : [0, 0, 0];
const highlights = Array.isArray(split.highlights) ? split.highlights : [0, 0, 0];
return color.map((channel, i) =>
clampUnit(
channel +
Number(shadows[i] ?? 0) * shadowMask * intensity +
Number(highlights[i] ?? 0) * highlightMask * intensity,
),
);
}
function applySaturation(color, params) {
const saturation = readParam(params, "saturation", -1, 1);
const vibrance = readParam(params, "vibrance", -1, 1);
if (saturation === 0 && vibrance === 0) return color;
const y = luma(color);
const currentSat = Math.max(
Math.abs(color[0] - y),
Math.abs(color[1] - y),
Math.abs(color[2] - y),
);
const vibranceWeight = 1 - clampUnit(currentSat * 2);
const factor = clamp(1 + saturation + vibrance * vibranceWeight, 0, 2.5);
return color.map((channel) => clampUnit(y + (channel - y) * factor));
}
function applyParams(color, params) {
let out = applyLiftGain(color, params);
out = applyExposure(out, params);
out = applyContrast(out, params);
out = applyWhiteBalance(out, params);
out = applySplitTone(out, params);
out = applySaturation(out, params);
return out;
}
function formatNumber(value) {
return clampUnit(value).toFixed(6);
}
export function buildCube(params = {}, size = DEFAULT_SIZE) {
if (!Number.isInteger(size) || size < 2 || size > MAX_SIZE) {
throw new Error(`LUT size must be an integer from 2 to ${MAX_SIZE}`);
}
const lines = [
`TITLE "media-use parametric grade"`,
"DOMAIN_MIN 0 0 0",
"DOMAIN_MAX 1 1 1",
`LUT_3D_SIZE ${size}`,
];
const denom = size - 1;
for (let b = 0; b < size; b++) {
for (let g = 0; g < size; g++) {
for (let r = 0; r < size; r++) {
const out = applyParams([r / denom, g / denom, b / denom], params);
lines.push(`${formatNumber(out[0])} ${formatNumber(out[1])} ${formatNumber(out[2])}`);
}
}
}
return `${lines.join("\n")}\n`;
}
export function paramsFromIntent(intent) {
const text = String(intent ?? "").toLowerCase();
const params = {};
let matched = false;
if (/\b(warm|golden|sunlit|sunny)\b/.test(text)) {
params.temperature = 0.18;
matched = true;
} else if (/\b(cool|blue|icy|crisp)\b/.test(text)) {
params.temperature = -0.16;
matched = true;
}
if (/\b(cinematic|film|movie)\b/.test(text)) {
params.contrast = 0.08;
params.saturation = 0.04;
matched = true;
}
if (/\b(punchy|contrast|dramatic|bold)\b/.test(text)) {
params.contrast = Math.max(params.contrast ?? 0, 0.22);
matched = true;
}
if (/\b(bright|airy|lift)\b/.test(text)) {
params.exposure = 0.16;
params.shadows = 0.08;
matched = true;
}
if (/\b(dark|moody|low-key)\b/.test(text)) {
params.exposure = -0.12;
params.contrast = Math.max(params.contrast ?? 0, 0.12);
matched = true;
}
if (/\b(vibrant|saturated|colorful)\b/.test(text)) {
params.saturation = Math.max(params.saturation ?? 0, 0.16);
params.vibrance = 0.12;
matched = true;
}
if (/\b(muted|desaturated|washed)\b/.test(text)) {
params.saturation = Math.min(params.saturation ?? 0, -0.16);
matched = true;
}
return matched ? params : null;
}
@@ -0,0 +1,80 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { buildCube, paramsFromIntent } from "./cube-build.mjs";
import { validateCube } from "./cube-validate.mjs";
function rows(cube) {
return cube
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => /^[+-]?(?:\d|\.\d)/.test(line))
.map((line) => line.split(/\s+/).map(Number));
}
function rowAt(cubeRows, size, r, g, b) {
return cubeRows[(b * size + g) * size + r];
}
function luma(row) {
return row[0] * 0.2126 + row[1] * 0.7152 + row[2] * 0.0722;
}
test("all-zero params produce a near-identity LUT", () => {
const cube = buildCube({}, 3);
assert.equal(validateCube(cube).ok, true);
const parsed = rows(cube);
for (let b = 0; b < 3; b++) {
for (let g = 0; g < 3; g++) {
for (let r = 0; r < 3; r++) {
const row = rowAt(parsed, 3, r, g, b);
assert.ok(Math.abs(row[0] - r / 2) < 0.000001);
assert.ok(Math.abs(row[1] - g / 2) < 0.000001);
assert.ok(Math.abs(row[2] - b / 2) < 0.000001);
}
}
}
});
test("positive exposure increases unclipped output luma", () => {
const identity = rows(buildCube({}, 5));
const exposed = rows(buildCube({ exposure: 0.3 }, 5));
for (let i = 0; i < identity.length; i++) {
const before = luma(identity[i]);
if (before > 0.02 && before < 0.95) {
assert.ok(luma(exposed[i]) > before, `row ${i} should brighten`);
}
}
});
test("positive temperature warms mid-gray", () => {
const parsed = rows(buildCube({ temperature: 0.2 }, 3));
const mid = rowAt(parsed, 3, 1, 1, 1);
assert.ok(mid[0] > 0.5, "red channel should rise");
assert.ok(mid[2] < 0.5, "blue channel should fall");
});
test("positive contrast darkens shadows and brightens highlights", () => {
const parsed = rows(buildCube({ contrast: 0.3 }, 5));
const shadow = rowAt(parsed, 5, 1, 1, 1);
const highlight = rowAt(parsed, 5, 3, 3, 3);
assert.ok(luma(shadow) < 0.25, "below-mid gray should darken");
assert.ok(luma(highlight) > 0.75, "above-mid gray should brighten");
});
test("outputs validate at the default size and are deterministic", () => {
const params = { exposure: 0.15, contrast: 0.2, temperature: -0.1, saturation: 0.12 };
const a = buildCube(params);
const b = buildCube(params);
assert.equal(a, b);
assert.equal(validateCube(a).ok, true);
assert.equal(validateCube(a).size, 33);
});
test("paramsFromIntent declines zero-overlap prompts and maps technical words", () => {
assert.equal(paramsFromIntent("zqxv imaginary neutron look"), null);
assert.deepEqual(paramsFromIntent("warm cinematic"), {
temperature: 0.18,
contrast: 0.08,
saturation: 0.04,
});
});
@@ -0,0 +1,198 @@
#!/usr/bin/env node
// Standalone mirror of packages/core/src/colorLuts.ts. media-use cannot import
// the TypeScript source at runtime, so cube-validate.test.mjs mirrors core
// parser cases to catch drift in accepted .cube files before freezing them.
import { readFileSync } from "node:fs";
import { resolve as resolvePath } from "node:path";
import { fileURLToPath } from "node:url";
export const DEFAULT_MAX_CUBE_LUT_SIZE = 64;
const DEFAULT_DOMAIN_MIN = [0, 0, 0];
const DEFAULT_DOMAIN_MAX = [1, 1, 1];
class CubeValidateError extends Error {
constructor(message, lineNumber = null) {
super(lineNumber == null ? message : `${message} at line ${lineNumber}`);
this.name = "CubeValidateError";
this.lineNumber = lineNumber;
}
}
function stripComment(line) {
let inQuote = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') inQuote = !inQuote;
if (char === "#" && !inQuote) return line.slice(0, i);
}
return line;
}
function parseFiniteNumber(value, lineNumber) {
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new CubeValidateError(`Invalid number "${value}"`, lineNumber);
}
return parsed;
}
function parseVec3(parts, keyword, lineNumber) {
if (parts.length !== 3) {
throw new CubeValidateError(`${keyword} expects three numbers`, lineNumber);
}
return [
parseFiniteNumber(parts[0], lineNumber),
parseFiniteNumber(parts[1], lineNumber),
parseFiniteNumber(parts[2], lineNumber),
];
}
function parseSize(value, keyword, lineNumber) {
if (!value) throw new CubeValidateError(`${keyword} expects a size`, lineNumber);
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 2) {
throw new CubeValidateError(`${keyword} must be an integer greater than 1`, lineNumber);
}
return parsed;
}
function validateDomain(domainMin, domainMax) {
if (
domainMax[0] <= domainMin[0] ||
domainMax[1] <= domainMin[1] ||
domainMax[2] <= domainMin[2]
) {
throw new CubeValidateError("DOMAIN_MAX values must be greater than DOMAIN_MIN values");
}
}
function isNumericDataLine(token) {
return /^[+-]?(?:\d|\.\d)/.test(token);
}
function parseCube(input, options = {}) {
const maxSize = options.maxSize ?? DEFAULT_MAX_CUBE_LUT_SIZE;
let domainMin = DEFAULT_DOMAIN_MIN;
let domainMax = DEFAULT_DOMAIN_MAX;
let lut1dSize = null;
let lut3dSize = null;
let rows = 0;
const lines = String(input)
.replace(/^\uFEFF/, "")
.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const lineNumber = i + 1;
const line = stripComment(lines[i] ?? "").trim();
if (!line) continue;
const parts = line.split(/\s+/);
const keyword = (parts[0] ?? "").toUpperCase();
const rest = parts.slice(1);
if (keyword === "TITLE") continue;
if (keyword === "DOMAIN_MIN") {
domainMin = parseVec3(rest, keyword, lineNumber);
continue;
}
if (keyword === "DOMAIN_MAX") {
domainMax = parseVec3(rest, keyword, lineNumber);
continue;
}
if (keyword === "LUT_3D_INPUT_RANGE") {
if (rest.length !== 2) {
throw new CubeValidateError(`${keyword} expects two numbers`, lineNumber);
}
const min = parseFiniteNumber(rest[0], lineNumber);
const max = parseFiniteNumber(rest[1], lineNumber);
if (max <= min) {
throw new CubeValidateError("LUT_3D_INPUT_RANGE max must exceed min", lineNumber);
}
domainMin = [min, min, min];
domainMax = [max, max, max];
continue;
}
if (keyword === "LUT_1D_SIZE") {
lut1dSize = parseSize(rest[0], keyword, lineNumber);
continue;
}
if (keyword === "LUT_3D_SIZE") {
lut3dSize = parseSize(rest[0], keyword, lineNumber);
if (lut3dSize > maxSize) {
throw new CubeValidateError(`LUT_3D_SIZE ${lut3dSize} exceeds max ${maxSize}`, lineNumber);
}
continue;
}
if (!isNumericDataLine(keyword)) {
if (keyword.startsWith("LUT_")) {
throw new CubeValidateError(`Unsupported cube keyword ${keyword}`, lineNumber);
}
continue;
}
if (!lut3dSize) {
if (lut1dSize) {
throw new CubeValidateError("1D cube LUTs are not supported yet", lineNumber);
}
throw new CubeValidateError("LUT data appears before LUT_3D_SIZE", lineNumber);
}
if (parts.length !== 3) {
throw new CubeValidateError("LUT data rows must contain three numbers", lineNumber);
}
parseFiniteNumber(parts[0], lineNumber);
parseFiniteNumber(parts[1], lineNumber);
parseFiniteNumber(parts[2], lineNumber);
rows++;
}
if (lut1dSize && lut3dSize) {
throw new CubeValidateError("Mixed 1D and 3D cube LUTs are not supported yet");
}
if (!lut3dSize) {
if (lut1dSize) throw new CubeValidateError("1D cube LUTs are not supported yet");
throw new CubeValidateError("Missing LUT_3D_SIZE");
}
validateDomain(domainMin, domainMax);
const expectedRows = lut3dSize * lut3dSize * lut3dSize;
if (rows !== expectedRows) {
throw new CubeValidateError(
`Expected ${expectedRows} LUT rows for size ${lut3dSize}, found ${rows}`,
);
}
return { size: lut3dSize };
}
export function validateCube(input, options = {}) {
try {
const parsed = parseCube(input, options);
return { ok: true, size: parsed.size };
} catch (err) {
return { ok: false, error: err.message };
}
}
export function validateCubeFile(filePath, options = {}) {
return validateCube(readFileSync(filePath, "utf8"), options);
}
function main(argv) {
const file = argv[2];
if (!file) {
console.error("usage: cube-validate.mjs <file.cube>");
process.exit(2);
}
const result = validateCubeFile(file);
if (!result.ok) {
console.error(`error: ${result.error}`);
process.exit(1);
}
console.log(`ok: LUT_3D_SIZE ${result.size}`);
}
if (process.argv[1] && resolvePath(process.argv[1]) === fileURLToPath(import.meta.url)) {
main(process.argv);
}
@@ -0,0 +1,125 @@
import { strict as assert } from "node:assert";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { test } from "node:test";
import { validateCube } from "./cube-validate.mjs";
const IDENTITY_2 = `
# comment
TITLE "Identity 2"
DOMAIN_MIN 0 0 0
DOMAIN_MAX 1 1 1
LUT_3D_SIZE 2
0 0 0
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1
`;
test("accepts a valid minimal 3D cube LUT", () => {
const result = validateCube(IDENTITY_2);
assert.deepEqual(result, { ok: true, size: 2 });
});
test("rejects oversize LUTs with the core parser message", () => {
const result = validateCube("LUT_3D_SIZE 65", { maxSize: 64 });
assert.equal(result.ok, false);
assert.match(result.error, /LUT_3D_SIZE 65 exceeds max 64/);
});
test("rejects data rows before LUT_3D_SIZE", () => {
const result = validateCube("0 0 0\nLUT_3D_SIZE 2");
assert.equal(result.ok, false);
assert.match(result.error, /LUT data appears before LUT_3D_SIZE/);
});
test("rejects missing LUT_3D_SIZE", () => {
const result = validateCube('TITLE "No Size"');
assert.equal(result.ok, false);
assert.match(result.error, /Missing LUT_3D_SIZE/);
});
test("rejects row count mismatches with the core parser message", () => {
const result = validateCube("LUT_3D_SIZE 2\n0 0 0");
assert.equal(result.ok, false);
assert.match(result.error, /Expected 8 LUT rows/);
});
test("rejects inverted domains", () => {
const result = validateCube(`
DOMAIN_MIN 0 0 0
DOMAIN_MAX 1 0 1
LUT_3D_SIZE 2
0 0 0
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1
`);
assert.equal(result.ok, false);
assert.match(result.error, /DOMAIN_MAX values must be greater than DOMAIN_MIN values/);
});
test("rejects unsupported 1D and mixed cube LUTs", () => {
const oneD = validateCube("LUT_1D_SIZE 2\n0 0 0\n1 1 1");
assert.equal(oneD.ok, false);
assert.match(oneD.error, /1D cube LUTs are not supported yet/);
const mixed = validateCube(`
LUT_1D_SIZE 2
LUT_3D_SIZE 2
0 0 0
1 0 0
0 1 0
1 1 0
0 0 1
1 0 1
0 1 1
1 1 1
`);
assert.equal(mixed.ok, false);
assert.match(mixed.error, /Mixed 1D and 3D cube LUTs are not supported yet/);
});
test("CLI exits zero for valid files and non-zero for invalid files", () => {
const dir = mkdtempSync(join(tmpdir(), "mu-cube-validate-"));
try {
const valid = join(dir, "valid.cube");
const invalid = join(dir, "invalid.cube");
writeFileSync(valid, IDENTITY_2);
writeFileSync(invalid, "LUT_3D_SIZE 65");
const out = execFileSync(
process.execPath,
[new URL("./cube-validate.mjs", import.meta.url).pathname, valid],
{
encoding: "utf8",
},
);
assert.match(out, /ok: LUT_3D_SIZE 2/);
assert.throws(
() =>
execFileSync(
process.execPath,
[new URL("./cube-validate.mjs", import.meta.url).pathname, invalid],
{
encoding: "utf8",
stdio: "pipe",
},
),
(err) => err.status === 1 && String(err.stderr).includes("exceeds max 64"),
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,184 @@
import { normalizeWords } from "./words.mjs";
const MIN_SEGMENT_SECONDS = 0.2;
const SILENCE_PAD_SECONDS = 0.15;
export function compileCutList(transcript, opts = {}) {
const words = normalizeWords(transcript);
if (opts.keep != null && hasRemovalSource(opts)) {
throw new Error("--keep is mutually exclusive with removal options");
}
if (opts.keep != null) {
const duration = durationFrom(words, opts);
const ranges = parseTimeRanges(opts.keep);
return finalizeKept(duration != null ? clampRanges(ranges, duration) : ranges);
}
const duration = durationFrom(words, opts);
if (!duration) return [];
const removals = [
...parseTimeRanges(opts.remove),
...wordIndexRanges(words, opts.removeWords),
...fillerRanges(words, opts.removeFillers),
...silenceRanges(words, opts.cutSilence),
];
const mergedRemovals = mergeRanges(clampRanges(removals, duration));
return finalizeKept(invertRanges(mergedRemovals, duration));
}
function hasRemovalSource(opts) {
return (
opts.remove != null ||
opts.removeWords != null ||
opts.removeFillers != null ||
opts.cutSilence != null
);
}
function durationFrom(words, opts) {
const explicit = Number(opts.duration ?? opts.totalDuration);
if (Number.isFinite(explicit) && explicit > 0) return explicit;
const last = words.at(-1);
return last && Number.isFinite(last.end) && last.end > 0 ? last.end : null;
}
function parseTimeRanges(value) {
if (value == null || value === false || value === "") return [];
if (typeof value === "string") {
return value
.split(",")
.map((part) => part.trim())
.filter(Boolean)
.map(parseRangeString);
}
if (!Array.isArray(value)) throw new Error("range list must be a string or array");
return value.map((range) => {
if (Array.isArray(range)) return cleanRange(Number(range[0]), Number(range[1]));
return cleanRange(Number(range?.start), Number(range?.end));
});
}
function parseRangeString(value) {
const match = value.match(/^([0-9]*\.?[0-9]+)\s*-\s*([0-9]*\.?[0-9]+)$/);
if (!match) throw new Error(`invalid range: ${value}`);
return cleanRange(Number(match[1]), Number(match[2]));
}
function cleanRange(start, end) {
if (!Number.isFinite(start) || !Number.isFinite(end)) {
throw new Error("range start/end must be finite numbers");
}
if (end < start) throw new Error(`range end ${end} is before start ${start}`);
return { start, end };
}
function wordIndexRanges(words, value) {
if (value == null || value === false || value === "") return [];
const ranges = typeof value === "string" ? value.split(",") : value;
if (!Array.isArray(ranges)) throw new Error("--remove-words must be a string or array");
return ranges
.map((range) => (typeof range === "string" ? range.trim() : range))
.filter(Boolean)
.map((range) => {
const [first, last = first] =
typeof range === "string" ? range.split("-").map((n) => n.trim()) : range;
const startIndex = Number(first);
const endIndex = Number(last);
if (!Number.isInteger(startIndex) || !Number.isInteger(endIndex)) {
throw new Error(`invalid word range: ${range}`);
}
if (startIndex < 0 || endIndex < startIndex || endIndex >= words.length) {
throw new Error(`word range out of bounds: ${range}`);
}
return { start: words[startIndex].start, end: words[endIndex].end };
});
}
function fillerRanges(words, value) {
if (value == null || value === false || value === "") return [];
const fillers = Array.isArray(value)
? value
: String(value)
.split(",")
.map((s) => s.trim());
const set = new Set(fillers.filter(Boolean).map(bareToken));
if (set.size === 0) return [];
// Whisper emits words with attached punctuation and arbitrary case
// ("UM," / "Um."), so compare bare tokens.
return words
.filter((word) => set.has(bareToken(word.text)))
.map((word) => ({ start: word.start, end: word.end }));
}
function bareToken(text) {
return String(text)
.toLowerCase()
.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, "");
}
function silenceRanges(words, value) {
if (value == null || value === false || value === "") return [];
const threshold = Number(value);
if (!Number.isFinite(threshold) || threshold <= 0) {
throw new Error("--cut-silence must be a positive number");
}
const ranges = [];
for (let i = 0; i < words.length - 1; i++) {
const current = words[i];
const next = words[i + 1];
const gap = next.start - current.end;
if (gap <= threshold) continue;
const start = current.end + SILENCE_PAD_SECONDS;
const end = next.start - SILENCE_PAD_SECONDS;
if (end > start) ranges.push({ start, end });
}
return ranges;
}
function clampRanges(ranges, duration) {
return ranges
.map((range) => ({
start: Math.max(0, Math.min(duration, range.start)),
end: Math.max(0, Math.min(duration, range.end)),
}))
.filter((range) => range.end > range.start);
}
function mergeRanges(ranges) {
const sorted = ranges
.map((range) => ({ start: round3(range.start), end: round3(range.end) }))
.sort((a, b) => a.start - b.start || a.end - b.end);
const merged = [];
for (const range of sorted) {
const prev = merged.at(-1);
if (prev && range.start <= prev.end) {
prev.end = Math.max(prev.end, range.end);
} else {
merged.push({ ...range });
}
}
return merged;
}
function invertRanges(removals, duration) {
const kept = [];
let cursor = 0;
for (const range of removals) {
if (range.start > cursor) kept.push({ start: cursor, end: range.start });
cursor = Math.max(cursor, range.end);
}
if (cursor < duration) kept.push({ start: cursor, end: duration });
return kept;
}
function finalizeKept(ranges) {
return mergeRanges(ranges)
.map((range) => ({ start: round3(range.start), end: round3(range.end) }))
.filter((range) => round3(range.end - range.start) >= MIN_SEGMENT_SECONDS);
}
function round3(n) {
return Math.round(Number(n) * 1000) / 1000;
}
@@ -0,0 +1,148 @@
import { strict as assert } from "node:assert";
import { execFileSync } from "node:child_process";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { compileCutList } from "./cutlist.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const SCRIPT = join(HERE, "..", "transcript-cut.mjs");
test("explicit --remove ranges invert to kept segments", () => {
const transcript = [
word("w0", "alpha", 0, 1),
word("w1", "beta", 1.2, 2),
word("w2", "gamma", 2.2, 5),
];
assert.deepEqual(compileCutList(transcript, { remove: "1-2.5" }), [
{ start: 0, end: 1 },
{ start: 2.5, end: 5 },
]);
});
test("--remove-words resolves inclusive word-index ranges to time ranges", () => {
const transcript = [
word("w0", "zero", 0, 0.5),
word("w1", "one", 0.6, 1),
word("w2", "two", 1.1, 1.5),
word("w3", "three", 2, 3),
];
assert.deepEqual(compileCutList(transcript, { removeWords: "1-2" }), [
{ start: 0, end: 0.6 },
{ start: 1.5, end: 3 },
]);
});
test("--remove-fillers drops case-insensitive matching words", () => {
const transcript = [
word("w0", "Hello", 0, 0.5),
word("w1", "Um", 0.5, 0.7),
word("w2", "world", 0.8, 1.2),
word("w3", "LIKE", 1.3, 1.5),
word("w4", "done", 1.6, 2),
];
assert.deepEqual(compileCutList(transcript, { removeFillers: "um,like" }), [
{ start: 0, end: 0.5 },
{ start: 0.7, end: 1.3 },
{ start: 1.5, end: 2 },
]);
});
test("--cut-silence removes only the center of long inter-word gaps", () => {
const transcript = [word("w0", "a", 0, 0.5), word("w1", "b", 2, 2.5), word("w2", "c", 2.7, 3)];
assert.deepEqual(compileCutList(transcript, { cutSilence: 0.8 }), [
{ start: 0, end: 0.65 },
{ start: 1.85, end: 3 },
]);
});
test("overlapping removal sources merge before inversion", () => {
const transcript = [
word("w0", "start", 0, 0.5),
word("w1", "um", 0.9, 1.1),
word("w2", "middle", 2.5, 2.8),
word("w3", "more", 3.1, 3.4),
word("w4", "end", 5.5, 6),
];
assert.deepEqual(
compileCutList(transcript, {
remove: "1-2.7",
removeWords: "2-3",
removeFillers: "um",
}),
[
{ start: 0, end: 0.9 },
{ start: 3.4, end: 6 },
],
);
});
test("kept slivers shorter than 0.2s are dropped", () => {
const transcript = [word("w0", "start", 0, 0.5), word("w1", "end", 2.5, 3)];
assert.deepEqual(compileCutList(transcript, { remove: "0.1-2.95" }), []);
});
test("--keep is inverse mode and coalesces direct kept ranges", () => {
const transcript = [word("w0", "start", 0, 0.5), word("w1", "end", 4.5, 5)];
assert.deepEqual(compileCutList(transcript, { keep: "3-4,1-2,1.5-2.5,4.1-4.2" }), [
{ start: 1, end: 2.5 },
{ start: 3, end: 4 },
]);
});
test("--plan on a fixture transcript prints the exact segment JSON", () => {
const dir = mkdtempSync(join(tmpdir(), "media-use-cutlist-"));
try {
const transcriptPath = join(dir, "fixture.json");
writeFileSync(
transcriptPath,
JSON.stringify([
word("w0", "hello", 0, 0.4),
word("w1", "um", 0.5, 0.65),
word("w2", "there", 0.7, 1),
word("w3", "pause", 2.2, 2.5),
word("w4", "end", 2.7, 3.2),
]),
);
const out = execFileSync(
process.execPath,
[
SCRIPT,
"--input",
"ignored.mp4",
"--transcript",
transcriptPath,
"--remove",
"0.9-1.2",
"--remove-fillers",
"um",
"--cut-silence",
"0.8",
"--plan",
],
{ encoding: "utf8" },
);
assert.deepEqual(JSON.parse(out), [
{ start: 0, end: 0.5 },
{ start: 0.65, end: 0.9 },
{ start: 2.05, end: 3.2 },
]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
function word(id, text, start, end) {
return { id, text, start, end };
}
@@ -0,0 +1,89 @@
import { wordListsFromMediaMeta } from "./words.mjs";
/**
* Speech spans from word timestamps.
*
* audio_meta.json word times are relative to EACH LINE'S OWN FILE, not to the
* composition. Without placement info, multiple lines would overlap at t=0 and
* merge into one bogus span. Placement options:
* offsets: { [voiceId]: startSeconds } explicit composition placement
* sequential: stack lines back to back (plus `gap` seconds between lines)
* A single word list (bare transcript) needs neither.
*/
export function speechSpans(meta, { mergeGap = 0.6, offsets, sequential = false, gap = 0 } = {}) {
const merge = Number(mergeGap);
const lists = wordListsFromMediaMeta(meta);
const voices = Array.isArray(meta?.voices) ? meta.voices : [];
if (lists.length > 1 && !offsets && !sequential) {
throw new Error(
"audio_meta has multiple voice lines with file-relative times; pass --sequential or --offsets so spans land at composition time",
);
}
const intervals = [];
let cursor = 0;
for (let i = 0; i < lists.length; i++) {
const voice = voices[i];
let offset = 0;
if (offsets) {
const id = voice?.id ?? String(i);
if (!(id in offsets)) throw new Error(`--offsets is missing voice "${id}"`);
offset = Number(offsets[id]) || 0;
} else if (sequential) {
offset = cursor;
const lineDuration = Number(voice?.duration_s) || Math.max(...lists[i].map((w) => w.end), 0);
cursor += lineDuration + (Number(gap) || 0);
}
for (const word of lists[i]) {
if (word.end > word.start)
intervals.push({ start: word.start + offset, end: word.end + offset });
}
}
return mergeIntervals(intervals, Number.isFinite(merge) && merge >= 0 ? merge : 0.6);
}
export function duckKeyframes(
spans,
{ duck = 0.25, attack = 0.15, release = 0.4, baseVolume = 1 } = {},
) {
const base = finiteOr(baseVolume, 1);
const ducked = round3(base * finiteOr(duck, 0.25));
const keyframes = [];
for (const span of spans) {
keyframes.push({
time: round3(Math.max(0, finiteOr(span.start, 0))),
volume: ducked,
duration: round3(finiteOr(attack, 0.15)),
});
keyframes.push({
time: round3(Math.max(0, finiteOr(span.end, 0))),
volume: round3(base),
duration: round3(finiteOr(release, 0.4)),
});
}
return keyframes.sort((a, b) => a.time - b.time);
}
function mergeIntervals(intervals, mergeGap) {
const sorted = intervals
.map((range) => ({ start: round3(range.start), end: round3(range.end) }))
.sort((a, b) => a.start - b.start || a.end - b.end);
const merged = [];
for (const range of sorted) {
const prev = merged.at(-1);
if (prev && (range.start <= prev.end || range.start - prev.end < mergeGap)) {
prev.end = Math.max(prev.end, range.end);
} else {
merged.push({ ...range });
}
}
return merged;
}
function finiteOr(value, fallback) {
const n = Number(value);
return Number.isFinite(n) ? n : fallback;
}
function round3(n) {
return Math.round(Number(n) * 1000) / 1000;
}
@@ -0,0 +1,118 @@
import { strict as assert } from "node:assert";
import { execFileSync } from "node:child_process";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join, dirname } from "node:path";
import { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { duckKeyframes, speechSpans } from "./duck.mjs";
const HERE = dirname(fileURLToPath(import.meta.url));
const SCRIPT = join(HERE, "..", "audio-duck.mjs");
test("speechSpans bridges gaps smaller than mergeGap", () => {
const meta = {
words: [word("w0", "one", 0, 0.5), word("w1", "two", 0.8, 1), word("w2", "three", 2, 2.2)],
};
assert.deepEqual(speechSpans(meta, { mergeGap: 0.4 }), [
{ start: 0, end: 1 },
{ start: 2, end: 2.2 },
]);
});
test("speechSpans refuses multi-line meta without placement (file-relative times)", () => {
const meta = {
voices: [
{ id: "a", words: [word("w0", "one", 0, 1)] },
{ id: "b", words: [word("w1", "two", 0, 1)] },
],
};
assert.throws(() => speechSpans(meta, { mergeGap: 0.2 }), /--sequential or --offsets/);
});
test("speechSpans sequential stacks lines by duration plus gap", () => {
const meta = {
voices: [
{ id: "a", duration_s: 2, words: [word("w0", "one", 0.1, 1.9)] },
{ id: "b", duration_s: 1, words: [word("w1", "two", 0.1, 0.9)] },
],
};
assert.deepEqual(speechSpans(meta, { mergeGap: 0.2, sequential: true, gap: 0.5 }), [
{ start: 0.1, end: 1.9 },
{ start: 2.6, end: 3.4 },
]);
});
test("speechSpans explicit offsets place each line at composition time", () => {
const meta = {
voices: [
{ id: "a", words: [word("w0", "one", 0, 1)] },
{ id: "b", words: [word("w1", "two", 0, 1)] },
],
};
assert.deepEqual(speechSpans(meta, { mergeGap: 0.2, offsets: { a: 0, b: 4 } }), [
{ start: 0, end: 1 },
{ start: 4, end: 5 },
]);
assert.throws(() => speechSpans(meta, { offsets: { a: 0 } }), /missing voice "b"/);
});
test("speechSpans returns empty spans for empty input", () => {
assert.deepEqual(speechSpans({ voices: [] }, { mergeGap: 0.6 }), []);
});
test("duckKeyframes shapes attack and release from base volume", () => {
assert.deepEqual(
duckKeyframes([{ start: 3, end: 5 }], {
duck: 0.25,
attack: 0.15,
release: 0.4,
baseVolume: 0.6,
}),
[
{ time: 3, volume: 0.15, duration: 0.15 },
{ time: 5, volume: 0.6, duration: 0.4 },
],
);
});
test("--json spans match --merge-gap semantics exactly", () => {
const dir = mkdtempSync(join(tmpdir(), "media-use-duck-"));
try {
const metaPath = join(dir, "audio_meta.json");
writeFileSync(
metaPath,
JSON.stringify({
voices: [
{
id: "narration",
words: [
word("w0", "one", 0, 0.4),
word("w1", "two", 0.9, 1.2),
word("w2", "three", 1.8, 2.1),
],
},
],
}),
);
const out = execFileSync(
process.execPath,
[SCRIPT, "--meta", metaPath, "--target", "#bgm", "--merge-gap", "0.6", "--json"],
{ encoding: "utf8" },
);
const parsed = JSON.parse(out);
assert.deepEqual(parsed.spans, [
{ start: 0, end: 1.2 },
{ start: 1.8, end: 2.1 },
]);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
function word(id, text, start, end) {
return { id, text, start, end };
}
@@ -0,0 +1,230 @@
export const ERROR_DIFFUSION_ALGORITHMS = {
"floyd-steinberg": {
kernel: [
[1, 0, 7],
[-1, 1, 3],
[0, 1, 5],
[1, 1, 1],
],
divisor: 16,
},
atkinson: {
kernel: [
[1, 0, 1],
[2, 0, 1],
[-1, 1, 1],
[0, 1, 1],
[1, 1, 1],
[0, 2, 1],
],
divisor: 8,
},
"jarvis-judice-ninke": {
kernel: [
[1, 0, 7],
[2, 0, 5],
[-2, 1, 3],
[-1, 1, 5],
[0, 1, 7],
[1, 1, 5],
[2, 1, 3],
[-2, 2, 1],
[-1, 2, 3],
[0, 2, 5],
[1, 2, 3],
[2, 2, 1],
],
divisor: 48,
},
stucki: {
kernel: [
[1, 0, 8],
[2, 0, 4],
[-2, 1, 2],
[-1, 1, 4],
[0, 1, 8],
[1, 1, 4],
[2, 1, 2],
[-2, 2, 1],
[-1, 2, 2],
[0, 2, 4],
[1, 2, 2],
[2, 2, 1],
],
divisor: 42,
},
burkes: {
kernel: [
[1, 0, 8],
[2, 0, 4],
[-2, 1, 2],
[-1, 1, 4],
[0, 1, 8],
[1, 1, 4],
[2, 1, 2],
],
divisor: 32,
},
sierra: {
kernel: [
[1, 0, 5],
[2, 0, 3],
[-2, 1, 2],
[-1, 1, 4],
[0, 1, 5],
[1, 1, 4],
[2, 1, 2],
[-1, 2, 2],
[0, 2, 3],
[1, 2, 2],
],
divisor: 32,
},
"sierra-lite": {
kernel: [
[1, 0, 2],
[-1, 1, 1],
[0, 1, 1],
],
divisor: 4,
},
"two-row-sierra": {
kernel: [
[1, 0, 4],
[2, 0, 3],
[-2, 1, 1],
[-1, 1, 2],
[0, 1, 3],
[1, 1, 2],
[2, 1, 1],
],
divisor: 16,
},
};
const DEFAULTS = {
algorithm: "floyd-steinberg",
brightness: 1,
contrast: 1.2,
detail: 1,
palette: ["#000000", "#ffffff"],
pointSize: 3,
};
export function errorDiffusionBufferLength(width, height, pointSize) {
return Math.ceil(width / pointSize) * Math.ceil(height / pointSize) * 3;
}
export function applyErrorDiffusionRgba(data, width, height, options = {}, errorBuffer) {
if (!Number.isInteger(width) || width < 1 || !Number.isInteger(height) || height < 1) {
throw new Error("width and height must be positive integers");
}
if (!data || data.length !== width * height * 4) {
throw new Error(`RGBA data must contain ${width * height * 4} bytes`);
}
const algorithm = options.algorithm ?? DEFAULTS.algorithm;
const diffusion = ERROR_DIFFUSION_ALGORITHMS[algorithm];
if (!diffusion) throw new Error(`unknown error-diffusion algorithm: ${algorithm}`);
const pointSize = integerInRange(options.pointSize ?? DEFAULTS.pointSize, 1, 20, "pointSize");
const brightness = numberInRange(options.brightness ?? DEFAULTS.brightness, 0.5, 2, "brightness");
const contrast = numberInRange(options.contrast ?? DEFAULTS.contrast, 0.5, 2, "contrast");
const detail = numberInRange(options.detail ?? DEFAULTS.detail, 0.1, 1, "detail");
const palette = parsePalette(options.palette ?? DEFAULTS.palette);
const blockColumns = Math.ceil(width / pointSize);
const blockRows = Math.ceil(height / pointSize);
const errorLength = errorDiffusionBufferLength(width, height, pointSize);
const errors = errorBuffer ?? new Float32Array(errorLength);
if (!(errors instanceof Float32Array) || errors.length !== errorLength) {
throw new Error(`errorBuffer must be a Float32Array of length ${errorLength}`);
}
errors.fill(0);
const centerOffset = Math.floor(pointSize / 2);
for (let blockRow = 0; blockRow < blockRows; blockRow++) {
const blockY = blockRow * pointSize;
for (let blockColumn = 0; blockColumn < blockColumns; blockColumn++) {
const blockX = blockColumn * pointSize;
const centerX = Math.min(blockX + centerOffset, width - 1);
const centerY = Math.min(blockY + centerOffset, height - 1);
const rgbaIndex = (centerY * width + centerX) * 4;
const errorIndex = (blockRow * blockColumns + blockColumn) * 3;
const red = correctedChannel(data[rgbaIndex], errors[errorIndex], brightness, contrast);
const green = correctedChannel(
data[rgbaIndex + 1],
errors[errorIndex + 1],
brightness,
contrast,
);
const blue = correctedChannel(
data[rgbaIndex + 2],
errors[errorIndex + 2],
brightness,
contrast,
);
const luminance = 0.299 * red + 0.587 * green + 0.114 * blue;
const output = palette[Math.min(palette.length - 1, Math.floor(luminance * palette.length))];
for (let y = blockY; y < Math.min(blockY + pointSize, height); y++) {
for (let x = blockX; x < Math.min(blockX + pointSize, width); x++) {
const outputIndex = (y * width + x) * 4;
data[outputIndex] = Math.round(output[0] * 255);
data[outputIndex + 1] = Math.round(output[1] * 255);
data[outputIndex + 2] = Math.round(output[2] * 255);
}
}
for (const [dx, dy, weight] of diffusion.kernel) {
const targetColumn = blockColumn + dx;
const targetRow = blockRow + dy;
if (
targetColumn < 0 ||
targetColumn >= blockColumns ||
targetRow < 0 ||
targetRow >= blockRows
) {
continue;
}
const target = (targetRow * blockColumns + targetColumn) * 3;
const scale = (weight / diffusion.divisor) * detail;
errors[target] += (red - output[0]) * scale;
errors[target + 1] += (green - output[1]) * scale;
errors[target + 2] += (blue - output[2]) * scale;
}
}
}
return data;
}
function correctedChannel(byte, error, brightness, contrast) {
return Math.min(1, Math.max(0, ((byte / 255 - 0.5) * contrast + 0.5) * brightness + error));
}
function parsePalette(colors) {
if (!Array.isArray(colors) || colors.length < 2 || colors.length > 6) {
throw new Error("palette must contain 2 to 6 colors");
}
return colors.map((color) => {
const match = /^#([0-9a-f]{6})$/i.exec(color);
if (!match) throw new Error(`palette color must use #rrggbb: ${color}`);
const value = Number.parseInt(match[1], 16);
return [(value >> 16) / 255, ((value >> 8) & 255) / 255, (value & 255) / 255];
});
}
function numberInRange(value, min, max, name) {
const number = Number(value);
if (!Number.isFinite(number) || number < min || number > max) {
throw new Error(`${name} must be between ${min} and ${max}`);
}
return number;
}
function integerInRange(value, min, max, name) {
const number = Number(value);
if (!Number.isInteger(number) || number < min || number > max) {
throw new Error(`${name} must be an integer between ${min} and ${max}`);
}
return number;
}
@@ -0,0 +1,102 @@
import assert from "node:assert/strict";
import test from "node:test";
import { ERROR_DIFFUSION_ALGORITHMS, applyErrorDiffusionRgba } from "./error-diffusion.mjs";
const EXPECTED_GRADIENTS = {
"floyd-steinberg": "00000101/00010101/00100101/00010111/01010101/01011011",
atkinson: "00000011/00001100/00010011/00010111/01001101/00111011",
"jarvis-judice-ninke": "00000011/00001011/00011001/00101111/00100111/01011011",
stucki: "00000011/00010101/00010110/00101011/00101101/01010111",
burkes: "00000101/00010011/00010110/00101011/01010111/00101011",
sierra: "00000011/00010101/00010110/00100111/00110111/00101101",
"sierra-lite": "00000101/00010101/00100101/00010110/01010111/01010101",
"two-row-sierra": "00000101/00010011/00010110/00101011/00101101/01011011",
};
test("exposes the eight article error-diffusion algorithms", () => {
assert.deepEqual(Object.keys(ERROR_DIFFUSION_ALGORITHMS), Object.keys(EXPECTED_GRADIENTS));
});
test("matches deterministic golden patterns for every diffusion kernel", () => {
const width = 8;
const height = 6;
const source = new Uint8ClampedArray(width * height * 4);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const value = Math.round((255 * (x + y * 0.7)) / (width - 1 + (height - 1) * 0.7));
const offset = (y * width + x) * 4;
source[offset] = value;
source[offset + 1] = Math.round(value * 0.8);
source[offset + 2] = Math.round(value * 0.55);
source[offset + 3] = 17 + x + y;
}
}
for (const [algorithm, expected] of Object.entries(EXPECTED_GRADIENTS)) {
const output = source.slice();
applyErrorDiffusionRgba(output, width, height, {
algorithm,
brightness: 1,
contrast: 1,
detail: 1,
palette: ["#000000", "#ffffff"],
pointSize: 1,
});
const rows = [];
for (let y = 0; y < height; y++) {
let row = "";
for (let x = 0; x < width; x++) row += output[(y * width + x) * 4] ? "1" : "0";
rows.push(row);
}
assert.equal(rows.join("/"), expected, algorithm);
for (let i = 0; i < width * height; i++) assert.equal(output[i * 4 + 3], source[i * 4 + 3]);
}
});
test("fills point-size blocks from their center sample", () => {
const data = new Uint8ClampedArray([
0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 255, 255, 255, 6, 0, 0, 0, 7, 255,
255, 255, 8,
]);
applyErrorDiffusionRgba(
data,
4,
2,
{
algorithm: "floyd-steinberg",
brightness: 1,
contrast: 1,
detail: 1,
palette: ["#000000", "#ffffff"],
pointSize: 2,
},
new Float32Array(6),
);
assert.deepEqual(
[...data],
[
255, 255, 255, 1, 255, 255, 255, 2, 255, 255, 255, 3, 255, 255, 255, 4, 255, 255, 255, 5, 255,
255, 255, 6, 255, 255, 255, 7, 255, 255, 255, 8,
],
);
});
test("preserves authored palette order and validates the public contract", () => {
const reversed = new Uint8ClampedArray([0, 0, 0, 255]);
applyErrorDiffusionRgba(reversed, 1, 1, {
palette: ["#ffffff", "#000000"],
});
assert.deepEqual([...reversed], [255, 255, 255, 255]);
assert.throws(
() => applyErrorDiffusionRgba(new Uint8ClampedArray(4), 1, 1, { palette: ["#000000"] }),
/2 to 6 colors/,
);
assert.throws(
() =>
applyErrorDiffusionRgba(new Uint8ClampedArray(4), 1, 1, {
algorithm: "ordered-bayer",
}),
/unknown error-diffusion algorithm/,
);
});
@@ -0,0 +1,70 @@
import { writeFileSync, copyFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
// ponytail: bound the download so a hostile/runaway URL can't fill the disk.
// 256MB covers any real media asset; raise if 4K video sources ever exceed it.
const MAX_FREEZE_BYTES = 256 * 1024 * 1024;
export async function freezeUrl(url, destPath) {
const where = String(url).slice(0, 80);
const res = await fetch(url);
if (!res.ok) throw new Error(`freeze failed: HTTP ${res.status} for ${where}`);
// Fail fast on an advertised oversize body before reading a single byte.
const declared = Number(res.headers.get("content-length"));
if (declared > MAX_FREEZE_BYTES)
throw new Error(
`freeze failed: ${declared} bytes exceeds ${MAX_FREEZE_BYTES} cap for ${where}`,
);
// Stream and abort once the cap is crossed, so a lying/chunked hostile URL
// can't buffer the whole payload into memory before the check (M1).
const chunks = [];
let total = 0;
for await (const chunk of res.body) {
total += chunk.length;
if (total > MAX_FREEZE_BYTES)
throw new Error(`freeze failed: stream exceeds ${MAX_FREEZE_BYTES} cap for ${where}`);
chunks.push(chunk);
}
if (total === 0) throw new Error(`freeze failed: empty response for ${where}`);
mkdirSync(dirname(destPath), { recursive: true });
writeFileSync(destPath, Buffer.concat(chunks, total));
return total;
}
export function freezeLocalFile(srcPath, destPath) {
mkdirSync(dirname(destPath), { recursive: true });
copyFileSync(srcPath, destPath);
}
// Ingest accepts a DIRECT public media URL only — not a platform page. yt-dlp is
// deliberately out (cloud IPs get blocked, and it's brittle); the supported case
// is "user points at their own file or a direct asset link". A direct URL is a
// non-platform host whose path ends in a known media extension.
const PLATFORM_HOSTS =
/(^|\.)(youtube\.com|youtu\.be|vimeo\.com|tiktok\.com|instagram\.com|twitter\.com|x\.com|facebook\.com|dailymotion\.com)$/i;
const MEDIA_EXT = /\.(mp3|wav|m4a|aac|ogg|flac|mp4|mov|webm|mkv|png|jpe?g|webp|gif|svg|avif)$/i;
// SSRF guard (m11): a user-supplied --from URL must not point at the local host
// or a private network. Blocks loopback/localhost, RFC1918, link-local, and the
// IPv6 equivalents on the literal hostname.
// ponytail: literal-host check only; a DNS name that *resolves* to a private IP
// (rebinding) still passes — add resolve-then-check if --from ever fetches from
// untrusted hostnames at scale.
const PRIVATE_HOST =
/^(localhost|.*\.local|.*\.internal|127\.|10\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|\[?(::1|::ffff:127\.|f[cd][0-9a-f]{2}:|fe80:))/i;
export function isDirectMediaUrl(u) {
let url;
try {
url = new URL(u);
} catch {
return false;
}
if (url.protocol !== "http:" && url.protocol !== "https:") return false;
if (PLATFORM_HOSTS.test(url.hostname)) return false;
if (PRIVATE_HOST.test(url.hostname)) return false;
return MEDIA_EXT.test(url.pathname);
}
@@ -0,0 +1,46 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { isDirectMediaUrl } from "./freeze.mjs";
test("accepts direct public media URLs", () => {
assert.equal(isDirectMediaUrl("https://cdn.example.com/clip.mp4"), true);
assert.equal(isDirectMediaUrl("https://example.com/a/b/track.mp3"), true);
assert.equal(isDirectMediaUrl("http://example.com/logo.svg"), true);
});
test("rejects platform pages (no yt-dlp)", () => {
assert.equal(isDirectMediaUrl("https://www.youtube.com/watch?v=abc"), false);
assert.equal(isDirectMediaUrl("https://youtu.be/abc"), false);
assert.equal(isDirectMediaUrl("https://vimeo.com/12345"), false);
assert.equal(isDirectMediaUrl("https://x.com/u/status/1"), false);
});
test("rejects non-direct / non-media URLs", () => {
assert.equal(isDirectMediaUrl("https://example.com/page"), false, "no media extension");
assert.equal(isDirectMediaUrl("ftp://example.com/a.mp4"), false, "non-http(s)");
assert.equal(isDirectMediaUrl("not a url"), false);
});
test("rejects local / private hosts (SSRF guard, m11)", () => {
for (const u of [
"http://localhost/a.mp4",
"http://127.0.0.1/a.mp4",
"http://127.1.2.3/a.mp4",
"http://0.0.0.0/a.mp4",
"http://10.0.0.5/a.mp4",
"http://192.168.1.1/a.mp4",
"http://172.16.0.1/a.mp4",
"http://172.31.255.255/a.mp4",
"http://169.254.169.254/a.mp4", // cloud metadata endpoint
"http://printer.local/a.mp4",
"http://svc.internal/a.mp4",
"http://[::1]/a.mp4",
"http://[fe80::1]/a.mp4",
"http://[fd00::1]/a.mp4",
]) {
assert.equal(isDirectMediaUrl(u), false, `should block ${u}`);
}
// A public host that merely starts with similar digits is still allowed.
assert.equal(isDirectMediaUrl("https://172.40.0.1/a.mp4"), true, "172.40 is public");
assert.equal(isDirectMediaUrl("https://11.example.com/a.mp4"), true);
});
@@ -0,0 +1,265 @@
// Vendored plain-JS copy of packages/core/src/mediaGradeAnalyzer.ts.
// packages/core/src/mediaGradeAnalyzer.vendoredParity.test.ts guards behavior drift.
import { execFileSync } from "node:child_process";
import { basename, extname } from "node:path";
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
const SAMPLE_FRAMES = 5;
const DEFAULT_TIMEOUT_MS = 15_000;
const ADJUST_LIMITS = {
exposure: { min: -2, max: 2 },
contrast: { min: -1, max: 1 },
whites: { min: -1, max: 1 },
blacks: { min: -1, max: 1 },
temperature: { min: -1, max: 1 },
tint: { min: -1, max: 1 },
};
function clamp(value, key) {
const limit = ADJUST_LIMITS[key];
if (!Number.isFinite(value)) return 0;
return Math.min(limit.max, Math.max(limit.min, value));
}
function round(value) {
const rounded = Math.round(value * 1000) / 1000;
return Object.is(rounded, -0) ? 0 : rounded;
}
function average(values) {
return values.reduce((sum, value) => sum + value, 0) / Math.max(1, values.length);
}
function probeMedia(mediaPath, ffprobePath) {
try {
const raw = execFileSync(
ffprobePath,
[
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=color_space,color_transfer,color_primaries,pix_fmt,duration:format=duration",
"-of",
"json",
"--",
mediaPath,
],
{ encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"] },
);
const parsed = JSON.parse(raw);
const stream = Array.isArray(parsed.streams) ? (parsed.streams[0] ?? {}) : {};
const duration = Number(stream.duration ?? parsed.format?.duration);
const text = (key) =>
typeof stream[key] === "string" && stream[key] ? stream[key] : "unknown";
return {
duration: Number.isFinite(duration) && duration > 0 ? duration : null,
colorSpace: text("color_space"),
transfer: text("color_transfer"),
primaries: text("color_primaries"),
pixelFormat: text("pix_fmt"),
};
} catch {
return {
duration: null,
colorSpace: "unknown",
transfer: "unknown",
primaries: "unknown",
pixelFormat: "unknown",
};
}
}
export function parseMediaTreatmentSignalStats(raw) {
const frames = [];
let current = null;
for (const line of String(raw).split(/\r?\n/)) {
// One digit is enough: .* consumes the rest without overlapping repetitions.
const frame = line.match(/^frame:\d.*pts_time:([+-]?(?:\d+(?:\.\d+)?|\.\d+))/);
if (frame) {
if (current) frames.push(current);
current = { ptsTime: Number(frame[1]) };
continue;
}
const stat = line.match(/lavfi\.signalstats\.([A-Z]+)=([+-]?(?:\d+(?:\.\d+)?|\.\d+))/);
if (!stat) continue;
current ??= {};
current[stat[1]] = Number(stat[2]);
}
if (current) frames.push(current);
return frames.filter(
(frame) =>
Number.isFinite(frame.YMIN) &&
Number.isFinite(frame.YLOW) &&
Number.isFinite(frame.YAVG) &&
Number.isFinite(frame.YHIGH) &&
Number.isFinite(frame.YMAX) &&
Number.isFinite(frame.UAVG) &&
Number.isFinite(frame.VAVG),
);
}
function summarizeFrames(frames) {
if (frames.length === 0) throw new Error("FFmpeg returned no analyzable video frames");
return {
frames: frames.length,
yMin: Math.min(...frames.map((frame) => frame.YMIN)),
yLow: average(frames.map((frame) => frame.YLOW)),
yAvg: average(frames.map((frame) => frame.YAVG)),
yHigh: average(frames.map((frame) => frame.YHIGH)),
yMax: Math.max(...frames.map((frame) => frame.YMAX)),
uAvg: average(frames.map((frame) => frame.UAVG)),
vAvg: average(frames.map((frame) => frame.VAVG)),
satAvg: average(frames.map((frame) => frame.SATAVG ?? 0)),
shadowClipRisk: average(frames.map((frame) => (frame.YLOW <= 16 ? 1 : 0))),
highlightClipRisk: average(frames.map((frame) => (frame.YHIGH >= 235 ? 1 : 0))),
};
}
function suggestedExposure(normalizedAverage, yLow, yHigh) {
if (normalizedAverage < 0.28 && yHigh / 255 < 0.65) {
return clamp((0.32 - normalizedAverage) * 1.2, "exposure");
}
if (normalizedAverage > 0.72 && yLow / 255 > 0.3) {
return clamp((0.68 - normalizedAverage) * 1.2, "exposure");
}
return 0;
}
function suggestedTemperature(uAverage, vAverage) {
const chromaWarmth = (vAverage - 128 + (128 - uAverage)) / 128;
return Math.abs(chromaWarmth) >= 0.08 ? clamp(-chromaWarmth * 0.25, "temperature") : 0;
}
function suggestedTint(uAverage, vAverage) {
const cast = uAverage + vAverage - 256;
return Math.abs(cast) >= 10 ? clamp(-cast / 512, "tint") : 0;
}
export function statsToAdjust(stats) {
const yMin = Number(stats.yMin);
const yLow = Number(stats.yLow ?? stats.yMin);
const yMax = Number(stats.yMax);
const yHigh = Number(stats.yHigh ?? stats.yMax);
const yAvg = Number(stats.yAvg);
const uAvg = Number(stats.uAvg);
const vAvg = Number(stats.vAvg);
const shadowClipRisk = Number(stats.shadowClipRisk ?? (yLow <= 16 ? 1 : 0));
const highlightClipRisk = Number(stats.highlightClipRisk ?? (yHigh >= 235 ? 1 : 0));
const percentileSpread = (yHigh - yLow) / 255;
const normalizedAverage = yAvg / 255;
const contrast = percentileSpread < 0.35 ? clamp((0.35 - percentileSpread) * 0.4, "contrast") : 0;
return {
adjust: {
exposure: round(suggestedExposure(normalizedAverage, yLow, yHigh)),
contrast: round(contrast),
blacks: round(clamp(shadowClipRisk * 0.08, "blacks")),
whites: round(clamp(-highlightClipRisk * 0.08, "whites")),
temperature: round(suggestedTemperature(uAvg, vAvg)),
tint: round(suggestedTint(uAvg, vAvg)),
},
measured: {
frames: Number(stats.frames ?? 1),
yMin: round(yMin),
yLow: round(yLow),
yAvg: round(yAvg),
yHigh: round(yHigh),
yMax: round(yMax),
uAvg: round(uAvg),
vAvg: round(vAvg),
satAvg: round(Number(stats.satAvg ?? 0)),
shadowClipRisk: round(shadowClipRisk),
highlightClipRisk: round(highlightClipRisk),
},
};
}
export function summarizeMediaTreatmentAnalysis(probe, frames) {
const result = statsToAdjust(summarizeFrames(frames));
const hdr = ["smpte2084", "arib-std-b67"].includes(probe.transfer);
const diagnosis = [];
if (result.measured.shadowClipRisk > 0) {
diagnosis.push("sampled frames contain deep or clipped shadows");
}
if (result.measured.highlightClipRisk > 0) {
diagnosis.push("sampled frames contain bright or clipped highlights");
}
if (diagnosis.length === 0) diagnosis.push("no obvious technical imbalance in sampled frames");
const warnings = [];
if (hdr) warnings.push("HDR transfer detected; the realtime treatment path is SDR/Rec.709.");
if (probe.colorSpace === "unknown" || probe.transfer === "unknown") {
warnings.push(
"Source color metadata is incomplete; camera LOG cannot be identified reliably from container metadata alone.",
);
}
return {
...result,
source: {
colorSpace: probe.colorSpace,
transfer: probe.transfer,
primaries: probe.primaries,
pixelFormat: probe.pixelFormat,
hdr,
log: "unknown",
},
diagnosis,
warnings,
};
}
export function analyzeMediaGrade(
mediaPath,
{ ffmpegPath = "ffmpeg", ffprobePath = "ffprobe" } = {},
) {
try {
const probe = probeMedia(mediaPath, ffprobePath);
const isImage = IMAGE_EXT.has(extname(mediaPath).toLowerCase());
const fps =
!isImage && probe.duration
? Math.max(0.1, Math.min(2, SAMPLE_FRAMES / probe.duration))
: null;
const filters = [
fps ? `fps=${fps.toFixed(4)}` : null,
"format=yuv444p",
"signalstats",
"metadata=print:file=-",
]
.filter(Boolean)
.join(",");
const raw = execFileSync(
ffmpegPath,
[
"-hide_banner",
"-nostdin",
"-v",
"error",
"-i",
mediaPath,
"-vf",
filters,
"-frames:v",
String(SAMPLE_FRAMES),
"-f",
"null",
"-",
],
{
encoding: "utf8",
timeout: Number(process.env.HYPERFRAMES_ANALYZE_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
stdio: ["ignore", "pipe", "pipe"],
},
);
return summarizeMediaTreatmentAnalysis(probe, parseMediaTreatmentSignalStats(raw));
} catch (error) {
throw new Error(`grade analysis failed for ${mediaPath}: ${error.message}`);
}
}
export function formatMeasuredNote(mediaPath, measured) {
return `media-use: measured ${basename(mediaPath)}: frames=${measured.frames}, YMIN=${measured.yMin}, YLOW=${measured.yLow}, YAVG=${measured.yAvg}, YHIGH=${measured.yHigh}, YMAX=${measured.yMax}, UAVG=${measured.uAvg}, VAVG=${measured.vAvg}; adjust is a starting suggestion`;
}
@@ -0,0 +1,285 @@
import { strict as assert } from "node:assert";
import { execFileSync, spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, rmSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { test } from "node:test";
import {
analyzeMediaGrade,
formatMeasuredNote,
parseMediaTreatmentSignalStats,
statsToAdjust,
summarizeMediaTreatmentAnalysis,
} from "./grade-analyzer.mjs";
const SIGNALSTATS = `frame:0 pts:0 pts_time:0
lavfi.signalstats.YMIN=0
lavfi.signalstats.YLOW=8
lavfi.signalstats.YAVG=100
lavfi.signalstats.YHIGH=240
lavfi.signalstats.YMAX=255
lavfi.signalstats.UAVG=120
lavfi.signalstats.VAVG=140
lavfi.signalstats.SATAVG=40
frame:1 pts:2000 pts_time:2
lavfi.signalstats.YMIN=10
lavfi.signalstats.YLOW=20
lavfi.signalstats.YAVG=130
lavfi.signalstats.YHIGH=220
lavfi.signalstats.YMAX=245
lavfi.signalstats.UAVG=130
lavfi.signalstats.VAVG=125
lavfi.signalstats.SATAVG=60`;
// The "Test: skills" CI job runs bare `node --test` with no ffmpeg on PATH (by
// design — skills tests are meant to be node-builtin-only). Tests that shell to
// ffmpeg skip there and run wherever ffmpeg is present (locally, dev).
const FFMPEG_SKIP =
spawnSync("ffmpeg", ["-version"], { stdio: "ignore" }).status === 0
? false
: "ffmpeg not on PATH";
const ADJUST_LIMITS = {
exposure: { min: -2, max: 2 },
contrast: { min: -1, max: 1 },
highlights: { min: -1, max: 1 },
shadows: { min: -1, max: 1 },
whites: { min: -1, max: 1 },
blacks: { min: -1, max: 1 },
temperature: { min: -1, max: 1 },
tint: { min: -1, max: 1 },
vibrance: { min: -1, max: 1 },
saturation: { min: -1, max: 1 },
};
function makeFrame(dir, name, color) {
const out = join(dir, name);
execFileSync(
"ffmpeg",
[
"-hide_banner",
"-loglevel",
"error",
"-f",
"lavfi",
"-i",
`color=c=${color}:s=64x64`,
"-frames:v",
"1",
"-y",
out,
],
{ stdio: "pipe" },
);
return out;
}
function assertWithinLimits(adjust) {
for (const [key, value] of Object.entries(adjust)) {
const limit = ADJUST_LIMITS[key];
assert.ok(limit, `unexpected adjust key ${key}`);
assert.ok(value >= limit.min && value <= limit.max, `${key} out of range: ${value}`);
}
}
test("under-exposed synthetic frame suggests positive exposure", { skip: FFMPEG_SKIP }, () => {
const dir = mkdtempSync(join(tmpdir(), "mu-grade-under-"));
try {
const file = makeFrame(dir, "under.png", "0x202020");
const { adjust, measured } = analyzeMediaGrade(file);
assert.ok(measured.frames >= 1);
assert.ok(adjust.exposure > 0, `expected positive exposure, got ${adjust.exposure}`);
assertWithinLimits(adjust);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test(
"over-exposed synthetic frame pulls exposure down without inventing clipping",
{ skip: FFMPEG_SKIP },
() => {
const dir = mkdtempSync(join(tmpdir(), "mu-grade-over-"));
try {
const file = makeFrame(dir, "over.png", "white");
const { adjust } = analyzeMediaGrade(file);
assert.ok(adjust.exposure < 0, `expected negative exposure, got ${adjust.exposure}`);
assert.ok(adjust.whites <= 0, `expected non-positive whites, got ${adjust.whites}`);
assertWithinLimits(adjust);
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);
test(
"warm-cast synthetic frame suggests negative temperature correction",
{ skip: FFMPEG_SKIP },
() => {
const dir = mkdtempSync(join(tmpdir(), "mu-grade-warm-"));
try {
const file = makeFrame(dir, "warm.png", "orange");
const { adjust } = analyzeMediaGrade(file);
assert.ok(adjust.temperature < 0, `expected cooling correction, got ${adjust.temperature}`);
assertWithinLimits(adjust);
} finally {
rmSync(dir, { recursive: true, force: true });
}
},
);
test("low-spread stats suggest positive contrast", () => {
const { adjust } = statsToAdjust({
frames: 1,
yMin: 104,
yMax: 116,
yAvg: 110,
uAvg: 128,
vAvg: 128,
});
assert.ok(adjust.contrast > 0, `expected positive contrast, got ${adjust.contrast}`);
assertWithinLimits(adjust);
});
test("malformed media fails cleanly", () => {
assert.throws(
() => analyzeMediaGrade(join(tmpdir(), "does-not-exist.png")),
/grade analysis failed/,
);
});
test(
"media path with shell metacharacters is passed as argv, not a shell string",
{
skip: FFMPEG_SKIP,
},
() => {
const dir = mkdtempSync(join(tmpdir(), "mu-grade-shell-"));
const sentinel = join(process.cwd(), "mu-grade-shell-sentinel.png");
try {
if (existsSync(sentinel)) unlinkSync(sentinel);
const file = makeFrame(dir, "frame; touch mu-grade-shell-sentinel.png", "orange");
const result = analyzeMediaGrade(file);
assert.ok(result.measured.frames >= 1);
assert.equal(existsSync(sentinel), false);
} finally {
if (existsSync(sentinel)) unlinkSync(sentinel);
rmSync(dir, { recursive: true, force: true });
}
},
);
test("measured note is a stderr-safe single-line summary", () => {
const note = formatMeasuredNote("/tmp/frame.png", {
frames: 1,
yMin: 10,
yMax: 240,
yAvg: 80,
uAvg: 120,
vAvg: 140,
});
assert.match(note, /^media-use: measured /);
assert.match(note, /YMIN=10/);
assert.match(note, /YMAX=240/);
assert.match(note, /YAVG=80/);
assert.equal(note.includes("\n"), false);
});
test("parses deterministic FFmpeg signalstats frames", () => {
const frames = parseMediaTreatmentSignalStats(SIGNALSTATS);
assert.equal(frames.length, 2);
assert.deepEqual(
{
ptsTime: frames[0]?.ptsTime,
YLOW: frames[0]?.YLOW,
YAVG: frames[0]?.YAVG,
YHIGH: frames[0]?.YHIGH,
SATAVG: frames[0]?.SATAVG,
},
{ ptsTime: 0, YLOW: 8, YAVG: 100, YHIGH: 240, SATAVG: 40 },
);
});
test("keeps adjust output while adding HDR metadata and warnings", () => {
const result = summarizeMediaTreatmentAnalysis(
{
duration: 4,
colorSpace: "bt2020nc",
transfer: "smpte2084",
primaries: "bt2020",
pixelFormat: "yuv420p10le",
},
parseMediaTreatmentSignalStats(SIGNALSTATS),
);
assert.deepEqual(
{
blacks: result.adjust.blacks,
whites: result.adjust.whites,
frames: result.measured.frames,
yLow: result.measured.yLow,
yHigh: result.measured.yHigh,
satAvg: result.measured.satAvg,
shadowClipRisk: result.measured.shadowClipRisk,
highlightClipRisk: result.measured.highlightClipRisk,
},
{
blacks: 0.04,
whites: -0.04,
frames: 2,
yLow: 14,
yHigh: 230,
satAvg: 50,
shadowClipRisk: 0.5,
highlightClipRisk: 0.5,
},
);
assert.equal(result.source.hdr, true);
assert.equal(result.source.log, "unknown");
assert.match(result.warnings[0] ?? "", /HDR/);
});
test("does not force middle-gray exposure onto healthy intentional contrast", () => {
const result = summarizeMediaTreatmentAnalysis(
{
duration: 4,
colorSpace: "bt709",
transfer: "bt709",
primaries: "bt709",
pixelFormat: "yuv420p",
},
[
{
YMIN: 20,
YLOW: 42,
YAVG: 122,
YHIGH: 190,
YMAX: 220,
UAVG: 128,
VAVG: 128,
SATAVG: 40,
},
{
YMIN: 18,
YLOW: 38,
YAVG: 118,
YHIGH: 196,
YMAX: 225,
UAVG: 128,
VAVG: 128,
SATAVG: 42,
},
],
);
assert.deepEqual(
{
exposure: result.adjust.exposure,
blacks: result.adjust.blacks,
whites: result.adjust.whites,
temperature: result.adjust.temperature,
tint: result.adjust.tint,
},
{ exposure: 0, blacks: 0, whites: 0, temperature: 0, tint: 0 },
);
});
@@ -0,0 +1,191 @@
import { execFileSync } from "node:child_process";
import { track } from "./telemetry.mjs";
// v0.3.0 is the first CLI that can use an OAuth session; v0.1.x/0.2.x reject it
// ("heygen-cli can't use OAuth yet"), and OAuth is what the free-usage path
// needs — so anything below this can't authenticate for free usage at all.
export const HEYGEN_MIN_VERSION = "0.3.0";
// Free-usage path is OAuth (`--oauth` → subscription/free credits); `--api-key`
// bills API credits, so the onboarding steers to OAuth. Keep pipe-to-shell
// installer text out of the runtime module; the docs are the safer source of
// truth for platform-specific setup.
export const HEYGEN_INSTALL_COMMAND =
"Install the CLI from https://developers.heygen.com/cli, then run: heygen auth login --oauth";
export const HEYGEN_AUTH_COMMAND = "heygen auth login --oauth";
export const HEYGEN_UPDATE_COMMAND = "heygen update";
export const HEYGEN_CLIENT_SOURCE_ARGV = ["--headers", "X-HeyGen-Client-Source: media-use"];
export const HEYGEN_NOT_FOUND_MESSAGE = `media-use: heygen CLI not found — it's the free path for bgm/image/voice/avatar-video. ${HEYGEN_INSTALL_COMMAND}`;
export const HEYGEN_NOT_AUTHENTICATED_MESSAGE = `media-use: heygen CLI not authenticated (free usage) — run: ${HEYGEN_AUTH_COMMAND}`;
export const HEYGEN_OUTDATED_MESSAGE = `media-use: heygen CLI is outdated — run: ${HEYGEN_UPDATE_COMMAND} (need >= v${HEYGEN_MIN_VERSION})`;
const ACTIONABLE_MESSAGES = new Set([
HEYGEN_NOT_FOUND_MESSAGE,
HEYGEN_NOT_AUTHENTICATED_MESSAGE,
HEYGEN_OUTDATED_MESSAGE,
]);
export function classifyHeygenError(err) {
return classifyHeygenErrorResult(err).message;
}
export function classifyHeygenErrorCode(err) {
return classifyHeygenErrorResult(err).code;
}
function classifyHeygenErrorResult(err) {
const detail = heygenErrorDetail(err);
const text = [err?.stderr, err?.stdout, err?.message, detail]
.map((value) => textOf(value))
.filter(Boolean)
.join("\n");
const lower = text.toLowerCase();
// Only ENOENT (spawn of a missing binary) or a shell's "command not found"
// mean the CLI itself is absent. A bare "not found" would misfire on the CLI's
// own resource errors (e.g. a stale voiceId → "voice not found"), whose message
// embeds the `heygen ...` command line — sending users to reinstall a CLI they
// just ran successfully. Keep this narrow.
if (err?.code === "ENOENT" || lower.includes("command not found")) {
return { code: "not_found", message: HEYGEN_NOT_FOUND_MESSAGE };
}
if (
lower.includes("unauthorized") ||
lower.includes("unauthenticated") ||
// \b401\b, not a bare "401" substring — otherwise request IDs (req-401abc),
// URLs, and retry-after headers would misclassify as an auth failure.
/\b401\b/.test(lower) ||
lower.includes("not logged in") ||
lower.includes("no api key") ||
lower.includes("missing api key") ||
lower.includes("invalid api key") ||
lower.includes("login required") ||
lower.includes("auth required") ||
lower.includes("authentication required")
) {
return { code: "not_authenticated", message: HEYGEN_NOT_AUTHENTICATED_MESSAGE };
}
const version = firstSemver(text);
if (version && versionLessThan(version, HEYGEN_MIN_VERSION)) {
return { code: "outdated", message: HEYGEN_OUTDATED_MESSAGE };
}
if (
lower.includes("rate limit") ||
lower.includes("quota") ||
lower.includes("insufficient credit") ||
lower.includes("too many requests") ||
lower.includes("throttled") ||
/\b429\b/.test(lower)
) {
return { code: "rate_limited", message: detail };
}
return { code: "other", message: detail };
}
// reportHeygenFailure's callers (voice-provider.mjs, heygen-search.mjs) are
// synchronous and several layers below the CLI's process.exit() calls, so
// they can't await this tracking call themselves. Stash each attempt's
// promise here so a caller closer to exit (resolve.mjs) can join it first —
// 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 {
console.error(`media-use: \`${context}\` failed: ${message}`);
}
try {
const tracked = Promise.resolve(
trackEvent("media_use_provider_error", { provider: "heygen", reason: code }),
).catch(() => {});
pendingFailureTracking.add(tracked);
void tracked.finally(() => pendingFailureTracking.delete(tracked));
return tracked;
} catch {
// Telemetry must never affect the provider failure path.
return Promise.resolve();
}
}
// Awaits every provider-error track fired since the last flush, so a caller
// about to process.exit() doesn't orphan one mid-request (both are separate,
// non-keepalive HTTP connections with no ordering guarantee otherwise).
// Never rejects: each tracked promise already swallows its own failure.
export async function flushHeygenFailureTracking() {
if (pendingFailureTracking.size === 0) return;
await Promise.all(pendingFailureTracking);
}
// Shared discovery/generation helper for the CLI-shelling providers (voice,
// avatar-video): run a heygen JSON subcommand, report+classify on failure,
// and hand the caller the classified reason via onError (used by callers that
// need to distinguish e.g. not_authenticated from other failures).
export function runHeygenJson(bin, argv, label, onError) {
let out;
try {
out = execFileSync(bin, argv, {
encoding: "utf8",
timeout: 120000,
stdio: ["pipe", "pipe", "pipe"],
});
} catch (err) {
reportHeygenFailure(err, `${bin} ${label}`);
onError?.(classifyHeygenErrorCode(err));
return null;
}
try {
return JSON.parse(out);
} catch {
console.error(`media-use: \`${bin} ${label}\` returned non-JSON output`);
return null;
}
}
export function firstSemver(text) {
const match = String(text || "").match(/\bv?(\d+)\.(\d+)\.(\d+)\b/);
return match ? `${match[1]}.${match[2]}.${match[3]}` : null;
}
export function versionLessThan(version, minimum) {
const left = versionParts(version);
const right = versionParts(minimum);
if (!left || !right) return false;
for (let i = 0; i < 3; i++) {
if (left[i] < right[i]) return true;
if (left[i] > right[i]) return false;
}
return false;
}
function heygenErrorDetail(err) {
return textOf(err?.stderr) || textOf(err?.stdout) || err?.message || String(err);
}
function textOf(value) {
return value == null ? "" : String(value).trim();
}
function versionParts(version) {
const match = String(version || "").match(/^v?(\d+)\.(\d+)\.(\d+)$/);
return match ? match.slice(1).map((part) => Number.parseInt(part, 10)) : null;
}
@@ -0,0 +1,301 @@
import { strict as assert } from "node:assert";
import { spawnSync } from "node:child_process";
import { test } from "node:test";
import {
classifyHeygenError,
classifyHeygenErrorCode,
consumeHeygenRemediation,
flushHeygenFailureTracking,
HEYGEN_NOT_AUTHENTICATED_MESSAGE,
HEYGEN_NOT_FOUND_MESSAGE,
HEYGEN_OUTDATED_MESSAGE,
reportHeygenFailure,
} from "./heygen-cli.mjs";
function captureFailureReport(err, context, trackEvent) {
const originalError = console.error;
const stderrCalls = [];
console.error = (...args) => stderrCalls.push(args);
try {
if (trackEvent) {
reportHeygenFailure(err, context, trackEvent);
} else {
reportHeygenFailure(err, context);
}
} finally {
console.error = originalError;
}
return stderrCalls;
}
test("classifies ENOENT-style missing heygen errors with install instructions", () => {
const message = classifyHeygenError({ code: "ENOENT", message: "spawn heygen ENOENT" });
assert.equal(message, HEYGEN_NOT_FOUND_MESSAGE);
});
test("classifies auth failures with login instructions", () => {
const message = classifyHeygenError({ stderr: Buffer.from("Error: not logged in") });
assert.equal(message, HEYGEN_NOT_AUTHENTICATED_MESSAGE);
});
test("classifies a real 401 as auth, but not a bare 401 substring in prose", () => {
assert.equal(
classifyHeygenError({ stderr: Buffer.from("HTTP 401 Unauthorized") }),
HEYGEN_NOT_AUTHENTICATED_MESSAGE,
);
// A request id that merely contains "401" must NOT read as an auth failure.
const noise = classifyHeygenError({ stderr: Buffer.from("upload failed (request req-401abc)") });
assert.notEqual(noise, HEYGEN_NOT_AUTHENTICATED_MESSAGE);
});
test("classifies old heygen versions with update instructions", () => {
const message = classifyHeygenError({
stderr: Buffer.from("heygen v0.1.5 does not support --headers"),
});
assert.equal(message, HEYGEN_OUTDATED_MESSAGE);
});
test("does not misclassify a resource 'not found' error as a missing CLI", () => {
// A stale voiceId makes `heygen voice speech create` fail with "voice not
// found"; the error message embeds the `heygen ...` command line. This must
// pass through as detail, not send the user to reinstall a working CLI.
const message = classifyHeygenError({
stderr: Buffer.from("Error: voice not found (id: stale-123)"),
message: "Command failed: heygen voice speech create --voice stale-123",
});
assert.notEqual(message, HEYGEN_NOT_FOUND_MESSAGE);
assert.equal(message, "Error: voice not found (id: stale-123)");
});
test("classifies a shell 'command not found' as a missing CLI", () => {
const message = classifyHeygenError({ stderr: Buffer.from("bash: heygen: command not found") });
assert.equal(message, HEYGEN_NOT_FOUND_MESSAGE);
});
test("passes through unrelated errors", () => {
const message = classifyHeygenError({
stderr: Buffer.from("rate limit exceeded"),
message: "Command failed",
});
assert.equal(message, "rate limit exceeded");
});
test("classifies existing HeyGen failures with stable reason codes", () => {
assert.equal(classifyHeygenErrorCode({ code: "ENOENT" }), "not_found");
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("HTTP 401 Unauthorized") }),
"not_authenticated",
);
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("heygen v0.1.5 is unsupported") }),
"outdated",
);
assert.equal(classifyHeygenErrorCode({ stderr: Buffer.from("provider unavailable") }), "other");
});
test("classifies rate-limit text case-insensitively", () => {
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("RATE LIMIT exceeded") }),
"rate_limited",
);
});
test("classifies quota and insufficient-credit errors as rate limited", () => {
for (const detail of ["Quota exhausted", "INSUFFICIENT CREDIT remaining"]) {
assert.equal(classifyHeygenErrorCode({ stderr: Buffer.from(detail) }), "rate_limited");
}
});
test("classifies the literal 429 reason phrase and throttling language as rate limited", () => {
for (const detail of ["Too Many Requests", "Error: throttled by upstream, retry later"]) {
assert.equal(classifyHeygenErrorCode({ stderr: Buffer.from(detail) }), "rate_limited");
}
});
test("does not misclassify unrelated errors that share a word with the new phrasing", () => {
// Shares "too many" with "too many requests" but is a distinct failure (fd
// exhaustion, not a rate limit) — the match must require the full phrase.
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("Too many open file descriptors") }),
"other",
);
});
test("classifies a bare 429 as rate limited without matching request IDs", () => {
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("HTTP 429 Too Many Requests") }),
"rate_limited",
);
assert.equal(
classifyHeygenErrorCode({ stderr: Buffer.from("request req-429abc failed") }),
"other",
);
});
test("tracks not-found failures without changing actionable output", () => {
const trackingCalls = [];
const stderrCalls = captureFailureReport({ code: "ENOENT" }, "heygen asset search", (...args) =>
trackingCalls.push(args),
);
assert.deepEqual(stderrCalls, [[HEYGEN_NOT_FOUND_MESSAGE]]);
assert.deepEqual(trackingCalls, [
["media_use_provider_error", { provider: "heygen", reason: "not_found" }],
]);
});
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(
{ stderr: Buffer.from("private provider detail") },
"heygen asset search",
(...args) => trackingCalls.push(args),
);
assert.deepEqual(stderrCalls, [
["media-use: `heygen asset search` failed: private provider detail"],
]);
assert.deepEqual(trackingCalls, [
["media_use_provider_error", { provider: "heygen", reason: "other" }],
]);
});
test("keeps failure output observable when telemetry is opted out", () => {
const previousOptOut = process.env.HYPERFRAMES_NO_TELEMETRY;
process.env.HYPERFRAMES_NO_TELEMETRY = "1";
try {
const stderrCalls = captureFailureReport({ code: "ENOENT" }, "heygen asset search");
assert.deepEqual(stderrCalls, [[HEYGEN_NOT_FOUND_MESSAGE]]);
} finally {
if (previousOptOut === undefined) {
delete process.env.HYPERFRAMES_NO_TELEMETRY;
} else {
process.env.HYPERFRAMES_NO_TELEMETRY = previousOptOut;
}
}
});
test("keeps failure output observable when tracking throws synchronously", () => {
const originalError = console.error;
const stderrCalls = [];
let thrown;
console.error = (...args) => stderrCalls.push(args);
try {
try {
reportHeygenFailure({ code: "ENOENT" }, "heygen asset search", () => {
throw new Error("tracking failed");
});
} catch (err) {
thrown = err;
}
} finally {
console.error = originalError;
}
assert.deepEqual(stderrCalls, [[HEYGEN_NOT_FOUND_MESSAGE]]);
assert.equal(thrown, undefined);
});
test("does not leave rejected tracking promises unhandled", () => {
const moduleUrl = new URL("./heygen-cli.mjs", import.meta.url).href;
const script = `
import { reportHeygenFailure } from ${JSON.stringify(moduleUrl)};
reportHeygenFailure(
{ stderr: "provider unavailable" },
"heygen asset search",
() => Promise.reject(new Error("tracking failed")),
);
await new Promise((resolve) => setImmediate(resolve));
`;
const child = spawnSync(
process.execPath,
["--unhandled-rejections=strict", "--input-type=module", "--eval", script],
{ encoding: "utf8", timeout: 5000 },
);
assert.equal(child.error, undefined);
assert.equal(child.signal, null);
assert.equal(child.status, 0, child.stderr);
assert.equal(child.stderr, "media-use: `heygen asset search` failed: provider unavailable\n");
});
test("flushHeygenFailureTracking waits for a pending report before resolving", async () => {
const events = [];
let releaseTrack;
const gate = new Promise((resolve) => {
releaseTrack = resolve;
});
// Mirrors the real call sites (voice-provider.mjs, heygen-search.mjs):
// fire-and-forget, the return value is never awaited by the caller.
reportHeygenFailure({ code: "ENOENT" }, "heygen voice speech", () =>
gate.then(() => {
events.push("track-settled");
}),
);
const flushed = flushHeygenFailureTracking().then(() => {
events.push("flush-resolved");
});
// Let several pending microtasks drain before releasing the gate, so this
// proves flush is genuinely still waiting on the tracked promise -- not
// merely that it hasn't had a tick yet.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
assert.deepEqual(events, [], "flush must not resolve while the tracked promise is still pending");
releaseTrack();
await flushed;
assert.deepEqual(
events,
["track-settled", "flush-resolved"],
"flush must resolve only after the pending track settles, in that order",
);
});
test("flushHeygenFailureTracking resolves immediately when nothing is pending", async () => {
await flushHeygenFailureTracking();
});
@@ -0,0 +1,45 @@
import { execFileSync } from "node:child_process";
import { HEYGEN_CLIENT_SOURCE_ARGV, reportHeygenFailure } from "./heygen-cli.mjs";
export function heygenSearch(subcommand, query, { type, limit = 5, minScore } = {}) {
// execFileSync with an argv array (no shell), so query/type/etc. are passed as
// literal arguments — no quoting tricks, no command injection. subcommand is a
// hardcoded multi-word string (e.g. "audio sounds list"), split into tokens.
// Tag the caller via the CLI's allowlisted attribution header (heygen >= v0.3.0).
const args = [...HEYGEN_CLIENT_SOURCE_ARGV, ...subcommand.split(" "), "--query", query];
if (type) args.push("--type", type);
args.push("--limit", String(limit));
// Server-side score floor. Honored by `audio sounds list`; the `asset search`
// backend rejects it, so only audio providers pass minScore (see image-provider).
if (minScore != null) args.push("--min-score", String(minScore));
let out;
try {
out = execFileSync("heygen", args, {
encoding: "utf8",
timeout: 15000,
stdio: ["pipe", "pipe", "pipe"],
});
} catch (err) {
// Don't swallow a broken command / auth failure as "no results" — that turns
// a typo or expired key into a silent dead end. Surface it, then give up.
reportHeygenFailure(err, `heygen ${subcommand}`);
return null;
}
let parsed;
try {
parsed = JSON.parse(out);
} catch {
console.error(`media-use: \`heygen ${subcommand}\` returned non-JSON output`);
return null;
}
if (parsed?.error) {
const e = parsed.error;
console.error(`media-use: \`heygen ${subcommand}\` error: ${e.message ?? JSON.stringify(e)}`);
return null;
}
const data = parsed?.data;
return Array.isArray(data) && data.length > 0 ? data : null;
}
@@ -0,0 +1,49 @@
import { strict as assert } from "node:assert";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { HEYGEN_CLIENT_SOURCE_HEADERS } from "../../audio/scripts/lib/heygen.mjs";
import { HEYGEN_CLIENT_SOURCE_ARGV } from "./heygen-cli.mjs";
import { heygenSearch } from "./heygen-search.mjs";
test("tags HeyGen searches with the shared media-use client source", () => {
const dir = mkdtempSync(join(tmpdir(), "media-use-heygen-search-"));
const capturePath = join(dir, "argv.log");
const heygenPath = join(dir, "heygen");
const previousPath = process.env.PATH;
const previousCapturePath = process.env.HEYGEN_CAPTURE_PATH;
writeFileSync(
heygenPath,
`#!/bin/sh
printf '%s\\n' "$*" >> "$HEYGEN_CAPTURE_PATH"
printf '%s\\n' '{"data":[{"id":"x"}]}'
`,
);
chmodSync(heygenPath, 0o755);
process.env.PATH = `${dir}:${previousPath ?? ""}`;
process.env.HEYGEN_CAPTURE_PATH = capturePath;
try {
const result = heygenSearch("audio sounds list", "ocean", { limit: 1 });
const argv = readFileSync(capturePath, "utf8").trim();
assert.deepEqual(result, [{ id: "x" }]);
assert.match(argv, /X-HeyGen-Client-Source: media-use/);
} finally {
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
if (previousCapturePath === undefined) delete process.env.HEYGEN_CAPTURE_PATH;
else process.env.HEYGEN_CAPTURE_PATH = previousCapturePath;
rmSync(dir, { recursive: true, force: true });
}
});
test("keeps CLI and REST media-use client source headers in lockstep", () => {
const [entry] = Object.entries(HEYGEN_CLIENT_SOURCE_HEADERS);
assert.ok(entry);
const [key, value] = entry;
assert.equal(HEYGEN_CLIENT_SOURCE_ARGV[1], `${key}: ${value}`);
});
@@ -0,0 +1,138 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { freezeUrl } from "./freeze.mjs";
import {
classifyHeygenErrorCode,
HEYGEN_AUTH_COMMAND,
HEYGEN_CLIENT_SOURCE_ARGV,
reportHeygenFailure,
runHeygenJson,
} from "./heygen-cli.mjs";
export const AVATAR_VIDEO_SIGNIN_MESSAGE = `media-use: avatar video is free for new API users — sign in: ${HEYGEN_AUTH_COMMAND}`;
// Cache only a truthy id -- a transient discovery failure must not poison the
// cache with `null` and permanently disable heygen.video for the rest of the
// process. `onError` lets the caller distinguish not_authenticated (and nudge
// onboarding) from any other discovery failure.
let cachedAvatarId;
function defaultAvatarId(onError) {
if (cachedAvatarId) return cachedAvatarId;
const j = runHeygenJson(
"heygen",
["avatar", "list", "--ownership", "public", "--limit", "1"],
"avatar list",
onError,
);
cachedAvatarId = j?.data?.[0]?.avatar_id || null;
return cachedAvatarId;
}
let cachedStarfishVoiceId;
function defaultStarfishVoiceId(onError) {
if (cachedStarfishVoiceId) return cachedStarfishVoiceId;
const j = runHeygenJson(
"heygen",
["voice", "list", "--engine", "starfish", "--limit", "1"],
"voice list",
onError,
);
cachedStarfishVoiceId = j?.data?.[0]?.voice_id || null;
return cachedStarfishVoiceId;
}
export async function heygenVideoGenerate(intent, ctx) {
let discoveryFailureReason = null;
const captureReason = (reason) => {
discoveryFailureReason ??= reason;
};
// Short-circuit: once one discovery call fails, the result is null either
// way, so don't attempt the second -- that would double-fire the onboarding
// message and the provider-error telemetry ping for what's really one failure.
const avatarId = ctx?.avatarId || defaultAvatarId(captureReason);
if (!avatarId) {
if (discoveryFailureReason === "not_authenticated") console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
return null;
}
const voiceId = ctx?.voiceId || defaultStarfishVoiceId(captureReason);
if (!voiceId) {
if (discoveryFailureReason === "not_authenticated") console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
return null;
}
let out;
try {
out = execFileSync(
"heygen",
[
...HEYGEN_CLIENT_SOURCE_ARGV,
"video",
"create",
"--wait",
"-d",
JSON.stringify({
type: "avatar",
avatar_id: avatarId,
script: intent,
voice_id: voiceId,
}),
],
{
encoding: "utf8",
timeout: 300000,
stdio: ["pipe", "pipe", "pipe"],
},
);
} catch (err) {
if (classifyHeygenErrorCode(err) === "not_authenticated") {
console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
}
reportHeygenFailure(err, "heygen video create");
return null;
}
let parsed;
try {
parsed = JSON.parse(out);
} catch {
console.error("media-use: `heygen video create` returned invalid JSON");
return null;
}
const videoUrl = parsed?.data?.video_url;
if (typeof videoUrl !== "string" || !videoUrl) {
console.error("media-use: `heygen video create` returned no video URL");
return null;
}
let tmpDir;
let tmpPath;
try {
tmpDir = mkdtempSync(join(tmpdir(), "media-use-heygen-video-"));
tmpPath = join(tmpDir, "video.mp4");
await freezeUrl(videoUrl, tmpPath);
} catch (err) {
if (tmpDir) {
try {
rmSync(tmpDir, { recursive: true, force: true });
} catch {
// Preserve the download failure if temporary-directory cleanup fails.
}
}
console.error(`media-use: heygen video download failed: ${err.message}`);
return null;
}
return {
localPath: tmpPath,
ext: ".mp4",
source: "generated",
metadata: {
description: intent,
provider: "heygen.video",
provenance: { prompt: intent },
},
};
}
@@ -0,0 +1,425 @@
import { strict as assert } from "node:assert";
import {
chmodSync,
mkdtempSync,
readFileSync,
rmSync,
statSync,
symlinkSync,
readdirSync,
writeFileSync,
} from "node:fs";
import http from "node:http";
import { tmpdir } from "node:os";
import { basename, dirname, join } from "node:path";
import { test } from "node:test";
import { HEYGEN_NOT_AUTHENTICATED_MESSAGE } from "./heygen-cli.mjs";
import { AVATAR_VIDEO_SIGNIN_MESSAGE } from "./heygen-video-provider.mjs";
function cleanupDownload(localPath) {
if (
basename(localPath) === "video.mp4" &&
basename(dirname(localPath)).startsWith("media-use-heygen-video-")
) {
rmSync(dirname(localPath), { recursive: true, force: true });
} else {
rmSync(localPath, { force: true });
}
}
const VIDEO_FIXTURE = Buffer.from("tiny heygen video fixture");
let importCount = 0;
async function freshGenerate() {
importCount += 1;
const module = await import(`./heygen-video-provider.mjs?test=${importCount}`);
return module.heygenVideoGenerate;
}
async function listenVideoServer() {
const server = http.createServer((req, res) => {
if (req.url !== "/video.mp4") {
res.writeHead(404).end();
return;
}
res.writeHead(200, {
"content-length": VIDEO_FIXTURE.length,
"content-type": "video/mp4",
});
res.end(VIDEO_FIXTURE);
});
await new Promise((resolve) => server.listen(0, resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
return {
server,
url: `http://127.0.0.1:${address.port}/video.mp4`,
};
}
async function listenFailingVideoServer() {
const server = http.createServer((req, res) => {
res.writeHead(500).end();
});
await new Promise((resolve) => server.listen(0, resolve));
const address = server.address();
assert.ok(address && typeof address !== "string");
return {
server,
url: `http://127.0.0.1:${address.port}/video.mp4`,
};
}
function closeServer(server) {
return new Promise((resolve, reject) => {
server.close((err) => (err ? reject(err) : resolve()));
});
}
async function withFakeHeygen(options, run) {
const dir = mkdtempSync(join(tmpdir(), "media-use-heygen-video-provider-"));
const capturePath = join(dir, "argv.log");
const heygenPath = join(dir, "heygen");
const previousEnv = {
PATH: process.env.PATH,
HEYGEN_CAPTURE_PATH: process.env.HEYGEN_CAPTURE_PATH,
HEYGEN_VIDEO_MODE: process.env.HEYGEN_VIDEO_MODE,
HEYGEN_VIDEO_RESPONSE: process.env.HEYGEN_VIDEO_RESPONSE,
HEYGEN_DISCOVERY_MODE: process.env.HEYGEN_DISCOVERY_MODE,
HYPERFRAMES_NO_TELEMETRY: process.env.HYPERFRAMES_NO_TELEMETRY,
};
writeFileSync(
heygenPath,
`#!/bin/sh
printf '%s\\n' "$*" >> "$HEYGEN_CAPTURE_PATH"
case "$*" in
*"avatar list"*)
case "$HEYGEN_DISCOVERY_MODE" in
auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
*) printf '%s\\n' '{"data":[{"avatar_id":"avatar-public-1"}]}' ;;
esac
;;
*"voice list"*)
case "$HEYGEN_DISCOVERY_MODE" in
auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
*) printf '%s\\n' '{"data":[{"voice_id":"voice-starfish-1"}]}' ;;
esac
;;
*"video create"*)
case "$HEYGEN_VIDEO_MODE" in
auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
other) printf '%s\\n' 'provider unavailable' >&2; exit 1 ;;
*) printf '%s\\n' "$HEYGEN_VIDEO_RESPONSE" ;;
esac
;;
esac
`,
);
chmodSync(heygenPath, 0o755);
process.env.PATH = `${dir}:${previousEnv.PATH ?? ""}`;
process.env.HEYGEN_CAPTURE_PATH = capturePath;
process.env.HEYGEN_VIDEO_MODE = options.mode ?? "success";
process.env.HEYGEN_VIDEO_RESPONSE = options.response ?? "";
process.env.HEYGEN_DISCOVERY_MODE = options.discoveryMode ?? "";
process.env.HYPERFRAMES_NO_TELEMETRY = "1";
try {
return await run({
invocations: () => readFileSync(capturePath, "utf8").trim().split("\n"),
});
} finally {
for (const [key, value] of Object.entries(previousEnv)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
rmSync(dir, { recursive: true, force: true });
}
}
function bodyFromInvocation(invocation) {
const marker = " -d ";
const start = invocation.indexOf(marker);
assert.notEqual(start, -1);
return JSON.parse(invocation.slice(start + marker.length));
}
test("downloads a generated avatar video and returns the generated MP4 result", async () => {
const { server, url } = await listenVideoServer();
let localPath;
try {
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: url } }) },
async ({ invocations }) => {
const heygenVideoGenerate = await freshGenerate();
const intent = "Welcome to the HyperFrames launch";
const result = await heygenVideoGenerate(intent, {});
localPath = result?.localPath;
const calls = invocations();
const create = calls.find((call) => call.includes("video create"));
assert.ok(create);
assert.match(create, /--headers X-HeyGen-Client-Source: media-use/);
assert.deepEqual(bodyFromInvocation(create), {
type: "avatar",
avatar_id: "avatar-public-1",
script: intent,
voice_id: "voice-starfish-1",
});
assert.ok(result);
assert.equal(join(tmpdir(), result.localPath.slice(tmpdir().length + 1)), result.localPath);
assert.match(result.localPath, /media-use-heygen-video-[^/\\]+[/\\]video\.mp4$/);
if (process.platform !== "win32") {
assert.equal(statSync(dirname(result.localPath)).mode & 0o777, 0o700);
}
assert.deepEqual(result, {
localPath: result.localPath,
ext: ".mp4",
source: "generated",
metadata: {
description: intent,
provider: "heygen.video",
provenance: { prompt: intent },
},
});
assert.deepEqual(readFileSync(result.localPath), VIDEO_FIXTURE);
},
);
} finally {
if (localPath) cleanupDownload(localPath);
await closeServer(server);
}
});
test("tags video creation but not avatar or voice discovery", async () => {
const { server, url } = await listenVideoServer();
let localPath;
try {
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: url } }) },
async ({ invocations }) => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Header regression guard", {});
localPath = result?.localPath;
const calls = invocations();
assert.equal(calls.length, 3);
assert.match(calls[0], /^avatar list --ownership public --limit 1$/);
assert.doesNotMatch(calls[0], /X-HeyGen-Client-Source/);
assert.match(calls[1], /^voice list --engine starfish --limit 1$/);
assert.doesNotMatch(calls[1], /X-HeyGen-Client-Source/);
assert.match(calls[2], /video create/);
assert.match(calls[2], /X-HeyGen-Client-Source: media-use/);
},
);
} finally {
if (localPath) cleanupDownload(localPath);
await closeServer(server);
}
});
test("uses explicit avatar and voice overrides without discovery", async () => {
const { server, url } = await listenVideoServer();
let localPath;
try {
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: url } }) },
async ({ invocations }) => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Use my presenter", {
avatarId: "avatar-override",
voiceId: "voice-override",
});
localPath = result?.localPath;
const calls = invocations();
assert.equal(calls.length, 1);
assert.match(calls[0], /video create/);
assert.deepEqual(bodyFromInvocation(calls[0]), {
type: "avatar",
avatar_id: "avatar-override",
script: "Use my presenter",
voice_id: "voice-override",
});
},
);
} finally {
if (localPath) cleanupDownload(localPath);
await closeServer(server);
}
});
test("caches discovered avatar and voice IDs for the process", async () => {
const { server, url } = await listenVideoServer();
const localPaths = new Set();
try {
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: url } }) },
async ({ invocations }) => {
const heygenVideoGenerate = await freshGenerate();
for (const intent of ["First avatar video", "Second avatar video"]) {
const result = await heygenVideoGenerate(intent, {});
if (result) localPaths.add(result.localPath);
}
const calls = invocations();
assert.equal(calls.filter((call) => call.startsWith("avatar list ")).length, 1);
assert.equal(calls.filter((call) => call.startsWith("voice list ")).length, 1);
assert.equal(calls.filter((call) => call.includes("video create")).length, 2);
},
);
} finally {
for (const localPath of localPaths) cleanupDownload(localPath);
await closeServer(server);
}
});
test("prints auth onboarding and reports an unauthenticated create failure", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
await withFakeHeygen({ mode: "auth" }, async () => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Sign-in failure", {
avatarId: "avatar-override",
voiceId: "voice-override",
});
assert.equal(result, null);
assert.ok(errors.includes(AVATAR_VIDEO_SIGNIN_MESSAGE));
assert.ok(errors.includes(HEYGEN_NOT_AUTHENTICATED_MESSAGE));
});
});
test("reports other create failures without auth onboarding", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
await withFakeHeygen({ mode: "other" }, async () => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Provider failure", {
avatarId: "avatar-override",
voiceId: "voice-override",
});
assert.equal(result, null);
assert.ok(!errors.includes(AVATAR_VIDEO_SIGNIN_MESSAGE));
assert.ok(errors.includes("media-use: `heygen video create` failed: provider unavailable"));
});
});
test("falls through on non-JSON and error responses", async (t) => {
t.mock.method(console, "error", () => {});
for (const response of ["not JSON", '{"error":{"message":"render failed"}}']) {
await withFakeHeygen({ response }, async () => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Unusable response", {
avatarId: "avatar-override",
voiceId: "voice-override",
});
assert.equal(result, null);
});
}
});
test("onboards and returns null when avatar/voice discovery itself is unauthenticated", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
// Both avatar list AND voice list would fail unauthenticated (discoveryMode
// "auth" applies to both in the fake CLI) -- the short-circuit after the
// first failure must mean only one is ever attempted, so the onboarding
// message and the provider-error telemetry ping each fire exactly once
// instead of double-firing for what's really one auth failure.
await withFakeHeygen({ discoveryMode: "auth" }, async ({ invocations }) => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Discovery auth failure", {});
assert.equal(result, null);
assert.equal(
errors.filter((message) => message === AVATAR_VIDEO_SIGNIN_MESSAGE).length,
1,
"onboarding message must fire exactly once, not once per failed discovery call",
);
const calls = invocations();
assert.equal(calls.length, 1, "must short-circuit after the first discovery failure");
assert.match(calls[0], /^avatar list /);
});
});
test("download failure after a successful create returns null and logs a diagnostic", async () => {
const { server, url } = await listenFailingVideoServer();
try {
await withFakeHeygen({ response: JSON.stringify({ data: { video_url: url } }) }, async () => {
const heygenVideoGenerate = await freshGenerate();
const result = await heygenVideoGenerate("Download failure", {
avatarId: "avatar-override",
voiceId: "voice-override",
});
assert.equal(result, null);
});
} finally {
await closeServer(server);
}
});
test("uses private unique downloads even when time is fixed and the old name is planted", async (t) => {
const root = mkdtempSync(join(tmpdir(), "hf-video-temp-test-"));
const previousTmpdir = process.env.TMPDIR;
process.env.TMPDIR = root;
t.mock.method(Date, "now", () => 123456);
t.mock.method(globalThis, "fetch", async () => ({
ok: true,
headers: { get: () => "4" },
body: [Buffer.from("new!")],
}));
try {
const victim = join(root, "victim");
writeFileSync(victim, "unchanged");
symlinkSync(victim, join(root, `media-use-heygen-video-${process.pid}-123456.mp4`));
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: "https://example.invalid/video.mp4" } }) },
async () => {
const generate = await freshGenerate();
const first = await generate("First", { avatarId: "a", voiceId: "v" });
const second = await generate("Second", { avatarId: "a", voiceId: "v" });
assert.ok(first && second);
assert.notEqual(first.localPath, second.localPath);
assert.equal(readFileSync(first.localPath, "utf8"), "new!");
assert.equal(readFileSync(second.localPath, "utf8"), "new!");
assert.equal(readFileSync(victim, "utf8"), "unchanged");
},
);
} finally {
if (previousTmpdir === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = previousTmpdir;
rmSync(root, { recursive: true, force: true });
}
});
test("removes private download staging on failure while returning null", async (t) => {
const root = mkdtempSync(join(tmpdir(), "hf-video-temp-fail-"));
const previousTmpdir = process.env.TMPDIR;
process.env.TMPDIR = root;
t.mock.method(globalThis, "fetch", async () => {
throw new Error("download failed");
});
t.mock.method(console, "error", () => {});
try {
await withFakeHeygen(
{ response: JSON.stringify({ data: { video_url: "https://example.invalid/video.mp4" } }) },
async () => {
const generate = await freshGenerate();
assert.equal(await generate("Failure", { avatarId: "a", voiceId: "v" }), null);
},
);
assert.deepEqual(readdirSync(root), []);
} finally {
if (previousTmpdir === undefined) delete process.env.TMPDIR;
else process.env.TMPDIR = previousTmpdir;
rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,44 @@
import { heygenSearch } from "./heygen-search.mjs";
export const imageProvider = {
async search(intent) {
const results = heygenSearch("asset search", intent, { type: "image" });
if (!results) return null;
const best = results[0];
return {
url: best.url,
source: "search",
// ext derived from the asset URL by resolve.mjs (.jpg/.png/.webp)
metadata: {
description: intent,
width: best.width || null,
height: best.height || null,
transparent: best.is_transparent || false,
provider: "heygen.asset.search",
provenance: { asset_id: best.id, score: best.score },
},
};
},
};
export const iconProvider = {
async search(intent) {
// No minScore: the `asset search` backend rejects --min-score and returns no score field.
const results = heygenSearch("asset search", intent, { type: "icon" });
if (!results) return null;
const best = results[0];
return {
url: best.url,
source: "search",
// ext derived from the asset URL by resolve.mjs — catalog icons are .png, not .svg
metadata: {
description: intent,
width: best.width || null,
height: best.height || null,
transparent: best.is_transparent ?? true,
provider: "heygen.asset.search",
provenance: { asset_id: best.id, score: best.score, type: "icon" },
},
};
},
};
@@ -0,0 +1,63 @@
import { writeFileSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { readManifest, indexPath } from "./manifest.mjs";
function pad(str, len) {
return String(str ?? "").padEnd(len);
}
function formatDur(record) {
if (record.duration == null) return "—";
return `${record.duration}s`;
}
function formatDims(record) {
if (record.width && record.height) return `${record.width}×${record.height}`;
if (record.type === "icon" && record.transparent) return "svg";
return "—";
}
export function generateIndexContent(records) {
const count = records.length;
const header = `# .media · ${count} asset${count === 1 ? "" : "s"}\n`;
if (count === 0) return header;
const cols = { id: 4, type: 5, dur: 4, dims: 5, path: 5, desc: 11 };
for (const r of records) {
cols.id = Math.max(cols.id, (r.id ?? "").length);
cols.type = Math.max(cols.type, (r.type ?? "").length);
cols.dur = Math.max(cols.dur, formatDur(r).length);
cols.dims = Math.max(cols.dims, formatDims(r).length);
cols.path = Math.max(cols.path, (r.path ?? "").length);
}
const heading =
pad("id", cols.id + 2) +
pad("type", cols.type + 2) +
pad("dur", cols.dur + 2) +
pad("dims", cols.dims + 2) +
pad("path", cols.path + 2) +
"description";
const lines = [header, heading];
for (const r of records) {
lines.push(
pad(r.id, cols.id + 2) +
pad(r.type, cols.type + 2) +
pad(formatDur(r), cols.dur + 2) +
pad(formatDims(r), cols.dims + 2) +
pad(r.path, cols.path + 2) +
(r.description ?? ""),
);
}
return lines.join("\n") + "\n";
}
export function regenerateIndex(projectDir) {
const records = readManifest(projectDir);
const content = generateIndexContent(records);
const p = indexPath(projectDir);
mkdirSync(dirname(p), { recursive: true });
writeFileSync(p, content);
return content;
}
@@ -0,0 +1,318 @@
// Declarative table of USER-INSTALLED local models, for the spec-gated fallback.
//
// These models run on the user's own machine for their own use; media-use
// recommends, spec-checks, and assists install; it does not bundle, redistribute,
// or sell them. Because nothing is redistributed, selection is purely by
// quality / size / spec-fit / word-timestamp support (there is deliberately NO
// license field gating availability).
//
// Tiers (`small`|`medium`|`large`|`xlarge`) are human labels; `needs.ramMB` is
// what selection actually gates on. selectModel() returns the best model that
// fits the machine's AVAILABLE RAM, best-first: by explicit `rank` when set
// (quality that is NOT size, e.g. ASR), else by RAM footprint (the quality
// proxy for generation). No fit -> recommend the CLI/cloud path.
//
// selectModelLadder() returns EVERY fitting model in that same order. Callers
// that can retry walk it so ONE unusable entry (gated weights, a missing
// binary, an OOM) demotes to the next tier instead of killing the local path.
//
// Picks reflect the 2026 research pass, verified live where noted.
export const CAPABILITIES = ["tts", "asr", "upscale", "videogen", "imagegen"];
const MODELS = {
tts: [
{
id: "kokoro",
tier: "medium",
sizeMB: 330,
needs: { ramMB: 2048, gpu: false },
wordTimestamps: "native",
install: "pip install kokoro",
invoke: "python -m kokoro --text {text} --voice {voice} --out {out}",
notes: "CPU, faster-than-realtime, native per-word timestamps. Default floor.",
},
{
id: "fish-speech",
tier: "large",
sizeMB: 1100,
needs: { ramMB: 16000, gpu: true, vramMB: 12000 },
wordTimestamps: "whisperx", // needs forced alignment (run ASR over output)
install: "pip install fish-speech",
invoke: "fish-speech synth --text {text} --ref {ref} --out {out}",
notes: "Expressive zero-shot voice cloning; meeting pick. WhisperX for word timing.",
},
],
asr: [
// Parakeet is BETTER than Whisper yet SMALLER (0.6B vs 1.5B), so quality is
// not size here: `rank` pins it ahead of whisper regardless of footprint.
// Open ASR Leaderboard avg WER: Parakeet ~6.05% vs whisper-large-v3 7.44%
// (~19% better); on NOISY test-other 4.73% vs 5.96%, and whisper-v3
// hallucinated to 308% WER on meetings where Parakeet held. 5-10x faster.
//
// Cohere Transcribe 2B tops the leaderboard (5.42%) and is nominally the most
// accurate, but its mlx-audio community MLX quants (4bit AND 8bit, with and
// without --language en) produced multilingual token-soup garbage AND ran
// 40-70x slower than Parakeet on a 24GB Mac (live-tested 2026-07). Excluded
// until the mlx-audio Cohere decoder stabilizes; Parakeet is the default.
{
id: "parakeet-mlx",
tier: "small",
rank: 0,
sizeMB: 2400,
needs: { ramMB: 4000, gpu: true },
wordTimestamps: "tokens", // sub-word tokens; merged to words by parakeet-words.mjs
repo: "mlx-community/parakeet-tdt-0.6b-v3",
install:
"uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx",
invoke:
"parakeet-mlx {audio} --model mlx-community/parakeet-tdt-0.6b-v3 --output-format json --output-dir {outdir}",
notes:
"NVIDIA Parakeet-TDT 0.6B via parakeet-mlx. VERIFIED on 24GB: accurate transcript, ~3s (cached model) for 8s audio, word timestamps drive transcript-cut. English + 25 European languages. Beats whisper.cpp on accuracy (6.05% vs 7.44% WER) AND speed (5-10x).",
},
{
id: "whisperx",
tier: "medium",
rank: 1,
sizeMB: 1500,
needs: { ramMB: 4096, gpu: false },
wordTimestamps: "native", // faster-whisper + wav2vec2 forced alignment
install: "pip install whisperx",
invoke: "whisperx {audio} --output_format json --out {out}",
notes:
"CPU-only fallback (no GPU): faster-whisper + wav2vec2 forced alignment, native word timestamps. The packaged `hyperframes transcribe` (whisper.cpp) is the zero-setup baseline below this.",
},
],
upscale: [
{
id: "real-esrgan",
tier: "medium",
sizeMB: 70,
needs: { ramMB: 2048, gpu: false },
wordTimestamps: false,
install: "brew install real-esrgan-ncnn-vulkan # or download the ncnn binary",
invoke: "realesrgan-ncnn-vulkan -i {in} -o {out} -s 4",
notes: "ncnn-vulkan binary, CPU-capable. GFPGAN for faces.",
},
{
id: "seedvr2",
tier: "large",
sizeMB: 6000,
needs: { ramMB: 24000, gpu: true, vramMB: 16000 },
wordTimestamps: false,
install: "pip install seedvr2",
invoke: "seedvr2 upscale --in {in} --out {out}",
notes: "Diffusion upscaler, GPU-only. Video2X for video.",
},
],
videogen: [
// 2026-07 X research pass + live verification on a 24GB M-series Mac -
// which reaches the q4 tier only: a 24GB machine cannot select the 32GB
// entry below it, so that tier's claims stay unverified until someone
// runs it on a 32GB+ machine.
// The Mac-local video story is LTX 2.3 on MLX via dgrauet/ltx-2-mlx (the
// pipeline these weights were converted for; also powers Phosphene).
// Wan 2.x MLX exists only as A14B conversions (too large for consumer
// unified memory); revisit when a 5B Wan MLX conversion lands.
// IMPORTANT: sizeMB below is the FULL repo, because that is what a run
// actually downloads. Both invokes pass a repo id to `--model`, and
// upstream resolve_model_dir() (ltx_pipelines_mlx/utils/_orchestration.py)
// calls snapshot_download(repo) with no allow_patterns - so the whole repo
// lands regardless of what you pre-fetched. A targeted `hf download
// --include` subset used to be documented here; it was removed because it
// is both ineffective (the runner refetches the rest at generate time) and
// insufficient (--two-stage needs transformer-dev AND transformer-distilled
// AND the x2 spatial upscaler; --distilled needs an upscaler too). The q4
// tier verified below only worked BECAUSE the download is unfiltered.
{
id: "ltx-2.3-mlx-q4",
tier: "medium",
sizeMB: 59700, // full repo, measured 59.69GB; gemma-3-12b-4bit text encoder adds ~7GB
needs: { ramMB: 16384, gpu: true },
wordTimestamps: false,
install:
'git clone https://github.com/dgrauet/ltx-2-mlx && cd ltx-2-mlx && uv sync --all-extras && export PATH="$PWD/.venv/bin:$PATH"',
invoke:
"ltx-2-mlx generate --prompt {prompt} --distilled --low-ram --model dgrauet/ltx-2.3-mlx-q4 --width {w} --height {h} --frames {frames} --frame-rate 24 --output {out}",
notes:
"LTX 2.3 int4 on MLX. Verified on 24GB unified: 512x320 x 33 frames in ~19 min cold (incl. text-encoder download), t2v with audio. Dims must be multiples of 64. i2v, retake/extend, keyframe interpolation supported.",
},
{
id: "ltx-2.3-mlx-q8",
tier: "large",
sizeMB: 87500, // full repo, measured 87.51GB
needs: { ramMB: 32768, gpu: true },
wordTimestamps: false,
install:
'git clone https://github.com/dgrauet/ltx-2-mlx && cd ltx-2-mlx && uv sync --all-extras && export PATH="$PWD/.venv/bin:$PATH"',
invoke:
"ltx-2-mlx generate --prompt {prompt} --two-stage --low-ram --model dgrauet/ltx-2.3-mlx-q8 --width {w} --height {h} --frames {frames} --frame-rate 24 --output {out}",
notes:
"LTX 2.3 int8 on MLX, two-stage (upstream production default; higher quality than the q4 distilled tier). Replaced dgrauet/ltx-2.3-mlx-bf16, which is gated (HTTP 401) and cannot be downloaded at all. Costs an 87.5GB download against q4's 59.7GB - a real tradeoff, not a rounding difference. --two-stage is dev model + CFG at half-res, upscale, then distilled LoRA refine (upstream's own help text), so it needs transformer-dev + transformer-distilled + spatial_upscaler_x2; the full snapshot carries all three. --low-ram matches this tier's 32GB floor (block streaming); 64-128GB Macs for long/HD runs. NOT live-verified on a 32GB+ machine - the q4 tier below is the verified one.",
},
],
imagegen: [
// 2026-07 X research + live verification on a 24GB M-series Mac. mflux
// (FLUX-on-MLX) is the Mac-native runner; FLUX is the quality leader. Two
// hard-won findings baked into `needs.ramMB`:
// 1. The OFFICIAL FLUX repos are HF-gated (license wall). Point --path at a
// non-gated community 4-bit re-upload (self-contained, incl. VAE).
// 2. Without --low-ram, FLUX's T5-XXL text encoder + transformer blow past
// 24GB into swap: a 768x512 run took 90 MINUTES. With --low-ram (streams
// components from disk) the SAME machine did 512x512 in ~20s at 7.6GB
// free. So the medium tier's needs.ramMB is the streamed floor, not the
// resident footprint; the large tiers are the no-streaming thresholds.
// The runner resolves `repo` to a local snapshot (hf download) before --path;
// a bare repo id in --path breaks mlx unflatten.
{
id: "flux-schnell-mflux-q4",
tier: "medium",
sizeMB: 8700,
needs: { ramMB: 8000, gpu: true },
repo: "dhairyashil/FLUX.1-schnell-mflux-4bit",
wordTimestamps: false,
install: "uv venv ~/.venvs/mflux && VIRTUAL_ENV=~/.venvs/mflux uv pip install mflux==0.9.6",
invoke:
"mflux-generate --model schnell --path {model_path} --low-ram --steps 4 --prompt {prompt} --width {w} --height {h} --seed {seed} --output {out}",
notes:
"FLUX.1 schnell int4. VERIFIED on 24GB (7.6GB free): --low-ram 512x512 in ~20s, photoreal. --low-ram is MANDATORY at this tier (streams to avoid swap). Few-step, fast.",
},
{
id: "flux2-klein-mflux-q4",
tier: "large",
sizeMB: 12000,
needs: { ramMB: 32000, gpu: true },
repo: "Runpod/FLUX.2-klein-4B-mflux-4bit",
wordTimestamps: false,
install: "uv venv ~/.venvs/mflux && VIRTUAL_ENV=~/.venvs/mflux uv pip install mflux",
invoke:
"mflux-generate --base-model flux2-klein-4b --path {model_path} --steps 8 --prompt {prompt} --width {w} --height {h} --seed {seed} --output {out}",
notes:
"FLUX.2 Klein 4B int4 (most-downloaded mflux community repo). Newer, higher quality than schnell; full-resident (no streaming) so needs 32GB+ to stay fast. Needs mflux >= 0.18 for the flux2-klein base model.",
},
{
id: "qwen-image-mflux",
tier: "xlarge",
sizeMB: 40000,
needs: { ramMB: 64000, gpu: true },
repo: "Qwen/Qwen-Image",
wordTimestamps: false,
install: "uv venv ~/.venvs/mflux && VIRTUAL_ENV=~/.venvs/mflux uv pip install mflux",
invoke:
"mflux-generate --base-model qwen --steps 20 --prompt {prompt} --width {w} --height {h} --seed {seed} --output {out}",
notes:
"Qwen-Image, top-tier quality. Heavy: 'several minutes' even on 128GB M4 Max, 'almost fried' a 32GB M4 Pro. 64GB+ only. Below that, the cloud upsell (codex) is faster and better.",
},
],
};
function tableFor(capability) {
const t = MODELS[capability];
if (!t) throw new Error(`unknown local-model capability: ${capability}`);
return t;
}
/** All local models for a capability. */
export function listModels(capability) {
return tableFor(capability).slice();
}
// Tokenize an `invoke` template on whitespace first, then substitute each
// token, so a `{prompt}`/`{model_path}` value with spaces stays a single argv
// entry. Shared by every local-model provider (mflux, LTX) that builds argv
// from a MODELS[...].invoke template.
export function buildArgv(template, vars) {
return template
.trim()
.split(/\s+/)
.map((tok) => tok.replace(/\{(\w+)\}/g, (_, k) => (k in vars ? String(vars[k]) : `{${k}}`)));
}
/** Does this machine meet a model's needs? Apple Silicon unified memory counts as VRAM. */
export function meetsSpecs(model, specs) {
const n = model.needs || {};
// Gate on AVAILABLE RAM when the probe reported it (the real budget with the
// OS + open apps resident); fall back to total RAM otherwise. Older specs
// objects (and unit fixtures) that only set ramMB keep working unchanged.
const budget = specs.availableRamMB ?? specs.ramMB;
if (n.ramMB && budget < n.ramMB) return false;
if (n.gpu && !specs.gpu?.present) return false;
if (n.vramMB) {
const vram = specs.gpu?.vramMB ?? 0;
if (vram < n.vramMB) return false;
}
return true;
}
// "Best model the machine can run" == best-first among those that fit. Ordering:
// 1. explicit `rank` (lower = better) when a model declares it. Needed where
// quality is NOT size: Parakeet-0.6B beats Whisper-large-1.5B at ASR, so
// footprint would pick the wrong one.
// 2. otherwise RAM footprint descending, the quality proxy for generation
// (a 40GB image model out-renders a 12GB one).
function rankedByPreference(table) {
return [...table].sort((a, b) => {
const ra = a.rank ?? Infinity;
const rb = b.rank ?? Infinity;
if (ra !== rb) return ra - rb;
return (b.needs?.ramMB ?? 0) - (a.needs?.ramMB ?? 0);
});
}
/**
* Every local model for a capability this machine can actually run, best-first
* (same ordering as selectModel, whose pick is this list's head).
*
* Callers that can retry should walk the whole list: a table entry can be
* unusable for reasons no spec check can see - weights pulled or gated behind a
* login, the runner missing from PATH, an OOM at a tier that nominally fits. On
* a single-select call any one of those fails the entire local path, because the
* cascade cannot tell "this model is broken" from "nothing here fits you".
* Demoting to the next fitting tier is almost always what the user wanted.
*/
export function selectModelLadder(capability, specs, { preferTier } = {}) {
const table = tableFor(capability);
const pool = preferTier ? table.filter((m) => m.tier === preferTier) : table;
return rankedByPreference(pool).filter((model) => meetsSpecs(model, specs));
}
/**
* Pick the best local model the machine can run for a capability: the
* highest-footprint model that fits the available-RAM budget (and GPU/VRAM).
* `preferTier` pins the search to one tier (e.g. force a smaller/faster model).
* Returns `{ model, tier }`, or `{ recommend: "cli", reason }` when nothing fits.
*/
export function selectModel(capability, specs, { preferTier } = {}) {
const table = tableFor(capability);
const [model] = selectModelLadder(capability, specs, { preferTier });
if (model) return { model, tier: model.tier };
const smallest = table.reduce((a, b) => (a.sizeMB <= b.sizeMB ? a : b));
return {
recommend: "cli",
reason: `machine does not meet specs for any local ${capability} model (smallest needs ~${smallest.needs.ramMB}MB RAM${smallest.needs.gpu ? " + GPU" : ""}); use the CLI path instead`,
};
}
/**
* Agent-facing ladder: every model for a capability, best-first, each flagged
* with whether it fits this machine and why. Lets the agent see the RAM-graded
* options and choose (e.g. trade the auto-picked best for a smaller/faster one,
* or step up to a cloud upsell) rather than only getting one auto-selection.
*/
export function describeModelLadder(capability, specs) {
const budget = specs.availableRamMB ?? specs.ramMB;
return rankedByPreference(tableFor(capability)).map((model) => {
const fits = meetsSpecs(model, specs);
return {
id: model.id,
tier: model.tier,
needsRamMB: model.needs?.ramMB ?? 0,
sizeMB: model.sizeMB,
fits,
reason: fits
? `fits (needs ~${model.needs?.ramMB}MB, ${budget}MB available)`
: `too big (needs ~${model.needs?.ramMB}MB${model.needs?.gpu ? " + GPU" : ""}, ${budget}MB available)`,
notes: model.notes,
};
});
}
@@ -0,0 +1,220 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import {
listModels,
meetsSpecs,
selectModel,
selectModelLadder,
describeModelLadder,
CAPABILITIES,
} from "./local-models.mjs";
const TIERS = ["small", "medium", "large", "xlarge"];
const strongGpu = {
ramMB: 64000,
gpu: { present: true, kind: "nvidia", vramMB: 24000 },
appleSilicon: false,
};
const cpuOnly = { ramMB: 16000, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
const tiny = { ramMB: 1024, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
test("every capability table is non-empty and well-formed", () => {
for (const cap of CAPABILITIES) {
const models = listModels(cap);
assert.ok(models.length > 0, `no models for ${cap}`);
for (const m of models) {
assert.ok(m.id && m.tier && m.needs, `${cap}/${m.id} missing fields`);
assert.ok(TIERS.includes(m.tier), `${cap}/${m.id} bad tier: ${m.tier}`);
assert.equal(typeof m.install, "string", `${cap}/${m.id} needs an install command`);
assert.equal(typeof m.invoke, "string", `${cap}/${m.id} needs an invoke command`);
// user-installed, local-use-only: there is NO license gate on selection
assert.equal("license" in m, false, `${cap}/${m.id} must not carry a license gate`);
}
}
});
test("meetsSpecs enforces RAM, GPU presence, and VRAM", () => {
const gpuModel = { needs: { ramMB: 8000, gpu: true, vramMB: 12000 } };
assert.equal(meetsSpecs(gpuModel, strongGpu), true);
assert.equal(meetsSpecs(gpuModel, cpuOnly), false, "no GPU -> fails a GPU model");
const cpuModel = { needs: { ramMB: 2000, gpu: false } };
assert.equal(meetsSpecs(cpuModel, cpuOnly), true);
assert.equal(meetsSpecs(cpuModel, tiny), false, "too little RAM");
});
test("Apple Silicon unified memory counts as VRAM", () => {
const apple = {
ramMB: 24000,
appleSilicon: true,
gpu: { present: true, kind: "apple", vramMB: 24000 },
};
const gpuModel = { needs: { ramMB: 8000, gpu: true, vramMB: 16000 } };
assert.equal(meetsSpecs(gpuModel, apple), true);
});
test("selectModel picks the large tier on a strong machine", () => {
const r = selectModel("tts", strongGpu);
assert.equal(r.tier, "large");
assert.ok(r.model.id);
});
test("selectModel falls back to medium on a CPU-only machine", () => {
const r = selectModel("tts", cpuOnly);
assert.equal(r.tier, "medium");
assert.equal(r.model.id, "kokoro", "Kokoro is the CPU/medium default (native word timestamps)");
});
test("selectModel recommends the CLI path when no tier fits", () => {
const r = selectModel("tts", tiny);
assert.equal(r.recommend, "cli");
assert.ok(r.reason && /spec/i.test(r.reason));
assert.equal(r.model, undefined);
});
test("preferTier:'medium' avoids the large model even on a strong machine", () => {
const r = selectModel("tts", strongGpu, { preferTier: "medium" });
assert.equal(r.tier, "medium");
});
test("selectModel gates on AVAILABLE RAM, not total, when both are present", () => {
// 64GB total but only 6GB free right now -> the large tier must not be chosen.
const busy = {
ramMB: 64000,
availableRamMB: 6000,
appleSilicon: true,
gpu: { present: true, kind: "apple", vramMB: 64000 },
};
const r = selectModel("tts", busy);
assert.equal(r.tier, "medium", "available RAM (6GB) rules out the 16GB large tier");
});
test("imagegen is a RAM-graduated ladder; agent picks the best that fits", () => {
const ladder = describeModelLadder("imagegen", {
ramMB: 24000,
availableRamMB: 12000,
appleSilicon: true,
gpu: { present: true, kind: "apple", vramMB: 24000 },
});
// best-first order, each flagged with fit
assert.ok(ladder.length >= 3, "imagegen offers multiple RAM tiers");
assert.ok(
ladder[0].needsRamMB >= ladder[ladder.length - 1].needsRamMB,
"ladder is ordered best (biggest) first",
);
// on 24GB / 12GB-free the schnell --low-ram tier fits, the 32GB+ tiers do not
const fitting = ladder.filter((m) => m.fits);
assert.ok(fitting.length >= 1, "at least the low-ram tier fits a 24GB Mac");
assert.ok(
fitting.every((m) => m.needsRamMB <= 12000),
"only sub-budget models flagged as fitting",
);
const pick = selectModel("imagegen", {
ramMB: 24000,
availableRamMB: 12000,
gpu: { present: true, vramMB: 24000 },
});
assert.equal(
pick.model.id,
"flux-schnell-mflux-q4",
"best fit on 24GB is the low-ram schnell tier",
);
});
test("imagegen on a 64GB Mac steps up to the higher-quality tier", () => {
const pick = selectModel("imagegen", {
ramMB: 96000,
availableRamMB: 80000,
gpu: { present: true, vramMB: 96000 },
});
assert.equal(pick.tier, "xlarge", "80GB free unlocks the top-quality model");
});
test("ASR prefers Parakeet by rank even though it is smaller than whisper", () => {
// quality != size for ASR: Parakeet 0.6B beats whisper-1.5B, so `rank` wins
// over footprint. On a capable machine both fit; Parakeet must be chosen.
const capable = {
ramMB: 24000,
availableRamMB: 12000,
appleSilicon: true,
gpu: { present: true, kind: "apple", vramMB: 24000 },
};
const pick = selectModel("asr", capable);
assert.equal(pick.model.id, "parakeet-mlx", "Parakeet is the rank-0 preferred ASR");
// whisperx (rank 1, CPU-only) is the fallback when no GPU
const cpu = { ramMB: 16000, availableRamMB: 12000, gpu: { present: false, vramMB: 0 } };
assert.equal(selectModel("asr", cpu).model.id, "whisperx", "CPU-only falls back to whisperx");
});
test("ASR offers word-timestamp-capable models (better than plain whisper)", () => {
const asr = listModels("asr");
assert.ok(
asr.every((m) => m.wordTimestamps),
"every ASR model must support word timestamps",
);
});
// A machine that clears BOTH videogen tiers (the 32GB entry and the 16GB one).
// The existing fixtures deliberately sit under the large tier's floor, which is
// exactly how a dead 32GB entry stayed invisible: nothing could select it.
const bothVideogenTiers = { availableRamMB: 40000, gpu: { present: true } };
test("selectModelLadder returns every fitting model, best-first", () => {
const ladder = selectModelLadder("videogen", bothVideogenTiers);
assert.deepEqual(
ladder.map((m) => m.tier),
["large", "medium"],
"both tiers fit 40GB, biggest first",
);
assert.equal(
selectModel("videogen", bothVideogenTiers).model.id,
ladder[0].id,
"selectModel's pick is the ladder's head",
);
});
test("selectModelLadder drops what the machine cannot run", () => {
const oneTier = selectModelLadder("videogen", { availableRamMB: 20000, gpu: { present: true } });
assert.deepEqual(
oneTier.map((m) => m.tier),
["medium"],
"20GB cannot reach the 32GB tier",
);
assert.deepEqual(
selectModelLadder("videogen", { availableRamMB: 100, gpu: { present: true } }),
[],
"nothing fits -> empty ladder, and selectModel recommends the CLI",
);
assert.equal(
selectModel("videogen", { availableRamMB: 100, gpu: { present: true } }).recommend,
"cli",
);
});
test("selectModelLadder honours preferTier", () => {
const pinned = selectModelLadder("videogen", bothVideogenTiers, { preferTier: "medium" });
assert.deepEqual(
pinned.map((m) => m.tier),
["medium"],
"preferTier pins the ladder to one tier",
);
});
test("an invoke that names an owner/repo model agrees with the entry id", () => {
// Guards a half-done repoint: moving an entry to different weights means
// changing BOTH the id and the --model argument. Change one and the table
// selects one model while the runner downloads another.
let checked = 0;
for (const cap of CAPABILITIES) {
for (const m of listModels(cap)) {
if (m.repo) continue; // entries with an explicit repo resolve through it
const named = /--model\s+(\S+)/.exec(m.invoke);
if (!named) continue;
const [, name] = named[1].split("/");
if (!name) continue; // a bare model name, not an owner/repo id
assert.equal(name, m.id, `${cap}/${m.id}: invoke runs ${named[1]}`);
checked += 1;
}
}
assert.ok(checked > 0, "no entry pins an owner/repo model - guard would be vacuous");
});
@@ -0,0 +1,78 @@
import { execFileSync } from "node:child_process";
import { selectModel, selectModelLadder } from "./local-models.mjs";
import { probeSpecs } from "./specs.mjs";
// Run a USER-INSTALLED local model for a capability (tts/asr/upscale).
// Walks the tiers the machine supports best-first (selectModelLadder), checking
// the tool is on PATH, filling the model's invoke template, and running it. A
// tier whose tool is missing or whose run fails demotes to the next fitting
// tier, so one unusable entry does not fail the capability. Returns:
// { model, tier, out } on success
// { recommend:"install", model, sizeMB, command, reason } tool isn't installed
// { recommend:"cli", reason } when no tier fits the machine
// `exec` / `which` are injectable for tests.
//
// ponytail: "installed" = the invoke's first token is on PATH (e.g. `whisperx`,
// `realesrgan-ncnn-vulkan`). For `python -m kokoro` this only proves python
// exists; good enough to gate — the recommend.command names the real package.
// Upgrade to a per-tool probe if a "python present but package missing" run ever
// produces a confusing error instead of a clean recommend.
function defaultWhich(bin) {
execFileSync("command", ["-v", bin], { stdio: "ignore", shell: true });
}
function defaultExec(cmd) {
execFileSync(cmd, { stdio: ["ignore", "pipe", "pipe"], shell: true, timeout: 600000 });
}
const fill = (tpl, vars) =>
tpl.replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? String(vars[k]) : ""));
export function runLocalModel(capability, opts = {}) {
const {
specs = probeSpecs(),
exec = defaultExec,
which = defaultWhich,
vars = {},
preferTier,
} = opts;
const ladder = selectModelLadder(capability, specs, { preferTier });
// no tier fits at all -> recommend the CLI path (selectModel words the reason)
if (!ladder.length) return selectModel(capability, specs, { preferTier });
// Best tier first, demoting past any tier that cannot run here: a missing
// tool or a failed run at the top tier must not hide a lower tier that works
// (fish-speech absent should still get you Kokoro). The last tier's failure is
// what gets reported, since by then nothing local ran.
let lastFailure = null;
for (const model of ladder) {
const bin = model.invoke.split(/\s+/)[0];
try {
which(bin);
} catch {
lastFailure = {
recommend: "install",
model: model.id,
sizeMB: model.sizeMB,
command: model.install,
reason: `${model.id} not installed (~${(model.sizeMB / 1000).toFixed(1)}GB to download once it is)`,
};
continue;
}
try {
exec(fill(model.invoke, vars));
} catch (e) {
lastFailure = {
recommend: "install",
model: model.id,
sizeMB: model.sizeMB,
command: model.install,
reason: e.message || String(e),
};
continue;
}
return { model: model.id, tier: model.tier, out: vars.out };
}
return lastFailure;
}
@@ -0,0 +1,121 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { runLocalModel } from "./local-run.mjs";
const strongCpu = { ramMB: 16000, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
const tiny = { ramMB: 512, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
const ok = () => {}; // which/exec that succeed
test("recommends the CLI path when no local tier fits the machine", () => {
const r = runLocalModel("tts", { specs: tiny, which: ok, exec: ok });
assert.equal(r.recommend, "cli");
});
test("recommends install when the tool is not on PATH", () => {
const r = runLocalModel("tts", {
specs: strongCpu,
which: () => {
throw new Error("not found");
},
exec: ok,
vars: { text: "hi", out: "/tmp/v.wav" },
});
assert.equal(r.recommend, "install");
assert.equal(r.model, "kokoro");
assert.match(r.command, /pip install kokoro/);
});
test("runs the model and returns the output path when installed", () => {
let ran = "";
const r = runLocalModel("tts", {
specs: strongCpu,
which: ok,
exec: (cmd) => {
ran = cmd;
},
vars: { text: "hello world", voice: "af_heart", out: "/tmp/v.wav" },
});
assert.equal(r.model, "kokoro");
assert.equal(r.out, "/tmp/v.wav");
assert.match(ran, /hello world/, "invoke template filled with vars");
assert.match(ran, /\/tmp\/v\.wav/);
});
test("a failing run degrades to an install recommendation, never throws", () => {
const r = runLocalModel("upscale", {
specs: strongCpu,
which: ok,
exec: () => {
throw new Error("boom");
},
vars: { in: "a.png", out: "b.png" },
});
assert.equal(r.recommend, "install");
});
test("a tier whose tool is missing demotes to the next tier that fits", () => {
// 64GB + GPU fits BOTH tts tiers, so the ladder has two rungs: fish-speech
// (its own binary) above Kokoro (`python -m kokoro`). fish-speech absent must
// not cost the user Kokoro.
const strongGpu = { ramMB: 64000, gpu: { present: true, vramMB: 24000 } };
let ran = "";
const r = runLocalModel("tts", {
specs: strongGpu,
which: (bin) => {
if (bin === "fish-speech") throw new Error("not found");
},
exec: (cmd) => {
ran = cmd;
},
vars: { text: "hello", voice: "af_heart", out: "/tmp/v.wav" },
});
assert.equal(r.model, "kokoro", "demoted past the missing fish-speech binary");
assert.equal(r.tier, "medium");
assert.match(ran, /kokoro/);
});
test("every fitting tier failing reports the last tier's install command", () => {
const strongGpu = { ramMB: 64000, gpu: { present: true, vramMB: 24000 } };
const r = runLocalModel("tts", {
specs: strongGpu,
which: ok,
exec: () => {
throw new Error("boom");
},
vars: { text: "hi", out: "/tmp/v.wav" },
});
assert.equal(r.recommend, "install");
assert.equal(r.model, "kokoro", "the smallest fitting tier is the actionable one");
});
test("the install recommendation states the download size", () => {
// nothing today tells the user what they are agreeing to before a tool
// starts pulling weights, so the size travels in the payload AND in the
// text a caller shows them
const r = runLocalModel("tts", {
specs: strongCpu,
which: () => {
throw new Error("not found");
},
});
assert.equal(r.recommend, "install");
assert.equal(typeof r.sizeMB, "number");
assert.ok(r.sizeMB > 0);
assert.match(r.reason, /GB to download/);
});
test("a failed run still reports the tier's size", () => {
const r = runLocalModel("tts", {
specs: strongCpu,
which: ok,
exec: () => {
throw new Error("boom");
},
});
assert.equal(r.recommend, "install");
assert.equal(typeof r.sizeMB, "number");
});
@@ -0,0 +1,222 @@
// Official brand marks — the `logo` type's provider tiers, tried in registry
// order. Every tier was verified against a 54-brand stress test (2026-07,
// 100% cascade hit). Hit counts below are a snapshot of that run — they
// drift as the alias/org maps grow; re-run the stress test to refresh them.
//
// 1. svgl — official full-color vector SVGs (+ wordmark variants);
// 40/54 first-hits. Search is substring-based, so
// entities go through alias normalization first
// ("nextjs" never matches "Next.js" raw).
// 2. simple-icons — monochrome official glyphs; caught the long tail the
// others miss (nike, visa, toyota, wechat, bytedance).
// Pinned CDN build for determinism.
// 3. github avatar — the org's official logo for brands with a GitHub
// presence. Known orgs only: guessing a login risks a
// same-named personal account.
// 4. domain favicon — small-raster last resort (DuckDuckGo ip3). Responses
// under ~500B are DDG's globe placeholder, not a hit.
//
// HeyGen asset search is deliberately absent: for brand queries it returns
// generic look-alike icons (0/3 in testing) — worse than a miss. A total miss
// falls through to resolve's normal failure path (`no provider could resolve
// logo`, exit 1).
import { mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const SVGL_API = "https://api.svgl.app";
const SIMPLE_ICONS_CDN = "https://cdn.jsdelivr.net/npm/simple-icons@16.25.0/icons";
const FAVICON_MIN_BYTES = 500;
// svgl search queries per entity, tried in order after the raw entity.
const SVGL_ALIASES = {
nextjs: ["next.js", "next"],
aws: ["amazon web services"],
huggingface: ["hugging face"],
cocacola: ["coca-cola"],
mcdonalds: ["mcdonald's"],
};
// simple-icons slugs that differ from the normalized entity.
const SIMPLE_ICON_SLUGS = {
nextjs: "nextdotjs",
aws: "amazonwebservices",
};
// Known GitHub orgs. Only mapped entities resolve at this tier — a brand name
// is NOT a GitHub login, and guessing hits same-named personal accounts.
const GITHUB_ORGS = {
slack: "slackhq",
meta: "facebook",
google: "google",
microsoft: "microsoft",
aws: "aws",
vercel: "vercel",
nextjs: "vercel",
alibaba: "alibaba",
heygen: "heygen-com",
};
// Favicon domains that aren't `<entity>.com`.
const FAVICON_DOMAINS = {
cocacola: "coca-cola.com",
aws: "aws.amazon.com",
nextjs: "nextjs.org",
};
const norm = (s) =>
String(s)
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
/** The brand entity for a query: --entity wins; else the intent minus filler. */
export function entityFrom(intent, entity) {
if (entity) return entity.toLowerCase().trim();
return String(intent)
.toLowerCase()
.replace(/\b(logo|logos|icon|brand|official|mark)\b/g, "")
.trim()
.replace(/\s+/g, " ");
}
/** Exact match after stripping case/spacing/punctuation — "Next.js" ≡ "nextjs". */
export function titleMatches(title, entity) {
return norm(title) === norm(entity);
}
export function svglQueriesFor(entity) {
return [entity, ...(SVGL_ALIASES[norm(entity)] || [])];
}
export function simpleIconSlugsFor(entity) {
const slugs = [norm(entity)];
const alias = SIMPLE_ICON_SLUGS[norm(entity)];
if (alias) slugs.push(alias);
return slugs;
}
export function githubOrgFor(entity) {
return GITHUB_ORGS[norm(entity)] || null;
}
export function faviconDomainFor(entity) {
return FAVICON_DOMAINS[norm(entity)] || `${norm(entity)}.com`;
}
async function fetchJson(url) {
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
if (!res.ok) return null;
return res.json();
}
async function urlExists(url) {
const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(10_000) });
return res.ok;
}
export async function svglSearch(intent, ctx = {}) {
const entity = entityFrom(intent, ctx.entity);
for (const q of svglQueriesFor(entity)) {
let items;
try {
items = await fetchJson(`${SVGL_API}?search=${encodeURIComponent(q)}`);
} catch {
return null; // network down — let the next tier try its own host
}
if (!Array.isArray(items)) continue;
const hit = items.find((it) => titleMatches(it.title, q) || titleMatches(it.title, entity));
if (!hit) continue;
const route = typeof hit.route === "string" ? hit.route : hit.route?.light;
if (!route) continue;
return {
url: route,
ext: ".svg",
source: "search",
metadata: {
description: `${hit.title} logo (official mark)`,
provider: "svgl",
provenance: { entity, query: q, route, wordmark: Boolean(hit.wordmark) },
},
};
}
return null;
}
export async function simpleIconsSearch(intent, ctx = {}) {
const entity = entityFrom(intent, ctx.entity);
for (const slug of simpleIconSlugsFor(entity)) {
const url = `${SIMPLE_ICONS_CDN}/${slug}.svg`;
let ok;
try {
ok = await urlExists(url);
} catch {
return null;
}
if (!ok) continue;
return {
url,
ext: ".svg",
source: "search",
metadata: {
description: `${entity} logo (official monochrome glyph)`,
provider: "simple-icons",
provenance: { entity, slug, pinned: "simple-icons@16.25.0" },
},
};
}
return null;
}
export async function githubAvatarSearch(intent, ctx = {}) {
const entity = entityFrom(intent, ctx.entity);
const org = githubOrgFor(entity);
if (!org) return null;
const url = `https://github.com/${org}.png?size=460`;
try {
if (!(await urlExists(url))) return null;
} catch {
return null;
}
return {
url,
ext: ".png",
source: "search",
metadata: {
description: `${entity} logo (GitHub org avatar)`,
provider: "github.avatar",
provenance: { entity, org },
},
};
}
export async function faviconSearch(intent, ctx = {}) {
const entity = entityFrom(intent, ctx.entity);
const domain = faviconDomainFor(entity);
const url = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
let body;
try {
const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
if (!res.ok) return null;
body = Buffer.from(await res.arrayBuffer());
} catch {
return null;
}
if (body.byteLength < FAVICON_MIN_BYTES) return null; // DDG placeholder, not a logo
// Hand the verified bytes over as a local file: the freeze step copies it
// instead of re-downloading, so the size check is authoritative over what
// gets frozen and the favicon tier costs one network round-trip, not two.
const bytes = body.byteLength;
const tmp = join(mkdtempSync(join(tmpdir(), "media-use-logo-")), `${domain}.ico`);
writeFileSync(tmp, body);
return {
localPath: tmp,
ext: ".ico",
source: "search",
metadata: {
description: `${entity} favicon (small raster — chip-size use only)`,
provider: "favicon.ddg",
provenance: { entity, domain, bytes, low_res: true },
},
};
}
@@ -0,0 +1,137 @@
import test from "node:test";
import assert from "node:assert";
import { readFileSync } from "node:fs";
import {
entityFrom,
titleMatches,
svglQueriesFor,
simpleIconSlugsFor,
githubOrgFor,
faviconDomainFor,
svglSearch,
simpleIconsSearch,
githubAvatarSearch,
faviconSearch,
} from "./logo-provider.mjs";
import { getProviders, runProviders } from "./registry.mjs";
test("entityFrom strips filler words from the intent; --entity wins", () => {
assert.equal(entityFrom("LinkedIn logo"), "linkedin");
assert.equal(entityFrom("official Slack brand mark"), "slack");
assert.equal(entityFrom("anything", "Notion"), "notion");
});
test("titleMatches ignores case, spacing, punctuation — and rejects lookalikes", () => {
assert.ok(titleMatches("Next.js", "nextjs"));
assert.ok(titleMatches("Coca-Cola", "coca cola"));
assert.ok(!titleMatches("Slackware", "slack"));
});
test("svgl queries include the alias forms the raw entity can't match", () => {
assert.ok(svglQueriesFor("nextjs").includes("next.js"));
assert.ok(svglQueriesFor("aws").includes("amazon web services"));
assert.deepEqual(svglQueriesFor("figma"), ["figma"]);
});
test("simple-icons slugs cover the renamed entries", () => {
assert.ok(simpleIconSlugsFor("nextjs").includes("nextdotjs"));
assert.ok(simpleIconSlugsFor("aws").includes("amazonwebservices"));
assert.deepEqual(simpleIconSlugsFor("nike"), ["nike"]);
});
test("github avatar tier never guesses an org", () => {
assert.equal(githubOrgFor("slack"), "slackhq");
assert.equal(githubOrgFor("heygen"), "heygen-com");
assert.equal(githubOrgFor("some-random-startup"), null);
});
test("favicon domain defaults to <entity>.com with explicit overrides", () => {
assert.equal(faviconDomainFor("cocacola"), "coca-cola.com");
assert.equal(faviconDomainFor("stripe"), "stripe.com");
});
// --- async tiers, network mocked -------------------------------------------
// The 54-brand stress test is a manual snapshot; these pin the same behavior
// as CI gates: descriptor shape, alias retry, error→null fallthrough, the
// placeholder filter, and the real cascade order under a mocked network.
const json = (data) => new Response(JSON.stringify(data), { status: 200 });
const status = (code) => new Response(null, { status: code });
const bin = (n) => new Response(new Uint8Array(n), { status: 200 });
test("svglSearch returns the descriptor shape on an exact title hit", async (t) => {
t.mock.method(globalThis, "fetch", async () =>
json([{ title: "Figma", route: "https://svgl.app/library/figma.svg" }]),
);
const res = await svglSearch("Figma logo", {});
assert.equal(res.url, "https://svgl.app/library/figma.svg");
assert.equal(res.ext, ".svg");
assert.equal(res.metadata.provider, "svgl");
});
test("svglSearch skips a non-array payload and retries with the alias query", async (t) => {
const seen = [];
t.mock.method(globalThis, "fetch", async (url) => {
seen.push(decodeURIComponent(String(url)));
return seen.length === 1
? json({ error: "unexpected shape" })
: json([{ title: "Next.js", route: "https://svgl.app/library/nextjs.svg" }]);
});
const res = await svglSearch("nextjs logo", {});
assert.equal(res.metadata.provenance.query, "next.js", "hit came from the alias query");
assert.ok(seen.length >= 2, "raw query then alias");
});
test("svglSearch returns null when the network is down — the cascade falls through", async (t) => {
t.mock.method(globalThis, "fetch", async () => {
throw new Error("network down");
});
assert.equal(await svglSearch("figma logo", {}), null);
});
test("simpleIconsSearch falls to the next slug on a 404", async (t) => {
const seen = [];
t.mock.method(globalThis, "fetch", async (url) => {
seen.push(String(url));
return String(url).includes("amazonwebservices") ? status(200) : status(404);
});
const res = await simpleIconsSearch("aws logo", {});
assert.ok(res.url.endsWith("amazonwebservices.svg"));
assert.equal(seen.length, 2, "plain slug 404s first, alias slug hits");
});
test("faviconSearch rejects DDG's sub-500B placeholder with null", async (t) => {
t.mock.method(globalThis, "fetch", async () => bin(120));
assert.equal(await faviconSearch("someco logo", {}), null);
});
test("faviconSearch hands verified bytes over as a local file — one fetch, no re-download", async (t) => {
const fetchMock = t.mock.method(globalThis, "fetch", async () => bin(600));
const res = await faviconSearch("someco logo", {});
assert.ok(res.localPath, "returns a localPath, not a url");
assert.equal(readFileSync(res.localPath).byteLength, 600, "frozen bytes are the verified bytes");
assert.equal(fetchMock.mock.callCount(), 1, "single network round-trip");
assert.equal(res.metadata.provenance.low_res, true);
});
test("githubAvatarSearch never touches the network for an unmapped entity", async (t) => {
const fetchMock = t.mock.method(globalThis, "fetch", async () => status(200));
assert.equal(await githubAvatarSearch("some-random-startup logo", {}), null);
assert.equal(fetchMock.mock.callCount(), 0);
});
test("the real logo cascade falls through tier by tier to the first hit", async (t) => {
t.mock.method(globalThis, "fetch", async (url) => {
const u = String(url);
if (u.includes("api.svgl.app")) return json([]); // tier 1: no hit
if (u.includes("jsdelivr")) return status(404); // tier 2: no such slug
// tier 3 (github) is never called: entity is unmapped
if (u.includes("duckduckgo")) return bin(600); // tier 4: real favicon
throw new Error(`unexpected fetch: ${u}`);
});
const res = await runProviders(getProviders("logo"), "search", "zzzbrand logo", {
entity: "zzzbrand",
});
assert.ok(res, "cascade must land on the favicon tier");
assert.equal(res.metadata.provider, "favicon.ddg");
});
@@ -0,0 +1,94 @@
import { execFileSync } from "node:child_process";
import { existsSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describeDownload, probeSpecs } from "./specs.mjs";
import { buildArgv, selectModel, selectModelLadder } from "./local-models.mjs";
export async function ltxVideoGenerate(
intent,
ctx,
execFn = execFileSync,
pathExists = existsSync,
unlinkFn = unlinkSync,
) {
const specs = ctx?.specs || probeSpecs();
const ladder = selectModelLadder("videogen", specs, { preferTier: ctx?.preferTier });
if (!ladder.length) {
const { reason } = selectModel("videogen", specs, { preferTier: ctx?.preferTier });
console.error(
`media-use: local video gen not enabled (${reason}). Enable a fitting free on-device LTX model to use this provider.`,
);
return null;
}
// Each attempt mints its own timestamped output path, so a partial artifact
// from a failed tier is orphaned rather than overwritten - and a lower tier
// then succeeding hides it. Discard it before demoting. Best-effort: a
// partial we cannot remove must never mask the real failure.
const discardPartial = (path) => {
try {
if (pathExists(path)) unlinkFn(path);
} catch {
// nothing actionable: the generate failure below is the real story
}
};
// Walk the whole ladder, best tier first. A tier that cannot run on this
// machine for a reason no spec check sees (runner off PATH, gated weights, an
// OOM) demotes to the next fitting tier instead of failing local video gen
// outright. Every demotion is reported: a silent drop to a smaller model
// leaves the caller wondering why the output looks the way it does.
for (const model of ladder) {
const bin = model.invoke.trim().split(/\s+/)[0];
try {
execFn("which", [bin], { stdio: ["ignore", "ignore", "ignore"] });
} catch {
console.error(
`media-use: local video gen not enabled (\`${bin}\` not on PATH). Install for free on-device LTX: ${model.install}. Heads up: ${model.id} ${describeDownload(model.sizeMB)}.`,
);
continue;
}
const outPath = join(tmpdir(), `media-use-ltx-${process.pid}-${Date.now()}.mp4`);
const argv = buildArgv(model.invoke, {
prompt: intent,
w: ctx?.width || 512,
h: ctx?.height || 320,
frames: ctx?.frames || 33,
out: outPath,
});
argv.shift();
try {
execFn(bin, argv, {
encoding: "utf8",
timeout: 1_800_000,
stdio: ["ignore", "pipe", "pipe"],
});
} catch (err) {
discardPartial(outPath);
console.error(
`media-use: local video gen (${model.id}) failed: ${err.stderr?.toString().trim().slice(-200) || err.message}`,
);
continue;
}
if (!pathExists(outPath)) {
console.error(
`media-use: local video gen (${model.id}) exited cleanly but wrote no output file`,
);
continue;
}
return {
localPath: outPath,
ext: ".mp4",
source: "generated",
metadata: {
description: intent,
provider: "ltx.local",
provenance: { prompt: intent },
},
};
}
return null;
}
@@ -0,0 +1,316 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { dirname } from "node:path";
import { tmpdir } from "node:os";
import { ltxVideoGenerate } from "./ltx-video-provider.mjs";
const fittingSpecs = { availableRamMB: 20000, gpu: { present: true } };
test("no fitting local model: falls through without checking for a binary", async (t) => {
t.mock.method(console, "error", () => {});
const calls = [];
const result = await ltxVideoGenerate(
"a calm ocean wave at sunset",
{ specs: { availableRamMB: 100, gpu: { present: true } } },
(...call) => calls.push(call),
() => true,
);
assert.equal(result, null);
assert.deepEqual(calls, []);
});
test("binary missing from PATH: prints the model install hint and falls through", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const calls = [];
const fakeExec = (...call) => {
calls.push(call);
throw new Error("not found");
};
const result = await ltxVideoGenerate(
"a calm ocean wave at sunset",
{ specs: fittingSpecs },
fakeExec,
);
assert.equal(result, null);
assert.equal(calls.length, 1);
assert.deepEqual(calls[0].slice(0, 2), ["which", ["ltx-2-mlx"]]);
assert.equal(errors.length, 1);
assert.match(errors[0], /git clone https:\/\/github\.com\/dgrauet\/ltx-2-mlx/);
// the install hint is the accept moment: say what the pull costs
assert.match(errors[0], /GB of weights to/);
});
test("generate argv substitutes a spaced prompt after tokenizing and uses verified defaults", async () => {
const calls = [];
const checkedPaths = [];
const fakeExec = (...call) => calls.push(call);
const pathExists = (path) => {
checkedPaths.push(path);
return false;
};
const intent = "a calm ocean wave at sunset";
const result = await ltxVideoGenerate(intent, { specs: fittingSpecs }, fakeExec, pathExists);
assert.equal(result, null);
assert.equal(calls.length, 2);
const [bin, argv, opts] = calls[1];
assert.equal(bin, "ltx-2-mlx");
assert.equal(opts.timeout, 1_800_000);
const expectedPairs = [
["--prompt", intent],
["--width", "512"],
["--height", "320"],
["--frames", "33"],
["--output", checkedPaths[0]],
];
let previousIndex = -1;
for (const [flag, value] of expectedPairs) {
const index = argv.indexOf(flag);
assert.ok(index > previousIndex, `${flag} should follow the previous required option`);
assert.equal(argv[index + 1], value);
previousIndex = index;
}
assert.equal(argv.filter((arg) => arg === intent).length, 1);
});
test("successful generation returns the generated MP4 result", async () => {
const calls = [];
const fakeExec = (...call) => calls.push(call);
const intent = "a calm ocean wave at sunset";
const result = await ltxVideoGenerate(intent, { specs: fittingSpecs }, fakeExec, () => true);
assert.ok(result);
assert.equal(calls.length, 2);
assert.equal(dirname(result.localPath), tmpdir());
assert.match(result.localPath, /media-use-ltx-\d+-\d+\.mp4$/);
assert.deepEqual(result, {
localPath: result.localPath,
ext: ".mp4",
source: "generated",
metadata: {
description: intent,
provider: "ltx.local",
provenance: { prompt: intent },
},
});
});
test("generate failure returns null instead of throwing", async (t) => {
t.mock.method(console, "error", () => {});
let calls = 0;
const fakeExec = () => {
calls += 1;
if (calls === 2) {
const error = new Error("generation failed");
error.stderr = "LTX failed";
throw error;
}
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: fittingSpecs },
fakeExec,
() => true,
);
assert.equal(result, null);
assert.equal(calls, 2);
});
test("missing generated output returns null and says so", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: fittingSpecs },
() => {},
() => false,
);
assert.equal(result, null);
assert.equal(errors.length, 1);
assert.match(errors[0], /wrote no output file/);
});
// 40GB clears BOTH videogen tiers, so the ladder has two rungs. `fittingSpecs`
// above sits under the large tier's floor on purpose: every other test in this
// file exercises the medium tier alone, which is precisely why a broken large
// tier could sit in the table unnoticed.
const bothTiersSpecs = { availableRamMB: 40000, gpu: { present: true } };
const isGenerate = (call) => call[0] !== "which";
test("a top tier that cannot run demotes to the next fitting tier", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const calls = [];
// The runner is installed, but the large tier's weights are gated: the
// download 401s and `generate` exits non-zero. The medium tier then works.
const fakeExec = (...call) => {
calls.push(call);
if (isGenerate(call) && call[1].includes("dgrauet/ltx-2.3-mlx-q8")) {
const err = new Error("exit 1");
err.stderr = "401 Client Error: Unauthorized for url: .../ltx-2.3-mlx-q8";
throw err;
}
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
fakeExec,
() => true,
);
assert.ok(result, "the medium tier still produced a video");
const generated = calls.filter(isGenerate).map((call) => call[1].join(" "));
assert.equal(generated.length, 2, "large attempted first, then medium");
assert.match(generated[0], /dgrauet\/ltx-2\.3-mlx-q8/);
assert.match(generated[1], /dgrauet\/ltx-2\.3-mlx-q4/);
// the demotion is reported, never silent: a smaller model changes the output
assert.equal(errors.length, 1);
assert.match(errors[0], /ltx-2\.3-mlx-q8\) failed/);
assert.match(errors[0], /401/);
});
test("every fitting tier failing returns null, one reason per tier", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const fakeExec = (...call) => {
if (isGenerate(call)) throw new Error("mlx out of memory");
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
fakeExec,
() => true,
);
assert.equal(result, null);
assert.equal(errors.length, 2, "both tiers tried, both reported");
assert.match(errors[0], /ltx-2\.3-mlx-q8/);
assert.match(errors[1], /ltx-2\.3-mlx-q4/);
});
test("preferTier pins the attempt to one tier instead of demoting", async (t) => {
t.mock.method(console, "error", () => {});
const calls = [];
const fakeExec = (...call) => {
calls.push(call);
if (isGenerate(call)) throw new Error("boom");
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs, preferTier: "large" },
fakeExec,
() => true,
);
assert.equal(result, null);
const generated = calls.filter(isGenerate).map((call) => call[1].join(" "));
assert.equal(generated.length, 1, "pinned to large: no demotion to medium");
assert.match(generated[0], /dgrauet\/ltx-2\.3-mlx-q8/);
});
// A failed attempt's temp path is minted per attempt (it carries a timestamp),
// so without cleanup a partial mp4 from a failed tier is orphaned rather than
// overwritten - and a lower tier then succeeding hides it. Partial video files
// are the expensive case, which is why this is pinned.
const outputOf = (argv) => argv[argv.indexOf("--output") + 1];
test("a failed attempt's partial output is discarded before demoting", async (t) => {
t.mock.method(console, "error", () => {});
const unlinked = [];
const attempted = [];
const fakeExec = (...call) => {
if (!isGenerate(call)) return;
attempted.push(outputOf(call[1]));
if (call[1].includes("dgrauet/ltx-2.3-mlx-q8")) {
// OOM mid-write is one of the advertised demotion cases
const err = new Error("exit 1");
err.stderr = "mlx.core.metal: out of memory";
throw err;
}
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
fakeExec,
() => true,
(path) => unlinked.push(path),
);
assert.ok(result, "the medium tier still produced a video");
assert.deepEqual(unlinked, [attempted[0]], "the failed large-tier partial is removed");
});
test("every tier failing discards every partial, one per attempt", async (t) => {
t.mock.method(console, "error", () => {});
const unlinked = [];
const attempted = [];
const fakeExec = (...call) => {
if (!isGenerate(call)) return;
attempted.push(outputOf(call[1]));
throw new Error("mlx out of memory");
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
fakeExec,
() => true,
(path) => unlinked.push(path),
);
assert.equal(result, null);
assert.equal(attempted.length, 2, "both tiers attempted");
assert.deepEqual(unlinked, attempted, "nothing is left behind on the all-fail path");
});
test("a successful generation is never discarded", async () => {
const unlinked = [];
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
() => {},
() => true,
(path) => unlinked.push(path),
);
assert.ok(result);
assert.deepEqual(unlinked, [], "the returned artifact must survive");
});
test("an unremovable partial does not mask the generate failure", async (t) => {
t.mock.method(console, "error", () => {});
const fakeExec = (...call) => {
if (isGenerate(call)) throw new Error("mlx out of memory");
};
const result = await ltxVideoGenerate(
"storm clouds",
{ specs: bothTiersSpecs },
fakeExec,
() => true,
() => {
throw new Error("EPERM: operation not permitted");
},
);
// cleanup is best-effort: a partial we cannot delete must not become the error
assert.equal(result, null);
});
@@ -0,0 +1,267 @@
import { existsSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { withReservedFile, withReservedFileSync } from "./manifest.mjs";
import { freezeUrl } from "./freeze.mjs";
import { tokenOverlap } from "./match.mjs";
import { buildCube } from "./cube-build.mjs";
import { validateCube, validateCubeFile } from "./cube-validate.mjs";
const SKILL_DIR = join(import.meta.dirname, "..", "..");
const LUT_DIR = join(SKILL_DIR, "luts");
const LUT_INDEX = join(LUT_DIR, "index.json");
export const LIBRARY_LUT_OFFLINE_CODE = "MEDIA_USE_LIBRARY_LUT_OFFLINE";
// Presets this released resolver can safely emit at this stack layer.
// Effect-backed treatment presets join after their runtime support lands.
export const RESOLVABLE_PRESET_IDS = [
"neutral",
"warm-daylight",
"clean-studio",
"skin-soft",
"food-pop",
"night-lift",
"muted-editorial",
"vintage-wash",
"mono-clean",
"mono-fade",
"soft-boost",
"bright-pop",
"deep-contrast",
"creator-camcorder",
"vhs-playback",
"home-movie-8mm",
"editorial-halftone",
"two-ink-print",
];
const PRESET_SYNONYMS = {
neutral: ["neutral", "identity", "none", "ungraded", "natural base"],
"warm-daylight": [
"warm daylight",
"warm natural light",
"golden daylight",
"sunlit",
"warm sunny",
"warm clean",
"clean warm",
"warm product",
],
"clean-studio": [
"clean studio",
"studio clean",
"cool studio",
"product studio",
"cool clean",
"clean cool",
"cool crisp",
],
"skin-soft": ["skin soft", "soft skin", "portrait soft", "beauty skin"],
"food-pop": ["food pop", "food vibrant", "appetizing", "restaurant color"],
"night-lift": ["night lift", "night", "low light lift", "city night"],
"muted-editorial": ["muted editorial", "editorial muted", "magazine muted"],
"vintage-wash": ["vintage wash", "vintage", "retro wash", "aged film"],
"mono-clean": ["mono clean", "black white clean", "monochrome clean"],
"mono-fade": ["mono fade", "black white fade", "faded monochrome"],
"soft-boost": [
"soft boost",
"soft bright",
"gentle boost",
"natural lift",
"natural light",
"gentle lift",
"soft natural",
],
"bright-pop": [
"bright pop",
"bright punchy",
"vivid bright",
"fresh pop",
"fresh",
"bright fresh",
"clean colorful",
],
"deep-contrast": ["deep contrast", "high contrast punchy", "punchy contrast", "bold contrast"],
"creator-camcorder": ["creator camcorder", "creator video", "ugc camera", "handheld creator"],
"vhs-playback": ["vhs playback", "vhs tape", "analog tape", "degraded tape"],
"home-movie-8mm": ["8mm home movie", "8mm film", "family film", "small gauge film"],
"editorial-halftone": ["editorial halftone", "halftone", "print dots", "newsprint"],
"two-ink-print": ["two ink print", "two ink editorial", "duotone print", "poster print"],
};
function presetCandidates() {
return RESOLVABLE_PRESET_IDS.map((id) => ({
kind: "preset",
preset: id,
synonyms: PRESET_SYNONYMS[id] ?? [],
text: [id, ...(PRESET_SYNONYMS[id] ?? [])].join(" "),
}));
}
export function readBundledLutIndex() {
if (!existsSync(LUT_INDEX)) return [];
const parsed = JSON.parse(readFileSync(LUT_INDEX, "utf8"));
const entries = Array.isArray(parsed) ? parsed : parsed.looks;
if (!Array.isArray(entries)) return [];
return entries.map((entry) => {
const params =
entry.params && typeof entry.params === "object" && !Array.isArray(entry.params)
? entry.params
: null;
const url = typeof entry.url === "string" && entry.url.trim() ? entry.url.trim() : null;
return {
id: String(entry.id),
description: String(entry.description ?? entry.id),
tags: Array.isArray(entry.tags) ? entry.tags.map(String) : [],
intensity: Number.isFinite(Number(entry.intensity)) ? Number(entry.intensity) : 1,
...(params && { params }),
...(url && { url }),
};
});
}
function libraryCandidates() {
return readBundledLutIndex().map((entry) => ({
kind: "library",
...entry,
text: [entry.id, entry.description, ...entry.tags].join(" "),
}));
}
export function matchColorLook(intent) {
const normalized = String(intent ?? "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
if (RESOLVABLE_PRESET_IDS.includes(normalized)) {
return { kind: "preset", preset: normalized, score: 99 };
}
const candidates = [...presetCandidates(), ...libraryCandidates()]
.map((candidate, index) => ({
...candidate,
index,
score: tokenOverlap(intent, candidate.text),
}))
.filter((candidate) => candidate.score >= 2)
.sort((a, b) => b.score - a.score || a.index - b.index);
if (candidates.length === 0) return null;
const best = candidates[0];
if (best.kind === "preset") {
return { kind: "preset", preset: best.preset, score: best.score };
}
return {
kind: "library",
id: best.id,
description: best.description,
tags: best.tags,
intensity: best.intensity,
...(best.params && { params: best.params }),
...(best.url && { url: best.url }),
score: best.score,
};
}
export function isLibraryLutOfflineMiss(err) {
return err?.code === LIBRARY_LUT_OFFLINE_CODE;
}
function libraryRecord(match, { id, localPath, fullPath, via }) {
return {
id,
localPath,
fullPath,
lut: { src: localPath, intensity: match.intensity },
source: "library",
description: match.description,
metadata: {
provider: "cube_lut.library",
provenance: {
look_id: match.id,
tags: match.tags,
via,
},
},
};
}
function assertValidCubeText(cube, label) {
const check = validateCube(cube);
if (!check.ok) throw new Error(`${label}: ${check.error}`);
}
function assertValidCubeFile(path, label) {
const check = validateCubeFile(path);
if (!check.ok) throw new Error(`${label}: ${check.error}`);
}
function offlineLibraryMiss(match) {
const err = new Error(`library LUT "${match.id}" is CDN-only and --local-only is set`);
err.code = LIBRARY_LUT_OFFLINE_CODE;
return err;
}
export async function freezeLibraryLut(match, { projectDir, type, localOnly = false }) {
if (!match || match.kind !== "library") {
throw new Error("freezeLibraryLut requires a library match");
}
// Prefer the CDN url so looks download on-demand (like bgm/image). Fall back
// to deterministic buildCube params when offline (--local-only) or if the
// download/validation fails, so resolution is never blocked on the network.
if (match.url && !localOnly) {
try {
return await withReservedFile(
projectDir,
type,
".cube",
async ({ id, localPath, fullPath }) => {
const tmpDir = mkdtempSync(join(dirname(fullPath), ".lut-download-"));
const tmpPath = join(tmpDir, "download.cube");
try {
// Download + validate in a private directory, then atomically rename. A crash
// (SIGKILL/OOM) between write and validate can't orphan an invalid .cube
// at the final path — only a validated cube is ever renamed into place.
await freezeUrl(match.url, tmpPath);
assertValidCubeFile(tmpPath, `downloaded library LUT ${match.id} failed validation`);
renameSync(tmpPath, fullPath);
return libraryRecord(match, { id, localPath, fullPath, via: "url" });
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
},
);
} catch (err) {
if (!match.params) {
throw new Error(`failed to freeze library LUT ${match.id}: ${err.message}`);
}
// else: fall through to the params fallback below
}
}
if (match.params) {
return withReservedFileSync(projectDir, type, ".cube", ({ id, localPath, fullPath }) => {
const tmpPath = `${fullPath}.tmp`;
try {
const cube = buildCube(match.params);
assertValidCubeText(cube, `invalid library LUT ${match.id}`);
// Write + validate at .tmp, then atomic rename — same no-orphan guarantee
// as the url path above.
writeFileSync(tmpPath, cube);
assertValidCubeFile(tmpPath, `invalid frozen LUT ${localPath}`);
renameSync(tmpPath, fullPath);
return libraryRecord(match, {
id,
localPath,
fullPath,
via: match.url ? "params-fallback" : "params",
});
} finally {
rmSync(tmpPath, { force: true });
}
});
}
if (match.url) throw offlineLibraryMiss(match); // url-only entry, offline
throw new Error(`misconfigured library LUT "${match.id}": expected params or url`);
}
@@ -0,0 +1,199 @@
import { strict as assert } from "node:assert";
import {
mkdtempSync,
rmSync,
existsSync,
readFileSync,
readdirSync,
statSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { test } from "node:test";
import {
LIBRARY_LUT_OFFLINE_CODE,
RESOLVABLE_PRESET_IDS,
freezeLibraryLut,
matchColorLook,
readBundledLutIndex,
} from "./lut-preset-provider.mjs";
import { buildCube } from "./cube-build.mjs";
import { validateCube, validateCubeFile } from "./cube-validate.mjs";
const REPO_ROOT = join(import.meta.dirname, "..", "..", "..", "..");
function corePresetIdsFromSource() {
const src = readFileSync(join(REPO_ROOT, "packages/core/src/colorGrading.ts"), "utf8");
const match = src.match(/export type HfColorGradingPresetId =([\s\S]*?);/);
assert.ok(match, "core preset union should be readable");
return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
}
test("warm daylight and warm natural light resolve to the core warm-daylight preset", () => {
assert.deepEqual(matchColorLook("warm daylight"), {
kind: "preset",
preset: "warm-daylight",
score: 2,
});
assert.equal(matchColorLook("warm natural light").preset, "warm-daylight");
});
test("high contrast punchy resolves to deep-contrast", () => {
assert.equal(matchColorLook("high contrast punchy").preset, "deep-contrast");
});
test("removed preset phrases resolve to surviving looks", () => {
assert.equal(matchColorLook("natural lift").preset, "soft-boost");
assert.equal(matchColorLook("fresh pop").preset, "bright-pop");
assert.equal(matchColorLook("warm clean").preset, "warm-daylight");
assert.equal(matchColorLook("cool clean").preset, "clean-studio");
});
test("complete-filter intent aliases resolve deterministically", () => {
assert.equal(matchColorLook("analog tape").preset, "vhs-playback");
assert.equal(matchColorLook("creator video").preset, "creator-camcorder");
});
test("library look freezes a validated cube from params offline (--local-only)", async () => {
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-provider-"));
try {
const match = matchColorLook("teal orange blockbuster");
assert.equal(match.kind, "library");
// localOnly forces the deterministic params path (no network); online, the
// same look downloads its .cube from the CDN url (via "url").
const frozen = await freezeLibraryLut(match, { projectDir, type: "grade", localOnly: true });
assert.match(frozen.localPath, /^\.media\/luts\/grade_001\.cube$/);
assert.ok(existsSync(join(projectDir, frozen.localPath)));
assert.equal(validateCubeFile(join(projectDir, frozen.localPath)).ok, true);
assert.equal(frozen.lut.src, frozen.localPath);
assert.equal(frozen.metadata.provenance.via, "params-fallback");
} finally {
rmSync(projectDir, { recursive: true, force: true });
}
});
test("every resolver preset exists in packages/core/src/colorGrading.ts", () => {
const corePresetIds = corePresetIdsFromSource();
assert.deepEqual(
RESOLVABLE_PRESET_IDS.filter((id) => !corePresetIds.includes(id)),
[],
);
for (const id of RESOLVABLE_PRESET_IDS) {
const match = matchColorLook(id);
assert.equal(match.kind, "preset");
assert.equal(match.preset, id);
}
});
test("zero-overlap intent returns no preset or library match", () => {
assert.equal(matchColorLook("zqxv imaginary neutron look"), null);
});
test("bundled LUT index entries resolve from params or url", () => {
for (const entry of readBundledLutIndex()) {
assert.ok(entry.id);
assert.ok(entry.description);
assert.ok(entry.params || entry.url, `${entry.id} should define params or url`);
if (entry.params) {
assert.equal(typeof entry.params, "object");
assert.equal(validateCube(buildCube(entry.params)).ok, true, `${entry.id} params validate`);
}
if (entry.url) assert.equal(typeof entry.url, "string");
}
});
test("url library entries respect localOnly and freeze through fetch", async () => {
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-url-provider-"));
const match = {
kind: "library",
id: "cdn-look",
description: "CDN-hosted look",
tags: ["cdn"],
intensity: 0.7,
url: "https://example.invalid/look.cube",
};
const originalFetch = globalThis.fetch;
let fetchCalls = 0;
try {
globalThis.fetch = async () => {
fetchCalls++;
throw new Error("network should be skipped under localOnly");
};
await assert.rejects(
freezeLibraryLut(match, { projectDir, type: "lut", localOnly: true }),
(err) => {
assert.equal(err.code, LIBRARY_LUT_OFFLINE_CODE);
assert.match(err.message, /--local-only/);
return true;
},
);
assert.equal(fetchCalls, 0);
const cube = buildCube({ contrast: 0.1 });
const body = Buffer.from(cube);
globalThis.fetch = async (url) => {
fetchCalls++;
assert.equal(url, match.url);
const directory = join(projectDir, ".media/luts");
const staging = readdirSync(directory).filter((name) => name.startsWith(".lut-download-"));
assert.equal(staging.length, 1);
if (process.platform !== "win32") {
assert.equal(statSync(join(directory, staging[0])).mode & 0o777, 0o700);
}
const victim = join(projectDir, "victim.cube");
writeFileSync(victim, "unchanged");
symlinkSync(victim, join(directory, "lut_001.cube.tmp"));
return {
ok: true,
headers: { get: () => String(body.length) },
body: [body],
};
};
const frozen = await freezeLibraryLut(match, { projectDir, type: "lut" });
assert.equal(fetchCalls, 1);
assert.match(frozen.localPath, /^\.media\/luts\/lut_001\.cube$/);
assert.equal(validateCubeFile(join(projectDir, frozen.localPath)).ok, true);
assert.equal(frozen.metadata.provenance.via, "url");
assert.equal(readFileSync(join(projectDir, "victim.cube"), "utf8"), "unchanged");
assert.deepEqual(readdirSync(join(projectDir, ".media/luts")).sort(), [
"lut_001.cube",
"lut_001.cube.tmp",
]);
} finally {
globalThis.fetch = originalFetch;
rmSync(projectDir, { recursive: true, force: true });
}
});
test("failed URL library freeze releases its reservation", async () => {
const projectDir = mkdtempSync(join(tmpdir(), "mu-lut-failure-"));
const originalFetch = globalThis.fetch;
try {
globalThis.fetch = async () => ({
ok: true,
headers: { get: () => "12" },
body: [Buffer.from("not a cube\n")],
});
await assert.rejects(
freezeLibraryLut(
{
kind: "library",
id: "broken-cdn-look",
description: "Broken CDN look",
tags: ["broken"],
intensity: 1,
url: "https://example.com/broken.cube",
},
{ projectDir, type: "lut" },
),
/failed to freeze library LUT/,
);
assert.deepStrictEqual(readdirSync(join(projectDir, ".media/luts")), []);
} finally {
globalThis.fetch = originalFetch;
rmSync(projectDir, { recursive: true, force: true });
}
});
@@ -0,0 +1,229 @@
import {
readFileSync,
appendFileSync,
mkdirSync,
existsSync,
readdirSync,
openSync,
closeSync,
writeFileSync,
rmSync,
statSync,
} from "node:fs";
import { join } from "node:path";
const MANIFEST_FILE = "manifest.jsonl";
const INDEX_FILE = "index.md";
const TYPE_DIRS = {
bgm: "audio/bgm",
sfx: "audio/sfx",
voice: "audio/voice",
image: "images",
icon: "images",
logo: "images",
brand: "images",
video: "video",
grade: "luts",
lut: "luts",
recipe: "recipes",
};
export function mediaDir(projectDir) {
return join(projectDir, ".media");
}
export function manifestPath(projectDir) {
return join(mediaDir(projectDir), MANIFEST_FILE);
}
export function indexPath(projectDir) {
return join(mediaDir(projectDir), INDEX_FILE);
}
export function typeSubdir(type) {
const sub = TYPE_DIRS[type];
if (!sub) throw new Error(`unknown media type: ${type}`);
return sub;
}
export function typeDirPath(projectDir, type) {
return join(mediaDir(projectDir), typeSubdir(type));
}
export function readManifest(projectDir) {
const p = manifestPath(projectDir);
if (!existsSync(p)) return [];
const raw = readFileSync(p, "utf8");
const records = [];
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
records.push(JSON.parse(trimmed));
} catch {
// ponytail: skip malformed lines, don't crash
}
}
return records;
}
export function appendRecord(projectDir, record) {
const dir = mediaDir(projectDir);
mkdirSync(dir, { recursive: true });
const typeDir = typeDirPath(projectDir, record.type);
mkdirSync(typeDir, { recursive: true });
const p = manifestPath(projectDir);
const line = JSON.stringify(record) + "\n";
appendFileSync(p, line);
}
// Match prompts forgivingly. Agents rarely re-emit a byte-identical intent, so
// keying cache lookups on exact equality meant "Calm piano" and "calm piano"
// re-searched and re-downloaded. Normalize (trim, lowercase, collapse internal
// whitespace) on both sides; the raw prompt is still stored for audit.
export function normalizePrompt(prompt) {
return String(prompt ?? "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
export function findByPrompt(projectDir, prompt, type) {
const key = normalizePrompt(prompt);
if (!key) return null;
const records = readManifest(projectDir);
return (
records.find(
(r) => normalizePrompt(r.provenance?.prompt) === key && (type == null || r.type === type),
) || null
);
}
export function findByEntity(projectDir, entity) {
const lower = entity.toLowerCase();
const records = readManifest(projectDir);
return records.find((r) => r.entity && r.entity.toLowerCase() === lower) || null;
}
export function nextId(projectDir, type) {
const records = readManifest(projectDir);
const prefix = type;
let max = 0;
for (const r of records) {
if (r.type !== type) continue;
const m = r.id?.match(new RegExp(`^${prefix}_(\\d+)$`));
if (m) max = Math.max(max, parseInt(m[1], 10));
}
return `${prefix}_${String(max + 1).padStart(3, "0")}`;
}
// Sync sleep (no busy-spin) for the allocation lock retry.
function sleepMs(ms) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
// Coarse per-project lock so concurrent resolves don't race on id allocation.
// ponytail: one lock file with a 15s stale-steal (a crashed holder can't wedge
// the project); fine for agent-scale concurrency — revisit if throughput needs
// finer locking. Date.now() is available here (a normal Node CLI, not a
// workflow DSL), so mtime-based staleness is safe.
const LOCK_STALE_MS = 15000;
const LOCK_TIMEOUT_MS = 20000;
function withLock(dir, fn) {
const lock = join(dir, ".lock");
const start = Date.now();
for (;;) {
try {
closeSync(openSync(lock, "wx")); // O_EXCL: atomic acquire
break;
} catch (err) {
if (err.code !== "EEXIST") throw err;
try {
if (Date.now() - statSync(lock).mtimeMs > LOCK_STALE_MS) {
rmSync(lock, { force: true }); // steal a stale lock from a dead holder
continue;
}
} catch {
continue; // lock vanished between check and stat — retry the acquire
}
if (Date.now() - start > LOCK_TIMEOUT_MS) {
throw new Error("media-use: timed out acquiring .media/.lock");
}
sleepMs(25);
}
}
try {
return fn();
} finally {
rmSync(lock, { force: true });
}
}
// Atomically allocate the next free id for `type` AND reserve its file, so a
// slow download/copy between allocation and appendRecord can't let a concurrent
// caller grab the same id (the MU-23 clobber). Under the lock we take the max id
// across BOTH the manifest and any already-reserved files in the type dir, then
// O_EXCL-create an empty placeholder at the target path; freeze/copy overwrites
// it. Returns { id, localPath }.
export function allocateId(projectDir, type, ext) {
mkdirSync(mediaDir(projectDir), { recursive: true });
const typeDir = typeDirPath(projectDir, type);
mkdirSync(typeDir, { recursive: true });
return withLock(mediaDir(projectDir), () => {
const re = new RegExp(`^${type}_(\\d+)`);
let max = 0;
for (const r of readManifest(projectDir)) {
if (r.type !== type) continue;
const m = r.id?.match(re);
if (m) max = Math.max(max, parseInt(m[1], 10));
}
for (const f of readdirSync(typeDir)) {
const m = f.match(re);
if (m) max = Math.max(max, parseInt(m[1], 10)); // skip ids reserved but not yet appended
}
const id = `${type}_${String(max + 1).padStart(3, "0")}`;
const localPath = `.media/${typeSubdir(type)}/${id}${ext}`;
writeFileSync(join(projectDir, localPath), "", { flag: "wx" }); // durable reservation
return { id, localPath };
});
}
function reservedFile(projectDir, type, ext) {
const allocation = allocateId(projectDir, type, ext);
return { ...allocation, fullPath: join(projectDir, allocation.localPath) };
}
function rollbackReservation(reservation) {
rmSync(reservation.fullPath, { force: true });
}
// A reservation is committed only when populate returns a non-null value.
// Throwing/rejecting or returning null means no usable asset was produced, so
// the placeholder must be released. Keeping this transaction beside allocateId
// prevents individual provider/cache/LUT paths from forgetting the rollback.
export function withReservedFileSync(projectDir, type, ext, populate) {
const reservation = reservedFile(projectDir, type, ext);
try {
const result = populate(reservation);
if (result == null) rollbackReservation(reservation);
return result;
} catch (error) {
rollbackReservation(reservation);
throw error;
}
}
export async function withReservedFile(projectDir, type, ext, populate) {
const reservation = reservedFile(projectDir, type, ext);
try {
const result = await populate(reservation);
if (result == null) rollbackReservation(reservation);
return result;
} catch (error) {
rollbackReservation(reservation);
throw error;
}
}
@@ -0,0 +1,388 @@
import { strict as assert } from "node:assert";
import {
mkdtempSync,
rmSync,
readFileSync,
writeFileSync,
mkdirSync,
existsSync,
readdirSync,
} from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
readManifest,
appendRecord,
findByPrompt,
findByEntity,
nextId,
allocateId,
withReservedFileSync,
normalizePrompt,
manifestPath,
mediaDir,
typeDirPath,
typeSubdir,
} from "./manifest.mjs";
import { regenerateIndex, generateIndexContent } from "./index-gen.mjs";
import {
contentHash,
cachePut,
cacheGet,
cacheGetByEntity,
importFromCache,
promote,
} from "./cache.mjs";
let tmp;
function setup() {
tmp = mkdtempSync(join(tmpdir(), "mu-test-"));
}
function cleanup() {
if (tmp) rmSync(tmp, { recursive: true, force: true });
}
function makeRecord(overrides = {}) {
return {
id: "bgm_001",
type: "bgm",
path: ".media/audio/bgm/bgm_001.wav",
source: "search",
description: "soft minimal ambient",
duration: 11,
provenance: { provider: "heygen.audio.sounds", prompt: "subtle tech" },
...overrides,
};
}
function runTests() {
const tests = [];
function test(name, fn) {
tests.push({ name, fn });
}
// --- manifest.mjs ---
test("readManifest returns empty array when no manifest exists", () => {
setup();
const result = readManifest(tmp);
assert.deepStrictEqual(result, []);
cleanup();
});
test("appendRecord writes valid JSONL and readManifest parses it back", () => {
setup();
const record = makeRecord();
appendRecord(tmp, record);
const records = readManifest(tmp);
assert.equal(records.length, 1);
assert.deepStrictEqual(records[0], record);
cleanup();
});
test("appendRecord creates .media/ and type subdirs on first write", () => {
setup();
appendRecord(tmp, makeRecord());
assert.ok(existsSync(mediaDir(tmp)));
assert.ok(existsSync(typeDirPath(tmp, "bgm")));
cleanup();
});
test("lut and grade artifacts use the shared .media/luts subdir", () => {
assert.equal(typeSubdir("lut"), "luts");
assert.equal(typeSubdir("grade"), "luts");
setup();
const allocated = allocateId(tmp, "lut", ".cube");
assert.equal(allocated.localPath, ".media/luts/lut_001.cube");
assert.ok(existsSync(join(tmp, allocated.localPath)));
cleanup();
});
test("appendRecord appends multiple records", () => {
setup();
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
appendRecord(tmp, makeRecord({ id: "bgm_002", provenance: { prompt: "energetic" } }));
const records = readManifest(tmp);
assert.equal(records.length, 2);
assert.equal(records[0].id, "bgm_001");
assert.equal(records[1].id, "bgm_002");
cleanup();
});
test("findByPrompt returns exact-match record", () => {
setup();
appendRecord(tmp, makeRecord());
const found = findByPrompt(tmp, "subtle tech", "bgm");
assert.ok(found);
assert.equal(found.id, "bgm_001");
cleanup();
});
test("findByPrompt returns null on miss", () => {
setup();
appendRecord(tmp, makeRecord());
assert.equal(findByPrompt(tmp, "nonexistent", "bgm"), null);
cleanup();
});
test("findByPrompt filters by type", () => {
setup();
appendRecord(tmp, makeRecord({ type: "sfx" }));
assert.equal(findByPrompt(tmp, "subtle tech", "bgm"), null);
assert.ok(findByPrompt(tmp, "subtle tech", "sfx"));
cleanup();
});
test("findByPrompt matches across case and whitespace variants", () => {
setup();
appendRecord(tmp, makeRecord({ provenance: { provider: "x", prompt: "calm ambient piano" } }));
assert.ok(findByPrompt(tmp, "Calm Ambient Piano", "bgm"), "case-insensitive");
assert.ok(findByPrompt(tmp, " calm ambient piano ", "bgm"), "whitespace-insensitive");
assert.equal(findByPrompt(tmp, "calm ambient guitar", "bgm"), null, "still a real miss");
cleanup();
});
test("normalizePrompt trims, lowercases, collapses whitespace", () => {
assert.equal(normalizePrompt(" Upbeat Tech Launch "), "upbeat tech launch");
assert.equal(normalizePrompt(null), "");
});
test("allocateId reserves the id on disk so a pre-append caller can't reuse it (MU-23)", () => {
setup();
const a = allocateId(tmp, "bgm", ".wav");
assert.equal(a.id, "bgm_001");
assert.ok(existsSync(join(tmp, a.localPath)), "placeholder reserved on disk");
// Second allocation BEFORE any manifest append (the download window) must not
// hand back bgm_001 again, even with a different extension.
const b = allocateId(tmp, "bgm", ".mp3");
assert.equal(b.id, "bgm_002");
assert.notEqual(a.localPath, b.localPath);
// Lock file is released (not left behind).
assert.ok(!existsSync(join(tmp, ".media", ".lock")), "lock released");
cleanup();
});
test("allocateId continues past the highest manifest id", () => {
setup();
appendRecord(tmp, makeRecord({ id: "bgm_005" }));
assert.equal(allocateId(tmp, "bgm", ".wav").id, "bgm_006");
cleanup();
});
test("failed reservation rollback preserves another completed reservation", () => {
setup();
const kept = withReservedFileSync(tmp, "bgm", ".wav", (reservation) => {
writeFileSync(reservation.fullPath, "completed asset");
return reservation;
});
assert.throws(
() =>
withReservedFileSync(tmp, "bgm", ".wav", () => {
throw new Error("populate failed");
}),
/populate failed/,
);
assert.equal(kept.id, "bgm_001");
assert.equal(readFileSync(kept.fullPath, "utf8"), "completed asset");
assert.deepStrictEqual(readdirSync(typeDirPath(tmp, "bgm")), ["bgm_001.wav"]);
assert.equal(allocateId(tmp, "bgm", ".wav").id, "bgm_002");
cleanup();
});
test("empty reservation result releases the placeholder", () => {
setup();
assert.equal(
withReservedFileSync(tmp, "image", ".jpg", () => null),
null,
);
assert.deepStrictEqual(readdirSync(typeDirPath(tmp, "image")), []);
cleanup();
});
test("findByEntity matches case-insensitively", () => {
setup();
appendRecord(tmp, makeRecord({ entity: "GitHub", type: "icon" }));
assert.ok(findByEntity(tmp, "github"));
assert.ok(findByEntity(tmp, "GITHUB"));
assert.equal(findByEntity(tmp, "gitlab"), null);
cleanup();
});
test("nextId generates sequential ids", () => {
setup();
assert.equal(nextId(tmp, "bgm"), "bgm_001");
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
assert.equal(nextId(tmp, "bgm"), "bgm_002");
appendRecord(tmp, makeRecord({ id: "bgm_002" }));
assert.equal(nextId(tmp, "bgm"), "bgm_003");
cleanup();
});
// --- index-gen.mjs ---
test("regenerateIndex produces plain-column table", () => {
setup();
appendRecord(tmp, makeRecord());
regenerateIndex(tmp);
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
assert.ok(content.includes("# .media · 1 asset"));
assert.ok(content.includes("bgm_001"));
assert.ok(content.includes("soft minimal ambient"));
assert.ok(content.includes("11s"));
cleanup();
});
test("regenerateIndex handles empty manifest", () => {
setup();
mkdirSync(join(tmp, ".media"), { recursive: true });
writeFileSync(manifestPath(tmp), "");
regenerateIndex(tmp);
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
assert.ok(content.includes("# .media · 0 assets"));
cleanup();
});
test("generateIndexContent includes dims for images", () => {
const records = [
makeRecord({ id: "img_001", type: "image", width: 1920, height: 1080, duration: null }),
];
const content = generateIndexContent(records);
assert.ok(content.includes("1920×1080"));
assert.ok(content.includes("img_001"));
});
test("regenerateIndex matches manifest content after multiple writes", () => {
setup();
appendRecord(tmp, makeRecord({ id: "bgm_001" }));
appendRecord(
tmp,
makeRecord({ id: "sfx_001", type: "sfx", description: "whoosh", duration: 3 }),
);
regenerateIndex(tmp);
const content = readFileSync(join(tmp, ".media", "index.md"), "utf8");
assert.ok(content.includes("# .media · 2 assets"));
assert.ok(content.includes("bgm_001"));
assert.ok(content.includes("sfx_001"));
assert.ok(content.includes("whoosh"));
cleanup();
});
// --- cache.mjs ---
test("cacheGet returns null when cache is empty", () => {
const result = cacheGet("nonexistent prompt", "bgm");
assert.equal(result, null);
});
test("cachePut + cacheGet round-trip", () => {
setup();
const filePath = join(tmp, "test.wav");
writeFileSync(filePath, "fake audio bytes for testing");
const record = makeRecord({ provenance: { prompt: "cache test" } });
const { sha } = cachePut(filePath, record);
assert.ok(sha);
assert.equal(sha.length, 64);
const found = cacheGet("cache test", "bgm");
assert.ok(found);
assert.equal(found.reusable, true);
assert.equal(found.sha, sha);
// cross-project reuse must survive trivial prompt variation, not just
// byte-identical intents (the whole point of normalizePrompt).
assert.ok(cacheGet(" Cache Test ", "bgm"), "cacheGet is case/whitespace-insensitive");
cleanup();
});
test("cacheGetByEntity finds cached asset", () => {
setup();
const filePath = join(tmp, "logo.png");
writeFileSync(filePath, "fake png bytes");
const record = makeRecord({
type: "icon",
entity: "TestCorp",
provenance: { prompt: "TestCorp logo" },
});
cachePut(filePath, record);
const found = cacheGetByEntity("testcorp");
assert.ok(found);
assert.equal(found.entity, "TestCorp");
cleanup();
});
test("contentHash is deterministic", () => {
setup();
const filePath = join(tmp, "det.bin");
writeFileSync(filePath, "deterministic content");
const h1 = contentHash(filePath);
const h2 = contentHash(filePath);
assert.equal(h1, h2);
cleanup();
});
test("promote copies project asset to global cache", () => {
setup();
const record = makeRecord();
appendRecord(tmp, record);
const filePath = join(tmp, record.path);
mkdirSync(join(filePath, ".."), { recursive: true });
writeFileSync(filePath, "promotable audio data");
const { sha } = promote(tmp, "bgm_001");
assert.ok(sha);
const cached = cacheGet("subtle tech", "bgm");
assert.ok(cached);
assert.equal(cached.sha, sha);
cleanup();
});
test("importFromCache copies cached file into project", () => {
setup();
const filePath = join(tmp, "source.wav");
writeFileSync(filePath, "importable audio");
const record = makeRecord({ provenance: { prompt: "import test" } });
const { sha } = cachePut(filePath, record);
const cached = cacheGet("import test", "bgm");
const projectDir = mkdtempSync(join(tmpdir(), "mu-import-"));
const imported = importFromCache(cached, projectDir, "bgm_001", ".media/audio/bgm/bgm_001.wav");
assert.ok(imported);
assert.equal(imported.id, "bgm_001");
assert.equal(imported.provenance.imported_from, sha);
assert.ok(existsSync(join(projectDir, ".media/audio/bgm/bgm_001.wav")));
rmSync(projectDir, { recursive: true, force: true });
cleanup();
});
// --- run ---
let passed = 0;
let failed = 0;
for (const { name, fn } of tests) {
try {
fn();
passed++;
console.log(` \x1b[32m✓\x1b[0m ${name}`);
} catch (err) {
failed++;
console.log(` \x1b[31m✗\x1b[0m ${name}`);
console.log(` ${err.message}`);
}
}
console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
}
console.log("media-use · manifest/index/cache tests\n");
runTests();
@@ -0,0 +1,47 @@
// Shared lexical-matching helpers used by both the assets/ scan (adopt.mjs) and
// the reuse-candidate ranker (candidates.mjs), and the type-equivalence check
// used by resolve.mjs and candidates.mjs. Kept in one place so the icon<->image
// equivalence and the token rules can't drift between the "do" path (resolve)
// and the "look" path (candidates).
// Common filler words that should never, on their own, make two strings match.
const MATCH_STOPWORDS = new Set([
"the",
"and",
"for",
"with",
"from",
"this",
"that",
"your",
"our",
]);
// Split into lowercased word tokens of length >= 3, minus stopwords.
export function matchTokens(text) {
return new Set(
String(text)
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter((t) => t.length >= 3 && !MATCH_STOPWORDS.has(t)),
);
}
// Count of shared meaningful word tokens between two strings. 0 = no lexical
// overlap (the candidate ranker still surfaces these, ordered after overlaps).
export function tokenOverlap(a, b) {
const ta = matchTokens(a);
const tb = matchTokens(b);
let n = 0;
for (const t of ta) if (tb.has(t)) n++;
return n;
}
// icon, image, and logo are interchangeable: all live in images/, and
// figma-imported brand marks are recorded as type image while agents ask for
// logos as icon or logo.
export function typesMatch(a, b) {
if (a === b) return true;
const visual = new Set(["icon", "image", "logo"]);
return visual.has(a) && visual.has(b);
}
@@ -0,0 +1,128 @@
import { execFileSync } from "node:child_process";
import { existsSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describeDownload, probeSpecs } from "./specs.mjs";
import { buildArgv, selectModelLadder } from "./local-models.mjs";
// Local image generation via mflux (FLUX-on-MLX), the Mac-native runner.
// Spec-gated: selectModelLadder("imagegen", specs) returns every FLUX-class
// model the machine's AVAILABLE RAM can actually run (medium FLUX-schnell
// --low-ram on ~24GB, up to Qwen-Image on 64GB+), best first. When nothing
// local fits, or no fitting tier can actually run here, this returns null so
// the registry falls through to the codex image upsell.
//
// The official FLUX repos are HF-gated, so the model entries point --path at
// non-gated community 4-bit re-uploads; the repo is resolved to a local snapshot
// (hf download, idempotent) because a bare repo id breaks mlx unflatten.
// Resolve an HF repo to its local snapshot dir. `hf download` is idempotent and
// prints the snapshot path as its last line.
function resolveSnapshot(repo, execFn, pathExists) {
const out = execFn("hf", ["download", repo], {
encoding: "utf8",
timeout: 1_800_000,
stdio: ["ignore", "pipe", "pipe"],
});
const path = out?.trim().split(/\r?\n/).pop()?.trim();
return path && pathExists(path) ? path : null;
}
export async function mfluxImageGenerate(
intent,
ctx,
execFn = execFileSync,
pathExists = existsSync,
unlinkFn = unlinkSync,
) {
const specs = ctx?.specs || probeSpecs();
const ladder = selectModelLadder("imagegen", specs, { preferTier: ctx?.preferTier });
if (!ladder.length) return null; // no local model fits -> codex upsell/fallback
// Each attempt mints its own timestamped output path, so a partial artifact
// from a failed tier is orphaned rather than overwritten - and a lower tier
// then succeeding hides it. Discard it before demoting. Best-effort: a
// partial we cannot remove must never mask the real failure.
const discardPartial = (path) => {
try {
if (pathExists(path)) unlinkFn(path);
} catch {
// nothing actionable: the generate failure below is the real story
}
};
// Best tier first, demoting past any tier that cannot run here (runner off
// PATH, a snapshot that won't download, an OOM) rather than failing local
// image gen outright. Every demotion is reported, so a quietly smaller model
// is never mistaken for the tier the machine nominally qualified for.
for (const model of ladder) {
const bin = model.invoke.trim().split(/\s+/)[0];
// Not installed? Surface the exact enable-command (before the model
// download) so the agent learns the free local path is available instead of
// silently taking the codex upsell.
try {
execFn("which", [bin], { stdio: ["ignore", "ignore", "ignore"] });
} catch {
console.error(
`media-use: local image gen not enabled (\`${bin}\` not on PATH). Install for free on-device FLUX: ${model.install}. Heads up: ${model.id} ${describeDownload(model.sizeMB)}.`,
);
continue;
}
const outPath = join(tmpdir(), `media-use-mflux-${process.pid}-${Date.now()}.png`);
const vars = {
prompt: intent,
w: ctx?.width || 512,
h: ctx?.height || 512,
seed: ctx?.seed ?? 42,
out: outPath,
};
if (model.repo && model.invoke.includes("{model_path}")) {
let snap = null;
let why = `could not resolve a local snapshot of ${model.repo}`;
try {
snap = resolveSnapshot(model.repo, execFn, pathExists);
} catch (err) {
why = `hf download failed: ${err.stderr?.toString().trim().slice(-200) || err.message}`;
}
if (!snap) {
console.error(`media-use: local image gen (${model.id}): ${why}`);
continue;
}
vars.model_path = snap;
}
const argv = buildArgv(model.invoke, vars);
argv.shift(); // drop the bin (already validated)
try {
execFn(bin, argv, {
encoding: "utf8",
timeout: 1_800_000,
stdio: ["ignore", "pipe", "pipe"],
});
} catch (err) {
discardPartial(outPath);
console.error(
`media-use: local image gen (${model.id}) failed: ${err.stderr?.toString().trim().slice(-200) || err.message}`,
);
continue;
}
if (!pathExists(outPath)) {
console.error(
`media-use: local image gen (${model.id}) exited cleanly but wrote no output file`,
);
continue;
}
return {
localPath: outPath,
ext: ".png",
source: "generated",
metadata: {
description: intent,
provider: `mflux.${model.id}`,
provenance: { model: model.id, tier: model.tier, prompt: intent },
},
};
}
return null;
}
@@ -0,0 +1,174 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mfluxImageGenerate } from "./mflux-provider.mjs";
// 40GB clears the 32GB klein tier and the 8GB schnell tier below it; the 64GB
// qwen tier stays out of reach. Two rungs is what makes demotion observable.
const bothTiersSpecs = { availableRamMB: 40000, gpu: { present: true } };
const SNAPSHOT = "/tmp/hf-snapshot";
// exec stub covering all three shells-out mflux does: the PATH probe, the
// idempotent `hf download`, and the generate itself.
function stubExec({ failGenerateFor = [], failWhichFor = [] } = {}) {
const calls = [];
const exec = (...call) => {
calls.push(call);
const [bin, argv] = call;
if (bin === "which") {
if (failWhichFor.includes(argv[0])) throw new Error("not found");
return "";
}
if (bin === "hf") return `Fetching 6 files...\n${SNAPSHOT}\n`;
if (failGenerateFor.some((id) => argv.join(" ").includes(id))) {
const err = new Error("exit 1");
err.stderr = "mlx.core.metal: out of memory";
throw err;
}
return "";
};
return { calls, exec };
}
const generateCalls = (calls) =>
calls.filter(([bin]) => bin !== "which" && bin !== "hf").map(([, argv]) => argv.join(" "));
test("no local model fits: falls through to the upsell without shelling out", async () => {
const { calls, exec } = stubExec();
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: { availableRamMB: 100, gpu: { present: true } } },
exec,
() => true,
);
assert.equal(result, null);
assert.deepEqual(calls, []);
});
test("a top tier that cannot run demotes to the next fitting tier", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const { calls, exec } = stubExec({ failGenerateFor: ["flux2-klein-4b"] });
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
);
assert.ok(result, "the schnell tier still produced an image");
assert.equal(result.metadata.provider, "mflux.flux-schnell-mflux-q4");
const generated = generateCalls(calls);
assert.equal(generated.length, 2, "klein attempted first, then schnell");
assert.match(generated[0], /flux2-klein-4b/);
assert.match(generated[1], /--model schnell/);
assert.equal(errors.length, 1, "the demotion is reported, not silent");
assert.match(errors[0], /flux2-klein-mflux-q4\) failed/);
});
test("a snapshot that will not resolve demotes rather than failing outright", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const calls = [];
const exec = (...call) => {
calls.push(call);
const [bin, argv] = call;
if (bin === "which") return "";
// klein's weights won't download; schnell's do
if (bin === "hf") {
if (argv[1].includes("FLUX.2-klein")) throw new Error("403 Forbidden");
return `${SNAPSHOT}\n`;
}
return "";
};
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
);
assert.ok(result, "demoted past the ungettable weights");
assert.equal(result.metadata.provider, "mflux.flux-schnell-mflux-q4");
assert.equal(errors.length, 1);
assert.match(errors[0], /hf download failed/);
});
test("runner missing from PATH reports the install hint per tier and returns null", async (t) => {
const errors = [];
t.mock.method(console, "error", (message) => errors.push(message));
const { exec } = stubExec({ failWhichFor: ["mflux-generate"] });
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
);
assert.equal(result, null);
assert.equal(errors.length, 2, "both fitting tiers reported");
assert.match(errors[0], /uv pip install mflux/);
// each hint states that tier's download cost before the user commits
assert.match(errors[0], /GB of weights to/);
assert.match(errors[1], /GB of weights to/);
});
// Same per-attempt temp path, same orphaning risk as the LTX provider: a
// partial png from a failed tier must not survive a lower tier succeeding.
const outputOf = (argv) => argv[argv.indexOf("--output") + 1];
const attemptedOutputs = (calls) =>
calls.filter(([bin]) => bin !== "which" && bin !== "hf").map(([, argv]) => outputOf(argv));
test("a failed attempt's partial output is discarded before demoting", async (t) => {
t.mock.method(console, "error", () => {});
const unlinked = [];
const { calls, exec } = stubExec({ failGenerateFor: ["flux2-klein-4b"] });
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
(path) => unlinked.push(path),
);
assert.ok(result, "the schnell tier still produced an image");
assert.deepEqual(unlinked, [attemptedOutputs(calls)[0]], "the failed klein partial is removed");
});
test("every tier failing discards every partial, one per attempt", async (t) => {
t.mock.method(console, "error", () => {});
const unlinked = [];
const { calls, exec } = stubExec({ failGenerateFor: ["flux2-klein-4b", "schnell"] });
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
(path) => unlinked.push(path),
);
assert.equal(result, null);
assert.deepEqual(unlinked, attemptedOutputs(calls), "nothing is left behind");
});
test("a successful generation is never discarded", async () => {
const unlinked = [];
const { exec } = stubExec();
const result = await mfluxImageGenerate(
"a red bicycle",
{ specs: bothTiersSpecs },
exec,
() => true,
(path) => unlinked.push(path),
);
assert.ok(result);
assert.deepEqual(unlinked, [], "the returned artifact must survive");
});
@@ -0,0 +1,49 @@
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const MISSES_FILE = "misses.jsonl";
function missesPath() {
return join(homedir(), ".media", MISSES_FILE);
}
export function recordMiss({ type, intent, provider_override, local_only }) {
try {
const dir = join(homedir(), ".media");
mkdirSync(dir, { recursive: true });
appendFileSync(
join(dir, MISSES_FILE),
JSON.stringify({
ts: new Date().toISOString(),
type,
intent,
provider_override: !!provider_override,
local_only: !!local_only,
}) + "\n",
);
} catch {
// local miss logging is best-effort; never surface into resolve
}
}
export function readMisses() {
const p = missesPath();
try {
if (!existsSync(p)) return [];
const raw = readFileSync(p, "utf8");
const records = [];
for (const line of raw.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
records.push(JSON.parse(trimmed));
} catch {
// skip malformed local lines, don't crash stats
}
}
return records;
} catch {
return [];
}
}
@@ -0,0 +1,78 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { readMisses, recordMiss } from "./misses.mjs";
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-misses-"));
const home = join(root, "home");
mkdirSync(home, { recursive: true });
process.env.HOME = home;
return { root, home };
}
function restoreEnv(saved) {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
test("recordMiss appends a well-formed local miss", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
recordMiss({ type: "bgm", intent: "moody synth pulse", provider_override: true });
const misses = readMisses();
assert.equal(misses.length, 1);
assert.equal(misses[0].type, "bgm");
assert.equal(misses[0].intent, "moody synth pulse");
assert.equal(misses[0].provider_override, true);
assert.equal(misses[0].local_only, false);
assert.ok(!Number.isNaN(Date.parse(misses[0].ts)));
const raw = readFileSync(join(home, ".media/misses.jsonl"), "utf8");
assert.match(raw, /moody synth pulse/);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
test("recordMiss swallows filesystem failures", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
writeFileSync(join(home, ".media"), "not a directory");
assert.doesNotThrow(() =>
recordMiss({ type: "image", intent: "unwritable", local_only: true }),
);
assert.equal(existsSync(join(home, ".media/misses.jsonl")), false);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
test("readMisses skips corrupt lines", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
mkdirSync(join(home, ".media"), { recursive: true });
writeFileSync(
join(home, ".media/misses.jsonl"),
[
JSON.stringify({ ts: "2026-07-09T00:00:00.000Z", type: "bgm", intent: "one" }),
"{not json",
JSON.stringify({ ts: "2026-07-09T00:00:01.000Z", type: "sfx", intent: "two" }),
].join("\n"),
);
assert.deepEqual(
readMisses().map((miss) => miss.intent),
["one", "two"],
);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,31 @@
import { existsSync } from "node:fs";
import { resolveSpawnCommand } from "../../audio/scripts/lib/tts.mjs";
// Sync-spawn analog of the audio engine's spawnP, for execFileSync call sites
// that must hard-fail (rather than fall through to another provider) when npx
// cannot be resolved. On Windows a bare "npx" is npx.cmd, which
// execFileSync/spawnSync cannot exec (spawnSync npx ENOENT) —
// resolveSpawnCommand reroutes it through node + npx-cli.js, no shell:true.
//
// `platform`/`env`/`pathExists` params (defaulting to the real values) exist
// so tests can exercise the win32 branch without mocking node:child_process
// (its ESM exports are non-configurable) — same idiom as spawnP and
// localTtsGenerate.
export function resolveNpxInvocation(
argv,
opts,
platform = process.platform,
env = process.env,
pathExists = existsSync,
) {
const resolved = resolveSpawnCommand("npx", argv, opts, platform, env, pathExists);
if (!resolved) {
// npx-on-win32 with no resolvable npx-cli.js — same terminal condition
// spawnP warns about, surfaced as a throw for callers with no fallback.
throw new Error(
"cannot run npx on Windows: npm's npx-cli.js was not found " +
"(install npm with Node, or run via npx/npm run so npm_execpath is set)",
);
}
return resolved;
}
@@ -0,0 +1,50 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveNpxInvocation } from "./npx-sync.mjs";
// Coverage parity with tts-local-provider.test.mjs, for the whisper.cpp
// fallback's call-site shape (transcribe.mjs runWhisper): the same three
// branches, but with the hard-fail contract — no fallback provider exists,
// so an unresolvable npx must throw actionably instead of returning null.
const envWithNpxCli = {
npm_execpath: "C:/Program Files/nodejs/node_modules/npm/bin/npm-cli.js",
npm_node_execpath: "C:/Program Files/nodejs/node.exe",
};
const npxCliPath = "C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js";
const pathExists = (path) => path === npxCliPath;
const WHISPER_ARGV = ["hyperframes", "transcribe", "C:\\media\\input.wav", "--dir", "C:\\work"];
const WHISPER_OPTS = { stdio: ["ignore", "pipe", "pipe"], timeout: 1_800_000 };
test("win32: routes the whisper fallback through node + npx-cli, never bare npx", () => {
const resolved = resolveNpxInvocation(
WHISPER_ARGV,
WHISPER_OPTS,
"win32",
envWithNpxCli,
pathExists,
);
assert.equal(resolved.cmd, envWithNpxCli.npm_node_execpath);
assert.equal(resolved.args[0], npxCliPath);
assert.deepEqual(resolved.args.slice(1, 3), ["hyperframes", "transcribe"]);
// execFileSync options survive the rerouting (the timeout bounds the
// whisper build/model download; the pipes surface its errors).
assert.deepEqual(resolved.opts.stdio, ["ignore", "pipe", "pipe"]);
assert.equal(resolved.opts.timeout, 1_800_000);
});
test("win32 without a resolvable npx-cli: throws the actionable install hint", () => {
assert.throws(
() => resolveNpxInvocation(WHISPER_ARGV, WHISPER_OPTS, "win32", {}, () => false),
/npx-cli\.js was not found/,
);
});
test("non-win32: spawns plain npx unchanged", () => {
const resolved = resolveNpxInvocation(WHISPER_ARGV, WHISPER_OPTS, "darwin", {}, () => false);
assert.equal(resolved.cmd, "npx");
assert.deepEqual(resolved.args, WHISPER_ARGV);
});
@@ -0,0 +1,27 @@
// Merge Parakeet-MLX token timestamps into word timestamps.
//
// parakeet-mlx JSON emits SUB-WORD tokens (" H", "ello", ...) with per-token
// start/end. Captions + transcript-cut need WORD timestamps, so join tokens
// into words on the space boundary: a token whose text starts with a space
// (or the very first token) begins a new word; the rest append. Output matches
// the { words: [{ text, start, end }] } shape the rest of media-use consumes
// (see words.mjs / cutlist.mjs).
export function mergeTokensToWords(parakeet) {
const sentences = Array.isArray(parakeet?.sentences) ? parakeet.sentences : [];
const words = [];
for (const s of sentences) {
for (const t of s.tokens ?? []) {
const raw = typeof t.text === "string" ? t.text : "";
const startsWord = raw.startsWith(" ") || words.length === 0;
if (startsWord) {
words.push({ text: raw.trim(), start: t.start, end: t.end });
} else {
const w = words[words.length - 1];
w.text += raw;
w.end = t.end;
}
}
}
return { text: (parakeet?.text ?? "").trim(), words: words.filter((w) => w.text.length > 0) };
}
@@ -0,0 +1,44 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { mergeTokensToWords } from "./parakeet-words.mjs";
test("mergeTokensToWords joins sub-word tokens on the space boundary", () => {
const parakeet = {
text: "Hello everyone. Um,",
sentences: [
{
tokens: [
{ text: " H", start: 0.0, end: 0.24 },
{ text: "ello", start: 0.24, end: 0.48 },
{ text: " everyone.", start: 0.48, end: 1.28 },
{ text: " Um,", start: 1.28, end: 1.92 },
],
},
],
};
const { words } = mergeTokensToWords(parakeet);
assert.deepEqual(words, [
{ text: "Hello", start: 0.0, end: 0.48 },
{ text: "everyone.", start: 0.48, end: 1.28 },
{ text: "Um,", start: 1.28, end: 1.92 },
]);
});
test("mergeTokensToWords spans multiple sentences and drops empties", () => {
const parakeet = {
text: "Hi there",
sentences: [
{ tokens: [{ text: "Hi", start: 0, end: 0.2 }] },
{ tokens: [{ text: " there", start: 0.5, end: 0.9 }] },
],
};
const { words } = mergeTokensToWords(parakeet);
assert.equal(words.length, 2);
assert.equal(words[1].text, "there");
assert.equal(words[1].start, 0.5);
});
test("mergeTokensToWords tolerates missing sentences/tokens", () => {
assert.deepEqual(mergeTokensToWords({}).words, []);
assert.deepEqual(mergeTokensToWords({ sentences: [{}] }).words, []);
});
@@ -0,0 +1,180 @@
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
/**
* Remembered defaults — the lightweight tier of HyperFrames user memory.
*
* Two files, same shape as the rest of media-use's storage split:
* - project `.media/preferences.json` — committed with the repo, so the whole
* team inherits it; written every time a brief answer is confirmed.
* - user `~/.media/preferences.json` — personal, cross-repo. A key is promoted
* here only once the same value has been confirmed in two different projects
* (`PROMOTE_AT`), so a one-off choice never pollutes the global defaults.
* Pre-promotion evidence accumulates in the user file's `sightings` ledger —
* project files can't see each other, so the cross-project count has to live
* user-side.
*
* Consumption contract (brief-contract § 2, Remembered defaults): a remembered
* value becomes the recommended option with a receipt naming its source — it
* never skips a question, and explicit request content always wins.
*/
const PREFS_FILE = "preferences.json";
/** Keys the brief contract records; `style_preset` is stored per workflow. */
export const PREFERENCE_KEYS = [
"destination",
"aspect",
"language",
"flow",
"storyboard",
"voice",
"style_preset",
];
/** A value must be confirmed in this many distinct projects to go user-tier. */
export const PROMOTE_AT = 2;
export function projectPrefsPath(projectDir) {
return join(resolve(projectDir), ".media", PREFS_FILE);
}
export function userPrefsPath() {
return join(homedir(), ".media", PREFS_FILE);
}
function emptyFile() {
return { version: 1, preferences: {}, sightings: {} };
}
function isRecord(value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Tolerant read — a missing or malformed file counts as empty. */
function readPrefsFile(path) {
try {
if (!existsSync(path)) return emptyFile();
const parsed = JSON.parse(readFileSync(path, "utf8"));
if (!isRecord(parsed)) return emptyFile();
return {
version: 1,
preferences: isRecord(parsed.preferences) ? parsed.preferences : {},
sightings: isRecord(parsed.sightings) ? parsed.sightings : {},
};
} catch {
return emptyFile();
}
}
/** Atomic write (tmp + rename) so a crash never leaves a torn file. */
function writePrefsFile(path, file) {
mkdirSync(dirname(path), { recursive: true });
const tmp = `${path}.tmp`;
writeFileSync(tmp, `${JSON.stringify(file, null, 2)}\n`);
renameSync(tmp, path);
}
/** `style_preset` entries are stored per workflow as `style_preset.<workflow>`. */
export function preferenceKeyFor(key, workflow) {
return key === "style_preset" && workflow ? `style_preset.${workflow}` : key;
}
function validEntry(entry) {
return isRecord(entry) && typeof entry.value === "string" && entry.value.length > 0;
}
/**
* The merged view the brief reads: user-tier promoted entries first, project
* entries on top (project wins). Each entry carries `source` plus the receipt
* material (`confirmed_in`, `updated_at`).
*/
export function mergedPreferences(projectDir) {
const user = readPrefsFile(userPrefsPath());
const project = readPrefsFile(projectPrefsPath(projectDir));
const merged = {};
for (const [key, entry] of Object.entries(user.preferences)) {
if (validEntry(entry)) merged[key] = { ...entry, source: "user" };
}
for (const [key, entry] of Object.entries(project.preferences)) {
if (validEntry(entry)) merged[key] = { ...entry, source: "project" };
}
return merged;
}
function dedupe(list) {
return [...new Set(list)];
}
/**
* Project tier: same value accumulates confirmations; a changed value starts
* provenance over (the old confirmations vouched for the old value).
*/
function recordProjectTier(projectDir, fullKey, value, projectName, now) {
const path = projectPrefsPath(projectDir);
const file = readPrefsFile(path);
const previous = file.preferences[fullKey];
const keepProvenance = validEntry(previous) && previous.value === value;
const confirmedIn = keepProvenance
? dedupe([...(Array.isArray(previous.confirmed_in) ? previous.confirmed_in : []), projectName])
: [projectName];
file.preferences[fullKey] = { value, confirmed_in: confirmedIn, updated_at: now };
writePrefsFile(path, file);
return confirmedIn;
}
/**
* User tier: accumulate this sighting in the ledger, and promote the key once
* the same value has been confirmed in PROMOTE_AT distinct projects.
*/
function recordUserSighting(fullKey, value, projectName, now) {
const path = userPrefsPath();
const file = readPrefsFile(path);
const keySightings = isRecord(file.sightings[fullKey]) ? file.sightings[fullKey] : {};
const seenIn = dedupe([
...(Array.isArray(keySightings[value]) ? keySightings[value] : []),
projectName,
]);
keySightings[value] = seenIn;
file.sightings[fullKey] = keySightings;
const promoted = seenIn.length >= PROMOTE_AT;
if (promoted) {
file.preferences[fullKey] = { value, confirmed_in: seenIn, updated_at: now };
}
writePrefsFile(path, file);
return promoted;
}
/**
* Record one confirmed brief answer. Always writes the project tier; feeds the
* user tier's sightings ledger and promotes once the same value has been
* confirmed in PROMOTE_AT distinct projects. Idempotent per project.
*/
export function recordPreference({ projectDir, key, value, workflow }) {
if (!PREFERENCE_KEYS.includes(key)) {
throw new Error(`unknown preference key: "${key}" (known: ${PREFERENCE_KEYS.join(", ")})`);
}
if (typeof value !== "string" || !value.trim()) {
throw new Error("a preference needs a non-empty string value");
}
if (key === "style_preset" && (!workflow || !String(workflow).trim())) {
throw new Error("style_preset is stored per workflow — pass --workflow <w>");
}
const fullKey = preferenceKeyFor(key, workflow);
const projectName = basename(resolve(projectDir));
const trimmed = value.trim();
const now = new Date().toISOString();
const confirmedIn = recordProjectTier(projectDir, fullKey, trimmed, projectName, now);
// Best-effort — a read-only home directory must never fail a brief.
let promoted = false;
try {
promoted = recordUserSighting(fullKey, trimmed, projectName, now);
} catch {
// The project record already landed; promotion just waits for next time.
}
return { key: fullKey, value: trimmed, confirmed_in: confirmedIn, promoted };
}
@@ -0,0 +1,189 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
mergedPreferences,
preferenceKeyFor,
projectPrefsPath,
recordPreference,
userPrefsPath,
} from "./prefs-store.mjs";
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-prefs-"));
const home = join(root, "home");
const projectA = join(root, "proj-a");
const projectB = join(root, "proj-b");
mkdirSync(home, { recursive: true });
mkdirSync(projectA, { recursive: true });
mkdirSync(projectB, { recursive: true });
process.env.HOME = home;
return { root, home, projectA, projectB };
}
function restoreEnv(saved) {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
function withSandbox(fn) {
const savedEnv = { ...process.env };
const box = sandbox();
try {
fn(box);
} finally {
restoreEnv(savedEnv);
rmSync(box.root, { recursive: true, force: true });
}
}
test("record writes the project tier and get merges it as source=project", () => {
withSandbox(({ projectA }) => {
const result = recordPreference({ projectDir: projectA, key: "destination", value: "x-feed" });
assert.equal(result.promoted, false);
assert.deepEqual(result.confirmed_in, ["proj-a"]);
const merged = mergedPreferences(projectA);
assert.equal(merged.destination.value, "x-feed");
assert.equal(merged.destination.source, "project");
const onDisk = JSON.parse(readFileSync(projectPrefsPath(projectA), "utf8"));
assert.equal(onDisk.preferences.destination.value, "x-feed");
});
});
test("one project never promotes: a fresh project sees nothing user-tier", () => {
withSandbox(({ projectA, projectB }) => {
recordPreference({ projectDir: projectA, key: "destination", value: "x-feed" });
// proj-b has no project entry, and the value was only confirmed once.
assert.equal(mergedPreferences(projectB).destination, undefined);
});
});
test("the same value confirmed in two projects promotes to the user tier", () => {
withSandbox(({ projectA, projectB }) => {
recordPreference({ projectDir: projectA, key: "destination", value: "x-feed" });
const second = recordPreference({ projectDir: projectB, key: "destination", value: "x-feed" });
assert.equal(second.promoted, true);
const userFile = JSON.parse(readFileSync(userPrefsPath(), "utf8"));
assert.deepEqual(userFile.preferences.destination.confirmed_in.sort(), ["proj-a", "proj-b"]);
// A third project with no local entry now inherits the promoted default.
const projectC = join(projectA, "..", "proj-c");
mkdirSync(projectC, { recursive: true });
const merged = mergedPreferences(projectC);
assert.equal(merged.destination.value, "x-feed");
assert.equal(merged.destination.source, "user");
});
});
test("record is idempotent per project — confirmed_in stays deduped", () => {
withSandbox(({ projectA }) => {
recordPreference({ projectDir: projectA, key: "language", value: "zh" });
const again = recordPreference({ projectDir: projectA, key: "language", value: "zh" });
assert.deepEqual(again.confirmed_in, ["proj-a"]);
assert.equal(again.promoted, false);
});
});
test("a changed value restarts provenance instead of inheriting confirmations", () => {
withSandbox(({ projectA, projectB }) => {
recordPreference({ projectDir: projectA, key: "destination", value: "x-feed" });
recordPreference({ projectDir: projectB, key: "destination", value: "x-feed" });
const switched = recordPreference({
projectDir: projectA,
key: "destination",
value: "youtube",
});
assert.deepEqual(switched.confirmed_in, ["proj-a"]);
assert.equal(switched.promoted, false);
// The previously promoted value keeps serving other projects until youtube
// earns its own two confirmations.
const userFile = JSON.parse(readFileSync(userPrefsPath(), "utf8"));
assert.equal(userFile.preferences.destination.value, "x-feed");
});
});
test("project tier overrides a promoted user-tier value", () => {
withSandbox(({ projectA, projectB }) => {
recordPreference({ projectDir: projectA, key: "storyboard", value: "yes" });
recordPreference({ projectDir: projectB, key: "storyboard", value: "yes" });
recordPreference({ projectDir: projectA, key: "storyboard", value: "no" });
const merged = mergedPreferences(projectA);
assert.equal(merged.storyboard.value, "no");
assert.equal(merged.storyboard.source, "project");
});
});
test("the retired mode key is rejected", () => {
withSandbox(({ projectA }) => {
assert.throws(
() => recordPreference({ projectDir: projectA, key: "mode", value: "collaborative" }),
/unknown preference key/,
);
});
});
test("the run-shape keys record and promote like any field", () => {
withSandbox(({ projectA, projectB }) => {
recordPreference({ projectDir: projectA, key: "flow", value: "automation" });
const promoted = recordPreference({ projectDir: projectB, key: "flow", value: "automation" });
assert.equal(promoted.promoted, true);
recordPreference({ projectDir: projectA, key: "storyboard", value: "yes" });
const merged = mergedPreferences(projectA);
assert.equal(merged.flow.value, "automation");
assert.equal(merged.storyboard.value, "yes");
assert.equal(merged.storyboard.source, "project");
});
});
test("style_preset is keyed per workflow", () => {
withSandbox(({ projectA }) => {
assert.equal(
preferenceKeyFor("style_preset", "faceless-explainer"),
"style_preset.faceless-explainer",
);
assert.equal(preferenceKeyFor("destination", "faceless-explainer"), "destination");
recordPreference({
projectDir: projectA,
key: "style_preset",
value: "pin-and-paper",
workflow: "faceless-explainer",
});
const merged = mergedPreferences(projectA);
assert.equal(merged["style_preset.faceless-explainer"].value, "pin-and-paper");
assert.equal(merged["style_preset"], undefined);
});
});
test("style_preset without a workflow is rejected, never stored bare", () => {
withSandbox(({ projectA }) => {
assert.throws(
() => recordPreference({ projectDir: projectA, key: "style_preset", value: "pin-and-paper" }),
/pass --workflow/,
);
assert.deepEqual(mergedPreferences(projectA), {});
});
});
test("unknown keys and empty values are rejected", () => {
withSandbox(({ projectA }) => {
assert.throws(() => recordPreference({ projectDir: projectA, key: "vibe", value: "x" }));
assert.throws(() => recordPreference({ projectDir: projectA, key: "voice", value: " " }));
});
});
test("corrupt files are treated as empty instead of crashing", () => {
withSandbox(({ projectA, home }) => {
mkdirSync(join(projectA, ".media"), { recursive: true });
writeFileSync(projectPrefsPath(projectA), "not json");
mkdirSync(join(home, ".media"), { recursive: true });
writeFileSync(userPrefsPath(), '{"preferences": 42}');
assert.deepEqual(mergedPreferences(projectA), {});
const result = recordPreference({ projectDir: projectA, key: "language", value: "zh" });
assert.equal(result.value, "zh");
});
});
@@ -0,0 +1,39 @@
import { execFileSync } from "node:child_process";
import { extname } from "node:path";
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".ico"]);
export function probe(filePath) {
const ext = extname(filePath).toLowerCase();
if (ext === ".svg") return { width: null, height: null, duration: null, codec: "svg" };
try {
// execFileSync (no shell) so a hostile filename like `"; rm -rf ~; ".png`
// can't break out of the quoting — filePath is passed as a literal argv entry.
const raw = execFileSync(
"ffprobe",
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "--", filePath],
{ encoding: "utf8", timeout: 5000 },
);
const info = JSON.parse(raw);
const stream = info.streams?.[0];
const format = info.format;
const isImage = IMAGE_EXT.has(ext);
const duration = isImage
? null
: parseFloat(format?.duration) || parseFloat(stream?.duration) || null;
const width = parseInt(stream?.width, 10) || null;
const height = parseInt(stream?.height, 10) || null;
const codec = stream?.codec_name || null;
return {
duration: duration != null ? Math.round(duration * 10) / 10 : null,
width,
height,
codec,
};
} catch {
return { duration: null, width: null, height: null, codec: null };
}
}
@@ -0,0 +1,31 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { probe } from "./probe.mjs";
// Regression for the shell-injection fix: probe() must pass the path as a literal
// argv entry, never through a shell. A filename containing shell metacharacters
// must NOT execute. Under the old execSync(`ffprobe ... "${path}"`) the embedded
// `touch` ran and created the marker; under execFileSync it cannot, regardless of
// whether ffprobe is installed (the injected command never reaches a shell).
test("probe does not execute shell metacharacters in a filename", () => {
const dir = mkdtempSync(join(tmpdir(), "probe-inject-"));
const marker = join(dir, "INJECTED");
// Slash-free basename (a real on-disk filename) that breaks out of the old
// double-quoted interpolation and would `touch INJECTED` in the cwd.
const evil = join(dir, `clip"; touch INJECTED; echo ".mp4`);
const prevCwd = process.cwd();
try {
writeFileSync(evil, "not real media");
process.chdir(dir); // so a leaked `touch INJECTED` would land next to `marker`
const meta = probe(evil);
assert.equal(existsSync(marker), false, "injected `touch` must not have run");
// Bogus/unreadable media still returns the null-shaped result, never throws.
assert.deepEqual(Object.keys(meta).sort(), ["codec", "duration", "height", "width"]);
} finally {
process.chdir(prevCwd);
rmSync(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,5 @@
// Back-compat surface for the v1 provider API. The ordered, capability-based
// registry now lives in registry.mjs; this re-exports the v1 helpers so existing
// callers keep working. New code should import from registry.mjs directly
// (getProviders / runCapability).
export { getProvider, listTypes } from "./registry.mjs";
@@ -0,0 +1,367 @@
import {
cpSync,
existsSync,
mkdirSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from "node:fs";
import { homedir } from "node:os";
import { basename, join, resolve } from "node:path";
import { appendRecord, mediaDir, nextId } from "./manifest.mjs";
import { regenerateIndex } from "./index-gen.mjs";
import { mergedPreferences } from "./prefs-store.mjs";
/**
* Recipes — the heavyweight tier of HyperFrames user memory.
*
* A recipe is the full confirmed bundle for one video type: the frozen design
* spec (`frame.md`), the storyboard skeleton (structure with the content
* blanked), and the confirmed brief values — frozen after the run's final
* approval, reused to start the next video of the same type from everything
* already approved.
*
* Storage is **named folders**, not content-addressed cache entries: a recipe
* is an evolving bundle with a `version`, so re-freezing the same name bumps
* the version and archives the old folder as `<name>@v<N>`. Two tiers, same
* split as everything else in media-use: project `.media/recipes/<name>/`
* (committed) and user `~/.media/recipes/<name>/` (a freeze is already a
* confirmed bundle, so it promotes immediately — no two-project rule here).
*/
/** Frontmatter keys that describe THIS video, not the reusable type. */
const FRONTMATTER_CONTENT_KEYS = new Set(["message", "audience", "mode"]);
/** BRIEF.md frontmatter keys that describe this run, not the reusable type —
* a recipe never locks the run's shape, so the intent layer always re-asks. */
const BRIEF_CONTENT_KEYS = new Set(["flow", "storyboard", "message", "audience"]);
/** Per-frame metadata that is content, not structure. */
const FRAME_CONTENT_KEYS = new Set([
"voiceover",
"vo",
"voice_over",
"narration",
"scene",
"description",
"summary",
"caption",
"asset_candidates",
]);
const FRAME_HEADING_RE = /^(#{2,3})\s+(?:frame|beat|scene)\s+\d+/i;
export function projectRecipesDir(projectDir) {
return join(mediaDir(projectDir), "recipes");
}
export function userRecipesDir() {
return join(homedir(), ".media", "recipes");
}
export function slugifyRecipeName(name) {
const slug = String(name ?? "")
.trim()
.toLowerCase()
.replace(/[\s_]+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
if (!slug) throw new Error(`recipe name "${name}" has no usable characters`);
return slug;
}
function frameTitle(headingLine) {
const dash = headingLine.split(/\s+—\s+/)[1];
if (dash && dash.trim()) return dash.trim();
return headingLine.replace(/^#+\s*/, "").trim();
}
/** Frontmatter: drop the content keys, keep structure/style keys verbatim. */
function skeletonFrontmatter(lines, out, contentKeys = FRONTMATTER_CONTENT_KEYS) {
if (lines[0]?.trim() !== "---") return 0;
out.push(lines[0]);
let i = 1;
while (i < lines.length && lines[i].trim() !== "---") {
const key = lines[i].match(/^(\w+)\s*:/)?.[1]?.toLowerCase();
if (!key || !contentKeys.has(key)) out.push(lines[i]);
i++;
}
if (i < lines.length) {
out.push(lines[i]); // closing ---
i++;
}
return i;
}
/** One line inside a frame section — returns the replacement lines (may be none). */
function skeletonFrameLine(line, state, out) {
const bulletKey = line.match(/^-\s+(\w+)\s*:/)?.[1]?.toLowerCase();
if (bulletKey) {
if (bulletKey === "status") out.push("- status: outline");
else if (!FRAME_CONTENT_KEYS.has(bulletKey)) out.push(line);
return;
}
if (!line.trim()) {
out.push(line);
return;
}
// Frame prose: one placeholder per frame in place of the narrative.
if (!state.proseReplaced) {
out.push(
`<fill in: this video's content for the "${state.title}" beat — keep the layout role, replace the words.>`,
);
state.proseReplaced = true;
}
}
/**
* Skeletonize a STORYBOARD.md: keep the reusable structure (frame count,
* durations, transitions, src paths, the Video direction block, style-ish
* frontmatter), reset every status to `outline`, and blank the content
* (message/audience, narration guides, per-frame prose) down to a fill-in
* placeholder that names the frame's role.
*/
/**
* Skeletonize a BRIEF.md: keep the frontmatter's reusable keys (workflow,
* destination, aspect, language, length, angle…), drop the run-shape and
* content keys (flow, storyboard, message, audience), and blank each body
* section down to a fill-in placeholder under its kept heading.
*/
export function skeletonizeBrief(source) {
const lines = String(source ?? "").split(/\r?\n/);
const out = [];
let i = skeletonFrontmatter(lines, out, BRIEF_CONTENT_KEYS);
for (; i < lines.length; i++) {
const heading = lines[i].match(/^##\s+(.+)$/);
if (heading) {
out.push(lines[i], "");
out.push(
`<fill in: this video's ${heading[1].trim().toLowerCase()} — the recipe keeps the shape, this run supplies the specifics.>`,
);
out.push("");
}
}
return out.join("\n").replace(/\n{3,}/g, "\n\n");
}
export function skeletonizeStoryboard(source) {
const lines = String(source ?? "").split(/\r?\n/);
const out = [];
const state = { inFrame: false, proseReplaced: false, title: "" };
for (let i = skeletonFrontmatter(lines, out); i < lines.length; i++) {
const line = lines[i];
if (/^#{2,3}\s/.test(line)) {
state.inFrame = FRAME_HEADING_RE.test(line);
state.proseReplaced = false;
state.title = state.inFrame ? frameTitle(line) : "";
out.push(line);
} else if (!state.inFrame) {
out.push(line);
} else {
skeletonFrameLine(line, state, out);
}
}
return out.join("\n").replace(/\n{3,}/g, "\n\n");
}
/** The run's workflow as BRIEF.md records it — the source of truth a freeze
* must not contradict. Undefined when no BRIEF.md (or no `workflow:`) exists. */
function briefWorkflow(root) {
const brief = join(root, "BRIEF.md");
if (!existsSync(brief)) return undefined;
const lines = readFileSync(brief, "utf8").split(/\r?\n/);
if (lines[0]?.trim() !== "---") return undefined;
for (let i = 1; i < lines.length && lines[i].trim() !== "---"; i++) {
const match = lines[i].match(/^workflow\s*:\s*(.+?)\s*$/);
if (match) return match[1].replace(/^["']|["']$/g, "") || undefined;
}
return undefined;
}
function readRecipeJson(dir) {
try {
const parsed = JSON.parse(readFileSync(join(dir, "recipe.json"), "utf8"));
if (typeof parsed !== "object" || parsed === null || typeof parsed.name !== "string") {
return null;
}
return parsed;
} catch {
return null;
}
}
function prefValue(prefs, key) {
return prefs[key]?.value;
}
/**
* Freeze the current project's approved run as a named recipe. Writes the
* project-tier folder + a manifest record, then copies to the user tier (a
* freeze is already confirmed — it promotes immediately).
*/
export function freezeRecipe({ projectDir, name, workflow, blocks }) {
const slug = slugifyRecipeName(name);
const root = resolve(projectDir);
const fromBrief = briefWorkflow(root);
const fromFlag = workflow && String(workflow).trim() ? String(workflow).trim() : undefined;
// BRIEF.md decides; the flag only covers projects briefed before it existed.
const resolvedWorkflow = fromBrief ?? fromFlag;
if (!resolvedWorkflow) {
throw new Error("no workflow found — BRIEF.md names none and no --workflow was given");
}
const frameSpec = join(root, "frame.md");
const storyboard = join(root, "STORYBOARD.md");
if (!existsSync(frameSpec)) throw new Error("no frame.md to freeze — run the design step first");
if (!existsSync(storyboard)) throw new Error("no STORYBOARD.md to freeze");
const dir = join(projectRecipesDir(root), slug);
let version = 1;
const previous = existsSync(dir) ? readRecipeJson(dir) : null;
if (previous) {
version = (Number.isInteger(previous.version) ? previous.version : 1) + 1;
const archive = `${dir}@v${previous.version ?? 1}`;
rmSync(archive, { recursive: true, force: true });
renameSync(dir, archive);
}
mkdirSync(dir, { recursive: true });
const prefs = mergedPreferences(root);
const recipe = {
version,
name: slug,
workflow: resolvedWorkflow,
approved_at: new Date().toISOString(),
source_project: basename(root),
destination: prefValue(prefs, "destination"),
aspect: prefValue(prefs, "aspect"),
language: prefValue(prefs, "language"),
voice: prefValue(prefs, "voice"),
// The bare-key fallback tolerates records made before the store required
// style_preset to be workflow-scoped.
style_preset:
prefValue(prefs, `style_preset.${resolvedWorkflow}`) ?? prefValue(prefs, "style_preset"),
blocks: Array.isArray(blocks) && blocks.length > 0 ? blocks : undefined,
};
writeFileSync(join(dir, "recipe.json"), `${JSON.stringify(recipe, null, 2)}\n`);
cpSync(frameSpec, join(dir, "frame.md"));
writeFileSync(
join(dir, "storyboard-skeleton.md"),
`${skeletonizeStoryboard(readFileSync(storyboard, "utf8")).trimEnd()}\n`,
);
// Best-effort fourth artifact — projects briefed before BRIEF.md existed
// (or by workflows that don't write one) freeze fine without it.
const brief = join(root, "BRIEF.md");
const briefSkeleton = existsSync(brief);
if (briefSkeleton) {
writeFileSync(
join(dir, "brief-skeleton.md"),
`${skeletonizeBrief(readFileSync(brief, "utf8")).trimEnd()}\n`,
);
}
const id = nextId(root, "recipe");
appendRecord(root, {
id,
type: "recipe",
path: `.media/recipes/${slug}/recipe.json`,
entity: slug,
description: `recipe: ${slug} (${recipe.workflow}, v${version})`,
provenance: { provider: "recipe.freeze", version, source_project: recipe.source_project },
});
regenerateIndex(root);
// User tier — best-effort, like every other promotion.
try {
const userDir = join(userRecipesDir(), slug);
mkdirSync(userDir, { recursive: true });
cpSync(dir, userDir, { recursive: true, force: true });
} catch {
// The project-tier freeze already landed.
}
return {
id,
slug,
version,
dir,
briefSkeleton,
workflow: resolvedWorkflow,
workflowOverridden: Boolean(fromBrief && fromFlag && fromBrief !== fromFlag),
};
}
function scanRecipesDir(dir, source) {
if (!existsSync(dir)) return [];
const found = [];
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name.includes("@v")) continue;
const recipe = readRecipeJson(join(dir, entry.name));
if (recipe) found.push({ ...recipe, source, dir: join(dir, entry.name) });
}
return found;
}
/** Two-tier merged listing (project wins), newest approval first. */
export function listRecipes({ projectDir, workflow }) {
const merged = new Map();
for (const recipe of scanRecipesDir(userRecipesDir(), "user")) merged.set(recipe.name, recipe);
for (const recipe of scanRecipesDir(projectRecipesDir(resolve(projectDir)), "project")) {
merged.set(recipe.name, recipe);
}
let list = [...merged.values()];
if (workflow) list = list.filter((r) => r.workflow === workflow);
return list.sort((a, b) =>
String(b.approved_at ?? "").localeCompare(String(a.approved_at ?? "")),
);
}
/**
* Adopt a recipe into the current project: import the folder from the user
* tier when the project doesn't have it, copy its frame.md over the project's,
* and hand back the values + the skeleton path for the storyboard draft.
*/
export function useRecipe({ projectDir, name }) {
const slug = slugifyRecipeName(name);
const root = resolve(projectDir);
let dir = join(projectRecipesDir(root), slug);
if (!readRecipeJson(dir)) {
const userDir = join(userRecipesDir(), slug);
if (!readRecipeJson(userDir)) {
const known = listRecipes({ projectDir: root }).map((r) => r.name);
throw new Error(
`no recipe named "${slug}"${known.length ? ` (known: ${known.join(", ")})` : ""}`,
);
}
mkdirSync(dir, { recursive: true });
cpSync(userDir, dir, { recursive: true, force: true });
const imported = readRecipeJson(dir);
appendRecord(root, {
id: nextId(root, "recipe"),
type: "recipe",
path: `.media/recipes/${slug}/recipe.json`,
entity: slug,
description: `recipe: ${slug} (${imported.workflow}, v${imported.version})`,
provenance: { provider: "recipe.local", imported_from: "user-tier" },
});
regenerateIndex(root);
}
const recipe = readRecipeJson(dir);
cpSync(join(dir, "frame.md"), join(root, "frame.md"));
return {
recipe,
dir,
frameSpecPath: "frame.md",
skeletonPath: `.media/recipes/${slug}/storyboard-skeleton.md`,
// Recipes frozen before BRIEF.md existed have no brief skeleton — degrade.
briefSkeletonPath: existsSync(join(dir, "brief-skeleton.md"))
? `.media/recipes/${slug}/brief-skeleton.md`
: undefined,
};
}
@@ -0,0 +1,367 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
freezeRecipe,
listRecipes,
skeletonizeBrief,
skeletonizeStoryboard,
slugifyRecipeName,
useRecipe,
} from "./recipe-store.mjs";
import { recordPreference } from "./prefs-store.mjs";
const STORYBOARD = `---
format: 1080x1080
message: "Ship a launch video in an afternoon"
arc: Hook → Problem → Solution
audience: indie devs on X
mode: collaborative
music: upbeat tech
---
## Frame 1 — Hook
- scene: Big type punches in
- duration: 3s
- transition_in: cut
- status: animated
- voiceover: "Ship a launch video in an afternoon."
- src: compositions/frames/01-hook.html
Open cold on the promise. This is the thesis.
## Frame 2 — Proof
- duration: 4s
- transition_in: crossfade
- status: animated
- asset_candidates: dashboard.png
- src: compositions/frames/02-proof.html
Real dashboard, real numbers.
Second prose line to collapse.
## Video direction
Punchy, one accent color, hard cuts on the beat.
`;
const BRIEF = `---
workflow: product-launch-video
flow: automation
storyboard: yes
message: "Ship a launch video in an afternoon"
audience: indie devs on X
destination: x-feed
aspect: 1080x1080
language: en
length: 60s
angle: feature-reveal
---
## Intent
Sell the afternoon-launch promise to indie devs.
## Assets
- public/dashboard.png — the real dashboard, proof beat.
## Notes
- No stock-photo aesthetics.
`;
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-recipes-"));
const home = join(root, "home");
const project = join(root, "my-launch");
mkdirSync(home, { recursive: true });
mkdirSync(project, { recursive: true });
writeFileSync(join(project, "frame.md"), "# Frame spec\nbackground: #101014\n");
writeFileSync(join(project, "STORYBOARD.md"), STORYBOARD);
process.env.HOME = home;
return { root, home, project };
}
function restoreEnv(saved) {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
function withSandbox(fn) {
const savedEnv = { ...process.env };
const box = sandbox();
try {
fn(box);
} finally {
restoreEnv(savedEnv);
rmSync(box.root, { recursive: true, force: true });
}
}
test("slugifyRecipeName normalizes and rejects empty", () => {
assert.equal(slugifyRecipeName("Weekly Changelog!"), "weekly-changelog");
assert.equal(slugifyRecipeName(" pr_reveal "), "pr-reveal");
assert.throws(() => slugifyRecipeName("!!!"));
});
test("skeletonize keeps structure, resets status, blanks content", () => {
const skeleton = skeletonizeStoryboard(STORYBOARD);
// Structure kept.
assert.match(skeleton, /format: 1080x1080/);
assert.match(skeleton, /arc: Hook → Problem → Solution/);
assert.match(skeleton, /music: upbeat tech/);
assert.match(skeleton, /- duration: 3s/);
assert.match(skeleton, /- src: compositions\/frames\/01-hook\.html/);
assert.match(skeleton, /## Video direction/);
assert.match(skeleton, /hard cuts on the beat/);
// Statuses reset.
assert.equal((skeleton.match(/- status: outline/g) ?? []).length, 2);
assert.doesNotMatch(skeleton, /animated/);
// Content blanked.
assert.doesNotMatch(skeleton, /message:/);
assert.doesNotMatch(skeleton, /audience:/);
assert.doesNotMatch(skeleton, /mode:/);
assert.doesNotMatch(skeleton, /voiceover/);
assert.doesNotMatch(skeleton, /asset_candidates/);
assert.doesNotMatch(skeleton, /thesis/);
assert.doesNotMatch(skeleton, /Real dashboard/);
assert.match(skeleton, /<fill in: this video's content for the "Hook" beat/);
assert.match(skeleton, /<fill in: this video's content for the "Proof" beat/);
});
test("skeletonizeBrief keeps reusable keys, drops run-shape, blanks body sections", () => {
const skeleton = skeletonizeBrief(BRIEF);
// Reusable frontmatter kept.
assert.match(skeleton, /workflow: product-launch-video/);
assert.match(skeleton, /destination: x-feed/);
assert.match(skeleton, /aspect: 1080x1080/);
assert.match(skeleton, /length: 60s/);
assert.match(skeleton, /angle: feature-reveal/);
// Run-shape and content keys dropped.
assert.doesNotMatch(skeleton, /^flow:/m);
assert.doesNotMatch(skeleton, /^storyboard:/m);
assert.doesNotMatch(skeleton, /^message:/m);
assert.doesNotMatch(skeleton, /^audience:/m);
// Body sections kept as headings, prose blanked to placeholders.
assert.match(skeleton, /## Intent/);
assert.match(skeleton, /## Assets/);
assert.match(skeleton, /<fill in: this video's intent/);
assert.match(skeleton, /<fill in: this video's assets/);
assert.doesNotMatch(skeleton, /afternoon-launch promise/);
assert.doesNotMatch(skeleton, /dashboard\.png/);
});
test("freeze writes the folder, manifest record, and user-tier copy", () => {
withSandbox(({ project, home }) => {
recordPreference({ projectDir: project, key: "destination", value: "x-feed" });
recordPreference({
projectDir: project,
key: "style_preset",
value: "pin-and-paper",
workflow: "product-launch-video",
});
const frozen = freezeRecipe({
projectDir: project,
name: "Weekly Launch",
workflow: "product-launch-video",
blocks: ["data-chart"],
});
assert.equal(frozen.slug, "weekly-launch");
assert.equal(frozen.version, 1);
const dir = join(project, ".media/recipes/weekly-launch");
const recipe = JSON.parse(readFileSync(join(dir, "recipe.json"), "utf8"));
assert.equal(recipe.workflow, "product-launch-video");
assert.equal(recipe.destination, "x-feed");
assert.equal(recipe.style_preset, "pin-and-paper");
assert.deepEqual(recipe.blocks, ["data-chart"]);
assert.ok(existsSync(join(dir, "frame.md")));
assert.match(readFileSync(join(dir, "storyboard-skeleton.md"), "utf8"), /- status: outline/);
const manifest = readFileSync(join(project, ".media/manifest.jsonl"), "utf8");
assert.match(manifest, /"type":"recipe"/);
assert.match(manifest, /"entity":"weekly-launch"/);
assert.ok(existsSync(join(home, ".media/recipes/weekly-launch/recipe.json")));
});
});
test("freeze with a BRIEF.md carries the brief skeleton; use hands its path back", () => {
withSandbox(({ project, root }) => {
writeFileSync(join(project, "BRIEF.md"), BRIEF);
const frozen = freezeRecipe({
projectDir: project,
name: "promo",
workflow: "product-launch-video",
});
assert.equal(frozen.briefSkeleton, true);
const skeleton = readFileSync(join(project, ".media/recipes/promo/brief-skeleton.md"), "utf8");
assert.match(skeleton, /destination: x-feed/);
assert.doesNotMatch(skeleton, /^flow:/m);
const fresh = join(root, "fresh-project");
mkdirSync(fresh, { recursive: true });
const used = useRecipe({ projectDir: fresh, name: "promo" });
assert.equal(used.briefSkeletonPath, ".media/recipes/promo/brief-skeleton.md");
assert.ok(existsSync(join(fresh, ".media/recipes/promo/brief-skeleton.md")));
});
});
test("freeze without a BRIEF.md degrades: no skeleton, use returns no path", () => {
withSandbox(({ project, root }) => {
const frozen = freezeRecipe({
projectDir: project,
name: "promo",
workflow: "product-launch-video",
});
assert.equal(frozen.briefSkeleton, false);
assert.ok(!existsSync(join(project, ".media/recipes/promo/brief-skeleton.md")));
const fresh = join(root, "fresh-project");
mkdirSync(fresh, { recursive: true });
const used = useRecipe({ projectDir: fresh, name: "promo" });
assert.equal(used.briefSkeletonPath, undefined);
});
});
test("freeze takes the workflow from BRIEF.md over a contradicting flag", () => {
withSandbox(({ project }) => {
writeFileSync(
join(project, "BRIEF.md"),
BRIEF.replace("workflow: product-launch-video", "workflow: general-video"),
);
const frozen = freezeRecipe({
projectDir: project,
name: "promo",
workflow: "faceless-explainer",
});
assert.equal(frozen.workflow, "general-video");
assert.equal(frozen.workflowOverridden, true);
const recipe = JSON.parse(
readFileSync(join(project, ".media/recipes/promo/recipe.json"), "utf8"),
);
assert.equal(recipe.workflow, "general-video");
});
});
test("freeze without BRIEF.md falls back to --workflow; with neither it refuses", () => {
withSandbox(({ project }) => {
const frozen = freezeRecipe({
projectDir: project,
name: "promo",
workflow: "product-launch-video",
});
assert.equal(frozen.workflow, "product-launch-video");
assert.equal(frozen.workflowOverridden, false);
assert.throws(() => freezeRecipe({ projectDir: project, name: "other" }), /no workflow found/);
});
});
test("freeze finds a legacy bare style_preset record; the scoped key wins over it", () => {
withSandbox(({ project }) => {
writeFileSync(join(project, "BRIEF.md"), BRIEF);
// A record made before the store required workflow scoping.
mkdirSync(join(project, ".media"), { recursive: true });
writeFileSync(
join(project, ".media/preferences.json"),
JSON.stringify({
version: 1,
preferences: {
style_preset: {
value: "source-paper-flowchart",
confirmed_in: ["my-launch"],
updated_at: "2026-07-15T00:00:00.000Z",
},
},
sightings: {},
}),
);
freezeRecipe({ projectDir: project, name: "promo" });
const legacy = JSON.parse(
readFileSync(join(project, ".media/recipes/promo/recipe.json"), "utf8"),
);
assert.equal(legacy.style_preset, "source-paper-flowchart");
recordPreference({
projectDir: project,
key: "style_preset",
value: "pin-and-paper",
workflow: "product-launch-video",
});
freezeRecipe({ projectDir: project, name: "promo" });
const scoped = JSON.parse(
readFileSync(join(project, ".media/recipes/promo/recipe.json"), "utf8"),
);
assert.equal(scoped.style_preset, "pin-and-paper");
});
});
test("re-freezing bumps the version and archives the old folder", () => {
withSandbox(({ project }) => {
freezeRecipe({ projectDir: project, name: "promo", workflow: "product-launch-video" });
const again = freezeRecipe({
projectDir: project,
name: "promo",
workflow: "product-launch-video",
});
assert.equal(again.version, 2);
assert.ok(existsSync(join(project, ".media/recipes/promo@v1/recipe.json")));
const current = JSON.parse(
readFileSync(join(project, ".media/recipes/promo/recipe.json"), "utf8"),
);
assert.equal(current.version, 2);
});
});
test("list merges tiers (project wins), filters by workflow, skips archives", () => {
withSandbox(({ project, root }) => {
freezeRecipe({ projectDir: project, name: "promo", workflow: "product-launch-video" });
freezeRecipe({ projectDir: project, name: "promo", workflow: "product-launch-video" });
freezeRecipe({ projectDir: project, name: "explainer", workflow: "faceless-explainer" });
const fresh = join(root, "fresh-project");
mkdirSync(fresh, { recursive: true });
const all = listRecipes({ projectDir: fresh });
assert.deepEqual(all.map((r) => r.source).sort(), ["user", "user"]);
assert.equal(all.find((r) => r.name === "promo").version, 2);
const filtered = listRecipes({ projectDir: fresh, workflow: "faceless-explainer" });
assert.deepEqual(
filtered.map((r) => r.name),
["explainer"],
);
});
});
test("use imports from the user tier into a fresh project and copies frame.md", () => {
withSandbox(({ project, root }) => {
freezeRecipe({ projectDir: project, name: "promo", workflow: "product-launch-video" });
const fresh = join(root, "fresh-project");
mkdirSync(fresh, { recursive: true });
const used = useRecipe({ projectDir: fresh, name: "Promo" });
assert.equal(used.recipe.name, "promo");
assert.equal(used.skeletonPath, ".media/recipes/promo/storyboard-skeleton.md");
assert.ok(existsSync(join(fresh, "frame.md")));
assert.ok(existsSync(join(fresh, ".media/recipes/promo/storyboard-skeleton.md")));
assert.match(
readFileSync(join(fresh, ".media/manifest.jsonl"), "utf8"),
/"imported_from":"user-tier"/,
);
});
});
test("use with an unknown name lists what exists", () => {
withSandbox(({ project }) => {
freezeRecipe({ projectDir: project, name: "promo", workflow: "product-launch-video" });
assert.throws(
() => useRecipe({ projectDir: project, name: "nope" }),
/no recipe named "nope" \(known: promo\)/,
);
});
});
@@ -0,0 +1,218 @@
// Provider registry — the v2 contract.
//
// Each media type maps to an ORDERED list of provider entries. Providers are
// tried in order; the first to return a non-null result wins, which keeps
// resolution deterministic (same request -> same provider -> same file ->
// reproducible renders). heygen-CLI is always first for the types it serves.
//
// An entry exposes any of three capability methods — search / generate /
// process — plus { name }. media-use holds no keys; each external tool owns its
// own auth. Providers, by type:
// - heygen CLI: catalog + TTS, first for every type it serves (OAuth free
// allowance first, then the user's HeyGen billing path)
// - mflux: local FLUX-class image gen, spec-selected to the machine's RAM
// (free, private, offline once cached)
// - codex CLI: image gen on the user's ChatGPT sub — the better-quality upsell
// and the fallback when no local model fits
// - Kokoro (via the hyperframes CLI): local voiceover, free/private fallback
// when HeyGen credentials are absent or --local-only is requested
//
// Generation is local-first, cloud-upsell. `ctx.provider` forces one provider
// (e.g. "make an image with codex").
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 {
svglSearch,
simpleIconsSearch,
githubAvatarSearch,
faviconSearch,
} from "./logo-provider.mjs";
import { heygenTtsGenerate } from "./voice-provider.mjs";
import { heygenVideoGenerate } from "./heygen-video-provider.mjs";
import { ltxVideoGenerate } from "./ltx-video-provider.mjs";
import { localTtsGenerate } from "./tts-local-provider.mjs";
import { codexImageGenerate } from "./codex-provider.mjs";
import { mfluxImageGenerate } from "./mflux-provider.mjs";
// Provider markers: `network` = hits a remote service (skipped by --local-only).
// `paid` = may cost wallet credits after any OAuth/web-plan free allowance
// (documentation for the agent's cost judgment, X4: agent-initiated paid should
// confirm). HeyGen catalog SEARCH is free; HeyGen TTS is free for eligible
// OAuth CLI users up to the monthly allowance, then follows the user's billing.
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. 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 }),
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
// RAM can run, spec-selected; free, private, kept under --local-only), then
// the codex CLI on the user's ChatGPT sub as the better-quality upsell and
// the fallback when no local model fits.
A("mflux.local", { generate: mfluxImageGenerate }),
N("codex.image_gen", { generate: codexImageGenerate }),
],
icon: [N("heygen.asset.search", { search: iconProvider.search })],
logo: [
// Official brand marks. Tiers verified by a 54-brand stress test (100%
// cascade hit); HeyGen asset search is deliberately absent — it returns
// generic look-alike icons for brand queries. All free, all network →
// --local-only leaves only the cache rungs.
N("svgl", { search: svglSearch }),
N("simple-icons", { search: simpleIconsSearch }),
N("github.avatar", { search: githubAvatarSearch }),
N("favicon.ddg", { search: faviconSearch }),
],
voice: [
// HeyGen TTS first when credentialed so CLI/OAuth users consume the free
// web-plan allowance (10 min/month) before any paid path. --local-only skips
// it and keeps Kokoro as the private/offline fallback.
// Deliberately kept `paid` (X4 confirm-before-call) even though the first
// 10 min/month are free: the client can't know the remaining allowance, so
// confirming is safer than risking a silent charge once it's spent. (A
// tri-state "quota-first, paid after" would need backend quota state.)
P("heygen.tts", { generate: heygenTtsGenerate }),
A("kokoro.local", { generate: localTtsGenerate }),
],
video: [
// HeyGen avatar video first when credentialed; --local-only skips it and
// keeps LTX as the local fallback.
P("heygen.video", { generate: heygenVideoGenerate }),
A("ltx.local", { generate: ltxVideoGenerate }),
],
brand: [
// Local design spec, not heygen — reads frame.md / design.md tokens.
A("design_spec", { search: brandProvider.search }),
],
grade: [
// Local deterministic cascade handled by resolve.mjs so grade records can
// carry an inline block as well as an optional frozen .cube file.
A("color_grade.local", { search: async () => null, generate: async () => null }),
],
lut: [
// Lower-level local LUT generation/freezing path handled by resolve.mjs.
A("cube_lut.local", { search: async () => null, generate: async () => null }),
],
};
function listFor(type) {
const list = REGISTRY[type];
if (!list) throw new Error(`unknown media type: ${type}`);
return list;
}
/** Ordered providers for a type. */
export function getProviders(type) {
return listFor(type);
}
/** All declared media types. */
export function listTypes() {
return Object.keys(REGISTRY);
}
/** Provider names available for a type, in cascade order (for --provider validation). */
export function providerNamesFor(type) {
return listFor(type).map((p) => p.name);
}
/**
* name -> cost tier ("local" | "network_free" | "network_paid") over a collection
* of ordered provider lists, i.e. the A / N / P distinction the constructors above
* already declare. Exported so the conflict rule below is testable against a
* fixture; production reads the REGISTRY-wide index built from it.
*
* A name declared under two media types must carry the same tier in both. If it
* didn't, "did this resolve cost credit" would depend on which type happened to
* serve it, and the telemetry property would mean nothing — so this throws at
* import rather than silently picking one.
*/
export function buildProviderTierIndex(providerLists) {
const tiers = new Map();
for (const list of providerLists) {
for (const p of list) {
const tier = p.paid ? "network_paid" : p.network ? "network_free" : "local";
const prior = tiers.get(p.name);
if (prior && prior !== tier)
throw new Error(
`provider "${p.name}" is declared ${prior} under one media type and ${tier} under another`,
);
tiers.set(p.name, tier);
}
}
return tiers;
}
const PROVIDER_TIERS = buildProviderTierIndex(Object.values(REGISTRY));
/**
* Cost tier of a provider by name, or undefined for a name the registry doesn't
* declare. The registry stays the single owner of "does this cost credit", so
* dashboards and callers never re-derive it from provider-name string matching.
*/
export function providerTierFor(name) {
return PROVIDER_TIERS.get(name);
}
/**
* Does an override token (full name like "codex.image_gen" or a prefix like
* "codex") match any provider declared for the type? Same match rule as
* runProviders, so validation and dispatch never disagree.
*/
export function providerMatches(type, want) {
return providerNamesFor(type).some((n) => n === want || n.startsWith(`${want}.`));
}
/**
* Back-compat shim for the v1 single-provider API. Returns the first declared
* provider for the type (tagged with `type`); throws for an unknown type.
* Kept for v1 callers only — new code should use getProviders/runCapability.
*/
export function getProvider(type) {
const first = listFor(type)[0] || {};
return { ...first, type };
}
/**
* Run a capability across an explicit ordered provider list. Tries each in
* order, returns the first non-null result, skips providers that don't expose
* the capability. Pure over its input — the unit-testable core of the cascade.
*
* Offline guard: a `network` provider is skipped when `ctx.localOnly` is set —
* unconditionally, even under a `ctx.provider` override. --local-only is a hard
* safety flag: it must never make a network call. Forcing a network provider
* while offline yields a clean miss (the caller explains the conflict), never a
* silent network request.
* Provider override: `ctx.provider` (a full name like "codex.image_gen" or a
* prefix like "codex") pins resolution to matching providers only — this is how
* a user "make an image WITH codex" forces the upsell instead of taking the
* free-first default.
*/
export async function runProviders(providers, capability, intent, ctx) {
const want = ctx?.provider;
for (const p of providers) {
if (want && p.name !== want && !p.name.startsWith(`${want}.`)) continue;
if (p.network && ctx?.localOnly) continue; // --local-only wins, even over --provider
const fn = p[capability];
if (typeof fn !== "function") continue;
const res = await fn(intent, ctx);
if (res) return res;
}
return null;
}
/** Run a capability over the providers for a type (deterministic, heygen-first). */
export async function runCapability(type, capability, intent, ctx) {
return runProviders(getProviders(type), capability, intent, ctx);
}
@@ -0,0 +1,261 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import {
getProviders,
getProvider,
listTypes,
providerMatches,
providerNamesFor,
runProviders,
runCapability,
providerTierFor,
buildProviderTierIndex,
} from "./registry.mjs";
// --- registry shape -------------------------------------------------------
test("listTypes exposes the v2 media types", () => {
const types = listTypes();
for (const t of [
"bgm",
"sfx",
"image",
"icon",
"logo",
"voice",
"video",
"brand",
"grade",
"lut",
]) {
assert.ok(types.includes(t), `missing type: ${t}`);
}
});
test("heygen provider is first for every type it serves", () => {
for (const t of ["bgm", "sfx", "image", "icon"]) {
const first = getProviders(t)[0];
assert.ok(first, `no enabled provider for ${t}`);
assert.match(first.name, /^heygen/, `${t} first provider is ${first.name}`);
}
});
test("sanctioned providers only: heygen, local mflux/kokoro/ltx, codex, design spec, logo tiers", () => {
const allowed =
/^heygen|^bundled\.sfx$|^mflux\.local$|^kokoro\.local$|^ltx\.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}`);
}
}
});
test("image cascade: heygen catalog, then local mflux, then the codex upsell", () => {
const ps = getProviders("image");
assert.match(ps[0].name, /^heygen/, "heygen catalog first");
const names = ps.map((p) => p.name);
const mflux = ps.find((p) => p.name === "mflux.local");
const codex = ps.find((p) => p.name === "codex.image_gen");
assert.ok(mflux && typeof mflux.generate === "function", "local mflux registered");
assert.ok(codex && typeof codex.generate === "function", "codex upsell registered");
assert.ok(names.indexOf("mflux.local") < names.indexOf("codex.image_gen"), "local before codex");
assert.ok(!mflux.network, "local mflux is kept under --local-only");
assert.ok(codex.network, "codex is network (skipped under --local-only)");
});
test("voice cascade: HeyGen TTS first, Kokoro remains the local fallback", () => {
const ps = getProviders("voice");
assert.equal(ps[0].name, "heygen.tts", "HeyGen TTS is first when credentials exist");
assert.ok(ps[0].network, "HeyGen TTS is network (skipped under --local-only)");
assert.ok(ps[0].paid, "HeyGen TTS may bill after the OAuth free allowance");
assert.equal(ps[1].name, "kokoro.local", "local Kokoro is the offline fallback");
assert.ok(!ps[1].network, "local Kokoro kept under --local-only");
assert.ok(!ps[1].paid, "local Kokoro is free");
});
test("video cascade: HeyGen first, LTX local fallback, generate-only", async () => {
assert.deepEqual(providerNamesFor("video"), ["heygen.video", "ltx.local"]);
assert.equal(providerMatches("video", "ltx.local"), true);
const ps = getProviders("video");
assert.ok(ps[0].network, "HeyGen video is network (skipped under --local-only)");
assert.ok(ps[0].paid, "HeyGen video may bill after the OAuth free allowance");
assert.ok(!ps[1].network, "local LTX is kept under --local-only");
assert.equal(await runCapability("video", "search", "x", {}), null);
});
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 },
{ name: "mflux.local", generate: async () => ({ hit: "local" }) },
{ name: "codex.image_gen", network: true, generate: async () => ({ hit: "codex" }) },
];
// no override: local wins (first generate to return non-null)
assert.deepEqual(await runProviders(providers, "generate", "x", {}), { hit: "local" });
// override to codex: skip local, use codex even though local would have worked
assert.deepEqual(await runProviders(providers, "generate", "x", { provider: "codex" }), {
hit: "codex",
});
// override matches the full name too
assert.deepEqual(
await runProviders(providers, "generate", "x", { provider: "codex.image_gen" }),
{ hit: "codex" },
);
// --local-only wins even over a forced network provider: no network call,
// clean miss (the caller surfaces the conflict). A forced LOCAL provider under
// --local-only still runs.
assert.equal(
await runProviders(providers, "generate", "x", { provider: "codex", localOnly: true }),
null,
);
assert.deepEqual(
await runProviders(providers, "generate", "x", { provider: "mflux", localOnly: true }),
{ hit: "local" },
);
});
test("getProvider returns the first provider with its type, throws for unknown", () => {
const p = getProvider("bgm");
assert.equal(p.type, "bgm");
assert.equal(typeof p.search, "function");
assert.throws(() => getProvider("unknown_type"), /unknown media type/);
});
test("getProviders throws for unknown type", () => {
assert.throws(() => getProviders("nope"), /unknown media type/);
});
// --- deterministic capability execution (runProviders core) ---------------
test("runProviders calls providers in order and returns the first non-null", async () => {
const calls = [];
const providers = [
{
name: "a",
enabled: true,
search: async () => {
calls.push("a");
return null;
},
},
{
name: "b",
enabled: true,
search: async () => {
calls.push("b");
return { hit: "b" };
},
},
{
name: "c",
enabled: true,
search: async () => {
calls.push("c");
return { hit: "c" };
},
},
];
const res = await runProviders(providers, "search", "x", {});
assert.deepEqual(res, { hit: "b" });
assert.deepEqual(calls, ["a", "b"], "must stop at first non-null, never call c");
});
test("runProviders skips providers missing the requested capability", async () => {
const providers = [
{ name: "a", enabled: true /* no search */ },
{ name: "b", enabled: true, search: async () => ({ hit: "b" }) },
];
const res = await runProviders(providers, "search", "x", {});
assert.deepEqual(res, { hit: "b" });
});
test("runProviders returns null when no provider yields a result", async () => {
const providers = [{ name: "a", enabled: true, search: async () => null }];
assert.equal(await runProviders(providers, "search", "x", {}), null);
});
test("runCapability('bgm','process') is null — process slot is graceful when unfilled", async () => {
assert.equal(await runCapability("bgm", "process", "x", {}), null);
});
// --- provider cost tier (telemetry) ---------------------------------------
test("providerTierFor reports the registry's own A/N/P declaration", () => {
assert.equal(providerTierFor("heygen.tts"), "network_paid");
assert.equal(providerTierFor("heygen.video"), "network_paid");
assert.equal(providerTierFor("heygen.audio.sounds"), "network_free");
assert.equal(providerTierFor("heygen.asset.search"), "network_free");
assert.equal(providerTierFor("codex.image_gen"), "network_free");
assert.equal(providerTierFor("bundled.sfx"), "local");
assert.equal(providerTierFor("kokoro.local"), "local");
});
test("providerTierFor agrees across every type that declares the same name", () => {
// heygen.audio.sounds serves both bgm and sfx; heygen.asset.search serves both
// image and icon. A name whose tier depended on the media type would make the
// telemetry property meaningless.
const byName = new Map();
for (const type of listTypes()) {
for (const name of providerNamesFor(type)) {
const tier = providerTierFor(name);
assert.ok(tier, `every declared provider has a tier (${type}/${name})`);
const prior = byName.get(name);
if (prior) assert.equal(tier, prior, `${name} must carry one tier across types`);
byName.set(name, tier);
}
}
});
test("providerTierFor returns undefined for a name the registry does not declare", () => {
assert.equal(providerTierFor("does.not.exist"), undefined);
assert.equal(providerTierFor(undefined), undefined);
assert.equal(providerTierFor(null), undefined);
assert.equal(providerTierFor(""), undefined);
});
test("buildProviderTierIndex throws when one name carries two tiers", () => {
assert.throws(
() =>
buildProviderTierIndex([
[{ name: "dual", network: true }],
[{ name: "dual", network: true, paid: true }],
]),
/declared network_free under one media type and network_paid under another/,
);
});
test("buildProviderTierIndex accepts the same name repeated at the same tier", () => {
const index = buildProviderTierIndex([
[{ name: "same", network: true }],
[{ name: "same", network: true }],
]);
assert.equal(index.get("same"), "network_free");
});
test("--local-only skips every network provider (even free remote ones)", async () => {
let remoteRan = false;
const providers = [
{
name: "heygen",
network: true,
search: async () => {
remoteRan = true;
return { hit: "net" };
},
},
{ name: "local", search: async () => ({ hit: "local" }) },
];
assert.deepEqual(await runProviders(providers, "search", "x", { localOnly: true }), {
hit: "local",
});
assert.equal(remoteRan, false, "the remote provider must not be called offline");
});
@@ -0,0 +1,24 @@
// Paginated text search over a manifest (project or global) by id / description
// / entity. Plain substring match — no vector DB (B4): the agent's own file
// search covers semantics; this just keeps result pages small enough to not
// blow the context window.
const PAGE = 10;
export function searchRecords(records, query, { page = 1, pageSize = PAGE } = {}) {
const q = String(query || "")
.trim()
.toLowerCase();
const matched = q
? records.filter((r) =>
[r.id, r.description, r.entity, r.type].some(
(f) => f && String(f).toLowerCase().includes(q),
),
)
: records.slice();
const total = matched.length;
const pages = Math.max(1, Math.ceil(total / pageSize));
const p = Math.min(Math.max(1, page), pages);
const start = (p - 1) * pageSize;
return { results: matched.slice(start, start + pageSize), total, page: p, pages, pageSize };
}
@@ -0,0 +1,32 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { searchRecords } from "./search.mjs";
const recs = Array.from({ length: 23 }, (_, i) => ({
id: `bgm_${i}`,
type: i % 2 ? "bgm" : "sfx",
description: i === 5 ? "energetic tech whoosh" : `track ${i}`,
}));
test("matches id / description / type, case-insensitively", () => {
assert.equal(searchRecords(recs, "ENERGETIC").results.length, 1);
assert.equal(searchRecords(recs, "energetic").results[0].id, "bgm_5");
assert.ok(searchRecords(recs, "sfx").total > 0);
});
test("caps page size and reports pagination", () => {
const r = searchRecords(recs, "", { pageSize: 10 });
assert.equal(r.results.length, 10, "never returns more than a page");
assert.equal(r.total, 23);
assert.equal(r.pages, 3);
assert.equal(r.page, 1);
});
test("page navigation returns the right slice, clamps out-of-range", () => {
assert.equal(searchRecords(recs, "", { page: 3, pageSize: 10 }).results.length, 3);
assert.equal(searchRecords(recs, "", { page: 99, pageSize: 10 }).page, 3, "clamped to last page");
});
test("empty query returns everything (paginated)", () => {
assert.equal(searchRecords(recs, "").total, 23);
});
@@ -0,0 +1,23 @@
import { heygenSearch } from "./heygen-search.mjs";
export const sfxProvider = {
async search(intent) {
const results = heygenSearch("audio sounds list", intent, {
type: "sound_effects",
minScore: 0.4,
});
if (!results) return null;
const best = results[0];
return {
url: best.audio_url,
source: "search",
// ext derived from audio_url by resolve.mjs — catalog SFX are .mp3 or .wav
metadata: {
description: best.description || best.name || intent,
duration: best.duration || null,
provider: "heygen.audio.sounds",
provenance: { track_id: best.id, score: best.score, query: intent },
},
};
},
};
@@ -0,0 +1,132 @@
// Machine-capability probe for the spec-gated local-model fallback.
//
// Local models are USER-INSTALLED and local-use-only — media-use recommends,
// spec-checks, and assists install, but never bundles or runs them as a service.
// This probe answers "what tier can this machine actually run?" so selection can
// offer a medium/large local model, or fall back to recommending the CLI path.
//
// `osMod` and `exec` are injectable for tests. `exec(cmd)` returns the command's
// stdout as a string, or throws / returns null on failure.
import os from "node:os";
import { statfsSync } from "node:fs";
import { dirname, join } from "node:path";
import { execSync } from "node:child_process";
function defaultExec(cmd) {
return execSync(cmd, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 4000 });
}
// Available (not just total) RAM — the real budget for "will this model fit
// alongside the OS + open apps". On unified-memory Macs the model shares system
// RAM, so a big model on a busy machine OOMs/thrashes even if total RAM looks
// ample. macOS: vm_stat free + inactive + speculative + purgeable pages (all
// reclaimable). Linux: /proc/meminfo MemAvailable. Fallback: total (optimistic
// but stable when the probe is unavailable, e.g. in tests).
function availableRamMB(platform, exec, totalMB) {
try {
if (platform === "darwin") {
const out = String(exec("vm_stat"));
const pageSize = parseInt((out.match(/page size of (\d+)/) || [])[1] || "16384", 10);
const pages = (name) => {
const m = out.match(new RegExp(`${name}:\\s+(\\d+)`));
return m ? parseInt(m[1], 10) : 0;
};
const reclaimable =
pages("Pages free") +
pages("Pages inactive") +
pages("Pages speculative") +
pages("Pages purgeable");
const mb = Math.round((reclaimable * pageSize) / (1024 * 1024));
if (mb > 0) return mb;
} else if (platform === "linux") {
const out = String(exec("cat /proc/meminfo"));
const m = out.match(/MemAvailable:\s+(\d+)\s+kB/);
if (m) return Math.round(parseInt(m[1], 10) / 1024);
}
} catch {
// probe unavailable — fall through to the total-RAM estimate
}
return totalMB;
}
function detectGpu(platform, arch, ramMB, exec) {
// Apple Silicon: Metal GPU with unified memory — VRAM tracks system RAM.
if (platform === "darwin" && arch === "arm64") {
return { present: true, kind: "apple", vramMB: ramMB };
}
// NVIDIA: query total VRAM. Any failure (no driver, no GPU) -> no GPU.
try {
const out = exec("nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits");
const mb = parseInt(String(out).trim().split(/\r?\n/)[0], 10);
if (Number.isFinite(mb) && mb > 0) return { present: true, kind: "nvidia", vramMB: mb };
} catch {
// fall through — no usable GPU
}
return { present: false, kind: null, vramMB: 0 };
}
export function probeSpecs({ osMod = os, exec = defaultExec } = {}) {
const platform = osMod.platform();
const arch = osMod.arch();
const cpuCores = osMod.cpus().length;
const ramMB = Math.round(osMod.totalmem() / (1024 * 1024));
return {
platform,
arch,
cpuCores,
ramMB,
availableRamMB: availableRamMB(platform, exec, ramMB),
appleSilicon: platform === "darwin" && arch === "arm64",
gpu: detectGpu(platform, arch, ramMB, exec),
};
}
// Where Hugging Face actually puts downloaded weights. A free-space check
// against cwd measures the wrong filesystem, so the disk question has to be
// asked about this directory. Precedence follows huggingface_hub's own order.
export function weightsCacheDir({ env = process.env, osMod = os } = {}) {
if (env.HF_HUB_CACHE) return env.HF_HUB_CACHE;
if (env.HUGGINGFACE_HUB_CACHE) return env.HUGGINGFACE_HUB_CACHE;
if (env.HF_HOME) return join(env.HF_HOME, "hub");
return join(osMod.homedir(), ".cache", "huggingface", "hub");
}
// Free space on the filesystem that will hold the weights. The cache dir
// usually does not exist until the first download and statfs throws on a
// missing path, so walk up to the deepest ancestor that does exist. Returns
// null when even the root cannot be read, so callers can say "unknown" instead
// of implying zero and scaring someone off a download that would have worked.
export function freeSpaceMB(dir, statfsFn = statfsSync) {
let path = dir;
for (;;) {
try {
const { bavail, bsize } = statfsFn(path);
return (bavail * bsize) / 1e6;
} catch {
const parent = dirname(path);
if (parent === path) return null;
path = parent;
}
}
}
// One line the user reads BEFORE agreeing to a pull that can be tens of GB.
// Always names the size and where it lands. When it will not fit we say so
// plainly rather than withholding the tier: a machine that could free up space
// should still be told the tier exists. `statfsFn` / `env` / `osMod` are
// injectable for tests.
export function describeDownload(
sizeMB,
{ statfsFn = statfsSync, env = process.env, osMod = os } = {},
) {
const dir = weightsCacheDir({ env, osMod });
const gb = (mb) => (mb / 1000).toFixed(1);
const head = `downloads ~${gb(sizeMB)}GB of weights to ${dir}`;
const free = freeSpaceMB(dir, statfsFn);
if (free == null) return `${head} (free space unknown)`;
if (free < sizeMB) {
return `${head}, but only ${gb(free)}GB is free there, so it will NOT fit as-is`;
}
return `${head} (${gb(free)}GB free there)`;
}
@@ -0,0 +1,157 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { describeDownload, freeSpaceMB, probeSpecs, weightsCacheDir } from "./specs.mjs";
// Fake os module + exec so the probe is deterministic across CI machines.
const fakeOs = (over = {}) => ({
platform: () => over.platform ?? "linux",
arch: () => over.arch ?? "x64",
cpus: () => Array.from({ length: over.cores ?? 8 }),
totalmem: () => (over.ramMB ?? 16384) * 1024 * 1024,
});
test("probeSpecs reports structured caps", () => {
const s = probeSpecs({ osMod: fakeOs({ cores: 12, ramMB: 32768 }), exec: () => null });
assert.equal(s.cpuCores, 12);
assert.equal(s.ramMB, 32768);
assert.equal(s.platform, "linux");
assert.equal(s.gpu.present, false);
// probe unavailable (exec returns null) -> availableRamMB falls back to total
assert.equal(s.availableRamMB, 32768);
});
test("availableRamMB is read from /proc/meminfo on Linux", () => {
const exec = (cmd) =>
cmd.includes("meminfo") ? "MemTotal: 33554432 kB\nMemAvailable: 8388608 kB\n" : null;
const s = probeSpecs({ osMod: fakeOs({ platform: "linux", ramMB: 32768 }), exec });
assert.equal(s.availableRamMB, 8192, "8388608 kB -> 8192 MB");
});
test("availableRamMB is summed from reclaimable vm_stat pages on macOS", () => {
const vmStat =
"Mach Virtual Memory Statistics: (page size of 16384 bytes)\n" +
"Pages free: 100000.\n" +
"Pages inactive: 200000.\n" +
"Pages speculative: 50000.\n" +
"Pages purgeable: 10000.\n";
const exec = (cmd) => (cmd.includes("vm_stat") ? vmStat : null);
const s = probeSpecs({
osMod: fakeOs({ platform: "darwin", arch: "arm64", ramMB: 24576 }),
exec,
});
// (100000+200000+50000+10000) pages * 16384 B / 1MiB = 5625 MB
assert.equal(s.availableRamMB, 5625);
});
test("Apple Silicon is detected as a unified-memory GPU", () => {
const s = probeSpecs({
osMod: fakeOs({ platform: "darwin", arch: "arm64", ramMB: 24576 }),
exec: () => null,
});
assert.equal(s.appleSilicon, true);
assert.equal(s.gpu.present, true);
assert.equal(s.gpu.kind, "apple");
// unified memory: VRAM tracks system RAM
assert.equal(s.gpu.vramMB, 24576);
});
test("NVIDIA GPU is detected via nvidia-smi VRAM query", () => {
const exec = (cmd) => (cmd.includes("nvidia-smi") ? "24564" : null);
const s = probeSpecs({ osMod: fakeOs({ platform: "linux" }), exec });
assert.equal(s.gpu.present, true);
assert.equal(s.gpu.kind, "nvidia");
assert.equal(s.gpu.vramMB, 24564);
});
test("no GPU when nvidia-smi is absent / fails", () => {
const s = probeSpecs({
osMod: fakeOs({ platform: "linux" }),
exec: () => {
throw new Error("command not found");
},
});
assert.equal(s.gpu.present, false);
assert.equal(s.gpu.vramMB, 0);
});
// --- download disclosure: what the user is agreeing to before the pull ---
const fakeHome = { homedir: () => "/home/tester" };
// statfs reports blocks, not bytes: bavail * bsize. 1 MB blocks keep the sums
// readable, and mirror the real struct's shape.
const fakeStatfs = (freeMB, existsOnly) => (path) => {
if (existsOnly && path !== existsOnly) throw new Error(`ENOENT: ${path}`);
return { bavail: freeMB, bsize: 1e6 };
};
test("weightsCacheDir follows huggingface_hub's precedence", () => {
assert.equal(
weightsCacheDir({ env: {}, osMod: fakeHome }),
"/home/tester/.cache/huggingface/hub",
);
assert.equal(weightsCacheDir({ env: { HF_HOME: "/data/hf" }, osMod: fakeHome }), "/data/hf/hub");
assert.equal(
weightsCacheDir({ env: { HF_HOME: "/data/hf", HUGGINGFACE_HUB_CACHE: "/c" }, osMod: fakeHome }),
"/c",
"HUGGINGFACE_HUB_CACHE outranks HF_HOME",
);
assert.equal(
weightsCacheDir({ env: { HF_HUB_CACHE: "/a", HUGGINGFACE_HUB_CACHE: "/c" }, osMod: fakeHome }),
"/a",
"HF_HUB_CACHE wins outright",
);
});
test("freeSpaceMB walks up to the deepest existing ancestor", () => {
// the cache dir does not exist until the first download, and statfs throws
// on a missing path - so the answer has to come from an ancestor
const statfs = fakeStatfs(4096, "/home");
assert.equal(freeSpaceMB("/home/tester/.cache/huggingface/hub", statfs), 4096);
});
test("freeSpaceMB reports null rather than zero when nothing can be read", () => {
assert.equal(
freeSpaceMB("/home/tester/.cache", () => {
throw new Error("EACCES");
}),
null,
"unknown must not be reported as no-space",
);
});
test("describeDownload names the size and where it lands", () => {
const msg = describeDownload(87500, {
statfsFn: fakeStatfs(200000),
env: {},
osMod: fakeHome,
});
assert.match(msg, /~87\.5GB/);
assert.match(msg, /\/home\/tester\/\.cache\/huggingface\/hub/);
assert.match(msg, /200\.0GB free/);
assert.equal(/NOT fit/.test(msg), false, "it fits, so no warning");
});
test("describeDownload says plainly when the weights will not fit", () => {
const msg = describeDownload(87500, {
statfsFn: fakeStatfs(14000),
env: {},
osMod: fakeHome,
});
assert.match(msg, /only 14\.0GB is free there/);
assert.match(msg, /will NOT fit as-is/);
// the tier is still described, not withheld: a machine that could free up
// space should know the tier exists
assert.match(msg, /~87\.5GB/);
});
test("describeDownload admits when free space is unknown", () => {
const msg = describeDownload(87500, {
statfsFn: () => {
throw new Error("EACCES");
},
env: {},
osMod: fakeHome,
});
assert.match(msg, /free space unknown/);
assert.equal(/NOT fit/.test(msg), false, "unknown is not a refusal");
});
@@ -0,0 +1,117 @@
import { statSync } from "node:fs";
import { readGlobalManifest } from "./cache.mjs";
import { readManifest } from "./manifest.mjs";
import { readMisses } from "./misses.mjs";
const TOP_MISSES = 5;
function emptyReport() {
return {
total_resolves: 0,
by_type: {},
by_source: {},
by_provider: {},
by_via: {},
misses: 0,
hit_rate: null,
top_missed_intents: {},
global_cache_assets: 0,
global_cache_disk_bytes: 0,
cross_project_reuse: 0,
};
}
function increment(map, key) {
if (!key) return;
map[key] = (map[key] || 0) + 1;
}
function timestampOf(record) {
return record?.ts || record?.timestamp || record?.created_at || record?.createdAt || null;
}
function inWindow(record, cutoff) {
if (!cutoff) return true;
const ts = timestampOf(record);
// Older manifest records may not carry a timestamp; keep them in the report
// because --days can only window records/misses that carry a ts/timestamp.
if (!ts) return true;
const time = Date.parse(ts);
return Number.isNaN(time) ? true : time >= cutoff;
}
function sourceOf(record) {
return record?._source || record?.source || record?.provenance?.source || "unknown";
}
function normalizeIntent(intent) {
return String(intent ?? "")
.trim()
.toLowerCase()
.replace(/\s+/g, " ");
}
function topMissedIntents(misses) {
const grouped = {};
for (const miss of misses) {
const type = miss?.type || "unknown";
const intent = normalizeIntent(miss?.intent);
if (!intent) continue;
grouped[type] ||= {};
grouped[type][intent] = (grouped[type][intent] || 0) + 1;
}
const out = {};
for (const [type, intents] of Object.entries(grouped)) {
out[type] = Object.entries(intents)
.map(([intent, count]) => ({ intent, count }))
.sort((a, b) => b.count - a.count || a.intent.localeCompare(b.intent))
.slice(0, TOP_MISSES);
}
return out;
}
function diskBytes(records) {
let total = 0;
for (const record of records) {
const p = record?.cached_path || record?.path;
if (!p) continue;
try {
total += statSync(p).size;
} catch {
// cache entries can outlive files; stats skips missing files
}
}
return total;
}
export function buildStats({ projectDir, days, now = Date.now() } = {}) {
// Only a positive finite --days windows the report; null / NaN / <= 0 mean
// "all time" rather than silently excluding everything (a negative cutoff
// would land in the future and drop every record). The reads below are each
// best-effort (they return [] / skip on IO errors), so there is no top-level
// catch masking a real logic bug as an all-zero "no usage" report.
const n = Number(days);
const cutoff = Number.isFinite(n) && n > 0 ? Number(now) - n * 24 * 60 * 60 * 1000 : null;
const records = (projectDir ? readManifest(projectDir) : []).filter((r) => inWindow(r, cutoff));
const misses = readMisses().filter((miss) => inWindow(miss, cutoff));
const globalRecords = readGlobalManifest();
const report = emptyReport();
report.total_resolves = records.length;
report.misses = misses.length;
for (const record of records) {
increment(report.by_type, record?.type || "unknown");
increment(report.by_source, sourceOf(record));
increment(report.by_provider, record?.provenance?.provider);
increment(report.by_via, record?.provenance?.via);
}
const attempts = report.total_resolves + report.misses;
report.hit_rate = attempts === 0 ? null : report.total_resolves / attempts;
report.top_missed_intents = topMissedIntents(misses);
report.global_cache_assets = globalRecords.length;
report.global_cache_disk_bytes = diskBytes(globalRecords);
report.cross_project_reuse = globalRecords.filter((r) => r?.provenance?.reused_by).length;
return report;
}
@@ -0,0 +1,165 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { buildStats } from "./stats.mjs";
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-stats-"));
const home = join(root, "home");
const projectDir = join(root, "project");
mkdirSync(home, { recursive: true });
mkdirSync(projectDir, { recursive: true });
process.env.HOME = home;
return { root, home, projectDir };
}
function restoreEnv(saved) {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
function seedManifest(dir, records) {
mkdirSync(join(dir, ".media"), { recursive: true });
writeFileSync(
join(dir, ".media/manifest.jsonl"),
records.map((record) => JSON.stringify(record)).join("\n") + "\n",
);
}
function isoDaysAgo(days) {
return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
}
test("buildStats aggregates resolves, misses, providers, sources, and reuse", () => {
const savedEnv = { ...process.env };
const { root, home, projectDir } = sandbox();
try {
seedManifest(projectDir, [
{
id: "bgm_001",
type: "bgm",
source: "search",
ts: isoDaysAgo(0),
provenance: { provider: "heygen.audio.sounds", prompt: "upbeat", via: "url" },
},
{
id: "image_001",
type: "image",
_source: "reused-explicit",
timestamp: isoDaysAgo(0),
provenance: { provider: "local", reused_by: "agent" },
},
{
id: "bgm_002",
type: "bgm",
source: "generated",
provenance: { provider: "codex" },
},
]);
const cachedFile = join(home, ".media/mu-v1-cache/asset.wav");
mkdirSync(join(cachedFile, ".."), { recursive: true });
writeFileSync(cachedFile, "12345");
seedManifest(home, [
{
id: "bgm_009",
type: "bgm",
reusable: true,
cached_path: cachedFile,
provenance: { provider: "heygen.audio.sounds", reused_by: "agent" },
},
]);
writeFileSync(
join(home, ".media/misses.jsonl"),
JSON.stringify({
ts: isoDaysAgo(0),
type: "bgm",
intent: "dark cinematic riser",
}) + "\n",
{ flag: "a" },
);
const stats = buildStats({ projectDir });
assert.equal(stats.total_resolves, 3);
assert.deepEqual(stats.by_type, { bgm: 2, image: 1 });
assert.deepEqual(stats.by_source, { search: 1, "reused-explicit": 1, generated: 1 });
assert.equal(stats.by_provider["heygen.audio.sounds"], 1);
assert.equal(stats.by_via.url, 1);
assert.equal(stats.misses, 1);
assert.equal(stats.hit_rate, 0.75);
assert.deepEqual(stats.top_missed_intents.bgm[0], {
intent: "dark cinematic riser",
count: 1,
});
assert.equal(stats.global_cache_assets, 1);
assert.equal(stats.global_cache_disk_bytes, 5);
assert.equal(stats.cross_project_reuse, 1);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
test("buildStats returns a zeroed report when nothing exists", () => {
const savedEnv = { ...process.env };
const { root, projectDir } = sandbox();
try {
const stats = buildStats({ projectDir });
assert.equal(stats.total_resolves, 0);
assert.deepEqual(stats.by_type, {});
assert.deepEqual(stats.by_source, {});
assert.deepEqual(stats.by_provider, {});
assert.deepEqual(stats.by_via, {});
assert.equal(stats.misses, 0);
assert.equal(stats.hit_rate, null);
assert.deepEqual(stats.top_missed_intents, {});
assert.equal(stats.global_cache_assets, 0);
assert.equal(stats.global_cache_disk_bytes, 0);
assert.equal(stats.cross_project_reuse, 0);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
test("buildStats returns JSON-safe output", () => {
const savedEnv = { ...process.env };
const { root, projectDir } = sandbox();
try {
const stats = buildStats({ projectDir });
assert.deepEqual(JSON.parse(JSON.stringify(stats)), stats);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
test("buildStats applies days window to timestamped records and misses", () => {
const savedEnv = { ...process.env };
const { root, home, projectDir } = sandbox();
try {
seedManifest(projectDir, [
{ id: "bgm_old", type: "bgm", ts: isoDaysAgo(100), provenance: { provider: "old" } },
{ id: "bgm_new", type: "bgm", ts: isoDaysAgo(1), provenance: { provider: "new" } },
{ id: "image_untimed", type: "image", provenance: { provider: "none" } },
]);
mkdirSync(join(home, ".media"), { recursive: true });
writeFileSync(
join(home, ".media/misses.jsonl"),
[
JSON.stringify({ ts: isoDaysAgo(100), type: "bgm", intent: "old miss" }),
JSON.stringify({ ts: isoDaysAgo(1), type: "bgm", intent: "new miss" }),
].join("\n"),
);
const stats = buildStats({ projectDir, days: 7 });
assert.equal(stats.total_resolves, 2);
assert.deepEqual(stats.by_type, { bgm: 1, image: 1 });
assert.equal(stats.misses, 1);
assert.equal(stats.top_missed_intents.bgm[0].intent, "new miss");
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
}
});
@@ -0,0 +1,222 @@
// Opt-out usage tracking for media-use, sharing the hyperframes CLI/studio
// identity (packages/cli/src/telemetry): the same install id from
// ~/.hyperframes/config.json, plus a $identify to the HeyGen account on sign-in,
// so a person is one PostHog profile across surfaces — not a fresh id per tool.
// Not fully anonymous by design (it must dedupe): pseudonymous before sign-in,
// account-linked after. Event PROPERTIES stay coarse — media TYPE, resolution
// SOURCE, winning PROVIDER — never the intent text, file names, or paths.
//
// Same public PostHog project key as the CLI (a write-only ingestion key, safe
// to ship), same opt-outs (DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY / CI / dev),
// and $ip:null so no IP is recorded. Fire-and-forget: telemetry never blocks a
// resolve and never throws into it.
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
const POSTHOG_API_KEY = "phc_zjjbX0PnWxERXrMHhkEJWj9A9BhGVLRReICgsfTMmpx";
const POSTHOG_HOST = "https://us.i.posthog.com";
const TIMEOUT_MS = 1500;
let identifiedAccount = false;
let warnedNonDefaultHost = false;
// Same CI/test signals the test suite itself sets (resolve.test.mjs's U7 test
// sets NODE_ENV=test and clears CI to prove the interception seam works) —
// reused here, not a new heuristic, so that deliberate test usage never
// triggers the warning below.
function isTestOrCiContext() {
return (
process.env.CI === "true" ||
process.env.CI === "1" ||
process.env.NODE_ENV === "test" ||
process.env.NODE_ENV === "development"
);
}
// Test-only interception seam: a real HTTP destination a test can point at,
// so a spawned-child test (resolve.test.mjs) can prove track() never reaches
// production rather than trusting DO_NOT_TRACK alone (a future call site or
// test could forget to set that env var). Falls back to the real production
// host whenever unset — production behavior is unchanged.
//
// Safety net: if this ever leaks into a real user's shell, track() would
// silently redirect to a likely-dead host and postBatch()'s catch{} would
// swallow the failure with zero signal. Surface one stderr warning outside
// test/CI contexts so a real user gets some indication instead of silence.
function posthogHost() {
const override = process.env.MEDIA_USE_TELEMETRY_HOST;
if (override && !warnedNonDefaultHost && !isTestOrCiContext()) {
warnedNonDefaultHost = true;
console.error(
`media-use: telemetry is redirected to a non-default host via MEDIA_USE_TELEMETRY_HOST (${override}) — unset it unless this is intentional.`,
);
}
return override || POSTHOG_HOST;
}
/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */
export function optedOut() {
return (
process.env.HYPERFRAMES_NO_TELEMETRY === "1" ||
process.env.DO_NOT_TRACK === "1" ||
process.env.CI === "true" ||
process.env.CI === "1" ||
process.env.NODE_ENV === "development"
);
}
// CLI + studio share one install identity in ~/.hyperframes/config.json
// (packages/cli/src/telemetry/config.ts — same path, same `anonymousId` /
// `telemetryNoticeShown` fields). Read and write that same file so media-use is
// the same PostHog person and shows the notice once per person, not per tool.
// Computed per call (not a module const) so it honors HOME at runtime — tests
// sandbox HOME, and os.homedir() re-reads it each call.
function sharedConfigPath() {
return join(homedir(), ".hyperframes", "config.json");
}
function readSharedConfig() {
try {
const file = sharedConfigPath();
if (existsSync(file)) {
const parsed = JSON.parse(readFileSync(file, "utf8"));
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
}
} catch {
// unreadable config → treat as empty; never throw
}
return {};
}
function writeSharedConfig(config) {
const dir = join(homedir(), ".hyperframes");
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "config.json"), JSON.stringify(config, null, 2) + "\n");
}
// Adopt a pre-existing media-use-only id (~/.media/anon-id from before this
// change) so upgraders keep their PostHog persona instead of resetting to a new
// one — otherwise cross-surface continuity would start over on upgrade.
function legacyMediaAnonId() {
try {
const file = join(homedir(), ".media", "anon-id");
if (existsSync(file)) {
const id = readFileSync(file, "utf8").trim();
if (id) return id;
}
} catch {
// ignore
}
return null;
}
// Stable per-machine id from the shared config; seeds it (adopting a legacy
// media-use id when present) if absent.
function anonymousId() {
try {
const config = readSharedConfig();
if (typeof config.anonymousId === "string" && config.anonymousId.trim()) {
return config.anonymousId.trim();
}
const id = legacyMediaAnonId() || randomUUID();
writeSharedConfig({ ...config, anonymousId: id });
return id;
} catch {
return "anon"; // best-effort; a shared bucket is fine if the fs is read-only
}
}
function heygenAccountDistinctId() {
const file = join(process.env.HEYGEN_CONFIG_DIR || join(homedir(), ".heygen"), "credentials");
try {
if (!existsSync(file)) return null;
const raw = readFileSync(file, "utf8").trim();
if (!raw.startsWith("{")) return null;
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const user = parsed.user;
if (!user || typeof user !== "object" || Array.isArray(user)) return null;
const id = typeof user.email === "string" && user.email.trim() ? user.email : user.username;
// Lowercased so this joins with the CLI's own identify call regardless of
// the account's stored email casing — two different-case distinct ids
// would otherwise split one person across two PostHog profiles.
return typeof id === "string" && id.trim() ? id.trim().toLowerCase() : null;
} catch {
return null;
}
}
function showTelemetryNotice() {
if (optedOut()) return;
try {
const config = readSharedConfig();
// Shared with the CLI (config.telemetryNoticeShown): shown once per person
// across surfaces, not once per tool.
if (config.telemetryNoticeShown === true) return;
console.error(
[
"media-use sends usage telemetry: media type, resolution source, and provider; never intent text, file names, or paths.",
"If you sign in to HeyGen, usage links to your account email or username. Opt out with HYPERFRAMES_NO_TELEMETRY=1 or DO_NOT_TRACK=1.",
].join("\n"),
);
writeSharedConfig({ ...config, telemetryNoticeShown: true });
} catch {
// notice is best-effort; never surface into the command
}
}
async function postBatch(batch) {
try {
await fetch(`${posthogHost()}/batch/`, {
method: "POST",
headers: { "Content-Type": "application/json", Connection: "close" },
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
signal: AbortSignal.timeout(TIMEOUT_MS),
});
} catch {
// telemetry is best-effort; never surface into the command
}
}
async function postEvent(event, properties, distinctId) {
await postBatch([
{
event,
properties: { ...properties, surface: "media-use", $ip: null },
distinct_id: distinctId,
timestamp: new Date().toISOString(),
},
]);
}
async function identifyAccount(anonId) {
if (optedOut() || identifiedAccount) return;
const distinctId = heygenAccountDistinctId();
if (!distinctId) return;
identifiedAccount = true;
await postEvent("$identify", { $anon_distinct_id: anonId }, distinctId);
}
/**
* Fire-and-forget a single event to PostHog. Best-effort: awaited with a short
* timeout so a short-lived script flushes before exit, but any failure (offline,
* opted out) is swallowed. `properties` must be non-PII (no intent/paths).
*/
export async function track(event, properties = {}) {
if (optedOut()) return;
showTelemetryNotice();
const anonId = anonymousId();
await identifyAccount(anonId);
await postEvent(event, properties, anonId);
}
export function __anonymousIdForTest() {
return anonymousId();
}
export function __resetTelemetryForTest() {
identifiedAccount = false;
warnedNonDefaultHost = false;
}
@@ -0,0 +1,373 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
chmodSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { __anonymousIdForTest, __resetTelemetryForTest, optedOut, track } from "./telemetry.mjs";
function sandbox() {
const root = mkdtempSync(join(tmpdir(), "mu-telemetry-"));
const home = join(root, "home");
mkdirSync(home, { recursive: true });
process.env.HOME = home;
return { root, home };
}
function restoreEnv(saved) {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
function withoutTelemetryOptOut() {
for (const k of ["DO_NOT_TRACK", "HYPERFRAMES_NO_TELEMETRY", "CI", "NODE_ENV"])
delete process.env[k];
}
function parseFetchBodies(calls) {
return calls.flatMap((call) => JSON.parse(call.options.body).batch);
}
test("optedOut respects DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY / CI", () => {
const saved = { ...process.env };
try {
for (const k of ["DO_NOT_TRACK", "HYPERFRAMES_NO_TELEMETRY", "CI", "NODE_ENV"])
delete process.env[k];
assert.equal(optedOut(), false, "default: tracking allowed");
process.env.DO_NOT_TRACK = "1";
assert.equal(optedOut(), true, "DO_NOT_TRACK opts out");
delete process.env.DO_NOT_TRACK;
process.env.HYPERFRAMES_NO_TELEMETRY = "1";
assert.equal(optedOut(), true, "HYPERFRAMES_NO_TELEMETRY opts out");
delete process.env.HYPERFRAMES_NO_TELEMETRY;
process.env.CI = "true";
assert.equal(optedOut(), true, "CI opts out");
} finally {
for (const k of Object.keys(process.env)) if (!(k in saved)) delete process.env[k];
Object.assign(process.env, saved);
}
});
test("track is a no-op (no network, resolves) when opted out", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const { root, home } = sandbox();
const calls = [];
globalThis.fetch = async (...args) => {
calls.push(args);
return { ok: true };
};
process.env.DO_NOT_TRACK = "1";
try {
// must resolve immediately without throwing or hitting the network
await track("media_use_resolve", { type: "bgm", source: "search" });
assert.equal(calls.length, 0);
assert.equal(existsSync(join(home, ".hyperframes/config.json")), false);
assert.equal(existsSync(join(home, ".media/telemetry-notice-shown")), false);
} finally {
globalThis.fetch = originalFetch;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("anonymous id uses the shared hyperframes config", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
withoutTelemetryOptOut();
mkdirSync(join(home, ".hyperframes"), { recursive: true });
writeFileSync(
join(home, ".hyperframes/config.json"),
JSON.stringify({ anonymousId: "shared-install-id", keep: true }),
);
mkdirSync(join(home, ".media"), { recursive: true });
writeFileSync(join(home, ".media/anon-id"), "old-media-id");
assert.equal(__anonymousIdForTest(), "shared-install-id");
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("anonymous id seeds missing config once and reuses it", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
withoutTelemetryOptOut();
const first = __anonymousIdForTest();
const configPath = join(home, ".hyperframes/config.json");
assert.ok(existsSync(configPath));
assert.match(
first,
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
const second = __anonymousIdForTest();
assert.equal(second, first);
assert.equal(JSON.parse(readFileSync(configPath, "utf8")).anonymousId, first);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("anonymous id adopts a legacy ~/.media/anon-id on upgrade (persona continuity)", () => {
const savedEnv = { ...process.env };
const { root, home } = sandbox();
try {
withoutTelemetryOptOut();
mkdirSync(join(home, ".media"), { recursive: true });
writeFileSync(join(home, ".media/anon-id"), "legacy-media-id");
// no ~/.hyperframes/config.json yet — the old media-use-only id must carry over
assert.equal(__anonymousIdForTest(), "legacy-media-id");
// and it is persisted into the shared config so CLI/studio see the same id
assert.equal(
JSON.parse(readFileSync(join(home, ".hyperframes/config.json"), "utf8")).anonymousId,
"legacy-media-id",
);
} finally {
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("track identifies a signed-in HeyGen account once and still sends events", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const { root, home } = sandbox();
const calls = [];
globalThis.fetch = async (url, options) => {
calls.push({ url, options });
return { ok: true };
};
try {
withoutTelemetryOptOut();
mkdirSync(join(home, ".hyperframes"), { recursive: true });
writeFileSync(
join(home, ".hyperframes/config.json"),
JSON.stringify({ anonymousId: "anon-1" }),
);
mkdirSync(join(home, ".heygen"), { recursive: true });
writeFileSync(
join(home, ".heygen/credentials"),
JSON.stringify({ user: { email: "alice@example.com", username: "alice" } }),
);
await track("media_use_resolve", { type: "bgm", source: "search" });
await track("media_use_resolve", { type: "image", source: "generated" });
const batch = parseFetchBodies(calls);
const identify = batch.filter((item) => item.event === "$identify");
assert.equal(identify.length, 1);
assert.equal(identify[0].distinct_id, "alice@example.com");
assert.equal(identify[0].properties.$anon_distinct_id, "anon-1");
assert.equal(batch.filter((item) => item.event === "media_use_resolve").length, 2);
} finally {
globalThis.fetch = originalFetch;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("track identifies with a lowercased email regardless of stored casing", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const { root, home } = sandbox();
const calls = [];
globalThis.fetch = async (url, options) => {
calls.push({ url, options });
return { ok: true };
};
try {
withoutTelemetryOptOut();
mkdirSync(join(home, ".hyperframes"), { recursive: true });
writeFileSync(
join(home, ".hyperframes/config.json"),
JSON.stringify({ anonymousId: "anon-3" }),
);
mkdirSync(join(home, ".heygen"), { recursive: true });
writeFileSync(
join(home, ".heygen/credentials"),
JSON.stringify({ user: { email: "Alice@Example.com", username: "alice" } }),
);
await track("media_use_resolve", { type: "bgm", source: "search" });
const batch = parseFetchBodies(calls);
const identify = batch.filter((item) => item.event === "$identify");
assert.equal(identify.length, 1);
// Lowercased so this joins with heygen-cli's own identify call regardless
// of the account's stored email casing -- otherwise the same person could
// split into two PostHog profiles by email case alone.
assert.equal(identify[0].distinct_id, "alice@example.com");
} finally {
globalThis.fetch = originalFetch;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("track does not identify when signed out", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const { root, home } = sandbox();
const calls = [];
globalThis.fetch = async (url, options) => {
calls.push({ url, options });
return { ok: true };
};
try {
withoutTelemetryOptOut();
mkdirSync(join(home, ".hyperframes"), { recursive: true });
writeFileSync(
join(home, ".hyperframes/config.json"),
JSON.stringify({ anonymousId: "anon-2" }),
);
await track("media_use_resolve", { type: "bgm", source: "search" });
const batch = parseFetchBodies(calls);
assert.equal(
batch.some((item) => item.event === "$identify"),
false,
);
assert.equal(batch[0].event, "media_use_resolve");
assert.equal(batch[0].distinct_id, "anon-2");
} finally {
globalThis.fetch = originalFetch;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("first run notice prints to stderr once and never stdout", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const originalError = console.error;
const originalLog = console.log;
const { root, home } = sandbox();
const stderr = [];
const stdout = [];
globalThis.fetch = async () => ({ ok: true });
console.error = (...args) => stderr.push(args.join(" "));
console.log = (...args) => stdout.push(args.join(" "));
try {
withoutTelemetryOptOut();
await track("media_use_resolve", { type: "bgm" });
await track("media_use_resolve", { type: "sfx" });
assert.equal(stderr.length, 1);
assert.match(stderr[0], /media-use sends usage telemetry/);
assert.equal(stdout.length, 0);
// notice-shown lives in the shared config (config.telemetryNoticeShown), so
// the CLI and media-use show it once per person — not a media-use-only marker.
assert.equal(
JSON.parse(readFileSync(join(home, ".hyperframes/config.json"), "utf8")).telemetryNoticeShown,
true,
);
} finally {
globalThis.fetch = originalFetch;
console.error = originalError;
console.log = originalLog;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("warns once on stderr when MEDIA_USE_TELEMETRY_HOST is set outside a test/CI context", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const originalError = console.error;
const { root } = sandbox();
const stderr = [];
globalThis.fetch = async () => ({ ok: true });
console.error = (...args) => stderr.push(args.join(" "));
try {
// No opt-out, no CI/NODE_ENV signal — mirrors a real user's shell where
// this test-only var leaked in by accident and tracking is not disabled.
withoutTelemetryOptOut();
process.env.MEDIA_USE_TELEMETRY_HOST = "http://127.0.0.1:1"; // nothing listens; irrelevant here
await track("media_use_resolve", { type: "bgm" });
await track("media_use_resolve", { type: "bgm" });
const warnings = stderr.filter((line) => line.includes("MEDIA_USE_TELEMETRY_HOST"));
assert.equal(warnings.length, 1, "expected exactly one warning across repeated calls");
} finally {
globalThis.fetch = originalFetch;
console.error = originalError;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("does not warn when MEDIA_USE_TELEMETRY_HOST is set the way the U7 interception test sets it", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const originalError = console.error;
const { root } = sandbox();
const stderr = [];
globalThis.fetch = async () => ({ ok: true });
console.error = (...args) => stderr.push(args.join(" "));
try {
withoutTelemetryOptOut();
// Same env shape resolve.test.mjs's U7 test uses to allow tracking through:
// DO_NOT_TRACK=0, CI unset/empty, NODE_ENV=test.
process.env.DO_NOT_TRACK = "0";
process.env.CI = "";
process.env.NODE_ENV = "test";
process.env.MEDIA_USE_TELEMETRY_HOST = "http://127.0.0.1:1";
await track("media_use_resolve", { type: "bgm" });
assert.equal(
stderr.some((line) => line.includes("MEDIA_USE_TELEMETRY_HOST")),
false,
"the U7 test's own use of the override must not print a spurious warning",
);
} finally {
globalThis.fetch = originalFetch;
console.error = originalError;
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
test("read-only telemetry state degrades without throwing", async () => {
const savedEnv = { ...process.env };
const originalFetch = globalThis.fetch;
const { root, home } = sandbox();
globalThis.fetch = async () => ({ ok: true });
try {
withoutTelemetryOptOut();
writeFileSync(join(home, ".hyperframes"), "not a directory");
writeFileSync(join(home, ".media"), "not a directory");
await track("media_use_resolve", { type: "bgm" });
} finally {
globalThis.fetch = originalFetch;
try {
chmodSync(join(home, ".hyperframes"), 0o600);
} catch {
// best effort for cleanup on platforms with different chmod behavior
}
restoreEnv(savedEnv);
rmSync(root, { recursive: true, force: true });
__resetTelemetryForTest();
}
});
@@ -0,0 +1,92 @@
import { execFileSync } from "node:child_process";
import { existsSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { resolveSpawnCommand } from "../../audio/scripts/lib/tts.mjs";
// Local voiceover via the packaged Kokoro-82M TTS (the `hyperframes tts` CLI),
// the free/private default now that HeyGen TTS costs wallet credits. Kokoro runs
// on-device (CPU, faster-than-realtime, bundled voices, native word timestamps),
// so no key and no per-call charge. When Kokoro is not set up, this returns null
// and the registry falls through to the HeyGen TTS upsell.
//
// Delegated to the hyperframes CLI (same as transcribe / remove-background), not
// re-implemented here. ffprobe reads the duration back for the ledger.
function probeDurationSeconds(file) {
try {
const out = execFileSync(
"ffprobe",
["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", "--", file],
{ encoding: "utf8", timeout: 15000 },
);
const d = parseFloat(String(out).trim());
return Number.isFinite(d) ? d : undefined;
} catch {
return undefined;
}
}
// `platform`/`execFn`/`env`/`pathExists` params (defaulting to the real
// values) exist so tests can exercise the win32 branch without mocking
// node:child_process (its ESM exports are non-configurable) — same idiom as
// spawnP in ../../audio/scripts/lib/tts.mjs.
export async function localTtsGenerate(
intent,
ctx,
platform = process.platform,
execFn = execFileSync,
env = process.env,
pathExists = existsSync,
) {
const outPath = join(tmpdir(), `media-use-kokoro-${process.pid}-${Date.now()}.wav`);
const argv = ["hyperframes", "tts", intent, "--output", outPath];
if (ctx?.voice) argv.push("--voice", ctx.voice);
if (ctx?.lang && ctx.lang !== "en") argv.push("--lang", ctx.lang);
// On Windows a bare "npx" is npx.cmd, which execFileSync cannot exec
// (spawnSync npx ENOENT) — resolveSpawnCommand reroutes it through
// node + npx-cli.js, same as the audio engine's TTS spawns.
const resolved = resolveSpawnCommand(
"npx",
argv,
{ encoding: "utf8", timeout: 300000, stdio: ["ignore", "pipe", "pipe"] },
platform,
env,
pathExists,
);
if (!resolved) {
// npx-on-win32 with no resolvable npx-cli.js — same terminal condition
// spawnP warns about. Fall through to the next provider rather than crash.
console.error(
"media-use: local voice not enabled (kokoro). Cannot run npx on Windows: " +
"npm's npx-cli.js was not found (install npm with Node, or run via npx/npm run so npm_execpath is set).",
);
return null;
}
try {
execFn(resolved.cmd, resolved.args, resolved.opts);
} catch (err) {
// `hyperframes tts` prints its "kokoro-onnx not installed" hint to stdout
// (clack UI), so read both streams and surface the actionable enable-command
// rather than a bare "Command failed": otherwise resolve silently falls
// through to the PAID HeyGen TTS upsell when free local voice was one pip away.
const out = `${err.stdout?.toString() ?? ""}${err.stderr?.toString() ?? ""}`.trim();
const hint = /not installed|pip install kokoro/i.test(out)
? "install for free on-device voice: pip install kokoro-onnx soundfile (or set HYPERFRAMES_PYTHON to a venv that has it)"
: out.slice(-200) || err.message;
console.error(`media-use: local voice not enabled (kokoro). ${hint}`);
return null;
}
if (!existsSync(outPath) || statSync(outPath).size === 0) return null;
return {
localPath: outPath,
ext: ".wav",
source: "generated",
metadata: {
description: intent,
provider: "kokoro.local",
duration: probeDurationSeconds(outPath),
provenance: { engine: "kokoro-82m", prompt: intent },
},
};
}
@@ -0,0 +1,72 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { localTtsGenerate } from "./tts-local-provider.mjs";
// Regression: on Windows, a bare "npx" is npx.cmd, which execFileSync cannot
// exec — the Kokoro delegation failed with `spawnSync npx ENOENT` instead of
// synthesizing (or cleanly falling through to the next provider). The spawn
// must route through node + npx-cli.js on win32, same as the audio engine's
// TTS spawns (see ../../audio/scripts/lib/tts.spawn.test.mjs).
const envWithNpxCli = {
npm_execpath: "C:/Program Files/nodejs/node_modules/npm/bin/npm-cli.js",
npm_node_execpath: "C:/Program Files/nodejs/node.exe",
};
const npxCliPath = "C:/Program Files/nodejs/node_modules/npm/bin/npx-cli.js";
const pathExists = (path) => path === npxCliPath;
test("win32: routes the hyperframes tts call through node + npx-cli, never bare npx", async () => {
const captured = [];
const fakeExec = (cmd, args, opts) => {
captured.push({ cmd, args, opts });
// Synthesize nothing — the provider then returns null via the
// missing-output check, which is fine: we only assert the spawn shape.
};
await localTtsGenerate(
"hello there",
{ voice: "am_michael" },
"win32",
fakeExec,
envWithNpxCli,
pathExists,
);
assert.equal(captured.length, 1);
assert.equal(captured[0].cmd, envWithNpxCli.npm_node_execpath);
assert.equal(captured[0].args[0], npxCliPath);
assert.deepEqual(captured[0].args.slice(1, 4), ["hyperframes", "tts", "hello there"]);
assert.ok(captured[0].args.includes("--voice"));
// execFileSync options survive the rerouting (pipes are what let the caller
// read the "kokoro-onnx not installed" hint back out).
assert.deepEqual(captured[0].opts.stdio, ["ignore", "pipe", "pipe"]);
});
test("win32 without a resolvable npx-cli: falls through to the next provider (null), no spawn", async () => {
const captured = [];
const fakeExec = (...call) => captured.push(call);
const result = await localTtsGenerate(
"hello",
{},
"win32",
fakeExec,
{}, // no npm_execpath, and pathExists finds nothing
() => false,
);
assert.equal(result, null);
assert.equal(captured.length, 0);
});
test("non-win32: spawns plain npx unchanged", async () => {
const captured = [];
const fakeExec = (cmd, args) => captured.push({ cmd, args });
await localTtsGenerate("hola", { lang: "es" }, "darwin", fakeExec, {}, () => false);
assert.equal(captured.length, 1);
assert.equal(captured[0].cmd, "npx");
assert.deepEqual(captured[0].args.slice(0, 3), ["hyperframes", "tts", "hola"]);
assert.ok(captured[0].args.includes("--lang"));
});
@@ -0,0 +1,57 @@
import { readFileSync, readdirSync, existsSync } from "node:fs";
import { join } from "node:path";
// Asset in-use detection: which manifest assets are actually referenced by the
// project's compositions. Answers "is this safe to prune?" from the CLI. (The
// Studio Asset tab computes its in-use filter separately from the live timeline;
// this is the skill-side equivalent for headless use.)
//
// ponytail: substring match of each asset's filename against the .html text
// (covers src= / href= / url() / data-* without parsing HTML). False positives
// only if a filename literally appears in prose; fine for a filter. Upgrade to
// attribute parsing if that ever bites.
function compositionHtml(projectDir) {
let files = [];
try {
files = readdirSync(projectDir).filter((f) => f.endsWith(".html"));
} catch {
return "";
}
const sub = join(projectDir, "compositions");
if (existsSync(sub)) {
try {
for (const f of readdirSync(sub))
if (f.endsWith(".html")) files.push(join("compositions", f));
} catch {
// ignore unreadable subdir
}
}
return files
.map((f) => {
try {
return readFileSync(join(projectDir, f), "utf8");
} catch {
return "";
}
})
.join("\n");
}
/** Tag each record with `inUse` (referenced by some composition). */
export function tagUsage(records, projectDir) {
const html = compositionHtml(projectDir);
return records.map((r) => {
const file = r.path ? r.path.split("/").pop() : r.id;
return { ...r, inUse: Boolean(file) && html.includes(file) };
});
}
/** Split records into { used, unused } for the filter. */
export function partitionUsage(records, projectDir) {
const tagged = tagUsage(records, projectDir);
return {
used: tagged.filter((r) => r.inUse),
unused: tagged.filter((r) => !r.inUse),
};
}
@@ -0,0 +1,54 @@
import { strict as assert } from "node:assert";
import { test } from "node:test";
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { tagUsage, partitionUsage } from "./usage.mjs";
function project(html) {
const dir = mkdtempSync(join(tmpdir(), "mu-usage-"));
writeFileSync(join(dir, "index.html"), html);
return dir;
}
const records = [
{ id: "bgm_001", path: ".media/audio/bgm/bgm_001.wav", description: "used track" },
{ id: "bgm_002", path: ".media/audio/bgm/bgm_002.wav", description: "orphan track" },
];
test("an asset referenced by a composition is marked in-use", () => {
const dir = project(`<audio src="assets/bgm_001.wav"></audio>`);
try {
const tagged = tagUsage(records, dir);
assert.equal(tagged.find((r) => r.id === "bgm_001").inUse, true);
assert.equal(tagged.find((r) => r.id === "bgm_002").inUse, false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("partitionUsage splits used vs unused for the filter", () => {
const dir = project(`<img src="bgm_001.wav">`);
try {
const { used, unused } = partitionUsage(records, dir);
assert.deepEqual(
used.map((r) => r.id),
["bgm_001"],
);
assert.deepEqual(
unused.map((r) => r.id),
["bgm_002"],
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test("no compositions -> everything reads as unused (safe default)", () => {
const dir = mkdtempSync(join(tmpdir(), "mu-usage-"));
try {
assert.equal(partitionUsage(records, dir).used.length, 0);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,57 @@
import { HEYGEN_CLIENT_SOURCE_ARGV, runHeygenJson } from "./heygen-cli.mjs";
// Voice / TTS generation via the HeyGen CLI — the only external CLI media-use
// shells (CLI-only invariant: media-use holds no keys; the CLI owns auth).
// Flags verified against `heygen voice speech create --help` (v0.3.0).
function result(url, duration, provider, intent) {
if (!url) return null;
return {
url,
source: "generated",
metadata: {
description: intent,
provider,
...(duration != null && { duration }),
provenance: { prompt: intent },
},
};
}
// HeyGen TTS requires a starfish-engine voice. Default to the first one the
// catalog returns (deterministic order); pass ctx.voiceId to override.
// ponytail: listed once per process; the resolved asset is frozen + cached after
// first use, so the network list only happens on a cache miss. Cache only a
// truthy id -- a transient list failure must not poison the cache with `null`
// and permanently disable TTS for the rest of the process.
let cachedVoiceId;
function defaultVoiceId() {
if (cachedVoiceId) return cachedVoiceId;
const j = runHeygenJson(
"heygen",
["voice", "list", "--engine", "starfish", "--limit", "1"],
"voice list",
);
cachedVoiceId = j?.data?.[0]?.voice_id || null;
return cachedVoiceId;
}
export async function heygenTtsGenerate(intent, ctx) {
const voiceId = ctx?.voiceId || defaultVoiceId();
if (!voiceId) return null;
const p = runHeygenJson(
"heygen",
[
...HEYGEN_CLIENT_SOURCE_ARGV,
"voice",
"speech",
"create",
"--text",
intent,
"--voice-id",
voiceId,
],
"tts",
);
return result(p?.data?.audio_url, p?.data?.duration, "heygen.tts", intent);
}
@@ -0,0 +1,46 @@
import { strict as assert } from "node:assert";
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { heygenTtsGenerate } from "./voice-provider.mjs";
test("tags TTS generation but not voice discovery with the media-use client source", async () => {
const dir = mkdtempSync(join(tmpdir(), "media-use-voice-provider-"));
const capturePath = join(dir, "argv.log");
const heygenPath = join(dir, "heygen");
const previousPath = process.env.PATH;
const previousCapturePath = process.env.HEYGEN_CAPTURE_PATH;
writeFileSync(
heygenPath,
`#!/bin/sh
printf '%s\\n' "$*" >> "$HEYGEN_CAPTURE_PATH"
case "$*" in
*"voice list"*) printf '%s\\n' '{"data":[{"voice_id":"voice-123"}]}' ;;
*"voice speech create"*) printf '%s\\n' '{"data":{"audio_url":"https://example.com/voice.mp3","duration":1.5}}' ;;
esac
`,
);
chmodSync(heygenPath, 0o755);
process.env.PATH = `${dir}:${previousPath ?? ""}`;
process.env.HEYGEN_CAPTURE_PATH = capturePath;
try {
const result = await heygenTtsGenerate("Hello from media-use", {});
const invocations = readFileSync(capturePath, "utf8").trim().split("\n");
assert.equal(invocations.length, 2);
assert.match(invocations[0], /^voice list /);
assert.doesNotMatch(invocations[0], /X-HeyGen-Client-Source/);
assert.match(invocations[1], /voice speech create/);
assert.match(invocations[1], /X-HeyGen-Client-Source: media-use/);
assert.equal(result?.url, "https://example.com/voice.mp3");
} finally {
if (previousPath === undefined) delete process.env.PATH;
else process.env.PATH = previousPath;
if (previousCapturePath === undefined) delete process.env.HEYGEN_CAPTURE_PATH;
else process.env.HEYGEN_CAPTURE_PATH = previousCapturePath;
rmSync(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,18 @@
export function normalizeWords(input) {
const raw = Array.isArray(input) ? input : Array.isArray(input?.words) ? input.words : [];
return raw
.map((w, index) => {
const text = String(w?.text ?? w?.word ?? "").trim();
const start = Number(w?.start);
const end = Number(w?.end);
if (!text || !Number.isFinite(start) || !Number.isFinite(end)) return null;
return { id: w?.id ?? `w${index}`, text, start, end };
})
.filter(Boolean);
}
export function wordListsFromMediaMeta(input) {
if (Array.isArray(input) || Array.isArray(input?.words)) return [normalizeWords(input)];
if (!Array.isArray(input?.voices)) return [];
return input.voices.map((voice) => normalizeWords(voice)).filter((words) => words.length > 0);
}