@hypit/hypit 0.2.6 → 0.2.7

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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/public/hyperframes.d.ts +19 -2
  3. package/package.json +2 -1
  4. package/packages/caption-fine/README.md +4 -4
  5. package/packages/credential-store-file/README.md +4 -0
  6. package/packages/credential-store-file/src/store.ts +2 -2
  7. package/packages/credential-store-platform/README.md +2 -0
  8. package/packages/hyperframes/README.md +7 -0
  9. package/packages/hyperframes/src/html-project.ts +99 -0
  10. package/packages/hyperframes/src/index.ts +2 -0
  11. package/packages/hyperframes/src/project.ts +28 -0
  12. package/packages/pixverse/README.md +35 -0
  13. package/packages/pixverse/package.json +21 -0
  14. package/packages/pixverse/src/activation.ts +11 -0
  15. package/packages/pixverse/src/index.ts +122 -0
  16. package/packages/pixverse/src/surface.ts +146 -0
  17. package/packages/provider-hyperframes-local/README.md +19 -7
  18. package/packages/provider-hyperframes-local/src/capture.ts +18 -8
  19. package/packages/provider-hyperframes-local/src/index.ts +1 -1
  20. package/packages/provider-hyperframes-local/src/output.ts +1 -1
  21. package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
  22. package/packages/provider-hyperframes-local/src/provider.ts +29 -2
  23. package/packages/provider-hyperframes-local/src/render.ts +45 -20
  24. package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
  25. package/packages/provider-whisperx-local/README.md +8 -5
  26. package/packages/render-hyperframes/README.md +9 -0
  27. package/packages/render-hyperframes/src/index.ts +3 -0
  28. package/packages/render-hyperframes/src/manifest.ts +7 -2
  29. package/packages/render-hyperframes/src/product.ts +41 -2
  30. package/packages/runtime-local/src/process-control.ts +4 -1
  31. package/packages/runtime-local/src/programs.ts +15 -3
  32. package/packages/runtime-local/src/supervisor.ts +8 -1
  33. package/packages/script/README.md +11 -4
  34. package/packages/script/src/format.ts +11 -24
  35. package/packages/script/src/manifest.ts +2 -2
  36. package/packages/script/src/parser.ts +16 -21
  37. package/packages/script/src/surface.ts +5 -3
  38. package/packages/script/src/types.ts +1 -1
  39. package/packages/source/README.md +1 -1
  40. package/packages/source/src/header.ts +2 -1
  41. package/packages/studio/README.md +13 -0
  42. package/packages/studio/src/feedback-server.ts +7 -0
  43. package/packages/studio/src/mutation-origin.ts +20 -0
  44. package/packages/studio/src/parameters.ts +3 -7
  45. package/packages/studio/src/preview/render.ts +7 -1
  46. package/packages/studio/src/server.ts +39 -0
  47. package/packages/studio/src/session.ts +7 -3
  48. package/packages/studio/src/ui/syntax.ts +11 -10
  49. package/packages/temporal-markup/EDITING.md +1 -1
  50. package/packages/video-cli/README.md +47 -4
  51. package/packages/video-cli/package.json +3 -0
  52. package/packages/video-cli/src/cli.ts +9 -9
  53. package/packages/video-cli/src/creation.ts +3 -3
  54. package/packages/video-cli/src/frame-grid.ts +34 -0
  55. package/packages/video-cli/src/index.ts +4 -0
  56. package/packages/video-cli/src/media-frames.ts +25 -0
  57. package/packages/video-cli/src/media.ts +198 -46
  58. package/packages/video-cli/src/process.ts +13 -3
  59. package/packages/video-cli/src/secret-input.ts +14 -0
  60. package/packages/video-cli/src/snapshot.ts +186 -0
  61. package/packages/yt-dlp/src/download.ts +32 -18
@@ -1,12 +1,22 @@
1
1
  # @hypit/provider-hyperframes-local
2
2
 
3
- Trusted local Provider for the `@hypit/render-hyperframes#render-visual` capability. It stages the
3
+ Trusted local Provider for `@hypit/render-hyperframes@1#render-visual` and `#render-frames`. It stages the
4
4
  Resource dependencies declared by a `HyperframesDocument`, renders a silent MP4 with the
5
5
  HyperFrames engine, probes the bytes, and returns a verified `RenderedVisual`. Before capturing a typed
6
6
  Surface it decodes the exact bytes and checks declared dimensions, still/frame timing, SDR/sRGB and
7
7
  opaque/straight-alpha facts. These checks validate the typed rendering input; they do not create
8
8
  content identity or hidden output metadata.
9
9
 
10
+ `render-frames` uses the same staging, source-frame mapping, readiness and opaque PNG capture as
11
+ video export, returning the PNGs before encoding. It accepts a compiled document or a materialized
12
+ HTML project. For selected frames it merges only their required source-frame windows; continuous
13
+ windows decode sequentially. A complete batch shares one staged project and browser lifetime.
14
+ The existing browser selection, explicit preparation, worker settings and cancellation apply.
15
+ `maxRenderedBytes` bounds the total returned PNG bytes, or the encoded MP4 for a video request.
16
+ `render-frames` has its own capability binding; it does not inherit a `render-visual` binding.
17
+ For immediate CLI invocation, the host calls the handler directly: Build admission reservations do
18
+ not coordinate separate CLI processes. Worker limits still bound browsers inside each invocation.
19
+
10
20
  Normalized transparent videos displayed by Media or project components use the ordinary video path.
