Add HyperFrames skills (from heygen-com/hyperframes, Apache-2.0)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FXVsD1DgdzZLiPuwtKzMSY
This commit is contained in:
300 files changed
+49175
No files matched your search
@@ -0,0 +1,196 @@
|
||||
// sfx.mjs — sound effects for the media audio engine. Each cue is searched on
|
||||
// Openverse (CC0 / CC BY only, see openverse.mjs). A cue Openverse cannot answer,
|
||||
// and every cue after Openverse is unreachable or rate limited, resolves against
|
||||
// the bundled 21-file library (assets/sfx/manifest.json) instead.
|
||||
//
|
||||
// A cue that matches nothing is skipped (recorded as an anomaly); SFX never
|
||||
// blocks a render. Every cue sits at volume ~0.35, under voice + BGM.
|
||||
|
||||
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { downloadTo } from "./heygen.mjs";
|
||||
import { OpenverseUnavailableError, searchOpenverseSfx, writeSfxCredit } from "./openverse.mjs";
|
||||
import { latestRecordFor } from "../../../scripts/lib/manifest.mjs";
|
||||
import { agentWritePath } from "./media-record.mjs";
|
||||
|
||||
export const BUNDLED_SFX_DIR = join(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"..",
|
||||
"..",
|
||||
"assets",
|
||||
"sfx",
|
||||
);
|
||||
const SFX_VOLUME = 0.35;
|
||||
const slug = (s) =>
|
||||
s
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.slice(0, 40) || "x";
|
||||
const r3 = (x) => Number(x.toFixed(3));
|
||||
|
||||
// Lookups by manifest key, file basename, and slug of either, so a cue can name
|
||||
// "whoosh", "whoosh.mp3", or "ui click" (→ slug match). { error } when unusable.
|
||||
function loadBundledLibrary(sfxLibDir) {
|
||||
const manifestPath = join(sfxLibDir, "manifest.json");
|
||||
if (!existsSync(manifestPath)) return { error: `no bundled SFX library at ${sfxLibDir}` };
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
||||
} catch (e) {
|
||||
return { error: `bundled SFX manifest parse failed (${e.message})` };
|
||||
}
|
||||
const byKey = new Map();
|
||||
for (const [key, entry] of Object.entries(manifest)) {
|
||||
if (!entry?.file || !isFinite(entry.duration)) continue;
|
||||
const rec = { key, file: entry.file, duration: entry.duration };
|
||||
byKey.set(key, rec);
|
||||
byKey.set(entry.file, rec);
|
||||
byKey.set(slug(key), rec);
|
||||
byKey.set(slug(entry.file.replace(/\.\w+$/, "")), rec);
|
||||
}
|
||||
return { byKey };
|
||||
}
|
||||
|
||||
// cues: [{ id, name }] (id = the line/frame/scene the cue fires in). Returns
|
||||
// { sfx: [{ id, name, file, source, offset_s, duration_s, volume, license?, credit? }], anomalies }.
|
||||
export async function resolveSfx({ cues, hyperframesDir, sfxLibDir = BUNDLED_SFX_DIR }) {
|
||||
const sfx = [];
|
||||
const anomalies = [];
|
||||
const destDir = join(hyperframesDir, "assets", "sfx");
|
||||
// Each effect's file this run: one effect named twice gets one file, and two effects never share one.
|
||||
const fileFor = new Map();
|
||||
const taken = () => new Set(fileFor.values());
|
||||
// One Openverse sound per effect name this run, so a repeated cue costs no second request.
|
||||
const openverseFor = new Map();
|
||||
let openverseDown = false;
|
||||
let library;
|
||||
|
||||
// Dedupe identical (id,name) cues — the same effect named twice in one line
|
||||
// downloads/copies once.
|
||||
const seen = new Set();
|
||||
const uniq = cues.filter((c) => {
|
||||
const k = `${c.id}:${c.name}`;
|
||||
if (seen.has(k)) return false;
|
||||
seen.add(k);
|
||||
return true;
|
||||
});
|
||||
|
||||
async function fromOpenverse({ id, name }) {
|
||||
const key = `openverse:${slug(name)}`;
|
||||
let entry = openverseFor.get(key);
|
||||
if (!entry) {
|
||||
const sound = await searchOpenverseSfx(name);
|
||||
if (!sound) return null;
|
||||
const file = agentWritePath(hyperframesDir, `assets/sfx/${slug(name)}${sound.ext}`, {
|
||||
anomalies,
|
||||
taken: taken(),
|
||||
});
|
||||
try {
|
||||
await downloadTo(sound.url, join(hyperframesDir, file));
|
||||
} catch (e) {
|
||||
anomalies.push(`sfx "${name}" (id ${id}): Openverse download failed — ${e.message}`);
|
||||
return null;
|
||||
}
|
||||
fileFor.set(key, file);
|
||||
writeSfxCredit(join(destDir, "CREDITS.md"), file, sound);
|
||||
entry = {
|
||||
file,
|
||||
source: "openverse",
|
||||
duration_s: r3(sound.durationS),
|
||||
license: sound.license,
|
||||
credit: sound.credit,
|
||||
};
|
||||
openverseFor.set(key, entry);
|
||||
}
|
||||
return { id, name, ...entry, offset_s: 0, volume: SFX_VOLUME };
|
||||
}
|
||||
|
||||
function fromBundled({ id, name }) {
|
||||
library ??= loadBundledLibrary(sfxLibDir);
|
||||
if (library.error) {
|
||||
anomalies.push(`sfx "${name}" (id ${id}): ${library.error} — skipped`);
|
||||
return null;
|
||||
}
|
||||
const { byKey } = library;
|
||||
const hit = byKey.get(name) ?? byKey.get(slug(name));
|
||||
if (!hit) {
|
||||
const known = [...new Set([...byKey.values()].map((v) => v.key))].slice(0, 8).join(", ");
|
||||
anomalies.push(
|
||||
`sfx "${name}" (id ${id}): not in bundled library — skipped (have: ${known}…)`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
mkdirSync(destDir, { recursive: true });
|
||||
const src = join(sfxLibDir, hit.file);
|
||||
const bytes = existsSync(src) ? readFileSync(src) : null;
|
||||
const isLibraryCopy = (rel) =>
|
||||
bytes &&
|
||||
existsSync(join(hyperframesDir, rel)) &&
|
||||
readFileSync(join(hyperframesDir, rel)).equals(bytes);
|
||||
const destRel =
|
||||
fileFor.get(hit.file) ??
|
||||
agentWritePath(hyperframesDir, `assets/sfx/${hit.file}`, {
|
||||
anomalies,
|
||||
taken: taken(),
|
||||
// An unrecorded copy of the library file is one an engine run made before the manifest had it.
|
||||
reusable: (rel) => !latestRecordFor(hyperframesDir, rel) && isLibraryCopy(rel),
|
||||
});
|
||||
const dest = join(hyperframesDir, destRel);
|
||||
// Some installs of the skill ship manifest.json without the mp3s. An entry
|
||||
// pointing at a file we never copied would silently drop downstream, so
|
||||
// skip the cue with a loud anomaly instead.
|
||||
if (bytes) {
|
||||
if (!isLibraryCopy(destRel)) copyFileSync(src, dest);
|
||||
} else if (!existsSync(dest)) {
|
||||
anomalies.push(
|
||||
`sfx "${name}" (id ${id}): bundled file ${hit.file} missing from the offline ` +
|
||||
`library (${sfxLibDir}) — skipped. Reinstall the media-use skill to ` +
|
||||
`restore assets/sfx/*.mp3.`,
|
||||
);
|
||||
return null;
|
||||
} // else the engine's earlier copy at dest stands in for the file this install lacks
|
||||
fileFor.set(hit.file, destRel);
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
file: destRel,
|
||||
source: "local",
|
||||
offset_s: 0,
|
||||
duration_s: r3(hit.duration),
|
||||
volume: SFX_VOLUME,
|
||||
};
|
||||
}
|
||||
|
||||
for (const cue of uniq) {
|
||||
let entry = null;
|
||||
if (!openverseDown) {
|
||||
try {
|
||||
entry = await fromOpenverse(cue);
|
||||
} catch (e) {
|
||||
if (!(e instanceof OpenverseUnavailableError)) throw e;
|
||||
openverseDown = true;
|
||||
anomalies.push(`${e.message}: the remaining SFX cues use the bundled library`);
|
||||
}
|
||||
}
|
||||
entry ??= fromBundled(cue);
|
||||
if (entry) sfx.push(entry);
|
||||
}
|
||||
return { sfx, anomalies };
|
||||
}
|
||||
|
||||
/**
|
||||
* One sound effect for `query`, saved under the project's assets/sfx/ (the entry point for hosts).
|
||||
* { sound: { file, source, duration_s, license?, credit? } | null, anomalies }.
|
||||
*/
|
||||
export async function findSoundEffect(query, { hyperframesDir, sfxLibDir = BUNDLED_SFX_DIR }) {
|
||||
const { sfx, anomalies } = await resolveSfx({
|
||||
cues: [{ id: "1", name: query }],
|
||||
hyperframesDir,
|
||||
sfxLibDir,
|
||||
});
|
||||
if (!sfx.length) return { sound: null, anomalies };
|
||||
const { file, source, duration_s, license, credit } = sfx[0];
|
||||
return { sound: { file, source, duration_s, ...(license && { license, credit }) }, anomalies };
|
||||
}
|
||||
Reference in new issue
Block a user