@hraness/slopcamera 3.2.7 → 3.3.1
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/NOTICE.md +1 -7
- package/PRIVACY.md +16 -7
- package/README.md +65 -20
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +6 -9
- package/apps/desktop/application/default-registry.ts +26 -10
- package/apps/desktop/application/operation.ts +14 -5
- package/apps/desktop/application/operations/index.ts +4 -1
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +528 -97
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +31 -2
- package/apps/desktop/cli/commands.ts +1215 -70
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +183 -23
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +26 -50
- package/apps/desktop/cli/menubar.ts +244 -0
- package/apps/desktop/cli/music-analysis-service.ts +61 -0
- package/apps/desktop/cli/portable-surface.ts +18 -5
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/application-node-planner.ts +0 -47
- package/apps/desktop/code/host-source-layout.ts +46 -0
- package/apps/desktop/code/public.ts +0 -1
- package/apps/desktop/code/runtime-identity.ts +50 -8
- package/apps/desktop/code/semantic-builder.ts +24 -43
- package/apps/desktop/code/source-bundle.ts +23 -6
- package/apps/desktop/code/source-typecheck.ts +131 -43
- package/apps/desktop/code/worker-client.ts +14 -2
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/music-analysis.ts +124 -1
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +737 -291
- package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
- package/apps/desktop/html-overlay/index.ts +9 -0
- package/apps/desktop/html-overlay/runtime.ts +2 -0
- package/apps/desktop/html-overlay/scene.ts +29 -0
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +32 -434
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -240
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/docs/studio.md +1 -1
- package/package.json +11 -24
- package/skills/slopcamera/SKILL.md +14 -1
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +2 -2
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +250 -102
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -23
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/support-completion.ts +16 -0
- package/src/support.ts +42 -0
- package/src/version.ts +1 -1
- package/apps/desktop/application/operations/recording/index.ts +0 -5
- package/apps/desktop/application/operations/recording/pause.ts +0 -34
- package/apps/desktop/application/operations/recording/resume.ts +0 -34
- package/apps/desktop/application/operations/recording/shared.ts +0 -199
- package/apps/desktop/application/operations/recording/start.ts +0 -169
- package/apps/desktop/application/operations/recording/stop.ts +0 -34
- package/apps/desktop/capture/protocol.ts +0 -689
- package/apps/desktop/cli/capture-bundle.ts +0 -1117
- package/apps/desktop/cli/recording-controller.ts +0 -1567
- package/apps/desktop/cli/recording-daemon.ts +0 -931
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-mcy8z0br.js +0 -192
- package/dist/index-p63wavx0.js +0 -2029
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-rvghzt7b.js +0 -2013
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-z7239b4h.js +0 -2363
- package/dist/skill-install-dh0ntqba.js +0 -10
- package/src/desktop.ts +0 -253
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { createBoundedJsonSnapshot } from "../../../src/code/json-snapshot";
|
|
8
|
+
import {
|
|
9
|
+
SpatialCameraIdSchema, SpatialTimeUsSchema, type EvaluatedSpatialScene, type SpatialSceneV1,
|
|
10
|
+
} from "../../../src/spatial-scene/contracts";
|
|
11
|
+
import {
|
|
12
|
+
auditSpatialSceneRendered, decodeObjectIdPixels, SPATIAL_RENDERED_AUDIT_COVERAGE,
|
|
13
|
+
SPATIAL_RENDERED_AUDIT_LIMITS, type SpatialRenderedAuditFrame, type SpatialRenderedAuditReport,
|
|
14
|
+
} from "../../../src/spatial-scene/audit-rendered";
|
|
15
|
+
import { spatialAuditDefaultTimesUs } from "../../../src/spatial-scene/audit";
|
|
16
|
+
import { evaluateSpatialScene } from "../../../src/spatial-scene/evaluate";
|
|
17
|
+
import { parseSpatialScene, parseSpatialValue, spatialAssetClosureDigests, spatialSceneSha256 } from "../../../src/spatial-scene/identity";
|
|
18
|
+
import { transformPoint, type Bounds, type Vec3 } from "../../../src/spatial-scene/math";
|
|
19
|
+
import { canonicalJson } from "../core/canonical-json";
|
|
20
|
+
import { assertHtmlOverlayGpuEvidenceProfile } from "../html-overlay/execution-profile";
|
|
21
|
+
import type { PreparedSpatialAsset } from "../html-overlay/spatial";
|
|
22
|
+
import { exactCapabilityByName } from "./capability-binding";
|
|
23
|
+
import { ApplicationError } from "./errors";
|
|
24
|
+
import { bindHtmlOverlayBrowserRuntime, HtmlOverlayBrowserRuntimeBindingSchema, type HtmlOverlayBrowserRuntimeBinding } from "./html-overlay-browser-runtime";
|
|
25
|
+
import { createHtmlOverlayExecutionBundle, HtmlOverlayExecutionIntegritySchema } from "./html-overlay-integrity";
|
|
26
|
+
import type { OperationExecutionContext } from "./operation";
|
|
27
|
+
import { assertMediaCapabilities, bindExpectedMediaCapabilities, MediaCapabilityBindingsSchema, mediaCapabilityCommand, mediaCapabilityRunner, type MediaCapabilityName } from "./operations/media/capabilities";
|
|
28
|
+
import { AbortBoundApplicationRunner, createMediaOperationWorkspace } from "./operations/media/shared";
|
|
29
|
+
import { throwIfAborted } from "./operations/shared";
|
|
30
|
+
import { frameName, partitionSpatialRenderWindow, readPhysical, SPATIAL_RENDER_LIMITS, verifyPng } from "./spatial-render";
|
|
31
|
+
import { withPreparedSpatialAssets } from "./spatial-assets";
|
|
32
|
+
|
|
33
|
+
function checked(condition: boolean, message: string): asserts condition {
|
|
34
|
+
if (!condition) throw new ApplicationError("invalid-data", message);
|
|
35
|
+
}
|
|
36
|
+
const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
37
|
+
|
|
38
|
+
export const SpatialRenderedAuditRequestSchema = z.strictObject({
|
|
39
|
+
cameraId: SpatialCameraIdSchema,
|
|
40
|
+
/** Exact integer-microsecond samples; omitted means the shared default cadence. */
|
|
41
|
+
timesUs: z.array(SpatialTimeUsSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples).optional(),
|
|
42
|
+
});
|
|
43
|
+
export type SpatialRenderedAuditRequest = z.infer<typeof SpatialRenderedAuditRequestSchema>;
|
|
44
|
+
|
|
45
|
+
export interface SpatialRenderedAuditPlan {
|
|
46
|
+
readonly scene: SpatialSceneV1;
|
|
47
|
+
readonly sceneSha256: string;
|
|
48
|
+
readonly request: SpatialRenderedAuditRequest;
|
|
49
|
+
readonly timesUs: readonly number[];
|
|
50
|
+
readonly width: number;
|
|
51
|
+
readonly height: number;
|
|
52
|
+
readonly costs: {
|
|
53
|
+
readonly sourceBytes: number;
|
|
54
|
+
readonly renderPixels: number;
|
|
55
|
+
readonly pngBytesBound: number;
|
|
56
|
+
readonly stagingBytesBound: number;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pure planning: parses and bounds the scene, camera and sample list before any
|
|
62
|
+
* capability or workspace is touched. Sample order is ascending and unique; the
|
|
63
|
+
* analyzer re-verifies every bound on its own input.
|
|
64
|
+
*/
|
|
65
|
+
export function planSpatialRenderedAudit(sceneInput: unknown, requestInput: unknown): SpatialRenderedAuditPlan {
|
|
66
|
+
const scene = parseSpatialScene(sceneInput);
|
|
67
|
+
const request = parseSpatialValue(SpatialRenderedAuditRequestSchema, requestInput, "spatial rendered audit request");
|
|
68
|
+
const camera = scene.cameras.find(item => item.cameraId === request.cameraId);
|
|
69
|
+
checked(camera !== undefined, `Unknown camera ${request.cameraId}.`);
|
|
70
|
+
const { width, height } = camera.projection;
|
|
71
|
+
checked(width <= SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension && height <= SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension
|
|
72
|
+
&& width * height <= SPATIAL_RENDERED_AUDIT_LIMITS.framePixels, "Rendered audit dimensions exceed the qualified raster budget.");
|
|
73
|
+
const timesUs = [...new Set(request.timesUs ?? spatialAuditDefaultTimesUs(scene.durationUs))].sort((a, b) => a - b);
|
|
74
|
+
checked(timesUs.every(timeUs => timeUs <= scene.durationUs), "A rendered audit sample is outside the scene clock.");
|
|
75
|
+
checked(scene.entities.length * timesUs.length <= SPATIAL_RENDERED_AUDIT_LIMITS.entitySamples, "Rendered audit exceeds its entity-sample budget; pass fewer --times-us samples.");
|
|
76
|
+
const sourceBytes = scene.assets.reduce((sum, asset) => sum + asset.payload.bytes, 0);
|
|
77
|
+
checked(sourceBytes <= SPATIAL_RENDER_LIMITS.sourceBytes, "Scene asset closure exceeds its byte budget.");
|
|
78
|
+
const renderPixels = width * height * timesUs.length;
|
|
79
|
+
const pngBytesBound = (width * height * 5 + height * 8 + 65_536) * timesUs.length;
|
|
80
|
+
const stagingBytesBound = sourceBytes + pngBytesBound + SPATIAL_RENDER_LIMITS.metadataBytes;
|
|
81
|
+
checked(renderPixels <= SPATIAL_RENDER_LIMITS.totalPixels && pngBytesBound <= SPATIAL_RENDER_LIMITS.pngBytes, "Rendered audit staging exceeds its pixel or byte budget.");
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
scene, sceneSha256: spatialSceneSha256(scene), request, timesUs, width, height,
|
|
84
|
+
costs: Object.freeze({ sourceBytes, renderPixels, pngBytesBound, stagingBytesBound }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The audit's browser claim: html-browser, plus FFmpeg only when a declared video asset needs frame extraction. */
|
|
89
|
+
export function spatialRenderedAuditCapabilityNames(scene: SpatialSceneV1): readonly MediaCapabilityName[] {
|
|
90
|
+
return Object.freeze(scene.assets.some(asset => asset.interpretation.kind === "video")
|
|
91
|
+
? ["ffmpeg", "ffprobe", "html-browser"] as const : ["html-browser"] as const);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SpatialRenderedAuditHostInput {
|
|
95
|
+
readonly scene: unknown;
|
|
96
|
+
/** Adapter-owned contained root holding the scene's declared asset payloads. */
|
|
97
|
+
readonly assetRoot: string;
|
|
98
|
+
readonly request: unknown;
|
|
99
|
+
readonly capabilityBindings?: z.infer<typeof MediaCapabilityBindingsSchema>;
|
|
100
|
+
readonly browserRuntime?: HtmlOverlayBrowserRuntimeBinding;
|
|
101
|
+
}
|
|
102
|
+
/** Trusted injection seam for deterministic host tests; never serialized. */
|
|
103
|
+
export interface SpatialRenderedAuditDependencies {
|
|
104
|
+
readonly bindBrowserRuntime?: typeof bindHtmlOverlayBrowserRuntime;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Per-vertex bounds over the exact prepared primitives actually lowered,
|
|
109
|
+
* unioned per declared asset. Splat entries contribute their decoded
|
|
110
|
+
* SPZ-position enclosure — the same bounds the object-ID proxy lowers.
|
|
111
|
+
*/
|
|
112
|
+
function preparedAssetBounds(assets: readonly PreparedSpatialAsset[]): Record<string, Bounds> {
|
|
113
|
+
const union = new Map<string, { min: [number, number, number]; max: [number, number, number] }>();
|
|
114
|
+
for (const asset of assets) {
|
|
115
|
+
if (asset.kind !== "geometry" && asset.kind !== "splat") continue;
|
|
116
|
+
const entry = union.get(asset.assetId) ?? { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
|
|
117
|
+
if (asset.kind === "splat") {
|
|
118
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
119
|
+
entry.min[axis] = Math.min(entry.min[axis]!, asset.bounds.min[axis]!);
|
|
120
|
+
entry.max[axis] = Math.max(entry.max[axis]!, asset.bounds.max[axis]!);
|
|
121
|
+
}
|
|
122
|
+
union.set(asset.assetId, entry);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
for (const primitive of asset.primitives) {
|
|
126
|
+
const count = primitive.indices?.length ?? primitive.positions.length / 3;
|
|
127
|
+
for (let index = 0; index < count; index++) {
|
|
128
|
+
const offset = (primitive.indices?.[index] ?? index) * 3;
|
|
129
|
+
const point = transformPoint(primitive.matrix, [primitive.positions[offset]!, primitive.positions[offset + 1]!, primitive.positions[offset + 2]!]);
|
|
130
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
131
|
+
entry.min[axis] = Math.min(entry.min[axis]!, point[axis]!);
|
|
132
|
+
entry.max[axis] = Math.max(entry.max[axis]!, point[axis]!);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
union.set(asset.assetId, entry);
|
|
137
|
+
}
|
|
138
|
+
return Object.fromEntries([...union].map(([assetId, bounds]) => [assetId, {
|
|
139
|
+
min: Object.freeze([...bounds.min]) as unknown as Vec3,
|
|
140
|
+
max: Object.freeze([...bounds.max]) as unknown as Vec3,
|
|
141
|
+
}])) as Record<string, Bounds>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Renders each sampled time through the real object-ID pass, decodes the exact
|
|
146
|
+
* PNG rasters, and runs the portable analyzer. Splats lower as their
|
|
147
|
+
* bounding-box proxies and report `proxy-coverage` — approximate attribution,
|
|
148
|
+
* never splat pixel truth; nothing is published and no receipt is written.
|
|
149
|
+
*/
|
|
150
|
+
export async function auditSpatialSceneRenderedHost(
|
|
151
|
+
context: OperationExecutionContext,
|
|
152
|
+
input: SpatialRenderedAuditHostInput,
|
|
153
|
+
dependencies: SpatialRenderedAuditDependencies = {},
|
|
154
|
+
): Promise<SpatialRenderedAuditReport> {
|
|
155
|
+
const plan = planSpatialRenderedAudit(input.scene, input.request);
|
|
156
|
+
const expectedCapabilities = input.capabilityBindings === undefined ? undefined : parseSpatialValue(MediaCapabilityBindingsSchema, input.capabilityBindings, "rendered audit capabilities");
|
|
157
|
+
const expectedRuntime = input.browserRuntime === undefined ? undefined : HtmlOverlayBrowserRuntimeBindingSchema.parse(
|
|
158
|
+
createBoundedJsonSnapshot(input.browserRuntime, SPATIAL_RENDER_LIMITS.metadataBytes, "Expected browser runtime").value,
|
|
159
|
+
);
|
|
160
|
+
const workspace = await createMediaOperationWorkspace(context);
|
|
161
|
+
let directory: string | undefined;
|
|
162
|
+
const assertCustody = async () => { await context.application.hostResourceLease?.assertOwned(); throwIfAborted(context.abortSignal); };
|
|
163
|
+
const cleanupErrors: unknown[] = [];
|
|
164
|
+
let failure: unknown, failed = false, stage = "preparation";
|
|
165
|
+
let completed: SpatialRenderedAuditReport | undefined;
|
|
166
|
+
try {
|
|
167
|
+
await assertCustody();
|
|
168
|
+
directory = await mkdtemp(join(workspace.path, "spatial-rendered-audit-"));
|
|
169
|
+
checked(await realpath(directory) === directory, "Rendered audit workspace must be physical.");
|
|
170
|
+
const names = spatialRenderedAuditCapabilityNames(plan.scene);
|
|
171
|
+
await assertMediaCapabilities(context, context.application, expectedCapabilities, names);
|
|
172
|
+
const capabilities = await bindExpectedMediaCapabilities(context.application, names, expectedCapabilities);
|
|
173
|
+
const runtime = await (dependencies.bindBrowserRuntime ?? bindHtmlOverlayBrowserRuntime)(exactCapabilityByName(capabilities, "html-browser"), context.abortSignal);
|
|
174
|
+
if (expectedRuntime !== undefined) {
|
|
175
|
+
checked(canonicalJson(runtime) === canonicalJson(expectedRuntime), "Browser runtime changed after rendered audit planning.");
|
|
176
|
+
}
|
|
177
|
+
const renderer = context.application.htmlOverlayRenderer;
|
|
178
|
+
checked(renderer !== undefined, "The application does not provide the qualified spatial browser renderer.");
|
|
179
|
+
const runner = new AbortBoundApplicationRunner(mediaCapabilityRunner(context.application, capabilities), context.abortSignal);
|
|
180
|
+
const ports = { runner, ...(names.includes("ffmpeg") ? { ffmpegCommand: mediaCapabilityCommand(capabilities, "ffmpeg"), ffprobeCommand: mediaCapabilityCommand(capabilities, "ffprobe") } : {}) };
|
|
181
|
+
const sourceManifests = spatialAssetClosureDigests(plan.scene.assets);
|
|
182
|
+
const frames: SpatialRenderedAuditFrame[] = [];
|
|
183
|
+
const assetBounds: Record<string, Bounds> = {};
|
|
184
|
+
let pngBytes = 0;
|
|
185
|
+
for (let offset = 0; offset < plan.timesUs.length; offset += SPATIAL_RENDER_LIMITS.batchFrames) {
|
|
186
|
+
await assertCustody();
|
|
187
|
+
const windowTimes = plan.timesUs.slice(offset, offset + SPATIAL_RENDER_LIMITS.batchFrames);
|
|
188
|
+
const windowSnapshots: EvaluatedSpatialScene[] = windowTimes.map(timeUs =>
|
|
189
|
+
evaluateSpatialScene(plan.scene, { cameraId: plan.request.cameraId, timeUs }));
|
|
190
|
+
await withPreparedSpatialAssets({
|
|
191
|
+
snapshots: windowSnapshots,
|
|
192
|
+
exactSceneTimesUs: windowTimes.map(timeUs => ({ numerator: String(timeUs), denominator: "1" })),
|
|
193
|
+
assetRoot: input.assetRoot, workspaceParent: directory,
|
|
194
|
+
}, ports, context.abortSignal, async (prepared) => {
|
|
195
|
+
checked(canonicalJson(prepared.receipt.sourceManifests) === canonicalJson(sourceManifests), "Asset preparation did not bind the complete source closure.");
|
|
196
|
+
for (const [assetId, bounds] of Object.entries(preparedAssetBounds(prepared.preparedAssets))) {
|
|
197
|
+
const existing = assetBounds[assetId];
|
|
198
|
+
assetBounds[assetId] = existing === undefined ? bounds : {
|
|
199
|
+
min: bounds.min.map((value, axis) => Math.min(value, existing.min[axis]!)) as unknown as Vec3,
|
|
200
|
+
max: bounds.max.map((value, axis) => Math.max(value, existing.max[axis]!)) as unknown as Vec3,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const preparedSplatIds = new Set(prepared.preparedAssets.filter(asset => asset.kind === "splat").map(asset => asset.entityId));
|
|
204
|
+
const loweredSnapshots = windowSnapshots.map(snapshot => ({
|
|
205
|
+
...snapshot,
|
|
206
|
+
entities: snapshot.entities.filter(entry => entry.entity.kind !== "splat" || preparedSplatIds.has(entry.entity.entityId)),
|
|
207
|
+
}));
|
|
208
|
+
const partitions = partitionSpatialRenderWindow({
|
|
209
|
+
snapshots: loweredSnapshots, preparedAssets: prepared.preparedAssets,
|
|
210
|
+
mode: { kind: "object-id", coverage: SPATIAL_RENDERED_AUDIT_COVERAGE },
|
|
211
|
+
frameRate: { numerator: 1, denominator: 1 },
|
|
212
|
+
});
|
|
213
|
+
for (const partition of partitions) {
|
|
214
|
+
await assertCustody();
|
|
215
|
+
const batch = partition.batch;
|
|
216
|
+
const batchDirectory = join(directory!, `audit-${offset + partition.offset}`);
|
|
217
|
+
await mkdir(batchDirectory, { mode: 0o700 });
|
|
218
|
+
const bundle = createHtmlOverlayExecutionBundle(batch.authoring, runtime, undefined);
|
|
219
|
+
checked(canonicalJson(bundle.libraryLocks.map(lock => lock.specifier).sort()) === canonicalJson(["three"])
|
|
220
|
+
&& bundle.libraryLocks.find(lock => lock.specifier === "three")?.version === "0.185.1", "Rendered audit libraries differ from the exact qualified profile.");
|
|
221
|
+
stage = "render";
|
|
222
|
+
const rendered = await renderer.renderFrames({
|
|
223
|
+
authoring: batch.authoring, browserRuntime: runtime, outputDirectory: batchDirectory,
|
|
224
|
+
resources: prepared.resources,
|
|
225
|
+
}, context.abortSignal);
|
|
226
|
+
await assertCustody();
|
|
227
|
+
const integrity = parseSpatialValue(HtmlOverlayExecutionIntegritySchema, rendered.executionIntegrity, "Renderer execution integrity");
|
|
228
|
+
checked(canonicalJson(integrity) === canonicalJson(bundle.integrity) && canonicalJson(rendered.libraryLocks) === canonicalJson(bundle.libraryLocks), "Renderer returned execution or library integrity different from the bound input.");
|
|
229
|
+
checked(assertHtmlOverlayGpuEvidenceProfile(undefined, rendered.gpuEvidence) === undefined, "Rendered audit must not claim hardware evidence on the software profile.");
|
|
230
|
+
const samples = windowTimes.slice(partition.offset, partition.offset + partition.length);
|
|
231
|
+
const expectedPattern = join(batchDirectory, "frames", "frame-%08d.png");
|
|
232
|
+
checked(rendered.frameCount === samples.length && rendered.framePattern === expectedPattern, "Renderer frame count or output location differs from the planned audit batch.");
|
|
233
|
+
const entries = await readdir(join(batchDirectory, "frames"));
|
|
234
|
+
checked(entries.length === samples.length, "Renderer produced an unexpected audit frame directory entry.");
|
|
235
|
+
for (const [index, timeUs] of samples.entries()) {
|
|
236
|
+
const bytes = await readPhysical(join(batchDirectory, "frames", frameName(index)), plan.costs.pngBytesBound, context.abortSignal);
|
|
237
|
+
pngBytes += bytes.length;
|
|
238
|
+
checked(pngBytes <= plan.costs.pngBytesBound, "Rendered audit PNG bytes exceeded the admitted staging estimate.");
|
|
239
|
+
const rgba = await verifyPng(bytes, plan.width, plan.height);
|
|
240
|
+
const evidence = batch.metadata.frames[index];
|
|
241
|
+
checked(evidence !== undefined && evidence.timeUs === timeUs, "Batch evidence does not align with the audited sample clock.");
|
|
242
|
+
frames.push({
|
|
243
|
+
timeUs, width: plan.width, height: plan.height, pngSha256: sha256(bytes),
|
|
244
|
+
counts: decodeObjectIdPixels(rgba, plan.width, plan.height),
|
|
245
|
+
objects: evidence.objects,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
await rm(batchDirectory, { recursive: true });
|
|
249
|
+
}
|
|
250
|
+
}, { tolerateSplatDecodeFailure: true });
|
|
251
|
+
}
|
|
252
|
+
completed = auditSpatialSceneRendered(plan.scene, frames, {
|
|
253
|
+
cameraId: plan.request.cameraId, assetBounds,
|
|
254
|
+
coverage: SPATIAL_RENDERED_AUDIT_COVERAGE,
|
|
255
|
+
});
|
|
256
|
+
} catch (error) { failure = error; failed = true; }
|
|
257
|
+
finally {
|
|
258
|
+
if (directory !== undefined) try { await rm(directory, { recursive: true, force: true }); } catch (error) { cleanupErrors.push(error); }
|
|
259
|
+
try { await workspace.dispose(); } catch (error) { cleanupErrors.push(error); }
|
|
260
|
+
}
|
|
261
|
+
if (failed || cleanupErrors.length > 0) {
|
|
262
|
+
const cause = failed ? failure : cleanupErrors[0];
|
|
263
|
+
const error = new ApplicationError(cause instanceof ApplicationError ? cause.code : "internal",
|
|
264
|
+
cause instanceof Error ? cause.message : "Rendered scene audit failed.", {
|
|
265
|
+
spatialRenderedAudit: { stage: failed ? stage : "cleanup" },
|
|
266
|
+
cleanupErrors: cleanupErrors.map(item => item instanceof Error ? item.message : String(item)),
|
|
267
|
+
});
|
|
268
|
+
error.cause = cause;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
checked(completed !== undefined, "Rendered audit did not produce a report.");
|
|
272
|
+
return completed;
|
|
273
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, readdir, realpath, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import { createBoundedJsonSnapshot } from "../../../src/code/json-snapshot";
|
|
8
|
+
import {
|
|
9
|
+
SpatialCameraIdSchema, SpatialTimeUsSchema, type EvaluatedSpatialScene, type SpatialSceneV1,
|
|
10
|
+
} from "../../../src/spatial-scene/contracts";
|
|
11
|
+
import { evaluateSpatialScene } from "../../../src/spatial-scene/evaluate";
|
|
12
|
+
import { parseSpatialScene, parseSpatialValue, spatialAssetClosureDigests, spatialSceneSha256 } from "../../../src/spatial-scene/identity";
|
|
13
|
+
import {
|
|
14
|
+
buildSpatialReviewReport, redactedSpatialReviewProviderError, SPATIAL_REVIEW_LIMITS,
|
|
15
|
+
SPATIAL_REVIEW_PROMPT_SHA256, SPATIAL_REVIEW_PROMPT_VERSION,
|
|
16
|
+
SPATIAL_REVIEW_UPLOAD_POLICY, spatialReviewDefaultTimesUs, SpatialReviewProviderError,
|
|
17
|
+
type SpatialReviewProvider, type SpatialReviewReport, type SpatialReviewRequestFrame,
|
|
18
|
+
} from "../../../src/spatial-scene/review";
|
|
19
|
+
import { canonicalJson } from "../core/canonical-json";
|
|
20
|
+
import { assertHtmlOverlayGpuEvidenceProfile } from "../html-overlay/execution-profile";
|
|
21
|
+
import { exactCapabilityByName } from "./capability-binding";
|
|
22
|
+
import { ApplicationError } from "./errors";
|
|
23
|
+
import { bindHtmlOverlayBrowserRuntime, HtmlOverlayBrowserRuntimeBindingSchema, type HtmlOverlayBrowserRuntimeBinding } from "./html-overlay-browser-runtime";
|
|
24
|
+
import { createHtmlOverlayExecutionBundle, HtmlOverlayExecutionIntegritySchema } from "./html-overlay-integrity";
|
|
25
|
+
import type { OperationExecutionContext } from "./operation";
|
|
26
|
+
import { assertMediaCapabilities, bindExpectedMediaCapabilities, MediaCapabilityBindingsSchema, mediaCapabilityCommand, mediaCapabilityRunner, type MediaCapabilityName } from "./operations/media/capabilities";
|
|
27
|
+
import { AbortBoundApplicationRunner, createMediaOperationWorkspace } from "./operations/media/shared";
|
|
28
|
+
import { throwIfAborted } from "./operations/shared";
|
|
29
|
+
import { frameName, partitionSpatialRenderWindow, readPhysical, SPATIAL_RENDER_LIMITS, verifyPng } from "./spatial-render";
|
|
30
|
+
import { withPreparedSpatialAssets } from "./spatial-assets";
|
|
31
|
+
|
|
32
|
+
function checked(condition: boolean, message: string): asserts condition {
|
|
33
|
+
if (!condition) throw new ApplicationError("invalid-data", message);
|
|
34
|
+
}
|
|
35
|
+
const sha256 = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
36
|
+
|
|
37
|
+
export const SpatialReviewRequestSchema = z.strictObject({
|
|
38
|
+
cameraId: SpatialCameraIdSchema,
|
|
39
|
+
/** Exact integer-microsecond samples; omitted means the fixed four-sample default. */
|
|
40
|
+
timesUs: z.array(SpatialTimeUsSchema).min(1).max(SPATIAL_REVIEW_LIMITS.frames).optional(),
|
|
41
|
+
});
|
|
42
|
+
export type SpatialReviewRequest = z.infer<typeof SpatialReviewRequestSchema>;
|
|
43
|
+
|
|
44
|
+
/** The consent grant the adapter-owned authorization envelope returns. */
|
|
45
|
+
export interface SpatialReviewGrant {
|
|
46
|
+
readonly acknowledgedAt: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The exact bounded upload an invocation-scoped consent envelope is asked to
|
|
51
|
+
* authorize before any capability probe, render, or provider dispatch.
|
|
52
|
+
*/
|
|
53
|
+
export interface SpatialReviewAuthorizationRequest {
|
|
54
|
+
readonly sceneSha256: string;
|
|
55
|
+
readonly cameraId: string;
|
|
56
|
+
readonly timesUs: readonly number[];
|
|
57
|
+
readonly maximumFrames: number;
|
|
58
|
+
readonly maximumFrameBytes: number;
|
|
59
|
+
readonly maximumUploadBytes: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Explicit host envelope. Generic workflow approval never grants this permission. */
|
|
63
|
+
export interface ApplicationSpatialReviewAuthorization {
|
|
64
|
+
authorize(request: SpatialReviewAuthorizationRequest): Promise<SpatialReviewGrant | undefined>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SpatialReviewPlan {
|
|
68
|
+
readonly scene: SpatialSceneV1;
|
|
69
|
+
/** The authored scene with the review camera's projection downscaled. */
|
|
70
|
+
readonly evaluationScene: SpatialSceneV1;
|
|
71
|
+
readonly sceneSha256: string;
|
|
72
|
+
readonly request: SpatialReviewRequest;
|
|
73
|
+
readonly timesUs: readonly number[];
|
|
74
|
+
readonly width: number;
|
|
75
|
+
readonly height: number;
|
|
76
|
+
readonly sourceWidth: number;
|
|
77
|
+
readonly sourceHeight: number;
|
|
78
|
+
readonly excludedEntityIds: readonly string[];
|
|
79
|
+
readonly costs: {
|
|
80
|
+
readonly sourceBytes: number;
|
|
81
|
+
readonly renderPixels: number;
|
|
82
|
+
readonly pngBytesBound: number;
|
|
83
|
+
readonly stagingBytesBound: number;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
type Projection = SpatialSceneV1["cameras"][number]["projection"];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Uniformly downscales one calibrated projection into the review raster budget.
|
|
91
|
+
* Perspective intrinsics scale per axis with the raster so the frustum is
|
|
92
|
+
* preserved exactly; orthographic world extents stay fixed and only raster
|
|
93
|
+
* density changes.
|
|
94
|
+
*/
|
|
95
|
+
function reviewProjection(projection: Projection): { readonly projection: Projection; readonly width: number; readonly height: number } {
|
|
96
|
+
const scale = Math.min(
|
|
97
|
+
1,
|
|
98
|
+
SPATIAL_REVIEW_LIMITS.dimension / projection.width,
|
|
99
|
+
SPATIAL_REVIEW_LIMITS.dimension / projection.height,
|
|
100
|
+
);
|
|
101
|
+
const width = Math.max(1, Math.floor(projection.width * scale));
|
|
102
|
+
const height = Math.max(1, Math.floor(projection.height * scale));
|
|
103
|
+
if (width === projection.width && height === projection.height) {
|
|
104
|
+
return { projection, width, height };
|
|
105
|
+
}
|
|
106
|
+
if (projection.kind === "perspective") {
|
|
107
|
+
const sx = width / projection.width;
|
|
108
|
+
const sy = height / projection.height;
|
|
109
|
+
return {
|
|
110
|
+
projection: {
|
|
111
|
+
...projection, width, height,
|
|
112
|
+
fx: projection.fx * sx, cx: projection.cx * sx,
|
|
113
|
+
fy: projection.fy * sy, cy: projection.cy * sy,
|
|
114
|
+
},
|
|
115
|
+
width, height,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { projection: { ...projection, width, height }, width, height };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Pure planning: parses and bounds the scene, camera and sample list before any
|
|
123
|
+
* capability, workspace, or provider is touched. Sample order is ascending and
|
|
124
|
+
* unique; the report builder re-verifies every bound on its own input.
|
|
125
|
+
*/
|
|
126
|
+
export function planSpatialReview(sceneInput: unknown, requestInput: unknown): SpatialReviewPlan {
|
|
127
|
+
const scene = parseSpatialScene(sceneInput);
|
|
128
|
+
const request = parseSpatialValue(SpatialReviewRequestSchema, requestInput, "spatial review request");
|
|
129
|
+
const camera = scene.cameras.find(item => item.cameraId === request.cameraId);
|
|
130
|
+
checked(camera !== undefined, `Unknown camera ${request.cameraId}.`);
|
|
131
|
+
const scaled = reviewProjection(camera.projection);
|
|
132
|
+
const timesUs = [...(request.timesUs ?? spatialReviewDefaultTimesUs(scene.durationUs))].sort((a, b) => a - b);
|
|
133
|
+
checked(new Set(timesUs).size === timesUs.length, "Review sample times must be unique.");
|
|
134
|
+
checked(timesUs.every(timeUs => timeUs <= scene.durationUs), "A review sample is outside the scene clock.");
|
|
135
|
+
checked(scene.entities.length * timesUs.length <= SPATIAL_REVIEW_LIMITS.entitySamples, "Scene review exceeds its entity-sample budget; pass fewer --times-us samples.");
|
|
136
|
+
const sourceBytes = scene.assets.reduce((sum, asset) => sum + asset.payload.bytes, 0);
|
|
137
|
+
checked(sourceBytes <= SPATIAL_RENDER_LIMITS.sourceBytes, "Scene asset closure exceeds its byte budget.");
|
|
138
|
+
const renderPixels = scaled.width * scaled.height * timesUs.length;
|
|
139
|
+
const perFrameBound = Math.min(
|
|
140
|
+
scaled.width * scaled.height * 5 + scaled.height * 8 + 65_536,
|
|
141
|
+
SPATIAL_REVIEW_LIMITS.pngBytes,
|
|
142
|
+
);
|
|
143
|
+
const pngBytesBound = perFrameBound * timesUs.length;
|
|
144
|
+
const stagingBytesBound = sourceBytes + pngBytesBound + SPATIAL_RENDER_LIMITS.metadataBytes;
|
|
145
|
+
const excludedEntityIds = scene.entities.filter(entity => entity.kind === "splat").map(entity => entity.entityId);
|
|
146
|
+
const evaluationScene = scaled.projection === camera.projection
|
|
147
|
+
? scene
|
|
148
|
+
: {
|
|
149
|
+
...scene,
|
|
150
|
+
cameras: scene.cameras.map(item => item.cameraId === request.cameraId
|
|
151
|
+
? { ...item, projection: scaled.projection }
|
|
152
|
+
: item),
|
|
153
|
+
};
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
scene, evaluationScene, sceneSha256: spatialSceneSha256(scene), request, timesUs,
|
|
156
|
+
width: scaled.width, height: scaled.height,
|
|
157
|
+
sourceWidth: camera.projection.width, sourceHeight: camera.projection.height,
|
|
158
|
+
excludedEntityIds: Object.freeze([...excludedEntityIds]),
|
|
159
|
+
costs: Object.freeze({ sourceBytes, renderPixels, pngBytesBound, stagingBytesBound }),
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The review's browser claim: html-browser, plus FFmpeg only when a declared video asset needs frame extraction. */
|
|
164
|
+
export function spatialReviewCapabilityNames(scene: SpatialSceneV1): readonly MediaCapabilityName[] {
|
|
165
|
+
return Object.freeze(scene.assets.some(asset => asset.interpretation.kind === "video")
|
|
166
|
+
? ["ffmpeg", "ffprobe", "html-browser"] as const : ["html-browser"] as const);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface SpatialReviewHostInput {
|
|
170
|
+
readonly scene: unknown;
|
|
171
|
+
/** Adapter-owned contained root holding the scene's declared asset payloads. */
|
|
172
|
+
readonly assetRoot: string;
|
|
173
|
+
readonly request: unknown;
|
|
174
|
+
/** Invocation-scoped consent grant; never part of serialized input. */
|
|
175
|
+
readonly grant: SpatialReviewGrant;
|
|
176
|
+
readonly capabilityBindings?: z.infer<typeof MediaCapabilityBindingsSchema>;
|
|
177
|
+
readonly browserRuntime?: HtmlOverlayBrowserRuntimeBinding;
|
|
178
|
+
}
|
|
179
|
+
/** Trusted injection seam for deterministic host tests; never serialized. */
|
|
180
|
+
export interface SpatialReviewDependencies {
|
|
181
|
+
readonly bindBrowserRuntime?: typeof bindHtmlOverlayBrowserRuntime;
|
|
182
|
+
readonly nextAttemptId?: () => string;
|
|
183
|
+
readonly provider?: SpatialReviewProvider;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function providerFailureCode(error: SpatialReviewProviderError): ApplicationError["code"] {
|
|
187
|
+
switch (error.code) {
|
|
188
|
+
case "aborted": return "cancelled";
|
|
189
|
+
case "gateway-outcome-unknown": return "ambiguous";
|
|
190
|
+
case "invalid-request": case "invalid-response": return "invalid-data";
|
|
191
|
+
default: return "unavailable"; // credential-missing, gateway-unavailable, model-unavailable
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Renders the selected beauty frames through the real browser pass in a private
|
|
197
|
+
* workspace, hashes each exact PNG immediately before dispatch, makes exactly
|
|
198
|
+
* one provider call, and assembles the strict advisory report. Splats are
|
|
199
|
+
* excluded from the batch (the software pass cannot represent them) and named
|
|
200
|
+
* in `render.excludedEntityIds`; nothing is published and no receipt is
|
|
201
|
+
* written. The caller supplies the consent grant; provider credentials and
|
|
202
|
+
* transport live entirely inside the injected provider seam.
|
|
203
|
+
*/
|
|
204
|
+
export async function reviewSpatialSceneHost(
|
|
205
|
+
context: OperationExecutionContext,
|
|
206
|
+
input: SpatialReviewHostInput,
|
|
207
|
+
dependencies: SpatialReviewDependencies = {},
|
|
208
|
+
): Promise<SpatialReviewReport> {
|
|
209
|
+
const plan = planSpatialReview(input.scene, input.request);
|
|
210
|
+
const expectedCapabilities = input.capabilityBindings === undefined ? undefined : parseSpatialValue(MediaCapabilityBindingsSchema, input.capabilityBindings, "review capabilities");
|
|
211
|
+
const expectedRuntime = input.browserRuntime === undefined ? undefined : HtmlOverlayBrowserRuntimeBindingSchema.parse(
|
|
212
|
+
createBoundedJsonSnapshot(input.browserRuntime, SPATIAL_RENDER_LIMITS.metadataBytes, "Expected browser runtime").value,
|
|
213
|
+
);
|
|
214
|
+
const workspace = await createMediaOperationWorkspace(context);
|
|
215
|
+
let directory: string | undefined;
|
|
216
|
+
const assertCustody = async () => { await context.application.hostResourceLease?.assertOwned(); throwIfAborted(context.abortSignal); };
|
|
217
|
+
const cleanupErrors: unknown[] = [];
|
|
218
|
+
let failure: unknown, failed = false, stage = "preparation";
|
|
219
|
+
let completed: SpatialReviewReport | undefined;
|
|
220
|
+
try {
|
|
221
|
+
await assertCustody();
|
|
222
|
+
directory = await mkdtemp(join(workspace.path, "spatial-review-"));
|
|
223
|
+
checked(await realpath(directory) === directory, "Scene review workspace must be physical.");
|
|
224
|
+
const names = spatialReviewCapabilityNames(plan.scene);
|
|
225
|
+
await assertMediaCapabilities(context, context.application, expectedCapabilities, names);
|
|
226
|
+
const capabilities = await bindExpectedMediaCapabilities(context.application, names, expectedCapabilities);
|
|
227
|
+
const runtime = await (dependencies.bindBrowserRuntime ?? bindHtmlOverlayBrowserRuntime)(exactCapabilityByName(capabilities, "html-browser"), context.abortSignal);
|
|
228
|
+
if (expectedRuntime !== undefined) {
|
|
229
|
+
checked(canonicalJson(runtime) === canonicalJson(expectedRuntime), "Browser runtime changed after review planning.");
|
|
230
|
+
}
|
|
231
|
+
const renderer = context.application.htmlOverlayRenderer;
|
|
232
|
+
checked(renderer !== undefined, "The application does not provide the qualified spatial browser renderer.");
|
|
233
|
+
const runner = new AbortBoundApplicationRunner(mediaCapabilityRunner(context.application, capabilities), context.abortSignal);
|
|
234
|
+
const ports = { runner, ...(names.includes("ffmpeg") ? { ffmpegCommand: mediaCapabilityCommand(capabilities, "ffmpeg"), ffprobeCommand: mediaCapabilityCommand(capabilities, "ffprobe") } : {}) };
|
|
235
|
+
const sourceManifests = spatialAssetClosureDigests(plan.scene.assets);
|
|
236
|
+
const frames: SpatialReviewRequestFrame[] = [];
|
|
237
|
+
let pngBytes = 0;
|
|
238
|
+
for (let offset = 0; offset < plan.timesUs.length; offset += SPATIAL_RENDER_LIMITS.batchFrames) {
|
|
239
|
+
await assertCustody();
|
|
240
|
+
const windowTimes = plan.timesUs.slice(offset, offset + SPATIAL_RENDER_LIMITS.batchFrames);
|
|
241
|
+
// The software beauty pass cannot lower splats; they are filtered before
|
|
242
|
+
// lowering and named as excluded entities, never silently rendered.
|
|
243
|
+
const windowSnapshots: EvaluatedSpatialScene[] = windowTimes.map(timeUs => {
|
|
244
|
+
const snapshot = evaluateSpatialScene(plan.evaluationScene, { cameraId: plan.request.cameraId, timeUs });
|
|
245
|
+
return { ...snapshot, entities: snapshot.entities.filter(entry => entry.entity.kind !== "splat") };
|
|
246
|
+
});
|
|
247
|
+
await withPreparedSpatialAssets({
|
|
248
|
+
snapshots: windowSnapshots,
|
|
249
|
+
exactSceneTimesUs: windowTimes.map(timeUs => ({ numerator: String(timeUs), denominator: "1" })),
|
|
250
|
+
assetRoot: input.assetRoot, workspaceParent: directory,
|
|
251
|
+
}, ports, context.abortSignal, async (prepared) => {
|
|
252
|
+
checked(canonicalJson(prepared.receipt.sourceManifests) === canonicalJson(sourceManifests), "Asset preparation did not bind the complete source closure.");
|
|
253
|
+
const partitions = partitionSpatialRenderWindow({
|
|
254
|
+
snapshots: windowSnapshots, preparedAssets: prepared.preparedAssets,
|
|
255
|
+
mode: { kind: "beauty" },
|
|
256
|
+
frameRate: { numerator: 1, denominator: 1 },
|
|
257
|
+
});
|
|
258
|
+
for (const partition of partitions) {
|
|
259
|
+
await assertCustody();
|
|
260
|
+
const batch = partition.batch;
|
|
261
|
+
const batchDirectory = join(directory!, `review-${offset + partition.offset}`);
|
|
262
|
+
await mkdir(batchDirectory, { mode: 0o700 });
|
|
263
|
+
const bundle = createHtmlOverlayExecutionBundle(batch.authoring, runtime, undefined);
|
|
264
|
+
checked(canonicalJson(bundle.libraryLocks.map(lock => lock.specifier).sort()) === canonicalJson(["three"])
|
|
265
|
+
&& bundle.libraryLocks.find(lock => lock.specifier === "three")?.version === "0.185.1", "Review libraries differ from the exact qualified profile.");
|
|
266
|
+
stage = "render";
|
|
267
|
+
const rendered = await renderer.renderFrames({
|
|
268
|
+
authoring: batch.authoring, browserRuntime: runtime, outputDirectory: batchDirectory,
|
|
269
|
+
resources: prepared.resources,
|
|
270
|
+
}, context.abortSignal);
|
|
271
|
+
await assertCustody();
|
|
272
|
+
const integrity = parseSpatialValue(HtmlOverlayExecutionIntegritySchema, rendered.executionIntegrity, "Renderer execution integrity");
|
|
273
|
+
checked(canonicalJson(integrity) === canonicalJson(bundle.integrity) && canonicalJson(rendered.libraryLocks) === canonicalJson(bundle.libraryLocks), "Renderer returned execution or library integrity different from the bound input.");
|
|
274
|
+
checked(assertHtmlOverlayGpuEvidenceProfile(undefined, rendered.gpuEvidence) === undefined, "Scene review must not claim hardware evidence on the software profile.");
|
|
275
|
+
const samples = windowTimes.slice(partition.offset, partition.offset + partition.length);
|
|
276
|
+
const expectedPattern = join(batchDirectory, "frames", "frame-%08d.png");
|
|
277
|
+
checked(rendered.frameCount === samples.length && rendered.framePattern === expectedPattern, "Renderer frame count or output location differs from the planned review batch.");
|
|
278
|
+
const entries = await readdir(join(batchDirectory, "frames"));
|
|
279
|
+
checked(entries.length === samples.length, "Renderer produced an unexpected review frame directory entry.");
|
|
280
|
+
for (const [index, timeUs] of samples.entries()) {
|
|
281
|
+
const bytes = await readPhysical(join(batchDirectory, "frames", frameName(index)), SPATIAL_REVIEW_LIMITS.pngBytes, context.abortSignal);
|
|
282
|
+
pngBytes += bytes.byteLength;
|
|
283
|
+
checked(pngBytes <= SPATIAL_REVIEW_LIMITS.uploadBytes, "Review PNG bytes exceeded the upload bound.");
|
|
284
|
+
await verifyPng(bytes, plan.width, plan.height);
|
|
285
|
+
frames.push({
|
|
286
|
+
index: offset + partition.offset + index, timeUs,
|
|
287
|
+
width: plan.width, height: plan.height,
|
|
288
|
+
bytes, sha256: sha256(bytes),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
await rm(batchDirectory, { recursive: true });
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
// The paid dispatch happens only after every rendered byte is verified and
|
|
296
|
+
// hashed; provider credentials and transport stay inside the provider seam.
|
|
297
|
+
stage = "critique";
|
|
298
|
+
const provider = dependencies.provider ?? context.application.spatialReviewProvider;
|
|
299
|
+
checked(provider !== undefined, "The application does not provide a bounded spatial review provider.");
|
|
300
|
+
const attemptId = (dependencies.nextAttemptId ?? randomUUID)();
|
|
301
|
+
let result;
|
|
302
|
+
try {
|
|
303
|
+
result = await provider.critique({
|
|
304
|
+
attemptId,
|
|
305
|
+
cloudUpload: { acknowledgedAt: input.grant.acknowledgedAt, policy: SPATIAL_REVIEW_UPLOAD_POLICY },
|
|
306
|
+
prompt: { sha256: SPATIAL_REVIEW_PROMPT_SHA256, version: SPATIAL_REVIEW_PROMPT_VERSION },
|
|
307
|
+
scene: {
|
|
308
|
+
sceneId: plan.scene.sceneId,
|
|
309
|
+
sceneSha256: plan.sceneSha256,
|
|
310
|
+
cameraId: plan.request.cameraId,
|
|
311
|
+
durationUs: plan.scene.durationUs,
|
|
312
|
+
timesUs: plan.timesUs,
|
|
313
|
+
entities: plan.scene.entities.slice(0, SPATIAL_REVIEW_LIMITS.promptEntities).map(entity => ({
|
|
314
|
+
entityId: entity.entityId, kind: entity.kind, name: entity.name,
|
|
315
|
+
})),
|
|
316
|
+
},
|
|
317
|
+
frames,
|
|
318
|
+
}, context.abortSignal);
|
|
319
|
+
} catch (error) {
|
|
320
|
+
// A non-typed provider failure is unaccountable: the dispatch may or may
|
|
321
|
+
// not have reached the wire, so it must surface as ambiguous and never
|
|
322
|
+
// be retried. Provider internals are redacted at this seam.
|
|
323
|
+
throw redactedSpatialReviewProviderError(error, true);
|
|
324
|
+
}
|
|
325
|
+
completed = buildSpatialReviewReport({
|
|
326
|
+
acknowledgedAt: input.grant.acknowledgedAt,
|
|
327
|
+
attemptId,
|
|
328
|
+
provider: result,
|
|
329
|
+
render: {
|
|
330
|
+
excludedEntityIds: plan.excludedEntityIds,
|
|
331
|
+
height: plan.height, width: plan.width,
|
|
332
|
+
sourceHeight: plan.sourceHeight, sourceWidth: plan.sourceWidth,
|
|
333
|
+
},
|
|
334
|
+
scene: plan.scene,
|
|
335
|
+
sceneSha256: plan.sceneSha256,
|
|
336
|
+
frames: frames.map(frame => ({
|
|
337
|
+
index: frame.index, timeUs: frame.timeUs, width: frame.width, height: frame.height,
|
|
338
|
+
pngSha256: frame.sha256, pngBytes: frame.bytes.byteLength,
|
|
339
|
+
})),
|
|
340
|
+
request: { cameraId: plan.request.cameraId, timesUs: plan.timesUs },
|
|
341
|
+
});
|
|
342
|
+
} catch (error) { failure = error; failed = true; }
|
|
343
|
+
finally {
|
|
344
|
+
if (directory !== undefined) try { await rm(directory, { recursive: true, force: true }); } catch (error) { cleanupErrors.push(error); }
|
|
345
|
+
try { await workspace.dispose(); } catch (error) { cleanupErrors.push(error); }
|
|
346
|
+
}
|
|
347
|
+
if (failed || cleanupErrors.length > 0) {
|
|
348
|
+
const cause = failed ? failure : cleanupErrors[0];
|
|
349
|
+
const error = new ApplicationError(
|
|
350
|
+
cause instanceof SpatialReviewProviderError ? providerFailureCode(cause)
|
|
351
|
+
: cause instanceof ApplicationError ? cause.code : "internal",
|
|
352
|
+
cause instanceof Error ? cause.message : "Scene review failed.", {
|
|
353
|
+
spatialReview: { stage: failed ? stage : "cleanup" },
|
|
354
|
+
cleanupErrors: cleanupErrors.map(item => item instanceof Error ? item.message : String(item)),
|
|
355
|
+
});
|
|
356
|
+
error.cause = cause;
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
checked(completed !== undefined, "Scene review did not produce a report.");
|
|
360
|
+
return completed;
|
|
361
|
+
}
|