@cueframe/kernel 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/_members/composition/ops/compositionOps.shared.d.ts +1 -1
  2. package/dist/_members/composition/projection/cropProjection.d.ts +2 -1
  3. package/dist/_members/composition-ops/index.d.ts +4 -0
  4. package/dist/_members/composition-ops/materializeCaptions.d.ts +19 -0
  5. package/dist/_members/core/render/compositionRender.d.ts +1 -1
  6. package/dist/_members/core/render/index.d.ts +1 -1
  7. package/dist/_members/core/render/trackProjection.d.ts +24 -0
  8. package/dist/_members/core/render/types.d.ts +1 -0
  9. package/dist/_members/errors/registry.d.ts +18 -0
  10. package/dist/_members/mcp/generated/harness.d.ts +1 -1
  11. package/dist/_members/mcp/server.d.ts +2 -0
  12. package/dist/authoring/build-tools.js +3 -3
  13. package/dist/authoring/context.js +2 -2
  14. package/dist/authoring/tools.js +2 -2
  15. package/dist/authoring.js +6 -6
  16. package/dist/{chunk-S3HHEJMZ.js → chunk-4FKNPTLX.js} +1 -1
  17. package/dist/{chunk-JVWLZNHC.js → chunk-ACURZCPN.js} +1 -1
  18. package/dist/{chunk-46PFSJZE.js → chunk-AOIY7NGF.js} +3059 -3120
  19. package/dist/{chunk-MJWRC7G7.js → chunk-BBNF4QLD.js} +2 -2
  20. package/dist/{chunk-5YO4LAB7.js → chunk-CFIQY4OE.js} +1 -1
  21. package/dist/{chunk-VVKBCBNR.js → chunk-DV7ZFVL4.js} +1 -1
  22. package/dist/{chunk-ULQJWGUV.js → chunk-E63UEO5C.js} +3 -0
  23. package/dist/{chunk-AMFFWX3C.js → chunk-FEG264CF.js} +55 -32
  24. package/dist/{chunk-EDBQPQAG.js → chunk-FGJ7GKWZ.js} +6 -6
  25. package/dist/{chunk-SPF3AXTB.js → chunk-FYS2XUKJ.js} +1 -1
  26. package/dist/{chunk-QISFYLBB.js → chunk-IMIUHG2E.js} +1 -1
  27. package/dist/{chunk-CYILSHK5.js → chunk-JPMVNMF3.js} +1 -1
  28. package/dist/chunk-KCXADPOC.js +1085 -0
  29. package/dist/{chunk-4S2YZQJP.js → chunk-N54EGUVM.js} +1 -1
  30. package/dist/{chunk-HUZN2HRT.js → chunk-OYWX4NEP.js} +48 -9
  31. package/dist/{chunk-ZUJCWFS7.js → chunk-SLUTOUWI.js} +1 -1
  32. package/dist/{chunk-DENF4TPI.js → chunk-T56BK7NS.js} +6 -4
  33. package/dist/{chunk-B4VMU2IJ.js → chunk-TBHSOEYK.js} +1 -1
  34. package/dist/{chunk-POTCIQTA.js → chunk-VEQTURKN.js} +90 -11
  35. package/dist/{chunk-OZNULDQE.js → chunk-XSBVOXSB.js} +1 -1
  36. package/dist/{chunk-URZR6C3N.js → chunk-Z4G3VM3H.js} +1 -1
  37. package/dist/compose.js +1 -1
  38. package/dist/composition/invariants.js +2 -2
  39. package/dist/composition/ops.js +1 -1
  40. package/dist/composition/projection.js +2 -2
  41. package/dist/composition/timeline.js +2 -2
  42. package/dist/composition-ops.js +36 -894
  43. package/dist/composition.js +4 -4
  44. package/dist/{context-KNFL62QR.js → context-YNR73FNI.js} +2 -2
  45. package/dist/core/render.js +7 -5
  46. package/dist/core/select.js +2 -2
  47. package/dist/core.js +5 -5
  48. package/dist/director/ai-sdk.js +3 -3
  49. package/dist/director/domain.js +2 -2
  50. package/dist/director.js +3 -3
  51. package/dist/errors.js +1 -1
  52. package/dist/mcp/adapter.js +3 -3
  53. package/dist/mcp/core.js +1 -1
  54. package/dist/mcp/curation.js +1 -1
  55. package/dist/mcp/scene-shot.js +2 -2
  56. package/dist/mcp/server.js +26 -5
  57. package/dist/mcp.js +28 -9
  58. package/dist/render-harness/cardAssetAdmission.js +2 -2
  59. package/dist/render-harness/cardFonts.js +2 -2
  60. package/dist/render-harness.js +7 -7
  61. package/package.json +11 -11
@@ -1,908 +1,47 @@
1
1
  import {
2
- playableSpans
3
- } from "./chunk-AMFFWX3C.js";
2
+ CaptionMaterializationError,
3
+ CompositionOp,
4
+ GRID_DEFAULT_MAX_MARKERS,
5
+ GRID_HARD_MAX_MARKERS,
6
+ GridError,
7
+ applyCaptionOpToStore,
8
+ applyOp,
9
+ applyOpToStore,
10
+ captionSourceMediaIds,
11
+ collectAlphaLayerClips,
12
+ collectRawComponentClips,
13
+ collectUnbakedComponentClips,
14
+ createRecordingStore,
15
+ invertOp,
16
+ materializeCaptionsFromComposition,
17
+ materializeCompositionCaptions,
18
+ materializeGridMarkers,
19
+ mediaFamily,
20
+ replaceRegion,
21
+ resolveClipKind,
22
+ wrapWithOpRecorder
23
+ } from "./chunk-KCXADPOC.js";
24
+ import "./chunk-FEG264CF.js";
4
25
  import "./chunk-QH3YI3ND.js";
5
- import "./chunk-CYILSHK5.js";
6
- import "./chunk-QISFYLBB.js";
26
+ import "./chunk-JPMVNMF3.js";
27
+ import "./chunk-IMIUHG2E.js";
7
28
  import "./chunk-VZ7TYX6Z.js";
8
- import "./chunk-DENF4TPI.js";
9
- import {
10
- createHeadlessStore,
11
- findClipById,
12
- findMarkerById,
13
- findTrackContainingClipId
14
- } from "./chunk-POTCIQTA.js";
29
+ import "./chunk-T56BK7NS.js";
30
+ import "./chunk-VEQTURKN.js";
15
31
  import "./chunk-AG7HKF2U.js";
16
- import {
17
- CaptionSegment,
18
- CaptionStyle,
19
- Composition,
20
- Format,
21
- Marker,
22
- PerClipEffect,
23
- TimelineClip,
24
- Track,
25
- Transition
26
- } from "./chunk-ID5QY7V6.js";
32
+ import "./chunk-ID5QY7V6.js";
27
33
  import "./chunk-WPZKOVYF.js";
28
- import {
29
- ReframeSegment
30
- } from "./chunk-B7J5UUUV.js";
34
+ import "./chunk-B7J5UUUV.js";
31
35
  import "./chunk-KYRDKK2D.js";
32
36
  import "./chunk-WREYUH2E.js";
