@hypit/hypit 0.2.6 → 0.2.8

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 (70) hide show
  1. package/README.md +1 -1
  2. package/dist/public/hyperframes.d.ts +19 -2
  3. package/examples/provider-package/README.md +2 -2
  4. package/package.json +2 -1
  5. package/packages/caption-fine/README.md +4 -4
  6. package/packages/credential-store-file/README.md +4 -0
  7. package/packages/credential-store-file/src/store.ts +2 -2
  8. package/packages/credential-store-platform/README.md +2 -0
  9. package/packages/hyperframes/README.md +7 -0
  10. package/packages/hyperframes/src/html-project.ts +99 -0
  11. package/packages/hyperframes/src/index.ts +2 -0
  12. package/packages/hyperframes/src/project.ts +28 -0
  13. package/packages/pixverse/README.md +35 -0
  14. package/packages/pixverse/package.json +21 -0
  15. package/packages/pixverse/src/activation.ts +11 -0
  16. package/packages/pixverse/src/index.ts +122 -0
  17. package/packages/pixverse/src/surface.ts +146 -0
  18. package/packages/provider-hiapi/README.md +1 -1
  19. package/packages/provider-hyperframes-local/README.md +19 -7
  20. package/packages/provider-hyperframes-local/src/capture.ts +18 -8
  21. package/packages/provider-hyperframes-local/src/index.ts +1 -1
  22. package/packages/provider-hyperframes-local/src/output.ts +1 -1
  23. package/packages/provider-hyperframes-local/src/process-tree.ts +8 -2
  24. package/packages/provider-hyperframes-local/src/provider.ts +29 -2
  25. package/packages/provider-hyperframes-local/src/render.ts +45 -20
  26. package/packages/provider-hyperframes-local/src/sampling.ts +3 -2
  27. package/packages/provider-hypihub/README.md +3 -4
  28. package/packages/provider-monid/README.md +1 -1
  29. package/packages/provider-tokendance/README.md +1 -1
  30. package/packages/provider-whisperx-local/README.md +8 -5
  31. package/packages/render-hyperframes/README.md +9 -0
  32. package/packages/render-hyperframes/src/index.ts +3 -0
  33. package/packages/render-hyperframes/src/manifest.ts +7 -2
  34. package/packages/render-hyperframes/src/product.ts +41 -2
  35. package/packages/runtime-local/src/process-control.ts +4 -1
  36. package/packages/runtime-local/src/programs.ts +15 -3
  37. package/packages/runtime-local/src/supervisor.ts +8 -1
  38. package/packages/script/README.md +11 -4
  39. package/packages/script/src/format.ts +11 -24
  40. package/packages/script/src/manifest.ts +2 -2
  41. package/packages/script/src/parser.ts +16 -21
  42. package/packages/script/src/surface.ts +5 -3
  43. package/packages/script/src/types.ts +1 -1
  44. package/packages/seedance/README.md +11 -18
  45. package/packages/seedance/src/index.ts +7 -7
  46. package/packages/seedance/src/surface.ts +9 -8
  47. package/packages/seedance-kits/README.md +4 -3
  48. package/packages/source/README.md +1 -1
  49. package/packages/source/src/header.ts +2 -1
  50. package/packages/studio/README.md +13 -0
  51. package/packages/studio/src/feedback-server.ts +7 -0
  52. package/packages/studio/src/mutation-origin.ts +20 -0
  53. package/packages/studio/src/parameters.ts +3 -7
  54. package/packages/studio/src/preview/render.ts +7 -1
  55. package/packages/studio/src/server.ts +39 -0
  56. package/packages/studio/src/session.ts +7 -3
  57. package/packages/studio/src/ui/syntax.ts +11 -10
  58. package/packages/temporal-markup/EDITING.md +1 -1
  59. package/packages/video-cli/README.md +47 -4
  60. package/packages/video-cli/package.json +3 -0
  61. package/packages/video-cli/src/cli.ts +9 -9
  62. package/packages/video-cli/src/creation.ts +3 -3
  63. package/packages/video-cli/src/frame-grid.ts +34 -0
  64. package/packages/video-cli/src/index.ts +4 -0
  65. package/packages/video-cli/src/media-frames.ts +25 -0
  66. package/packages/video-cli/src/media.ts +198 -46
  67. package/packages/video-cli/src/process.ts +13 -3
  68. package/packages/video-cli/src/secret-input.ts +14 -0
  69. package/packages/video-cli/src/snapshot.ts +186 -0
  70. 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;
@@ -123,12 +123,11 @@ size and part concurrency, retries a failed part with a fresh signed URL, comple
123
123
  one upload, and then passes the returned HTTPS URL to generation or transcription. Signing requests
124
124
  contain at most the service's 128-part limit; all batches belong to the same upload. One
125
125
  Resource identity with the same declared person-reference classification is uploaded once within one Runtime operation. Hypit keeps no upload catalog or
126
- cross-Build cache. Seedance visual references can carry `personReference` in their media fields;
126
+ cross-Build cache. Seedance visual references require boolean `personReference` in their media fields;
127
127
  the mapping declares it as a resource-transport field and the upload session receives
128
- `is_person_reference`, preserving true, false and omission. It stays out of the generation body.
128
+ `is_person_reference`, preserving true and false. It stays out of the generation body.
129
129
  This covers reference images, reference videos, and first/last frames for every declared Seedance
130
- variant. Omission remains absent on the wire; HypiHub's upload API currently defaults it to false,
131
- so omission does not enable detection or person-reference preparation.
130
+ variant. Seedance rejects omission before upload; no automatic face detection is requested.
132
131
  HypiHub stores the authored classification and prepares the applicable upstream person reference;
133
132
  this Provider does not detect faces or select an upstream private-avatar group.
134
133
 
@@ -25,7 +25,7 @@ each media input as a typed item with its `role` (`first_frame`, `last_frame`, `
25
25
 
26
26
  The Seedance endpoints add `generate_audio`. Monid documents no web search field for them, so
27
27
  `web-search="true"` is unsupported, and Seedance 2.5 frame mode (`first-frame` present) requires
28
- `aspect-ratio="adaptive"`. Seedance visual references may carry `person-reference`; the Provider
28
+ `aspect-ratio="adaptive"`. Seedance visual references require `person-reference`; the Provider
29
29
  accepts the declaration and transmits nothing for it, since the endpoint has no field for it.
30
30
  Seedance rejects reference images and videos that contain a real human face; Monid offers no way to
31
31
  register authorized portrait material, so such a request fails with the upstream moderation error.
@@ -19,7 +19,7 @@ the models the Distribution already describes.
19
19
  Video requests write the prompt and each media input as one item of the protocol's `content`
20
20
  array with its `role` (`first_frame`, `last_frame`, `reference_image`, `reference_video`,
21
21
  `reference_audio`), then `resolution`, `ratio`, `duration` and, for Seedance, `generate_audio`;
22
- `web-search="true"` adds `tools: [{ "type": "web_search" }]`. Seedance visual references may carry
22
+ `web-search="true"` adds `tools: [{ "type": "web_search" }]`. Seedance visual references require
23
23
  `person-reference`; the Provider accepts the declaration and transmits nothing for it, since the Ark
24
24
  protocol has no such field. Seedance 2.0 and 2.5 reject reference images and videos that contain a
25
25
  real human face; TokenDance offers no way to register authorized portrait material, so such a request
@@ -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);