Setting the file. One moment.
Openverse · Media Use · heygen-com/hyperframes · Skills Docs
ContentsBack to the top of the page Resolve
audio/scripts/lib/ openverse.mjs
JavaScript · 134 lines · 5 KB
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\n These 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 }