@hraness/slopcamera 3.2.8 → 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/README.md +29 -9
- package/apps/desktop/application/context.ts +6 -0
- package/apps/desktop/application/default-registry.ts +26 -0
- package/apps/desktop/application/operation.ts +14 -1
- package/apps/desktop/application/operations/index.ts +4 -0
- 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 +514 -11
- 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 +29 -1
- package/apps/desktop/cli/commands.ts +1188 -4
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +172 -6
- package/apps/desktop/cli/main.ts +3 -3
- package/apps/desktop/cli/portable-surface.ts +6 -1
- 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/semantic-builder.ts +24 -0
- 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/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +725 -273
- 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 +25 -429
- 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 -226
- 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/package.json +7 -5
- package/skills/slopcamera/SKILL.md +6 -1
- 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 +1 -1
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -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 +222 -4
- 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 -0
- 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/version.ts +1 -1
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7131pg1c.js +0 -1820
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-fava6pge.js +0 -192
- package/dist/index-h1k0fnjq.js +0 -2363
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-zfnddgay.js +0 -2029
- package/dist/skill-install-dh0ntqba.js +0 -10
|
@@ -1,13 +1,49 @@
|
|
|
1
1
|
import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
|
|
2
|
+
import { SlopcameraCloudError } from "../../../src/cloud-errors";
|
|
3
|
+
import {
|
|
4
|
+
composeSlopcameraImageGallery,
|
|
5
|
+
parseSlopcameraGalleryVary,
|
|
6
|
+
planSlopcameraGallery,
|
|
7
|
+
slopcameraGalleryLimits,
|
|
8
|
+
type SlopcameraGalleryCandidate,
|
|
9
|
+
type SlopcameraGalleryKind,
|
|
10
|
+
type SlopcameraGalleryPlan,
|
|
11
|
+
type SlopcameraGalleryResolvedCandidate,
|
|
12
|
+
} from "../../../src/image-gallery";
|
|
13
|
+
import {
|
|
14
|
+
galleryProbeRenderRequest,
|
|
15
|
+
galleryProbeScene,
|
|
16
|
+
type SlopcameraGalleryProbeMode,
|
|
17
|
+
} from "../../../src/spatial-scene/probe";
|
|
18
|
+
import {
|
|
19
|
+
planSlopcameraSceneGallery,
|
|
20
|
+
slopcameraSceneGalleryLimits,
|
|
21
|
+
type SlopcameraSceneGalleryVariant,
|
|
22
|
+
} from "../../../src/scene-gallery";
|
|
23
|
+
import {
|
|
24
|
+
parseSpatialScene,
|
|
25
|
+
spatialSceneSha256,
|
|
26
|
+
} from "../../../src/spatial-scene/identity";
|
|
27
|
+
import {
|
|
28
|
+
SPATIAL_SCENE_LIMITS,
|
|
29
|
+
type SpatialSceneV1,
|
|
30
|
+
} from "../../../src/spatial-scene/contracts";
|
|
2
31
|
import { createDirectingBlobSession } from "./directing-blob";
|
|
3
32
|
import { assembleDirectingClips, extractDirectingEndpoint, importDirectingAnchor } from "./directing-media";
|
|
4
33
|
import { executeDirectingCommand } from "./directing-service";
|
|
5
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
-
import
|
|
35
|
+
import sharp from "sharp";
|
|
36
|
+
import { executeSpatialAssetCommand } from "./spatial-asset-service";
|
|
37
|
+
import { executeSpatialDesignCommand } from "./spatial-design-service";
|
|
38
|
+
import {
|
|
39
|
+
executeSpatialSceneCommand,
|
|
40
|
+
publishSpatialSource,
|
|
41
|
+
readSpatialJson,
|
|
42
|
+
} from "./spatial-scene-service";
|
|
7
43
|
import { executeSpatialProjectCommand } from "./spatial-project-service";
|
|
8
44
|
import { executeSpatialWorldCommand } from "./spatial-world-service";
|
|
9
45
|
import { constants } from "node:fs";
|
|
10
|
-
import { link, lstat, open, realpath, rm } from "node:fs/promises";
|
|
46
|
+
import { link, lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
11
47
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
12
48
|
import {
|
|
13
49
|
createDefaultHostResourceCoordinator,
|
|
@@ -21,6 +57,9 @@ import {
|
|
|
21
57
|
AnalyzerEvidenceV1Schema,
|
|
22
58
|
AlignmentCandidateIdSchema,
|
|
23
59
|
AudioAlignmentAnalysisV1Schema,
|
|
60
|
+
CinemaContinuityReportSchema,
|
|
61
|
+
CinemaPlanIdSchema,
|
|
62
|
+
CinemaRenderReceiptV1Schema,
|
|
24
63
|
EventKindSchema,
|
|
25
64
|
MusicAnalysisV1Schema,
|
|
26
65
|
OverlayOperationSchema,
|
|
@@ -50,12 +89,18 @@ import {
|
|
|
50
89
|
applyProjectInactivityPlan,
|
|
51
90
|
analyzeAudioAlignment,
|
|
52
91
|
applyAudioAlignmentCandidate,
|
|
92
|
+
analyzeCinemaPlan,
|
|
53
93
|
addZoom,
|
|
94
|
+
assertCinemaPlanComposition,
|
|
95
|
+
auditCinemaContinuity,
|
|
96
|
+
auditCinemaTemporal,
|
|
54
97
|
buildProjectOutputTimeMap,
|
|
55
98
|
buildSourceTimeMap,
|
|
56
99
|
canonicalSlopcameraPersistenceDocument,
|
|
57
100
|
canonicalJson,
|
|
101
|
+
compileCinemaSequence,
|
|
58
102
|
compileProjectRenderPlan,
|
|
103
|
+
createCinemaPlanScaffold,
|
|
59
104
|
compileRenderPlan,
|
|
60
105
|
createNodeBundleFileSystem,
|
|
61
106
|
createDefaultEditPlan,
|
|
@@ -70,11 +115,17 @@ import {
|
|
|
70
115
|
normalizeProjectEditPlan,
|
|
71
116
|
planAutomaticInactivityCuts,
|
|
72
117
|
planAutomaticZooms,
|
|
118
|
+
planCinemaGallery,
|
|
73
119
|
projectFillerCut,
|
|
74
120
|
removeOverlay,
|
|
75
121
|
removeZoom,
|
|
76
122
|
saveEditPlan,
|
|
77
123
|
saveAnalysisArtifact,
|
|
124
|
+
saveImmutableText,
|
|
125
|
+
loadProjectCinemaPlan,
|
|
126
|
+
projectCinemaRenderPlanPath,
|
|
127
|
+
saveProjectCinemaPlan,
|
|
128
|
+
saveProjectCinemaRevision,
|
|
78
129
|
saveProjectEditPlan,
|
|
79
130
|
setSpeed,
|
|
80
131
|
rebaseProjectEditPlan,
|
|
@@ -133,6 +184,7 @@ import {
|
|
|
133
184
|
import { resolveEmojiAsset, searchEmojiAssets, inspectEmojiAssets } from "./emoji-assets";
|
|
134
185
|
import { asCliError, CliError, EXIT_CODE } from "./errors";
|
|
135
186
|
import { commandHelp, completions } from "./help";
|
|
187
|
+
import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
|
|
136
188
|
import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
|
|
137
189
|
import { executeHtmlSceneCommand } from "./html-scene";
|
|
138
190
|
import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
|
|
@@ -178,6 +230,7 @@ import {
|
|
|
178
230
|
} from "./project-service";
|
|
179
231
|
import { resolveVerifiedProjectMedia } from "./project-media-integrity";
|
|
180
232
|
import { buildFfmpegInvocation, prepareOverlaySources } from "./renderer";
|
|
233
|
+
import { buildCinemaFfmpegInvocation } from "./cinema-renderer";
|
|
181
234
|
import { buildProjectFfmpegInvocation } from "./project-renderer";
|
|
182
235
|
import {
|
|
183
236
|
DEFAULT_PROJECT_INACTIVITY_CONFIG,
|
|
@@ -186,6 +239,7 @@ import {
|
|
|
186
239
|
import { resolveAnimatedPlaybackWindow } from "./overlay-playback";
|
|
187
240
|
import { analyzeProjectScenes } from "./scene-analysis-service";
|
|
188
241
|
import type { SceneDescriptionProvider } from "@hraness/slopcamera/scene";
|
|
242
|
+
import type { SpatialReviewProvider } from "../../../src/spatial-scene/review";
|
|
189
243
|
import { parseCliTime } from "./time";
|
|
190
244
|
import {
|
|
191
245
|
DEFAULT_FACE_ANALYSIS_CONFIG,
|
|
@@ -214,6 +268,10 @@ import {
|
|
|
214
268
|
createGatewaySceneProvider,
|
|
215
269
|
type GatewaySceneProviderOptions,
|
|
216
270
|
} from "./gateway-scene-provider";
|
|
271
|
+
import {
|
|
272
|
+
createGatewaySpatialReviewProvider,
|
|
273
|
+
type GatewaySpatialReviewProviderOptions,
|
|
274
|
+
} from "./gateway-review-provider";
|
|
217
275
|
import {
|
|
218
276
|
GatewayMediaCatalogError,
|
|
219
277
|
createFileGatewayMediaCatalogSnapshotStore,
|
|
@@ -297,7 +355,7 @@ import {
|
|
|
297
355
|
workflowRunStore,
|
|
298
356
|
} from "./workflow-runs";
|
|
299
357
|
|
|
300
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
358
|
+
export const SLOPCAMERA_VERSION = "3.3.1";
|
|
301
359
|
|
|
302
360
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
303
361
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -319,8 +377,36 @@ export interface CliDependencies {
|
|
|
319
377
|
readonly paths?: RepositoryPaths;
|
|
320
378
|
readonly runner?: ProcessRunner;
|
|
321
379
|
readonly sceneProviderFactory?: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
380
|
+
readonly spatialReviewProviderFactory?: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
|
|
322
381
|
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
323
382
|
readonly version?: string;
|
|
383
|
+
/**
|
|
384
|
+
* Test/review seam for gallery scene renders (`ai image gallery
|
|
385
|
+
* --preview probe` and `ai scene gallery`): renders one bounded scene
|
|
386
|
+
* source into a still. The production default runs the real
|
|
387
|
+
* `scene.render` operation.
|
|
388
|
+
*/
|
|
389
|
+
readonly renderGalleryScene?: (input: {
|
|
390
|
+
readonly application: ApplicationContext;
|
|
391
|
+
readonly assets?: readonly {
|
|
392
|
+
readonly assetId: string;
|
|
393
|
+
readonly artifact: {
|
|
394
|
+
readonly bytes: number;
|
|
395
|
+
readonly path: string;
|
|
396
|
+
readonly sha256: string;
|
|
397
|
+
};
|
|
398
|
+
}[];
|
|
399
|
+
readonly request?: unknown;
|
|
400
|
+
readonly sceneSha256?: string;
|
|
401
|
+
readonly source: { readonly path: string };
|
|
402
|
+
}) => Promise<{
|
|
403
|
+
readonly artifact: {
|
|
404
|
+
readonly bytes: number;
|
|
405
|
+
readonly path: string;
|
|
406
|
+
readonly sha256: string;
|
|
407
|
+
};
|
|
408
|
+
readonly receipt?: { readonly path: string };
|
|
409
|
+
}>;
|
|
324
410
|
}
|
|
325
411
|
|
|
326
412
|
interface CommandContext {
|
|
@@ -345,8 +431,10 @@ interface CommandContext {
|
|
|
345
431
|
readonly paths: RepositoryPaths;
|
|
346
432
|
readonly runner: ProcessRunner;
|
|
347
433
|
readonly sceneProviderFactory: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
434
|
+
readonly spatialReviewProviderFactory: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
|
|
348
435
|
readonly sleep: (milliseconds: number) => Promise<void>;
|
|
349
436
|
readonly version: string;
|
|
437
|
+
readonly renderGalleryScene?: CliDependencies["renderGalleryScene"];
|
|
350
438
|
}
|
|
351
439
|
|
|
352
440
|
function contextWithHostResourceLease(
|
|
@@ -567,6 +655,13 @@ function applicationContext(
|
|
|
567
655
|
machineStateRoot: context.stateRoot,
|
|
568
656
|
paths: context.paths,
|
|
569
657
|
runner: context.runner,
|
|
658
|
+
// The review provider is inert until scene.review reaches its consent gate;
|
|
659
|
+
// the credential loader runs only inside the single paid dispatch.
|
|
660
|
+
spatialReviewProvider: context.spatialReviewProviderFactory({
|
|
661
|
+
credential: () => loadGatewayCredential(context.io.env),
|
|
662
|
+
fetch: context.fetch,
|
|
663
|
+
transport: context.gatewayCatalogTransport,
|
|
664
|
+
}),
|
|
570
665
|
};
|
|
571
666
|
return {
|
|
572
667
|
...application,
|
|
@@ -671,6 +766,16 @@ function writeValue(io: CliIo, json: boolean, value: unknown, human: () => strin
|
|
|
671
766
|
else writeLine(io, human());
|
|
672
767
|
}
|
|
673
768
|
|
|
769
|
+
function writeCapabilityManifest(io: CliIo, json: boolean, toolVersion: string): void {
|
|
770
|
+
const manifest = createLocalSlopcameraCapabilityManifest(
|
|
771
|
+
createApplicationOperationRegistry({ toolVersion }),
|
|
772
|
+
toolVersion,
|
|
773
|
+
);
|
|
774
|
+
writeValue(io, json, manifest, () => manifest.modules.map(module => (
|
|
775
|
+
`${module.moduleId}@${module.version}\t${module.trust}\toperations=${module.operations.length} workflows=${module.workflows.length} tools=${module.tools.length}\t${module.title}`
|
|
776
|
+
)).join("\n"));
|
|
777
|
+
}
|
|
778
|
+
|
|
674
779
|
function isWithin(root: string, candidate: string): boolean {
|
|
675
780
|
const pathRelative = relative(root, candidate);
|
|
676
781
|
return pathRelative === "" || (!pathRelative.startsWith("..") && !isAbsolute(pathRelative));
|
|
@@ -2410,6 +2515,227 @@ async function handleProjectRender(
|
|
|
2410
2515
|
writeValue(context.io, command.json, completed, () => `rendered ${outputRelative}; plan=${planArtifactPath}`);
|
|
2411
2516
|
}
|
|
2412
2517
|
|
|
2518
|
+
async function handleProjectCinema(
|
|
2519
|
+
context: CommandContext,
|
|
2520
|
+
command: Extract<CliCommand, { readonly kind: "project-cinema" }>,
|
|
2521
|
+
): Promise<void> {
|
|
2522
|
+
const project = await openProject(context.paths.projectRoot, command.project);
|
|
2523
|
+
const edit = await loadCurrentProjectPlan(project);
|
|
2524
|
+
const timestamp = context.io.now().toISOString();
|
|
2525
|
+
|
|
2526
|
+
if (command.action === "init") {
|
|
2527
|
+
const existing = await loadProjectCinemaPlan(project.fileSystem);
|
|
2528
|
+
if (existing !== null && !command.force) {
|
|
2529
|
+
throw new CliError(
|
|
2530
|
+
"conflict",
|
|
2531
|
+
"Project already has a cinema sidecar. Pass --force to replace it.",
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
let plan;
|
|
2535
|
+
try {
|
|
2536
|
+
plan = createCinemaPlanScaffold(
|
|
2537
|
+
project.project,
|
|
2538
|
+
edit,
|
|
2539
|
+
CinemaPlanIdSchema.parse(`cinema_${randomUUID().replaceAll("-", "").slice(0, 16)}`),
|
|
2540
|
+
timestamp,
|
|
2541
|
+
);
|
|
2542
|
+
} catch (error) {
|
|
2543
|
+
if (error instanceof TypeError) throw new CliError("conflict", error.message);
|
|
2544
|
+
throw error;
|
|
2545
|
+
}
|
|
2546
|
+
const contents = `${canonicalJson(plan)}\n`;
|
|
2547
|
+
await saveProjectCinemaPlan(project.fileSystem, plan);
|
|
2548
|
+
const revisionPath = await saveProjectCinemaRevision(
|
|
2549
|
+
project.fileSystem,
|
|
2550
|
+
contents,
|
|
2551
|
+
sha256Hex(contents),
|
|
2552
|
+
);
|
|
2553
|
+
writeValue(context.io, command.json, {
|
|
2554
|
+
cinemaPlanSha256: plan.cinemaPlanSha256,
|
|
2555
|
+
plan,
|
|
2556
|
+
planPath: "cinema/current.json",
|
|
2557
|
+
revisionPath,
|
|
2558
|
+
shots: plan.shots.length,
|
|
2559
|
+
}, () => `cinema sidecar ${plan.cinemaPlanSha256} with ${plan.shots.length} shots; revision=${revisionPath}`);
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
const cinema = await loadProjectCinemaPlan(project.fileSystem);
|
|
2564
|
+
if (cinema === null) {
|
|
2565
|
+
throw new CliError(
|
|
2566
|
+
"not-found",
|
|
2567
|
+
"Project has no cinema sidecar. Run slopcamera project cinema init first.",
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
try {
|
|
2571
|
+
assertCinemaPlanComposition(cinema);
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
if (error instanceof TypeError) throw new CliError("conflict", error.message);
|
|
2574
|
+
throw error;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
if (command.action === "check") {
|
|
2578
|
+
const analysis = analyzeCinemaPlan(cinema, project.project, edit);
|
|
2579
|
+
const audit = auditCinemaContinuity(cinema);
|
|
2580
|
+
const findings = [...analysis.findings, ...audit.findings].sort((left, right) => (
|
|
2581
|
+
left.timeUs - right.timeUs
|
|
2582
|
+
|| left.code.localeCompare(right.code)
|
|
2583
|
+
|| left.shotIds.join("").localeCompare(right.shotIds.join(""))
|
|
2584
|
+
));
|
|
2585
|
+
const report = CinemaContinuityReportSchema.parse({
|
|
2586
|
+
cinemaPlanSha256: cinema.cinemaPlanSha256,
|
|
2587
|
+
findings,
|
|
2588
|
+
kind: "slopcamera.cinema-continuity-report",
|
|
2589
|
+
schemaVersion: 1,
|
|
2590
|
+
});
|
|
2591
|
+
const contents = `${canonicalJson(report)}\n`;
|
|
2592
|
+
const auditPath = `cinema/audits/${sha256Hex(contents)}.json`;
|
|
2593
|
+
await saveImmutableText(project.fileSystem, auditPath, contents, sha256Hex(contents));
|
|
2594
|
+
const errors = findings.filter(finding => finding.severity === "error").length;
|
|
2595
|
+
writeValue(context.io, command.json, {
|
|
2596
|
+
auditPath,
|
|
2597
|
+
findings,
|
|
2598
|
+
ok: errors === 0,
|
|
2599
|
+
stale: findings.some(finding => finding.code === "stale-sidecar"),
|
|
2600
|
+
}, () => `${findings.length} findings (${errors} errors); audit=${auditPath}`);
|
|
2601
|
+
if (errors > 0) {
|
|
2602
|
+
throw new CliError(
|
|
2603
|
+
"conflict",
|
|
2604
|
+
`Cinema sidecar has ${errors} blocking findings; see ${auditPath}.`,
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
if (command.action === "audit") {
|
|
2611
|
+
const report = auditCinemaTemporal(cinema, project.project, edit);
|
|
2612
|
+
const contents = `${canonicalJson(report)}\n`;
|
|
2613
|
+
const auditPath = `cinema/audits/${sha256Hex(contents)}.json`;
|
|
2614
|
+
await saveImmutableText(project.fileSystem, auditPath, contents, sha256Hex(contents));
|
|
2615
|
+
const warnings = report.findings.filter(finding => finding.severity !== "advisory").length;
|
|
2616
|
+
writeValue(context.io, command.json, {
|
|
2617
|
+
auditPath,
|
|
2618
|
+
findings: report.findings,
|
|
2619
|
+
metrics: report.metrics,
|
|
2620
|
+
}, () => `${report.findings.length} temporal findings (${warnings} warnings); audit=${auditPath}`);
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
if (command.action === "gallery") {
|
|
2625
|
+
const gallery = planCinemaGallery(cinema, command.axis);
|
|
2626
|
+
const contents = `${canonicalJson(gallery)}\n`;
|
|
2627
|
+
const galleryAbsolute = await resolveSafePath(project.directory.path, command.output ?? `cinema/galleries/${sha256Hex(contents)}.json`);
|
|
2628
|
+
const galleryRelative = relative(project.directory.path, galleryAbsolute);
|
|
2629
|
+
if (!galleryRelative.startsWith("cinema/") || isAbsolute(galleryRelative)) {
|
|
2630
|
+
throw new CliError("unsafe-path", "Cinema gallery output must remain under the project's cinema/ directory.");
|
|
2631
|
+
}
|
|
2632
|
+
await saveImmutableText(project.fileSystem, galleryRelative, contents, sha256Hex(contents));
|
|
2633
|
+
writeValue(context.io, command.json, {
|
|
2634
|
+
axis: gallery.axis,
|
|
2635
|
+
candidates: gallery.candidates.map(candidate => ({
|
|
2636
|
+
candidateId: candidate.candidateId,
|
|
2637
|
+
cinemaPlanSha256: candidate.cinemaPlanSha256,
|
|
2638
|
+
label: candidate.label,
|
|
2639
|
+
})),
|
|
2640
|
+
galleryPath: galleryRelative,
|
|
2641
|
+
selection: gallery.selection,
|
|
2642
|
+
}, () => `${gallery.candidates.length} candidates on ${gallery.axis}; gallery=${galleryRelative}`);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
const placeholders = command.action === "animatic" || command.allowPlaceholders
|
|
2647
|
+
? "allow" as const
|
|
2648
|
+
: "reject" as const;
|
|
2649
|
+
let renderPlan;
|
|
2650
|
+
try {
|
|
2651
|
+
renderPlan = compileCinemaSequence(cinema, project.project, edit, { placeholders });
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
if (error instanceof TypeError) throw new CliError("conflict", error.message);
|
|
2654
|
+
throw error;
|
|
2655
|
+
}
|
|
2656
|
+
const planContents = `${canonicalJson(renderPlan)}\n`;
|
|
2657
|
+
const planPath = projectCinemaRenderPlanPath(renderPlan.planSha256);
|
|
2658
|
+
await saveImmutableText(
|
|
2659
|
+
project.fileSystem,
|
|
2660
|
+
planPath,
|
|
2661
|
+
planContents,
|
|
2662
|
+
sha256Hex(planContents),
|
|
2663
|
+
);
|
|
2664
|
+
const planDocumentSha256 = sha256Hex(planContents);
|
|
2665
|
+
|
|
2666
|
+
if (command.action === "plan") {
|
|
2667
|
+
writeValue(context.io, command.json, {
|
|
2668
|
+
advisories: renderPlan.advisories,
|
|
2669
|
+
plan: renderPlan,
|
|
2670
|
+
planPath,
|
|
2671
|
+
}, () => (
|
|
2672
|
+
`cinema render plan ${renderPlan.planSha256}; shots=${renderPlan.shots.length} cues=${renderPlan.audioCues.length} advisories=${renderPlan.advisories.length}`
|
|
2673
|
+
));
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
const tier = command.action === "animatic" ? "preview" as const : "final" as const;
|
|
2678
|
+
const requested = command.output
|
|
2679
|
+
?? `renders/cinema/${command.action}-${renderPlan.planSha256.slice(0, 16)}.mp4`;
|
|
2680
|
+
const renderLeaf = await resolveProjectRenderLeaf(project.directory.path, requested, { videoOutput: true });
|
|
2681
|
+
const receiptRelative = `${renderLeaf.relative}.cinema-receipt.json`;
|
|
2682
|
+
const receiptLeaf = await resolveProjectRenderLeaf(project.directory.path, receiptRelative);
|
|
2683
|
+
const ffmpeg = await requireRequestedCapability(context, "ffmpeg");
|
|
2684
|
+
const built = await buildCinemaFfmpegInvocation(renderPlan, {
|
|
2685
|
+
dryRun: command.dryRun,
|
|
2686
|
+
ffmpeg,
|
|
2687
|
+
outputPath: renderLeaf.absolute,
|
|
2688
|
+
projectDirectory: project.directory.path,
|
|
2689
|
+
repositoryRoot: context.paths.repositoryRoot,
|
|
2690
|
+
tier,
|
|
2691
|
+
});
|
|
2692
|
+
const output = {
|
|
2693
|
+
invocation: built.invocation,
|
|
2694
|
+
planPath,
|
|
2695
|
+
projectId: project.project.projectId,
|
|
2696
|
+
receiptPath: command.dryRun ? null : receiptRelative,
|
|
2697
|
+
tier,
|
|
2698
|
+
};
|
|
2699
|
+
if (command.dryRun) {
|
|
2700
|
+
writeValue(context.io, command.json, output, () => `dry-run ${built.argv.join(" ")}`);
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
const placeholderShotIds = renderPlan.shots
|
|
2704
|
+
.filter(shot => shot.placeholder !== undefined)
|
|
2705
|
+
.map(shot => shot.shotId);
|
|
2706
|
+
const outputIntegrity = await executeAtomicRender({
|
|
2707
|
+
argv: built.argv,
|
|
2708
|
+
companion: {
|
|
2709
|
+
finalPath: receiptLeaf.absolute,
|
|
2710
|
+
publish: async integrity => await project.fileSystem.writeTextAtomic(
|
|
2711
|
+
receiptRelative,
|
|
2712
|
+
`${canonicalJson(CinemaRenderReceiptV1Schema.parse({
|
|
2713
|
+
cinemaPlanSha256: cinema.cinemaPlanSha256,
|
|
2714
|
+
createdAt: context.io.now().toISOString(),
|
|
2715
|
+
kind: "slopcamera.cinema-render-receipt",
|
|
2716
|
+
output: { ...integrity, path: renderLeaf.relative },
|
|
2717
|
+
placeholders: placeholderShotIds,
|
|
2718
|
+
plan: { path: planPath, sha256: planDocumentSha256 },
|
|
2719
|
+
projectId: project.project.projectId,
|
|
2720
|
+
schemaVersion: 1,
|
|
2721
|
+
tier,
|
|
2722
|
+
}))}\n`,
|
|
2723
|
+
),
|
|
2724
|
+
},
|
|
2725
|
+
failureLabel: "FFmpeg cinema render failed",
|
|
2726
|
+
finalOutputPath: renderLeaf.absolute,
|
|
2727
|
+
maximumOutputBytes: MAXIMUM_LEGACY_PROJECT_RENDER_OUTPUT_BYTES,
|
|
2728
|
+
runner: context.runner,
|
|
2729
|
+
});
|
|
2730
|
+
const completed = { ...output, output: outputIntegrity };
|
|
2731
|
+
writeValue(
|
|
2732
|
+
context.io,
|
|
2733
|
+
command.json,
|
|
2734
|
+
completed,
|
|
2735
|
+
() => `rendered ${renderLeaf.relative}; plan=${planPath}`,
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2413
2739
|
function projectPlacement(
|
|
2414
2740
|
project: Awaited<ReturnType<typeof openProject>>,
|
|
2415
2741
|
assetId: string,
|
|
@@ -4457,6 +4783,20 @@ function gatewayCliError(error: unknown): CliError {
|
|
|
4457
4783
|
if (error instanceof GatewayProviderOptionsError) {
|
|
4458
4784
|
return new CliError("usage", error.message);
|
|
4459
4785
|
}
|
|
4786
|
+
if (error instanceof SlopcameraCloudError) {
|
|
4787
|
+
return new CliError(
|
|
4788
|
+
error.code === "INVALID_ARGUMENT"
|
|
4789
|
+
? "usage"
|
|
4790
|
+
: error.code === "OUTPUT_WRITE_FAILED"
|
|
4791
|
+
? "conflict"
|
|
4792
|
+
: error.code === "AUTHENTICATION_REQUIRED"
|
|
4793
|
+
? "authorization-required"
|
|
4794
|
+
: error.code === "GENERATION_INVALID_RESPONSE"
|
|
4795
|
+
? "invalid-data"
|
|
4796
|
+
: "unavailable",
|
|
4797
|
+
error.message,
|
|
4798
|
+
);
|
|
4799
|
+
}
|
|
4460
4800
|
return asCliError(error);
|
|
4461
4801
|
}
|
|
4462
4802
|
|
|
@@ -5112,6 +5452,819 @@ async function handleAiImageGenerate(
|
|
|
5112
5452
|
});
|
|
5113
5453
|
}
|
|
5114
5454
|
|
|
5455
|
+
async function dispatchGalleryCandidate(
|
|
5456
|
+
context: CommandContext,
|
|
5457
|
+
command: Extract<CliCommand, { readonly kind: "ai-image-gallery" }>,
|
|
5458
|
+
plan: SlopcameraGalleryPlan,
|
|
5459
|
+
candidate: SlopcameraGalleryCandidate,
|
|
5460
|
+
timeoutMs: number,
|
|
5461
|
+
): Promise<SlopcameraGalleryResolvedCandidate> {
|
|
5462
|
+
const tracker = await createGatewayJobTracker(context, {
|
|
5463
|
+
model: command.model,
|
|
5464
|
+
operation: "image.generate",
|
|
5465
|
+
request: {
|
|
5466
|
+
candidate: {
|
|
5467
|
+
id: candidate.id,
|
|
5468
|
+
index: candidate.index,
|
|
5469
|
+
label: candidate.label,
|
|
5470
|
+
},
|
|
5471
|
+
count: 1,
|
|
5472
|
+
galleryKind: plan.kind,
|
|
5473
|
+
promptCharacters: candidate.prompt.length,
|
|
5474
|
+
promptSha256: sha256Hex(candidate.prompt),
|
|
5475
|
+
subjectSha256: sha256Hex(plan.subject),
|
|
5476
|
+
},
|
|
5477
|
+
});
|
|
5478
|
+
const settled = (
|
|
5479
|
+
partial: Omit<SlopcameraGalleryResolvedCandidate, "id" | "index" | "job" | "label" | "prompt">,
|
|
5480
|
+
): SlopcameraGalleryResolvedCandidate => ({
|
|
5481
|
+
id: candidate.id,
|
|
5482
|
+
index: candidate.index,
|
|
5483
|
+
job: tracker.path,
|
|
5484
|
+
label: candidate.label,
|
|
5485
|
+
prompt: candidate.prompt,
|
|
5486
|
+
...partial,
|
|
5487
|
+
});
|
|
5488
|
+
try {
|
|
5489
|
+
const bundle = await executeTrackedGatewayOperation(
|
|
5490
|
+
context,
|
|
5491
|
+
tracker,
|
|
5492
|
+
async service => await service.generateImage(
|
|
5493
|
+
{
|
|
5494
|
+
consent: gatewayConsent(context),
|
|
5495
|
+
model: command.model,
|
|
5496
|
+
prompt: candidate.prompt,
|
|
5497
|
+
},
|
|
5498
|
+
{
|
|
5499
|
+
...(context.abortSignal === undefined
|
|
5500
|
+
? {}
|
|
5501
|
+
: { signal: context.abortSignal }),
|
|
5502
|
+
timeoutMs,
|
|
5503
|
+
},
|
|
5504
|
+
),
|
|
5505
|
+
result => result,
|
|
5506
|
+
);
|
|
5507
|
+
const output = bundle.outputs[0];
|
|
5508
|
+
if (output === undefined) {
|
|
5509
|
+
return settled({
|
|
5510
|
+
error: "The Gateway image job completed without an image output.",
|
|
5511
|
+
status: "failed",
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
const bytes = await readBoundedPhysicalBytes(
|
|
5515
|
+
output.path,
|
|
5516
|
+
CLI_GATEWAY_IMAGE_INPUT_BYTES,
|
|
5517
|
+
);
|
|
5518
|
+
const generated = settled({
|
|
5519
|
+
bytes,
|
|
5520
|
+
mediaType: output.mediaType,
|
|
5521
|
+
path: output.path,
|
|
5522
|
+
...(bundle.receipt.routing.generationId === undefined
|
|
5523
|
+
? {}
|
|
5524
|
+
: { requestId: bundle.receipt.routing.generationId }),
|
|
5525
|
+
sha256: output.sha256,
|
|
5526
|
+
status: "generated",
|
|
5527
|
+
...(bundle.receipt.warnings.length === 0
|
|
5528
|
+
? {}
|
|
5529
|
+
: { warnings: bundle.receipt.warnings }),
|
|
5530
|
+
});
|
|
5531
|
+
if (command.preview === "probe") {
|
|
5532
|
+
return await attachGalleryProbePreview(context, plan, generated);
|
|
5533
|
+
}
|
|
5534
|
+
return generated;
|
|
5535
|
+
} catch (error) {
|
|
5536
|
+
// A settled paid job stays a gallery row: the receipt keeps the failure
|
|
5537
|
+
// and the durable job path so review never depends on provider success.
|
|
5538
|
+
return settled({
|
|
5539
|
+
error: gatewayCliError(error).message,
|
|
5540
|
+
status: "failed",
|
|
5541
|
+
});
|
|
5542
|
+
}
|
|
5543
|
+
}
|
|
5544
|
+
|
|
5545
|
+
const GALLERY_PROBE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/svg+xml"]);
|
|
5546
|
+
|
|
5547
|
+
/**
|
|
5548
|
+
* Renders one settled candidate inside the fixed probe scene and returns the
|
|
5549
|
+
* candidate row with a distinct `cellImage` render artifact. A render failure
|
|
5550
|
+
* keeps the flat candidate cell and records the failure as a warning — the
|
|
5551
|
+
* paid candidate bytes stay reviewable and promotable.
|
|
5552
|
+
*/
|
|
5553
|
+
async function attachGalleryProbePreview(
|
|
5554
|
+
context: CommandContext,
|
|
5555
|
+
plan: SlopcameraGalleryPlan,
|
|
5556
|
+
resolved: SlopcameraGalleryResolvedCandidate,
|
|
5557
|
+
): Promise<SlopcameraGalleryResolvedCandidate> {
|
|
5558
|
+
const probeFailed = (message: string): SlopcameraGalleryResolvedCandidate => ({
|
|
5559
|
+
...resolved,
|
|
5560
|
+
warnings: [...(resolved.warnings ?? []), `Probe preview failed: ${message}`],
|
|
5561
|
+
});
|
|
5562
|
+
try {
|
|
5563
|
+
if (
|
|
5564
|
+
resolved.bytes === undefined
|
|
5565
|
+
|| resolved.mediaType === undefined
|
|
5566
|
+
|| resolved.path === undefined
|
|
5567
|
+
|| resolved.sha256 === undefined
|
|
5568
|
+
) {
|
|
5569
|
+
return probeFailed("the candidate artifact lacks published bytes.");
|
|
5570
|
+
}
|
|
5571
|
+
if (!GALLERY_PROBE_MIME_TYPES.has(resolved.mediaType)) {
|
|
5572
|
+
return probeFailed(`candidate media type ${resolved.mediaType} cannot be a scene asset.`);
|
|
5573
|
+
}
|
|
5574
|
+
const [repositoryRoot, candidatePath] = await Promise.all([
|
|
5575
|
+
realpath(context.paths.repositoryRoot),
|
|
5576
|
+
realpath(resolved.path),
|
|
5577
|
+
]);
|
|
5578
|
+
if (!isWithin(repositoryRoot, candidatePath)) {
|
|
5579
|
+
return probeFailed("the candidate artifact escapes the repository root.");
|
|
5580
|
+
}
|
|
5581
|
+
if (
|
|
5582
|
+
createHash("sha256").update(resolved.bytes).digest("hex")
|
|
5583
|
+
!== resolved.sha256
|
|
5584
|
+
) {
|
|
5585
|
+
return probeFailed("the candidate bytes do not match their published artifact digest.");
|
|
5586
|
+
}
|
|
5587
|
+
const metadata = await sharp(resolved.bytes, {
|
|
5588
|
+
failOn: "warning",
|
|
5589
|
+
limitInputPixels: slopcameraGalleryLimits.candidatePixels,
|
|
5590
|
+
}).metadata();
|
|
5591
|
+
if (metadata.width === undefined || metadata.height === undefined) {
|
|
5592
|
+
return probeFailed("the candidate image did not expose its dimensions.");
|
|
5593
|
+
}
|
|
5594
|
+
const mode = plan.kind as SlopcameraGalleryProbeMode;
|
|
5595
|
+
const scene = galleryProbeScene(mode, {
|
|
5596
|
+
assetId: "asset_candidate",
|
|
5597
|
+
dependencies: [],
|
|
5598
|
+
interpretation: {
|
|
5599
|
+
alpha: metadata.hasAlpha === true ? "straight" : "opaque",
|
|
5600
|
+
colorSpace: "srgb",
|
|
5601
|
+
height: metadata.height,
|
|
5602
|
+
kind: "image",
|
|
5603
|
+
mimeType: resolved.mediaType as "image/png" | "image/jpeg" | "image/svg+xml",
|
|
5604
|
+
width: metadata.width,
|
|
5605
|
+
},
|
|
5606
|
+
payload: {
|
|
5607
|
+
bytes: resolved.bytes.byteLength,
|
|
5608
|
+
path: basename(candidatePath),
|
|
5609
|
+
sha256: resolved.sha256,
|
|
5610
|
+
},
|
|
5611
|
+
provenance: {
|
|
5612
|
+
description: `Gallery candidate ${resolved.id} probe preview.`,
|
|
5613
|
+
source: "generated",
|
|
5614
|
+
},
|
|
5615
|
+
});
|
|
5616
|
+
const scenePath = join(dirname(candidatePath), "probe-scene.json");
|
|
5617
|
+
const handle = await open(
|
|
5618
|
+
scenePath,
|
|
5619
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
5620
|
+
0o600,
|
|
5621
|
+
);
|
|
5622
|
+
try {
|
|
5623
|
+
await handle.writeFile(`${canonicalJson(scene)}\n`);
|
|
5624
|
+
} finally {
|
|
5625
|
+
await handle.close();
|
|
5626
|
+
}
|
|
5627
|
+
const source = { path: relative(repositoryRoot, scenePath) };
|
|
5628
|
+
// The render phase admits its own local/browser vector per settled
|
|
5629
|
+
// candidate; paid provider waits never hold render capacity.
|
|
5630
|
+
const renderClaims = physicalHostResourceClaims(
|
|
5631
|
+
[
|
|
5632
|
+
{ amount: 1, resource: "cpu" },
|
|
5633
|
+
{ amount: 1, resource: "local-io" },
|
|
5634
|
+
{ amount: 1, resource: "browser" },
|
|
5635
|
+
{ amount: 1, resource: "output-publication" },
|
|
5636
|
+
],
|
|
5637
|
+
context.hostResourceCoordinator,
|
|
5638
|
+
);
|
|
5639
|
+
const rendered = await withHostResourceClaims(
|
|
5640
|
+
context,
|
|
5641
|
+
missingHostResourceClaims(
|
|
5642
|
+
context.hostResourceLease?.claims ?? [],
|
|
5643
|
+
renderClaims,
|
|
5644
|
+
),
|
|
5645
|
+
async admitted => {
|
|
5646
|
+
const input = {
|
|
5647
|
+
request: galleryProbeRenderRequest(),
|
|
5648
|
+
sceneSha256: spatialSceneSha256(scene),
|
|
5649
|
+
source,
|
|
5650
|
+
};
|
|
5651
|
+
if (context.renderGalleryScene !== undefined) {
|
|
5652
|
+
return await context.renderGalleryScene({
|
|
5653
|
+
...input,
|
|
5654
|
+
application: applicationContext(admitted),
|
|
5655
|
+
});
|
|
5656
|
+
}
|
|
5657
|
+
const registry = createApplicationOperationRegistry({
|
|
5658
|
+
toolVersion: context.version,
|
|
5659
|
+
});
|
|
5660
|
+
const result = await registry.execute(
|
|
5661
|
+
{
|
|
5662
|
+
abortSignal: context.abortSignal ?? new AbortController().signal,
|
|
5663
|
+
application: applicationContext(admitted),
|
|
5664
|
+
},
|
|
5665
|
+
{
|
|
5666
|
+
input,
|
|
5667
|
+
kind: "scene.render",
|
|
5668
|
+
version: 1,
|
|
5669
|
+
},
|
|
5670
|
+
);
|
|
5671
|
+
const output = result.output as {
|
|
5672
|
+
readonly artifact: {
|
|
5673
|
+
readonly bytes: number;
|
|
5674
|
+
readonly path: string;
|
|
5675
|
+
readonly sha256: string;
|
|
5676
|
+
};
|
|
5677
|
+
};
|
|
5678
|
+
return { artifact: output.artifact };
|
|
5679
|
+
},
|
|
5680
|
+
context.abortSignal,
|
|
5681
|
+
);
|
|
5682
|
+
const stillPath = resolve(context.paths.repositoryRoot, rendered.artifact.path);
|
|
5683
|
+
if (!isWithin(context.paths.repositoryRoot, stillPath)) {
|
|
5684
|
+
return probeFailed("the rendered still escapes the repository root.");
|
|
5685
|
+
}
|
|
5686
|
+
const stillBytes = await readBoundedPhysicalBytes(
|
|
5687
|
+
stillPath,
|
|
5688
|
+
CLI_GATEWAY_IMAGE_INPUT_BYTES,
|
|
5689
|
+
);
|
|
5690
|
+
if (
|
|
5691
|
+
stillBytes.byteLength !== rendered.artifact.bytes
|
|
5692
|
+
|| createHash("sha256").update(stillBytes).digest("hex") !== rendered.artifact.sha256
|
|
5693
|
+
) {
|
|
5694
|
+
return probeFailed("the rendered still does not match its published artifact digest.");
|
|
5695
|
+
}
|
|
5696
|
+
return {
|
|
5697
|
+
...resolved,
|
|
5698
|
+
cellImage: {
|
|
5699
|
+
bytes: stillBytes,
|
|
5700
|
+
mediaType: "image/png",
|
|
5701
|
+
path: stillPath,
|
|
5702
|
+
sha256: rendered.artifact.sha256,
|
|
5703
|
+
},
|
|
5704
|
+
};
|
|
5705
|
+
} catch (error) {
|
|
5706
|
+
return probeFailed(error instanceof Error ? error.message : String(error));
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
/**
|
|
5711
|
+
* Renders one derived variant scene into a beauty still through the qualified
|
|
5712
|
+
* `scene.render` operation (or the test seam) and returns the resolved gallery
|
|
5713
|
+
* candidate. A render or write failure stays in the sheet as a failed row
|
|
5714
|
+
* carrying the variant's patch provenance.
|
|
5715
|
+
*/
|
|
5716
|
+
async function dispatchSceneVariant(
|
|
5717
|
+
context: CommandContext,
|
|
5718
|
+
baseScene: SpatialSceneV1,
|
|
5719
|
+
variant: SlopcameraSceneGalleryVariant,
|
|
5720
|
+
request: {
|
|
5721
|
+
readonly cameraId: string;
|
|
5722
|
+
readonly mode: { readonly kind: "beauty" };
|
|
5723
|
+
readonly selection: { readonly kind: "frame"; readonly timeUs: number };
|
|
5724
|
+
},
|
|
5725
|
+
derivedPath: string,
|
|
5726
|
+
bases: {
|
|
5727
|
+
readonly repositoryRoot: string;
|
|
5728
|
+
readonly scene: string;
|
|
5729
|
+
readonly variants: string;
|
|
5730
|
+
},
|
|
5731
|
+
): Promise<SlopcameraGalleryResolvedCandidate> {
|
|
5732
|
+
const base = {
|
|
5733
|
+
id: variant.id,
|
|
5734
|
+
index: variant.index,
|
|
5735
|
+
label: variant.label,
|
|
5736
|
+
prompt: variant.prompt,
|
|
5737
|
+
scene: {
|
|
5738
|
+
diff: variant.diff,
|
|
5739
|
+
patchSha256: variant.patchSha256,
|
|
5740
|
+
sceneSha256: variant.sceneSha256,
|
|
5741
|
+
},
|
|
5742
|
+
};
|
|
5743
|
+
let derivedArtifact: {
|
|
5744
|
+
readonly bytes: Uint8Array;
|
|
5745
|
+
readonly mediaType: "application/json";
|
|
5746
|
+
readonly path: string;
|
|
5747
|
+
readonly sha256: string;
|
|
5748
|
+
} | undefined;
|
|
5749
|
+
let renderReceiptPath: string | undefined;
|
|
5750
|
+
try {
|
|
5751
|
+
const variantDirectory = dirname(derivedPath);
|
|
5752
|
+
await mkdir(variantDirectory, { mode: 0o700, recursive: true });
|
|
5753
|
+
// Stage every declared payload beneath the variant directory so the
|
|
5754
|
+
// derived manifest's contained relative paths resolve for this render
|
|
5755
|
+
// and for a standalone re-render after promotion. Inherited payloads
|
|
5756
|
+
// resolve beside the authored scene; variant-authored payloads resolve
|
|
5757
|
+
// beside the variants file.
|
|
5758
|
+
const baseAssets = new Map(
|
|
5759
|
+
baseScene.assets.map(asset => [asset.assetId, asset]),
|
|
5760
|
+
);
|
|
5761
|
+
const staged = new Map<string, string>();
|
|
5762
|
+
const assets: {
|
|
5763
|
+
readonly assetId: string;
|
|
5764
|
+
readonly artifact: {
|
|
5765
|
+
readonly bytes: number;
|
|
5766
|
+
readonly path: string;
|
|
5767
|
+
readonly sha256: string;
|
|
5768
|
+
};
|
|
5769
|
+
}[] = [];
|
|
5770
|
+
for (const asset of variant.scene.assets) {
|
|
5771
|
+
const inherited = baseAssets.get(asset.assetId);
|
|
5772
|
+
const unchanged = inherited !== undefined
|
|
5773
|
+
&& inherited.payload.sha256 === asset.payload.sha256
|
|
5774
|
+
&& inherited.payload.bytes === asset.payload.bytes
|
|
5775
|
+
&& inherited.payload.path === asset.payload.path;
|
|
5776
|
+
const sourcePath = resolve(
|
|
5777
|
+
unchanged ? bases.scene : bases.variants,
|
|
5778
|
+
asset.payload.path,
|
|
5779
|
+
);
|
|
5780
|
+
const target = join(variantDirectory, asset.payload.path);
|
|
5781
|
+
const binding = [
|
|
5782
|
+
sourcePath,
|
|
5783
|
+
String(asset.payload.bytes),
|
|
5784
|
+
asset.payload.sha256,
|
|
5785
|
+
].join("\u0000");
|
|
5786
|
+
const assetBinding = {
|
|
5787
|
+
assetId: asset.assetId,
|
|
5788
|
+
artifact: {
|
|
5789
|
+
bytes: asset.payload.bytes,
|
|
5790
|
+
path: relative(context.paths.repositoryRoot, target),
|
|
5791
|
+
sha256: asset.payload.sha256,
|
|
5792
|
+
},
|
|
5793
|
+
};
|
|
5794
|
+
const previous = staged.get(target);
|
|
5795
|
+
if (previous !== undefined) {
|
|
5796
|
+
if (previous !== binding) {
|
|
5797
|
+
throw new CliError(
|
|
5798
|
+
"conflict",
|
|
5799
|
+
`Variant ${variant.id} binds two payloads at ${asset.payload.path}.`,
|
|
5800
|
+
);
|
|
5801
|
+
}
|
|
5802
|
+
assets.push(assetBinding);
|
|
5803
|
+
continue;
|
|
5804
|
+
}
|
|
5805
|
+
const physicalSourcePath = await realpath(sourcePath);
|
|
5806
|
+
if (
|
|
5807
|
+
physicalSourcePath !== sourcePath
|
|
5808
|
+
|| !isWithin(bases.repositoryRoot, physicalSourcePath)
|
|
5809
|
+
) {
|
|
5810
|
+
throw new CliError(
|
|
5811
|
+
"unsafe-path",
|
|
5812
|
+
`Asset payload ${asset.payload.path} must be a physical file inside the repository.`,
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
const payload = await readBoundedPhysicalBytes(
|
|
5816
|
+
sourcePath,
|
|
5817
|
+
asset.payload.bytes,
|
|
5818
|
+
);
|
|
5819
|
+
if (
|
|
5820
|
+
payload.byteLength !== asset.payload.bytes
|
|
5821
|
+
|| createHash("sha256").update(payload).digest("hex")
|
|
5822
|
+
!== asset.payload.sha256
|
|
5823
|
+
) {
|
|
5824
|
+
throw new CliError(
|
|
5825
|
+
"invalid-data",
|
|
5826
|
+
`Asset ${asset.assetId} payload does not match its manifest.`,
|
|
5827
|
+
);
|
|
5828
|
+
}
|
|
5829
|
+
await mkdir(dirname(target), { mode: 0o700, recursive: true });
|
|
5830
|
+
const handle = await open(
|
|
5831
|
+
target,
|
|
5832
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
|
|
5833
|
+
| (constants.O_NOFOLLOW ?? 0),
|
|
5834
|
+
0o600,
|
|
5835
|
+
);
|
|
5836
|
+
try {
|
|
5837
|
+
await handle.writeFile(payload);
|
|
5838
|
+
await handle.sync();
|
|
5839
|
+
} finally {
|
|
5840
|
+
await handle.close();
|
|
5841
|
+
}
|
|
5842
|
+
staged.set(target, binding);
|
|
5843
|
+
assets.push(assetBinding);
|
|
5844
|
+
}
|
|
5845
|
+
// The derived scene is a new no-replace document beside the sheet; the
|
|
5846
|
+
// authored source scene is never touched.
|
|
5847
|
+
await publishSpatialSource(derivedPath, variant.scene);
|
|
5848
|
+
const sceneBytes = await readBoundedPhysicalBytes(
|
|
5849
|
+
derivedPath,
|
|
5850
|
+
SPATIAL_SCENE_LIMITS.sourceBytes,
|
|
5851
|
+
);
|
|
5852
|
+
derivedArtifact = {
|
|
5853
|
+
bytes: sceneBytes,
|
|
5854
|
+
mediaType: "application/json",
|
|
5855
|
+
path: derivedPath,
|
|
5856
|
+
sha256: createHash("sha256").update(sceneBytes).digest("hex"),
|
|
5857
|
+
};
|
|
5858
|
+
const source = {
|
|
5859
|
+
path: relative(context.paths.repositoryRoot, derivedPath),
|
|
5860
|
+
};
|
|
5861
|
+
const rendered = context.renderGalleryScene === undefined
|
|
5862
|
+
? await (async () => {
|
|
5863
|
+
const registry = createApplicationOperationRegistry({
|
|
5864
|
+
toolVersion: context.version,
|
|
5865
|
+
});
|
|
5866
|
+
const result = await registry.execute(
|
|
5867
|
+
{
|
|
5868
|
+
abortSignal: context.abortSignal ?? new AbortController().signal,
|
|
5869
|
+
application: applicationContext(context),
|
|
5870
|
+
},
|
|
5871
|
+
{
|
|
5872
|
+
input: {
|
|
5873
|
+
assets,
|
|
5874
|
+
request,
|
|
5875
|
+
sceneSha256: variant.sceneSha256,
|
|
5876
|
+
source,
|
|
5877
|
+
},
|
|
5878
|
+
kind: "scene.render",
|
|
5879
|
+
version: 1,
|
|
5880
|
+
},
|
|
5881
|
+
);
|
|
5882
|
+
return result.output as {
|
|
5883
|
+
readonly artifact: {
|
|
5884
|
+
readonly bytes: number;
|
|
5885
|
+
readonly path: string;
|
|
5886
|
+
readonly sha256: string;
|
|
5887
|
+
};
|
|
5888
|
+
readonly receipt: { readonly path: string };
|
|
5889
|
+
};
|
|
5890
|
+
})()
|
|
5891
|
+
: await context.renderGalleryScene({
|
|
5892
|
+
application: applicationContext(context),
|
|
5893
|
+
assets,
|
|
5894
|
+
request,
|
|
5895
|
+
sceneSha256: variant.sceneSha256,
|
|
5896
|
+
source,
|
|
5897
|
+
});
|
|
5898
|
+
if (rendered.receipt !== undefined) {
|
|
5899
|
+
const receiptPath = resolve(
|
|
5900
|
+
context.paths.repositoryRoot,
|
|
5901
|
+
rendered.receipt.path,
|
|
5902
|
+
);
|
|
5903
|
+
if (!isWithin(context.paths.repositoryRoot, receiptPath)) {
|
|
5904
|
+
throw new CliError(
|
|
5905
|
+
"unsafe-path",
|
|
5906
|
+
"A render receipt escapes the repository root.",
|
|
5907
|
+
);
|
|
5908
|
+
}
|
|
5909
|
+
renderReceiptPath = receiptPath;
|
|
5910
|
+
}
|
|
5911
|
+
const stillPath = resolve(
|
|
5912
|
+
context.paths.repositoryRoot,
|
|
5913
|
+
rendered.artifact.path,
|
|
5914
|
+
);
|
|
5915
|
+
if (!isWithin(context.paths.repositoryRoot, stillPath)) {
|
|
5916
|
+
throw new CliError(
|
|
5917
|
+
"unsafe-path",
|
|
5918
|
+
"A rendered variant still escapes the repository root.",
|
|
5919
|
+
);
|
|
5920
|
+
}
|
|
5921
|
+
const stillBytes = await readBoundedPhysicalBytes(
|
|
5922
|
+
stillPath,
|
|
5923
|
+
CLI_GATEWAY_IMAGE_INPUT_BYTES,
|
|
5924
|
+
);
|
|
5925
|
+
if (
|
|
5926
|
+
stillBytes.byteLength !== rendered.artifact.bytes
|
|
5927
|
+
|| createHash("sha256").update(stillBytes).digest("hex")
|
|
5928
|
+
!== rendered.artifact.sha256
|
|
5929
|
+
) {
|
|
5930
|
+
throw new CliError(
|
|
5931
|
+
"conflict",
|
|
5932
|
+
"A rendered variant still does not match its published artifact digest.",
|
|
5933
|
+
);
|
|
5934
|
+
}
|
|
5935
|
+
return {
|
|
5936
|
+
...base,
|
|
5937
|
+
...derivedArtifact,
|
|
5938
|
+
cellImage: {
|
|
5939
|
+
bytes: stillBytes,
|
|
5940
|
+
mediaType: "image/png",
|
|
5941
|
+
path: stillPath,
|
|
5942
|
+
sha256: rendered.artifact.sha256,
|
|
5943
|
+
},
|
|
5944
|
+
...(renderReceiptPath === undefined ? {} : { job: renderReceiptPath }),
|
|
5945
|
+
status: "generated",
|
|
5946
|
+
};
|
|
5947
|
+
} catch (error) {
|
|
5948
|
+
return {
|
|
5949
|
+
...base,
|
|
5950
|
+
...derivedArtifact,
|
|
5951
|
+
...(renderReceiptPath === undefined ? {} : { job: renderReceiptPath }),
|
|
5952
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5953
|
+
status: "failed",
|
|
5954
|
+
};
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
|
|
5958
|
+
async function handleAiSceneGallery(
|
|
5959
|
+
context: CommandContext,
|
|
5960
|
+
command: Extract<CliCommand, { readonly kind: "ai-scene-gallery" }>,
|
|
5961
|
+
): Promise<void> {
|
|
5962
|
+
const scenePath = resolve(context.paths.repositoryRoot, command.scene);
|
|
5963
|
+
const variantsPath = resolve(context.paths.repositoryRoot, command.variants);
|
|
5964
|
+
const [repositoryRoot, physicalScenePath, physicalVariantsPath] = await Promise.all([
|
|
5965
|
+
realpath(context.paths.repositoryRoot),
|
|
5966
|
+
realpath(scenePath),
|
|
5967
|
+
realpath(variantsPath),
|
|
5968
|
+
]);
|
|
5969
|
+
if (
|
|
5970
|
+
physicalScenePath !== resolve(
|
|
5971
|
+
repositoryRoot,
|
|
5972
|
+
relative(context.paths.repositoryRoot, scenePath),
|
|
5973
|
+
)
|
|
5974
|
+
|| physicalVariantsPath !== resolve(
|
|
5975
|
+
repositoryRoot,
|
|
5976
|
+
relative(context.paths.repositoryRoot, variantsPath),
|
|
5977
|
+
)
|
|
5978
|
+
|| !isWithin(repositoryRoot, physicalScenePath)
|
|
5979
|
+
|| !isWithin(repositoryRoot, physicalVariantsPath)
|
|
5980
|
+
) {
|
|
5981
|
+
throw new CliError(
|
|
5982
|
+
"unsafe-path",
|
|
5983
|
+
"Scene gallery inputs must be physical files inside the repository.",
|
|
5984
|
+
);
|
|
5985
|
+
}
|
|
5986
|
+
const scene = parseSpatialScene(await readSpatialJson(scenePath));
|
|
5987
|
+
const variantsValue = await readSpatialJson(
|
|
5988
|
+
variantsPath,
|
|
5989
|
+
slopcameraSceneGalleryLimits.variantsBytes,
|
|
5990
|
+
);
|
|
5991
|
+
// Every variant patch is validated and applied before the first render.
|
|
5992
|
+
const gallery = planSlopcameraSceneGallery({
|
|
5993
|
+
scene,
|
|
5994
|
+
variants: variantsValue,
|
|
5995
|
+
});
|
|
5996
|
+
const cameraId = command.camera ?? gallery.scene.cameras[0]?.cameraId;
|
|
5997
|
+
if (cameraId === undefined) {
|
|
5998
|
+
throw new CliError(
|
|
5999
|
+
"invalid-data",
|
|
6000
|
+
"The scene declares no camera to render from.",
|
|
6001
|
+
);
|
|
6002
|
+
}
|
|
6003
|
+
if (
|
|
6004
|
+
!gallery.scene.cameras.some(camera => camera.cameraId === cameraId)
|
|
6005
|
+
) {
|
|
6006
|
+
throw new CliError(
|
|
6007
|
+
"invalid-data",
|
|
6008
|
+
`--camera ${cameraId} is not a scene camera.`,
|
|
6009
|
+
);
|
|
6010
|
+
}
|
|
6011
|
+
const timeUs = command.timeUs ?? 0;
|
|
6012
|
+
const outputDir = await resolveSafePath(
|
|
6013
|
+
context.paths.repositoryRoot,
|
|
6014
|
+
command.outputDir,
|
|
6015
|
+
);
|
|
6016
|
+
await mkdir(outputDir, { mode: 0o700, recursive: true });
|
|
6017
|
+
for (const name of ["gallery.png", "receipt.json", "variants"] as const) {
|
|
6018
|
+
const existing = await lstat(join(outputDir, name)).then(
|
|
6019
|
+
() => true,
|
|
6020
|
+
() => false,
|
|
6021
|
+
);
|
|
6022
|
+
if (existing) {
|
|
6023
|
+
throw new CliError(
|
|
6024
|
+
"conflict",
|
|
6025
|
+
`Gallery outputs are never replaced; ${name} already exists in ${command.outputDir}.`,
|
|
6026
|
+
);
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
const renderRequest = {
|
|
6030
|
+
cameraId,
|
|
6031
|
+
mode: { kind: "beauty" as const },
|
|
6032
|
+
selection: { kind: "frame" as const, timeUs },
|
|
6033
|
+
};
|
|
6034
|
+
const bases = {
|
|
6035
|
+
repositoryRoot,
|
|
6036
|
+
scene: dirname(physicalScenePath),
|
|
6037
|
+
variants: dirname(physicalVariantsPath),
|
|
6038
|
+
};
|
|
6039
|
+
const resolved = await mapBounded(
|
|
6040
|
+
gallery.variants,
|
|
6041
|
+
slopcameraGalleryLimits.concurrency,
|
|
6042
|
+
async variant =>
|
|
6043
|
+
await dispatchSceneVariant(
|
|
6044
|
+
context,
|
|
6045
|
+
gallery.scene,
|
|
6046
|
+
variant,
|
|
6047
|
+
renderRequest,
|
|
6048
|
+
join(outputDir, "variants", variant.id, "scene.json"),
|
|
6049
|
+
bases,
|
|
6050
|
+
),
|
|
6051
|
+
);
|
|
6052
|
+
const plan: SlopcameraGalleryPlan = {
|
|
6053
|
+
aspect: [16, 9],
|
|
6054
|
+
axes: [],
|
|
6055
|
+
candidates: gallery.variants.map(variant => ({
|
|
6056
|
+
id: variant.id,
|
|
6057
|
+
index: variant.index,
|
|
6058
|
+
label: variant.label,
|
|
6059
|
+
prompt: variant.prompt,
|
|
6060
|
+
})),
|
|
6061
|
+
kind: "image",
|
|
6062
|
+
subject: gallery.scene.sceneId,
|
|
6063
|
+
tiled: false,
|
|
6064
|
+
};
|
|
6065
|
+
const receipt = await composeSlopcameraImageGallery({
|
|
6066
|
+
candidates: resolved,
|
|
6067
|
+
...(command.cell === undefined ? {} : { cellEdge: command.cell }),
|
|
6068
|
+
model: "local/scene-render",
|
|
6069
|
+
outputDir,
|
|
6070
|
+
plan,
|
|
6071
|
+
receipt: {
|
|
6072
|
+
baseSceneSha256: gallery.baseSceneSha256,
|
|
6073
|
+
kind: "slopcamera.scene-gallery",
|
|
6074
|
+
},
|
|
6075
|
+
});
|
|
6076
|
+
const summary = {
|
|
6077
|
+
baseSceneSha256: gallery.baseSceneSha256,
|
|
6078
|
+
gallery: displayPath(context.paths.repositoryRoot, receipt.gallery.path),
|
|
6079
|
+
kind: receipt.kind,
|
|
6080
|
+
receipt: displayPath(context.paths.repositoryRoot, receipt.receiptPath),
|
|
6081
|
+
variants: receipt.candidates.map(candidate => ({
|
|
6082
|
+
id: candidate.id,
|
|
6083
|
+
index: candidate.index,
|
|
6084
|
+
...(candidate.cellImage === undefined
|
|
6085
|
+
? {}
|
|
6086
|
+
: {
|
|
6087
|
+
cellImage: {
|
|
6088
|
+
...(candidate.cellImage.path === undefined
|
|
6089
|
+
? {}
|
|
6090
|
+
: {
|
|
6091
|
+
path: displayPath(
|
|
6092
|
+
context.paths.repositoryRoot,
|
|
6093
|
+
candidate.cellImage.path,
|
|
6094
|
+
),
|
|
6095
|
+
}),
|
|
6096
|
+
sha256: candidate.cellImage.sha256,
|
|
6097
|
+
},
|
|
6098
|
+
}),
|
|
6099
|
+
...(candidate.job === undefined
|
|
6100
|
+
? {}
|
|
6101
|
+
: { job: displayPath(context.paths.repositoryRoot, candidate.job) }),
|
|
6102
|
+
...(candidate.path === undefined
|
|
6103
|
+
? {}
|
|
6104
|
+
: { path: displayPath(context.paths.repositoryRoot, candidate.path) }),
|
|
6105
|
+
...(candidate.scene === undefined ? {} : { scene: candidate.scene }),
|
|
6106
|
+
status: candidate.status,
|
|
6107
|
+
})),
|
|
6108
|
+
};
|
|
6109
|
+
writeValue(
|
|
6110
|
+
context.io,
|
|
6111
|
+
command.json,
|
|
6112
|
+
summary,
|
|
6113
|
+
() =>
|
|
6114
|
+
[
|
|
6115
|
+
`scene.gallery ${receipt.counts.generated}/${receipt.counts.requested} variants rendered`,
|
|
6116
|
+
...receipt.candidates.map(candidate => (
|
|
6117
|
+
`#${String(candidate.index)} ${candidate.id}\t${candidate.status}`
|
|
6118
|
+
+ (candidate.cellImage?.path === undefined
|
|
6119
|
+
? ""
|
|
6120
|
+
: `\tstill ${displayPath(context.paths.repositoryRoot, candidate.cellImage.path)}`)
|
|
6121
|
+
)),
|
|
6122
|
+
].join("\n"),
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
|
|
6126
|
+
async function handleAiImageGallery(
|
|
6127
|
+
context: CommandContext,
|
|
6128
|
+
command: Extract<CliCommand, { readonly kind: "ai-image-gallery" }>,
|
|
6129
|
+
): Promise<void> {
|
|
6130
|
+
await withGatewayErrors(async () => {
|
|
6131
|
+
const timeoutMs = gatewayTimeoutMs(command.timeout);
|
|
6132
|
+
const vary = command.vary === undefined
|
|
6133
|
+
? undefined
|
|
6134
|
+
: parseSlopcameraGalleryVary(command.vary);
|
|
6135
|
+
let explicitCandidates: readonly unknown[] | undefined;
|
|
6136
|
+
if (command.candidatesFile !== undefined) {
|
|
6137
|
+
const text = await readGatewayTextFile(
|
|
6138
|
+
context.io.cwd(),
|
|
6139
|
+
command.candidatesFile,
|
|
6140
|
+
64 * 1024,
|
|
6141
|
+
);
|
|
6142
|
+
let parsedCandidates: unknown;
|
|
6143
|
+
try {
|
|
6144
|
+
parsedCandidates = JSON.parse(text) as unknown;
|
|
6145
|
+
} catch {
|
|
6146
|
+
throw new CliError(
|
|
6147
|
+
"usage",
|
|
6148
|
+
"--candidates must be valid JSON.",
|
|
6149
|
+
);
|
|
6150
|
+
}
|
|
6151
|
+
if (!Array.isArray(parsedCandidates)) {
|
|
6152
|
+
throw new CliError(
|
|
6153
|
+
"usage",
|
|
6154
|
+
"--candidates must be a JSON array of {id, prompt|variant} entries.",
|
|
6155
|
+
);
|
|
6156
|
+
}
|
|
6157
|
+
explicitCandidates = parsedCandidates;
|
|
6158
|
+
}
|
|
6159
|
+
const plan = planSlopcameraGallery({
|
|
6160
|
+
...(explicitCandidates === undefined
|
|
6161
|
+
? {}
|
|
6162
|
+
: { candidates: explicitCandidates }),
|
|
6163
|
+
...(command.count === undefined ? {} : { count: command.count }),
|
|
6164
|
+
...(command.galleryKind === undefined
|
|
6165
|
+
? {}
|
|
6166
|
+
: { kind: command.galleryKind as SlopcameraGalleryKind }),
|
|
6167
|
+
subject: command.subject,
|
|
6168
|
+
...(command.tiled === undefined ? {} : { tiled: command.tiled }),
|
|
6169
|
+
...(vary === undefined ? {} : { vary }),
|
|
6170
|
+
});
|
|
6171
|
+
const outputDir = resolve(context.io.cwd(), command.outputDir);
|
|
6172
|
+
await mkdir(outputDir, { recursive: true });
|
|
6173
|
+
for (const name of ["gallery.png", "receipt.json"] as const) {
|
|
6174
|
+
const existing = await lstat(join(outputDir, name)).then(
|
|
6175
|
+
() => true,
|
|
6176
|
+
() => false,
|
|
6177
|
+
);
|
|
6178
|
+
if (existing) {
|
|
6179
|
+
throw new CliError(
|
|
6180
|
+
"conflict",
|
|
6181
|
+
`Gallery outputs are never replaced; ${name} already exists in ${command.outputDir}.`,
|
|
6182
|
+
);
|
|
6183
|
+
}
|
|
6184
|
+
}
|
|
6185
|
+
const resolved = await mapBounded(
|
|
6186
|
+
plan.candidates,
|
|
6187
|
+
slopcameraGalleryLimits.concurrency,
|
|
6188
|
+
async candidate => await dispatchGalleryCandidate(
|
|
6189
|
+
context,
|
|
6190
|
+
command,
|
|
6191
|
+
plan,
|
|
6192
|
+
candidate,
|
|
6193
|
+
timeoutMs,
|
|
6194
|
+
),
|
|
6195
|
+
);
|
|
6196
|
+
const receipt = await composeSlopcameraImageGallery({
|
|
6197
|
+
candidates: resolved,
|
|
6198
|
+
...(command.cell === undefined ? {} : { cellEdge: command.cell }),
|
|
6199
|
+
model: command.model,
|
|
6200
|
+
outputDir,
|
|
6201
|
+
plan,
|
|
6202
|
+
});
|
|
6203
|
+
const summary = {
|
|
6204
|
+
candidates: receipt.candidates.map(candidate => ({
|
|
6205
|
+
id: candidate.id,
|
|
6206
|
+
index: candidate.index,
|
|
6207
|
+
...(candidate.job === undefined
|
|
6208
|
+
? {}
|
|
6209
|
+
: { job: displayPath(context.paths.repositoryRoot, candidate.job) }),
|
|
6210
|
+
...(candidate.path === undefined
|
|
6211
|
+
? {}
|
|
6212
|
+
: { path: displayPath(context.paths.repositoryRoot, candidate.path) }),
|
|
6213
|
+
...(candidate.sha256 === undefined ? {} : { sha256: candidate.sha256 }),
|
|
6214
|
+
status: candidate.status,
|
|
6215
|
+
...(candidate.tiled === undefined ? {} : { tiled: candidate.tiled }),
|
|
6216
|
+
...(candidate.cellImage === undefined
|
|
6217
|
+
? {}
|
|
6218
|
+
: {
|
|
6219
|
+
cellImage: {
|
|
6220
|
+
...(candidate.cellImage.path === undefined
|
|
6221
|
+
? {}
|
|
6222
|
+
: {
|
|
6223
|
+
path: displayPath(
|
|
6224
|
+
context.paths.repositoryRoot,
|
|
6225
|
+
candidate.cellImage.path,
|
|
6226
|
+
),
|
|
6227
|
+
}),
|
|
6228
|
+
sha256: candidate.cellImage.sha256,
|
|
6229
|
+
},
|
|
6230
|
+
}),
|
|
6231
|
+
...(candidate.error === undefined ? {} : { error: candidate.error }),
|
|
6232
|
+
})),
|
|
6233
|
+
counts: receipt.counts,
|
|
6234
|
+
gallery: {
|
|
6235
|
+
bytes: receipt.gallery.bytes,
|
|
6236
|
+
height: receipt.gallery.height,
|
|
6237
|
+
path: displayPath(context.paths.repositoryRoot, receipt.gallery.path),
|
|
6238
|
+
sha256: receipt.gallery.sha256,
|
|
6239
|
+
width: receipt.gallery.width,
|
|
6240
|
+
},
|
|
6241
|
+
kind: receipt.galleryKind,
|
|
6242
|
+
model: receipt.model,
|
|
6243
|
+
receiptPath: displayPath(
|
|
6244
|
+
context.paths.repositoryRoot,
|
|
6245
|
+
receipt.receiptPath,
|
|
6246
|
+
),
|
|
6247
|
+
subject: receipt.subject,
|
|
6248
|
+
};
|
|
6249
|
+
writeValue(
|
|
6250
|
+
context.io,
|
|
6251
|
+
command.json,
|
|
6252
|
+
summary,
|
|
6253
|
+
() => [
|
|
6254
|
+
`image.gallery ${receipt.counts.generated}/${receipt.counts.requested} candidates generated (${receipt.model})`,
|
|
6255
|
+
...receipt.candidates.map(candidate => (
|
|
6256
|
+
`#${String(candidate.index)} ${candidate.id}\t${candidate.status}`
|
|
6257
|
+
+ (candidate.job === undefined
|
|
6258
|
+
? ""
|
|
6259
|
+
: `\tjob ${displayPath(context.paths.repositoryRoot, candidate.job)}`)
|
|
6260
|
+
)),
|
|
6261
|
+
`gallery ${summary.gallery.path}`,
|
|
6262
|
+
`receipt ${summary.receiptPath}`,
|
|
6263
|
+
].join("\n"),
|
|
6264
|
+
);
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
|
|
5115
6268
|
async function handleAiVideoGenerate(
|
|
5116
6269
|
context: CommandContext,
|
|
5117
6270
|
command: Extract<CliCommand, { readonly kind: "ai-video-generate" }>,
|
|
@@ -5769,6 +6922,18 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
5769
6922
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
5770
6923
|
return;
|
|
5771
6924
|
}
|
|
6925
|
+
case "spatial-asset": {
|
|
6926
|
+
const output = await executeSpatialAssetCommand(applicationContext(context), command, {
|
|
6927
|
+
...(context.abortSignal === undefined ? {} : { signal: context.abortSignal }),
|
|
6928
|
+
});
|
|
6929
|
+
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
6930
|
+
return;
|
|
6931
|
+
}
|
|
6932
|
+
case "spatial-design": {
|
|
6933
|
+
const output = await executeSpatialDesignCommand(applicationContext(context), command, context.abortSignal);
|
|
6934
|
+
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
6935
|
+
return;
|
|
6936
|
+
}
|
|
5772
6937
|
case "spatial-scene": {
|
|
5773
6938
|
const output = await executeSpatialSceneCommand(applicationContext(context), command, context.abortSignal);
|
|
5774
6939
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
@@ -5781,6 +6946,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
5781
6946
|
}
|
|
5782
6947
|
case "help": writeLine(context.io, commandHelp(command.topic)); return;
|
|
5783
6948
|
case "version": writeLine(context.io, context.version); return;
|
|
6949
|
+
case "capabilities": writeCapabilityManifest(context.io, command.json, context.version); return;
|
|
5784
6950
|
case "operations-list": {
|
|
5785
6951
|
const operations = operationDiscoveryList(
|
|
5786
6952
|
createApplicationOperationRegistry({ toolVersion: context.version }),
|
|
@@ -6290,6 +7456,8 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6290
7456
|
case "ai-models-list": await handleAiModelsList(context, command); return;
|
|
6291
7457
|
case "ai-models-show": await handleAiModelsShow(context, command); return;
|
|
6292
7458
|
case "ai-image-generate": await handleAiImageGenerate(context, command); return;
|
|
7459
|
+
case "ai-image-gallery": await handleAiImageGallery(context, command); return;
|
|
7460
|
+
case "ai-scene-gallery": await handleAiSceneGallery(context, command); return;
|
|
6293
7461
|
case "ai-video-generate": await handleAiVideoGenerate(context, command); return;
|
|
6294
7462
|
case "ai-speech-generate": await handleAiSpeechGenerate(context, command); return;
|
|
6295
7463
|
case "ai-transcribe": await handleAiTranscribe(context, command); return;
|
|
@@ -6429,6 +7597,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6429
7597
|
case "project-metadata-edit": await handleProjectMetadataEdit(context, command); return;
|
|
6430
7598
|
case "project-overlay-edit": await handleProjectOverlayEdit(context, command); return;
|
|
6431
7599
|
case "project-render": await handleProjectRender(context, command); return;
|
|
7600
|
+
case "project-cinema": await handleProjectCinema(context, command); return;
|
|
6432
7601
|
case "align-analyze": await handleAlignAnalyze(context, command); return;
|
|
6433
7602
|
case "align-apply": await handleAlignApply(context, command); return;
|
|
6434
7603
|
case "inspect": {
|
|
@@ -6545,7 +7714,9 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6545
7714
|
case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
|
|
6546
7715
|
case "directing": return undefined; // The directing store owns its explicit lease.
|
|
6547
7716
|
case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
|
|
6548
|
-
case "spatial-
|
|
7717
|
+
case "spatial-asset": return undefined; // Content-addressed asset admission owns its publication custody.
|
|
7718
|
+
case "spatial-design": return undefined; // Fresh directories and no-replace source publication own custody.
|
|
7719
|
+
case "spatial-scene": return command.action === "init" || command.action === "patch" || command.action === "camera-track" || command.action === "solve" ? { kind: "workspace-private" } : undefined;
|
|
6549
7720
|
case "spatial-project": return undefined; // Its explicit application adapter owns one version-aware lease.
|
|
6550
7721
|
case "project-camera-edit": return command.action === "show"
|
|
6551
7722
|
? undefined
|
|
@@ -6555,6 +7726,7 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6555
7726
|
case "project-metadata-edit":
|
|
6556
7727
|
case "project-overlay-edit":
|
|
6557
7728
|
case "project-render":
|
|
7729
|
+
case "project-cinema":
|
|
6558
7730
|
case "align-analyze":
|
|
6559
7731
|
case "align-apply":
|
|
6560
7732
|
case "analyze-faces":
|
|
@@ -6583,11 +7755,14 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6583
7755
|
case "diagram-render":
|
|
6584
7756
|
case "image-vectorize": return { kind: "workspace-private" };
|
|
6585
7757
|
case "ai-image-generate":
|
|
7758
|
+
case "ai-image-gallery":
|
|
7759
|
+
case "ai-scene-gallery":
|
|
6586
7760
|
case "ai-video-generate":
|
|
6587
7761
|
case "ai-speech-generate":
|
|
6588
7762
|
case "ai-transcribe": return undefined;
|
|
6589
7763
|
case "help":
|
|
6590
7764
|
case "version":
|
|
7765
|
+
case "capabilities":
|
|
6591
7766
|
case "operations-list":
|
|
6592
7767
|
case "operations-show":
|
|
6593
7768
|
case "diagram-check":
|
|
@@ -6697,6 +7872,10 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6697
7872
|
for (const completion of completions(command.words)) writeLine(io, completion);
|
|
6698
7873
|
return 0;
|
|
6699
7874
|
}
|
|
7875
|
+
if (command.kind === "capabilities") {
|
|
7876
|
+
writeCapabilityManifest(io, command.json, dependencies.version ?? SLOPCAMERA_VERSION);
|
|
7877
|
+
return 0;
|
|
7878
|
+
}
|
|
6700
7879
|
const paths = dependencies.paths ?? await resolveRepositoryPaths(io.cwd(), io.env);
|
|
6701
7880
|
const stateRoot = resolve(dependencies.stateRoot
|
|
6702
7881
|
?? defaultCliStateRoot(io.platform, io.env));
|
|
@@ -6764,8 +7943,13 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6764
7943
|
runner,
|
|
6765
7944
|
sceneProviderFactory: dependencies.sceneProviderFactory
|
|
6766
7945
|
?? (options => createGatewaySceneProvider(options)),
|
|
7946
|
+
spatialReviewProviderFactory: dependencies.spatialReviewProviderFactory
|
|
7947
|
+
?? (options => createGatewaySpatialReviewProvider(options)),
|
|
6767
7948
|
sleep: dependencies.sleep ?? (async milliseconds => await Bun.sleep(milliseconds)),
|
|
6768
7949
|
version: dependencies.version ?? SLOPCAMERA_VERSION,
|
|
7950
|
+
...(dependencies.renderGalleryScene === undefined
|
|
7951
|
+
? {}
|
|
7952
|
+
: { renderGalleryScene: dependencies.renderGalleryScene }),
|
|
6769
7953
|
};
|
|
6770
7954
|
const mutationTarget = await resolveMutationTarget(paths, stateRoot, command);
|
|
6771
7955
|
await withCommandHostResources(context, command, async admittedContext => {
|