@hypit/hypit 0.1.13 → 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 (130) hide show
  1. package/README.md +37 -3
  2. package/bin/hypit.mjs +0 -2
  3. package/dist/public/browser-capture.d.ts +9 -3
  4. package/dist/public/caption.d.ts +2 -0
  5. package/dist/public/narrative.d.ts +2 -0
  6. package/dist/public/runtime-kit.d.ts +2 -2
  7. package/dist/public/speech.d.ts +2 -0
  8. package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
  9. package/package.json +2 -1
  10. package/packages/browser-capture/README.md +9 -2
  11. package/packages/browser-capture/package.json +5 -0
  12. package/packages/browser-capture/src/browser.ts +22 -8
  13. package/packages/browser-capture/src/index.ts +8 -2
  14. package/packages/caption/README.md +1 -1
  15. package/packages/caption/src/display.ts +1 -0
  16. package/packages/caption-fine/README.md +19 -5
  17. package/packages/caption-fine/package.json +1 -0
  18. package/packages/caption-fine/src/joined-box.ts +114 -0
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +99 -92
  21. package/packages/caption-fine/src/spacing.ts +8 -61
  22. package/packages/caption-fine-studio/src/index.ts +2 -4
  23. package/packages/cli/README.md +3 -2
  24. package/packages/cli/package.json +1 -0
  25. package/packages/cli/src/arguments.ts +2 -2
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +21 -9
  28. package/packages/cli/src/machine-view.ts +1 -1
  29. package/packages/cli/src/main.ts +2 -1
  30. package/packages/cli/src/oauth.ts +50 -7
  31. package/packages/cli/src/output.ts +1 -0
  32. package/packages/credential-store-file/README.md +60 -0
  33. package/packages/credential-store-file/package.json +21 -0
  34. package/packages/credential-store-file/src/activation.ts +29 -0
  35. package/packages/credential-store-file/src/index.ts +1 -0
  36. package/packages/credential-store-file/src/store.ts +89 -0
  37. package/packages/credential-store-os/src/store.ts +5 -1
  38. package/packages/fonts-open/src/surface.ts +8 -2
  39. package/packages/hyperframes/README.md +16 -2
  40. package/packages/hyperframes/src/browser-program.ts +6 -1
  41. package/packages/hyperframes/src/document.ts +31 -21
  42. package/packages/hyperframes/src/project.ts +11 -20
  43. package/packages/media-execution/README.md +7 -0
  44. package/packages/media-execution/src/execute.ts +5 -5
  45. package/packages/media-execution/src/index.ts +1 -1
  46. package/packages/media-execution/src/process-env.ts +20 -0
  47. package/packages/media-execution/src/surface.ts +83 -76
  48. package/packages/narrative/README.md +3 -1
  49. package/packages/narrative/src/schema.ts +1 -0
  50. package/packages/narrative/src/types.ts +2 -0
  51. package/packages/package-loader-node/README.md +10 -0
  52. package/packages/package-loader-node/src/index.ts +1 -0
  53. package/packages/package-loader-node/src/loader.ts +25 -5
  54. package/packages/package-loader-node/src/location.ts +11 -2
  55. package/packages/provider-hyperframes-local/README.md +81 -8
  56. package/packages/provider-hyperframes-local/package.json +13 -3
  57. package/packages/provider-hyperframes-local/src/activation.ts +20 -6
  58. package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
  59. package/packages/provider-hyperframes-local/src/browser.ts +118 -0
  60. package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
  61. package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
  62. package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
  63. package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
  64. package/packages/provider-hyperframes-local/src/capture.ts +12 -5
  65. package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
  66. package/packages/provider-hyperframes-local/src/options.ts +2 -2
  67. package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
  68. package/packages/provider-hyperframes-local/src/process.ts +22 -0
  69. package/packages/provider-hyperframes-local/src/program.ts +25 -33
  70. package/packages/provider-hyperframes-local/src/provider.ts +4 -7
  71. package/packages/provider-hyperframes-local/src/render.ts +9 -4
  72. package/packages/provider-whisperx-local/README.md +26 -10
  73. package/packages/provider-whisperx-local/src/activation.ts +10 -1
  74. package/packages/provider-whisperx-local/src/program.ts +8 -4
  75. package/packages/runtime-host-node/README.md +5 -0
  76. package/packages/runtime-host-node/src/index.ts +8 -2
  77. package/packages/runtime-host-node/src/packages.ts +13 -7
  78. package/packages/runtime-kit/README.md +6 -2
  79. package/packages/runtime-kit/src/index.ts +2 -2
  80. package/packages/runtime-local/README.md +16 -1
  81. package/packages/runtime-local/src/config.ts +1 -1
  82. package/packages/runtime-local/src/credentials.ts +10 -5
  83. package/packages/runtime-local/src/host.ts +2 -0
  84. package/packages/runtime-local/src/index.ts +1 -0
  85. package/packages/runtime-local/src/programs.ts +53 -17
  86. package/packages/script/README.md +81 -34
  87. package/packages/script/bin/migrate-0.2.mjs +90 -0
  88. package/packages/script/src/edit.ts +28 -60
  89. package/packages/script/src/lexical.ts +59 -130
  90. package/packages/script/src/manifest.ts +11 -11
  91. package/packages/script/src/narrative.ts +4 -2
  92. package/packages/script/src/parser.ts +140 -193
  93. package/packages/script/src/types.ts +2 -3
  94. package/packages/studio/src/preview/runtime-shim.ts +17 -3
  95. package/packages/studio/src/ui/code.ts +1 -1
  96. package/packages/studio/src/ui/main.ts +2 -2
  97. package/packages/studio/src/ui/markers.ts +3 -3
  98. package/packages/studio/src/ui/selection.ts +1 -1
  99. package/packages/studio/src/ui/syntax.ts +42 -21
  100. package/packages/temporal-markup/README.md +1 -1
  101. package/packages/video-cli/README.md +14 -6
  102. package/packages/video-cli/package.json +1 -0
  103. package/packages/video-cli/src/capture.ts +17 -2
  104. package/packages/video-cli/src/creation.ts +4 -6
  105. package/packages/video-cli/src/distribution.ts +3 -10
  106. package/packages/video-cli/src/media.ts +10 -3
  107. package/packages/video-cli/src/version.ts +1 -1
  108. package/packages/whisperx/README.md +56 -3
  109. package/packages/whisperx/src/component.ts +2 -5
  110. package/packages/whisperx/src/evidence.ts +4 -3
  111. package/packages/whisperx/src/index.ts +1 -0
  112. package/packages/whisperx/src/manifest.ts +2 -3
  113. package/packages/whisperx/src/surface.ts +3 -6
  114. package/packages/whisperx/src/types.ts +9 -1
  115. package/packages/yt-dlp/README.md +10 -3
  116. package/packages/yt-dlp/package.json +5 -0
  117. package/packages/yt-dlp/src/download.ts +11 -29
  118. package/packages/yt-dlp/src/environment.ts +36 -0
  119. package/packages/yt-dlp/src/index.ts +1 -0
  120. package/services/whisperx/README.md +45 -12
  121. package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
  122. package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
  123. package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
  124. package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
  125. package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
  126. package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
  127. package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
  128. package/services/yt-dlp/README.md +14 -15
  129. package/services/yt-dlp/pyproject.toml +1 -1
  130. package/services/yt-dlp/uv.lock +513 -2