11
21
  The engine decodes them to PNG frames with alpha, then Chrome blends them with lower layers and
12
22
  the authored Canvas background. Final frames use fast PNG encoding after Chrome has composited
@@ -73,8 +83,7 @@ an opaque session and chooses PNG separately at capture. It does not patch engin
73
83
  The Runtime Adapter declares one managed browser program. Prepare it explicitly:
74
84
 
75
85
  ```sh
76
- hypit programs up --runtime ./hypit.runtime.json --endpoint hyperframes.local
77
- hypit doctor --runtime ./hypit.runtime.json
86
+ hypit programs prepare --runtime ./hypit.runtime.json --endpoint hyperframes.local
78
87
  ```
79
88
 
80
89
  Use the instance name from the Profile. `runtime up` also prepares it and starts the Runtime Worker.
@@ -110,17 +119,20 @@ receipt is stored. To install into an empty location, explicitly choose `browser
110
119
 
111
120
  `config.chromePath` selects a user-managed Chrome/Chromium executable. It cannot be combined with
112
121
  `browserVersion` or `browserDownloadBaseUrl`; invalid combinations fail instead of assigning precedence. Relative paths resolve
113
- from the Runtime Profile root. This mode never downloads or repairs a browser. Its version remains
122
+ from the Runtime data root, as does `browserCacheDirectory`. The Profile's `dataRoot` itself resolves
123
+ relative to the Profile file. This mode never downloads or repairs a browser. Its version remains
114
124
  under the user's control, including system-browser auto-updates. `HYPERFRAMES_BROWSER_PATH` and
115
125
  `PRODUCER_HEADLESS_SHELL_PATH` do not select browsers in this Provider; configure `chromePath`.
116
126
  On platforms without a supported managed download, explicitly select an installed browser.
117
127
 
118
128
  `doctor` displays the selected path and its source, and only inspects it. Build preflight, rendering
119
- and previews never install a browser. `programs up` / `runtime up` display the selected managed
129
+ and previews never install a browser. `programs prepare`, `programs up` and `runtime up` display the selected managed
120
130
  version, installation location and download URL before running preparation. The probe runs `--version` and checks
121
131
  FFmpeg/FFprobe; it does not promise GPU or page compatibility. Capture receives that same selected
122
- path as the engine's `chromePath`, including its GPU probe. Active Workers keep their loaded package
123
- recommendation; restart them explicitly after changing Profile or package dependencies.
132
+ path as the engine's `chromePath`, including its GPU probe. New Builds read current Endpoint
133
+ configuration and project implementation; active Builds keep their selected configuration. A
134
+ Distribution update or change to the Worker's inherited environment requires an explicit Worker
135
+ restart when active work permits. Ordinary Profile edits do not require restarting every process.
124
136
 
125
137
  The Provider's `hypit.dependencyInstallEnv` disables Puppeteer's browser download while preparing its
126
138
  engine/producer npm dependencies. The repository `.puppeteerrc.cjs` does the same for checkout installs.
@@ -11,7 +11,8 @@ import { CaptureConcurrency } from "./concurrency.js";
11
11
  import { createOpaqueFrameCapture } from "./opaque-capture.js";
12
12
 
13
13
  export type CaptureInput = {
14
- readonly document: HyperframesDocument;
14
+ readonly document: Pick<HyperframesDocument, "frameRate" | "frameCount" | "canvas">;
15
+ readonly frames?: readonly number[];
15
16
  readonly range: MediaFrameRange;
16
17
  readonly config: ReturnType<typeof resolveExecutionOptions> & { readonly chromePath: string };
17
18
  readonly directory: string;
@@ -24,14 +25,16 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
24
25
  onProgress: (event: HyperframesRenderProgress) => void): Promise<string> {
25
26
  const { document, range, config, directory: work } = input;
26
27
  const signal = controller.signal;
27
- const frameCount = range.endFrameExclusive - range.startFrame;
28
+ const frameCount = input.frames?.length ?? range.endFrameExclusive - range.startFrame;
29
+ const selected = input.frames === undefined ? undefined : new Set(input.frames);
30
+ const at = (index: number) => input.frames?.[index] ?? range.startFrame + index;
28
31
  const fps = { num: document.frameRate.numerator, den: document.frameRate.denominator };
29
32
  const limit = renderWorkerLimit(config, frameCount, fps.num / fps.den);
30
33
  const concurrency = new CaptureConcurrency(limit, config.workers === "auto");
31
34
  // Short contiguous batches retain sequential capture while allowing free browsers
32
35
  // to help with expensive passages. This queue exists only inside this render.
33
36
  const batchCount = Math.max(limit, Math.ceil(frameCount / Math.max(1, Math.round(fps.num / fps.den))));
34
- const batches = distributeFrameRange(range, batchCount);
37
+ const batches = distributeFrameRange({ startFrame: 0, endFrameExclusive: frameCount }, batchCount);
35
38
  let nextBatch = 0;
36
39
  const started = performance.now();
37
40
  const elapsedMs = () => Math.round(performance.now() - started);
@@ -81,7 +84,8 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
81
84
  const sources = new Map<string, { frames: Map<number, string>; width: number; height: number }>();
82
85
  let sourceFrames = 0;
83
86
  // One source at a time bounds decoder pressure independently of browser concurrency.
84
- const windows = sourceWindows(slots, range);
87
+ const windows = sourceWindows(slots, input.frames === undefined ? [range]
88
+ : input.frames.map(frame => ({ startFrame: frame, endFrameExclusive: frame + 1 })));
85
89
  const sourceTotal = windows.reduce((sum, source) => sum + source.windows.reduce((count, window) =>
86
90
  count + window.endFrameExclusive - window.startFrame, 0), 0);
87
91
  if (sourceTotal > 0) onProgress({ phase: "decoding", completed: 0, total: sourceTotal, elapsedMs: elapsedMs() });
@@ -119,7 +123,7 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
119
123
  const frameIndex = sourceFrameAt(slot, frame);
120
124
  const framePath = sources.get(slot.src)?.frames.get(frameIndex);
121
125
  // Browser initialization may seek outside the requested interval.
122
- if (frame < range.startFrame || frame >= range.endFrameExclusive) continue;
126
+ if (selected === undefined ? frame < range.startFrame || frame >= range.endFrameExclusive : !selected.has(frame)) continue;
123
127
  assert(framePath !== undefined, `HyperFrames has no decoded frame ${frameIndex} for ${slot.id}`);
124
128
  payloads.set(slot.id, { framePath, frameIndex });
125
129
  }
@@ -132,7 +136,7 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
132
136
  await mkdir(outputFrames);
133
137
  onProgress?.({ phase: "prepared", workers: concurrency.target, sourceFrames, elapsedMs: elapsedMs() });
134
138
  const jobs: Promise<void>[] = [];
135
- let active = 0, open = 0, initializing = 0, capturedFrames = 0;
139
+ let active = 0, open = 0, initializing = 0, capturedFrames = 0, capturedBytes = 0;
136
140
  let startupMs = 0;
137
141
  const launch = (): void => {
138
142
  const worker = jobs.length;
@@ -187,13 +191,18 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
187
191
  if (active > concurrency.target) break;
188
192
  const batch = batches[nextBatch++];
189
193
  if (batch === undefined) break;
190
- for (let frame = batch.startFrame; frame < batch.endFrameExclusive; frame++) {
194
+ for (let index = batch.startFrame; index < batch.endFrameExclusive; index++) {
195
+ const frame = at(index);
191
196
  signal.throwIfAborted();
192
197
  const captured = await stage(`worker ${worker} frame ${frame}`, config.frameTimeoutMs, () => captureFrame(frame));
198
+ if (input.frames !== undefined) {
199
+ capturedBytes += captured.buffer.byteLength;
200
+ assert(capturedBytes <= config.maxRenderedBytes, "HyperFrames PNG output exceeds its byte limit");
201
+ }
193
202
  timing.seekMs += captured.seekMs;
194
203
  timing.prepareMs += captured.prepareMs;
195
204
  timing.screenshotMs += captured.screenshotMs;
196
- await writeFile(join(outputFrames, `${String(frame - range.startFrame).padStart(9, "0")}.png`), captured.buffer);
205
+ await writeFile(join(outputFrames, `${String(index).padStart(9, "0")}.png`), captured.buffer);
197
206
  completed++;
198
207
  capturedFrames++;
199
208
  if (performance.now() - lastProgressAt >= 1_000) {
@@ -239,6 +248,7 @@ export async function captureStagedVisual(input: CaptureInput, controller: Abort
239
248
  if (failure !== undefined) throw failure;
240
249
  signal.throwIfAborted();
241
250
  await Promise.all(closing.values());
251
+ if (input.frames !== undefined) return outputFrames;
242
252
  const output = join(work, "visual.mp4");
243
253
  onProgress({ phase: "encoding", elapsedMs: elapsedMs() });
244
254
  const crf = { draft: 28, standard: 23, high: 18 }[config.quality];
@@ -6,5 +6,5 @@ export type {
6
6
  HyperframesWorkers,
7
7
  } from "./provider.js";
8
8
 
9
- export { renderHyperframesVisual } from "./render.js";
9
+ export { renderHyperframesVisual, renderHyperframesFrames } from "./render.js";
10
10
  export type { RenderHyperframesVisualOptions, HyperframesRenderProgress } from "./render.js";
@@ -38,7 +38,7 @@ function outputFrameCount(stream: ProbeStream): number {
38
38
 
39
39
  export async function verifyOutput(args: {
40
40
  readonly path: string;
41
- readonly document: HyperframesDocument;
41
+ readonly document: Pick<HyperframesDocument, "frameRate" | "frameCount" | "canvas">;
42
42
  readonly ffprobePath: string;
43
43
  readonly timeoutMs: number;
44
44
  readonly maxOutputBytes: number;
@@ -33,7 +33,10 @@ export async function killRenderTree(pid: number): Promise<void> {
33
33
  for (const child of descendants.reverse()) {
34
34
  for (const target of [-child, child]) {
35
35
  try { process.kill(target, "SIGKILL"); }
36
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; }
36
+ catch (error) {
37
+ const code = (error as NodeJS.ErrnoException).code;
38
+ if (code !== "ESRCH" && !(target < 0 && code === "EPERM")) throw error;
39
+ }
37
40
  }
38
41
  }
39
42
  const deadline = Date.now() + cleanupMs;
@@ -76,7 +79,10 @@ export function killRenderDescendantsSync(pid: number): void {
76
79
  for (const child of descendants.slice(1).reverse()) {
77
80
  for (const target of [-child, child]) {
78
81
  try { process.kill(target, "SIGKILL"); }
79
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "ESRCH") throw error; }
82
+ catch (error) {
83
+ const code = (error as NodeJS.ErrnoException).code;
84
+ if (code !== "ESRCH" && !(target < 0 && code === "EPERM")) throw error;
85
+ }
80
86
  }
81
87
  }
82
88
  }
@@ -1,8 +1,8 @@
1
1
  import { defineEndpointPackage } from "@hypit/endpoint-kit";
2
2
  import { mediaTypes } from "@hypit/media";
3
- import { renderHyperframesCapabilities, verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
3
+ import { renderHyperframesCapabilities, renderHyperframesTypes, verifyHyperframesFramesRequest, hyperframesFramesDomain, verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
4
4
  import { canonicalize } from "@hypit/protocol";
5
- import { renderHyperframesVisual, resolveExecutionOptions, renderWorkerLimit } from "./render.js";
5
+ import { renderHyperframesVisual, renderHyperframesFrames, resolveExecutionOptions, renderWorkerLimit } from "./render.js";
6
6
  import type { HyperframesExecutionOptions } from "./options.js";
7
7
  import { renderProgressReporter } from "./progress.js";
8
8
 
@@ -67,6 +67,33 @@ export function createLocalHyperframesProvider(config: CreateLocalHyperframesPro
67
67
  await progress.flush();
68
68
  }
69
69
  },
70
+ }, {
71
+ lifecycle: "immediate" as const,
72
+ capability: renderHyperframesCapabilities.renderFrames,
73
+ returns: renderHyperframesTypes.frames,
74
+ ...(config.browserCapacity === undefined ? {} : {
75
+ resources: [{ id: browsers, limit: config.browserCapacity }],
76
+ unitsForRequest: (request: import("@hypit/endpoint-kit").EndpointRequest) => {
77
+ verifyHyperframesFramesRequest(request.constraints);
78
+ const document = hyperframesFramesDomain(request.constraints);
79
+ return { [browsers]: renderWorkerLimit(reserved, request.constraints.frames.length,
80
+ document.frameRate.numerator / document.frameRate.denominator) };
81
+ },
82
+ }),
83
+ handler: async context => {
84
+ const request = context.need.constraints;
85
+ verifyHyperframesFramesRequest(request);
86
+ const progress = renderProgressReporter(context.reportProgress, request.frames.length);
87
+ try {
88
+ const frames = await renderHyperframesFrames(request, { ...config,
89
+ ...(execution.chromePath === undefined ? { browserVersion: execution.browserVersion! } : { chromePath: execution.chromePath }),
90
+ browserCacheDirectory: execution.browserCacheDirectory,
91
+ workers: execution.workers, maxWorkers,
92
+ resources: context.resources, onProgress: progress.onProgress,
93
+ ...(context.reportDiagnostic === undefined ? {} : { onDiagnostic: context.reportDiagnostic }) });
94
+ return { value: { kind: "inline", value: canonicalize(frames) } };
95
+ } finally { await progress.flush(); }
96
+ },
70
97
  }],
71
98
  });
72
99
  }
@@ -3,12 +3,12 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type { EndpointInvocationContext } from "@hypit/endpoint-kit";
6
- import { stageHyperframesProject } from "@hypit/hyperframes/project";
6
+ import { stageHyperframesProject, stageHyperframesHtmlProject } from "@hypit/hyperframes/project";
7
7
  import { sealRenderedVisual } from "@hypit/media";
8
8
  import type { MediaFrameRange, RenderedVisual } from "@hypit/media";
9
9
  import { verifyCompositableSurfaceFile } from "@hypit/media-execution";
10
- import { verifyHyperframesVisualRequest } from "@hypit/render-hyperframes";
11
- import type { HyperframesVisualRequest } from "@hypit/render-hyperframes";
10
+ import { verifyHyperframesVisualRequest, verifyHyperframesFramesRequest, hyperframesFramesDomain } from "@hypit/render-hyperframes";
11
+ import type { HyperframesVisualRequest, HyperframesFramesRequest, HyperframesFrames } from "@hypit/render-hyperframes";
12
12
  import { isStreamingResourceStore } from "@hypit/runtime";
13
13
  import type { HyperframesExecutionOptions } from "./options.js";
14
14
  import { assert, positiveInteger } from "./process.js";
@@ -75,10 +75,22 @@ export async function renderHyperframesVisual(
75
75
  options: RenderHyperframesVisualOptions,
76
76
  ): Promise<RenderedVisual> {
77
77
  verifyHyperframesVisualRequest(request);
78
+ return capture(request, options) as Promise<RenderedVisual>;
79
+ }
80
+
81
+ export async function renderHyperframesFrames(request: HyperframesFramesRequest, options: RenderHyperframesVisualOptions): Promise<HyperframesFrames> {
82
+ verifyHyperframesFramesRequest(request);
83
+ return capture(request, options) as Promise<HyperframesFrames>;
84
+ }
85
+
86
+ async function capture(request: HyperframesVisualRequest | HyperframesFramesRequest, options: RenderHyperframesVisualOptions): Promise<RenderedVisual | HyperframesFrames> {
78
87
  const config = { ...resolveExecutionOptions(options), chromePath: browserExecutablePath(options) };
79
- const { document } = request;
80
- const range = request.range ?? { startFrame: 0, endFrameExclusive: document.frameCount };
81
- const frameCount = range.endFrameExclusive - range.startFrame;
88
+ const frames = "frames" in request ? request.frames : undefined;
89
+ const document = "frames" in request ? hyperframesFramesDomain(request) : request.document;
90
+ const range = frames === undefined
91
+ ? (request as HyperframesVisualRequest).range ?? { startFrame: 0, endFrameExclusive: document.frameCount }
92
+ : { startFrame: frames[0]!, endFrameExclusive: frames.at(-1)! + 1 };
93
+ const frameCount = frames?.length ?? range.endFrameExclusive - range.startFrame;
82
94
  const controller = new AbortController();
83
95
  const signal = options.signal === undefined ? controller.signal : AbortSignal.any([controller.signal, options.signal]);
84
96
  const started = performance.now();
@@ -103,23 +115,25 @@ export async function renderHyperframesVisual(
103
115
  signal.throwIfAborted();
104
116
  await requireBrowserExecutable(config.chromePath, config.browserVersion);
105
117
  await options.onDiagnostic?.({ level: "info", message:
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}` });
118
+ `Capture ${frameCount} frames; workers ${config.workers} (limit ${renderWorkerLimit(config, frameCount, document.frameRate.numerator / document.frameRate.denominator)}); opaque fast PNG; GPU ${config.browserGpu}; browser ${config.chromePath}`
119
+ + (frames === undefined ? `; quality ${config.quality}; encoder ${config.ffmpegPath}` : "; PNG output") });
107
120
  options.onProgress?.({ phase: "staging", elapsedMs: 0 });
