hyperframes 0.8.150 → 0.8.151
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-3ZIFEN2U.js → add-QKZZG7A2.js} +11 -11
- package/dist/{animatedGifPrep-77LPFLKJ.js → animatedGifPrep-YQDPYGOD.js} +6 -6
- package/dist/api.js +16 -16
- package/dist/{asset-EYSZEHM6.js → asset-3Q737Y4H.js} +2 -2
- package/dist/{assetCataloger-KX3HD7ZI.js → assetCataloger-BTHEBJJB.js} +2 -2
- package/dist/{auth-L6IWNRXY.js → auth-XL7EU3SO.js} +1 -1
- package/dist/{autoUpdate-2O3HZB65.js → autoUpdate-PJFLTQ3U.js} +2 -2
- package/dist/{backgroundChecks-BH2LMJYE.js → backgroundChecks-T5TIR5WK.js} +3 -3
- package/dist/backgroundChecksWorker.js +3 -3
- package/dist/{batchRender-ZBKAYLWU.js → batchRender-NFLVIDB4.js} +3 -3
- package/dist/{beats-IVLF2566.js → beats-JU4GZ26R.js} +12 -12
- package/dist/{benchmark-SYZFW2G7.js → benchmark-7U2YWLTW.js} +8 -8
- package/dist/{browser-OSW3L25S.js → browser-D57UPYDA.js} +5 -5
- package/dist/{bundleWithLocalizedFonts-I4CWOXLK.js → bundleWithLocalizedFonts-26MXQOZG.js} +2 -2
- package/dist/{capture-AZOQ57TM.js → capture-47BV25G3.js} +2 -2
- package/dist/{capture-V5ICGSVJ.js → capture-LT252COQ.js} +10 -10
- package/dist/{catalog-VBJOA2Y5.js → catalog-BEZXOV4O.js} +11 -11
- package/dist/{catch-up-TKSEDNJK.js → catch-up-KVV5X6BF.js} +4 -4
- package/dist/{check-3HFISKDB.js → check-6KQ433E6.js} +189 -53
- package/dist/{checkBrowser-MXBDNZJB.js → checkBrowser-PRDR6XBY.js} +170 -28
- package/dist/{chunk-AFRXL75S.js → chunk-35PWWBGZ.js} +1 -1
- package/dist/{chunk-WLIUULZQ.js → chunk-3SVC7SW4.js} +28 -14
- package/dist/{chunk-EL5QVJEG.js → chunk-3ZPFRDEJ.js} +1 -1
- package/dist/{chunk-UQYQ7TBA.js → chunk-4LE27ST4.js} +1 -1
- package/dist/{chunk-DUUFDLC4.js → chunk-4MLYMDQ4.js} +1 -1
- package/dist/{chunk-Z6WZDGCS.js → chunk-4PKQEKAI.js} +2 -2
- package/dist/{chunk-VQ7YIURZ.js → chunk-4YZYYP7V.js} +5 -5
- package/dist/{chunk-XPQSL5VS.js → chunk-54DMWFQZ.js} +8 -1
- package/dist/{chunk-UVVI2NJQ.js → chunk-BRF3O35X.js} +6 -6
- package/dist/chunk-DHPFXAUF.js +165 -0
- package/dist/{chunk-6SFPQ2RZ.js → chunk-DIOBKUBV.js} +2 -2
- package/dist/{chunk-XQMEGSHG.js → chunk-FAIYEIZ4.js} +1 -1
- package/dist/{chunk-LW5VFBSY.js → chunk-FLVI7FHU.js} +1 -1
- package/dist/{chunk-JDB65PPT.js → chunk-GNTVYF7E.js} +8 -7
- package/dist/{chunk-423DJVX2.js → chunk-HVXRYMGQ.js} +1 -1
- package/dist/{chunk-F2RLIE7P.js → chunk-IP73NMQT.js} +11 -11
- package/dist/{chunk-2KAL22B5.js → chunk-IPDS2SWI.js} +1 -1
- package/dist/{chunk-6KWA3XPS.js → chunk-IZ7L6V76.js} +1 -1
- package/dist/{chunk-5NAYNDBT.js → chunk-IZPN423R.js} +6 -2
- package/dist/{chunk-4WFNTWKZ.js → chunk-KQGWP4KH.js} +2 -2
- package/dist/{chunk-NFQY7EIC.js → chunk-KXX6NLOT.js} +1 -1
- package/dist/{chunk-S5F26DUL.js → chunk-L3N7RJHS.js} +3 -3
- package/dist/{chunk-73RBX2E7.js → chunk-L6RIMCMG.js} +3 -3
- package/dist/{chunk-7IEGPJ4P.js → chunk-LBT4AMSF.js} +1 -1
- package/dist/{chunk-6NPFJ255.js → chunk-LKQS72D5.js} +1 -1
- package/dist/{chunk-RW7NLGPD.js → chunk-N3I6RAOR.js} +22 -14
- package/dist/{chunk-M6UDJ2E7.js → chunk-N4IXP3MY.js} +2 -2
- package/dist/{chunk-7E4VHPKJ.js → chunk-O3DUX6P2.js} +5 -5
- package/dist/{chunk-O4DC5CHF.js → chunk-OSAH7S5M.js} +1 -1
- package/dist/{chunk-UJB7356Y.js → chunk-OZ4K46OV.js} +3 -3
- package/dist/{chunk-6QB7F2II.js → chunk-POEEDPJI.js} +1 -1
- package/dist/{chunk-DOVJRUFM.js → chunk-PUIELIFA.js} +1 -1
- package/dist/chunk-QBYHNRHU.js +3795 -0
- package/dist/{chunk-XANZ4R4P.js → chunk-QF7AE3DB.js} +3 -3
- package/dist/{chunk-A6ZO4BX7.js → chunk-QLGBNGXC.js} +2 -2
- package/dist/{chunk-FPAT5HTI.js → chunk-RJRGR5KF.js} +10 -10
- package/dist/{chunk-AXPNVC3N.js → chunk-RRBKEHST.js} +1 -1
- package/dist/{chunk-UAIUVPZE.js → chunk-RYL6ZIJP.js} +6 -6
- package/dist/{chunk-TZPZE5PS.js → chunk-S22FKF6G.js} +6 -3
- package/dist/{chunk-I6DATIHC.js → chunk-TCWMDCBX.js} +2 -2
- package/dist/{chunk-M5B42RL5.js → chunk-WUUOFMHU.js} +3 -3
- package/dist/{chunk-BLRJKBKP.js → chunk-XGLNQDLN.js} +1 -1
- package/dist/{chunk-FN6KAS7A.js → chunk-XKKBIFE5.js} +7 -7
- package/dist/{chunk-ZNHAJCEA.js → chunk-XNDXENJQ.js} +27 -12
- package/dist/{chunk-PCWOSS7G.js → chunk-XQKVEQNY.js} +2 -2
- package/dist/{chunk-R5A7454P.js → chunk-XYHBVSWD.js} +2 -2
- package/dist/{chunk-7JTFPUMA.js → chunk-YEBKEWKB.js} +1 -1
- package/dist/{chunk-CZQLCBWY.js → chunk-YG2Y55KF.js} +1 -1
- package/dist/{chunk-B3XLIBFL.js → chunk-YIJMVFEJ.js} +1 -1
- package/dist/{chunk-OQ7XMM3S.js → chunk-YZTVHRJ2.js} +1 -1
- package/dist/{chunk-3UAIVF4L.js → chunk-ZD3RDNZI.js} +1 -1
- package/dist/{chunk-AKYQGZLU.js → chunk-ZGCRVM5M.js} +2 -2
- package/dist/{clean-7HU7S7DT.js → clean-RLTHFTQA.js} +16 -16
- package/dist/cli.js +52 -52
- package/dist/{cloud-HAXHBO25.js → cloud-Q5TGBTW3.js} +4 -4
- package/dist/{cloudrun-A3PIJ6DX.js → cloudrun-4UZF74MV.js} +3 -3
- package/dist/{compare-6IPSQMSM.js → compare-Q5UNR2V4.js} +23 -23
- package/dist/{compiler-UQP2VXN6.js → compiler-ZXT27Z62.js} +3 -3
- package/dist/{component-FF3AU24M.js → component-H734P32V.js} +3 -3
- package/dist/{compositions-JSH4MZXB.js → compositions-YEHDSTDR.js} +10 -10
- package/dist/{delete-G763SW64.js → delete-LDBMPWRT.js} +2 -2
- package/dist/{dist-Z7Q3JS7K.js → dist-7JXLFD6U.js} +2 -2
- package/dist/{dist-HUMFCJ6T.js → dist-RGE4QASU.js} +1 -1
- package/dist/{doctor-WZB6WMBY.js → doctor-XNJFIWJ7.js} +12 -12
- package/dist/{events-MUWJJCC7.js → events-G6UFLNXR.js} +2 -2
- package/dist/{events-T4YFQC5G.js → events-UKCPUWMH.js} +3 -3
- package/dist/{feedback-AUTHQNN2.js → feedback-CK3KZ7DE.js} +6 -6
- package/dist/{ffmpeg-TZB5WAYE.js → ffmpeg-MJFMBBSF.js} +2 -2
- package/dist/{figma-NBG6WBI7.js → figma-3D5C737B.js} +3 -3
- package/dist/fontLocalizeCli.js +8 -8
- package/dist/{get-OXBJLKLV.js → get-QGTQUWFT.js} +2 -2
- package/dist/{gpuPolicy-HBCCEQUS.js → gpuPolicy-G5QAQDYF.js} +6 -6
- package/dist/{grade-compare-2LX4FDH7.js → grade-compare-XIHOM62P.js} +25 -25
- package/dist/{help-LJI46EDO.js → help-U5UBXT52.js} +1 -1
- package/dist/{history-MFKAQEHU.js → history-5J73QZSA.js} +5 -5
- package/dist/hyperframe-runtime.js +54 -54
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +54 -54
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/{info-D6Y5O2QE.js → info-RJHQQQUP.js} +7 -7
- package/dist/{init-47NZPEBI.js → init-M6UQOYYC.js} +28 -28
- package/dist/{inspect-U5UXJK5D.js → inspect-PRQWDBTP.js} +24 -24
- package/dist/{installer-AW7VFVTQ.js → installer-ZL7UKYK5.js} +4 -4
- package/dist/{keyframes-5G334NWB.js → keyframes-VMUJYTCQ.js} +6 -6
- package/dist/{lambda-GASGXN3A.js → lambda-ON42DRQD.js} +4 -4
- package/dist/{layout-OXAQFLJS.js → layout-QKBFVDVY.js} +24 -24
- package/dist/{lint-F36QFSNF.js → lint-ZQHHL2SV.js} +7 -7
- package/dist/{lintProject-SSEXWCH6.js → lintProject-3Z6OJCSO.js} +1 -1
- package/dist/{list-TR6QCI53.js → list-YBRKIWGF.js} +2 -2
- package/dist/{login-6LA2RWTC.js → login-4B2YXGLB.js} +4 -4
- package/dist/{manager-ES2UFT4X.js → manager-X44GKUEL.js} +3 -3
- package/dist/{manager-GWU5J6RJ.js → manager-ZLJD3EAY.js} +2 -2
- package/dist/{media-treatment-BOHYDWVY.js → media-treatment-5C5EJAAP.js} +11 -11
- package/dist/{models-6GW5Q3IW.js → models-R7ZDWBJ6.js} +12 -12
- package/dist/{motionShot-O7AOMPUN.js → motionShot-ZHESZNUL.js} +13 -13
- package/dist/{normalize-audio-UO5B4O76.js → normalize-audio-EVWZVTBM.js} +4 -4
- package/dist/{open-JV2EHVKM.js → open-TV6HT4UT.js} +4 -4
- package/dist/{parakeetRunner-23DZUF6G.js → parakeetRunner-YVVF4CG6.js} +13 -13
- package/dist/{pipeline-KTICSIK7.js → pipeline-ETR5SEBS.js} +8 -8
- package/dist/{play-CMUYPXBC.js → play-C6WONRZV.js} +17 -17
- package/dist/{preflight-JOD4OXGC.js → preflight-JYOX64YP.js} +4 -4
- package/dist/{present-ZDT5FM2H.js → present-RANXTVPA.js} +13 -13
- package/dist/{preview-ASHLGKDD.js → preview-KF6UMBT2.js} +18 -18
- package/dist/{producer-FFH2KUAZ.js → producer-CY2GXBWN.js} +1 -1
- package/dist/{projectConfig-FHIAOPKW.js → projectConfig-KPXCNMBM.js} +6 -6
- package/dist/{publication-WO5ZXURT.js → publication-Y36N4A7E.js} +2 -2
- package/dist/{publish-QWO2W7FB.js → publish-RCQJO35C.js} +8 -8
- package/dist/{remove-background-BO5MQR25.js → remove-background-3ZMXVWLB.js} +9 -9
- package/dist/{render-SDHFXL3Y.js → render-FKVRCDT2.js} +57 -34
- package/dist/{render-XTJKGT3G.js → render-PTNEFO5U.js} +8 -8
- package/dist/{render-HS3YOLSD.js → render-WUSTRMUD.js} +3 -3
- package/dist/{render-batch-J64AJHQY.js → render-batch-AQHU2JWZ.js} +3 -3
- package/dist/renderSetupWorker.js +4 -4
- package/dist/{resolver-IHNWMDJA.js → resolver-L5BDZJKU.js} +4 -4
- package/dist/{sherpa-FXBBYMDR.js → sherpa-3S3ISG7G.js} +12 -12
- package/dist/skills/hyperframes/references/frame-worker-core.md +11 -9
- package/dist/skills/hyperframes/references/subagent-dispatch.md +1 -1
- package/dist/skills/media-use/audio/scripts/lib/tts.cleanup.test.mjs +175 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.mjs +24 -19
- package/dist/skills/media-use/references/grading.md +1 -2
- package/dist/skills/media-use/references/meta.md +17 -17
- package/dist/skills/media-use/references/operations.md +6 -6
- package/dist/skills/media-use/references/setup-providers.md +3 -4
- package/dist/skills/media-use/scripts/lib/cutlist.mjs +9 -2
- package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +12 -0
- package/dist/skills/media-use/scripts/lib/telemetry.mjs +4 -1
- package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +20 -0
- package/dist/skills/media-use/scripts/transcript-cut.mjs +23 -4
- package/dist/skills/media-use/scripts/transcript-cut.test.mjs +224 -0
- package/dist/{skills-ALLSN7TB.js → skills-X4WQEKNF.js} +5 -5
- package/dist/{skillsUpdateCheck-EIT5NWM6.js → skillsUpdateCheck-UYCVHDZ6.js} +3 -3
- package/dist/{snapshot-Y3I76OIZ.js → snapshot-YEYHQEZQ.js} +250 -139
- package/dist/{src-7V6LBC56.js → src-2NN2WB6X.js} +7 -5
- package/dist/{src-S5IZVJUX.js → src-OLBDPEG6.js} +7 -7
- package/dist/{staticProjectServer-QLWWGHZE.js → staticProjectServer-7H35NMWB.js} +13 -13
- package/dist/studio/assets/{hyperframes-player-kup62bDR.js → hyperframes-player-DJt8E7k4.js} +1 -1
- package/dist/studio/assets/{index-CS_m0TkX.js → index-BGks9KLI.js} +73 -73
- package/dist/studio/assets/{index-B8BBYY8L.js → index-DWIHrAY2.js} +1 -1
- package/dist/studio/assets/{index-DQ5jGodF.js → index-DgbLblGR.js} +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/{studio-api-UBFLZ3DA.js → studio-api-HGM2A5I3.js} +7 -7
- package/dist/{studioServer-REOVCIFP.js → studioServer-M6XFL7N6.js} +43 -43
- package/dist/{telemetry-C3GDIXHO.js → telemetry-KVCZKVTE.js} +3 -3
- package/dist/{timeline-H42EGLMT.js → timeline-VYYKZR4V.js} +66 -13
- package/dist/{tokens-2K7ISTKV.js → tokens-B5NFFL55.js} +2 -2
- package/dist/{transcribe-NUPASAUP.js → transcribe-SBETOIWI.js} +11 -11
- package/dist/{transcribe-DVM7UFZZ.js → transcribe-WNFRYZAC.js} +17 -17
- package/dist/{updateCheck-3RWFSEZD.js → updateCheck-5MM2DFUC.js} +2 -2
- package/dist/{upgrade-YKKL36N7.js → upgrade-LWID54R3.js} +2 -2
- package/dist/{urlDownloader-6JNJYLPN.js → urlDownloader-CVLNXFDG.js} +5 -5
- package/dist/{validate-Z7KQT7GO.js → validate-V4CGQ6OF.js} +18 -18
- package/dist/{video-EXL2JDSL.js → video-VJB2KRSS.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-CEND6BLF.js +0 -3795
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
8
|
+
const ttsUrl = new URL("./tts.mjs", import.meta.url).href;
|
|
9
|
+
const stub = `#!${process.execPath}
|
|
10
|
+
const fs = require("node:fs");
|
|
11
|
+
const path = require("node:path");
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
if (args[0] === "--version") {
|
|
14
|
+
console.log("Python 3.12.0");
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
const input = args[2];
|
|
18
|
+
const output = args[0] === "-c" ? args[4] : args[args.indexOf("--output") + 1];
|
|
19
|
+
const text = fs.readFileSync(input, "utf8");
|
|
20
|
+
fs.writeFileSync(output + ".started", input);
|
|
21
|
+
const finish = () => {
|
|
22
|
+
if (text === "slow" && !fs.existsSync(process.env.RELEASE_FILE)) {
|
|
23
|
+
setTimeout(finish, 5);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const consumed = fs.readFileSync(input, "utf8");
|
|
27
|
+
fs.writeFileSync(output + ".json", JSON.stringify({ input, text, consumed, args }));
|
|
28
|
+
if (process.env.CASE_MODE === "nonzero") process.exit(7);
|
|
29
|
+
if (process.env.CASE_MODE !== "no-output") fs.writeFileSync(output, consumed);
|
|
30
|
+
};
|
|
31
|
+
setTimeout(finish, 10);
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const exercise = `
|
|
35
|
+
import assert from "node:assert/strict";
|
|
36
|
+
import fs from "node:fs";
|
|
37
|
+
import childProcess from "node:child_process";
|
|
38
|
+
import { syncBuiltinESMExports } from "node:module";
|
|
39
|
+
import { join, dirname } from "node:path";
|
|
40
|
+
const { CASE_ROOT: root, CASE_MODE: mode, CASE_PROVIDER: provider } = process.env;
|
|
41
|
+
const temp = join(root, "tmp");
|
|
42
|
+
const preexisting = join(temp, "hf-txt-preexisting");
|
|
43
|
+
fs.mkdirSync(preexisting);
|
|
44
|
+
fs.writeFileSync(join(preexisting, "marker"), "keep");
|
|
45
|
+
const injected = Object.assign(new Error("synthetic " + mode), { code: "EACCES" });
|
|
46
|
+
const originalMkdtemp = fs.mkdtempSync;
|
|
47
|
+
const originalWrite = fs.writeFileSync;
|
|
48
|
+
const originalRm = fs.rmSync;
|
|
49
|
+
if (mode === "allocation") fs.mkdtempSync = (prefix, ...args) => {
|
|
50
|
+
if (prefix.startsWith(join(temp, "hf-txt-"))) throw injected;
|
|
51
|
+
return originalMkdtemp(prefix, ...args);
|
|
52
|
+
};
|
|
53
|
+
if (mode === "write") fs.writeFileSync = (file, ...args) => {
|
|
54
|
+
if (String(file).endsWith("line.txt")) {
|
|
55
|
+
originalWrite(file, "partial");
|
|
56
|
+
throw injected;
|
|
57
|
+
}
|
|
58
|
+
return originalWrite(file, ...args);
|
|
59
|
+
};
|
|
60
|
+
if (mode === "spawn-throw") childProcess.spawn = () => { throw injected; };
|
|
61
|
+
if (mode === "cleanup") fs.rmSync = (dir, ...args) => {
|
|
62
|
+
if (String(dir).startsWith(join(temp, "hf-txt-"))) throw injected;
|
|
63
|
+
return originalRm(dir, ...args);
|
|
64
|
+
};
|
|
65
|
+
syncBuiltinESMExports();
|
|
66
|
+
const { synthesizeOne } = await import(${JSON.stringify(ttsUrl)});
|
|
67
|
+
const project = join(root, "project");
|
|
68
|
+
const wavAbs = join(project, "line.wav");
|
|
69
|
+
const request = (chosen = provider, text = "synthetic café", output = wavAbs) =>
|
|
70
|
+
synthesizeOne({ provider: chosen, text, voiceId: "synthetic-voice", lang: "es",
|
|
71
|
+
speed: 0.8, wavAbs: output, hyperframesDir: project });
|
|
72
|
+
const owned = () => fs.readdirSync(temp).filter(name => name !== "hf-txt-preexisting");
|
|
73
|
+
if (["allocation", "write", "spawn-throw"].includes(mode)) {
|
|
74
|
+
await assert.rejects(request(), error => error === injected);
|
|
75
|
+
assert.equal(fs.existsSync(wavAbs), false);
|
|
76
|
+
} else if (mode === "concurrent") {
|
|
77
|
+
const slowWav = join(project, "slow.wav");
|
|
78
|
+
const slow = request("elevenlabs", "slow", slowWav);
|
|
79
|
+
try {
|
|
80
|
+
const deadline = Date.now() + 5000;
|
|
81
|
+
while (!fs.existsSync(slowWav + ".started")) {
|
|
82
|
+
assert.ok(Date.now() < deadline, "slow subprocess must start");
|
|
83
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
84
|
+
}
|
|
85
|
+
const slowInput = fs.readFileSync(slowWav + ".started", "utf8");
|
|
86
|
+
assert.deepEqual(await request("kokoro"), { ok: true, words: null });
|
|
87
|
+
assert.deepEqual(owned(), [dirname(slowInput).split(/[\\\\/]/).pop()]);
|
|
88
|
+
assert.equal(fs.readFileSync(slowInput, "utf8"), "slow");
|
|
89
|
+
} finally {
|
|
90
|
+
fs.writeFileSync(process.env.RELEASE_FILE, "release");
|
|
91
|
+
await slow;
|
|
92
|
+
}
|
|
93
|
+
assert.equal(fs.readFileSync(slowWav, "utf8"), "slow");
|
|
94
|
+
} else {
|
|
95
|
+
const result = await request();
|
|
96
|
+
const label = provider === "elevenlabs" ? "elevenlabs (python)" : "kokoro (npx hyperframes tts)";
|
|
97
|
+
if (["nonzero", "enoent", "no-output"].includes(mode)) {
|
|
98
|
+
const error = mode === "no-output" ? label + " produced no wav file" :
|
|
99
|
+
label + " exited with status " + (mode === "enoent" ? -1 : 7);
|
|
100
|
+
assert.deepEqual(result, { ok: false, words: null, error });
|
|
101
|
+
assert.equal(fs.existsSync(wavAbs), false);
|
|
102
|
+
} else {
|
|
103
|
+
assert.deepEqual(result, { ok: true, words: null });
|
|
104
|
+
assert.equal(fs.readFileSync(wavAbs, "utf8"), "synthetic café");
|
|
105
|
+
}
|
|
106
|
+
if (mode !== "enoent") {
|
|
107
|
+
const log = JSON.parse(fs.readFileSync(wavAbs + ".json", "utf8"));
|
|
108
|
+
assert.equal(log.text, "synthetic café");
|
|
109
|
+
assert.equal(log.consumed, log.text);
|
|
110
|
+
if (provider === "kokoro") assert.deepEqual(log.args.slice(-4), ["--lang", "es", "--speed", "0.8"]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (mode === "cleanup") assert.equal(owned().length, 1);
|
|
114
|
+
else assert.deepEqual(owned(), [], "owned text directories must be removed");
|
|
115
|
+
assert.equal(fs.readFileSync(join(preexisting, "marker"), "utf8"), "keep");
|
|
116
|
+
`;
|
|
117
|
+
|
|
118
|
+
function runCase(provider, mode) {
|
|
119
|
+
const root = mkdtempSync(join(tmpdir(), "tts-cleanup-test-"));
|
|
120
|
+
try {
|
|
121
|
+
for (const dir of ["home", "tmp", "bin", "project"]) mkdirSync(join(root, dir));
|
|
122
|
+
if (mode !== "enoent") {
|
|
123
|
+
for (const name of ["npx", "python3"]) {
|
|
124
|
+
const executable = join(root, "bin", name);
|
|
125
|
+
writeFileSync(executable, stub);
|
|
126
|
+
chmodSync(executable, 0o755);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const result = spawnSync(process.execPath, ["--input-type=module", "-e", exercise], {
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
timeout: 15000,
|
|
132
|
+
env: {
|
|
133
|
+
HOME: join(root, "home"),
|
|
134
|
+
USERPROFILE: join(root, "home"),
|
|
135
|
+
TMPDIR: join(root, "tmp"),
|
|
136
|
+
TMP: join(root, "tmp"),
|
|
137
|
+
TEMP: join(root, "tmp"),
|
|
138
|
+
PATH: join(root, "bin"),
|
|
139
|
+
CASE_ROOT: root,
|
|
140
|
+
CASE_PROVIDER: provider,
|
|
141
|
+
CASE_MODE: mode,
|
|
142
|
+
RELEASE_FILE: join(root, "release"),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
assert.equal(result.error, undefined);
|
|
146
|
+
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(root, { recursive: true, force: true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const provider of ["elevenlabs", "kokoro"]) {
|
|
153
|
+
for (const mode of [
|
|
154
|
+
"success",
|
|
155
|
+
"nonzero",
|
|
156
|
+
"no-output",
|
|
157
|
+
"enoent",
|
|
158
|
+
"spawn-throw",
|
|
159
|
+
"allocation",
|
|
160
|
+
"write",
|
|
161
|
+
"cleanup",
|
|
162
|
+
]) {
|
|
163
|
+
test(
|
|
164
|
+
`synthesizeOne(${provider}) preserves ${mode} behavior and cleans only its text directory`,
|
|
165
|
+
{ skip: process.platform === "win32" },
|
|
166
|
+
() => runCase(provider, mode),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
test(
|
|
172
|
+
"concurrent providers retain each other's text until their subprocess finishes",
|
|
173
|
+
{ skip: process.platform === "win32" },
|
|
174
|
+
() => runCase("kokoro", "concurrent"),
|
|
175
|
+
);
|
|
@@ -234,7 +234,7 @@ import os, sys
|
|
|
234
234
|
from elevenlabs.client import ElevenLabs
|
|
235
235
|
from elevenlabs import save
|
|
236
236
|
client = ElevenLabs(api_key=os.environ["ELEVENLABS_API_KEY"])
|
|
237
|
-
text = open(sys.argv[1]).read()
|
|
237
|
+
text = open(sys.argv[1], encoding="utf-8").read()
|
|
238
238
|
audio = client.text_to_speech.convert(
|
|
239
239
|
text=text, voice_id=sys.argv[2],
|
|
240
240
|
model_id="eleven_multilingual_v2", output_format="mp3_44100_128",
|
|
@@ -274,23 +274,21 @@ export async function synthesizeOne({
|
|
|
274
274
|
} catch {
|
|
275
275
|
return { ok: false, words: null };
|
|
276
276
|
}
|
|
277
|
-
|
|
278
|
-
"-c",
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
wavAbs,
|
|
283
|
-
]);
|
|
284
|
-
const r = await spawnP(cmd, args, {});
|
|
285
|
-
return synthResult(r, wavAbs, "elevenlabs (python)");
|
|
277
|
+
return withTmpText(text, async (textPath) => {
|
|
278
|
+
const { cmd, args } = pythonInvocation(["-c", ELEVENLABS_PY, textPath, voiceId, wavAbs]);
|
|
279
|
+
const r = await spawnP(cmd, args, {});
|
|
280
|
+
return synthResult(r, wavAbs, "elevenlabs (python)");
|
|
281
|
+
});
|
|
286
282
|
}
|
|
287
283
|
// kokoro — via the published CLI; --output is relative to the project dir.
|
|
288
284
|
const wavRel = relTo(hyperframesDir, wavAbs);
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
285
|
+
return withTmpText(text, async (textPath) => {
|
|
286
|
+
const args = ["hyperframes", "tts", textPath, "--voice", voiceId, "--output", wavRel];
|
|
287
|
+
if (lang !== "en") args.push("--lang", lang);
|
|
288
|
+
if (speed !== 1) args.push("--speed", String(speed));
|
|
289
|
+
const r = await spawnP("npx", args, { cwd: hyperframesDir });
|
|
290
|
+
return synthResult(r, wavAbs, "kokoro (npx hyperframes tts)");
|
|
291
|
+
});
|
|
294
292
|
}
|
|
295
293
|
|
|
296
294
|
// Shape a spawn result into { ok, words, error }, naming why on failure so the
|
|
@@ -378,11 +376,18 @@ export async function transcribeWav({ wavRel, lang = "en", hyperframesDir }) {
|
|
|
378
376
|
}
|
|
379
377
|
|
|
380
378
|
// ── tiny local utils ──────────────────────────────────────────────────────────
|
|
381
|
-
function
|
|
379
|
+
async function withTmpText(text, run) {
|
|
382
380
|
const td = mkdtempSync(join(tmpdir(), "hf-txt-"));
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
381
|
+
try {
|
|
382
|
+
const p = join(td, "line.txt");
|
|
383
|
+
writeFileSync(p, text);
|
|
384
|
+
return await run(p);
|
|
385
|
+
} finally {
|
|
386
|
+
// Cleanup must preserve the synthesis result or the original write/spawn error.
|
|
387
|
+
try {
|
|
388
|
+
rmSync(td, { recursive: true, force: true });
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
386
391
|
}
|
|
387
392
|
function relTo(base, abs) {
|
|
388
393
|
return abs.startsWith(base + "/") ? abs.slice(base.length + 1) : abs;
|
|
@@ -17,7 +17,7 @@ Stop here and use that treatment workflow for requests such as retro, old home
|
|
|
17
17
|
video, camcorder, film, print, ASCII, glitch, privacy, or a media reveal. Do not
|
|
18
18
|
assemble those from a generic LUT plus handmade CSS vignette/grain/opacity.
|
|
19
19
|
|
|
20
|
-
**Never `cat`/read a `.cube` file into context.** A 3D LUT is ~size^3 lines of raw numbers (33^3 ≈ 36k lines at the default size). It bloats context and carries zero human/agent-legible signal. To understand or choose a LUT, use `hyperframes grade-compare` to see it rendered
|
|
20
|
+
**Never `cat`/read a `.cube` file into context.** A 3D LUT is ~size^3 lines of raw numbers (33^3 ≈ 36k lines at the default size). It bloats context and carries zero human/agent-legible signal. To understand or choose a LUT, use `hyperframes grade-compare` to see it rendered. `npx hyperframes media-use resolve --type lut --intent "<look description>" --project .` validates generated or downloaded LUTs before freezing them; the validator is bundled in the CLI, not shipped as a standalone skill script. Read `<project>/.media/index.md` or `luts/index.json` for the description. Never read the LUT body itself.
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npx hyperframes media-use resolve --type grade --intent "warm daylight" --project . --json
|
|
@@ -158,5 +158,4 @@ validates generated or downloaded cubes as it freezes them under
|
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
160
|
npx hyperframes media-use resolve --type lut --intent "teal orange blockbuster" --project . --json
|
|
161
|
-
node skills/media-use/scripts/lib/cube-validate.mjs .media/luts/lut_001.cube
|
|
162
161
|
```
|
|
@@ -4,22 +4,22 @@ Maintainer-facing reference. Nothing here changes how you resolve or operate on
|
|
|
4
4
|
|
|
5
5
|
## What it owns (the gaps HyperFrames leaves)
|
|
6
6
|
|
|
7
|
-
HyperFrames owns media _playback_; media-use owns everything else. Each row is enforced by `
|
|
8
|
-
|
|
9
|
-
| HyperFrames gap | media-use owns it via
|
|
10
|
-
| ------------------------------------------ |
|
|
11
|
-
| Audio-only, no image/icon | `resolve --type image\|icon` (heygen asset search)
|
|
12
|
-
| No third-party brand logos | `resolve --type logo` (theSVG → GitHub org avatar → domain favicon)
|
|
13
|
-
| No voice / audio generation | `resolve --type voice` (HeyGen TTS free-usage path; optional local Kokoro) + the audio engine (`audio/scripts/audio.mjs`)
|
|
14
|
-
| Scattered/duplicated audio engine | one consolidated engine under `audio/` (hyperframes-media retired)
|
|
15
|
-
| No agent media-ops (cut/reframe/transform) | `references/operations.md` + `resolve --from` to register outputs
|
|
16
|
-
| No transcript-driven cutting | `scripts/transcript-cut.mjs` compiles word-timestamp edits into cut lists
|
|
17
|
-
| No auto-duck / publish loudness | `scripts/audio-duck.mjs` + `references/operations.md` loudnorm/sidechain recipes
|
|
18
|
-
| No cross-project memory | global content-addressed cache + auto-promote (`~/.media`)
|
|
19
|
-
| Grade recipes and LUT freezing | `resolve --type grade` emits a paste-ready recipe and `resolve --type lut` freezes validated `.cube` files; direct element analysis/authoring lives in `hyperframes media-treatment`
|
|
20
|
-
| No image generation | RAM-graded local mflux (FLUX)
|
|
21
|
-
| No video generation | `resolve --type video` — HeyGen avatar video first (free-usage path, sign-in nudge on auth failure), local LTX fallback (`videogen`
|
|
22
|
-
| Weak local-model defaults | HeyGen free-usage path via the `heygen` CLI; local open-source tools only as opt-in alternatives
|
|
7
|
+
HyperFrames owns media _playback_; media-use owns everything else. Each row is enforced by `packages/cli/src/media-use/lib/coverage.test.mjs` so the claim can't rot.
|
|
8
|
+
|
|
9
|
+
| HyperFrames gap | media-use owns it via |
|
|
10
|
+
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
11
|
+
| Audio-only, no image/icon | `resolve --type image\|icon` (heygen asset search) |
|
|
12
|
+
| No third-party brand logos | `resolve --type logo` (theSVG → GitHub org avatar → domain favicon) |
|
|
13
|
+
| No voice / audio generation | `resolve --type voice` (HeyGen TTS free-usage path; optional local Kokoro) + the audio engine (`audio/scripts/audio.mjs`) |
|
|
14
|
+
| Scattered/duplicated audio engine | one consolidated engine under `audio/` (hyperframes-media retired) |
|
|
15
|
+
| No agent media-ops (cut/reframe/transform) | `references/operations.md` + `resolve --from` to register outputs |
|
|
16
|
+
| No transcript-driven cutting | `scripts/transcript-cut.mjs` compiles word-timestamp edits into cut lists |
|
|
17
|
+
| No auto-duck / publish loudness | `scripts/audio-duck.mjs` + `references/operations.md` loudnorm/sidechain recipes |
|
|
18
|
+
| No cross-project memory | global content-addressed cache + auto-promote (`~/.media`) |
|
|
19
|
+
| Grade recipes and LUT freezing | `resolve --type grade` emits a paste-ready recipe and `resolve --type lut` freezes validated `.cube` files; direct element analysis/authoring lives in `hyperframes media-treatment` |
|
|
20
|
+
| No image generation | `resolve --type image` — RAM-graded local mflux (FLUX), codex `image_gen` upsell |
|
|
21
|
+
| No video generation | `resolve --type video` — HeyGen avatar video first (free-usage path, sign-in nudge on auth failure), local LTX fallback (`videogen` ladder); image-to-video, photo-avatar, dub/translate remain manual `heygen` CLI recipes (`references/operations.md`) |
|
|
22
|
+
| Weak local-model defaults | HeyGen free-usage path via the `heygen` CLI; local open-source tools only as opt-in alternatives |
|
|
23
23
|
|
|
24
24
|
## Usage stats
|
|
25
25
|
|
|
@@ -47,7 +47,7 @@ dev). Same public PostHog project key and opt-outs as the `hyperframes` CLI.
|
|
|
47
47
|
|
|
48
48
|
HeyGen request tagging: every generating `heygen` call (TTS, avatar video, catalog
|
|
49
49
|
search) carries the allowlisted `X-HeyGen-Client-Source: media-use` header, sourced
|
|
50
|
-
from one shared constant (`HEYGEN_CLIENT_SOURCE_ARGV` in `
|
|
50
|
+
from one shared constant (`HEYGEN_CLIENT_SOURCE_ARGV` in `packages/cli/src/media-use/lib/heygen-cli.mjs`)
|
|
51
51
|
so a future call site can't silently ship untagged. Read-only discovery calls
|
|
52
52
|
(`voice list`, `avatar list`) are intentionally left untagged.
|
|
53
53
|
|
|
@@ -210,9 +210,9 @@ ffmpeg -i mix.wav \
|
|
|
210
210
|
GENERATES. Two paths, best-for-the-machine picked automatically:
|
|
211
211
|
|
|
212
212
|
1. **Local (default, free, private): mflux** (FLUX-on-MLX). `resolve` spec-checks
|
|
213
|
-
AVAILABLE RAM and runs the best FLUX-class model that fits,
|
|
214
|
-
|
|
215
|
-
The RAM ladder
|
|
213
|
+
AVAILABLE RAM and runs the best FLUX-class model that fits, using the
|
|
214
|
+
CLI-bundled `imagegen` ladder (its modules are not shipped in the standalone
|
|
215
|
+
skill). The RAM ladder:
|
|
216
216
|
|
|
217
217
|
| Tier | Model | Needs (available RAM) | Notes |
|
|
218
218
|
| ------ | -------------------- | --------------------- | ----------------------------------- |
|
|
@@ -245,7 +245,7 @@ clip), so treat the fallback as "HeyGen wasn't reachable," not "upgrade the
|
|
|
245
245
|
quality":
|
|
246
246
|
|
|
247
247
|
- **HeyGen avatar video (default, free for new API users):**
|
|
248
|
-
|
|
248
|
+
The CLI-bundled provider shells the
|
|
249
249
|
`heygen` CLI — never the raw API — auto-picking a public avatar and a
|
|
250
250
|
starfish voice (override with `--avatar-id`/`--voice-id`, threaded through
|
|
251
251
|
as `ctx.avatarId`/`ctx.voiceId`). If the CLI reports `not_authenticated`,
|
|
@@ -253,13 +253,13 @@ quality":
|
|
|
253
253
|
new API users — sign in) to stderr and falls through to LTX instead of
|
|
254
254
|
hard-failing.
|
|
255
255
|
- **Local fallback: LTX 2.3 on MLX** via `dgrauet/ltx-2-mlx`, the `videogen`
|
|
256
|
-
ladder in
|
|
256
|
+
ladder bundled in the HyperFrames CLI. Generative clips
|
|
257
257
|
(t2v), spec-gated to RAM. Verified on 24GB: 512x320 x 33f with audio.
|
|
258
258
|
|
|
259
259
|
Every generating `heygen` call from media-use — TTS, avatar video, and
|
|
260
260
|
catalog search — sends the allowlisted `X-HeyGen-Client-Source: media-use`
|
|
261
261
|
header (persistent flag, works on every subcommand) via the shared
|
|
262
|
-
`HEYGEN_CLIENT_SOURCE_ARGV` constant
|
|
262
|
+
CLI-internal `HEYGEN_CLIENT_SOURCE_ARGV` constant, so usage
|
|
263
263
|
tags correctly in billing/resource meta and shows up in the API dashboards.
|
|
264
264
|
Read-only discovery (`avatar list`, `voice list`) doesn't need it.
|
|
265
265
|
|
|
@@ -36,8 +36,8 @@ before resolving bgm/sfx/image/icon/voice/avatar-video; music and sound effects
|
|
|
36
36
|
a host app's own tools need none of it. Local tools are opt-in
|
|
37
37
|
alternatives where they exist: mflux for image, Kokoro for voice, Parakeet for
|
|
38
38
|
transcription, and LTX for local video generation. `resolve` spec-checks
|
|
39
|
-
AVAILABLE RAM for those local ladders
|
|
40
|
-
|
|
39
|
+
AVAILABLE RAM for those local ladders inside the CLI (the image tiers are listed
|
|
40
|
+
in `references/operations.md`).
|
|
41
41
|
|
|
42
42
|
| Type | Provider / path |
|
|
43
43
|
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -84,8 +84,7 @@ private, on-device path instead of or ahead of HeyGen for that type. Only
|
|
|
84
84
|
| `npx hyperframes` | Kokoro TTS (voice), Parakeet or whisper.cpp (transcribe), remove-background | via the hyperframes CLI; Parakeet on macOS, Windows and glibc 2.32+ Linux: `npx hyperframes models install parakeet`; whisper.cpp is built on first use (Homebrew on macOS, else git+cmake) |
|
|
85
85
|
|
|
86
86
|
The RAM-graded local-model shortlist + exact per-tier install/invoke lives in
|
|
87
|
-
|
|
88
|
-
to see which model fits this machine). Without a tool on PATH, its provider
|
|
87
|
+
the CLI's bundled media-use engine, not in the standalone skill. Without a tool on PATH, its provider
|
|
89
88
|
prints a one-line diagnostic to stderr and resolve falls through where another
|
|
90
89
|
provider exists (e.g. no `mflux` -> codex image upsell; no `parakeet-mlx` -> whisper.cpp).
|
|
91
90
|
|
|
@@ -148,7 +148,7 @@ function clampRanges(ranges, duration) {
|
|
|
148
148
|
|
|
149
149
|
function mergeRanges(ranges) {
|
|
150
150
|
const sorted = ranges
|
|
151
|
-
.map((range) => ({ start:
|
|
151
|
+
.map((range) => ({ start: round6(range.start), end: round6(range.end) }))
|
|
152
152
|
.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
153
153
|
const merged = [];
|
|
154
154
|
for (const range of sorted) {
|
|
@@ -173,12 +173,19 @@ function invertRanges(removals, duration) {
|
|
|
173
173
|
return kept;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
// Range bounds are rounded only to shed float noise, never to the millisecond:
|
|
177
|
+
// these values become ffmpeg's -ss/-to, where a millisecond is wide enough to
|
|
178
|
+
// step a boundary onto the next frame at 29.97/30/60 fps.
|
|
176
179
|
function finalizeKept(ranges) {
|
|
177
180
|
return mergeRanges(ranges)
|
|
178
|
-
.map((range) => ({ start:
|
|
181
|
+
.map((range) => ({ start: round6(range.start), end: round6(range.end) }))
|
|
179
182
|
.filter((range) => round3(range.end - range.start) >= MIN_SEGMENT_SECONDS);
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
function round6(n) {
|
|
186
|
+
return Math.round(Number(n) * 1e6) / 1e6;
|
|
187
|
+
}
|
|
188
|
+
|
|
182
189
|
function round3(n) {
|
|
183
190
|
return Math.round(Number(n) * 1000) / 1000;
|
|
184
191
|
}
|
|
@@ -113,6 +113,18 @@ test("--keep is inverse mode and coalesces direct kept ranges", () => {
|
|
|
113
113
|
]);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
test("kept ranges keep frame precision instead of rounding to the millisecond", () => {
|
|
117
|
+
const transcript = [word("w0", "start", 0, 1), word("w1", "end", 14, 15)];
|
|
118
|
+
|
|
119
|
+
// Frame 386 at 30 fps is 12.866666 s. These bounds become ffmpeg's -ss/-to, so
|
|
120
|
+
// rounding them to 12.867 hands ffmpeg a time just past the frame and the
|
|
121
|
+
// segment starts one frame late.
|
|
122
|
+
assert.deepEqual(compileCutList(transcript, { keep: "0-1,12.866666666666667-14" }), [
|
|
123
|
+
{ start: 0, end: 1 },
|
|
124
|
+
{ start: 12.866667, end: 14 },
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
116
128
|
test("--plan on a fixture transcript prints the exact segment JSON", () => {
|
|
117
129
|
const dir = mkdtempSync(join(tmpdir(), "media-use-cutlist-"));
|
|
118
130
|
try {
|
|
@@ -35,9 +35,12 @@ function posthogHost() {
|
|
|
35
35
|
return override || POSTHOG_HOST;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/** True when telemetry must NOT be sent (opt-out envs, CI, dev). */
|
|
38
|
+
/** True when telemetry must NOT be sent (opt-out envs, CI, dev, shared config). */
|
|
39
39
|
export function optedOut() {
|
|
40
40
|
return (
|
|
41
|
+
// `hyperframes telemetry disable` writes telemetryEnabled: false to the
|
|
42
|
+
// shared config; one opt-out must cover the CLI and the media-use scripts.
|
|
43
|
+
readSharedConfig().telemetryEnabled === false ||
|
|
41
44
|
process.env.HYPERFRAMES_NO_TELEMETRY === "1" ||
|
|
42
45
|
process.env.DO_NOT_TRACK === "1" ||
|
|
43
46
|
process.env.CI === "true" ||
|
|
@@ -57,6 +57,26 @@ test("optedOut respects DO_NOT_TRACK / HYPERFRAMES_NO_TELEMETRY / CI", () => {
|
|
|
57
57
|
}
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test("optedOut honors telemetryEnabled: false from the shared config", () => {
|
|
61
|
+
const savedEnv = { ...process.env };
|
|
62
|
+
const { root, home } = sandbox();
|
|
63
|
+
try {
|
|
64
|
+
withoutTelemetryOptOut();
|
|
65
|
+
const configPath = join(home, ".hyperframes", "config.json");
|
|
66
|
+
assert.equal(optedOut(), false, "no config: tracking allowed");
|
|
67
|
+
mkdirSync(join(home, ".hyperframes"), { recursive: true });
|
|
68
|
+
writeFileSync(configPath, JSON.stringify({ telemetryEnabled: false }));
|
|
69
|
+
assert.equal(optedOut(), true, "telemetryEnabled: false opts out");
|
|
70
|
+
writeFileSync(configPath, JSON.stringify({ telemetryEnabled: true }));
|
|
71
|
+
assert.equal(optedOut(), false, "telemetryEnabled: true allows tracking");
|
|
72
|
+
writeFileSync(configPath, "{not json");
|
|
73
|
+
assert.equal(optedOut(), false, "unreadable config: tracking allowed");
|
|
74
|
+
} finally {
|
|
75
|
+
restoreEnv(savedEnv);
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
60
80
|
test("track is a no-op (no network, resolves) when opted out", async () => {
|
|
61
81
|
const savedEnv = { ...process.env };
|
|
62
82
|
const originalFetch = globalThis.fetch;
|
|
@@ -98,7 +98,10 @@ function run() {
|
|
|
98
98
|
// with priming silence (~25-35ms for AAC), which concat then bakes in as a
|
|
99
99
|
// gap at each cut -- a defect distinct from, and surviving, the fades
|
|
100
100
|
// below. PCM has no priming, so audio is encoded exactly once, at concat.
|
|
101
|
-
|
|
101
|
+
// The video container is .mov, not .mkv: ffmpeg writes Matroska
|
|
102
|
+
// timestamps in whole milliseconds, which moves frames off the source's
|
|
103
|
+
// frame grid, and the concat copy below carries that into the output.
|
|
104
|
+
const ext = args.copy ? extname(outPath) || ".mp4" : isAudioOnly(outPath) ? ".wav" : ".mov";
|
|
102
105
|
const out = join(tmpDir, `segment-${String(index).padStart(4, "0")}${ext}`);
|
|
103
106
|
// --copy stays fade-free (stream copy cannot filter). A segment's true
|
|
104
107
|
// start/end (index 0's start, the last segment's end) borders nothing
|
|
@@ -183,20 +186,27 @@ function cutSegment(inputPath, segment, outPath, copy, fade) {
|
|
|
183
186
|
"-y",
|
|
184
187
|
"-nostdin",
|
|
185
188
|
"-ss",
|
|
186
|
-
|
|
189
|
+
seekTime(segment.start),
|
|
187
190
|
"-i",
|
|
188
191
|
inputPath,
|
|
189
192
|
"-to",
|
|
190
|
-
|
|
193
|
+
seekTime(segment.end - segment.start),
|
|
191
194
|
];
|
|
192
195
|
if (copy) {
|
|
193
196
|
argv.push("-c", "copy", "-avoid_negative_ts", "make_zero");
|
|
194
|
-
} else if (extname(outPath).toLowerCase() === ".
|
|
197
|
+
} else if (extname(outPath).toLowerCase() === ".mov") {
|
|
195
198
|
// Concat splices raw segment edges together; without a short ramp the
|
|
196
199
|
// waveform steps discontinuously at every boundary and you hear a click.
|
|
197
200
|
if (fade) argv.push("-af", fade);
|
|
198
201
|
// Video intermediate: keep the picture cheap and the audio uncompressed.
|
|
199
202
|
argv.push("-c:v", "libx264", "-preset", "veryfast", "-crf", "18", "-c:a", "pcm_s16le");
|
|
203
|
+
// A keyframe every second. The renderer seeks into this footage, and x264
|
|
204
|
+
// on its own leaves up to 250 frames between keyframes.
|
|
205
|
+
argv.push("-force_key_frames", "expr:gte(t,n_forced)");
|
|
206
|
+
// Constant frame rate from the first frame. Left to itself, ffmpeg 7 starts
|
|
207
|
+
// a range that begins between two source frames one frame late, and .mov
|
|
208
|
+
// rounds that lead-in to the millisecond.
|
|
209
|
+
argv.push("-fps_mode", "cfr");
|
|
200
210
|
} else {
|
|
201
211
|
if (fade) argv.push("-af", fade);
|
|
202
212
|
argv.push("-c:a", "pcm_s16le");
|
|
@@ -265,6 +275,15 @@ function fmt(n) {
|
|
|
265
275
|
.replace(/\.?0+$/, "");
|
|
266
276
|
}
|
|
267
277
|
|
|
278
|
+
// -ss/-to name frames, and ffmpeg's accurate seek drops everything before the
|
|
279
|
+
// time it is given: at 30 fps, frame 386 sits at 12.866666 s, and a 3-decimal
|
|
280
|
+
// -ss of 12.867 starts that segment on frame 387 instead, leaving the cut a
|
|
281
|
+
// frame short from the join on. So the values ffmpeg gets keep frame precision,
|
|
282
|
+
// while fmt() above stays at 3 decimals for the times that are only printed.
|
|
283
|
+
function seekTime(n) {
|
|
284
|
+
return Number(n).toFixed(6);
|
|
285
|
+
}
|
|
286
|
+
|
|
268
287
|
function round3(n) {
|
|
269
288
|
return Math.round(Number(n) * 1000) / 1000;
|
|
270
289
|
}
|