@1agh/maude 0.43.0 → 0.44.0
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/apps/studio/acp/bridge.ts +44 -10
- package/apps/studio/acp/env.ts +23 -0
- package/apps/studio/acp/login-state.ts +263 -0
- package/apps/studio/acp/plugin-bootstrap.ts +31 -25
- package/apps/studio/acp/probe.ts +152 -1
- package/apps/studio/annotations-layer.tsx +396 -46
- package/apps/studio/api.ts +395 -2
- package/apps/studio/bin/_audio-search.mjs +140 -0
- package/apps/studio/bin/_import-asset-pdf-worker.mjs +34 -0
- package/apps/studio/bin/_import-asset.mjs +815 -0
- package/apps/studio/bin/_import-brand.mjs +635 -0
- package/apps/studio/bin/_import-tokens-alias-resolver.mjs +95 -0
- package/apps/studio/bin/_import-tokens.mjs +1201 -0
- package/apps/studio/bin/_transcribe.mjs +419 -0
- package/apps/studio/bin/_transcribe.test.mjs +80 -0
- package/apps/studio/bin/audio-search.sh +28 -0
- package/apps/studio/bin/generate.sh +154 -0
- package/apps/studio/bin/import-asset.sh +34 -0
- package/apps/studio/bin/import-brand.sh +29 -0
- package/apps/studio/bin/import-tokens.sh +33 -0
- package/apps/studio/bin/photo-bg-remove.sh +119 -21
- package/apps/studio/bin/transcribe.sh +39 -0
- package/apps/studio/canvas-edit.ts +197 -2
- package/apps/studio/canvas-lib.tsx +115 -6
- package/apps/studio/client/app.jsx +1954 -685
- package/apps/studio/client/generate-dialog.jsx +352 -0
- package/apps/studio/client/github.js +14 -0
- package/apps/studio/client/inspector-controls.jsx +781 -0
- package/apps/studio/client/panels/BrandUploadPanel.jsx +233 -0
- package/apps/studio/client/panels/ChatPanel.jsx +15 -33
- package/apps/studio/client/panels/IntroVideoDialog.jsx +44 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +12 -0
- package/apps/studio/client/panels/ReadinessList.jsx +227 -7
- package/apps/studio/client/panels/SettingsPanel.jsx +828 -0
- package/apps/studio/client/panels/SetupChecklist.jsx +223 -0
- package/apps/studio/client/photo-knobs.jsx +167 -172
- package/apps/studio/client/styles/3-shell-maude.css +334 -28
- package/apps/studio/client/styles/4-components.css +223 -0
- package/apps/studio/client/styles/6-acp-chat.css +86 -0
- package/apps/studio/client/tour/quick-setup-tour.js +39 -0
- package/apps/studio/commands/annotation-strokes-command.ts +13 -3
- package/apps/studio/config.schema.json +70 -0
- package/apps/studio/context-menu.tsx +11 -2
- package/apps/studio/context.ts +16 -0
- package/apps/studio/design-setup-readiness.ts +115 -0
- package/apps/studio/dist/client.bundle.js +2097 -2088
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/draw/palette.ts +34 -0
- package/apps/studio/footage/schema.test.ts +92 -0
- package/apps/studio/footage/schema.ts +179 -1
- package/apps/studio/generation/adapters/elevenlabs.ts +370 -0
- package/apps/studio/generation/adapters/gemini.ts +542 -0
- package/apps/studio/generation/adapters/groq.test.ts +80 -0
- package/apps/studio/generation/adapters/groq.ts +247 -0
- package/apps/studio/generation/audio-library.test.ts +95 -0
- package/apps/studio/generation/audio-library.ts +156 -0
- package/apps/studio/generation/captions.test.ts +78 -0
- package/apps/studio/generation/captions.ts +132 -0
- package/apps/studio/generation/download.ts +76 -0
- package/apps/studio/generation/elevenlabs.test.ts +223 -0
- package/apps/studio/generation/gemini.test.ts +350 -0
- package/apps/studio/generation/jobs.test.ts +128 -0
- package/apps/studio/generation/jobs.ts +243 -0
- package/apps/studio/generation/keys.test.ts +75 -0
- package/apps/studio/generation/keys.ts +162 -0
- package/apps/studio/generation/prefs.test.ts +95 -0
- package/apps/studio/generation/prefs.ts +82 -0
- package/apps/studio/generation/registry.ts +88 -0
- package/apps/studio/generation/types.test.ts +123 -0
- package/apps/studio/generation/types.ts +266 -0
- package/apps/studio/generation/whisper-models.test.ts +152 -0
- package/apps/studio/generation/whisper-models.ts +257 -0
- package/apps/studio/generation/whisper-spike-results.md +61 -0
- package/apps/studio/git/service.ts +3 -1
- package/apps/studio/http.ts +867 -9
- package/apps/studio/media/intro.mp4 +0 -0
- package/apps/studio/media-commit-chain.ts +119 -0
- package/apps/studio/paths.ts +10 -0
- package/apps/studio/readiness.ts +108 -20
- package/apps/studio/scaffold-design.ts +1145 -0
- package/apps/studio/server.ts +13 -1
- package/apps/studio/test/_helpers.ts +15 -2
- package/apps/studio/test/acp-bridge.test.ts +34 -1
- package/apps/studio/test/acp-env.test.ts +30 -0
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +23 -57
- package/apps/studio/test/acp-session-plugins.test.ts +72 -1
- package/apps/studio/test/annotation-strokes-command.test.ts +17 -0
- package/apps/studio/test/annotations-layer.test.ts +174 -0
- package/apps/studio/test/canvas-origin-gate.test.ts +37 -0
- package/apps/studio/test/csp-canvas-shell.test.ts +11 -0
- package/apps/studio/test/csrf-write-guard.test.ts +23 -1
- package/apps/studio/test/design-setup-readiness.test.ts +155 -0
- package/apps/studio/test/element-structural-edit.test.ts +108 -0
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +13 -0
- package/apps/studio/test/generate-route.test.ts +106 -0
- package/apps/studio/test/import-asset-browser.test.ts +64 -0
- package/apps/studio/test/import-asset-route.test.ts +71 -0
- package/apps/studio/test/import-asset.test.ts +399 -0
- package/apps/studio/test/import-brand-browser.test.ts +88 -0
- package/apps/studio/test/import-brand-route.test.ts +120 -0
- package/apps/studio/test/import-brand.test.ts +206 -0
- package/apps/studio/test/import-tokens.test.ts +722 -0
- package/apps/studio/test/media-commit-chain.test.ts +210 -0
- package/apps/studio/test/photo-bg-remove-validation.test.ts +71 -0
- package/apps/studio/test/readiness.test.ts +13 -11
- package/apps/studio/test/scaffold-design.test.ts +122 -0
- package/apps/studio/test/tour-overlay.test.tsx +16 -0
- package/apps/studio/tool-palette.tsx +11 -6
- package/apps/studio/ui-prefs.ts +130 -0
- package/apps/studio/use-canvas-media-drop.tsx +4 -2
- package/apps/studio/use-chrome-visibility.tsx +8 -2
- package/apps/studio/whats-new.json +96 -0
- package/apps/studio/ws.ts +5 -0
- package/cli/bin/maude.mjs +6 -6
- package/cli/commands/design.mjs +33 -0
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/pkg-root.mjs +107 -0
- package/cli/lib/pkg-root.test.mjs +79 -0
- package/cli/lib/reconstruct-toolset.test.mjs +194 -0
- package/cli/lib/update-check.mjs +8 -0
- package/package.json +12 -10
- package/plugins/design/dependencies.json +18 -0
|
@@ -150,6 +150,40 @@ export function oklchToHex(o: Oklch): string {
|
|
|
150
150
|
return toHex(oklchToRgb(o));
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function srgb8ToLinear(c8: number): number {
|
|
154
|
+
const c = c8 / 255;
|
|
155
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Convert clamped sRGB 0–255 to OKLCH — the exact inverse of {@link oklchToRgb}
|
|
160
|
+
* (same Björn Ottosson constants). Used by DDR-172's token importer to
|
|
161
|
+
* normalize hex/rgb() input colors into a DS's declared `oklch` colorSpace.
|
|
162
|
+
*/
|
|
163
|
+
export function rgbToOklch({ r, g, b }: Rgb): Oklch {
|
|
164
|
+
const rLin = srgb8ToLinear(r);
|
|
165
|
+
const gLin = srgb8ToLinear(g);
|
|
166
|
+
const bLin = srgb8ToLinear(b);
|
|
167
|
+
|
|
168
|
+
const lc = 0.4122214708 * rLin + 0.5363325363 * gLin + 0.0514459929 * bLin;
|
|
169
|
+
const mc = 0.2119034982 * rLin + 0.6806995451 * gLin + 0.1073969566 * bLin;
|
|
170
|
+
const sc = 0.0883024619 * rLin + 0.2817188376 * gLin + 0.6299787005 * bLin;
|
|
171
|
+
|
|
172
|
+
const l_ = Math.cbrt(lc);
|
|
173
|
+
const m_ = Math.cbrt(mc);
|
|
174
|
+
const s_ = Math.cbrt(sc);
|
|
175
|
+
|
|
176
|
+
const l = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
177
|
+
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
178
|
+
const bComp = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
179
|
+
|
|
180
|
+
const c = Math.sqrt(a * a + bComp * bComp);
|
|
181
|
+
let h = (Math.atan2(bComp, a) * 180) / Math.PI;
|
|
182
|
+
if (h < 0) h += 360;
|
|
183
|
+
|
|
184
|
+
return { l, c, h };
|
|
185
|
+
}
|
|
186
|
+
|
|
153
187
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
154
188
|
// Ramp generation
|
|
155
189
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
import { describe, expect, test } from 'bun:test';
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
AI_GENERATED_TAG,
|
|
10
11
|
EDL_VERSION,
|
|
11
12
|
type Edl,
|
|
12
13
|
emptyFootageAnalysis,
|
|
13
14
|
FOOTAGE_ANALYSIS_VERSION,
|
|
14
15
|
type FootageAnalysis,
|
|
16
|
+
generatedClipAnalysis,
|
|
15
17
|
isEmptyAnalysis,
|
|
16
18
|
validateEdl,
|
|
17
19
|
validateFootageAnalysis,
|
|
@@ -192,4 +194,94 @@ describe('validateEdl', () => {
|
|
|
192
194
|
test('rejects a music bed with a non-relative asset', () => {
|
|
193
195
|
expect(validateEdl({ ...GOOD_EDL, music: { asset: 'http://x/y.mp3' } }).ok).toBe(false);
|
|
194
196
|
});
|
|
197
|
+
|
|
198
|
+
// feature-ai-media-generation Phase 2 — layered audio tracks + captions.
|
|
199
|
+
test('accepts layered audioTracks (music/VO/SFX)', () => {
|
|
200
|
+
const r = validateEdl({
|
|
201
|
+
...GOOD_EDL,
|
|
202
|
+
audioTracks: [
|
|
203
|
+
{ asset: 'assets/aaaaaaaa.mp3', kind: 'music', gainDb: -12, fadeOutFrames: 30 },
|
|
204
|
+
{ asset: 'assets/bbbbbbbb.mp3', kind: 'voiceover', startFrame: 15, durationFrames: 90 },
|
|
205
|
+
{ asset: 'assets/cccccccc.mp3', kind: 'sfx', startFrame: 40, name: 'whoosh' },
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
expect(r).toEqual({ ok: true, errors: [] });
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('rejects an audio track with a bad kind / non-relative asset / out-of-range gain', () => {
|
|
212
|
+
expect(
|
|
213
|
+
validateEdl({ ...GOOD_EDL, audioTracks: [{ asset: 'assets/a.mp3', kind: 'bass' }] }).ok
|
|
214
|
+
).toBe(false);
|
|
215
|
+
expect(validateEdl({ ...GOOD_EDL, audioTracks: [{ asset: 'http://x/a.mp3' }] }).ok).toBe(false);
|
|
216
|
+
expect(
|
|
217
|
+
validateEdl({ ...GOOD_EDL, audioTracks: [{ asset: 'assets/a.mp3', gainDb: 999 }] }).ok
|
|
218
|
+
).toBe(false);
|
|
219
|
+
expect(validateEdl({ ...GOOD_EDL, audioTracks: [{ kind: 'music' }] }).ok).toBe(false); // asset required
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('accepts a captions track', () => {
|
|
223
|
+
const r = validateEdl({
|
|
224
|
+
...GOOD_EDL,
|
|
225
|
+
captions: {
|
|
226
|
+
style: 'lower-third',
|
|
227
|
+
cues: [
|
|
228
|
+
{ startSec: 0, endSec: 0.8, text: 'Hello there.' },
|
|
229
|
+
{ startSec: 1.0, endSec: 1.8, text: 'This is Maude.' },
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
expect(r).toEqual({ ok: true, errors: [] });
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('rejects captions with a bad style, missing text, or non-array cues', () => {
|
|
237
|
+
expect(validateEdl({ ...GOOD_EDL, captions: { style: 'sideways', cues: [] } }).ok).toBe(false);
|
|
238
|
+
expect(validateEdl({ ...GOOD_EDL, captions: { cues: [{ startSec: 0, endSec: 1 }] } }).ok).toBe(
|
|
239
|
+
false
|
|
240
|
+
); // text required
|
|
241
|
+
expect(validateEdl({ ...GOOD_EDL, captions: { cues: 'nope' } }).ok).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('rejects an unknown top-level key (audioTracks typo)', () => {
|
|
245
|
+
expect(validateEdl({ ...GOOD_EDL, audioTrack: [] }).ok).toBe(false);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe('generatedClipAnalysis (Task 3.2 — AI-clip provenance stub)', () => {
|
|
250
|
+
test('produces a VALID, empty-shots, ai-generated-tagged analysis', () => {
|
|
251
|
+
const a = generatedClipAnalysis('assets/deadbeef.mp4', {
|
|
252
|
+
provider: 'gemini',
|
|
253
|
+
model: 'veo-3.1-generate-preview',
|
|
254
|
+
prompt: 'a drone push over a mountain lake at dawn',
|
|
255
|
+
});
|
|
256
|
+
expect(validateFootageAnalysis(a)).toEqual({ ok: true, errors: [] });
|
|
257
|
+
expect(a.asset).toBe('assets/deadbeef.mp4');
|
|
258
|
+
// Empty shots ⇒ the reel's shot-aware cache still runs the analyst on it.
|
|
259
|
+
expect(isEmptyAnalysis(a)).toBe(true);
|
|
260
|
+
expect(a.tags).toContain(AI_GENERATED_TAG);
|
|
261
|
+
expect(a.tags).toContain('gemini');
|
|
262
|
+
expect(a.summary).toContain('mountain lake');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('tolerates a missing prompt/provider and stays valid', () => {
|
|
266
|
+
const a = generatedClipAnalysis('assets/abcd1234.webm');
|
|
267
|
+
expect(validateFootageAnalysis(a)).toEqual({ ok: true, errors: [] });
|
|
268
|
+
expect(a.tags).toEqual([AI_GENERATED_TAG]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('maps C0/C1 control chars and caps an oversized prompt under the summary limit', () => {
|
|
272
|
+
const a = generatedClipAnalysis('assets/beadfeed.mp4', {
|
|
273
|
+
provider: 'gemini',
|
|
274
|
+
// C0 newline + C1 NEL (\u0085) must both collapse to a space — no fabricated lines.
|
|
275
|
+
prompt: `injected line\n\u0085next${'x'.repeat(8000)}`,
|
|
276
|
+
});
|
|
277
|
+
expect(validateFootageAnalysis(a).ok).toBe(true);
|
|
278
|
+
expect(a.summary?.length ?? 0).toBeLessThanOrEqual(3901);
|
|
279
|
+
// No C0 (0x00–0x1f) or C1 (0x7f–0x9f) control byte survives into the sidecar.
|
|
280
|
+
expect(
|
|
281
|
+
[...(a.summary ?? '')].some((c) => {
|
|
282
|
+
const n = c.charCodeAt(0);
|
|
283
|
+
return n <= 0x1f || (n >= 0x7f && n <= 0x9f);
|
|
284
|
+
})
|
|
285
|
+
).toBe(false);
|
|
286
|
+
});
|
|
195
287
|
});
|
|
@@ -197,6 +197,52 @@ export interface EdlMusic {
|
|
|
197
197
|
fadeOutFrames?: number;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/** feature-ai-media-generation Phase 2 — the role an audio track plays in the cut. */
|
|
201
|
+
export type EdlAudioKind = 'music' | 'voiceover' | 'sfx';
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* One audio track in the cut (DDR-164 Phase 2). Generalizes the single `music`
|
|
205
|
+
* bed into a LIST so a reel can layer generated music + voiceover + per-beat
|
|
206
|
+
* SFX. `startFrame`/`durationFrames` place + trim it on the OUTPUT timeline (at
|
|
207
|
+
* the Edl's `fps`); omit both for a whole-reel bed. The codegen emits an
|
|
208
|
+
* `<Audio src="assets/…">` inside a `<Sequence from={startFrame}>`.
|
|
209
|
+
*/
|
|
210
|
+
export interface EdlAudioTrack {
|
|
211
|
+
/** `assets/<sha8>.<ext>` audio asset. */
|
|
212
|
+
asset: string;
|
|
213
|
+
/** Role — drives default gain/ducking guidance in the codegen. Default 'music'. */
|
|
214
|
+
kind?: EdlAudioKind;
|
|
215
|
+
/** Where the track enters, OUTPUT frames from the start. Default 0. */
|
|
216
|
+
startFrame?: number;
|
|
217
|
+
/** Trim length in OUTPUT frames. Omit ⇒ play to the end of the source. */
|
|
218
|
+
durationFrames?: number;
|
|
219
|
+
/** Volume adjustment in dB (negative = quieter; e.g. a −12 dB music bed under VO). */
|
|
220
|
+
gainDb?: number;
|
|
221
|
+
fadeInFrames?: number;
|
|
222
|
+
fadeOutFrames?: number;
|
|
223
|
+
/** Stable Timeline identity ("vo", "bed", "whoosh-3"). */
|
|
224
|
+
name?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** One rendered caption cue on the OUTPUT timeline — SECONDS (matches captions.ts). */
|
|
228
|
+
export interface EdlCaptionCue {
|
|
229
|
+
startSec: number;
|
|
230
|
+
endSec: number;
|
|
231
|
+
text: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The caption/subtitle track (DDR-164 Phase 2), fed by `generation/captions.ts`
|
|
236
|
+
* (whisper.cpp local or ElevenLabs Scribe / Groq cloud). Stored as parsed cues
|
|
237
|
+
* (seconds) rather than a raw SRT file so the codegen can render them as overlays
|
|
238
|
+
* without re-parsing; the codegen converts seconds → frames at the Edl's `fps`.
|
|
239
|
+
*/
|
|
240
|
+
export interface EdlCaptions {
|
|
241
|
+
cues: EdlCaptionCue[];
|
|
242
|
+
/** Where the caption sits on screen. Default 'lower-third'. */
|
|
243
|
+
style?: 'lower-third' | 'centered' | 'top';
|
|
244
|
+
}
|
|
245
|
+
|
|
200
246
|
/**
|
|
201
247
|
* The edit decision list — the director's output, one per cut. `beats` is the
|
|
202
248
|
* ordered timeline; the composition's total length = sum(durationFrames) −
|
|
@@ -211,7 +257,12 @@ export interface Edl {
|
|
|
211
257
|
width?: number;
|
|
212
258
|
height?: number;
|
|
213
259
|
beats?: EdlBeat[];
|
|
260
|
+
/** A single whole-reel music bed (kept for backwards compatibility). */
|
|
214
261
|
music?: EdlMusic | null;
|
|
262
|
+
/** feature-ai-media-generation Phase 2 — layered audio (music/VO/SFX). */
|
|
263
|
+
audioTracks?: EdlAudioTrack[];
|
|
264
|
+
/** feature-ai-media-generation Phase 2 — the caption/subtitle track. */
|
|
265
|
+
captions?: EdlCaptions | null;
|
|
215
266
|
}
|
|
216
267
|
|
|
217
268
|
// ── shared helpers ───────────────────────────────────────────────────────────
|
|
@@ -231,6 +282,60 @@ export function emptyFootageAnalysis(asset?: string): FootageAnalysis {
|
|
|
231
282
|
return { version: FOOTAGE_ANALYSIS_VERSION, ...(asset ? { asset } : {}), shots: [] };
|
|
232
283
|
}
|
|
233
284
|
|
|
285
|
+
/** The provenance tag every AI-generated clip's sidecar carries. The
|
|
286
|
+
* footage-analyst preserves it when it enriches the stub with real shots. It is
|
|
287
|
+
* an ADVISORY hint, not a trust boundary — the sidecar is versioned + peer-
|
|
288
|
+
* editable (DDR-054), so a peer can add/strip it; nothing security-relevant keys
|
|
289
|
+
* off it (it only labels a beat in the UI). */
|
|
290
|
+
export const AI_GENERATED_TAG = 'ai-generated';
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* A provenance `FootageAnalysis` stub for an AI-GENERATED clip (DDR-164 Phase 3,
|
|
294
|
+
* Task 3.2). Written by the `/_api/generate-jobs` route the moment a video job
|
|
295
|
+
* lands, so the generated clip is immediately KNOWN to the footage pipeline —
|
|
296
|
+
* `getAnalysis` returns non-null and the reel director can see the clip is
|
|
297
|
+
* synthetic (the `ai-generated` tag) and read what it was generated FOR (the
|
|
298
|
+
* prompt, as the summary) without re-watching.
|
|
299
|
+
*
|
|
300
|
+
* It carries NO shots on purpose: the `footage-analyst` still probes the
|
|
301
|
+
* keyframes and fills the real shot list (so `isEmptyAnalysis` is true and the
|
|
302
|
+
* reel's shot-aware cache re-analyzes it), but the provenance tag + summary
|
|
303
|
+
* survive. Duration/dimensions are unknown until that probe, so they are omitted.
|
|
304
|
+
*/
|
|
305
|
+
export function generatedClipAnalysis(
|
|
306
|
+
asset: string,
|
|
307
|
+
provenance: { provider?: string; model?: string; prompt?: string } = {}
|
|
308
|
+
): FootageAnalysis {
|
|
309
|
+
const tags = [AI_GENERATED_TAG];
|
|
310
|
+
if (provenance.provider) tags.push(provenance.provider);
|
|
311
|
+
const label = [provenance.provider, provenance.model].filter(Boolean).join(' · ');
|
|
312
|
+
// This is the LOCAL generation request's prompt (not a peer file), but its text
|
|
313
|
+
// becomes the sidecar `summary`, which IS versioned + peer-syncable (DDR-054)
|
|
314
|
+
// and read by the footage agents downstream. Map C0/C1 control chars to spaces
|
|
315
|
+
// (matching generation's sanitizeReuseText) so it can't fabricate multi-line
|
|
316
|
+
// instructions, then collapse whitespace; bounded to the summary cap below. The
|
|
317
|
+
// agents' own "treat sidecar text as data" posture is the mitigation for
|
|
318
|
+
// arbitrary PEER-authored summaries — this only cleans what we write.
|
|
319
|
+
const prompt = [...(provenance.prompt ?? '')]
|
|
320
|
+
.map((ch) => {
|
|
321
|
+
const c = ch.charCodeAt(0);
|
|
322
|
+
return (c >= 0x00 && c <= 0x1f) || (c >= 0x7f && c <= 0x9f) ? ' ' : ch;
|
|
323
|
+
})
|
|
324
|
+
.join('')
|
|
325
|
+
.replace(/\s+/g, ' ')
|
|
326
|
+
.trim();
|
|
327
|
+
let summary = `AI-generated clip${label ? ` (${label})` : ''}${prompt ? ` — ${prompt}` : ''}`;
|
|
328
|
+
if (summary.length > 3900) summary = `${summary.slice(0, 3900)}…`;
|
|
329
|
+
return {
|
|
330
|
+
version: FOOTAGE_ANALYSIS_VERSION,
|
|
331
|
+
asset,
|
|
332
|
+
keyframes: 0,
|
|
333
|
+
shots: [],
|
|
334
|
+
summary,
|
|
335
|
+
tags,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
234
339
|
// ── structural validation (dependency-free — no Ajv) ─────────────────────────
|
|
235
340
|
// Mirrors photo/schema.ts: the `/_api/footage` route validates untrusted JSON
|
|
236
341
|
// with these before persisting. Unknown keys / wrong types / out-of-range
|
|
@@ -432,7 +537,7 @@ export function validateEdl(input: unknown): ValidationResult {
|
|
|
432
537
|
assertKeys(
|
|
433
538
|
errors,
|
|
434
539
|
input,
|
|
435
|
-
['version', 'title', 'fps', 'width', 'height', 'beats', 'music'],
|
|
540
|
+
['version', 'title', 'fps', 'width', 'height', 'beats', 'music', 'audioTracks', 'captions'],
|
|
436
541
|
'root'
|
|
437
542
|
);
|
|
438
543
|
if ('version' in input && input.version != null && typeof input.version !== 'number')
|
|
@@ -480,5 +585,78 @@ export function validateEdl(input: unknown): ValidationResult {
|
|
|
480
585
|
}
|
|
481
586
|
}
|
|
482
587
|
|
|
588
|
+
// feature-ai-media-generation Phase 2 — layered audio tracks (music/VO/SFX).
|
|
589
|
+
if ('audioTracks' in input && input.audioTracks != null) {
|
|
590
|
+
if (!Array.isArray(input.audioTracks)) errors.push('audioTracks: must be an array');
|
|
591
|
+
else if (input.audioTracks.length > 64) errors.push('audioTracks: more than 64 items');
|
|
592
|
+
else
|
|
593
|
+
input.audioTracks.forEach((raw, i) => {
|
|
594
|
+
const where = `audioTracks[${i}]`;
|
|
595
|
+
if (!isPlainObject(raw)) {
|
|
596
|
+
errors.push(`${where}: must be an object`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
assertKeys(
|
|
600
|
+
errors,
|
|
601
|
+
raw,
|
|
602
|
+
[
|
|
603
|
+
'asset',
|
|
604
|
+
'kind',
|
|
605
|
+
'startFrame',
|
|
606
|
+
'durationFrames',
|
|
607
|
+
'gainDb',
|
|
608
|
+
'fadeInFrames',
|
|
609
|
+
'fadeOutFrames',
|
|
610
|
+
'name',
|
|
611
|
+
],
|
|
612
|
+
where
|
|
613
|
+
);
|
|
614
|
+
assetRel(errors, raw, 'asset', where);
|
|
615
|
+
if (!('asset' in raw) || raw.asset == null) errors.push(`${where}.asset: required`);
|
|
616
|
+
if (
|
|
617
|
+
'kind' in raw &&
|
|
618
|
+
raw.kind != null &&
|
|
619
|
+
!['music', 'voiceover', 'sfx'].includes(raw.kind as string)
|
|
620
|
+
)
|
|
621
|
+
errors.push(`${where}.kind: must be music|voiceover|sfx`);
|
|
622
|
+
intGe(errors, raw, 'startFrame', 0, where);
|
|
623
|
+
intGe(errors, raw, 'durationFrames', 1, where);
|
|
624
|
+
num(errors, raw, 'gainDb', -60, 24, where);
|
|
625
|
+
intGe(errors, raw, 'fadeInFrames', 0, where);
|
|
626
|
+
intGe(errors, raw, 'fadeOutFrames', 0, where);
|
|
627
|
+
str(errors, raw, 'name', 120, where);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// feature-ai-media-generation Phase 2 — the caption/subtitle track.
|
|
632
|
+
if ('captions' in input && input.captions != null) {
|
|
633
|
+
const c = input.captions;
|
|
634
|
+
if (!isPlainObject(c)) errors.push('captions: must be an object');
|
|
635
|
+
else {
|
|
636
|
+
assertKeys(errors, c, ['cues', 'style'], 'captions');
|
|
637
|
+
if (
|
|
638
|
+
'style' in c &&
|
|
639
|
+
c.style != null &&
|
|
640
|
+
!['lower-third', 'centered', 'top'].includes(c.style as string)
|
|
641
|
+
)
|
|
642
|
+
errors.push('captions.style: must be lower-third|centered|top');
|
|
643
|
+
if (!('cues' in c) || !Array.isArray(c.cues)) errors.push('captions.cues: must be an array');
|
|
644
|
+
else if (c.cues.length > 5000) errors.push('captions.cues: more than 5000 items');
|
|
645
|
+
else
|
|
646
|
+
c.cues.forEach((raw, i) => {
|
|
647
|
+
const where = `captions.cues[${i}]`;
|
|
648
|
+
if (!isPlainObject(raw)) {
|
|
649
|
+
errors.push(`${where}: must be an object`);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
assertKeys(errors, raw, ['startSec', 'endSec', 'text'], where);
|
|
653
|
+
num(errors, raw, 'startSec', 0, 24 * 3600, where);
|
|
654
|
+
num(errors, raw, 'endSec', 0, 24 * 3600, where);
|
|
655
|
+
str(errors, raw, 'text', 2000, where);
|
|
656
|
+
if (!('text' in raw) || raw.text == null) errors.push(`${where}.text: required`);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
483
661
|
return { ok: errors.length === 0, errors };
|
|
484
662
|
}
|