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.
- package/dist/{add-5ZTD3MUZ.js → add-ODTNPRGG.js} +14 -14
- package/dist/{agentPromptGenerator-VIIPZCJ3.js → agentPromptGenerator-KYO5MKTW.js} +1 -1
- package/dist/{animatedGifPrep-AGASIWVA.js → animatedGifPrep-PEP5GWOJ.js} +9 -9
- package/dist/api.js +17 -17
- package/dist/{asset-LSTEMPIB.js → asset-EZG42XPA.js} +2 -2
- package/dist/{assetCataloger-BKBMNL3A.js → assetCataloger-OHAJFCA7.js} +9 -9
- package/dist/{audio-KGK44XE4.js → audio-65R4T74I.js} +16 -15
- package/dist/{auth-MJWKYK77.js → auth-VP2SIZFN.js} +1 -1
- package/dist/{autoUpdate-2GGDKYR2.js → autoUpdate-RVSUX4SS.js} +2 -2
- package/dist/{backgroundChecks-7GRX5VQY.js → backgroundChecks-LEHK74NS.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-BCV2LELR.js → batchRender-3X6CL7PK.js} +5 -5
- package/dist/{beats-7BCFRBAL.js → beats-66VPQLSC.js} +6 -6
- package/dist/{benchmark-2R6CN5MQ.js → benchmark-PBJVTOJP.js} +10 -10
- package/dist/{browser-6AFGOI7G.js → browser-TPL5MWGC.js} +3 -3
- package/dist/{bundleWithLocalizedFonts-2EGPA5ZO.js → bundleWithLocalizedFonts-67MLTHKM.js} +2 -2
- package/dist/{capture-HOIAW6MO.js → capture-F3GNMRZM.js} +13 -13
- package/dist/{capture-HA2UHLIU.js → capture-YYRE7EV3.js} +2 -2
- package/dist/{captureCompositionFrame-4236FVWJ.js → captureCompositionFrame-PXV2ISHD.js} +13 -13
- package/dist/{catalog-A6CZAWW4.js → catalog-6SDD3OHF.js} +14 -14
- package/dist/{catch-up-TNA4DJOA.js → catch-up-CWIVA5VG.js} +4 -4
- package/dist/{check-5E5U2EQZ.js → check-YQ6REK2S.js} +34 -34
- package/dist/{checkBrowser-FNINXBMZ.js → checkBrowser-5XZCBRXM.js} +31 -31
- package/dist/{chunk-2ULXZ4PG.js → chunk-2TAS5O4P.js} +56 -11
- package/dist/{chunk-3TXNNBI2.js → chunk-37CNYTQQ.js} +2 -2
- package/dist/chunk-3NPOFC43.js +3805 -0
- package/dist/{chunk-EEP5Y5IE.js → chunk-46HEFGXJ.js} +1 -1
- package/dist/{chunk-2XA7KXQO.js → chunk-4CTNDBE3.js} +2 -2
- package/dist/{chunk-MFAETWIF.js → chunk-55YELV7B.js} +1 -1
- package/dist/{chunk-LRGYPXHF.js → chunk-5GUKZRQN.js} +1 -1
- package/dist/{chunk-SAX7HQ5E.js → chunk-6ASIXMFP.js} +1 -1
- package/dist/{chunk-SN7KI5JA.js → chunk-6GPR3G3C.js} +2 -2
- package/dist/{chunk-E32ZQ2WO.js → chunk-6RIZY7OP.js} +1 -1
- package/dist/{chunk-5CS4T6C6.js → chunk-7L37AB57.js} +1 -1
- package/dist/{chunk-BCHSTE7U.js → chunk-7PS6RVWD.js} +1 -1
- package/dist/{chunk-X3CG5YQA.js → chunk-7RAY4WZP.js} +3 -3
- package/dist/{chunk-YWHCXIMD.js → chunk-A6QCO474.js} +5 -5
- package/dist/{chunk-YMS5FOMC.js → chunk-B2A56S5C.js} +1 -1
- package/dist/{chunk-IWTNTUPQ.js → chunk-BH7TTIPN.js} +11 -13
- package/dist/{chunk-DWUGKEHR.js → chunk-BRAPBUPQ.js} +1 -1
- package/dist/{chunk-R2BFTECP.js → chunk-CX3USX23.js} +3 -3
- package/dist/{chunk-JB6ARMQS.js → chunk-D42NU3DZ.js} +1 -1
- package/dist/{chunk-GDEV42FA.js → chunk-EMAWIZ54.js} +481 -36
- package/dist/{chunk-UNNXFKVG.js → chunk-EMSDWBG3.js} +2 -2
- package/dist/{chunk-CXABAXB3.js → chunk-ESBMUZXZ.js} +1 -1
- package/dist/{chunk-EM7C7H7S.js → chunk-F3OPUWQX.js} +7 -7
- package/dist/{chunk-YOZPQCSR.js → chunk-FX6RUJUY.js} +3 -3
- package/dist/{chunk-PSKEKWVU.js → chunk-G2UANIDX.js} +1 -1
- package/dist/{chunk-UKOTVJFV.js → chunk-H5SM5PJI.js} +1 -1
- package/dist/{chunk-YWNJTZGN.js → chunk-H6UY725W.js} +2 -2
- package/dist/{chunk-PFYWVUHA.js → chunk-HDIRZGK4.js} +1 -1
- package/dist/{chunk-HR23IKLZ.js → chunk-HHSJJFVM.js} +2 -2
- package/dist/{chunk-LLC4CS6I.js → chunk-HXUTX5GR.js} +3 -3
- package/dist/{chunk-RGXVEYHI.js → chunk-I3IH5MV5.js} +6 -10
- package/dist/{chunk-Q33P4EAC.js → chunk-I45HL3VP.js} +3 -3
- package/dist/{chunk-TYXH3EKP.js → chunk-I5JRXMCV.js} +16 -1
- package/dist/{chunk-4N4P3ZWL.js → chunk-I7QMXZSB.js} +2 -2
- package/dist/{chunk-ICJEAFPD.js → chunk-IDFNKFPL.js} +7 -7
- package/dist/{chunk-VNVQGDPC.js → chunk-IDGLGXPA.js} +1 -1
- package/dist/{chunk-2PVE4WS2.js → chunk-IPDKZ5IN.js} +33 -6
- package/dist/{chunk-ABZYYUUH.js → chunk-ISTRDGIX.js} +1 -1
- package/dist/{chunk-3MDJIWOV.js → chunk-IVEFOUZI.js} +46 -30
- package/dist/{chunk-4KKKBEP3.js → chunk-J5XEZ24I.js} +3 -3
- package/dist/{chunk-7HJALUIO.js → chunk-K2ZKXTVL.js} +1 -1
- package/dist/{chunk-TE427YID.js → chunk-K73RJB23.js} +2 -2
- package/dist/{chunk-3QVMJ7GX.js → chunk-KQ4JEJLF.js} +9 -9
- package/dist/{chunk-J3QM7AHP.js → chunk-KROXPHXX.js} +9 -3
- package/dist/{chunk-OLZENUPI.js → chunk-LLFQA7XF.js} +1 -1
- package/dist/chunk-LLMSX6V6.js +17 -0
- package/dist/{chunk-WTFUDNW2.js → chunk-LYKSTRAY.js} +3 -3
- package/dist/{chunk-VDM3FEDP.js → chunk-OAG47RE6.js} +1 -1
- package/dist/{chunk-FGJEUV4S.js → chunk-PHMBMOXF.js} +1 -1
- package/dist/{chunk-7MB2ZAQF.js → chunk-PMMQRJDE.js} +1 -1
- package/dist/{chunk-ADDHW2FS.js → chunk-Q447CX26.js} +82 -12
- package/dist/{chunk-YALM6XN4.js → chunk-Q47FAYYK.js} +3 -3
- package/dist/{chunk-ZOCHDXDG.js → chunk-RTBSA3EA.js} +1 -1
- package/dist/{chunk-UJKUB2LH.js → chunk-RUEGYANA.js} +11 -11
- package/dist/{chunk-OYLO6TYK.js → chunk-RUOG4VB7.js} +1 -1
- package/dist/{chunk-ZRHO6YMM.js → chunk-SKYPMHQC.js} +4 -4
- package/dist/{chunk-ES426XRJ.js → chunk-SQVRXHOW.js} +3 -3
- package/dist/{chunk-QL2PNIXC.js → chunk-TAX2ZJAP.js} +24 -16
- package/dist/{chunk-KN46TGHV.js → chunk-TOI35GN2.js} +1 -1
- package/dist/{chunk-74JMKLVU.js → chunk-UPR3MU6Z.js} +1 -1
- package/dist/{chunk-JIUTXKWA.js → chunk-WQLKDL35.js} +2 -1
- package/dist/{chunk-WHPN2TIT.js → chunk-X6L6MD5B.js} +5 -5
- package/dist/{chunk-7NRI7HYX.js → chunk-XWC7CNPY.js} +1 -1
- package/dist/{clean-HGASSMWI.js → clean-7J6ZCCGV.js} +18 -18
- package/dist/cli.js +53 -53
- package/dist/{cloud-JPDVNXHN.js → cloud-7IJCJCDD.js} +4 -4
- package/dist/{cloudrun-AMMGSDRP.js → cloudrun-UNZRIIDB.js} +7 -7
- package/dist/commands/contrast-audit.browser.js +1 -2
- package/dist/commands/layout-audit.browser.js +196 -0
- package/dist/{compare-LHHVHUN3.js → compare-Q7VW5JZT.js} +28 -28
- package/dist/{compiler-Q3N6QXMT.js → compiler-KPHYGFXL.js} +6 -6
- package/dist/{component-D72J32Q7.js → component-UUBREJKN.js} +3 -3
- package/dist/{compositions-UQRJ5ROG.js → compositions-T4FTKIDP.js} +13 -13
- package/dist/{delete-P42VIDDA.js → delete-WZQF2IIA.js} +2 -2
- package/dist/{deploy-WZTH57O4.js → deploy-JU7A3XNC.js} +1 -1
- package/dist/{deterministicFonts-VQ5UBMXY.js → deterministicFonts-EGAAE7QL.js} +3 -3
- package/dist/{dist-SENYPQGW.js → dist-LM7AVHOU.js} +3 -3
- package/dist/{dist-DYS24AMA.js → dist-ULGTPSU4.js} +8 -4
- package/dist/docs/data-attributes.md +1 -1
- package/dist/{doctor-KEJIG2SK.js → doctor-VSNGRKXJ.js} +13 -13
- package/dist/{events-CYKSNR7N.js → events-CRW2DYUT.js} +3 -3
- package/dist/{events-UZXFQPMT.js → events-E63YBUL2.js} +3 -3
- package/dist/{feedback-LXNQF7CA.js → feedback-OSZWVR6N.js} +8 -8
- package/dist/{ffmpeg-BYHON577.js → ffmpeg-JLH3Z7G7.js} +9 -9
- package/dist/{figma-EIQ35PS6.js → figma-2BQNNR53.js} +3 -3
- package/dist/fontLocalizeCli.js +12 -12
- package/dist/{get-CC2YL7QO.js → get-P2XI53QW.js} +2 -2
- package/dist/{gpuPolicy-VFSACUQV.js → gpuPolicy-NTKQJY75.js} +9 -9
- package/dist/{grade-compare-TUQL7IV2.js → grade-compare-OYASIHIB.js} +29 -29
- package/dist/{gsapParserAcorn-7NCK2LVT.js → gsapParserAcorn-SKODXC2N.js} +2 -2
- package/dist/{help-ROROO5K6.js → help-HK6K2N4G.js} +1 -1
- package/dist/{history-PNELYLXX.js → history-JMTYKLXW.js} +6 -6
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +63 -63
- package/dist/hyperframes-player.global.js +1 -1
- package/dist/{info-4FZTH6AS.js → info-AW7CIVRE.js} +9 -9
- package/dist/{init-I43GHHVP.js → init-6KIDG4LB.js} +30 -30
- package/dist/{inspect-IT4TYIZ3.js → inspect-B7BLYMRZ.js} +29 -29
- package/dist/{installer-CWOJVK5K.js → installer-ZP2LQZUQ.js} +7 -7
- package/dist/{keyframes-3FBOGVD4.js → keyframes-EQ36A5JE.js} +7 -7
- package/dist/{lambda-H3V32I7M.js → lambda-E6VURLIS.js} +9 -9
- package/dist/{layout-BZ5LYJ5C.js → layout-XYC64QQQ.js} +29 -29
- package/dist/{lint-ZTNSGP2S.js → lint-V5DJYWBX.js} +9 -9
- package/dist/{lintProject-C2ONSH2U.js → lintProject-JATQKR64.js} +3 -3
- package/dist/{list-XM4GWJYY.js → list-RUGVRTVD.js} +2 -2
- package/dist/{login-H6HYKMWX.js → login-ZTKGKXGS.js} +4 -4
- package/dist/{manager-TWVDSJXO.js → manager-JUP7PM6N.js} +10 -10
- package/dist/{media-treatment-N7MY5NXB.js → media-treatment-VIUPK4OR.js} +15 -15
- package/dist/{models-WEMIMYBD.js → models-LJS2HBOZ.js} +13 -13
- package/dist/{motionShot-RHICTVPU.js → motionShot-3UAXZ7SN.js} +16 -16
- package/dist/{normalize-audio-ZFUZCJE3.js → normalize-audio-SBYI4YIH.js} +4 -4
- package/dist/{open-IBJ4FL3G.js → open-S27TUD3A.js} +14 -14
- package/dist/{parakeetRunner-XXO4WSLR.js → parakeetRunner-ETLMLJ4L.js} +14 -14
- package/dist/{pipeline-6ML3LUVK.js → pipeline-I2M22M24.js} +10 -10
- package/dist/{play-RV4NAVF5.js → play-COJU6VJK.js} +21 -21
- package/dist/{preflight-R3DEFMLA.js → preflight-FZ24NCHH.js} +10 -10
- package/dist/{present-5TBJGE6B.js → present-YAHKLC5D.js} +17 -17
- package/dist/{preview-FLKZ5CXJ.js → preview-KQOEU5YE.js} +20 -20
- package/dist/{producer-7XYSPVKH.js → producer-DTNNG4T6.js} +1 -1
- package/dist/{projectConfig-TBU2VWZX.js → projectConfig-4A76VYUP.js} +9 -9
- package/dist/{publication-CWWUJYJ5.js → publication-SQPMC2W3.js} +4 -4
- package/dist/{publish-FSNE4FEB.js → publish-QUEJBGGT.js} +11 -11
- package/dist/{remove-background-MDFWRWCT.js → remove-background-FP2ETAQH.js} +11 -11
- package/dist/{render-F4JQ553X.js → render-BIFIL4U6.js} +31 -31
- package/dist/{render-DBGQZAZT.js → render-GQFXJWLV.js} +17 -29
- package/dist/{render-LWZKXCLY.js → render-P664SMC7.js} +5 -5
- package/dist/{render-batch-PU7NMLIJ.js → render-batch-ZCPMYH6Z.js} +5 -5
- package/dist/renderSetupWorker.js +3 -3
- package/dist/{resolver-RUBGFXPC.js → resolver-BUC2GFZQ.js} +6 -6
- package/dist/{sherpa-X2TBNO6P.js → sherpa-HFT6XEP6.js} +13 -13
- package/dist/skills/hyperframes/SKILL.md +52 -174
- package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/FRAME.md +289 -0
- package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/caption-skin.html +220 -0
- package/dist/skills/hyperframes/assets/frame-presets/biennale-yellow/frame-showcase.html +1214 -0
- package/dist/skills/hyperframes/assets/frame-presets/blockframe/FRAME.md +275 -0
- package/dist/skills/hyperframes/assets/frame-presets/blockframe/caption-skin.html +223 -0
- package/dist/skills/hyperframes/assets/frame-presets/blockframe/frame-showcase.html +1347 -0
- package/dist/skills/hyperframes/assets/frame-presets/blue-professional/FRAME.md +307 -0
- package/dist/skills/hyperframes/assets/frame-presets/blue-professional/caption-skin.html +222 -0
- package/dist/skills/hyperframes/assets/frame-presets/blue-professional/frame-showcase.html +1372 -0
- package/dist/skills/hyperframes/assets/frame-presets/bold-poster/FRAME.md +275 -0
- package/dist/skills/hyperframes/assets/frame-presets/bold-poster/caption-skin.html +229 -0
- package/dist/skills/hyperframes/assets/frame-presets/bold-poster/frame-showcase.html +1140 -0
- package/dist/skills/hyperframes/assets/frame-presets/broadside/FRAME.md +284 -0
- package/dist/skills/hyperframes/assets/frame-presets/broadside/caption-skin.html +224 -0
- package/dist/skills/hyperframes/assets/frame-presets/broadside/frame-showcase.html +1166 -0
- package/dist/skills/hyperframes/assets/frame-presets/capsule/FRAME.md +284 -0
- package/dist/skills/hyperframes/assets/frame-presets/capsule/caption-skin.html +223 -0
- package/dist/skills/hyperframes/assets/frame-presets/capsule/frame-showcase.html +1465 -0
- package/dist/skills/hyperframes/assets/frame-presets/cartesian/FRAME.md +278 -0
- package/dist/skills/hyperframes/assets/frame-presets/cartesian/caption-skin.html +221 -0
- package/dist/skills/hyperframes/assets/frame-presets/cartesian/frame-showcase.html +1263 -0
- package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/FRAME.md +265 -0
- package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/caption-skin.html +237 -0
- package/dist/skills/hyperframes/assets/frame-presets/cobalt-grid/frame-showcase.html +1468 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/FRAME.md +277 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/caption-skin.html +223 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/EBGaramond-400.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/EBGaramond-700.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/Inter-400.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/Inter-700.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/JetBrainsMono-400.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/JetBrainsMono-700.woff2 +0 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-eb-garamond.txt +93 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-inter.txt +93 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/fonts/OFL-jetbrains-mono.txt +93 -0
- package/dist/skills/hyperframes/assets/frame-presets/code-editorial/frame-showcase.html +1438 -0
- package/dist/skills/hyperframes/assets/frame-presets/coral/FRAME.md +314 -0
- package/dist/skills/hyperframes/assets/frame-presets/coral/caption-skin.html +225 -0
- package/dist/skills/hyperframes/assets/frame-presets/coral/frame-showcase.html +1268 -0
- package/dist/skills/hyperframes/assets/frame-presets/creative-mode/FRAME.md +333 -0
- package/dist/skills/hyperframes/assets/frame-presets/creative-mode/caption-skin.html +226 -0
- package/dist/skills/hyperframes/assets/frame-presets/creative-mode/frame-showcase.html +1330 -0
- package/dist/skills/hyperframes/assets/frame-presets/daisy-days/FRAME.md +274 -0
- package/dist/skills/hyperframes/assets/frame-presets/daisy-days/caption-skin.html +229 -0
- package/dist/skills/hyperframes/assets/frame-presets/daisy-days/frame-showcase.html +1444 -0
- package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/FRAME.md +265 -0
- package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/caption-skin.html +222 -0
- package/dist/skills/hyperframes/assets/frame-presets/editorial-forest/frame-showcase.html +1273 -0
- package/dist/skills/hyperframes/references/captions.md +46 -0
- package/dist/skills/hyperframes/references/film.md +47 -0
- package/dist/skills/hyperframes/references/format.md +157 -0
- package/dist/skills/hyperframes/references/look.md +69 -0
- package/dist/skills/hyperframes/references/motion.md +94 -0
- package/dist/skills/hyperframes/references/overlays.md +39 -0
- package/dist/skills/hyperframes/references/plugin-installation.md +3 -3
- package/dist/skills/hyperframes/references/review.md +51 -0
- package/dist/skills/hyperframes/references/runtimes.md +43 -0
- package/dist/skills/hyperframes/references/skill-lifecycle.md +21 -13
- package/dist/skills/hyperframes/references/sound.md +43 -0
- package/dist/skills/hyperframes/references/spec.md +76 -0
- package/dist/skills/hyperframes/references/story.md +103 -0
- package/dist/skills/hyperframes/references/storyboard-format.md +30 -22
- package/dist/skills/hyperframes/references/storyboard.md +36 -0
- package/dist/skills/hyperframes/references/types/3d.md +25 -0
- package/dist/skills/hyperframes/references/types/character.md +20 -0
- package/dist/skills/hyperframes/references/types/data.md +19 -0
- package/dist/skills/hyperframes/references/types/explainer.md +34 -0
- package/dist/skills/hyperframes/references/types/launch.md +31 -0
- package/dist/skills/hyperframes/references/types/music.md +24 -0
- package/dist/skills/hyperframes/references/types/vertical.md +23 -0
- package/dist/skills/hyperframes/scripts/bake-basemap.mjs +324 -0
- package/dist/skills/hyperframes/scripts/lib/main-module.test.mjs +1 -6
- package/dist/skills/hyperframes/scripts/locate.mjs +228 -0
- package/dist/skills/hyperframes-cli/SKILL.md +76 -179
- package/dist/skills/hyperframes-cli/references/beats.md +1 -1
- package/dist/skills/hyperframes-cli/references/cloud.md +2 -2
- package/dist/skills/hyperframes-cli/references/cloudrun.md +1 -1
- package/dist/skills/hyperframes-cli/references/conventions.md +17 -0
- package/dist/skills/hyperframes-cli/references/feedback.md +14 -0
- package/dist/skills/hyperframes-cli/references/init-and-scaffold.md +11 -7
- package/dist/skills/hyperframes-cli/references/lambda.md +1 -1
- package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +19 -8
- package/dist/skills/hyperframes-cli/references/preview-render.md +8 -2
- package/dist/skills/hyperframes-cli/references/upgrade-info-misc.md +8 -0
- package/dist/skills/media-use/SKILL.md +47 -91
- package/dist/skills/media-use/audio/references/bgm.md +1 -1
- package/dist/skills/media-use/audio/references/captions/authoring.md +1 -2
- package/dist/skills/media-use/audio/references/captions/motion.md +5 -5
- package/dist/skills/media-use/audio/references/tts.md +0 -3
- package/dist/skills/media-use/audio/scripts/audio.mjs +5 -6
- package/dist/skills/media-use/audio/scripts/carve.mjs +560 -0
- package/dist/skills/media-use/audio/scripts/carve.test.mjs +254 -0
- package/dist/skills/media-use/audio/scripts/extract-audio-data.py +199 -0
- package/dist/skills/media-use/references/local.md +24 -0
- package/dist/skills/media-use/references/memory.md +4 -4
- package/dist/skills/media-use/references/mixing.md +71 -0
- package/dist/skills/media-use/references/operations.md +1 -1
- package/dist/skills/media-use/scripts/compatibility.test.mjs +1 -56
- package/dist/skills/media-use/scripts/lib/prefs-store.mjs +1 -1
- package/dist/skills/media-use/scripts/lib/recipe-store.mjs +1 -1
- package/dist/skills/media-use/scripts/prefs.mjs +2 -3
- package/dist/skills/media-use/scripts/recipe.mjs +1 -3
- package/dist/{skills-WHKMWO2D.js → skills-3WEVO3NU.js} +20 -13
- package/dist/{skillsUpdateCheck-XQGZGJJG.js → skillsUpdateCheck-BFPWFOK5.js} +3 -3
- package/dist/{snapshot-M4NUUSNU.js → snapshot-WDQ6S7CJ.js} +457 -98
- package/dist/{src-6MSC62U2.js → src-NPR2JZOJ.js} +11 -11
- package/dist/{src-4UYM2K3S.js → src-RZ574OKP.js} +8 -8
- package/dist/{staticProjectServer-ERDTFP45.js → staticProjectServer-FTGTEZI4.js} +17 -17
- package/dist/studio/assets/{hyperframes-player-DEncBxR7.js → hyperframes-player-CoOdf435.js} +1 -1
- package/dist/studio/assets/{index-Cc22mgvY.js → index-CR85xg2x.js} +1 -1
- package/dist/studio/assets/{index-BABXr78Q.js → index-C_6rTpaB.js} +5 -5
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-SAKD3VKC.js → studio-api-TAWB7B23.js} +11 -11
- package/dist/{studioServer-IRI7EVOM.js → studioServer-HRQGJTBU.js} +48 -48
- package/dist/{telemetry-F4XADVAF.js → telemetry-QZ6HJF4W.js} +3 -3
- package/dist/templates/_shared/AGENTS.md +14 -19
- package/dist/templates/_shared/CLAUDE.md +14 -19
- package/dist/templates/blank/index.html +10 -4
- package/dist/{timeline-5I444RRE.js → timeline-IKUEW42T.js} +20 -19
- package/dist/{tokens-LRRYM35A.js → tokens-AUGUEWHS.js} +2 -2
- package/dist/{transcribe-44XDFNVU.js → transcribe-7734CG3N.js} +18 -18
- package/dist/{transcribe-WHYHWAB2.js → transcribe-FWU6EMEO.js} +12 -12
- package/dist/{updateCheck-7CPH3CAZ.js → updateCheck-VIPQ3FPW.js} +2 -2
- package/dist/{upgrade-WW4GQ2RI.js → upgrade-4VNDQTVO.js} +2 -2
- package/dist/{urlDownloader-OHLLP45A.js → urlDownloader-6CXL4CL7.js} +8 -8
- package/dist/{validate-VNCDH2LW.js → validate-QWCMVWID.js} +23 -23
- package/dist/{video-5OHTLL2D.js → video-245Q5WN4.js} +9 -9
- package/dist/warmCheck.js +36 -36
- package/package.json +1 -1
- package/dist/chunk-R2SQBPMH.js +0 -3805
- package/dist/skills/hyperframes/references/brief-contract.md +0 -108
- package/dist/skills/hyperframes/references/brief-format.md +0 -72
- package/dist/skills/hyperframes/references/capability-menu.md +0 -47
- package/dist/skills/hyperframes/references/frame-worker-core.md +0 -82
- package/dist/skills/hyperframes/references/intent-interview.md +0 -73
- package/dist/skills/hyperframes/references/pitch-round.md +0 -40
- package/dist/skills/hyperframes/references/production-loop.md +0 -26
- package/dist/skills/hyperframes/references/review-loop.md +0 -37
- package/dist/skills/hyperframes/references/route-briefs.md +0 -11
- package/dist/skills/hyperframes/references/routes/embedded-captions.md +0 -11
- package/dist/skills/hyperframes/references/routes/faceless-explainer.md +0 -12
- package/dist/skills/hyperframes/references/routes/general-video.md +0 -12
- package/dist/skills/hyperframes/references/routes/motion-graphics.md +0 -11
- package/dist/skills/hyperframes/references/routes/music-to-video.md +0 -12
- package/dist/skills/hyperframes/references/routes/pr-to-video.md +0 -28
- package/dist/skills/hyperframes/references/routes/product-launch-video.md +0 -13
- package/dist/skills/hyperframes/references/routes/remotion-to-hyperframes.md +0 -9
- package/dist/skills/hyperframes/references/routes/slideshow.md +0 -11
- package/dist/skills/hyperframes/references/routes/talking-head-recut.md +0 -11
- package/dist/skills/hyperframes/references/script-format.md +0 -52
- package/dist/skills/hyperframes/references/subagent-dispatch.md +0 -35
- package/dist/skills/hyperframes/references/workflow-catalog.md +0 -10
- package/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +0 -176
- package/dist/skills/media-use/audio/scripts/lib/host-audio.mjs +0 -28
- 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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 `"`.
|
|
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 `"`), 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
|
|
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
|
|
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
|
-
}
|