hyperframes 0.8.184 → 0.8.185

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 (309) hide show
  1. package/dist/{add-5ZTD3MUZ.js → add-ODTNPRGG.js} +14 -14
  2. package/dist/{agentPromptGenerator-VIIPZCJ3.js → agentPromptGenerator-KYO5MKTW.js} +1 -1
  3. package/dist/{animatedGifPrep-AGASIWVA.js → animatedGifPrep-PEP5GWOJ.js} +9 -9
  4. package/dist/api.js +17 -17
  5. package/dist/{asset-LSTEMPIB.js → asset-EZG42XPA.js} +2 -2
  6. package/dist/{assetCataloger-BKBMNL3A.js → assetCataloger-OHAJFCA7.js} +9 -9
  7. package/dist/{audio-KGK44XE4.js → audio-65R4T74I.js} +16 -15
  8. package/dist/{auth-MJWKYK77.js → auth-VP2SIZFN.js} +1 -1
  9. package/dist/{autoUpdate-2GGDKYR2.js → autoUpdate-RVSUX4SS.js} +2 -2
  10. package/dist/{backgroundChecks-7GRX5VQY.js → backgroundChecks-LEHK74NS.js} +3 -3
  11. package/dist/backgroundChecksWorker.js +3 -3
  12. package/dist/{batchRender-BCV2LELR.js → batchRender-3X6CL7PK.js} +5 -5
  13. package/dist/{beats-7BCFRBAL.js → beats-66VPQLSC.js} +6 -6
  14. package/dist/{benchmark-2R6CN5MQ.js → benchmark-PBJVTOJP.js} +10 -10
  15. package/dist/{browser-6AFGOI7G.js → browser-TPL5MWGC.js} +3 -3
  16. package/dist/{bundleWithLocalizedFonts-2EGPA5ZO.js → bundleWithLocalizedFonts-67MLTHKM.js} +2 -2
  17. package/dist/{capture-HOIAW6MO.js → capture-F3GNMRZM.js} +13 -13
  18. package/dist/{capture-HA2UHLIU.js → capture-YYRE7EV3.js} +2 -2
  19. package/dist/{captureCompositionFrame-4236FVWJ.js → captureCompositionFrame-PXV2ISHD.js} +13 -13
  20. package/dist/{catalog-A6CZAWW4.js → catalog-6SDD3OHF.js} +14 -14
  21. package/dist/{catch-up-TNA4DJOA.js → catch-up-CWIVA5VG.js} +4 -4
  22. package/dist/{check-5E5U2EQZ.js → check-YQ6REK2S.js} +34 -34
  23. package/dist/{checkBrowser-FNINXBMZ.js → checkBrowser-5XZCBRXM.js} +31 -31
  24. package/dist/{chunk-2ULXZ4PG.js → chunk-2TAS5O4P.js} +56 -11
  25. package/dist/{chunk-3TXNNBI2.js → chunk-37CNYTQQ.js} +2 -2
  26. package/dist/chunk-3NPOFC43.js +3805 -0
  27. package/dist/{chunk-EEP5Y5IE.js → chunk-46HEFGXJ.js} +1 -1
  28. package/dist/{chunk-2XA7KXQO.js → chunk-4CTNDBE3.js} +2 -2
  29. package/dist/{chunk-MFAETWIF.js → chunk-55YELV7B.js} +1 -1
  30. package/dist/{chunk-LRGYPXHF.js → chunk-5GUKZRQN.js} +1 -1
  31. package/dist/{chunk-SAX7HQ5E.js → chunk-6ASIXMFP.js} +1 -1
  32. package/dist/{chunk-SN7KI5JA.js → chunk-6GPR3G3C.js} +2 -2
  33. package/dist/{chunk-E32ZQ2WO.js → chunk-6RIZY7OP.js} +1 -1
  34. package/dist/{chunk-5CS4T6C6.js → chunk-7L37AB57.js} +1 -1
  35. package/dist/{chunk-BCHSTE7U.js → chunk-7PS6RVWD.js} +1 -1
  36. package/dist/{chunk-X3CG5YQA.js → chunk-7RAY4WZP.js} +3 -3
  37. package/dist/{chunk-YWHCXIMD.js → chunk-A6QCO474.js} +5 -5
  38. package/dist/{chunk-YMS5FOMC.js → chunk-B2A56S5C.js} +1 -1
  39. package/dist/{chunk-IWTNTUPQ.js → chunk-BH7TTIPN.js} +11 -13
  40. package/dist/{chunk-DWUGKEHR.js → chunk-BRAPBUPQ.js} +1 -1
  41. package/dist/{chunk-R2BFTECP.js → chunk-CX3USX23.js} +3 -3
  42. package/dist/{chunk-JB6ARMQS.js → chunk-D42NU3DZ.js} +1 -1
  43. package/dist/{chunk-GDEV42FA.js → chunk-EMAWIZ54.js} +481 -36
  44. package/dist/{chunk-UNNXFKVG.js → chunk-EMSDWBG3.js} +2 -2
  45. package/dist/{chunk-CXABAXB3.js → chunk-ESBMUZXZ.js} +1 -1
  46. package/dist/{chunk-EM7C7H7S.js → chunk-F3OPUWQX.js} +7 -7
  47. package/dist/{chunk-YOZPQCSR.js → chunk-FX6RUJUY.js} +3 -3
  48. package/dist/{chunk-PSKEKWVU.js → chunk-G2UANIDX.js} +1 -1
  49. package/dist/{chunk-UKOTVJFV.js → chunk-H5SM5PJI.js} +1 -1
  50. package/dist/{chunk-YWNJTZGN.js → chunk-H6UY725W.js} +2 -2
  51. package/dist/{chunk-PFYWVUHA.js → chunk-HDIRZGK4.js} +1 -1
  52. package/dist/{chunk-HR23IKLZ.js → chunk-HHSJJFVM.js} +2 -2
  53. package/dist/{chunk-LLC4CS6I.js → chunk-HXUTX5GR.js} +3 -3
  54. package/dist/{chunk-RGXVEYHI.js → chunk-I3IH5MV5.js} +6 -10
  55. package/dist/{chunk-Q33P4EAC.js → chunk-I45HL3VP.js} +3 -3
  56. package/dist/{chunk-TYXH3EKP.js → chunk-I5JRXMCV.js} +16 -1
  57. package/dist/{chunk-4N4P3ZWL.js → chunk-I7QMXZSB.js} +2 -2
  58. package/dist/{chunk-ICJEAFPD.js → chunk-IDFNKFPL.js} +7 -7
  59. package/dist/{chunk-VNVQGDPC.js → chunk-IDGLGXPA.js} +1 -1
  60. package/dist/{chunk-2PVE4WS2.js → chunk-IPDKZ5IN.js} +33 -6
  61. package/dist/{chunk-ABZYYUUH.js → chunk-ISTRDGIX.js} +1 -1
  62. package/dist/{chunk-3MDJIWOV.js → chunk-IVEFOUZI.js} +46 -30
  63. package/dist/{chunk-4KKKBEP3.js → chunk-J5XEZ24I.js} +3 -3
  64. package/dist/{chunk-7HJALUIO.js → chunk-K2ZKXTVL.js} +1 -1
  65. package/dist/{chunk-TE427YID.js → chunk-K73RJB23.js} +2 -2
  66. package/dist/{chunk-3QVMJ7GX.js → chunk-KQ4JEJLF.js} +9 -9
  67. package/dist/{chunk-J3QM7AHP.js → chunk-KROXPHXX.js} +9 -3
  68. package/dist/{chunk-OLZENUPI.js → chunk-LLFQA7XF.js} +1 -1
  69. package/dist/chunk-LLMSX6V6.js +17 -0
  70. package/dist/{chunk-WTFUDNW2.js → chunk-LYKSTRAY.js} +3 -3
  71. package/dist/{chunk-VDM3FEDP.js → chunk-OAG47RE6.js} +1 -1
  72. package/dist/{chunk-FGJEUV4S.js → chunk-PHMBMOXF.js} +1 -1
  73. package/dist/{chunk-7MB2ZAQF.js → chunk-PMMQRJDE.js} +1 -1
  74. package/dist/{chunk-ADDHW2FS.js → chunk-Q447CX26.js} +82 -12
  75. package/dist/{chunk-YALM6XN4.js → chunk-Q47FAYYK.js} +3 -3
  76. package/dist/{chunk-ZOCHDXDG.js → chunk-RTBSA3EA.js} +1 -1
  77. package/dist/{chunk-UJKUB2LH.js → chunk-RUEGYANA.js} +11 -11
  78. package/dist/{chunk-OYLO6TYK.js → chunk-RUOG4VB7.js} +1 -1
  79. package/dist/{chunk-ZRHO6YMM.js → chunk-SKYPMHQC.js} +4 -4
  80. package/dist/{chunk-ES426XRJ.js → chunk-SQVRXHOW.js} +3 -3
  81. package/dist/{chunk-QL2PNIXC.js → chunk-TAX2ZJAP.js} +24 -16
  82. package/dist/{chunk-KN46TGHV.js → chunk-TOI35GN2.js} +1 -1
  83. package/dist/{chunk-74JMKLVU.js → chunk-UPR3MU6Z.js} +1 -1
  84. package/dist/{chunk-JIUTXKWA.js → chunk-WQLKDL35.js} +2 -1
  85. package/dist/{chunk-WHPN2TIT.js → chunk-X6L6MD5B.js} +5 -5
  86. package/dist/{chunk-7NRI7HYX.js → chunk-XWC7CNPY.js} +1 -1
  87. package/dist/{clean-HGASSMWI.js → clean-7J6ZCCGV.js} +18 -18
  88. package/dist/cli.js +53 -53
  89. package/dist/{cloud-JPDVNXHN.js → cloud-7IJCJCDD.js} +4 -4
  90. package/dist/{cloudrun-AMMGSDRP.js → cloudrun-UNZRIIDB.js} +7 -7
  91. package/dist/commands/contrast-audit.browser.js +1 -2
  92. package/dist/commands/layout-audit.browser.js +196 -0
  93. package/dist/{compare-LHHVHUN3.js → compare-Q7VW5JZT.js} +28 -28
  94. package/dist/{compiler-Q3N6QXMT.js → compiler-KPHYGFXL.js} +6 -6
  95. package/dist/{component-D72J32Q7.js → component-UUBREJKN.js} +3 -3
  96. package/dist/{compositions-UQRJ5ROG.js → compositions-T4FTKIDP.js} +13 -13
  97. package/dist/{delete-P42VIDDA.js → delete-WZQF2IIA.js} +2 -2
  98. package/dist/{deploy-WZTH57O4.js → deploy-JU7A3XNC.js} +1 -1
  99. package/dist/{deterministicFonts-VQ5UBMXY.js → deterministicFonts-EGAAE7QL.js} +3 -3
  100. package/dist/{dist-SENYPQGW.js → dist-LM7AVHOU.js} +3 -3
  101. package/dist/{dist-DYS24AMA.js → dist-ULGTPSU4.js} +8 -4
  102. package/dist/docs/data-attributes.md +1 -1
  103. package/dist/{doctor-KEJIG2SK.js → doctor-VSNGRKXJ.js} +13 -13
  104. package/dist/{events-CYKSNR7N.js → events-CRW2DYUT.js} +3 -3
  105. package/dist/{events-UZXFQPMT.js → events-E63YBUL2.js} +3 -3
  106. package/dist/{feedback-LXNQF7CA.js → feedback-OSZWVR6N.js} +8 -8
  107. package/dist/{ffmpeg-BYHON577.js → ffmpeg-JLH3Z7G7.js} +9 -9
  108. package/dist/{figma-EIQ35PS6.js → figma-2BQNNR53.js} +3 -3
  109. package/dist/fontLocalizeCli.js +12 -12
  110. package/dist/{get-CC2YL7QO.js → get-P2XI53QW.js} +2 -2
  111. package/dist/{gpuPolicy-VFSACUQV.js → gpuPolicy-NTKQJY75.js} +9 -9
  112. package/dist/{grade-compare-TUQL7IV2.js → grade-compare-OYASIHIB.js} +29 -29
  113. package/dist/{gsapParserAcorn-7NCK2LVT.js → gsapParserAcorn-SKODXC2N.js} +2 -2
  114. package/dist/{help-ROROO5K6.js → help-HK6K2N4G.js} +1 -1
  115. package/dist/{history-PNELYLXX.js → history-JMTYKLXW.js} +6 -6
  116. package/dist/hyperframe.manifest.json +1 -1
  117. package/dist/hyperframe.runtime.iife.js +63 -63
  118. package/dist/hyperframes-player.global.js +1 -1
  119. package/dist/{info-4FZTH6AS.js → info-AW7CIVRE.js} +9 -9
  120. package/dist/{init-I43GHHVP.js → init-6KIDG4LB.js} +30 -30
  121. package/dist/{inspect-IT4TYIZ3.js → inspect-B7BLYMRZ.js} +29 -29
  122. package/dist/{installer-CWOJVK5K.js → installer-ZP2LQZUQ.js} +7 -7
  123. package/dist/{keyframes-3FBOGVD4.js → keyframes-EQ36A5JE.js} +7 -7
  124. package/dist/{lambda-H3V32I7M.js → lambda-E6VURLIS.js} +9 -9
  125. package/dist/{layout-BZ5LYJ5C.js → layout-XYC64QQQ.js} +29 -29
  126. package/dist/{lint-ZTNSGP2S.js → lint-V5DJYWBX.js} +9 -9
  127. package/dist/{lintProject-C2ONSH2U.js → lintProject-JATQKR64.js} +3 -3
  128. package/dist/{list-XM4GWJYY.js → list-RUGVRTVD.js} +2 -2
  129. package/dist/{login-H6HYKMWX.js → login-ZTKGKXGS.js} +4 -4
  130. package/dist/{manager-TWVDSJXO.js → manager-JUP7PM6N.js} +10 -10
  131. package/dist/{media-treatment-N7MY5NXB.js → media-treatment-VIUPK4OR.js} +15 -15
  132. package/dist/{models-WEMIMYBD.js → models-LJS2HBOZ.js} +13 -13
  133. package/dist/{motionShot-RHICTVPU.js → motionShot-3UAXZ7SN.js} +16 -16
  134. package/dist/{normalize-audio-ZFUZCJE3.js → normalize-audio-SBYI4YIH.js} +4 -4
  135. package/dist/{open-IBJ4FL3G.js → open-S27TUD3A.js} +14 -14
  136. package/dist/{parakeetRunner-XXO4WSLR.js → parakeetRunner-ETLMLJ4L.js} +14 -14
  137. package/dist/{pipeline-6ML3LUVK.js → pipeline-I2M22M24.js} +10 -10
  138. package/dist/{play-RV4NAVF5.js → play-COJU6VJK.js} +21 -21
  139. package/dist/{preflight-R3DEFMLA.js → preflight-FZ24NCHH.js} +10 -10
  140. package/dist/{present-5TBJGE6B.js → present-YAHKLC5D.js} +17 -17
  141. package/dist/{preview-FLKZ5CXJ.js → preview-KQOEU5YE.js} +20 -20
  142. package/dist/{producer-7XYSPVKH.js → producer-DTNNG4T6.js} +1 -1
  143. package/dist/{projectConfig-TBU2VWZX.js → projectConfig-4A76VYUP.js} +9 -9
  144. package/dist/{publication-CWWUJYJ5.js → publication-SQPMC2W3.js} +4 -4
  145. package/dist/{publish-FSNE4FEB.js → publish-QUEJBGGT.js} +11 -11
  146. package/dist/{remove-background-MDFWRWCT.js → remove-background-FP2ETAQH.js} +11 -11
  147. package/dist/{render-F4JQ553X.js → render-BIFIL4U6.js} +31 -31
  148. package/dist/{render-DBGQZAZT.js → render-GQFXJWLV.js} +17 -29
  149. package/dist/{render-LWZKXCLY.js → render-P664SMC7.js} +5 -5
  150. package/dist/{render-batch-PU7NMLIJ.js → render-batch-ZCPMYH6Z.js} +5 -5
  151. package/dist/renderSetupWorker.js +3 -3
  152. package/dist/{resolver-RUBGFXPC.js → resolver-BUC2GFZQ.js} +6 -6
  153. package/dist/{sherpa-X2TBNO6P.js → sherpa-HFT6XEP6.js} +13 -13
  154. package/dist/skills/hyperframes/SKILL.md +52 -174
  155. package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/FRAME.md +289 -0
  156. package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/caption-skin.html +220 -0
  157. package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
  158. package/dist/skills/hyperframes/assets/frame-presets/blockframe/FRAME.md +275 -0
  159. package/dist/skills/hyperframes/assets/frame-presets/blockframe/caption-skin.html +223 -0
  160. package/dist/skills/hyperframes/assets/frame-presets/blockframe/frame-showcase.html +1347 -0
  161. package/dist/skills/hyperframes/assets/frame-presets/blue-professional/FRAME.md +307 -0
  162. package/dist/skills/hyperframes/assets/frame-presets/blue-professional/caption-skin.html +222 -0
  163. package/dist/skills/hyperframes/assets/frame-presets/blue-professional/frame-showcase.html +1372 -0
  164. package/dist/skills/hyperframes/assets/frame-presets/bold-poster/FRAME.md +275 -0
  165. package/dist/skills/hyperframes/assets/frame-presets/bold-poster/caption-skin.html +229 -0
  166. package/dist/skills/hyperframes/assets/frame-presets/bold-poster/frame-showcase.html +1140 -0
  167. package/dist/skills/hyperframes/assets/frame-presets/broadside/FRAME.md +284 -0
  168. package/dist/skills/hyperframes/assets/frame-presets/broadside/caption-skin.html +224 -0
  169. package/dist/skills/hyperframes/assets/frame-presets/broadside/frame-showcase.html +1166 -0
  170. package/dist/skills/hyperframes/assets/frame-presets/capsule/FRAME.md +284 -0
  171. package/dist/skills/hyperframes/assets/frame-presets/capsule/caption-skin.html +223 -0
  172. package/dist/skills/hyperframes/assets/frame-presets/capsule/frame-showcase.html +1465 -0
  173. package/dist/skills/hyperframes/assets/frame-presets/cartesian/FRAME.md +278 -0
  174. package/dist/skills/hyperframes/assets/frame-presets/cartesian/caption-skin.html +221 -0
  175. package/dist/skills/hyperframes/assets/frame-presets/cartesian/frame-showcase.html +1263 -0
  176. package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/FRAME.md +265 -0
  177. package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/caption-skin.html +237 -0
  178. package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
  179. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/FRAME.md +277 -0
  180. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/caption-skin.html +223 -0
  181. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/EBGaramond-400.woff2 +0 -0
  182. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/EBGaramond-700.woff2 +0 -0
  183. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/Inter-400.woff2 +0 -0
  184. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/Inter-700.woff2 +0 -0
  185. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/JetBrainsMono-400.woff2 +0 -0
  186. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/JetBrainsMono-700.woff2 +0 -0
  187. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-eb-garamond.txt +93 -0
  188. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-inter.txt +93 -0
  189. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-jetbrains-mono.txt +93 -0
  190. package/dist/skills/hyperframes/assets/frame-presets/code-editorial/frame-showcase.html +1438 -0
  191. package/dist/skills/hyperframes/assets/frame-presets/coral/FRAME.md +314 -0
  192. package/dist/skills/hyperframes/assets/frame-presets/coral/caption-skin.html +225 -0
  193. package/dist/skills/hyperframes/assets/frame-presets/coral/frame-showcase.html +1268 -0
  194. package/dist/skills/hyperframes/assets/frame-presets/creative-mode/FRAME.md +333 -0
  195. package/dist/skills/hyperframes/assets/frame-presets/creative-mode/caption-skin.html +226 -0
  196. package/dist/skills/hyperframes/assets/frame-presets/creative-mode/frame-showcase.html +1330 -0
  197. package/dist/skills/hyperframes/assets/frame-presets/daisy-days/FRAME.md +274 -0
  198. package/dist/skills/hyperframes/assets/frame-presets/daisy-days/caption-skin.html +229 -0
  199. package/dist/skills/hyperframes/assets/frame-presets/daisy-days/frame-showcase.html +1444 -0
  200. package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/FRAME.md +265 -0
  201. package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/caption-skin.html +222 -0
  202. package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/frame-showcase.html +1273 -0
  203. package/dist/skills/hyperframes/references/captions.md +46 -0
  204. package/dist/skills/hyperframes/references/film.md +47 -0
  205. package/dist/skills/hyperframes/references/format.md +157 -0
  206. package/dist/skills/hyperframes/references/look.md +69 -0
  207. package/dist/skills/hyperframes/references/motion.md +94 -0
  208. package/dist/skills/hyperframes/references/overlays.md +39 -0
  209. package/dist/skills/hyperframes/references/plugin-installation.md +3 -3
  210. package/dist/skills/hyperframes/references/review.md +51 -0
  211. package/dist/skills/hyperframes/references/runtimes.md +43 -0
  212. package/dist/skills/hyperframes/references/skill-lifecycle.md +21 -13
  213. package/dist/skills/hyperframes/references/sound.md +43 -0
  214. package/dist/skills/hyperframes/references/spec.md +76 -0
  215. package/dist/skills/hyperframes/references/story.md +103 -0
  216. package/dist/skills/hyperframes/references/storyboard-format.md +30 -22
  217. package/dist/skills/hyperframes/references/storyboard.md +36 -0
  218. package/dist/skills/hyperframes/references/types/3d.md +25 -0
  219. package/dist/skills/hyperframes/references/types/character.md +20 -0
  220. package/dist/skills/hyperframes/references/types/data.md +19 -0
  221. package/dist/skills/hyperframes/references/types/explainer.md +34 -0
  222. package/dist/skills/hyperframes/references/types/launch.md +31 -0
  223. package/dist/skills/hyperframes/references/types/music.md +24 -0
  224. package/dist/skills/hyperframes/references/types/vertical.md +23 -0
  225. package/dist/skills/hyperframes/scripts/bake-basemap.mjs +324 -0
  226. package/dist/skills/hyperframes/scripts/lib/main-module.test.mjs +1 -6
  227. package/dist/skills/hyperframes/scripts/locate.mjs +228 -0
  228. package/dist/skills/hyperframes-cli/SKILL.md +76 -179
  229. package/dist/skills/hyperframes-cli/references/beats.md +1 -1
  230. package/dist/skills/hyperframes-cli/references/cloud.md +2 -2
  231. package/dist/skills/hyperframes-cli/references/cloudrun.md +1 -1
  232. package/dist/skills/hyperframes-cli/references/conventions.md +17 -0
  233. package/dist/skills/hyperframes-cli/references/feedback.md +14 -0
  234. package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +11 -7
  235. package/dist/skills/hyperframes-cli/references/lambda.md +1 -1
  236. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +19 -8
  237. package/dist/skills/hyperframes-cli/references/preview-render.md +8 -2
  238. package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +8 -0
  239. package/dist/skills/media-use/SKILL.md +47 -91
  240. package/dist/skills/media-use/audio/references/bgm.md +1 -1
  241. package/dist/skills/media-use/audio/references/captions/authoring.md +1 -2
  242. package/dist/skills/media-use/audio/references/captions/motion.md +5 -5
  243. package/dist/skills/media-use/audio/references/tts.md +0 -3
  244. package/dist/skills/media-use/audio/scripts/audio.mjs +5 -6
  245. package/dist/skills/media-use/audio/scripts/carve.mjs +560 -0
  246. package/dist/skills/media-use/audio/scripts/carve.test.mjs +254 -0
  247. package/dist/skills/media-use/audio/scripts/extract-audio-data.py +199 -0
  248. package/dist/skills/media-use/references/local.md +24 -0
  249. package/dist/skills/media-use/references/memory.md +4 -4
  250. package/dist/skills/media-use/references/mixing.md +71 -0
  251. package/dist/skills/media-use/references/operations.md +1 -1
  252. package/dist/skills/media-use/scripts/compatibility.test.mjs +1 -56
  253. package/dist/skills/media-use/scripts/lib/prefs-store.mjs +1 -1
  254. package/dist/skills/media-use/scripts/lib/recipe-store.mjs +1 -1
  255. package/dist/skills/media-use/scripts/prefs.mjs +2 -3
  256. package/dist/skills/media-use/scripts/recipe.mjs +1 -3
  257. package/dist/{skills-WHKMWO2D.js → skills-3WEVO3NU.js} +20 -13
  258. package/dist/{skillsUpdateCheck-XQGZGJJG.js → skillsUpdateCheck-BFPWFOK5.js} +3 -3
  259. package/dist/{snapshot-M4NUUSNU.js → snapshot-WDQ6S7CJ.js} +457 -98
  260. package/dist/{src-6MSC62U2.js → src-NPR2JZOJ.js} +11 -11
  261. package/dist/{src-4UYM2K3S.js → src-RZ574OKP.js} +8 -8
  262. package/dist/{staticProjectServer-ERDTFP45.js → staticProjectServer-FTGTEZI4.js} +17 -17
  263. package/dist/studio/assets/{hyperframes-player-DEncBxR7.js → hyperframes-player-CoOdf435.js} +1 -1
  264. package/dist/studio/assets/{index-Cc22mgvY.js → index-CR85xg2x.js} +1 -1
  265. package/dist/studio/assets/{index-BABXr78Q.js → index-C_6rTpaB.js} +5 -5
  266. package/dist/studio/index.html +1 -1
  267. package/dist/{studio-api-SAKD3VKC.js → studio-api-TAWB7B23.js} +11 -11
  268. package/dist/{studioServer-IRI7EVOM.js → studioServer-HRQGJTBU.js} +48 -48
  269. package/dist/{telemetry-F4XADVAF.js → telemetry-QZ6HJF4W.js} +3 -3
  270. package/dist/templates/_shared/AGENTS.md +14 -19
  271. package/dist/templates/_shared/CLAUDE.md +14 -19
  272. package/dist/templates/blank/index.html +10 -4
  273. package/dist/{timeline-5I444RRE.js → timeline-IKUEW42T.js} +20 -19
  274. package/dist/{tokens-LRRYM35A.js → tokens-AUGUEWHS.js} +2 -2
  275. package/dist/{transcribe-44XDFNVU.js → transcribe-7734CG3N.js} +18 -18
  276. package/dist/{transcribe-WHYHWAB2.js → transcribe-FWU6EMEO.js} +12 -12
  277. package/dist/{updateCheck-7CPH3CAZ.js → updateCheck-VIPQ3FPW.js} +2 -2
  278. package/dist/{upgrade-WW4GQ2RI.js → upgrade-4VNDQTVO.js} +2 -2
  279. package/dist/{urlDownloader-OHLLP45A.js → urlDownloader-6CXL4CL7.js} +8 -8
  280. package/dist/{validate-VNCDH2LW.js → validate-QWCMVWID.js} +23 -23
  281. package/dist/{video-5OHTLL2D.js → video-245Q5WN4.js} +9 -9
  282. package/dist/warmCheck.js +36 -36
  283. package/package.json +1 -1
  284. package/dist/chunk-R2SQBPMH.js +0 -3805
  285. package/dist/skills/hyperframes/references/brief-contract.md +0 -108
  286. package/dist/skills/hyperframes/references/brief-format.md +0 -72
  287. package/dist/skills/hyperframes/references/capability-menu.md +0 -47
  288. package/dist/skills/hyperframes/references/frame-worker-core.md +0 -82
  289. package/dist/skills/hyperframes/references/intent-interview.md +0 -73
  290. package/dist/skills/hyperframes/references/pitch-round.md +0 -40
  291. package/dist/skills/hyperframes/references/production-loop.md +0 -26
  292. package/dist/skills/hyperframes/references/review-loop.md +0 -37
  293. package/dist/skills/hyperframes/references/route-briefs.md +0 -11
  294. package/dist/skills/hyperframes/references/routes/embedded-captions.md +0 -11
  295. package/dist/skills/hyperframes/references/routes/faceless-explainer.md +0 -12
  296. package/dist/skills/hyperframes/references/routes/general-video.md +0 -12
  297. package/dist/skills/hyperframes/references/routes/motion-graphics.md +0 -11
  298. package/dist/skills/hyperframes/references/routes/music-to-video.md +0 -12
  299. package/dist/skills/hyperframes/references/routes/pr-to-video.md +0 -28
  300. package/dist/skills/hyperframes/references/routes/product-launch-video.md +0 -13
  301. package/dist/skills/hyperframes/references/routes/remotion-to-hyperframes.md +0 -9
  302. package/dist/skills/hyperframes/references/routes/slideshow.md +0 -11
  303. package/dist/skills/hyperframes/references/routes/talking-head-recut.md +0 -11
  304. package/dist/skills/hyperframes/references/script-format.md +0 -52
  305. package/dist/skills/hyperframes/references/subagent-dispatch.md +0 -35
  306. package/dist/skills/hyperframes/references/workflow-catalog.md +0 -10
  307. package/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +0 -176
  308. package/dist/skills/media-use/audio/scripts/lib/host-audio.mjs +0 -28
  309. package/dist/skills/media-use/audio/scripts/lib/host-audio.test.mjs +0 -106
