@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.
Files changed (150) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,736 @@
1
+ import { z } from "zod";
2
+
3
+ import { SourceIntervalSchema } from "./edit";
4
+ import { ColorGradeSelectionSchema } from "./media-effects";
5
+ import {
6
+ ProjectAssetIdSchema,
7
+ ProjectPlacementIdSchema,
8
+ ProjectStreamIdSchema,
9
+ VideoProjectIdSchema,
10
+ } from "./project";
11
+ import { ProjectRenderTierSchema } from "./project-render";
12
+ import { Yuv420pDimensionSchema } from "./render";
13
+ import {
14
+ IsoTimestampSchema,
15
+ MicrosecondsSchema,
16
+ PositiveMicrosecondsSchema,
17
+ RepositoryRelativePathSchema,
18
+ Sha256Schema,
19
+ SignedMicrosecondsSchema,
20
+ type ReadonlyInferred,
21
+ } from "./recording";
22
+ import {
23
+ SpatialDigestSchema,
24
+ SpatialShotIdSchema,
25
+ } from "../../../src/spatial-scene/contracts";
26
+
27
+ export const CINEMA_LIMITS = Object.freeze({
28
+ audioCues: 256,
29
+ beats: 128,
30
+ clipBytes: 1_099_511_627_776,
31
+ handleUs: 30_000_000,
32
+ looks: 32,
33
+ markers: 256,
34
+ mediaSlices: 4_096,
35
+ outputDurationUs: 3_600_000_000,
36
+ shots: 64,
37
+ transitionDurationUs: 10_000_000,
38
+ });
39
+
40
+ const OPAQUE_ID_SUFFIX = /^[a-z0-9][a-z0-9_-]{7,63}$/u;
41
+
42
+ function opaqueId<Prefix extends string>(prefix: Prefix) {
43
+ return z.string().superRefine((value, context) => {
44
+ if (!value.startsWith(prefix) || !OPAQUE_ID_SUFFIX.test(value.slice(prefix.length))) {
45
+ context.addIssue({
46
+ code: "custom",
47
+ message: `Expected an opaque ${prefix} identifier.`,
48
+ });
49
+ }
50
+ });
51
+ }
52
+
53
+ export const CinemaPlanIdSchema = opaqueId("cinema_").brand<"CinemaPlanId">();
54
+ export const CinemaShotIdSchema = opaqueId("cshot_").brand<"CinemaShotId">();
55
+ export const CinemaMarkerIdSchema = opaqueId("cmark_").brand<"CinemaMarkerId">();
56
+ export const CinemaBeatIdSchema = opaqueId("cbeat_").brand<"CinemaBeatId">();
57
+ export const CinemaLookIdSchema = opaqueId("clook_").brand<"CinemaLookId">();
58
+ export const CinemaCueIdSchema = opaqueId("ccue_").brand<"CinemaCueId">();
59
+
60
+ /**
61
+ * A retained, content-addressed media artifact that can feed a cinema
62
+ * sequence directly: a materialized spatial shot render or transition clip.
63
+ */
64
+ export const CinemaClipArtifactSchema = z.strictObject({
65
+ bytes: z.number().int().safe().positive().max(CINEMA_LIMITS.clipBytes),
66
+ container: z.enum(["mkv", "mov", "mp4", "webm"]),
67
+ durationUs: PositiveMicrosecondsSchema,
68
+ frameRate: z.number().finite().positive().max(1_000),
69
+ path: RepositoryRelativePathSchema,
70
+ pixelHeight: Yuv420pDimensionSchema,
71
+ pixelWidth: Yuv420pDimensionSchema,
72
+ sha256: Sha256Schema,
73
+ });
74
+
75
+ export const CinemaPlacementShotSourceSchema = z.strictObject({
76
+ kind: z.literal("placement"),
77
+ placementId: ProjectPlacementIdSchema,
78
+ /** Shot body window on the project clock; handles extend it inside coverage. */
79
+ range: SourceIntervalSchema,
80
+ streamId: ProjectStreamIdSchema,
81
+ });
82
+
83
+ export const CinemaSpatialShotSourceSchema = z.strictObject({
84
+ /**
85
+ * A materialized render retained beside the project. Absent artifacts admit
86
+ * placeholder animatics; a final compile requires the rendered media.
87
+ */
88
+ artifact: CinemaClipArtifactSchema.optional(),
89
+ kind: z.literal("spatial"),
90
+ requestSha256: SpatialDigestSchema,
91
+ sceneSha256: SpatialDigestSchema,
92
+ shotId: SpatialShotIdSchema,
93
+ shotSha256: SpatialDigestSchema,
94
+ /** Window inside the retained artifact consumed by the shot body. */
95
+ sourceRange: SourceIntervalSchema,
96
+ });
97
+
98
+ export const CinemaShotSourceSchema = z.discriminatedUnion("kind", [
99
+ CinemaPlacementShotSourceSchema,
100
+ CinemaSpatialShotSourceSchema,
101
+ ]);
102
+
103
+ export const CINEMA_SCREEN_DIRECTIONS = ["left", "right", "center"] as const;
104
+ export const CINEMA_EYELINES = ["left", "right", "center"] as const;
105
+
106
+ /**
107
+ * Optional authored continuity evidence. Audits report only on declared
108
+ * values; they never infer direction, exposure, or focus from pixels.
109
+ */
110
+ export const CinemaShotContinuitySchema = z.strictObject({
111
+ cameraKey: z.string().min(1).max(256).optional(),
112
+ exposureEv: z.number().finite().min(-32).max(32).optional(),
113
+ eyeline: z.enum(CINEMA_EYELINES).optional(),
114
+ focusDistanceM: z.number().finite().positive().max(1_000_000).optional(),
115
+ screenDirection: z.enum(CINEMA_SCREEN_DIRECTIONS).optional(),
116
+ });
117
+
118
+ export const CinemaFitSchema = z.enum(["contain", "cover"]);
119
+
120
+ export const CinemaShotSchema = z.strictObject({
121
+ continuity: CinemaShotContinuitySchema.optional(),
122
+ fit: CinemaFitSchema.default("contain"),
123
+ handles: z.strictObject({
124
+ postRollUs: MicrosecondsSchema.max(CINEMA_LIMITS.handleUs),
125
+ preRollUs: MicrosecondsSchema.max(CINEMA_LIMITS.handleUs),
126
+ }).default({ postRollUs: 0, preRollUs: 0 }),
127
+ lookId: CinemaLookIdSchema.optional(),
128
+ shotId: CinemaShotIdSchema,
129
+ source: CinemaShotSourceSchema,
130
+ });
131
+
132
+ export const CINEMA_TRANSITION_DIRECTIONS = [
133
+ "left",
134
+ "right",
135
+ "up",
136
+ "down",
137
+ ] as const;
138
+
139
+ export const CinemaTransitionSchema = z.discriminatedUnion("kind", [
140
+ z.strictObject({ kind: z.literal("cut") }),
141
+ z.strictObject({
142
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
143
+ kind: z.literal("dissolve"),
144
+ }),
145
+ z.strictObject({
146
+ direction: z.enum(CINEMA_TRANSITION_DIRECTIONS),
147
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
148
+ kind: z.literal("wipe"),
149
+ }),
150
+ z.strictObject({
151
+ color: z.string().regex(/^#[a-fA-F0-9]{6}$/u),
152
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
153
+ kind: z.literal("dip-to-color"),
154
+ }),
155
+ z.strictObject({
156
+ blurSigma: z.number().finite().min(1).max(64),
157
+ direction: z.enum(CINEMA_TRANSITION_DIRECTIONS),
158
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
159
+ kind: z.literal("whip-pan"),
160
+ }),
161
+ z.strictObject({
162
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
163
+ kind: z.literal("light-flash"),
164
+ peakHoldUs: MicrosecondsSchema.max(1_000_000).default(0),
165
+ }),
166
+ z.strictObject({
167
+ clip: CinemaClipArtifactSchema.optional(),
168
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs),
169
+ kind: z.literal("spatial"),
170
+ }),
171
+ z.strictObject({
172
+ basis: z.enum(["declared", "perceptual"]),
173
+ kind: z.literal("match-cut"),
174
+ }),
175
+ ]);
176
+
177
+ export const CinemaAnchorSchema = z.discriminatedUnion("kind", [
178
+ z.strictObject({ kind: z.literal("sequence"), offsetUs: MicrosecondsSchema }),
179
+ z.strictObject({
180
+ kind: z.literal("shot"),
181
+ offsetUs: SignedMicrosecondsSchema,
182
+ shotId: CinemaShotIdSchema,
183
+ }),
184
+ ]);
185
+
186
+ export const CinemaMarkerSchema = z.strictObject({
187
+ anchor: CinemaAnchorSchema,
188
+ label: z.string().min(1).max(512),
189
+ markerId: CinemaMarkerIdSchema,
190
+ });
191
+
192
+ export const CINEMA_BEAT_KINDS = [
193
+ "act-turn",
194
+ "climax",
195
+ "inciting-incident",
196
+ "midpoint",
197
+ "resolution",
198
+ "setup",
199
+ ] as const;
200
+
201
+ export const CinemaBeatSchema = z.strictObject({
202
+ anchor: CinemaAnchorSchema,
203
+ beatId: CinemaBeatIdSchema,
204
+ kind: z.enum(CINEMA_BEAT_KINDS),
205
+ label: z.string().min(1).max(512),
206
+ });
207
+
208
+ export const CinemaLookSchema = z.strictObject({
209
+ grade: ColorGradeSelectionSchema,
210
+ lookId: CinemaLookIdSchema,
211
+ });
212
+
213
+ export const CINEMA_CUE_ROLES = [
214
+ "atmosphere",
215
+ "dialogue",
216
+ "music",
217
+ "sfx",
218
+ ] as const;
219
+
220
+ export const CinemaAudioCueSchema = z.strictObject({
221
+ assetId: ProjectAssetIdSchema,
222
+ assetRange: SourceIntervalSchema,
223
+ at: CinemaAnchorSchema,
224
+ cueId: CinemaCueIdSchema,
225
+ fadeInUs: MicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs).default(0),
226
+ fadeOutUs: MicrosecondsSchema.max(CINEMA_LIMITS.transitionDurationUs).default(0),
227
+ gainDb: z.number().finite().min(-60).max(24).default(0),
228
+ role: z.enum(CINEMA_CUE_ROLES),
229
+ streamId: ProjectStreamIdSchema,
230
+ }).superRefine((cue, context) => {
231
+ const durationUs = cue.assetRange.endUs - cue.assetRange.startUs;
232
+ if (cue.fadeInUs + cue.fadeOutUs > durationUs) {
233
+ context.addIssue({
234
+ code: "custom",
235
+ message: "Audio cue fades cannot exceed the cue duration.",
236
+ });
237
+ }
238
+ });
239
+
240
+ export const CinemaOutputSchema = z.strictObject({
241
+ background: z.string().regex(/^#[a-fA-F0-9]{6}(?:[a-fA-F0-9]{2})?$/u).default("#000000ff"),
242
+ frameRate: z.number().finite().positive().max(240).default(24),
243
+ pixelHeight: Yuv420pDimensionSchema.default(1_080),
244
+ pixelWidth: Yuv420pDimensionSchema.default(1_920),
245
+ }).superRefine((output, context) => {
246
+ if (output.pixelWidth * output.pixelHeight > 134_217_728) {
247
+ context.addIssue({ code: "custom", message: "Cinema output exceeds the 128-megapixel safety limit." });
248
+ }
249
+ });
250
+
251
+ /**
252
+ * Content-addressed cinematic sequencing sidecar. It binds one exact project
253
+ * structure digest plus one exact project edit-plan digest, and owns shot
254
+ * order, handles, markers, beats, transitions, looks, and audio cues without
255
+ * mutating the underlying edit plan.
256
+ */
257
+ export const ProjectCinemaPlanV1Schema = z.strictObject({
258
+ audioCues: z.array(CinemaAudioCueSchema).max(CINEMA_LIMITS.audioCues).default([]),
259
+ beats: z.array(CinemaBeatSchema).max(CINEMA_LIMITS.beats).default([]),
260
+ cinemaPlanSha256: Sha256Schema,
261
+ createdAt: IsoTimestampSchema,
262
+ kind: z.literal("slopcamera.project-cinema-plan"),
263
+ looks: z.array(CinemaLookSchema).max(CINEMA_LIMITS.looks).default([]),
264
+ markers: z.array(CinemaMarkerSchema).max(CINEMA_LIMITS.markers).default([]),
265
+ output: CinemaOutputSchema.default({
266
+ background: "#000000ff",
267
+ frameRate: 24,
268
+ pixelHeight: 1_080,
269
+ pixelWidth: 1_920,
270
+ }),
271
+ planId: CinemaPlanIdSchema,
272
+ projectEditPlanSha256: Sha256Schema,
273
+ projectId: VideoProjectIdSchema,
274
+ projectStructureSha256: Sha256Schema,
275
+ schemaVersion: z.literal(1),
276
+ shots: z.array(CinemaShotSchema).min(1).max(CINEMA_LIMITS.shots),
277
+ transitions: z.array(CinemaTransitionSchema).max(CINEMA_LIMITS.shots - 1).default([]),
278
+ updatedAt: IsoTimestampSchema,
279
+ }).superRefine((plan, context) => {
280
+ if (Date.parse(plan.updatedAt) < Date.parse(plan.createdAt)) {
281
+ context.addIssue({ code: "custom", message: "updatedAt cannot precede createdAt." });
282
+ }
283
+ const shotIds = plan.shots.map(shot => shot.shotId);
284
+ if (new Set<string>(shotIds).size !== shotIds.length) {
285
+ context.addIssue({ code: "custom", message: "Cinema shot IDs must be unique." });
286
+ }
287
+ if (plan.transitions.length !== plan.shots.length - 1) {
288
+ context.addIssue({
289
+ code: "custom",
290
+ message: "Cinema plans require exactly one transition between each adjacent shot pair.",
291
+ });
292
+ }
293
+ const lookIds = plan.looks.map(look => look.lookId);
294
+ if (new Set<string>(lookIds).size !== lookIds.length) {
295
+ context.addIssue({ code: "custom", message: "Cinema look IDs must be unique." });
296
+ }
297
+ const knownLooks = new Set<string>(lookIds);
298
+ for (const shot of plan.shots) {
299
+ if (shot.lookId !== undefined && !knownLooks.has(shot.lookId)) {
300
+ context.addIssue({ code: "custom", message: `Cinema shot ${shot.shotId} references an unknown look.` });
301
+ }
302
+ }
303
+ const knownShots = new Set<string>(shotIds);
304
+ for (const [label, entries] of [
305
+ ["marker", plan.markers.map(entry => ({ anchor: entry.anchor, id: entry.markerId }))],
306
+ ["beat", plan.beats.map(entry => ({ anchor: entry.anchor, id: entry.beatId }))],
307
+ ["audio cue", plan.audioCues.map(entry => ({ anchor: entry.at, id: entry.cueId }))],
308
+ ] as const) {
309
+ const ids = entries.map(entry => entry.id);
310
+ if (new Set<string>(ids).size !== ids.length) {
311
+ context.addIssue({ code: "custom", message: `Cinema ${label} IDs must be unique.` });
312
+ }
313
+ for (const entry of entries) {
314
+ if (entry.anchor.kind === "shot" && !knownShots.has(entry.anchor.shotId)) {
315
+ context.addIssue({ code: "custom", message: `Cinema ${label} ${entry.id} anchors an unknown shot.` });
316
+ }
317
+ }
318
+ }
319
+ });
320
+
321
+ export type CinemaShotId = z.infer<typeof CinemaShotIdSchema>;
322
+ export type CinemaFit = z.infer<typeof CinemaFitSchema>;
323
+ export type CinemaClipArtifact = ReadonlyInferred<typeof CinemaClipArtifactSchema>;
324
+ export type CinemaShotSource = ReadonlyInferred<typeof CinemaShotSourceSchema>;
325
+ export type CinemaShotContinuity = ReadonlyInferred<typeof CinemaShotContinuitySchema>;
326
+ export type CinemaShot = ReadonlyInferred<typeof CinemaShotSchema>;
327
+ export type CinemaTransition = ReadonlyInferred<typeof CinemaTransitionSchema>;
328
+ export type CinemaAnchor = ReadonlyInferred<typeof CinemaAnchorSchema>;
329
+ export type CinemaMarker = ReadonlyInferred<typeof CinemaMarkerSchema>;
330
+ export type CinemaBeat = ReadonlyInferred<typeof CinemaBeatSchema>;
331
+ export type CinemaLook = ReadonlyInferred<typeof CinemaLookSchema>;
332
+ export type CinemaAudioCue = ReadonlyInferred<typeof CinemaAudioCueSchema>;
333
+ export type CinemaOutput = ReadonlyInferred<typeof CinemaOutputSchema>;
334
+ export type ProjectCinemaPlanV1 = ReadonlyInferred<typeof ProjectCinemaPlanV1Schema>;
335
+
336
+ export function parseProjectCinemaPlanV1(input: unknown): ProjectCinemaPlanV1 {
337
+ return ProjectCinemaPlanV1Schema.parse(input);
338
+ }
339
+
340
+ const CinemaMediaInputBaseShape = {
341
+ bytes: z.number().int().safe().positive(),
342
+ path: RepositoryRelativePathSchema,
343
+ sha256: Sha256Schema,
344
+ streamIndex: z.number().int().safe().nonnegative(),
345
+ } as const;
346
+
347
+ /** One contiguous decoded window consumed from a retained media file. */
348
+ export const CinemaMediaSliceSchema = z.strictObject({
349
+ ...CinemaMediaInputBaseShape,
350
+ /** Half-open window inside the physical file. */
351
+ fileRange: SourceIntervalSchema,
352
+ /** Exact window this slice occupies on the cinema output clock. */
353
+ outputRange: SourceIntervalSchema,
354
+ });
355
+
356
+ export const CinemaResolvedShotMediaSchema = z.strictObject({
357
+ body: z.array(CinemaMediaSliceSchema),
358
+ inHandle: z.array(CinemaMediaSliceSchema),
359
+ outHandle: z.array(CinemaMediaSliceSchema),
360
+ });
361
+
362
+ /** Placeholder colors derive deterministically from the shot identity. */
363
+ export const CinemaShotPlaceholderSchema = z.strictObject({
364
+ color: z.string().regex(/^#[a-f0-9]{6}$/u),
365
+ reason: z.literal("missing-artifact"),
366
+ });
367
+
368
+ export const CinemaResolvedShotSchema = z.strictObject({
369
+ audio: z.strictObject({
370
+ gainDb: z.number().finite().min(-96).max(24),
371
+ media: CinemaResolvedShotMediaSchema,
372
+ pan: z.number().finite().min(-1).max(1),
373
+ }).optional(),
374
+ durationUs: PositiveMicrosecondsSchema,
375
+ fit: CinemaFitSchema,
376
+ handles: z.strictObject({
377
+ postRollUs: MicrosecondsSchema,
378
+ preRollUs: MicrosecondsSchema,
379
+ }),
380
+ lookId: CinemaLookIdSchema.optional(),
381
+ outputRange: SourceIntervalSchema,
382
+ placeholder: CinemaShotPlaceholderSchema.optional(),
383
+ shotId: CinemaShotIdSchema,
384
+ /** Decoded source frame geometry used to apply the declared fit. */
385
+ sourceFrame: z.strictObject({
386
+ pixelHeight: Yuv420pDimensionSchema,
387
+ pixelWidth: Yuv420pDimensionSchema,
388
+ }),
389
+ video: CinemaResolvedShotMediaSchema,
390
+ });
391
+
392
+ export const CinemaResolvedTransitionSchema = z.discriminatedUnion("kind", [
393
+ z.strictObject({ kind: z.literal("cut") }),
394
+ z.strictObject({
395
+ afterShotId: CinemaShotIdSchema,
396
+ beforeShotId: CinemaShotIdSchema,
397
+ durationUs: PositiveMicrosecondsSchema,
398
+ kind: z.literal("dissolve"),
399
+ outputRange: SourceIntervalSchema,
400
+ }),
401
+ z.strictObject({
402
+ afterShotId: CinemaShotIdSchema,
403
+ beforeShotId: CinemaShotIdSchema,
404
+ direction: z.enum(CINEMA_TRANSITION_DIRECTIONS),
405
+ durationUs: PositiveMicrosecondsSchema,
406
+ kind: z.literal("wipe"),
407
+ outputRange: SourceIntervalSchema,
408
+ }),
409
+ z.strictObject({
410
+ afterShotId: CinemaShotIdSchema,
411
+ beforeShotId: CinemaShotIdSchema,
412
+ color: z.string().regex(/^#[a-fA-F0-9]{6}$/u),
413
+ durationUs: PositiveMicrosecondsSchema,
414
+ kind: z.literal("dip-to-color"),
415
+ outputRange: SourceIntervalSchema,
416
+ }),
417
+ z.strictObject({
418
+ afterShotId: CinemaShotIdSchema,
419
+ beforeShotId: CinemaShotIdSchema,
420
+ blurSigma: z.number().finite().min(1).max(64),
421
+ direction: z.enum(CINEMA_TRANSITION_DIRECTIONS),
422
+ durationUs: PositiveMicrosecondsSchema,
423
+ kind: z.literal("whip-pan"),
424
+ outputRange: SourceIntervalSchema,
425
+ }),
426
+ z.strictObject({
427
+ afterShotId: CinemaShotIdSchema,
428
+ beforeShotId: CinemaShotIdSchema,
429
+ durationUs: PositiveMicrosecondsSchema,
430
+ kind: z.literal("light-flash"),
431
+ outputRange: SourceIntervalSchema,
432
+ peakHoldUs: MicrosecondsSchema,
433
+ }),
434
+ z.strictObject({
435
+ afterShotId: CinemaShotIdSchema,
436
+ beforeShotId: CinemaShotIdSchema,
437
+ clip: CinemaClipArtifactSchema,
438
+ durationUs: PositiveMicrosecondsSchema,
439
+ kind: z.literal("spatial"),
440
+ outputRange: SourceIntervalSchema,
441
+ }),
442
+ z.strictObject({
443
+ afterShotId: CinemaShotIdSchema,
444
+ basis: z.enum(["declared", "perceptual"]),
445
+ beforeShotId: CinemaShotIdSchema,
446
+ kind: z.literal("match-cut"),
447
+ }),
448
+ ]);
449
+
450
+ export const CinemaResolvedAudioCueSchema = z.strictObject({
451
+ ...CinemaMediaInputBaseShape,
452
+ assetId: ProjectAssetIdSchema,
453
+ cueId: CinemaCueIdSchema,
454
+ fadeInUs: MicrosecondsSchema,
455
+ fadeOutUs: MicrosecondsSchema,
456
+ fileRange: SourceIntervalSchema,
457
+ gainDb: z.number().finite().min(-60).max(24),
458
+ outputRange: SourceIntervalSchema,
459
+ role: z.enum(CINEMA_CUE_ROLES),
460
+ });
461
+
462
+ export const CINEMA_ADVISORY_CODES = [
463
+ "match-cut-declared",
464
+ "match-cut-perceptual-unverified",
465
+ "shot-placeholder",
466
+ ] as const;
467
+
468
+ export const CinemaAdvisorySchema = z.strictObject({
469
+ code: z.enum(CINEMA_ADVISORY_CODES),
470
+ cueId: CinemaCueIdSchema.optional(),
471
+ message: z.string().min(1).max(2_048),
472
+ shotId: CinemaShotIdSchema.optional(),
473
+ transitionIndex: z.number().int().safe().nonnegative().optional(),
474
+ });
475
+
476
+ /**
477
+ * Fully resolved cinema render plan: every consumed media window, transition
478
+ * recipe, audio cue, and the exact output clock are bound by content digests.
479
+ */
480
+ export const CinemaRenderPlanV1Schema = z.strictObject({
481
+ advisories: z.array(CinemaAdvisorySchema).max(4_096),
482
+ audioCues: z.array(CinemaResolvedAudioCueSchema).max(CINEMA_LIMITS.audioCues),
483
+ cinemaPlanSha256: Sha256Schema,
484
+ kind: z.literal("slopcamera.cinema-render-plan"),
485
+ looks: z.array(CinemaLookSchema).max(CINEMA_LIMITS.looks),
486
+ output: z.strictObject({
487
+ background: z.string().regex(/^#[a-fA-F0-9]{6}(?:[a-fA-F0-9]{2})?$/u),
488
+ durationUs: PositiveMicrosecondsSchema.max(CINEMA_LIMITS.outputDurationUs),
489
+ frameRate: z.number().finite().positive().max(240),
490
+ pixelHeight: Yuv420pDimensionSchema,
491
+ pixelWidth: Yuv420pDimensionSchema,
492
+ }).superRefine((output, context) => {
493
+ if (output.pixelWidth * output.pixelHeight > 134_217_728) {
494
+ context.addIssue({ code: "custom", message: "Cinema render output exceeds the 128-megapixel safety limit." });
495
+ }
496
+ }),
497
+ planSha256: Sha256Schema,
498
+ projectEditPlanSha256: Sha256Schema,
499
+ projectId: VideoProjectIdSchema,
500
+ projectStructureSha256: Sha256Schema,
501
+ schemaVersion: z.literal(1),
502
+ shots: z.array(CinemaResolvedShotSchema).min(1).max(CINEMA_LIMITS.shots),
503
+ transitions: z.array(CinemaResolvedTransitionSchema).max(CINEMA_LIMITS.shots - 1),
504
+ }).superRefine((plan, context) => {
505
+ const shotIds = plan.shots.map(shot => shot.shotId);
506
+ if (new Set<string>(shotIds).size !== shotIds.length) {
507
+ context.addIssue({ code: "custom", message: "Resolved cinema shot IDs must be unique." });
508
+ }
509
+ if (plan.transitions.length !== plan.shots.length - 1) {
510
+ context.addIssue({ code: "custom", message: "Resolved cinema transitions must cover every adjacent shot pair." });
511
+ }
512
+ let expectedStartUs = 0;
513
+ for (const [index, shot] of plan.shots.entries()) {
514
+ if (
515
+ shot.outputRange.startUs !== expectedStartUs
516
+ || shot.outputRange.endUs - shot.outputRange.startUs !== shot.durationUs
517
+ ) {
518
+ context.addIssue({ code: "custom", message: "Resolved cinema shots must tile the output clock contiguously." });
519
+ break;
520
+ }
521
+ expectedStartUs = shot.outputRange.endUs;
522
+ const transition = plan.transitions[index];
523
+ if (transition !== undefined && transition.kind !== "cut" && transition.kind !== "match-cut") {
524
+ if (
525
+ transition.outputRange.startUs !== expectedStartUs
526
+ || transition.durationUs !== transition.outputRange.endUs - transition.outputRange.startUs
527
+ ) {
528
+ context.addIssue({ code: "custom", message: "Resolved cinema transitions must tile the output clock contiguously." });
529
+ break;
530
+ }
531
+ expectedStartUs = transition.outputRange.endUs;
532
+ }
533
+ }
534
+ if (expectedStartUs !== plan.output.durationUs) {
535
+ context.addIssue({ code: "custom", message: "Resolved cinema output duration must equal its tiled segments." });
536
+ }
537
+ const integrityByPath = new Map<string, { readonly bytes: number; readonly sha256: string }>();
538
+ const sliceCount = plan.shots.reduce((total, shot) => (
539
+ total
540
+ + shot.video.body.length + shot.video.inHandle.length + shot.video.outHandle.length
541
+ + (shot.audio === undefined
542
+ ? 0
543
+ : shot.audio.media.body.length
544
+ + shot.audio.media.inHandle.length
545
+ + shot.audio.media.outHandle.length)
546
+ ), 0) + plan.audioCues.length;
547
+ if (sliceCount > CINEMA_LIMITS.mediaSlices) {
548
+ context.addIssue({ code: "custom", message: "Resolved cinema media slice budget exceeded." });
549
+ }
550
+ for (const slice of [
551
+ ...plan.shots.flatMap(shot => [
552
+ ...shot.video.body,
553
+ ...shot.video.inHandle,
554
+ ...shot.video.outHandle,
555
+ ...(shot.audio === undefined ? [] : [
556
+ ...shot.audio.media.body,
557
+ ...shot.audio.media.inHandle,
558
+ ...shot.audio.media.outHandle,
559
+ ]),
560
+ ]),
561
+ ...plan.audioCues,
562
+ ]) {
563
+ if (slice.outputRange.endUs > plan.output.durationUs) {
564
+ context.addIssue({ code: "custom", message: "Resolved cinema slice exceeds the output duration." });
565
+ break;
566
+ }
567
+ const prior = integrityByPath.get(slice.path);
568
+ if (prior !== undefined && (prior.bytes !== slice.bytes || prior.sha256 !== slice.sha256)) {
569
+ context.addIssue({ code: "custom", message: "Resolved cinema slices disagree about media integrity." });
570
+ break;
571
+ }
572
+ integrityByPath.set(slice.path, { bytes: slice.bytes, sha256: slice.sha256 });
573
+ }
574
+ });
575
+
576
+ export type CinemaResolvedShotMedia = ReadonlyInferred<typeof CinemaResolvedShotMediaSchema>;
577
+ export type CinemaShotPlaceholder = ReadonlyInferred<typeof CinemaShotPlaceholderSchema>;
578
+ export type CinemaMediaSlice = ReadonlyInferred<typeof CinemaMediaSliceSchema>;
579
+ export type CinemaResolvedShot = ReadonlyInferred<typeof CinemaResolvedShotSchema>;
580
+ export type CinemaResolvedTransition = ReadonlyInferred<typeof CinemaResolvedTransitionSchema>;
581
+ export type CinemaResolvedAudioCue = ReadonlyInferred<typeof CinemaResolvedAudioCueSchema>;
582
+ export type CinemaAdvisory = ReadonlyInferred<typeof CinemaAdvisorySchema>;
583
+ export type CinemaRenderPlanV1 = ReadonlyInferred<typeof CinemaRenderPlanV1Schema>;
584
+
585
+ export function parseCinemaRenderPlanV1(input: unknown): CinemaRenderPlanV1 {
586
+ return CinemaRenderPlanV1Schema.parse(input);
587
+ }
588
+
589
+ export const CinemaRenderInvocationSchema = z.strictObject({
590
+ arguments: z.array(z.string()),
591
+ executable: z.literal("ffmpeg"),
592
+ filterGraph: z.strictObject({
593
+ bytes: z.number().int().safe().positive().max(32 * 1024 * 1024),
594
+ path: RepositoryRelativePathSchema,
595
+ sha256: Sha256Schema,
596
+ }),
597
+ outputPath: RepositoryRelativePathSchema,
598
+ renderPlanSha256: Sha256Schema,
599
+ });
600
+
601
+ export type CinemaRenderInvocation = ReadonlyInferred<typeof CinemaRenderInvocationSchema>;
602
+
603
+ export const CinemaRenderReceiptV1Schema = z.strictObject({
604
+ cinemaPlanSha256: Sha256Schema,
605
+ createdAt: IsoTimestampSchema,
606
+ kind: z.literal("slopcamera.cinema-render-receipt"),
607
+ output: z.strictObject({
608
+ bytes: z.number().int().safe().positive(),
609
+ path: RepositoryRelativePathSchema,
610
+ sha256: Sha256Schema,
611
+ }),
612
+ placeholders: z.array(CinemaShotIdSchema).max(CINEMA_LIMITS.shots),
613
+ plan: z.strictObject({
614
+ path: RepositoryRelativePathSchema,
615
+ sha256: Sha256Schema,
616
+ }),
617
+ projectId: VideoProjectIdSchema,
618
+ schemaVersion: z.literal(1),
619
+ tier: ProjectRenderTierSchema,
620
+ });
621
+
622
+ export type CinemaRenderReceiptV1 = ReadonlyInferred<typeof CinemaRenderReceiptV1Schema>;
623
+
624
+ export const CINEMA_FINDING_SEVERITIES = ["advisory", "warning", "error"] as const;
625
+
626
+ export const CINEMA_FINDING_CODES = [
627
+ "axis-crossing",
628
+ "cue-out-of-range",
629
+ "eyeline-mismatch",
630
+ "exposure-jump",
631
+ "focus-jump",
632
+ "insufficient-handles",
633
+ "jump-cut",
634
+ "match-on-action",
635
+ "missing-media-coverage",
636
+ "output-bounds-exceeded",
637
+ "shot-media-missing",
638
+ "stale-sidecar",
639
+ "transition-unresolved",
640
+ ] as const;
641
+
642
+ export const CinemaContinuityFindingSchema = z.strictObject({
643
+ code: z.enum(CINEMA_FINDING_CODES),
644
+ message: z.string().min(1).max(2_048),
645
+ severity: z.enum(CINEMA_FINDING_SEVERITIES),
646
+ shotIds: z.array(CinemaShotIdSchema).min(1).max(2),
647
+ timeUs: MicrosecondsSchema,
648
+ transitionIndex: z.number().int().safe().nonnegative().optional(),
649
+ });
650
+
651
+ export const CinemaContinuityReportSchema = z.strictObject({
652
+ cinemaPlanSha256: Sha256Schema,
653
+ findings: z.array(CinemaContinuityFindingSchema).max(4_096),
654
+ kind: z.literal("slopcamera.cinema-continuity-report"),
655
+ schemaVersion: z.literal(1),
656
+ });
657
+
658
+ export type CinemaContinuityFinding = ReadonlyInferred<typeof CinemaContinuityFindingSchema>;
659
+ export type CinemaContinuityReport = ReadonlyInferred<typeof CinemaContinuityReportSchema>;
660
+
661
+ export const CINEMA_TEMPORAL_FINDING_CODES = [
662
+ "cue-concurrency",
663
+ "pacing-collapse",
664
+ "transition-overrun",
665
+ "uniform-pacing",
666
+ ] as const;
667
+
668
+ export const CinemaTemporalFindingSchema = z.strictObject({
669
+ code: z.enum(CINEMA_TEMPORAL_FINDING_CODES),
670
+ message: z.string().min(1).max(2_048),
671
+ severity: z.enum(CINEMA_FINDING_SEVERITIES),
672
+ shotIds: z.array(CinemaShotIdSchema).min(1).max(4),
673
+ timeUs: MicrosecondsSchema,
674
+ transitionIndex: z.number().int().safe().nonnegative().optional(),
675
+ });
676
+
677
+ /**
678
+ * Layout-derived temporal metrics for one compiled cinema plan. Durations are
679
+ * integer microseconds; ratios are exact plan arithmetic, never pixel-derived.
680
+ */
681
+ export const CinemaTemporalReportSchema = z.strictObject({
682
+ cinemaPlanSha256: Sha256Schema,
683
+ findings: z.array(CinemaTemporalFindingSchema).max(4_096),
684
+ kind: z.literal("slopcamera.cinema-temporal-report"),
685
+ metrics: z.strictObject({
686
+ audioCueCount: z.number().int().min(0).max(CINEMA_LIMITS.audioCues),
687
+ maxConcurrentCues: z.number().int().min(0).max(CINEMA_LIMITS.audioCues),
688
+ maxShotUs: MicrosecondsSchema,
689
+ medianShotUs: MicrosecondsSchema,
690
+ minShotUs: MicrosecondsSchema,
691
+ shotCount: z.number().int().min(1).max(CINEMA_LIMITS.shots),
692
+ totalDurationUs: MicrosecondsSchema,
693
+ transitionCount: z.number().int().min(0).max(CINEMA_LIMITS.shots),
694
+ transitionOverheadRatio: z.number().finite().min(0).max(1),
695
+ }),
696
+ schemaVersion: z.literal(1),
697
+ });
698
+
699
+ export const CINEMA_GALLERY_AXES = [
700
+ "audio",
701
+ "looks",
702
+ "mixed",
703
+ "pacing",
704
+ "structure",
705
+ "transitions",
706
+ ] as const;
707
+
708
+ export const CinemaGalleryAxisSchema = z.enum(CINEMA_GALLERY_AXES);
709
+
710
+ export const CinemaGalleryCandidateSchema = z.strictObject({
711
+ candidateId: z.string().min(1).max(256),
712
+ cinemaPlanSha256: Sha256Schema,
713
+ label: z.string().min(1).max(256),
714
+ parameter: z.string().min(1).max(128),
715
+ plan: ProjectCinemaPlanV1Schema,
716
+ });
717
+
718
+ /**
719
+ * A bounded gallery of candidate cinema plans. Every candidate re-validates
720
+ * and re-hashes through the canonical composition digest; `selection` stays
721
+ * null because the planner never promotes a candidate.
722
+ */
723
+ export const CinemaGalleryPlanSchema = z.strictObject({
724
+ axis: CinemaGalleryAxisSchema,
725
+ candidates: z.array(CinemaGalleryCandidateSchema).min(1).max(CINEMA_LIMITS.shots),
726
+ cinemaPlanSha256: Sha256Schema,
727
+ kind: z.literal("slopcamera.cinema-gallery-plan"),
728
+ schemaVersion: z.literal(1),
729
+ selection: z.null(),
730
+ });
731
+
732
+ export type CinemaTemporalFinding = ReadonlyInferred<typeof CinemaTemporalFindingSchema>;
733
+ export type CinemaTemporalReport = ReadonlyInferred<typeof CinemaTemporalReportSchema>;
734
+ export type CinemaGalleryAxis = ReadonlyInferred<typeof CinemaGalleryAxisSchema>;
735
+ export type CinemaGalleryCandidate = ReadonlyInferred<typeof CinemaGalleryCandidateSchema>;
736
+ export type CinemaGalleryPlan = ReadonlyInferred<typeof CinemaGalleryPlanSchema>;