@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.
Files changed (188) hide show
  1. package/NOTICE.md +1 -7
  2. package/PRIVACY.md +16 -7
  3. package/README.md +65 -20
  4. package/apps/desktop/README.md +14 -130
  5. package/apps/desktop/application/context.ts +6 -9
  6. package/apps/desktop/application/default-registry.ts +26 -10
  7. package/apps/desktop/application/operation.ts +14 -5
  8. package/apps/desktop/application/operations/index.ts +4 -1
  9. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  10. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  11. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  12. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  13. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  14. package/apps/desktop/application/registry.ts +2 -2
  15. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  16. package/apps/desktop/application/spatial-assets.ts +160 -31
  17. package/apps/desktop/application/spatial-render.ts +217 -20
  18. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  19. package/apps/desktop/application/spatial-review.ts +361 -0
  20. package/apps/desktop/application/spatial-spz.ts +48 -3
  21. package/apps/desktop/application/spatial-world-import.ts +26 -5
  22. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  23. package/apps/desktop/cli/args.ts +528 -97
  24. package/apps/desktop/cli/capability-manifest.ts +374 -0
  25. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  26. package/apps/desktop/cli/command-host-resources.ts +31 -2
  27. package/apps/desktop/cli/commands.ts +1215 -70
  28. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  29. package/apps/desktop/cli/help.ts +183 -23
  30. package/apps/desktop/cli/html-scene.ts +139 -28
  31. package/apps/desktop/cli/io.ts +2 -1
  32. package/apps/desktop/cli/main.ts +26 -50
  33. package/apps/desktop/cli/menubar.ts +244 -0
  34. package/apps/desktop/cli/music-analysis-service.ts +61 -0
  35. package/apps/desktop/cli/portable-surface.ts +18 -5
  36. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  37. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  38. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  39. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  40. package/apps/desktop/cli/workflow-code.ts +1 -1
  41. package/apps/desktop/code/application-node-planner.ts +0 -47
  42. package/apps/desktop/code/host-source-layout.ts +46 -0
  43. package/apps/desktop/code/public.ts +0 -1
  44. package/apps/desktop/code/runtime-identity.ts +50 -8
  45. package/apps/desktop/code/semantic-builder.ts +24 -43
  46. package/apps/desktop/code/source-bundle.ts +23 -6
  47. package/apps/desktop/code/source-typecheck.ts +131 -43
  48. package/apps/desktop/code/worker-client.ts +14 -2
  49. package/apps/desktop/contracts/cinema.ts +736 -0
  50. package/apps/desktop/contracts/index.ts +1 -0
  51. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  52. package/apps/desktop/contracts/spatial-world.ts +26 -3
  53. package/apps/desktop/core/cinema-plan.ts +1320 -0
  54. package/apps/desktop/core/index.ts +1 -0
  55. package/apps/desktop/core/music-analysis.ts +124 -1
  56. package/apps/desktop/core/storage.ts +75 -0
  57. package/apps/desktop/dist/cli/main.js +737 -291
  58. package/apps/desktop/html-overlay/audio-reactivity.ts +127 -0
  59. package/apps/desktop/html-overlay/index.ts +9 -0
  60. package/apps/desktop/html-overlay/runtime.ts +2 -0
  61. package/apps/desktop/html-overlay/scene.ts +29 -0
  62. package/apps/desktop/html-overlay/spatial.ts +619 -57
  63. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  64. package/apps/desktop/workflows/index.ts +6 -0
  65. package/dist/cli.js +32 -434
  66. package/dist/code/advanced.js +1 -269
  67. package/dist/code/index.js +16 -2239
  68. package/dist/generate.js +1 -23
  69. package/dist/host-resources.js +1 -25
  70. package/dist/index-0bbesn4m.js +35 -0
  71. package/dist/index-296qhpf2.js +3 -0
  72. package/dist/index-3cxf2c58.js +3 -0
  73. package/dist/index-77fjfg9f.js +3 -0
  74. package/dist/index-80a36bc0.js +3 -0
  75. package/dist/index-a3vvs3rf.js +3 -0
  76. package/dist/index-b3geqczn.js +5 -0
  77. package/dist/index-cgpkyjqa.js +3 -0
  78. package/dist/index-f0ptv8dm.js +7 -0
  79. package/dist/index-grk47tgs.js +3 -0
  80. package/dist/index-qry58nj2.js +10 -0
  81. package/dist/index.js +1 -240
  82. package/dist/operations.js +1 -28
  83. package/dist/skill-install-gx7afxsd.js +2 -0
  84. package/dist/vectorize/worker.js +1 -141
  85. package/dist/workflow.js +1 -15
  86. package/docs/README.md +5 -1
  87. package/docs/studio.md +1 -1
  88. package/package.json +11 -24
  89. package/skills/slopcamera/SKILL.md +14 -1
  90. package/skills/slopcamera/references/diagrams.md +5 -14
  91. package/skills/slopcamera/references/directed-scenes.md +22 -2
  92. package/skills/slopcamera/references/gateway-media.md +15 -1
  93. package/skills/slopcamera/references/image-galleries.md +166 -0
  94. package/skills/slopcamera/references/install.md +2 -2
  95. package/skills/slopcamera/references/music-video.md +23 -3
  96. package/skills/slopcamera/references/parametric-design.md +14 -0
  97. package/skills/slopcamera/references/reference-led-3d.md +1 -1
  98. package/skills/slopcamera/references/scene-building.md +32 -0
  99. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  100. package/skills/slopcamera/references/support.md +25 -0
  101. package/skills/slopcamera/references/web-media-excerpts.md +120 -0
  102. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  103. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  104. package/src/capability-manifest.ts +361 -0
  105. package/src/cli.ts +250 -102
  106. package/src/code/canonical-json.ts +9 -0
  107. package/src/generate.ts +2 -2
  108. package/src/icon.ts +1111 -0
  109. package/src/image-gallery.ts +1049 -0
  110. package/src/index.ts +11 -23
  111. package/src/mcp/boundary.ts +11 -11
  112. package/src/mcp/index.ts +3 -0
  113. package/src/mcp/server.ts +2 -1
  114. package/src/mcp/tools.ts +1642 -130
  115. package/src/mcp/types.ts +19 -0
  116. package/src/operations.ts +412 -0
  117. package/src/portable-capability-manifest.ts +67 -0
  118. package/src/scene-gallery.ts +184 -0
  119. package/src/spatial-scene/asset-admission.ts +183 -0
  120. package/src/spatial-scene/audit-rendered.ts +692 -0
  121. package/src/spatial-scene/audit.ts +617 -0
  122. package/src/spatial-scene/behavior-audit.ts +252 -0
  123. package/src/spatial-scene/behavior-bake.ts +233 -0
  124. package/src/spatial-scene/behavior-fns.ts +585 -0
  125. package/src/spatial-scene/behavior-gallery.ts +109 -0
  126. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  127. package/src/spatial-scene/behavior-trace.ts +325 -0
  128. package/src/spatial-scene/behavior.ts +618 -0
  129. package/src/spatial-scene/build.ts +632 -0
  130. package/src/spatial-scene/camera-rig.ts +125 -0
  131. package/src/spatial-scene/camera-track.ts +4 -2
  132. package/src/spatial-scene/character.ts +124 -0
  133. package/src/spatial-scene/contracts.ts +75 -7
  134. package/src/spatial-scene/design-templates.ts +351 -0
  135. package/src/spatial-scene/design.ts +327 -0
  136. package/src/spatial-scene/direction-compile.ts +584 -0
  137. package/src/spatial-scene/direction.ts +122 -0
  138. package/src/spatial-scene/effects.ts +149 -0
  139. package/src/spatial-scene/evaluate.ts +109 -29
  140. package/src/spatial-scene/gallery.ts +277 -0
  141. package/src/spatial-scene/generate.ts +239 -0
  142. package/src/spatial-scene/geometry-native.ts +165 -0
  143. package/src/spatial-scene/geometry.ts +1216 -0
  144. package/src/spatial-scene/gltf.ts +600 -77
  145. package/src/spatial-scene/identity.ts +55 -2
  146. package/src/spatial-scene/index.ts +36 -0
  147. package/src/spatial-scene/inspect.ts +35 -6
  148. package/src/spatial-scene/material-lighting.ts +609 -0
  149. package/src/spatial-scene/math.ts +21 -4
  150. package/src/spatial-scene/motion-evidence.ts +88 -0
  151. package/src/spatial-scene/parametric.ts +765 -0
  152. package/src/spatial-scene/particle-preparation.ts +183 -0
  153. package/src/spatial-scene/particle.ts +195 -0
  154. package/src/spatial-scene/patch.ts +109 -15
  155. package/src/spatial-scene/performance.ts +1613 -0
  156. package/src/spatial-scene/probe.ts +137 -0
  157. package/src/spatial-scene/recipe-pack.ts +86 -0
  158. package/src/spatial-scene/render-effects.ts +240 -0
  159. package/src/spatial-scene/review.ts +639 -0
  160. package/src/spatial-scene/simulation-bake.ts +206 -0
  161. package/src/spatial-scene/simulation.ts +187 -0
  162. package/src/spatial-scene/solve.ts +366 -0
  163. package/src/spatial-scene/temporal-audit.ts +354 -0
  164. package/src/support-completion.ts +16 -0
  165. package/src/support.ts +42 -0
  166. package/src/version.ts +1 -1
  167. package/apps/desktop/application/operations/recording/index.ts +0 -5
  168. package/apps/desktop/application/operations/recording/pause.ts +0 -34
  169. package/apps/desktop/application/operations/recording/resume.ts +0 -34
  170. package/apps/desktop/application/operations/recording/shared.ts +0 -199
  171. package/apps/desktop/application/operations/recording/start.ts +0 -169
  172. package/apps/desktop/application/operations/recording/stop.ts +0 -34
  173. package/apps/desktop/capture/protocol.ts +0 -689
  174. package/apps/desktop/cli/capture-bundle.ts +0 -1117
  175. package/apps/desktop/cli/recording-controller.ts +0 -1567
  176. package/apps/desktop/cli/recording-daemon.ts +0 -931
  177. package/dist/index-42zsesc1.js +0 -2257
  178. package/dist/index-7308egqr.js +0 -48
  179. package/dist/index-8txs6fkn.js +0 -712
  180. package/dist/index-mcy8z0br.js +0 -192
  181. package/dist/index-p63wavx0.js +0 -2029
  182. package/dist/index-r7gdhmsp.js +0 -424
  183. package/dist/index-rvghzt7b.js +0 -2013
  184. package/dist/index-sh6xbav6.js +0 -1219
  185. package/dist/index-z1w83f81.js +0 -4
  186. package/dist/index-z7239b4h.js +0 -2363
  187. package/dist/skill-install-dh0ntqba.js +0 -10
  188. package/src/desktop.ts +0 -253
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { constants } from "node:fs";
3
- import { open, readdir } from "node:fs/promises";
3
+ import { lstat, open, readdir } from "node:fs/promises";
4
4
  import { join, relative, resolve } from "node:path";
