hyperframes 0.8.169 → 0.8.171
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-7GLP4BPM.js → add-VMMGTSGT.js} +12 -11
- package/dist/{animatedGifPrep-NEBEGVSO.js → animatedGifPrep-MVP5LCEM.js} +5 -5
- package/dist/api.js +11 -11
- package/dist/{asset-RORTT5HS.js → asset-ZERIT5FT.js} +2 -2
- package/dist/{assetCataloger-RLJ6MDGF.js → assetCataloger-RN5A2SVA.js} +5 -5
- package/dist/{audio-OKDGU7TY.js → audio-RQUGFWYT.js} +11 -11
- package/dist/{auth-QWYNCU5U.js → auth-I3FNB6N2.js} +1 -1
- package/dist/{autoUpdate-EDJJPWAU.js → autoUpdate-IBAS4JVK.js} +2 -2
- package/dist/{backgroundChecks-KQR6FGVU.js → backgroundChecks-7UQBIPQC.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-YKTTAIHH.js → batchRender-KWBVAUE3.js} +3 -3
- package/dist/{beats-I6AGLTLW.js → beats-F2GFGFCV.js} +4 -4
- package/dist/{benchmark-NBSV7PNB.js → benchmark-XPQYX3OX.js} +8 -8
- package/dist/{browser-FPEL5FGW.js → browser-QCV62F7R.js} +3 -3
- package/dist/{bundleWithLocalizedFonts-TYQXMNSU.js → bundleWithLocalizedFonts-42V34HJF.js} +2 -2
- package/dist/{capture-FXGOI23U.js → capture-OKPMCIAC.js} +8 -8
- package/dist/{capture-5BRJJMZB.js → capture-WE32B5UZ.js} +2 -2
- package/dist/{catalog-SG3ZIS45.js → catalog-2LWFMJM3.js} +12 -11
- package/dist/{catch-up-ETXUT6Q5.js → catch-up-OWUGMOBS.js} +4 -4
- package/dist/{check-KXNNIEUR.js → check-7BK6UM3M.js} +35 -34
- package/dist/{checkBrowser-546UMI3N.js → checkBrowser-VG745SEX.js} +32 -31
- package/dist/{chunk-YZRS3COO.js → chunk-4QIJFIR5.js} +1 -1
- package/dist/{chunk-UCNQE52B.js → chunk-4RXJG3SY.js} +197 -26
- package/dist/{chunk-JPN7SVWC.js → chunk-5GQB63WX.js} +7 -7
- package/dist/{chunk-FSX7GTBB.js → chunk-5HLJ4JMB.js} +1 -1
- package/dist/{chunk-FQJDV2RI.js → chunk-6I24ZSCR.js} +1 -1
- package/dist/chunk-6M3IPJS6.js +3803 -0
- package/dist/{chunk-7635SDI5.js → chunk-77QCYRQH.js} +2 -2
- package/dist/{chunk-TJSTGSAW.js → chunk-7FMUTGOH.js} +2 -77
- package/dist/{chunk-QD5VJOQK.js → chunk-7YZSOHSC.js} +3 -3
- package/dist/{chunk-AP5AVVAB.js → chunk-A3SQJCPU.js} +1 -1
- package/dist/{chunk-XK5SZNYE.js → chunk-AAYNQ7WL.js} +9 -9
- package/dist/{chunk-T4HL3SZH.js → chunk-AGHHZIZN.js} +1 -1
- package/dist/{chunk-A6K7M46D.js → chunk-AWSVUIWG.js} +1 -1
- package/dist/{chunk-RUYYAL3T.js → chunk-BMPNGUYD.js} +2 -2
- package/dist/{chunk-BNEKKUPG.js → chunk-BYFG74OA.js} +2 -2
- package/dist/{chunk-VCXVZRR2.js → chunk-CO6V4KXX.js} +1 -1
- package/dist/{chunk-5ETJPA5Z.js → chunk-CSABTZ6S.js} +9 -9
- package/dist/{chunk-DCG22HKT.js → chunk-D2QELIQB.js} +72 -22
- package/dist/{chunk-J5Z56PU3.js → chunk-EDGJ2OMH.js} +1 -1
- package/dist/{chunk-SJPNVLYS.js → chunk-ERJHLFPD.js} +2 -2
- package/dist/{chunk-P24X6CGY.js → chunk-FAORS3UR.js} +28 -13
- package/dist/{chunk-EEP5Y5IE.js → chunk-FE2XTCTM.js} +18 -1
- package/dist/{chunk-TVDXMJ43.js → chunk-GBWSBTEE.js} +1 -1
- package/dist/{chunk-JGYHWVXQ.js → chunk-GMJ3ILLW.js} +34 -13
- package/dist/{chunk-K5PU5ISL.js → chunk-GSABLXZV.js} +1 -1
- package/dist/{chunk-KA4LSAD5.js → chunk-GU5FLCZV.js} +1 -1
- package/dist/{chunk-YT6TFGIS.js → chunk-IWLOARQZ.js} +2 -2
- package/dist/{chunk-EFP25BUY.js → chunk-JAERN4RH.js} +1 -1
- package/dist/chunk-JHOCK3DG.js +27 -0
- package/dist/{chunk-5SQHHBAR.js → chunk-JMJTXBVF.js} +22 -12
- package/dist/{chunk-NKG3LTPI.js → chunk-KQCWIE76.js} +2 -2
- package/dist/{chunk-S6U7YLPJ.js → chunk-KUW4LWY6.js} +1 -1
- package/dist/{chunk-PVTHDNDZ.js → chunk-L32QQH2K.js} +1 -1
- package/dist/{chunk-TEFHMDVR.js → chunk-LMMFO5UV.js} +2 -2
- package/dist/{chunk-CESKA6VO.js → chunk-NSJLCWLG.js} +1 -1
- package/dist/{chunk-MCPDZWZY.js → chunk-NYCJCQNY.js} +4 -4
- package/dist/{chunk-2F4HJFGW.js → chunk-PAFCKH3S.js} +2 -2
- package/dist/{chunk-U7P2XTRY.js → chunk-PECICVTJ.js} +2 -2
- package/dist/{chunk-GVX2443P.js → chunk-PH3RS5X4.js} +2 -2
- package/dist/{chunk-Q5K73VW3.js → chunk-PXA4S6OE.js} +2 -2
- package/dist/{chunk-O3J7BHBU.js → chunk-QZUVPWC4.js} +1 -1
- package/dist/{chunk-ZFJ5RI7A.js → chunk-RSW6LGGW.js} +3 -3
- package/dist/{chunk-EELTMBCC.js → chunk-RTW6WSC7.js} +4 -3
- package/dist/{chunk-CNP6AMFM.js → chunk-SLTUINB4.js} +2 -2
- package/dist/{chunk-RZKQ3G6B.js → chunk-T5LGOTQR.js} +1 -1
- package/dist/{chunk-WAVFNZB7.js → chunk-TZ3I23TZ.js} +21 -7
- package/dist/{chunk-QLHXC4E6.js → chunk-UJQQITCY.js} +1 -1
- package/dist/{chunk-XZNHVWDD.js → chunk-UUP4KOMD.js} +4 -4
- package/dist/{chunk-UISU4SYG.js → chunk-V2T7FFPB.js} +1 -1
- package/dist/{chunk-7FHFPHEM.js → chunk-VKFYI7AQ.js} +182 -77
- package/dist/{chunk-QABAVWEX.js → chunk-W47D3KOB.js} +1 -1
- package/dist/{chunk-YLAREDAU.js → chunk-X3U6KEID.js} +1 -1
- package/dist/{chunk-5FWVFCQM.js → chunk-XGFYN53T.js} +2 -2
- package/dist/{chunk-M4N6IHCQ.js → chunk-XRQBHOBT.js} +7 -1
- package/dist/{chunk-XJQ6NN2L.js → chunk-ZBUGFQ7Y.js} +51 -25
- package/dist/{clean-ARH23EGW.js → clean-FCBNQ3W2.js} +13 -13
- package/dist/cli.js +53 -53
- package/dist/{cloud-GBY7MRZM.js → cloud-3FZBQBOM.js} +4 -4
- package/dist/{cloudrun-HBVA5UZB.js → cloudrun-CDFL5JBL.js} +9 -11
- package/dist/{compare-Z4YNESYN.js → compare-E4332OOZ.js} +26 -25
- package/dist/{compiler-6WJ5SVTD.js → compiler-GU5GPO5A.js} +2 -2
- package/dist/{component-AR4MS4R7.js → component-VKSV3OKW.js} +3 -3
- package/dist/{compositions-ERZVZBYW.js → compositions-GOVXHFXC.js} +9 -9
- package/dist/{delete-NWSW3BET.js → delete-JYZTEBLQ.js} +2 -2
- package/dist/{deterministicFonts-KXVO7QEL.js → deterministicFonts-RWGQPEZU.js} +2 -2
- package/dist/{dist-VKLSNKRZ.js → dist-IDZRZHGE.js} +2 -2
- package/dist/{doctor-P4QZQRCB.js → doctor-ND3FZM6S.js} +7 -7
- package/dist/{events-F2UVJQIC.js → events-27JU7QRQ.js} +2 -2
- package/dist/{events-SV46AWMI.js → events-VL2JIO4R.js} +3 -3
- package/dist/{feedback-XS4IK6SW.js → feedback-PKVQWCVT.js} +6 -6
- package/dist/{figma-DELISEUP.js → figma-FZ4T4SE3.js} +3 -3
- package/dist/fontLocalizeCli.js +9 -9
- package/dist/{get-DNGXCEFB.js → get-HUAIHWXR.js} +2 -2
- package/dist/{gpuPolicy-JG5PUOJP.js → gpuPolicy-QWJYZLVJ.js} +5 -5
- package/dist/{grade-compare-F46JWE4O.js → grade-compare-CJ2ROYD6.js} +26 -25
- package/dist/{help-XVEBMX3U.js → help-OJ3QDJYS.js} +1 -1
- package/dist/{history-NS4PKG4S.js → history-P4LA65T5.js} +7 -7
- package/dist/hyperframe-runtime.js +58 -57
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +58 -57
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/{info-DJ6VNESS.js → info-QHRJW6Y3.js} +7 -7
- package/dist/{init-TSRTMVRN.js → init-AG6YVVJN.js} +21 -20
- package/dist/{inspect-VTZXXDJ4.js → inspect-TT66DYEC.js} +27 -26
- package/dist/{installer-3ERHU4CD.js → installer-5ZKUQBWW.js} +4 -4
- package/dist/{keyframes-6DN7BOYJ.js → keyframes-CJJGCW3R.js} +6 -6
- package/dist/{lambda-RCOIEGL6.js → lambda-3BMEOSP7.js} +10 -12
- package/dist/{layout-FFGRLVK4.js → layout-BSDCTMJK.js} +27 -26
- package/dist/{lint-MBYIDFKS.js → lint-6CL2R2FK.js} +6 -6
- package/dist/{list-KEZDFW5R.js → list-5VYOLEFN.js} +2 -2
- package/dist/{login-7ZV374QL.js → login-PNRREAOC.js} +4 -4
- package/dist/{media-treatment-LGWWRXAX.js → media-treatment-KHJX73WZ.js} +8 -8
- package/dist/{models-TC4K6UQP.js → models-INX67ETR.js} +8 -8
- package/dist/{motionShot-4E3RTV5O.js → motionShot-VIUXVMTV.js} +12 -12
- package/dist/{normalize-audio-XCGB2IQP.js → normalize-audio-T5QMN476.js} +4 -4
- package/dist/{open-JNNJSNTH.js → open-T57KLGFX.js} +4 -4
- package/dist/{parakeetRunner-S3DG4BJB.js → parakeetRunner-PCCBODBS.js} +8 -8
- package/dist/{pipeline-3SIYCBLU.js → pipeline-GDC7SL5N.js} +5 -5
- package/dist/{play-LBPOOD3V.js → play-TQWHYMAT.js} +19 -18
- package/dist/{present-PWEN5SDL.js → present-U5OFT6IF.js} +14 -14
- package/dist/{preview-IS32RLKM.js → preview-WWTDQQSB.js} +20 -16
- package/dist/{producer-4UUUEPPS.js → producer-23NJEJRO.js} +1 -1
- package/dist/{projectConfig-K347VIO3.js → projectConfig-OXNA35QN.js} +7 -6
- package/dist/{publication-4CQS532O.js → publication-GY3OJ6VH.js} +2 -2
- package/dist/{publish-OJYF37FY.js → publish-WZD4NLWM.js} +8 -7
- package/dist/{remove-background-HO6YL2HU.js → remove-background-Q7CQ3PJF.js} +6 -6
- package/dist/{render-L76SEAMC.js → render-24JWWXMA.js} +3 -3
- package/dist/{render-QBQYF2NC.js → render-FTVXKXSQ.js} +24 -23
- package/dist/{render-Q6QAXJCN.js → render-RZUU76V5.js} +8 -8
- package/dist/{render-batch-JLANN5Y2.js → render-batch-FCR7RX6U.js} +7 -20
- package/dist/renderSetupWorker.js +9 -1
- package/dist/{resolver-EDUM4DHG.js → resolver-4SPHOGY7.js} +5 -4
- package/dist/{sherpa-QEM5XYHD.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-FSZK6FIG.js → skills-7LBLAEH4.js} +5 -5
- package/dist/{skillsUpdateCheck-3354HXDJ.js → skillsUpdateCheck-AHK7CQUP.js} +3 -3
- package/dist/{snapshot-5VVMFEMM.js → snapshot-DOS6NQXM.js} +29 -28
- package/dist/{src-RYILCNU7.js → src-J6KABXGZ.js} +8 -8
- package/dist/{src-ZGMLMQ3B.js → src-VFTQPSQD.js} +4 -4
- package/dist/{staticProjectServer-PIVSGEAO.js → staticProjectServer-2YNWMUSU.js} +15 -14
- package/dist/studio/assets/{hyperframes-player-B04Yx96S.js → hyperframes-player-DohJxMIU.js} +2 -2
- package/dist/studio/assets/{index-D2e0J6kh.js → index-BOkm4Ooc.js} +101 -101
- package/dist/studio/assets/{index-D9CDX_Rr.js → index-DfwtrPPt.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-PHADR6VB.js → studio-api-NEBM56HH.js} +8 -8
- package/dist/{studioServer-5AVDKN5V.js → studioServer-LYDRHW7D.js} +45 -39
- package/dist/{telemetry-JCTHMPSB.js → telemetry-FAYON6XS.js} +3 -3
- package/dist/{timeline-QU2F6R2M.js → timeline-UKJ53XLC.js} +16 -16
- package/dist/{tokens-YYK4G36C.js → tokens-SO3ZXEFM.js} +2 -2
- package/dist/{transcribe-7ORH7R6O.js → transcribe-IM5UNBSH.js} +6 -6
- package/dist/{transcribe-PRG4FLLA.js → transcribe-PS5XSTXN.js} +12 -12
- package/dist/{updateCheck-NV7QNANK.js → updateCheck-ZV422M72.js} +2 -2
- package/dist/{upgrade-UZXN36J2.js → upgrade-GHG5FWZJ.js} +2 -2
- package/dist/{urlDownloader-AW57H722.js → urlDownloader-BOLOMGC7.js} +4 -4
- package/dist/{validate-QD53ABUM.js → validate-K4ZGXH2M.js} +20 -20
- package/dist/{video-VLVPYQQR.js → video-HUNOZDLT.js} +5 -5
- package/package.json +1 -1
- package/dist/chunk-BIV5HVTQ.js +0 -3803
|
@@ -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
|
+
}
|