dsh-hyperframes 0.3.1 → 0.4.0

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 (375) hide show
  1. package/README.en.md +6 -5
  2. package/README.md +7 -6
  3. package/lib/index.d.ts +1 -1
  4. package/lib/index.js +1 -0
  5. package/package.json +63 -63
  6. package/skills/embedded-captions/SKILL.md +28 -3
  7. package/skills/embedded-captions/references/aesthetic-principles.md +2 -3
  8. package/skills/embedded-captions/references/rail.md +1 -1
  9. package/skills/embedded-captions/scripts/check-occlusion.cjs +4 -1
  10. package/skills/embedded-captions/scripts/check-overflow.cjs +1 -0
  11. package/skills/embedded-captions/scripts/check-rail-climax.cjs +5 -1
  12. package/skills/embedded-captions/scripts/fixtures/heroless/theme.json +11 -0
  13. package/skills/embedded-captions/scripts/hf-cli.cjs +33 -0
  14. package/skills/embedded-captions/scripts/make-theme.cjs +95 -57
  15. package/skills/embedded-captions/scripts/make-theme.test.mjs +131 -0
  16. package/skills/embedded-captions/scripts/matte.cjs +1 -13
  17. package/skills/embedded-captions/scripts/measure-layout.cjs +2 -1
  18. package/skills/embedded-captions/scripts/preview-frames.cjs +39 -11
  19. package/skills/embedded-captions/scripts/preview-frames.test.mjs +24 -0
  20. package/skills/embedded-captions/scripts/render-and-composite.sh +8 -52
  21. package/skills/embedded-captions/scripts/safe-zones.cjs +4 -1
  22. package/skills/embedded-captions/scripts/transcribe.cjs +5 -14
  23. package/skills/embedded-captions/scripts/transcribe.test.mjs +41 -0
  24. package/skills/faceless-explainer/SKILL.md +20 -16
  25. package/skills/faceless-explainer/references/story-design.md +1 -1
  26. package/skills/faceless-explainer/scripts/assemble-index.mjs +6 -4
  27. package/skills/faceless-explainer/scripts/assemble-index.test.mjs +57 -2
  28. package/skills/faceless-explainer/scripts/audio.mjs +2 -0
  29. package/skills/faceless-explainer/scripts/build-frame.mjs +6 -3
  30. package/skills/faceless-explainer/scripts/captions.mjs +7 -4
  31. package/skills/faceless-explainer/scripts/frame-packets.mjs +4 -4
  32. package/skills/faceless-explainer/scripts/lib/bgm-volume.mjs +4 -0
  33. package/skills/faceless-explainer/scripts/lib/captured-fonts.mjs +58 -0
  34. package/skills/{hyperframes-core → faceless-explainer}/scripts/lib/frame-packets-core.mjs +5 -23
  35. package/skills/faceless-explainer/scripts/lib/storyboard.mjs +1 -1
  36. package/skills/faceless-explainer/scripts/lib/transitions.json +3 -3
  37. package/skills/faceless-explainer/scripts/transitions.mjs +1 -0
  38. package/skills/faceless-explainer/scripts/transitions.test.mjs +2 -0
  39. package/skills/faceless-explainer/sub-agents/frame-worker.md +1 -1
  40. package/skills/figma/SKILL.md +2 -0
  41. package/skills/general-video/SKILL.md +13 -11
  42. package/skills/general-video/scripts/frame-packets.mjs +4 -4
  43. package/skills/general-video/scripts/lib/frame-packets-core.mjs +194 -0
  44. package/skills/general-video/sub-agents/frame-worker.md +1 -1
  45. package/skills/hyperframes/SKILL.md +40 -30
  46. package/skills/{hyperframes-core → hyperframes}/references/brief-contract.md +15 -15
  47. package/skills/{hyperframes-core → hyperframes}/references/brief-format.md +8 -8
  48. package/skills/hyperframes/references/capability-menu.md +2 -2
  49. package/skills/{hyperframes-core → hyperframes}/references/frame-worker-core.md +1 -1
  50. package/skills/hyperframes/references/intent-interview.md +16 -16
  51. package/skills/hyperframes/references/plugin-installation.md +43 -0
  52. package/skills/{hyperframes-core → hyperframes}/references/production-loop.md +11 -11
  53. package/skills/hyperframes/references/review-loop.md +37 -0
  54. package/skills/hyperframes/references/route-briefs.md +1 -1
  55. package/skills/hyperframes/references/routes/motion-graphics.md +1 -1
  56. package/skills/{hyperframes-core → hyperframes}/references/script-format.md +3 -3
  57. package/skills/hyperframes/references/skill-lifecycle.md +2 -0
  58. package/skills/{hyperframes-core → hyperframes}/references/storyboard-format.md +3 -34
  59. package/skills/hyperframes/scripts/lib/frame-packets-core.mjs +194 -0
  60. package/skills/hyperframes/scripts/plugin-cli.mjs +88 -0
  61. package/skills/hyperframes/scripts/plugin-cli.test.mjs +117 -0
  62. package/skills/hyperframes-animation/SKILL.md +5 -1
  63. package/skills/hyperframes-animation/adapters/gsap.md +1 -1
  64. package/skills/hyperframes-animation/adapters/lottie.md +15 -2
  65. package/skills/hyperframes-animation/adapters/three.md +14 -0
  66. package/skills/hyperframes-animation/blueprints/titlecard-reveal.md +1 -1
  67. package/skills/hyperframes-animation/references/motion-blur.md +133 -0
  68. package/skills/hyperframes-animation/rules/cursor-click-ripple.md +1 -1
  69. package/skills/hyperframes-animation/rules/dynamic-content-sequencing.md +1 -1
  70. package/skills/hyperframes-animation/rules/hacker-flip-3d.md +1 -1
  71. package/skills/hyperframes-animation/rules/kinetic-beat-slam.md +2 -2
  72. package/skills/hyperframes-animation/rules/motion-blur-streak.md +1 -1
  73. package/skills/hyperframes-animation/rules/vertical-spring-ticker.md +1 -1
  74. package/skills/hyperframes-animation/scripts/package-loader.mjs +47 -16
  75. package/skills/hyperframes-animation/scripts/package-loader.test.mjs +52 -0
  76. package/skills/hyperframes-animation/techniques.md +1 -1
  77. package/skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md +3 -3
  78. package/skills/hyperframes-animation/transitions/css-push.md +4 -4
  79. package/skills/hyperframes-audio/SKILL.md +30 -22
  80. package/skills/hyperframes-audio/scripts/carve.mjs +6 -3
  81. package/skills/hyperframes-audio/scripts/carve.test.mjs +16 -1
  82. package/skills/hyperframes-cli/SKILL.md +44 -35
  83. package/skills/hyperframes-cli/references/init-and-scaffold.md +8 -6
  84. package/skills/hyperframes-cli/references/lint-validate-inspect.md +3 -3
  85. package/skills/hyperframes-cli/references/preview-render.md +35 -32
  86. package/skills/hyperframes-cli/references/upgrade-info-misc.md +50 -1
  87. package/skills/hyperframes-core/SKILL.md +30 -24
  88. package/skills/hyperframes-core/references/composition-patterns.md +3 -42
  89. package/skills/hyperframes-core/references/creator-editing-recipes.md +131 -7
  90. package/skills/hyperframes-core/references/data-attributes.md +11 -11
  91. package/skills/hyperframes-core/references/determinism-rules.md +2 -19
  92. package/skills/hyperframes-core/references/full-screen-motion.md +0 -9
  93. package/skills/hyperframes-core/references/minimal-composition.md +2 -2
  94. package/skills/hyperframes-core/references/sub-compositions.md +3 -66
  95. package/skills/hyperframes-core/references/tailwind.md +1 -20
  96. package/skills/hyperframes-core/references/tracks-and-clips.md +2 -6
  97. package/skills/hyperframes-core/references/variables-and-media.md +3 -3
  98. package/skills/hyperframes-creative/SKILL.md +2 -0
  99. package/skills/hyperframes-creative/references/house-style.md +15 -15
  100. package/skills/hyperframes-creative/references/story-spine.md +3 -1
  101. package/skills/hyperframes-creative/references/storyboard-recipe.md +57 -0
  102. package/skills/hyperframes-creative/references/typography.md +1 -1
  103. package/skills/hyperframes-creative/scripts/package-loader.mjs +47 -16
  104. package/skills/hyperframes-creative/scripts/package-loader.test.mjs +52 -0
  105. package/skills/hyperframes-keyframes/SKILL.md +4 -2
  106. package/skills/hyperframes-registry/SKILL.md +3 -1
  107. package/skills/hyperframes-registry/references/contributing.md +8 -2
  108. package/skills/hyperframes-registry/references/discovery.md +20 -34
  109. package/skills/hyperframes-studio/SKILL.md +77 -0
  110. package/skills/media-use/SKILL.md +14 -12
  111. package/skills/media-use/audio/references/captions/authoring.md +4 -0
  112. package/skills/media-use/audio/references/remove-background.md +1 -1
  113. package/skills/media-use/audio/references/tts-to-captions.md +11 -2
  114. package/skills/media-use/audio/references/tts.md +84 -6
  115. package/skills/media-use/audio/scripts/audio.mjs +15 -6
  116. package/skills/media-use/audio/scripts/gemini-pipeline.test.mjs +113 -0
  117. package/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
  118. package/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
  119. package/skills/media-use/audio/scripts/lib/bgm-volume.mjs +4 -0
  120. package/skills/media-use/audio/scripts/lib/bgm.mjs +2 -6
  121. package/skills/media-use/audio/scripts/lib/gemini-auth.mjs +43 -0
  122. package/skills/media-use/audio/scripts/lib/gemini-auth.py +52 -0
  123. package/skills/media-use/audio/scripts/lib/gemini-auth.test.mjs +86 -0
  124. package/skills/media-use/audio/scripts/lib/gemini-auth_test.py +90 -0
  125. package/skills/media-use/audio/scripts/lib/gemini-tts.mjs +122 -0
  126. package/skills/media-use/audio/scripts/lib/gemini-tts.test.mjs +243 -0
  127. package/skills/media-use/audio/scripts/lib/heygen.mjs +2 -1
  128. package/skills/media-use/audio/scripts/lib/tts.mjs +15 -3
  129. package/skills/media-use/audio/scripts/wait-bgm.mjs +8 -4
  130. package/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
  131. package/skills/media-use/references/audio.md +3 -2
  132. package/skills/media-use/references/grading.md +7 -7
  133. package/skills/media-use/references/media-treatment-recipes.md +1 -1
  134. package/skills/media-use/references/memory.md +2 -2
  135. package/skills/media-use/references/meta.md +2 -2
  136. package/skills/media-use/references/operations.md +15 -13
  137. package/skills/media-use/references/resolve.md +22 -11
  138. package/skills/media-use/references/setup-providers.md +11 -11
  139. package/skills/media-use/scripts/audio-duck.mjs +19 -27
  140. package/skills/media-use/scripts/compatibility.test.mjs +96 -0
  141. package/skills/media-use/scripts/lib/config-lock.mjs +45 -0
  142. package/skills/media-use/scripts/lib/duck.mjs +16 -10
  143. package/skills/media-use/scripts/lib/media-fetch.mjs +85 -0
  144. package/skills/media-use/scripts/lib/media-home.mjs +20 -0
  145. package/skills/media-use/scripts/lib/npx-sync.mjs +28 -2
  146. package/skills/media-use/scripts/lib/prefs-store.mjs +2 -2
  147. package/skills/media-use/scripts/lib/recipe-store.mjs +2 -2
  148. package/skills/media-use/scripts/lib/telemetry.mjs +29 -41
  149. package/skills/media-use/scripts/lib/transcriptCutFade.mjs +21 -0
  150. package/skills/media-use/scripts/prefs.mjs +1 -1
  151. package/skills/media-use/scripts/recipe.mjs +1 -1
  152. package/skills/media-use/scripts/resolve-plugin.test.mjs +62 -0
  153. package/skills/media-use/scripts/resolve.mjs +27 -1267
  154. package/skills/media-use/scripts/transcribe.mjs +39 -25
  155. package/skills/media-use/scripts/transcribe.test.mjs +112 -0
  156. package/skills/media-use/scripts/transcript-cut.mjs +39 -8
  157. package/skills/media-use/scripts/transcript-cut.test.mjs +117 -0
  158. package/skills/motion-graphics/SKILL.md +3 -1
  159. package/skills/motion-graphics/agents/builder.md +1 -0
  160. package/skills/motion-graphics/agents/director.md +2 -1
  161. package/skills/motion-graphics/catalog-map.md +25 -13
  162. package/skills/motion-graphics/categories/charts/module.md +2 -1
  163. package/skills/motion-graphics/categories/lower-thirds/module.md +4 -0
  164. package/skills/motion-graphics/categories/news/module.md +1 -1
  165. package/skills/motion-graphics/references/motion-vocabulary.md +4 -0
  166. package/skills/music-to-video/SKILL.md +8 -6
  167. package/skills/music-to-video/references/montage.md +3 -6
  168. package/skills/music-to-video/references/motion-primitives/3d-card-flip/index.html +10 -111
  169. package/skills/music-to-video/references/motion-primitives/3d-card-flip/scene.html +119 -0
  170. package/skills/music-to-video/references/motion-primitives/bg-flow-field/index.html +11 -284
  171. package/skills/music-to-video/references/motion-primitives/bg-flow-field/scene.html +294 -0
  172. package/skills/music-to-video/references/motion-primitives/binary-decrypt/index.html +10 -76
  173. package/skills/music-to-video/references/motion-primitives/binary-decrypt/scene.html +85 -0
  174. package/skills/music-to-video/references/motion-primitives/blur-resolve/index.html +10 -52
  175. package/skills/music-to-video/references/motion-primitives/blur-resolve/scene.html +60 -0
  176. package/skills/music-to-video/references/motion-primitives/braam-punch/index.html +10 -92
  177. package/skills/music-to-video/references/motion-primitives/braam-punch/scene.html +100 -0
  178. package/skills/music-to-video/references/motion-primitives/chromatic-split/index.html +10 -103
  179. package/skills/music-to-video/references/motion-primitives/chromatic-split/scene.html +111 -0
  180. package/skills/music-to-video/references/motion-primitives/chrome-sweep/index.html +10 -60
  181. package/skills/music-to-video/references/motion-primitives/chrome-sweep/scene.html +68 -0
  182. package/skills/music-to-video/references/motion-primitives/counting-punch/index.html +10 -110
  183. package/skills/music-to-video/references/motion-primitives/counting-punch/scene.html +119 -0
  184. package/skills/music-to-video/references/motion-primitives/crash-zoom-in/index.html +10 -112
  185. package/skills/music-to-video/references/motion-primitives/crash-zoom-in/scene.html +120 -0
  186. package/skills/music-to-video/references/motion-primitives/datamosh-smear/index.html +10 -121
  187. package/skills/music-to-video/references/motion-primitives/datamosh-smear/scene.html +131 -0
  188. package/skills/music-to-video/references/motion-primitives/directional-fill/index.html +10 -90
  189. package/skills/music-to-video/references/motion-primitives/directional-fill/scene.html +103 -0
  190. package/skills/music-to-video/references/motion-primitives/dolly-zoom/index.html +10 -94
  191. package/skills/music-to-video/references/motion-primitives/dolly-zoom/scene.html +102 -0
  192. package/skills/music-to-video/references/motion-primitives/electric-arc/index.html +10 -100
  193. package/skills/music-to-video/references/motion-primitives/electric-arc/scene.html +105 -0
  194. package/skills/music-to-video/references/motion-primitives/flash-cut/index.html +10 -89
  195. package/skills/music-to-video/references/motion-primitives/flash-cut/scene.html +97 -0
  196. package/skills/music-to-video/references/motion-primitives/gooey-metaball/index.html +10 -197
  197. package/skills/music-to-video/references/motion-primitives/gooey-metaball/scene.html +206 -0
  198. package/skills/music-to-video/references/motion-primitives/hard-cut/index.html +10 -74
  199. package/skills/music-to-video/references/motion-primitives/hard-cut/scene.html +82 -0
  200. package/skills/music-to-video/references/motion-primitives/hypercut-whip/index.html +10 -36
  201. package/skills/music-to-video/references/motion-primitives/hypercut-whip/scene.html +44 -0
  202. package/skills/music-to-video/references/motion-primitives/iris-open/index.html +10 -61
  203. package/skills/music-to-video/references/motion-primitives/iris-open/scene.html +69 -0
  204. package/skills/music-to-video/references/motion-primitives/kinetic-letter-in/index.html +10 -55
  205. package/skills/music-to-video/references/motion-primitives/kinetic-letter-in/scene.html +64 -0
  206. package/skills/music-to-video/references/motion-primitives/liquid-morph/index.html +10 -88
  207. package/skills/music-to-video/references/motion-primitives/liquid-morph/scene.html +96 -0
  208. package/skills/music-to-video/references/motion-primitives/mask-reveal/index.html +10 -66
  209. package/skills/music-to-video/references/motion-primitives/mask-reveal/scene.html +74 -0
  210. package/skills/music-to-video/references/motion-primitives/mosaic-pack/index.html +10 -81
  211. package/skills/music-to-video/references/motion-primitives/mosaic-pack/scene.html +89 -0
  212. package/skills/music-to-video/references/motion-primitives/neon-flicker/index.html +10 -65
  213. package/skills/music-to-video/references/motion-primitives/neon-flicker/scene.html +73 -0
  214. package/skills/music-to-video/references/motion-primitives/outline-to-fill/index.html +10 -74
  215. package/skills/music-to-video/references/motion-primitives/outline-to-fill/scene.html +82 -0
  216. package/skills/music-to-video/references/motion-primitives/palette-flip/index.html +10 -89
  217. package/skills/music-to-video/references/motion-primitives/palette-flip/scene.html +97 -0
  218. package/skills/music-to-video/references/motion-primitives/particle-burst/index.html +10 -98
  219. package/skills/music-to-video/references/motion-primitives/particle-burst/scene.html +106 -0
  220. package/skills/music-to-video/references/motion-primitives/pixel-dissolve/index.html +10 -78
  221. package/skills/music-to-video/references/motion-primitives/pixel-dissolve/scene.html +86 -0
  222. package/skills/music-to-video/references/motion-primitives/radial-burst-lines/index.html +10 -117
  223. package/skills/music-to-video/references/motion-primitives/radial-burst-lines/scene.html +130 -0
  224. package/skills/music-to-video/references/motion-primitives/screen-shake/index.html +10 -70
  225. package/skills/music-to-video/references/motion-primitives/screen-shake/scene.html +78 -0
  226. package/skills/music-to-video/references/motion-primitives/slot-machine-reveal/index.html +10 -87
  227. package/skills/music-to-video/references/motion-primitives/slot-machine-reveal/scene.html +100 -0
  228. package/skills/music-to-video/references/motion-primitives/spotlight-sweep/index.html +10 -94
  229. package/skills/music-to-video/references/motion-primitives/spotlight-sweep/scene.html +102 -0
  230. package/skills/music-to-video/references/motion-primitives/staggered-exit/index.html +10 -74
  231. package/skills/music-to-video/references/motion-primitives/staggered-exit/scene.html +82 -0
  232. package/skills/music-to-video/references/motion-primitives/text-spectral-rays/index.html +11 -271
  233. package/skills/music-to-video/references/motion-primitives/text-spectral-rays/scene.html +280 -0
  234. package/skills/music-to-video/references/motion-primitives/text-wave-distort/index.html +10 -92
  235. package/skills/music-to-video/references/motion-primitives/text-wave-distort/scene.html +106 -0
  236. package/skills/music-to-video/references/motion-primitives/tile-mosaic/index.html +10 -101
  237. package/skills/music-to-video/references/motion-primitives/tile-mosaic/scene.html +109 -0
  238. package/skills/music-to-video/references/motion-primitives/typewriter-reveal/index.html +10 -79
  239. package/skills/music-to-video/references/motion-primitives/typewriter-reveal/scene.html +91 -0
  240. package/skills/music-to-video/references/motion-primitives/word-grid-burst/index.html +10 -83
  241. package/skills/music-to-video/references/motion-primitives/word-grid-burst/scene.html +96 -0
  242. package/skills/music-to-video/references/planning.md +2 -2
  243. package/skills/music-to-video/scripts/lib/storyboard.mjs +1 -1
  244. package/skills/music-to-video/sub-agents/frame-worker.md +2 -2
  245. package/skills/pr-to-video/SKILL.md +20 -16
  246. package/skills/pr-to-video/references/code-vocabulary.md +14 -12
  247. package/skills/pr-to-video/references/story-design.md +1 -1
  248. package/skills/pr-to-video/scripts/assemble-index.mjs +6 -4
  249. package/skills/pr-to-video/scripts/assemble-index.test.mjs +57 -2
  250. package/skills/pr-to-video/scripts/audio.mjs +2 -0
  251. package/skills/pr-to-video/scripts/build-frame.mjs +6 -3
  252. package/skills/pr-to-video/scripts/captions.mjs +7 -4
  253. package/skills/pr-to-video/scripts/captions.test.mjs +41 -0
  254. package/skills/pr-to-video/scripts/frame-packets.mjs +4 -4
  255. package/skills/pr-to-video/scripts/lib/bgm-volume.mjs +4 -0
  256. package/skills/pr-to-video/scripts/lib/captured-fonts.mjs +58 -0
  257. package/skills/pr-to-video/scripts/lib/frame-packets-core.mjs +194 -0
  258. package/skills/pr-to-video/scripts/lib/storyboard.mjs +1 -1
  259. package/skills/pr-to-video/scripts/lib/transitions.json +3 -3
  260. package/skills/pr-to-video/scripts/transitions.mjs +7 -5
  261. package/skills/pr-to-video/sub-agents/frame-worker.md +1 -1
  262. package/skills/product-launch-video/SKILL.md +20 -16
  263. package/skills/product-launch-video/references/story-design.md +1 -1
  264. package/skills/product-launch-video/scripts/assemble-index.mjs +6 -4
  265. package/skills/product-launch-video/scripts/assemble-index.test.mjs +57 -2
  266. package/skills/product-launch-video/scripts/audio.mjs +2 -0
  267. package/skills/product-launch-video/scripts/audio.test.mjs +16 -0
  268. package/skills/product-launch-video/scripts/build-frame-subsets.test.mjs +152 -0
  269. package/skills/product-launch-video/scripts/build-frame.mjs +6 -3
  270. package/skills/product-launch-video/scripts/captions.mjs +7 -4
  271. package/skills/product-launch-video/scripts/frame-packets.mjs +4 -4
  272. package/skills/product-launch-video/scripts/lib/bgm-volume.mjs +4 -0
  273. package/skills/product-launch-video/scripts/lib/captured-fonts.mjs +58 -0
  274. package/skills/product-launch-video/scripts/lib/frame-packets-core.mjs +194 -0
  275. package/skills/product-launch-video/scripts/lib/storyboard.mjs +1 -1
  276. package/skills/product-launch-video/scripts/lib/transitions.json +3 -3
  277. package/skills/product-launch-video/scripts/transitions.mjs +7 -5
  278. package/skills/product-launch-video/scripts/transitions.test.mjs +68 -0
  279. package/skills/product-launch-video/sub-agents/frame-worker.md +1 -1
  280. package/skills/remotion-to-hyperframes/SKILL.md +7 -4
  281. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/expected.json +5 -5
  282. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/hf-src/compositions/title.html +31 -0
  283. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-1-title-card/hf-src/index.html +10 -25
  284. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/README.md +8 -8
  285. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/expected.json +9 -9
  286. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-1.html +35 -0
  287. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-2.html +35 -0
  288. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/compositions/scene-3.html +36 -0
  289. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-2-multi-scene/hf-src/index.html +28 -71
  290. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/expected.json +3 -3
  291. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-1.html +52 -0
  292. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-2.html +99 -0
  293. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/compositions/scene-3.html +55 -0
  294. package/skills/remotion-to-hyperframes/assets/test-corpus/tier-3-data-driven/hf-src/index.html +13 -212
  295. package/skills/remotion-to-hyperframes/references/api-map.md +16 -16
  296. package/skills/remotion-to-hyperframes/references/limitations.md +7 -7
  297. package/skills/remotion-to-hyperframes/references/lottie.md +5 -7
  298. package/skills/remotion-to-hyperframes/references/media.md +3 -3
  299. package/skills/remotion-to-hyperframes/references/parameters.md +5 -0
  300. package/skills/remotion-to-hyperframes/references/sequencing.md +71 -18
  301. package/skills/slideshow/SKILL.md +4 -1
  302. package/skills/slideshow/references/standalone-harness.md +0 -3
  303. package/skills/talking-head-recut/SKILL.md +10 -1
  304. package/skills/hyperframes-core/references/review-loop.md +0 -41
  305. package/skills/media-use/scripts/lib/adopt.mjs +0 -128
  306. package/skills/media-use/scripts/lib/adopt.test.mjs +0 -95
  307. package/skills/media-use/scripts/lib/bgm-provider.mjs +0 -20
  308. package/skills/media-use/scripts/lib/brand-provider.mjs +0 -59
  309. package/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +0 -108
  310. package/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +0 -85
  311. package/skills/media-use/scripts/lib/cache.mjs +0 -150
  312. package/skills/media-use/scripts/lib/candidates.mjs +0 -91
  313. package/skills/media-use/scripts/lib/candidates.test.mjs +0 -155
  314. package/skills/media-use/scripts/lib/codex-provider.mjs +0 -161
  315. package/skills/media-use/scripts/lib/codex-provider.test.mjs +0 -34
  316. package/skills/media-use/scripts/lib/coverage.test.mjs +0 -117
  317. package/skills/media-use/scripts/lib/cube-build.mjs +0 -181
  318. package/skills/media-use/scripts/lib/cube-build.test.mjs +0 -80
  319. package/skills/media-use/scripts/lib/cube-validate.mjs +0 -198
  320. package/skills/media-use/scripts/lib/cube-validate.test.mjs +0 -125
  321. package/skills/media-use/scripts/lib/cutlist.test.mjs +0 -148
  322. package/skills/media-use/scripts/lib/duck.test.mjs +0 -118
  323. package/skills/media-use/scripts/lib/error-diffusion.test.mjs +0 -102
  324. package/skills/media-use/scripts/lib/freeze.mjs +0 -70
  325. package/skills/media-use/scripts/lib/freeze.test.mjs +0 -46
  326. package/skills/media-use/scripts/lib/grade-analyzer.mjs +0 -264
  327. package/skills/media-use/scripts/lib/grade-analyzer.test.mjs +0 -285
  328. package/skills/media-use/scripts/lib/heygen-cli.mjs +0 -191
  329. package/skills/media-use/scripts/lib/heygen-cli.test.mjs +0 -301
  330. package/skills/media-use/scripts/lib/heygen-search.mjs +0 -45
  331. package/skills/media-use/scripts/lib/heygen-search.test.mjs +0 -49
  332. package/skills/media-use/scripts/lib/heygen-video-provider.mjs +0 -127
  333. package/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +0 -344
  334. package/skills/media-use/scripts/lib/image-provider.mjs +0 -44
  335. package/skills/media-use/scripts/lib/local-models.mjs +0 -318
  336. package/skills/media-use/scripts/lib/local-models.test.mjs +0 -220
  337. package/skills/media-use/scripts/lib/local-run.mjs +0 -78
  338. package/skills/media-use/scripts/lib/local-run.test.mjs +0 -121
  339. package/skills/media-use/scripts/lib/logo-provider.mjs +0 -222
  340. package/skills/media-use/scripts/lib/logo-provider.test.mjs +0 -137
  341. package/skills/media-use/scripts/lib/ltx-video-provider.mjs +0 -94
  342. package/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +0 -316
  343. package/skills/media-use/scripts/lib/lut-preset-provider.mjs +0 -266
  344. package/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +0 -176
  345. package/skills/media-use/scripts/lib/manifest.test.mjs +0 -388
  346. package/skills/media-use/scripts/lib/match.mjs +0 -47
  347. package/skills/media-use/scripts/lib/mflux-provider.mjs +0 -128
  348. package/skills/media-use/scripts/lib/mflux-provider.test.mjs +0 -174
  349. package/skills/media-use/scripts/lib/misses.mjs +0 -49
  350. package/skills/media-use/scripts/lib/misses.test.mjs +0 -78
  351. package/skills/media-use/scripts/lib/npx-sync.test.mjs +0 -50
  352. package/skills/media-use/scripts/lib/parakeet-words.test.mjs +0 -44
  353. package/skills/media-use/scripts/lib/prefs-store.test.mjs +0 -189
  354. package/skills/media-use/scripts/lib/probe.mjs +0 -39
  355. package/skills/media-use/scripts/lib/probe.test.mjs +0 -31
  356. package/skills/media-use/scripts/lib/providers.mjs +0 -5
  357. package/skills/media-use/scripts/lib/recipe-store.test.mjs +0 -367
  358. package/skills/media-use/scripts/lib/registry.mjs +0 -218
  359. package/skills/media-use/scripts/lib/registry.test.mjs +0 -261
  360. package/skills/media-use/scripts/lib/search.mjs +0 -24
  361. package/skills/media-use/scripts/lib/search.test.mjs +0 -32
  362. package/skills/media-use/scripts/lib/sfx-provider.mjs +0 -23
  363. package/skills/media-use/scripts/lib/specs.mjs +0 -132
  364. package/skills/media-use/scripts/lib/specs.test.mjs +0 -157
  365. package/skills/media-use/scripts/lib/stats.mjs +0 -117
  366. package/skills/media-use/scripts/lib/stats.test.mjs +0 -165
  367. package/skills/media-use/scripts/lib/telemetry.test.mjs +0 -373
  368. package/skills/media-use/scripts/lib/tts-local-provider.mjs +0 -92
  369. package/skills/media-use/scripts/lib/tts-local-provider.test.mjs +0 -72
  370. package/skills/media-use/scripts/lib/usage.mjs +0 -57
  371. package/skills/media-use/scripts/lib/usage.test.mjs +0 -54
  372. package/skills/media-use/scripts/lib/voice-provider.mjs +0 -57
  373. package/skills/media-use/scripts/lib/voice-provider.test.mjs +0 -46
  374. package/skills/media-use/scripts/resolve.test.mjs +0 -1120
  375. /package/skills/{hyperframes-core → hyperframes}/references/subagent-dispatch.md +0 -0
