@hypit/hypit 0.1.14 → 0.2.1
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 +1 -1
- 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/provider-package/hypit.runtime.json +2 -2
- package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
- package/package.json +2 -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-file/README.md +3 -1
- package/packages/credential-store-os/README.md +4 -0
- package/packages/credential-store-os/src/store.ts +5 -1
- package/packages/credential-store-platform/README.md +62 -0
- package/packages/credential-store-platform/package.json +23 -0
- package/packages/credential-store-platform/src/activation.ts +39 -0
- package/packages/credential-store-platform/src/index.ts +1 -0
- package/packages/credential-store-platform/src/store.ts +89 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -19,7 +19,9 @@ semantic projection combines this evidence with one explicit Script Segment late
|
|
|
19
19
|
## Install
|
|
20
20
|
|
|
21
21
|
For an ordinary installed Distribution, select the local WhisperX Endpoint and run
|
|
22
|
-
`hypit programs
|
|
22
|
+
`hypit programs prepare --endpoint <instance>` after setting `alignmentLanguages` in its Profile.
|
|
23
|
+
`hypit programs up --endpoint <instance>` also prepares resources and starts the service.
|
|
24
|
+
The Runtime creates or reconciles the cold environment in
|
|
23
25
|
the machine Program Home and reuses a running service across projects and sessions. The commands below are contributor/operator
|
|
24
26
|
diagnostics for a deliberately managed deployment:
|
|
25
27
|
|
|
@@ -28,20 +30,28 @@ WhisperX 3.8.6 supports Python 3.10 through 3.13. The checked-in lock selects Py
|
|
|
28
30
|
```bash
|
|
29
31
|
uv python install 3.13
|
|
30
32
|
uv sync --project services/whisperx --frozen
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
HYPIT_WHISPERX_ALIGNMENT_LANGUAGES="zh en" services/whisperx/.venv/bin/hypit-whisperx-prepare
|
|
34
|
+
HYPIT_WHISPERX_ALIGNMENT_LANGUAGES="zh en" services/whisperx/.venv/bin/hypit-whisperx-check --models
|
|
33
35
|
```
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
`hypit-whisperx-prepare` explicitly prepares the selected ASR model, language alignment weights and
|
|
38
|
+
NLTK sentence data. `hypit-whisperx-check --models` reads those resources without downloading.
|
|
39
|
+
The service loads prepared resources only. Hugging Face calls use local-files-only APIs; the
|
|
40
|
+
pinned WhisperX torchaudio and NLTK branches have no equivalent switch, so this dedicated inference
|
|
41
|
+
process replaces their downloader entry points with an error. Preparation runs in a separate process
|
|
42
|
+
and retains their native downloaders. These adaptations belong here, not in Runtime or Core.
|
|
43
|
+
The default Pyannote VAD checkpoint ships inside the pinned WhisperX wheel.
|
|
44
|
+
|
|
45
|
+
Native HF and torch caches are reused by default. An explicit model cache root selects its
|
|
46
|
+
`huggingface/` and `torch/` subdirectories for both preparation and inference. No receipts, hashes or
|
|
47
|
+
parallel resource inventory are maintained. Resource checks read model/tokenizer files and the
|
|
48
|
+
selected language's sentence data. The example shell paths are POSIX; Windows environments use
|
|
49
|
+
`.venv\Scripts\<command>.exe`.
|
|
40
50
|
|
|
41
51
|
## Run
|
|
42
52
|
|
|
43
53
|
```bash
|
|
44
|
-
|
|
54
|
+
services/whisperx/.venv/bin/hypit-whisperx-service
|
|
45
55
|
curl http://127.0.0.1:8765/health
|
|
46
56
|
```
|
|
47
57
|
|
|
@@ -65,7 +75,9 @@ Configuration is deployment state:
|
|
|
65
75
|
| `HYPIT_WHISPERX_COMPUTE` | `int8` on CPU | CTranslate2 compute type |
|
|
66
76
|
| `HYPIT_WHISPERX_BATCH_SIZE` | `8` | bounded ASR batch size |
|
|
67
77
|
| `HYPIT_WHISPERX_INPUT_ROOTS` | OS temp directory | path-separated roots the service may read |
|
|
68
|
-
| `HYPIT_WHISPERX_NLTK_DATA` | user
|
|
78
|
+
| `HYPIT_WHISPERX_NLTK_DATA` | user Hypit cache | selected prepared NLTK data root |
|
|
79
|
+
| `HYPIT_WHISPERX_ALIGNMENT_LANGUAGES` | empty | space-separated languages to prepare; no implicit downloads for others |
|
|
80
|
+
| `HYPIT_WHISPERX_MODEL_CACHE` | upstream caches | optional root for HF and torch model resources |
|
|
69
81
|
| `HYPIT_WHISPERX_MAX_REQUEST_BYTES` | `65536` | HTTP JSON bound |
|
|
70
82
|
| `HYPIT_WHISPERX_MAX_AUDIO_BYTES` | `536870912` | staged canonical WAV bound |
|
|
71
83
|
|
|
@@ -74,9 +86,30 @@ WhisperX version. A mismatch fails before transcription results are accepted.
|
|
|
74
86
|
|
|
75
87
|
The service logs ASR loading, transcription, language-model loading and word alignment where those
|
|
76
88
|
operations run. Completion entries include elapsed times. A loading entry means the library call
|
|
77
|
-
has begun
|
|
89
|
+
has begun reading prepared local resources; downloads appear only in preparation logs.
|
|
78
90
|
Transcripts and audio content are not included in these service progress entries. `/health` answers
|
|
79
|
-
after ASR loading
|
|
91
|
+
after ASR loading; it does not report all language caches as ready. A first request can load a
|
|
92
|
+
prepared aligner into memory, but cannot download it.
|
|
93
|
+
|
|
94
|
+
## Preparing sentence data through a proxy
|
|
95
|
+
|
|
96
|
+
NLTK's downloader refuses a proxied request unless the operator explicitly trusts that proxy:
|
|
97
|
+
it cannot enforce its direct-connection address checks through a proxy. This can stop preparation
|
|
98
|
+
while fetching the NLTK data index, even when uv and Hugging Face downloads work.
|
|
99
|
+
|
|
100
|
+
For a proxy you trust, scope NLTK's native opt-in to the explicit preparation command:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
NLTK_ALLOW_PROXIED_URLOPEN=1 hypit programs prepare --endpoint whisperx.local
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use your selected Endpoint instance id. On PowerShell, set `$env:NLTK_ALLOW_PROXIED_URLOPEN = "1"`
|
|
107
|
+
for that preparation session, then restore its previous value. The preparation process already
|
|
108
|
+
inherits its caller's environment; no Hypit-specific proxy flag is needed. This setting authorizes
|
|
109
|
+
NLTK to use the configured proxy; it neither selects a mirror nor enables inference downloads.
|
|
110
|
+
Without that trust choice, use a deliberately selected direct route or supply the sentence data
|
|
111
|
+
in the selected NLTK data directory before preparation. Hypit does not change the choice automatically.
|
|
112
|
+
Existing usable sentence data is read locally without refreshing the download index.
|
|
80
113
|
|
|
81
114
|
## Package preparation
|
|
82
115
|
|
|
@@ -8,6 +8,7 @@ from . import PROTOCOL, SERVICE_VERSION
|
|
|
8
8
|
from .audio import AudioInputError, CanonicalAudio, read_canonical_audio
|
|
9
9
|
from .config import ServiceConfig
|
|
10
10
|
from .engine import InferenceBusyError, InferenceInputError, normalize_language
|
|
11
|
+
from .resources import UnpreparedResourceError
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class AlignmentEngine(Protocol):
|
|
@@ -82,6 +83,8 @@ class WhisperXApplication:
|
|
|
82
83
|
raise RequestError(400, "INVALID_INPUT", str(error)) from error
|
|
83
84
|
except InferenceBusyError as error:
|
|
84
85
|
raise RequestError(503, "BUSY", str(error)) from error
|
|
86
|
+
except UnpreparedResourceError as error:
|
|
87
|
+
raise RequestError(503, "RESOURCE_NOT_PREPARED", str(error)) from error
|
|
85
88
|
return _json_response(200, result)
|
|
86
89
|
|
|
87
90
|
def error(self, error: RequestError) -> ApplicationResponse:
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import importlib.metadata as metadata
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
|
+
import argparse
|
|
6
7
|
|
|
7
8
|
from . import PROTOCOL, SERVICE_VERSION
|
|
8
9
|
from .config import ServiceConfig
|
|
@@ -12,6 +13,9 @@ from .resources import assert_punkt_tab
|
|
|
12
13
|
def main() -> None:
|
|
13
14
|
if not ((3, 10) <= sys.version_info[:2] < (3, 14)):
|
|
14
15
|
raise RuntimeError(f"WhisperX service requires Python >=3.10,<3.14; got {sys.version.split()[0]}")
|
|
16
|
+
parser = argparse.ArgumentParser()
|
|
17
|
+
parser.add_argument("--models", action="store_true", help="Check selected cached models without downloading")
|
|
18
|
+
arguments = parser.parse_args()
|
|
15
19
|
config = ServiceConfig.from_environment()
|
|
16
20
|
assert_punkt_tab(config.nltk_data_root)
|
|
17
21
|
import numpy # noqa: F401
|
|
@@ -26,6 +30,9 @@ def main() -> None:
|
|
|
26
30
|
}
|
|
27
31
|
if versions["whisperx"] != "3.8.6":
|
|
28
32
|
raise RuntimeError(f"expected whisperx 3.8.6, got {versions['whisperx']}")
|
|
33
|
+
if arguments.models:
|
|
34
|
+
from .models import check_models
|
|
35
|
+
check_models(config)
|
|
29
36
|
print(json.dumps({
|
|
30
37
|
"ok": True,
|
|
31
38
|
"protocol": PROTOCOL,
|
|
@@ -31,6 +31,8 @@ class ServiceConfig:
|
|
|
31
31
|
nltk_data_root: Path
|
|
32
32
|
max_request_bytes: int
|
|
33
33
|
max_audio_bytes: int
|
|
34
|
+
model_cache: Path | None
|
|
35
|
+
alignment_languages: tuple[str, ...]
|
|
34
36
|
|
|
35
37
|
@classmethod
|
|
36
38
|
def from_environment(cls, environment: Mapping[str, str] | None = None) -> "ServiceConfig":
|
|
@@ -57,7 +59,12 @@ class ServiceConfig:
|
|
|
57
59
|
if not roots:
|
|
58
60
|
raise ValueError("HYPIT_WHISPERX_INPUT_ROOTS must contain at least one path")
|
|
59
61
|
|
|
62
|
+
languages = tuple(dict.fromkeys(env.get("HYPIT_WHISPERX_ALIGNMENT_LANGUAGES", "").split()))
|
|
63
|
+
if any(not language.isascii() or not language.isalpha() or language != language.lower() for language in languages):
|
|
64
|
+
raise ValueError("HYPIT_WHISPERX_ALIGNMENT_LANGUAGES must contain lowercase language codes")
|
|
60
65
|
return cls(
|
|
66
|
+
model_cache=Path(env["HYPIT_WHISPERX_MODEL_CACHE"]).expanduser().resolve() if env.get("HYPIT_WHISPERX_MODEL_CACHE") else None,
|
|
67
|
+
alignment_languages=languages,
|
|
61
68
|
port=_positive_integer(env.get("HYPIT_WHISPERX_PORT", "8765"), "HYPIT_WHISPERX_PORT", 65535),
|
|
62
69
|
model=model,
|
|
63
70
|
device=device,
|
|
@@ -10,7 +10,8 @@ from typing import Any, Mapping
|
|
|
10
10
|
|
|
11
11
|
from .audio import CanonicalAudio
|
|
12
12
|
from .config import ServiceConfig
|
|
13
|
-
from .resources import assert_punkt_tab
|
|
13
|
+
from .resources import assert_punkt_tab, assert_sentence_data
|
|
14
|
+
from .models import asr_path, alignment_path, alignment_selection, hf_cache, torch_cache, use_local_resources_only
|
|
14
15
|
|
|
15
16
|
logger = logging.getLogger("hypit.whisperx")
|
|
16
17
|
|
|
@@ -91,6 +92,7 @@ def normalize_alignment(language: str, raw: Mapping[str, Any]) -> dict[str, obje
|
|
|
91
92
|
|
|
92
93
|
class WhisperXEngine:
|
|
93
94
|
def __init__(self, config: ServiceConfig):
|
|
95
|
+
use_local_resources_only()
|
|
94
96
|
assert_punkt_tab(config.nltk_data_root)
|
|
95
97
|
os.environ["NLTK_DATA"] = str(config.nltk_data_root)
|
|
96
98
|
try:
|
|
@@ -109,11 +111,12 @@ class WhisperXEngine:
|
|
|
109
111
|
self._inference_lock = threading.Lock()
|
|
110
112
|
self._alignment_models: dict[str, tuple[object, object]] = {}
|
|
111
113
|
started = time.monotonic()
|
|
112
|
-
logger.info("loading ASR model=%s
|
|
114
|
+
logger.info("loading prepared ASR model=%s", config.model)
|
|
113
115
|
self._asr = whisperx_module.load_model(
|
|
114
|
-
config
|
|
116
|
+
asr_path(config),
|
|
115
117
|
config.device,
|
|
116
118
|
compute_type=config.compute,
|
|
119
|
+
local_files_only=True,
|
|
117
120
|
)
|
|
118
121
|
logger.info("ASR model ready in %.1fs", time.monotonic() - started)
|
|
119
122
|
self._whisperx_version = metadata.version("whisperx")
|
|
@@ -131,10 +134,18 @@ class WhisperXEngine:
|
|
|
131
134
|
model = self._alignment_models.get(language)
|
|
132
135
|
if model is None:
|
|
133
136
|
started = time.monotonic()
|
|
134
|
-
logger.info("loading alignment model for language=%s
|
|
137
|
+
logger.info("loading prepared alignment model for language=%s", language)
|
|
138
|
+
try:
|
|
139
|
+
kind, selected = alignment_path(self._config, language)
|
|
140
|
+
except ValueError as error:
|
|
141
|
+
raise InferenceInputError(str(error)) from error
|
|
142
|
+
assert_sentence_data(self._config.nltk_data_root, language)
|
|
135
143
|
loaded = self._whisperx.load_align_model(
|
|
136
144
|
language_code=language,
|
|
137
145
|
device=self._config.device,
|
|
146
|
+
model_name=selected,
|
|
147
|
+
model_dir=str(torch_cache(self._config)) if kind == "torchaudio" else hf_cache(self._config),
|
|
148
|
+
model_cache_only=True,
|
|
138
149
|
)
|
|
139
150
|
if not isinstance(loaded, tuple) or len(loaded) != 2:
|
|
140
151
|
raise RuntimeError("WhisperX returned an invalid alignment model")
|
|
@@ -148,6 +159,11 @@ class WhisperXEngine:
|
|
|
148
159
|
raise InferenceInputError(
|
|
149
160
|
f"model {self._config.model!r} is English-only and cannot transcribe {language!r}"
|
|
150
161
|
)
|
|
162
|
+
if language is not None:
|
|
163
|
+
try:
|
|
164
|
+
alignment_selection(language)
|
|
165
|
+
except ValueError as error:
|
|
166
|
+
raise InferenceInputError(str(error)) from error
|
|
151
167
|
samples = self._numpy.frombuffer(audio.pcm_s16le, dtype="<i2").astype(self._numpy.float32)
|
|
152
168
|
samples /= 32768.0
|
|
153
169
|
samples = self._numpy.ascontiguousarray(samples)
|
|
@@ -162,7 +178,7 @@ class WhisperXEngine:
|
|
|
162
178
|
language=language,
|
|
163
179
|
)
|
|
164
180
|
logger.info("transcription completed in %.1fs", time.monotonic() - started)
|
|
165
|
-
detected = transcription.get("language") or language
|
|
181
|
+
detected = transcription.get("language") or language
|
|
166
182
|
if not isinstance(detected, str) or not detected.strip():
|
|
167
183
|
raise RuntimeError("WhisperX did not return a valid language")
|
|
168
184
|
segments = transcription.get("segments", [])
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""WhisperX-owned model selection and explicit preparation.
|
|
2
|
+
|
|
3
|
+
The pinned upstream libraries own their model tables and cache formats. The service
|
|
4
|
+
uses those same caches, but has no authority to fetch missing execution resources.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
from .resources import UnpreparedResourceError
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .config import ServiceConfig
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def missing_resource(subject: str) -> UnpreparedResourceError:
|
|
18
|
+
return UnpreparedResourceError(f"{subject} is not prepared; run hypit programs prepare --endpoint <your-whisperx-endpoint> with the selected model and alignmentLanguages")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def forbid_inference_downloads(*args: object, **kwargs: object) -> None:
|
|
22
|
+
raise missing_resource("A WhisperX execution resource")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def use_local_resources_only() -> None:
|
|
26
|
+
# This is the dedicated inference process, not the preparation process or Host.
|
|
27
|
+
# WhisperX 3.8.6 has no no-download option for its torchaudio and NLTK branches.
|
|
28
|
+
# Refuse at their download boundary as well as using HF's local-files-only API.
|
|
29
|
+
import nltk
|
|
30
|
+
import torch.hub
|
|
31
|
+
nltk.download = forbid_inference_downloads
|
|
32
|
+
torch.hub.download_url_to_file = forbid_inference_downloads
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def hf_cache(config: ServiceConfig) -> str | None:
|
|
36
|
+
return str(config.model_cache / "huggingface") if config.model_cache is not None else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def torch_cache(config: ServiceConfig) -> Path:
|
|
40
|
+
if config.model_cache is not None:
|
|
41
|
+
return config.model_cache / "torch"
|
|
42
|
+
import torch.hub
|
|
43
|
+
return Path(torch.hub.get_dir()) / "checkpoints"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def alignment_selection(language: str) -> tuple[str, str]:
|
|
47
|
+
from whisperx.alignment import DEFAULT_ALIGN_MODELS_HF, DEFAULT_ALIGN_MODELS_TORCH
|
|
48
|
+
if language in DEFAULT_ALIGN_MODELS_TORCH:
|
|
49
|
+
return "torchaudio", DEFAULT_ALIGN_MODELS_TORCH[language]
|
|
50
|
+
if language in DEFAULT_ALIGN_MODELS_HF:
|
|
51
|
+
return "huggingface", DEFAULT_ALIGN_MODELS_HF[language]
|
|
52
|
+
raise ValueError(f"WhisperX has no default alignment model for language {language!r}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def asr_path(config: ServiceConfig, *, prepare: bool = False) -> str:
|
|
56
|
+
if prepare:
|
|
57
|
+
try:
|
|
58
|
+
return asr_path(config)
|
|
59
|
+
except RuntimeError:
|
|
60
|
+
pass
|
|
61
|
+
if Path(config.model).is_dir():
|
|
62
|
+
path = Path(config.model).resolve()
|
|
63
|
+
else:
|
|
64
|
+
from faster_whisper.utils import download_model
|
|
65
|
+
try:
|
|
66
|
+
path = Path(download_model(config.model, cache_dir=hf_cache(config), local_files_only=not prepare))
|
|
67
|
+
except Exception as error:
|
|
68
|
+
if prepare:
|
|
69
|
+
raise
|
|
70
|
+
raise missing_resource(f"ASR model {config.model!r}") from error
|
|
71
|
+
for name in ("model.bin", "config.json", "tokenizer.json"):
|
|
72
|
+
if not (path / name).is_file():
|
|
73
|
+
raise missing_resource(f"ASR model {config.model!r}: {name}")
|
|
74
|
+
return str(path)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def alignment_path(config: ServiceConfig, language: str) -> tuple[str, str]:
|
|
78
|
+
kind, name = alignment_selection(language)
|
|
79
|
+
if kind == "torchaudio":
|
|
80
|
+
import torchaudio
|
|
81
|
+
bundle = getattr(torchaudio.pipelines, name)
|
|
82
|
+
# The pinned torchaudio bundle owns its checkpoint URL. Its cache uses
|
|
83
|
+
# that URL's basename; do not duplicate the language or URL table here.
|
|
84
|
+
checkpoint = torch_cache(config) / Path(urlparse(bundle._path).path).name
|
|
85
|
+
if not checkpoint.is_file():
|
|
86
|
+
raise missing_resource(f"Alignment model {name!r} for {language}")
|
|
87
|
+
return kind, name
|
|
88
|
+
from huggingface_hub import snapshot_download
|
|
89
|
+
try:
|
|
90
|
+
path = Path(snapshot_download(name, cache_dir=hf_cache(config), local_files_only=True))
|
|
91
|
+
except Exception as error:
|
|
92
|
+
raise missing_resource(f"Alignment model {name!r} for {language}") from error
|
|
93
|
+
weights = any((path / item).is_file() for item in (
|
|
94
|
+
"model.safetensors", "pytorch_model.bin", "model.safetensors.index.json", "pytorch_model.bin.index.json",
|
|
95
|
+
))
|
|
96
|
+
if not weights or not (path / "config.json").is_file():
|
|
97
|
+
raise missing_resource(f"Alignment model {name!r} for {language}")
|
|
98
|
+
# Actually read tokenizer/processor resources, not only the cache directory.
|
|
99
|
+
from transformers import Wav2Vec2Processor
|
|
100
|
+
try:
|
|
101
|
+
Wav2Vec2Processor.from_pretrained(str(path), local_files_only=True)
|
|
102
|
+
except (OSError, ValueError) as error:
|
|
103
|
+
raise missing_resource(f"Alignment processor {name!r} for {language}") from error
|
|
104
|
+
return kind, str(path)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def check_models(config: ServiceConfig) -> None:
|
|
108
|
+
asr_path(config)
|
|
109
|
+
from .resources import assert_sentence_data
|
|
110
|
+
for language in config.alignment_languages:
|
|
111
|
+
alignment_path(config, language)
|
|
112
|
+
assert_sentence_data(config.nltk_data_root, language)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def prepare_models(config: ServiceConfig) -> None:
|
|
116
|
+
print(f"Preparing ASR model {config.model}", flush=True)
|
|
117
|
+
asr_path(config, prepare=True)
|
|
118
|
+
from whisperx import load_align_model
|
|
119
|
+
for language in config.alignment_languages:
|
|
120
|
+
kind, name = alignment_selection(language)
|
|
121
|
+
try:
|
|
122
|
+
alignment_path(config, language)
|
|
123
|
+
except RuntimeError:
|
|
124
|
+
print(f"Preparing alignment model {name} for {language}", flush=True)
|
|
125
|
+
cache = str(torch_cache(config)) if kind == "torchaudio" else hf_cache(config)
|
|
126
|
+
if cache is not None:
|
|
127
|
+
Path(cache).mkdir(parents=True, exist_ok=True)
|
|
128
|
+
# Upstream downloads and loads the exact selected checkpoint; the
|
|
129
|
+
# returned model is discarded here, not kept in the service's RAM.
|
|
130
|
+
load_align_model(language_code=language, device="cpu", model_dir=cache)
|
|
131
|
+
alignment_path(config, language)
|
|
@@ -3,17 +3,35 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
from .resources import
|
|
6
|
+
from .resources import prepare_punkt_tab, assert_sentence_data
|
|
7
|
+
from .config import ServiceConfig
|
|
8
|
+
from .models import prepare_models, alignment_selection
|
|
9
|
+
from dataclasses import replace
|
|
7
10
|
|
|
8
11
|
|
|
9
12
|
def main() -> None:
|
|
10
|
-
|
|
13
|
+
config = ServiceConfig.from_environment()
|
|
14
|
+
parser = argparse.ArgumentParser(description="Prepare the selected WhisperX models and sentence data")
|
|
11
15
|
parser.add_argument(
|
|
12
16
|
"--nltk-data",
|
|
13
17
|
type=Path,
|
|
14
|
-
default=
|
|
18
|
+
default=config.nltk_data_root,
|
|
15
19
|
help="NLTK data root (defaults to the SVML user cache)",
|
|
16
20
|
)
|
|
17
21
|
arguments = parser.parse_args()
|
|
22
|
+
config = replace(config, nltk_data_root=arguments.nltk_data)
|
|
23
|
+
for language in config.alignment_languages:
|
|
24
|
+
if config.model.endswith(".en") and language != "en":
|
|
25
|
+
raise ValueError(f"ASR model {config.model!r} is English-only; select a multilingual model to prepare {language}")
|
|
26
|
+
alignment_selection(language)
|
|
18
27
|
path = prepare_punkt_tab(arguments.nltk_data)
|
|
28
|
+
for language in config.alignment_languages:
|
|
29
|
+
try:
|
|
30
|
+
assert_sentence_data(arguments.nltk_data, language)
|
|
31
|
+
except RuntimeError:
|
|
32
|
+
import nltk
|
|
33
|
+
if not nltk.download("punkt_tab", download_dir=str(arguments.nltk_data), quiet=False, force=True, raise_on_error=True):
|
|
34
|
+
raise RuntimeError("NLTK could not prepare sentence data")
|
|
35
|
+
assert_sentence_data(arguments.nltk_data, language)
|
|
36
|
+
prepare_models(config)
|
|
19
37
|
print(path)
|
|
@@ -3,6 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
|
|
6
|
+
class UnpreparedResourceError(RuntimeError):
|
|
7
|
+
"""A selected local execution resource needs explicit preparation."""
|
|
8
|
+
|
|
9
|
+
|
|
6
10
|
def default_nltk_data_root() -> Path:
|
|
7
11
|
return Path.home() / ".cache" / "hypit" / "whisperx" / "nltk_data"
|
|
8
12
|
|
|
@@ -11,12 +15,11 @@ def assert_punkt_tab(root: Path) -> None:
|
|
|
11
15
|
import nltk
|
|
12
16
|
|
|
13
17
|
location = str(root.expanduser().resolve())
|
|
14
|
-
|
|
15
|
-
nltk.data.path.insert(0, location)
|
|
18
|
+
nltk.data.path[:] = [location]
|
|
16
19
|
try:
|
|
17
20
|
nltk.data.find("tokenizers/punkt_tab", paths=[location])
|
|
18
21
|
except LookupError as error:
|
|
19
|
-
raise
|
|
22
|
+
raise UnpreparedResourceError(
|
|
20
23
|
"NLTK punkt_tab data is unavailable; run `hypit-whisperx-prepare`"
|
|
21
24
|
) from error
|
|
22
25
|
|
|
@@ -39,3 +42,16 @@ def prepare_punkt_tab(root: Path) -> Path:
|
|
|
39
42
|
raise RuntimeError("NLTK could not install punkt_tab")
|
|
40
43
|
assert_punkt_tab(root)
|
|
41
44
|
return target
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def assert_sentence_data(root: Path, language: str) -> None:
|
|
48
|
+
from whisperx.utils import PUNKT_LANGUAGES
|
|
49
|
+
from nltk.tokenize.punkt import load_punkt_params
|
|
50
|
+
from nltk.data import FileSystemPathPointer
|
|
51
|
+
# Use the same sentence tokenizer selection as the pinned WhisperX version.
|
|
52
|
+
name = PUNKT_LANGUAGES.get(language, "english")
|
|
53
|
+
try:
|
|
54
|
+
assert_punkt_tab(root)
|
|
55
|
+
load_punkt_params(FileSystemPathPointer(str(root / "tokenizers" / "punkt_tab" / name)))
|
|
56
|
+
except (OSError, ValueError) as error:
|
|
57
|
+
raise UnpreparedResourceError(f"NLTK sentence data for {language} ({name}) is unavailable; run hypit-whisperx-prepare") from error
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
# Pinned yt-dlp
|
|
2
2
|
|
|
3
|
-
This project
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This project owns the locked downloader and its EJS solver dependencies. Prepare it explicitly with
|
|
4
|
+
`hypit media prepare-fetch`; `hypit media fetch` only uses the resulting executable and never runs uv.
|
|
5
|
+
The preparation command reports the executable path under the machine Host state. Use that exact
|
|
6
|
+
path for `--version`, `--help` or deliberately chosen site-specific options. On Windows it is an
|
|
7
|
+
`.exe` inside the virtual environment's `Scripts` directory.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
For a contributor-managed environment, run `uv sync --project services/yt-dlp --frozen` explicitly,
|
|
10
|
+
then invoke `.venv/bin/yt-dlp` (Windows: `.venv\Scripts\yt-dlp.exe`) from that project.
|
|
11
|
+
Direct invocation accepts upstream options and config; the Hypit fetch wrapper instead ignores
|
|
12
|
+
user config/plugins, disables updates and remote components, and uses its calling Node executable
|
|
13
|
+
for JavaScript challenges. The locked extras supply the solver before fetching begins.
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
Replace the project path with this directory in the installed Hypit Distribution. Direct invocation
|
|
15
|
-
accepts yt-dlp's own options, including source-specific format or authentication settings. Choose
|
|
16
|
-
those for the actual download. `ffmpeg` on PATH merges separate video and audio streams.
|
|
17
|
-
|
|
18
|
-
`packages/yt-dlp` is the Node wrapper used by Video CLI. It stages a download and saves the requested
|
|
19
|
-
project file; later commands read that file.
|
|
15
|
+
`ffmpeg` on PATH merges separate video and audio streams; the CLI also requires `ffprobe` for the
|
|
16
|
+
saved-file report. These tools are supplied by the operator's package manager, never installed by
|
|
17
|
+
fetch. Site support follows the selected yt-dlp extractors. A source requiring authentication or
|
|
18
|
+
additional options can use an explicit direct invocation and then supply the local file to Hypit.
|
|
@@ -6,7 +6,7 @@ readme = "README.md"
|
|
|
6
6
|
requires-python = ">=3.10,<3.14"
|
|
7
7
|
license = { text = "Hypit Open Source License (Apache-2.0 with additional conditions); see LICENSE at the repository root" }
|
|
8
8
|
dependencies = [
|
|
9
|
-
"yt-dlp==2026.8.19",
|
|
9
|
+
"yt-dlp[default]==2026.8.19",
|
|
10
10
|
]
|
|
11
11
|
|
|
12
12
|
[tool.uv]
|