hyperframes 0.8.169 → 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.
Files changed (178) hide show
  1. package/dist/{add-7GLP4BPM.js → add-KDCZCEMQ.js} +11 -11
  2. package/dist/{animatedGifPrep-NEBEGVSO.js → animatedGifPrep-MVP5LCEM.js} +5 -5
  3. package/dist/api.js +11 -11
  4. package/dist/{asset-RORTT5HS.js → asset-6VH3OCEC.js} +2 -2
  5. package/dist/{assetCataloger-RLJ6MDGF.js → assetCataloger-RN5A2SVA.js} +5 -5
  6. package/dist/{audio-OKDGU7TY.js → audio-636CQSXK.js} +11 -11
  7. package/dist/{auth-QWYNCU5U.js → auth-AHLKM4M2.js} +1 -1
  8. package/dist/{autoUpdate-EDJJPWAU.js → autoUpdate-5UA6TJW7.js} +2 -2
  9. package/dist/{backgroundChecks-KQR6FGVU.js → backgroundChecks-DP242536.js} +3 -3
  10. package/dist/backgroundChecksWorker.js +3 -3
  11. package/dist/{batchRender-YKTTAIHH.js → batchRender-KWBVAUE3.js} +3 -3
  12. package/dist/{beats-I6AGLTLW.js → beats-HKY5L463.js} +4 -4
  13. package/dist/{benchmark-NBSV7PNB.js → benchmark-4YQDVIO5.js} +8 -8
  14. package/dist/{browser-FPEL5FGW.js → browser-7ZPWXEYW.js} +3 -3
  15. package/dist/{bundleWithLocalizedFonts-TYQXMNSU.js → bundleWithLocalizedFonts-42V34HJF.js} +2 -2
  16. package/dist/{capture-FXGOI23U.js → capture-572X5NDD.js} +8 -8
  17. package/dist/{capture-5BRJJMZB.js → capture-VXMUZSBI.js} +2 -2
  18. package/dist/{catalog-SG3ZIS45.js → catalog-PUUTHPP7.js} +11 -11
  19. package/dist/{catch-up-ETXUT6Q5.js → catch-up-K6CV44WQ.js} +4 -4
  20. package/dist/{check-KXNNIEUR.js → check-MBDLSWCY.js} +32 -32
  21. package/dist/{checkBrowser-546UMI3N.js → checkBrowser-33UWWDZG.js} +29 -29
  22. package/dist/{chunk-EELTMBCC.js → chunk-2E7NDKTN.js} +1 -1
  23. package/dist/{chunk-DCG22HKT.js → chunk-2UD6O6QG.js} +6 -6
  24. package/dist/{chunk-5SQHHBAR.js → chunk-44GV6ZMC.js} +1 -1
  25. package/dist/{chunk-FQJDV2RI.js → chunk-4B6PUT4R.js} +1 -1
  26. package/dist/{chunk-UCNQE52B.js → chunk-4RXJG3SY.js} +197 -26
  27. package/dist/chunk-6M3IPJS6.js +3803 -0
  28. package/dist/{chunk-TJSTGSAW.js → chunk-7FMUTGOH.js} +2 -77
  29. package/dist/{chunk-T4HL3SZH.js → chunk-AGHHZIZN.js} +1 -1
  30. package/dist/{chunk-CNP6AMFM.js → chunk-AUMJ3QSO.js} +2 -2
  31. package/dist/{chunk-A6K7M46D.js → chunk-AWSVUIWG.js} +1 -1
  32. package/dist/{chunk-5ETJPA5Z.js → chunk-BAW7UNLK.js} +9 -9
  33. package/dist/{chunk-XK5SZNYE.js → chunk-CFBGKAKQ.js} +9 -9
  34. package/dist/{chunk-VCXVZRR2.js → chunk-EHDKCMKX.js} +1 -1
  35. package/dist/{chunk-AP5AVVAB.js → chunk-EJYBYR4C.js} +1 -1
  36. package/dist/{chunk-SJPNVLYS.js → chunk-ERJHLFPD.js} +2 -2
  37. package/dist/{chunk-P24X6CGY.js → chunk-FAORS3UR.js} +28 -13
  38. package/dist/{chunk-EEP5Y5IE.js → chunk-FE2XTCTM.js} +18 -1
  39. package/dist/{chunk-FSX7GTBB.js → chunk-FSP2JNAP.js} +1 -1
  40. package/dist/{chunk-ZFJ5RI7A.js → chunk-G6GKIAEM.js} +3 -3
  41. package/dist/{chunk-TVDXMJ43.js → chunk-GBWSBTEE.js} +1 -1
  42. package/dist/{chunk-K5PU5ISL.js → chunk-GG3V5BHG.js} +1 -1
  43. package/dist/{chunk-KA4LSAD5.js → chunk-GU5FLCZV.js} +1 -1
  44. package/dist/{chunk-EFP25BUY.js → chunk-JAERN4RH.js} +1 -1
  45. package/dist/{chunk-YT6TFGIS.js → chunk-KUFEPCR3.js} +1 -1
  46. package/dist/{chunk-S6U7YLPJ.js → chunk-KUW4LWY6.js} +1 -1
  47. package/dist/{chunk-TEFHMDVR.js → chunk-LMMFO5UV.js} +2 -2
  48. package/dist/{chunk-PVTHDNDZ.js → chunk-LWB3KZ5E.js} +1 -1
  49. package/dist/{chunk-QD5VJOQK.js → chunk-MSFON5DJ.js} +3 -3
  50. package/dist/{chunk-XJQ6NN2L.js → chunk-MXMIVNUE.js} +19 -23
  51. package/dist/{chunk-CESKA6VO.js → chunk-NSJLCWLG.js} +1 -1
  52. package/dist/{chunk-MCPDZWZY.js → chunk-NYCJCQNY.js} +4 -4
  53. package/dist/{chunk-RUYYAL3T.js → chunk-OGVQJHNM.js} +2 -2
  54. package/dist/{chunk-J5Z56PU3.js → chunk-PCX3YF2U.js} +1 -1
  55. package/dist/{chunk-U7P2XTRY.js → chunk-PECICVTJ.js} +2 -2
  56. package/dist/{chunk-GVX2443P.js → chunk-PH3RS5X4.js} +2 -2
  57. package/dist/{chunk-YZRS3COO.js → chunk-PP64Q43K.js} +1 -1
  58. package/dist/{chunk-Q5K73VW3.js → chunk-PXA4S6OE.js} +2 -2
  59. package/dist/{chunk-5FWVFCQM.js → chunk-QO3JEPEW.js} +2 -2
  60. package/dist/{chunk-O3J7BHBU.js → chunk-QZUVPWC4.js} +1 -1
  61. package/dist/{chunk-2F4HJFGW.js → chunk-T56MM7RC.js} +2 -2
  62. package/dist/{chunk-RZKQ3G6B.js → chunk-T5LGOTQR.js} +1 -1
  63. package/dist/{chunk-WAVFNZB7.js → chunk-UDDJQEGS.js} +1 -1
  64. package/dist/{chunk-BNEKKUPG.js → chunk-UQPH3Y45.js} +2 -2
  65. package/dist/{chunk-XZNHVWDD.js → chunk-UUP4KOMD.js} +4 -4
  66. package/dist/{chunk-UISU4SYG.js → chunk-V2T7FFPB.js} +1 -1
  67. package/dist/{chunk-7FHFPHEM.js → chunk-VKFYI7AQ.js} +182 -77
  68. package/dist/{chunk-JPN7SVWC.js → chunk-WLNNWLIE.js} +7 -7
  69. package/dist/{chunk-7635SDI5.js → chunk-WZQSL7UX.js} +2 -2
  70. package/dist/{chunk-YLAREDAU.js → chunk-X3U6KEID.js} +1 -1
  71. package/dist/{chunk-M4N6IHCQ.js → chunk-XRQBHOBT.js} +7 -1
  72. package/dist/{chunk-NKG3LTPI.js → chunk-YFGJVADL.js} +1 -1
  73. package/dist/{chunk-QLHXC4E6.js → chunk-ZINH7YA2.js} +1 -1
  74. package/dist/{clean-ARH23EGW.js → clean-5AXXHD45.js} +12 -12
  75. package/dist/cli.js +53 -53
  76. package/dist/{cloud-GBY7MRZM.js → cloud-DQCCYKOB.js} +4 -4
  77. package/dist/{cloudrun-HBVA5UZB.js → cloudrun-CDFL5JBL.js} +9 -11
  78. package/dist/{compare-Z4YNESYN.js → compare-6RB5X3SO.js} +24 -24
  79. package/dist/{compiler-6WJ5SVTD.js → compiler-GU5GPO5A.js} +2 -2
  80. package/dist/{component-AR4MS4R7.js → component-7RRNNC5F.js} +3 -3
  81. package/dist/{compositions-ERZVZBYW.js → compositions-CANET3EJ.js} +9 -9
  82. package/dist/{delete-NWSW3BET.js → delete-KEZJPDWJ.js} +2 -2
  83. package/dist/{deterministicFonts-KXVO7QEL.js → deterministicFonts-RWGQPEZU.js} +2 -2
  84. package/dist/{dist-VKLSNKRZ.js → dist-IDZRZHGE.js} +2 -2
  85. package/dist/{doctor-P4QZQRCB.js → doctor-5RAEGA6G.js} +7 -7
  86. package/dist/{events-F2UVJQIC.js → events-4ZJSJ76H.js} +2 -2
  87. package/dist/{events-SV46AWMI.js → events-SHOWEHI5.js} +3 -3
  88. package/dist/{feedback-XS4IK6SW.js → feedback-4IIGUUYZ.js} +6 -6
  89. package/dist/{figma-DELISEUP.js → figma-BFDECPE5.js} +3 -3
  90. package/dist/fontLocalizeCli.js +9 -9
  91. package/dist/{get-DNGXCEFB.js → get-BO32YZUQ.js} +2 -2
  92. package/dist/{gpuPolicy-JG5PUOJP.js → gpuPolicy-QWJYZLVJ.js} +5 -5
  93. package/dist/{grade-compare-F46JWE4O.js → grade-compare-SW4SAG5R.js} +24 -24
  94. package/dist/{help-XVEBMX3U.js → help-7AMSMPUF.js} +1 -1
  95. package/dist/{history-NS4PKG4S.js → history-VXAQFJJV.js} +6 -6
  96. package/dist/hyperframe-runtime.js +58 -57
  97. package/dist/hyperframe.manifest.json +1 -1
  98. package/dist/hyperframe.runtime.iife.js +58 -57
  99. package/dist/hyperframes-player.global.js +1 -1
  100. package/dist/{info-DJ6VNESS.js → info-KN4TX6T5.js} +7 -7
  101. package/dist/{init-TSRTMVRN.js → init-7A2T7LDW.js} +20 -20
  102. package/dist/{inspect-VTZXXDJ4.js → inspect-CCCV6JFV.js} +25 -25
  103. package/dist/{installer-3ERHU4CD.js → installer-ZCCZUOPN.js} +4 -4
  104. package/dist/{keyframes-6DN7BOYJ.js → keyframes-K6XNZGK3.js} +6 -6
  105. package/dist/{lambda-RCOIEGL6.js → lambda-B2K237DX.js} +10 -12
  106. package/dist/{layout-FFGRLVK4.js → layout-HAPR5OV4.js} +25 -25
  107. package/dist/{lint-MBYIDFKS.js → lint-3COFAXAJ.js} +6 -6
  108. package/dist/{list-KEZDFW5R.js → list-QCNHO7DR.js} +2 -2
  109. package/dist/{login-7ZV374QL.js → login-BAFYZS6W.js} +4 -4
  110. package/dist/{media-treatment-LGWWRXAX.js → media-treatment-C53ZBW2B.js} +8 -8
  111. package/dist/{models-TC4K6UQP.js → models-INX67ETR.js} +8 -8
  112. package/dist/{motionShot-4E3RTV5O.js → motionShot-UOS26VLB.js} +12 -12
  113. package/dist/{normalize-audio-XCGB2IQP.js → normalize-audio-I7HGJGA5.js} +4 -4
  114. package/dist/{open-JNNJSNTH.js → open-RFXGV4RY.js} +4 -4
  115. package/dist/{parakeetRunner-S3DG4BJB.js → parakeetRunner-PCCBODBS.js} +8 -8
  116. package/dist/{pipeline-3SIYCBLU.js → pipeline-GDC7SL5N.js} +5 -5
  117. package/dist/{play-LBPOOD3V.js → play-DRCWK35L.js} +17 -17
  118. package/dist/{present-PWEN5SDL.js → present-HUJV553D.js} +13 -13
  119. package/dist/{preview-IS32RLKM.js → preview-CZENYIGN.js} +15 -15
  120. package/dist/{producer-4UUUEPPS.js → producer-23NJEJRO.js} +1 -1
  121. package/dist/{projectConfig-K347VIO3.js → projectConfig-R3FCIARW.js} +6 -6
  122. package/dist/{publication-4CQS532O.js → publication-GY3OJ6VH.js} +2 -2
  123. package/dist/{publish-OJYF37FY.js → publish-XBW4NVFY.js} +7 -7
  124. package/dist/{remove-background-HO6YL2HU.js → remove-background-Q7CQ3PJF.js} +6 -6
  125. package/dist/{render-L76SEAMC.js → render-24JWWXMA.js} +3 -3
  126. package/dist/{render-Q6QAXJCN.js → render-DOHHCT2G.js} +8 -8
  127. package/dist/{render-QBQYF2NC.js → render-WWAXTKYR.js} +22 -22
  128. package/dist/{render-batch-JLANN5Y2.js → render-batch-EK4ZBQHH.js} +3 -3
  129. package/dist/renderSetupWorker.js +9 -1
  130. package/dist/{resolver-EDUM4DHG.js → resolver-5Y6VNMTC.js} +4 -4
  131. package/dist/{sherpa-QEM5XYHD.js → sherpa-VMADZBF7.js} +7 -7
  132. package/dist/skills/media-use/SKILL.md +3 -3
  133. package/dist/skills/media-use/audio/references/requirements.md +7 -7
  134. package/dist/skills/media-use/audio/references/sfx.md +19 -13
  135. package/dist/skills/media-use/audio/scripts/audio.mjs +9 -9
  136. package/dist/skills/media-use/audio/scripts/audio.test.mjs +17 -10
  137. package/dist/skills/media-use/audio/scripts/lib/media-record.mjs +7 -3
  138. package/dist/skills/media-use/audio/scripts/lib/media-record.test.mjs +22 -3
  139. package/dist/skills/media-use/audio/scripts/lib/openverse.mjs +134 -0
  140. package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +120 -89
  141. package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +222 -12
  142. package/dist/skills/media-use/references/audio.md +1 -1
  143. package/dist/skills/media-use/references/resolve.md +1 -1
  144. package/dist/skills/media-use/references/setup-providers.md +13 -12
  145. package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +4 -7
  146. package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +5 -5
  147. package/dist/skills/media-use/scripts/lib/manifest.d.mts +1 -0
  148. package/dist/skills/media-use/scripts/lib/manifest.mjs +9 -6
  149. package/dist/skills/media-use/scripts/lib/openverse.mjs +134 -0
  150. package/dist/skills/media-use/scripts/lib/openverse.test.mjs +219 -0
  151. package/dist/skills/media-use/scripts/lib/registry.mjs +4 -2
  152. package/dist/skills/media-use/scripts/lib/registry.test.mjs +13 -13
  153. package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -13
  154. package/dist/skills/media-use/scripts/resolve.mjs +45 -55
  155. package/dist/{skills-FSZK6FIG.js → skills-ONFYYL6R.js} +5 -5
  156. package/dist/{skillsUpdateCheck-3354HXDJ.js → skillsUpdateCheck-JTHZOHRT.js} +3 -3
  157. package/dist/{snapshot-5VVMFEMM.js → snapshot-SUA62VQ2.js} +27 -27
  158. package/dist/{src-RYILCNU7.js → src-J6KABXGZ.js} +8 -8
  159. package/dist/{src-ZGMLMQ3B.js → src-VFTQPSQD.js} +4 -4
  160. package/dist/{staticProjectServer-PIVSGEAO.js → staticProjectServer-XQPUABYX.js} +13 -13
  161. package/dist/studio/assets/{hyperframes-player-B04Yx96S.js → hyperframes-player-Dsmhm7Q8.js} +1 -1
  162. package/dist/studio/assets/{index-D2e0J6kh.js → index-CV00F8qG.js} +4 -4
  163. package/dist/studio/assets/{index-D9CDX_Rr.js → index-DT1wigvW.js} +1 -1
  164. package/dist/studio/index.html +1 -1
  165. package/dist/{studio-api-PHADR6VB.js → studio-api-MBDDFEA2.js} +7 -7
  166. package/dist/{studioServer-5AVDKN5V.js → studioServer-7HWDCLKT.js} +37 -37
  167. package/dist/{telemetry-JCTHMPSB.js → telemetry-763NW3QF.js} +3 -3
  168. package/dist/{timeline-QU2F6R2M.js → timeline-ZLFPNI27.js} +15 -15
  169. package/dist/{tokens-YYK4G36C.js → tokens-RXJLVZKD.js} +2 -2
  170. package/dist/{transcribe-7ORH7R6O.js → transcribe-IM5UNBSH.js} +6 -6
  171. package/dist/{transcribe-PRG4FLLA.js → transcribe-UDISJXCZ.js} +12 -12
  172. package/dist/{updateCheck-NV7QNANK.js → updateCheck-V2RXHFQL.js} +2 -2
  173. package/dist/{upgrade-UZXN36J2.js → upgrade-MJI3EW3W.js} +2 -2
  174. package/dist/{urlDownloader-AW57H722.js → urlDownloader-BOLOMGC7.js} +4 -4
  175. package/dist/{validate-QD53ABUM.js → validate-IM5NPJHI.js} +19 -19
  176. package/dist/{video-VLVPYQQR.js → video-HUNOZDLT.js} +5 -5
  177. package/package.json +1 -1
  178. package/dist/chunk-BIV5HVTQ.js +0 -3803
