hyperframes 0.8.168 → 0.8.170
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-VCX3UE2B.js → add-KDCZCEMQ.js} +11 -11
- package/dist/{animatedGifPrep-TWD5BNVK.js → animatedGifPrep-MVP5LCEM.js} +5 -5
- package/dist/api.js +11 -11
- package/dist/{asset-BBJ3GAL7.js → asset-6VH3OCEC.js} +2 -2
- package/dist/{assetCataloger-S47GINRQ.js → assetCataloger-RN5A2SVA.js} +5 -5
- package/dist/{audio-MEDCANA3.js → audio-636CQSXK.js} +11 -11
- package/dist/{auth-LZYMCNTB.js → auth-AHLKM4M2.js} +1 -1
- package/dist/{autoUpdate-AXTW2NKX.js → autoUpdate-5UA6TJW7.js} +2 -2
- package/dist/{backgroundChecks-3YDYYI3D.js → backgroundChecks-DP242536.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-UDE7TOKY.js → batchRender-KWBVAUE3.js} +3 -3
- package/dist/{beats-HHXMPMGT.js → beats-HKY5L463.js} +4 -4
- package/dist/{benchmark-2BY7PBAP.js → benchmark-4YQDVIO5.js} +8 -8
- package/dist/{browser-CXB4KTJP.js → browser-7ZPWXEYW.js} +3 -3
- package/dist/{bundleWithLocalizedFonts-M7CRHOS7.js → bundleWithLocalizedFonts-42V34HJF.js} +2 -2
- package/dist/{capture-WHG37ZLF.js → capture-572X5NDD.js} +9 -9
- package/dist/{capture-PRTE4Z7E.js → capture-VXMUZSBI.js} +2 -2
- package/dist/{catalog-WRM2373Z.js → catalog-PUUTHPP7.js} +11 -11
- package/dist/{catch-up-WHLD6E5F.js → catch-up-K6CV44WQ.js} +4 -4
- package/dist/{check-R5E7WLKF.js → check-MBDLSWCY.js} +35 -34
- package/dist/{checkBrowser-Y4CZVGIV.js → checkBrowser-33UWWDZG.js} +30 -30
- package/dist/{chunk-ID7B7NGN.js → chunk-2E7NDKTN.js} +1 -1
- package/dist/{chunk-EW2QBTYO.js → chunk-2UD6O6QG.js} +10 -7
- package/dist/{chunk-QKJ4KGNR.js → chunk-44GV6ZMC.js} +1 -1
- package/dist/{chunk-YTZA25TZ.js → chunk-4B6PUT4R.js} +1 -1
- package/dist/{chunk-N7QCMEQG.js → chunk-4RXJG3SY.js} +197 -26
- package/dist/chunk-6M3IPJS6.js +3803 -0
- package/dist/{chunk-TJSTGSAW.js → chunk-7FMUTGOH.js} +2 -77
- package/dist/{chunk-AQZZZTFL.js → chunk-AGHHZIZN.js} +1 -1
- package/dist/{chunk-FA3TWKZO.js → chunk-AUMJ3QSO.js} +2 -2
- package/dist/{chunk-ATOXD5VV.js → chunk-AWSVUIWG.js} +1 -1
- package/dist/{chunk-MD4S2RXW.js → chunk-BAW7UNLK.js} +9 -9
- package/dist/{chunk-RG66EU4L.js → chunk-CFBGKAKQ.js} +9 -9
- package/dist/{chunk-ATCB4PMO.js → chunk-EHDKCMKX.js} +1 -1
- package/dist/{chunk-LGCHE2F4.js → chunk-EJYBYR4C.js} +1 -1
- package/dist/{chunk-LUSWL6OJ.js → chunk-ERJHLFPD.js} +2 -2
- package/dist/{chunk-KDC5D5VM.js → chunk-FAORS3UR.js} +28 -13
- package/dist/{chunk-EEP5Y5IE.js → chunk-FE2XTCTM.js} +18 -1
- package/dist/{chunk-YULVV525.js → chunk-FSP2JNAP.js} +1 -1
- package/dist/{chunk-NT3WPYAU.js → chunk-G6GKIAEM.js} +3 -3
- package/dist/{chunk-4CIMRQQY.js → chunk-GBWSBTEE.js} +1 -1
- package/dist/{chunk-O7BABHM4.js → chunk-GG3V5BHG.js} +1 -1
- package/dist/{chunk-E7CIMKJS.js → chunk-GU5FLCZV.js} +1 -1
- package/dist/{chunk-XR3XDG7V.js → chunk-JAERN4RH.js} +1 -1
- package/dist/{chunk-2GSGFDEP.js → chunk-KUFEPCR3.js} +7 -7
- package/dist/{chunk-LZGRJDNJ.js → chunk-KUW4LWY6.js} +1 -1
- package/dist/{chunk-BTCUP5ZY.js → chunk-LMMFO5UV.js} +2 -2
- package/dist/{chunk-HL6GWREP.js → chunk-LWB3KZ5E.js} +1 -1
- package/dist/{chunk-EKEK3GOB.js → chunk-MSFON5DJ.js} +3 -3
- package/dist/{chunk-CDDIEZ4D.js → chunk-MXMIVNUE.js} +19 -23
- package/dist/{chunk-4WPRJ7ID.js → chunk-NSJLCWLG.js} +1 -1
- package/dist/{chunk-MOJKJ4K4.js → chunk-NYCJCQNY.js} +4 -4
- package/dist/{chunk-PYSGYMM4.js → chunk-OGVQJHNM.js} +2 -2
- package/dist/{chunk-C3WPYYGK.js → chunk-PCX3YF2U.js} +1 -1
- package/dist/{chunk-5VGEWXJL.js → chunk-PECICVTJ.js} +2 -2
- package/dist/{chunk-TW7H5CJO.js → chunk-PH3RS5X4.js} +2 -2
- package/dist/{chunk-XOODFU75.js → chunk-PP64Q43K.js} +1 -1
- package/dist/{chunk-DO3UPPO4.js → chunk-PXA4S6OE.js} +2 -2
- package/dist/{chunk-54MI3SGP.js → chunk-QO3JEPEW.js} +2 -2
- package/dist/{chunk-SYKPCN32.js → chunk-QZUVPWC4.js} +3 -1
- package/dist/{chunk-EXMTTXKQ.js → chunk-T56MM7RC.js} +2 -2
- package/dist/{chunk-BOT7PGM5.js → chunk-T5LGOTQR.js} +1 -1
- package/dist/{chunk-UWQL2MP4.js → chunk-UDDJQEGS.js} +25 -6
- package/dist/{chunk-BC2XZL6O.js → chunk-UQPH3Y45.js} +2 -2
- package/dist/{chunk-4PHDDZVL.js → chunk-UUP4KOMD.js} +4 -4
- package/dist/{chunk-BV2NH7ZA.js → chunk-V2T7FFPB.js} +1 -1
- package/dist/{chunk-CAHSX6IW.js → chunk-VKFYI7AQ.js} +182 -77
- package/dist/{chunk-2F7S7OK2.js → chunk-WICIV6R3.js} +23 -1
- package/dist/{chunk-4LXK7TOM.js → chunk-WLNNWLIE.js} +7 -7
- package/dist/{chunk-OQGORCT3.js → chunk-WZQSL7UX.js} +2 -2
- package/dist/{chunk-NYIKKCMC.js → chunk-X3U6KEID.js} +1 -1
- package/dist/{chunk-M4N6IHCQ.js → chunk-XRQBHOBT.js} +7 -1
- package/dist/{chunk-BWFUUGIZ.js → chunk-YFGJVADL.js} +10 -5
- package/dist/{chunk-VTBME36F.js → chunk-ZINH7YA2.js} +1 -1
- package/dist/{clean-UPXYD6VX.js → clean-5AXXHD45.js} +12 -12
- package/dist/cli.js +53 -53
- package/dist/{cloud-74GU7KDD.js → cloud-DQCCYKOB.js} +4 -4
- package/dist/{cloudrun-KHMVQL7C.js → cloudrun-CDFL5JBL.js} +9 -11
- package/dist/{compare-BG77ZXYF.js → compare-6RB5X3SO.js} +25 -25
- package/dist/{compiler-OSQ4L7FP.js → compiler-GU5GPO5A.js} +2 -2
- package/dist/{component-34PVECJG.js → component-7RRNNC5F.js} +3 -3
- package/dist/{compositions-KCVIKKSR.js → compositions-CANET3EJ.js} +9 -9
- package/dist/{delete-NU2ENZYQ.js → delete-KEZJPDWJ.js} +2 -2
- package/dist/{deterministicFonts-KXVO7QEL.js → deterministicFonts-RWGQPEZU.js} +2 -2
- package/dist/{dist-3VKMLNXT.js → dist-IDZRZHGE.js} +2 -2
- package/dist/{doctor-GVELHIHI.js → doctor-5RAEGA6G.js} +7 -7
- package/dist/{events-E6CVR42M.js → events-4ZJSJ76H.js} +2 -2
- package/dist/{events-UP3IQXPM.js → events-SHOWEHI5.js} +3 -3
- package/dist/{feedback-IRYQ4YKX.js → feedback-4IIGUUYZ.js} +6 -6
- package/dist/{figma-BLQBHAPG.js → figma-BFDECPE5.js} +3 -3
- package/dist/fontLocalizeCli.js +9 -9
- package/dist/{get-HESG62RX.js → get-BO32YZUQ.js} +2 -2
- package/dist/{gpuPolicy-CHXUX2ZT.js → gpuPolicy-QWJYZLVJ.js} +5 -5
- package/dist/{grade-compare-ZDJMJUYY.js → grade-compare-SW4SAG5R.js} +25 -25
- package/dist/{help-2BQBFF4Z.js → help-7AMSMPUF.js} +1 -1
- package/dist/{history-4IZQ6NRV.js → history-VXAQFJJV.js} +6 -6
- package/dist/hyperframe-runtime.js +63 -62
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +63 -62
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/{info-JNGAI3JU.js → info-KN4TX6T5.js} +7 -7
- package/dist/{init-ZQO6G5C7.js → init-7A2T7LDW.js} +20 -20
- package/dist/{inspect-DWMKJEVE.js → inspect-CCCV6JFV.js} +26 -26
- package/dist/{installer-UKZT4SKK.js → installer-ZCCZUOPN.js} +4 -4
- package/dist/{keyframes-FC273E5N.js → keyframes-K6XNZGK3.js} +6 -6
- package/dist/{lambda-GSZCV77P.js → lambda-B2K237DX.js} +10 -12
- package/dist/{layout-ASSCBJCO.js → layout-HAPR5OV4.js} +26 -26
- package/dist/{lint-PSCEVVNL.js → lint-3COFAXAJ.js} +6 -6
- package/dist/{list-KO4N2VJL.js → list-QCNHO7DR.js} +2 -2
- package/dist/{login-ZJEBRFV5.js → login-BAFYZS6W.js} +4 -4
- package/dist/{media-treatment-HGHLLZR3.js → media-treatment-C53ZBW2B.js} +8 -8
- package/dist/{models-NTAVDCAV.js → models-INX67ETR.js} +8 -8
- package/dist/{motionShot-SJENYTRO.js → motionShot-UOS26VLB.js} +13 -13
- package/dist/{normalize-audio-PQIC7IPK.js → normalize-audio-I7HGJGA5.js} +4 -4
- package/dist/{open-EI7UEAZZ.js → open-RFXGV4RY.js} +4 -4
- package/dist/{parakeetRunner-7SDEI3IZ.js → parakeetRunner-PCCBODBS.js} +8 -8
- package/dist/{pipeline-EXHHCUE7.js → pipeline-GDC7SL5N.js} +5 -5
- package/dist/{play-Y4EMM2OJ.js → play-DRCWK35L.js} +17 -17
- package/dist/{present-LFMGQGX6.js → present-HUJV553D.js} +13 -13
- package/dist/{preview-EYQG7L6X.js → preview-CZENYIGN.js} +15 -15
- package/dist/{producer-FKOEAHJW.js → producer-23NJEJRO.js} +1 -1
- package/dist/{projectConfig-SXW3QMZE.js → projectConfig-R3FCIARW.js} +6 -6
- package/dist/{publication-ZC4LDEXQ.js → publication-GY3OJ6VH.js} +2 -2
- package/dist/{publish-4ZCLHA5W.js → publish-XBW4NVFY.js} +7 -7
- package/dist/{remove-background-IGDRS2QV.js → remove-background-Q7CQ3PJF.js} +6 -6
- package/dist/{render-ZN777RDD.js → render-24JWWXMA.js} +3 -3
- package/dist/{render-TA3DG3LZ.js → render-DOHHCT2G.js} +8 -8
- package/dist/{render-QJCBJQOM.js → render-WWAXTKYR.js} +23 -23
- package/dist/{render-batch-2JQNFAL4.js → render-batch-EK4ZBQHH.js} +3 -3
- package/dist/renderSetupWorker.js +9 -1
- package/dist/{resolver-JZF5G6YC.js → resolver-5Y6VNMTC.js} +4 -4
- package/dist/{sherpa-MDVSUF6Y.js → sherpa-VMADZBF7.js} +7 -7
- package/dist/skills/media-use/SKILL.md +3 -3
- package/dist/skills/media-use/audio/references/requirements.md +7 -7
- package/dist/skills/media-use/audio/references/sfx.md +19 -13
- package/dist/skills/media-use/audio/scripts/audio.mjs +9 -9
- package/dist/skills/media-use/audio/scripts/audio.test.mjs +17 -10
- package/dist/skills/media-use/audio/scripts/lib/media-record.mjs +7 -3
- package/dist/skills/media-use/audio/scripts/lib/media-record.test.mjs +22 -3
- package/dist/skills/media-use/audio/scripts/lib/openverse.mjs +134 -0
- package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +120 -89
- package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +222 -12
- package/dist/skills/media-use/references/audio.md +1 -1
- package/dist/skills/media-use/references/resolve.md +1 -1
- package/dist/skills/media-use/references/setup-providers.md +13 -12
- package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +4 -7
- package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +5 -5
- package/dist/skills/media-use/scripts/lib/manifest.d.mts +1 -0
- package/dist/skills/media-use/scripts/lib/manifest.mjs +9 -6
- package/dist/skills/media-use/scripts/lib/openverse.mjs +134 -0
- package/dist/skills/media-use/scripts/lib/openverse.test.mjs +219 -0
- package/dist/skills/media-use/scripts/lib/registry.mjs +4 -2
- package/dist/skills/media-use/scripts/lib/registry.test.mjs +13 -13
- package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -13
- package/dist/skills/media-use/scripts/resolve.mjs +45 -55
- package/dist/{skills-JVNMX37H.js → skills-ONFYYL6R.js} +5 -5
- package/dist/{skillsUpdateCheck-KNTQFGOI.js → skillsUpdateCheck-JTHZOHRT.js} +3 -3
- package/dist/{snapshot-RDQTFIPX.js → snapshot-SUA62VQ2.js} +28 -28
- package/dist/{src-ZPEZRIUC.js → src-J6KABXGZ.js} +8 -8
- package/dist/{src-5XO7KBBL.js → src-VFTQPSQD.js} +4 -4
- package/dist/{staticProjectServer-RLERPIY6.js → staticProjectServer-XQPUABYX.js} +13 -13
- package/dist/studio/assets/{hyperframes-player-mYPt9XVX.js → hyperframes-player-Dsmhm7Q8.js} +1 -1
- package/dist/studio/assets/{index-B-v5xQjD.js → index-CV00F8qG.js} +4 -4
- package/dist/studio/assets/{index-DtHmIKM0.js → index-DT1wigvW.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-4INCUOCU.js → studio-api-MBDDFEA2.js} +7 -7
- package/dist/{studioServer-DMDE7IZU.js → studioServer-7HWDCLKT.js} +38 -38
- package/dist/{telemetry-4Y5DYI6H.js → telemetry-763NW3QF.js} +3 -3
- package/dist/{timeline-XJRKELVE.js → timeline-ZLFPNI27.js} +15 -15
- package/dist/{tokens-G4VKTABI.js → tokens-RXJLVZKD.js} +2 -2
- package/dist/{transcribe-JRSQRPVH.js → transcribe-IM5UNBSH.js} +6 -6
- package/dist/{transcribe-Y46B2ZVD.js → transcribe-UDISJXCZ.js} +12 -12
- package/dist/{updateCheck-NGYDWGQL.js → updateCheck-V2RXHFQL.js} +2 -2
- package/dist/{upgrade-M2LQP5S7.js → upgrade-MJI3EW3W.js} +2 -2
- package/dist/{urlDownloader-UFDUP3VF.js → urlDownloader-BOLOMGC7.js} +4 -4
- package/dist/{validate-B3AUFCC5.js → validate-IM5NPJHI.js} +20 -20
- package/dist/{video-WLCNVSVU.js → video-HUNOZDLT.js} +5 -5
- package/package.json +1 -1
- package/dist/chunk-QE7XLLGT.js +0 -3803
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
//
|
|
7
7
|
// node <MEDIA_DIR>/scripts/audio.mjs --request ./audio_request.json --hyperframes . --out ./audio_meta.json
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
9
|
+
// TTS and BGM degrade on ONE switch — whether HeyGen is configured (credential
|
|
10
|
+
// present, NOT the CLI). SFX needs no credential. This mirrors ../SKILL.md:
|
|
11
11
|
//
|
|
12
12
|
// TTS : HeyGen REST → ElevenLabs → Kokoro (CLI)
|
|
13
13
|
// BGM : HeyGen retrieve → (no credential) Lyria/MusicGen generate
|
|
14
|
-
// SFX :
|
|
14
|
+
// SFX : Openverse (CC0 / CC BY) → (offline, rate limited, no match) bundled library
|
|
15
15
|
//
|
|
16
16
|
// ── audio_request.json (input) ────────────────────────────────────────────────
|
|
17
17
|
// {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// bgm: { path, volume, mode, query?, duration_s? } | null,
|
|
32
32
|
// bgm_pending, bgm_provider, bgm_pid, bgm_log, bgm_mode, bgm_target_duration_s, …,
|
|
33
33
|
// voices: [ { id, path, duration_s, words: [{id,text,start,end}] } ],
|
|
34
|
-
// sfx: [ { id, name, file, source, offset_s, duration_s, volume } ],
|
|
34
|
+
// sfx: [ { id, name, file, source, offset_s, duration_s, volume, license?, credit? } ],
|
|
35
35
|
// total_duration_s }
|
|
36
36
|
//
|
|
37
37
|
// --only tts,bgm,sfx runs a subset and MERGES into an existing --out (so a
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
withWordIds,
|
|
53
53
|
} from "./lib/tts.mjs";
|
|
54
54
|
import { generateBgmDetached, inferBgmPrompt, retrieveBgm } from "./lib/bgm.mjs";
|
|
55
|
-
import { resolveSfx } from "./lib/sfx.mjs";
|
|
55
|
+
import { BUNDLED_SFX_DIR, resolveSfx } from "./lib/sfx.mjs";
|
|
56
56
|
import { mapWithConcurrency } from "./lib/concurrency.mjs";
|
|
57
57
|
import { openAudioMeta } from "./lib/audio-meta.mjs";
|
|
58
58
|
import { recordInManifest, voicePaths, writtenAssets } from "./lib/media-record.mjs";
|
|
@@ -83,7 +83,7 @@ const ttsConcurrency = Math.max(1, Number(process.env.HYPERFRAMES_TTS_CONCURRENC
|
|
|
83
83
|
const hyperframesDir = resolve(flag("hyperframes", "."));
|
|
84
84
|
const requestPath = resolve(flag("request", join(hyperframesDir, "audio_request.json")));
|
|
85
85
|
const outPath = resolve(flag("out", join(hyperframesDir, "audio_meta.json")));
|
|
86
|
-
const sfxLibDir = resolve(flag("sfx-lib",
|
|
86
|
+
const sfxLibDir = resolve(flag("sfx-lib", BUNDLED_SFX_DIR));
|
|
87
87
|
const lyriaRecipe = resolve(flag("lyria-recipe", join(HERE, "lyria-recipe.py")));
|
|
88
88
|
const onlyArg = flag("only", "tts,bgm,sfx");
|
|
89
89
|
const only = new Set(
|
|
@@ -275,12 +275,12 @@ if (only.has("sfx")) {
|
|
|
275
275
|
.map((name) => ({ id: String(l.id), name: String(name).trim() }))
|
|
276
276
|
.filter((c) => c.name),
|
|
277
277
|
);
|
|
278
|
-
const
|
|
279
|
-
const res = await resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxLibDir });
|
|
278
|
+
const res = await resolveSfx({ cues, hyperframesDir, sfxLibDir });
|
|
280
279
|
sfx = res.sfx;
|
|
281
280
|
anomalies.push(...res.anomalies);
|
|
281
|
+
const openverse = sfx.filter((s) => s.source === "openverse").length;
|
|
282
282
|
console.error(
|
|
283
|
-
`· sfx: ${sfx.length} cue(s) resolved (${
|
|
283
|
+
`· sfx: ${sfx.length} cue(s) resolved (${openverse} Openverse, ${sfx.length - openverse} bundled library)`,
|
|
284
284
|
);
|
|
285
285
|
}
|
|
286
286
|
|
|
@@ -10,7 +10,11 @@ import { resolveSfx } from "./lib/sfx.mjs";
|
|
|
10
10
|
|
|
11
11
|
// Proves the relocated engine (skills/media-use/audio/) still resolves its
|
|
12
12
|
// bundled SFX library from the moved location — the path most likely to break
|
|
13
|
-
// on a subtree move.
|
|
13
|
+
// on a subtree move. Openverse is unreachable unless a test serves it.
|
|
14
|
+
const offline = async () => {
|
|
15
|
+
throw new TypeError("fetch failed");
|
|
16
|
+
};
|
|
17
|
+
globalThis.fetch = offline;
|
|
14
18
|
|
|
15
19
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
16
20
|
const sfxLibDir = join(HERE, "..", "assets", "sfx"); // same offset the engine uses
|
|
@@ -21,7 +25,6 @@ test("bundled SFX library resolves from the relocated path", async () => {
|
|
|
21
25
|
try {
|
|
22
26
|
const { sfx, anomalies } = await resolveSfx({
|
|
23
27
|
cues: [{ id: "1", name: "whoosh" }],
|
|
24
|
-
heygenOK: false,
|
|
25
28
|
hyperframesDir: dir,
|
|
26
29
|
sfxLibDir,
|
|
27
30
|
});
|
|
@@ -39,7 +42,6 @@ test("an unknown cue is reported, not fatal", async () => {
|
|
|
39
42
|
try {
|
|
40
43
|
const { sfx, anomalies } = await resolveSfx({
|
|
41
44
|
cues: [{ id: "1", name: "definitely-not-a-real-sfx" }],
|
|
42
|
-
heygenOK: false,
|
|
43
45
|
hyperframesDir: dir,
|
|
44
46
|
sfxLibDir,
|
|
45
47
|
});
|
|
@@ -59,7 +61,6 @@ test("a person's own file under a bundled name survives, and the cue gets the ne
|
|
|
59
61
|
const resolve = () =>
|
|
60
62
|
resolveSfx({
|
|
61
63
|
cues: [{ id: "1", name: "whoosh" }],
|
|
62
|
-
heygenOK: false,
|
|
63
64
|
hyperframesDir: dir,
|
|
64
65
|
sfxLibDir,
|
|
65
66
|
});
|
|
@@ -89,18 +90,24 @@ test("two effects never share a file when one's name is taken by the person, run
|
|
|
89
90
|
try {
|
|
90
91
|
mkdirSync(join(dir, "assets", "sfx"), { recursive: true });
|
|
91
92
|
writeFileSync(join(dir, "assets", "sfx", "glitch.mp3"), "the person's own glitch");
|
|
92
|
-
const realFetch = globalThis.fetch;
|
|
93
93
|
globalThis.fetch = async (url) => {
|
|
94
|
-
const query = new URL(url).searchParams.get("
|
|
94
|
+
const query = new URL(url).searchParams.get("q");
|
|
95
95
|
if (query)
|
|
96
|
-
return Response.json({
|
|
96
|
+
return Response.json({
|
|
97
|
+
results: [
|
|
98
|
+
{
|
|
99
|
+
license: "cc0",
|
|
100
|
+
filetype: "mp3",
|
|
101
|
+
duration: 500,
|
|
102
|
+
url: `https://sound.test/${encodeURIComponent(query)}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
97
106
|
return new Response(`bytes of ${new URL(url).pathname}`);
|
|
98
107
|
};
|
|
99
108
|
const run = async (names) => {
|
|
100
109
|
const { sfx } = await resolveSfx({
|
|
101
110
|
cues: names.map((name, index) => ({ id: String(index), name })),
|
|
102
|
-
heygenOK: true,
|
|
103
|
-
headers: {},
|
|
104
111
|
hyperframesDir: dir,
|
|
105
112
|
sfxLibDir,
|
|
106
113
|
});
|
|
@@ -119,7 +126,7 @@ test("two effects never share a file when one's name is taken by the person, run
|
|
|
119
126
|
"assets/sfx/glitch-2.mp3",
|
|
120
127
|
]);
|
|
121
128
|
} finally {
|
|
122
|
-
globalThis.fetch =
|
|
129
|
+
globalThis.fetch = offline;
|
|
123
130
|
}
|
|
124
131
|
} finally {
|
|
125
132
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -43,7 +43,12 @@ export function voicePaths(hyperframesDir, lines, anomalies) {
|
|
|
43
43
|
return paths;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
// A cue's source as the manifest records it, and who provided it. A source missing here records as the person's
|
|
47
|
+
// file, so every rerun would write a numbered copy beside it.
|
|
48
|
+
const SFX_SOURCES = {
|
|
49
|
+
openverse: { source: "search", provider: "openverse.audio" },
|
|
50
|
+
local: { source: "bundled", provider: "bundled.sfx" },
|
|
51
|
+
};
|
|
47
52
|
|
|
48
53
|
// The files one engine run wrote, each with how it was made, for the project's media manifest.
|
|
49
54
|
export function writtenAssets({ only, lines, voices, ttsProvider, bgm, bgmFields, sfx }) {
|
|
@@ -73,8 +78,7 @@ export function writtenAssets({ only, lines, voices, ttsProvider, bgm, bgmFields
|
|
|
73
78
|
}
|
|
74
79
|
if (only.has("sfx")) {
|
|
75
80
|
for (const cue of new Map(sfx.map((entry) => [entry.file, entry])).values()) {
|
|
76
|
-
const source = SFX_SOURCES[cue.source];
|
|
77
|
-
const provider = source === "search" ? "heygen" : "bundled.sfx";
|
|
81
|
+
const { source, provider } = SFX_SOURCES[cue.source];
|
|
78
82
|
assets.push({
|
|
79
83
|
path: cue.file,
|
|
80
84
|
type: "sfx",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { test } from "node:test";
|
|
3
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { findByPrompt, readManifest } from "../../../scripts/lib/manifest.mjs";
|
|
@@ -97,7 +97,7 @@ test("music and sound effects are marked by where they came from", () => {
|
|
|
97
97
|
sfx: [
|
|
98
98
|
{ file: "assets/sfx/whoosh.mp3", name: "whoosh", source: "local" },
|
|
99
99
|
{ file: "assets/sfx/whoosh.mp3", name: "whoosh", source: "local" },
|
|
100
|
-
{ file: "assets/sfx/glass.mp3", name: "glass", source: "
|
|
100
|
+
{ file: "assets/sfx/glass.mp3", name: "glass", source: "openverse" },
|
|
101
101
|
],
|
|
102
102
|
});
|
|
103
103
|
|
|
@@ -106,11 +106,30 @@ test("music and sound effects are marked by where they came from", () => {
|
|
|
106
106
|
[
|
|
107
107
|
["assets/bgm/track.mp3", "search", undefined],
|
|
108
108
|
["assets/sfx/whoosh.mp3", "bundled", "bundled.sfx"],
|
|
109
|
-
["assets/sfx/glass.mp3", "search", "
|
|
109
|
+
["assets/sfx/glass.mp3", "search", "openverse.audio"],
|
|
110
110
|
],
|
|
111
111
|
);
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
test("a rerun of an Openverse sound effect writes over its own file, not a numbered copy", (t) => {
|
|
115
|
+
const dir = project(t);
|
|
116
|
+
mkdirSync(join(dir, "assets/sfx"), { recursive: true });
|
|
117
|
+
const anomalies = [];
|
|
118
|
+
for (let run = 0; run < 3; run++) {
|
|
119
|
+
const file = agentWritePath(dir, "assets/sfx/door-knock.mp3", { anomalies });
|
|
120
|
+
writeFileSync(join(dir, file), "knock");
|
|
121
|
+
const sfx = [{ file, name: "door knock", source: "openverse", duration_s: 1 }];
|
|
122
|
+
recordInManifest(
|
|
123
|
+
dir,
|
|
124
|
+
writtenAssets({ only: new Set(["sfx"]), lines: [], voices: [], sfx, ...noBgm }),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
assert.deepEqual(readdirSync(join(dir, "assets/sfx")), ["door-knock.mp3"]);
|
|
129
|
+
assert.deepEqual(anomalies, []);
|
|
130
|
+
assert.equal(readManifest(dir).length, 1);
|
|
131
|
+
});
|
|
132
|
+
|
|
114
133
|
test("music made locally is marked generated once it is ready, not while pending", () => {
|
|
115
134
|
const written = (bgm_pending) =>
|
|
116
135
|
writtenAssets({
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Sound effects from Openverse (https://api.openverse.org): free, no key.
|
|
2
|
+
// Anonymous use is rate limited (20 requests a minute, 200 a day); callers fall
|
|
3
|
+
// back to the bundled library when it is unavailable.
|
|
4
|
+
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { dirname } from "node:path";
|
|
7
|
+
|
|
8
|
+
const OPENVERSE_AUDIO_URL = "https://api.openverse.org/v1/audio/";
|
|
9
|
+
// The only licences media-use accepts: no NonCommercial, ShareAlike or NoDerivatives.
|
|
10
|
+
export const OPENVERSE_SFX_LICENSES = new Set(["cc0", "by"]);
|
|
11
|
+
const PLAYABLE_FILETYPES = new Set(["mp3", "wav", "ogg", "flac"]);
|
|
12
|
+
const MAX_SFX_MS = 15000;
|
|
13
|
+
const IDEAL_SFX_MS = 4000;
|
|
14
|
+
const BY_PENALTY = 2;
|
|
15
|
+
const SEARCH_TIMEOUT_MS = 10000;
|
|
16
|
+
|
|
17
|
+
// Openverse text lands in markdown and JSON: one line, no control or markdown-structure characters, bounded.
|
|
18
|
+
const oneLine = (text) =>
|
|
19
|
+
String(text ?? "")
|
|
20
|
+
.replace(/[\p{Cc}`[\]<>\\]+/gu, " ")
|
|
21
|
+
.trim()
|
|
22
|
+
.slice(0, 300);
|
|
23
|
+
// Openverse's audio providers (api.openverse.org/v1/audio/stats/); a new provider's link is dropped until listed.
|
|
24
|
+
const SOURCE_HOSTS = new Set(["freesound.org", "www.jamendo.com", "commons.wikimedia.org"]);
|
|
25
|
+
function httpsUrl(url, hosts) {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
return parsed.protocol === "https:" && (!hosts || hosts.has(parsed.hostname))
|
|
29
|
+
? oneLine(parsed.href)
|
|
30
|
+
: "";
|
|
31
|
+
} catch {
|
|
32
|
+
return "";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Openverse could not answer (offline, rate limited, server error): stop asking it this run. */
|
|
37
|
+
export class OpenverseUnavailableError extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "OpenverseUnavailableError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function usable(result) {
|
|
45
|
+
return (
|
|
46
|
+
OPENVERSE_SFX_LICENSES.has(result?.license) &&
|
|
47
|
+
PLAYABLE_FILETYPES.has(result.filetype) &&
|
|
48
|
+
typeof result.url === "string" &&
|
|
49
|
+
result.url.startsWith("https://") &&
|
|
50
|
+
Number.isFinite(result.duration) &&
|
|
51
|
+
result.duration > 0 &&
|
|
52
|
+
result.duration <= MAX_SFX_MS
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Lower is better: relevance position, then seconds past a typical cue length, then CC0 over CC BY.
|
|
57
|
+
function penalty(result, position) {
|
|
58
|
+
const longBy = Math.max(0, result.duration - IDEAL_SFX_MS) / 1000;
|
|
59
|
+
return position + longBy + (result.license === "by" ? BY_PENALTY : 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Best usable sound in Openverse's relevance-ordered results, or null. */
|
|
63
|
+
export function pickOpenverseSfx(results) {
|
|
64
|
+
let best = null;
|
|
65
|
+
results.forEach((result, position) => {
|
|
66
|
+
if (!usable(result)) return;
|
|
67
|
+
const score = penalty(result, position);
|
|
68
|
+
if (!best || score < best.score) best = { result, score };
|
|
69
|
+
});
|
|
70
|
+
if (!best) return null;
|
|
71
|
+
const r = best.result;
|
|
72
|
+
return {
|
|
73
|
+
id: r.id,
|
|
74
|
+
url: r.url,
|
|
75
|
+
ext: `.${r.filetype}`,
|
|
76
|
+
title: oneLine(r.title),
|
|
77
|
+
creator: oneLine(r.creator),
|
|
78
|
+
license: r.license,
|
|
79
|
+
licenseUrl: httpsUrl(r.license_url),
|
|
80
|
+
sourceUrl: httpsUrl(r.foreign_landing_url, SOURCE_HOSTS),
|
|
81
|
+
credit: oneLine(r.attribution),
|
|
82
|
+
durationS: Math.round(r.duration) / 1000,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Best licensed sound effect for `query`, null on no match; throws OpenverseUnavailableError. */
|
|
87
|
+
export async function searchOpenverseSfx(query, { fetchImpl = fetch } = {}) {
|
|
88
|
+
const params = new URLSearchParams({
|
|
89
|
+
q: query,
|
|
90
|
+
page_size: "20",
|
|
91
|
+
license: [...OPENVERSE_SFX_LICENSES].join(","),
|
|
92
|
+
});
|
|
93
|
+
let response;
|
|
94
|
+
try {
|
|
95
|
+
response = await fetchImpl(`${OPENVERSE_AUDIO_URL}?${params}`, {
|
|
96
|
+
headers: { Accept: "application/json" },
|
|
97
|
+
signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS),
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw new OpenverseUnavailableError(`Openverse unreachable (${error.message})`);
|
|
101
|
+
}
|
|
102
|
+
if (response.status === 429)
|
|
103
|
+
throw new OpenverseUnavailableError("Openverse rate limit reached (HTTP 429)");
|
|
104
|
+
if (!response.ok) throw new OpenverseUnavailableError(`Openverse HTTP ${response.status}`);
|
|
105
|
+
let body;
|
|
106
|
+
try {
|
|
107
|
+
body = await response.json();
|
|
108
|
+
} catch {
|
|
109
|
+
throw new OpenverseUnavailableError("Openverse returned non-JSON");
|
|
110
|
+
}
|
|
111
|
+
return pickOpenverseSfx(Array.isArray(body?.results) ? body.results : []);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Record a CC BY sound's credit for `file` in the project's credits file (one line per file). */
|
|
115
|
+
export function writeSfxCredit(creditsPath, file, sound) {
|
|
116
|
+
if (sound.license !== "by") return;
|
|
117
|
+
// The sound may come from a saved record, not this run's pick: clean it again here.
|
|
118
|
+
const sourceUrl = httpsUrl(sound.sourceUrl, SOURCE_HOSTS);
|
|
119
|
+
const source = sourceUrl ? ` Source: ${sourceUrl}` : "";
|
|
120
|
+
const line = `- \`${file}\`: ${oneLine(sound.credit)}${source}`;
|
|
121
|
+
let prior = "";
|
|
122
|
+
try {
|
|
123
|
+
prior = readFileSync(creditsPath, "utf8");
|
|
124
|
+
} catch (error) {
|
|
125
|
+
if (error.code !== "ENOENT") throw error;
|
|
126
|
+
}
|
|
127
|
+
const kept = prior
|
|
128
|
+
.split("\n")
|
|
129
|
+
.filter((l) => l.startsWith("- ") && !l.startsWith(`- \`${file}\`:`));
|
|
130
|
+
const header =
|
|
131
|
+
"# Sound effect credits\n\nThese sounds are licensed CC BY: credit them wherever the video is published.\n\n";
|
|
132
|
+
mkdirSync(dirname(creditsPath), { recursive: true });
|
|
133
|
+
writeFileSync(creditsPath, `${header}${[...kept, line].join("\n")}\n`);
|
|
134
|
+
}
|
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
// sfx.mjs — sound effects for the media audio engine.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// HeyGen credential present → retrieve EVERY cue from HeyGen's audio library
|
|
6
|
-
// (/v3/audio/sounds, type=sound_effects, min_score=0.4). The bundled
|
|
7
|
-
// library is NOT consulted.
|
|
8
|
-
// HeyGen credential absent → resolve cues against the bundled 21-file
|
|
9
|
-
// library (assets/sfx/manifest.json), copying matched files into the
|
|
10
|
-
// project. Offline, deterministic, free.
|
|
1
|
+
// sfx.mjs — sound effects for the media audio engine. Each cue is searched on
|
|
2
|
+
// Openverse (CC0 / CC BY only, see openverse.mjs). A cue Openverse cannot answer,
|
|
3
|
+
// and every cue after Openverse is unreachable or rate limited, resolves against
|
|
4
|
+
// the bundled 21-file library (assets/sfx/manifest.json) instead.
|
|
11
5
|
//
|
|
12
6
|
// A cue that matches nothing is skipped (recorded as an anomaly); SFX never
|
|
13
7
|
// blocks a render. Every cue sits at volume ~0.35, under voice + BGM.
|
|
14
8
|
|
|
15
9
|
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import {
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { downloadTo } from "./heygen.mjs";
|
|
13
|
+
import { OpenverseUnavailableError, searchOpenverseSfx, writeSfxCredit } from "./openverse.mjs";
|
|
18
14
|
import { latestRecordFor } from "../../../scripts/lib/manifest.mjs";
|
|
19
15
|
import { agentWritePath } from "./media-record.mjs";
|
|
20
16
|
|
|
17
|
+
export const BUNDLED_SFX_DIR = join(
|
|
18
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
"..",
|
|
20
|
+
"..",
|
|
21
|
+
"assets",
|
|
22
|
+
"sfx",
|
|
23
|
+
);
|
|
21
24
|
const SFX_VOLUME = 0.35;
|
|
22
25
|
const slug = (s) =>
|
|
23
26
|
s
|
|
@@ -27,14 +30,42 @@ const slug = (s) =>
|
|
|
27
30
|
.slice(0, 40) || "x";
|
|
28
31
|
const r3 = (x) => Number(x.toFixed(3));
|
|
29
32
|
|
|
33
|
+
// Lookups by manifest key, file basename, and slug of either, so a cue can name
|
|
34
|
+
// "whoosh", "whoosh.mp3", or "ui click" (→ slug match). { error } when unusable.
|
|
35
|
+
function loadBundledLibrary(sfxLibDir) {
|
|
36
|
+
const manifestPath = join(sfxLibDir, "manifest.json");
|
|
37
|
+
if (!existsSync(manifestPath)) return { error: `no bundled SFX library at ${sfxLibDir}` };
|
|
38
|
+
let manifest;
|
|
39
|
+
try {
|
|
40
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return { error: `bundled SFX manifest parse failed (${e.message})` };
|
|
43
|
+
}
|
|
44
|
+
const byKey = new Map();
|
|
45
|
+
for (const [key, entry] of Object.entries(manifest)) {
|
|
46
|
+
if (!entry?.file || !isFinite(entry.duration)) continue;
|
|
47
|
+
const rec = { key, file: entry.file, duration: entry.duration };
|
|
48
|
+
byKey.set(key, rec);
|
|
49
|
+
byKey.set(entry.file, rec);
|
|
50
|
+
byKey.set(slug(key), rec);
|
|
51
|
+
byKey.set(slug(entry.file.replace(/\.\w+$/, "")), rec);
|
|
52
|
+
}
|
|
53
|
+
return { byKey };
|
|
54
|
+
}
|
|
55
|
+
|
|
30
56
|
// cues: [{ id, name }] (id = the line/frame/scene the cue fires in). Returns
|
|
31
|
-
// { sfx: [{ id, name, file, source, offset_s, duration_s, volume }], anomalies }.
|
|
32
|
-
export async function resolveSfx({ cues,
|
|
57
|
+
// { sfx: [{ id, name, file, source, offset_s, duration_s, volume, license?, credit? }], anomalies }.
|
|
58
|
+
export async function resolveSfx({ cues, hyperframesDir, sfxLibDir = BUNDLED_SFX_DIR }) {
|
|
33
59
|
const sfx = [];
|
|
34
60
|
const anomalies = [];
|
|
35
61
|
const destDir = join(hyperframesDir, "assets", "sfx");
|
|
36
62
|
// Each effect's file this run: one effect named twice gets one file, and two effects never share one.
|
|
37
63
|
const fileFor = new Map();
|
|
64
|
+
const taken = () => new Set(fileFor.values());
|
|
65
|
+
// One Openverse sound per effect name this run, so a repeated cue costs no second request.
|
|
66
|
+
const openverseFor = new Map();
|
|
67
|
+
let openverseDown = false;
|
|
68
|
+
let library;
|
|
38
69
|
|
|
39
70
|
// Dedupe identical (id,name) cues — the same effect named twice in one line
|
|
40
71
|
// downloads/copies once.
|
|
@@ -46,112 +77,82 @@ export async function resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxL
|
|
|
46
77
|
return true;
|
|
47
78
|
});
|
|
48
79
|
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
async function fromOpenverse({ id, name }) {
|
|
81
|
+
const key = `openverse:${slug(name)}`;
|
|
82
|
+
let entry = openverseFor.get(key);
|
|
83
|
+
if (!entry) {
|
|
84
|
+
const sound = await searchOpenverseSfx(name);
|
|
85
|
+
if (!sound) return null;
|
|
86
|
+
const file = agentWritePath(hyperframesDir, `assets/sfx/${slug(name)}${sound.ext}`, {
|
|
87
|
+
anomalies,
|
|
88
|
+
taken: taken(),
|
|
89
|
+
});
|
|
51
90
|
try {
|
|
52
|
-
|
|
53
|
-
// silently drops most named cues — floor to 0.4. (BGM/music score high
|
|
54
|
-
// and keep the default.)
|
|
55
|
-
const results = await searchSounds(name, "sound_effects", headers, {
|
|
56
|
-
limit: 3,
|
|
57
|
-
minScore: 0.4,
|
|
58
|
-
});
|
|
59
|
-
if (!results.length) {
|
|
60
|
-
anomalies.push(`sfx "${name}" (id ${id}): no HeyGen match — skipped`);
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const top = results[0];
|
|
64
|
-
const file =
|
|
65
|
-
fileFor.get(slug(name)) ??
|
|
66
|
-
agentWritePath(hyperframesDir, `assets/sfx/${slug(name)}.mp3`, {
|
|
67
|
-
anomalies,
|
|
68
|
-
taken: new Set(fileFor.values()),
|
|
69
|
-
});
|
|
70
|
-
await downloadTo(top.audio_url, join(hyperframesDir, file));
|
|
71
|
-
fileFor.set(slug(name), file);
|
|
72
|
-
sfx.push({
|
|
73
|
-
id,
|
|
74
|
-
name,
|
|
75
|
-
file,
|
|
76
|
-
source: "heygen",
|
|
77
|
-
offset_s: 0,
|
|
78
|
-
duration_s: typeof top.duration === "number" ? r3(top.duration) : 1.0,
|
|
79
|
-
volume: SFX_VOLUME,
|
|
80
|
-
});
|
|
91
|
+
await downloadTo(sound.url, join(hyperframesDir, file));
|
|
81
92
|
} catch (e) {
|
|
82
|
-
anomalies.push(`sfx "${name}" (id ${id}):
|
|
93
|
+
anomalies.push(`sfx "${name}" (id ${id}): Openverse download failed — ${e.message}`);
|
|
94
|
+
return null;
|
|
83
95
|
}
|
|
96
|
+
fileFor.set(key, file);
|
|
97
|
+
writeSfxCredit(join(destDir, "CREDITS.md"), file, sound);
|
|
98
|
+
entry = {
|
|
99
|
+
file,
|
|
100
|
+
source: "openverse",
|
|
101
|
+
duration_s: r3(sound.durationS),
|
|
102
|
+
license: sound.license,
|
|
103
|
+
credit: sound.credit,
|
|
104
|
+
};
|
|
105
|
+
openverseFor.set(key, entry);
|
|
84
106
|
}
|
|
85
|
-
return {
|
|
107
|
+
return { id, name, ...entry, offset_s: 0, volume: SFX_VOLUME };
|
|
86
108
|
}
|
|
87
109
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
let manifest;
|
|
96
|
-
try {
|
|
97
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
98
|
-
} catch (e) {
|
|
99
|
-
anomalies.push(`SFX manifest parse failed (${e.message}) — all cues dropped`);
|
|
100
|
-
return { sfx, anomalies };
|
|
101
|
-
}
|
|
102
|
-
// Build lookups: by manifest key, by file basename, and by slug of either, so
|
|
103
|
-
// a cue can name "whoosh", "whoosh.mp3", or "ui click" (→ slug match).
|
|
104
|
-
const byKey = new Map();
|
|
105
|
-
for (const [key, entry] of Object.entries(manifest)) {
|
|
106
|
-
if (!entry?.file || !isFinite(entry.duration)) continue;
|
|
107
|
-
const rec = { key, file: entry.file, duration: entry.duration };
|
|
108
|
-
byKey.set(key, rec);
|
|
109
|
-
byKey.set(entry.file, rec);
|
|
110
|
-
byKey.set(slug(key), rec);
|
|
111
|
-
byKey.set(slug(entry.file.replace(/\.\w+$/, "")), rec);
|
|
112
|
-
}
|
|
113
|
-
mkdirSync(destDir, { recursive: true });
|
|
114
|
-
for (const { id, name } of uniq) {
|
|
110
|
+
function fromBundled({ id, name }) {
|
|
111
|
+
library ??= loadBundledLibrary(sfxLibDir);
|
|
112
|
+
if (library.error) {
|
|
113
|
+
anomalies.push(`sfx "${name}" (id ${id}): ${library.error} — skipped`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const { byKey } = library;
|
|
115
117
|
const hit = byKey.get(name) ?? byKey.get(slug(name));
|
|
116
118
|
if (!hit) {
|
|
117
119
|
const known = [...new Set([...byKey.values()].map((v) => v.key))].slice(0, 8).join(", ");
|
|
118
120
|
anomalies.push(
|
|
119
121
|
`sfx "${name}" (id ${id}): not in bundled library — skipped (have: ${known}…)`,
|
|
120
122
|
);
|
|
121
|
-
|
|
123
|
+
return null;
|
|
122
124
|
}
|
|
125
|
+
mkdirSync(destDir, { recursive: true });
|
|
123
126
|
const src = join(sfxLibDir, hit.file);
|
|
124
|
-
const
|
|
127
|
+
const bytes = existsSync(src) ? readFileSync(src) : null;
|
|
125
128
|
const isLibraryCopy = (rel) =>
|
|
126
|
-
|
|
129
|
+
bytes &&
|
|
127
130
|
existsSync(join(hyperframesDir, rel)) &&
|
|
128
|
-
readFileSync(join(hyperframesDir, rel)).equals(
|
|
131
|
+
readFileSync(join(hyperframesDir, rel)).equals(bytes);
|
|
129
132
|
const destRel =
|
|
130
133
|
fileFor.get(hit.file) ??
|
|
131
134
|
agentWritePath(hyperframesDir, `assets/sfx/${hit.file}`, {
|
|
132
135
|
anomalies,
|
|
133
|
-
taken:
|
|
136
|
+
taken: taken(),
|
|
134
137
|
// An unrecorded copy of the library file is one an engine run made before the manifest had it.
|
|
135
138
|
reusable: (rel) => !latestRecordFor(hyperframesDir, rel) && isLibraryCopy(rel),
|
|
136
139
|
});
|
|
137
140
|
const dest = join(hyperframesDir, destRel);
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
// instead, so the audio_meta never references a missing file.
|
|
143
|
-
if (library) {
|
|
141
|
+
// Some installs of the skill ship manifest.json without the mp3s. An entry
|
|
142
|
+
// pointing at a file we never copied would silently drop downstream, so
|
|
143
|
+
// skip the cue with a loud anomaly instead.
|
|
144
|
+
if (bytes) {
|
|
144
145
|
if (!isLibraryCopy(destRel)) copyFileSync(src, dest);
|
|
145
146
|
} else if (!existsSync(dest)) {
|
|
146
147
|
anomalies.push(
|
|
147
148
|
`sfx "${name}" (id ${id}): bundled file ${hit.file} missing from the offline ` +
|
|
148
149
|
`library (${sfxLibDir}) — skipped. Reinstall the media-use skill to ` +
|
|
149
|
-
`restore assets/sfx/*.mp3
|
|
150
|
+
`restore assets/sfx/*.mp3.`,
|
|
150
151
|
);
|
|
151
|
-
|
|
152
|
+
return null;
|
|
152
153
|
} // else the engine's earlier copy at dest stands in for the file this install lacks
|
|
153
154
|
fileFor.set(hit.file, destRel);
|
|
154
|
-
|
|
155
|
+
return {
|
|
155
156
|
id,
|
|
156
157
|
name,
|
|
157
158
|
file: destRel,
|
|
@@ -159,7 +160,37 @@ export async function resolveSfx({ cues, heygenOK, headers, hyperframesDir, sfxL
|
|
|
159
160
|
offset_s: 0,
|
|
160
161
|
duration_s: r3(hit.duration),
|
|
161
162
|
volume: SFX_VOLUME,
|
|
162
|
-
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const cue of uniq) {
|
|
167
|
+
let entry = null;
|
|
168
|
+
if (!openverseDown) {
|
|
169
|
+
try {
|
|
170
|
+
entry = await fromOpenverse(cue);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (!(e instanceof OpenverseUnavailableError)) throw e;
|
|
173
|
+
openverseDown = true;
|
|
174
|
+
anomalies.push(`${e.message}: the remaining SFX cues use the bundled library`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
entry ??= fromBundled(cue);
|
|
178
|
+
if (entry) sfx.push(entry);
|
|
163
179
|
}
|
|
164
180
|
return { sfx, anomalies };
|
|
165
181
|
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* One sound effect for `query`, saved under the project's assets/sfx/ (the entry point for hosts).
|
|
185
|
+
* { sound: { file, source, duration_s, license?, credit? } | null, anomalies }.
|
|
186
|
+
*/
|
|
187
|
+
export async function findSoundEffect(query, { hyperframesDir, sfxLibDir = BUNDLED_SFX_DIR }) {
|
|
188
|
+
const { sfx, anomalies } = await resolveSfx({
|
|
189
|
+
cues: [{ id: "1", name: query }],
|
|
190
|
+
hyperframesDir,
|
|
191
|
+
sfxLibDir,
|
|
192
|
+
});
|
|
193
|
+
if (!sfx.length) return { sound: null, anomalies };
|
|
194
|
+
const { file, source, duration_s, license, credit } = sfx[0];
|
|
195
|
+
return { sound: { file, source, duration_s, ...(license && { license, credit }) }, anomalies };
|
|
196
|
+
}
|