@hraness/slopcamera 3.2.7 → 3.2.8
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 +37 -12
- package/apps/desktop/README.md +14 -130
- package/apps/desktop/application/context.ts +0 -9
- package/apps/desktop/application/default-registry.ts +0 -10
- package/apps/desktop/application/operation.ts +1 -5
- package/apps/desktop/application/operations/index.ts +0 -1
- package/apps/desktop/cli/args.ts +14 -86
- package/apps/desktop/cli/command-host-resources.ts +2 -1
- package/apps/desktop/cli/commands.ts +28 -67
- package/apps/desktop/cli/help.ts +12 -18
- package/apps/desktop/cli/html-scene.ts +139 -28
- package/apps/desktop/cli/io.ts +2 -1
- package/apps/desktop/cli/main.ts +23 -47
- 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 +12 -4
- 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 +0 -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/core/music-analysis.ts +124 -1
- package/apps/desktop/dist/cli/main.js +244 -250
- 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/dist/cli.js +83 -81
- package/dist/{index-rvghzt7b.js → index-7131pg1c.js} +43 -236
- package/dist/{index-mcy8z0br.js → index-fava6pge.js} +1 -1
- package/dist/{index-z7239b4h.js → index-h1k0fnjq.js} +1 -1
- package/dist/{index-p63wavx0.js → index-zfnddgay.js} +1 -1
- package/dist/index.js +4 -18
- package/dist/operations.js +2 -2
- package/dist/vectorize/worker.js +1 -1
- package/dist/workflow.js +3 -3
- package/docs/README.md +1 -1
- package/docs/studio.md +1 -1
- package/package.json +6 -21
- package/skills/slopcamera/SKILL.md +8 -0
- package/skills/slopcamera/references/diagrams.md +5 -14
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/music-video.md +23 -3
- package/skills/slopcamera/references/reference-led-3d.md +1 -1
- package/skills/slopcamera/references/support.md +25 -0
- package/skills/slopcamera/references/web-media-excerpts.md +120 -0
- package/src/cli.ts +28 -98
- package/src/index.ts +0 -23
- 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/src/desktop.ts +0 -253
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportUsefulResult, type UsefulResultObserver } from "../../../src/support-completion";
|
|
1
2
|
import { createDirectingBlobSession } from "./directing-blob";
|
|
2
3
|
import { assembleDirectingClips, extractDirectingEndpoint, importDirectingAnchor } from "./directing-media";
|
|
3
4
|
import { executeDirectingCommand } from "./directing-service";
|
|
@@ -135,6 +136,7 @@ import { commandHelp, completions } from "./help";
|
|
|
135
136
|
import { PlaywrightHtmlOverlayRenderer } from "./html-overlay-renderer";
|
|
136
137
|
import { executeHtmlSceneCommand } from "./html-scene";
|
|
137
138
|
import { BunProcessRunner, processIo, writeJson, writeLine, type CliIo, type ProcessRunner } from "./io";
|
|
139
|
+
import { launchMenubar, manageMenubar, reportOutputsRoot } from "./menubar";
|
|
138
140
|
import {
|
|
139
141
|
codePreparationHostResourceClaims,
|
|
140
142
|
combineHostResourceClaims,
|
|
@@ -175,12 +177,6 @@ import {
|
|
|
175
177
|
type OpenProject,
|
|
176
178
|
} from "./project-service";
|
|
177
179
|
import { resolveVerifiedProjectMedia } from "./project-media-integrity";
|
|
178
|
-
import {
|
|
179
|
-
executeRecordingAction,
|
|
180
|
-
type RecordingController,
|
|
181
|
-
type RecordingSnapshot,
|
|
182
|
-
type RecordingStartOptions,
|
|
183
|
-
} from "./recording-controller";
|
|
184
180
|
import { buildFfmpegInvocation, prepareOverlaySources } from "./renderer";
|
|
185
181
|
import { buildProjectFfmpegInvocation } from "./project-renderer";
|
|
186
182
|
import {
|
|
@@ -301,7 +297,7 @@ import {
|
|
|
301
297
|
workflowRunStore,
|
|
302
298
|
} from "./workflow-runs";
|
|
303
299
|
|
|
304
|
-
export const SLOPCAMERA_VERSION = "3.2.
|
|
300
|
+
export const SLOPCAMERA_VERSION = "3.2.8";
|
|
305
301
|
|
|
306
302
|
// Legacy direct renders predate per-target output contracts. Keep them
|
|
307
303
|
// bounded generously enough for long-form production while preventing one
|
|
@@ -310,6 +306,7 @@ const MAXIMUM_LEGACY_RECORDING_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
|
|
|
310
306
|
const MAXIMUM_LEGACY_PROJECT_RENDER_OUTPUT_BYTES = 32 * 1024 * 1024 * 1024;
|
|
311
307
|
|
|
312
308
|
export interface CliDependencies {
|
|
309
|
+
readonly onUsefulResult?: UsefulResultObserver;
|
|
313
310
|
readonly abortSignal?: AbortSignal;
|
|
314
311
|
readonly stateRoot?: string;
|
|
315
312
|
readonly clock?: () => number;
|
|
@@ -320,7 +317,6 @@ export interface CliDependencies {
|
|
|
320
317
|
readonly hostResourceCoordinator?: HostResourceCoordinator;
|
|
321
318
|
readonly io?: CliIo;
|
|
322
319
|
readonly paths?: RepositoryPaths;
|
|
323
|
-
readonly recordingController?: RecordingController;
|
|
324
320
|
readonly runner?: ProcessRunner;
|
|
325
321
|
readonly sceneProviderFactory?: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
326
322
|
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
@@ -347,7 +343,6 @@ interface CommandContext {
|
|
|
347
343
|
readonly hostResourceCoordinator: HostResourceCoordinator;
|
|
348
344
|
readonly io: CliIo;
|
|
349
345
|
readonly paths: RepositoryPaths;
|
|
350
|
-
readonly recordingController: RecordingController | undefined;
|
|
351
346
|
readonly runner: ProcessRunner;
|
|
352
347
|
readonly sceneProviderFactory: (options: GatewaySceneProviderOptions) => SceneDescriptionProvider;
|
|
353
348
|
readonly sleep: (milliseconds: number) => Promise<void>;
|
|
@@ -541,7 +536,6 @@ function applicationContext(
|
|
|
541
536
|
context: CommandContext,
|
|
542
537
|
providerOptions?: GatewayProviderOptions,
|
|
543
538
|
): ApplicationContext {
|
|
544
|
-
const recordingController = context.recordingController;
|
|
545
539
|
const toApplicationCapability = (capability: Capability) => ({
|
|
546
540
|
available: capability.available,
|
|
547
541
|
...(capability.command === undefined ? {} : { command: capability.command }),
|
|
@@ -572,21 +566,6 @@ function applicationContext(
|
|
|
572
566
|
: { hostResourceLease: context.hostResourceLease }),
|
|
573
567
|
machineStateRoot: context.stateRoot,
|
|
574
568
|
paths: context.paths,
|
|
575
|
-
...(recordingController === undefined
|
|
576
|
-
? {}
|
|
577
|
-
: {
|
|
578
|
-
recordingController: {
|
|
579
|
-
execute: async (
|
|
580
|
-
action: "pause" | "resume" | "start" | "stop",
|
|
581
|
-
options?: unknown,
|
|
582
|
-
) => await executeRecordingAction(
|
|
583
|
-
recordingController,
|
|
584
|
-
action,
|
|
585
|
-
options as RecordingStartOptions | undefined,
|
|
586
|
-
),
|
|
587
|
-
status: async () => await recordingController.status(),
|
|
588
|
-
},
|
|
589
|
-
}),
|
|
590
569
|
runner: context.runner,
|
|
591
570
|
};
|
|
592
571
|
return {
|
|
@@ -1715,11 +1694,6 @@ async function handleRender(
|
|
|
1715
1694
|
writeValue(context.io, command.json, completed, () => `rendered ${output.outputPath}; plan=${artifactPath}`);
|
|
1716
1695
|
}
|
|
1717
1696
|
|
|
1718
|
-
function snapshotHuman(snapshot: RecordingSnapshot): string {
|
|
1719
|
-
if (snapshot.state === "idle") return "idle";
|
|
1720
|
-
return `${snapshot.state} ${snapshot.recordingId ?? "unknown"} ${humanTime(snapshot.logicalTimeUs)} segments=${snapshot.completedSegmentCount}`;
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
1697
|
async function handleProjectEdit(
|
|
1724
1698
|
context: CommandContext,
|
|
1725
1699
|
command: Extract<CliCommand, { readonly kind: "project-edit" }>,
|
|
@@ -6260,18 +6234,14 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6260
6234
|
for (const completion of completions(command.words)) writeLine(context.io, completion);
|
|
6261
6235
|
return;
|
|
6262
6236
|
case "doctor": {
|
|
6263
|
-
const [capabilities, emoji,
|
|
6237
|
+
const [capabilities, emoji, macVersion] = await Promise.all([
|
|
6264
6238
|
context.capabilities(),
|
|
6265
6239
|
inspectEmojiAssets(context.paths.repositoryRoot),
|
|
6266
|
-
context.recordingController === undefined
|
|
6267
|
-
? Promise.resolve(null)
|
|
6268
|
-
: context.recordingController.status(),
|
|
6269
6240
|
context.io.platform === "darwin"
|
|
6270
6241
|
? context.runner.run(["/usr/bin/sw_vers", "-productVersion"], { maxOutputBytes: 4_096 })
|
|
6271
6242
|
: Promise.resolve({ exitCode: 0, stderr: "", stdout: "not-macos" }),
|
|
6272
6243
|
]);
|
|
6273
6244
|
const output = {
|
|
6274
|
-
activeRecording: active,
|
|
6275
6245
|
artifactRoot: displayPath(context.paths.repositoryRoot, context.paths.artifactRoot),
|
|
6276
6246
|
gatewayCredential: inspectGatewayCredential(context.io.env),
|
|
6277
6247
|
emoji: {
|
|
@@ -6290,7 +6260,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6290
6260
|
`artifacts ${output.artifactRoot}`,
|
|
6291
6261
|
`platform ${output.platform.name} ${output.platform.macOSVersion}`,
|
|
6292
6262
|
...capabilities.map((item) => `${item.name} ${item.available ? item.version ?? item.command : "unavailable"}`),
|
|
6293
|
-
`recording ${active === null ? "controller-unavailable" : snapshotHuman(active)}`,
|
|
6294
6263
|
`emoji ${emoji.provenance} ${emoji.installedCount}/${emoji.catalogCount}; generate: ${emoji.generationCommand}`,
|
|
6295
6264
|
].join("\n"));
|
|
6296
6265
|
return;
|
|
@@ -6326,6 +6295,15 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6326
6295
|
case "ai-transcribe": await handleAiTranscribe(context, command); return;
|
|
6327
6296
|
case "media-audio": await handleMediaAudio(context, command); return;
|
|
6328
6297
|
case "media-color": await handleMediaColor(context, command); return;
|
|
6298
|
+
case "menubar": {
|
|
6299
|
+
if (command.action === "run") await launchMenubar(context.io, context.paths.repositoryRoot, command.json, command.mode);
|
|
6300
|
+
else await manageMenubar(context.io, context.paths.repositoryRoot, command.action, command.json);
|
|
6301
|
+
return;
|
|
6302
|
+
}
|
|
6303
|
+
case "outputs": {
|
|
6304
|
+
await reportOutputsRoot(context.io, context.stateRoot, command.json);
|
|
6305
|
+
return;
|
|
6306
|
+
}
|
|
6329
6307
|
case "recordings-list": {
|
|
6330
6308
|
const directories = (await listRecordingDirectories(context.paths.artifactRoot)).slice(0, command.limit);
|
|
6331
6309
|
const recordings = await Promise.all(directories.map(async ({ id }) => {
|
|
@@ -6509,32 +6487,6 @@ async function dispatch(context: CommandContext, command: CliCommand): Promise<v
|
|
|
6509
6487
|
).join("\n"));
|
|
6510
6488
|
return;
|
|
6511
6489
|
}
|
|
6512
|
-
case "record": {
|
|
6513
|
-
if (context.recordingController === undefined) throw new CliError("unavailable", "Recording controller is unavailable.");
|
|
6514
|
-
const options = command.action === "start"
|
|
6515
|
-
? {
|
|
6516
|
-
camera: !command.webcam
|
|
6517
|
-
? { kind: "disabled" as const }
|
|
6518
|
-
: command.cameraDeviceId === undefined
|
|
6519
|
-
? { kind: "default" as const }
|
|
6520
|
-
: { deviceId: command.cameraDeviceId, kind: "device" as const },
|
|
6521
|
-
displays: command.displays.length === 0
|
|
6522
|
-
? { kind: "all" as const }
|
|
6523
|
-
: { displayIds: command.displays, kind: "selected" as const },
|
|
6524
|
-
microphone: !command.microphone
|
|
6525
|
-
? { kind: "disabled" as const }
|
|
6526
|
-
: command.microphoneDeviceId === undefined
|
|
6527
|
-
? { kind: "default" as const }
|
|
6528
|
-
: { deviceId: command.microphoneDeviceId, kind: "device" as const },
|
|
6529
|
-
strictInputs: command.strictInputs,
|
|
6530
|
-
systemAudio: command.systemAudio,
|
|
6531
|
-
typedText: command.typedText,
|
|
6532
|
-
}
|
|
6533
|
-
: undefined;
|
|
6534
|
-
const snapshot = await executeRecordingAction(context.recordingController, command.action, options);
|
|
6535
|
-
writeValue(context.io, command.json, snapshot, () => snapshotHuman(snapshot));
|
|
6536
|
-
return;
|
|
6537
|
-
}
|
|
6538
6490
|
case "edit": await handleEdit(context, command); return;
|
|
6539
6491
|
case "analyze-inactivity": await handleInactivity(context, command); return;
|
|
6540
6492
|
case "analyze-faces": await handleFaceAnalysis(context, command); return;
|
|
@@ -6662,11 +6614,12 @@ function commandMutationReference(command: CliCommand): MutationReference | unde
|
|
|
6662
6614
|
case "fillers-list":
|
|
6663
6615
|
case "emoji-search":
|
|
6664
6616
|
case "emoji-resolve":
|
|
6617
|
+
case "menubar":
|
|
6618
|
+
case "outputs":
|
|
6665
6619
|
case "complete": return undefined;
|
|
6666
|
-
//
|
|
6667
|
-
// atomic rename
|
|
6668
|
-
case "projects-create":
|
|
6669
|
-
case "record": return undefined;
|
|
6620
|
+
// Fresh projects write outside an existing mutable bundle and publish by
|
|
6621
|
+
// atomic rename.
|
|
6622
|
+
case "projects-create": return undefined;
|
|
6670
6623
|
}
|
|
6671
6624
|
}
|
|
6672
6625
|
|
|
@@ -6808,7 +6761,6 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6808
6761
|
?? createDefaultHostResourceCoordinator(),
|
|
6809
6762
|
io,
|
|
6810
6763
|
paths,
|
|
6811
|
-
recordingController: dependencies.recordingController,
|
|
6812
6764
|
runner,
|
|
6813
6765
|
sceneProviderFactory: dependencies.sceneProviderFactory
|
|
6814
6766
|
?? (options => createGatewaySceneProvider(options)),
|
|
@@ -6828,6 +6780,15 @@ export async function runCli(argv: readonly string[], dependencies: CliDependenc
|
|
|
6828
6780
|
await dispatch(admittedContext, command);
|
|
6829
6781
|
});
|
|
6830
6782
|
});
|
|
6783
|
+
if (command.kind === "projects-create" || command.kind === "project-inspect"
|
|
6784
|
+
|| command.kind === "project-add" || command.kind === "inspect"
|
|
6785
|
+
|| command.kind === "project-edit" || command.kind === "project-camera-edit"
|
|
6786
|
+
|| command.kind === "project-metadata-edit" || command.kind === "project-overlay-edit"
|
|
6787
|
+
|| command.kind === "edit"
|
|
6788
|
+
|| command.kind === "render-run" && !command.dryRun
|
|
6789
|
+
|| command.kind === "project-render" && command.action === "run" && !command.dryRun) {
|
|
6790
|
+
reportUsefulResult(dependencies.onUsefulResult);
|
|
6791
|
+
}
|
|
6831
6792
|
return 0;
|
|
6832
6793
|
} catch (error) {
|
|
6833
6794
|
const failure = asCliError(error);
|
package/apps/desktop/cli/help.ts
CHANGED
|
@@ -22,12 +22,15 @@ Commands:
|
|
|
22
22
|
code init|check|plan|run Author, preflight, and run trusted TypeScript workflows
|
|
23
23
|
runs list|show|resume|approve|cancel
|
|
24
24
|
Inspect and control durable workflow runs
|
|
25
|
-
doctor Check local
|
|
25
|
+
doctor Check local render and asset capabilities
|
|
26
26
|
ai models|image|video|speech|transcribe
|
|
27
27
|
Discover and run Vercel AI Gateway media models
|
|
28
28
|
media audio|color Apply local non-destructive audio and video effects
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
menubar [--background] Run the prebuilt menu-bar companion
|
|
30
|
+
menubar install|uninstall|status Manage its per-user LaunchAgent
|
|
31
|
+
support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
|
|
32
|
+
Optional support; no feature requires payment
|
|
33
|
+
outputs Print the agent outputs directory
|
|
31
34
|
recordings list List recording bundles
|
|
32
35
|
projects list|create List projects or create one from a recording
|
|
33
36
|
project inspect|add|edit|render
|
|
@@ -43,6 +46,10 @@ Commands:
|
|
|
43
46
|
render plan|run <recording> Resolve or execute a render plan
|
|
44
47
|
assets emoji search|resolve Find checked local emoji overlays
|
|
45
48
|
|
|
49
|
+
Agents: after useful work, read slopcamera support protocol --json for the optional
|
|
50
|
+
closeout protocol. Discovery uses stderr; it does not claim an invitation.
|
|
51
|
+
HRANESS_SUPPORT_AUDIENCE=off suppresses offers and incidental discovery.
|
|
52
|
+
|
|
46
53
|
Run slopcamera help <command> for command-specific help.`;
|
|
47
54
|
|
|
48
55
|
const HELP: Readonly<Record<string, string>> = {
|
|
@@ -419,19 +426,6 @@ Apply accepts only candidates with safe acoustic boundaries and current placemen
|
|
|
419
426
|
[--min-duration <time>] [--min-confidence <0..1>] [--limit <1..1000>] [--json]
|
|
420
427
|
|
|
421
428
|
Face track IDs describe local geometry continuity inside one immutable analysis. They do not identify a person.`,
|
|
422
|
-
record: `Usage:
|
|
423
|
-
slopcamera record start [--display <id> ...]
|
|
424
|
-
[--camera-device <id>] [--microphone-device <id>]
|
|
425
|
-
[--webcam <true|false>] [--microphone <true|false>]
|
|
426
|
-
[--system-audio <true|false>] [--typed-text <true|false>]
|
|
427
|
-
[--strict-inputs] [--json]
|
|
428
|
-
slopcamera record pause|resume|stop|status [--json]
|
|
429
|
-
|
|
430
|
-
Capture defaults to every current display, system audio, the default microphone,
|
|
431
|
-
and the default camera. Repeat --display to record an exact non-empty subset.
|
|
432
|
-
Device IDs select the exact camera or microphone reported by slopcamera doctor;
|
|
433
|
-
unknown or duplicate IDs fail closed. Typed-text capture remains opt-in with
|
|
434
|
-
--typed-text true.`,
|
|
435
429
|
recordings: `Usage: slopcamera recordings list [--limit <n>] [--json]`,
|
|
436
430
|
projects: `Usage:
|
|
437
431
|
slopcamera projects list [--limit <n>] [--json]
|
|
@@ -481,10 +475,11 @@ export function commandHelp(topic: readonly string[]): string {
|
|
|
481
475
|
|
|
482
476
|
export function completions(words: readonly string[]): readonly string[] {
|
|
483
477
|
const topLevel = [
|
|
484
|
-
"operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "media", "
|
|
478
|
+
"operations", "diagram", "direct", "studio", "image", "html", "workflows", "code", "runs", "doctor", "ai", "media", "menubar", "support", "outputs", "recordings", "projects", "project", "inspect", "events", "edit", "analyze", "align", "faces", "fillers", "render", "assets",
|
|
485
479
|
];
|
|
486
480
|
if (words.length <= 1) return topLevel;
|
|
487
481
|
const command = words[0];
|
|
482
|
+
if (command === "support") return ["protocol", "offer", "shown", "release", "dismiss", "snooze", "enable", "status"];
|
|
488
483
|
if (command === "html") return ["catalog", "scaffold", "render"];
|
|
489
484
|
if (command === "direct") return ["init", "anchor", "plan", "start", "inspect", "revise", "generate", "resume", "review", "assemble", "cleanup"];
|
|
490
485
|
if (command === "studio") return words[2] === "assets" || words[1] === "assets" ? ["search", "describe", "plan", "import"] : ["init", "bundle", "plan", "probe", "run", "encode", "asset", "assemble", "inspect", "reconcile", "assets"];
|
|
@@ -494,7 +489,6 @@ export function completions(words: readonly string[]): readonly string[] {
|
|
|
494
489
|
if (command === "workflows") return ["list", "show", "plan", "run"];
|
|
495
490
|
if (command === "code") return ["init", "check", "plan", "run"];
|
|
496
491
|
if (command === "runs") return ["list", "show", "resume", "approve", "cancel"];
|
|
497
|
-
if (command === "record") return ["start", "pause", "resume", "stop", "status"];
|
|
498
492
|
if (command === "recordings") return ["list"];
|
|
499
493
|
if (command === "projects") return ["list", "create"];
|
|
500
494
|
if (command === "project") return ["inspect", "add", "edit", "render"];
|
|
@@ -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",
|