@@ -1,54 +1,46 @@
1
- import { execFile } from "node:child_process";
1
+ import { fileURLToPath } from "node:url";
2
+ import { join } from "node:path";
2
3
 
3
4
  import { probeMediaToolchain } from "@hypit/media-execution";
4
5
  import type { ManagedProgram, ManagedProgramState } from "@hypit/runtime-kit";
5
-
6
- function run(executable: string, args: readonly string[]): Promise<{ ok: boolean; output: string }> {
7
- return new Promise((resolve) => {
8
- execFile(executable, [...args], { timeout: 15_000, shell: false, windowsHide: true }, (error, stdout, stderr) => {
9
- resolve(error === null
10
- ? { ok: true, output: stdout.trim() }
11
- : { ok: false, output: (stderr.trim() || error.message).split("\n").at(-1) ?? "" });
12
- });
13
- });
14
- }
6
+ import { browserCacheDirectory, browserDownloadBaseUrl, browserDownloadUrl, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
7
+ import type { BrowserOptions } from "./browser.js";
15
8
 
16
9
  /**
17
- * HyperFrames owns the browser cache behind its CLI. It is deployment state,
18
- * not an author dependency, so the Endpoint lifecycle prepares it only
19
- * when this Provider is selected.
10
+ * This Provider owns browser selection and preparation. Probes never install;
11
+ * rendering receives the same selected executable instead of invoking engine discovery.
20
12
  */
21
13
  export function localHyperframesBrowserProgram(
22
- input: {
14
+ input: BrowserOptions & {
23
15
  readonly id: string;
24
16
  readonly nodePath: string;
25
- readonly hyperframesCliPath: string | (() => string);
26
17
  readonly ffprobePath: string;
27
18
  readonly ffmpegPath?: string;
28
19
  },
29
20
  ): ManagedProgram {
30
- const cliPath = () => typeof input.hyperframesCliPath === "string" ? input.hyperframesCliPath : input.hyperframesCliPath();
21
+ const version = selectedBrowserVersion(input);
22
+ const selectedPath = browserExecutablePath(input);
23
+ const baseUrl = browserDownloadBaseUrl(input);
31
24
  const probeBrowser = async (): Promise<ManagedProgramState> => {
32
- let executable: string;
33
- try { executable = cliPath(); } catch (error) { return { state: "down", detail: error instanceof Error ? error.message : String(error) }; }
34
- const located = await run(input.nodePath, [executable, "browser", "path"]);
35
- if (!located.ok) return { state: "down", detail: `HyperFrames browser is unavailable: ${located.output}` };
36
- const path = located.output.trim();
37
- if (path.length === 0 || path.includes("\n") || path.includes("\r")) {
38
- return { state: "mismatch", detail: "HyperFrames returned an invalid browser path" };
39
- }
40
- const version = await run(path, ["--version"]);
41
- if (!version.ok || version.output.length === 0) {
42
- return { state: "mismatch", detail: `HyperFrames browser cannot start: ${version.output}` };
43
- }
25
+ try { await requireBrowserExecutable(selectedPath, version); }
26
+ catch (error) { return { state: "down", detail: error instanceof Error ? error.message : String(error) }; }
44
27
  return { state: "ready" };
45
28
  };
46
29
  return {
47
30
  id: input.id,
48
- installation: {
49
- probe: probeBrowser,
50
- get commands() { return [{ command: input.nodePath, args: [cliPath(), "browser", "ensure"] }]; },
51
- },
31
+ ...(configuredBrowserPath(input) === undefined ? {
32
+ // Projects sharing this installation also share the existing Program lifecycle lock/logs.
33
+ stateRoot: join(browserCacheDirectory(input), ".hypit-render-program"),
34
+ installation: {
35
+ probe: probeBrowser,
36
+ commands: [{ label: `Install Chrome Headless Shell ${version} at ${selectedPath} from ${browserDownloadUrl(input)}`, command: input.nodePath, args: [
37
+ "--import", import.meta.resolve("tsx"),
38
+ "--import", new URL("./capture-bootstrap.ts", import.meta.url).href,
39
+ fileURLToPath(new URL("./browser-install.ts", import.meta.url)), browserCacheDirectory(input), version!,
40
+ ...(baseUrl === undefined ? [] : [baseUrl]),
41
+ ] }],
42
+ },
43
+ } : {}),
52
44
  async probe(): Promise<ManagedProgramState> {
53
45
  const browser = await probeBrowser();
54
46
  if (browser.state !== "ready") return browser;
@@ -2,7 +2,6 @@ import { defineEndpointPackage } from "@hypit/endpoint-kit";
2
2
  import { mediaTypes } from "@hypit/media";
3
3
  import { renderHyperframesCapabilities, verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
4
4
  import { canonicalize } from "@hypit/protocol";
5
- import { resolveNodePackageExecutable } from "@hypit/package-loader-node";
6
5
  import { renderHyperframesVisual, resolveExecutionOptions, renderWorkerLimit } from "./render.js";
7
6
  import type { HyperframesExecutionOptions } from "./options.js";
8
7
  import { renderProgressReporter } from "./progress.js";
@@ -14,17 +13,12 @@ export type CreateLocalHyperframesProviderOptions = HyperframesExecutionOptions
14
13
  readonly pool?: string;
15
14
  /** Used by managed browser installation, not frame capture. */
16
15
  readonly nodePath?: string;
17
- readonly hyperframesCliPath?: string;
18
16
  /** Whole render requests admitted concurrently; independent of frame workers. */
19
17
  readonly defaultConcurrency?: number;
20
18
  /** Shared Chrome slots across Need executions using this pool. */
21
19
  readonly browserCapacity?: number;
22
20
  };
23
21
 
24
- export function defaultHyperframesCliPath(): string {
25
- return resolveNodePackageExecutable("hyperframes", "hyperframes", { from: import.meta.url });
26
- }
27
-
28
22
  export function createLocalHyperframesProvider(config: CreateLocalHyperframesProviderOptions) {
29
23
  const execution = resolveExecutionOptions(config);
30
24
  const pool = config.pool ?? config.instance ?? "hyperframes.local";
@@ -62,7 +56,10 @@ export function createLocalHyperframesProvider(config: CreateLocalHyperframesPro
62
56
  : request.range.endFrameExclusive - request.range.startFrame;
63
57
  const progress = renderProgressReporter(context.reportProgress, frameCount);
64
58
  try {
65
- const visual = await renderHyperframesVisual(request, { ...config, workers: execution.workers, maxWorkers,
59
+ const visual = await renderHyperframesVisual(request, { ...config,
60
+ ...(execution.chromePath === undefined ? { browserVersion: execution.browserVersion! } : { chromePath: execution.chromePath }),
61
+ browserCacheDirectory: execution.browserCacheDirectory,
62
+ workers: execution.workers, maxWorkers,
66
63
  resources: context.resources, onProgress: progress.onProgress,
67
64
  ...(context.reportDiagnostic === undefined ? {} : { onDiagnostic: context.reportDiagnostic }) });
68
65
  return { value: { kind: "inline", value: canonicalize(visual) } };
@@ -6,7 +6,7 @@ import type { EndpointInvocationContext } from "@hypit/endpoint-kit";
6
6
  import { stageHyperframesProject } from "@hypit/hyperframes/project";
7
7
  import { sealRenderedVisual } from "@hypit/media";
8
8
  import type { MediaFrameRange, RenderedVisual } from "@hypit/media";
9
- import { verifyCompositableSurfaceBytes } from "@hypit/media-execution";
9
+ import { verifyCompositableSurfaceFile } from "@hypit/media-execution";
10
10
  import { verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
11
11
  import type { HyperframesVisualRequest } from "@hypit/render-hyperframes";
12
12
  import { isStreamingResourceStore } from "@hypit/runtime";
@@ -15,6 +15,7 @@ import { assert, positiveInteger } from "./process.js";
15
15
  import { runCaptureProcess } from "./capture-process.js";
16
16
  import { finished } from "node:stream/promises";
17
17
  import { autoWorkerLimit } from "./concurrency.js";
18
+ import { browserCacheDirectory, browserExecutablePath, configuredBrowserPath, requireBrowserExecutable, selectedBrowserVersion } from "./browser.js";
18
19
 
19
20
  export type HyperframesRenderProgress =
20
21
  | { readonly phase: "staging" | "encoding" | "storing"; readonly elapsedMs: number }
@@ -43,6 +44,9 @@ export function resolveExecutionOptions(options: HyperframesExecutionOptions) {
43
44
  const browserGpu = options.browserGpu ?? "hardware";
44
45
  assert(["auto", "software", "hardware"].includes(browserGpu), "HyperFrames browserGpu is invalid");
45
46
  return {
47
+ chromePath: configuredBrowserPath(options),
48
+ browserVersion: selectedBrowserVersion(options),
49
+ browserCacheDirectory: browserCacheDirectory(options),
46
50
  workers,
47
51
  maxWorkers: workers === "auto" ? positiveInteger(options.maxWorkers ?? autoWorkerLimit(), "maxWorkers") : workers,
48
52
  quality, browserGpu,
@@ -71,7 +75,7 @@ export async function renderHyperframesVisual(
71
75
  options: RenderHyperframesVisualOptions,
72
76
  ): Promise<RenderedVisual> {
73
77
  verifyHyperframesVisualRequest(request);
74
- const config = resolveExecutionOptions(options);
78
+ const config = { ...resolveExecutionOptions(options), chromePath: browserExecutablePath(options) };
75
79
  const { document } = request;
76
80
  const range = request.range ?? { startFrame: 0, endFrameExclusive: document.frameCount };
77
81
  const frameCount = range.endFrameExclusive - range.startFrame;
@@ -97,8 +101,9 @@ export async function renderHyperframesVisual(
97
101
  };
98
102
  try {
99
103
  signal.throwIfAborted();
104
+ await requireBrowserExecutable(config.chromePath, config.browserVersion);
100
105
  await options.onDiagnostic?.({ level: "info", message:
101
- `Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; encoder ${config.ffmpegPath}` });
106
+ `Render ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; quality ${config.quality}; GPU ${config.browserGpu}; browser ${config.chromePath}; encoder ${config.ffmpegPath}` });
102
107
  options.onProgress?.({ phase: "staging", elapsedMs: 0 });
103
108
  work = await mkdtemp(join(tmpdir(), "hypit-hyperframes-local-"));
104
109
  await stageHyperframesProject({ document, directory: work, signal,
@@ -109,7 +114,7 @@ export async function renderHyperframesVisual(
109
114
  assert(bytes !== undefined, `HyperFrames Artifact ${artifact.resource} is unavailable`);
110
115
  return bytes;
111
116
  },
112
- validateSurface: (surface, bytes, probeSignal) => verifyCompositableSurfaceBytes({ surface, bytes,
117
+ validateSurface: (surface, path, probeSignal) => verifyCompositableSurfaceFile({ surface, path,
113
118
  ffprobePath: config.ffprobePath, processTimeoutMs: config.processTimeoutMs,
114
119
  maxProbeOutputBytes: config.maxProcessOutputBytes, signal: probeSignal! }),
115
120
  });
@@ -43,7 +43,8 @@ For multilingual transcription with quality as the priority and suitable hardwar
43
43
  "expectedModel": "large-v3",
44
44
  "expectedDevice": "cuda",
45
45
  "expectedCompute": "float16",
46
- "expectedBatchSize": 4
46
+ "expectedBatchSize": 4,
47
+ "alignmentLanguages": ["zh", "en"]
47
48
  }
48
49
  }
49
50
  ```
@@ -57,7 +58,11 @@ or hosted service for the actual language, material and time available rather th
57
58
  successively larger models as a routine sequence.
58
59
 
59
60
  `expectedModel` selects speech recognition. WhisperX separately loads the language-specific
60
- alignment model when that language is first requested. A larger ASR model can improve the words
61
+ alignment model from prepared local resources when that language is first requested. Set
62
+ `alignmentLanguages` to the language codes needed by this production before preparation. This is a
63
+ preparation demand, not a central language whitelist; already cached supported languages remain usable.
64
+ Omission prepares only ASR and sentence data, and does not imply every alignment model is installed.
65
+ A larger ASR model can improve the words
61
66
  fed into alignment, but does not select a larger aligner or guarantee better timing by itself.
62
67
  The health response identifies the loaded ASR configuration; it does not establish that every
63
68
  language's alignment weights are cached. See [WhisperX usage](https://github.com/m-bain/whisperX#usage-)
@@ -65,8 +70,9 @@ and [faster-whisper deployment](https://github.com/SYSTRAN/faster-whisper#usage)
65
70
 
66
71
  ## Preparation and downloads
67
72
 
68
- The Runtime installs and starts the packaged service in the machine Program Home when this Endpoint
69
- is selected. The environment is reused across projects and sessions. The current local package and
73
+ An explicit `programs up` or `runtime up` prepares and starts the selected packaged service in the
74
+ machine Program Home. Selecting an Endpoint alone does not install it. The environment is reused
75
+ across projects and sessions. The current local package and
70
76
  service are trusted code; this is not a community-plugin sandbox.
71
77
 
72
78
  `hypit paths` reports the machine `hostState`. The managed installation lives below
@@ -75,21 +81,31 @@ service are trusted code; this is not a community-plugin sandbox.
75
81
  the service host includes its port. The default service is `http://127.0.0.1:8765` and exposes
76
82
  its configuration through `/health`. Inspect an existing Profile's address and expected settings
77
83
  when locating that service. A custom `serviceCommand` supplies its own installation and start command.
78
- `hypit programs status` checks the Programs selected by the supplied Profile; `programs up` prepares
79
- and starts them. The managed installation does not require a global `whisperx` shell command.
84
+ `hypit programs prepare --endpoint <instance>` prepares the configured ASR model, requested alignment
85
+ models and sentence data without starting a process. It also works when the service is already online.
86
+ `programs up` prepares and starts the selected Programs; `programs status` reports process readiness.
87
+ Adding a language requires preparation, not a service restart, when its cache location is unchanged.
88
+ `modelCacheDirectory` optionally selects a root with `huggingface/` and `torch/` subdirectories;
89
+ relative paths resolve against the Runtime data root. Omission retains the upstream Hugging Face and
90
+ torch caches and their environment settings. Changing a running service's cache selection requires
91
+ an explicit restart when idle, just like changing its model or hardware. The managed installation
92
+ does not require a global `whisperx` shell command.
80
93
 
81
94
  Preparation and service processes inherit the environment of the command starting them. Set
82
95
  network and cache variables there before `programs up` or `runtime up`. A service already running
83
96
  retains its earlier environment. Inspect its reported log before deciding whether a selected
84
97
  Program needs restarting, and account for active work using it.
85
98
 
99
+ If NLTK refuses a proxied fetch during preparation, follow the service’s
100
+ [explicit proxy preparation](../../services/whisperx/README.md#preparing-sentence-data-through-a-proxy).
101
+
86
102
  Preparation commands write `install.log`; the running service writes `program.log`, with stderr in
87
103
  `program.err.log` on Windows. Inspect the stderr file for Python model-loading and download messages.
88
104
  `programs status` reports these files as `installationLogPath`, `logPath` and `errorLogPath` when they
89
- exist, even before installation finishes. Preparation notices name the Python environment and NLTK
90
- commands separately. The service logs the start and completion of ASR loading, transcription,
91
- language-alignment model loading and alignment, with elapsed times. Loading may include a download;
92
- transfer details come from the underlying client, not an estimated percentage from the Provider.
105
+ exist, even before installation finishes. Preparation notices name the Python environment and selected model/language resources separately. The service logs the start and completion of ASR loading, transcription,
106
+ language-alignment model loading and alignment, with elapsed times. Downloads happen only in
107
+ preparation; startup and inference only load local resources. A missing resource fails with a
108
+ preparation instruction. Transfer details come from the underlying client during preparation.
93
109
  The service health endpoint becomes available after ASR loading. A startup readiness wait expiring
94
110
  can leave that process still loading. Its PID is recorded when spawned; repeated `up` observes it,
95
111
  and `programs down` can stop it during loading. PID liveness and service readiness are separate facts.
@@ -1,3 +1,4 @@
1
+ import { resolve } from "node:path";
1
2
  import {
2
3
  createRuntimeEndpointAdapterFacet,
3
4
  runtimeConfigExact,
@@ -18,7 +19,7 @@ const localWhisperXRuntimeAdapter = createRuntimeEndpointAdapterFacet({
18
19
  "baseUrl", "expectedModel", "expectedDevice", "expectedCompute", "expectedBatchSize",
19
20
  "expectedServiceVersion", "expectedWhisperXVersion",
20
21
  "defaultConcurrency", "requestTimeoutMs", "maxResponseBytes",
21
- "serviceCommand",
22
+ "serviceCommand", "alignmentLanguages", "modelCacheDirectory",
22
23
  ], "local WhisperX");
23
24
  const baseUrl = runtimeConfigString(config.baseUrl, "WhisperX baseUrl");
24
25
  if (baseUrl !== undefined) {
@@ -37,6 +38,12 @@ const localWhisperXRuntimeAdapter = createRuntimeEndpointAdapterFacet({
37
38
  const defaultConcurrency = runtimeConfigPositiveInteger(config.defaultConcurrency, "WhisperX defaultConcurrency");
38
39
  const requestTimeoutMs = runtimeConfigPositiveInteger(config.requestTimeoutMs, "WhisperX requestTimeoutMs");
39
40
  const maxResponseBytes = runtimeConfigPositiveInteger(config.maxResponseBytes, "WhisperX maxResponseBytes");
41
+ const modelCacheDirectory = runtimeConfigString(config.modelCacheDirectory, "WhisperX modelCacheDirectory");
42
+ const alignmentLanguages = config.alignmentLanguages;
43
+ if (alignmentLanguages !== undefined && (!Array.isArray(alignmentLanguages)
44
+ || alignmentLanguages.some((item) => typeof item !== "string" || !/^[a-z]+$/u.test(item)))) {
45
+ throw new Error("WhisperX alignmentLanguages must be an array of lowercase language codes");
46
+ }
40
47
  const serviceCommandValue = config.serviceCommand;
41
48
  if (serviceCommandValue !== undefined
42
49
  && (!Array.isArray(serviceCommandValue) || serviceCommandValue.length === 0
@@ -80,6 +87,8 @@ const localWhisperXRuntimeAdapter = createRuntimeEndpointAdapterFacet({
80
87
  expectedServiceVersion: selectedServiceVersion,
81
88
  expectedWhisperXVersion: selectedWhisperXVersion,
82
89
  ...(serviceCommand === undefined ? {} : { serviceCommand }),
90
+ ...(alignmentLanguages === undefined ? {} : { alignmentLanguages: alignmentLanguages as string[] }),
91
+ ...(modelCacheDirectory === undefined ? {} : { modelCacheDirectory: resolve(context.dataRoot, modelCacheDirectory) }),
83
92
  }),
84
93
  };
85
94
  },
@@ -52,6 +52,8 @@ export type LocalWhisperXProgramOptions = {
52
52
  readonly expectedServiceVersion: string;
53
53
  readonly expectedWhisperXVersion: string;
54
54
  readonly serviceCommand?: ManagedProgramCommand;
55
+ readonly alignmentLanguages?: readonly string[];
56
+ readonly modelCacheDirectory?: string;
55
57
  };
56
58
 
57
59
  export function localWhisperXProgram(options: LocalWhisperXProgramOptions): ManagedProgram {
@@ -85,11 +87,13 @@ export function localWhisperXProgram(options: LocalWhisperXProgramOptions): Mana
85
87
  HYPIT_WHISPERX_COMPUTE: expected.compute,
86
88
  HYPIT_WHISPERX_BATCH_SIZE: String(expected.batchSize),
87
89
  HYPIT_WHISPERX_NLTK_DATA: nltkData,
90
+ ...(options.modelCacheDirectory === undefined ? {} : { HYPIT_WHISPERX_MODEL_CACHE: options.modelCacheDirectory }),
91
+ HYPIT_WHISPERX_ALIGNMENT_LANGUAGES: (options.alignmentLanguages ?? []).join(" "),
88
92
  };
89
93
  const check: ManagedProgramCommand = {
90
94
  command: pythonEnvironmentCommand(environment, "hypit-whisperx-check"),
91
- args: [],
92
- env: { HYPIT_WHISPERX_NLTK_DATA: nltkData },
95
+ args: ["--models"],
96
+ env: serviceEnvironment,
93
97
  };
94
98
  const managedStart: ManagedProgramCommand = {
95
99
  command: pythonEnvironmentCommand(environment, "hypit-whisperx-service"),
@@ -131,10 +135,10 @@ export function localWhisperXProgram(options: LocalWhisperXProgramOptions): Mana
131
135
  args: ["sync", "--project", localWhisperXManagedProject, "--frozen", "--no-editable"],
132
136
  env: { UV_PROJECT_ENVIRONMENT: environment },
133
137
  }, {
134
- label: "Prepare NLTK sentence data",
138
+ label: `Prepare WhisperX ${expected.model} and alignment resources for ${(options.alignmentLanguages ?? []).join(", ") || "no selected languages"}`,
135
139
  command: pythonEnvironmentCommand(environment, "hypit-whisperx-prepare"),
136
140
  args: ["--nltk-data", nltkData],
137
- env: { HYPIT_WHISPERX_NLTK_DATA: nltkData },
141
+ env: serviceEnvironment,
138
142
  }],
139
143
  },
140
144
  } : {}),
@@ -53,3 +53,8 @@ to start execution, then `const runtime = await host.createRuntime(); await runt
53
53
  to submit work. `RuntimeHostExecution` does not run a second embedded Worker loop. A local carrier
54
54
  may stop accepting new Builds and drain; its assigned Builds stay in place and all carriers continue
55
55
  using the same resource accounting. Process policy belongs to the local Runtime, not this Host ABI.
56
+
57
+ `prepareHostPackages` accepts exact specifier strings or `{ specifier, env }` installation inputs.
58
+ The optional environment is applied only to that npm child process, merged over its inherited
59
+ environment. It is neither persisted as installation status nor returned in package reports.
60
+ The shared installer does not interpret SDK-specific variables; their owning packages supply them.
@@ -70,17 +70,20 @@ export type BuildView = {
70
70
  }[];
71
71
  };
72
72
 
73
- export type RuntimeHostCredentialStatus = {
73
+ export type RuntimeHostCredentialDescription = {
74
74
  readonly endpoint: string;
75
75
  readonly slot: string;
76
76
  readonly label: string;
77
77
  readonly kind: "secret" | "json";
78
78
  readonly ref: CredentialRef;
79
79
  readonly acquisition?: CredentialAcquisition;
80
- readonly configured: boolean;
81
80
  readonly writable: boolean;
82
81
  };
83
82
 
83
+ export type RuntimeHostCredentialStatus = RuntimeHostCredentialDescription & {
84
+ readonly configured: boolean;
85
+ };
86
+
84
87
  export type RuntimeHostControl = {
85
88
  /** Build evidence while active. Finished evidence is read through its Result Repository. */
86
89
  logs?(build: string, lines: number): Promise<import("@hypit/runtime").ExecutionLogView | undefined>;
@@ -105,6 +108,8 @@ export type RuntimeHostResultControl = {
105
108
  };
106
109
 
107
110
  export type RuntimeHostCredentialControl = {
111
+ /** Endpoint declarations and Store write capability; never reads an existing secret. */
112
+ describeCredentials(endpoint?: string): Promise<readonly RuntimeHostCredentialDescription[]>;
108
113
  credentials(endpoint?: string): Promise<readonly RuntimeHostCredentialStatus[]>;
109
114
  putCredential(endpoint: string, slot: string, secret: string): Promise<RuntimeHostCredentialStatus>;
110
115
  deleteCredential(endpoint: string, slot: string): Promise<{
@@ -240,6 +245,7 @@ export type RuntimeController = {
240
245
  down(options?: { readonly maxWaitMs?: number }): Promise<RuntimeWorkerState>;
241
246
  };
242
247
  readonly programs: {
248
+ prepare(options?: { readonly endpoints?: readonly string[]; readonly onProgress?: (event: ManagedProgramProgress) => void }): Promise<{ readonly dataRoot: string; readonly programs: readonly ManagedProgramReport[] }>;
243
249
  up(options: {
244
250
  readonly maxWaitMs?: number;
245
251
  readonly onProgress?: (event: ManagedProgramProgress) => void;
@@ -73,7 +73,7 @@ export async function inspectHostPackage(
73
73
  : undefined;
74
74
  }
75
75
 
76
- async function runNpm(root: string, specifiers: readonly string[], logPath: string): Promise<void> {
76
+ async function runNpm(root: string, specifiers: readonly string[], logPath: string, env?: Readonly<Record<string, string>>): Promise<void> {
77
77
  // npm's prefix and cwd must denote the same physical project (not /tmp vs /private/tmp).
78
78
  const cwd = await realpath(root);
79
79
  const npmArgs = [
@@ -96,6 +96,7 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
96
96
  cwd,
97
97
  shell: false,
98
98
  windowsHide: true,
99
+ env: { ...process.env, ...env },
99
100
  stdio: ["ignore", log.fd, log.fd],
100
101
  });
101
102
  child.on("error", (error) => reject(new Error(`Cannot start npm: ${error.message}. Log: ${logPath}`, { cause: error })));
@@ -114,21 +115,26 @@ async function runNpm(root: string, specifiers: readonly string[], logPath: stri
114
115
  * package.json, lockfile and dependencies; Hypit keeps no parallel inventory.
115
116
  */
116
117
  export async function prepareHostPackages(
117
- specifiers: readonly string[],
118
+ specifiers: readonly (string | { readonly specifier: string; readonly env?: Readonly<Record<string, string>> })[],
118
119
  options: {
119
120
  readonly root: string;
120
121
  readonly onProgress?: (event: HostPackageProgress) => void;
121
122
  },
122
123
  ): Promise<readonly HostPackageReport[]> {
123
124
  const root = resolve(options.root);
124
- const bySpecifier = new Map<string, RegistryPackageSpec>();
125
+ const bySpecifier = new Map<string, RegistryPackageSpec & { readonly env?: Readonly<Record<string, string>> }>();
125
126
  for (const specifier of specifiers) {
126
- const parsed = parseRegistryPackageSpec(specifier);
127
- bySpecifier.set(parsed.specifier, parsed);
127
+ const parsed = parseRegistryPackageSpec(typeof specifier === "string" ? specifier : specifier.specifier);
128
+ const env = { ...bySpecifier.get(parsed.specifier)?.env };
129
+ for (const [key, value] of Object.entries(typeof specifier === "string" ? {} : specifier.env ?? {})) {
130
+ if (env[key] !== undefined && env[key] !== value) throw new Error(`Conflicting installation environment ${key} for ${parsed.specifier}`);
131
+ env[key] = value;
132
+ }
133
+ bySpecifier.set(parsed.specifier, { ...parsed, ...(Object.keys(env).length === 0 ? {} : { env }) });
128
134
  }
129
135
  const required = [...bySpecifier.values()].sort((left, right) => left.specifier.localeCompare(right.specifier));
130
136
  const reports: HostPackageReport[] = [];
131
- for (const item of required) {
137
+ for (const { env, ...item } of required) {
132
138
  const installation = externalPackageInstallRoot(root, item.name, item.version);
133
139
  options.onProgress?.({ ...item, phase: "checking" });
134
140
  const missing = await installedVersion(installation, item.name) !== item.version;
@@ -142,7 +148,7 @@ export async function prepareHostPackages(
142
148
  dependencies: { [item.name]: item.version },
143
149
  }, null, 2)}\n`, "utf8");
144
150
  options.onProgress?.({ ...item, phase: "installing", logPath });
145
- await runNpm(installation, [item.specifier], logPath);
151
+ await runNpm(installation, [item.specifier], logPath, env);
146
152
  }
147
153
  const version = await installedVersion(installation, item.name);
148
154
  if (version !== item.version) {
@@ -43,8 +43,12 @@ video package by name.
43
43
 
44
44
  `ManagedProgram.installation.prepareBeforeStart` lets a Provider reconcile its installed environment
45
45
  before a cold start, even when the installation probe already passes. The declared commands use the
46
- Provider's ordinary package manager. A healthy running Program is reused before this preparation is
47
- considered; Runtime does not inspect source files or infer implementation versions.
46
+ Provider's ordinary package manager. `programs prepare` runs the declared installation independently
47
+ of process startup; `programs up` also checks its resources when the process is already healthy.
48
+ The installation probe covers the owner's selected execution resources, while the process probe
49
+ reports service readiness. Cold reconciliation still applies only before starting a new process.
50
+ Runtime does not name models, inspect source files or infer implementation versions. Preparation
51
+ may acquire resources; execution must consume already prepared resources and report what is missing.
48
52
  `ManagedProgramCommand.label` optionally names a command's purpose for progress and log headings.
49
53
  It is display text supplied by the Program owner, not a phase to persist or interpret. Runtime
50
54
  reports generic process/probe facts; the service itself owns domain-specific progress in its logs.
@@ -42,11 +42,11 @@ export type ManagedProgramCommand = {
42
42
  };
43
43
 
44
44
  export type ManagedProgramInstallation = {
45
- /** Truthful inspection of the installed program; no installer-owned receipt. */
45
+ /** Read-only inspection of the selected environment and resources; no downloads or receipts. */
46
46
  probe(): Promise<ManagedProgramState>;
47
47
  /** Reconcile an installed environment before a cold start, using its package manager's cache. */
48
48
  readonly prepareBeforeStart?: boolean;
49
- /** Commands run when installation is missing, or before a cold start when requested above. */
49
+ /** Explicit preparation commands, run for missing resources or cold reconciliation above. */
50
50
  readonly commands: readonly ManagedProgramCommand[];
51
51
  };
52
52
 
@@ -242,7 +242,12 @@ controller returned by `await host.controller()`, and calls `build(...)` on the
242
242
  `await host.createRuntime()`. There is one production process-lifecycle path.
243
243
 
244
244
  Managed Programs have independent lifetimes. A healthy WhisperX service keeps its model loaded;
245
- `prepareBeforeStart` reconciles a cold installation through uv's source-aware synchronization.
245
+ `programs prepare --endpoint <instance>` prepares selected resources without starting or stopping
246
+ that service. Its installation probe is independent of process health, so an online service does not
247
+ hide a newly requested language model. `programs up` prepares missing resources and starts the service
248
+ if needed. `prepareBeforeStart` additionally reconciles a cold installation through the package
249
+ manager's source-aware synchronization. The Provider owns the resource choices and commands;
250
+ Runtime only invokes them under the existing lifecycle lock.
246
251
  `runtime down` stops the coordinator and asks its executors to stop; Programs are stopped separately.
247
252
  Distribution changes and shell environment changes still concern the coordinator's bootstrap process.
248
253
  Inspect active work before restarting it. Environment-backed credentials use that inherited process
@@ -258,3 +263,13 @@ capacity release and every completed Result. They use the production execution p
258
263
  This is a load experiment rather than part of every package regression. It reports fixture preparation,
259
264
  local progress, completion, executor RSS and cleanup separately; total test time also includes creating
260
265
  and removing the temporary Result repositories. Neither suite uses a completion-time performance target.
266
+
267
+
268
+ ### Credential management without reading the old value
269
+
270
+ `openCredentials(endpoint)` opens only the selected Endpoint's credential control.
271
+ `describeCredentials(endpoint)` returns its declared slots and each Store's write capability without
272
+ resolving secrets. `credentials(endpoint)` also reads current values to report status and propagates
273
+ read failures. Login and logout use the former: a damaged old credential cannot prevent replacement
274
+ or deletion. Successful writes and deletions report their operation's result without rereading the
275
+ secret. Execution still resolves credentials normally and reports Store errors.
@@ -462,7 +462,7 @@ export async function prepareRuntimeConfigPackages(
462
462
  ...document.endpoints.map((item) => item.use),
463
463
  ...credentials.credentials.map((item) => item.use),
464
464
  ], options.distributionPackageRoot);
465
- return await prepareHostPackages(requirements.map((item) => item.specifier), {
465
+ return await prepareHostPackages(requirements, {
466
466
  root: hypitHostPackageRoot(options.hostStateRoot),
467
467
  ...(options.onProgress === undefined ? {} : { onProgress: options.onProgress }),
468
468
  });
@@ -28,16 +28,21 @@ export function createLocalCredentialControl(
28
28
  : `Endpoint ${endpoint} repeats credential slot ${slot}`);
29
29
  return matches[0]!;
30
30
  };
31
- const status = async (item: typeof descriptions[number]) => ({
31
+ const describe = async (item: typeof descriptions[number]) => ({
32
32
  ...structuredClone(item),
33
- configured: await options.credentialStore.resolve(item.ref) !== undefined,
34
33
  writable: await writableCredentialStore(options.credentialStore, item.ref) !== undefined,
35
34
  });
36
35
 
37
36
  return {
37
+ async describeCredentials(endpoint) {
38
+ return await Promise.all(descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint).map(describe));
39
+ },
38
40
  async credentials(endpoint) {
39
41
  const selected = descriptions.filter((item) => endpoint === undefined || item.endpoint === endpoint);
40
- return await Promise.all(selected.map(status));
42
+ return await Promise.all(selected.map(async (item) => ({
43
+ ...await describe(item),
44
+ configured: await options.credentialStore.resolve(item.ref) !== undefined,
45
+ })));
41
46
  },
42
47
  async putCredential(endpoint, slot, secret) {
43
48
  assert(secret.length > 0, "credential secret is empty");
@@ -45,14 +50,14 @@ export function createLocalCredentialControl(
45
50
  const store = await writableCredentialStore(options.credentialStore, item.ref);
46
51
  assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
47
52
  await store.put(item.ref, { secret });
48
- return await status(item);
53
+ return { ...structuredClone(item), writable: true, configured: true };
49
54
  },
50
55
  async deleteCredential(endpoint, slot) {
51
56
  const item = credential(endpoint, slot);
52
57
  const store = await writableCredentialStore(options.credentialStore, item.ref);
53
58
  assert(store !== undefined, `CredentialStore ${item.ref.store} is not writable`);
54
59
  const deleted = await store.delete(item.ref);
55
- return { deleted, credential: await status(item) };
60
+ return { deleted, credential: { ...structuredClone(item), writable: true, configured: false } };
56
61
  },
57
62
  close() {
58
63
  return options.close?.();
@@ -23,6 +23,7 @@ import {
23
23
  } from "./config.js";
24
24
  import {
25
25
  bringManagedProgramsUp,
26
+ prepareManagedPrograms,
26
27
  reportManagedPrograms,
27
28
  takeManagedProgramsDown,
28
29
  } from "./programs.js";
@@ -82,6 +83,7 @@ export async function openLocalRuntimeHost(
82
83
  ),
83
84
  },
84
85
  programs: {
86
+ prepare: async (programOptions) => await prepareManagedPrograms(profile, { ...programOptions, packageRoot, ...distribution }),
85
87
  up: async (programOptions) => await bringManagedProgramsUp(profile, { ...programOptions, packageRoot, ...distribution }),
86
88
  down: async (scope) => await takeManagedProgramsDown(profile, { ...scope, packageRoot, ...distribution }),
87
89
  report: async (scope) => await reportManagedPrograms(profile, { ...scope, packageRoot, ...distribution }),
@@ -28,6 +28,7 @@ export {
28
28
  } from "./config.js";
29
29
  export {
30
30
  bringManagedProgramsUp,
31
+ prepareManagedPrograms,
31
32
  reportManagedPrograms,
32
33
  takeManagedProgramsDown,
33
34
  } from "./programs.js";