33
- import "./chunk-ULQJWGUV.js";
37
+ import "./chunk-E63UEO5C.js";
34
38
  import "./chunk-7PA7FGLD.js";
35
- import {
36
- SCENE_SLOT_MEDIA_FORMATS,
37
- SceneShotSpecV2
38
- } from "./chunk-CL5Y6V6K.js";
39
+ import "./chunk-CL5Y6V6K.js";
39
40
  import "./chunk-2L7PYYJK.js";
40
- import {
41
- introducedCaptionFaceIssues
42
- } from "./chunk-AVTEPAWI.js";
41
+ import "./chunk-AVTEPAWI.js";
43
42
  import "./chunk-2ESYSVXG.js";
44
-
45
- // ../composition-ops/src/index.ts
46
- import { z } from "zod";
47
-
48
- // ../composition-ops/src/opRecorder.ts
49
- var genTrackId = (kind) => `track-${kind}-${crypto.randomUUID().slice(0, 8)}`;
50
- function wrapWithOpRecorder(base, opts) {
51
- const { record, getComposition } = opts;
52
- const captionTarget = {
53
- getState: () => ({ composition: getComposition() }),
54
- setCaptionStyle: (style) => base.setCaptionStyle(style),
55
- setCaptions: (segments, style) => base.setCaptions(segments, style)
56
- };
57
- const wrapped = {
58
- addClip: (clip, options) => {
59
- const before = getComposition();
60
- const c = clip.kind ? clip : { ...clip, kind: "video" };
61
- if (options?.newTrack) {
62
- const newTrackId = options.newTrackId ?? genTrackId("video");
63
- record({ type: "clip.add", clip: c, newTrack: true, newTrackId }, before);
64
- base.addClip(c, { newTrack: true, newTrackId });
65
- } else {
66
- record({ type: "clip.add", clip: c }, before);
67
- base.addClip(c);
68
- }
69
- },
70
- removeClip: (id) => {
71
- record({ type: "clip.remove", clipId: id }, getComposition());
72
- base.removeClip(id);
73
- },
74
- updateClip: (id, updates) => {
75
- record({ type: "clip.update", clipId: id, patch: updates }, getComposition());
76
- base.updateClip(id, updates);
77
- },
78
- removeClipsByMediaId: (mediaId) => {
79
- record({ type: "removeClipsByMediaId", mediaId }, getComposition());
80
- base.removeClipsByMediaId(mediaId);
81
- },
82
- setClipProperties: (id, props) => {
83
- const { playbackRate, ...patch } = props;
84
- if (playbackRate !== void 0) {
85
- if (opts.onUnjournalable) {
86
- const result = base.setClipProperties(id, props);
87
- opts.onUnjournalable("setClipProperties.playbackRate");
88
- return result;
89
- }
90
- throw new Error(
91
- "opRecorder.setClipProperties: playbackRate is not serializable as a clip.update op (source.playbackRate triggers a cross-field ripple). Use the server-authoritative ripple path."
92
- );
93
- }
94
- record({ type: "clip.update", clipId: id, patch }, getComposition());
95
- return base.setClipProperties(id, props);
96
- },
97
- addOverlay: (clip) => {
98
- const before = getComposition();
99
- const track = before.tracks.find((t) => t.kind === "overlay");
100
- let trackId = track?.id;
101
- if (!trackId) {
102
- trackId = genTrackId("overlay");
103
- record({ type: "addTrack", kind: "overlay", id: trackId }, before);
104
- base.addTrack("overlay", void 0, trackId);
105
- }
106
- record({ type: "clip.add", clip, trackId }, getComposition());
107
- base.addOverlay(clip);
108
- },
109
- removeOverlay: (id) => {
110
- record({ type: "clip.remove", clipId: id }, getComposition());
111
- base.removeOverlay(id);
112
- },
113
- updateOverlay: (id, updates) => {
114
- record({ type: "clip.update", clipId: id, patch: updates }, getComposition());
115
- base.updateOverlay(id, updates);
116
- },
117
- loadOverlays: (clips) => {
118
- record({ type: "loadOverlays", clips }, getComposition());
119
- base.loadOverlays(clips);
120
- },
121
- addAudioClip: (clip) => {
122
- const before = getComposition();
123
- const track = before.tracks.find((t) => t.kind === "audio");
124
- let trackId = track?.id;
125
- if (!trackId) {
126
- trackId = genTrackId("audio");
127
- record({ type: "addTrack", kind: "audio", id: trackId }, before);
128
- base.addTrack("audio", void 0, trackId);
129
- }
130
- record({ type: "clip.add", clip, trackId }, getComposition());
131
- base.addAudioClip(clip);
132
- },
133
- removeAudioClip: (id) => {
134
- record({ type: "clip.remove", clipId: id }, getComposition());
135
- base.removeAudioClip(id);
136
- },
137
- updateAudioClip: (id, updates) => {
138
- record({ type: "clip.update", clipId: id, patch: updates }, getComposition());
139
- base.updateAudioClip(id, updates);
140
- },
141
- addMarker: (marker) => {
142
- record({ type: "addMarker", marker }, getComposition());
143
- base.addMarker(marker);
144
- },
145
- removeMarker: (id) => {
146
- record({ type: "removeMarker", markerId: id }, getComposition());
147
- base.removeMarker(id);
148
- },
149
- updateMarker: (id, updates) => {
150
- record({ type: "updateMarker", markerId: id, updates }, getComposition());
151
- base.updateMarker(id, updates);
152
- },
153
- setCropIntents: (clipId, segments) => {
154
- record({ type: "setCropIntents", clipId, segments }, getComposition());
155
- base.setCropIntents(clipId, segments);
156
- },
157
- setEffects: (clipId, effects) => {
158
- record({ type: "setEffects", clipId, effects }, getComposition());
159
- base.setEffects(clipId, effects);
160
- },
161
- // The two caption writes go through the same function `applyOpToStore` uses, so the op this
162
- // journals is exactly the op a replay applies. Calling the store's setter directly left this
163
- // lane — Studio's composition store and the in-app agent capability are built on it — able to
164
- // write a face the op gate refuses and to journal an op `applyOp` would refuse on replay.
165
- // The op is applied FIRST: a refusal must leave nothing in the journal.
166
- setCaptionStyle: (style) => {
167
- const before = getComposition();
168
- const op = { type: "setCaptionStyle", style };
169
- applyCaptionOpToStore(captionTarget, op);
170
- record(op, before);
171
- },
172
- setCaptions: (segments, style) => {
173
- const before = getComposition();
174
- const op = {
175
- type: "setCaptions",
176
- segments,
177
- ...style !== void 0 ? { style } : {}
178
- };
179
- applyCaptionOpToStore(captionTarget, op);
180
- record(op, before);
181
- },
182
- setFormat: (format) => {
183
- record({ type: "setFormat", format }, getComposition());
184
- base.setFormat(format);
185
- },
186
- addTrack: (kind, name, id) => {
187
- const useId = id ?? genTrackId(kind);
188
- record({ type: "addTrack", kind, ...name !== void 0 ? { name } : {}, id: useId }, getComposition());
189
- base.addTrack(kind, name, useId);
190
- },
191
- removeTrack: (trackId) => {
192
- record({ type: "removeTrack", trackId }, getComposition());
193
- base.removeTrack(trackId);
194
- },
195
- updateTrack: (trackId, updates) => {
196
- record({ type: "updateTrack", trackId, updates }, getComposition());
197
- base.updateTrack(trackId, updates);
198
- },
199
- reorderTracks: (fromIndex, toIndex) => {
200
- record({ type: "reorderTracks", fromIndex, toIndex }, getComposition());
201
- base.reorderTracks(fromIndex, toIndex);
202
- },
203
- moveClipToTrack: (clipId, targetTrackId, newStartTime) => {
204
- record(
205
- {
206
- type: "moveClipToTrack",
207
- clipId,
208
- targetTrackId,
209
- ...newStartTime !== void 0 ? { newStartTime } : {}
210
- },
211
- getComposition()
212
- );
213
- base.moveClipToTrack(clipId, targetTrackId, newStartTime);
214
- },
215
- duplicateClip: (id) => {
216
- record({ type: "duplicateClip", clipId: id }, getComposition());
217
- return base.duplicateClip(id);
218
- },
219
- cutAndRipple: (start, end) => {
220
- record({ type: "cutAndRipple", start, end }, getComposition());
221
- base.cutAndRipple(start, end);
222
- },
223
- ripple: (time, amount) => {
224
- record({ type: "ripple", time, amount }, getComposition());
225
- base.ripple(time, amount);
226
- },
227
- addExcludedRange: (clipId, sourceStart, sourceEnd, reason) => {
228
- record(
229
- {
230
- type: "addExcludedRange",
231
- clipId,
232
- sourceStart,
233
- sourceEnd,
234
- ...reason !== void 0 ? { reason } : {}
235
- },
236
- getComposition()
237
- );
238
- base.addExcludedRange(clipId, sourceStart, sourceEnd, reason);
239
- },
240
- removeExcludedRange: (clipId, rangeIdx) => {
241
- record({ type: "removeExcludedRange", clipId, rangeIdx }, getComposition());
242
- base.removeExcludedRange(clipId, rangeIdx);
243
- }
244
- };
245
- return { ...base, ...wrapped };
246
- }
247
-
248
- // ../composition-ops/src/recordingStore.ts
249
- function createRecordingStore(initial) {
250
- const base = createHeadlessStore(initial);
251
- const ops = [];
252
- const wrapped = wrapWithOpRecorder(base, {
253
- record: (op, _before) => ops.push(op),
254
- getComposition: () => base.getState().composition
255
- });
256
- return {
257
- ...wrapped,
258
- getOps: () => [...ops]
259
- };
260
- }
261
-
262
- // ../composition-ops/src/materializeGrid.ts
263
- var GridError = class extends Error {
264
- constructor(code, message, opIndex) {
265
- super(message);
266
- this.code = code;
267
- this.opIndex = opIndex;
268
- this.name = "GridError";
269
- }
270
- code;
271
- opIndex;
272
- };
273
- var GRID_DEFAULT_MAX_MARKERS = 500;
274
- var GRID_HARD_MAX_MARKERS = 2e3;
275
- function materializeGridMarkers(placement, gridPointsMs, opts) {
276
- const { grid, clipId } = opts;
277
- const cap = opts.maxMarkers ?? GRID_DEFAULT_MAX_MARKERS;
278
- const rate = placement.playbackRate ?? 1;
279
- const trimStart = placement.trim?.start ?? 0;
280
- const trimEnd = placement.trim?.end ?? trimStart + placement.duration * rate;
281
- const spans = playableSpans(trimStart, trimEnd, placement.excludedRanges);
282
- const markers = [];
283
- let n = 0;
284
- for (const ms of gridPointsMs) {
285
- const sourceSec = ms / 1e3;
286
- let playedBefore = 0;
287
- let visible = false;
288
- for (const span of spans) {
289
- if (sourceSec < span.start) break;
290
- if (sourceSec < span.end) {
291
- playedBefore += sourceSec - span.start;
292
- visible = true;
293
- break;
294
- }
295
- playedBefore += span.end - span.start;
296
- }
297
- if (!visible) continue;
298
- markers.push({
299
- id: `grid-${grid}-${clipId}-${n}`,
300
- startTime: placement.startTime + playedBefore / rate,
301
- kind: grid,
302
- name: `${grid}-${n}`,
303
- metadata: { sourceClipId: clipId }
304
- });
305
- n++;
306
- }
307
- if (markers.length > cap) {
308
- throw new GridError(
309
- "grid_cap_exceeded",
310
- `materializeGrid: ${markers.length} '${grid}' markers fall inside clip '${clipId}' \u2014 over the cap of ${cap}. Raise maxMarkers (\u2264 ${GRID_HARD_MAX_MARKERS}) or narrow the clip's visible window (trim / excludedRanges) so fewer grid points land.`
311
- );
312
- }
313
- return markers;
314
- }
315
-
316
- // ../composition-ops/src/replaceRegion.ts
317
- var EPS = 1e-3;
318
- function replaceRegion(input) {
319
- const { cutSchedule, programAudioSourceId, durationByMediaId } = input;
320
- if (cutSchedule.length === 0) {
321
- throw new Error("replaceRegion: empty cut schedule");
322
- }
323
- const sorted = [...cutSchedule].sort((a, b) => a.startSec - b.startSec);
324
- for (let i = 0; i < sorted.length; i++) {
325
- const s = sorted[i];
326
- if (s.endSec <= s.startSec) throw new Error(`replaceRegion: cut span '${s.candidateId}' has non-positive duration [${s.startSec},${s.endSec}]`);
327
- const dur = durationByMediaId[s.mediaId];
328
- if (dur !== void 0 && (s.sourceInSec < -EPS || s.sourceOutSec > dur + EPS)) {
329
- throw new Error(`replaceRegion: cut span '${s.candidateId}' source [${s.sourceInSec},${s.sourceOutSec}] out of media [0,${dur}]`);
330
- }
331
- if (i > 0) {
332
- const prev = sorted[i - 1];
333
- if (Math.abs(s.startSec - prev.endSec) > EPS) {
334
- throw new Error(`replaceRegion: cut schedule is not contiguous \u2014 gap/overlap between ${prev.endSec} and ${s.startSec}`);
335
- }
336
- }
337
- }
338
- const regionStart = sorted[0].startSec;
339
- const regionEnd = sorted[sorted.length - 1].endSec;
340
- const audioPinned = programAudioSourceId !== void 0;
341
- const mediaClip = (id, startTime, duration, mediaId, trimStart, trimEnd, extra = {}) => ({
342
- id,
343
- startTime,
344
- duration,
345
- source: { kind: "media", mediaId, trim: { start: trimStart, end: trimEnd }, ...extra }
346
- });
347
- const videoTrack = {
348
- id: `switch-cut-${stableSuffix(sorted)}`,
349
- kind: "video",
350
- name: input.trackName ?? "Switch Cut",
351
- contents: sorted.map(
352
- (s, i) => mediaClip(`cut-${i}-${s.candidateId}`, s.startSec, s.endSec - s.startSec, s.mediaId, s.sourceInSec, s.sourceOutSec, audioPinned ? { muted: true } : {})
353
- )
354
- };
355
- const tracks = [...input.composition.tracks, videoTrack];
356
- if (audioPinned) {
357
- const dur = durationByMediaId[programAudioSourceId];
358
- const audioOut = regionEnd - regionStart;
359
- if (dur !== void 0 && audioOut > dur + EPS) {
360
- throw new Error(`replaceRegion: program audio source '${programAudioSourceId}' (${dur}s) shorter than the region (${audioOut}s)`);
361
- }
362
- tracks.push({
363
- id: `switch-audio-${programAudioSourceId}`,
364
- kind: "audio",
365
- name: "Program Audio",
366
- contents: [
367
- mediaClip(`program-audio-${programAudioSourceId}`, regionStart, regionEnd - regionStart, programAudioSourceId, 0, regionEnd - regionStart, { sync: "independent" })
368
- ]
369
- });
370
- }
371
- const next = { ...input.composition, tracks };
372
- return Composition.parse(next);
373
- }
374
- function stableSuffix(spans) {
375
- return spans.map((s) => `${s.candidateId}@${s.startSec.toFixed(2)}`).join("_").slice(0, 48).replace(/[^a-zA-Z0-9_@.-]/g, "");
376
- }
377
-
378
- // ../composition-ops/src/alphaLayers.ts
379
- function collectAlphaLayerClips(composition) {
380
- const tracks = composition?.tracks;
381
- if (!Array.isArray(tracks)) return [];
382
- const out = [];
383
- for (const track of tracks) {
384
- const contents = track?.contents;
385
- if (!Array.isArray(contents)) continue;
386
- for (const c of contents) {
387
- const src = c?.source;
388
- if (!src || typeof src !== "object") continue;
389
- if (src.kind !== "alpha-layer") continue;
390
- const componentId = src.componentId;
391
- const clipId = c.id;
392
- const startTime = c.startTime;
393
- const duration = c.duration;
394
- if (typeof componentId !== "string" || componentId.length === 0) continue;
395
- if (typeof clipId !== "string" || clipId.length === 0) continue;
396
- if (typeof startTime !== "number" || typeof duration !== "number" || duration <= 0) continue;
397
- const rawProps = src.props;
398
- const props = rawProps && typeof rawProps === "object" && !Array.isArray(rawProps) ? rawProps : {};
399
- const rawAssets = src.assets;
400
- const assets = rawAssets && typeof rawAssets === "object" && !Array.isArray(rawAssets) ? Object.fromEntries(
401
- Object.entries(rawAssets).filter(
402
- (entry) => typeof entry[1] === "string" && entry[1].length > 0
403
- )
404
- ) : {};
405
- out.push({ clipId, componentId, props, assets, startTime, duration });
406
- }
407
- }
408
- return out;
409
- }
410
- function collectRawComponentClips(composition) {
411
- const tracks = composition?.tracks;
412
- if (!Array.isArray(tracks)) return [];
413
- const out = [];
414
- for (const track of tracks) {
415
- const contents = track?.contents;
416
- if (!Array.isArray(contents)) continue;
417
- for (const c of contents) {
418
- const src = c?.source;
419
- if (!src || typeof src !== "object") continue;
420
- if (src.kind !== "component") continue;
421
- const id = c.id;
422
- out.push(typeof id === "string" && id.length > 0 ? id : "(unnamed clip)");
423
- }
424
- }
425
- return out;
426
- }
427
- function collectUnbakedComponentClips(composition) {
428
- const tracks = composition?.tracks;
429
- if (!Array.isArray(tracks)) return [];
430
- const out = [];
431
- for (const track of tracks) {
432
- const contents = track?.contents;
433
- if (!Array.isArray(contents)) continue;
434
- for (const c of contents) {
435
- const src = c?.source;
436
- if (!src || typeof src !== "object") continue;
437
- if (src.kind !== "overlay") continue;
438
- if (src.primitiveId !== "custom") continue;
439
- const params = src.params;
440
- const componentId = params && typeof params === "object" ? params.componentId : void 0;
441
- const clipId = c.id;
442
- if (typeof componentId !== "string" || componentId.length === 0) continue;
443
- if (typeof clipId !== "string" || clipId.length === 0) continue;
444
- out.push({ clipId, componentId });
445
- }
446
- }
447
- return out;
448
- }
449
-
450
- // ../composition-ops/src/index.ts
451
- var SceneAssetBindings = z.record(z.string().min(1), z.string().min(1));
452
- function mediaFamily(mimeType) {
453
- const top = mimeType.split("/")[0]?.trim().toLowerCase();
454
- if (top === "video") return "video";
455
- if (top === "audio") return "audio";
456
- if (top === "image") return "image";
457
- throw new Error(
458
- `mediaFamily: cannot derive a timeline family from mimeType ${JSON.stringify(mimeType)} \u2014 expected a video/*, audio/* or image/* type.`
459
- );
460
- }
461
- function deriveFromSource(sourceKind) {
462
- switch (sourceKind) {
463
- case "overlay":
464
- return "overlay";
465
- case "effect":
466
- return "effect";
467
- case "component":
468
- return "overlay";
469
- case "alpha-layer":
470
- return "overlay";
471
- case "card":
472
- return "overlay";
473
- case "media":
474
- return void 0;
475
- // ambiguous — caller must disambiguate
476
- case "composition":
477
- return "video";
478
- default: {
479
- const _exhaustive = sourceKind;
480
- throw new Error(`deriveFromSource: unknown source kind ${String(_exhaustive)}`);
481
- }
482
- }
483
- }
484
- function resolveClipKind(clip, track) {
485
- const resolved = clip.kind ?? deriveFromSource(clip.source.kind) ?? track?.kind;
486
- if (!resolved) {
487
- throw new Error(
488
- `resolveClipKind: clip with a 'media' source has no 'kind' and no track context \u2014 cannot determine its family (video | audio | image). Set clip.kind or target a track.`
489
- );
490
- }
491
- return resolved;
492
- }
493
- var CompositionOp = z.discriminatedUnion("type", [
494
- z.object({ type: z.literal("clip.add"), clip: TimelineClip, trackId: z.string().optional(), newTrack: z.boolean().optional(), newTrackId: z.string().optional() }).strict().describe(
495
- "Add a clip to the timeline. The clip self-routes to a track by FAMILY (`clip.kind`, else derived from `source.kind`, else the target track) \u2014 a `media` clip MUST carry `kind` (video|audio|image) or a `trackId`, else it is rejected as ambiguous. `trackId` targets an existing SAME-family track; `newTrack:true` (with optional pinned `newTrackId`) creates a fresh track of that family."
496
- ),
497
- z.object({ type: z.literal("clip.remove"), clipId: z.string() }).strict().describe(
498
- "Remove a clip by id (located across every track)."
499
- ),
500
- z.object({ type: z.literal("clip.update"), clipId: z.string(), patch: TimelineClip.partial() }).strict().describe(
501
- "Patch an existing clip by id (cross-family). The patch is a partial TimelineClip. NOTE: `source` is REPLACED wholesale \u2014 the patch merges at the top level only, so a partial `source` WIPES the source fields you omit. Patch the whole source object when changing it. (To change reframe/excluded ranges, prefer setCropIntents / addExcludedRange, not a source patch.)"
502
- ),
503
- z.object({ type: z.literal("removeClipsByMediaId"), mediaId: z.string() }).strict().describe(
504
- "Remove EVERY clip sourced from a given mediaId (e.g. after deleting that media)."
505
- ),
506
- z.object({ type: z.literal("loadOverlays"), clips: z.array(TimelineClip) }).strict().describe(
507
- "Replace the overlay channel wholesale with the given clips (bulk set, not a merge)."
508
- ),
509
- z.object({ type: z.literal("addMarker"), marker: Marker }).strict().describe(
510
- "Add a timeline marker (a named time point/range; carry extensions in `metadata`)."
511
- ),
512
- z.object({ type: z.literal("removeMarker"), markerId: z.string() }).strict().describe(
513
- "Remove a marker by id."
514
- ),
515
- z.object({ type: z.literal("updateMarker"), markerId: z.string(), updates: Marker.partial() }).strict().describe(
516
- "Patch a marker's fields by id (partial merge)."
517
- ),
518
- z.object({ type: z.literal("setCropIntents"), clipId: z.string(), segments: z.array(ReframeSegment).nullable() }).strict().describe(
519
- "Set (or CLEAR, with `segments: null`) the camera/reframe channel on a MEDIA clip \u2014 the time-aligned source\u2192output viewport (focus, zoom, ease, bias). This is the PREFERRED way to author reframing; do NOT reframe via a whole-source `clip.update` patch. Segments tile the clip's duration."
520
- ),
521
- z.object({ type: z.literal("setEffects"), clipId: z.string(), effects: z.array(PerClipEffect) }).strict().describe(
522
- "Set (or CLEAR, with `effects: []`) a clip's EFFECT channel \u2014 the registered effect primitives stacked onto that clip, each `{ primitiveId, params? }`. WHOLESALE: the array REPLACES the clip's current effects, so send the full stack (read the clip first to add one). This is the ONLY way to change effects on an existing clip \u2014 do NOT reach for a `clip.update` patch, whose top-level merge is the source-clobbering footgun documented above."
523
- ),
524
- z.object({ type: z.literal("setCaptionStyle"), style: CaptionStyle.partial() }).strict().describe(
525
- "Merge style overrides (font/color/position/emphasis) onto the composition's caption layer. Partial \u2014 omitted fields keep their current value; creates the caption container if absent."
526
- ),
527
- z.object({ type: z.literal("setCaptionDepth"), zPlane: z.enum(["front", "behind-subject"]) }).strict().describe(
528
- "Set the caption compositing plane. Use `behind-subject` to place captions behind the segmented foreground subject; use `front` for conventional captions above all video layers."
529
- ),
530
- z.object({ type: z.literal("setCaptions"), segments: z.array(CaptionSegment), style: CaptionStyle.partial().optional() }).strict().describe(
531
- "Replace the caption SEGMENTS wholesale \u2014 author the word-timed caption layer directly (each segment = one sentence/page of `words` with timeline-ms `startMs`/`endMs`, optional `emphasis`). This is how a client with its OWN word timings (e.g. a generated VO take) gets word-synced captions without a source transcript; the compose lane's transcript slice writes the same layer. `style`, when given, merges with setCaptionStyle semantics; an omitted style keeps the existing one. Renderer paginates within a segment and never pools words across two \u2014 keep segments sentence-bounded."
532
- ),
533
- z.object({ type: z.literal("applyMotionPreset"), clipId: z.string(), preset: z.string(), params: z.object({ enterMs: z.number().optional(), exitMs: z.number().optional(), staggerMs: z.number().optional() }).strict().optional() }).strict().describe(
534
- "Shell a STATIC `card` clip with a frame-driven MOTION preset \u2014 the public card-shelling op (the layout renders VERBATIM; only the container animates in/out). The server wraps the card in the chosen `preset` (fade-up / slide-in-left / slide-in-right / scale-pop / stagger-reveal), transpiles it, mints it as a LIVE component, and swaps the clip source to that component (the bakeless live lane \u2014 no box). `params` tunes timing (`enterMs`/`exitMs`, `staggerMs` for stagger-reveal). The target must be a static card (components already carry motion; library overlays already move); re-run overwrites idempotently ONLY while the target is still a card (an uncompilable shell); once shelled, the clip is a COMPONENT \u2014 to change its motion, remove and re-add the card, then re-shell. Honored under `dry_run` (validates preset + target without transpiling or persisting). ORDERING: the preset resolves its target against the PERSISTED composition \u2014 a card added in the SAME batch is not yet visible to it. Persist the clip.add batch first, then shell in a following call."
535
- ),
536
- // Scene shots (spec §"Authoring, persistence, and discovery"). Route-materialized: the host
537
- // registers the generated wrapper source through its component store and rewrites these to
538
- // addTrack / clip.add / clip.update carrying `props.componentRef`; the agent edits a scene
539
- // spec, never wrapper TSX or a hash. Exactly one of `trackId` (a dedicated overlay track) or
540
- // `newTrack` (mint one) is required on `scene.add`.
541
- z.object({
542
- type: z.literal("scene.add"),
543
- clipId: z.string().min(1),
544
- trackId: z.string().min(1).optional(),
545
- newTrack: z.boolean().optional(),
546
- newTrackId: z.string().min(1).optional(),
547
- startTime: z.number().nonnegative(),
548
- duration: z.number().positive(),
549
- transitionIn: Transition.optional(),
550
- scene: SceneShotSpecV2,
551
- assets: SceneAssetBindings
552
- }).strict().describe(
553
- // Lives here, not in the published api/openapi.json. It used to exist ONLY
554
- // in that document, which made the contract hand-maintained and put it out
555
- // of reach of a scripted sync from this SSOT.
556
- `Author a scene shot and place it on a dedicated overlay track. The host validates the scene, generates and registers an immutable wrapper component source, and emits the ordinary track/clip operations carrying \`props.componentRef\` \u2014 you edit a scene SPEC, never wrapper TSX or a source hash. Registration and the composition update are ONE undoable edit; a failure publishes nothing and leaves no clip pointing at an unresolved source. Pass EXACTLY ONE of \`trackId\` (an existing dedicated scene track) or \`newTrack: true\` (mint one). That track holds scene-shot clips and gaps only \u2014 no generic component/primitive/media clips in v1 \u2014 and existing generic overlay tracks are unaffected. Scene clips use front zPlane and full-frame placement: outer per-clip \`region\`, \`position\`, \`scale\`, \`rotation\`, \`fit\`, \`effects\`, \`animate\`, \`params.transition\`, \`params.sequenceGroup\` and internal fadeIn/fadeOut are REFUSED with a path, because the shot's own layer transforms are the single transform owner. TEXT: \`space: "screen"\` text is composed above the world result, stays fixed in the frame and stays sharp through focus; \`space: "world"\` text is rasterized into the 3D scene, moves with the camera and is occluded by geometry. Screenshots and world text are UNLIT and colour-managed so product UI colours are not tinted by the studio lighting, while device bodies are lit and cast shadows; an unlit surface still occludes. SCOPE GAP (pre-existing, neither closed nor changed by scene shots): global wrap effects \u2014 composition-level grades and camera effects \u2014 do not reach front-overlay clips, so they do not affect a scene shot. Author the look INSIDE the shot (\`post.grain\`, \`post.vignette\`, \`post.focus\`, \`backdrop\`, \`environment\`) instead. DISCOVERY: there is no built-in scene primitive to look up in list_catalog; the wrapper this operation registers appears afterwards as an ordinary INSTALLED component under the existing catalog rules. ASSETS: \`assets\` binds each slot name a layer references to a media id in the same project, and the slot's declared KIND decides what it admits \u2014 an image slot takes ${SCENE_SLOT_MEDIA_FORMATS.image}, a video slot ${SCENE_SLOT_MEDIA_FORMATS.video}. Binding the wrong kind is refused with the slot's path.`
557
- ),
558
- z.object({
559
- type: z.literal("scene.update"),
560
- clipId: z.string().min(1),
561
- startTime: z.number().nonnegative().optional(),
562
- duration: z.number().positive().optional(),
563
- transitionIn: Transition.optional(),
564
- scene: SceneShotSpecV2.optional(),
565
- assets: SceneAssetBindings.optional()
566
- }).strict().describe(`Revise a scene shot in place by clip id. Send a replacement \`scene\` and/or \`assets\`; timing changes use the ordinary clip timing fields (\`startTime\`, \`duration\`, \`transitionIn\`). Omitted fields keep their persisted values and the MERGED result is re-validated as a whole, so a partial edit cannot leave the shot invalid. Changing only values or asset IDs updates props and bindings and REUSES the pinned source; changing the referenced SLOT SET regenerates the wrapper manifest and registers a new immutable source version atomically. Undo restores the source reference, spec, bindings and timing together. Topology, asset bindings, text content, font properties, device kind and colours are STATIC within a shot's animation \u2014 change them here (a revised spec) rather than trying to animate them. This is not a source trim or slip: a shortened or extended clip resamples the SAME normalized animation over its new duration, and trim/slip requests on generated animation are rejected rather than reinterpreted as media trims. In one batch, \`scene.update\` may target a shot added by an earlier \`scene.add\`. ASSETS: \`assets\` binds each slot name a layer references to a media id in the same project, and the slot's declared KIND decides what it admits \u2014 an image slot takes ${SCENE_SLOT_MEDIA_FORMATS.image}, a video slot ${SCENE_SLOT_MEDIA_FORMATS.video}. Binding the wrong kind is refused with the slot's path.`),
567
- z.object({ type: z.literal("materializeGrid"), clipId: z.string(), grid: z.enum(["beat", "word", "shot"]), maxMarkers: z.number().int().positive().max(2e3).optional() }).strict().describe(
568
- 'Materialize a clip\'s SOURCE-media temporal GRID as timeline markers \u2014 the rhythm authority you cut to. `grid:"beat"` reads a MUSIC bed\'s beat map (get_media_context.beatGrid), `grid:"word"` the VO word onsets (wordGrid); each grid point is projected from media time to TIMELINE time through the clip\'s placement (startTime, trim, playbackRate, excludedRanges) and written as a `kind:"beat"|"word"` marker (`metadata.sourceClipId` set). Points outside the clip\'s visible window are skipped. Re-running REPLACES the prior markers from the same clip+grid (idempotent). Land cuts and transition MIDPOINTS on the markers (a 0.5s transition starts 0.25s before its marker). `maxMarkers` (default 500, ceiling 2000) \u2014 exceeding it fails loud with the count. `grid:"shot"` is not yet exposed (shot_grid_unavailable). The target must be a media clip; absent evidence fails loud (grid_unavailable) naming how to get it (declare audioRole:"music" at ingest / transcribe).'
569
- ),
570
- z.object({
571
- type: z.literal("captions.fromTranscript"),
572
- mediaId: z.string().min(1),
573
- window: z.object({ startSec: z.number().min(0), endSec: z.number().positive() }).strict().optional()
574
- }).strict().describe(
575
- "Attach caption CONTENT from the media's cached transcript \u2014 no pasting words. ROUTE-MATERIALIZED like applyMotionPreset/materializeGrid: the server slices the word-timed transcript to `window` (source seconds; absent = the whole media), rebases times, sentence-splits into segments, and applies it as `setCaptions`. 422 media_no_transcript when the media has no transcript or no words in the window. For your OWN word timings use setCaptions directly."
576
- ),
577
- z.object({ type: z.literal("setFormat"), format: Format.pick({ aspectRatio: true, fps: true, resolution: true }) }).strict().describe(
578
- "Set the output format \u2014 `aspectRatio` (16:9|9:16|1:1|4:5) plus optional `fps` and `resolution`. Merges: an omitted field keeps the current value."
579
- ),
580
- z.object({ type: z.literal("addTrack"), kind: Track.shape.kind, name: z.string().optional(), id: z.string().optional() }).strict().describe(
581
- "Add an EMPTY track of a given family (video|audio|image|overlay|effect). Optional pinned `id`. The new track is appended, so it starts at the BACK: index 0 is the top layer and higher indices are further back (see `reorderTracks`). Track order only ranks clips on the same plane \u2014 every overlay draws above the video lane unless its `zPlane` is `behind-subject`, whatever its track index."
582
- ),
583
- z.object({ type: z.literal("removeTrack"), trackId: z.string() }).strict().describe(
584
- "Remove a track AND all its clips by id."
585
- ),
586
- z.object({ type: z.literal("updateTrack"), trackId: z.string(), updates: Track.pick({ name: true, enabled: true, volume: true }).partial() }).strict().describe(
587
- "Patch a track's `name` / `enabled` by id."
588
- ),
589
- z.object({ type: z.literal("reorderTracks"), fromIndex: z.number(), toIndex: z.number() }).strict().describe(
590
- 'Move a track from one index to another. ORDER: index 0 is the TOP layer, not the bottom \u2014 moving a backdrop to index 0 puts it in FRONT of everything and hides the composition. Higher index = further back. PRECEDENCE: track order decides compositing only among clips on the same plane. Overlays are composited in a lane ABOVE the video lane whatever their track index; the one exception is `zPlane:"behind-subject"`, which moves the overlay inside its host clip, behind the segmented foreground. So if reordering tracks does not change what you see, the plane split already decided it.'
591
- ),
592
- z.object({ type: z.literal("moveClipToTrack"), clipId: z.string(), targetTrackId: z.string(), newStartTime: z.number().optional() }).strict().describe(
593
- "Move a clip to another SAME-family track, optionally at a new `newStartTime`."
594
- ),
595
- z.object({ type: z.literal("duplicateClip"), clipId: z.string(), newClipId: z.string().optional() }).strict().describe(
596
- "Duplicate a clip by id. Pin `newClipId` to make the copy's id deterministic (required for a clean undo/inverse)."
597
- ),
598
- z.object({ type: z.literal("cutAndRipple"), start: z.number(), end: z.number() }).strict().describe(
599
- "Server-authoritative: delete the [start,end] time window (seconds) and ripple every downstream clip/marker/caption earlier to close the gap."
600
- ),
601
- z.object({ type: z.literal("ripple"), time: z.number(), amount: z.number() }).strict().describe(
602
- "Server-authoritative: shift everything at/after `time` (seconds) by `amount` seconds (+ opens a gap, \u2212 closes one)."
603
- ),
604
- z.object({ type: z.literal("addExcludedRange"), clipId: z.string(), sourceStart: z.number(), sourceEnd: z.number(), reason: z.string().optional() }).strict().describe(
605
- "Server-authoritative: exclude a [sourceStart,sourceEnd] span (seconds) of a clip's SOURCE (an internal trim) and ripple downstream \u2014 the preferred way to drop a sub-range vs a source patch."
606
- ),
607
- z.object({ type: z.literal("removeExcludedRange"), clipId: z.string(), rangeIdx: z.number() }).strict().describe(
608
- "Server-authoritative: remove a clip's previously-added excluded range by index (re-includes that span, ripples downstream)."
609
- )
610
- ]).describe(
611
- "One scoped composition operation. Discriminated by `type` \u2014 `clip.add` / `clip.remove` / `clip.update` patch clips across any family; the rest cover markers, reframe, effects, captions, format, tracks, and the server-authoritative ripple recomputes. Every variant is `.strict()`: an unknown field is refused, not ignored."
612
- );
613
- function mergedCaptionStyle(composition, patch) {
614
- const existing = composition.captions?.style ?? {};
615
- if (!patch) return existing;
616
- const defined = Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== void 0));
617
- return { ...existing, ...defined };
618
- }
619
- function assertCaptionFacesDrawable(opType, composition, patch) {
620
- const existing = composition.captions?.style ?? {};
621
- const issues = introducedCaptionFaceIssues(existing, mergedCaptionStyle(composition, patch));
622
- if (issues.length === 0) return;
623
- throw new Error(
624
- `${opType}: this edit introduces a caption face with no bytes behind it \u2014 ` + issues.map((issue) => `style.${issue.field}: ${issue.message}`).join(" ")
625
- );
626
- }
627
- function applyCaptionOpToStore(store, op) {
628
- const composition = store.getState().composition;
629
- assertCaptionFacesDrawable(op.type, composition, op.style);
630
- if (op.type === "setCaptionStyle") {
631
- store.setCaptionStyle(op.style);
632
- return;
633
- }
634
- store.setCaptions(op.segments, op.style);
635
- }
636
- function applyOp(composition, op) {
637
- const store = createHeadlessStore(composition);
638
- applyOpToStore(store, op);
639
- return store.getState().composition;
640
- }
641
- function applyOpToStore(store, op) {
642
- const composition = store.getState().composition;
643
- switch (op.type) {
644
- case "clip.add": {
645
- if (findClipById(composition, op.clip.id)) {
646
- throw new Error(`clip.add: clip '${op.clip.id}' already exists`);
647
- }
648
- if (op.newTrackId !== void 0 && composition.tracks.some((track) => track.id === op.newTrackId)) {
649
- throw new Error(`clip.add: track '${op.newTrackId}' already exists`);
650
- }
651
- if (op.trackId !== void 0) {
652
- const track = composition.tracks.find((t) => t.id === op.trackId);
653
- if (!track) throw new Error(`clip.add: track '${op.trackId}' not found`);
654
- const kind = resolveClipKind(op.clip, track);
655
- if (kind !== track.kind) {
656
- throw new Error(
657
- `clip.add: clip family '${kind}' does not match target track '${op.trackId}' (kind '${track.kind}')`
658
- );
659
- }
660
- store.addClipToTrack(op.clip, { trackId: op.trackId });
661
- } else {
662
- const kind = resolveClipKind(op.clip);
663
- store.addClipToTrack(
664
- op.clip,
665
- op.newTrack ? { kind, newTrack: true, newTrackId: op.newTrackId } : { kind }
666
- );
667
- }
668
- break;
669
- }
670
- case "clip.remove":
671
- store.removeClip(op.clipId);
672
- break;
673
- case "clip.update":
674
- store.updateClip(op.clipId, op.patch);
675
- break;
676
- case "removeClipsByMediaId":
677
- store.removeClipsByMediaId(op.mediaId);
678
- break;
679
- case "loadOverlays":
680
- store.loadOverlays(op.clips);
681
- break;
682
- case "addMarker":
683
- store.addMarker(op.marker);
684
- break;
685
- case "removeMarker":
686
- store.removeMarker(op.markerId);
687
- break;
688
- case "updateMarker":
689
- store.updateMarker(op.markerId, op.updates);
690
- break;
691
- case "setCropIntents":
692
- store.setCropIntents(op.clipId, op.segments);
693
- break;
694
- case "setEffects": {
695
- if (!findClipById(composition, op.clipId)) {
696
- throw new Error(`setEffects: clip '${op.clipId}' not found`);
697
- }
698
- store.setEffects(op.clipId, op.effects);
699
- break;
700
- }
701
- case "setCaptionStyle":
702
- applyCaptionOpToStore(store, op);
703
- break;
704
- case "setCaptionDepth":
705
- store.setCaptionDepth(op.zPlane);
706
- break;
707
- case "setCaptions":
708
- applyCaptionOpToStore(store, op);
709
- break;
710
- case "applyMotionPreset":
711
- throw new Error(
712
- `applyOp: 'applyMotionPreset' is route-materialized \u2014 it needs a director transpile of the generated motion shell and is rewritten to a clip.update at /composition/apply before reaching the pure engine.`
713
- );
714
- case "materializeGrid":
715
- throw new Error(
716
- `applyOp: 'materializeGrid' is route-materialized \u2014 it reads the clip's source-media grid (beat/word/shot) via a media reader and is rewritten to removeMarker+addMarker ops at /composition/apply before reaching the pure engine.`
717
- );
718
- case "captions.fromTranscript":
719
- throw new Error(
720
- `applyOp: 'captions.fromTranscript' is route-materialized \u2014 it is resolved to setCaptions at /composition/apply before reaching the pure engine.`
721
- );
722
- case "scene.add":
723
- case "scene.update":
724
- throw new Error(
725
- `applyOp: '${op.type}' is route-materialized \u2014 the host validates the scene, registers the generated scene-shot wrapper source through its component store and rewrites the request to ordinary addTrack / clip.add / clip.update ops carrying \`props.componentRef\` before reaching the pure engine.`
726
- );
727
- case "setFormat":
728
- store.setFormat(op.format);
729
- break;
730
- case "addTrack":
731
- store.addTrack(op.kind, op.name, op.id);
732
- break;
733
- case "removeTrack":
734
- store.removeTrack(op.trackId);
735
- break;
736
- case "updateTrack":
737
- store.updateTrack(op.trackId, op.updates);
738
- break;
739
- case "reorderTracks":
740
- store.reorderTracks(op.fromIndex, op.toIndex);
741
- break;
742
- case "moveClipToTrack":
743
- store.moveClipToTrack(op.clipId, op.targetTrackId, op.newStartTime);
744
- break;
745
- case "duplicateClip": {
746
- const minted = store.duplicateClip(op.clipId);
747
- if (op.newClipId !== void 0 && minted !== void 0 && minted !== op.newClipId) {
748
- store.updateClip(minted, { id: op.newClipId });
749
- }
750
- break;
751
- }
752
- case "cutAndRipple":
753
- store.cutAndRipple(op.start, op.end);
754
- break;
755
- case "ripple":
756
- store.ripple(op.time, op.amount);
757
- break;
758
- case "addExcludedRange":
759
- store.addExcludedRange(op.clipId, op.sourceStart, op.sourceEnd, op.reason);
760
- break;
761
- case "removeExcludedRange":
762
- store.removeExcludedRange(op.clipId, op.rangeIdx);
763
- break;
764
- default: {
765
- const _exhaustive = op;
766
- throw new Error(`applyOp: unknown op ${_exhaustive.type}`);
767
- }
768
- }
769
- }
770
- function invertOp(before, op) {
771
- switch (op.type) {
772
- case "clip.add":
773
- return { type: "clip.remove", clipId: op.clip.id };
774
- case "clip.remove": {
775
- const found = findClipById(before, op.clipId);
776
- if (!found) {
777
- throw new Error(`invertOp(clip.remove): clip '${op.clipId}' not in the pre-image`);
778
- }
779
- const track = before.tracks[found.trackIndex];
780
- return { type: "clip.add", clip: found.clip, trackId: track.id };
781
- }
782
- case "clip.update": {
783
- const found = findClipById(before, op.clipId);
784
- if (!found) {
785
- throw new Error(`invertOp(clip.update): clip '${op.clipId}' not in the pre-image`);
786
- }
787
- const prior = found.clip;
788
- const inversePatch = {};
789
- for (const key of Object.keys(op.patch)) inversePatch[key] = prior[key];
790
- return { type: "clip.update", clipId: op.clipId, patch: inversePatch };
791
- }
792
- case "addMarker":
793
- return { type: "removeMarker", markerId: op.marker.id };
794
- case "removeMarker": {
795
- const found = findMarkerById(before, op.markerId);
796
- if (!found) {
797
- throw new Error(`invertOp(removeMarker): marker '${op.markerId}' not in the pre-image`);
798
- }
799
- return { type: "addMarker", marker: found.marker };
800
- }
801
- case "updateMarker": {
802
- const found = findMarkerById(before, op.markerId);
803
- if (!found) {
804
- throw new Error(`invertOp(updateMarker): marker '${op.markerId}' not in the pre-image`);
805
- }
806
- const prior = found.marker;
807
- const inverseUpdates = {};
808
- for (const key of Object.keys(op.updates)) {
809
- inverseUpdates[key] = prior[key];
810
- }
811
- return { type: "updateMarker", markerId: op.markerId, updates: inverseUpdates };
812
- }
813
- case "setCropIntents": {
814
- const found = findClipById(before, op.clipId);
815
- if (!found) {
816
- throw new Error(`invertOp(setCropIntents): clip '${op.clipId}' not in the pre-image`);
817
- }
818
- const src = found.clip.source;
819
- const priorSegments = src.kind === "media" && src.reframe ? src.reframe.segments : null;
820
- return { type: "setCropIntents", clipId: op.clipId, segments: priorSegments };
821
- }
822
- case "setEffects": {
823
- const found = findClipById(before, op.clipId);
824
- if (!found) {
825
- throw new Error(`invertOp(setEffects): clip '${op.clipId}' not in the pre-image`);
826
- }
827
- return { type: "setEffects", clipId: op.clipId, effects: found.clip.effects ?? [] };
828
- }
829
- case "setFormat": {
830
- const f = before.format;
831
- return {
832
- type: "setFormat",
833
- format: {
834
- aspectRatio: f.aspectRatio,
835
- ...f.fps !== void 0 ? { fps: f.fps } : {},
836
- ...f.resolution !== void 0 ? { resolution: f.resolution } : {}
837
- }
838
- };
839
- }
840
- case "setCaptionDepth":
841
- return { type: "setCaptionDepth", zPlane: before.captions?.zPlane ?? "front" };
842
- case "addTrack":
843
- if (op.id === void 0) {
844
- throw new Error("invertOp(addTrack): no pinned `id` \u2014 cannot target the minted track");
845
- }
846
- return { type: "removeTrack", trackId: op.id };
847
- case "updateTrack": {
848
- const track = before.tracks.find((t) => t.id === op.trackId);
849
- if (!track) {
850
- throw new Error(`invertOp(updateTrack): track '${op.trackId}' not in the pre-image`);
851
- }
852
- const inverseUpdates = {};
853
- if ("name" in op.updates) inverseUpdates.name = track.name;
854
- if ("enabled" in op.updates) inverseUpdates.enabled = track.enabled;
855
- return { type: "updateTrack", trackId: op.trackId, updates: inverseUpdates };
856
- }
857
- case "reorderTracks": {
858
- const len = before.tracks.length;
859
- const restingIndex = Math.max(0, Math.min(op.toIndex, len - 1));
860
- return { type: "reorderTracks", fromIndex: restingIndex, toIndex: op.fromIndex };
861
- }
862
- case "moveClipToTrack": {
863
- const found = findTrackContainingClipId(before, op.clipId);
864
- if (!found) {
865
- throw new Error(`invertOp(moveClipToTrack): clip '${op.clipId}' not in the pre-image`);
866
- }
867
- const priorClip = found.track.contents[found.clipIndex];
868
- return {
869
- type: "moveClipToTrack",
870
- clipId: op.clipId,
871
- targetTrackId: found.track.id,
872
- newStartTime: priorClip.startTime
873
- };
874
- }
875
- case "duplicateClip":
876
- if (op.newClipId === void 0) {
877
- throw new Error(
878
- "invertOp(duplicateClip): no `newClipId` \u2014 the copy id is store-minted and non-deterministic. Pin `newClipId` on the op for a deterministic inverse."
879
- );
880
- }
881
- return { type: "clip.remove", clipId: op.newClipId };
882
- case "cutAndRipple":
883
- case "ripple":
884
- case "addExcludedRange":
885
- case "removeExcludedRange":
886
- case "removeTrack":
887
- case "removeClipsByMediaId":
888
- case "loadOverlays":
889
- case "setCaptionStyle":
890
- case "setCaptions":
891
- case "applyMotionPreset":
892
- case "materializeGrid":
893
- case "captions.fromTranscript":
894
- case "scene.add":
895
- case "scene.update":
896
- throw new Error(
897
- `invertOp: '${op.type}' has no per-op inverse in scope (server-authoritative cross-field recompute or whole-channel/positional restore) \u2014 fail loud, no fake inverse.`
898
- );
899
- default: {
900
- const _exhaustive = op;
901
- throw new Error(`invertOp: unknown op ${_exhaustive.type}`);
902
- }
903
- }
904
- }
905
43
  export {
44
+ CaptionMaterializationError,
906
45
  CompositionOp,
907
46
  GRID_DEFAULT_MAX_MARKERS,
908
47
  GRID_HARD_MAX_MARKERS,
@@ -910,11 +49,14 @@ export {
910
49
  applyCaptionOpToStore,
911
50
  applyOp,
912
51
  applyOpToStore,
52
+ captionSourceMediaIds,
913
53
  collectAlphaLayerClips,
914
54
  collectRawComponentClips,
915
55
  collectUnbakedComponentClips,
916
56
  createRecordingStore,
917
57
  invertOp,
58
+ materializeCaptionsFromComposition,
59
+ materializeCompositionCaptions,
918
60
  materializeGridMarkers,
919
61
  mediaFamily,
920
62
  replaceRegion,