108
121
  work = await mkdtemp(join(tmpdir(), "hypit-hyperframes-local-"));
109
- await stageHyperframesProject({ document, directory: work, signal,
110
- read: async (artifact, readSignal) => {
122
+ const read: import("@hypit/hyperframes/project").HyperframesArtifactReader = async (artifact, readSignal) => {
111
123
  const io = { signal: readSignal! };
112
124
  const bytes = isStreamingResourceStore(options.resources)
113
125
  ? await options.resources.open(artifact.resource, io) : await options.resources.get(artifact.resource, io);
114
126
  assert(bytes !== undefined, `HyperFrames Artifact ${artifact.resource} is unavailable`);
115
127
  return bytes;
116
- },
128
+ };
129
+ if ("project" in request) await stageHyperframesHtmlProject({ project: request.project, directory: work, signal, read });
130
+ else await stageHyperframesProject({ document: request.document, directory: work, signal, read,
117
131
  validateSurface: (surface, path, probeSignal) => verifyCompositableSurfaceFile({ surface, path,
118
132
  ffprobePath: config.ffprobePath, processTimeoutMs: config.processTimeoutMs,
119
133
  maxProbeOutputBytes: config.maxProcessOutputBytes, signal: probeSignal! }),
120
134
  });
121
135
  changePhase("decoding source frames");
122
- await runCaptureProcess({ document, range, config, directory: work,
136
+ await runCaptureProcess({ document, range, ...(frames === undefined ? {} : { frames }), config, directory: work,
123
137
  engineModule: import.meta.resolve("@hyperframes/engine"),
124
138
  producerModule: import.meta.resolve("@hyperframes/producer"),
125
139
  }, signal, (event) => {
@@ -132,22 +146,33 @@ export async function renderHyperframesVisual(
132
146
  signal.throwIfAborted();
133
147
  changePhase("storing output");
134
148
  options.onProgress?.({ phase: "storing", elapsedMs: Math.round(performance.now() - started) });
135
- const output = join(work, "visual.mp4");
136
- let artifact;
137
- if (isStreamingResourceStore(options.resources)) {
138
- const stream = createReadStream(output, { signal });
139
- const closed = finished(stream).catch(() => {});
140
- try { artifact = await options.resources.putStream(stream, "video/mp4", { signal }); }
141
- finally { stream.destroy(); await closed; }
149
+ const store = async (output: string, mediaType: string) => {
150
+ if (isStreamingResourceStore(options.resources)) {
151
+ const stream = createReadStream(output, { signal });
152
+ const closed = finished(stream).catch(() => {});
153
+ try { return await options.resources.putStream(stream, mediaType, { signal }); }
154
+ finally { stream.destroy(); await closed; }
155
+ }
156
+ return options.resources.put(await readFile(output, { signal }), mediaType, { signal });
157
+ };
158
+ let result: RenderedVisual | HyperframesFrames;
159
+ if (frames === undefined) {
160
+ result = sealRenderedVisual({ frameRate: document.frameRate, frameCount, canvas: document.canvas,
161
+ artifact: await store(join(work, "visual.mp4"), "video/mp4") });
142
162
  } else {
143
- artifact = await options.resources.put(await readFile(output, { signal }), "video/mp4", { signal });
163
+ const artifacts = [];
164
+ for (let index = 0; index < frames.length; index++) {
165
+ signal.throwIfAborted();
166
+ artifacts.push(await store(join(work, "frames", `${String(index).padStart(9, "0")}.png`), "image/png"));
167
+ }
168
+ result = artifacts;
144
169
  }
145
170
  signal.throwIfAborted();
146
171
 
147
172
  changePhase("complete");
148
173
  await Promise.all(diagnostics);
149
174
  options.onProgress?.({ phase: "complete", frames: frameCount, elapsedMs: Math.round(performance.now() - started) });
150
- return sealRenderedVisual({ frameRate: document.frameRate, frameCount, canvas: document.canvas, artifact });
175
+ return result;
151
176
  } catch (error) {
152
177
  if (signal.aborted) throw signal.reason;
153
178
  controller.abort(error);
@@ -51,9 +51,10 @@ export function sourceFrameAt(slot: VideoSlot, frame: number): number {
51
51
  return value;
52
52
  }
53
53
 
54
- export function sourceWindows(slots: readonly VideoSlot[], range: MediaFrameRange) {
54
+ export function sourceWindows(slots: readonly VideoSlot[], selection: MediaFrameRange | readonly MediaFrameRange[]) {
55
+ const ranges: readonly MediaFrameRange[] = Array.isArray(selection) ? selection : [selection as MediaFrameRange];
55
56
  const sources = new Map<string, { fps: VideoSlot["sourceFps"]; windows: MediaFrameRange[] }>();
56
- for (const slot of slots) {
57
+ for (const range of ranges) for (const slot of slots) {
57
58
  const first = Math.max(range.startFrame, slot.startFrame);
58
59
  const last = Math.min(range.endFrameExclusive, slot.endFrameExclusive) - 1;
59
60
  if (last < first) continue;
@@ -91,16 +91,19 @@ torch caches and their environment settings. Changing a running service's cache
91
91
  an explicit restart when idle, just like changing its model or hardware. The managed installation
92
92
  does not require a global `whisperx` shell command.
93
93
 
94
- Preparation and service processes inherit the environment of the command starting them. Set
95
- network and cache variables there before `programs up` or `runtime up`. A service already running
96
- retains its earlier environment. Inspect its reported log before deciding whether a selected
97
- Program needs restarting, and account for active work using it.
94
+ Preparation and service processes inherit the environment of the command starting them. Set download
95
+ route variables on `programs prepare` (or the preparation performed by `programs up` / `runtime up`).
96
+ Changing only a download source does not require restarting a running service using the same cache.
97
+ Cache settings must select the same resource location for preparation and the service. A running
98
+ service retains its earlier environment; arrange an idle restart when changing that location or its
99
+ inference settings. Inspect its reported configuration and logs, and account for active work.
98
100
 
99
101
  If NLTK refuses a proxied fetch during preparation, follow the service’s
100
102
  [explicit proxy preparation](../../services/whisperx/README.md#preparing-sentence-data-through-a-proxy).
101
103
 
102
104
  Preparation commands write `install.log`; the running service writes `program.log`, with stderr in
103
- `program.err.log` on Windows. Inspect the stderr file for Python model-loading and download messages.
105
+ `program.err.log` on Windows. Inspect service logs for Python model-loading and inference messages;
106
+ download messages belong to preparation's `install.log`.
104
107
  `programs status` reports these files as `installationLogPath`, `logPath` and `errorLogPath` when they
105
108
  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
109
  language-alignment model loading and alignment, with elapsed times. Downloads happen only in
@@ -6,6 +6,15 @@ semantic coverage through the same frame and audio pipeline.
6
6
 
7
7
  Explicit author and capability boundary for final HyperFrames video rendering.
8
8
 
9
+ `@hypit/render-hyperframes@1#render-frames` captures an existing programme's selected frames.
10
+ `hyperframesFramesRequest({ document, frames })` accepts a `HyperframesDocument` and strictly
11
+ increasing original-frame indices. `{ project: { html, assets }, frames }` accepts already
12
+ materialized HTML with its URL-addressed assets. The `Frames` result is a PNG `BlobRef[]` in request
13
+ order; the request already owns the frame identities. Canvas and clock stay in the input.
14
+ This capability requests picture capture only. File names, contact sheets and pagination belong to
15
+ the caller. `hypit snapshot` invokes it immediately through a selected Profile; there is no Build
16
+ or video encoding in that observation path.
17
+
9
18
  The package owns `<render:Video composition={...}/>` and lowers it to ordinary Operations that:
10
19
 
11
20
  1. obtain the `ProgramSpace` from the Timeline input and compile the referenced `Composition` into a `HyperframesDocument`;
@@ -4,3 +4,6 @@ export { renderHyperframesCapabilities, renderHyperframesManifest, renderHyperfr
4
4
  export { hyperframesVisualRequest, verifyHyperframesVisualRequest } from "./product.js";
5
5
  export type { HyperframesVisualRequest } from "./product.js";
6
6
  export { decodeHyperframesRenderSurface } from "./surface.js";
7
+ export { renderHyperframesTypes } from "./manifest.js";
8
+ export { hyperframesFramesRequest, hyperframesFramesDomain, verifyHyperframesFramesRequest, verifyHyperframesFrames } from "./product.js";
9
+ export type { HyperframesFramesRequest, HyperframesFrames } from "./product.js";
@@ -12,6 +12,7 @@ import type {
12
12
  CapabilityRef,
13
13
  ModuleManifest,
14
14
  ProducerRef,
15
+ TypeRef,
15
16
  } from "@hypit/protocol";
16
17
  import {
17
18
  mediaPipelineModuleRef,
@@ -20,7 +21,11 @@ import {
20
21
  export const renderHyperframesModuleRef = { name: "@hypit/render-hyperframes", version: "1" } as const;
21
22
  export const renderHyperframesCapabilities = {
22
23
  renderVisual: { module: renderHyperframesModuleRef, name: "render-visual" },
24
+ renderFrames: { module: renderHyperframesModuleRef, name: "render-frames" },
23
25
  } satisfies Record<string, CapabilityRef>;
26
+ export const renderHyperframesTypes = {
27
+ frames: { module: renderHyperframesModuleRef, name: "Frames" },
28
+ } satisfies Record<string, TypeRef>;
24
29
  export const renderHyperframesProducers = {
25
30
  requestVisualRange: { module: renderHyperframesModuleRef, name: "request-visual-range" },
26
31
  requestVisual: { module: renderHyperframesModuleRef, name: "request-visual-render" },
@@ -70,11 +75,11 @@ export const renderHyperframesManifest: ModuleManifest = {
70
75
  { module: hyperframesModuleRef },
71
76
  { module: mediaPipelineModuleRef },
72
77
  ],
73
- types: [],
78
+ types: [{ name: renderHyperframesTypes.frames.name }],
74
79
  capabilities: [{
75
80
  name: renderHyperframesCapabilities.renderVisual.name,
76
81
  returns: mediaTypes.renderedVisual,
77
- }],
82
+ }, { name: renderHyperframesCapabilities.renderFrames.name, returns: renderHyperframesTypes.frames }],
78
83
  producers: [
79
84
  {
80
85
  name: renderHyperframesProducers.requestVisual.name,
@@ -1,6 +1,8 @@
1
1
  import type { CanonicalValue } from "@hypit/protocol";
2
- import { assertHyperframesDocument } from "@hypit/hyperframes";
3
- import type { HyperframesDocument } from "@hypit/hyperframes";
2
+ import { assertHyperframesDocument, assertHyperframesHtmlProject, hyperframesHtmlDomain } from "@hypit/hyperframes";
3
+ import type { HyperframesDocument, HyperframesHtmlProject } from "@hypit/hyperframes";
4
+ import type { BlobRef } from "@hypit/protocol";
5
+ import { isResourceId } from "@hypit/protocol";
4
6
  import { verifyMediaFrameRange } from "@hypit/media";
5
7
  import type { MediaFrameRange } from "@hypit/media";
6
8
 
@@ -9,6 +11,43 @@ export type HyperframesVisualRequest = {
9
11
  readonly range?: MediaFrameRange;
10
12
  };
11
13
 
14
+ /** Select original programme frames, in strictly increasing order. */
15
+ export type HyperframesFramesRequest = ({ readonly document: HyperframesDocument } | { readonly project: HyperframesHtmlProject }) & {
16
+ readonly frames: readonly number[];
17
+ };
18
+ /** PNG images in request order. The request already supplies each frame's identity. */
19
+ export type HyperframesFrames = readonly BlobRef[];
20
+
21
+ export function hyperframesFramesDomain(request: HyperframesFramesRequest) {
22
+ return "document" in request ? request.document : hyperframesHtmlDomain(request.project.html);
23
+ }
24
+
25
+ export function verifyHyperframesFramesRequest(value: unknown): asserts value is HyperframesFramesRequest {
26
+ if (!value || typeof value !== "object" || Array.isArray(value)
27
+ || Object.keys(value).some(key => !["document", "project", "frames"].includes(key))
28
+ || ("document" in value) === ("project" in value)) throw new Error("Frame request needs exactly one document or HTML project, and frames");
29
+ const request = value as HyperframesFramesRequest;
30
+ if ("document" in request) assertHyperframesDocument(request.document);
31
+ else assertHyperframesHtmlProject(request.project);
32
+ const { frameCount } = hyperframesFramesDomain(request);
33
+ if (!Array.isArray(request.frames) || request.frames.length === 0 || request.frames.some((frame, index) =>
34
+ !Number.isSafeInteger(frame) || frame < 0 || frame >= frameCount || (index > 0 && frame <= request.frames[index - 1]!))) {
35
+ throw new Error(`Frames must be strictly increasing integers in [0, ${frameCount})`);
36
+ }
37
+ }
38
+
39
+ export function hyperframesFramesRequest(request: HyperframesFramesRequest): CanonicalValue {
40
+ verifyHyperframesFramesRequest(request);
41
+ return request as unknown as CanonicalValue;
42
+ }
43
+
44
+ export function verifyHyperframesFrames(value: unknown): asserts value is HyperframesFrames {
45
+ if (!Array.isArray(value) || value.length === 0 || value.some(blob => !blob || blob.kind !== "blob"
46
+ || !isResourceId(blob.resource) || blob.mediaType !== "image/png" || !Number.isSafeInteger(blob.size) || blob.size <= 0)) {
47
+ throw new Error("HyperFrames frames must be PNG BlobRefs in request order");
48
+ }
49
+ }
50
+
12
51
  export function verifyHyperframesVisualRequest(value: unknown): asserts value is HyperframesVisualRequest {
13
52
  if (value === null || typeof value !== "object" || Array.isArray(value)
14
53
  || Object.keys(value).some((key) => key !== "document" && key !== "range")) {
@@ -23,7 +23,10 @@ function signal(pid: number, force: boolean): ProcessStopResult {
23
23
  process.kill(target, name);
24
24
  return "sent";
25
25
  } catch (error) {
26
- if (errorCode(error) === "EPERM") return "denied";
26
+ if (errorCode(error) === "EPERM") {
27
+ if (target < 0) continue;
28
+ return "denied";
29
+ }
27
30
  if (errorCode(error) !== "ESRCH") throw error;
28
31
  }
29
32
  }
@@ -196,13 +196,25 @@ function powershellLiteral(value: string): string {
196
196
  * as `-e` followed by the first word of the script, and node exits on the syntax error. Given one
197
197
  * string it passes that string through as the command line, which is what this builds.
198
198
  */
199
- function windowsCommandLine(args: readonly string[]): string {
199
+ export function windowsCommandLine(args: readonly string[]): string {
200
200
  return args.map((value) => {
201
201
  if (value !== "" && !/[\s"]/u.test(value)) return value;
202
202
  // A quote is escaped by the backslashes before it, so those double; a trailing run doubles too,
203
203
  // because the closing quote would otherwise escape itself against them.
204
- const escaped = value.replace(/(\\*)"/gu, '$1$1\\"').replace(/(\\*)$/u, "$1$1");
205
- return `"${escaped}"`;
204
+ let escaped = '"';
205
+ let backslashes = 0;
206
+ for (const character of value) {
207
+ if (character === "\\") {
208
+ backslashes++;
209
+ } else if (character === '"') {
210
+ escaped += "\\".repeat(backslashes * 2 + 1) + '"';
211
+ backslashes = 0;
212
+ } else {
213
+ escaped += "\\".repeat(backslashes) + character;
214
+ backslashes = 0;
215
+ }
216
+ }
217
+ return escaped + "\\".repeat(backslashes * 2) + '"';
206
218
  }).join(" ");
207
219
  }
208
220
 
@@ -119,7 +119,14 @@ export async function superviseBuilds(options: {
119
119
  if (options.signal.aborted) break;
120
120
  accepting ??= launch();
121
121
  const carrier = accepting;
122
- await carrier.ready;
122
+ try {
123
+ await carrier.ready;
124
+ } catch (error) {
125
+ if (accepting === carrier) accepting = undefined;
126
+ if (options.signal.aborted) break;
127
+ await finishInterrupted(build, `Build executor startup failed: ${error instanceof Error ? error.message : String(error)}; create a new Build to continue`);
128
+ continue;
129
+ }
123
130
  if (carrier.exited) break;
124
131
  if (accepting !== carrier) continue;
125
132
  await state.execution.start(build);
@@ -27,8 +27,10 @@ parser is inside a Segment, a valid bare tag such as `<ALICE>` is a Role Cue. Th
27
27
  not indentation: the compact spelling
28
28
  `<opening><ALICE>I speak first.<BOB>I answer.</opening>` has the same semantic value.
29
29
 
30
- A Role Cue is optional. Text before the first Role Cue is a roleless Turn, and Role state is reset
31
- when every Segment closes; a Role can never leak into the following Segment.
30
+ A Role Cue is optional. A Segment without Role Cues contains a roleless Turn. If Roles are used,
31
+ the first Cue must precede that Segment's spoken text. Role state resets when the Segment closes;
32
+ a Role can never leak into the following Segment. Role labels may use Unicode letters, marks and
33
+ numbers; capitalization does not distinguish Roles from Segments.
32
34
 
33
35
  An empty Segment such as `<empty></empty>` (or `<empty/>`) is valid. `empty` is an ordinary
34
36
  author-chosen name, not a reserved keyword. It retains the Segment identity and both boundary anchors
@@ -101,7 +103,7 @@ Units; it does not split the Segment, cut the picture or end a Selection.
101
103
  <script id="story">
102
104
  <exchange>
103
105
  <HOST> I use it || every day, || since <2012 | twenty twelve>.
104
- <GUEST> Even @{proof} on holiday @{/proof}?
106
+ <GUEST> Even @{proof}on holiday?@{/proof}
105
107
  <HOST> @{answer!} Especially then.
106
108
  </exchange>
107
109
  </script>
@@ -118,13 +120,18 @@ These units support precise timing and highlighting. A Caption Cue can hold a wh
118
120
  `||` chooses its handoff independently of character counts or visual line wrapping.
119
121
 
120
122
  Annotations do not create speech boundaries. Comments are transparent (`hel<!--note-->lo`
121
- remains `hello`); a postfix attribute or `||` inside a word is invalid. Script analyzes a complete
123
+ remains `hello`), including when an attribute follows a comment or zero-width marker:
124
+ `hello<!--note-->{emphasis}` and `hello@{beat!}{emphasis}` still annotate `hello`.
125
+ An authored space before the attribute remains invalid. A postfix attribute or `||` inside a word
126
+ is invalid. Script analyzes a complete
122
127
  prose run before binding these constructs. An explicit Dual correspondence and a speaker/Segment
123
128
  boundary remain authored structure. Shared Dual groups still expose their internal speech anchors.
124
129
 
125
130
  A Dual display side is literal authored text, including symbols and emoji: `<😀|smile>` and
126
131
  `<.|dot>` have explicit speech correspondence and require no invented speech token for the symbol.
127
132
  A literal-only display is one display surface within that correspondence.
133
+ Every Dual needs at least one spoken word on its explicit or shared speech side. `<API|...>` cannot
134
+ provide timed correspondence for `API` and is rejected; punctuation and markers are not spoken words.
128
135
 
129
136
  ## Display spelling and separators
130
137