@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,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
|
|
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(
|
|
141
|
-
|| !Number.isSafeInteger(
|
|
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(
|
|
145
|
-
|| Math.abs(
|
|
146
|
-
|| !Number.isSafeInteger(
|
|
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:
|
|
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,
|
|
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",
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
293
|
-
"
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
package/apps/desktop/cli/io.ts
CHANGED
|
@@ -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",
|
package/apps/desktop/cli/main.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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] === "
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
}
|