@@ -0,0 +1,254 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { tmpdir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ DEFAULT_STRENGTH,
10
+ carveSources,
11
+ groupSourceRefusal,
12
+ loadCore,
13
+ parseArgs,
14
+ } from "./carve.mjs";
15
+
16
+ const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url));
17
+
18
+ /** A voice as `detectTracks` yields it: an id plus its raw opening tag. */
19
+ function voice(id, group) {
20
+ const attr = group === undefined ? "" : ` data-audio-group="${group}"`;
21
+ return { id, tag: `<audio id="${id}"${attr} src="${id}.wav"></audio>` };
22
+ }
23
+
24
+ /** An audio member as `main` describes it for the source decision. */
25
+ function member(id, group, nameKind) {
26
+ return { id, group, nameKind };
27
+ }
28
+
29
+ /** A bed as `mediaElements` yields it: `kind` is what decides group membership. */
30
+ function bed(id, group, kind = "audio") {
31
+ const attr = group === undefined ? "" : ` data-audio-group="${group}"`;
32
+ return { id, kind, tag: `<${kind} id="${id}"${attr} src="${id}.mp3"></${kind}>` };
33
+ }
34
+
35
+ async function inTempDir(run) {
36
+ const dir = mkdtempSync(join(tmpdir(), "carve-test-"));
37
+ try {
38
+ return await run(dir);
39
+ } finally {
40
+ rmSync(dir, { recursive: true, force: true });
41
+ }
42
+ }
43
+
44
+ test("voices sharing one group carve against the group, not their ids", () => {
45
+ // SKILL.md's invariant: "A carve against more than one clip id is wrong.
46
+ // Group the clips and carve against the group." Naming the group lets
47
+ // membership resolve at analysis time, so a voice added later is covered.
48
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover"), voice("vo3", "voiceover")];
49
+ assert.deepEqual(carveSources(voices, undefined, []), ["voiceover"]);
50
+ });
51
+
52
+ test("a single grouped voice still records the group", () => {
53
+ assert.deepEqual(carveSources([voice("vo1", "voiceover")], undefined, []), ["voiceover"]);
54
+ });
55
+
56
+ test("ungrouped voices keep their ids, so the lint rule can still say so", () => {
57
+ // Not silently inventing a group: `audio_carve_ungrouped_sources` is the
58
+ // right signal here, and it needs the ids to fire on.
59
+ assert.deepEqual(carveSources([voice("vo1"), voice("vo2")], undefined, []), ["vo1", "vo2"]);
60
+ });
61
+
62
+ test("voices in DIFFERENT groups keep their ids", () => {
63
+ // One carve cannot name two groups, and picking either would silently drop
64
+ // the other's members from the analysis.
65
+ const voices = [voice("vo1", "narration"), voice("vo2", "interview")];
66
+ assert.deepEqual(carveSources(voices, undefined, []), ["vo1", "vo2"]);
67
+ });
68
+
69
+ test("a partially grouped set keeps its ids", () => {
70
+ const voices = [voice("vo1", "voiceover"), voice("vo2")];
71
+ assert.deepEqual(carveSources(voices, undefined, []), ["vo1", "vo2"]);
72
+ });
73
+
74
+ test("an empty group attribute is not a group", () => {
75
+ assert.deepEqual(carveSources([voice("vo1", ""), voice("vo2", "")], undefined, []), [
76
+ "vo1",
77
+ "vo2",
78
+ ]);
79
+ });
80
+
81
+ // The nine cases above pass `[]` explicitly because they predate the membership
82
+ // check and are about the bed and the group attributes alone. `members` is a
83
+ // required argument: with `[]` the `mixed` refusal cannot fire, so a default
84
+ // would let a call that forgot it return the group form and silently undo the
85
+ // widening fix — see the wiring test at the bottom of this file.
86
+
87
+ test("a bed inside the voices' group keeps clip ids, so it is never its own source", () => {
88
+ // `resolveCarveSourceIds` expands a group to every CURRENT member, and it gets
89
+ // no host element to exclude — so naming a group the bed belongs to puts the
90
+ // bed in its own voice list on the next analysis, and it is carved against
91
+ // itself. This run cannot see it (main sums the detected voices directly), so
92
+ // the check has to happen here.
93
+ const voices = [voice("vo1", "mix"), voice("vo2", "mix")];
94
+ assert.deepEqual(carveSources(voices, bed("bgm", "mix"), []), ["vo1", "vo2"]);
95
+ });
96
+
97
+ test("a bed in a DIFFERENT group leaves the group form alone", () => {
98
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover")];
99
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), []), ["voiceover"]);
100
+ });
101
+
102
+ test("an ungrouped bed leaves the group form alone", () => {
103
+ assert.deepEqual(carveSources([voice("vo1", "voiceover")], bed("bgm"), []), ["voiceover"]);
104
+ });
105
+
106
+ test("a VIDEO bed is immune — group membership is audio-only", () => {
107
+ // `data-audio-group` on a <video> is ignored by core, so expanding the group
108
+ // can never pull this bed in and declining would be a false positive.
109
+ const voices = [voice("vo1", "mix"), voice("vo2", "mix")];
110
+ assert.deepEqual(carveSources(voices, bed("clip", "mix", "video"), []), ["mix"]);
111
+ });
112
+
113
+ test("an sfx member of the voices' group blocks the group form", () => {
114
+ // The group resolves wider than the analysis: `resolveCarveSourceIds` expands
115
+ // it to every current member and `resolveCarveVoices` keeps any audio with a
116
+ // src, so an sfx clip sharing the voice group enters the sidechain on the next
117
+ // analysis and ducks the bed under a whoosh. This run cannot see it — it sums
118
+ // the voices `detectTracks` returned, which correctly excluded the sfx.
119
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover")];
120
+ const members = [
121
+ member("vo1", "voiceover", "voice"),
122
+ member("vo2", "voiceover", "voice"),
123
+ member("whoosh", "voiceover", "sfx"),
124
+ member("bgm", "music", "music"),
125
+ ];
126
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), members), ["vo1", "vo2"]);
127
+ });
128
+
129
+ test("a music member of the voices' group blocks it too", () => {
130
+ const voices = [voice("vo1", "voiceover")];
131
+ const members = [member("vo1", "voiceover", "voice"), member("pad", "voiceover", "music")];
132
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), members), ["vo1"]);
133
+ });
134
+
135
+ test("a voice member this run did NOT analyse keeps the group form", () => {
136
+ // The designed case, and the one a membership check must not break: a voice
137
+ // that does not overlap the bed is left out of the analysis on purpose, and
138
+ // covering it on a later analysis without editing `sources` is the entire
139
+ // reason SKILL.md says to name the group. Refusing here would collapse the
140
+ // group form into clip ids for every ordinary narration sequence.
141
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover")];
142
+ const members = [
143
+ member("vo1", "voiceover", "voice"),
144
+ member("vo2", "voiceover", "voice"),
145
+ member("vo-outro", "voiceover", "voice"),
146
+ ];
147
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), members), ["voiceover"]);
148
+ });
149
+
150
+ test("an unclassifiable member keeps the group form", () => {
151
+ // `unknown` is what `detectTracks` itself treats as a possible voice, so it is
152
+ // not evidence of a non-voice member — loose in the safe direction, same as
153
+ // detection.
154
+ const voices = [voice("vo1", "voiceover")];
155
+ const members = [member("vo1", "voiceover", "voice"), member("track7", "voiceover", "unknown")];
156
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), members), ["voiceover"]);
157
+ });
158
+
159
+ test("an sfx member of a DIFFERENT group is irrelevant", () => {
160
+ const voices = [voice("vo1", "voiceover")];
161
+ const members = [member("vo1", "voiceover", "voice"), member("whoosh", "sfx", "sfx")];
162
+ assert.deepEqual(carveSources(voices, bed("bgm", "music"), members), ["voiceover"]);
163
+ });
164
+
165
+ test("groupSourceRefusal names which member blocked the group, and why", () => {
166
+ // The stderr note is built from this, so it has to carry the ids: "sources are
167
+ // clip ids" plus `audio_carve_ungrouped_sources` reads as nonsense to an author
168
+ // who did group their clips.
169
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover")];
170
+ assert.deepEqual(
171
+ groupSourceRefusal(voices, bed("bgm", "music"), [member("whoosh", "voiceover", "sfx")]),
172
+ { group: "voiceover", reason: "mixed", ids: ["whoosh"] },
173
+ );
174
+ assert.deepEqual(groupSourceRefusal(voices, bed("bgm", "voiceover"), []), {
175
+ group: "voiceover",
176
+ reason: "bed",
177
+ ids: ["bgm"],
178
+ });
179
+ assert.equal(groupSourceRefusal(voices, bed("bgm", "music"), []), null);
180
+ });
181
+
182
+ test("the CLI still runs when invoked through a symlinked path", () => {
183
+ // argv[1] keeps the invoked spelling while import.meta.url is the realpath, so
184
+ // a raw compare in the entry guard skips main() and the CLI exits 0 having
185
+ // written nothing. macOS /tmp -> /private/tmp reaches this with no symlink of
186
+ // one's own; so does any skill install placed behind a link.
187
+ return inTempDir((dir) => {
188
+ const link = join(dir, "scripts-link");
189
+ symlinkSync(SCRIPTS_DIR, link, "junction");
190
+ let status = 0;
191
+ let stderr = "";
192
+ try {
193
+ execFileSync(process.execPath, [join(link, "carve.mjs")], { encoding: "utf-8" });
194
+ } catch (error) {
195
+ status = error.status;
196
+ stderr = String(error.stderr);
197
+ }
198
+ // Reaching parseArgs is the proof main() ran at all: no args is an error, and
199
+ // the broken guard's symptom is a silent exit 0 with no output.
200
+ assert.equal(status, 1, `expected the usage error, got status ${status}`);
201
+ assert.match(stderr, /--comp is required/);
202
+ });
203
+ });
204
+
205
+ test("loadCore honours an import-only export map, as the published core has", () => {
206
+ // The published manifest carries only `import` + `types` for these subpaths, so
207
+ // `require.resolve` cannot resolve them at all and the fallback has to read the
208
+ // export map itself. A fixture pins that without depending on npm.
209
+ return inTempDir(async (dir) => {
210
+ const pkgDir = join(dir, "node_modules", "@hyperframes", "core");
211
+ mkdirSync(join(pkgDir, "dist"), { recursive: true });
212
+ writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "fixture-project" }));
213
+ writeFileSync(
214
+ join(pkgDir, "package.json"),
215
+ JSON.stringify({
216
+ name: "@hyperframes/core",
217
+ version: "0.0.0-fixture",
218
+ type: "module",
219
+ exports: {
220
+ "./package.json": "./package.json",
221
+ "./audio-carve": { import: "./dist/audioCarve.js" },
222
+ "./audio-fx": { import: "./dist/audioFx.js" },
223
+ },
224
+ }),
225
+ );
226
+ writeFileSync(join(pkgDir, "dist", "audioCarve.js"), 'export const marker = "carve";\n');
227
+ writeFileSync(join(pkgDir, "dist", "audioFx.js"), 'export const marker = "fx";\n');
228
+
229
+ const core = await loadCore(dir);
230
+ assert.equal(core.carve.marker, "carve");
231
+ assert.equal(core.fx.marker, "fx");
232
+ });
233
+ });
234
+
235
+ test("members is required, so dropping it cannot silently restore the group form", () => {
236
+ // The gap this closes: `main()` is the only code that BUILDS `members`, and no
237
+ // test runs `main()` (the symlink test stops at the usage error, a real run
238
+ // needs ffmpeg). With `members = []` defaulted, a refactor that dropped the
239
+ // third argument would return the group form again with the whole suite green
240
+ // — the same signature as the bug itself: first pass correct, persisted
241
+ // attribute wrong, nothing red. Omitting it now throws instead.
242
+ const voices = [voice("vo1", "voiceover"), voice("vo2", "voiceover")];
243
+ assert.throws(() => carveSources(voices, bed("bgm", "music")), TypeError);
244
+ assert.throws(() => groupSourceRefusal(voices, bed("bgm", "music")), TypeError);
245
+ });
246
+
247
+ test("the default strength is 0.8, and --strength still overrides it", () => {
248
+ // 0.25 left the bed audibly fighting the voice on narrated builds; a carve at
249
+ // the default has to already be a finished mix. Pinned so it cannot drift back
250
+ // unnoticed — core pins its own DEFAULT_CARVE the same way.
251
+ assert.equal(DEFAULT_STRENGTH, 0.8);
252
+ assert.equal(parseArgs(["--comp", "x.html"]).strength, 0.8);
253
+ assert.equal(parseArgs(["--comp", "x.html", "--strength", "0.25"]).strength, 0.25);
254
+ });
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Extract per-frame audio visualization data from an audio or video file.
4
+
5
+ Outputs JSON with RMS amplitude and frequency band data at the target FPS,
6
+ ready to embed in a HyperFrames composition.
7
+
8
+ Usage:
9
+ python extract-audio-data.py input.mp3 -o audio-data.json
10
+ python extract-audio-data.py input.mp4 --fps 30 --bands 16 -o audio-data.json
11
+
12
+ Requirements:
13
+ - Python 3.9+
14
+ - ffmpeg (for decoding audio)
15
+ - numpy (pip install numpy)
16
+ """
17
+
18
+ import argparse
19
+ import json
20
+ import subprocess
21
+ import sys
22
+
23
+ import numpy as np
24
+
25
+ # Windows sizes stdio to the ANSI code page (cp1252). These scripts emit UTF-8 on
26
+ # every platform; say so rather than depending on the console's code page. Carry
27
+ # `errors` across: reconfigure() resets it to "strict", and CPython deliberately gives
28
+ # stderr "backslashreplace" so the diagnostic path can never itself raise.
29
+ for _stream in (sys.stdout, sys.stderr):
30
+ if hasattr(_stream, "reconfigure"):
31
+ _stream.reconfigure(encoding="utf-8", errors=_stream.errors)
32
+
33
+ # ---------------------------------------------------------------------------
34
+ # FFT parameters
35
+ #
36
+ # A 4096-sample window gives ~10.8 Hz per bin at 44100Hz — enough to resolve
37
+ # low-frequency bands cleanly. The per-frame audio slice (44100/30 = 1470
38
+ # samples at 30fps) is too small and causes low bands to map to the same bins.
39
+ #
40
+ # Frequency range 30Hz–16kHz covers the useful range for music. Below 30Hz is
41
+ # sub-bass most speakers can't reproduce; above 16kHz is noise/harmonics that
42
+ # don't contribute to perceived rhythm or melody.
43
+ # ---------------------------------------------------------------------------
44
+
45
+ SAMPLE_RATE = 44100
46
+ FFT_SIZE = 4096
47
+ MIN_FREQ = 30.0
48
+ MAX_FREQ = 16000.0
49
+
50
+
51
+ def decode_audio(path: str) -> np.ndarray:
52
+ """Decode audio to mono float32 samples via ffmpeg."""
53
+ cmd = [
54
+ "ffmpeg", "-i", path,
55
+ "-vn", "-ac", "1", "-ar", str(SAMPLE_RATE),
56
+ "-f", "s16le", "-acodec", "pcm_s16le",
57
+ "-loglevel", "error",
58
+ "pipe:1",
59
+ ]
60
+ result = subprocess.run(cmd, capture_output=True)
61
+ if result.returncode != 0:
62
+ # Decoding ffmpeg's diagnostics strictly makes the reporter the thing that
63
+ # crashes: a Windows ffmpeg emits cp1252 bytes, and UnicodeDecodeError here
64
+ # would bury the actual failure it was trying to report.
65
+ print(f"ffmpeg error: {result.stderr.decode('utf-8', errors='replace')}", file=sys.stderr)
66
+ sys.exit(1)
67
+ return np.frombuffer(result.stdout, dtype=np.int16).astype(np.float32) / 32768.0
68
+
69
+
70
+ def compute_band_edges(n_bands: int) -> np.ndarray:
71
+ """Logarithmically-spaced frequency band edges from MIN_FREQ to MAX_FREQ."""
72
+ return np.array([
73
+ MIN_FREQ * (MAX_FREQ / MIN_FREQ) ** (i / n_bands)
74
+ for i in range(n_bands + 1)
75
+ ])
76
+
77
+
78
+ def compute_fft_bands(
79
+ windowed: np.ndarray, freq_per_bin: float, n_bins: int,
80
+ band_edges: np.ndarray, n_bands: int,
81
+ ) -> np.ndarray:
82
+ """Compute peak magnitude in logarithmically-spaced frequency bands."""
83
+ magnitudes = np.abs(np.fft.rfft(windowed))
84
+
85
+ bands = np.zeros(n_bands)
86
+ for b in range(n_bands):
87
+ low_bin = max(0, int(band_edges[b] / freq_per_bin))
88
+ high_bin = min(n_bins, int(band_edges[b + 1] / freq_per_bin))
89
+ if high_bin <= low_bin:
90
+ high_bin = low_bin + 1
91
+ # Clamp to valid range to avoid empty slices
92
+ low_bin = min(low_bin, n_bins - 1)
93
+ high_bin = min(high_bin, n_bins)
94
+ bands[b] = np.max(magnitudes[low_bin:high_bin])
95
+
96
+ return bands
97
+
98
+
99
+ def extract(path: str, fps: int, n_bands: int) -> dict:
100
+ """Extract per-frame audio data."""
101
+ print(f"Decoding audio from {path}...", file=sys.stderr)
102
+ samples = decode_audio(path)
103
+ duration = len(samples) / SAMPLE_RATE
104
+ frame_step = SAMPLE_RATE // fps
105
+ total_frames = int(duration * fps)
106
+
107
+ print(f"Duration: {duration:.1f}s, {total_frames} frames at {fps}fps", file=sys.stderr)
108
+ print(f"FFT window: {FFT_SIZE} samples ({SAMPLE_RATE / FFT_SIZE:.1f} Hz/bin)", file=sys.stderr)
109
+ print(f"Frequency range: {MIN_FREQ:.0f}-{MAX_FREQ:.0f} Hz, {n_bands} bands", file=sys.stderr)
110
+
111
+ # Precompute constants
112
+ hann = np.hanning(FFT_SIZE)
113
+ band_edges = compute_band_edges(n_bands)
114
+ freq_per_bin = SAMPLE_RATE / FFT_SIZE
115
+ n_bins = FFT_SIZE // 2 + 1
116
+ half_fft = FFT_SIZE // 2
117
+
118
+ # Pass 1: extract raw values
119
+ rms_values = np.zeros(total_frames)
120
+ band_values = np.zeros((total_frames, n_bands))
121
+
122
+ for f in range(total_frames):
123
+ # RMS from the frame's audio slice
124
+ rms_start = f * frame_step
125
+ rms_end = rms_start + frame_step
126
+ frame_slice = samples[rms_start:min(rms_end, len(samples))]
127
+ if len(frame_slice) > 0:
128
+ rms_values[f] = np.sqrt(np.mean(frame_slice ** 2))
129
+
130
+ # FFT from a centered 4096-sample window
131
+ center = rms_start + frame_step // 2
132
+ win_start = center - half_fft
133
+ win_end = center + half_fft
134
+
135
+ if win_start >= 0 and win_end <= len(samples):
136
+ window = samples[win_start:win_end] * hann
137
+ else:
138
+ # Zero-pad at edges
139
+ padded = np.zeros(FFT_SIZE)
140
+ src_start = max(0, win_start)
141
+ src_end = min(len(samples), win_end)
142
+ dst_start = src_start - win_start
143
+ dst_end = dst_start + (src_end - src_start)
144
+ padded[dst_start:dst_end] = samples[src_start:src_end]
145
+ window = padded * hann
146
+
147
+ band_values[f] = compute_fft_bands(window, freq_per_bin, n_bins, band_edges, n_bands)
148
+
149
+ # Pass 2: normalize
150
+ peak_rms = rms_values.max() if total_frames > 0 else 1.0
151
+ if peak_rms > 0:
152
+ rms_values /= peak_rms
153
+
154
+ # Per-band normalization so treble is visible alongside louder bass
155
+ band_peaks = band_values.max(axis=0)
156
+ band_peaks[band_peaks == 0] = 1.0
157
+ band_values /= band_peaks
158
+
159
+ # Build output
160
+ frames = []
161
+ for f in range(total_frames):
162
+ frames.append({
163
+ "time": round(f / fps, 4),
164
+ "rms": round(float(rms_values[f]), 4),
165
+ "bands": [round(float(b), 4) for b in band_values[f]],
166
+ })
167
+
168
+ return {
169
+ "duration": round(duration, 4),
170
+ "fps": fps,
171
+ "bands": n_bands,
172
+ "totalFrames": total_frames,
173
+ "frames": frames,
174
+ }
175
+
176
+
177
+ def main():
178
+ parser = argparse.ArgumentParser(description="Extract per-frame audio visualization data")
179
+ parser.add_argument("input", help="Audio or video file")
180
+ parser.add_argument("-o", "--output", default="audio-data.json", help="Output JSON path")
181
+ parser.add_argument("--fps", type=int, default=30, help="Frames per second (default: 30)")
182
+ parser.add_argument("--bands", type=int, default=16, help="Number of frequency bands (default: 16)")
183
+ args = parser.parse_args()
184
+
185
+ if args.fps < 1:
186
+ parser.error("--fps must be at least 1")
187
+ if args.bands < 1:
188
+ parser.error("--bands must be at least 1")
189
+
190
+ data = extract(args.input, args.fps, args.bands)
191
+
192
+ with open(args.output, "w", encoding="utf-8") as f:
193
+ json.dump(data, f)
194
+
195
+ print(f"Wrote {args.output} ({data['totalFrames']} frames, {data['bands']} bands)", file=sys.stderr)
196
+
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,24 @@
1
+ # Local media commands
2
+
3
+ These run on this machine, free, and write into the project.
4
+
5
+ ## Grade and style footage and images
6
+
7
+ - Measure, then correct: `npx hyperframes media-treatment --selector '#hero' --analyze --json`, then `--grading '<json>' --apply`. `--dry-run` previews; `--clear` removes the treatment.
8
+ - `--file` defaults to `index.html`: for media inside a scene, pass `--file compositions/<scene>.html`.
9
+ - Grain, blur, vignette, presets, palettes: `npx hyperframes media-treatment --capabilities --json`, then `--capability <id>` for one family's controls and ranges.
10
+ - The result is a `data-color-grading` attribute on the media element. It treats the whole element in SDR, so it can't blur one region such as a face.
11
+ - To animate it, set the start value in the element's inline `style` and tween these custom properties with finite `tl.to()` on the composition's timeline: `--hf-color-grading-` followed by `intensity`, `lut-intensity`, `blur`, `kuwahara`, `pixelate`, `ascii` or `dither` (each 0–1), `exposure` (−2 to 2) or `bloom` (0–3).
12
+ - Compare candidate grades on one frame: `npx hyperframes grade-compare --for frame.png --grades grades.json --json`, or `--luts a.cube,b.cube`.
13
+
14
+ ## Cutouts
15
+
16
+ - `npx hyperframes remove-background <in> -o cutout.webm` (`.png` for an image; `--quality best` for a closer colour match over its source). `--background-output plate.webm` also writes the background with the subject cut out.
17
+ - A cutout over its own source: both clips `data-start="0"` and `data-media-start="0"`, so they decode in sync.
18
+ - The runtime forces `opacity: 1` on a timed clip. To fade a cutout, wrap it in an untimed `<div>` and animate the wrapper.
19
+
20
+ ## Sound
21
+
22
+ - Match loudness: `npx hyperframes normalize-audio --reference <id> --target <id> --write`, or `--lufs=-16` (the default target; write a negative value with `=`) without a reference. `--write` edits the volume in the HTML.
23
+ - Beat grid: `npx hyperframes beats --json` reads the local music `<audio>` (marked `data-timeline-role="music"`, or an id containing music, bgm or soundtrack) and writes `beats/<path>.json`, which Studio shows as its beat grid. A beat time minus a scene's `data-start` is its time inside that scene.
24
+ - Local transcription, or importing `.srt`, `.vtt` or Whisper JSON: `npx hyperframes transcribe <file> --json`. The default model translates non-English speech: see `../../hyperframes/references/captions.md`.
@@ -7,10 +7,10 @@ The lightweight tier of user memory: confirmed brief answers (destination, aspec
7
7
  ```bash
8
8
  node <SKILL_DIR>/scripts/prefs.mjs get --hyperframes . --json # merged view (project overrides user)
9
9
  node <SKILL_DIR>/scripts/prefs.mjs record --hyperframes . --key destination --value x-feed
10
- node <SKILL_DIR>/scripts/prefs.mjs record --hyperframes . --key style_preset --value pin-and-paper --workflow faceless-explainer
10
+ node <SKILL_DIR>/scripts/prefs.mjs record --hyperframes . --key style_preset --value pin-and-paper --workflow explainer
11
11
  ```
12
12
 
13
- Only what the user actually confirmed gets recorded — never an inferred or defaulted value. How workflows consume these (a remembered value becomes the recommended default with a receipt, and never skips a question) is the brief contract's rule: `hyperframes/references/brief-contract.md` § 2, Remembered defaults.
13
+ Only what the user actually confirmed gets recorded — never an inferred or defaulted value. A remembered value becomes the recommended default, shown with where it came from; it never skips a question.
14
14
 
15
15
  ## Recipes — frozen video bundles
16
16
 
@@ -18,11 +18,11 @@ The heavyweight tier of user memory: one approved run frozen as a named, version
18
18
 
19
19
  ```bash
20
20
  node <SKILL_DIR>/scripts/recipe.mjs freeze --hyperframes . --name weekly-promo # workflow read from BRIEF.md (--workflow only for briefless projects)
21
- node <SKILL_DIR>/scripts/recipe.mjs list --hyperframes . --workflow product-launch-video
21
+ node <SKILL_DIR>/scripts/recipe.mjs list --hyperframes . --workflow explainer
22
22
  node <SKILL_DIR>/scripts/recipe.mjs use --hyperframes . --name weekly-promo # also: resolve.mjs --type recipe --entity weekly-promo
23
23
  ```
24
24
 
25
- The freeze is offered once after the final approval (`hyperframes/references/review-loop.md` § 4), and the intent layer (`/hyperframes` → `references/intent-interview.md`, step 1) checks for a match before its first question. Adopting a recipe fills the brief, the design spec, and the storyboard skeleton — and unlike preferences it may skip the questions it answers: the bundle was approved as a whole, and adoption itself is the question.
25
+ Offer the freeze once, after the final approval; before the first question about a new video, check `recipe.mjs list` for a match. Adopting a recipe fills the brief, the design spec, and the storyboard skeleton — and unlike preferences it may skip the questions it answers: the bundle was approved as a whole, and adoption itself is the question.
26
26
 
27
27
  ## Files
28
28
 
@@ -0,0 +1,71 @@
1
+ # Mixing in HyperFrames
2
+
3
+ Preview and render run the same Web Audio graph, so what plays while scrubbing is what renders. Everything lives in attributes on the `<audio>` or `<video>` element.
4
+
5
+ ## Level and fades
6
+
7
+ | Attribute | Meaning |
8
+ | ------------------------------- | ----------------------------------------------------------------------------------------- |
9
+ | `data-volume` | Track gain, 1 = 0 dB |
10
+ | `data-fade-in`, `data-fade-out` | Edge fades in seconds |
11
+ | `data-playback-rate` | Constant speed, 0.1–10, pitch preserved; give a video and its paired audio the same value |
12
+
13
+ A crossfade is two clips on different tracks that overlap, each with a fade.
14
+
15
+ ## `data-automation`: envelopes
16
+
17
+ ```html
18
+ <audio
19
+ id="music"
20
+ data-automation='{"version":1,"lanes":[{"target":"volume","points":[{"t":0,"v":1},{"t":2.5,"v":0.35}]}]}'
21
+ …
22
+ ></audio>
23
+ ```
24
+
25
+ - `target` is `volume`, `rate` (a speed ramp, 0.1–10, overrides `data-playback-rate`) or `fx.<nodeId>.<param>`.
26
+ - `t` is seconds from the start of the clip, not of the composition. `v` is in the parameter's unit: 0..1 for volume, dB for a gain, Hz for a frequency.
27
+ - A lane holds its first value back to the clip's start and its last value to its end, so a bed that starts before the voice needs an explicit full-level point at `t: 0`.
28
+ - `curve` (-1..1) bends the segment leaving a point. 512 points per lane at most.
29
+ - Don't also tween `volume` with GSAP on a track that has a volume lane or a `data-volume`; lint flags both.
30
+
31
+ ## `data-fx-chain`: effects
32
+
33
+ ```json
34
+ {
35
+ "version": 1,
36
+ "nodes": [
37
+ { "type": "highpass", "id": "n1", "label": "Remove rumble", "params": { "frequency": 80 } },
38
+ {
39
+ "type": "peaking",
40
+ "id": "n2",
41
+ "label": "Clarity",
42
+ "params": { "frequency": 3000, "gain": 2.5, "q": 1 }
43
+ },
44
+ { "type": "limiter", "id": "n3", "params": { "limit": -1 } }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ - Write the attribute double-quoted with the JSON's quotes as `&quot;`.
50
+ - Order is signal order. `id` is what automation targets (`fx.n2.gain`); give every node one, and a `label` when it does a named job. `enabled: false` bypasses a node.
51
+ - Effects: `gain`, `highpass`, `lowpass`, `peaking`, `lowshelf`, `highshelf`, `compressor`, `limiter`, `gate`, `saturate`, `delay`, `reverb`, `chorus`, `phaser`, `bitcrush`.
52
+ - `compressor`, `limiter`, `gate` and `bitcrush` can't be automated; a lane on them does nothing. Automate a `gain` before them instead.
53
+ - Never set `fromCarve` by hand.
54
+ - `reverb` and `delay` make the rendered track longer than its source; that is expected.
55
+
56
+ | It sounds like | Reach for |
57
+ | --------------------------- | ------------------------------------------- |
58
+ | Hum, rumble, handling noise | `highpass` at 80 Hz |
59
+ | Boomy, too close | `peaking` 200 Hz, −4 dB |
60
+ | Muffled | `peaking` 250 Hz, −3 dB |
61
+ | Words hard to make out | `peaking` 3 kHz, +2.5 dB, or duck the music |
62
+ | Harsh | `peaking` 3.2 kHz, −3 dB |
63
+ | Peaks clipping | `limiter` last in the chain |
64
+
65
+ ## Music under a voice
66
+
67
+ Duck the bed with a `volume` lane on the music: full level before the voice, down to about 0.3–0.4 while anyone speaks (word timings from `npx hyperframes transcribe` or the voiceover give the times), back up after. Then carve the bed, which dips only the bands the voice uses: `node <SKILL_DIR>/audio/scripts/carve.mjs --comp index.html` (needs `ffmpeg` and `@hyperframes/core` in the project; `--dry-run` to see the plan). It is the same analysis as Studio's carve panel. It reads only double-quoted attributes (JSON quotes as `&quot;`), so write the bed's chain and lanes that way or it overwrites them. Never put a carve on a voice track.
68
+
69
+ ## Group buses
70
+
71
+ Give clips the same `data-audio-group="<id>"` to treat them as one group. An `<hf-audio-group id="<id>">` element with that id makes the group a submix bus with one `data-fx-chain`, one volume and one automation clock for every member. Keep one role per group: `voiceover`, `music`, `sfx`.
@@ -16,7 +16,7 @@ ffmpeg -i in.mp4 -ss 00:00:12 -to 00:00:20 -c copy out.mp4 # 0:12–0:20, no r
16
16
  ```
17
17
 
18
18
  In-composition trimming usually needs **no new file**: a clip plays a sub-window
19
- via `data-media-start` + `data-duration` (see hyperframes-core). Only cut a
19
+ via `data-media-start` + `data-duration` (see `/hyperframes` → `references/format.md`). Only cut a
20
20
  physical file when exporting/assembling outside the composition.
21
21
 
22
22
  ## Reframe / crop: change aspect ratio
@@ -1,10 +1,8 @@
1
- import assert from "node:assert/strict";
2
- import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { cpSync, mkdtempSync, rmSync } from "node:fs";
3
2
  import { tmpdir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  import { execFileSync } from "node:child_process";
6
5
  import test from "node:test";
7
- import { pathToFileURL } from "node:url";
8
6
 
9
7
  process.env.HYPERFRAMES_MEDIA_HOME = mkdtempSync(join(tmpdir(), "media-use-compat-home-"));
10
8
 
@@ -41,56 +39,3 @@ test("published audio helpers import without the CLI package tree", () => {
41
39
  rmSync(root, { recursive: true, force: true });
42
40
  }
43
41
  });
44
-
45
- for (const name of ["faceless-explainer", "pr-to-video", "product-launch-video"]) {
46
- test(name + " assembles from its installed directory alone", () => {
47
- const root = mkdtempSync(join(tmpdir(), "installed-workflow-"));
48
- try {
49
- const skill = join(root, name);
50
- cpSync(new URL("../../" + name, import.meta.url), skill, { recursive: true });
51
- const project = join(root, "project");
52
- mkdirSync(join(project, "compositions/frames"), { recursive: true });
53
- writeFileSync(
54
- join(project, "STORYBOARD.md"),
55
- "---\nformat: 1920x1080\nmessage: Test\n---\n\n## Frame 1\n- duration: 3s\n- src: compositions/frames/01-a.html\n",
56
- );
57
- writeFileSync(
58
- join(project, "compositions/frames/01-a.html"),
59
- '<template><div data-composition-id="01-a" data-duration="3" data-width="1920" data-height="1080"><section class="clip" data-start="0" data-duration="3"></section></div></template>',
60
- );
61
- run(join(skill, "scripts"), "assemble-index.mjs", [
62
- "--hyperframes",
63
- project,
64
- "--storyboard",
65
- join(project, "STORYBOARD.md"),
66
- ]);
67
- assert.match(readFileSync(join(project, "index.html"), "utf8"), /01-a/);
68
- importPacketBuilder(join(skill, "scripts/frame-packets.mjs"));
69
- } finally {
70
- rmSync(root, { recursive: true, force: true });
71
- }
72
- });
73
- }
74
-
75
- test("general-video packet builder starts from its installed directory alone", () => {
76
- const root = mkdtempSync(join(tmpdir(), "installed-packets-"));
77
- try {
78
- cpSync(new URL("../../general-video", import.meta.url), root, { recursive: true });
79
- importPacketBuilder(join(root, "scripts/frame-packets.mjs"));
80
- } finally {
81
- rmSync(root, { recursive: true, force: true });
82
- }
83
- });
84
-
85
- function importPacketBuilder(path) {
86
- execFileSync(
87
- process.execPath,
88
- [
89
- "--input-type=module",
90
- "-e",
91
- 'const m = await import(process.argv[1]); if (typeof m.buildFramePackets !== "function") throw new Error("missing builder");',
92
- pathToFileURL(path).href,
93
- ],
94
- { stdio: "pipe" },
95
- );
96
- }