@@ -0,0 +1,90 @@
1
+ """Credential-boundary tests; stub Google's transport, never use real secrets."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import sys
7
+ from pathlib import Path
8
+ import tempfile
9
+ import types
10
+ import unittest
11
+ from unittest.mock import Mock, patch
12
+
13
+
14
+ # Keep the credential subprocess protocol UTF-8 on every platform.
15
+ for _stream in (sys.stdout, sys.stderr):
16
+ if hasattr(_stream, "reconfigure"):
17
+ _stream.reconfigure(encoding="utf-8", errors=_stream.errors)
18
+
19
+ spec = importlib.util.spec_from_file_location("gemini_auth", Path(__file__).with_name("gemini-auth.py"))
20
+ auth = importlib.util.module_from_spec(spec)
21
+ spec.loader.exec_module(auth)
22
+
23
+
24
+ class AuthenticationTests(unittest.TestCase):
25
+ def setUp(self):
26
+ self.info = {"type": "service_account", "project_id": "account-project", "token_uri": "https://untrusted.invalid/token"}
27
+ self.credentials = Mock(token="fixture-token")
28
+ self.factory = Mock(return_value=self.credentials)
29
+ self.request = Mock(return_value=object())
30
+ transport = types.ModuleType("google.auth.transport.requests")
31
+ transport.Request = self.request
32
+ oauth = types.ModuleType("google.oauth2")
33
+ oauth.service_account = types.SimpleNamespace(Credentials=types.SimpleNamespace(from_service_account_info=self.factory))
34
+ modules = {name: types.ModuleType(name) for name in ("google", "google.auth", "google.auth.transport")}
35
+ modules.update({"google.auth.transport.requests": transport, "google.oauth2": oauth})
36
+ self.addCleanup(patch.stopall)
37
+ patch.dict("sys.modules", modules).start()
38
+ patch.dict(os.environ, {"GCS_CREDS": json.dumps(self.info)}, clear=True).start()
39
+
40
+ def test_scope_endpoint_and_refresh(self):
41
+ self.assertEqual(auth.authenticate(), {"token": "fixture-token", "project": "account-project"})
42
+ info = self.factory.call_args.args[0]
43
+ self.assertEqual(info["token_uri"], "https://oauth2.googleapis.com/token")
44
+ self.assertEqual(self.factory.call_args.kwargs["scopes"], ["https://www.googleapis.com/auth/generative-language.retriever"])
45
+ self.credentials.refresh.assert_called_once_with(self.request.return_value)
46
+
47
+ def test_file_takes_precedence_over_json_environment(self):
48
+ with tempfile.TemporaryDirectory() as directory:
49
+ path = Path(directory) / "service.json"
50
+ path.write_text(json.dumps({**self.info, "project_id": "file-project"}), encoding="utf-8")
51
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = str(path)
52
+ os.environ["GCS_CREDS"] = "invalid-json"
53
+ self.assertEqual(auth.authenticate()["project"], "file-project")
54
+
55
+ def test_quota_project_precedence(self):
56
+ os.environ["GCLOUD_PROJECT_ID"] = "alias-project"
57
+ self.assertEqual(auth.authenticate()["project"], "alias-project")
58
+ os.environ["GOOGLE_CLOUD_PROJECT"] = "explicit-project"
59
+ self.assertEqual(auth.authenticate()["project"], "explicit-project")
60
+
61
+ def test_user_adc_is_rejected_before_exchange(self):
62
+ os.environ["GCS_CREDS"] = json.dumps({"type": "authorized_user"})
63
+ self.assertIn("service-account JSON", auth.authenticate()["error"])
64
+ self.factory.assert_not_called()
65
+
66
+ def test_invalid_json_or_missing_file_does_not_fall_back(self):
67
+ os.environ["GCS_CREDS"] = "private-invalid-json"
68
+ self.assertIn("error", auth.authenticate())
69
+ os.environ["GCS_CREDS"] = json.dumps(self.info)
70
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "/missing/service-account-fixture.json"
71
+ self.assertIn("error", auth.authenticate())
72
+ self.factory.assert_not_called()
73
+
74
+ def test_library_errors_are_redacted(self):
75
+ for boundary in (self.factory, self.credentials.refresh):
76
+ with self.subTest(boundary=boundary):
77
+ boundary.side_effect = RuntimeError("private-key-and-token")
78
+ result = auth.authenticate()
79
+ self.assertIn("error", result)
80
+ self.assertNotIn("private-key-and-token", json.dumps(result))
81
+ boundary.side_effect = None
82
+
83
+ def test_missing_dependency_has_actionable_error(self):
84
+ with patch.dict("sys.modules", {"google.auth.transport.requests": None}):
85
+ self.assertIn("pip install google-auth requests", auth.authenticate()["error"])
86
+ self.factory.assert_not_called()
87
+
88
+
89
+ if __name__ == "__main__":
90
+ unittest.main()
@@ -0,0 +1,122 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { geminiAuth } from "./gemini-auth.mjs";
4
+
5
+ export const GEMINI_TTS_MODEL = "gemini-3.8-flash-tts";
6
+
7
+ export const GEMINI_TTS_MODELS = [
8
+ GEMINI_TTS_MODEL,
9
+ "gemini-3.8-flash-lite-tts",
10
+ "gemini-3.1-flash-tts-preview",
11
+ "gemini-2.5-pro-preview-tts",
12
+ "gemini-2.5-flash-preview-tts",
13
+ ];
14
+
15
+ // 3.8 returns WAV; older models return PCM that we wrap without resampling.
16
+ // The shared engine transcribes the saved audio for word timings.
17
+ export async function synthesizeGemini(
18
+ { text, voiceId = "Kore", model = GEMINI_TTS_MODEL, style, speed = 1, wavAbs },
19
+ { fetchImpl = fetch, authenticate = geminiAuth } = {},
20
+ ) {
21
+ let secret;
22
+ try {
23
+ if (!GEMINI_TTS_MODELS.includes(model)) {
24
+ throw new Error(`Unsupported Gemini TTS model: ${model}`);
25
+ }
26
+ if (speed !== 1) throw new Error("Gemini TTS uses style for pacing; omit speed or use 1");
27
+ const modern = model.startsWith("gemini-3.8-");
28
+ if (!modern && /^(voice_|voicekey_)/.test(voiceId)) {
29
+ throw new Error(
30
+ "Custom Gemini voices require a 3.8 TTS model; use a prebuilt voice with older models",
31
+ );
32
+ }
33
+ const auth = authenticate();
34
+ secret = auth.secret;
35
+ const content = {
36
+ type: "text",
37
+ text:
38
+ !modern && style ? `Read the following text with this delivery: ${style}\n\n${text}` : text,
39
+ };
40
+ if (modern && style) content.annotations = [{ type: "speech_metadata", style }];
41
+ const response = await fetchImpl(
42
+ "https://generativelanguage.googleapis.com/v1beta/interactions",
43
+ {
44
+ method: "POST",
45
+ headers: { "Content-Type": "application/json", ...auth.headers },
46
+ signal: AbortSignal.timeout(120_000),
47
+ body: JSON.stringify({
48
+ model,
49
+ input: [{ type: "user_input", content: [content] }],
50
+ response_format: modern ? { type: "audio", mime_type: "audio/wav" } : { type: "audio" },
51
+ generation_config: { speech_config: [{ voice: voiceId }] },
52
+ store: false,
53
+ }),
54
+ },
55
+ );
56
+ if (!response.ok) {
57
+ const detail = await response.text();
58
+ throw new Error(`Gemini TTS HTTP ${response.status}: ${detail}`);
59
+ }
60
+ const payload = await response.json();
61
+ if (payload.status !== "completed") {
62
+ throw new Error(`Gemini TTS did not complete (${payload.status ?? "missing status"})`);
63
+ }
64
+ const audio = (payload.steps ?? [])
65
+ .filter((step) => step.type === "model_output")
66
+ .flatMap((step) => step.content ?? [])
67
+ .filter((part) => part.type === "audio");
68
+ if (audio.length !== 1 || !audio[0].data) {
69
+ throw new Error("Gemini TTS returned no single audio block");
70
+ }
71
+ let bytes = Buffer.from(audio[0].data, "base64");
72
+ if (!modern && /^audio\/l16(?:;|$)/i.test(audio[0].mime_type ?? "")) {
73
+ bytes = pcmToWav(bytes, audio[0].mime_type);
74
+ } else if (audio[0].mime_type !== "audio/wav") {
75
+ throw new Error("Gemini TTS returned an unsupported audio format");
76
+ }
77
+ if (
78
+ bytes.length <= 44 ||
79
+ bytes.toString("ascii", 0, 4) !== "RIFF" ||
80
+ bytes.toString("ascii", 8, 12) !== "WAVE"
81
+ ) {
82
+ throw new Error("Gemini TTS returned invalid WAV audio");
83
+ }
84
+ mkdirSync(dirname(wavAbs), { recursive: true });
85
+ writeFileSync(wavAbs, bytes);
86
+ return { ok: true, words: null };
87
+ } catch (error) {
88
+ // Error responses must never echo the credential into logs or metadata.
89
+ const message = String(error?.message ?? error);
90
+ return {
91
+ ok: false,
92
+ words: null,
93
+ error: secret ? message.split(secret).join("[redacted]") : message,
94
+ };
95
+ }
96
+ }
97
+
98
+ function pcmToWav(pcm, mime) {
99
+ const rate = Number(/(?:^|;)\s*rate=(\d+)(?:;|$)/i.exec(mime)?.[1]);
100
+ const channels = /(?:^|;)\s*channels=([^;]+)/i.exec(mime)?.[1];
101
+ const codec = /(?:^|;)\s*codec=([^;]+)/i.exec(mime)?.[1];
102
+ if ((channels && channels.trim() !== "1") || (codec && codec.trim() !== "pcm")) {
103
+ throw new Error("Gemini TTS returned unsupported PCM channels or codec");
104
+ }
105
+ if (!Number.isInteger(rate) || rate < 8000 || rate > 96000 || !pcm.length || pcm.length % 2) {
106
+ throw new Error("Gemini TTS returned invalid PCM audio or sample rate");
107
+ }
108
+ const header = Buffer.alloc(44);
109
+ header.write("RIFF");
110
+ header.writeUInt32LE(36 + pcm.length, 4);
111
+ header.write("WAVEfmt ", 8);
112
+ header.writeUInt32LE(16, 16);
113
+ header.writeUInt16LE(1, 20);
114
+ header.writeUInt16LE(1, 22);
115
+ header.writeUInt32LE(rate, 24);
116
+ header.writeUInt32LE(rate * 2, 28);
117
+ header.writeUInt16LE(2, 32);
118
+ header.writeUInt16LE(16, 34);
119
+ header.write("data", 36);
120
+ header.writeUInt32LE(pcm.length, 40);
121
+ return Buffer.concat([header, pcm]);
122
+ }
@@ -0,0 +1,243 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { synthesizeGemini, GEMINI_TTS_MODEL } from "./gemini-tts.mjs";
7
+ import { pickProvider, resolveVoiceId, synthesizeOne } from "./tts.mjs";
8
+
9
+ function fixture(t) {
10
+ const dir = mkdtempSync(join(tmpdir(), "gemini-tts-"));
11
+ const saved = { ...process.env };
12
+ process.env.GEMINI_API_KEY = "test-gemini-key";
13
+ delete process.env.GOOGLE_API_KEY;
14
+ delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
15
+ delete process.env.GCS_CREDS;
16
+ t.after(() => {
17
+ process.env = saved;
18
+ rmSync(dir, { recursive: true, force: true });
19
+ });
20
+ const wav = Buffer.alloc(48);
21
+ wav.write("RIFF");
22
+ wav.writeUInt32LE(40, 4);
23
+ wav.write("WAVEfmt ", 8);
24
+ wav.writeUInt32LE(16, 16);
25
+ wav.writeUInt16LE(1, 20);
26
+ wav.writeUInt16LE(1, 22);
27
+ wav.writeUInt32LE(24000, 24);
28
+ wav.writeUInt32LE(48000, 28);
29
+ wav.writeUInt16LE(2, 32);
30
+ wav.writeUInt16LE(16, 34);
31
+ wav.write("data", 36);
32
+ wav.writeUInt32LE(4, 40);
33
+ const audio = { type: "audio", mime_type: "audio/wav", data: wav.toString("base64") };
34
+ const payload = { status: "completed", steps: [{ type: "model_output", content: [audio] }] };
35
+ return {
36
+ wav,
37
+ audio,
38
+ payload,
39
+ args: { text: "Hello there.", voiceId: "Kore", wavAbs: join(dir, "voice", "one.wav") },
40
+ };
41
+ }
42
+
43
+ test("Gemini preserves verbatim text, directs style separately, saves WAV and requests transcription", async (t) => {
44
+ const { wav, payload, args } = fixture(t);
45
+ t.mock.method(globalThis, "fetch", async (url, options) => {
46
+ assert.equal(url, "https://generativelanguage.googleapis.com/v1beta/interactions");
47
+ assert.equal(options.headers["x-goog-api-key"], "test-gemini-key");
48
+ const body = JSON.parse(options.body);
49
+ assert.equal(body.model, GEMINI_TTS_MODEL);
50
+ assert.equal(body.store, false);
51
+ assert.deepEqual(body.input[0].content, [
52
+ {
53
+ type: "text",
54
+ text: args.text,
55
+ annotations: [{ type: "speech_metadata", style: "Warm and calm" }],
56
+ },
57
+ ]);
58
+ assert.deepEqual(body.generation_config.speech_config, [{ voice: "Kore" }]);
59
+ assert.equal(body.response_format.mime_type, "audio/wav");
60
+ return Response.json(payload);
61
+ });
62
+ const result = await synthesizeOne({ ...args, provider: "gemini", style: "Warm and calm" });
63
+ assert.deepEqual(result, { ok: true, words: null });
64
+ assert.deepEqual(readFileSync(args.wavAbs), wav);
65
+ });
66
+
67
+ test("Flash-Lite and GOOGLE_API_KEY work without a Gemini key", async (t) => {
68
+ const { payload, args } = fixture(t);
69
+ delete process.env.GEMINI_API_KEY;
70
+ process.env.GOOGLE_API_KEY = "test-google-key";
71
+ assert.equal(pickProvider("gemini"), "gemini");
72
+ assert.equal(await resolveVoiceId({ provider: "gemini" }), "Kore");
73
+ const result = await synthesizeGemini(
74
+ { ...args, model: "gemini-3.8-flash-lite-tts" },
75
+ {
76
+ fetchImpl: async (_, options) => {
77
+ assert.equal(options.headers["x-goog-api-key"], "test-google-key");
78
+ const body = JSON.parse(options.body);
79
+ assert.equal(body.model, "gemini-3.8-flash-lite-tts");
80
+ assert.equal(body.input[0].content[0].annotations, undefined);
81
+ return Response.json(payload);
82
+ },
83
+ },
84
+ );
85
+ assert.equal(result.ok, true);
86
+ });
87
+
88
+ test("Gemini remains opt-in even with a key and preserves explicitly chosen voices", async (t) => {
89
+ fixture(t);
90
+ assert.notEqual(pickProvider(), "gemini");
91
+ assert.equal(
92
+ await resolveVoiceId({ provider: "gemini", userVoice: "voice_custom" }),
93
+ "voice_custom",
94
+ );
95
+ delete process.env.GEMINI_API_KEY;
96
+ assert.throws(() => pickProvider("gemini"), /GEMINI_API_KEY or GOOGLE_API_KEY/);
97
+ });
98
+
99
+ test("invalid config fails before spending a generation request", async (t) => {
100
+ const { args } = fixture(t);
101
+ const deps = { fetchImpl: () => assert.fail("must not call the API") };
102
+ for (const change of [{ model: "gemini-2.5-flash" }, { speed: 1.2 }]) {
103
+ const result = await synthesizeGemini({ ...args, ...change }, deps);
104
+ assert.equal(result.ok, false);
105
+ assert.ok(result.error);
106
+ }
107
+ delete process.env.GEMINI_API_KEY;
108
+ assert.match((await synthesizeGemini(args, deps)).error, /needs GEMINI_API_KEY/);
109
+ assert.equal(existsSync(args.wavAbs), false);
110
+ });
111
+
112
+ test("HTTP and network failures stay actionable without leaking the key", async (t) => {
113
+ const { args } = fixture(t);
114
+ const result = await synthesizeGemini(args, {
115
+ fetchImpl: async () => new Response("quota exceeded test-gemini-key", { status: 429 }),
116
+ });
117
+ assert.equal(result.ok, false);
118
+ assert.match(result.error, /HTTP 429: quota exceeded/);
119
+ assert.ok(!result.error.includes("test-gemini-key"));
120
+ const timeout = await synthesizeGemini(args, {
121
+ fetchImpl: async () => {
122
+ throw new Error("timed out");
123
+ },
124
+ });
125
+ assert.match(timeout.error, /timed out/);
126
+ assert.equal(existsSync(args.wavAbs), false);
127
+ });
128
+
129
+ test("incomplete, missing, raw PCM and malformed audio cannot become a successful WAV", async (t) => {
130
+ const { args, payload, audio } = fixture(t);
131
+ const cases = [
132
+ { ...payload, status: "incomplete" },
133
+ { status: "completed", steps: [] },
134
+ ...[{ ...audio, mime_type: "audio/l16" }, { ...audio, data: "not audio" }, audio].map(
135
+ (part, i) => ({
136
+ status: "completed",
137
+ steps: [{ type: "model_output", content: i === 2 ? [part, part] : [part] }],
138
+ }),
139
+ ),
140
+ ];
141
+ for (const body of cases) {
142
+ const result = await synthesizeGemini(args, { fetchImpl: async () => Response.json(body) });
143
+ assert.equal(result.ok, false);
144
+ assert.equal(existsSync(args.wavAbs), false);
145
+ }
146
+ });
147
+
148
+ for (const model of [
149
+ "gemini-3.1-flash-tts-preview",
150
+ "gemini-2.5-pro-preview-tts",
151
+ "gemini-2.5-flash-preview-tts",
152
+ ]) {
153
+ test(`${model} uses legacy delivery prompts and wraps PCM as mono WAV`, async (t) => {
154
+ const { args, wav } = fixture(t);
155
+ const pcm = wav.subarray(44);
156
+ const result = await synthesizeGemini(
157
+ { ...args, model, style: "Warm and clear" },
158
+ {
159
+ fetchImpl: async (_, options) => {
160
+ const body = JSON.parse(options.body);
161
+ assert.deepEqual(body.response_format, { type: "audio" });
162
+ assert.equal(body.input[0].content[0].annotations, undefined);
163
+ assert.ok(body.input[0].content[0].text.endsWith(args.text));
164
+ assert.ok(body.input[0].content[0].text.includes("Warm and clear"));
165
+ return Response.json({
166
+ status: "completed",
167
+ steps: [
168
+ {
169
+ type: "model_output",
170
+ content: [
171
+ {
172
+ type: "audio",
173
+ mime_type: "audio/L16;codec=pcm;rate=24000",
174
+ data: pcm.toString("base64"),
175
+ },
176
+ ],
177
+ },
178
+ ],
179
+ });
180
+ },
181
+ },
182
+ );
183
+ assert.equal(result.ok, true, result.error);
184
+ assert.deepEqual(readFileSync(args.wavAbs), wav);
185
+ });
186
+ }
187
+
188
+ test("service-account bearer and quota project reach synthesis, and token errors are redacted", async (t) => {
189
+ const { args, payload } = fixture(t);
190
+ const authenticate = () => ({
191
+ headers: { Authorization: "Bearer secret-token", "x-goog-user-project": "test-project" },
192
+ secret: "secret-token",
193
+ });
194
+ const ok = await synthesizeGemini(args, {
195
+ authenticate,
196
+ fetchImpl: async (_, options) => {
197
+ assert.equal(options.headers.Authorization, "Bearer secret-token");
198
+ assert.equal(options.headers["x-goog-user-project"], "test-project");
199
+ assert.equal(options.headers["x-goog-api-key"], undefined);
200
+ return Response.json(payload);
201
+ },
202
+ });
203
+ assert.equal(ok.ok, true);
204
+ const failed = await synthesizeGemini(args, {
205
+ authenticate,
206
+ fetchImpl: async () => new Response("denied secret-token", { status: 403 }),
207
+ });
208
+ assert.match(failed.error, /HTTP 403/);
209
+ assert.ok(!failed.error.includes("secret-token"));
210
+ });
211
+
212
+ test("older models reject unsupported PCM and custom voices", async (t) => {
213
+ const { args } = fixture(t);
214
+ for (const mime of [
215
+ "audio/l16",
216
+ "audio/l16;rate=0",
217
+ "audio/l16;rate=24000;channels=2",
218
+ "audio/l16;rate=24000;codec=other",
219
+ ]) {
220
+ const result = await synthesizeGemini(
221
+ { ...args, model: "gemini-3.1-flash-tts-preview" },
222
+ {
223
+ fetchImpl: async () =>
224
+ Response.json({
225
+ status: "completed",
226
+ steps: [
227
+ {
228
+ type: "model_output",
229
+ content: [{ type: "audio", mime_type: mime, data: "AAAAAA==" }],
230
+ },
231
+ ],
232
+ }),
233
+ },
234
+ );
235
+ assert.equal(result.ok, false);
236
+ assert.equal(existsSync(args.wavAbs), false);
237
+ }
238
+ const result = await synthesizeGemini(
239
+ { ...args, model: "gemini-2.5-pro-preview-tts", voiceId: "voice_custom" },
240
+ { fetchImpl: () => assert.fail("must not generate") },
241
+ );
242
+ assert.match(result.error, /Custom Gemini voices require/);
243
+ });
@@ -1,3 +1,4 @@
1
+ import { fetchMedia } from "../../../scripts/lib/media-fetch.mjs";
1
2
  // heygen.mjs — vendored HeyGen REST helpers (auth + transport) for the audio
2
3
  // pipeline. The credential resolver matches the hyperframes CLI auth: first
3
4
  // usable source wins — $HEYGEN_API_KEY / $HYPERFRAMES_API_KEY → a nearby .env → ~/.heygen/
@@ -125,7 +126,7 @@ export async function heygenJSON(path, { method = "GET", headers = {}, body } =
125
126
 
126
127
  // Download a (presigned) URL to destPath; returns byte length.
127
128
  export async function downloadTo(url, destPath) {
128
- const res = await fetch(url);
129
+ const res = await fetchMedia(url);
129
130
  if (!res.ok) throw new Error(`download HTTP ${res.status}: ${String(url).slice(0, 80)}`);
130
131
  const bytes = Buffer.from(await res.arrayBuffer());
131
132
  mkdirSync(dirname(destPath), { recursive: true });
@@ -1,3 +1,4 @@
1
+ import { fetchMedia } from "../../../scripts/lib/media-fetch.mjs";
1
2
  // tts.mjs — multi-provider TTS for the media audio engine. The provider chain,
2
3
  // auto-detected from env, is the one documented in ../SKILL.md:
3
4
  //
@@ -19,6 +20,8 @@ import { tmpdir } from "node:os";
19
20
  import { dirname, join } from "node:path";
20
21
  import { heygenAuthHeaders, heygenCredential, heygenJSON } from "./heygen.mjs";
21
22
  import { pythonInvocation } from "./python.mjs";
23
+ import { synthesizeGemini } from "./gemini-tts.mjs";
24
+ import { geminiConfigured } from "./gemini-auth.mjs";
22
25
 
23
26
  // ── provider detection ────────────────────────────────────────────────────────
24
27
  export function heygenAvailable() {
@@ -36,8 +39,12 @@ export function elevenlabsAvailable() {
36
39
  // First available provider wins; an explicit choice is honored (and validated).
37
40
  export function pickProvider(userProvider) {
38
41
  if (userProvider) {
39
- if (!["heygen", "elevenlabs", "kokoro"].includes(userProvider))
40
- throw new Error(`invalid provider "${userProvider}" (heygen | elevenlabs | kokoro)`);
42
+ if (!["heygen", "elevenlabs", "kokoro", "gemini"].includes(userProvider))
43
+ throw new Error(`invalid provider "${userProvider}" (heygen | elevenlabs | kokoro | gemini)`);
44
+ if (userProvider === "gemini" && !geminiConfigured())
45
+ throw new Error(
46
+ "provider=gemini needs GEMINI_API_KEY or GOOGLE_API_KEY, or service-account credentials (GOOGLE_APPLICATION_CREDENTIALS or GCS_CREDS)",
47
+ );
41
48
  if (userProvider === "heygen" && !heygenAvailable())
42
49
  throw new Error(
43
50
  "provider=heygen but no HeyGen credentials (set $HEYGEN_API_KEY or run `npx hyperframes auth login`)",
@@ -55,6 +62,7 @@ export function pickProvider(userProvider) {
55
62
  // their own defaults.
56
63
  export async function resolveVoiceId({ provider, userVoice, lang = "en" }) {
57
64
  if (userVoice) return userVoice;
65
+ if (provider === "gemini") return "Kore";
58
66
  if (provider === "elevenlabs") return "21m00Tcm4TlvDq8ikWAM"; // Rachel
59
67
  if (provider === "kokoro") {
60
68
  if (lang === "en") return "am_michael";
@@ -247,10 +255,14 @@ export async function synthesizeOne({
247
255
  voiceId,
248
256
  lang = "en",
249
257
  speed = 1.0,
258
+ model,
259
+ style,
250
260
  wavAbs,
251
261
  hyperframesDir,
252
262
  }) {
253
263
  if (provider === "heygen") return synthesizeHeygen({ text, voiceId, lang, speed, wavAbs });
264
+ if (provider === "gemini")
265
+ return synthesizeGemini({ text, voiceId, model, style, speed, wavAbs });
254
266
  if (provider === "elevenlabs") {
255
267
  // The Python helper writes straight to wavAbs; unlike heygen (transcodeToWav)
256
268
  // and kokoro (the `hyperframes tts` CLI), it does NOT create the parent dir,
@@ -312,7 +324,7 @@ export async function synthesizeHeygen({ text, voiceId, lang, speed, wavAbs }, d
312
324
  if (!inner.audio_url) {
313
325
  return { ok: false, words: null, error: "HeyGen /voices/speech returned no audio_url" };
314
326
  }
315
- const res = await fetchImpl(inner.audio_url);
327
+ const res = await fetchMedia(inner.audio_url, { fetchImpl });
316
328
  if (!res.ok) {
317
329
  return { ok: false, words: null, error: `audio_url fetch failed: HTTP ${res.status}` };
318
330
  }
@@ -48,10 +48,14 @@ function isProcessAlive(pid) {
48
48
  }
49
49
 
50
50
  function readTail(path, maxChars = 6000) {
51
- if (!path || !existsSync(path)) return "";
52
- const s = statSync(path);
53
- const txt = readFileSync(path, "utf8");
54
- return txt.slice(Math.max(0, txt.length - Math.min(maxChars, s.size)));
51
+ if (!path) return "";
52
+ try {
53
+ const txt = readFileSync(path, "utf8");
54
+ return txt.slice(Math.max(0, txt.length - maxChars));
55
+ } catch (error) {
56
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return "";
57
+ throw error;
58
+ }
55
59
  }
56
60
 
57
61
  function detectFailure(logTail) {
@@ -0,0 +1,65 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { test } from "node:test";
4
+ import { runInNewContext } from "node:vm";
5
+
6
+ const source = readFileSync(new URL("./wait-bgm.mjs", import.meta.url), "utf8");
7
+ const start = source.indexOf("function readTail(");
8
+ const end = source.indexOf("\nfunction detectFailure(", start);
9
+ function load(read) {
10
+ return runInNewContext(`${source.slice(start, end)}; readTail`, {
11
+ readFileSync: read,
12
+ existsSync: () => true,
13
+ statSync: () => {
14
+ throw new Error("pathname stat must not precede log read");
15
+ },
16
+ });
17
+ }
18
+
19
+ test("reads a log once without a separate pathname check", () => {
20
+ let calls = 0;
21
+ const read = load((path, encoding) => {
22
+ calls++;
23
+ assert.equal(path, "renderer.log");
24
+ assert.equal(encoding, "utf8");
25
+ return "prefix\nRuntimeError: failed";
26
+ });
27
+ assert.equal(read("renderer.log", 20), "RuntimeError: failed");
28
+ assert.equal(calls, 1);
29
+ });
30
+
31
+ test("preserves UTF-16 character tail boundaries and zero-length tails", () => {
32
+ const txt = "🎵日本語".repeat(2000);
33
+ const read = load(() => txt);
34
+ assert.equal(read("log"), txt.slice(-6000));
35
+ assert.equal(read("log", 0), "");
36
+ });
37
+
38
+ for (const code of ["ENOENT", "ENOTDIR"]) {
39
+ test(`a log removed during polling is absent (${code})`, () => {
40
+ const read = load(() => {
41
+ throw Object.assign(new Error(code), { code });
42
+ });
43
+ assert.equal(read("log"), "");
44
+ });
45
+ }
46
+
47
+ test("preserves unexpected read errors", () => {
48
+ const error = Object.assign(new Error("denied"), { code: "EACCES" });
49
+ assert.throws(
50
+ () =>
51
+ load(() => {
52
+ throw error;
53
+ })("log"),
54
+ (caught) => caught === error,
55
+ );
56
+ });
57
+
58
+ test("empty log path does not access the filesystem", () => {
59
+ assert.equal(
60
+ load(() => {
61
+ throw new Error("unexpected read");
62
+ })(""),
63
+ "",
64
+ );
65
+ });
@@ -9,9 +9,10 @@ shot), use the shared engine at `audio/scripts/audio.mjs`. It takes a neutral
9
9
  node <SKILL_DIR>/audio/scripts/audio.mjs --request ./audio_request.json --out ./audio_meta.json
10
10
  ```
11
11
 
12
- - **Request** `{ provider?, lang?, speed?, lines: [{ id, text, sfx?: [names] }], bgm: { mode?, query?, prompt? } }`: `id` joins each line back to your model; `bgm.mode` = `retrieve | generate | none` (omit for auto). `--only tts,bgm,sfx` runs a subset and merges into an existing `--out`.
12
+ - **Request** `{ provider?, voice?, lang?, speed?, tts_model?, style?, lines: [{ id, text, style?, sfx?: [names] }], bgm: { mode?, query?, prompt? } }`: `id` joins each line back to your model; `bgm.mode` = `retrieve | generate | none` (omit for auto). `--only tts,bgm,sfx` runs a subset and merges into an existing `--out`.
13
+ - **Gemini narration** is opt-in with `provider: "gemini"` and an API key or service-account credentials. `tts_model` defaults to `gemini-3.8-flash-tts`; 3.8 Flash-Lite, 3.1 Flash Preview, and 2.5 Pro/Flash Preview TTS are also supported. `style` directs delivery; a line's style overrides the request's style. CLI overrides: `--tts-model`, `--style`. See `audio/references/tts.md` for a complete request. The existing automatic provider order is unchanged.
13
14
  - **Output** `audio_meta.json` (id-keyed): `voices[].{path,duration_s,words[]}` (word timestamps for captions), `sfx[]`, `bgm`, `total_duration_s`.
14
- - **HeyGen free-usage path**: HeyGen CLI auth unlocks TTS plus music/SFX retrieval. Local/provider-specific generators are explicit alternatives where installed; run `node <SKILL_DIR>/scripts/resolve.mjs --doctor` before assuming retrieval or TTS will work.
15
+ - **HeyGen free-usage path**: HeyGen CLI auth unlocks TTS plus music/SFX retrieval. Local/provider-specific generators are explicit alternatives where installed; run `npx hyperframes media-use resolve --doctor` before assuming retrieval or TTS will work.
15
16
  - If BGM took the generate path (`bgm_pending: true`), run `audio/scripts/wait-bgm.mjs` before final render.
16
17
 
17
18
  Single-shot helpers: `audio/scripts/heygen-tts.mjs` (one voice file). Transcription / background removal / captions use the `hyperframes` CLI (`transcribe`, `remove-background`), see the per-topic guides in `audio/references/` (`tts.md`, `bgm.md`, `sfx.md`, `transcribe.md`, `remove-background.md`, `captions/`).