@hypit/hypit 0.1.12 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -2
- package/bin/hypit.mjs +0 -2
- package/dist/public/model-kit.d.ts +16 -0
- package/package.json +3 -1
- package/packages/cli/README.md +3 -0
- package/packages/cli/package.json +1 -0
- package/packages/cli/src/commands/environment.ts +12 -7
- package/packages/cli/src/commands/results.ts +19 -3
- package/packages/cli/src/main.ts +2 -1
- package/packages/cli/src/oauth.ts +50 -7
- package/packages/cli/src/output.ts +2 -2
- package/packages/cli/src/source-discovery.ts +2 -2
- package/packages/credential-store-file/README.md +60 -0
- package/packages/credential-store-file/package.json +21 -0
- package/packages/credential-store-file/src/activation.ts +29 -0
- package/packages/credential-store-file/src/index.ts +1 -0
- package/packages/credential-store-file/src/store.ts +89 -0
- package/packages/fonts-open/src/surface.ts +8 -2
- package/packages/hyperframes/README.md +16 -2
- package/packages/hyperframes/src/browser-program.ts +6 -1
- package/packages/hyperframes/src/document.ts +31 -21
- package/packages/hyperframes/src/project.ts +11 -20
- package/packages/media-execution/README.md +7 -0
- package/packages/media-execution/src/execute.ts +5 -5
- package/packages/media-execution/src/index.ts +1 -1
- package/packages/media-execution/src/process-env.ts +20 -0
- package/packages/media-execution/src/surface.ts +83 -76
- package/packages/model-kit/README.md +28 -0
- package/packages/model-kit/src/index.ts +58 -19
- package/packages/package-loader-node/README.md +10 -0
- package/packages/package-loader-node/src/index.ts +1 -0
- package/packages/package-loader-node/src/loader.ts +25 -5
- package/packages/package-loader-node/src/location.ts +11 -2
- package/packages/project-context-node/README.md +2 -0
- package/packages/project-context-node/src/project-context.ts +5 -3
- package/packages/provider-hyperframes-local/README.md +95 -13
- package/packages/provider-hyperframes-local/package.json +13 -3
- package/packages/provider-hyperframes-local/src/activation.ts +20 -6
- package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
- package/packages/provider-hyperframes-local/src/browser.ts +118 -0
- package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
- package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
- package/packages/provider-hyperframes-local/src/capture-process.ts +54 -59
- package/packages/provider-hyperframes-local/src/capture-worker.ts +28 -7
- package/packages/provider-hyperframes-local/src/capture.ts +12 -5
- package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
- package/packages/provider-hyperframes-local/src/options.ts +2 -2
- package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
- package/packages/provider-hyperframes-local/src/process.ts +22 -0
- package/packages/provider-hyperframes-local/src/program.ts +25 -33
- package/packages/provider-hyperframes-local/src/provider.ts +4 -7
- package/packages/provider-hyperframes-local/src/render.ts +9 -4
- package/packages/provider-hypihub/README.md +3 -0
- package/packages/runtime-host-node/README.md +5 -0
- package/packages/runtime-host-node/src/index.ts +7 -2
- package/packages/runtime-host-node/src/packages.ts +13 -7
- package/packages/runtime-local/README.md +10 -0
- package/packages/runtime-local/src/config.ts +1 -1
- package/packages/runtime-local/src/credentials.ts +10 -5
- package/packages/runtime-local/src/runtime.ts +1 -1
- package/packages/seedance/README.md +42 -0
- package/packages/seedance/src/index.ts +5 -22
- package/packages/seedance/src/surface.ts +2 -3
- package/packages/seedance/src/validation.ts +16 -0
- package/packages/studio/src/preview/runtime-shim.ts +17 -3
- package/packages/studio/src/server.ts +2 -2
- package/packages/video-cli/README.md +5 -1
- package/packages/video-cli/package.json +1 -0
- package/packages/video-cli/src/distribution.ts +3 -10
- package/packages/video-cli/src/version.ts +1 -1
- package/packages/workspace-fs-node/src/workspace.ts +2 -2
- package/packages/yt-dlp/README.md +3 -2
- package/packages/yt-dlp/package.json +4 -0
- package/packages/yt-dlp/src/download.ts +10 -14
|
@@ -229,23 +229,22 @@ function exactFontStyle(element: VisualElement, stableId: StableDomId): string[]
|
|
|
229
229
|
];
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
): string {
|
|
232
|
+
type ElementContext = {
|
|
233
|
+
readonly trackId: string;
|
|
234
|
+
readonly presentId: string;
|
|
235
|
+
readonly presentStart: string;
|
|
236
|
+
readonly presentDuration: string;
|
|
237
|
+
readonly presentDurationFrames: number;
|
|
238
|
+
readonly presentStartFrame: number;
|
|
239
|
+
readonly programNumerator: number;
|
|
240
|
+
readonly programDenominator: number;
|
|
241
|
+
readonly stackIndex: number;
|
|
242
|
+
readonly emittedFilterIds: Set<string>;
|
|
243
|
+
readonly stableId: StableDomId;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/** Every emitted element, including SVG mask sources, keeps the same authored frame clock. */
|
|
247
|
+
function elementPresentation(element: VisualElement, context: ElementContext) {
|
|
249
248
|
const id = context.stableId([context.trackId, context.presentId, element.id]);
|
|
250
249
|
const animationName = element.animation === undefined
|
|
251
250
|
? undefined
|
|
@@ -277,6 +276,16 @@ function renderElement(
|
|
|
277
276
|
? ""
|
|
278
277
|
: ` data-hypit-frame-animation data-hypit-animation-start-frame="${context.presentStartFrame}" data-hypit-animation-duration-frames="${animationDurationFrames}" data-hypit-animation-sample-frames="${context.presentDurationFrames}" data-hypit-animation-properties="${animationProperties.join(",")}"`;
|
|
279
278
|
const common = `${commonAttributes}${animationAttributes} style="${escapeHtml(inlineStyle)}"`;
|
|
279
|
+
return { inlineStyle, commonAttributes: `${commonAttributes}${animationAttributes}`, common };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderElement(
|
|
283
|
+
element: VisualElement,
|
|
284
|
+
children: ReadonlyMap<string, readonly VisualElement[]>,
|
|
285
|
+
context: ElementContext,
|
|
286
|
+
): string {
|
|
287
|
+
const { inlineStyle, commonAttributes, common } = elementPresentation(element, context);
|
|
288
|
+
const id = context.stableId([context.trackId, context.presentId, element.id]);
|
|
280
289
|
if (element.kind === "mask") {
|
|
281
290
|
const direct = children.get(element.id) ?? [];
|
|
282
291
|
const maskRoot = direct.find((child) => child.id === element.maskElement);
|
|
@@ -295,6 +304,7 @@ function renderElement(
|
|
|
295
304
|
throw new Error(`Local mask ${element.id} mask source must be one terminal owned element.`);
|
|
296
305
|
}
|
|
297
306
|
const maskSource = (() => {
|
|
307
|
+
const sourcePresentation = elementPresentation(maskRoot, context);
|
|
298
308
|
if (maskRoot.kind === "text") {
|
|
299
309
|
const alignment = maskRoot.style.find((declaration) => declaration.name === "text-align")?.value;
|
|
300
310
|
const anchor = alignment === "right" || alignment === "end" ? "end" : alignment === "left" || alignment === "start" ? "start" : "middle";
|
|
@@ -310,17 +320,17 @@ function renderElement(
|
|
|
310
320
|
? blockAlignment === "flex-start" ? "0" : blockAlignment === "flex-end" ? "100%" : "50%"
|
|
311
321
|
: String(blockAlignment === "flex-start" ? paddingTop : blockAlignment === "flex-end" ? maskHeight - paddingBottom : (paddingTop + maskHeight - paddingBottom) / 2);
|
|
312
322
|
const baseline = blockAlignment === "flex-start" ? "text-before-edge" : blockAlignment === "flex-end" ? "text-after-edge" : "central";
|
|
313
|
-
const textStyle =
|
|
314
|
-
return `<text${
|
|
323
|
+
const textStyle = `${sourcePresentation.inlineStyle};fill:currentColor`;
|
|
324
|
+
return `<text ${sourcePresentation.commonAttributes} x="${x}" y="${y}" text-anchor="${anchor}" dominant-baseline="${baseline}" style="${escapeHtml(textStyle)}">${escapeHtml(maskRoot.text)}</text>`;
|
|
315
325
|
}
|
|
316
326
|
if (maskRoot.kind === "text-flow" || maskRoot.kind === "path-text") {
|
|
317
327
|
return `<foreignObject x="0" y="0" width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="position:relative;width:100%;height:100%">${renderOwned(maskRoot)}</div></foreignObject>`;
|
|
318
328
|
}
|
|
319
329
|
if (maskRoot.kind === "image") {
|
|
320
|
-
return `<image x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.artifact.resource))}"
|
|
330
|
+
return `<image ${sourcePresentation.common} x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.artifact.resource))}"/>`;
|
|
321
331
|
}
|
|
322
332
|
if (maskRoot.kind === "surface" && maskRoot.surface.timing.kind === "still") {
|
|
323
|
-
return `<image data-hypit-surface-resource="${maskRoot.surface.artifact.resource}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.surface.artifact.resource))}"
|
|
333
|
+
return `<image ${sourcePresentation.common} data-hypit-surface-resource="${maskRoot.surface.artifact.resource}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" href="${escapeHtml(hyperframesResourceUri(maskRoot.surface.artifact.resource))}"/>`;
|
|
324
334
|
}
|
|
325
335
|
throw new Error(`Local mask ${element.id} requires a terminal owned text, image or still Surface mask source.`);
|
|
326
336
|
})();
|
|
@@ -12,9 +12,10 @@ export type HyperframesArtifactReader = (
|
|
|
12
12
|
artifact: BlobRef,
|
|
13
13
|
signal?: AbortSignal,
|
|
14
14
|
) => Promise<Uint8Array | AsyncIterable<Uint8Array>>;
|
|
15
|
+
/** Inspect the completed staged file; its lifetime belongs to the staging caller. */
|
|
15
16
|
export type HyperframesSurfaceValidator = (
|
|
16
17
|
surface: CompositableSurfaceRef,
|
|
17
|
-
|
|
18
|
+
path: string,
|
|
18
19
|
signal?: AbortSignal,
|
|
19
20
|
) => Promise<void>;
|
|
20
21
|
|
|
@@ -39,9 +40,7 @@ function extension(mediaType: string): string {
|
|
|
39
40
|
/**
|
|
40
41
|
* Lay one document out as a directory HyperFrames can render.
|
|
41
42
|
*
|
|
42
|
-
*
|
|
43
|
-
* Provider serves it to the capture engine, and the AWS Provider tars it into a site. If
|
|
44
|
-
* the two laid it out differently, one Need would name two different projects.
|
|
43
|
+
* A local or hosted Provider can serve the same staged document to its capture engine.
|
|
45
44
|
* Resource names are local to this staged project and carry no content claim.
|
|
46
45
|
*/
|
|
47
46
|
export async function stageHyperframesProject(options: {
|
|
@@ -63,25 +62,25 @@ export async function stageHyperframesProject(options: {
|
|
|
63
62
|
await mkdir(artifactDirectory, { recursive: true });
|
|
64
63
|
const paths = new Map<string, string>();
|
|
65
64
|
const surfaces = new Map(document.surfaces.map((surface) => [surface.artifact.resource, surface]));
|
|
66
|
-
await Promise.allSettled(document.artifacts.map(async (artifact) => {
|
|
65
|
+
await Promise.allSettled(document.artifacts.map(async (artifact, index) => {
|
|
67
66
|
try {
|
|
68
67
|
signal.throwIfAborted();
|
|
69
|
-
|
|
68
|
+
// Resource identities are portable protocol values, not filesystem names.
|
|
69
|
+
const name = `asset-${index}${extension(artifact.mediaType)}`;
|
|
70
70
|
const opened = await read(artifact, signal);
|
|
71
71
|
signal.throwIfAborted();
|
|
72
72
|
const chunks = opened instanceof Uint8Array
|
|
73
73
|
? (async function* () { yield opened; })()
|
|
74
74
|
: opened;
|
|
75
75
|
const surface = surfaces.get(artifact.resource);
|
|
76
|
-
const retained: Uint8Array[] = [];
|
|
77
76
|
let size = 0;
|
|
78
|
-
const
|
|
77
|
+
const path = join(artifactDirectory, name);
|
|
78
|
+
const target = await open(path, "w");
|
|
79
79
|
try {
|
|
80
80
|
for await (const chunk of chunks) {
|
|
81
81
|
signal.throwIfAborted();
|
|
82
|
-
await target.
|
|
82
|
+
await target.writeFile(chunk);
|
|
83
83
|
size += chunk.byteLength;
|
|
84
|
-
if (surface !== undefined) retained.push(Uint8Array.from(chunk));
|
|
85
84
|
}
|
|
86
85
|
} finally {
|
|
87
86
|
await target.close();
|
|
@@ -89,17 +88,9 @@ export async function stageHyperframesProject(options: {
|
|
|
89
88
|
if (size !== artifact.size) {
|
|
90
89
|
throw new Error(`HyperFrames Artifact ${artifact.resource} size differs`);
|
|
91
90
|
}
|
|
92
|
-
const bytes = surface === undefined ? undefined : (() => {
|
|
93
|
-
const value = new Uint8Array(size);
|
|
94
|
-
let offset = 0;
|
|
95
|
-
for (const chunk of retained) {
|
|
96
|
-
value.set(chunk, offset);
|
|
97
|
-
offset += chunk.byteLength;
|
|
98
|
-
}
|
|
99
|
-
return value;
|
|
100
|
-
})();
|
|
101
91
|
if (surface !== undefined) {
|
|
102
|
-
|
|
92
|
+
// The validator borrows the completed staged file for this call only.
|
|
93
|
+
await options.validateSurface!(structuredClone(surface), path, signal);
|
|
103
94
|
}
|
|
104
95
|
paths.set(artifact.resource, `./artifacts/${name}`);
|
|
105
96
|
} catch (error) {
|
|
@@ -26,6 +26,13 @@ deployments can reuse the same functions to preserve stream selection, timing, c
|
|
|
26
26
|
This is not an author package, Provider, queue or Core extension. It performs no endpoint selection,
|
|
27
27
|
credential lookup or SVML parsing.
|
|
28
28
|
|
|
29
|
+
`verifyCompositableSurfaceFile({ surface, path, ... })` inspects a file already staged by an execution
|
|
30
|
+
owner. That owner must keep it unchanged until the call settles, including cancellation; inspection
|
|
31
|
+
neither copies nor removes it. `verifyCompositableSurfaceBytes({ surface, bytes, ... })` remains a
|
|
32
|
+
convenience wrapper for callers that have bytes, creates its own temporary file, and removes that
|
|
33
|
+
file only after probing has settled. Both use the same decoded Surface checks and return no extra
|
|
34
|
+
result facts. Filesystem paths belong to this Node execution boundary, not to Surface or Blob refs.
|
|
35
|
+
|
|
29
36
|
Audio gain envelopes and audible subranges are evaluated per sample after tempo/looping and Clip
|
|
30
37
|
fades, before the requested range is cropped. Original source and envelope progress therefore survive
|
|
31
38
|
local silence and range rendering. This is generic AudioProgramPlan execution, independent of author
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
} from "@hypit/protocol";
|
|
34
34
|
import type { BlobRef, CanonicalValue, StoredValue } from "@hypit/protocol";
|
|
35
35
|
|
|
36
|
+
import { mediaProcessEnv } from "./process-env.js";
|
|
36
37
|
import { parseMediaInspection } from "./probe.js";
|
|
37
38
|
import {
|
|
38
39
|
compositeAnimatedWebpFrame,
|
|
@@ -98,10 +99,7 @@ async function runProcess(args: {
|
|
|
98
99
|
shell: false,
|
|
99
100
|
windowsHide: true,
|
|
100
101
|
stdio: ["ignore", "pipe", "pipe"],
|
|
101
|
-
env: {
|
|
102
|
-
PATH: process.env.PATH ?? "",
|
|
103
|
-
...(args.sharedLibraryPath === undefined ? {} : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
|
|
104
|
-
},
|
|
102
|
+
env: mediaProcessEnv(args.sharedLibraryPath === undefined ? undefined : { LD_LIBRARY_PATH: args.sharedLibraryPath }),
|
|
105
103
|
});
|
|
106
104
|
const stdout: Buffer[] = [];
|
|
107
105
|
let stdoutBytes = 0;
|
|
@@ -678,8 +676,10 @@ export async function executeNormalizeMedia(
|
|
|
678
676
|
"scale=trunc(iw*max(sar\\,1)/2)*2:trunc(ih*max(1/sar\\,1)/2)*2",
|
|
679
677
|
"setsar=1",
|
|
680
678
|
].join(",");
|
|
679
|
+
// FFmpeg enables autorotation by default. Its positive boolean option changed
|
|
680
|
+
// syntax across releases; no override is needed to materialize source rotation.
|
|
681
681
|
const visualInput = animation === undefined
|
|
682
|
-
? [...encoding.inputArgs, "-
|
|
682
|
+
? [...encoding.inputArgs, "-i", input, "-map", `0:${plan.video.index}`]
|
|
683
683
|
: ["-f", "concat", "-safe", "0", "-i", await animatedWebpConcat(env, animation, work), "-map", "0:v:0"];
|
|
684
684
|
// The execution format must retain the source's alpha while materializing
|
|
685
685
|
// the program clock. Both encodings publish the same SynchronizedMedia type.
|
|
@@ -10,7 +10,7 @@ export {
|
|
|
10
10
|
executeTransformMedia,
|
|
11
11
|
} from "./execute.js";
|
|
12
12
|
export { parseMediaInspection } from "./probe.js";
|
|
13
|
-
export { verifyCompositableSurfaceBytes } from "./surface.js";
|
|
13
|
+
export { verifyCompositableSurfaceBytes, verifyCompositableSurfaceFile } from "./surface.js";
|
|
14
14
|
export { probeMediaToolchain } from "./toolchain.js";
|
|
15
15
|
export type { MediaToolchainState } from "./toolchain.js";
|
|
16
16
|
export type * from "./execute.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const windowsProcessCreation = ["PATHEXT", "SYSTEMROOT", "WINDIR", "ComSpec"] as const;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ffmpeg and ffprobe are started as PATH executables. The Host environment is
|
|
5
|
+
* not forwarded, so credential values stay out of those processes. Windows still
|
|
6
|
+
* needs the process-creation variables CreateProcess uses to resolve `.exe`.
|
|
7
|
+
*/
|
|
8
|
+
export function mediaProcessEnv(
|
|
9
|
+
extra?: Readonly<Record<string, string>>,
|
|
10
|
+
): NodeJS.ProcessEnv {
|
|
11
|
+
const env: NodeJS.ProcessEnv = { PATH: process.env.PATH ?? "" };
|
|
12
|
+
if (process.platform === "win32") {
|
|
13
|
+
for (const name of windowsProcessCreation) {
|
|
14
|
+
const value = process.env[name];
|
|
15
|
+
if (value !== undefined && value.length > 0) env[name] = value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
if (extra !== undefined) Object.assign(env, extra);
|
|
19
|
+
return env;
|
|
20
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdtemp, rm, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
|
|
6
6
|
import { assertCompositableSurfaceRef } from "@hypit/media";
|
|
7
|
+
import { mediaProcessEnv } from "./process-env.js";
|
|
7
8
|
import type { CompositableSurfaceRef } from "@hypit/media";
|
|
8
9
|
|
|
9
10
|
type JsonObject = Record<string, unknown>;
|
|
@@ -57,7 +58,7 @@ async function runProcess(args: {
|
|
|
57
58
|
shell: false,
|
|
58
59
|
windowsHide: true,
|
|
59
60
|
stdio: ["ignore", "pipe", "pipe"],
|
|
60
|
-
env:
|
|
61
|
+
env: mediaProcessEnv(),
|
|
61
62
|
});
|
|
62
63
|
const stdout: Buffer[] = [];
|
|
63
64
|
let bytes = 0;
|
|
@@ -192,95 +193,101 @@ async function pixelFormatAlpha(
|
|
|
192
193
|
return result;
|
|
193
194
|
}
|
|
194
195
|
|
|
195
|
-
|
|
196
|
-
if (mediaType === "image/png") return ".png";
|
|
197
|
-
if (mediaType === "image/webp") return ".webp";
|
|
198
|
-
if (mediaType === "video/webm") return ".webm";
|
|
199
|
-
if (mediaType === "video/mp4") return ".mp4";
|
|
200
|
-
throw new Error(`Surface media type ${mediaType} is unsupported`);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Decode and verify one typed Surface before its bytes enter a renderer.
|
|
205
|
-
*
|
|
206
|
-
* This is an admission gate, not a second media Product. Success returns no
|
|
207
|
-
* side-channel facts; anything a later graph step needs must be an explicit output.
|
|
208
|
-
*/
|
|
209
|
-
export async function verifyCompositableSurfaceBytes(options: {
|
|
196
|
+
type SurfaceInspectionOptions = {
|
|
210
197
|
readonly surface: CompositableSurfaceRef;
|
|
211
|
-
readonly bytes: Uint8Array;
|
|
212
198
|
readonly ffprobePath?: string;
|
|
213
199
|
readonly processTimeoutMs?: number;
|
|
214
200
|
readonly maxProbeOutputBytes?: number;
|
|
215
201
|
readonly signal?: AbortSignal;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Inspect bytes supplied by callers that do not already own a staged file. */
|
|
205
|
+
export async function verifyCompositableSurfaceBytes(options: SurfaceInspectionOptions & {
|
|
206
|
+
readonly bytes: Uint8Array;
|
|
207
|
+
}): Promise<void> {
|
|
208
|
+
options.signal?.throwIfAborted();
|
|
209
|
+
assertCompositableSurfaceRef(options.surface);
|
|
210
|
+
assert(options.bytes.byteLength === options.surface.artifact.size,
|
|
211
|
+
`Surface ${options.surface.artifact.resource} byte size differs`);
|
|
212
|
+
const directory = await mkdtemp(join(tmpdir(), "hypit-surface-verify-"));
|
|
213
|
+
try {
|
|
214
|
+
// FFprobe detects the encoded media itself; both entry points inspect the same formats.
|
|
215
|
+
const path = join(directory, "surface");
|
|
216
|
+
await writeFile(path, options.bytes, { signal: options.signal });
|
|
217
|
+
await verifyCompositableSurfaceFile({ ...options, path });
|
|
218
|
+
} finally {
|
|
219
|
+
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Inspect an existing execution file without copying it. The caller owns the file
|
|
225
|
+
* and keeps it unchanged until this call settles. Success returns no side-channel facts.
|
|
226
|
+
*/
|
|
227
|
+
export async function verifyCompositableSurfaceFile(options: SurfaceInspectionOptions & {
|
|
228
|
+
readonly path: string;
|
|
216
229
|
}): Promise<void> {
|
|
217
230
|
const controller = new AbortController();
|
|
218
231
|
const signal = options.signal === undefined ? controller.signal : AbortSignal.any([controller.signal, options.signal]);
|
|
219
232
|
signal.throwIfAborted();
|
|
220
233
|
assertCompositableSurfaceRef(options.surface);
|
|
221
|
-
assert(options.
|
|
234
|
+
assert((await stat(options.path)).size === options.surface.artifact.size,
|
|
222
235
|
`Surface ${options.surface.artifact.resource} byte size differs`);
|
|
236
|
+
signal.throwIfAborted();
|
|
223
237
|
const ffprobePath = options.ffprobePath ?? "ffprobe";
|
|
224
238
|
const timeoutMs = options.processTimeoutMs ?? 120_000;
|
|
225
239
|
const maxOutputBytes = options.maxProbeOutputBytes ?? 8 * 1024 * 1024;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
assert(rotation(stream) === 0, "Surface must not depend on display rotation metadata");
|
|
257
|
-
assert(stream.field_order === undefined || stream.field_order === "progressive" || stream.field_order === "unknown",
|
|
258
|
-
"Surface must be progressive");
|
|
259
|
-
assertSrgb(stream);
|
|
240
|
+
const pending = [
|
|
241
|
+
runProcess({
|
|
242
|
+
executable: ffprobePath,
|
|
243
|
+
argv: ["-v", "error", "-print_format", "json", "-show_streams", "-count_frames", options.path],
|
|
244
|
+
timeoutMs,
|
|
245
|
+
maxOutputBytes, signal,
|
|
246
|
+
}),
|
|
247
|
+
pixelFormatAlpha(ffprobePath, timeoutMs, maxOutputBytes, signal),
|
|
248
|
+
] as const;
|
|
249
|
+
await Promise.allSettled(pending.map(async (job) => {
|
|
250
|
+
try { return await job; } catch (error) { controller.abort(error); throw error; }
|
|
251
|
+
}));
|
|
252
|
+
signal.throwIfAborted();
|
|
253
|
+
const [probe, formats] = await Promise.all(pending);
|
|
254
|
+
const root = json(probe, "ffprobe Surface query");
|
|
255
|
+
assert(Array.isArray(root.streams) && root.streams.length === 1,
|
|
256
|
+
"Surface must contain exactly one visual stream and no audio or auxiliary streams");
|
|
257
|
+
const stream = root.streams[0] as ProbeStream;
|
|
258
|
+
assert(stream.codec_type === "video", "Surface stream is not visual");
|
|
259
|
+
assert(stream.disposition?.attached_pic !== 1, "Surface cannot be an attached-picture stream");
|
|
260
|
+
const width = positiveInteger(stream.width, "Surface width");
|
|
261
|
+
const height = positiveInteger(stream.height, "Surface height");
|
|
262
|
+
assert(width === options.surface.width && height === options.surface.height,
|
|
263
|
+
"Surface decoded dimensions differ from its declaration");
|
|
264
|
+
assert(stream.sample_aspect_ratio === undefined || stream.sample_aspect_ratio === "1:1",
|
|
265
|
+
"Surface must use square pixels");
|
|
266
|
+
assert(rotation(stream) === 0, "Surface must not depend on display rotation metadata");
|
|
267
|
+
assert(stream.field_order === undefined || stream.field_order === "progressive" || stream.field_order === "unknown",
|
|
268
|
+
"Surface must be progressive");
|
|
269
|
+
assertSrgb(stream);
|
|
260
270
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
assert(typeof stream.pix_fmt === "string" && stream.pix_fmt.length > 0,
|
|
272
|
+
"Surface pixel format is absent");
|
|
273
|
+
const pixelAlpha = formats.get(stream.pix_fmt);
|
|
274
|
+
assert(pixelAlpha !== undefined, `Surface pixel format ${stream.pix_fmt} is unknown to ffprobe`);
|
|
275
|
+
const taggedAlpha = stream.tags?.alpha_mode === "1" || stream.tags?.alpha_mode === "straight";
|
|
276
|
+
const hasAlpha = pixelAlpha || taggedAlpha;
|
|
277
|
+
if (options.surface.alphaMode === "straight") {
|
|
278
|
+
assert(hasAlpha, "Surface declares straight alpha but its bytes carry no alpha channel");
|
|
279
|
+
} else {
|
|
280
|
+
assert(!hasAlpha, "Surface declares opaque pixels but its encoded format carries alpha");
|
|
281
|
+
}
|
|
272
282
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
} finally {
|
|
284
|
-
await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
283
|
+
const frameCount = positiveInteger(stream.nb_read_frames ?? stream.nb_frames, "Surface frame count");
|
|
284
|
+
if (options.surface.timing.kind === "still") {
|
|
285
|
+
assert(frameCount === 1, "Still Surface must decode to exactly one frame");
|
|
286
|
+
} else {
|
|
287
|
+
assert(frameCount === options.surface.timing.frameCount,
|
|
288
|
+
"Surface decoded frame count differs from its declaration");
|
|
289
|
+
const rate = rational(stream.avg_frame_rate ?? stream.r_frame_rate, "Surface frame rate");
|
|
290
|
+
assert(sameRational(rate, options.surface.timing.frameRate),
|
|
291
|
+
"Surface decoded frame rate differs from its declaration");
|
|
285
292
|
}
|
|
286
293
|
}
|
|
@@ -50,3 +50,31 @@ exact fields, constraints, model identity, capability name and validator. The he
|
|
|
50
50
|
Provider routing, fallback, credentials or Runtime authority.
|
|
51
51
|
|
|
52
52
|
This is a package-authoring utility, not an author-importable model by itself.
|
|
53
|
+
|
|
54
|
+
## Model-owned request checks
|
|
55
|
+
|
|
56
|
+
An endpoint may supply two synchronous, pure functions in addition to its port table:
|
|
57
|
+
|
|
58
|
+
- `validateInputs(inputs)` checks values already supplied, both in drafts and complete requests.
|
|
59
|
+
Text and media may still be missing from a draft. Use this for rules such as a reference's media
|
|
60
|
+
type; do not require a future input to exist.
|
|
61
|
+
- `validateRequest(request)` checks additional relationships that require the complete request.
|
|
62
|
+
It runs after the port table and `validateInputs`, when every required input is available.
|
|
63
|
+
|
|
64
|
+
Both return `void` and throw a useful error to reject. They read the supplied values only: no IO,
|
|
65
|
+
Provider selection, request mutation or saved validation result. Existing port rules remain in the
|
|
66
|
+
port table. The helper supplies structural validation before calling either function.
|
|
67
|
+
|
|
68
|
+
Register each rule once in the endpoint definition. The helper connects these functions to Type
|
|
69
|
+
validators, media/Text bindings, finalization, generation and planning. Planning checks the known
|
|
70
|
+
values and leaves future media on the existing graph edges. A directly supplied request or an
|
|
71
|
+
existing Need receives full validation, just as it does during execution.
|
|
72
|
+
|
|
73
|
+
For a public request builder, use `model.endpoints.image.sealRequest(ports)`: it canonicalizes the
|
|
74
|
+
request and applies all model rules. `endpoint.validateRequest(value)` checks an existing complete
|
|
75
|
+
request; `endpoint.validateDraft(value)` checks a partially assembled one. Calling the common
|
|
76
|
+
generation port helpers alone applies only the port table, not these model-owned functions.
|
|
77
|
+
|
|
78
|
+
Keep service-specific limits in the Provider. Its existing `supports` and pre-submission preparation
|
|
79
|
+
should share its own checks, rejecting known unsupported values before resource transfer. This does
|
|
80
|
+
not require the Provider to import the Model implementation or add any fields to the request.
|