Setting the file. One moment.
Sfx · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page ⋯
scripts/11 files
audio/scripts/lib/sfx.mjs
audio/scripts/lib/ sfx.mjs
JavaScript · 143 lines · 5 KB
15 import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs" ;
16 import { join } from "node:path" ;
17 import { downloadTo, searchSounds } from "./heygen.mjs" ;
18
19 const SFX_VOLUME = 0.35 ;
20 const slug = ( s ) =>
21 s
22 . toLowerCase ()
23 . replace ( / [ ^ a-z0-9] + / g , "-" )
24 . replace ( / ^ - +| - +$ / g , "" )
25 . slice ( 0 , 40 ) || "x" ;
26 const r3 = ( x ) => Number (x. toFixed ( 3 ));
27
28 // cues: [{ id, name }] (id = the line/frame/scene the cue fires in). Returns
29 // { sfx: [{ id, name, file, source, offset_s, duration_s, volume }], anomalies }.
30 export async function resolveSfx ({ cues , heygenOK , headers , hyperframesDir , sfxLibDir }) {
31 const sfx = [];
32 const anomalies = [];
33 const destDir = join (hyperframesDir, "assets" , "sfx" );
34
35 // Dedupe identical (id,name) cues — the same effect named twice in one line
36 // downloads/copies once.
37 const seen = new Set ();
38 const uniq = cues. filter (( c ) => {
39 const k = `${ c . id }:${ c . name }` ;
40 if (seen. has (k)) return false ;
41 seen. add (k);
42 return true ;
43 });
44
45 if (heygenOK) {
46 for ( const { id , name } of uniq) {
47 try {
48 // SFX hits score low (~0.5–0.67), below the API's default 0.7 which
49 // silently drops most named cues — floor to 0.4. (BGM/music score high
50 // and keep the default.)
51 const results = await searchSounds (name, "sound_effects" , headers, {
52 limit: 3 ,
53 minScore: 0.4 ,
54 });
55 if ( ! results. length ) {
56 anomalies. push ( `sfx "${ name }" (id ${ id }): no HeyGen match — skipped` );
57 continue ;
58 }
59 const top = results[ 0 ];
60 const file = `assets/sfx/${ slug ( name ) }.mp3` ;
61 await downloadTo (top.audio_url, join (hyperframesDir, file));
62 sfx. push ({
63 id,
64 name,
65 file,
66 source: "heygen" ,
67 offset_s: 0 ,
68 duration_s: typeof top.duration === "number" ? r3 (top.duration) : 1.0 ,
69 volume: SFX_VOLUME ,
70 });
71 } catch (e) {
72 anomalies. push ( `sfx "${ name }" (id ${ id }): retrieval failed — ${ e . message }` );
73 }
74 }
75 return { sfx, anomalies };
76 }
77
78 // ── offline: bundled library ──
79 const manifestPath = join (sfxLibDir, "manifest.json" );
80 if ( ! existsSync (manifestPath)) {
81 if (uniq. length )
82 anomalies. push ( `no HeyGen credential and no SFX library at ${ sfxLibDir } — all cues dropped` );
83 return { sfx, anomalies };
84 }
85 let manifest;
86 try {
87 manifest = JSON . parse ( readFileSync (manifestPath, "utf8" ));
88 } catch (e) {
89 anomalies. push ( `SFX manifest parse failed (${ e . message }) — all cues dropped` );
90 return { sfx, anomalies };
91 }
92 // Build lookups: by manifest key, by file basename, and by slug of either, so
93 // a cue can name "whoosh", "whoosh.mp3", or "ui click" (→ slug match).
94 const byKey = new Map ();
95 for ( const [ key , entry ] of Object. entries (manifest)) {
96 if ( ! entry?.file || ! isFinite (entry.duration)) continue ;
97 const rec = { key, file: entry.file, duration: entry.duration };
98 byKey. set (key, rec);
99 byKey. set (entry.file, rec);
100 byKey. set ( slug (key), rec);
101 byKey. set ( slug (entry.file. replace ( / \. \w +$ / , "" )), rec);
102 }
103 mkdirSync (destDir, { recursive: true });
104 for ( const { id , name } of uniq) {
105 const hit = byKey. get (name) ?? byKey. get ( slug (name));
106 if ( ! hit) {
107 const known = [ ...new Set ([ ... byKey. values ()]. map (( v ) => v.key))]. slice ( 0 , 8 ). join ( ", " );
108 anomalies. push (
109 `sfx "${ name }" (id ${ id }): not in bundled library — skipped (have: ${ known }…)` ,
110 );
111 continue ;
112 }
113 const src = join (sfxLibDir, hit.file);
114 const destRel = `assets/sfx/${ hit . file }` ;
115 const dest = join (hyperframesDir, destRel);
116 // The bundled library may be incomplete: some installs of the skill ship
117 // manifest.json without the actual mp3s. Pushing an sfx entry that points at
118 // a file we never copied produces a dangling reference that silently drops
119 // downstream ("not on disk"). Surface it as a loud anomaly and skip the cue
120 // instead, so the audio_meta never references a missing file.
121 if ( ! existsSync (dest)) {
122 if ( ! existsSync (src)) {
123 anomalies. push (
124 `sfx "${ name }" (id ${ id }): bundled file ${ hit . file } missing from the offline ` +
125 `library (${ sfxLibDir }) — skipped. Reinstall the media-use skill to ` +
126 `restore assets/sfx/*.mp3, or configure a HeyGen credential for retrieval.` ,
127 );
128 continue ;
129 }
130 copyFileSync (src, dest);
131 }
132 sfx. push ({
133 id,
134 name,
135 file: destRel,
136 source: "local" ,
137 offset_s: 0 ,
138 duration_s: r3 (hit.duration),
139 volume: SFX_VOLUME ,
140 });
141 }
142 return { sfx, anomalies };
143 }