hyperframes 0.8.57 → 0.8.59
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/cli.js +5300 -3428
- package/dist/docs/data-attributes.md +2 -1
- package/dist/fontLocalizeCli.js +73 -12
- package/dist/hyperframe-runtime.js +25 -25
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +25 -25
- package/dist/hyperframes-player.global.js +3 -3
- package/dist/registry/localSemantic.js +548 -0
- package/dist/skills/hyperframes-cli/SKILL.md +2 -2
- package/dist/skills/media-use/SKILL.md +96 -0
- package/dist/skills/media-use/audio/assets/sfx/CREDITS.md +35 -0
- package/dist/skills/media-use/audio/assets/sfx/chime.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/click-soft.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/click.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/error.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-1.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-2.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/glitch-3.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/impact-bass-1.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/impact-bass-2.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/key-press.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/manifest.json +97 -0
- package/dist/skills/media-use/audio/assets/sfx/notification.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/ping.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/pop.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/riser.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/sparkle.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/typing.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh-cinematic.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh-short.mp3 +0 -0
- package/dist/skills/media-use/audio/assets/sfx/whoosh.mp3 +0 -0
- package/dist/skills/media-use/audio/references/bgm.md +74 -0
- package/dist/skills/media-use/audio/references/captions/authoring.md +163 -0
- package/dist/skills/media-use/audio/references/captions/motion.md +87 -0
- package/dist/skills/media-use/audio/references/captions/transcript-handling.md +97 -0
- package/dist/skills/media-use/audio/references/remove-background.md +143 -0
- package/dist/skills/media-use/audio/references/requirements.md +29 -0
- package/dist/skills/media-use/audio/references/sfx.md +42 -0
- package/dist/skills/media-use/audio/references/transcribe.md +52 -0
- package/dist/skills/media-use/audio/references/tts-to-captions.md +26 -0
- package/dist/skills/media-use/audio/references/tts.md +165 -0
- package/dist/skills/media-use/audio/scripts/audio.mjs +295 -0
- package/dist/skills/media-use/audio/scripts/audio.test.mjs +49 -0
- package/dist/skills/media-use/audio/scripts/heygen-tts.mjs +121 -0
- package/dist/skills/media-use/audio/scripts/lib/audio-meta.mjs +39 -0
- package/dist/skills/media-use/audio/scripts/lib/audio-meta.test.mjs +136 -0
- package/dist/skills/media-use/audio/scripts/lib/bgm.mjs +257 -0
- package/dist/skills/media-use/audio/scripts/lib/bgm.test.mjs +30 -0
- package/dist/skills/media-use/audio/scripts/lib/concurrency.mjs +14 -0
- package/dist/skills/media-use/audio/scripts/lib/concurrency.test.mjs +41 -0
- package/dist/skills/media-use/audio/scripts/lib/heygen.mjs +156 -0
- package/dist/skills/media-use/audio/scripts/lib/heygen.test.mjs +103 -0
- package/dist/skills/media-use/audio/scripts/lib/python.mjs +63 -0
- package/dist/skills/media-use/audio/scripts/lib/python.test.mjs +68 -0
- package/dist/skills/media-use/audio/scripts/lib/sfx.mjs +143 -0
- package/dist/skills/media-use/audio/scripts/lib/sfx.test.mjs +69 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.mjs +379 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.spawn.test.mjs +158 -0
- package/dist/skills/media-use/audio/scripts/lib/tts.test.mjs +157 -0
- package/dist/skills/media-use/audio/scripts/lyria-recipe.py +136 -0
- package/dist/skills/media-use/audio/scripts/wait-bgm.mjs +171 -0
- package/dist/skills/media-use/audio/scripts/wait-bgm.test.mjs +65 -0
- package/dist/skills/media-use/luts/README.md +28 -0
- package/dist/skills/media-use/luts/index.json +57 -0
- package/dist/skills/media-use/references/audio.md +19 -0
- package/dist/skills/media-use/references/grading.md +160 -0
- package/dist/skills/media-use/references/media-treatment-recipes.md +929 -0
- package/dist/skills/media-use/references/media-treatments.md +202 -0
- package/dist/skills/media-use/references/memory.md +36 -0
- package/dist/skills/media-use/references/meta.md +61 -0
- package/dist/skills/media-use/references/operations.md +325 -0
- package/dist/skills/media-use/references/resolve.md +150 -0
- package/dist/skills/media-use/references/setup-providers.md +80 -0
- package/dist/skills/media-use/references/telemetry-dashboard.md +49 -0
- package/dist/skills/media-use/registry/localSemantic.js +548 -0
- package/dist/skills/media-use/scripts/audio-duck.mjs +113 -0
- package/dist/skills/media-use/scripts/compatibility.test.mjs +27 -0
- package/dist/skills/media-use/scripts/dither.mjs +328 -0
- package/dist/skills/media-use/scripts/dither.test.mjs +176 -0
- package/dist/skills/media-use/scripts/eval.mjs +369 -0
- package/dist/skills/media-use/scripts/lib/adopt.mjs +128 -0
- package/dist/skills/media-use/scripts/lib/adopt.test.mjs +95 -0
- package/dist/skills/media-use/scripts/lib/bgm-provider.mjs +20 -0
- package/dist/skills/media-use/scripts/lib/brand-provider.mjs +59 -0
- package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.mjs +119 -0
- package/dist/skills/media-use/scripts/lib/bundled-sfx-provider.test.mjs +139 -0
- package/dist/skills/media-use/scripts/lib/cache.mjs +150 -0
- package/dist/skills/media-use/scripts/lib/candidates.mjs +91 -0
- package/dist/skills/media-use/scripts/lib/candidates.test.mjs +155 -0
- package/dist/skills/media-use/scripts/lib/codex-provider.mjs +161 -0
- package/dist/skills/media-use/scripts/lib/codex-provider.test.mjs +34 -0
- package/dist/skills/media-use/scripts/lib/coverage.test.mjs +132 -0
- package/dist/skills/media-use/scripts/lib/cube-build.mjs +181 -0
- package/dist/skills/media-use/scripts/lib/cube-build.test.mjs +80 -0
- package/dist/skills/media-use/scripts/lib/cube-validate.mjs +198 -0
- package/dist/skills/media-use/scripts/lib/cube-validate.test.mjs +125 -0
- package/dist/skills/media-use/scripts/lib/cutlist.mjs +184 -0
- package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +162 -0
- package/dist/skills/media-use/scripts/lib/duck.mjs +95 -0
- package/dist/skills/media-use/scripts/lib/duck.test.mjs +153 -0
- package/dist/skills/media-use/scripts/lib/error-diffusion.mjs +230 -0
- package/dist/skills/media-use/scripts/lib/error-diffusion.test.mjs +102 -0
- package/dist/skills/media-use/scripts/lib/freeze.mjs +62 -0
- package/dist/skills/media-use/scripts/lib/freeze.test.mjs +46 -0
- package/dist/skills/media-use/scripts/lib/grade-analyzer.mjs +265 -0
- package/dist/skills/media-use/scripts/lib/grade-analyzer.test.mjs +285 -0
- package/dist/skills/media-use/scripts/lib/heygen-cli.mjs +191 -0
- package/dist/skills/media-use/scripts/lib/heygen-cli.test.mjs +301 -0
- package/dist/skills/media-use/scripts/lib/heygen-search.mjs +45 -0
- package/dist/skills/media-use/scripts/lib/heygen-search.test.mjs +49 -0
- package/dist/skills/media-use/scripts/lib/heygen-video-provider.mjs +138 -0
- package/dist/skills/media-use/scripts/lib/heygen-video-provider.test.mjs +435 -0
- package/dist/skills/media-use/scripts/lib/image-provider.mjs +44 -0
- package/dist/skills/media-use/scripts/lib/index-gen.mjs +63 -0
- package/dist/skills/media-use/scripts/lib/local-media-search.mjs +32 -0
- package/dist/skills/media-use/scripts/lib/local-models.mjs +318 -0
- package/dist/skills/media-use/scripts/lib/local-models.test.mjs +220 -0
- package/dist/skills/media-use/scripts/lib/local-run.mjs +78 -0
- package/dist/skills/media-use/scripts/lib/local-run.test.mjs +121 -0
- package/dist/skills/media-use/scripts/lib/logo-provider.mjs +223 -0
- package/dist/skills/media-use/scripts/lib/logo-provider.test.mjs +154 -0
- package/dist/skills/media-use/scripts/lib/ltx-video-provider.mjs +94 -0
- package/dist/skills/media-use/scripts/lib/ltx-video-provider.test.mjs +316 -0
- package/dist/skills/media-use/scripts/lib/lut-preset-provider.mjs +271 -0
- package/dist/skills/media-use/scripts/lib/lut-preset-provider.test.mjs +199 -0
- package/dist/skills/media-use/scripts/lib/manifest.mjs +229 -0
- package/dist/skills/media-use/scripts/lib/manifest.test.mjs +388 -0
- package/dist/skills/media-use/scripts/lib/match.mjs +47 -0
- package/dist/skills/media-use/scripts/lib/media-fetch.mjs +71 -0
- package/dist/skills/media-use/scripts/lib/media-fetch.test.mjs +169 -0
- package/dist/skills/media-use/scripts/lib/media-search.d.mts +9 -0
- package/dist/skills/media-use/scripts/lib/media-search.mjs +107 -0
- package/dist/skills/media-use/scripts/lib/mflux-provider.mjs +128 -0
- package/dist/skills/media-use/scripts/lib/mflux-provider.test.mjs +174 -0
- package/dist/skills/media-use/scripts/lib/misses.mjs +49 -0
- package/dist/skills/media-use/scripts/lib/misses.test.mjs +78 -0
- package/dist/skills/media-use/scripts/lib/npx-sync.mjs +31 -0
- package/dist/skills/media-use/scripts/lib/npx-sync.test.mjs +50 -0
- package/dist/skills/media-use/scripts/lib/parakeet-words.mjs +27 -0
- package/dist/skills/media-use/scripts/lib/parakeet-words.test.mjs +44 -0
- package/dist/skills/media-use/scripts/lib/prefs-store.mjs +180 -0
- package/dist/skills/media-use/scripts/lib/prefs-store.test.mjs +189 -0
- package/dist/skills/media-use/scripts/lib/probe.mjs +39 -0
- package/dist/skills/media-use/scripts/lib/probe.test.mjs +31 -0
- package/dist/skills/media-use/scripts/lib/providers.mjs +5 -0
- package/dist/skills/media-use/scripts/lib/recipe-store.mjs +367 -0
- package/dist/skills/media-use/scripts/lib/recipe-store.test.mjs +367 -0
- package/dist/skills/media-use/scripts/lib/registry.mjs +218 -0
- package/dist/skills/media-use/scripts/lib/registry.test.mjs +262 -0
- package/dist/skills/media-use/scripts/lib/search.mjs +24 -0
- package/dist/skills/media-use/scripts/lib/search.test.mjs +32 -0
- package/dist/skills/media-use/scripts/lib/sfx-provider.mjs +23 -0
- package/dist/skills/media-use/scripts/lib/specs.mjs +132 -0
- package/dist/skills/media-use/scripts/lib/specs.test.mjs +157 -0
- package/dist/skills/media-use/scripts/lib/stats.mjs +117 -0
- package/dist/skills/media-use/scripts/lib/stats.test.mjs +165 -0
- package/dist/skills/media-use/scripts/lib/telemetry.mjs +193 -0
- package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +373 -0
- package/dist/skills/media-use/scripts/lib/tts-local-provider.mjs +92 -0
- package/dist/skills/media-use/scripts/lib/tts-local-provider.test.mjs +72 -0
- package/dist/skills/media-use/scripts/lib/usage.mjs +57 -0
- package/dist/skills/media-use/scripts/lib/usage.test.mjs +54 -0
- package/dist/skills/media-use/scripts/lib/voice-provider.mjs +57 -0
- package/dist/skills/media-use/scripts/lib/voice-provider.test.mjs +46 -0
- package/dist/skills/media-use/scripts/lib/words.mjs +18 -0
- package/dist/skills/media-use/scripts/prefs.mjs +70 -0
- package/dist/skills/media-use/scripts/recipe.mjs +103 -0
- package/dist/skills/media-use/scripts/resolve.mjs +1287 -0
- package/dist/skills/media-use/scripts/transcribe.mjs +180 -0
- package/dist/skills/media-use/scripts/transcript-cut.mjs +239 -0
- package/dist/skills/registry/localSemantic.js +548 -0
- package/dist/studio/assets/{hyperframes-player-c23wnIfc.js → hyperframes-player-BBub3ZdI.js} +3 -3
- package/dist/studio/assets/{index-IDNr3X1H.js → index-BxZ5Cw5w.js} +1 -1
- package/dist/studio/assets/{index-DgkAzx3n.js → index-CI7WON-E.js} +1 -1
- package/dist/studio/assets/{index-gbOCDCuZ.js → index-D1KNPijK.js} +192 -192
- package/dist/studio/assets/{index-DKJpHYYd.css → index-DZBHG9TW.css} +1 -1
- package/dist/studio/index.d.ts +577 -8
- package/dist/studio/index.html +2 -2
- package/dist/studio/index.js +22537 -21600
- package/dist/studio/index.js.map +1 -1
- package/dist/studio/styles.css +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
copyFileSync,
|
|
9
|
+
renameSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
rmSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { homedir, tmpdir } from "node:os";
|
|
14
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
15
|
+
import { parseArgs } from "node:util";
|
|
16
|
+
import { mergeTokensToWords } from "./lib/parakeet-words.mjs";
|
|
17
|
+
import { track } from "./lib/telemetry.mjs";
|
|
18
|
+
import { resolveNpxInvocation } from "./lib/npx-sync.mjs";
|
|
19
|
+
|
|
20
|
+
// The DEFAULT local transcription path. Prefers NVIDIA Parakeet-TDT via
|
|
21
|
+
// parakeet-mlx, which beats whisper.cpp on the Open ASR Leaderboard (~6.05% vs
|
|
22
|
+
// 7.44% avg WER, and 4.73% vs 5.96% on noisy test-other) and is 5-10x faster
|
|
23
|
+
// with native punctuation. Emits { text, words:[{text,start,end}] } (word
|
|
24
|
+
// timestamps merged from Parakeet's sub-word tokens) for transcript-cut /
|
|
25
|
+
// captions / the audio engine.
|
|
26
|
+
//
|
|
27
|
+
// Parakeet v3 covers English + 25 European languages. For other languages, or
|
|
28
|
+
// when parakeet-mlx is not installed, it falls back to whisper.cpp via
|
|
29
|
+
// `hyperframes transcribe` (99 languages; the CLI resolves/builds whisper.cpp
|
|
30
|
+
// on first use — it is not bundled). `--engine` forces one.
|
|
31
|
+
|
|
32
|
+
const { values: args } = parseArgs({
|
|
33
|
+
options: {
|
|
34
|
+
input: { type: "string", short: "i" },
|
|
35
|
+
out: { type: "string", short: "o" },
|
|
36
|
+
engine: { type: "string", default: "auto" }, // auto | parakeet | whisper
|
|
37
|
+
model: { type: "string", default: "mlx-community/parakeet-tdt-0.6b-v3" },
|
|
38
|
+
json: { type: "boolean", default: false },
|
|
39
|
+
help: { type: "boolean", short: "h", default: false },
|
|
40
|
+
},
|
|
41
|
+
strict: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
if (args.help) {
|
|
45
|
+
console.log(`media-use transcribe: better-than-whisper local ASR (Parakeet), whisper.cpp fallback
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
node transcribe.mjs --input audio.wav [--out audio.transcribe.json] [--engine auto|parakeet|whisper]
|
|
49
|
+
|
|
50
|
+
Parakeet (default) beats whisper.cpp on accuracy + speed for English/European
|
|
51
|
+
languages; whisper.cpp (99 languages) is the fallback. Install Parakeet once:
|
|
52
|
+
uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!args.input) {
|
|
57
|
+
console.error("error: --input is required");
|
|
58
|
+
process.exit(2);
|
|
59
|
+
}
|
|
60
|
+
const inputPath = resolve(args.input);
|
|
61
|
+
if (!existsSync(inputPath)) {
|
|
62
|
+
console.error(`error: input not found: ${inputPath}`);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
const outPath = resolve(
|
|
66
|
+
args.out || `${inputPath.slice(0, -extname(inputPath).length)}.transcribe.json`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Locate the parakeet-mlx runner the same way the CLI does: env override, then
|
|
70
|
+
// the documented ~/.venvs/parakeet install, then PATH. Checking the venv (not
|
|
71
|
+
// just PATH) is what keeps a user who followed the install docs verbatim from
|
|
72
|
+
// silently falling through to whisper. Returns the runner path, or null.
|
|
73
|
+
function resolveParakeet() {
|
|
74
|
+
for (const p of [
|
|
75
|
+
process.env.HYPERFRAMES_PARAKEET,
|
|
76
|
+
join(homedir(), ".venvs", "parakeet", "bin", "parakeet-mlx"),
|
|
77
|
+
]) {
|
|
78
|
+
if (p && existsSync(p)) return p;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
execFileSync("parakeet-mlx", ["--help"], {
|
|
82
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
83
|
+
timeout: 20000,
|
|
84
|
+
});
|
|
85
|
+
return "parakeet-mlx";
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Write via a sibling temp + atomic rename so a SIGKILL mid-write can't leave a
|
|
92
|
+
// truncated transcript at outPath (downstream reads it as valid JSON).
|
|
93
|
+
function atomicWrite(target, data) {
|
|
94
|
+
const tmp = `${target}.tmp-${process.pid}`;
|
|
95
|
+
writeFileSync(tmp, data);
|
|
96
|
+
renameSync(tmp, target);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function report(engine, wordCount) {
|
|
100
|
+
if (args.json) console.log(JSON.stringify({ ok: true, out: outPath, engine, words: wordCount }));
|
|
101
|
+
else
|
|
102
|
+
console.log(
|
|
103
|
+
`transcribed ${basename(inputPath)} -> ${outPath}${wordCount != null ? ` (${wordCount} words,` : " ("}${engine})`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function runParakeet(runner) {
|
|
108
|
+
const workDir = mkdtempSync(join(tmpdir(), "media-use-asr-"));
|
|
109
|
+
try {
|
|
110
|
+
execFileSync(
|
|
111
|
+
runner,
|
|
112
|
+
[inputPath, "--model", args.model, "--output-format", "json", "--output-dir", workDir],
|
|
113
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 1_800_000 },
|
|
114
|
+
);
|
|
115
|
+
const jsonPath = join(workDir, `${basename(inputPath, extname(inputPath))}.json`);
|
|
116
|
+
if (!existsSync(jsonPath)) throw new Error("parakeet produced no JSON");
|
|
117
|
+
const merged = mergeTokensToWords(JSON.parse(readFileSync(jsonPath, "utf8")));
|
|
118
|
+
atomicWrite(outPath, JSON.stringify(merged, null, 2));
|
|
119
|
+
report("parakeet", merged.words.length);
|
|
120
|
+
} finally {
|
|
121
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// whisper.cpp via the hyperframes CLI (fetched/built on first use — see
|
|
126
|
+
// SKILL.md): writes transcript.json into --dir; relocate to --out.
|
|
127
|
+
function runWhisper() {
|
|
128
|
+
const workDir = mkdtempSync(join(tmpdir(), "media-use-whisper-"));
|
|
129
|
+
try {
|
|
130
|
+
// On Windows a bare "npx" is npx.cmd, which execFileSync cannot exec
|
|
131
|
+
// (spawnSync npx ENOENT) — resolveNpxInvocation reroutes it through
|
|
132
|
+
// node + npx-cli.js (and throws actionably when it can't), same
|
|
133
|
+
// mechanism as the audio engine's TTS spawns.
|
|
134
|
+
const resolved = resolveNpxInvocation(
|
|
135
|
+
["hyperframes", "transcribe", inputPath, "--dir", workDir],
|
|
136
|
+
{ stdio: ["ignore", "pipe", "pipe"], timeout: 1_800_000 },
|
|
137
|
+
);
|
|
138
|
+
execFileSync(resolved.cmd, resolved.args, resolved.opts);
|
|
139
|
+
const produced = join(workDir, "transcript.json");
|
|
140
|
+
if (!existsSync(produced)) throw new Error("whisper produced no transcript.json");
|
|
141
|
+
const tmp = `${outPath}.tmp-${process.pid}`;
|
|
142
|
+
copyFileSync(produced, tmp);
|
|
143
|
+
renameSync(tmp, outPath); // atomic publish
|
|
144
|
+
let words;
|
|
145
|
+
try {
|
|
146
|
+
const t = JSON.parse(readFileSync(outPath, "utf8"));
|
|
147
|
+
words = Array.isArray(t?.words) ? t.words.length : undefined;
|
|
148
|
+
} catch {
|
|
149
|
+
/* leave undefined */
|
|
150
|
+
}
|
|
151
|
+
report("whisper", words);
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const parakeetBin = resolveParakeet();
|
|
159
|
+
const engine =
|
|
160
|
+
args.engine === "parakeet" || args.engine === "whisper"
|
|
161
|
+
? args.engine
|
|
162
|
+
: parakeetBin
|
|
163
|
+
? "parakeet"
|
|
164
|
+
: "whisper";
|
|
165
|
+
if (engine === "parakeet") {
|
|
166
|
+
if (!parakeetBin) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
"parakeet-mlx not found (checked $HYPERFRAMES_PARAKEET, ~/.venvs/parakeet, and PATH). Install: uv venv ~/.venvs/parakeet && VIRTUAL_ENV=~/.venvs/parakeet uv pip install parakeet-mlx (or use --engine whisper)",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
runParakeet(parakeetBin);
|
|
172
|
+
} else {
|
|
173
|
+
runWhisper();
|
|
174
|
+
}
|
|
175
|
+
await track("media_use_transcribe", { engine });
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (args.json) console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
178
|
+
else console.error(`error: transcription failed: ${err.message}`);
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
8
|
+
import { compileCutList } from "./lib/cutlist.mjs";
|
|
9
|
+
import { track } from "./lib/telemetry.mjs";
|
|
10
|
+
|
|
11
|
+
const { values: args } = parseArgs({
|
|
12
|
+
options: {
|
|
13
|
+
input: { type: "string" },
|
|
14
|
+
transcript: { type: "string" },
|
|
15
|
+
remove: { type: "string" },
|
|
16
|
+
"remove-words": { type: "string" },
|
|
17
|
+
"remove-fillers": { type: "string" },
|
|
18
|
+
"cut-silence": { type: "string" },
|
|
19
|
+
keep: { type: "string" },
|
|
20
|
+
copy: { type: "boolean", default: false },
|
|
21
|
+
plan: { type: "boolean", default: false },
|
|
22
|
+
out: { type: "string" },
|
|
23
|
+
json: { type: "boolean", default: false },
|
|
24
|
+
help: { type: "boolean", short: "h", default: false },
|
|
25
|
+
},
|
|
26
|
+
strict: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (args.help) {
|
|
30
|
+
console.log(`media-use transcript-cut — compile transcript edits into video cuts
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
node transcript-cut.mjs --input in.mp4 --transcript transcript.json --remove "12-15" --out out.mp4
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--input Source video/audio file
|
|
37
|
+
--transcript JSON word transcript, array or { words: [...] }
|
|
38
|
+
--remove Time ranges to remove, seconds: a-b,c-d
|
|
39
|
+
--remove-words Word-index ranges to remove: 12-18,40-41
|
|
40
|
+
--remove-fillers Comma list of filler words to remove
|
|
41
|
+
--cut-silence Remove inter-word gaps longer than this many seconds
|
|
42
|
+
--keep Inverse mode: direct kept ranges, mutually exclusive with removal
|
|
43
|
+
--copy Use stream copy for faster, keyframe-snapped cuts
|
|
44
|
+
--plan Print kept segment JSON and exit without ffmpeg
|
|
45
|
+
--out Output file
|
|
46
|
+
--json Output JSON status
|
|
47
|
+
--help, -h Show this help`);
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
run();
|
|
53
|
+
await track("media_use_transcript_cut", {
|
|
54
|
+
mode: args.plan ? "plan" : "encode",
|
|
55
|
+
remove_fillers: !!args["remove-fillers"],
|
|
56
|
+
cut_silence: !!args["cut-silence"],
|
|
57
|
+
ranges: !!args.remove,
|
|
58
|
+
keep: !!args.keep,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
if (args.json) console.log(JSON.stringify({ ok: false, error: err.message }));
|
|
62
|
+
else console.error(`error: ${err.message}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function run() {
|
|
67
|
+
if (!args.transcript) throw new Error("--transcript is required");
|
|
68
|
+
const transcript = JSON.parse(readFileSync(resolve(args.transcript), "utf8"));
|
|
69
|
+
const segments = compileCutList(transcript, {
|
|
70
|
+
remove: args.remove,
|
|
71
|
+
removeWords: args["remove-words"],
|
|
72
|
+
removeFillers: args["remove-fillers"],
|
|
73
|
+
cutSilence: args["cut-silence"],
|
|
74
|
+
keep: args.keep,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (args.plan) {
|
|
78
|
+
console.log(JSON.stringify(segments));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!args.input || !args.out)
|
|
83
|
+
throw new Error("--input and --out are required unless --plan is set");
|
|
84
|
+
if (segments.length === 0) throw new Error("cut list has no kept segments");
|
|
85
|
+
|
|
86
|
+
const inputPath = resolve(args.input);
|
|
87
|
+
const outPath = resolve(args.out);
|
|
88
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
89
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "media-use-cut-"));
|
|
90
|
+
const keptSeconds = sumDurations(segments);
|
|
91
|
+
const totalSeconds = probeDuration(inputPath);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const parts = segments.map((segment, index) => {
|
|
95
|
+
const out = join(
|
|
96
|
+
tmpDir,
|
|
97
|
+
`segment-${String(index).padStart(4, "0")}${extname(outPath) || ".mp4"}`,
|
|
98
|
+
);
|
|
99
|
+
cutSegment(inputPath, segment, out, Boolean(args.copy));
|
|
100
|
+
return out;
|
|
101
|
+
});
|
|
102
|
+
const listPath = join(tmpDir, "list.txt");
|
|
103
|
+
writeFileSync(
|
|
104
|
+
listPath,
|
|
105
|
+
parts.map((part) => `file '${escapeConcatPath(part)}'`).join("\n") + "\n",
|
|
106
|
+
);
|
|
107
|
+
// Encode to a sibling temp (same extension so ffmpeg picks the right muxer),
|
|
108
|
+
// then atomic-rename so a SIGKILL mid-encode can't leave a truncated outPath.
|
|
109
|
+
const tmpOut = `${outPath}.part${extname(outPath) || ".mp4"}`;
|
|
110
|
+
execFileSync(
|
|
111
|
+
"ffmpeg",
|
|
112
|
+
["-y", "-f", "concat", "-safe", "0", "-i", listPath, "-c", "copy", tmpOut],
|
|
113
|
+
{
|
|
114
|
+
stdio: "ignore",
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
renameSync(tmpOut, outPath);
|
|
118
|
+
} finally {
|
|
119
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Stream copy can only cut on keyframes; on sparse-keyframe footage the snap
|
|
123
|
+
// can silently swallow the whole cut. Compare, then surface the drift in BOTH
|
|
124
|
+
// the stderr warning (human) and the --json result (pipelines).
|
|
125
|
+
let copyDrift = null;
|
|
126
|
+
if (args.copy) {
|
|
127
|
+
const outSeconds = probeDuration(outPath);
|
|
128
|
+
if (Math.abs(outSeconds - keptSeconds) > 1) {
|
|
129
|
+
copyDrift = { produced_s: round3(outSeconds), expected_s: round3(keptSeconds) };
|
|
130
|
+
if (!args.json) {
|
|
131
|
+
console.error(
|
|
132
|
+
`warning: --copy keyframe snapping produced ${round3(outSeconds)}s instead of ${round3(keptSeconds)}s kept; drop --copy for frame-accurate cuts`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (args.json) {
|
|
139
|
+
console.log(
|
|
140
|
+
JSON.stringify({
|
|
141
|
+
ok: true,
|
|
142
|
+
input: inputPath,
|
|
143
|
+
out: outPath,
|
|
144
|
+
segments,
|
|
145
|
+
kept_s: round3(keptSeconds),
|
|
146
|
+
total_s: round3(totalSeconds),
|
|
147
|
+
...(copyDrift && { copy_drift: copyDrift }),
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
console.log(
|
|
154
|
+
`cut ${inputPath} -> ${outPath} (${segments.length} segments, ${fmt(keptSeconds)}s kept of ${fmt(
|
|
155
|
+
totalSeconds,
|
|
156
|
+
)}s)`,
|
|
157
|
+
);
|
|
158
|
+
console.log(`next: resolve --from ${outPath} --type <type>`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cutSegment(inputPath, segment, outPath, copy) {
|
|
162
|
+
const argv = [
|
|
163
|
+
"-y",
|
|
164
|
+
"-nostdin",
|
|
165
|
+
"-ss",
|
|
166
|
+
fmt(segment.start),
|
|
167
|
+
"-i",
|
|
168
|
+
inputPath,
|
|
169
|
+
"-to",
|
|
170
|
+
fmt(segment.end - segment.start),
|
|
171
|
+
];
|
|
172
|
+
if (copy) {
|
|
173
|
+
argv.push("-c", "copy", "-avoid_negative_ts", "make_zero");
|
|
174
|
+
} else {
|
|
175
|
+
argv.push(...encodeArgsFor(extname(outPath).toLowerCase()));
|
|
176
|
+
}
|
|
177
|
+
argv.push(outPath);
|
|
178
|
+
execFileSync("ffmpeg", argv, { stdio: "ignore" });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Codec set per output container. Audio-only outputs must not get the
|
|
182
|
+
// video-centric aac/x264 set (aac inside .wav breaks timing entirely).
|
|
183
|
+
function encodeArgsFor(ext) {
|
|
184
|
+
if (ext === ".wav") return ["-c:a", "pcm_s16le"];
|
|
185
|
+
if (ext === ".mp3") return ["-c:a", "libmp3lame", "-q:a", "2"];
|
|
186
|
+
if (ext === ".m4a" || ext === ".aac") return ["-c:a", "aac"];
|
|
187
|
+
if (ext === ".flac") return ["-c:a", "flac"];
|
|
188
|
+
return [
|
|
189
|
+
"-c:v",
|
|
190
|
+
"libx264",
|
|
191
|
+
"-preset",
|
|
192
|
+
"veryfast",
|
|
193
|
+
"-crf",
|
|
194
|
+
"18",
|
|
195
|
+
"-c:a",
|
|
196
|
+
"aac",
|
|
197
|
+
"-movflags",
|
|
198
|
+
"+faststart",
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function probeDuration(filePath) {
|
|
203
|
+
const raw = execFileSync(
|
|
204
|
+
"ffprobe",
|
|
205
|
+
[
|
|
206
|
+
"-v",
|
|
207
|
+
"error",
|
|
208
|
+
"-show_entries",
|
|
209
|
+
"format=duration",
|
|
210
|
+
"-of",
|
|
211
|
+
"default=noprint_wrappers=1:nokey=1",
|
|
212
|
+
"--",
|
|
213
|
+
filePath,
|
|
214
|
+
],
|
|
215
|
+
{ encoding: "utf8" },
|
|
216
|
+
);
|
|
217
|
+
const duration = Number(raw.trim());
|
|
218
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
219
|
+
throw new Error(`could not probe duration: ${filePath}`);
|
|
220
|
+
return duration;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function escapeConcatPath(filePath) {
|
|
224
|
+
return filePath.replace(/'/g, "'\\''");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sumDurations(segments) {
|
|
228
|
+
return segments.reduce((sum, segment) => sum + (segment.end - segment.start), 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function fmt(n) {
|
|
232
|
+
return round3(n)
|
|
233
|
+
.toFixed(3)
|
|
234
|
+
.replace(/\.?0+$/, "");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function round3(n) {
|
|
238
|
+
return Math.round(Number(n) * 1000) / 1000;
|
|
239
|
+
}
|