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,138 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { freezeUrl } from "./freeze.mjs";
6
+ import {
7
+ classifyHeygenErrorCode,
8
+ HEYGEN_AUTH_COMMAND,
9
+ HEYGEN_CLIENT_SOURCE_ARGV,
10
+ reportHeygenFailure,
11
+ runHeygenJson,
12
+ } from "./heygen-cli.mjs";
13
+
14
+ export const AVATAR_VIDEO_SIGNIN_MESSAGE = `media-use: avatar video is free for new API users — sign in: ${HEYGEN_AUTH_COMMAND}`;
15
+
16
+ // Cache only a truthy id -- a transient discovery failure must not poison the
17
+ // cache with `null` and permanently disable heygen.video for the rest of the
18
+ // process. `onError` lets the caller distinguish not_authenticated (and nudge
19
+ // onboarding) from any other discovery failure.
20
+ let cachedAvatarId;
21
+ function defaultAvatarId(onError) {
22
+ if (cachedAvatarId) return cachedAvatarId;
23
+ const j = runHeygenJson(
24
+ "heygen",
25
+ ["avatar", "list", "--ownership", "public", "--limit", "1"],
26
+ "avatar list",
27
+ onError,
28
+ );
29
+ cachedAvatarId = j?.data?.[0]?.avatar_id || null;
30
+ return cachedAvatarId;
31
+ }
32
+
33
+ let cachedStarfishVoiceId;
34
+ function defaultStarfishVoiceId(onError) {
35
+ if (cachedStarfishVoiceId) return cachedStarfishVoiceId;
36
+ const j = runHeygenJson(
37
+ "heygen",
38
+ ["voice", "list", "--engine", "starfish", "--limit", "1"],
39
+ "voice list",
40
+ onError,
41
+ );
42
+ cachedStarfishVoiceId = j?.data?.[0]?.voice_id || null;
43
+ return cachedStarfishVoiceId;
44
+ }
45
+
46
+ export async function heygenVideoGenerate(intent, ctx) {
47
+ let discoveryFailureReason = null;
48
+ const captureReason = (reason) => {
49
+ discoveryFailureReason ??= reason;
50
+ };
51
+
52
+ // Short-circuit: once one discovery call fails, the result is null either
53
+ // way, so don't attempt the second -- that would double-fire the onboarding
54
+ // message and the provider-error telemetry ping for what's really one failure.
55
+ const avatarId = ctx?.avatarId || defaultAvatarId(captureReason);
56
+ if (!avatarId) {
57
+ if (discoveryFailureReason === "not_authenticated") console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
58
+ return null;
59
+ }
60
+ const voiceId = ctx?.voiceId || defaultStarfishVoiceId(captureReason);
61
+ if (!voiceId) {
62
+ if (discoveryFailureReason === "not_authenticated") console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
63
+ return null;
64
+ }
65
+
66
+ let out;
67
+ try {
68
+ out = execFileSync(
69
+ "heygen",
70
+ [
71
+ ...HEYGEN_CLIENT_SOURCE_ARGV,
72
+ "video",
73
+ "create",
74
+ "--wait",
75
+ "-d",
76
+ JSON.stringify({
77
+ type: "avatar",
78
+ avatar_id: avatarId,
79
+ script: intent,
80
+ voice_id: voiceId,
81
+ }),
82
+ ],
83
+ {
84
+ encoding: "utf8",
85
+ timeout: 300000,
86
+ stdio: ["pipe", "pipe", "pipe"],
87
+ },
88
+ );
89
+ } catch (err) {
90
+ if (classifyHeygenErrorCode(err) === "not_authenticated") {
91
+ console.error(AVATAR_VIDEO_SIGNIN_MESSAGE);
92
+ }
93
+ reportHeygenFailure(err, "heygen video create");
94
+ return null;
95
+ }
96
+
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(out);
100
+ } catch {
101
+ console.error("media-use: `heygen video create` returned invalid JSON");
102
+ return null;
103
+ }
104
+ const videoUrl = parsed?.data?.video_url;
105
+ if (typeof videoUrl !== "string" || !videoUrl) {
106
+ console.error("media-use: `heygen video create` returned no video URL");
107
+ return null;
108
+ }
109
+
110
+ let tmpDir;
111
+ let tmpPath;
112
+ try {
113
+ tmpDir = mkdtempSync(join(tmpdir(), "media-use-heygen-video-"));
114
+ tmpPath = join(tmpDir, "video.mp4");
115
+ await freezeUrl(videoUrl, tmpPath);
116
+ } catch (err) {
117
+ if (tmpDir) {
118
+ try {
119
+ rmSync(tmpDir, { recursive: true, force: true });
120
+ } catch {
121
+ // Preserve the download failure if temporary-directory cleanup fails.
122
+ }
123
+ }
124
+ console.error(`media-use: heygen video download failed: ${err.message}`);
125
+ return null;
126
+ }
127
+
128
+ return {
129
+ localPath: tmpPath,
130
+ ext: ".mp4",
131
+ source: "generated",
132
+ metadata: {
133
+ description: intent,
134
+ provider: "heygen.video",
135
+ provenance: { prompt: intent },
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,435 @@
1
+ import { strict as assert } from "node:assert";
2
+ import {
3
+ chmodSync,
4
+ mkdtempSync,
5
+ readFileSync,
6
+ rmSync,
7
+ statSync,
8
+ symlinkSync,
9
+ readdirSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import http from "node:http";
13
+ import { tmpdir } from "node:os";
14
+ import { basename, dirname, join } from "node:path";
15
+ import { test } from "node:test";
16
+ import { HEYGEN_NOT_AUTHENTICATED_MESSAGE } from "./heygen-cli.mjs";
17
+ import { AVATAR_VIDEO_SIGNIN_MESSAGE } from "./heygen-video-provider.mjs";
18
+
19
+ function cleanupDownload(localPath) {
20
+ if (
21
+ basename(localPath) === "video.mp4" &&
22
+ basename(dirname(localPath)).startsWith("media-use-heygen-video-")
23
+ ) {
24
+ rmSync(dirname(localPath), { recursive: true, force: true });
25
+ } else {
26
+ rmSync(localPath, { force: true });
27
+ }
28
+ }
29
+
30
+ const VIDEO_FIXTURE = Buffer.from("tiny heygen video fixture");
31
+ let importCount = 0;
32
+
33
+ async function freshGenerate() {
34
+ importCount += 1;
35
+ const module = await import(`./heygen-video-provider.mjs?test=${importCount}`);
36
+ return module.heygenVideoGenerate;
37
+ }
38
+
39
+ async function listenVideoServer(t) {
40
+ const server = http.createServer((req, res) => {
41
+ if (req.url !== "/video.mp4") {
42
+ res.writeHead(404).end();
43
+ return;
44
+ }
45
+ res.writeHead(200, {
46
+ "content-length": VIDEO_FIXTURE.length,
47
+ "content-type": "video/mp4",
48
+ });
49
+ res.end(VIDEO_FIXTURE);
50
+ });
51
+ await new Promise((resolve) => server.listen(0, resolve));
52
+ const address = server.address();
53
+ assert.ok(address && typeof address !== "string");
54
+ // Keep the provider URL public; only the test transport reaches the local fixture.
55
+ const url = "https://media.example.com/video.mp4";
56
+ const originalFetch = globalThis.fetch;
57
+ t.mock.method(globalThis, "fetch", (requested, options) => {
58
+ assert.equal(requested, url);
59
+ assert.equal(options.redirect, "manual");
60
+ return originalFetch(`http://127.0.0.1:${address.port}/video.mp4`, options);
61
+ });
62
+ return { server, url };
63
+ }
64
+
65
+ async function listenFailingVideoServer(t) {
66
+ const server = http.createServer((req, res) => {
67
+ res.writeHead(500).end();
68
+ });
69
+ await new Promise((resolve) => server.listen(0, resolve));
70
+ const address = server.address();
71
+ assert.ok(address && typeof address !== "string");
72
+ // Keep the provider URL public; only the test transport reaches the local fixture.
73
+ const url = "https://media.example.com/video.mp4";
74
+ const originalFetch = globalThis.fetch;
75
+ t.mock.method(globalThis, "fetch", (requested, options) => {
76
+ assert.equal(requested, url);
77
+ assert.equal(options.redirect, "manual");
78
+ return originalFetch(`http://127.0.0.1:${address.port}/video.mp4`, options);
79
+ });
80
+ return { server, url };
81
+ }
82
+
83
+ function closeServer(server) {
84
+ return new Promise((resolve, reject) => {
85
+ server.close((err) => (err ? reject(err) : resolve()));
86
+ });
87
+ }
88
+
89
+ async function withFakeHeygen(options, run) {
90
+ const dir = mkdtempSync(join(tmpdir(), "media-use-heygen-video-provider-"));
91
+ const capturePath = join(dir, "argv.log");
92
+ const heygenPath = join(dir, "heygen");
93
+ const previousEnv = {
94
+ PATH: process.env.PATH,
95
+ HEYGEN_CAPTURE_PATH: process.env.HEYGEN_CAPTURE_PATH,
96
+ HEYGEN_VIDEO_MODE: process.env.HEYGEN_VIDEO_MODE,
97
+ HEYGEN_VIDEO_RESPONSE: process.env.HEYGEN_VIDEO_RESPONSE,
98
+ HEYGEN_DISCOVERY_MODE: process.env.HEYGEN_DISCOVERY_MODE,
99
+ HYPERFRAMES_NO_TELEMETRY: process.env.HYPERFRAMES_NO_TELEMETRY,
100
+ };
101
+
102
+ writeFileSync(
103
+ heygenPath,
104
+ `#!/bin/sh
105
+ printf '%s\\n' "$*" >> "$HEYGEN_CAPTURE_PATH"
106
+ case "$*" in
107
+ *"avatar list"*)
108
+ case "$HEYGEN_DISCOVERY_MODE" in
109
+ auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
110
+ *) printf '%s\\n' '{"data":[{"avatar_id":"avatar-public-1"}]}' ;;
111
+ esac
112
+ ;;
113
+ *"voice list"*)
114
+ case "$HEYGEN_DISCOVERY_MODE" in
115
+ auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
116
+ *) printf '%s\\n' '{"data":[{"voice_id":"voice-starfish-1"}]}' ;;
117
+ esac
118
+ ;;
119
+ *"video create"*)
120
+ case "$HEYGEN_VIDEO_MODE" in
121
+ auth) printf '%s\\n' 'HTTP 401 Unauthorized' >&2; exit 1 ;;
122
+ other) printf '%s\\n' 'provider unavailable' >&2; exit 1 ;;
123
+ *) printf '%s\\n' "$HEYGEN_VIDEO_RESPONSE" ;;
124
+ esac
125
+ ;;
126
+ esac
127
+ `,
128
+ );
129
+ chmodSync(heygenPath, 0o755);
130
+ process.env.PATH = `${dir}:${previousEnv.PATH ?? ""}`;
131
+ process.env.HEYGEN_CAPTURE_PATH = capturePath;
132
+ process.env.HEYGEN_VIDEO_MODE = options.mode ?? "success";
133
+ process.env.HEYGEN_VIDEO_RESPONSE = options.response ?? "";
134
+ process.env.HEYGEN_DISCOVERY_MODE = options.discoveryMode ?? "";
135
+ process.env.HYPERFRAMES_NO_TELEMETRY = "1";
136
+
137
+ try {
138
+ return await run({
139
+ invocations: () => readFileSync(capturePath, "utf8").trim().split("\n"),
140
+ });
141
+ } finally {
142
+ for (const [key, value] of Object.entries(previousEnv)) {
143
+ if (value === undefined) delete process.env[key];
144
+ else process.env[key] = value;
145
+ }
146
+ rmSync(dir, { recursive: true, force: true });
147
+ }
148
+ }
149
+
150
+ function bodyFromInvocation(invocation) {
151
+ const marker = " -d ";
152
+ const start = invocation.indexOf(marker);
153
+ assert.notEqual(start, -1);
154
+ return JSON.parse(invocation.slice(start + marker.length));
155
+ }
156
+
157
+ test("downloads a generated avatar video and returns the generated MP4 result", async (t) => {
158
+ const { server, url } = await listenVideoServer(t);
159
+ let localPath;
160
+ try {
161
+ await withFakeHeygen(
162
+ { response: JSON.stringify({ data: { video_url: url } }) },
163
+ async ({ invocations }) => {
164
+ const heygenVideoGenerate = await freshGenerate();
165
+ const intent = "Welcome to the HyperFrames launch";
166
+ const result = await heygenVideoGenerate(intent, {});
167
+ localPath = result?.localPath;
168
+ const calls = invocations();
169
+ const create = calls.find((call) => call.includes("video create"));
170
+
171
+ assert.ok(create);
172
+ assert.match(create, /--headers X-HeyGen-Client-Source: media-use/);
173
+ assert.deepEqual(bodyFromInvocation(create), {
174
+ type: "avatar",
175
+ avatar_id: "avatar-public-1",
176
+ script: intent,
177
+ voice_id: "voice-starfish-1",
178
+ });
179
+ assert.ok(result);
180
+ assert.equal(join(tmpdir(), result.localPath.slice(tmpdir().length + 1)), result.localPath);
181
+ assert.match(result.localPath, /media-use-heygen-video-[^/\\]+[/\\]video\.mp4$/);
182
+ if (process.platform !== "win32") {
183
+ assert.equal(statSync(dirname(result.localPath)).mode & 0o777, 0o700);
184
+ }
185
+ assert.deepEqual(result, {
186
+ localPath: result.localPath,
187
+ ext: ".mp4",
188
+ source: "generated",
189
+ metadata: {
190
+ description: intent,
191
+ provider: "heygen.video",
192
+ provenance: { prompt: intent },
193
+ },
194
+ });
195
+ assert.deepEqual(readFileSync(result.localPath), VIDEO_FIXTURE);
196
+ },
197
+ );
198
+ } finally {
199
+ if (localPath) cleanupDownload(localPath);
200
+ await closeServer(server);
201
+ }
202
+ });
203
+
204
+ test("tags video creation but not avatar or voice discovery", async (t) => {
205
+ const { server, url } = await listenVideoServer(t);
206
+ let localPath;
207
+ try {
208
+ await withFakeHeygen(
209
+ { response: JSON.stringify({ data: { video_url: url } }) },
210
+ async ({ invocations }) => {
211
+ const heygenVideoGenerate = await freshGenerate();
212
+ const result = await heygenVideoGenerate("Header regression guard", {});
213
+ localPath = result?.localPath;
214
+ const calls = invocations();
215
+
216
+ assert.equal(calls.length, 3);
217
+ assert.match(calls[0], /^avatar list --ownership public --limit 1$/);
218
+ assert.doesNotMatch(calls[0], /X-HeyGen-Client-Source/);
219
+ assert.match(calls[1], /^voice list --engine starfish --limit 1$/);
220
+ assert.doesNotMatch(calls[1], /X-HeyGen-Client-Source/);
221
+ assert.match(calls[2], /video create/);
222
+ assert.match(calls[2], /X-HeyGen-Client-Source: media-use/);
223
+ },
224
+ );
225
+ } finally {
226
+ if (localPath) cleanupDownload(localPath);
227
+ await closeServer(server);
228
+ }
229
+ });
230
+
231
+ test("uses explicit avatar and voice overrides without discovery", async (t) => {
232
+ const { server, url } = await listenVideoServer(t);
233
+ let localPath;
234
+ try {
235
+ await withFakeHeygen(
236
+ { response: JSON.stringify({ data: { video_url: url } }) },
237
+ async ({ invocations }) => {
238
+ const heygenVideoGenerate = await freshGenerate();
239
+ const result = await heygenVideoGenerate("Use my presenter", {
240
+ avatarId: "avatar-override",
241
+ voiceId: "voice-override",
242
+ });
243
+ localPath = result?.localPath;
244
+ const calls = invocations();
245
+
246
+ assert.equal(calls.length, 1);
247
+ assert.match(calls[0], /video create/);
248
+ assert.deepEqual(bodyFromInvocation(calls[0]), {
249
+ type: "avatar",
250
+ avatar_id: "avatar-override",
251
+ script: "Use my presenter",
252
+ voice_id: "voice-override",
253
+ });
254
+ },
255
+ );
256
+ } finally {
257
+ if (localPath) cleanupDownload(localPath);
258
+ await closeServer(server);
259
+ }
260
+ });
261
+
262
+ test("caches discovered avatar and voice IDs for the process", async (t) => {
263
+ const { server, url } = await listenVideoServer(t);
264
+ const localPaths = new Set();
265
+ try {
266
+ await withFakeHeygen(
267
+ { response: JSON.stringify({ data: { video_url: url } }) },
268
+ async ({ invocations }) => {
269
+ const heygenVideoGenerate = await freshGenerate();
270
+ for (const intent of ["First avatar video", "Second avatar video"]) {
271
+ const result = await heygenVideoGenerate(intent, {});
272
+ if (result) localPaths.add(result.localPath);
273
+ }
274
+ const calls = invocations();
275
+
276
+ assert.equal(calls.filter((call) => call.startsWith("avatar list ")).length, 1);
277
+ assert.equal(calls.filter((call) => call.startsWith("voice list ")).length, 1);
278
+ assert.equal(calls.filter((call) => call.includes("video create")).length, 2);
279
+ },
280
+ );
281
+ } finally {
282
+ for (const localPath of localPaths) cleanupDownload(localPath);
283
+ await closeServer(server);
284
+ }
285
+ });
286
+
287
+ test("prints auth onboarding and reports an unauthenticated create failure", async (t) => {
288
+ const errors = [];
289
+ t.mock.method(console, "error", (message) => errors.push(message));
290
+
291
+ await withFakeHeygen({ mode: "auth" }, async () => {
292
+ const heygenVideoGenerate = await freshGenerate();
293
+ const result = await heygenVideoGenerate("Sign-in failure", {
294
+ avatarId: "avatar-override",
295
+ voiceId: "voice-override",
296
+ });
297
+
298
+ assert.equal(result, null);
299
+ assert.ok(errors.includes(AVATAR_VIDEO_SIGNIN_MESSAGE));
300
+ assert.ok(errors.includes(HEYGEN_NOT_AUTHENTICATED_MESSAGE));
301
+ });
302
+ });
303
+
304
+ test("reports other create failures without auth onboarding", async (t) => {
305
+ const errors = [];
306
+ t.mock.method(console, "error", (message) => errors.push(message));
307
+
308
+ await withFakeHeygen({ mode: "other" }, async () => {
309
+ const heygenVideoGenerate = await freshGenerate();
310
+ const result = await heygenVideoGenerate("Provider failure", {
311
+ avatarId: "avatar-override",
312
+ voiceId: "voice-override",
313
+ });
314
+
315
+ assert.equal(result, null);
316
+ assert.ok(!errors.includes(AVATAR_VIDEO_SIGNIN_MESSAGE));
317
+ assert.ok(errors.includes("media-use: `heygen video create` failed: provider unavailable"));
318
+ });
319
+ });
320
+
321
+ test("falls through on non-JSON and error responses", async (t) => {
322
+ t.mock.method(console, "error", () => {});
323
+
324
+ for (const response of ["not JSON", '{"error":{"message":"render failed"}}']) {
325
+ await withFakeHeygen({ response }, async () => {
326
+ const heygenVideoGenerate = await freshGenerate();
327
+ const result = await heygenVideoGenerate("Unusable response", {
328
+ avatarId: "avatar-override",
329
+ voiceId: "voice-override",
330
+ });
331
+
332
+ assert.equal(result, null);
333
+ });
334
+ }
335
+ });
336
+
337
+ test("onboards and returns null when avatar/voice discovery itself is unauthenticated", async (t) => {
338
+ const errors = [];
339
+ t.mock.method(console, "error", (message) => errors.push(message));
340
+
341
+ // Both avatar list AND voice list would fail unauthenticated (discoveryMode
342
+ // "auth" applies to both in the fake CLI) -- the short-circuit after the
343
+ // first failure must mean only one is ever attempted, so the onboarding
344
+ // message and the provider-error telemetry ping each fire exactly once
345
+ // instead of double-firing for what's really one auth failure.
346
+ await withFakeHeygen({ discoveryMode: "auth" }, async ({ invocations }) => {
347
+ const heygenVideoGenerate = await freshGenerate();
348
+ const result = await heygenVideoGenerate("Discovery auth failure", {});
349
+
350
+ assert.equal(result, null);
351
+ assert.equal(
352
+ errors.filter((message) => message === AVATAR_VIDEO_SIGNIN_MESSAGE).length,
353
+ 1,
354
+ "onboarding message must fire exactly once, not once per failed discovery call",
355
+ );
356
+ const calls = invocations();
357
+ assert.equal(calls.length, 1, "must short-circuit after the first discovery failure");
358
+ assert.match(calls[0], /^avatar list /);
359
+ });
360
+ });
361
+
362
+ test("download failure after a successful create returns null and logs a diagnostic", async (t) => {
363
+ const { server, url } = await listenFailingVideoServer(t);
364
+ try {
365
+ await withFakeHeygen({ response: JSON.stringify({ data: { video_url: url } }) }, async () => {
366
+ const heygenVideoGenerate = await freshGenerate();
367
+ const result = await heygenVideoGenerate("Download failure", {
368
+ avatarId: "avatar-override",
369
+ voiceId: "voice-override",
370
+ });
371
+
372
+ assert.equal(result, null);
373
+ });
374
+ } finally {
375
+ await closeServer(server);
376
+ }
377
+ });
378
+
379
+ test("uses private unique downloads even when time is fixed and the old name is planted", async (t) => {
380
+ const root = mkdtempSync(join(tmpdir(), "hf-video-temp-test-"));
381
+ const previousTmpdir = process.env.TMPDIR;
382
+ process.env.TMPDIR = root;
383
+ t.mock.method(Date, "now", () => 123456);
384
+ t.mock.method(globalThis, "fetch", async () => ({
385
+ ok: true,
386
+ headers: { get: () => "4" },
387
+ body: [Buffer.from("new!")],
388
+ }));
389
+ try {
390
+ const victim = join(root, "victim");
391
+ writeFileSync(victim, "unchanged");
392
+ symlinkSync(victim, join(root, `media-use-heygen-video-${process.pid}-123456.mp4`));
393
+ await withFakeHeygen(
394
+ { response: JSON.stringify({ data: { video_url: "https://example.invalid/video.mp4" } }) },
395
+ async () => {
396
+ const generate = await freshGenerate();
397
+ const first = await generate("First", { avatarId: "a", voiceId: "v" });
398
+ const second = await generate("Second", { avatarId: "a", voiceId: "v" });
399
+ assert.ok(first && second);
400
+ assert.notEqual(first.localPath, second.localPath);
401
+ assert.equal(readFileSync(first.localPath, "utf8"), "new!");
402
+ assert.equal(readFileSync(second.localPath, "utf8"), "new!");
403
+ assert.equal(readFileSync(victim, "utf8"), "unchanged");
404
+ },
405
+ );
406
+ } finally {
407
+ if (previousTmpdir === undefined) delete process.env.TMPDIR;
408
+ else process.env.TMPDIR = previousTmpdir;
409
+ rmSync(root, { recursive: true, force: true });
410
+ }
411
+ });
412
+
413
+ test("removes private download staging on failure while returning null", async (t) => {
414
+ const root = mkdtempSync(join(tmpdir(), "hf-video-temp-fail-"));
415
+ const previousTmpdir = process.env.TMPDIR;
416
+ process.env.TMPDIR = root;
417
+ t.mock.method(globalThis, "fetch", async () => {
418
+ throw new Error("download failed");
419
+ });
420
+ t.mock.method(console, "error", () => {});
421
+ try {
422
+ await withFakeHeygen(
423
+ { response: JSON.stringify({ data: { video_url: "https://example.invalid/video.mp4" } }) },
424
+ async () => {
425
+ const generate = await freshGenerate();
426
+ assert.equal(await generate("Failure", { avatarId: "a", voiceId: "v" }), null);
427
+ },
428
+ );
429
+ assert.deepEqual(readdirSync(root), []);
430
+ } finally {
431
+ if (previousTmpdir === undefined) delete process.env.TMPDIR;
432
+ else process.env.TMPDIR = previousTmpdir;
433
+ rmSync(root, { recursive: true, force: true });
434
+ }
435
+ });
@@ -0,0 +1,44 @@
1
+ import { heygenSearch } from "./heygen-search.mjs";
2
+
3
+ export const imageProvider = {
4
+ async search(intent) {
5
+ const results = heygenSearch("asset search", intent, { type: "image" });
6
+ if (!results) return null;
7
+ const best = results[0];
8
+ return {
9
+ url: best.url,
10
+ source: "search",
11
+ // ext derived from the asset URL by resolve.mjs (.jpg/.png/.webp)
12
+ metadata: {
13
+ description: intent,
14
+ width: best.width || null,
15
+ height: best.height || null,
16
+ transparent: best.is_transparent || false,
17
+ provider: "heygen.asset.search",
18
+ provenance: { asset_id: best.id, score: best.score },
19
+ },
20
+ };
21
+ },
22
+ };
23
+
24
+ export const iconProvider = {
25
+ async search(intent) {
26
+ // No minScore: the `asset search` backend rejects --min-score and returns no score field.
27
+ const results = heygenSearch("asset search", intent, { type: "icon" });
28
+ if (!results) return null;
29
+ const best = results[0];
30
+ return {
31
+ url: best.url,
32
+ source: "search",
33
+ // ext derived from the asset URL by resolve.mjs — catalog icons are .png, not .svg
34
+ metadata: {
35
+ description: intent,
36
+ width: best.width || null,
37
+ height: best.height || null,
38
+ transparent: best.is_transparent ?? true,
39
+ provider: "heygen.asset.search",
40
+ provenance: { asset_id: best.id, score: best.score, type: "icon" },
41
+ },
42
+ };
43
+ },
44
+ };
@@ -0,0 +1,63 @@
1
+ import { writeFileSync, mkdirSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { readManifest, indexPath } from "./manifest.mjs";
4
+
5
+ function pad(str, len) {
6
+ return String(str ?? "").padEnd(len);
7
+ }
8
+
9
+ function formatDur(record) {
10
+ if (record.duration == null) return "—";
11
+ return `${record.duration}s`;
12
+ }
13
+
14
+ function formatDims(record) {
15
+ if (record.width && record.height) return `${record.width}×${record.height}`;
16
+ if (record.type === "icon" && record.transparent) return "svg";
17
+ return "—";
18
+ }
19
+
20
+ export function generateIndexContent(records) {
21
+ const count = records.length;
22
+ const header = `# .media · ${count} asset${count === 1 ? "" : "s"}\n`;
23
+ if (count === 0) return header;
24
+
25
+ const cols = { id: 4, type: 5, dur: 4, dims: 5, path: 5, desc: 11 };
26
+ for (const r of records) {
27
+ cols.id = Math.max(cols.id, (r.id ?? "").length);
28
+ cols.type = Math.max(cols.type, (r.type ?? "").length);
29
+ cols.dur = Math.max(cols.dur, formatDur(r).length);
30
+ cols.dims = Math.max(cols.dims, formatDims(r).length);
31
+ cols.path = Math.max(cols.path, (r.path ?? "").length);
32
+ }
33
+
34
+ const heading =
35
+ pad("id", cols.id + 2) +
36
+ pad("type", cols.type + 2) +
37
+ pad("dur", cols.dur + 2) +
38
+ pad("dims", cols.dims + 2) +
39
+ pad("path", cols.path + 2) +
40
+ "description";
41
+
42
+ const lines = [header, heading];
43
+ for (const r of records) {
44
+ lines.push(
45
+ pad(r.id, cols.id + 2) +
46
+ pad(r.type, cols.type + 2) +
47
+ pad(formatDur(r), cols.dur + 2) +
48
+ pad(formatDims(r), cols.dims + 2) +
49
+ pad(r.path, cols.path + 2) +
50
+ (r.description ?? ""),
51
+ );
52
+ }
53
+ return lines.join("\n") + "\n";
54
+ }
55
+
56
+ export function regenerateIndex(projectDir) {
57
+ const records = readManifest(projectDir);
58
+ const content = generateIndexContent(records);
59
+ const p = indexPath(projectDir);
60
+ mkdirSync(dirname(p), { recursive: true });
61
+ writeFileSync(p, content);
62
+ return content;
63
+ }