hyperframes 0.8.57 → 0.8.59

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 (182) hide show
  1. package/dist/cli.js +5300 -3428
  2. package/dist/docs/data-attributes.md +2 -1
  3. package/dist/fontLocalizeCli.js +73 -12
  4. package/dist/hyperframe-runtime.js +25 -25
  5. package/dist/hyperframe.manifest.json +1 -1
  6. package/dist/hyperframe.runtime.iife.js +25 -25
  7. package/dist/hyperframes-player.global.js +3 -3
  8. package/dist/registry/localSemantic.js +548 -0
  9. package/dist/skills/hyperframes-cli/SKILL.md +2 -2
  10. package/dist/skills/media-use/SKILL.md +96 -0
  11. package/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
  12. package/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
  13. package/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
  14. package/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
  15. package/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
  16. package/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
  17. package/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
  18. package/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
  19. package/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
  20. package/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
  21. package/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
  22. package/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
  23. package/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
  24. package/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
  25. package/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
  26. package/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
  27. package/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
  28. package/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
  29. package/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
  30. package/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
  31. package/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
  32. package/dist/skills/media-use/audio/references/bgm.md +74 -0
  33. package/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
  34. package/dist/skills/media-use/audio/references/captions/motion.md +87 -0
  35. package/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
  36. package/dist/skills/media-use/audio/references/remove-background.md +143 -0
  37. package/dist/skills/media-use/audio/references/requirements.md +29 -0
  38. package/dist/skills/media-use/audio/references/sfx.md +42 -0
  39. package/dist/skills/media-use/audio/references/transcribe.md +52 -0
  40. package/dist/skills/media-use/audio/references/tts-to-captions.md +26 -0
  41. package/dist/skills/media-use/audio/references/tts.md +165 -0
  42. package/dist/skills/media-use/audio/scripts/audio.mjs +295 -0
  43. package/dist/skills/media-use/audio/scripts/audio.test.mjs +49 -0
  44. package/dist/skills/media-use/audio/scripts/heygen-tts.mjs +121 -0
  45. package/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
  46. package/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
  47. package/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
  48. package/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +30 -0
  49. package/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
  50. package/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
  51. package/dist/skills/media-use/audio/scripts/lib/heygen.mjs +156 -0
  52. package/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +103 -0
  53. package/dist/skills/media-use/audio/scripts/lib/python.mjs +63 -0
  54. package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +68 -0
  55. package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +143 -0
  56. package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +69 -0
  57. package/dist/skills/media-use/audio/scripts/lib/tts.mjs +379 -0
  58. package/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
  59. package/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +157 -0
  60. package/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
  61. package/dist/skills/media-use/audio/scripts/wait-bgm.mjs +171 -0
  62. package/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
  63. package/dist/skills/media-use/luts/README.md +28 -0
  64. package/dist/skills/media-use/luts/index.json +57 -0
  65. package/dist/skills/media-use/references/audio.md +19 -0
  66. package/dist/skills/media-use/references/grading.md +160 -0
  67. package/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
  68. package/dist/skills/media-use/references/media-treatments.md +202 -0
  69. package/dist/skills/media-use/references/memory.md +36 -0
  70. package/dist/skills/media-use/references/meta.md +61 -0
  71. package/dist/skills/media-use/references/operations.md +325 -0
  72. package/dist/skills/media-use/references/resolve.md +150 -0
  73. package/dist/skills/media-use/references/setup-providers.md +80 -0
  74. package/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
  75. package/dist/skills/media-use/registry/localSemantic.js +548 -0
  76. package/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
  77. package/dist/skills/media-use/scripts/compatibility.test.mjs +27 -0
  78. package/dist/skills/media-use/scripts/dither.mjs +328 -0
  79. package/dist/skills/media-use/scripts/dither.test.mjs +176 -0
  80. package/dist/skills/media-use/scripts/eval.mjs +369 -0
  81. package/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
  82. package/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
  83. package/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
  84. package/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
  85. package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
  86. package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
  87. package/dist/skills/media-use/scripts/lib/cache.mjs +150 -0
  88. package/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
  89. package/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
  90. package/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
  91. package/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
  92. package/dist/skills/media-use/scripts/lib/coverage.test.mjs +132 -0
  93. package/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
  94. package/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
  95. package/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
  96. package/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
  97. package/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
  98. package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
  99. package/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
  100. package/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
  101. package/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
  102. package/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
  103. package/dist/skills/media-use/scripts/lib/freeze.mjs +62 -0
  104. package/dist/skills/media-use/scripts/lib/freeze.test.mjs +46 -0
  105. package/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
  106. package/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
  107. package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
  108. package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
  109. package/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
  110. package/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
  111. package/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
  112. package/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
  113. package/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
  114. package/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
  115. package/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
  116. package/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
  117. package/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
  118. package/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
  119. package/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
  120. package/dist/skills/media-use/scripts/lib/logo-provider.mjs +223 -0
  121. package/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +154 -0
  122. package/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
  123. package/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
  124. package/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
  125. package/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
  126. package/dist/skills/media-use/scripts/lib/manifest.mjs +229 -0
  127. package/dist/skills/media-use/scripts/lib/manifest.test.mjs +388 -0
  128. package/dist/skills/media-use/scripts/lib/match.mjs +47 -0
  129. package/dist/skills/media-use/scripts/lib/media-fetch.mjs +71 -0
  130. package/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +169 -0
  131. package/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
  132. package/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
  133. package/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
  134. package/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
  135. package/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
  136. package/dist/skills/media-use/scripts/lib/misses.test.mjs +78 -0
  137. package/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
  138. package/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
  139. package/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
  140. package/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
  141. package/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
  142. package/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
  143. package/dist/skills/media-use/scripts/lib/probe.mjs +39 -0
  144. package/dist/skills/media-use/scripts/lib/probe.test.mjs +31 -0
  145. package/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
  146. package/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
  147. package/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
  148. package/dist/skills/media-use/scripts/lib/registry.mjs +218 -0
  149. package/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
  150. package/dist/skills/media-use/scripts/lib/search.mjs +24 -0
  151. package/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
  152. package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
  153. package/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
  154. package/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
  155. package/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
  156. package/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
  157. package/dist/skills/media-use/scripts/lib/telemetry.mjs +193 -0
  158. package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +373 -0
  159. package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +92 -0
  160. package/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
  161. package/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
  162. package/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
  163. package/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
  164. package/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
  165. package/dist/skills/media-use/scripts/lib/words.mjs +18 -0
  166. package/dist/skills/media-use/scripts/prefs.mjs +70 -0
  167. package/dist/skills/media-use/scripts/recipe.mjs +103 -0
  168. package/dist/skills/media-use/scripts/resolve.mjs +1287 -0
  169. package/dist/skills/media-use/scripts/transcribe.mjs +180 -0
  170. package/dist/skills/media-use/scripts/transcript-cut.mjs +239 -0
  171. package/dist/skills/registry/localSemantic.js +548 -0
  172. package/dist/studio/assets/{hyperframes-player-c23wnIfc.js → hyperframes-player-BBub3ZdI.js} +3 -3
  173. package/dist/studio/assets/{index-IDNr3X1H.js → index-BxZ5Cw5w.js} +1 -1
  174. package/dist/studio/assets/{index-DgkAzx3n.js → index-CI7WON-E.js} +1 -1
  175. package/dist/studio/assets/{index-gbOCDCuZ.js → index-D1KNPijK.js} +192 -192
  176. package/dist/studio/assets/{index-DKJpHYYd.css → index-DZBHG9TW.css} +1 -1
  177. package/dist/studio/index.d.ts +577 -8
  178. package/dist/studio/index.html +2 -2
  179. package/dist/studio/index.js +22537 -21600
  180. package/dist/studio/index.js.map +1 -1
  181. package/dist/studio/styles.css +1 -1
  182. package/package.json +3 -3
