@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
|
@@ -89,7 +89,7 @@ function validatePcm(pcm: MonoPcm): void {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function fftPowerSpectrum(samples: readonly number[]): Float64Array {
|
|
92
|
+
export function fftPowerSpectrum(samples: readonly number[]): Float64Array {
|
|
93
93
|
let size = 1;
|
|
94
94
|
while (size < samples.length) size *= 2;
|
|
95
95
|
const real = new Float64Array(size);
|
|
@@ -145,6 +145,129 @@ function fftPowerSpectrum(samples: readonly number[]): Float64Array {
|
|
|
145
145
|
return power;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export const AUDIO_REACTIVITY_PROFILE = "bands-v1" as const;
|
|
149
|
+
export const AUDIO_REACTIVITY_SAMPLE_RATE_HZ = 24_000;
|
|
150
|
+
export const AUDIO_REACTIVITY_RATE_HZ = 60;
|
|
151
|
+
export const AUDIO_REACTIVITY_MAX_DURATION_US = 600_000_000;
|
|
152
|
+
|
|
153
|
+
const AUDIO_REACTIVITY_WINDOW_SAMPLES = 2_048;
|
|
154
|
+
const AUDIO_REACTIVITY_HOP_SAMPLES = AUDIO_REACTIVITY_SAMPLE_RATE_HZ / AUDIO_REACTIVITY_RATE_HZ;
|
|
155
|
+
const AUDIO_REACTIVITY_MAXIMUM_SAMPLES = AUDIO_REACTIVITY_MAX_DURATION_US * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / 1_000_000;
|
|
156
|
+
const AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE = 16;
|
|
157
|
+
// Suppress quiet input below -80 dBFS RMS, and band leakage below 2% of the
|
|
158
|
+
// simultaneous broadband RMS. Subtracting the floor avoids a hard onset step.
|
|
159
|
+
const AUDIO_REACTIVITY_ABSOLUTE_RMS_FLOOR = 0.0001;
|
|
160
|
+
const AUDIO_REACTIVITY_LEAKAGE_RMS_RATIO = 0.02;
|
|
161
|
+
const AUDIO_REACTIVITY_FLAT_RANGE_RATIO = 0.001;
|
|
162
|
+
const AUDIO_REACTIVITY_ATTACK_SECONDS = 0.03;
|
|
163
|
+
const AUDIO_REACTIVITY_RELEASE_SECONDS = 0.18;
|
|
164
|
+
// The FFT already applies a symmetric Hann window and divides power by N².
|
|
165
|
+
// Correct its mean squared gain when recovering one-sided RMS energy.
|
|
166
|
+
const AUDIO_REACTIVITY_HANN_POWER_GAIN = 3 * (AUDIO_REACTIVITY_WINDOW_SAMPLES - 1) / (8 * AUDIO_REACTIVITY_WINDOW_SAMPLES);
|
|
167
|
+
|
|
168
|
+
export interface AudioReactivityBands {
|
|
169
|
+
readonly bass: readonly number[];
|
|
170
|
+
readonly mid: readonly number[];
|
|
171
|
+
readonly treble: readonly number[];
|
|
172
|
+
readonly energy: readonly number[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
type MutableAudioReactivityBands = { [Band in keyof AudioReactivityBands]: number[] };
|
|
176
|
+
const AUDIO_REACTIVITY_BAND_NAMES = ["bass", "mid", "treble", "energy"] as const;
|
|
177
|
+
|
|
178
|
+
function audioReactivityPercentile(ordered: readonly number[], fraction: number): number {
|
|
179
|
+
const position = (ordered.length - 1) * fraction;
|
|
180
|
+
const left = Math.floor(position);
|
|
181
|
+
const a = ordered[left]!;
|
|
182
|
+
return a + ((ordered[Math.min(left + 1, ordered.length - 1)] ?? a) - a) * (position - left);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeAndSmoothAudioReactivity(values: readonly number[], energy: readonly number[], band: boolean): readonly number[] {
|
|
186
|
+
const gated = values.map((value, index) => Math.max(0, value - Math.max(
|
|
187
|
+
AUDIO_REACTIVITY_ABSOLUTE_RMS_FLOOR,
|
|
188
|
+
band ? energy[index]! * AUDIO_REACTIVITY_LEAKAGE_RMS_RATIO : 0,
|
|
189
|
+
)));
|
|
190
|
+
const ordered = [...gated].sort((left, right) => left - right);
|
|
191
|
+
let low = audioReactivityPercentile(ordered, 0.05);
|
|
192
|
+
let high = audioReactivityPercentile(ordered, 0.95);
|
|
193
|
+
// Events occupying less than 5% of the track still have a useful range.
|
|
194
|
+
if (high <= EPSILON) high = ordered.at(-1)!;
|
|
195
|
+
// A sustained tone has energy even when its useful percentile span is flat.
|
|
196
|
+
if (high - low <= Math.max(EPSILON, high * AUDIO_REACTIVITY_FLAT_RANGE_RATIO)) low = 0;
|
|
197
|
+
const span = high - low;
|
|
198
|
+
const attack = 1 - Math.exp(-1 / (AUDIO_REACTIVITY_RATE_HZ * AUDIO_REACTIVITY_ATTACK_SECONDS));
|
|
199
|
+
const release = 1 - Math.exp(-1 / (AUDIO_REACTIVITY_RATE_HZ * AUDIO_REACTIVITY_RELEASE_SECONDS));
|
|
200
|
+
let previous = 0;
|
|
201
|
+
const result = gated.map(value => {
|
|
202
|
+
const target = span <= EPSILON ? 0 : Math.min(1, Math.max(0, (value - low) / span));
|
|
203
|
+
previous += (target - previous) * (target > previous ? attack : release);
|
|
204
|
+
return Math.min(1, Math.max(0, previous));
|
|
205
|
+
});
|
|
206
|
+
// This endpoint is at the exact PCM duration, not the next regular grid tick.
|
|
207
|
+
result.push(0);
|
|
208
|
+
return Object.freeze(result);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Analyze already-decoded mono PCM on a fixed absolute timeline. Each band has
|
|
213
|
+
* ceil(sampleCount / 400) samples at i / 60 seconds, then a zero endpoint at
|
|
214
|
+
* durationUs. Windows are centered with zero padding; their 42.7 ms lookahead
|
|
215
|
+
* and offline normalization are intentional. This detects energy, not beats.
|
|
216
|
+
*/
|
|
217
|
+
export function analyzeAudioReactivity(pcm: {
|
|
218
|
+
readonly sampleRateHz: number;
|
|
219
|
+
readonly samples: readonly number[] | Float32Array;
|
|
220
|
+
}): { readonly durationUs: number; readonly bands: AudioReactivityBands } {
|
|
221
|
+
if (pcm.sampleRateHz !== AUDIO_REACTIVITY_SAMPLE_RATE_HZ) {
|
|
222
|
+
throw new TypeError(`Audio reactivity PCM must use exactly ${AUDIO_REACTIVITY_SAMPLE_RATE_HZ} Hz.`);
|
|
223
|
+
}
|
|
224
|
+
if ((!Array.isArray(pcm.samples) && !(pcm.samples instanceof Float32Array))
|
|
225
|
+
|| pcm.samples.length === 0 || pcm.samples.length > AUDIO_REACTIVITY_MAXIMUM_SAMPLES) {
|
|
226
|
+
throw new TypeError(`Audio reactivity PCM must contain 1–${AUDIO_REACTIVITY_MAXIMUM_SAMPLES} samples.`);
|
|
227
|
+
}
|
|
228
|
+
let nonzero = false;
|
|
229
|
+
for (let index = 0; index < pcm.samples.length; index++) {
|
|
230
|
+
const sample = pcm.samples[index]!;
|
|
231
|
+
if (!Number.isFinite(sample) || Math.abs(sample) > AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE) {
|
|
232
|
+
throw new TypeError(`Audio reactivity PCM samples must be finite numbers within ±${AUDIO_REACTIVITY_MAXIMUM_PCM_AMPLITUDE}.`);
|
|
233
|
+
}
|
|
234
|
+
nonzero ||= sample !== 0;
|
|
235
|
+
}
|
|
236
|
+
const durationUs = Math.round(pcm.samples.length * 1_000_000 / AUDIO_REACTIVITY_SAMPLE_RATE_HZ);
|
|
237
|
+
const count = Math.ceil(pcm.samples.length / AUDIO_REACTIVITY_HOP_SAMPLES);
|
|
238
|
+
if (!nonzero) {
|
|
239
|
+
const silence = Object.freeze(Array<number>(count + 1).fill(0));
|
|
240
|
+
return Object.freeze({ durationUs, bands: Object.freeze({ bass: silence, mid: silence, treble: silence, energy: silence }) });
|
|
241
|
+
}
|
|
242
|
+
const raw: MutableAudioReactivityBands = { bass: [], mid: [], treble: [], energy: [] };
|
|
243
|
+
const window = Array<number>(AUDIO_REACTIVITY_WINDOW_SAMPLES).fill(0);
|
|
244
|
+
for (let frame = 0; frame < count; frame++) {
|
|
245
|
+
const start = frame * AUDIO_REACTIVITY_HOP_SAMPLES - AUDIO_REACTIVITY_WINDOW_SAMPLES / 2;
|
|
246
|
+
for (let index = 0; index < AUDIO_REACTIVITY_WINDOW_SAMPLES; index++) {
|
|
247
|
+
const source = start + index;
|
|
248
|
+
window[index] = source < 0 || source >= pcm.samples.length ? 0 : pcm.samples[source]!;
|
|
249
|
+
}
|
|
250
|
+
const spectrum = fftPowerSpectrum(window);
|
|
251
|
+
const power = { bass: 0, mid: 0, treble: 0, energy: 0 };
|
|
252
|
+
for (let bin = 0; bin < spectrum.length; bin++) {
|
|
253
|
+
const frequencyHz = bin * AUDIO_REACTIVITY_SAMPLE_RATE_HZ / AUDIO_REACTIVITY_WINDOW_SAMPLES;
|
|
254
|
+
const value = spectrum[bin]! * (bin === 0 || bin === spectrum.length - 1 ? 1 : 2) / AUDIO_REACTIVITY_HANN_POWER_GAIN;
|
|
255
|
+
power.energy += value;
|
|
256
|
+
if (frequencyHz >= 35 && frequencyHz < 180) power.bass += value;
|
|
257
|
+
else if (frequencyHz >= 180 && frequencyHz < 2_000) power.mid += value;
|
|
258
|
+
else if (frequencyHz >= 2_000 && frequencyHz <= 12_000) power.treble += value;
|
|
259
|
+
}
|
|
260
|
+
for (const name of AUDIO_REACTIVITY_BAND_NAMES) raw[name].push(Math.sqrt(power[name]));
|
|
261
|
+
}
|
|
262
|
+
const bands = Object.freeze({
|
|
263
|
+
bass: normalizeAndSmoothAudioReactivity(raw.bass, raw.energy, true),
|
|
264
|
+
mid: normalizeAndSmoothAudioReactivity(raw.mid, raw.energy, true),
|
|
265
|
+
treble: normalizeAndSmoothAudioReactivity(raw.treble, raw.energy, true),
|
|
266
|
+
energy: normalizeAndSmoothAudioReactivity(raw.energy, raw.energy, false),
|
|
267
|
+
});
|
|
268
|
+
return Object.freeze({ durationUs, bands });
|
|
269
|
+
}
|
|
270
|
+
|
|
148
271
|
/** Fold pitched energy from C2 through B6 into twelve pitch classes. */
|
|
149
272
|
export function computeChroma(frame: readonly number[], sampleRateHz: number): readonly number[] {
|
|
150
273
|
if (!Number.isSafeInteger(sampleRateHz) || sampleRateHz < 8_000 || sampleRateHz > 192_000) {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
FaceAnalysisV1Schema,
|
|
19
19
|
MusicAnalysisV1Schema,
|
|
20
20
|
ProjectInactivityAnalysisV1Schema,
|
|
21
|
+
ProjectCinemaPlanV1Schema,
|
|
21
22
|
ProjectEditPlanV1Schema,
|
|
22
23
|
RecordingManifestV1Schema,
|
|
23
24
|
RepositoryRelativePathSchema,
|
|
@@ -30,6 +31,7 @@ import {
|
|
|
30
31
|
type FaceAnalysisV1,
|
|
31
32
|
type MusicAnalysisV1,
|
|
32
33
|
type ProjectInactivityAnalysisV1,
|
|
34
|
+
type ProjectCinemaPlanV1,
|
|
33
35
|
type ProjectEditPlanV1,
|
|
34
36
|
type RecordingManifestV1,
|
|
35
37
|
type SceneAnalysisV1,
|
|
@@ -41,6 +43,7 @@ import { canonicalJson, sha256Hex } from "./canonical-json";
|
|
|
41
43
|
export const RECORDING_MANIFEST_PATH = "manifest.json";
|
|
42
44
|
export const VIDEO_PROJECT_PATH = "project.json";
|
|
43
45
|
export const CURRENT_PROJECT_EDIT_PLAN_PATH = "edits/current.json";
|
|
46
|
+
export const CURRENT_PROJECT_CINEMA_PLAN_PATH = "cinema/current.json";
|
|
44
47
|
const MAXIMUM_STRUCTURED_FILE_BYTES = 256 * 1024 * 1024;
|
|
45
48
|
const IMMUTABLE_COPY_TEMP_PREFIX = ".slopcamera-copy-";
|
|
46
49
|
|
|
@@ -89,6 +92,7 @@ export interface BundleFileIntegrity {
|
|
|
89
92
|
export type SlopcameraPersistenceDocument =
|
|
90
93
|
| AnalysisArtifact
|
|
91
94
|
| EditPlanV1
|
|
95
|
+
| ProjectCinemaPlanV1
|
|
92
96
|
| ProjectEditPlanV1
|
|
93
97
|
| RecordingManifestV1
|
|
94
98
|
| VideoProjectV1;
|
|
@@ -264,6 +268,77 @@ export async function saveProjectEditRevision(
|
|
|
264
268
|
return path;
|
|
265
269
|
}
|
|
266
270
|
|
|
271
|
+
export function projectCinemaRevisionPath(artifactSha256: string): string {
|
|
272
|
+
const sha256 = Sha256Schema.parse(artifactSha256);
|
|
273
|
+
return RepositoryRelativePathSchema.parse(`cinema/revisions/${sha256}.json`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function projectCinemaRenderPlanPath(planSha256: string): string {
|
|
277
|
+
const sha256 = Sha256Schema.parse(planSha256);
|
|
278
|
+
return RepositoryRelativePathSchema.parse(`cinema/plans/${sha256}.json`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Load the live cinema sidecar, or null when the project has none. Its absence
|
|
283
|
+
* is meaningful: project render planning must stay byte-identical without it.
|
|
284
|
+
*/
|
|
285
|
+
export async function loadProjectCinemaPlan(
|
|
286
|
+
fileSystem: BundleFileSystem,
|
|
287
|
+
path = CURRENT_PROJECT_CINEMA_PLAN_PATH,
|
|
288
|
+
): Promise<ProjectCinemaPlanV1 | null> {
|
|
289
|
+
RepositoryRelativePathSchema.parse(path);
|
|
290
|
+
let text: string;
|
|
291
|
+
try {
|
|
292
|
+
text = await fileSystem.readText(path);
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
const input: unknown = JSON.parse(text);
|
|
298
|
+
return ProjectCinemaPlanV1Schema.parse(input);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function saveProjectCinemaPlan(
|
|
302
|
+
fileSystem: BundleFileSystem,
|
|
303
|
+
plan: ProjectCinemaPlanV1,
|
|
304
|
+
path = CURRENT_PROJECT_CINEMA_PLAN_PATH,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
RepositoryRelativePathSchema.parse(path);
|
|
307
|
+
const parsed = ProjectCinemaPlanV1Schema.parse(canonicalSlopcameraPersistenceDocument(
|
|
308
|
+
ProjectCinemaPlanV1Schema.parse(plan),
|
|
309
|
+
));
|
|
310
|
+
await fileSystem.writeTextAtomic(path, `${canonicalJson(parsed)}\n`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Publish one content-addressed cinema sidecar revision without replacement. */
|
|
314
|
+
export async function saveProjectCinemaRevision(
|
|
315
|
+
fileSystem: BundleFileSystem,
|
|
316
|
+
contents: string,
|
|
317
|
+
expectedArtifactSha256: string,
|
|
318
|
+
): Promise<string> {
|
|
319
|
+
const sha256 = Sha256Schema.parse(expectedArtifactSha256);
|
|
320
|
+
const actualSha256 = sha256Hex(contents);
|
|
321
|
+
if (actualSha256 !== sha256) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Project cinema revision artifact hash mismatch: expected ${sha256}, received ${actualSha256}.`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
if (fileSystem.writeTextNoReplace === undefined) {
|
|
327
|
+
throw new Error("Bundle file system does not support immutable no-replace publication.");
|
|
328
|
+
}
|
|
329
|
+
const path = projectCinemaRevisionPath(sha256);
|
|
330
|
+
const disposition = await fileSystem.writeTextNoReplace(path, contents);
|
|
331
|
+
const installed = await fileSystem.readText(path);
|
|
332
|
+
if (installed !== contents) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
disposition === "exists"
|
|
335
|
+
? `Project cinema revision path already contains different bytes: ${path}`
|
|
336
|
+
: `Published project cinema revision failed read-back verification: ${path}`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return path;
|
|
340
|
+
}
|
|
341
|
+
|
|
267
342
|
export type AnalysisArtifact =
|
|
268
343
|
| AudioAlignmentAnalysisV1
|
|
269
344
|
| FaceAnalysisV1
|