@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
|
@@ -1,12 +1,49 @@
|
|
|
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";
|
|
1
31
|
import { createDirectingBlobSession } from "./directing-blob";
|
|
2
32
|
import { assembleDirectingClips, extractDirectingEndpoint, importDirectingAnchor } from "./directing-media";
|
|
3
33
|
import { executeDirectingCommand } from "./directing-service";
|
|
4
34
|
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
-
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";
|
|
6
43
|
import { executeSpatialProjectCommand } from "./spatial-project-service";
|
|
7
44
|
import { executeSpatialWorldCommand } from "./spatial-world-service";
|
|
8
45
|
import { constants } from "node:fs";
|
|
9
|
-
import { link, lstat, open, realpath, rm } from "node:fs/promises";
|
|
46
|
+
import { link, lstat, mkdir, open, realpath, rm } from "node:fs/promises";
|
|
10
47
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
11
48
|
import {
|
|
12
49
|
createDefaultHostResourceCoordinator,
|
|
@@ -20,6 +57,9 @@ import {
|
|
|
20
57
|
AnalyzerEvidenceV1Schema,
|
|
21
58
|
AlignmentCandidateIdSchema,
|
|
22
59
|
AudioAlignmentAnalysisV1Schema,
|
|
60
|
+
CinemaContinuityReportSchema,
|
|
61
|
+
CinemaPlanIdSchema,
|
|
62
|
+
CinemaRenderReceiptV1Schema,
|
|
23
63
|
EventKindSchema,
|
|
24
64
|
MusicAnalysisV1Schema,
|
|
25
65
|
OverlayOperationSchema,
|
|
@@ -49,12 +89,18 @@ import {
|
|
|
49
89
|
applyProjectInactivityPlan,
|
|
50
90
|
analyzeAudioAlignment,
|
|
51
91
|
applyAudioAlignmentCandidate,
|
|
92
|
+
analyzeCinemaPlan,
|
|
52
93
|
addZoom,
|
|
94
|
+
assertCinemaPlanComposition,
|
|
95
|
+
auditCinemaContinuity,
|
|
96
|
+
auditCinemaTemporal,
|
|
53
97
|
buildProjectOutputTimeMap,
|
|
54
98
|
buildSourceTimeMap,
|
|
55
99
|
canonicalSlopcameraPersistenceDocument,
|
|
56
100
|
canonicalJson,
|
|
101
|
+
compileCinemaSequence,
|
|
57
102
|
compileProjectRenderPlan,
|
|
103
|
+
createCinemaPlanScaffold,
|
|
58
104
|
compileRenderPlan,
|
|
59
105
|
createNodeBundleFileSystem,
|
|
60
106
|
createDefaultEditPlan,
|
|
@@ -69,11 +115,17 @@ import {
|
|
|
69
115
|
normalizeProjectEditPlan,
|
|
70
116
|
planAutomaticInactivityCuts,
|
|
71
117
|
planAutomaticZooms,
|
|
118
|
+
planCinemaGallery,
|
|
72
119
|
projectFillerCut,
|
|
73
120
|
removeOverlay,
|
|
74
121
|
removeZoom,
|
|
75
122
|
saveEditPlan,
|
|
76
123
|
saveAnalysisArtifact,
|
|
124
|
+
saveImmutableText,
|
|
125
|
+
loadProjectCinemaPlan,
|
|
126
|
+
projectCinemaRenderPlanPath,
|
|
127
|
+
saveProjectCinemaPlan,
|
|
128
|
+
saveProjectCinemaRevision,
|
|
77
129
|
saveProjectEditPlan,
|
|
78
130
|
setSpeed,
|
|
79
131
|
rebaseProjectEditPlan,
|
|
@@ -132,9 +184,11 @@ import {
|
|
|
132
184
|
import { resolveEmojiAsset, searchEmojiAssets, inspectEmojiAssets } from "./emoji-assets";
|
|
133
185
|
import { asCliError, CliError, EXIT_CODE } from "./errors";
|
|
134
186
|
import { commandHelp, completions } from "./help";
|
|
187
|
+
import { createLocalSlopcameraCapabilityManifest } from "./capability-manifest";
|
|
135
188
|
import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
|
|
136
189
|
import { executeHtmlSceneCommand } from "./html-scene";
|
|
137
190
|
import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
|
|
191
|
+
import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
|
|
138
192
|
import {
|
|
139
193
|
codePreparationHostResourceClaims,
|
|
140
194
|
combineHostResourceClaims,
|
|
@@ -175,13 +229,8 @@ import {
|
|
|
175
229
|
type OpenProject,
|
|
176
230
|
} from "./project-service";
|
|
177
231
|
import { resolveVerifiedProjectMedia } from "./project-media-integrity";
|
|
178
|
-
import {
|
|
179
|
-
executeRecordingAction,
|
|
180
|
-
type RecordingController,
|
|
181
|
-
type RecordingSnapshot,
|
|
182
|
-
type RecordingStartOptions,
|
|
183
|
-
} from "./recording-controller";
|
|
184
232
|
import { buildFfmpegInvocation, prepareOverlaySources } from "./renderer";
|
|
233
|
+
import { buildCinemaFfmpegInvocation } from "./cinema-renderer";
|
|
185
234
|
import { buildProjectFfmpegInvocation } from "./project-renderer";
|
|
186
235
|
import {
|
|
187
236
|
DEFAULT_PROJECT_INACTIVITY_CONFIG,
|
|
@@ -190,6 +239,7 @@ import {
|
|
|
190
239
|
import { resolveAnimatedPlaybackWindow } from "./overlay-playback";
|
|
191
240
|
import { analyzeProjectScenes } from "./scene-analysis-service";
|
|
192
241
|
import type { SceneDescriptionProvider } from "@hraness/slopcamera/scene";
|
|
242
|
+
import type { SpatialReviewProvider } from "../../../src/spatial-scene/review";
|
|
193
243
|
import { parseCliTime } from "./time";
|
|
194
244
|
import {
|
|
195
245
|
DEFAULT_FACE_ANALYSIS_CONFIG,
|
|
@@ -218,6 +268,10 @@ import {
|
|
|
218
268
|
createGatewaySceneProvider,
|
|
219
269
|
type GatewaySceneProviderOptions,
|
|
220
270
|
} from "./gateway-scene-provider";
|
|
271
|
+
import {
|
|
272
|
+
createGatewaySpatialReviewProvider,
|
|
273
|
+
type GatewaySpatialReviewProviderOptions,
|
|
274
|
+
} from "./gateway-review-provider";
|
|
221
275
|
import {
|
|
222
276
|
GatewayMediaCatalogError,
|
|
223
277
|
createFileGatewayMediaCatalogSnapshotStore,
|
|
@@ -301,7 +355,7 @@ import {
|
|
|
301
355
|
workflowRunStore,
|
|
302
356
|
} from "./workflow-runs";
|
|
303
357
|
|
|
304
|
-
export const SLOPCAMERA_VERSION = "3.
|
|
358
|
+
export const SLOPCAMERA_VERSION = "3.3.1";
|
|
305
359
|
|
|
306
360
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
307
361
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -310,6 +364,7 @@ const MAXIMUM_LEGACY_RECORDING_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
|
|
|
310
364
|
const MAXIMUM_LEGACY_PROJECT_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
|
|
311
365
|
|
|
312
366
|
export interface CliDependencies {
|
|
367
|
+
readonly onUsefulResult?: UsefulResultObserver;
|
|
313
368
|
readonly abortSignal?: AbortSignal;
|
|
314
369
|
readonly stateRoot?: string;
|
|
315
370
|
readonly clock?: () => number;
|
|
@@ -320,11 +375,38 @@ export interface CliDependencies {
|
|
|
320
375
|
readonly hostResourceCoordinator?: HostResourceCoordinator;
|
|
321
376
|
readonly io?: CliIo;
|
|
322
377
|
readonly paths?: RepositoryPaths;
|
|
323
|
-
readonly recordingController?: RecordingController;
|
|
324
378
|
readonly runner?: ProcessRunner;
|
|
325
379
|
readonly sceneProviderFactory?: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
380
|
+
readonly spatialReviewProviderFactory?: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
|
|
326
381
|
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
327
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
|
+
}>;
|
|
328
410
|
}
|
|
329
411
|
|
|
330
412
|
interface CommandContext {
|
|
@@ -347,11 +429,12 @@ interface CommandContext {
|
|
|
347
429
|
readonly hostResourceCoordinator: HostResourceCoordinator;
|
|
348
430
|
readonly io: CliIo;
|
|
349
431
|
readonly paths: RepositoryPaths;
|
|
350
|
-
readonly recordingController: RecordingController | undefined;
|
|
351
432
|
readonly runner: ProcessRunner;
|
|
352
433
|
readonly sceneProviderFactory: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
434
|
+
readonly spatialReviewProviderFactory: (options: GatewaySpatialReviewProviderOptions) => SpatialReviewProvider;
|
|
353
435
|
readonly sleep: (milliseconds: number) => Promise<void>;
|
|
354
436
|
readonly version: string;
|
|
437
|
+
readonly renderGalleryScene?: CliDependencies["renderGalleryScene"];
|
|
355
438
|
}
|
|
356
439
|
|
|
357
440
|
function contextWithHostResourceLease(
|
|
@@ -541,7 +624,6 @@ function applicationContext(
|
|
|
541
624
|
context: CommandContext,
|
|
542
625
|
providerOptions?: GatewayProviderOptions,
|
|
543
626
|
): ApplicationContext {
|
|
544
|
-
const recordingController = context.recordingController;
|
|
545
627
|
const toApplicationCapability = (capability: Capability) => ({
|
|
546
628
|
available: capability.available,
|
|
547
629
|
...(capability.command === undefined ? {} : { command: capability.command }),
|
|
@@ -572,22 +654,14 @@ function applicationContext(
|
|
|
572
654
|
: { hostResourceLease: context.hostResourceLease }),
|
|
573
655
|
machineStateRoot: context.stateRoot,
|
|
574
656
|
paths: context.paths,
|
|
575
|
-
...(recordingController === undefined
|
|
576
|
-
? {}
|
|
577
|
-
: {
|
|
578
|
-
recordingController: {
|
|
579
|
-
execute: async (
|
|
580
|
-
action: "pause" | "resume" | "start" | "stop",
|
|
581
|
-
options?: unknown,
|
|
582
|
-
) => await executeRecordingAction(
|
|
583
|
-
recordingController,
|
|
584
|
-
action,
|
|
585
|
-
options as RecordingStartOptions | undefined,
|
|
586
|
-
),
|
|
587
|
-
status: async () => await recordingController.status(),
|
|
588
|
-
},
|
|
589
|
-
}),
|
|
590
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
|
+
}),
|
|
591
665
|
};
|
|
592
666
|
return {
|
|
593
667
|
...application,
|
|
@@ -692,6 +766,16 @@ function writeValue(io: CliIo, json: boolean, value: unknown, human: () => strin
|
|
|
692
766
|
else writeLine(io, human());
|
|
693
767
|
}
|
|
694
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
|
+
|
|
695
779
|
function isWithin(root: string, candidate: string): boolean {
|
|
696
780
|
const pathRelative = relative(root, candidate);
|
|
697
781
|
return pathRelative === "" || (!pathRelative.startsWith("..") && !isAbsolute(pathRelative));
|
|
@@ -1715,11 +1799,6 @@ async function handleRender(
|
|
|
1715
1799
|
writeValue(context.io, command.json, completed, () => `rendered ${output.outputPath}; plan=${artifactPath}`);
|
|
1716
1800
|
}
|
|
1717
1801
|
|
|
1718
|
-
function snapshotHuman(snapshot: RecordingSnapshot): string {
|
|
1719
|
-
if (snapshot.state === "idle") return "idle";
|
|
1720
|
-
return `${snapshot.state} ${snapshot.recordingId ?? "unknown"} ${humanTime(snapshot.logicalTimeUs)} segments=${snapshot.completedSegmentCount}`;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
1802
|
async function handleProjectEdit(
|
|
1724
1803
|
context: CommandContext,
|
|
1725
1804
|
command: Extract<CliCommand, { readonly kind: "project-edit" }>,
|
|
@@ -2436,6 +2515,227 @@ async function handleProjectRender(
|
|
|
2436
2515
|
writeValue(context.io, command.json, completed, () => `rendered ${outputRelative}; plan=${planArtifactPath}`);
|
|
2437
2516
|
}
|
|
2438
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
|
+
|
|
2439
2739
|
function projectPlacement(
|
|
2440
2740
|
project: Awaited<ReturnType<typeof openProject>>,
|
|
2441
2741
|
assetId: string,
|
|
@@ -4483,6 +4783,20 @@ function gatewayCliError(error: unknown): CliError {
|
|
|
4483
4783
|
if (error instanceof GatewayProviderOptionsError) {
|
|
4484
4784
|
return new CliError("usage", error.message);
|
|
4485
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
|
+
}
|
|
4486
4800
|
return asCliError(error);
|
|
4487
4801
|
}
|
|
4488
4802
|
|
|
@@ -5138,6 +5452,819 @@ async function handleAiImageGenerate(
|
|
|
5138
5452
|
});
|
|
5139
5453
|
}
|
|
5140
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
|
+
|
|
5141
6268
|
async function handleAiVideoGenerate(
|
|
5142
6269
|
context: CommandContext,
|
|
5143
6270
|
command: Extract<CliCommand, { readonly kind: "ai-video-generate" }>,
|
|
@@ -5795,6 +6922,18 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
5795
6922
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
5796
6923
|
return;
|
|
5797
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
|
+
}
|
|
5798
6937
|
case "spatial-scene": {
|
|
5799
6938
|
const output = await executeSpatialSceneCommand(applicationContext(context), command, context.abortSignal);
|
|
5800
6939
|
writeValue(context.io, command.json, output, () => JSON.stringify(output, null, 2));
|
|
@@ -5807,6 +6946,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
5807
6946
|
}
|
|
5808
6947
|
case "help": writeLine(context.io, commandHelp(command.topic)); return;
|
|
5809
6948
|
case "version": writeLine(context.io, context.version); return;
|
|
6949
|
+
case "capabilities": writeCapabilityManifest(context.io, command.json, context.version); return;
|
|
5810
6950
|
case "operations-list": {
|
|
5811
6951
|
const operations = operationDiscoveryList(
|
|
5812
6952
|
createApplicationOperationRegistry({ toolVersion: context.version }),
|
|
@@ -6260,18 +7400,14 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6260
7400
|
for (const completion of completions(command.words)) writeLine(context.io, completion);
|
|
6261
7401
|
return;
|
|
6262
7402
|
case "doctor": {
|
|
6263
|
-
const [capabilities, emoji,
|
|
7403
|
+
const [capabilities, emoji, macVersion] = await Promise.all([
|
|
6264
7404
|
context.capabilities(),
|
|
6265
7405
|
inspectEmojiAssets(context.paths.repositoryRoot),
|
|
6266
|
-
context.recordingController === undefined
|
|
6267
|
-
? Promise.resolve(null)
|
|
6268
|
-
: context.recordingController.status(),
|
|
6269
7406
|
context.io.platform === "darwin"
|
|
6270
7407
|
? context.runner.run(["/usr/bin/sw_vers", "-productVersion"], { maxOutputBytes: 4_096 })
|
|
6271
7408
|
: Promise.resolve({ exitCode: 0, stderr: "", stdout: "not-macos" }),
|
|
6272
7409
|
]);
|
|
6273
7410
|
const output = {
|
|
6274
|
-
activeRecording: active,
|
|
6275
7411
|
artifactRoot: displayPath(context.paths.repositoryRoot, context.paths.artifactRoot),
|
|
6276
7412
|
gatewayCredential: inspectGatewayCredential(context.io.env),
|
|
6277
7413
|
emoji: {
|
|
@@ -6290,7 +7426,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6290
7426
|
`artifacts ${output.artifactRoot}`,
|
|
6291
7427
|
`platform ${output.platform.name} ${output.platform.macOSVersion}`,
|
|
6292
7428
|
...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
|
|
6293
|
-
`recording ${active === null ? "controller-unavailable" : snapshotHuman(active)}`,
|
|
6294
7429
|
`emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
|
|
6295
7430
|
].join("\n"));
|
|
6296
7431
|
return;
|
|
@@ -6321,11 +7456,22 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6321
7456
|
case "ai-models-list": await handleAiModelsList(context, command); return;
|
|
6322
7457
|
case "ai-models-show": await handleAiModelsShow(context, command); return;
|
|
6323
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;
|
|
6324
7461
|
case "ai-video-generate": await handleAiVideoGenerate(context, command); return;
|
|
6325
7462
|
case "ai-speech-generate": await handleAiSpeechGenerate(context, command); return;
|
|
6326
7463
|
case "ai-transcribe": await handleAiTranscribe(context, command); return;
|
|
6327
7464
|
case "media-audio": await handleMediaAudio(context, command); return;
|
|
6328
7465
|
case "media-color": await handleMediaColor(context, command); return;
|
|
7466
|
+
case "menubar": {
|
|
7467
|
+
if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
|
|
7468
|
+
else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
|
|
7469
|
+
return;
|
|
7470
|
+
}
|
|
7471
|
+
case "outputs": {
|
|
7472
|
+
await reportOutputsRoot(context.io, context.stateRoot, command.json);
|
|
7473
|
+
return;
|
|
7474
|
+
}
|
|
6329
7475
|
case "recordings-list": {
|
|
6330
7476
|
const directories = (await listRecordingDirectories(context.paths.artifactRoot)).slice(0, command.limit);
|
|
6331
7477
|
const recordings = await Promise.all(directories.map(async ({ id }) => {
|
|
@@ -6451,6 +7597,7 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6451
7597
|
case "project-metadata-edit": await handleProjectMetadataEdit(context, command); return;
|
|
6452
7598
|
case "project-overlay-edit": await handleProjectOverlayEdit(context, command); return;
|
|
6453
7599
|
case "project-render": await handleProjectRender(context, command); return;
|
|
7600
|
+
case "project-cinema": await handleProjectCinema(context, command); return;
|
|
6454
7601
|
case "align-analyze": await handleAlignAnalyze(context, command); return;
|
|
6455
7602
|
case "align-apply": await handleAlignApply(context, command); return;
|
|
6456
7603
|
case "inspect": {
|
|
@@ -6509,32 +7656,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6509
7656
|
).join("\n"));
|
|
6510
7657
|
return;
|
|
6511
7658
|
}
|
|
6512
|
-
case "record": {
|
|
6513
|
-
if (context.recordingController === undefined) throw new CliError("unavailable", "Recording controller is unavailable.");
|
|
6514
|
-
const options = command.action === "start"
|
|
6515
|
-
? {
|
|
6516
|
-
camera: !command.webcam
|
|
6517
|
-
? { kind: "disabled" as const }
|
|
6518
|
-
: command.cameraDeviceId === undefined
|
|
6519
|
-
? { kind: "default" as const }
|
|
6520
|
-
: { deviceId: command.cameraDeviceId, kind: "device" as const },
|
|
6521
|
-
displays: command.displays.length === 0
|
|
6522
|
-
? { kind: "all" as const }
|
|
6523
|
-
: { displayIds: command.displays, kind: "selected" as const },
|
|
6524
|
-
microphone: !command.microphone
|
|
6525
|
-
? { kind: "disabled" as const }
|
|
6526
|
-
: command.microphoneDeviceId === undefined
|
|
6527
|
-
? { kind: "default" as const }
|
|
6528
|
-
: { deviceId: command.microphoneDeviceId, kind: "device" as const },
|
|
6529
|
-
strictInputs: command.strictInputs,
|
|
6530
|
-
systemAudio: command.systemAudio,
|
|
6531
|
-
typedText: command.typedText,
|
|
6532
|
-
}
|
|
6533
|
-
: undefined;
|
|
6534
|
-
const snapshot = await executeRecordingAction(context.recordingController, command.action, options);
|
|
6535
|
-
writeValue(context.io, command.json, snapshot, () => snapshotHuman(snapshot));
|
|
6536
|
-
return;
|
|
6537
|
-
}
|
|
6538
7659
|
case "edit": await handleEdit(context, command); return;
|
|
6539
7660
|
case "analyze-inactivity": await handleInactivity(context, command); return;
|
|
6540
7661
|
case "analyze-faces": await handleFaceAnalysis(context, command); return;
|
|
@@ -6593,7 +7714,9 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6593
7714
|
case "studio": return undefined; // Native jobs and the machine custody marker own explicit leases.
|
|
6594
7715
|
case "directing": return undefined; // The directing store owns its explicit lease.
|
|
6595
7716
|
case "spatial-world": return undefined; // Immutable world attempts and imports own their publication custody.
|
|
6596
|
-
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;
|
|
6597
7720
|
case "spatial-project": return undefined; // Its explicit application adapter owns one version-aware lease.
|
|
6598
7721
|
case "project-camera-edit": return command.action === "show"
|
|
6599
7722
|
? undefined
|
|
@@ -6603,6 +7726,7 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6603
7726
|
case "project-metadata-edit":
|
|
6604
7727
|
case "project-overlay-edit":
|
|
6605
7728
|
case "project-render":
|
|
7729
|
+
case "project-cinema":
|
|
6606
7730
|
case "align-analyze":
|
|
6607
7731
|
case "align-apply":
|
|
6608
7732
|
case "analyze-faces":
|
|
@@ -6631,11 +7755,14 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6631
7755
|
case "diagram-render":
|
|
6632
7756
|
case "image-vectorize": return { kind: "workspace-private" };
|
|
6633
7757
|
case "ai-image-generate":
|
|
7758
|
+
case "ai-image-gallery":
|
|
7759
|
+
case "ai-scene-gallery":
|
|
6634
7760
|
case "ai-video-generate":
|
|
6635
7761
|
case "ai-speech-generate":
|
|
6636
7762
|
case "ai-transcribe": return undefined;
|
|
6637
7763
|
case "help":
|
|
6638
7764
|
case "version":
|
|
7765
|
+
case "capabilities":
|
|
6639
7766
|
case "operations-list":
|
|
6640
7767
|
case "operations-show":
|
|
6641
7768
|
case "diagram-check":
|
|
@@ -6662,11 +7789,12 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6662
7789
|
case "fillers-list":
|
|
6663
7790
|
case "emoji-search":
|
|
6664
7791
|
case "emoji-resolve":
|
|
7792
|
+
case "menubar":
|
|
7793
|
+
case "outputs":
|
|
6665
7794
|
case "complete": return undefined;
|
|
6666
|
-
//
|
|
6667
|
-
// atomic rename
|
|
6668
|
-
case "projects-create":
|
|
6669
|
-
case "record": return undefined;
|
|
7795
|
+
// Fresh projects write outside an existing mutable bundle and publish by
|
|
7796
|
+
// atomic rename.
|
|
7797
|
+
case "projects-create": return undefined;
|
|
6670
7798
|
}
|
|
6671
7799
|
}
|
|
6672
7800
|
|
|
@@ -6744,6 +7872,10 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6744
7872
|
for (const completion of completions(command.words)) writeLine(io, completion);
|
|
6745
7873
|
return 0;
|
|
6746
7874
|
}
|
|
7875
|
+
if (command.kind === "capabilities") {
|
|
7876
|
+
writeCapabilityManifest(io, command.json, dependencies.version ?? SLOPCAMERA_VERSION);
|
|
7877
|
+
return 0;
|
|
7878
|
+
}
|
|
6747
7879
|
const paths = dependencies.paths ?? await resolveRepositoryPaths(io.cwd(), io.env);
|
|
6748
7880
|
const stateRoot = resolve(dependencies.stateRoot
|
|
6749
7881
|
?? defaultCliStateRoot(io.platform, io.env));
|
|
@@ -6808,12 +7940,16 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6808
7940
|
?? createDefaultHostResourceCoordinator(),
|
|
6809
7941
|
io,
|
|
6810
7942
|
paths,
|
|
6811
|
-
recordingController: dependencies.recordingController,
|
|
6812
7943
|
runner,
|
|
6813
7944
|
sceneProviderFactory: dependencies.sceneProviderFactory
|
|
6814
7945
|
?? (options => createGatewaySceneProvider(options)),
|
|
7946
|
+
spatialReviewProviderFactory: dependencies.spatialReviewProviderFactory
|
|
7947
|
+
?? (options => createGatewaySpatialReviewProvider(options)),
|
|
6815
7948
|
sleep: dependencies.sleep ?? (async milliseconds => await Bun.sleep(milliseconds)),
|
|
6816
7949
|
version: dependencies.version ?? SLOPCAMERA_VERSION,
|
|
7950
|
+
...(dependencies.renderGalleryScene === undefined
|
|
7951
|
+
? {}
|
|
7952
|
+
: { renderGalleryScene: dependencies.renderGalleryScene }),
|
|
6817
7953
|
};
|
|
6818
7954
|
const mutationTarget = await resolveMutationTarget(paths, stateRoot, command);
|
|
6819
7955
|
await withCommandHostResources(context, command, async admittedContext => {
|
|
@@ -6828,6 +7964,15 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6828
7964
|
await dispatch(admittedContext, command);
|
|
6829
7965
|
});
|
|
6830
7966
|
});
|
|
7967
|
+
if (command.kind === "projects-create" || command.kind === "project-inspect"
|
|
7968
|
+
|| command.kind === "project-add" || command.kind === "inspect"
|
|
7969
|
+
|| command.kind === "project-edit" || command.kind === "project-camera-edit"
|
|
7970
|
+
|| command.kind === "project-metadata-edit" || command.kind === "project-overlay-edit"
|
|
7971
|
+
|| command.kind === "edit"
|
|
7972
|
+
|| command.kind === "render-run" && !command.dryRun
|
|
7973
|
+
|| command.kind === "project-render" && command.action === "run" && !command.dryRun) {
|
|
7974
|
+
reportUsefulResult(dependencies.onUsefulResult);
|
|
7975
|
+
}
|
|
6831
7976
|
return 0;
|
|
6832
7977
|
} catch (error) {
|
|
6833
7978
|
const failure = asCliError(error);
|