@@ -0,0 +1,121 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { test } from "node:test";
3
+ import { runLocalModel } from "./local-run.mjs";
4
+
5
+ const strongCpu = { ramMB: 16000, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
6
+ const tiny = { ramMB: 512, gpu: { present: false, vramMB: 0 }, appleSilicon: false };
7
+ const ok = () => {}; // which/exec that succeed
8
+
9
+ test("recommends the CLI path when no local tier fits the machine", () => {
10
+ const r = runLocalModel("tts", { specs: tiny, which: ok, exec: ok });
11
+ assert.equal(r.recommend, "cli");
12
+ });
13
+
14
+ test("recommends install when the tool is not on PATH", () => {
15
+ const r = runLocalModel("tts", {
16
+ specs: strongCpu,
17
+ which: () => {
18
+ throw new Error("not found");
19
+ },
20
+ exec: ok,
21
+ vars: { text: "hi", out: "/tmp/v.wav" },
22
+ });
23
+ assert.equal(r.recommend, "install");
24
+ assert.equal(r.model, "kokoro");
25
+ assert.match(r.command, /pip install kokoro/);
26
+ });
27
+
28
+ test("runs the model and returns the output path when installed", () => {
29
+ let ran = "";
30
+ const r = runLocalModel("tts", {
31
+ specs: strongCpu,
32
+ which: ok,
33
+ exec: (cmd) => {
34
+ ran = cmd;
35
+ },
36
+ vars: { text: "hello world", voice: "af_heart", out: "/tmp/v.wav" },
37
+ });
38
+ assert.equal(r.model, "kokoro");
39
+ assert.equal(r.out, "/tmp/v.wav");
40
+ assert.match(ran, /hello world/, "invoke template filled with vars");
41
+ assert.match(ran, /\/tmp\/v\.wav/);
42
+ });
43
+
44
+ test("a failing run degrades to an install recommendation, never throws", () => {
45
+ const r = runLocalModel("upscale", {
46
+ specs: strongCpu,
47
+ which: ok,
48
+ exec: () => {
49
+ throw new Error("boom");
50
+ },
51
+ vars: { in: "a.png", out: "b.png" },
52
+ });
53
+ assert.equal(r.recommend, "install");
54
+ });
55
+
56
+ test("a tier whose tool is missing demotes to the next tier that fits", () => {
57
+ // 64GB + GPU fits BOTH tts tiers, so the ladder has two rungs: fish-speech
58
+ // (its own binary) above Kokoro (`python -m kokoro`). fish-speech absent must
59
+ // not cost the user Kokoro.
60
+ const strongGpu = { ramMB: 64000, gpu: { present: true, vramMB: 24000 } };
61
+ let ran = "";
62
+ const r = runLocalModel("tts", {
63
+ specs: strongGpu,
64
+ which: (bin) => {
65
+ if (bin === "fish-speech") throw new Error("not found");
66
+ },
67
+ exec: (cmd) => {
68
+ ran = cmd;
69
+ },
70
+ vars: { text: "hello", voice: "af_heart", out: "/tmp/v.wav" },
71
+ });
72
+
73
+ assert.equal(r.model, "kokoro", "demoted past the missing fish-speech binary");
74
+ assert.equal(r.tier, "medium");
75
+ assert.match(ran, /kokoro/);
76
+ });
77
+
78
+ test("every fitting tier failing reports the last tier's install command", () => {
79
+ const strongGpu = { ramMB: 64000, gpu: { present: true, vramMB: 24000 } };
80
+ const r = runLocalModel("tts", {
81
+ specs: strongGpu,
82
+ which: ok,
83
+ exec: () => {
84
+ throw new Error("boom");
85
+ },
86
+ vars: { text: "hi", out: "/tmp/v.wav" },
87
+ });
88
+
89
+ assert.equal(r.recommend, "install");
90
+ assert.equal(r.model, "kokoro", "the smallest fitting tier is the actionable one");
91
+ });
92
+
93
+ test("the install recommendation states the download size", () => {
94
+ // nothing today tells the user what they are agreeing to before a tool
95
+ // starts pulling weights, so the size travels in the payload AND in the
96
+ // text a caller shows them
97
+ const r = runLocalModel("tts", {
98
+ specs: strongCpu,
99
+ which: () => {
100
+ throw new Error("not found");
101
+ },
102
+ });
103
+
104
+ assert.equal(r.recommend, "install");
105
+ assert.equal(typeof r.sizeMB, "number");
106
+ assert.ok(r.sizeMB > 0);
107
+ assert.match(r.reason, /GB to download/);
108
+ });
109
+
110
+ test("a failed run still reports the tier's size", () => {
111
+ const r = runLocalModel("tts", {
112
+ specs: strongCpu,
113
+ which: ok,
114
+ exec: () => {
115
+ throw new Error("boom");
116
+ },
117
+ });
118
+
119
+ assert.equal(r.recommend, "install");
120
+ assert.equal(typeof r.sizeMB, "number");
121
+ });
@@ -0,0 +1,223 @@
1
+ import { fetchMedia } from "./media-fetch.mjs";
2
+ // Official brand marks — the `logo` type's provider tiers, tried in registry
3
+ // order. Every tier was verified against a 54-brand stress test (2026-07,
4
+ // 100% cascade hit). Hit counts below are a snapshot of that run — they
5
+ // drift as the alias/org maps grow; re-run the stress test to refresh them.
6
+ //
7
+ // 1. svgl — official full-color vector SVGs (+ wordmark variants);
8
+ // 40/54 first-hits. Search is substring-based, so
9
+ // entities go through alias normalization first
10
+ // ("nextjs" never matches "Next.js" raw).
11
+ // 2. simple-icons — monochrome official glyphs; caught the long tail the
12
+ // others miss (nike, visa, toyota, wechat, bytedance).
13
+ // Pinned CDN build for determinism.
14
+ // 3. github avatar — the org's official logo for brands with a GitHub
15
+ // presence. Known orgs only: guessing a login risks a
16
+ // same-named personal account.
17
+ // 4. domain favicon — small-raster last resort (DuckDuckGo ip3). Responses
18
+ // under ~500B are DDG's globe placeholder, not a hit.
19
+ //
20
+ // HeyGen asset search is deliberately absent: for brand queries it returns
21
+ // generic look-alike icons (0/3 in testing) — worse than a miss. A total miss
22
+ // falls through to resolve's normal failure path (`no provider could resolve
23
+ // logo`, exit 1).
24
+
25
+ import { mkdtempSync, writeFileSync } from "node:fs";
26
+ import { tmpdir } from "node:os";
27
+ import { join } from "node:path";
28
+
29
+ const SVGL_API = "https://api.svgl.app";
30
+ const SIMPLE_ICONS_CDN = "https://cdn.jsdelivr.net/npm/simple-icons@16.25.0/icons";
31
+ const FAVICON_MIN_BYTES = 500;
32
+
33
+ // svgl search queries per entity, tried in order after the raw entity.
34
+ const SVGL_ALIASES = {
35
+ nextjs: ["next.js", "next"],
36
+ aws: ["amazon web services"],
37
+ huggingface: ["hugging face"],
38
+ cocacola: ["coca-cola"],
39
+ mcdonalds: ["mcdonald's"],
40
+ };
41
+
42
+ // simple-icons slugs that differ from the normalized entity.
43
+ const SIMPLE_ICON_SLUGS = {
44
+ nextjs: "nextdotjs",
45
+ aws: "amazonwebservices",
46
+ };
47
+
48
+ // Known GitHub orgs. Only mapped entities resolve at this tier — a brand name
49
+ // is NOT a GitHub login, and guessing hits same-named personal accounts.
50
+ const GITHUB_ORGS = {
51
+ slack: "slackhq",
52
+ meta: "facebook",
53
+ google: "google",
54
+ microsoft: "microsoft",
55
+ aws: "aws",
56
+ vercel: "vercel",
57
+ nextjs: "vercel",
58
+ alibaba: "alibaba",
59
+ heygen: "heygen-com",
60
+ };
61
+
62
+ // Favicon domains that aren't `<entity>.com`.
63
+ const FAVICON_DOMAINS = {
64
+ cocacola: "coca-cola.com",
65
+ aws: "aws.amazon.com",
66
+ nextjs: "nextjs.org",
67
+ };
68
+
69
+ const norm = (s) =>
70
+ String(s)
71
+ .toLowerCase()
72
+ .replace(/[^a-z0-9]/g, "");
73
+
74
+ /** The brand entity for a query: --entity wins; else the intent minus filler. */
75
+ export function entityFrom(intent, entity) {
76
+ if (entity) return entity.toLowerCase().trim();
77
+ return String(intent)
78
+ .toLowerCase()
79
+ .replace(/\b(logo|logos|icon|brand|official|mark)\b/g, "")
80
+ .trim()
81
+ .replace(/\s+/g, " ");
82
+ }
83
+
84
+ /** Exact match after stripping case/spacing/punctuation — "Next.js" ≡ "nextjs". */
85
+ export function titleMatches(title, entity) {
86
+ return norm(title) === norm(entity);
87
+ }
88
+
89
+ export function svglQueriesFor(entity) {
90
+ return [entity, ...(SVGL_ALIASES[norm(entity)] || [])];
91
+ }
92
+
93
+ export function simpleIconSlugsFor(entity) {
94
+ const slugs = [norm(entity)];
95
+ const alias = SIMPLE_ICON_SLUGS[norm(entity)];
96
+ if (alias) slugs.push(alias);
97
+ return slugs;
98
+ }
99
+
100
+ export function githubOrgFor(entity) {
101
+ return GITHUB_ORGS[norm(entity)] || null;
102
+ }
103
+
104
+ export function faviconDomainFor(entity) {
105
+ return FAVICON_DOMAINS[norm(entity)] || `${norm(entity)}.com`;
106
+ }
107
+
108
+ async function fetchJson(url) {
109
+ const res = await fetchMedia(url, { signal: AbortSignal.timeout(10_000) });
110
+ if (!res.ok) return null;
111
+ return res.json();
112
+ }
113
+
114
+ async function urlExists(url) {
115
+ const res = await fetchMedia(url, { method: "HEAD", signal: AbortSignal.timeout(10_000) });
116
+ return res.ok;
117
+ }
118
+
119
+ export async function svglSearch(intent, ctx = {}) {
120
+ const entity = entityFrom(intent, ctx.entity);
121
+ for (const q of svglQueriesFor(entity)) {
122
+ let items;
123
+ try {
124
+ items = await fetchJson(`${SVGL_API}?search=${encodeURIComponent(q)}`);
125
+ } catch {
126
+ return null; // network down — let the next tier try its own host
127
+ }
128
+ if (!Array.isArray(items)) continue;
129
+ const hit = items.find((it) => titleMatches(it.title, q) || titleMatches(it.title, entity));
130
+ if (!hit) continue;
131
+ const route = typeof hit.route === "string" ? hit.route : hit.route?.light;
132
+ if (!route) continue;
133
+ return {
134
+ url: route,
135
+ ext: ".svg",
136
+ source: "search",
137
+ metadata: {
138
+ description: `${hit.title} logo (official mark)`,
139
+ provider: "svgl",
140
+ provenance: { entity, query: q, route, wordmark: Boolean(hit.wordmark) },
141
+ },
142
+ };
143
+ }
144
+ return null;
145
+ }
146
+
147
+ export async function simpleIconsSearch(intent, ctx = {}) {
148
+ const entity = entityFrom(intent, ctx.entity);
149
+ for (const slug of simpleIconSlugsFor(entity)) {
150
+ const url = `${SIMPLE_ICONS_CDN}/${slug}.svg`;
151
+ let ok;
152
+ try {
153
+ ok = await urlExists(url);
154
+ } catch {
155
+ return null;
156
+ }
157
+ if (!ok) continue;
158
+ return {
159
+ url,
160
+ ext: ".svg",
161
+ source: "search",
162
+ metadata: {
163
+ description: `${entity} logo (official monochrome glyph)`,
164
+ provider: "simple-icons",
165
+ provenance: { entity, slug, pinned: "simple-icons@16.25.0" },
166
+ },
167
+ };
168
+ }
169
+ return null;
170
+ }
171
+
172
+ export async function githubAvatarSearch(intent, ctx = {}) {
173
+ const entity = entityFrom(intent, ctx.entity);
174
+ const org = githubOrgFor(entity);
175
+ if (!org) return null;
176
+ const url = `https://github.com/${org}.png?size=460`;
177
+ try {
178
+ if (!(await urlExists(url))) return null;
179
+ } catch {
180
+ return null;
181
+ }
182
+ return {
183
+ url,
184
+ ext: ".png",
185
+ source: "search",
186
+ metadata: {
187
+ description: `${entity} logo (GitHub org avatar)`,
188
+ provider: "github.avatar",
189
+ provenance: { entity, org },
190
+ },
191
+ };
192
+ }
193
+
194
+ export async function faviconSearch(intent, ctx = {}) {
195
+ const entity = entityFrom(intent, ctx.entity);
196
+ const domain = faviconDomainFor(entity);
197
+ const url = `https://icons.duckduckgo.com/ip3/${domain}.ico`;
198
+ let body;
199
+ try {
200
+ const res = await fetchMedia(url, { signal: AbortSignal.timeout(10_000) });
201
+ if (!res.ok) return null;
202
+ body = Buffer.from(await res.arrayBuffer());
203
+ } catch {
204
+ return null;
205
+ }
206
+ if (body.byteLength < FAVICON_MIN_BYTES) return null; // DDG placeholder, not a logo
207
+ // Hand the verified bytes over as a local file: the freeze step copies it
208
+ // instead of re-downloading, so the size check is authoritative over what
209
+ // gets frozen and the favicon tier costs one network round-trip, not two.
210
+ const bytes = body.byteLength;
211
+ const tmp = join(mkdtempSync(join(tmpdir(), "media-use-logo-")), `${domain}.ico`);
212
+ writeFileSync(tmp, body);
213
+ return {
214
+ localPath: tmp,
215
+ ext: ".ico",
216
+ source: "search",
217
+ metadata: {
218
+ description: `${entity} favicon (small raster — chip-size use only)`,
219
+ provider: "favicon.ddg",
220
+ provenance: { entity, domain, bytes, low_res: true },
221
+ },
222
+ };
223
+ }
@@ -0,0 +1,154 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert";
3
+ import { readFileSync } from "node:fs";
4
+ import {
5
+ entityFrom,
6
+ titleMatches,
7
+ svglQueriesFor,
8
+ simpleIconSlugsFor,
9
+ githubOrgFor,
10
+ faviconDomainFor,
11
+ svglSearch,
12
+ simpleIconsSearch,
13
+ githubAvatarSearch,
14
+ faviconSearch,
15
+ } from "./logo-provider.mjs";
16
+ import { getProviders, runProviders } from "./registry.mjs";
17
+
18
+ test("entityFrom strips filler words from the intent; --entity wins", () => {
19
+ assert.equal(entityFrom("LinkedIn logo"), "linkedin");
20
+ assert.equal(entityFrom("official Slack brand mark"), "slack");
21
+ assert.equal(entityFrom("anything", "Notion"), "notion");
22
+ });
23
+
24
+ test("titleMatches ignores case, spacing, punctuation — and rejects lookalikes", () => {
25
+ assert.ok(titleMatches("Next.js", "nextjs"));
26
+ assert.ok(titleMatches("Coca-Cola", "coca cola"));
27
+ assert.ok(!titleMatches("Slackware", "slack"));
28
+ });
29
+
30
+ test("svgl queries include the alias forms the raw entity can't match", () => {
31
+ assert.ok(svglQueriesFor("nextjs").includes("next.js"));
32
+ assert.ok(svglQueriesFor("aws").includes("amazon web services"));
33
+ assert.deepEqual(svglQueriesFor("figma"), ["figma"]);
34
+ });
35
+
36
+ test("simple-icons slugs cover the renamed entries", () => {
37
+ assert.ok(simpleIconSlugsFor("nextjs").includes("nextdotjs"));
38
+ assert.ok(simpleIconSlugsFor("aws").includes("amazonwebservices"));
39
+ assert.deepEqual(simpleIconSlugsFor("nike"), ["nike"]);
40
+ });
41
+
42
+ test("github avatar tier never guesses an org", () => {
43
+ assert.equal(githubOrgFor("slack"), "slackhq");
44
+ assert.equal(githubOrgFor("heygen"), "heygen-com");
45
+ assert.equal(githubOrgFor("some-random-startup"), null);
46
+ });
47
+
48
+ test("favicon domain defaults to <entity>.com with explicit overrides", () => {
49
+ assert.equal(faviconDomainFor("cocacola"), "coca-cola.com");
50
+ assert.equal(faviconDomainFor("stripe"), "stripe.com");
51
+ });
52
+
53
+ // --- async tiers, network mocked -------------------------------------------
54
+ // The 54-brand stress test is a manual snapshot; these pin the same behavior
55
+ // as CI gates: descriptor shape, alias retry, error→null fallthrough, the
56
+ // placeholder filter, and the real cascade order under a mocked network.
57
+
58
+ const json = (data) => new Response(JSON.stringify(data), { status: 200 });
59
+ const status = (code) => new Response(null, { status: code });
60
+ const bin = (n) => new Response(new Uint8Array(n), { status: 200 });
61
+
62
+ test("svglSearch returns the descriptor shape on an exact title hit", async (t) => {
63
+ t.mock.method(globalThis, "fetch", async () =>
64
+ json([{ title: "Figma", route: "https://svgl.app/library/figma.svg" }]),
65
+ );
66
+ const res = await svglSearch("Figma logo", {});
67
+ assert.equal(res.url, "https://svgl.app/library/figma.svg");
68
+ assert.equal(res.ext, ".svg");
69
+ assert.equal(res.metadata.provider, "svgl");
70
+ });
71
+
72
+ test("svglSearch skips a non-array payload and retries with the alias query", async (t) => {
73
+ const seen = [];
74
+ t.mock.method(globalThis, "fetch", async (url) => {
75
+ seen.push(decodeURIComponent(String(url)));
76
+ return seen.length === 1
77
+ ? json({ error: "unexpected shape" })
78
+ : json([{ title: "Next.js", route: "https://svgl.app/library/nextjs.svg" }]);
79
+ });
80
+ const res = await svglSearch("nextjs logo", {});
81
+ assert.equal(res.metadata.provenance.query, "next.js", "hit came from the alias query");
82
+ assert.ok(seen.length >= 2, "raw query then alias");
83
+ });
84
+
85
+ test("svglSearch returns null when the network is down — the cascade falls through", async (t) => {
86
+ t.mock.method(globalThis, "fetch", async () => {
87
+ throw new Error("network down");
88
+ });
89
+ assert.equal(await svglSearch("figma logo", {}), null);
90
+ });
91
+
92
+ test("simpleIconsSearch falls to the next slug on a 404", async (t) => {
93
+ const seen = [];
94
+ t.mock.method(globalThis, "fetch", async (url) => {
95
+ seen.push(String(url));
96
+ return String(url).includes("amazonwebservices") ? status(200) : status(404);
97
+ });
98
+ const res = await simpleIconsSearch("aws logo", {});
99
+ assert.ok(res.url.endsWith("amazonwebservices.svg"));
100
+ assert.equal(seen.length, 2, "plain slug 404s first, alias slug hits");
101
+ });
102
+
103
+ test("faviconSearch rejects DDG's sub-500B placeholder with null", async (t) => {
104
+ t.mock.method(globalThis, "fetch", async () => bin(120));
105
+ assert.equal(await faviconSearch("someco logo", {}), null);
106
+ });
107
+
108
+ test("faviconSearch hands verified bytes over as a local file — one fetch, no re-download", async (t) => {
109
+ const fetchMock = t.mock.method(globalThis, "fetch", async () => bin(600));
110
+ const res = await faviconSearch("someco logo", {});
111
+ assert.ok(res.localPath, "returns a localPath, not a url");
112
+ assert.equal(readFileSync(res.localPath).byteLength, 600, "frozen bytes are the verified bytes");
113
+ assert.equal(fetchMock.mock.callCount(), 1, "single network round-trip");
114
+ assert.equal(res.metadata.provenance.low_res, true);
115
+ });
116
+
117
+ test("githubAvatarSearch never touches the network for an unmapped entity", async (t) => {
118
+ const fetchMock = t.mock.method(globalThis, "fetch", async () => status(200));
119
+ assert.equal(await githubAvatarSearch("some-random-startup logo", {}), null);
120
+ assert.equal(fetchMock.mock.callCount(), 0);
121
+ });
122
+
123
+ test("the real logo cascade falls through tier by tier to the first hit", async (t) => {
124
+ t.mock.method(globalThis, "fetch", async (url) => {
125
+ const u = String(url);
126
+ if (u.includes("api.svgl.app")) return json([]); // tier 1: no hit
127
+ if (u.includes("jsdelivr")) return status(404); // tier 2: no such slug
128
+ // tier 3 (github) is never called: entity is unmapped
129
+ if (u.includes("duckduckgo")) return bin(600); // tier 4: real favicon
130
+ throw new Error(`unexpected fetch: ${u}`);
131
+ });
132
+ const res = await runProviders(getProviders("logo"), "search", "zzzbrand logo", {
133
+ entity: "zzzbrand",
134
+ });
135
+ assert.ok(res, "cascade must land on the favicon tier");
136
+ assert.equal(res.metadata.provider, "favicon.ddg");
137
+ });
138
+
139
+ for (const search of [simpleIconsSearch, githubAvatarSearch]) {
140
+ test(`${search.name} rejects private HEAD redirects`, async (t) => {
141
+ const seen = [];
142
+ t.mock.method(globalThis, "fetch", async (url, options) => {
143
+ seen.push(url);
144
+ assert.equal(options.method, "HEAD");
145
+ assert.ok(options.signal);
146
+ return options.redirect === "manual"
147
+ ? new Response(null, { status: 302, headers: { location: "http://127.0.0.1/private" } })
148
+ : new Response(null, { status: 200 });
149
+ });
150
+ assert.equal(await search("vercel logo"), null);
151
+ assert.equal(seen.length, 1);
152
+ assert.ok(!seen[0].includes("127.0.0.1"));
153
+ });
154
+ }
@@ -0,0 +1,94 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, unlinkSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describeDownload, probeSpecs } from "./specs.mjs";
6
+ import { buildArgv, selectModel, selectModelLadder } from "./local-models.mjs";
7
+
8
+ export async function ltxVideoGenerate(
9
+ intent,
10
+ ctx,
11
+ execFn = execFileSync,
12
+ pathExists = existsSync,
13
+ unlinkFn = unlinkSync,
14
+ ) {
15
+ const specs = ctx?.specs || probeSpecs();
16
+ const ladder = selectModelLadder("videogen", specs, { preferTier: ctx?.preferTier });
17
+ if (!ladder.length) {
18
+ const { reason } = selectModel("videogen", specs, { preferTier: ctx?.preferTier });
19
+ console.error(
20
+ `media-use: local video gen not enabled (${reason}). Enable a fitting free on-device LTX model to use this provider.`,
21
+ );
22
+ return null;
23
+ }
24
+
25
+ // Each attempt mints its own timestamped output path, so a partial artifact
26
+ // from a failed tier is orphaned rather than overwritten - and a lower tier
27
+ // then succeeding hides it. Discard it before demoting. Best-effort: a
28
+ // partial we cannot remove must never mask the real failure.
29
+ const discardPartial = (path) => {
30
+ try {
31
+ if (pathExists(path)) unlinkFn(path);
32
+ } catch {
33
+ // nothing actionable: the generate failure below is the real story
34
+ }
35
+ };
36
+
37
+ // Walk the whole ladder, best tier first. A tier that cannot run on this
38
+ // machine for a reason no spec check sees (runner off PATH, gated weights, an
39
+ // OOM) demotes to the next fitting tier instead of failing local video gen
40
+ // outright. Every demotion is reported: a silent drop to a smaller model
41
+ // leaves the caller wondering why the output looks the way it does.
42
+ for (const model of ladder) {
43
+ const bin = model.invoke.trim().split(/\s+/)[0];
44
+ try {
45
+ execFn("which", [bin], { stdio: ["ignore", "ignore", "ignore"] });
46
+ } catch {
47
+ console.error(
48
+ `media-use: local video gen not enabled (\`${bin}\` not on PATH). Install for free on-device LTX: ${model.install}. Heads up: ${model.id} ${describeDownload(model.sizeMB)}.`,
49
+ );
50
+ continue;
51
+ }
52
+
53
+ const outPath = join(tmpdir(), `media-use-ltx-${process.pid}-${Date.now()}.mp4`);
54
+ const argv = buildArgv(model.invoke, {
55
+ prompt: intent,
56
+ w: ctx?.width || 512,
57
+ h: ctx?.height || 320,
58
+ frames: ctx?.frames || 33,
59
+ out: outPath,
60
+ });
61
+ argv.shift();
62
+
63
+ try {
64
+ execFn(bin, argv, {
65
+ encoding: "utf8",
66
+ timeout: 1_800_000,
67
+ stdio: ["ignore", "pipe", "pipe"],
68
+ });
69
+ } catch (err) {
70
+ discardPartial(outPath);
71
+ console.error(
72
+ `media-use: local video gen (${model.id}) failed: ${err.stderr?.toString().trim().slice(-200) || err.message}`,
73
+ );
74
+ continue;
75
+ }
76
+ if (!pathExists(outPath)) {
77
+ console.error(
78
+ `media-use: local video gen (${model.id}) exited cleanly but wrote no output file`,
79
+ );
80
+ continue;
81
+ }
82
+ return {
83
+ localPath: outPath,
84
+ ext: ".mp4",
85
+ source: "generated",
86
+ metadata: {
87
+ description: intent,
88
+ provider: "ltx.local",
89
+ provenance: { prompt: intent },
90
+ },
91
+ };
92
+ }
93
+ return null;
94
+ }