@@ -0,0 +1,219 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "node:test";
6
+ import {
7
+ OpenverseUnavailableError,
8
+ pickOpenverseSfx,
9
+ searchOpenverseSfx,
10
+ writeSfxCredit,
11
+ } from "./openverse.mjs";
12
+
13
+ const result = (overrides = {}) => ({
14
+ id: "ov-1",
15
+ title: "Deep Whoosh",
16
+ creator: "Kinoton",
17
+ license: "cc0",
18
+ license_url: "https://creativecommons.org/publicdomain/zero/1.0/",
19
+ foreign_landing_url: "https://freesound.org/people/Kinoton/sounds/351256",
20
+ url: "https://cdn.freesound.org/previews/351/351256_2247456-hq.mp3",
21
+ filetype: "mp3",
22
+ duration: 1200,
23
+ attribution: '"Deep Whoosh" by Kinoton is marked with CC0 1.0.',
24
+ ...overrides,
25
+ });
26
+
27
+ test("only CC0 and CC BY sounds are ever picked", () => {
28
+ for (const license of ["by-nc", "by-sa", "by-nd", "by-nc-sa", "by-nc-nd", "pdm", "sampling+"])
29
+ assert.equal(pickOpenverseSfx([result({ license })]), null, license);
30
+ assert.equal(pickOpenverseSfx([result({ license: "by" })]).license, "by");
31
+ assert.equal(pickOpenverseSfx([result({ license: "cc0" })]).license, "cc0");
32
+ });
33
+
34
+ test("sounds too long for a cue, or not playable, are skipped", () => {
35
+ assert.equal(pickOpenverseSfx([result({ duration: 15001 })]), null);
36
+ assert.equal(pickOpenverseSfx([result({ duration: null })]), null);
37
+ assert.equal(pickOpenverseSfx([result({ filetype: "aiff" })]), null);
38
+ assert.equal(pickOpenverseSfx([result({ url: "http://insecure.test/a.mp3" })]), null);
39
+ });
40
+
41
+ test("ranking keeps relevance order, then prefers short clips and CC0 over CC BY", () => {
42
+ const pick = (results) => pickOpenverseSfx(results).id;
43
+ assert.equal(pick([result({ id: "first" }), result({ id: "second" })]), "first");
44
+ assert.equal(pick([result({ id: "long", duration: 12000 }), result({ id: "short" })]), "short");
45
+ assert.equal(pick([result({ id: "by", license: "by" }), result({ id: "cc0" })]), "cc0");
46
+ assert.equal(
47
+ pick([
48
+ result({ id: "by", license: "by" }),
49
+ ...Array(3).fill(result({ license: "by-nc" })),
50
+ result({ id: "cc0" }),
51
+ ]),
52
+ "by",
53
+ "a much more relevant CC BY sound still wins",
54
+ );
55
+ });
56
+
57
+ test("a pick carries what the project needs to play and credit it", () => {
58
+ assert.deepEqual(pickOpenverseSfx([result({ license: "by", filetype: "wav", duration: 450 })]), {
59
+ id: "ov-1",
60
+ url: "https://cdn.freesound.org/previews/351/351256_2247456-hq.mp3",
61
+ ext: ".wav",
62
+ title: "Deep Whoosh",
63
+ creator: "Kinoton",
64
+ license: "by",
65
+ licenseUrl: "https://creativecommons.org/publicdomain/zero/1.0/",
66
+ sourceUrl: "https://freesound.org/people/Kinoton/sounds/351256",
67
+ credit: '"Deep Whoosh" by Kinoton is marked with CC0 1.0.',
68
+ durationS: 0.45,
69
+ });
70
+ });
71
+
72
+ test("Openverse text is kept to one bounded line, and only https links survive", () => {
73
+ const pick = pickOpenverseSfx([
74
+ result({
75
+ license: "by",
76
+ title: "Pop\n# Heading",
77
+ attribution: `"Pop" by Ana.\n- \`assets/sfx/other.mp3\`: forged credit\r\u0007${"x".repeat(400)}`,
78
+ foreign_landing_url: "javascript:alert(1)",
79
+ license_url: "http://creativecommons.org/licenses/by/4.0/",
80
+ }),
81
+ ]);
82
+ assert.equal(pick.title, "Pop # Heading");
83
+ assert.equal(pick.credit.length, 300);
84
+ assert.ok(!/\p{Cc}/u.test(pick.credit), "no line breaks or control characters");
85
+ assert.equal(pick.sourceUrl, "");
86
+ assert.equal(pick.licenseUrl, "");
87
+ });
88
+
89
+ test("search asks Openverse for JSON and only the allowed licences", async () => {
90
+ const calls = [];
91
+ const sound = await searchOpenverseSfx("ui click", {
92
+ fetchImpl: async (url, init) => {
93
+ calls.push({ url: new URL(url), init });
94
+ return Response.json({ results: [result({ license: "by-nc" }), result({ id: "ok" })] });
95
+ },
96
+ });
97
+ assert.equal(sound.id, "ok", "the licence field is checked even on a filtered response");
98
+ assert.equal(calls.length, 1);
99
+ assert.equal(calls[0].url.origin + calls[0].url.pathname, "https://api.openverse.org/v1/audio/");
100
+ assert.equal(calls[0].url.searchParams.get("q"), "ui click");
101
+ assert.equal(calls[0].url.searchParams.get("license"), "cc0,by");
102
+ assert.equal(calls[0].init.headers.Accept, "application/json");
103
+ });
104
+
105
+ test("no usable result is a miss, not an error", async () => {
106
+ const fetchImpl = async () => Response.json({ results: [] });
107
+ assert.equal(await searchOpenverseSfx("nothing", { fetchImpl }), null);
108
+ });
109
+
110
+ test("rate limits, server errors, bad bodies and network failures make Openverse unavailable", async () => {
111
+ const failures = [
112
+ [async () => new Response("slow down", { status: 429 }), /rate limit/],
113
+ [async () => new Response("down", { status: 503 }), /HTTP 503/],
114
+ [async () => new Response("<html>"), /non-JSON/],
115
+ [
116
+ async () => {
117
+ throw new TypeError("fetch failed");
118
+ },
119
+ /unreachable \(fetch failed\)/,
120
+ ],
121
+ ];
122
+ for (const [fetchImpl, message] of failures)
123
+ await assert.rejects(searchOpenverseSfx("whoosh", { fetchImpl }), (error) => {
124
+ assert.ok(error instanceof OpenverseUnavailableError);
125
+ assert.match(error.message, message);
126
+ return true;
127
+ });
128
+ });
129
+
130
+ test("credits list each CC BY file once and leave CC0 out", () => {
131
+ const dir = mkdtempSync(join(tmpdir(), "openverse-credits-"));
132
+ try {
133
+ const path = join(dir, "assets", "sfx", "CREDITS.md");
134
+ const by = {
135
+ license: "by",
136
+ credit: '"Pop" by Ana is licensed under CC BY 4.0.',
137
+ sourceUrl: "https://freesound.org/s/1",
138
+ };
139
+ writeSfxCredit(path, "assets/sfx/zap.mp3", { ...by, license: "cc0" });
140
+ assert.throws(() => readFileSync(path), /ENOENT/);
141
+ writeSfxCredit(path, "assets/sfx/pop.mp3", by);
142
+ writeSfxCredit(path, "assets/sfx/pop.mp3", {
143
+ ...by,
144
+ credit: '"Pop 2" by Ana is licensed under CC BY 4.0.',
145
+ });
146
+ writeSfxCredit(path, "assets/sfx/ding.mp3", by);
147
+ const lines = readFileSync(path, "utf8")
148
+ .split("\n")
149
+ .filter((l) => l.startsWith("- "));
150
+ assert.deepEqual(lines, [
151
+ '- `assets/sfx/pop.mp3`: "Pop 2" by Ana is licensed under CC BY 4.0. Source: https://freesound.org/s/1',
152
+ '- `assets/sfx/ding.mp3`: "Pop" by Ana is licensed under CC BY 4.0. Source: https://freesound.org/s/1',
153
+ ]);
154
+ } finally {
155
+ rmSync(dir, { recursive: true, force: true });
156
+ }
157
+ });
158
+
159
+ test("a credit with no https source link is written without one", () => {
160
+ const dir = mkdtempSync(join(tmpdir(), "openverse-credits-"));
161
+ try {
162
+ const path = join(dir, "CREDITS.md");
163
+ writeSfxCredit(path, "assets/sfx/pop.mp3", { license: "by", credit: "c.", sourceUrl: "" });
164
+ assert.match(readFileSync(path, "utf8"), /\n- `assets\/sfx\/pop\.mp3`: c\.\n$/);
165
+ } finally {
166
+ rmSync(dir, { recursive: true, force: true });
167
+ }
168
+ });
169
+
170
+ test("a hostile credit is written as one safe line, with no link off Openverse's providers", () => {
171
+ const dir = mkdtempSync(join(tmpdir(), "openverse-credits-"));
172
+ try {
173
+ const path = join(dir, "CREDITS.md");
174
+ const hostile = `"Pop" by Ana.\n- \`assets/sfx/x.mp3\`: forged [click](https://evil.test) <img src=x> \\${"x".repeat(5000)}`;
175
+ for (const sourceUrl of [
176
+ "javascript:alert(1)",
177
+ "https://evil.test/a",
178
+ "https://freesound.org/s/1\n- forged",
179
+ ]) {
180
+ writeSfxCredit(path, "assets/sfx/pop.mp3", { license: "by", credit: hostile, sourceUrl });
181
+ const lines = readFileSync(path, "utf8")
182
+ .split("\n")
183
+ .filter((l) => l.startsWith("- "));
184
+ assert.equal(lines.length, 1, sourceUrl);
185
+ assert.ok(
186
+ lines[0].startsWith('- `assets/sfx/pop.mp3`: "Pop" by Ana. - '),
187
+ lines[0].slice(0, 80),
188
+ );
189
+ assert.ok(
190
+ !/[\p{Cc}`[\]<>\\]/u.test(lines[0].slice("- `assets/sfx/pop.mp3`:".length)),
191
+ sourceUrl,
192
+ );
193
+ assert.ok(lines[0].length < 400, "bounded");
194
+ assert.doesNotMatch(lines[0], /javascript:|evil\.test\/a/);
195
+ }
196
+ assert.match(readFileSync(path, "utf8"), /Source: https:\/\/freesound\.org\/s\/1-%20forged\n$/);
197
+ } finally {
198
+ rmSync(dir, { recursive: true, force: true });
199
+ }
200
+ });
201
+
202
+ test("earlier credit lines survive a new credit", () => {
203
+ const dir = mkdtempSync(join(tmpdir(), "openverse-credits-"));
204
+ try {
205
+ const path = join(dir, "CREDITS.md");
206
+ writeFileSync(path, "# Sound effect credits\n\n- `assets/sfx/old.mp3`: kept\n");
207
+ writeSfxCredit(path, "assets/sfx/new.mp3", {
208
+ license: "by",
209
+ credit: "c.",
210
+ sourceUrl: "https://freesound.org/s/2",
211
+ });
212
+ assert.match(
213
+ readFileSync(path, "utf8"),
214
+ /- `assets\/sfx\/old\.mp3`: kept\n- `assets\/sfx\/new\.mp3`: c\. Source: https:\/\/freesound\.org\/s\/2\n$/,
215
+ );
216
+ } finally {
217
+ rmSync(dir, { recursive: true, force: true });
218
+ }
219
+ });
@@ -10,6 +10,8 @@
10
10
  // own auth. Providers, by type:
11
11
  // - heygen CLI: catalog + TTS, first for every type it serves (OAuth free
12
12
  // allowance first, then the user's HeyGen billing path)
13
+ // - Openverse: free, keyless sound effects (CC0 / CC BY), with the bundled
14
+ // library as the offline fallback
13
15
  // - mflux: local FLUX-class image gen, spec-selected to the machine's RAM
14
16
  // (free, private, offline once cached)
15
17
  // - codex CLI: image gen on the user's ChatGPT sub — the better-quality upsell
@@ -21,7 +23,7 @@
21
23
  // (e.g. "make an image with codex").
22
24
 
23
25
  import { bgmProvider } from "./bgm-provider.mjs";
24
- import { sfxProvider } from "./sfx-provider.mjs";
26
+ import { OPENVERSE_SFX_PROVIDER, sfxProvider } from "./sfx-provider.mjs";
25
27
  import { bundledSfxProvider } from "./bundled-sfx-provider.mjs";
26
28
  import { imageProvider, iconProvider } from "./image-provider.mjs";
27
29
  import { brandProvider } from "./brand-provider.mjs";
@@ -46,8 +48,8 @@ const P = (name, caps) => ({ name, network: true, paid: true, ...caps }); // rem
46
48
  const REGISTRY = {
47
49
  bgm: [N("heygen.audio.sounds", { search: bgmProvider.search })],
48
50
  sfx: [
51
+ N(OPENVERSE_SFX_PROVIDER, { search: sfxProvider.search }),
49
52
  A("bundled.sfx", { search: bundledSfxProvider.search }),
50
- N("heygen.audio.sounds", { search: sfxProvider.search }),
51
53
  ],
52
54
  image: [
53
55
  N("heygen.asset.search", { search: imageProvider.search }),
@@ -32,18 +32,18 @@ test("listTypes exposes the v2 media types", () => {
32
32
  }
33
33
  });
34
34
 
35
- test("heygen provider is first for every type it serves except bundled sfx", () => {
35
+ test("heygen provider is first for every type it serves", () => {
36
36
  for (const t of ["bgm", "image", "icon"]) {
37
37
  const first = getProviders(t)[0];
38
38
  assert.ok(first, `no enabled provider for ${t}`);
39
39
  assert.match(first.name, /^heygen/, `${t} first provider is ${first.name}`);
40
40
  }
41
- assert.equal(getProviders("sfx")[0].name, "bundled.sfx");
41
+ assert.ok(!getProviders("sfx").some((p) => p.name.startsWith("heygen")), "sfx never asks HeyGen");
42
42
  });
43
43
 
44
- test("sanctioned providers only: heygen, local mflux/kokoro/ltx, codex, design spec, logo tiers", () => {
44
+ test("sanctioned providers only: heygen, Openverse, local mflux/kokoro/ltx, codex, design spec, logo tiers", () => {
45
45
  const allowed =
46
- /^heygen|^bundled\.sfx$|^mflux\.local$|^kokoro\.local$|^ltx\.local$|^codex\.image_gen$|^design_spec$|^thesvg$|^github\.avatar$|^favicon\.ddg$|^color_grade\.local$|^cube_lut\.local$/;
46
+ /^heygen|^openverse\.audio$|^bundled\.sfx$|^mflux\.local$|^kokoro\.local$|^ltx\.local$|^codex\.image_gen$|^design_spec$|^thesvg$|^github\.avatar$|^favicon\.ddg$|^color_grade\.local$|^cube_lut\.local$/;
47
47
  for (const t of listTypes()) {
48
48
  for (const p of getProviders(t)) {
49
49
  assert.ok(allowed.test(p.name), `${t} lists unsanctioned provider: ${p.name}`);
@@ -85,13 +85,14 @@ test("video cascade: HeyGen first, LTX local fallback, generate-only", async ()
85
85
  assert.equal(await runCapability("video", "search", "x", {}), null);
86
86
  });
87
87
 
88
- test("sfx cascade: bundled library first, HeyGen catalog remains the network fallback", () => {
88
+ test("sfx cascade: Openverse first, the bundled library is the offline fallback", () => {
89
89
  const ps = getProviders("sfx");
90
- assert.equal(ps[0].name, "bundled.sfx");
91
- assert.equal(typeof ps[0].search, "function");
92
- assert.ok(!ps[0].network, "bundled SFX remain available offline");
93
- assert.equal(ps[1].name, "heygen.audio.sounds");
94
- assert.ok(ps[1].network, "HeyGen SFX catalog is network-only");
90
+ assert.deepEqual(
91
+ ps.map((p) => p.name),
92
+ ["openverse.audio", "bundled.sfx"],
93
+ );
94
+ assert.ok(ps[0].network && !ps[0].paid, "Openverse is free and network-only");
95
+ assert.ok(!ps[1].network, "bundled SFX remain available offline");
95
96
  });
96
97
 
97
98
  test("ctx.provider forces one generator (e.g. 'make an image WITH codex')", async () => {
@@ -201,9 +202,8 @@ test("providerTierFor reports the registry's own A/N/P declaration", () => {
201
202
  });
202
203
 
203
204
  test("providerTierFor agrees across every type that declares the same name", () => {
204
- // heygen.audio.sounds serves both bgm and sfx; heygen.asset.search serves both
205
- // image and icon. A name whose tier depended on the media type would make the
206
- // telemetry property meaningless.
205
+ // heygen.asset.search serves both image and icon. A name whose tier depended
206
+ // on the media type would make the telemetry property meaningless.
207
207
  const byName = new Map();
208
208
  for (const type of listTypes()) {
209
209
  for (const name of providerNamesFor(type)) {
@@ -1,22 +1,32 @@
1
- import { heygenSearch } from "./heygen-search.mjs";
1
+ import { OpenverseUnavailableError, searchOpenverseSfx } from "./openverse.mjs";
2
+
3
+ export const OPENVERSE_SFX_PROVIDER = "openverse.audio";
2
4
 
3
5
  export const sfxProvider = {
4
6
  async search(intent) {
5
- const results = heygenSearch("audio sounds list", intent, {
6
- type: "sound_effects",
7
- minScore: 0.4,
8
- });
9
- if (!results) return null;
10
- const best = results[0];
7
+ let sound;
8
+ try {
9
+ sound = await searchOpenverseSfx(intent);
10
+ } catch (error) {
11
+ if (!(error instanceof OpenverseUnavailableError)) throw error;
12
+ console.error(`media-use: ${error.message}; using the bundled sound effects`);
13
+ return null;
14
+ }
15
+ if (!sound) return null;
11
16
  return {
12
- url: best.audio_url,
17
+ url: sound.url,
18
+ ext: sound.ext,
13
19
  source: "search",
14
- // ext derived from audio_url by resolve.mjs — catalog SFX are .mp3 or .wav
15
20
  metadata: {
16
- description: best.description || best.name || intent,
17
- duration: best.duration || null,
18
- provider: "heygen.audio.sounds",
19
- provenance: { track_id: best.id, score: best.score, query: intent },
21
+ description: sound.title || intent,
22
+ duration: sound.durationS,
23
+ provider: OPENVERSE_SFX_PROVIDER,
24
+ provenance: {
25
+ openverse_id: sound.id,
26
+ license: sound.license,
27
+ credit: sound.credit,
28
+ source_url: sound.sourceUrl,
29
+ },
20
30
  },
21
31
  };
22
32
  },
@@ -12,6 +12,8 @@ import {
12
12
  findByPrompt,
13
13
  findByEntity,
14
14
  nextId,
15
+ typeDirPath,
16
+ withMediaLock,
15
17
  withReservedFile,
16
18
  withReservedFileSync,
17
19
  } from "./lib/manifest.mjs";
@@ -55,6 +57,8 @@ import {
55
57
  versionLessThan,
56
58
  } from "./lib/heygen-cli.mjs";
57
59
  import { BundledSfxAssetsError, inspectBundledSfxAssets } from "./lib/bundled-sfx-provider.mjs";
60
+ import { OPENVERSE_SFX_PROVIDER } from "./lib/sfx-provider.mjs";
61
+ import { writeSfxCredit } from "./lib/openverse.mjs";
58
62
  import {
59
63
  fetchMediaVectors,
60
64
  mediaVectorRows,
@@ -441,57 +445,43 @@ async function run() {
441
445
  return resolveColor(type, intent, { projectDir });
442
446
  }
443
447
 
444
- // SFX search is bundled, local-index, then HeyGen.
448
+ // SFX search is Openverse, the bundled library, then the local media index.
445
449
  let searchResult = null;
446
450
  let providerFailure = null;
447
451
  let localIndexFailure = null;
448
452
  try {
449
- if (type === "sfx" && !args.provider) {
450
- searchResult = await runCapability(type, "search", intent, {
451
- ...ctx,
452
- provider: "bundled.sfx",
453
- });
454
- if (!searchResult && !localOnly) {
455
- const registry =
456
- process.env.HYPERFRAMES_REGISTRY ||
457
- "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry";
458
- try {
459
- await fetchMediaVectors(registry);
460
- const ranked = await rankMediaRowsWithVectors(
461
- intent,
462
- mediaVectorRows().filter((row) => row.kind === "sfx"),
463
- );
464
- const row = ranked.rows[0];
465
- const candidates = row
466
- ? [resolve(row.file), join(import.meta.dirname, "..", "..", "..", row.file)]
467
- : [];
468
- const localPath = candidates.find((candidate) => existsSync(candidate));
469
- if (row && localPath) {
470
- searchResult = {
471
- localPath,
472
- ext: extname(localPath),
473
- source: "local-index",
474
- metadata: {
475
- description: row.description,
476
- duration: row.duration ?? null,
477
- provider: "catalog.local",
478
- provenance: { library_key: row.id, tier: ranked.tier },
479
- },
480
- };
481
- }
482
- } catch (error) {
483
- localIndexFailure = error;
453
+ searchResult = await runCapability(type, "search", intent, ctx);
454
+ if (!searchResult && type === "sfx" && !args.provider && !localOnly) {
455
+ const registry =
456
+ process.env.HYPERFRAMES_REGISTRY ||
457
+ "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry";
458
+ try {
459
+ await fetchMediaVectors(registry);
460
+ const ranked = await rankMediaRowsWithVectors(
461
+ intent,
462
+ mediaVectorRows().filter((row) => row.kind === "sfx"),
463
+ );
464
+ const row = ranked.rows[0];
465
+ const candidates = row
466
+ ? [resolve(row.file), join(import.meta.dirname, "..", "..", "..", row.file)]
467
+ : [];
468
+ const localPath = candidates.find((candidate) => existsSync(candidate));
469
+ if (row && localPath) {
470
+ searchResult = {
471
+ localPath,
472
+ ext: extname(localPath),
473
+ source: "local-index",
474
+ metadata: {
475
+ description: row.description,
476
+ duration: row.duration ?? null,
477
+ provider: "catalog.local",
478
+ provenance: { library_key: row.id, tier: ranked.tier },
479
+ },
480
+ };
484
481
  }
482
+ } catch (error) {
483
+ localIndexFailure = error;
485
484
  }
486
- if (!searchResult) searchResult = await runCapability(type, "search", intent, ctx);
487
- // Keep HeyGen remediation diagnostics while bundled remains authoritative.
488
- else if (!localOnly)
489
- await runCapability(type, "search", intent, {
490
- ...ctx,
491
- provider: "heygen.audio.sounds",
492
- });
493
- } else {
494
- searchResult = await runCapability(type, "search", intent, ctx);
495
485
  }
496
486
  } catch (error) {
497
487
  providerFailure = error;
@@ -602,16 +592,6 @@ async function run() {
602
592
  },
603
593
  };
604
594
 
605
- const heygenRemediation = consumeHeygenRemediation();
606
- if (
607
- searchResult.metadata?.provider === "bundled.sfx" &&
608
- !localOnly &&
609
- !args.provider &&
610
- heygenRemediation
611
- ) {
612
- record.advisory = heygenRemediation;
613
- }
614
-
615
595
  appendRecord(projectDir, record);
616
596
  regenerateIndex(projectDir);
617
597
  // Promote fetched assets into the global cache for reuse.
@@ -1304,6 +1284,16 @@ async function reuseGlobal(shaArg) {
1304
1284
  }
1305
1285
 
1306
1286
  async function result(record, source) {
1287
+ const { provider, license, credit, source_url: sourceUrl } = record.provenance ?? {};
1288
+ // Under the media lock: two resolves at once each read, then rewrite, the whole credits file.
1289
+ if (provider === OPENVERSE_SFX_PROVIDER)
1290
+ withMediaLock(projectDir, () =>
1291
+ writeSfxCredit(join(typeDirPath(projectDir, "sfx"), "CREDITS.md"), record.path, {
1292
+ license,
1293
+ credit,
1294
+ sourceUrl,
1295
+ }),
1296
+ );
1307
1297
  // Non-PII usage event: which media type, how it resolved, which provider won.
1308
1298
  // Never the intent text or paths. Awaited so a short-lived run flushes it.
1309
1299
  await track("media_use_resolve", {
@@ -6,8 +6,8 @@ var __filename = __hf_fileURLToPath(import.meta.url);
6
6
  var __dirname = __hf_dirname(__filename);
7
7
  import {
8
8
  trackSkillsInstallSkipped
9
- } from "./chunk-FQJDV2RI.js";
10
- import "./chunk-FSX7GTBB.js";
9
+ } from "./chunk-4B6PUT4R.js";
10
+ import "./chunk-FSP2JNAP.js";
11
11
  import {
12
12
  diag
13
13
  } from "./chunk-VWG7OQKU.js";
@@ -35,15 +35,15 @@ import {
35
35
  isCoreSkill,
36
36
  presentSkills,
37
37
  pruneOrphanedLockEntries
38
- } from "./chunk-VCXVZRR2.js";
38
+ } from "./chunk-EHDKCMKX.js";
39
39
  import {
40
40
  withMeta
41
- } from "./chunk-K5PU5ISL.js";
41
+ } from "./chunk-GG3V5BHG.js";
42
42
  import "./chunk-K4WIOZFP.js";
43
43
  import "./chunk-XUXGIA7H.js";
44
44
  import "./chunk-WERLJGIC.js";
45
45
  import "./chunk-ISN5GRYI.js";
46
- import "./chunk-BNEKKUPG.js";
46
+ import "./chunk-UQPH3Y45.js";
47
47
  import {
48
48
  buildNpxCommand
49
49
  } from "./chunk-6BEFEWFB.js";
@@ -9,13 +9,13 @@ import {
9
9
  printSkillsUpdateNotice,
10
10
  refreshSkillsCache,
11
11
  skillsCheckDue
12
- } from "./chunk-VCXVZRR2.js";
13
- import "./chunk-K5PU5ISL.js";
12
+ } from "./chunk-EHDKCMKX.js";
13
+ import "./chunk-GG3V5BHG.js";
14
14
  import "./chunk-K4WIOZFP.js";
15
15
  import "./chunk-XUXGIA7H.js";
16
16
  import "./chunk-WERLJGIC.js";
17
17
  import "./chunk-ISN5GRYI.js";
18
- import "./chunk-BNEKKUPG.js";
18
+ import "./chunk-UQPH3Y45.js";
19
19
  import "./chunk-KNU5COPB.js";
20
20
  import "./chunk-M2WBOQ2Q.js";
21
21
  export {