hyperframes 0.8.185 → 0.8.188
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-ODTNPRGG.js → add-UORF6235.js} +12 -12
- package/dist/{animatedGifPrep-PEP5GWOJ.js → animatedGifPrep-23MLSEXF.js} +7 -7
- package/dist/api.js +16 -16
- package/dist/{asset-EZG42XPA.js → asset-DOKDVHCW.js} +2 -2
- package/dist/{assetCataloger-OHAJFCA7.js → assetCataloger-CD27MHEY.js} +7 -7
- package/dist/audio-3NMZHNNU.js +2317 -0
- package/dist/{auth-VP2SIZFN.js → auth-AWKTSKGC.js} +1 -1
- package/dist/{autoUpdate-RVSUX4SS.js → autoUpdate-VBOI4CNU.js} +2 -2
- package/dist/{backgroundChecks-LEHK74NS.js → backgroundChecks-S4R454YU.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-3X6CL7PK.js → batchRender-EGPYNDHK.js} +4 -4
- package/dist/{beats-66VPQLSC.js → beats-YENEQUOA.js} +5 -5
- package/dist/{benchmark-PBJVTOJP.js → benchmark-THSJIQG7.js} +9 -9
- package/dist/{browser-TPL5MWGC.js → browser-BDMVQ6WJ.js} +3 -3
- package/dist/{bundleWithLocalizedFonts-67MLTHKM.js → bundleWithLocalizedFonts-3QG722YD.js} +2 -2
- package/dist/{capture-YYRE7EV3.js → capture-WM4XDMFX.js} +2 -2
- package/dist/{capture-F3GNMRZM.js → capture-Z6BGCTK3.js} +10 -10
- package/dist/{captureCompositionFrame-PXV2ISHD.js → captureCompositionFrame-EMHSAIBZ.js} +15 -11
- package/dist/{catalog-6SDD3OHF.js → catalog-7TV6UNCG.js} +12 -12
- package/dist/{catch-up-CWIVA5VG.js → catch-up-63IUDZRW.js} +4 -4
- package/dist/{check-YQ6REK2S.js → check-7RHZL4DV.js} +32 -32
- package/dist/{checkBrowser-5XZCBRXM.js → checkBrowser-PF56CT5X.js} +29 -29
- package/dist/{chunk-TAX2ZJAP.js → chunk-2DHCCBCN.js} +9 -9
- package/dist/{chunk-K73RJB23.js → chunk-2WUDFZCR.js} +2 -2
- package/dist/{chunk-4CTNDBE3.js → chunk-44QWRPGE.js} +2 -2
- package/dist/{chunk-X6L6MD5B.js → chunk-46VJ2UAV.js} +6 -6
- package/dist/{chunk-BRAPBUPQ.js → chunk-57XL23DE.js} +1 -1
- package/dist/{chunk-J36JVBLR.js → chunk-5C7R55BJ.js} +1 -1
- package/dist/{chunk-SKYPMHQC.js → chunk-5DPV7LU3.js} +3 -3
- package/dist/{chunk-D42NU3DZ.js → chunk-5OZI55AC.js} +1 -1
- package/dist/{chunk-5UBZPG74.js → chunk-6A45RKAK.js} +1 -1
- package/dist/{chunk-I7QMXZSB.js → chunk-6HYPEIH7.js} +2 -2
- package/dist/{chunk-KQ4JEJLF.js → chunk-6HZ7TSJJ.js} +14 -8
- package/dist/{chunk-6ASIXMFP.js → chunk-6OWNUXTU.js} +1 -1
- package/dist/{chunk-WQLKDL35.js → chunk-6Q5CFUAP.js} +1 -1
- package/dist/{chunk-G2UANIDX.js → chunk-B2P4JRUJ.js} +1 -1
- package/dist/chunk-BPX7LZG6.js +3896 -0
- package/dist/{chunk-EMAWIZ54.js → chunk-BU4HRISA.js} +58 -21
- package/dist/{chunk-ESBMUZXZ.js → chunk-BWEZDN6U.js} +1 -1
- package/dist/{chunk-5UDRPPN2.js → chunk-CE7EQUBV.js} +8 -5
- package/dist/{chunk-UPR3MU6Z.js → chunk-DA4TQFSF.js} +1 -1
- package/dist/{chunk-HDIRZGK4.js → chunk-DG4ME554.js} +1 -1
- package/dist/{chunk-RTBSA3EA.js → chunk-DLFAVRF3.js} +1 -1
- package/dist/{chunk-TTVHYD5R.js → chunk-EI7FP7F3.js} +1 -1
- package/dist/{chunk-RUEGYANA.js → chunk-FOLMMQF3.js} +11 -11
- package/dist/{chunk-5GUKZRQN.js → chunk-FSTLXRIN.js} +1 -1
- package/dist/{chunk-CX3USX23.js → chunk-FWVILDOZ.js} +3 -3
- package/dist/{chunk-IVEFOUZI.js → chunk-GYBPAKBM.js} +2287 -2264
- package/dist/{chunk-HXUTX5GR.js → chunk-HKUZCSNZ.js} +2 -2
- package/dist/{chunk-PMMQRJDE.js → chunk-HPQSUPR7.js} +1 -1
- package/dist/{chunk-7RAY4WZP.js → chunk-I7C5GUEQ.js} +3 -3
- package/dist/{chunk-55YELV7B.js → chunk-IA2IDJAG.js} +1 -1
- package/dist/{chunk-7PS6RVWD.js → chunk-IJYR4TTA.js} +1 -1
- package/dist/{chunk-6RIZY7OP.js → chunk-JWRT2IXO.js} +1 -1
- package/dist/{chunk-2TAS5O4P.js → chunk-JYC53CW4.js} +108 -20
- package/dist/{chunk-IDFNKFPL.js → chunk-KDBPZOXK.js} +18 -7
- package/dist/{chunk-ISTRDGIX.js → chunk-KWSEIVR6.js} +1 -1
- package/dist/{chunk-IPDKZ5IN.js → chunk-KY7375C4.js} +1 -1
- package/dist/{chunk-F3OPUWQX.js → chunk-L243NE72.js} +7 -7
- package/dist/{chunk-H6UY725W.js → chunk-LWY3PFJ7.js} +2 -2
- package/dist/{chunk-6GPR3G3C.js → chunk-LWYZN7HS.js} +1 -1
- package/dist/{chunk-OAG47RE6.js → chunk-MJKUCR3C.js} +1 -1
- package/dist/{chunk-SQVRXHOW.js → chunk-MTPPEEYZ.js} +3 -3
- package/dist/{chunk-TOI35GN2.js → chunk-MXXX6WVC.js} +1 -1
- package/dist/{chunk-BH7TTIPN.js → chunk-NFHRGHCJ.js} +5 -5
- package/dist/{chunk-IDGLGXPA.js → chunk-NFLZNSUZ.js} +1 -1
- package/dist/{chunk-K2ZKXTVL.js → chunk-NMOTNNP2.js} +1 -1
- package/dist/{chunk-LYKSTRAY.js → chunk-OJOZXWLC.js} +3 -3
- package/dist/{chunk-FX6RUJUY.js → chunk-OKSYDQHY.js} +3 -3
- package/dist/{chunk-H5SM5PJI.js → chunk-PMXR3IUH.js} +1 -1
- package/dist/{chunk-A6QCO474.js → chunk-PQLFJYGP.js} +5 -5
- package/dist/{chunk-PFD25NAC.js → chunk-RIJHDTM5.js} +4 -0
- package/dist/{chunk-Q47FAYYK.js → chunk-SMF6YAUL.js} +9 -11
- package/dist/{chunk-EMSDWBG3.js → chunk-T3OXKSRG.js} +2 -2
- package/dist/{chunk-VHZ6DL3M.js → chunk-TMK4Q7JA.js} +10 -7
- package/dist/{chunk-Q447CX26.js → chunk-TSFA2Y5C.js} +60 -47
- package/dist/{chunk-XWC7CNPY.js → chunk-UF6RCYA5.js} +1 -1
- package/dist/{chunk-I45HL3VP.js → chunk-V6NM4KL3.js} +3 -3
- package/dist/{chunk-FFJ5TDRD.js → chunk-VHBBV5DH.js} +10 -3
- package/dist/{chunk-HHSJJFVM.js → chunk-W2KGJHUL.js} +2 -2
- package/dist/{chunk-37CNYTQQ.js → chunk-X556BGL7.js} +1 -1
- package/dist/{chunk-UJKQWGFF.js → chunk-X7HBSTCZ.js} +8 -0
- package/dist/{chunk-CML25PAF.js → chunk-XNFDDVJS.js} +27 -3
- package/dist/{chunk-J5XEZ24I.js → chunk-XPVN4R66.js} +3 -3
- package/dist/{chunk-I3IH5MV5.js → chunk-YCW345M4.js} +222 -118
- package/dist/{chunk-KROXPHXX.js → chunk-YV4WT7CJ.js} +5 -9
- package/dist/{chunk-PHMBMOXF.js → chunk-YZ73IPW2.js} +48 -4
- package/dist/{clean-7J6ZCCGV.js → clean-43O47OTN.js} +16 -16
- package/dist/cli.js +53 -53
- package/dist/{cloud-7IJCJCDD.js → cloud-ZFWKFFSU.js} +4 -4
- package/dist/{cloudrun-UNZRIIDB.js → cloudrun-XBBESKEU.js} +5 -5
- package/dist/{compare-Q7VW5JZT.js → compare-OW4ELTFZ.js} +26 -26
- package/dist/{compiler-KPHYGFXL.js → compiler-23MT6CX6.js} +4 -4
- package/dist/{component-UUBREJKN.js → component-SDV7LWH6.js} +3 -3
- package/dist/{compositions-T4FTKIDP.js → compositions-DI5QZCEK.js} +11 -11
- package/dist/{delete-WZQF2IIA.js → delete-QY2NSQ2G.js} +4 -4
- package/dist/deploy-MJVC4EFY.js +300 -0
- package/dist/{destroy-4B5HW5FH.js → destroy-SSPAKADI.js} +13 -3
- package/dist/{dist-LM7AVHOU.js → dist-JH32CBQL.js} +1 -1
- package/dist/{dist-ULGTPSU4.js → dist-RFTGYT26.js} +7 -5
- package/dist/{doctor-VSNGRKXJ.js → doctor-7ZOECPHR.js} +11 -11
- package/dist/{events-E63YBUL2.js → events-WIX4JM2K.js} +3 -3
- package/dist/{events-CRW2DYUT.js → events-XVQQGECC.js} +2 -2
- package/dist/{feedback-OSZWVR6N.js → feedback-GBYBR6UO.js} +7 -7
- package/dist/{ffmpeg-JLH3Z7G7.js → ffmpeg-QGQBYMO3.js} +7 -7
- package/dist/{figma-2BQNNR53.js → figma-2CMKUGJQ.js} +3 -3
- package/dist/fontLocalizeCli.js +9 -9
- package/dist/{get-P2XI53QW.js → get-RWCNUOHU.js} +4 -4
- package/dist/{gpuPolicy-NTKQJY75.js → gpuPolicy-UGWSN32D.js} +7 -7
- package/dist/{grade-compare-OYASIHIB.js → grade-compare-J2MX7HZL.js} +27 -27
- package/dist/{help-HK6K2N4G.js → help-SH3ID7MV.js} +1 -1
- package/dist/{history-JMTYKLXW.js → history-C7V5QH7J.js} +6 -6
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +70 -69
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/{info-AW7CIVRE.js → info-ZN3JJ3IN.js} +8 -8
- package/dist/{init-6KIDG4LB.js → init-KRNGSRNH.js} +65 -47
- package/dist/{inspect-B7BLYMRZ.js → inspect-AYDZXTX7.js} +27 -27
- package/dist/{installer-ZP2LQZUQ.js → installer-N6AIK6XQ.js} +5 -5
- package/dist/{keyframes-EQ36A5JE.js → keyframes-2RFUCEDK.js} +6 -6
- package/dist/{lambda-E6VURLIS.js → lambda-VW3CF6EE.js} +20 -13
- package/dist/{layout-XYC64QQQ.js → layout-OE64APE7.js} +27 -27
- package/dist/{lint-V5DJYWBX.js → lint-UXJTX5WS.js} +7 -7
- package/dist/{lintProject-JATQKR64.js → lintProject-XM3DLUKG.js} +1 -1
- package/dist/{list-RUGVRTVD.js → list-KOL6JPZJ.js} +7 -7
- package/dist/{login-ZTKGKXGS.js → login-MAQXOPDG.js} +4 -4
- package/dist/{manager-JUP7PM6N.js → manager-3LZKRK7P.js} +8 -8
- package/dist/{media-treatment-VIUPK4OR.js → media-treatment-COLSXXQJ.js} +13 -13
- package/dist/{models-LJS2HBOZ.js → models-XXWMGOD7.js} +11 -11
- package/dist/{motionShot-3UAXZ7SN.js → motionShot-RECBNTBZ.js} +14 -14
- package/dist/{normalize-4UDISLPL.js → normalize-3AYFSN2F.js} +1 -1
- package/dist/{normalize-audio-SBYI4YIH.js → normalize-audio-LKVGHZWR.js} +6 -6
- package/dist/{open-S27TUD3A.js → open-UEQVSRKH.js} +12 -12
- package/dist/{parakeetRunner-ETLMLJ4L.js → parakeetRunner-QVPCZ36U.js} +12 -12
- package/dist/{pipeline-I2M22M24.js → pipeline-5VSMQBKW.js} +8 -8
- package/dist/{play-COJU6VJK.js → play-MNEABYN4.js} +19 -19
- package/dist/{policies-XXNGXWHV.js → policies-MBWGLMWB.js} +17 -0
- package/dist/{preflight-FZ24NCHH.js → preflight-ONJ74DWQ.js} +8 -8
- package/dist/{present-YAHKLC5D.js → present-GQ24GTYX.js} +15 -15
- package/dist/{preview-KQOEU5YE.js → preview-ALL7O4RG.js} +18 -18
- package/dist/{producer-DTNNG4T6.js → producer-43L2UDF6.js} +1 -1
- package/dist/{progress-NHJIHAAG.js → progress-PX3PVAIM.js} +5 -3
- package/dist/{projectConfig-4A76VYUP.js → projectConfig-VJXUXK7U.js} +7 -7
- package/dist/{publication-SQPMC2W3.js → publication-L6E5NTFV.js} +3 -3
- package/dist/{publish-QUEJBGGT.js → publish-LCLBJEPR.js} +9 -9
- package/dist/{remove-background-FP2ETAQH.js → remove-background-PDL4KPR4.js} +9 -9
- package/dist/{render-P664SMC7.js → render-7HVKJ7T4.js} +9 -7
- package/dist/{render-BIFIL4U6.js → render-J33ZHR6I.js} +44 -28
- package/dist/{render-GQFXJWLV.js → render-OPCRLZVL.js} +31 -22
- package/dist/{render-batch-ZCPMYH6Z.js → render-batch-ZHHIGXU3.js} +5 -5
- package/dist/renderSetupWorker.js +1 -1
- package/dist/{resolver-BUC2GFZQ.js → resolver-KNNIMMBM.js} +5 -5
- package/dist/{sam-MC24SZHC.js → sam-FHJ7IL7T.js} +3 -1
- package/dist/{sherpa-HFT6XEP6.js → sherpa-GI62H3H5.js} +11 -11
- package/dist/{sites-MAVH57MO.js → sites-5PWHAMEG.js} +1 -1
- package/dist/skills/hyperframes/references/motion.md +1 -0
- package/dist/{skills-3WEVO3NU.js → skills-IUTD6VQO.js} +5 -5
- package/dist/{skillsUpdateCheck-BFPWFOK5.js → skillsUpdateCheck-XZE7MJG6.js} +3 -3
- package/dist/{snapshot-WDQ6S7CJ.js → snapshot-XEXTHLZP.js} +37 -34
- package/dist/{src-NPR2JZOJ.js → src-6STRGCDC.js} +8 -8
- package/dist/{src-RZ574OKP.js → src-ZPIV4PGX.js} +10 -6
- package/dist/{state-ORRPOLMQ.js → state-JPNCCXAV.js} +3 -1
- package/dist/{staticProjectServer-FTGTEZI4.js → staticProjectServer-HJLT7MHE.js} +15 -15
- package/dist/studio/assets/{hyperframes-player-CoOdf435.js → hyperframes-player-DwWIKKZG.js} +1 -1
- package/dist/studio/assets/{index-CR85xg2x.js → index-Cjwz7_13.js} +1 -1
- package/dist/studio/assets/{index-C_6rTpaB.js → index-M8lEpS0Q.js} +131 -131
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-TAWB7B23.js → studio-api-OBHEB5ND.js} +9 -9
- package/dist/{studioServer-HRQGJTBU.js → studioServer-KRIZFYML.js} +47 -46
- package/dist/{telemetry-QZ6HJF4W.js → telemetry-N5OOVBCP.js} +3 -3
- package/dist/templates/_shared/AGENTS.md +5 -0
- package/dist/templates/_shared/CLAUDE.md +5 -0
- package/dist/{timeline-IKUEW42T.js → timeline-YUFH65OG.js} +17 -17
- package/dist/{tokens-AUGUEWHS.js → tokens-K4VYLSWJ.js} +2 -2
- package/dist/{transcribe-7734CG3N.js → transcribe-CY4CVO54.js} +17 -17
- package/dist/{transcribe-FWU6EMEO.js → transcribe-GMQXKDYN.js} +10 -10
- package/dist/{updateCheck-VIPQ3FPW.js → updateCheck-2625RXK4.js} +2 -2
- package/dist/{upgrade-4VNDQTVO.js → upgrade-6ZY5MSBQ.js} +2 -2
- package/dist/{urlDownloader-6CXL4CL7.js → urlDownloader-PVJWG7V3.js} +6 -6
- package/dist/{validate-QWCMVWID.js → validate-XSRHEE7E.js} +21 -21
- package/dist/{video-245Q5WN4.js → video-MBFHC54F.js} +7 -7
- package/dist/warmCheck.js +34 -34
- package/package.json +18 -2
- package/dist/audio-65R4T74I.js +0 -1154
- package/dist/chunk-3NPOFC43.js +0 -3805
- package/dist/deploy-JU7A3XNC.js +0 -196
package/dist/audio-65R4T74I.js
DELETED
|
@@ -1,1154 +0,0 @@
|
|
|
1
|
-
import { createRequire as __hf_createRequire } from "node:module";
|
|
2
|
-
import { fileURLToPath as __hf_fileURLToPath } from "node:url";
|
|
3
|
-
import { dirname as __hf_dirname } from "node:path";
|
|
4
|
-
var require = __hf_createRequire(import.meta.url);
|
|
5
|
-
var __filename = __hf_fileURLToPath(import.meta.url);
|
|
6
|
-
var __dirname = __hf_dirname(__filename);
|
|
7
|
-
import {
|
|
8
|
-
uploadAssetViaDirectUpload
|
|
9
|
-
} from "./chunk-5UDRPPN2.js";
|
|
10
|
-
import {
|
|
11
|
-
HyperframesCloudClient
|
|
12
|
-
} from "./chunk-5UBZPG74.js";
|
|
13
|
-
import {
|
|
14
|
-
describeProject
|
|
15
|
-
} from "./chunk-BH7TTIPN.js";
|
|
16
|
-
import "./chunk-LLMSX6V6.js";
|
|
17
|
-
import {
|
|
18
|
-
decodeAudio,
|
|
19
|
-
envelope,
|
|
20
|
-
medianInterval,
|
|
21
|
-
probeAudio,
|
|
22
|
-
rhythm,
|
|
23
|
-
runLadder,
|
|
24
|
-
signalDuration,
|
|
25
|
-
signalProvider,
|
|
26
|
-
snapper,
|
|
27
|
-
tempoOf
|
|
28
|
-
} from "./chunk-FFJ5TDRD.js";
|
|
29
|
-
import {
|
|
30
|
-
resolveProject
|
|
31
|
-
} from "./chunk-5GUKZRQN.js";
|
|
32
|
-
import "./chunk-6JR2XUEE.js";
|
|
33
|
-
import "./chunk-G2UANIDX.js";
|
|
34
|
-
import "./chunk-K2FLNJVB.js";
|
|
35
|
-
import {
|
|
36
|
-
ensureDOMParser
|
|
37
|
-
} from "./chunk-442JNXRM.js";
|
|
38
|
-
import "./chunk-WQLKDL35.js";
|
|
39
|
-
import "./chunk-H5SM5PJI.js";
|
|
40
|
-
import "./chunk-VWG7OQKU.js";
|
|
41
|
-
import {
|
|
42
|
-
assertKnownFlags
|
|
43
|
-
} from "./chunk-HTIUNMTP.js";
|
|
44
|
-
import {
|
|
45
|
-
defineCommand
|
|
46
|
-
} from "./chunk-S3S4TGEH.js";
|
|
47
|
-
import "./chunk-54HJ63Q2.js";
|
|
48
|
-
import {
|
|
49
|
-
CliUsageError
|
|
50
|
-
} from "./chunk-JVBRJMOP.js";
|
|
51
|
-
import "./chunk-ZTZA4UWZ.js";
|
|
52
|
-
import "./chunk-I3IH5MV5.js";
|
|
53
|
-
import "./chunk-IZCW2NKT.js";
|
|
54
|
-
import "./chunk-KROXPHXX.js";
|
|
55
|
-
import "./chunk-RQVDTPGD.js";
|
|
56
|
-
import "./chunk-Q47FAYYK.js";
|
|
57
|
-
import "./chunk-3NPOFC43.js";
|
|
58
|
-
import "./chunk-VC76R6W2.js";
|
|
59
|
-
import "./chunk-UJKQWGFF.js";
|
|
60
|
-
import "./chunk-B2A56S5C.js";
|
|
61
|
-
import "./chunk-OFQWTKPW.js";
|
|
62
|
-
import "./chunk-DPVXBVLL.js";
|
|
63
|
-
import "./chunk-3GXQPCJC.js";
|
|
64
|
-
import "./chunk-634HSJ3J.js";
|
|
65
|
-
import "./chunk-FXMKHHPO.js";
|
|
66
|
-
import "./chunk-T6AGUTZJ.js";
|
|
67
|
-
import "./chunk-XT6MXC7A.js";
|
|
68
|
-
import "./chunk-4UMPIPCQ.js";
|
|
69
|
-
import "./chunk-Q447CX26.js";
|
|
70
|
-
import "./chunk-VAS6HQX3.js";
|
|
71
|
-
import {
|
|
72
|
-
resolveAuthoredAssetFile
|
|
73
|
-
} from "./chunk-6BNTGNGD.js";
|
|
74
|
-
import "./chunk-RUOG4VB7.js";
|
|
75
|
-
import "./chunk-MMQ52NIZ.js";
|
|
76
|
-
import "./chunk-BSQ7XANM.js";
|
|
77
|
-
import "./chunk-SDYDNHEE.js";
|
|
78
|
-
import "./chunk-IXLUTE6N.js";
|
|
79
|
-
import "./chunk-I5JRXMCV.js";
|
|
80
|
-
import "./chunk-W2LGJSEN.js";
|
|
81
|
-
import {
|
|
82
|
-
withMeta
|
|
83
|
-
} from "./chunk-ISTRDGIX.js";
|
|
84
|
-
import "./chunk-K4WIOZFP.js";
|
|
85
|
-
import "./chunk-XUXGIA7H.js";
|
|
86
|
-
import "./chunk-WERLJGIC.js";
|
|
87
|
-
import "./chunk-ISN5GRYI.js";
|
|
88
|
-
import "./chunk-I7QMXZSB.js";
|
|
89
|
-
import "./chunk-KNU5COPB.js";
|
|
90
|
-
import "./chunk-M2WBOQ2Q.js";
|
|
91
|
-
|
|
92
|
-
// src/commands/audio.ts
|
|
93
|
-
import { resolve as resolve2 } from "path";
|
|
94
|
-
|
|
95
|
-
// src/audio/analysis/providers/api.ts
|
|
96
|
-
import { randomUUID } from "crypto";
|
|
97
|
-
|
|
98
|
-
// src/audio/analysis/providers/shared.ts
|
|
99
|
-
import { execFile } from "child_process";
|
|
100
|
-
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
101
|
-
import { tmpdir } from "os";
|
|
102
|
-
import { join } from "path";
|
|
103
|
-
import { promisify } from "util";
|
|
104
|
-
var runFile = promisify(execFile);
|
|
105
|
-
var unavailable = (reason) => ({ ok: false, reason });
|
|
106
|
-
var AVAILABLE = { ok: true };
|
|
107
|
-
function encodeWav(signal) {
|
|
108
|
-
const channels = signal.channels.length;
|
|
109
|
-
const frames = signal.channels[0]?.length ?? 0;
|
|
110
|
-
const data = frames * channels * 2;
|
|
111
|
-
const wav = Buffer.alloc(44 + data);
|
|
112
|
-
wav.write("RIFF", 0);
|
|
113
|
-
wav.writeUInt32LE(36 + data, 4);
|
|
114
|
-
wav.write("WAVEfmt ", 8);
|
|
115
|
-
wav.writeUInt32LE(16, 16);
|
|
116
|
-
wav.writeUInt16LE(1, 20);
|
|
117
|
-
wav.writeUInt16LE(channels, 22);
|
|
118
|
-
wav.writeUInt32LE(signal.sampleRate, 24);
|
|
119
|
-
wav.writeUInt32LE(signal.sampleRate * channels * 2, 28);
|
|
120
|
-
wav.writeUInt16LE(channels * 2, 32);
|
|
121
|
-
wav.writeUInt16LE(16, 34);
|
|
122
|
-
wav.write("data", 36);
|
|
123
|
-
wav.writeUInt32LE(data, 40);
|
|
124
|
-
let offset = 44;
|
|
125
|
-
for (let i = 0; i < frames; i++)
|
|
126
|
-
for (const channel of signal.channels) {
|
|
127
|
-
const sample = Math.max(-1, Math.min(1, channel[i]));
|
|
128
|
-
wav.writeInt16LE(Math.round(sample * 32767), offset);
|
|
129
|
-
offset += 2;
|
|
130
|
-
}
|
|
131
|
-
return wav;
|
|
132
|
-
}
|
|
133
|
-
async function withWavFile(signal, use) {
|
|
134
|
-
const dir = await mkdtemp(join(tmpdir(), "hf-audio-"));
|
|
135
|
-
try {
|
|
136
|
-
const path = join(dir, "audio.wav");
|
|
137
|
-
await writeFile(path, encodeWav(signal));
|
|
138
|
-
return await use(path);
|
|
139
|
-
} finally {
|
|
140
|
-
await rm(dir, { recursive: true, force: true });
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
function perSignal(compute) {
|
|
144
|
-
const answers = /* @__PURE__ */ new WeakMap();
|
|
145
|
-
return (signal) => {
|
|
146
|
-
let answer = answers.get(signal);
|
|
147
|
-
if (!answer) answers.set(signal, answer = compute(signal));
|
|
148
|
-
return answer;
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
function downbeatsWithMeter(beats, downbeats) {
|
|
152
|
-
const bar = medianInterval(downbeats);
|
|
153
|
-
const beat = medianInterval(beats);
|
|
154
|
-
const meter = bar && beat ? Math.round(bar / beat) : null;
|
|
155
|
-
return { times: downbeats, meter: meter && meter > 0 ? meter : null };
|
|
156
|
-
}
|
|
157
|
-
var round = (t) => Math.round(t * 1e3) / 1e3;
|
|
158
|
-
var rounded = (times) => times.map(round);
|
|
159
|
-
|
|
160
|
-
// src/audio/analysis/providers/api.ts
|
|
161
|
-
var URL_ENV = "HYPERFRAMES_AUDIO_API_URL";
|
|
162
|
-
var KEY_ENV = "HYPERFRAMES_AUDIO_API_KEY";
|
|
163
|
-
var TIMEOUT_ENV = "HYPERFRAMES_AUDIO_API_TIMEOUT_MS";
|
|
164
|
-
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
165
|
-
var UPLOAD_RATE = 16e3;
|
|
166
|
-
var SINC_ZEROS = 16;
|
|
167
|
-
var ROLLOFF = 0.9;
|
|
168
|
-
var SINC_STEPS = 512;
|
|
169
|
-
var HANN_SINC = Float32Array.from({ length: SINC_ZEROS * SINC_STEPS + 2 }, (_, k) => {
|
|
170
|
-
const x = k / SINC_STEPS;
|
|
171
|
-
if (x >= SINC_ZEROS) return 0;
|
|
172
|
-
const sinc = x === 0 ? 1 : Math.sin(Math.PI * x) / (Math.PI * x);
|
|
173
|
-
return sinc * (0.5 + 0.5 * Math.cos(Math.PI * x / SINC_ZEROS));
|
|
174
|
-
});
|
|
175
|
-
function resample(input, from, to) {
|
|
176
|
-
if (from === to) return input;
|
|
177
|
-
const step = from / to;
|
|
178
|
-
const scale = Math.min(1, to / from) * ROLLOFF;
|
|
179
|
-
const reach = SINC_ZEROS / scale;
|
|
180
|
-
const output = new Float32Array(Math.round(input.length / step));
|
|
181
|
-
for (let i = 0; i < output.length; i++) {
|
|
182
|
-
const center = i * step;
|
|
183
|
-
const last = Math.min(input.length - 1, Math.floor(center + reach));
|
|
184
|
-
let sum = 0;
|
|
185
|
-
let weights = 0;
|
|
186
|
-
for (let n = Math.max(0, Math.ceil(center - reach)); n <= last; n++) {
|
|
187
|
-
const at = Math.abs(n - center) * scale * SINC_STEPS;
|
|
188
|
-
const k = Math.floor(at);
|
|
189
|
-
const weight = HANN_SINC[k] + (at - k) * (HANN_SINC[k + 1] - HANN_SINC[k]);
|
|
190
|
-
sum += input[n] * weight;
|
|
191
|
-
weights += weight;
|
|
192
|
-
}
|
|
193
|
-
output[i] = weights ? sum / weights : 0;
|
|
194
|
-
}
|
|
195
|
-
return output;
|
|
196
|
-
}
|
|
197
|
-
function monoInput(channels) {
|
|
198
|
-
const mono = new Float32Array(channels[0]?.length ?? 0);
|
|
199
|
-
let loudest2 = mono;
|
|
200
|
-
let loudestEnergy = 0;
|
|
201
|
-
for (const channel of channels) {
|
|
202
|
-
let energy = 0;
|
|
203
|
-
for (let i = 0; i < mono.length; i++) {
|
|
204
|
-
const value = channel[i];
|
|
205
|
-
mono[i] += value / channels.length;
|
|
206
|
-
energy += value * value;
|
|
207
|
-
}
|
|
208
|
-
if (energy > loudestEnergy) {
|
|
209
|
-
loudest2 = channel;
|
|
210
|
-
loudestEnergy = energy;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
const monoEnergy = mono.reduce((energy, value) => energy + value * value, 0);
|
|
214
|
-
return monoEnergy < loudestEnergy * 0.1 ? loudest2 : mono;
|
|
215
|
-
}
|
|
216
|
-
function uploaded(signal) {
|
|
217
|
-
return {
|
|
218
|
-
sampleRate: UPLOAD_RATE,
|
|
219
|
-
channels: [resample(monoInput(signal.channels), signal.sampleRate, UPLOAD_RATE)]
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
var askedTogether = (capability) => capability === "beats" || capability === "downbeats" ? ["beats", "downbeats"] : [capability];
|
|
223
|
-
function config() {
|
|
224
|
-
const url = process.env[URL_ENV]?.trim();
|
|
225
|
-
const key = process.env[KEY_ENV]?.trim();
|
|
226
|
-
const timeout = Number(process.env[TIMEOUT_ENV]);
|
|
227
|
-
return { url, key, timeoutMs: timeout > 0 ? timeout : DEFAULT_TIMEOUT_MS };
|
|
228
|
-
}
|
|
229
|
-
var isNumberList = (value) => Array.isArray(value) && value.every((t) => typeof t === "number" && Number.isFinite(t));
|
|
230
|
-
function checked(capability, body) {
|
|
231
|
-
const value = body[capability];
|
|
232
|
-
if (!value || typeof value !== "object")
|
|
233
|
-
throw new Error(`the service did not answer ${capability}`);
|
|
234
|
-
const valid = {
|
|
235
|
-
beats: () => isNumberList(value.times),
|
|
236
|
-
downbeats: () => isNumberList(value.times),
|
|
237
|
-
sections: () => Array.isArray(value.spans) && value.spans.every((s2) => isNumberList([s2.start, s2.end])),
|
|
238
|
-
describe: () => Array.isArray(value.tags) && value.tags.every(
|
|
239
|
-
(t) => typeof t.label === "string" && typeof t.score === "number"
|
|
240
|
-
)
|
|
241
|
-
}[capability];
|
|
242
|
-
if (!valid())
|
|
243
|
-
throw new Error(`the service answered ${capability} in a shape this CLI cannot read`);
|
|
244
|
-
}
|
|
245
|
-
async function failure(response) {
|
|
246
|
-
const text = await response.text().catch(() => "");
|
|
247
|
-
let detail = text.slice(0, 200);
|
|
248
|
-
try {
|
|
249
|
-
detail = JSON.parse(text).error?.message ?? detail;
|
|
250
|
-
} catch {
|
|
251
|
-
}
|
|
252
|
-
return new Error(`the service answered ${response.status}${detail ? `: ${detail}` : ""}`);
|
|
253
|
-
}
|
|
254
|
-
var uploadedAsset = perSignal(async (analyzed) => {
|
|
255
|
-
const { url, key, timeoutMs } = config();
|
|
256
|
-
if (!url || !key) throw new Error(`${URL_ENV} and ${KEY_ENV} must both be set`);
|
|
257
|
-
const deadline = AbortSignal.timeout(timeoutMs);
|
|
258
|
-
const boundedFetch = (input, init) => fetch(input, {
|
|
259
|
-
...init,
|
|
260
|
-
redirect: "error",
|
|
261
|
-
signal: deadline
|
|
262
|
-
});
|
|
263
|
-
const client = new HyperframesCloudClient({
|
|
264
|
-
baseUrl: new URL(url).origin,
|
|
265
|
-
getAuthHeaders: () => ({ "x-api-key": key }),
|
|
266
|
-
fetchImpl: boundedFetch
|
|
267
|
-
});
|
|
268
|
-
const asset = await uploadAssetViaDirectUpload({
|
|
269
|
-
client,
|
|
270
|
-
bytes: new Uint8Array(encodeWav(uploaded(analyzed))),
|
|
271
|
-
filename: "audio.wav",
|
|
272
|
-
contentType: "audio/wav",
|
|
273
|
-
idempotencyKey: randomUUID(),
|
|
274
|
-
fetchImpl: boundedFetch
|
|
275
|
-
});
|
|
276
|
-
return asset.asset_id;
|
|
277
|
-
});
|
|
278
|
-
async function ask(analyzed, capabilities2) {
|
|
279
|
-
const { url, key, timeoutMs } = config();
|
|
280
|
-
if (!url || !key) throw new Error(`${URL_ENV} and ${KEY_ENV} must both be set`);
|
|
281
|
-
const request = {
|
|
282
|
-
version: 1,
|
|
283
|
-
capabilities: capabilities2,
|
|
284
|
-
audio: { type: "asset_id", asset_id: await uploadedAsset(analyzed) }
|
|
285
|
-
};
|
|
286
|
-
const response = await fetch(url, {
|
|
287
|
-
method: "POST",
|
|
288
|
-
redirect: "error",
|
|
289
|
-
headers: {
|
|
290
|
-
"x-api-key": key,
|
|
291
|
-
"idempotency-key": randomUUID(),
|
|
292
|
-
"content-type": "application/json"
|
|
293
|
-
},
|
|
294
|
-
body: JSON.stringify(request),
|
|
295
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
296
|
-
});
|
|
297
|
-
if (!response.ok) throw await failure(response);
|
|
298
|
-
const body = await response.json();
|
|
299
|
-
return body?.data ?? body ?? {};
|
|
300
|
-
}
|
|
301
|
-
var requests = /* @__PURE__ */ new Map();
|
|
302
|
-
function answerFor(signal, capability) {
|
|
303
|
-
const together = askedTogether(capability);
|
|
304
|
-
const key = together.join(",");
|
|
305
|
-
let request = requests.get(key);
|
|
306
|
-
if (!request) requests.set(key, request = perSignal((s2) => ask(s2, together)));
|
|
307
|
-
return request(signal);
|
|
308
|
-
}
|
|
309
|
-
function beatSnapper(signal, beatSeconds) {
|
|
310
|
-
const measured2 = rhythm(signal).beats.times;
|
|
311
|
-
const period = beatSeconds ?? medianInterval(measured2) ?? 0;
|
|
312
|
-
return snapper(measured2, period / 4, "signal beats");
|
|
313
|
-
}
|
|
314
|
-
function snappedBeats(signal, body) {
|
|
315
|
-
const times = body.beats.times;
|
|
316
|
-
const onBeat = beatSnapper(signal, medianInterval(times));
|
|
317
|
-
return { times: rounded(times.map(onBeat.snap)), snap: onBeat.report() };
|
|
318
|
-
}
|
|
319
|
-
function snappedDownbeats(signal, body) {
|
|
320
|
-
const { times, meter = null } = body.downbeats;
|
|
321
|
-
const beat = body.beats ? medianInterval(body.beats.times) : null;
|
|
322
|
-
const bar = medianInterval(times);
|
|
323
|
-
const onBeat = beatSnapper(signal, beat ?? (bar && meter ? bar / meter : null));
|
|
324
|
-
return { times: rounded(times.map(onBeat.snap)), meter, snap: onBeat.report() };
|
|
325
|
-
}
|
|
326
|
-
function snappedSections(signal, body) {
|
|
327
|
-
const onBeat = beatSnapper(signal, null);
|
|
328
|
-
const end = (signal.channels[0]?.length ?? 0) / signal.sampleRate;
|
|
329
|
-
const move = (t) => t <= 0 || t >= end ? t : onBeat.snap(t);
|
|
330
|
-
const spans = body.sections.spans.map((s2) => ({
|
|
331
|
-
...s2,
|
|
332
|
-
start: rounded([move(s2.start)])[0],
|
|
333
|
-
end: rounded([move(s2.end)])[0]
|
|
334
|
-
}));
|
|
335
|
-
return { spans, snap: onBeat.report() };
|
|
336
|
-
}
|
|
337
|
-
function measured(capability, signal, body) {
|
|
338
|
-
const value = capability === "beats" ? snappedBeats(signal, body) : capability === "downbeats" ? snappedDownbeats(signal, body) : capability === "sections" ? snappedSections(signal, body) : body.describe;
|
|
339
|
-
return body.source ? { ...value, source: body.source } : value;
|
|
340
|
-
}
|
|
341
|
-
var API_CAPABILITIES = ["beats", "downbeats", "sections", "describe"];
|
|
342
|
-
var apiProvider = {
|
|
343
|
-
id: "api",
|
|
344
|
-
local: false,
|
|
345
|
-
capabilities: API_CAPABILITIES,
|
|
346
|
-
available() {
|
|
347
|
-
const { url, key } = config();
|
|
348
|
-
if (!url) return unavailable(`${URL_ENV} is not set`);
|
|
349
|
-
if (!key) return unavailable(`${KEY_ENV} is not set`);
|
|
350
|
-
return AVAILABLE;
|
|
351
|
-
},
|
|
352
|
-
async analyze(capability, signal) {
|
|
353
|
-
const asked = capability;
|
|
354
|
-
const body = await answerFor(signal, asked);
|
|
355
|
-
checked(asked, body);
|
|
356
|
-
return measured(asked, signal, body);
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
// src/audio/analysis/providers/apple.ts
|
|
361
|
-
import { createHash } from "crypto";
|
|
362
|
-
import { existsSync } from "fs";
|
|
363
|
-
import { mkdir, rename, writeFile as writeFile2 } from "fs/promises";
|
|
364
|
-
import { homedir } from "os";
|
|
365
|
-
import { join as join2 } from "path";
|
|
366
|
-
var SOUND_ANALYSIS = "/System/Library/Frameworks/SoundAnalysis.framework";
|
|
367
|
-
var MUSIC_UNDERSTANDING_MACOS = 27;
|
|
368
|
-
var SOUND_TAGGER = `
|
|
369
|
-
import Foundation
|
|
370
|
-
import SoundAnalysis
|
|
371
|
-
|
|
372
|
-
final class Collector: NSObject, SNResultsObserving {
|
|
373
|
-
var sums: [String: Double] = [:]
|
|
374
|
-
var windows = 0
|
|
375
|
-
var failure: Error?
|
|
376
|
-
func request(_ request: SNRequest, didProduce result: SNResult) {
|
|
377
|
-
guard let result = result as? SNClassificationResult else { return }
|
|
378
|
-
windows += 1
|
|
379
|
-
for c in result.classifications { sums[c.identifier, default: 0] += c.confidence }
|
|
380
|
-
}
|
|
381
|
-
func request(_ request: SNRequest, didFailWithError error: Error) { failure = error }
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
do {
|
|
385
|
-
let analyzer = try SNAudioFileAnalyzer(url: URL(fileURLWithPath: CommandLine.arguments[1]))
|
|
386
|
-
let collector = Collector()
|
|
387
|
-
try analyzer.add(try SNClassifySoundRequest(classifierIdentifier: .version1), withObserver: collector)
|
|
388
|
-
analyzer.analyze()
|
|
389
|
-
if let failure = collector.failure { throw failure }
|
|
390
|
-
let count = Double(max(1, collector.windows))
|
|
391
|
-
var tags: [[String: Any]] = []
|
|
392
|
-
for (label, sum) in collector.sums.sorted(by: { $0.value > $1.value }).prefix(10) {
|
|
393
|
-
let score: Double = sum / count
|
|
394
|
-
if score >= 0.01 { tags.append(["label": label, "score": (score * 1000).rounded() / 1000]) }
|
|
395
|
-
}
|
|
396
|
-
let json = try JSONSerialization.data(withJSONObject: ["tags": tags, "windows": collector.windows])
|
|
397
|
-
FileHandle.standardOutput.write(json)
|
|
398
|
-
} catch {
|
|
399
|
-
FileHandle.standardError.write(Data("\\(error)".utf8))
|
|
400
|
-
exit(1)
|
|
401
|
-
}
|
|
402
|
-
`;
|
|
403
|
-
var CLASSIFIER_WINDOW_SECONDS = 3;
|
|
404
|
-
function atLeastOneWindow(signal) {
|
|
405
|
-
const length = Math.ceil(CLASSIFIER_WINDOW_SECONDS * signal.sampleRate);
|
|
406
|
-
if ((signal.channels[0]?.length ?? 0) >= length) return signal;
|
|
407
|
-
return {
|
|
408
|
-
...signal,
|
|
409
|
-
channels: signal.channels.map((channel) => {
|
|
410
|
-
const padded = new Float32Array(length);
|
|
411
|
-
padded.set(channel);
|
|
412
|
-
return padded;
|
|
413
|
-
})
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
var helperDir = () => join2(homedir(), ".cache", "hyperframes", "audio-helpers");
|
|
417
|
-
function helperPath(name, source) {
|
|
418
|
-
const hash = createHash("sha256").update(source).digest("hex").slice(0, 12);
|
|
419
|
-
return join2(helperDir(), `${name}-${hash}`);
|
|
420
|
-
}
|
|
421
|
-
async function hasSwiftCompiler() {
|
|
422
|
-
try {
|
|
423
|
-
await runFile("xcrun", ["--find", "swiftc"], { timeout: 3e4 });
|
|
424
|
-
return true;
|
|
425
|
-
} catch {
|
|
426
|
-
return false;
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
var firstLine = (error) => (error.stderr || String(error)).trim().split("\n")[0];
|
|
430
|
-
var builds = /* @__PURE__ */ new Map();
|
|
431
|
-
function compiled(name, source) {
|
|
432
|
-
const binary = helperPath(name, source);
|
|
433
|
-
if (existsSync(binary)) return Promise.resolve(binary);
|
|
434
|
-
let build = builds.get(binary);
|
|
435
|
-
if (!build) {
|
|
436
|
-
build = (async () => {
|
|
437
|
-
await mkdir(helperDir(), { recursive: true });
|
|
438
|
-
const file = `${binary}.swift`;
|
|
439
|
-
await writeFile2(file, source);
|
|
440
|
-
const args = [
|
|
441
|
-
"--sdk",
|
|
442
|
-
"macosx",
|
|
443
|
-
"swiftc",
|
|
444
|
-
"-O",
|
|
445
|
-
"-swift-version",
|
|
446
|
-
"5",
|
|
447
|
-
file,
|
|
448
|
-
"-o",
|
|
449
|
-
`${binary}.tmp`
|
|
450
|
-
];
|
|
451
|
-
await runFile("xcrun", args, { timeout: 5 * 6e4 }).catch((error) => {
|
|
452
|
-
throw new Error(`could not build the ${name} helper: ${firstLine(error)}`);
|
|
453
|
-
});
|
|
454
|
-
await rename(`${binary}.tmp`, binary);
|
|
455
|
-
return binary;
|
|
456
|
-
})();
|
|
457
|
-
builds.set(binary, build);
|
|
458
|
-
}
|
|
459
|
-
return build;
|
|
460
|
-
}
|
|
461
|
-
async function macosMajor() {
|
|
462
|
-
if (process.platform !== "darwin") return null;
|
|
463
|
-
const { stdout } = await runFile("sw_vers", ["-productVersion"], { timeout: 1e4 });
|
|
464
|
-
return Number.parseInt(stdout, 10) || null;
|
|
465
|
-
}
|
|
466
|
-
var appleSoundProvider = {
|
|
467
|
-
id: "apple-sound",
|
|
468
|
-
local: true,
|
|
469
|
-
capabilities: ["describe"],
|
|
470
|
-
async available() {
|
|
471
|
-
if (process.platform !== "darwin") return unavailable("macOS only");
|
|
472
|
-
if (!existsSync(SOUND_ANALYSIS))
|
|
473
|
-
return unavailable("SoundAnalysis.framework is not on this Mac");
|
|
474
|
-
if (existsSync(helperPath("apple-sound", SOUND_TAGGER))) return AVAILABLE;
|
|
475
|
-
if (!await hasSwiftCompiler()) return unavailable("no Swift compiler to build the helper");
|
|
476
|
-
return AVAILABLE;
|
|
477
|
-
},
|
|
478
|
-
async analyze(_capability, signal) {
|
|
479
|
-
const helper = await compiled("apple-sound", SOUND_TAGGER);
|
|
480
|
-
const { stdout } = await withWavFile(
|
|
481
|
-
atLeastOneWindow(signal),
|
|
482
|
-
(wav) => runFile(helper, [wav], { timeout: 5 * 6e4, maxBuffer: 8 * 1024 * 1024 })
|
|
483
|
-
);
|
|
484
|
-
const { tags } = JSON.parse(stdout);
|
|
485
|
-
return {
|
|
486
|
-
tags: tags.map((t) => ({ label: t.label, score: Math.round(t.score * 1e3) / 1e3 }))
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
};
|
|
490
|
-
var appleMuProvider = {
|
|
491
|
-
id: "apple-mu",
|
|
492
|
-
local: true,
|
|
493
|
-
capabilities: ["beats", "downbeats", "sections"],
|
|
494
|
-
async available() {
|
|
495
|
-
const major = await macosMajor();
|
|
496
|
-
if (major === null) return unavailable("macOS only");
|
|
497
|
-
if (major < MUSIC_UNDERSTANDING_MACOS)
|
|
498
|
-
return unavailable(
|
|
499
|
-
`Music Understanding needs macOS ${MUSIC_UNDERSTANDING_MACOS}; this Mac runs ${major}`
|
|
500
|
-
);
|
|
501
|
-
return unavailable("no Music Understanding helper yet: it needs the macOS 27 SDK to build");
|
|
502
|
-
},
|
|
503
|
-
analyze() {
|
|
504
|
-
return Promise.reject(new Error("Music Understanding is not available"));
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
// src/audio/analysis/providers/beatThis.ts
|
|
509
|
-
import { existsSync as existsSync2 } from "fs";
|
|
510
|
-
import { homedir as homedir2 } from "os";
|
|
511
|
-
import { join as join3 } from "path";
|
|
512
|
-
var PYTHON_ENV = "HYPERFRAMES_BEAT_THIS_PYTHON";
|
|
513
|
-
var CHECKPOINT_ENV = "HYPERFRAMES_BEAT_THIS_CHECKPOINT";
|
|
514
|
-
function checkpointPath() {
|
|
515
|
-
const configured = process.env[CHECKPOINT_ENV]?.trim();
|
|
516
|
-
if (configured) return configured;
|
|
517
|
-
const torchHome = process.env.TORCH_HOME ?? join3(homedir2(), ".cache", "torch");
|
|
518
|
-
return join3(torchHome, "hub", "checkpoints", "beat_this-small0.ckpt");
|
|
519
|
-
}
|
|
520
|
-
var FIND_PACKAGE = "import importlib.util,sys; sys.exit(importlib.util.find_spec('beat_this') is None)";
|
|
521
|
-
var TRACK = `
|
|
522
|
-
import json, sys
|
|
523
|
-
from beat_this.inference import File2Beats
|
|
524
|
-
beats, downbeats = File2Beats(checkpoint_path=sys.argv[2], device="cpu", dbn=False)(sys.argv[1])
|
|
525
|
-
print(json.dumps({"beats": [float(t) for t in beats], "downbeats": [float(t) for t in downbeats]}))
|
|
526
|
-
`;
|
|
527
|
-
var installed = /* @__PURE__ */ new Map();
|
|
528
|
-
function hasPackage(python) {
|
|
529
|
-
let found = installed.get(python);
|
|
530
|
-
if (!found) {
|
|
531
|
-
found = runFile(python, ["-c", FIND_PACKAGE], { timeout: 3e4, windowsHide: true }).then(
|
|
532
|
-
() => true,
|
|
533
|
-
() => false
|
|
534
|
-
);
|
|
535
|
-
installed.set(python, found);
|
|
536
|
-
}
|
|
537
|
-
return found;
|
|
538
|
-
}
|
|
539
|
-
var track = perSignal(
|
|
540
|
-
(signal) => withWavFile(signal, async (wav) => {
|
|
541
|
-
const python = process.env[PYTHON_ENV].trim();
|
|
542
|
-
const { stdout } = await runFile(python, ["-c", TRACK, wav, checkpointPath()], {
|
|
543
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
544
|
-
timeout: 10 * 6e4,
|
|
545
|
-
windowsHide: true
|
|
546
|
-
});
|
|
547
|
-
const answer = JSON.parse(stdout.trim().split("\n").pop());
|
|
548
|
-
return { beats: rounded(answer.beats), downbeats: rounded(answer.downbeats) };
|
|
549
|
-
})
|
|
550
|
-
);
|
|
551
|
-
var beatThisProvider = {
|
|
552
|
-
id: "beat-this",
|
|
553
|
-
local: true,
|
|
554
|
-
capabilities: ["beats", "downbeats"],
|
|
555
|
-
async available() {
|
|
556
|
-
const python = process.env[PYTHON_ENV]?.trim();
|
|
557
|
-
if (!python) return unavailable(`${PYTHON_ENV} is not set`);
|
|
558
|
-
if (!existsSync2(python)) return unavailable(`${PYTHON_ENV} names no file`);
|
|
559
|
-
if (!existsSync2(checkpointPath()))
|
|
560
|
-
return unavailable(`no Beat This! weights at ${checkpointPath()}`);
|
|
561
|
-
if (!await hasPackage(python)) return unavailable(`beat_this is not installed for ${python}`);
|
|
562
|
-
return AVAILABLE;
|
|
563
|
-
},
|
|
564
|
-
async analyze(capability, signal) {
|
|
565
|
-
const { beats, downbeats } = await track(signal);
|
|
566
|
-
return capability === "beats" ? { times: beats } : downbeatsWithMeter(beats, downbeats);
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
|
|
570
|
-
// src/audio/analysis/fileTime.ts
|
|
571
|
-
var round2 = (t) => Math.round(t * 1e3) / 1e3;
|
|
572
|
-
function withSnap(value, at) {
|
|
573
|
-
return value.snap ? { ...value, snap: { ...value.snap, unsnapped: value.snap.unsnapped.map(at) } } : value;
|
|
574
|
-
}
|
|
575
|
-
var SHIFTS = {
|
|
576
|
-
moments: (m, at) => Object.fromEntries(Object.entries(m).map(([k, t]) => [k, at(t)])),
|
|
577
|
-
onsets: (o, at) => ({ ...o, events: o.events.map((e) => ({ ...e, t: at(e.t) })) }),
|
|
578
|
-
silence: (spans, at) => spans.map((s2) => ({ start: at(s2.start), end: at(s2.end) })),
|
|
579
|
-
loudness: (l, at) => ({
|
|
580
|
-
...l,
|
|
581
|
-
clipping: {
|
|
582
|
-
...l.clipping,
|
|
583
|
-
firstAt: l.clipping.firstAt === null ? null : at(l.clipping.firstAt)
|
|
584
|
-
}
|
|
585
|
-
}),
|
|
586
|
-
envelope: (e, _at, offset) => ({ startsAt: round2(offset), ...e }),
|
|
587
|
-
beats: (b, at) => withSnap({ ...b, times: b.times.map(at) }, at),
|
|
588
|
-
downbeats: (b, at) => withSnap({ ...b, times: b.times.map(at) }, at),
|
|
589
|
-
bpm: (tempo, at) => ({
|
|
590
|
-
...tempo,
|
|
591
|
-
curve: tempo.curve.map((p) => ({ ...p, t: at(p.t) }))
|
|
592
|
-
}),
|
|
593
|
-
sections: (s2, at) => withSnap(
|
|
594
|
-
{
|
|
595
|
-
...s2,
|
|
596
|
-
spans: s2.spans.map((span) => ({ ...span, start: at(span.start), end: at(span.end) }))
|
|
597
|
-
},
|
|
598
|
-
at
|
|
599
|
-
)
|
|
600
|
-
};
|
|
601
|
-
function shiftedTimes(capability, value, offset) {
|
|
602
|
-
if (value === null || typeof value !== "object") return value;
|
|
603
|
-
const shift = SHIFTS[capability];
|
|
604
|
-
return shift ? shift(value, (t) => round2(t + offset), offset) : value;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// src/audio/analysis/analyze.ts
|
|
608
|
-
var DEFAULT_CAPABILITIES = [
|
|
609
|
-
"kind",
|
|
610
|
-
"moments",
|
|
611
|
-
"onsets",
|
|
612
|
-
"loudness",
|
|
613
|
-
"silence",
|
|
614
|
-
"envelope"
|
|
615
|
-
];
|
|
616
|
-
function audioAnalysisProviders() {
|
|
617
|
-
return [apiProvider, appleMuProvider, beatThisProvider, appleSoundProvider, signalProvider];
|
|
618
|
-
}
|
|
619
|
-
function envelopeHopMs(seconds, asked) {
|
|
620
|
-
if (asked !== void 0) return asked;
|
|
621
|
-
return Math.max(5, Math.ceil(seconds * 1e3 / 2e3));
|
|
622
|
-
}
|
|
623
|
-
var round3 = (t) => Math.round(t * 1e3) / 1e3;
|
|
624
|
-
function within(value, from, to) {
|
|
625
|
-
if (!value || typeof value !== "object" || !("events" in value)) return value;
|
|
626
|
-
const onsets = value;
|
|
627
|
-
return { ...onsets, events: onsets.events.filter((e) => e.t >= from && e.t < to) };
|
|
628
|
-
}
|
|
629
|
-
function frame(signal, context) {
|
|
630
|
-
const before = context?.before ?? 0;
|
|
631
|
-
const skip = Math.round(before * signal.sampleRate);
|
|
632
|
-
const length = signal.channels[0]?.length ?? 0;
|
|
633
|
-
const end = Math.max(skip, length - Math.round((context?.after ?? 0) * signal.sampleRate));
|
|
634
|
-
const cut = skip > 0 || end < length;
|
|
635
|
-
const windowed = cut ? { ...signal, channels: signal.channels.map((c) => c.subarray(skip, end)) } : signal;
|
|
636
|
-
return { windowed, before, end: end / signal.sampleRate, cut };
|
|
637
|
-
}
|
|
638
|
-
function optionsFor(capability, request, seconds) {
|
|
639
|
-
const options = { ...request.options?.[capability] };
|
|
640
|
-
if (capability === "envelope")
|
|
641
|
-
options.hopMs = envelopeHopMs(seconds, options.hopMs);
|
|
642
|
-
return options;
|
|
643
|
-
}
|
|
644
|
-
async function analyzeSignal(signal, request, providers = audioAnalysisProviders()) {
|
|
645
|
-
const { beats, bpm } = request.provider ?? {};
|
|
646
|
-
if (beats && bpm && beats !== bpm) throw new Error("beats and bpm must use the same provider");
|
|
647
|
-
const beatProvider = beats ?? bpm;
|
|
648
|
-
const framing = frame(signal, request.context);
|
|
649
|
-
const analysis = {
|
|
650
|
-
analyzed: round3(signalDuration(framing.windowed)),
|
|
651
|
-
values: {},
|
|
652
|
-
provenance: {},
|
|
653
|
-
ladder: {},
|
|
654
|
-
warnings: []
|
|
655
|
-
};
|
|
656
|
-
const ladders = /* @__PURE__ */ new Map();
|
|
657
|
-
const ladderFor = (capability) => {
|
|
658
|
-
let result = ladders.get(capability);
|
|
659
|
-
if (!result) {
|
|
660
|
-
const withContext = capability === "onsets" && framing.cut;
|
|
661
|
-
result = runLadder(capability, withContext ? signal : framing.windowed, providers, {
|
|
662
|
-
quality: request.quality,
|
|
663
|
-
provider: capability === "beats" ? beatProvider : request.provider?.[capability],
|
|
664
|
-
options: optionsFor(capability, request, analysis.analyzed)
|
|
665
|
-
});
|
|
666
|
-
ladders.set(capability, result);
|
|
667
|
-
}
|
|
668
|
-
return result;
|
|
669
|
-
};
|
|
670
|
-
for (const capability of request.capabilities) {
|
|
671
|
-
const withContext = capability === "onsets" && framing.cut;
|
|
672
|
-
const result = capability === "bpm" ? tempoFromBeats(await ladderFor("beats")) : await ladderFor(capability);
|
|
673
|
-
const value = withContext ? within(result.value, framing.before, framing.end) : result.value;
|
|
674
|
-
const offset = (request.fileOffset ?? 0) - (withContext ? framing.before : 0);
|
|
675
|
-
analysis.values[capability] = shiftedTimes(capability, value, offset);
|
|
676
|
-
analysis.provenance[capability] = result.provenance;
|
|
677
|
-
analysis.ladder[capability] = result.steps;
|
|
678
|
-
const said = new Set(analysis.warnings);
|
|
679
|
-
analysis.warnings.push(...result.warnings.filter((w) => !said.has(w)));
|
|
680
|
-
}
|
|
681
|
-
return analysis;
|
|
682
|
-
}
|
|
683
|
-
function tempoFromBeats(beats) {
|
|
684
|
-
const times = beats.value?.times;
|
|
685
|
-
return { ...beats, value: times ? tempoOf(times) : null };
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// src/audio/analysis/targets.ts
|
|
689
|
-
import { dirname, join as join4, resolve } from "path";
|
|
690
|
-
var round4 = (t) => Math.round(t * 1e3) / 1e3;
|
|
691
|
-
function placementOf(row) {
|
|
692
|
-
return {
|
|
693
|
-
start: row.absStart,
|
|
694
|
-
end: row.absEnd,
|
|
695
|
-
dataStart: row.start,
|
|
696
|
-
mediaStart: row.mediaStart ?? 0,
|
|
697
|
-
playbackRate: row.playbackRate ?? 1,
|
|
698
|
-
volume: row.volume ?? 1
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
function filmTime(placement, fileTime2) {
|
|
702
|
-
return round4(placement.start + (fileTime2 - placement.mediaStart) / placement.playbackRate);
|
|
703
|
-
}
|
|
704
|
-
function fileTime(placement, filmTime2) {
|
|
705
|
-
return round4(placement.mediaStart + (filmTime2 - placement.start) * placement.playbackRate);
|
|
706
|
-
}
|
|
707
|
-
var soundingRows = (timeline) => timeline.tracks.filter((track2) => track2.kind === "audio" || track2.kind === "video").flatMap((track2) => track2.rows).filter((row) => row.src && row.volume !== 0 && /^(audio|video)$/.test(row.kind)).sort((a, b) => a.absStart - b.absStart);
|
|
708
|
-
function matches(row, query) {
|
|
709
|
-
switch (query.by) {
|
|
710
|
-
case "all":
|
|
711
|
-
case "element":
|
|
712
|
-
return true;
|
|
713
|
-
case "at":
|
|
714
|
-
return row.absStart <= query.t && query.t < row.absEnd;
|
|
715
|
-
case "range":
|
|
716
|
-
return row.absStart < query.end && query.start < row.absEnd;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
function targetOf(projectDir, row, query) {
|
|
720
|
-
const placement = placementOf(row);
|
|
721
|
-
const from = query.by === "range" ? Math.max(placement.start, query.start) : placement.start;
|
|
722
|
-
const to = query.by === "range" ? Math.min(placement.end, query.end) : placement.end;
|
|
723
|
-
return {
|
|
724
|
-
path: resolveAuthoredAssetFile(projectDir, row.file, row.src) ?? resolve(dirname(join4(projectDir, row.file)), row.src),
|
|
725
|
-
file: row.src,
|
|
726
|
-
ref: row.ref || null,
|
|
727
|
-
kind: row.kind,
|
|
728
|
-
placement,
|
|
729
|
-
window: {
|
|
730
|
-
from: fileTime(placement, from),
|
|
731
|
-
seconds: round4((to - from) * placement.playbackRate)
|
|
732
|
-
}
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
function filmTargets(projectDir, timeline, query) {
|
|
736
|
-
if (query.by === "element") {
|
|
737
|
-
const id = query.ref.startsWith("#") ? query.ref.slice(1) : null;
|
|
738
|
-
const found = timeline.tracks.flatMap((track2) => track2.rows).filter((row2) => row2.ref === query.ref || id !== null && row2.id === id);
|
|
739
|
-
if (found.length !== 1)
|
|
740
|
-
throw new Error(
|
|
741
|
-
`${query.ref} ${found.length ? `matches ${found.length} clips` : "was not found"}; use a ref from hyperframes timeline`
|
|
742
|
-
);
|
|
743
|
-
const row = found[0];
|
|
744
|
-
if (!row.src || !/^(audio|video)$/.test(row.kind))
|
|
745
|
-
throw new Error(`${query.ref} is not an audio or video clip`);
|
|
746
|
-
return [targetOf(projectDir, row, query)];
|
|
747
|
-
}
|
|
748
|
-
return soundingRows(timeline).filter((row) => matches(row, query)).map((row) => targetOf(projectDir, row, query));
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
// src/audio/analysis/format.ts
|
|
752
|
-
var BARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
753
|
-
var SPARK_WIDTH = 48;
|
|
754
|
-
var ONSETS_SHOWN = 12;
|
|
755
|
-
var s = (t) => t.toFixed(3);
|
|
756
|
-
function sparkline(envelope2, width = SPARK_WIDTH) {
|
|
757
|
-
const { values } = envelope2;
|
|
758
|
-
if (values.length === 0) return "";
|
|
759
|
-
const per = Math.max(1, Math.ceil(values.length / width));
|
|
760
|
-
const slices = [];
|
|
761
|
-
for (let i = 0; i < values.length; i += per) slices.push(Math.max(...values.slice(i, i + per)));
|
|
762
|
-
const top = Math.max(...slices);
|
|
763
|
-
const floor = Math.max(Math.min(...slices), top - 60);
|
|
764
|
-
if (top - floor < 1e-9) return BARS[0].repeat(slices.length);
|
|
765
|
-
return slices.map(
|
|
766
|
-
(v) => BARS[Math.round((Math.max(v, floor) - floor) / (top - floor) * (BARS.length - 1))]
|
|
767
|
-
).join("");
|
|
768
|
-
}
|
|
769
|
-
function momentsLine(moments, at) {
|
|
770
|
-
return ` onset ${at(moments.onset)} steepest ${at(moments.steepest)} peak ${at(moments.peak)} tail end ${at(moments.tailEnd)}`;
|
|
771
|
-
}
|
|
772
|
-
function hitsLine(onsets, at) {
|
|
773
|
-
if (!onsets.events.length) return null;
|
|
774
|
-
const shown = onsets.events.slice(0, ONSETS_SHOWN).map((e) => `${at(e.t)}(${e.strength.toFixed(2)})`);
|
|
775
|
-
const more = onsets.events.length - shown.length;
|
|
776
|
-
return ` hits ${shown.join(" ")}${more > 0 ? ` +${more}` : ""}`;
|
|
777
|
-
}
|
|
778
|
-
function loudnessLine(loudness, at) {
|
|
779
|
-
const lufs = loudness.lufsIntegrated === null ? "below gate" : `${loudness.lufsIntegrated} LUFS`;
|
|
780
|
-
const { count, firstAt } = loudness.clipping;
|
|
781
|
-
const clip = count && firstAt !== null ? `, clips ${count}\xD7 from ${at(firstAt)}` : "";
|
|
782
|
-
return ` loudness ${lufs}, true peak ${loudness.truePeakDb} dBTP${clip}`;
|
|
783
|
-
}
|
|
784
|
-
function silenceLine(silence, at) {
|
|
785
|
-
return silence.length ? ` silent ${silence.map((g) => `${at(g.start)}\u2013${at(g.end)}`).join(" ")}` : null;
|
|
786
|
-
}
|
|
787
|
-
function snapNote(snap) {
|
|
788
|
-
if (!snap) return "";
|
|
789
|
-
const total = snap.snapped + snap.unsnapped.length;
|
|
790
|
-
const median = snap.medianShiftMs === null ? "" : `, median ${snap.medianShiftMs} ms`;
|
|
791
|
-
return `; ${snap.snapped}/${total} snapped to ${snap.reference}${median}`;
|
|
792
|
-
}
|
|
793
|
-
var listed = (times, at) => {
|
|
794
|
-
const shown = times.slice(0, ONSETS_SHOWN).map(at).join(" ");
|
|
795
|
-
return times.length > ONSETS_SHOWN ? `${shown} +${times.length - ONSETS_SHOWN}` : shown;
|
|
796
|
-
};
|
|
797
|
-
function beatsLine(beats, at) {
|
|
798
|
-
if (!beats.times.length) return ` beats none${beats.reason ? ` (${beats.reason})` : ""}`;
|
|
799
|
-
return ` beats ${beats.times.length}: ${listed(beats.times, at)}${snapNote(beats.snap)}`;
|
|
800
|
-
}
|
|
801
|
-
function downbeatsLine(downbeats, at) {
|
|
802
|
-
if (!downbeats.times.length) return null;
|
|
803
|
-
const meter = downbeats.meter ? ` in ${downbeats.meter}` : "";
|
|
804
|
-
const low = downbeats.confidence === "low" ? ", a guess" : "";
|
|
805
|
-
return ` downbeats${meter}${low}: ${listed(downbeats.times, at)}${snapNote(downbeats.snap)}`;
|
|
806
|
-
}
|
|
807
|
-
function tempoLine(tempo) {
|
|
808
|
-
if (tempo.value === null) return null;
|
|
809
|
-
const bpms = tempo.curve.map((p) => p.bpm);
|
|
810
|
-
const spread = bpms.length && Math.max(...bpms) - Math.min(...bpms) >= 2 ? ` (${Math.min(...bpms)}\u2013${Math.max(...bpms)})` : "";
|
|
811
|
-
return ` tempo ${tempo.value} BPM${spread}`;
|
|
812
|
-
}
|
|
813
|
-
function sectionsLine(sections, at) {
|
|
814
|
-
const spans = sections.spans.map((s2) => `${s2.label} ${at(s2.start)}\u2013${at(s2.end)}`).join(" ");
|
|
815
|
-
return ` sections ${spans}${snapNote(sections.snap)}`;
|
|
816
|
-
}
|
|
817
|
-
function describeLine(description) {
|
|
818
|
-
const tags = description.tags.map((t) => `${t.label} ${t.score.toFixed(2)}`).join(", ");
|
|
819
|
-
return ` sounds like ${[description.text, tags].filter(Boolean).join(" \u2014 ")}`;
|
|
820
|
-
}
|
|
821
|
-
var LINES = [
|
|
822
|
-
["moments", momentsLine],
|
|
823
|
-
["onsets", hitsLine],
|
|
824
|
-
["loudness", loudnessLine],
|
|
825
|
-
["silence", silenceLine],
|
|
826
|
-
["envelope", (envelope2) => ` ${sparkline(envelope2)}`],
|
|
827
|
-
["describe", describeLine],
|
|
828
|
-
["bpm", tempoLine],
|
|
829
|
-
["beats", beatsLine],
|
|
830
|
-
["downbeats", downbeatsLine],
|
|
831
|
-
["sections", sectionsLine]
|
|
832
|
-
];
|
|
833
|
-
function headline(item, where) {
|
|
834
|
-
const kind = item.kind;
|
|
835
|
-
const span = item.placement ? `film ${s(item.placement.start)}\u2013${s(item.placement.end)}, file from ${s(item.placement.mediaStart)}` : `${s(item.duration ?? 0)} s`;
|
|
836
|
-
return `${where} ${span}${kind ? ` ${kind.value} (${kind.confidence})` : ""}`;
|
|
837
|
-
}
|
|
838
|
-
function formatItem(item) {
|
|
839
|
-
const where = item.ref ? `${item.file} ${item.ref}` : item.file;
|
|
840
|
-
if (item.error) return [`${where}: ${item.error}`];
|
|
841
|
-
const at = (t) => s(item.placement ? filmTime(item.placement, t) : t);
|
|
842
|
-
const lines = [headline(item, where)];
|
|
843
|
-
for (const [capability, line] of LINES) {
|
|
844
|
-
const value = item[capability];
|
|
845
|
-
const text = value ? line(value, at) : null;
|
|
846
|
-
if (text) lines.push(text);
|
|
847
|
-
}
|
|
848
|
-
for (const warning of item.warnings) lines.push(` ! ${warning}`);
|
|
849
|
-
return lines;
|
|
850
|
-
}
|
|
851
|
-
function formatReport(report) {
|
|
852
|
-
if (report.items.length === 0) return "No audio matched.";
|
|
853
|
-
return report.items.map((item) => formatItem(item).join("\n")).join("\n\n");
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// src/audio/analysis/run.ts
|
|
857
|
-
import { existsSync as existsSync3 } from "fs";
|
|
858
|
-
var round5 = (t) => Math.round(t * 1e3) / 1e3;
|
|
859
|
-
var EDGE_CONTEXT_SECONDS = 0.1;
|
|
860
|
-
async function previewTarget(path, file, at, mediaStart = 0) {
|
|
861
|
-
if (at === null) return { path, file, ref: null, kind: "file", placement: null };
|
|
862
|
-
const stream = existsSync3(path) ? await probeAudio(path) : null;
|
|
863
|
-
if (stream && mediaStart >= stream.duration)
|
|
864
|
-
throw new Error(
|
|
865
|
-
`skipping ${mediaStart} s leaves nothing of ${file}, which is ${round5(stream.duration)} s long`
|
|
866
|
-
);
|
|
867
|
-
const length = stream ? stream.duration - mediaStart : 0;
|
|
868
|
-
return {
|
|
869
|
-
path,
|
|
870
|
-
file,
|
|
871
|
-
ref: null,
|
|
872
|
-
kind: "file",
|
|
873
|
-
placement: {
|
|
874
|
-
start: at,
|
|
875
|
-
end: round5(at + length),
|
|
876
|
-
dataStart: at,
|
|
877
|
-
mediaStart,
|
|
878
|
-
playbackRate: 1,
|
|
879
|
-
volume: 1
|
|
880
|
-
},
|
|
881
|
-
window: { from: mediaStart, seconds: length }
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
var emptyItem = (target) => ({
|
|
885
|
-
file: target.file,
|
|
886
|
-
ref: target.ref,
|
|
887
|
-
element: target.kind === "file" ? null : target.kind,
|
|
888
|
-
placement: target.placement,
|
|
889
|
-
timebase: "file",
|
|
890
|
-
window: null,
|
|
891
|
-
duration: null,
|
|
892
|
-
sampleRate: null,
|
|
893
|
-
channels: null,
|
|
894
|
-
provenance: {},
|
|
895
|
-
warnings: []
|
|
896
|
-
});
|
|
897
|
-
function unreadable(target) {
|
|
898
|
-
if (/^[a-z][a-z\d+.-]*:\/\//i.test(target.file)) return "remote sources are not analyzed";
|
|
899
|
-
if (!existsSync3(target.path)) return "file not found";
|
|
900
|
-
return null;
|
|
901
|
-
}
|
|
902
|
-
function decodeSpan(target, stream) {
|
|
903
|
-
const from = target.window?.from ?? 0;
|
|
904
|
-
const seconds = target.window?.seconds ?? stream.duration - from;
|
|
905
|
-
const before = Math.min(from, EDGE_CONTEXT_SECONDS);
|
|
906
|
-
const after = Math.max(0, Math.min(EDGE_CONTEXT_SECONDS, stream.duration - from - seconds));
|
|
907
|
-
return {
|
|
908
|
-
from,
|
|
909
|
-
read: { from: from - before, seconds: seconds + before + after },
|
|
910
|
-
context: { before, after }
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
var loudest = (signal) => Math.max(0, ...envelope(signal, { hopMs: 10, scale: "linear" }).values);
|
|
914
|
-
function retained(signal, before, after) {
|
|
915
|
-
const skip = Math.round(before * signal.sampleRate);
|
|
916
|
-
const length = signal.channels[0]?.length ?? 0;
|
|
917
|
-
const end = Math.max(skip, length - Math.round(after * signal.sampleRate));
|
|
918
|
-
if (!skip && end >= length) return signal;
|
|
919
|
-
return { ...signal, channels: signal.channels.map((channel) => channel.subarray(skip, end)) };
|
|
920
|
-
}
|
|
921
|
-
async function skippedPeak(target, kept, stream) {
|
|
922
|
-
const skipped = target.kind === "file" ? target.window?.from ?? 0 : 0;
|
|
923
|
-
if (skipped <= 0) return [];
|
|
924
|
-
const head = await decodeAudio(target.path, { from: 0, seconds: skipped }, stream);
|
|
925
|
-
if (!head || loudest(head.signal) <= loudest(kept)) return [];
|
|
926
|
-
return [
|
|
927
|
-
`the loudest part of ${target.file} is in the first ${round5(skipped)} s, which is skipped`
|
|
928
|
-
];
|
|
929
|
-
}
|
|
930
|
-
async function analyzeTarget(target, request, full) {
|
|
931
|
-
const item = emptyItem(target);
|
|
932
|
-
const problem = unreadable(target);
|
|
933
|
-
if (problem) return { ...item, error: problem };
|
|
934
|
-
const stream = await probeAudio(target.path);
|
|
935
|
-
if (!stream) return { ...item, error: "no audio stream" };
|
|
936
|
-
const { from, read, context } = decodeSpan(target, stream);
|
|
937
|
-
const decoded = await decodeAudio(target.path, read, stream);
|
|
938
|
-
if (!decoded) return { ...item, error: "no audio stream" };
|
|
939
|
-
const analysis = await analyzeSignal(decoded.signal, { ...request, fileOffset: from, context });
|
|
940
|
-
return {
|
|
941
|
-
...item,
|
|
942
|
-
window: { from: round5(from), to: round5(from + analysis.analyzed) },
|
|
943
|
-
duration: round5(stream.duration),
|
|
944
|
-
sampleRate: stream.sampleRate,
|
|
945
|
-
channels: stream.channels,
|
|
946
|
-
...analysis.values,
|
|
947
|
-
provenance: analysis.provenance,
|
|
948
|
-
...full ? { ladder: analysis.ladder } : {},
|
|
949
|
-
warnings: [
|
|
950
|
-
...analysis.warnings,
|
|
951
|
-
...await skippedPeak(
|
|
952
|
-
target,
|
|
953
|
-
retained(decoded.signal, context.before, context.after),
|
|
954
|
-
stream
|
|
955
|
-
)
|
|
956
|
-
]
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
async function analyzeTargets(targets, request, full = false) {
|
|
960
|
-
const items = [];
|
|
961
|
-
for (const target of targets) {
|
|
962
|
-
try {
|
|
963
|
-
items.push(await analyzeTarget(target, request, full));
|
|
964
|
-
} catch (error) {
|
|
965
|
-
const reason = error instanceof Error ? error.message.split("\n")[0] : String(error);
|
|
966
|
-
items.push({ ...emptyItem(target), error: `could not read: ${reason}` });
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
return { version: 1, items };
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
// src/commands/audio.ts
|
|
973
|
-
var examples = [
|
|
974
|
-
["Where a sound effect lands, its hits and loudness", "hyperframes audio sfx/whoosh.mp3"],
|
|
975
|
-
[
|
|
976
|
-
"Where a file not in the film yet would land if it started at 4.2 s",
|
|
977
|
-
"hyperframes audio sfx/hit.wav --place-at 4.2"
|
|
978
|
-
],
|
|
979
|
-
["Every sound playing at 12 s in the film", "hyperframes audio --at 12"],
|
|
980
|
-
["One clip of the film, as JSON", "hyperframes audio --element '#riser' --json"],
|
|
981
|
-
[
|
|
982
|
-
"The beat grid, tempo and sections of a music bed",
|
|
983
|
-
"hyperframes audio music.mp3 --only beats,downbeats,bpm,sections"
|
|
984
|
-
],
|
|
985
|
-
[
|
|
986
|
-
"Only onsets, from 60\u2013200 Hz, finer",
|
|
987
|
-
"hyperframes audio drums.wav --only onsets --band 60,200 --hop-ms 2"
|
|
988
|
-
]
|
|
989
|
-
];
|
|
990
|
-
var CAPABILITIES = /* @__PURE__ */ new Set([
|
|
991
|
-
...DEFAULT_CAPABILITIES,
|
|
992
|
-
"beats",
|
|
993
|
-
"downbeats",
|
|
994
|
-
"bpm",
|
|
995
|
-
"sections",
|
|
996
|
-
"describe"
|
|
997
|
-
]);
|
|
998
|
-
var QUALITIES = /* @__PURE__ */ new Set(["best", "local", "fast"]);
|
|
999
|
-
function number(name, raw) {
|
|
1000
|
-
if (raw === void 0) return void 0;
|
|
1001
|
-
const value = Number(raw);
|
|
1002
|
-
if (!Number.isFinite(value)) throw new CliUsageError(`--${name} must be a number, got ${raw}`);
|
|
1003
|
-
return value;
|
|
1004
|
-
}
|
|
1005
|
-
function pair(name, raw) {
|
|
1006
|
-
if (raw === void 0) return void 0;
|
|
1007
|
-
const parts = raw.split(",").map(Number);
|
|
1008
|
-
if (parts.length !== 2 || parts.some((n) => !Number.isFinite(n)) || parts[0] >= parts[1])
|
|
1009
|
-
throw new CliUsageError(`--${name} takes two increasing numbers, like 2.5,4`);
|
|
1010
|
-
return parts;
|
|
1011
|
-
}
|
|
1012
|
-
function capabilities(raw) {
|
|
1013
|
-
if (!raw) return [...DEFAULT_CAPABILITIES];
|
|
1014
|
-
const asked = raw.split(",").map((c) => c.trim()).filter(Boolean);
|
|
1015
|
-
if (asked.includes("words"))
|
|
1016
|
-
throw new CliUsageError("words come from hyperframes transcribe, not hyperframes audio");
|
|
1017
|
-
const unknown = asked.filter((c) => !CAPABILITIES.has(c));
|
|
1018
|
-
if (unknown.length)
|
|
1019
|
-
throw new CliUsageError(
|
|
1020
|
-
`--only does not know ${unknown.join(", ")}; use ${[...CAPABILITIES].join(", ")}`
|
|
1021
|
-
);
|
|
1022
|
-
return asked;
|
|
1023
|
-
}
|
|
1024
|
-
function pins(raw) {
|
|
1025
|
-
if (!raw) return void 0;
|
|
1026
|
-
return Object.fromEntries(
|
|
1027
|
-
raw.split(",").map((entry) => {
|
|
1028
|
-
const [capability, id] = entry.split("=").map((s2) => s2.trim());
|
|
1029
|
-
if (!capability || !id || !CAPABILITIES.has(capability))
|
|
1030
|
-
throw new CliUsageError(`--provider takes capability=provider pairs, like onsets=signal`);
|
|
1031
|
-
return [capability, id];
|
|
1032
|
-
})
|
|
1033
|
-
);
|
|
1034
|
-
}
|
|
1035
|
-
function filmQuery(args) {
|
|
1036
|
-
const given = ["element", "at", "range"].filter((k) => args[k] !== void 0);
|
|
1037
|
-
if (given.length > 1) throw new CliUsageError(`use one of --element, --at, --range`);
|
|
1038
|
-
if (args.element) return { by: "element", ref: args.element };
|
|
1039
|
-
if (args.at !== void 0) return { by: "at", t: number("at", args.at) };
|
|
1040
|
-
const range = pair("range", args.range);
|
|
1041
|
-
return range ? { by: "range", start: range[0], end: range[1] } : null;
|
|
1042
|
-
}
|
|
1043
|
-
function analyzeRequest(flags) {
|
|
1044
|
-
const quality = flags.quality ?? "best";
|
|
1045
|
-
if (!QUALITIES.has(quality)) throw new CliUsageError("--quality is best, local or fast");
|
|
1046
|
-
const method = flags["onset-method"];
|
|
1047
|
-
if (method && method !== "spectral-flux" && method !== "energy")
|
|
1048
|
-
throw new CliUsageError("--onset-method is spectral-flux or energy");
|
|
1049
|
-
const band = pair("band", flags.band);
|
|
1050
|
-
return {
|
|
1051
|
-
capabilities: capabilities(flags.only),
|
|
1052
|
-
quality,
|
|
1053
|
-
provider: pins(flags.provider),
|
|
1054
|
-
options: {
|
|
1055
|
-
onsets: { method, band, threshold: number("threshold", flags.threshold) },
|
|
1056
|
-
envelope: { band, hopMs: number("hop-ms", flags["hop-ms"]) },
|
|
1057
|
-
moments: {
|
|
1058
|
-
onsetRatio: number("onset-ratio", flags["onset-ratio"]),
|
|
1059
|
-
tailDb: number("tail-db", flags["tail-db"])
|
|
1060
|
-
},
|
|
1061
|
-
silence: { thresholdDb: number("silence-db", flags["silence-db"]) }
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
async function fileTargets(files, flags) {
|
|
1066
|
-
const start = number("place-at", flags["place-at"]);
|
|
1067
|
-
if (start !== void 0 && files.length !== 1)
|
|
1068
|
-
throw new CliUsageError("--place-at places exactly one file");
|
|
1069
|
-
const skip = number("skip-file-seconds", flags["skip-file-seconds"]);
|
|
1070
|
-
if (skip !== void 0 && start === void 0)
|
|
1071
|
-
throw new CliUsageError("--skip-file-seconds only works with --place-at");
|
|
1072
|
-
const mediaStart = skip ?? 0;
|
|
1073
|
-
const cwd = process.cwd();
|
|
1074
|
-
return Promise.all(
|
|
1075
|
-
files.map((file) => previewTarget(resolve2(cwd, file), file, start ?? null, mediaStart))
|
|
1076
|
-
);
|
|
1077
|
-
}
|
|
1078
|
-
async function clipTargets(flags, query) {
|
|
1079
|
-
const project = resolveProject(flags.dir);
|
|
1080
|
-
ensureDOMParser();
|
|
1081
|
-
const timeline = await describeProject(project.indexPath);
|
|
1082
|
-
return filmTargets(project.dir, timeline, query ?? { by: "all" });
|
|
1083
|
-
}
|
|
1084
|
-
var audio_default = defineCommand({
|
|
1085
|
-
meta: {
|
|
1086
|
-
name: "audio",
|
|
1087
|
-
description: "Measure how sound moves in time: where it lands, its hits, loudness and silence"
|
|
1088
|
-
},
|
|
1089
|
-
args: {
|
|
1090
|
-
files: { type: "positional", description: "Audio or video files to analyze", required: false },
|
|
1091
|
-
dir: { type: "string", description: "Project directory, for film queries (default: current)" },
|
|
1092
|
-
element: {
|
|
1093
|
-
type: "string",
|
|
1094
|
-
description: "One audio or video clip of the film: #id, hf:id or audio/N"
|
|
1095
|
-
},
|
|
1096
|
-
at: { type: "string", description: "Every sound playing at this film second" },
|
|
1097
|
-
range: { type: "string", description: "Every sound playing in start,end film seconds" },
|
|
1098
|
-
"place-at": {
|
|
1099
|
-
type: "string",
|
|
1100
|
-
description: "For one file not in the film yet: the film second it would start at"
|
|
1101
|
-
},
|
|
1102
|
-
"skip-file-seconds": {
|
|
1103
|
-
type: "string",
|
|
1104
|
-
description: "With --place-at: seconds cut from the start of the file"
|
|
1105
|
-
},
|
|
1106
|
-
only: {
|
|
1107
|
-
type: "string",
|
|
1108
|
-
description: `Capabilities to measure (default ${DEFAULT_CAPABILITIES.join(",")}; also beats,downbeats,bpm,sections,describe)`
|
|
1109
|
-
},
|
|
1110
|
-
quality: {
|
|
1111
|
-
type: "string",
|
|
1112
|
-
description: "best (any provider), local (this machine), fast (signal only)"
|
|
1113
|
-
},
|
|
1114
|
-
provider: { type: "string", description: "Pin capabilities to providers: onsets=signal,..." },
|
|
1115
|
-
"onset-method": { type: "string", description: "spectral-flux (default) or energy" },
|
|
1116
|
-
threshold: { type: "string", description: "Onset strength floor, 0\u20131 (default 0.1)" },
|
|
1117
|
-
band: { type: "string", description: "Measure onsets and envelope in lo,hi Hz only" },
|
|
1118
|
-
"hop-ms": {
|
|
1119
|
-
type: "string",
|
|
1120
|
-
description: "Envelope hop in ms (default: about 2,000 values per item)"
|
|
1121
|
-
},
|
|
1122
|
-
"onset-ratio": {
|
|
1123
|
-
type: "string",
|
|
1124
|
-
description: "Moments: onset is where RMS first reaches this share of the peak (0.2)"
|
|
1125
|
-
},
|
|
1126
|
-
"tail-db": {
|
|
1127
|
-
type: "string",
|
|
1128
|
-
description: "Moments: the tail ends this many dB under the peak (-40)"
|
|
1129
|
-
},
|
|
1130
|
-
"silence-db": { type: "string", description: "Silence below this level in dBFS (-50)" },
|
|
1131
|
-
full: {
|
|
1132
|
-
type: "boolean",
|
|
1133
|
-
description: "Include each capability's provider ladder",
|
|
1134
|
-
default: false
|
|
1135
|
-
},
|
|
1136
|
-
json: { type: "boolean", description: "Output as JSON", default: false }
|
|
1137
|
-
},
|
|
1138
|
-
setup({ rawArgs, cmd }) {
|
|
1139
|
-
assertKnownFlags({ args: cmd.args }, rawArgs);
|
|
1140
|
-
},
|
|
1141
|
-
async run({ args }) {
|
|
1142
|
-
const flags = args;
|
|
1143
|
-
const files = args._.map(String);
|
|
1144
|
-
const query = filmQuery(flags);
|
|
1145
|
-
if (files.length && query) throw new CliUsageError("give files or a film query, not both");
|
|
1146
|
-
const targets = files.length ? await fileTargets(files, flags) : await clipTargets(flags, query);
|
|
1147
|
-
const report = await analyzeTargets(targets, analyzeRequest(flags), Boolean(args.full));
|
|
1148
|
-
console.log(args.json ? JSON.stringify(withMeta(report)) : formatReport(report));
|
|
1149
|
-
}
|
|
1150
|
-
});
|
|
1151
|
-
export {
|
|
1152
|
-
audio_default as default,
|
|
1153
|
-
examples
|
|
1154
|
-
};
|