@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.
Files changed (82) hide show
  1. package/README.md +2 -2
  2. package/dist/public/browser-capture.d.ts +9 -3
  3. package/dist/public/caption.d.ts +2 -0
  4. package/dist/public/narrative.d.ts +2 -0
  5. package/dist/public/runtime-kit.d.ts +2 -2
  6. package/dist/public/speech.d.ts +2 -0
  7. package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
  8. package/package.json +1 -1
  9. package/packages/browser-capture/README.md +9 -2
  10. package/packages/browser-capture/package.json +5 -0
  11. package/packages/browser-capture/src/browser.ts +22 -8
  12. package/packages/browser-capture/src/index.ts +8 -2
  13. package/packages/caption/README.md +1 -1
  14. package/packages/caption/src/display.ts +1 -0
  15. package/packages/caption-fine/README.md +19 -5
  16. package/packages/caption-fine/package.json +1 -0
  17. package/packages/caption-fine/src/joined-box.ts +114 -0
  18. package/packages/caption-fine/src/manifest.ts +1 -1
  19. package/packages/caption-fine/src/render.ts +99 -92
  20. package/packages/caption-fine/src/spacing.ts +8 -61
  21. package/packages/caption-fine-studio/src/index.ts +2 -4
  22. package/packages/cli/README.md +3 -2
  23. package/packages/cli/src/arguments.ts +2 -2
  24. package/packages/cli/src/command.ts +1 -1
  25. package/packages/cli/src/commands/environment.ts +9 -2
  26. package/packages/cli/src/machine-view.ts +1 -1
  27. package/packages/cli/src/output.ts +1 -0
  28. package/packages/credential-store-os/src/store.ts +5 -1
  29. package/packages/narrative/README.md +3 -1
  30. package/packages/narrative/src/schema.ts +1 -0
  31. package/packages/narrative/src/types.ts +2 -0
  32. package/packages/provider-whisperx-local/README.md +26 -10
  33. package/packages/provider-whisperx-local/src/activation.ts +10 -1
  34. package/packages/provider-whisperx-local/src/program.ts +8 -4
  35. package/packages/runtime-host-node/src/index.ts +1 -0
  36. package/packages/runtime-kit/README.md +6 -2
  37. package/packages/runtime-kit/src/index.ts +2 -2
  38. package/packages/runtime-local/README.md +6 -1
  39. package/packages/runtime-local/src/host.ts +2 -0
  40. package/packages/runtime-local/src/index.ts +1 -0
  41. package/packages/runtime-local/src/programs.ts +53 -17
  42. package/packages/script/README.md +81 -34
  43. package/packages/script/bin/migrate-0.2.mjs +90 -0
  44. package/packages/script/src/edit.ts +28 -60
  45. package/packages/script/src/lexical.ts +59 -130
  46. package/packages/script/src/manifest.ts +11 -11
  47. package/packages/script/src/narrative.ts +4 -2
  48. package/packages/script/src/parser.ts +140 -193
  49. package/packages/script/src/types.ts +2 -3
  50. package/packages/studio/src/ui/code.ts +1 -1
  51. package/packages/studio/src/ui/main.ts +2 -2
  52. package/packages/studio/src/ui/markers.ts +3 -3
  53. package/packages/studio/src/ui/selection.ts +1 -1
  54. package/packages/studio/src/ui/syntax.ts +42 -21
  55. package/packages/temporal-markup/README.md +1 -1
  56. package/packages/video-cli/README.md +9 -5
  57. package/packages/video-cli/src/capture.ts +17 -2
  58. package/packages/video-cli/src/creation.ts +4 -6
  59. package/packages/video-cli/src/media.ts +10 -3
  60. package/packages/whisperx/README.md +56 -3
  61. package/packages/whisperx/src/component.ts +2 -5
  62. package/packages/whisperx/src/evidence.ts +4 -3
  63. package/packages/whisperx/src/index.ts +1 -0
  64. package/packages/whisperx/src/manifest.ts +2 -3
  65. package/packages/whisperx/src/surface.ts +3 -6
  66. package/packages/whisperx/src/types.ts +9 -1
  67. package/packages/yt-dlp/README.md +9 -3
  68. package/packages/yt-dlp/package.json +2 -1
  69. package/packages/yt-dlp/src/download.ts +10 -24
  70. package/packages/yt-dlp/src/environment.ts +36 -0
  71. package/packages/yt-dlp/src/index.ts +1 -0
  72. package/services/whisperx/README.md +45 -12
  73. package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
  74. package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
  75. package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
  76. package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
  77. package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
  78. package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
  79. package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
  80. package/services/yt-dlp/README.md +14 -15
  81. package/services/yt-dlp/pyproject.toml +1 -1
  82. package/services/yt-dlp/uv.lock +513 -2
@@ -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 up --endpoint <instance>`. The Runtime creates or reconciles the cold environment in
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
- uv run --project services/whisperx --frozen hypit-whisperx-prepare
32
- uv run --project services/whisperx --frozen hypit-whisperx-check
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
- The first model start may download ASR weights; first use of a language can download its alignment
36
- model during the request. Production should put the relevant
37
- Hugging Face cache on persistent storage. `hypit-whisperx-prepare` separately installs NLTK's
38
- `punkt_tab` sentence data through NLTK's own downloader. This resource is required by WhisperX
39
- alignment and is prepared explicitly before the warm service starts, never inside an inference request.
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
- uv run --project services/whisperx --frozen hypit-whisperx-service
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 SVML cache | prepared, identity-checked NLTK data root |
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 and may include a weight download; the download client supplies any transfer progress.
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, so a healthy service may still prepare a language model on its first request.
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; uncached weights may download during this call", config.model)
114
+ logger.info("loading prepared ASR model=%s", config.model)
113
115
  self._asr = whisperx_module.load_model(
114
- config.model,
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; uncached weights may download during this call", language)
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 or "en"
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 default_nltk_data_root, prepare_punkt_tab
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
- parser = argparse.ArgumentParser(description="Install NLTK data required by Hypit WhisperX")
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=default_nltk_data_root(),
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
- if location not in nltk.data.path:
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 RuntimeError(
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 holds the locked Python downloader used by `hypit media fetch`. `uv` installs the
4
- selected version and its dependencies when invoked. Site support follows that version's extractors;
5
- the downloaded file preserves the source used by the production.
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
- It is invoked one command at a time rather than run as a service:
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
- ```bash
10
- uv run --project "/path/to/hypit/services/yt-dlp" --frozen yt-dlp --version
11
- uv run --project "/path/to/hypit/services/yt-dlp" --frozen yt-dlp --help
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]