@hypit/hypit 0.1.14 → 0.2.0
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/README.md +2 -2
- package/dist/public/browser-capture.d.ts +9 -3
- package/dist/public/caption.d.ts +2 -0
- package/dist/public/narrative.d.ts +2 -0
- package/dist/public/runtime-kit.d.ts +2 -2
- package/dist/public/speech.d.ts +2 -0
- package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
- package/package.json +1 -1
- package/packages/browser-capture/README.md +9 -2
- package/packages/browser-capture/package.json +5 -0
- package/packages/browser-capture/src/browser.ts +22 -8
- package/packages/browser-capture/src/index.ts +8 -2
- package/packages/caption/README.md +1 -1
- package/packages/caption/src/display.ts +1 -0
- package/packages/caption-fine/README.md +19 -5
- package/packages/caption-fine/package.json +1 -0
- package/packages/caption-fine/src/joined-box.ts +114 -0
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +99 -92
- package/packages/caption-fine/src/spacing.ts +8 -61
- package/packages/caption-fine-studio/src/index.ts +2 -4
- package/packages/cli/README.md +3 -2
- package/packages/cli/src/arguments.ts +2 -2
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +9 -2
- package/packages/cli/src/machine-view.ts +1 -1
- package/packages/cli/src/output.ts +1 -0
- package/packages/credential-store-os/src/store.ts +5 -1
- package/packages/narrative/README.md +3 -1
- package/packages/narrative/src/schema.ts +1 -0
- package/packages/narrative/src/types.ts +2 -0
- package/packages/provider-whisperx-local/README.md +26 -10
- package/packages/provider-whisperx-local/src/activation.ts +10 -1
- package/packages/provider-whisperx-local/src/program.ts +8 -4
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-kit/README.md +6 -2
- package/packages/runtime-kit/src/index.ts +2 -2
- package/packages/runtime-local/README.md +6 -1
- package/packages/runtime-local/src/host.ts +2 -0
- package/packages/runtime-local/src/index.ts +1 -0
- package/packages/runtime-local/src/programs.ts +53 -17
- package/packages/script/README.md +81 -34
- package/packages/script/bin/migrate-0.2.mjs +90 -0
- package/packages/script/src/edit.ts +28 -60
- package/packages/script/src/lexical.ts +59 -130
- package/packages/script/src/manifest.ts +11 -11
- package/packages/script/src/narrative.ts +4 -2
- package/packages/script/src/parser.ts +140 -193
- package/packages/script/src/types.ts +2 -3
- package/packages/studio/src/ui/code.ts +1 -1
- package/packages/studio/src/ui/main.ts +2 -2
- package/packages/studio/src/ui/markers.ts +3 -3
- package/packages/studio/src/ui/selection.ts +1 -1
- package/packages/studio/src/ui/syntax.ts +42 -21
- package/packages/temporal-markup/README.md +1 -1
- package/packages/video-cli/README.md +9 -5
- package/packages/video-cli/src/capture.ts +17 -2
- package/packages/video-cli/src/creation.ts +4 -6
- package/packages/video-cli/src/media.ts +10 -3
- package/packages/whisperx/README.md +56 -3
- package/packages/whisperx/src/component.ts +2 -5
- package/packages/whisperx/src/evidence.ts +4 -3
- package/packages/whisperx/src/index.ts +1 -0
- package/packages/whisperx/src/manifest.ts +2 -3
- package/packages/whisperx/src/surface.ts +3 -6
- package/packages/whisperx/src/types.ts +9 -1
- package/packages/yt-dlp/README.md +9 -3
- package/packages/yt-dlp/package.json +2 -1
- package/packages/yt-dlp/src/download.ts +10 -24
- package/packages/yt-dlp/src/environment.ts +36 -0
- package/packages/yt-dlp/src/index.ts +1 -0
- package/services/whisperx/README.md +45 -12
- package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
- package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
- package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
- package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
- package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
- package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
- package/services/yt-dlp/README.md +14 -15
- package/services/yt-dlp/pyproject.toml +1 -1
- package/services/yt-dlp/uv.lock +513 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* SVML has a small, closed grammar — tags, quoted strings, whole-value
|
|
5
5
|
* references, and Script prose with markers — so it is tokenized directly rather
|
|
6
6
|
* than through a general highlighter that has no grammar for it. The Script
|
|
7
|
-
* markers are the point: `@claim …
|
|
7
|
+
* markers are the point: `@{claim} … @{/claim}` is what a Media Item binds to, so it
|
|
8
8
|
* has to read as a distinct thing from an ordinary attribute.
|
|
9
9
|
*
|
|
10
10
|
* Tokens are non-overlapping and in source order. Gaps between them are plain
|
|
@@ -33,12 +33,30 @@ export type Token = {
|
|
|
33
33
|
|
|
34
34
|
const NAME = /^[A-Za-z_][A-Za-z0-9_.:-]*/u;
|
|
35
35
|
const ATTRIBUTE = /^[A-Za-z_][A-Za-z0-9_.:-]*/u;
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
/** All control sigils belong inside a delimited Script marker. */
|
|
37
|
+
const MARKER = /^@\{(?:(~)?([a-z][a-z0-9_-]{0,63})(!)?|\/([a-z][a-z0-9_-]{0,63})(~)?)\}/u;
|
|
38
|
+
|
|
39
|
+
function unescapedIndex(source: string, character: string, from: number, to = source.length): number {
|
|
40
|
+
for (let index = from; index < to; index++) {
|
|
41
|
+
if (source[index] === "\\") { index++; continue; }
|
|
42
|
+
if (source[index] === character) return index;
|
|
43
|
+
}
|
|
44
|
+
return -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function scriptDecoration(source: string, cursor: number, push: Push, markers = true, attributes = true): number {
|
|
48
|
+
if (source[cursor] === "\\") return cursor + 2;
|
|
49
|
+
const marker = markers ? MARKER.exec(source.slice(cursor)) : null;
|
|
50
|
+
if (marker) {
|
|
51
|
+
push(cursor, cursor + marker[0].length, "marker", marker[2] ?? marker[4]);
|
|
52
|
+
return cursor + marker[0].length;
|
|
53
|
+
}
|
|
54
|
+
if (attributes && source[cursor] === "{") {
|
|
55
|
+
const end = unescapedIndex(source, "}", cursor + 1);
|
|
56
|
+
if (end >= 0) { push(cursor, end + 1, "attr"); return end + 1; }
|
|
57
|
+
}
|
|
58
|
+
return cursor + 1;
|
|
59
|
+
}
|
|
42
60
|
|
|
43
61
|
function localName(tag: string): string {
|
|
44
62
|
const colon = tag.indexOf(":");
|
|
@@ -145,11 +163,12 @@ function tokenizeAttributes(source: string, from: number, push: Push): number {
|
|
|
145
163
|
return cursor;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
|
-
/** Script prose: Segment and Role Cue tags stay tags; `@id` markers stand out. */
|
|
166
|
+
/** Script prose: Segment and Role Cue tags stay tags; `@{id}` markers stand out. */
|
|
149
167
|
function tokenizeScriptBody(source: string, from: number, tag: string, push: Push): number {
|
|
150
168
|
const close = `</${tag}>`;
|
|
151
169
|
let cursor = from;
|
|
152
170
|
while (cursor < source.length) {
|
|
171
|
+
if (source[cursor] === "\\") { cursor += 2; continue; }
|
|
153
172
|
if (source.startsWith(close, cursor)) {
|
|
154
173
|
push(cursor, cursor + 2, "punct");
|
|
155
174
|
push(cursor + 2, cursor + 2 + tag.length, "tag");
|
|
@@ -164,6 +183,20 @@ function tokenizeScriptBody(source: string, from: number, tag: string, push: Pus
|
|
|
164
183
|
continue;
|
|
165
184
|
}
|
|
166
185
|
if (source[cursor] === "<") {
|
|
186
|
+
const endDual = unescapedIndex(source, ">", cursor + 1);
|
|
187
|
+
const pipe = endDual < 0 ? -1 : unescapedIndex(source, "|", cursor + 1, endDual);
|
|
188
|
+
if (pipe >= 0) {
|
|
189
|
+
const shared = !source.slice(pipe + 1, endDual).trim();
|
|
190
|
+
push(cursor, cursor + 1, "punct");
|
|
191
|
+
cursor++;
|
|
192
|
+
while (cursor < pipe) cursor = scriptDecoration(source, cursor, push, shared, true);
|
|
193
|
+
push(pipe, pipe + 1, "punct");
|
|
194
|
+
cursor = pipe + 1;
|
|
195
|
+
while (cursor < endDual) cursor = scriptDecoration(source, cursor, push, true, false);
|
|
196
|
+
push(endDual, endDual + 1, "punct");
|
|
197
|
+
cursor = endDual + 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
167
200
|
const closing = source[cursor + 1] === "/";
|
|
168
201
|
const nameStart = cursor + (closing ? 2 : 1);
|
|
169
202
|
const name = NAME.exec(source.slice(nameStart));
|
|
@@ -177,19 +210,7 @@ function tokenizeScriptBody(source: string, from: number, tag: string, push: Pus
|
|
|
177
210
|
cursor = stop;
|
|
178
211
|
continue;
|
|
179
212
|
}
|
|
180
|
-
|
|
181
|
-
if (source[cursor] === "@" || (source[cursor] === "~" && source[cursor + 1] === "@")) {
|
|
182
|
-
const marker = MARKER.exec(source.slice(cursor));
|
|
183
|
-
if (marker !== null) {
|
|
184
|
-
// Strip the affinity sigils, the `@` and the closing `/` so an opening
|
|
185
|
-
// marker, its closing marker and a Moment all report the same name.
|
|
186
|
-
const id = marker[0].replace(/^~?@\/?/u, "").replace(/[!~]$/u, "");
|
|
187
|
-
push(cursor, cursor + marker[0].length, "marker", id);
|
|
188
|
-
cursor += marker[0].length;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
cursor += 1;
|
|
213
|
+
cursor = scriptDecoration(source, cursor, push);
|
|
193
214
|
}
|
|
194
215
|
return cursor;
|
|
195
216
|
}
|
|
@@ -75,7 +75,7 @@ time and implements its own schedule or state, while the shared temporal protoco
|
|
|
75
75
|
time came from. An outer lifetime and child activations are separate inputs when a component persists
|
|
76
76
|
between events.
|
|
77
77
|
|
|
78
|
-
Script's
|
|
78
|
+
Script's delimited `@{...}` Selection and Moment syntax belong to `@hypit/script`; media playback belongs
|
|
79
79
|
to `@hypit/media-track`; a graphic component's reveal or preset semantics belong to that component.
|
|
80
80
|
|
|
81
81
|
## Independently bound endpoints
|
|
@@ -42,7 +42,8 @@ in [file CredentialStore](../credential-store-file/README.md#select-it-before-lo
|
|
|
42
42
|
`hypit doctor --endpoint <name>` checks a selected Endpoint;
|
|
43
43
|
`hypit runtime up --endpoint <name>` prepares that Endpoint and starts the Worker. Repeat the flag
|
|
44
44
|
for several chosen Endpoints; omitting it prepares the whole Profile. `hypit programs up --endpoint
|
|
45
|
-
<name>` prepares a local helper independently of the Worker.
|
|
45
|
+
<name>` prepares and starts a local helper independently of the Worker. `hypit programs prepare
|
|
46
|
+
--endpoint <name>` only prepares its selected resources, including for a service already running.
|
|
46
47
|
|
|
47
48
|
With the selected execution environment:
|
|
48
49
|
|
|
@@ -67,9 +68,11 @@ hypit transcribe reference.mp4 --to notes/reference.transcript.json --language e
|
|
|
67
68
|
hypit measure main.svml --segment hook --language en --pace normal --rounding round
|
|
68
69
|
```
|
|
69
70
|
|
|
70
|
-
For `transcribe`, set `--language` to
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
For `transcribe`, set `--language` to an explicit lowercase two- or three-letter spoken language code,
|
|
72
|
+
such as `en`, `zh` or `ko`. The selected service owns which languages it can align. Chinese speech uses `zh`, including
|
|
73
|
+
Chinese speech containing English names. The request selects the recognition language and
|
|
74
|
+
language-specific aligner; ASR size remains a deployment choice. Caption font and Script's
|
|
75
|
+
simplified/traditional characters are independent authoring choices.
|
|
73
76
|
|
|
74
77
|
`transcribe` uses the Profile's `whisperx-alignment` Endpoint (after
|
|
75
78
|
extracting 16 kHz mono speech audio with ffmpeg). Direct invocation forwards the Provider's progress
|
|
@@ -142,7 +145,8 @@ Grid `samples` retain the requested times; `frames` contain the actual extracted
|
|
|
142
145
|
It also reports every page path for `tiles`. The media layer reads existing timed text; transcription
|
|
143
146
|
and its Endpoint remain separate. `boundaries` reports adjacent-frame
|
|
144
147
|
change candidates and their measured scores; it does not suppress short changes or call them shots.
|
|
145
|
-
`fetch`
|
|
148
|
+
`prepare-fetch` explicitly prepares the locked downloader environment; `fetch` requires it and
|
|
149
|
+
turns a link into a file with the pinned yt-dlp; [the downloader package](../yt-dlp/README.md)
|
|
146
150
|
owns its dependencies, download choices and file handling. Commands that create evidence write only
|
|
147
151
|
what `--to` names and refuse to overwrite. `vocabulary` reads the installed
|
|
148
152
|
manifests: every package with its tags and models, or one package's Surfaces with their attributes,
|
|
@@ -25,6 +25,9 @@ Browser options:
|
|
|
25
25
|
--scale <number> Device pixel ratio (default: 1)
|
|
26
26
|
--channel <name> Installed Chrome channel, e.g. chrome
|
|
27
27
|
--browser <path> Explicit browser executable instead of a channel
|
|
28
|
+
--browser-version <version> Exact Chrome for Testing version
|
|
29
|
+
--browser-cache <directory> Cache shared by installation and capture
|
|
30
|
+
--browser-download-base-url <url> Compatible archive source for install-browser
|
|
28
31
|
--headed Show the browser
|
|
29
32
|
--timeout-ms <milliseconds> Page operation and navigation timeout
|
|
30
33
|
--json Print all saved files with their actual dimensions
|
|
@@ -70,6 +73,7 @@ export async function runCaptureCli(argv: readonly string[], io: CliIo, cwd = pr
|
|
|
70
73
|
args: (separator < 0 ? argv : argv.slice(0, separator)).slice(2), allowPositionals: true,
|
|
71
74
|
options: {
|
|
72
75
|
viewport: { type: "string" }, scale: { type: "string" }, channel: { type: "string" },
|
|
76
|
+
"browser-version": { type: "string" }, "browser-cache": { type: "string" }, "browser-download-base-url": { type: "string" },
|
|
73
77
|
browser: { type: "string" }, headed: { type: "boolean" }, "timeout-ms": { type: "string" },
|
|
74
78
|
json: { type: "boolean" }, debug: { type: "boolean" }, verbose: { type: "boolean" },
|
|
75
79
|
"no-color": { type: "boolean" }, color: { type: "string" }, help: { type: "boolean" },
|
|
@@ -81,14 +85,19 @@ export async function runCaptureCli(argv: readonly string[], io: CliIo, cwd = pr
|
|
|
81
85
|
});
|
|
82
86
|
const values = parsed.values;
|
|
83
87
|
if (values.help) { writeCaptureHelp(io); return; }
|
|
88
|
+
const managedBrowser = {
|
|
89
|
+
...(values["browser-version"] === undefined ? {} : { version: values["browser-version"] }),
|
|
90
|
+
...(values["browser-cache"] === undefined ? {} : { cacheDirectory: resolve(cwd, values["browser-cache"]) }),
|
|
91
|
+
...(values["browser-download-base-url"] === undefined ? {} : { downloadBaseUrl: values["browser-download-base-url"] }),
|
|
92
|
+
};
|
|
84
93
|
if (command === "install-browser") {
|
|
85
94
|
if (parsed.positionals.length > 0 || args.length > 0) throw new Error("install-browser takes no positional arguments");
|
|
86
95
|
for (const name of Object.keys(values)) {
|
|
87
|
-
if (!["json", "debug", "verbose", "color", "no-color"].includes(name)) throw new Error(`install-browser does not use --${name}`);
|
|
96
|
+
if (!["json", "debug", "verbose", "color", "no-color", "browser-version", "browser-cache", "browser-download-base-url"].includes(name)) throw new Error(`install-browser does not use --${name}`);
|
|
88
97
|
}
|
|
89
98
|
io.writeProgress?.("Preparing the capture browser…\n");
|
|
90
99
|
const { installCaptureBrowser } = await import("@hypit/browser-capture");
|
|
91
|
-
const path = await installCaptureBrowser();
|
|
100
|
+
const path = await installCaptureBrowser(managedBrowser);
|
|
92
101
|
io.write(values.json ? `${JSON.stringify({ format: "hypit.capture-browser@1", path })}\n` : `Capture browser ready: ${path}\n`);
|
|
93
102
|
return;
|
|
94
103
|
}
|
|
@@ -138,6 +147,12 @@ export async function runCaptureCli(argv: readonly string[], io: CliIo, cwd = pr
|
|
|
138
147
|
});
|
|
139
148
|
};
|
|
140
149
|
}
|
|
150
|
+
if (Object.keys(managedBrowser).length > 0) options = { ...options, browser: { ...options.browser, ...managedBrowser } };
|
|
151
|
+
if (values.browser || values.channel) {
|
|
152
|
+
if (Object.keys(managedBrowser).length > 0) throw new Error("Choose managed browser options or --browser/--channel");
|
|
153
|
+
const { browser: _managed, ...externalOptions } = options;
|
|
154
|
+
options = externalOptions;
|
|
155
|
+
}
|
|
141
156
|
const launch = { ...options.launch };
|
|
142
157
|
if (values.browser) { launch.executablePath = resolve(cwd, values.browser); delete launch.channel; }
|
|
143
158
|
if (values.channel) {
|
|
@@ -21,7 +21,7 @@ import { speechEvidenceTypes } from "@hypit/speech-evidence";
|
|
|
21
21
|
import type { AlignedTranscriptEvidence } from "@hypit/speech-evidence";
|
|
22
22
|
import { sealText } from "@hypit/text";
|
|
23
23
|
import { whisperXCapabilities, whisperXRequestForEvidenceAudio } from "@hypit/whisperx";
|
|
24
|
-
import
|
|
24
|
+
import { parseWhisperXLanguage } from "@hypit/whisperx";
|
|
25
25
|
|
|
26
26
|
import { videoCliDistribution } from "./distribution.js";
|
|
27
27
|
import { runProcess } from "./process.js";
|
|
@@ -282,9 +282,7 @@ async function transcribe(argv: readonly string[], io: CliIo, environment: Creat
|
|
|
282
282
|
const parsed = parseArguments(argv, ["--language", "--to", "--runtime", "--workspace"]);
|
|
283
283
|
assert(parsed.positionals.length === 1, "transcribe takes exactly one audio or video file");
|
|
284
284
|
const source = resolve(environment.cwd, parsed.positionals[0]!);
|
|
285
|
-
const language = parsed.options.get("--language");
|
|
286
|
-
assert(language === "en" || language === "zh" || language === "es",
|
|
287
|
-
"transcribe requires --language en|zh|es for the spoken language (use --language zh for Chinese)");
|
|
285
|
+
const language = parseWhisperXLanguage(parsed.options.get("--language"), "transcribe --language");
|
|
288
286
|
const to = await destination(parsed, environment.cwd);
|
|
289
287
|
const { profile, host } = await environment.openHost(
|
|
290
288
|
parsed.options.get("--runtime"),
|
|
@@ -298,7 +296,7 @@ async function transcribe(argv: readonly string[], io: CliIo, environment: Creat
|
|
|
298
296
|
id: "need:hypit-transcribe",
|
|
299
297
|
capability: whisperXCapabilities.alignment,
|
|
300
298
|
returns: speechEvidenceTypes.alignedTranscript,
|
|
301
|
-
constraints: whisperXRequestForEvidenceAudio(audio, { language
|
|
299
|
+
constraints: whisperXRequestForEvidenceAudio(audio, { language }),
|
|
302
300
|
result: "record:hypit-transcribe",
|
|
303
301
|
};
|
|
304
302
|
const provider = await selectedProvider(host, need, profile);
|
|
@@ -437,7 +435,7 @@ export function writeCreationHelp(io: CliIo, topic?: CreationCommand): void {
|
|
|
437
435
|
"hypit transcribe",
|
|
438
436
|
"Establish word times with the whisperx-alignment Endpoint of the selected Runtime Profile.",
|
|
439
437
|
"",
|
|
440
|
-
" hypit transcribe <audio|video> --to <transcript.json> --language
|
|
438
|
+
" hypit transcribe <audio|video> --to <transcript.json> --language <code> [--runtime <profile>] [--workspace <project>]",
|
|
441
439
|
"",
|
|
442
440
|
"Extracts 16 kHz mono speech audio with ffmpeg and writes every word with its start and end in",
|
|
443
441
|
"seconds. One immediate request; no Build, Result or state.",
|
|
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { dirname, extname, join, resolve } from "node:path";
|
|
4
4
|
|
|
5
5
|
import type { CliIo } from "@hypit/cli";
|
|
6
|
-
import { downloadVideo, isVideoUrl } from "@hypit/yt-dlp";
|
|
6
|
+
import { downloadVideo, isVideoUrl, prepareVideoDownload } from "@hypit/yt-dlp";
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
|
|
9
9
|
import { runProcess, runProcessOutput, runProcessWithInput } from "./process.js";
|
|
@@ -15,7 +15,7 @@ import type { FrameWords, TranscriptWord } from "./transcript.js";
|
|
|
15
15
|
* the input media. These commands expose evidence; editorial interpretation belongs to the author.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
export const mediaCommands = ["probe", "cut", "frames", "tile", "tiles", "boundaries", "fetch"] as const;
|
|
18
|
+
export const mediaCommands = ["probe", "cut", "frames", "tile", "tiles", "boundaries", "fetch", "prepare-fetch"] as const;
|
|
19
19
|
export type MediaCommand = typeof mediaCommands[number];
|
|
20
20
|
|
|
21
21
|
function assert(condition: unknown, message: string): asserts condition {
|
|
@@ -673,6 +673,7 @@ export function writeMediaHelp(io: CliIo, topic?: MediaCommand): void {
|
|
|
673
673
|
" Paginated grids (3 rows by default). Range files contain { start, end, id?, frames?, every? }."],
|
|
674
674
|
boundaries: [" hypit media boundaries <file> [--rate <samples/s>] [--threshold <0..1>]",
|
|
675
675
|
" Mechanical adjacent-frame change candidates with scores; never editorial shot labels."],
|
|
676
|
+
"prepare-fetch": [" hypit media prepare-fetch", " Explicitly prepare the pinned yt-dlp environment; does not fetch media."],
|
|
676
677
|
fetch: [" hypit media fetch <url> --to <video.mp4>", " A link turned into a file with the pinned yt-dlp, video and audio together."],
|
|
677
678
|
};
|
|
678
679
|
const chosen = topic === undefined ? mediaCommands : [topic];
|
|
@@ -697,5 +698,11 @@ export async function runMediaCli(argv: readonly string[], io: CliIo, cwd = proc
|
|
|
697
698
|
else if (command === "tile") await tile(rest, io, cwd);
|
|
698
699
|
else if (command === "tiles") await tiles(rest, io, cwd);
|
|
699
700
|
else if (command === "boundaries") await boundaries(rest, io, cwd);
|
|
700
|
-
else
|
|
701
|
+
else if (command === "prepare-fetch") {
|
|
702
|
+
const parsed = parseArguments(rest, []);
|
|
703
|
+
assert(parsed.positionals.length === 0, "prepare-fetch takes no positional arguments");
|
|
704
|
+
io.writeProgress?.("Preparing the selected yt-dlp environment…\n");
|
|
705
|
+
const executable = prepareVideoDownload();
|
|
706
|
+
io.write(parsed.json ? `${JSON.stringify({ executable })}\n` : `yt-dlp ready: ${executable}\n`);
|
|
707
|
+
} else await fetch(rest, io, cwd);
|
|
701
708
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Explicit WhisperX model-family capability for the official speech program. Importing this package
|
|
4
4
|
selects WhisperX; Runtime registration only binds the resulting alignment Need to a concrete
|
|
5
5
|
execution endpoint. The default Hypit Skill path uses the HypiHub-hosted WhisperX endpoint; the
|
|
6
|
-
trusted local worker remains an explicit
|
|
6
|
+
trusted local worker remains an explicit deployment choice.
|
|
7
7
|
|
|
8
8
|
The package contains no credentials, Python environment or queue. Providers translate the typed
|
|
9
9
|
request directly into provider-neutral `AlignedTranscriptEvidence`. There is no vendor-shaped
|
|
@@ -11,7 +11,8 @@ Evidence wrapper or pass-through normalization node in the graph.
|
|
|
11
11
|
|
|
12
12
|
`<whisperx:SemanticTake>` is the real-media semantic Surface. It consumes one normalized
|
|
13
13
|
`SynchronizedMedia` and exactly one Script Segment. When that Segment contains Tokens, it also
|
|
14
|
-
requires
|
|
14
|
+
requires an explicit lowercase two- or three-letter language code, such as `language="ko"`.
|
|
15
|
+
This package checks the code's form, not a cross-Provider support list. The language is passed directly to
|
|
15
16
|
WhisperX; Script text and audio are not used to choose it implicitly. `@hypit/media-pipeline`
|
|
16
17
|
projects the Take's audio to canonical 16 kHz mono `SpeechEvidenceAudio`; WhisperX sees only those
|
|
17
18
|
bytes. A deterministic local alignment then combines the returned evidence with the Segment and
|
|
@@ -38,4 +39,56 @@ translates their local frames when assembling the final ProgramSpace and complet
|
|
|
38
39
|
|
|
39
40
|
`@hypit/provider-hypihub` is the default concrete adapter; it uploads the canonical evidence audio
|
|
40
41
|
and requests verbose JSON with segment- and word-level timestamps. `@hypit/provider-whisperx-local`
|
|
41
|
-
remains available as an explicit local
|
|
42
|
+
remains available as an explicit local deployment.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Language selection and local preparation
|
|
46
|
+
|
|
47
|
+
```svml
|
|
48
|
+
<whisperx:SemanticTake id="opening" narrative={story}
|
|
49
|
+
segment={story.segment.opening} media={opening-media.media} language="ko"/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The selected service owns which languages it can align. The local service uses its pinned WhisperX
|
|
53
|
+
version's default language-to-alignment-model mapping: the author does not choose weight URLs in
|
|
54
|
+
SVML. ASR size and hardware are separate deployment choices (`expectedModel`, `expectedDevice`,
|
|
55
|
+
`expectedCompute`). A multilingual ASR such as `small` can serve Korean; an English-only `.en`
|
|
56
|
+
model cannot and is rejected explicitly. Unknown alignment languages fail in the service without
|
|
57
|
+
switching language, model family or Provider. Locale aliases such as `zh-CN`, language names and
|
|
58
|
+
`auto` are not interpreted by this Surface; use the service's explicit code.
|
|
59
|
+
|
|
60
|
+
For local execution, merge this into the existing Runtime Profile:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"endpoints": {
|
|
65
|
+
"whisperx.local": {
|
|
66
|
+
"use": "@hypit/provider-whisperx-local",
|
|
67
|
+
"config": { "expectedModel": "small", "alignmentLanguages": ["ko"] }
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"bindings": {
|
|
71
|
+
"@hypit/whisperx@1#whisperx-alignment": "whisperx.local"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`alignmentLanguages` declares resources to prepare on this machine. Each SVML `language` states
|
|
77
|
+
what that particular Take uses; the preparation list never supplies an implicit author language.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
hypit programs prepare --runtime hypit.runtime.json --endpoint whisperx.local
|
|
81
|
+
hypit runtime up --runtime hypit.runtime.json --endpoint whisperx.local
|
|
82
|
+
hypit build video.svrun --runtime hypit.runtime.json --follow
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The Run references the authored SVML. Preparation downloads missing resources without starting the
|
|
86
|
+
service; `runtime up` prepares if needed and starts the helper and Worker. Build only uses prepared
|
|
87
|
+
resources. Adding a language to the same cache requires preparation, not a service restart.
|
|
88
|
+
See the [local Provider README](../provider-whisperx-local/README.md) for cache and hardware choices.
|
|
89
|
+
Other Endpoints needed by the Run must also be prepared and available.
|
|
90
|
+
|
|
91
|
+
For reference analysis rather than an authored SemanticTake, use the same language code with
|
|
92
|
+
`hypit transcribe source.mp4 --language ko --to transcript.json --runtime hypit.runtime.json`.
|
|
93
|
+
Hosted execution uses the selected hosted deployment's models and preparation; the local
|
|
94
|
+
`alignmentLanguages` option does not configure a remote service.
|
|
@@ -6,7 +6,7 @@ import { canonicalize } from "@hypit/protocol";
|
|
|
6
6
|
|
|
7
7
|
import { whisperXRequestForEvidenceAudio } from "./evidence.js";
|
|
8
8
|
import { whisperXCapabilities, whisperXProducers } from "./manifest.js";
|
|
9
|
-
import
|
|
9
|
+
import { parseWhisperXLanguage } from "./types.js";
|
|
10
10
|
|
|
11
11
|
function inline(value: StoredValue, subject: string): CanonicalValue {
|
|
12
12
|
if (value.kind !== "inline") throw new Error(`${subject} must be inline`);
|
|
@@ -19,10 +19,7 @@ export const whisperXComponent = {
|
|
|
19
19
|
producer: whisperXProducers.request,
|
|
20
20
|
handler: ({ inputs }) => {
|
|
21
21
|
const evidence = inline(inputs.evidence!.value, "SpeechEvidenceAudio") as unknown as SpeechEvidenceAudio;
|
|
22
|
-
const language = inline(inputs.language!.value, "WhisperXLanguage")
|
|
23
|
-
if (language !== "en" && language !== "zh" && language !== "es") {
|
|
24
|
-
throw new Error("WhisperXLanguage must be en, zh, or es");
|
|
25
|
-
}
|
|
22
|
+
const language = parseWhisperXLanguage(inline(inputs.language!.value, "WhisperXLanguage"));
|
|
26
23
|
return {
|
|
27
24
|
outputs: {},
|
|
28
25
|
needs: { alignment: canonicalize(whisperXRequestForEvidenceAudio(evidence, { language })) },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertSpeechEvidenceAudioIdentity } from "@hypit/speech";
|
|
2
2
|
import type { SpeechEvidenceAudio } from "@hypit/speech";
|
|
3
3
|
import type { WhisperXAlignmentRequest, WhisperXLanguage } from "./types.js";
|
|
4
|
+
import { parseWhisperXLanguage } from "./types.js";
|
|
4
5
|
|
|
5
6
|
function assert(condition: unknown, message: string): asserts condition {
|
|
6
7
|
if (!condition) throw new Error(message);
|
|
@@ -14,9 +15,9 @@ export function verifyWhisperXAlignmentRequest(value: unknown): WhisperXAlignmen
|
|
|
14
15
|
assert(request.audio?.kind === "blob"
|
|
15
16
|
&& request.audio.mediaType === "audio/wav"
|
|
16
17
|
&& Number.isSafeInteger(request.sampleFrames)
|
|
17
|
-
&& request.sampleFrames > 0
|
|
18
|
-
&& (request.language === "en" || request.language === "zh" || request.language === "es"),
|
|
18
|
+
&& request.sampleFrames > 0,
|
|
19
19
|
"WhisperX alignment request is invalid");
|
|
20
|
+
parseWhisperXLanguage(request.language);
|
|
20
21
|
return request;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -64,6 +65,6 @@ export function whisperXRequestForEvidenceAudio(
|
|
|
64
65
|
return {
|
|
65
66
|
audio: evidence.artifact,
|
|
66
67
|
sampleFrames: evidence.sampleFrames,
|
|
67
|
-
language: options.language,
|
|
68
|
+
language: parseWhisperXLanguage(options.language),
|
|
68
69
|
};
|
|
69
70
|
}
|
|
@@ -12,3 +12,4 @@ export { decodeWhisperXSemanticTakeSurface } from "./surface.js";
|
|
|
12
12
|
export { interpretWhisperXTranscript } from "./transcript.js";
|
|
13
13
|
export type { WhisperXTranscriptResponse } from "./transcript.js";
|
|
14
14
|
export type * from "./types.js";
|
|
15
|
+
export { parseWhisperXLanguage } from "./types.js";
|
|
@@ -38,8 +38,7 @@ export const whisperXMarkupSurfaces = [{
|
|
|
38
38
|
accepts: [mediaTypes.synchronized],
|
|
39
39
|
summary: "Selects the already normalized SynchronizedMedia represented by this Segment." },
|
|
40
40
|
{ name: "language", kind: "literal", required: false,
|
|
41
|
-
|
|
42
|
-
summary: "For a Segment with Tokens, explicitly selects the English, Chinese or Spanish WhisperX models." },
|
|
41
|
+
summary: "For a Segment with Tokens, explicitly states its spoken language code. The selected service owns the available alignment models." },
|
|
43
42
|
],
|
|
44
43
|
ports: [
|
|
45
44
|
{ name: "take", type: speechTypes.semanticTake,
|
|
@@ -50,7 +49,7 @@ export const whisperXMarkupSurfaces = [{
|
|
|
50
49
|
notes: [
|
|
51
50
|
"A Segment with Tokens states all five attributes and sends its prepared audio for alignment.",
|
|
52
51
|
"An empty Segment omits language and maps its authored start/end Anchors directly to the prepared-media boundaries.",
|
|
53
|
-
"For speech,
|
|
52
|
+
"For speech, each alignment call states an explicit lowercase two- or three-letter language code; language is not inferred from Script text or audio.",
|
|
54
53
|
"Importing this package is what selects the WhisperX model family; the Runtime separately binds the alignment Need to an Endpoint.",
|
|
55
54
|
],
|
|
56
55
|
},
|
|
@@ -13,7 +13,7 @@ import { mediaTypes } from "@hypit/media";
|
|
|
13
13
|
|
|
14
14
|
import { whisperXBoundarySemanticTakeFragment, whisperXSemanticTakeFragment } from "./fragment.js";
|
|
15
15
|
import { whisperXTypes } from "./manifest.js";
|
|
16
|
-
import
|
|
16
|
+
import { parseWhisperXLanguage } from "./types.js";
|
|
17
17
|
|
|
18
18
|
function reference(
|
|
19
19
|
element: StructuredElement,
|
|
@@ -61,16 +61,13 @@ export const decodeWhisperXSemanticTakeSurface: StructuredSurfaceHandler = ({ el
|
|
|
61
61
|
fragments: [whisperXBoundarySemanticTakeFragment],
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
-
const language = stringAttribute(element, "language");
|
|
65
|
-
if (language !== "en" && language !== "zh" && language !== "es") {
|
|
66
|
-
throw new Error(`${element.name}.language must be en, zh, or es`);
|
|
67
|
-
}
|
|
64
|
+
const language = parseWhisperXLanguage(stringAttribute(element, "language"), `${element.name}.language`);
|
|
68
65
|
const languageId = `${id}.language`;
|
|
69
66
|
return {
|
|
70
67
|
records: [{
|
|
71
68
|
id: languageId,
|
|
72
69
|
type: whisperXTypes.language,
|
|
73
|
-
value: { kind: "inline", value: language
|
|
70
|
+
value: { kind: "inline", value: language },
|
|
74
71
|
range: element.range,
|
|
75
72
|
}],
|
|
76
73
|
components: [{
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { SpeechEvidenceAudio } from "@hypit/speech";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
/** Explicit language code. Executable language support belongs to the selected service. */
|
|
4
|
+
export type WhisperXLanguage = string;
|
|
5
|
+
|
|
6
|
+
export function parseWhisperXLanguage(value: unknown, subject = "WhisperX language"): WhisperXLanguage {
|
|
7
|
+
if (typeof value !== "string" || !/^[a-z]{2,3}$/u.test(value) || value === "und") {
|
|
8
|
+
throw new Error(`${subject} must be an explicit lowercase two- or three-letter spoken language code, such as en, zh, or ko; auto and und are not explicit languages`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
13
|
export type WhisperXAlignmentRequest = {
|
|
6
14
|
readonly audio: SpeechEvidenceAudio["artifact"];
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
`@hypit/yt-dlp` owns video download for the Hypit Distribution. Video CLI exposes it as:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
+
hypit media prepare-fetch
|
|
6
7
|
hypit media fetch "https://example.com/watch?v=VIDEO_ID" --to references/source.mp4
|
|
7
8
|
```
|
|
8
9
|
|
|
@@ -18,9 +19,14 @@ Runtime Profile and creates no Build.
|
|
|
18
19
|
preparation and overwrite policy. The target extension must be `.mp4`, `.mkv`, `.webm` or `.mov`.
|
|
19
20
|
|
|
20
21
|
The downloader locates its declared `@hypit/yt-dlp-service-runtime` package through the active
|
|
21
|
-
Distribution/package resolver
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
Distribution/package resolver. `prepareVideoDownload()` (`media prepare-fetch`) explicitly runs
|
|
23
|
+
`uv sync --frozen` into `<hostState>/programs/yt-dlp/<version>/.venv`. `downloadVideo()` only invokes
|
|
24
|
+
that prepared executable and refuses a missing environment. Ship `pyproject.toml` and `uv.lock`;
|
|
25
|
+
the caller's location and a `services/` ancestor are irrelevant. `uv` is needed for preparation;
|
|
26
|
+
`ffmpeg` must be on PATH for fetching, and the CLI uses `ffprobe` to report the saved media.
|
|
27
|
+
The default upstream extras include the EJS solver in that locked environment. Fetch explicitly
|
|
28
|
+
selects the calling Node executable, ignores user yt-dlp configuration/plugins, and disables updates
|
|
29
|
+
and remote component acquisition. Fetching source media remains a network operation.
|
|
24
30
|
|
|
25
31
|
The request selects `bv*+ba/b`, with `res:1080,vcodec:h264` format preferences and `--no-playlist`.
|
|
26
32
|
Available source streams determine the result. It stages the download in the OS temporary directory,
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* rather than in a folder. `yt-dlp` is what turns one into a file; everything after that reads the
|
|
6
6
|
* file and never learns where it came from.
|
|
7
7
|
*
|
|
8
|
-
* The tool is a pinned Python dependency
|
|
8
|
+
* The tool is a pinned Python dependency prepared explicitly by `hypit media prepare-fetch`, not a
|
|
9
9
|
* binary the machine happens to carry. `yt-dlp` releases constantly because it is chasing sites that
|
|
10
10
|
* keep changing, so an unpinned copy makes the same link fetch differently on two machines. This is
|
|
11
11
|
* the same shape WhisperX and OpenCV already use for their Python programs.
|
|
@@ -13,9 +13,9 @@
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
14
|
import { copyFile, mkdtemp, readdir, rename, rm } from "node:fs/promises";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import {
|
|
16
|
+
import { extname, join } from "node:path";
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { requireVideoDownload } from "./environment.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Whether this is a link to fetch rather than a path to open.
|
|
@@ -33,19 +33,6 @@ export function isVideoUrl(value: string): boolean {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* The locked uv project is a Distribution package asset, the same way WhisperX and
|
|
38
|
-
* OpenCV locate their Python programs. Walking parents of this file only works
|
|
39
|
-
* while the module still sits above `services/` in a contributor checkout.
|
|
40
|
-
*/
|
|
41
|
-
function serviceProject(): string {
|
|
42
|
-
return dirname(resolveNodePackageResource(
|
|
43
|
-
"@hypit/yt-dlp-service-runtime",
|
|
44
|
-
"pyproject.toml",
|
|
45
|
-
{ from: import.meta.url },
|
|
46
|
-
));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
36
|
/**
|
|
50
37
|
* Fetch one video into exactly the file the caller named.
|
|
51
38
|
*
|
|
@@ -60,10 +47,14 @@ export async function downloadVideo(url: string, target: string): Promise<void>
|
|
|
60
47
|
if (!["mp4", "mkv", "webm", "mov"].includes(container)) {
|
|
61
48
|
throw new Error(`${target} must end in .mp4, .mkv, .webm or .mov`);
|
|
62
49
|
}
|
|
50
|
+
const executable = requireVideoDownload();
|
|
51
|
+
const media = spawnSync("ffmpeg", ["-version"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
52
|
+
if (media.status !== 0) throw new Error("FFmpeg is unavailable on PATH; install your selected media toolchain before fetching video");
|
|
63
53
|
const work = await mkdtemp(join(tmpdir(), "hypit-fetch-"));
|
|
64
54
|
try {
|
|
65
|
-
const result = spawnSync(
|
|
66
|
-
"
|
|
55
|
+
const result = spawnSync(executable, [
|
|
56
|
+
"--ignore-config", "--no-update", "--no-remote-components", "--no-plugin-dirs",
|
|
57
|
+
"--no-js-runtimes", "--js-runtimes", `node:${process.execPath}`,
|
|
67
58
|
"--no-playlist", "--no-progress", "--quiet",
|
|
68
59
|
"--format", "bv*+ba/b",
|
|
69
60
|
"--merge-output-format", container,
|
|
@@ -72,12 +63,7 @@ export async function downloadVideo(url: string, target: string): Promise<void>
|
|
|
72
63
|
url,
|
|
73
64
|
], { encoding: "utf8", windowsHide: true, timeout: 900_000 });
|
|
74
65
|
|
|
75
|
-
if (result.error !== undefined
|
|
76
|
-
throw new Error(
|
|
77
|
-
"uv is not installed, and a link is fetched by a pinned yt-dlp that uv installs. It is the same "
|
|
78
|
-
+ "tool the WhisperX and OpenCV programs need. Install it (https://docs.astral.sh/uv/), or download "
|
|
79
|
-
+ "the video yourself and pass the path instead.");
|
|
80
|
-
}
|
|
66
|
+
if (result.error !== undefined) throw result.error;
|
|
81
67
|
if (result.status !== 0) {
|
|
82
68
|
throw new Error(`yt-dlp could not fetch ${url}: ${(result.stderr ?? "").trim().slice(-2000)}`);
|
|
83
69
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { resolveNodePackageResource } from "@hypit/package-loader-node";
|
|
5
|
+
import { hypitHostStateRoot, pythonEnvironmentCommand } from "@hypit/runtime-host-node";
|
|
6
|
+
|
|
7
|
+
/** One selected, locked environment. Fetching media never invokes its installer. */
|
|
8
|
+
export function videoDownloadEnvironment() {
|
|
9
|
+
const project = dirname(resolveNodePackageResource("@hypit/yt-dlp-service-runtime", "pyproject.toml", { from: import.meta.url }));
|
|
10
|
+
const version = /"yt-dlp(?:\[[^\]]+\])?==([^"]+)"/u.exec(readFileSync(join(project, "pyproject.toml"), "utf8"))?.[1];
|
|
11
|
+
if (version === undefined) throw new Error("yt-dlp runtime must declare its exact upstream version");
|
|
12
|
+
const environment = join(hypitHostStateRoot(), "programs", "yt-dlp", version, ".venv");
|
|
13
|
+
return { project, version, environment, executable: pythonEnvironmentCommand(environment, "yt-dlp") };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function requireVideoDownload(): string {
|
|
17
|
+
const selected = videoDownloadEnvironment();
|
|
18
|
+
const result = spawnSync(selected.executable, ["--ignore-config", "--version"], { encoding: "utf8", windowsHide: true, timeout: 15_000 });
|
|
19
|
+
// PyPI normalizes date-version leading zeroes; the executable retains them.
|
|
20
|
+
const release = (value: string) => value.trim().split(".").map((part) => part.replace(/^0+(?=\d)/u, "")).join(".");
|
|
21
|
+
if (result.status !== 0 || release(result.stdout ?? "") !== release(selected.version)) {
|
|
22
|
+
throw new Error(`yt-dlp ${selected.version} is not ready at ${selected.executable}; run hypit media prepare-fetch. ${result.error?.message ?? result.stderr?.trim() ?? ""}`);
|
|
23
|
+
}
|
|
24
|
+
return selected.executable;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Explicit provisioning. uv owns dependency resolution and the environment. */
|
|
28
|
+
export function prepareVideoDownload(): string {
|
|
29
|
+
const selected = videoDownloadEnvironment();
|
|
30
|
+
const result = spawnSync("uv", ["sync", "--project", selected.project, "--frozen", "--no-dev"], {
|
|
31
|
+
env: { ...process.env, UV_PROJECT_ENVIRONMENT: selected.environment },
|
|
32
|
+
stdio: ["ignore", "inherit", "inherit"], windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
if (result.error || result.status !== 0) throw new Error(`yt-dlp preparation failed: ${result.error?.message ?? `uv exited ${result.status}`}`);
|
|
35
|
+
return requireVideoDownload();
|
|
36
|
+
}
|