5
5
  import { z } from "zod";
6
6
  import type { ApplicationContext } from "../application/context";
@@ -28,8 +28,10 @@ import {
28
28
  import { HtmlSceneInputSchema } from "../html-overlay/scene";
29
29
  import { HtmlOverlayActiveLibraryLocksSchema, getApprovedHtmlOverlayLibraryLock } from "../html-overlay/libraries";
30
30
  import { assertHtmlOverlayGpuEvidenceProfile } from "../html-overlay/execution-profile";
31
+ import { HTML_AUDIO_REACTIVITY_MAX_BYTES, HTML_AUDIO_REACTIVITY_RESOURCE_NAME, HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH, HtmlOverlayAudioReactivitySchema } from "../html-overlay/audio-reactivity";
31
32
  import { CliError } from "./errors";
32
33
  import { ingestProjectMedia, SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS } from "./media-ingest";
34
+ import { analyzeRetainedAudioReactivity } from "./music-analysis-service";
33
35
  import { withMutationLock } from "./mutation-lock";
34
36
  import { ensurePhysicalPrivateDirectoryWithin } from "./paths";
35
37
  import { resolveVerifiedProjectMedia } from "./project-media-integrity";
@@ -38,8 +40,10 @@ import { readSpatialJson } from "./spatial-scene-service";
38
40
  const MAX_MEDIA_BYTES = 512 * 1024 * 1024;
39
41
  const MAX_REQUEST_BYTES = 2 * 1024 * 1024;
40
42
  const PROCESS_TIMEOUT_MS = 5 * 60_000;
43
+ const SCENE_MEDIA_LIMIT_MESSAGE = "The lossless HTML scene intermediate may have reached the 512 MiB media bound; reduce canvas, frame rate, or duration and retry.";
44
+ const SCENE_MEDIA_TOO_LARGE_MESSAGE = "Lossless HTML scene intermediate exceeds the 512 MiB media bound; reduce canvas, frame rate, or duration and retry.";
41
45
 
42
- export type HtmlSceneStage = "source" | "browser" | "frames" | "encode" | "verify" | "project" | "complete";
46
+ export type HtmlSceneStage = "source" | "browser" | "audio-analysis" | "frames" | "encode" | "verify" | "project" | "complete";
43
47
  export interface HtmlSceneDependencies {
44
48
  /** Internal controlled-test seams; invocation input cannot replace host capabilities. */
45
49
  readonly bindBrowser?: typeof bindHtmlOverlayBrowserRuntime;
@@ -106,6 +110,9 @@ export async function planHtmlScene(application: ApplicationContext, input: unkn
106
110
  audio: source.request.audio === undefined ? "none" : "explicit-local-import",
107
111
  timingPolicy: "ceil-frames-trim-or-pad-audio-v1",
108
112
  audioValidated: false,
113
+ audioReactivity: source.request.audio?.reactivity === undefined ? null : {
114
+ profile: "bands-v1", validated: false, resourceName: HTML_AUDIO_REACTIVITY_RESOURCE_NAME,
115
+ },
109
116
  };
110
117
  }
111
118
 
@@ -121,6 +128,44 @@ const ProbeSchema = z.object({
121
128
  format: z.object({ duration: z.string() }),
122
129
  });
123
130
 
131
+ function probeTimeUs(text: string | undefined): number {
132
+ return text !== undefined && /^-?\d+(?:\.\d+)?$/u.test(text)
133
+ ? Math.round(Number(text) * 1_000_000) : Number.NaN;
134
+ }
135
+
136
+ /**
137
+ * Verify the lossless RGB intermediate before it is retained or imported.
138
+ * FFmpeg's `-fs` option can stop with a successful exit after writing a
139
+ * playable but truncated MP4. Checking the declared frame count here turns
140
+ * that silent truncation into an actionable bounded-render error.
141
+ */
142
+ export function verifyHtmlSceneIntermediateProbe(input: unknown, expected: {
143
+ readonly width: number; readonly height: number; readonly frameCount: number;
144
+ readonly durationUs: number;
145
+ }) {
146
+ const parsed = ProbeSchema.safeParse(input);
147
+ const videos = parsed.success ? parsed.data.streams.filter(stream => stream.codec_type === "video") : [];
148
+ const audios = parsed.success ? parsed.data.streams.filter(stream => stream.codec_type === "audio") : [];
149
+ const video = videos[0];
150
+ const durationUs = parsed.success ? probeTimeUs(parsed.data.format.duration) : Number.NaN;
151
+ const valid = parsed.success && videos.length === 1 && audios.length === 0
152
+ && video?.codec_name === "h264"
153
+ && video.width === expected.width && video.height === expected.height
154
+ && Number(video.nb_read_frames) === expected.frameCount
155
+ && Number.isSafeInteger(probeTimeUs(video.start_time))
156
+ && Math.abs(probeTimeUs(video.start_time)) <= 1
157
+ && Number.isSafeInteger(probeTimeUs(video.duration))
158
+ && Math.abs(probeTimeUs(video.duration) - expected.durationUs) <= 50_000
159
+ && Number.isSafeInteger(durationUs)
160
+ && Math.abs(durationUs - expected.durationUs) <= 50_000;
161
+ if (!valid) {
162
+ const reported = video?.nb_read_frames === undefined ? "an unknown number of"
163
+ : `${video.nb_read_frames} of ${expected.frameCount}`;
164
+ throw new CliError("invalid-data", `Lossless HTML scene intermediate failed frame verification (reported ${reported} frames). ${SCENE_MEDIA_LIMIT_MESSAGE}`);
165
+ }
166
+ return { frameCount: expected.frameCount, width: video.width, height: video.height, durationUs };
167
+ }
168
+
124
169
  export function verifyHtmlSceneProbe(input: unknown, expected: {
125
170
  readonly width: number; readonly height: number; readonly frameCount: number;
126
171
  readonly durationUs: number; readonly audio: boolean;
@@ -129,26 +174,24 @@ export function verifyHtmlSceneProbe(input: unknown, expected: {
129
174
  const videos = probe.streams.filter(stream => stream.codec_type === "video");
130
175
  const audios = probe.streams.filter(stream => stream.codec_type === "audio");
131
176
  const video = videos[0];
132
- const timeUs = (text: string | undefined): number => text !== undefined && /^-?\d+(?:\.\d+)?$/u.test(text)
133
- ? Math.round(Number(text) * 1_000_000) : Number.NaN;
134
- const durationUs = timeUs(probe.format.duration);
177
+ const durationUs = probeTimeUs(probe.format.duration);
135
178
  if (videos.length !== 1 || video?.codec_name !== "h264"
136
179
  || video.pix_fmt !== "yuv420p" || video.color_range !== "tv" || video.color_space !== "bt709"
137
180
  || video.color_transfer !== "bt709" || video.color_primaries !== "bt709"
138
181
  || video.width !== expected.width || video.height !== expected.height
139
182
  || Number(video.nb_read_frames) !== expected.frameCount
140
- || !Number.isSafeInteger(timeUs(video.start_time)) || Math.abs(timeUs(video.start_time)) > 1
141
- || !Number.isSafeInteger(timeUs(video.duration)) || Math.abs(timeUs(video.duration) - expected.durationUs) > 2
183
+ || !Number.isSafeInteger(probeTimeUs(video.start_time)) || Math.abs(probeTimeUs(video.start_time)) > 1
184
+ || !Number.isSafeInteger(probeTimeUs(video.duration)) || Math.abs(probeTimeUs(video.duration) - expected.durationUs) > 2
142
185
  || !Number.isSafeInteger(durationUs) || Math.abs(durationUs - expected.durationUs) > 50_000
143
186
  || audios.length !== (expected.audio ? 1 : 0)
144
- || audios.some(audio => audio.codec_name !== "aac" || !Number.isSafeInteger(timeUs(audio.start_time))
145
- || Math.abs(timeUs(audio.start_time)) > 30_000
146
- || !Number.isSafeInteger(timeUs(audio.duration)) || Math.abs(timeUs(audio.duration) - expected.durationUs) > 30_000
187
+ || audios.some(audio => audio.codec_name !== "aac" || !Number.isSafeInteger(probeTimeUs(audio.start_time))
188
+ || Math.abs(probeTimeUs(audio.start_time)) > 30_000
189
+ || !Number.isSafeInteger(probeTimeUs(audio.duration)) || Math.abs(probeTimeUs(audio.duration) - expected.durationUs) > 30_000
147
190
  || audio.channels !== 2 || audio.sample_rate !== "48000")) {
148
191
  throw new CliError("invalid-data", "Encoded scene failed dimensions, frame count, duration, color, or audio verification.");
149
192
  }
150
193
  return { frameCount: expected.frameCount, width: video.width, height: video.height, durationUs,
151
- audio: audios.length === 0 ? null : { codec: "aac", channels: 2, sampleRateHz: 48000, startTimeUs: timeUs(audios[0]!.start_time), durationUs: timeUs(audios[0]!.duration) } };
194
+ audio: audios.length === 0 ? null : { codec: "aac", channels: 2, sampleRateHz: 48000, startTimeUs: probeTimeUs(audios[0]!.start_time), durationUs: probeTimeUs(audios[0]!.duration) } };
152
195
  }
153
196
 
154
197
  function placement(asset: ProjectAssetV1, durationUs: number, suffix: string) {
@@ -200,9 +243,9 @@ export async function renderHtmlScene(
200
243
  });
201
244
  return await withMutationLock(jobDirectory, { command: "html render", label: source.request.name }, async lease => {
202
245
  const fence = async () => { await active(); await lease.assertOwned(); };
203
- const retainJson = async (path: string, value: unknown) => {
246
+ const retainJson = async (path: string, value: unknown, maximumBytes = 4 * 1024 * 1024) => {
204
247
  await fs.writeTextNoReplace!(path, `${canonicalJson(value)}\n`, fence);
205
- return await artifact(path, 4 * 1024 * 1024);
248
+ return await artifact(path, maximumBytes);
206
249
  };
207
250
  const run = async (argv: readonly [string, ...string[]]) => {
208
251
  await fence();
@@ -224,6 +267,8 @@ export async function renderHtmlScene(
224
267
  retainedResources.push({ ...resource, ...loaded.artifact, path: relative(application.paths.repositoryRoot, join(jobDirectory, destination)), absolutePath: join(jobDirectory, destination) });
225
268
  }
226
269
  let audio: Awaited<ReturnType<typeof ingestProjectMedia>> | undefined;
270
+ let authoring = source.authoring;
271
+ let audioReactivity: MediaArtifactReference | undefined;
227
272
  if (source.request.audio !== undefined) {
228
273
  audio = await ingestProjectMedia({ ffprobe: tool("ffprobe").command, now: application.clock.now(),
229
274
  projectDirectory: jobDirectory, repositoryRoot: application.paths.repositoryRoot, role: "music",
@@ -235,33 +280,69 @@ export async function renderHtmlScene(
235
280
  || soundtrack.segments[0]!.fileRange.startUs !== 0) {
236
281
  throw new CliError("invalid-data", "A scene soundtrack must begin at the imported media timeline origin. Supply an audio file without a delayed stream start.");
237
282
  }
283
+ if (source.request.audio.reactivity !== undefined) {
284
+ const segment = soundtrack.segments[0]!;
285
+ if (source.request.audio.reactivity.resource === undefined) {
286
+ dependencies.progress?.("audio-analysis");
287
+ const analysis = await analyzeRetainedAudioReactivity({ repositoryRoot: application.paths.repositoryRoot,
288
+ jobDirectory, audio: segment, durationUs: Math.min(source.durationUs, segment.assetRange.endUs),
289
+ streamIndex: segment.streamIndex, ffmpeg: tool("ffmpeg").command, run, fence });
290
+ audioReactivity = await retainJson("source/audio-reactivity.json", analysis, HTML_AUDIO_REACTIVITY_MAX_BYTES);
291
+ const declaration = { name: HTML_AUDIO_REACTIVITY_RESOURCE_NAME, urlPath: HTML_AUDIO_REACTIVITY_RESOURCE_URL_PATH,
292
+ mediaType: "application/json", transport: "fetch" as const };
293
+ retainedResources.push({ ...declaration, ...audioReactivity, absolutePath: join(jobDirectory, "source/audio-reactivity.json") });
294
+ authoring = HtmlOverlayAuthoringInputSchema.parse({ ...authoring,
295
+ resources: [...authoring.resources, { ...declaration, bytes: audioReactivity.bytes, sha256: audioReactivity.sha256 }] });
296
+ } else {
297
+ const retained = source.resources.find(item => item.declaration.name === HTML_AUDIO_REACTIVITY_RESOURCE_NAME);
298
+ if (retained === undefined) throw new CliError("invalid-data", "Audio reactivity source does not declare its retained resource.");
299
+ if (retained.loaded.artifact.bytes > HTML_AUDIO_REACTIVITY_MAX_BYTES) {
300
+ throw new CliError("invalid-data", "Audio reactivity resource exceeds its 4 MiB sidecar bound.");
301
+ }
302
+ if (source.request.audio.reactivity.sha256 !== retained.loaded.artifact.sha256) {
303
+ throw new CliError("conflict", "Audio reactivity source does not match the retained sidecar bytes.");
304
+ }
305
+ let parsed: unknown;
306
+ try { parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(retained.loaded.data)); }
307
+ catch { throw new CliError("invalid-data", "Audio reactivity resource is not valid UTF-8 JSON."); }
308
+ const analysis = HtmlOverlayAudioReactivitySchema.parse(parsed);
309
+ if (analysis.source.bytes !== segment.bytes || analysis.source.sha256 !== segment.sha256) {
310
+ throw new CliError("conflict", "Audio reactivity resource does not match the retained soundtrack.");
311
+ }
312
+ audioReactivity = retainedResources.find(item => item.name === HTML_AUDIO_REACTIVITY_RESOURCE_NAME);
313
+ if (audioReactivity === undefined) throw new CliError("invalid-data", "Audio reactivity resource was not retained.");
314
+ }
315
+ }
238
316
  }
317
+ const retainedAudio = source.request.audio;
239
318
  const retainedInput = HtmlSceneInputSchema.parse({ ...source.request,
240
319
  document: { path: relative(application.paths.repositoryRoot, join(jobDirectory, "source/scene.html")) },
241
320
  resources: retainedResources.map(({ absolutePath: _absolutePath, bytes: _bytes, sha256: _sha256, ...resource }) => resource),
242
- ...(audio === undefined ? {} : { audio: { path: relative(application.paths.repositoryRoot, audio.absolutePath) } }),
321
+ ...(audio === undefined || retainedAudio === undefined ? {} : { audio: { ...retainedAudio, path: relative(application.paths.repositoryRoot, audio.absolutePath),
322
+ ...(retainedAudio.reactivity === undefined ? {} : { reactivity: { profile: "bands-v1", resource: HTML_AUDIO_REACTIVITY_RESOURCE_NAME,
323
+ sha256: audioReactivity?.sha256 ?? retainedAudio.reactivity.sha256 } }) } }),
243
324
  });
244
325
  const retainedSource = await retainJson("source.json", retainedInput);
245
- const authoringReceipt = await retainJson("authoring.json", source.authoring);
326
+ const authoringReceipt = await retainJson("authoring.json", authoring);
246
327
  const intent = await retainJson("intent.json", { kind: "slopcamera.html-scene-intent", schemaVersion: 1,
247
328
  source: retainedSource, document: retainedDocument, authoring: authoringReceipt, tools, frameCount: source.frameCount,
248
329
  durationUs: source.durationUs, requestedDurationUs: source.request.timing.durationUs,
249
330
  timingPolicy: "ceil-frames-trim-or-pad-audio-v1" });
250
331
  dependencies.progress?.("frames");
251
332
  const frameDirectory = await ensurePhysicalPrivateDirectoryWithin(jobDirectory, "render");
252
- const frames = await application.htmlOverlayRenderer!.renderFrames({ authoring: source.authoring,
333
+ const frames = await application.htmlOverlayRenderer!.renderFrames({ authoring,
253
334
  browserRuntime, outputDirectory: frameDirectory, resources: retainedResources,
254
335
  ...(source.request.executionProfile === undefined ? {} : { executionProfile: source.request.executionProfile }) }, signal);
255
336
  await fence();
256
337
  if (frames.frameCount !== source.frameCount || frames.framePattern !== join(frameDirectory, "frames", "frame-%08d.png")) {
257
338
  throw new CliError("invalid-data", "Scene renderer returned an unexpected frame count or output location.");
258
339
  }
259
- const expectedIntegrity = createHtmlOverlayExecutionBundle(source.authoring, browserRuntime, source.request.executionProfile).integrity;
340
+ const expectedIntegrity = createHtmlOverlayExecutionBundle(authoring, browserRuntime, source.request.executionProfile).integrity;
260
341
  if (canonicalJsonSha256(HtmlOverlayExecutionIntegritySchema.parse(frames.executionIntegrity)) !== canonicalJsonSha256(expectedIntegrity)) {
261
342
  throw new CliError("conflict", "Scene renderer returned integrity evidence that differs from the bound browser execution.");
262
343
  }
263
344
  if (canonicalJsonSha256(HtmlOverlayActiveLibraryLocksSchema.parse(frames.libraryLocks))
264
- !== canonicalJsonSha256(source.authoring.libraries.map(getApprovedHtmlOverlayLibraryLock))) {
345
+ !== canonicalJsonSha256(authoring.libraries.map(getApprovedHtmlOverlayLibraryLock))) {
265
346
  throw new CliError("conflict", "Scene renderer returned library locks that differ from the authoring selection.");
266
347
  }
267
348
  assertHtmlOverlayGpuEvidenceProfile(source.request.executionProfile, frames.gpuEvidence);
@@ -289,14 +370,44 @@ export async function renderHtmlScene(
289
370
  const frameRate = String(source.request.timing.fps), duration = (source.durationUs / 1_000_000).toFixed(6);
290
371
  const background = source.request.background.replace("#", "0x");
291
372
  dependencies.progress?.("encode");
292
- await run([tool("ffmpeg").command, "-nostdin", "-v", "error", "-n", "-threads", "2",
293
- "-protocol_whitelist", "file", "-f", "image2", "-framerate", frameRate, "-start_number", "0", "-i", frames.framePattern,
294
- "-filter_complex_threads", "1", "-filter_complex",
295
- `color=c=${background}:s=${source.width}x${source.height}:r=${frameRate},format=rgba[bg];[bg][0:v]overlay=shortest=1:format=auto,format=rgb24[v]`,
296
- "-map", "[v]", "-an", "-frames:v", String(source.frameCount), "-c:v", "libx264rgb", "-crf", "0", "-preset", "fast",
297
- "-threads", "2", "-color_trc", "iec61966-2-1", "-color_primaries", "bt709", "-colorspace", "rgb", "-color_range", "pc",
298
- "-fs", String(MAX_MEDIA_BYTES), videoPath]);
299
- const videoArtifact = await artifact("scene.mp4");
373
+ try {
374
+ await run([tool("ffmpeg").command, "-nostdin", "-v", "error", "-n", "-threads", "2",
375
+ "-protocol_whitelist", "file", "-f", "image2", "-framerate", frameRate, "-start_number", "0", "-i", frames.framePattern,
376
+ "-filter_complex_threads", "1", "-filter_complex",
377
+ `color=c=${background}:s=${source.width}x${source.height}:r=${frameRate},format=rgba[bg];[bg][0:v]overlay=shortest=1:format=auto,format=rgb24[v]`,
378
+ "-map", "[v]", "-an", "-frames:v", String(source.frameCount), "-c:v", "libx264rgb", "-crf", "0", "-preset", "fast",
379
+ "-threads", "2", "-color_trc", "iec61966-2-1", "-color_primaries", "bt709", "-colorspace", "rgb", "-color_range", "pc",
380
+ "-fs", String(MAX_MEDIA_BYTES), videoPath]);
381
+ } catch (error) {
382
+ const details = await lstat(videoPath, { bigint: true }).catch(() => undefined);
383
+ if (details?.isFile() && details.size >= BigInt(MAX_MEDIA_BYTES)) throw new CliError("invalid-data", SCENE_MEDIA_LIMIT_MESSAGE);
384
+ throw error;
385
+ }
386
+ const intermediateProbe = await (async () => {
387
+ try {
388
+ return await run([tool("ffprobe").command, "-v", "error", ...SELF_CONTAINED_MEDIA_INPUT_ARGUMENTS,
389
+ "-count_frames", "-show_entries", "stream=codec_type,codec_name,width,height,nb_read_frames,start_time,duration:format=duration", "-of", "json", videoPath]);
390
+ } catch (error) {
391
+ const details = await lstat(videoPath, { bigint: true }).catch(() => undefined);
392
+ if (details?.isFile() && details.size >= BigInt(MAX_MEDIA_BYTES)) throw new CliError("invalid-data", SCENE_MEDIA_LIMIT_MESSAGE);
393
+ throw error;
394
+ }
395
+ })();
396
+ let intermediateJson: unknown;
397
+ try { intermediateJson = JSON.parse(intermediateProbe.stdout) as unknown; }
398
+ catch { throw new CliError("invalid-data", `Lossless HTML scene intermediate could not be inspected. ${SCENE_MEDIA_LIMIT_MESSAGE}`); }
399
+ verifyHtmlSceneIntermediateProbe(intermediateJson, source);
400
+ const videoArtifact = await (async () => {
401
+ try {
402
+ return await artifact("scene.mp4");
403
+ } catch (error) {
404
+ const details = await lstat(videoPath, { bigint: true }).catch(() => undefined);
405
+ if (details?.isFile() && details.size > BigInt(MAX_MEDIA_BYTES)) {
406
+ throw new CliError("invalid-data", SCENE_MEDIA_TOO_LARGE_MESSAGE);
407
+ }
408
+ throw error;
409
+ }
410
+ })();
300
411
  if (await verifyFrames() !== frameSha256) throw new CliError("conflict", "Scene frame bytes changed during encoding.");
301
412
  const audioStream = audio?.asset.streams.find(stream => stream.kind === "audio");
302
413
  if (audio !== undefined) for (const stream of audio.asset.streams) for (const segment of stream.segments) {
@@ -348,14 +459,14 @@ export async function renderHtmlScene(
348
459
  const receipt = await retainJson("receipt.json", { kind: "slopcamera.html-scene-receipt", schemaVersion: 1,
349
460
  intent, source: retainedSource, document: retainedDocument, authoring: authoringReceipt, frameReceipt, video: videoArtifact, output,
350
461
  projectId, projectSha256: canonicalJsonSha256(project), planSha256: canonicalJsonSha256(plan), verification,
351
- audio: audio?.asset ?? null, timingPolicy: "ceil-frames-trim-or-pad-audio-v1" });
462
+ audio: audio?.asset ?? null, audioReactivity: audioReactivity ?? null, timingPolicy: "ceil-frames-trim-or-pad-audio-v1" });
352
463
  await projectFs.writeTextNoReplace!("html-scene.json", canonicalJson({ source: retainedSource, receipt }), fence);
353
464
  await projectFs.writeTextNoReplace!("edits/current.json", canonicalJson(plan), fence);
354
465
  await projectFs.writeTextNoReplace!("project.json", canonicalJson(project), fence);
355
466
  await fence();
356
467
  dependencies.progress?.("complete");
357
468
  return { kind: "slopcamera.html-scene-export" as const, schemaVersion: 1 as const, output, receipt,
358
- source: retainedSource, projectId, projectPath: relative(application.paths.repositoryRoot, join(projectDirectory, "project.json")), verification };
469
+ source: retainedSource, audioReactivity: audioReactivity ?? null, projectId, projectPath: relative(application.paths.repositoryRoot, join(projectDirectory, "project.json")), verification };
359
470
  });
360
471
  }
361
472
 
@@ -1,3 +1,4 @@
1
+ import { quietSupportChildEnvironment } from "../../../src/support-completion";
1
2
  import { CliError } from "./errors";
2
3
 
3
4
  export interface CliIo {
@@ -167,7 +168,7 @@ export class BunProcessRunner implements ProcessRunner {
167
168
  }
168
169
  const child = Bun.spawn([...argv], {
169
170
  ...(options.cwd === undefined ? {} : { cwd: options.cwd }),
170
- env: environment,
171
+ env: quietSupportChildEnvironment(environment),
171
172
  stdio: [
172
173
  "ignore",
173
174
  "pipe",
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { join, resolve } from "node:path";
3
+ import { runProductSupportCommand, showProductSupportInvitation, standaloneSupportEnvironment } from "../../../src/support";
4
+
4
5
  import { parseCliArgs } from "./args";
5
6
  import { runCli } from "./commands";
6
7
  import { asCliError, CliError, EXIT_CODE } from "./errors";
@@ -11,27 +12,8 @@ import {
11
12
  runPortableSurface,
12
13
  type PortableSurfaceDependencies,
13
14
  } from "./portable-surface";
14
- import { RecordingDaemonClient, runRecordingDaemon } from "./recording-daemon";
15
- import { renamedEnvironmentValue } from "./renamed-environment";
16
- import { rootHelpIntro } from "./root-help-intro";
17
-
18
- function valueAfter(argv: readonly string[], name: string): string {
19
- const index = argv.indexOf(name);
20
- const value = index === -1 ? undefined : argv[index + 1];
21
- if (value === undefined || value.startsWith("--")) throw new CliError("usage", `${name} requires a value.`);
22
- return value;
23
- }
24
15
 
25
- export function daemonCommandFor(executableInput: string, entrypointInput: string): readonly [string, ...string[]] {
26
- const executable = resolve(executableInput);
27
- if (entrypointInput.includes("$bunfs")) return [executable];
28
- const entrypoint = resolve(entrypointInput);
29
- return executable === entrypoint ? [executable] : [executable, entrypoint];
30
- }
31
-
32
- function daemonCommand(): readonly [string, ...string[]] {
33
- return daemonCommandFor(process.execPath, import.meta.path);
34
- }
16
+ import { rootHelpIntro } from "./root-help-intro";
35
17
 
36
18
  export function isEmbeddedVectorizeWorkerInvocation(
37
19
  argv: readonly string[],
@@ -46,6 +28,7 @@ export function isEmbeddedVectorizeWorkerInvocation(
46
28
  export async function main(
47
29
  argv: readonly string[] = process.argv.slice(2),
48
30
  portableDependencies: PortableSurfaceDependencies = {},
31
+ supportEnvironment?: Readonly<Record<string, string | undefined>>,
49
32
  ): Promise<number> {
50
33
  if (isEmbeddedVectorizeWorkerInvocation(argv)) {
51
34
  // The bundled headless supervisor preserves process isolation by spawning
@@ -55,26 +38,15 @@ export async function main(
55
38
  await import("../../../src/vectorize/worker.ts");
56
39
  return typeof process.exitCode === "number" ? process.exitCode : 0;
57
40
  }
58
- if (argv[0] === "__record_daemon") {
59
- if (argv.length !== 5 || argv[1] !== "--artifact-root" || argv[3] !== "--helper") {
60
- throw new CliError("usage", "Invalid internal recording-daemon invocation.");
61
- }
62
- const paths = await resolveRepositoryPaths(process.cwd(), process.env);
63
- const artifactRoot = valueAfter(argv, "--artifact-root");
64
- if (resolve(artifactRoot) !== resolve(paths.artifactRoot)) {
65
- throw new CliError("unsafe-path", "Recording daemon artifact root differs from the repository-owned root.");
66
- }
67
- await runRecordingDaemon({
68
- artifactRoot,
69
- helperExecutable: valueAfter(argv, "--helper"),
70
- });
71
- return 0;
41
+ if (argv[0] === "support") {
42
+ return await runProductSupportCommand(argv.slice(1),
43
+ supportEnvironment === undefined ? {} : { env: supportEnvironment });
72
44
  }
73
45
  const unifiedArgv = canonicalizeUnifiedCliArgs(argv);
74
46
  const portableExitCode = await runPortableSurface(unifiedArgv, portableDependencies);
75
47
  if (portableExitCode !== undefined) return portableExitCode;
76
48
  const earlyCommand = parseCliArgs(unifiedArgv);
77
- if (earlyCommand.kind === "help" || earlyCommand.kind === "version" || earlyCommand.kind === "complete") {
49
+ if (earlyCommand.kind === "help" || earlyCommand.kind === "version" || earlyCommand.kind === "complete" || earlyCommand.kind === "capabilities") {
78
50
  const intro = rootHelpIntro(unifiedArgv, {
79
51
  isTTY: process.stdout.isTTY === true, term: process.env.TERM, columns: process.stdout.columns,
80
52
  });
@@ -82,22 +54,16 @@ export async function main(
82
54
  return await runCli(unifiedArgv, { io: processIo });
83
55
  }
84
56
  const paths = await resolveRepositoryPaths(processIo.cwd(), processIo.env);
85
- const helperExecutable = renamedEnvironmentValue(processIo.env, "SLOPCAMERA_CAPTURE_HELPER")
86
- ?? join(paths.desktopRoot, "capture", "dist", "slopcamera-capture");
87
- const recordingController = new RecordingDaemonClient({
88
- artifactRoot: paths.artifactRoot,
89
- daemonCommand: daemonCommand(),
90
- helperExecutable,
91
- });
92
- if (earlyCommand.kind === "html-render" || earlyCommand.kind === "spatial-world" || earlyCommand.kind === "directing" || earlyCommand.kind === "studio"
93
- || earlyCommand.kind === "spatial-scene" && earlyCommand.action === "camera-track") {
57
+ if (earlyCommand.kind === "html-render" || earlyCommand.kind === "spatial-world" || earlyCommand.kind === "spatial-asset" || earlyCommand.kind === "spatial-design" || earlyCommand.kind === "directing" || earlyCommand.kind === "studio"
58
+ || earlyCommand.kind === "spatial-scene" && (earlyCommand.action === "camera-track" || earlyCommand.action === "render-audit" || earlyCommand.action === "review")) {
94
59
  const controller = new AbortController();
95
60
  const cancel = () => controller.abort(new CliError("cancelled", "Command interrupted."));
96
61
  process.on("SIGINT", cancel);
97
62
  process.on("SIGTERM", cancel);
98
63
  try {
99
- return await runCli(unifiedArgv, { io: processIo, paths, recordingController,
100
- runner: new BunProcessRunner(), abortSignal: controller.signal });
64
+ return await runCli(unifiedArgv, { io: processIo, paths,
65
+ runner: new BunProcessRunner(), abortSignal: controller.signal,
66
+ ...(portableDependencies.onUsefulResult === undefined ? {} : { onUsefulResult: portableDependencies.onUsefulResult }) });
101
67
  } finally {
102
68
  process.off("SIGINT", cancel);
103
69
  process.off("SIGTERM", cancel);
@@ -106,16 +72,26 @@ export async function main(
106
72
  return await runCli(unifiedArgv, {
107
73
  io: processIo,
108
74
  paths,
109
- recordingController,
110
75
  runner: new BunProcessRunner(),
76
+ ...(portableDependencies.onUsefulResult === undefined ? {} : { onUsefulResult: portableDependencies.onUsefulResult }),
111
77
  });
112
78
  }
113
79
 
114
80
  export async function runMainEntrypoint(
115
81
  portableDependencies: PortableSurfaceDependencies = {},
82
+ standalone = false,
116
83
  ): Promise<void> {
117
84
  try {
118
- process.exitCode = await main(process.argv.slice(2), portableDependencies);
85
+ const argv = process.argv.slice(2);
86
+ const env = standalone && !isEmbeddedVectorizeWorkerInvocation(argv)
87
+ ? standaloneSupportEnvironment() : undefined;
88
+ let usefulResult = false;
89
+ process.exitCode = await main(argv, env === undefined ? portableDependencies : {
90
+ ...portableDependencies, onUsefulResult: () => { usefulResult = true; },
91
+ }, env);
92
+ if (env !== undefined && usefulResult && process.exitCode === 0) {
93
+ await showProductSupportInvitation({ env });
94
+ }
119
95
  } catch (error) {
120
96
  const failure = asCliError(error);
121
97
  process.stderr.write(`slopcamera: ${failure.message}\n`);
@@ -124,5 +100,5 @@ export async function runMainEntrypoint(
124
100
  }
125
101
 
126
102
  if (import.meta.main) {
127
- await runMainEntrypoint();
103
+ await runMainEntrypoint({}, true);
128
104
  }