@mengine/medeo-client 2.0.1-alpha.23 → 2.0.1-alpha.25

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/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as VideoDocumentValidationError, A as resolveEntityTimelineLayout, B as selectAudioScriptSegment, C as canonicalJson, Ct as effectiveVideoClipDurationMs, D as resolveFieldPath, Dt as videoDocumentMirrorSchema, E as assertFieldChanges, Et as recordEntries, F as assembleCaptionContent, G as createRelationId, H as variantBaseEntityIds, I as assembleEntityContent, J as buildInitialVideoDocument, K as hasSequence, L as assemblePhoneticScriptContent, M as validateEntityRelationTransition, N as decodeEntityRelationRows, O as projectEntityTimeline, Ot as base64ToBytes, P as ScriptCompositionError, Q as toVideoDocument, R as assembleScriptText, S as scriptSourceAssetOf, St as DEFAULT_UNIT_TIME_MS, T as applyFieldChanges, Tt as partUnionToDraft, U as isJsonObject, V as updateEntityFields, W as createEntityId, X as derivePositionFromAbs, Y as buildSpeechHostMap, Z as fromVideoDocument, _ as audioScriptAssetHash, _t as syncAggregatedClipsTimePosition, a as createMirrorVideoDocument, at as ensureLaneTrack, b as phoneticScriptAssetHash, bt as partDurationMs, c as DocumentMutationGuard, ct as laneTrackId, d as LoroEntityDraft, dt as arrangeMainTrackSeamlessly, et as assertValidVideoDocument, f as readEntityDocument, ft as fillMainTrackTimeGaps, g as audioScriptAssetFields, gt as resolveSpeechOverlapByShiftingVideos, h as audioScriptAssetContent, ht as resolveAllSpeechOverlaps, i as MirrorVideoDocumentAdapter, it as LANE_KINDS_IN_STACK_ORDER, j as EntityTimelineProjectionError, k as EntityProjectionGraph, kt as bytesToBase64, l as LORO_ENTITY_SCHEMA, lt as solveVideoDocument, m as entityOrderGroups, mt as recalculateTimelineDuration, n as createPlainMemoryAdapter, nt as partUnionSchema, o as createMirrorVideoDocumentAdapter, ot as findLaneTrack, p as readEntityRelationRecords, pt as reassignSpeechesToVideoClipsByTime, q as isMediaAssetVariantKind, r as generatePartId, rt as videoDocumentSchema, s as readVideoDocumentFromDraft, st as hasResolvedPlacement, t as PlainMemoryAdapter, tt as validateVideoDocument, u as LoroEntityDocument, ut as cascadeAfterVideoClipChanges, v as audioScriptAssetOf, vt as TIMELINE_SKELETON_DURATION_MS, w as equalJson, wt as speedOf, x as phoneticScriptAssetOf, xt as safeDurationMs, y as phoneticScriptAssetContent, yt as isEmptyVideoClip, z as findComposedAudioScript } from "./document-7Vi1AeQc.js";
1
+ import { $ as fromVideoDocument, A as resolveEntityTimelineLayout, At as base64ToBytes, B as assembleScriptText, C as canonicalJson, Ct as safeDurationMs, D as resolveFieldPath, Dt as partUnionToDraft, E as assertFieldChanges, Et as speedOf, F as relationsTo, G as isJsonObject, H as selectAudioScriptSegment, I as ScriptCompositionError, J as hasSequence, K as createEntityId, L as assembleCaptionContent, M as validateEntityRelationTransition, N as decodeEntityRelationRows, O as projectEntityTimeline, Ot as recordEntries, P as relationsFrom, Q as derivePositionFromAbs, R as assembleEntityContent, S as scriptSourceAssetOf, St as partDurationMs, T as applyFieldChanges, Tt as effectiveVideoClipDurationMs, U as updateEntityFields, V as findComposedAudioScript, W as variantBaseEntityIds, X as buildInitialVideoDocument, Y as isMediaAssetVariantKind, Z as buildSpeechHostMap, _ as audioScriptAssetHash, _t as resolveAllSpeechOverlaps, a as createMirrorVideoDocument, at as videoDocumentSchema, b as phoneticScriptAssetHash, bt as TIMELINE_SKELETON_DURATION_MS, c as DocumentMutationGuard, ct as findLaneTrack, d as LoroEntityDraft, dt as solveVideoDocument, et as toVideoDocument, f as readEntityDocument, ft as cascadeAfterVideoClipChanges, g as audioScriptAssetFields, gt as recalculateTimelineDuration, h as audioScriptAssetContent, ht as reassignSpeechesToVideoClipsByTime, i as MirrorVideoDocumentAdapter, it as partUnionSchema, j as EntityTimelineProjectionError, jt as bytesToBase64, k as EntityProjectionGraph, kt as videoDocumentMirrorSchema, l as LORO_ENTITY_SCHEMA, lt as hasResolvedPlacement, m as entityOrderGroups, mt as fillMainTrackTimeGaps, n as createPlainMemoryAdapter, nt as assertValidVideoDocument, o as createMirrorVideoDocumentAdapter, ot as LANE_KINDS_IN_STACK_ORDER, p as readEntityRelationRecords, pt as arrangeMainTrackSeamlessly, q as createRelationId, r as generatePartId, rt as validateVideoDocument, s as readVideoDocumentFromDraft, st as ensureLaneTrack, t as PlainMemoryAdapter, tt as VideoDocumentValidationError, u as LoroEntityDocument, ut as laneTrackId, v as audioScriptAssetOf, vt as resolveSpeechOverlapByShiftingVideos, w as equalJson, wt as DEFAULT_UNIT_TIME_MS, x as phoneticScriptAssetOf, xt as isEmptyVideoClip, y as phoneticScriptAssetContent, yt as syncAggregatedClipsTimePosition, z as assemblePhoneticScriptContent } from "./document-Bx9xf40e.js";
2
2
  import { addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, t as schemas_exports } from "./schemas.js";
3
3
  import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
4
4
  import { sha256 } from "@noble/hashes/sha2.js";
@@ -216,19 +216,34 @@ function findSseFrameBoundary(buffer) {
216
216
  //#endregion
217
217
  //#region src/entity-crdt/compile-rows.ts
218
218
  /** Lower a detached ordinary-object edit against its actual causal Loro baseline. */
219
- function compileEntityRows(snapshot, rows, validate) {
219
+ function compileEntityRows(snapshot, rows, validate, options = {}) {
220
220
  const doc = new LoroDoc();
221
221
  doc.import(snapshot);
222
- return applyEntityRows(new LoroEntityDocument(doc, validate), rows);
222
+ return applyEntityRows(new LoroEntityDocument(doc, validate), rows, options);
223
+ }
224
+ /** Publish final rows as imported causal operations, outside the caller's local undo history. */
225
+ function publishEntityRows(editor, rows, options = {}) {
226
+ const from = editor.doc.oplogVersion();
227
+ const compiled = compileEntityRows(editor.doc.export({ mode: "snapshot" }), rows, void 0, options);
228
+ editor.import(compiled.update);
229
+ return {
230
+ update: editor.doc.export({
231
+ mode: "update",
232
+ from
233
+ }),
234
+ state: editor.snapshot()
235
+ };
223
236
  }
224
237
  /** Compile an edit on an existing peer, retaining its native local undo history. */
225
- function applyEntityRows(editor, rows) {
238
+ function applyEntityRows(editor, rows, options = {}) {
226
239
  const baseline = editor.snapshot();
227
240
  const before = baseline.rows;
228
241
  const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
229
242
  const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
230
243
  const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
231
244
  const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
245
+ const oldClipContent = new Map(before.relations.filter((row) => row.relationKind === "contain" && oldEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
246
+ const newClipContent = new Map(rows.relations.filter((row) => row.relationKind === "contain" && newEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
232
247
  if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
233
248
  validateEntityRelationTransition(before, rows);
234
249
  return {
@@ -255,28 +270,16 @@ function applyEntityRows(editor, rows) {
255
270
  if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
256
271
  }
257
272
  }
258
- for (const row of rows.relations) {
259
- if (oldRelations.has(row.relationId)) continue;
260
- if (row.relationKind === "generated") draft.linkGenerated({
261
- relationId: row.relationId,
262
- outputEntityId: row.endpoint0EntityId,
263
- inputEntityId: row.endpoint1EntityId,
264
- trace: row.trace
265
- });
266
- else if (row.relationKind === "clip-anchor") draft.linkClipAnchor({
267
- relationId: row.relationId,
268
- childClipEntityId: row.endpoint0EntityId,
269
- hostClipEntityId: row.endpoint1EntityId,
270
- trace: row.trace
271
- });
272
- else draft.link(row);
273
- }
273
+ for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
274
+ for (const [clipId, relationId] of newClipContent) if (oldClipContent.get(clipId) !== relationId) draft.selectClipContent(relationId);
274
275
  const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
275
276
  for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
276
277
  const next = afterOrder.get(group) ?? [];
277
278
  if (!equalJson(beforeOrder.get(group) ?? [], next)) draft.order(group, next);
278
279
  }
279
280
  for (const row of before.entities) if (!newEntities.has(row.entityId)) draft.delete(row.entityId);
281
+ for (const [slot, entityId] of Object.entries(options.projectAttachments ?? {})) draft.attach(slot, entityId);
282
+ for (const [clipEntityId, phoneticScriptEntityId] of Object.entries(options.phoneticScriptSelections ?? {})) draft.selectPhoneticScript(clipEntityId, phoneticScriptEntityId);
280
283
  }),
281
284
  state: editor.snapshot()
282
285
  };
@@ -420,14 +423,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
420
423
  };
421
424
  entities.push(track);
422
425
  }
423
- const bindings = relations.filter((row) => row.relationKind === "timeline-track" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
426
+ const bindings = relations.filter((row) => row.relationKind === "contain" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
424
427
  if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
425
428
  if (bindings.length === 0) {
426
429
  const relationId = createRelationId(idFactory("relation"));
427
430
  if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
428
431
  relations.push({
429
432
  relationId,
430
- relationKind: "timeline-track",
433
+ relationKind: "contain",
431
434
  endpoint0EntityId: timeline.entityId,
432
435
  endpoint1EntityId: track.entityId,
433
436
  metadata: {},
@@ -450,10 +453,16 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
450
453
  //#endregion
451
454
  //#region src/entity-editor/entity-graph-http-client.ts
452
455
  const MENGINE_API_PREFIX = "/api/mengine/v1";
453
- const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left, right) => {
454
- if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
455
- return left < right ? -1 : left > right ? 1 : 0;
456
- } };
456
+ var EntityGraphRevisionConflictError = class extends MengineHttpRequestError {
457
+ expectedRevision;
458
+ currentRevision;
459
+ constructor(expectedRevision, currentRevision, payload) {
460
+ super(409, payload);
461
+ this.expectedRevision = expectedRevision;
462
+ this.currentRevision = currentRevision;
463
+ this.name = "EntityGraphRevisionConflictError";
464
+ }
465
+ };
457
466
  /** Browser-safe authenticated Loro client for a document's Entity graph. */
458
467
  var EntityGraphHttpClient = class {
459
468
  options;
@@ -470,6 +479,8 @@ var EntityGraphHttpClient = class {
470
479
  }
471
480
  /**
472
481
  * Compile edits against the original causal snapshot and publish native Loro operations.
482
+ * The base revision is sent as an optimistic precondition, so a stale adoption
483
+ * fails loudly instead of letting Loro choose between concurrent selections.
473
484
  *
474
485
  * Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
475
486
  * transient create-then-remove never becomes a request to delete unknown
@@ -480,13 +491,17 @@ var EntityGraphHttpClient = class {
480
491
  const next = cloneAndValidateRows(nextRows);
481
492
  assertMediaAssetWritePolicy(baseState.rows, next);
482
493
  const compiled = compileEntityRows(base64ToBytes(baseState.loroSnapshot), next);
483
- return this.commitUpdate(bytesToBase64(compiled.update));
494
+ return this.commitUpdate(bytesToBase64(compiled.update), baseState.revision);
484
495
  }
485
- /** Send the same prepared bytes on retry; never recompile an unconfirmed edit. */
486
- async commitUpdate(update) {
496
+ /** Send prepared bytes directly; omit `expectedRevision` for immutable fact publication. */
497
+ async commitUpdate(update, expectedRevision) {
498
+ if (expectedRevision !== void 0 && !isNonNegativeSafeInteger(expectedRevision)) throw new Error("expectedRevision must be a nonnegative safe integer");
487
499
  return this.request({
488
500
  method: "POST",
489
- body: JSON.stringify({ update })
501
+ body: JSON.stringify({
502
+ update,
503
+ ...expectedRevision === void 0 ? {} : { expected_revision: expectedRevision }
504
+ })
490
505
  });
491
506
  }
492
507
  async request(init) {
@@ -495,15 +510,26 @@ var EntityGraphHttpClient = class {
495
510
  headers: this.transport.headers()
496
511
  });
497
512
  const payload = await safeReadJson(response);
498
- if (!response.ok) throw new MengineHttpRequestError(response.status, payload);
513
+ if (!response.ok) throw parseRevisionConflict(response.status, payload) ?? new MengineHttpRequestError(response.status, payload);
499
514
  return parseWireState(payload, this.options.docId);
500
515
  }
501
516
  };
517
+ function parseRevisionConflict(status, value) {
518
+ if (status !== 409 || !isRecord(value) || !isRecord(value.error) || value.error.code !== "revision_conflict") return;
519
+ const error = value.error;
520
+ if (typeof error.message !== "string" || !isNonNegativeSafeInteger(error.expected_revision) || !isNonNegativeSafeInteger(error.current_revision)) return;
521
+ return new EntityGraphRevisionConflictError(error.expected_revision, error.current_revision, { error: {
522
+ code: "revision_conflict",
523
+ message: error.message,
524
+ expected_revision: error.expected_revision,
525
+ current_revision: error.current_revision
526
+ } });
527
+ }
502
528
  function parseWireState(value, expectedDocId) {
503
529
  if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
504
530
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
505
531
  const rows = parseWireRows(value.rows);
506
- decodeEntityRelationRows(rows, numericMarkerValidation$1);
532
+ decodeEntityRelationRows(rows, {});
507
533
  const attached = value.audio_script_entity_id;
508
534
  if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
509
535
  if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
@@ -544,7 +570,7 @@ function parseWireRelation(value) {
544
570
  }
545
571
  function cloneAndValidateRows(rows) {
546
572
  const clone = structuredClone(rows);
547
- decodeEntityRelationRows(clone, numericMarkerValidation$1);
573
+ decodeEntityRelationRows(clone, {});
548
574
  return clone;
549
575
  }
550
576
  function requiredString(value, field) {
@@ -568,10 +594,6 @@ async function safeReadJson(response) {
568
594
  }
569
595
  //#endregion
570
596
  //#region src/entity-editor/media-asset-import.ts
571
- const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right) => {
572
- if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
573
- return left - right;
574
- } };
575
597
  /**
576
598
  * Import at the editor boundary, not at asset generation time. Existing
577
599
  * provenance is retained.
@@ -587,17 +609,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
587
609
  "image",
588
610
  "video",
589
611
  "audio",
590
- "voiceover"
612
+ "voiceover-recording"
591
613
  ].includes(fact.kind)) throw new Error("Unsupported media kind");
592
614
  if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
593
- if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
594
- decodeEntityRelationRows(initial, numericCoordinates);
615
+ if ((fact.kind === "audio" || fact.kind === "voiceover-recording") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
616
+ decodeEntityRelationRows(initial, {});
595
617
  assertCanonicalEditorResources(initial);
596
618
  const entities = structuredClone([...initial.entities]);
597
619
  const relations = structuredClone([...initial.relations]);
598
- const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
620
+ const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
599
621
  const entityKind = fact.kind;
600
- const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
622
+ const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
601
623
  const durationMs = fact.kind === "image" ? null : fact.durationMs;
602
624
  const asset = ensureAsset(entities, {
603
625
  system,
@@ -628,12 +650,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
628
650
  payload: { durationMs }
629
651
  });
630
652
  relations.push(assetLink(entityId, asset.entityId, idFactory));
631
- if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
653
+ if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
632
654
  const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
633
655
  if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
634
656
  relations.push({
635
657
  relationId: createRelationId(idFactory("relation")),
636
- relationKind: "phonetic-script-render",
658
+ relationKind: "derived-from",
637
659
  endpoint0EntityId: entityId,
638
660
  endpoint1EntityId: phonetic.entityId,
639
661
  metadata: {},
@@ -645,7 +667,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
645
667
  entities,
646
668
  relations
647
669
  };
648
- decodeEntityRelationRows(rows, numericCoordinates);
670
+ decodeEntityRelationRows(rows, {});
649
671
  assertCanonicalEditorResources(rows);
650
672
  return {
651
673
  rows,
@@ -788,19 +810,19 @@ var EntityTimelineEditor = class {
788
810
  return cloneRows(this.state);
789
811
  }
790
812
  insertClip(input) {
791
- const requestedOrder = input.clipPayload?.order;
813
+ const requestedOrder = input.order;
792
814
  const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
793
815
  return this.insertPlacedClip({
794
816
  trackEntityId: input.trackEntityId,
795
817
  contentEntityId: input.contentEntityId,
796
- sourceRange: input.sourceRange,
797
- duration: input.duration,
798
- placement: input.targetRange === void 0 ? {
818
+ inMs: input.inMs,
819
+ outMs: input.outMs,
820
+ placement: input.startMs === void 0 ? {
799
821
  kind: "sequential",
800
822
  order
801
823
  } : {
802
824
  kind: "absolute",
803
- targetRange: input.targetRange
825
+ startMs: input.startMs
804
826
  },
805
827
  clipPayload: input.clipPayload
806
828
  });
@@ -812,31 +834,14 @@ var EntityTimelineEditor = class {
812
834
  this.transact((draft) => {
813
835
  const { marker } = requireClipTopology(draft, input.clipEntityId);
814
836
  const payload = { ...cloneJsonObject(marker.payload, "Sequence Marker payload") };
815
- if (input.sourceRange !== void 0) payload.sourceRange = cloneJsonValue(input.sourceRange, "sourceRange");
816
- if (input.duration !== void 0) payload.duration = cloneJsonValue(input.duration, "duration");
817
- if (Object.hasOwn(input, "targetRange")) if (input.targetRange === void 0) {
818
- delete payload.targetRange;
819
- if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {
820
- const trackId = otherEndpoint(requireOneRelation(draft, createEntityId(input.clipEntityId), "track-clip"), input.clipEntityId);
821
- const clipPayload = { ...cloneJsonObject(requireEntityKind(draft, input.clipEntityId, "clip").payload, "Clip payload") };
822
- clipPayload.order ??= nextSequentialOrder(draft, trackId);
823
- replaceEntity(draft, createEntityId(input.clipEntityId), {
824
- ...requireEntityKind(draft, input.clipEntityId, "clip"),
825
- payload: clipPayload
826
- });
827
- }
828
- } else {
829
- payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
830
- const clip = requireEntityKind(draft, input.clipEntityId, "clip");
831
- const clipPayload = { ...cloneJsonObject(clip.payload, "Clip payload") };
832
- delete clipPayload.order;
833
- replaceEntity(draft, clip.entityId, {
834
- ...clip,
835
- payload: clipPayload
836
- });
837
- }
838
- if (Object.hasOwn(input, "timeRemapping")) if (input.timeRemapping === void 0) delete payload.timeRemapping;
839
- else payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
837
+ if (input.inMs !== void 0) payload.inMs = finiteNonNegative(input.inMs, "inMs");
838
+ if (input.outMs !== void 0) payload.outMs = finitePositive(input.outMs, "outMs");
839
+ if (Object.hasOwn(input, "startMs")) if (input.startMs === void 0) {
840
+ delete payload.startMs;
841
+ if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {}
842
+ } else payload.startMs = finiteNonNegative(input.startMs, "startMs");
843
+ if (Object.hasOwn(input, "rate")) if (input.rate === void 0) delete payload.rate;
844
+ else payload.rate = finitePositive(input.rate, "linear speed rate");
840
845
  replaceEntity(draft, marker.entityId, {
841
846
  ...marker,
842
847
  payload
@@ -947,28 +952,18 @@ var EntityTimelineEditor = class {
947
952
  const content = requireSequenceContent(draft, input.contentEntityId);
948
953
  const payload = {
949
954
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
950
- sourceRange: cloneJsonValue(input.sourceRange, "sourceRange"),
951
- duration: cloneJsonValue(input.duration, "duration")
955
+ ...requireWindow(input.inMs, input.outMs)
952
956
  };
953
- delete payload.targetRange;
954
- delete payload.timeRemapping;
955
- if (input.targetRange !== void 0) payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
956
- if (input.timeRemapping !== void 0) payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
957
+ delete payload.startMs;
958
+ delete payload.rate;
959
+ if (input.startMs !== void 0) payload.startMs = finiteNonNegative(input.startMs, "startMs");
960
+ if (input.rate !== void 0) payload.rate = finitePositive(input.rate, "linear speed rate");
957
961
  replaceEntity(draft, topology.marker.entityId, {
958
962
  ...topology.marker,
959
963
  payload
960
964
  });
961
- if (!hasOutgoingClipAnchor(draft, topology.clip.entityId)) {
962
- const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
963
- if (input.targetRange === void 0) clipPayload.order ??= nextSequentialOrder(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId));
964
- else delete clipPayload.order;
965
- replaceEntity(draft, topology.clip.entityId, {
966
- ...topology.clip,
967
- payload: clipPayload
968
- });
969
- }
970
- this.relinkRelation(draft, topology.markerContentRelation, {
971
- endpoint0EntityId: topology.marker.entityId,
965
+ this.relinkRelation(draft, topology.contentRelation, {
966
+ endpoint0EntityId: topology.clip.entityId,
972
967
  endpoint1EntityId: content.entityId
973
968
  });
974
969
  });
@@ -979,15 +974,11 @@ var EntityTimelineEditor = class {
979
974
  const imported = importMediaAsset(draft, input.media, this.idFactory);
980
975
  replaceRows(draft, imported.rows);
981
976
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
982
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
977
+ const window = requireWindow(input.inMs, input.outMs);
983
978
  const clipId = this.insertPlacedClipIntoDraft(draft, {
984
979
  trackEntityId: track.entityId,
985
980
  contentEntityId: imported.contentEntityId,
986
- sourceRange,
987
- duration: {
988
- mode: "fixed",
989
- value: sourceRange.end - sourceRange.start
990
- },
981
+ ...window,
991
982
  placement: input.placement,
992
983
  ...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
993
984
  ...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
@@ -1022,15 +1013,11 @@ var EntityTimelineEditor = class {
1022
1013
  for (const [offset, clip] of input.clips.entries()) {
1023
1014
  const imported = importMediaAsset(draft, clip.media, this.idFactory);
1024
1015
  replaceRows(draft, imported.rows);
1025
- const sourceRange = requirePositiveRange(clip.sourceRange, "sourceRange");
1016
+ const window = requireWindow(clip.inMs, clip.outMs);
1026
1017
  created.push(this.insertPlacedClipIntoDraft(draft, {
1027
1018
  trackEntityId: track.entityId,
1028
1019
  contentEntityId: imported.contentEntityId,
1029
- sourceRange,
1030
- duration: {
1031
- mode: "fixed",
1032
- value: sourceRange.end - sourceRange.start
1033
- },
1020
+ ...window,
1034
1021
  placement: {
1035
1022
  kind: "sequential",
1036
1023
  order: ordered.length + offset
@@ -1052,23 +1039,18 @@ var EntityTimelineEditor = class {
1052
1039
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1053
1040
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1054
1041
  replaceRows(draft, imported.rows);
1055
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
1042
+ const window = requireWindow(input.inMs, input.outMs);
1056
1043
  const markerPayloadValue = {
1057
1044
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1058
- sourceRange,
1059
- duration: {
1060
- mode: "fixed",
1061
- value: sourceRange.end - sourceRange.start
1062
- }
1045
+ ...window
1063
1046
  };
1064
- delete markerPayloadValue.timeRemapping;
1065
- resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
1047
+ delete markerPayloadValue.rate;
1066
1048
  replaceEntity(draft, topology.marker.entityId, {
1067
1049
  ...topology.marker,
1068
1050
  payload: markerPayloadValue
1069
1051
  });
1070
- this.relinkRelation(draft, topology.markerContentRelation, {
1071
- endpoint0EntityId: topology.marker.entityId,
1052
+ this.relinkRelation(draft, topology.contentRelation, {
1053
+ endpoint0EntityId: topology.clip.entityId,
1072
1054
  endpoint1EntityId: createEntityId(imported.contentEntityId)
1073
1055
  });
1074
1056
  this.resolveOneVoiceoverOverlap(draft);
@@ -1090,42 +1072,26 @@ var EntityTimelineEditor = class {
1090
1072
  setClipSpeed(input) {
1091
1073
  this.transact((draft) => {
1092
1074
  const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1093
- const content = requireSequenceContent(draft, otherEndpoint(initialTopology.markerContentRelation, initialTopology.marker.entityId));
1075
+ const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
1094
1076
  if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
1095
1077
  const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
1096
- const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
1097
- if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
1078
+ const priorWindow = markerWindow(initialPayload);
1079
+ if (input.rate === null && initialPayload.rate === void 0) return;
1098
1080
  this.normalizeMainSequential(draft);
1099
1081
  const topology = requireClipTopology(draft, input.clipEntityId);
1100
1082
  const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1101
- let sourceRange = priorSource;
1102
- if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
1103
- start: 0,
1104
- end: markerDuration(initialPayload, priorSource)
1105
- };
1106
- if (input.timeRemapping === null) {
1107
- delete payload.timeRemapping;
1108
- payload.sourceRange = sourceRange;
1109
- payload.duration = {
1110
- mode: "fixed",
1111
- value: sourceRange.end - sourceRange.start
1112
- };
1113
- } else {
1114
- const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
1115
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1083
+ const window = content.entityKind === "image" && initialPayload.rate === void 0 && input.rate !== null ? {
1084
+ inMs: 0,
1085
+ outMs: markerDuration(initialPayload, priorWindow)
1086
+ } : priorWindow;
1087
+ Object.assign(payload, window);
1088
+ if (input.rate === null) delete payload.rate;
1089
+ else {
1090
+ const rate = finitePositive(input.rate, "linear speed rate");
1091
+ const duration = Math.round((window.outMs - window.inMs) / rate);
1116
1092
  if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
1117
- payload.sourceRange = sourceRange;
1118
- payload.duration = {
1119
- mode: "fixed",
1120
- value: duration
1121
- };
1122
- payload.timeRemapping = {
1123
- kind: "linear",
1124
- rate,
1125
- ...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
1126
- };
1093
+ payload.rate = rate;
1127
1094
  }
1128
- resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
1129
1095
  replaceEntity(draft, topology.marker.entityId, {
1130
1096
  ...topology.marker,
1131
1097
  payload
@@ -1137,21 +1103,15 @@ var EntityTimelineEditor = class {
1137
1103
  this.transact((draft) => {
1138
1104
  this.normalizeMainSequential(draft);
1139
1105
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1140
- const content = requireSequenceContent(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId));
1106
+ const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
1141
1107
  if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip trimming is supported only for Video or Image content");
1142
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
1108
+ const window = requireWindow(input.inMs, input.outMs);
1143
1109
  const payload = {
1144
1110
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1145
- sourceRange
1111
+ ...window
1146
1112
  };
1147
- const rate = linearRate(payload.timeRemapping);
1148
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1113
+ const duration = markerDuration(payload, window);
1149
1114
  if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Trim produces an invalid duration");
1150
- payload.duration = {
1151
- mode: "fixed",
1152
- value: duration
1153
- };
1154
- resizeAbsoluteTarget(payload, duration);
1155
1115
  replaceEntity(draft, topology.marker.entityId, {
1156
1116
  ...topology.marker,
1157
1117
  payload
@@ -1181,15 +1141,11 @@ var EntityTimelineEditor = class {
1181
1141
  if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
1182
1142
  const imported = importMediaAsset(draft, replacement.media, this.idFactory);
1183
1143
  replaceRows(draft, imported.rows);
1184
- const sourceRange = requirePositiveRange(replacement.sourceRange, "sourceRange");
1144
+ const window = requireWindow(replacement.inMs, replacement.outMs);
1185
1145
  created.push(this.insertPlacedClipIntoDraft(draft, {
1186
1146
  trackEntityId: track.entityId,
1187
1147
  contentEntityId: imported.contentEntityId,
1188
- sourceRange,
1189
- duration: {
1190
- mode: "fixed",
1191
- value: sourceRange.end - sourceRange.start
1192
- },
1148
+ ...window,
1193
1149
  placement: {
1194
1150
  kind: "sequential",
1195
1151
  order: temporaryOrder + offset
@@ -1223,10 +1179,14 @@ var EntityTimelineEditor = class {
1223
1179
  deleteClip(input) {
1224
1180
  this.transact((draft) => {
1225
1181
  const topology = requireClipTopology(draft, input.clipEntityId);
1226
- assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["track-clip", "clip-marker"]));
1227
- assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["clip-marker", "marker-content"]));
1228
- const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "clip-marker").length === 1;
1229
- const removedRelationIds = new Set([topology.trackRelation.relationId, topology.clipMarkerRelation.relationId]);
1182
+ assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["contain", "marker"]));
1183
+ assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
1184
+ const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
1185
+ const removedRelationIds = new Set([
1186
+ topology.trackRelation.relationId,
1187
+ topology.clipMarkerRelation.relationId,
1188
+ topology.contentRelation.relationId
1189
+ ]);
1230
1190
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
1231
1191
  draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
1232
1192
  draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
@@ -1247,28 +1207,20 @@ var EntityTimelineEditor = class {
1247
1207
  if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
1248
1208
  this.applyClipPlacement(draft, childId, {
1249
1209
  kind: "absolute",
1250
- targetRange: {
1251
- start: placement.start,
1252
- end: placement.start + placement.duration
1253
- }
1210
+ startMs: placement.start
1254
1211
  });
1255
1212
  }
1256
1213
  else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
1257
1214
  for (const target of targets) this.removeClipGraph(draft, target);
1258
- normalizeSequentialOrders(draft);
1259
1215
  if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
1260
1216
  });
1261
1217
  }
1262
1218
  updateClip(input) {
1263
1219
  this.transact((draft) => {
1264
1220
  const clip = requireEntityKind(draft, input.clipEntityId, "clip");
1265
- const priorOrder = clip.payload.order;
1266
1221
  replaceEntity(draft, clip.entityId, {
1267
1222
  ...clip,
1268
- payload: {
1269
- ...cloneJsonObject(input.payload, "Clip payload"),
1270
- ...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
1271
- }
1223
+ payload: cloneJsonObject(input.payload, "Clip payload")
1272
1224
  });
1273
1225
  });
1274
1226
  }
@@ -1278,13 +1230,13 @@ var EntityTimelineEditor = class {
1278
1230
  const placement = input.placement ?? {
1279
1231
  kind: "anchored",
1280
1232
  hostClipEntityId: input.hostClipEntityId,
1281
- anchorOffset: input.anchorOffset
1233
+ startMs: input.startMs
1282
1234
  };
1283
- if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/anchorOffset, not both");
1235
+ if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.startMs !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/startMs, not both");
1284
1236
  requireVolume(input.volume);
1285
1237
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1286
1238
  replaceRows(draft, imported.rows);
1287
- const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
1239
+ const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
1288
1240
  const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
1289
1241
  this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
1290
1242
  const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
@@ -1294,14 +1246,8 @@ var EntityTimelineEditor = class {
1294
1246
  clipEntityId: input.voiceoverClipEntityId,
1295
1247
  trackEntityId: speechTrack.entityId,
1296
1248
  contentEntityId: voiceover.entityId,
1297
- sourceRange: {
1298
- start: 0,
1299
- end: input.media.durationMs
1300
- },
1301
- duration: {
1302
- mode: "fixed",
1303
- value: input.media.durationMs
1304
- },
1249
+ inMs: 0,
1250
+ outMs: input.media.durationMs,
1305
1251
  placement,
1306
1252
  clipPayload: { volume: input.volume }
1307
1253
  });
@@ -1315,8 +1261,8 @@ var EntityTimelineEditor = class {
1315
1261
  volume: input.volume
1316
1262
  }
1317
1263
  });
1318
- this.relinkRelation(draft, topology.markerContentRelation, {
1319
- endpoint0EntityId: topology.marker.entityId,
1264
+ this.relinkRelation(draft, topology.contentRelation, {
1265
+ endpoint0EntityId: topology.clip.entityId,
1320
1266
  endpoint1EntityId: voiceover.entityId
1321
1267
  });
1322
1268
  this.applyClipPlacement(draft, topology.clip.entityId, placement);
@@ -1325,14 +1271,8 @@ var EntityTimelineEditor = class {
1325
1271
  ...placed.marker,
1326
1272
  payload: {
1327
1273
  ...placed.marker.payload,
1328
- sourceRange: {
1329
- start: 0,
1330
- end: input.media.durationMs
1331
- },
1332
- duration: {
1333
- mode: "fixed",
1334
- value: input.media.durationMs
1335
- }
1274
+ inMs: 0,
1275
+ outMs: input.media.durationMs
1336
1276
  }
1337
1277
  });
1338
1278
  }
@@ -1381,11 +1321,8 @@ var EntityTimelineEditor = class {
1381
1321
  clipEntityId: input.bgmClipEntityId,
1382
1322
  trackEntityId: track.entityId,
1383
1323
  contentEntityId: imported.contentEntityId,
1384
- sourceRange: {
1385
- start: 0,
1386
- end: input.media.durationMs
1387
- },
1388
- duration: { mode: "from-source" },
1324
+ inMs: 0,
1325
+ outMs: input.media.durationMs,
1389
1326
  placement: {
1390
1327
  kind: "sequential",
1391
1328
  order: 0
@@ -1397,7 +1334,7 @@ var EntityTimelineEditor = class {
1397
1334
  ...topology.marker,
1398
1335
  payload: {
1399
1336
  ...topology.marker.payload,
1400
- durationPolicy: "timeline"
1337
+ fillTimeline: true
1401
1338
  }
1402
1339
  });
1403
1340
  return clipId;
@@ -1430,7 +1367,7 @@ var EntityTimelineEditor = class {
1430
1367
  const assembled = new EntityProjectionGraph(draft);
1431
1368
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1432
1369
  const topology = requireClipTopology(draft, clip.entityId);
1433
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1370
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1434
1371
  const prior = assembled.entity(caption.entityId).payload.style;
1435
1372
  const style = isJsonObject(prior) ? prior : {};
1436
1373
  for (const updated of updateEntityFields(draft, caption.entityId, { style: {
@@ -1467,14 +1404,8 @@ var EntityTimelineEditor = class {
1467
1404
  return this.insertPlacedClipIntoDraft(draft, {
1468
1405
  trackEntityId: track.entityId,
1469
1406
  contentEntityId: content.entityId,
1470
- sourceRange: {
1471
- start: 0,
1472
- end: durationMs
1473
- },
1474
- duration: {
1475
- mode: "fixed",
1476
- value: durationMs
1477
- },
1407
+ inMs: 0,
1408
+ outMs: durationMs,
1478
1409
  placement: input.placement,
1479
1410
  ...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
1480
1411
  });
@@ -1487,8 +1418,7 @@ var EntityTimelineEditor = class {
1487
1418
  const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
1488
1419
  const markerId = this.mintEntityId(draft);
1489
1420
  const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
1490
- delete clipPayload.order;
1491
- const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
1421
+ const markerPlacement = applyPlacementPayload(input.placement);
1492
1422
  const clip = {
1493
1423
  entityId: clipId,
1494
1424
  entityKind: "clip",
@@ -1498,13 +1428,12 @@ var EntityTimelineEditor = class {
1498
1428
  entityId: markerId,
1499
1429
  entityKind: "sequence-marker",
1500
1430
  payload: markerPayload({
1501
- sourceRange: input.sourceRange,
1502
- duration: input.duration,
1431
+ ...requireWindow(input.inMs, input.outMs),
1503
1432
  ...markerPlacement
1504
1433
  })
1505
1434
  };
1506
1435
  draft.entities.push(clip, marker);
1507
- draft.relations.push(this.createRelation(draft, "track-clip", track.entityId, clipId), this.createRelation(draft, "clip-marker", clipId, markerId), this.createRelation(draft, "marker-content", markerId, content.entityId));
1436
+ draft.relations.push(this.createRelation(draft, "contain", track.entityId, clipId), this.createRelation(draft, "marker", clipId, markerId), this.createRelation(draft, "contain", clipId, content.entityId));
1508
1437
  if (input.placement.kind === "anchored") {
1509
1438
  const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
1510
1439
  if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
@@ -1514,20 +1443,7 @@ var EntityTimelineEditor = class {
1514
1443
  }
1515
1444
  ensureRoleTrack(draft, timelineEntityId, role) {
1516
1445
  const existing = findRoleTrack(draft, timelineEntityId, role);
1517
- if (existing !== void 0) {
1518
- if (existing.payload.order === void 0) {
1519
- const replacement = {
1520
- ...existing,
1521
- payload: {
1522
- ...existing.payload,
1523
- order: roleOrder[role]
1524
- }
1525
- };
1526
- replaceEntity(draft, existing.entityId, replacement);
1527
- return replacement;
1528
- }
1529
- return existing;
1530
- }
1446
+ if (existing !== void 0) return existing;
1531
1447
  const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
1532
1448
  const track = {
1533
1449
  entityId: this.mintEntityId(draft),
@@ -1538,7 +1454,7 @@ var EntityTimelineEditor = class {
1538
1454
  }
1539
1455
  };
1540
1456
  draft.entities.push(track);
1541
- draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
1457
+ draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
1542
1458
  return track;
1543
1459
  }
1544
1460
  /** The existing pronunciation variant a voiceover consumes; composition is required. */
@@ -1549,13 +1465,13 @@ var EntityTimelineEditor = class {
1549
1465
  }
1550
1466
  /** Preserve the factual synthesis input as `[rendered Audio, PhoneticScript]`. */
1551
1467
  ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
1552
- const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
1468
+ const renders = draft.relations.filter((relation) => relation.relationKind === "derived-from" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
1553
1469
  if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
1554
1470
  if (renders[0] !== void 0) {
1555
1471
  if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
1556
1472
  throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
1557
1473
  }
1558
- draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
1474
+ draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
1559
1475
  }
1560
1476
  reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
1561
1477
  const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
@@ -1586,24 +1502,18 @@ var EntityTimelineEditor = class {
1586
1502
  clipEntityId: caption.captionClipEntityId,
1587
1503
  trackEntityId: captionTrack.entityId,
1588
1504
  contentEntityId: content.entityId,
1589
- sourceRange: {
1590
- start: 0,
1591
- end: durationMs
1592
- },
1593
- duration: {
1594
- mode: "fixed",
1595
- value: durationMs
1596
- },
1505
+ inMs: 0,
1506
+ outMs: durationMs,
1597
1507
  placement: {
1598
1508
  kind: "anchored",
1599
1509
  hostClipEntityId: voiceoverClipEntityId,
1600
- anchorOffset: startMs
1510
+ startMs
1601
1511
  }
1602
1512
  });
1603
1513
  continue;
1604
1514
  }
1605
1515
  const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
1606
- const content = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1516
+ const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1607
1517
  const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
1608
1518
  const style = caption.style === void 0 ? priorStyle : {
1609
1519
  ...priorStyle,
@@ -1622,17 +1532,16 @@ var EntityTimelineEditor = class {
1622
1532
  this.applyClipPlacement(draft, topology.clip.entityId, {
1623
1533
  kind: "anchored",
1624
1534
  hostClipEntityId: voiceoverClipEntityId,
1625
- anchorOffset: startMs
1535
+ startMs
1626
1536
  });
1627
1537
  const placed = requireClipTopology(draft, topology.clip.entityId);
1538
+ const window = markerWindow(placed.marker.payload);
1539
+ const rate = (window.outMs - window.inMs) / durationMs;
1628
1540
  replaceEntity(draft, placed.marker.entityId, {
1629
1541
  ...placed.marker,
1630
1542
  payload: {
1631
1543
  ...placed.marker.payload,
1632
- duration: {
1633
- mode: "fixed",
1634
- value: durationMs
1635
- }
1544
+ ...rate === 1 ? {} : { rate }
1636
1545
  }
1637
1546
  });
1638
1547
  }
@@ -1640,7 +1549,7 @@ var EntityTimelineEditor = class {
1640
1549
  ensureRoleTrackForClip(draft, clipEntityId, role) {
1641
1550
  const topology = requireClipTopology(draft, clipEntityId);
1642
1551
  const track = requireEntityKind(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId), "track");
1643
- const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
1552
+ const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
1644
1553
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1645
1554
  }
1646
1555
  /**
@@ -1649,13 +1558,13 @@ var EntityTimelineEditor = class {
1649
1558
  */
1650
1559
  removeCaptionClipGraph(draft, clipEntityId) {
1651
1560
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1652
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1561
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1653
1562
  const contentRelations = incidentRelations(draft, caption.entityId);
1654
- const allowed = new Set(["caption-alignment", "marker-content"]);
1563
+ const allowed = new Set(["caption-alignment", "contain"]);
1655
1564
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1656
1565
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1657
1566
  this.removeClipGraph(draft, clipEntityId);
1658
- if (incidentRelations(draft, caption.entityId).some((relation) => relation.relationKind === "marker-content")) return;
1567
+ if (containing(draft, caption.entityId, "contained").length > 0) return;
1659
1568
  const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
1660
1569
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1661
1570
  draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
@@ -1703,10 +1612,7 @@ var EntityTimelineEditor = class {
1703
1612
  const overlapMs = Math.round(prior.end - current.start);
1704
1613
  for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
1705
1614
  kind: "absolute",
1706
- targetRange: {
1707
- start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
1708
- end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
1709
- }
1615
+ startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
1710
1616
  });
1711
1617
  return;
1712
1618
  }
@@ -1715,18 +1621,16 @@ var EntityTimelineEditor = class {
1715
1621
  const topology = requireClipTopology(draft, clipEntityId);
1716
1622
  const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
1717
1623
  const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1718
- delete clipPayload.order;
1719
- delete markerPayloadValue.targetRange;
1720
- delete markerPayloadValue.anchorOffset;
1624
+ delete markerPayloadValue.startMs;
1721
1625
  const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
1722
1626
  const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
1723
1627
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1724
- if (placement.kind === "sequential") clipPayload.order = finiteNumber(placement.order, "Clip order");
1725
- else if (placement.kind === "absolute") markerPayloadValue.targetRange = cloneJsonValue(placement.targetRange, "targetRange");
1628
+ if (placement.kind === "sequential") setClipOrder(draft, topology.clip.entityId, placement.order);
1629
+ else if (placement.kind === "absolute") markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1726
1630
  else {
1727
1631
  const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
1728
1632
  if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
1729
- markerPayloadValue.anchorOffset = finiteNonNegative(placement.anchorOffset, "anchorOffset");
1633
+ markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1730
1634
  draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
1731
1635
  }
1732
1636
  replaceEntity(draft, topology.clip.entityId, {
@@ -1741,14 +1645,10 @@ var EntityTimelineEditor = class {
1741
1645
  reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
1742
1646
  const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
1743
1647
  if (hosts.length === 0) {
1744
- const child = layout.clips.get(childClipEntityId);
1745
- if (child === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
1648
+ if (layout.clips.get(childClipEntityId) === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
1746
1649
  this.applyClipPlacement(draft, childClipEntityId, {
1747
1650
  kind: "absolute",
1748
- targetRange: {
1749
- start: absoluteStart,
1750
- end: absoluteStart + child.duration
1751
- }
1651
+ startMs: absoluteStart
1752
1652
  });
1753
1653
  return;
1754
1654
  }
@@ -1756,7 +1656,7 @@ var EntityTimelineEditor = class {
1756
1656
  this.applyClipPlacement(draft, childClipEntityId, {
1757
1657
  kind: "anchored",
1758
1658
  hostClipEntityId: host.clipEntityId,
1759
- anchorOffset: Math.max(0, Math.round(absoluteStart - host.start))
1659
+ startMs: Math.max(0, Math.round(absoluteStart - host.start))
1760
1660
  });
1761
1661
  }
1762
1662
  removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
@@ -1773,12 +1673,12 @@ var EntityTimelineEditor = class {
1773
1673
  removeClipGraph(draft, clipEntityId) {
1774
1674
  const topology = requireClipTopology(draft, clipEntityId);
1775
1675
  assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
1776
- "track-clip",
1777
- "clip-marker",
1676
+ "contain",
1677
+ "marker",
1778
1678
  "clip-anchor"
1779
1679
  ]));
1780
- assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["clip-marker", "marker-content"]));
1781
- const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "clip-marker").length === 1;
1680
+ assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
1681
+ const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
1782
1682
  const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
1783
1683
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
1784
1684
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
@@ -1824,7 +1724,7 @@ var EntityTimelineEditor = class {
1824
1724
  }
1825
1725
  };
1826
1726
  function validateFinalRows(rows) {
1827
- decodeEntityRelationRows(rows, numericMarkerValidation);
1727
+ decodeEntityRelationRows(rows, {});
1828
1728
  assertCanonicalEditorResources(rows);
1829
1729
  validateTimelineHierarchy(rows);
1830
1730
  }
@@ -1832,13 +1732,13 @@ function validateTimelineHierarchy(rows) {
1832
1732
  for (const entity of rows.entities) {
1833
1733
  if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
1834
1734
  if (entity.entityKind !== "clip") continue;
1835
- const trackRelations = incidentRelations(rows, entity.entityId).filter((relation) => relation.relationKind === "track-clip");
1735
+ const trackRelations = containing(rows, entity.entityId, "contained");
1836
1736
  if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
1837
1737
  requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
1838
1738
  }
1839
1739
  }
1840
1740
  function requireTrackTimeline(rows, trackEntityId) {
1841
- const timelineRelations = incidentRelations(rows, trackEntityId).filter((relation) => relation.relationKind === "timeline-track");
1741
+ const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
1842
1742
  if (timelineRelations.filter((relation) => {
1843
1743
  const otherId = otherEndpoint(relation, trackEntityId);
1844
1744
  return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
@@ -1846,20 +1746,36 @@ function requireTrackTimeline(rows, trackEntityId) {
1846
1746
  }
1847
1747
  function requireClipTopology(rows, clipEntityId) {
1848
1748
  const clip = requireEntityKind(rows, clipEntityId, "clip");
1849
- const trackRelation = requireOneRelation(rows, clip.entityId, "track-clip");
1749
+ const trackRelation = requireOneContain(rows, clip.entityId, "contained");
1850
1750
  requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
1851
- const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "clip-marker");
1751
+ const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
1852
1752
  const marker = requireEntityKind(rows, otherEndpoint(clipMarkerRelation, clip.entityId), "sequence-marker");
1853
- const markerContentRelation = requireOneRelation(rows, marker.entityId, "marker-content");
1854
- requireSequenceContent(rows, otherEndpoint(markerContentRelation, marker.entityId));
1753
+ const contentRelation = requireOneContain(rows, clip.entityId, "container");
1754
+ requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
1855
1755
  return {
1856
1756
  clip,
1857
1757
  marker,
1858
1758
  trackRelation,
1859
1759
  clipMarkerRelation,
1860
- markerContentRelation
1760
+ contentRelation
1861
1761
  };
1862
1762
  }
1763
+ /**
1764
+ * One `contain` Relation with this entity on the stated side.
1765
+ *
1766
+ * A Clip is contained by its Track and contains its content; a Track is
1767
+ * contained by the Timeline and contains its Clips. Both are `contain`, so the
1768
+ * endpoint position is the only thing that tells them apart.
1769
+ */
1770
+ function requireOneContain(rows, entityId, side) {
1771
+ const matches = containing(rows, entityId, side);
1772
+ if (matches.length !== 1) throw new Error(`Entity "${entityId}" must be the ${side} of exactly one contain Relation`);
1773
+ return matches[0];
1774
+ }
1775
+ /** `contain` is ordered, so a lookup says which side it wants. */
1776
+ function containing(rows, entityId, side) {
1777
+ return (side === "container" ? relationsFrom : relationsTo)(rows.relations, "contain", entityId);
1778
+ }
1863
1779
  function requireOneRelation(rows, entityId, relationKind) {
1864
1780
  const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
1865
1781
  if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
@@ -1898,40 +1814,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
1898
1814
  function outgoingAnchorChildren(rows, hostIds) {
1899
1815
  return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
1900
1816
  }
1901
- function applyPlacementPayload(clipPayload, placement) {
1902
- if (placement.kind === "sequential") {
1903
- clipPayload.order = finiteNumber(placement.order, "Clip order");
1904
- return {};
1905
- }
1906
- if (placement.kind === "absolute") return { targetRange: cloneJsonValue(placement.targetRange, "targetRange") };
1907
- return { anchorOffset: finiteNonNegative(placement.anchorOffset, "anchorOffset") };
1817
+ /** What the placement puts on the Marker; a sequential one puts nothing there. */
1818
+ function applyPlacementPayload(placement) {
1819
+ if (placement.kind === "sequential") return {};
1820
+ return { startMs: finiteNonNegative(placement.startMs, "startMs") };
1908
1821
  }
1909
1822
  function requireAvailableEntityId(rows, value) {
1910
1823
  const entityId = createEntityId(value);
1911
1824
  if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
1912
1825
  return entityId;
1913
1826
  }
1827
+ /**
1828
+ * Put a Clip at `order` among the Clips its Track contains.
1829
+ *
1830
+ * Where a Clip sits is the position of its membership Relation, so moving one
1831
+ * is moving that Relation — nothing on the Clip changes, and there are no
1832
+ * sibling positions left to keep in step.
1833
+ */
1914
1834
  function setClipOrder(rows, clipEntityId, order) {
1915
- const clip = requireEntityKind(rows, clipEntityId, "clip");
1916
- replaceEntity(rows, clip.entityId, {
1917
- ...clip,
1918
- payload: {
1919
- ...clip.payload,
1920
- order
1921
- }
1922
- });
1923
- }
1924
- function normalizeSequentialOrders(rows) {
1925
- for (const track of rows.entities.filter((entity) => entity.entityKind === "track")) rows.relations.filter((relation) => relation.relationKind === "track-clip" && relation.endpoint0EntityId === track.entityId).map((relation) => rows.entities.find((entity) => entity.entityId === relation.endpoint1EntityId)).filter((entity) => entity?.entityKind === "clip" && typeof entity.payload.order === "number").sort((left, right) => Number(left.payload.order) - Number(right.payload.order)).forEach((clip, order) => setClipOrder(rows, clip.entityId, order));
1835
+ const membership = relationsTo(rows.relations, "contain", clipEntityId)[0];
1836
+ if (membership === void 0) throw new Error(`Clip "${clipEntityId}" is contained by no Track`);
1837
+ const slots = [];
1838
+ const siblings = [];
1839
+ for (const [index, relation] of rows.relations.entries()) {
1840
+ if (relation.relationKind !== "contain" || relation.endpoint0EntityId !== membership.endpoint0EntityId) continue;
1841
+ slots.push(index);
1842
+ siblings.push(relation);
1843
+ }
1844
+ const from = siblings.indexOf(membership);
1845
+ const moved = siblings.filter((relation) => relation !== membership);
1846
+ moved.splice(Math.max(0, Math.min(order, moved.length)), 0, membership);
1847
+ if (from < 0) return;
1848
+ slots.forEach((slot, at) => rows.relations[slot] = moved[at]);
1926
1849
  }
1927
1850
  function findRoleTrack(rows, timelineEntityId, role) {
1928
1851
  const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
1929
- const tracks = incidentRelations(rows, timeline.entityId).filter((relation) => relation.relationKind === "timeline-track").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
1852
+ const tracks = containing(rows, timeline.entityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
1930
1853
  if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
1931
1854
  return tracks[0];
1932
1855
  }
1933
1856
  function clipsOnTrack(rows, trackEntityId) {
1934
- return incidentRelations(rows, trackEntityId).filter((relation) => relation.relationKind === "track-clip").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
1857
+ return containing(rows, trackEntityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
1935
1858
  }
1936
1859
  function clipRole(rows, clipEntityId) {
1937
1860
  const topology = requireClipTopology(rows, clipEntityId);
@@ -1964,35 +1887,27 @@ function requireVolume(value) {
1964
1887
  if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
1965
1888
  return volume;
1966
1889
  }
1967
- function requirePositiveRange(value, label) {
1968
- if (!isJsonObject(value)) throw new Error(`${label} must be an object`);
1969
- const start = finiteNonNegative(value.start, `${label}.start`);
1970
- const end = finitePositive(value.end, `${label}.end`);
1971
- if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must be a positive whole-millisecond range`);
1890
+ function requireWindow(inMs, outMs, label = "Sequence Marker") {
1891
+ const start = finiteNonNegative(inMs, `${label} inMs`);
1892
+ const end = finitePositive(outMs, `${label} outMs`);
1893
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must state a positive whole-millisecond window`);
1972
1894
  return {
1973
- start,
1974
- end
1895
+ inMs: start,
1896
+ outMs: end
1975
1897
  };
1976
1898
  }
1977
- function markerDuration(payload, sourceRange) {
1978
- const duration = payload.duration;
1979
- if (!isJsonObject(duration)) throw new Error("Sequence Marker duration must be an object");
1980
- if (duration.mode === "from-source") return sourceRange.end - sourceRange.start;
1981
- if (duration.mode !== "fixed") throw new Error("Sequence Marker duration mode is unsupported");
1982
- return finitePositive(duration.value, "Sequence Marker fixed duration");
1983
- }
1984
- function linearRate(value) {
1985
- if (value === void 0) return 1;
1986
- if (!isJsonObject(value) || value.kind !== "linear") throw new Error("Only linear Clip time remapping is supported");
1987
- return finitePositive(value.rate, "linear speed rate");
1988
- }
1989
- function resizeAbsoluteTarget(payload, duration) {
1990
- if (payload.targetRange === void 0) return;
1991
- const target = requirePositiveRange(payload.targetRange, "targetRange");
1992
- payload.targetRange = {
1993
- start: target.start,
1994
- end: target.start + duration
1995
- };
1899
+ function markerWindow(payload) {
1900
+ return requireWindow(payload.inMs, payload.outMs);
1901
+ }
1902
+ /**
1903
+ * How long the Clip runs. The window and the rate are the only stored facts;
1904
+ * the played length follows from them rather than being kept in step by hand.
1905
+ */
1906
+ function markerDuration(payload, window = markerWindow(payload)) {
1907
+ return Math.round((window.outMs - window.inMs) / markerRate(payload));
1908
+ }
1909
+ function markerRate(payload) {
1910
+ return payload.rate === void 0 ? 1 : finitePositive(payload.rate, "linear speed rate");
1996
1911
  }
1997
1912
  function replaceRows(rows, replacement) {
1998
1913
  rows.entities = structuredClone([...replacement.entities]);
@@ -2003,9 +1918,9 @@ function finiteNonNegative(value, label) {
2003
1918
  if (result < 0) throw new Error(`${label} must be non-negative`);
2004
1919
  return result;
2005
1920
  }
1921
+ /** How many Clips the Track already holds — the index a new one lands at. */
2006
1922
  function nextSequentialOrder(rows, trackEntityId) {
2007
- const orders = rows.relations.filter((relation) => relation.relationKind === "track-clip" && relation.endpoint0EntityId === trackEntityId).map((relation) => rows.entities.find((entity) => entity.entityId === relation.endpoint1EntityId)?.payload.order).filter((order) => typeof order === "number" && Number.isFinite(order));
2008
- return orders.length === 0 ? 0 : Math.max(...orders) + 1;
1923
+ return relationsFrom(rows.relations, "contain", trackEntityId).length;
2009
1924
  }
2010
1925
  function otherEndpoint(relation, entityId) {
2011
1926
  if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
@@ -2025,19 +1940,12 @@ function cloneJsonObject(value, label) {
2025
1940
  if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
2026
1941
  return cloned;
2027
1942
  }
2028
- function cloneJsonValue(value, label) {
2029
- return cloneJsonObject({ value }, label).value;
2030
- }
2031
1943
  function cloneRows(rows) {
2032
1944
  return {
2033
1945
  entities: rows.entities.map((entity) => structuredClone(entity)),
2034
1946
  relations: rows.relations.map((relation) => structuredClone(relation))
2035
1947
  };
2036
1948
  }
2037
- const numericMarkerValidation = { compareMarkerPoints: (_marker, _range, left, right) => {
2038
- if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
2039
- return left - right;
2040
- } };
2041
1949
  //#endregion
2042
1950
  //#region src/entity-editor/revise-audio-script.ts
2043
1951
  /** Revise text as an independent authored document, never as a version-family mutation. */
@@ -2054,26 +1962,8 @@ function reviseAudioScript(rows, input) {
2054
1962
  ...input,
2055
1963
  captionUsages: input.captionUsages ?? []
2056
1964
  });
2057
- const prior = rows.relations.filter((r) => r.relationKind === "generated" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId);
2058
- if (prior.length) {
2059
- if (prior.length !== 1 || prior[0].trace.signature !== signature) throw new Error("AudioScript edit identity conflict");
2060
- const captionUsages = (input.captionUsages ?? []).map((usage, index) => {
2061
- const edge = rows.relations.find((r) => r.relationKind === "marker-content" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId && r.trace.usageIndex === index);
2062
- if (!edge || edge.endpoint0EntityId !== usage.markerEntityId) throw new Error("Caption usage conflict after revision");
2063
- return {
2064
- clipEntityId: usage.clipEntityId,
2065
- markerEntityId: usage.markerEntityId,
2066
- previousCaptionEntityId: usage.captionEntityId,
2067
- captionEntityId: String(edge.endpoint1EntityId)
2068
- };
2069
- });
2070
- return {
2071
- rows,
2072
- audioScriptEntityId: String(prior[0].endpoint0EntityId),
2073
- captionUsages,
2074
- changed: false
2075
- };
2076
- }
1965
+ const prior = rows.relations.filter((r) => r.relationKind === "derived-from" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId);
1966
+ if (prior.length && (prior.length !== 1 || prior[0].trace.signature !== signature)) throw new Error("AudioScript edit identity conflict");
2077
1967
  const source = rows.entities.find((row) => row.entityId === input.sourceAudioScriptEntityId);
2078
1968
  if (source?.entityKind !== "audio-script") throw new Error("AudioScript source is missing");
2079
1969
  assembleScriptText({
@@ -2094,39 +1984,37 @@ function reviseAudioScript(rows, input) {
2094
1984
  text
2095
1985
  };
2096
1986
  });
2097
- if (!changedSegments.size) return {
2098
- rows,
2099
- audioScriptEntityId: source.entityId,
2100
- captionUsages: [],
2101
- changed: false
2102
- };
1987
+ if (!changedSegments.size) {
1988
+ if (prior.length) throw new Error("AudioScript edit identity conflict");
1989
+ return {
1990
+ rows,
1991
+ audioScriptEntityId: source.entityId,
1992
+ captionUsages: [],
1993
+ changed: false
1994
+ };
1995
+ }
2103
1996
  delete payload.assetContentHash;
2104
1997
  const id = hash(input.editId);
2105
1998
  const audioScriptEntityId = createEntityId(`entity_script_revision_${id}`);
2106
- const result = {
2107
- entities: [...rows.entities, {
2108
- entityId: audioScriptEntityId,
2109
- entityKind: "audio-script",
2110
- payload
2111
- }],
2112
- relations: [...rows.relations, {
2113
- relationId: createRelationId(`relation_script_revision_${id}`),
2114
- relationKind: "generated",
2115
- endpoint0EntityId: audioScriptEntityId,
2116
- endpoint1EntityId: source.entityId,
2117
- metadata: {},
2118
- trace: {
2119
- command: "revise-audio-script",
2120
- editId: input.editId,
2121
- signature
2122
- }
2123
- }]
1999
+ const audioScript = {
2000
+ entityId: audioScriptEntityId,
2001
+ entityKind: "audio-script",
2002
+ payload
2003
+ };
2004
+ const provenance = {
2005
+ relationId: createRelationId(`relation_script_revision_${id}`),
2006
+ relationKind: "derived-from",
2007
+ endpoint0EntityId: audioScriptEntityId,
2008
+ endpoint1EntityId: source.entityId,
2009
+ metadata: {},
2010
+ trace: {
2011
+ command: "revise-audio-script",
2012
+ editId: input.editId,
2013
+ signature
2014
+ }
2124
2015
  };
2125
- const entities = [...result.entities];
2126
- let relations = [...result.relations];
2127
- const captionUsages = [];
2128
2016
  const selectedMarkers = /* @__PURE__ */ new Set();
2129
- for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2017
+ const planCaption = (usage, index, targetAudioScriptEntityId) => {
2130
2018
  if (selectedMarkers.has(usage.markerEntityId)) throw new Error("Duplicate caption usage");
2131
2019
  selectedMarkers.add(usage.markerEntityId);
2132
2020
  const caption = rows.entities.find((row) => row.entityId === usage.captionEntityId);
@@ -2137,52 +2025,131 @@ function reviseAudioScript(rows, input) {
2137
2025
  if (isJsonObject(selection) && selection.textRange !== void 0 && typeof selection.segmentId === "string" && changedSegments.has(selection.segmentId) && usage.selection === void 0) throw new Error("Changed partial Caption requires an explicit selection");
2138
2026
  const bases = caption.payload.baseEntityIds;
2139
2027
  if (!Array.isArray(bases) || !bases.includes(source.entityId)) throw new Error("Caption requires a direct AudioScript base");
2140
- const clipMarkers = rows.relations.filter((r) => r.relationKind === "clip-marker" && r.endpoint0EntityId === usage.clipEntityId);
2141
- const markerContents = rows.relations.filter((r) => r.relationKind === "marker-content" && r.endpoint0EntityId === usage.markerEntityId);
2142
- const markerOwners = rows.relations.filter((r) => (r.relationKind === "clip-marker" || r.relationKind === "axvideo-marker") && r.endpoint1EntityId === usage.markerEntityId);
2143
- if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 || markerContents.length !== 1 || markerContents[0].endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict");
2028
+ const clipMarkers = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint0EntityId === usage.clipEntityId);
2029
+ const clipContents = rows.relations.filter((r) => r.relationKind === "contain" && r.endpoint0EntityId === usage.clipEntityId);
2030
+ const markerOwners = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint1EntityId === usage.markerEntityId);
2031
+ if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 || clipContents.length !== 1) throw new Error("Caption usage conflict");
2144
2032
  const newCaptionId = createEntityId(`entity_caption_revision_${id}_${index}`);
2145
- const clone = {
2146
- ...caption,
2147
- entityId: newCaptionId,
2148
- payload: {
2149
- ...structuredClone(caption.payload),
2150
- baseEntityIds: bases.map((base) => base === source.entityId ? audioScriptEntityId : base),
2151
- ...usage.selection === void 0 ? {} : { selection: structuredClone(usage.selection) }
2152
- }
2153
- };
2154
- entities.push(clone);
2155
- assembleCaptionContent({
2156
- entities,
2157
- relations
2158
- }, newCaptionId);
2159
- relations = relations.filter((r) => r.relationId !== markerContents[0].relationId);
2160
- relations.push({
2161
- ...markerContents[0],
2033
+ return {
2034
+ clone: {
2035
+ ...caption,
2036
+ entityId: newCaptionId,
2037
+ payload: {
2038
+ ...structuredClone(caption.payload),
2039
+ baseEntityIds: bases.map((base) => base === source.entityId ? targetAudioScriptEntityId : base),
2040
+ ...usage.selection === void 0 ? {} : { selection: structuredClone(usage.selection) }
2041
+ }
2042
+ },
2043
+ clipContent: clipContents[0],
2044
+ newCaptionId,
2162
2045
  relationId: createRelationId(`relation_caption_revision_${id}_${index}`),
2163
- endpoint1EntityId: newCaptionId,
2164
2046
  trace: {
2165
2047
  command: "revise-audio-script",
2166
2048
  editId: input.editId,
2167
2049
  usageIndex: index
2168
2050
  }
2051
+ };
2052
+ };
2053
+ if (prior.length) {
2054
+ const currentAudioScriptEntityId = prior[0].endpoint0EntityId;
2055
+ if (!equalJson(prior[0], {
2056
+ ...provenance,
2057
+ relationId: prior[0].relationId,
2058
+ endpoint0EntityId: currentAudioScriptEntityId
2059
+ })) throw new Error("AudioScript edit identity conflict");
2060
+ const preservedScripts = rows.entities.filter((row) => row.entityId === currentAudioScriptEntityId);
2061
+ const preservedScript = preservedScripts[0]?.entityKind === "audio-script" ? {
2062
+ ...preservedScripts[0],
2063
+ payload: Object.fromEntries(Object.entries(preservedScripts[0].payload).filter(([key]) => key !== "assetContentHash"))
2064
+ } : preservedScripts[0];
2065
+ if (preservedScripts.length !== 1 || !equalJson(preservedScript, {
2066
+ ...audioScript,
2067
+ entityId: currentAudioScriptEntityId
2068
+ })) throw new Error("AudioScript edit identity conflict");
2069
+ let relations = [...rows.relations];
2070
+ let changed = false;
2071
+ const captionUsages = [];
2072
+ for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2073
+ const plan = planCaption(usage, index, currentAudioScriptEntityId);
2074
+ const preservedCaptions = rows.entities.filter((row) => row.entityId === plan.newCaptionId);
2075
+ if (preservedCaptions.length !== 1 || !equalJson(preservedCaptions[0], plan.clone)) throw new Error("Caption usage conflict after revision");
2076
+ const plannedEdge = {
2077
+ relationId: plan.relationId,
2078
+ relationKind: "contain",
2079
+ endpoint0EntityId: plan.clipContent.endpoint0EntityId,
2080
+ endpoint1EntityId: plan.newCaptionId,
2081
+ metadata: structuredClone(plan.clipContent.metadata),
2082
+ trace: plan.trace
2083
+ };
2084
+ if (plan.clipContent.relationId === plan.relationId) {
2085
+ if (!equalJson(plan.clipContent, plannedEdge)) throw new Error("Caption usage conflict after revision");
2086
+ } else {
2087
+ if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict after revision");
2088
+ relations = relations.filter((row) => row.relationId !== plan.clipContent.relationId);
2089
+ relations.push(plannedEdge);
2090
+ changed = true;
2091
+ }
2092
+ captionUsages.push({
2093
+ clipEntityId: usage.clipEntityId,
2094
+ markerEntityId: usage.markerEntityId,
2095
+ previousCaptionEntityId: usage.captionEntityId,
2096
+ captionEntityId: plan.newCaptionId
2097
+ });
2098
+ }
2099
+ if (!changed) return {
2100
+ rows,
2101
+ audioScriptEntityId: currentAudioScriptEntityId,
2102
+ captionUsages,
2103
+ changed: false
2104
+ };
2105
+ const revised = {
2106
+ entities: rows.entities,
2107
+ relations
2108
+ };
2109
+ decodeAndValidate(rows, revised);
2110
+ return {
2111
+ rows: revised,
2112
+ audioScriptEntityId: currentAudioScriptEntityId,
2113
+ captionUsages,
2114
+ changed: true
2115
+ };
2116
+ }
2117
+ const result = {
2118
+ entities: [...rows.entities, audioScript],
2119
+ relations: [...rows.relations, provenance]
2120
+ };
2121
+ const entities = [...result.entities];
2122
+ let relations = [...result.relations];
2123
+ const captionUsages = [];
2124
+ for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2125
+ const plan = planCaption(usage, index, audioScriptEntityId);
2126
+ if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict");
2127
+ entities.push(plan.clone);
2128
+ assembleCaptionContent({
2129
+ entities,
2130
+ relations
2131
+ }, plan.newCaptionId);
2132
+ relations = relations.filter((r) => r.relationId !== plan.clipContent.relationId);
2133
+ relations.push({
2134
+ relationId: plan.relationId,
2135
+ relationKind: "contain",
2136
+ endpoint0EntityId: plan.clipContent.endpoint0EntityId,
2137
+ endpoint1EntityId: plan.newCaptionId,
2138
+ metadata: structuredClone(plan.clipContent.metadata),
2139
+ trace: plan.trace
2169
2140
  });
2170
2141
  captionUsages.push({
2171
2142
  clipEntityId: usage.clipEntityId,
2172
2143
  markerEntityId: usage.markerEntityId,
2173
2144
  previousCaptionEntityId: usage.captionEntityId,
2174
- captionEntityId: newCaptionId
2145
+ captionEntityId: plan.newCaptionId
2175
2146
  });
2176
2147
  }
2177
2148
  const revised = {
2178
2149
  entities,
2179
2150
  relations
2180
2151
  };
2181
- decodeEntityRelationRows(revised, { compareMarkerPoints: (_marker, _range, left, right) => {
2182
- if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
2183
- return left - right;
2184
- } });
2185
- validateEntityRelationTransition(rows, revised);
2152
+ decodeAndValidate(rows, revised);
2186
2153
  return {
2187
2154
  rows: revised,
2188
2155
  audioScriptEntityId,
@@ -2190,6 +2157,10 @@ function reviseAudioScript(rows, input) {
2190
2157
  changed: true
2191
2158
  };
2192
2159
  }
2160
+ function decodeAndValidate(previous, revised) {
2161
+ decodeEntityRelationRows(revised, {});
2162
+ validateEntityRelationTransition(previous, revised);
2163
+ }
2193
2164
  function hash(value) {
2194
2165
  return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
2195
2166
  }
@@ -2264,7 +2235,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2264
2235
  relations
2265
2236
  }, {
2266
2237
  assetId: speech.origin_speech_id,
2267
- kind: "voiceover",
2238
+ kind: "voiceover-recording",
2268
2239
  durationMs: speech.media_duration_ms,
2269
2240
  storageKey: speech.audio_storage_key,
2270
2241
  voice: {
@@ -2302,7 +2273,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2302
2273
  ...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
2303
2274
  }, oldTrack.id);
2304
2275
  trackIds.set(oldTrack, track);
2305
- link("timeline-track", timeline, track);
2276
+ link("contain", timeline, track);
2306
2277
  }
2307
2278
  for (const oldTrack of legacy.tracks ?? []) {
2308
2279
  const track = trackIds.get(oldTrack);
@@ -2312,7 +2283,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2312
2283
  const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
2313
2284
  const clip = addEntity("clip", {
2314
2285
  ...volume === void 0 ? {} : { volume },
2315
- ...item.time_position.mode === "sequential" && !(role === "video_clip" && normalizedMainRanges !== void 0) || role === "bgm" ? { order: role === "bgm" ? 0 : order } : {}
2286
+ order: role === "bgm" ? 0 : order
2316
2287
  }, item.part_id);
2317
2288
  let content = contentByPartId.get(item.part_id);
2318
2289
  let duration;
@@ -2323,37 +2294,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2323
2294
  const fact = facts.get(video.origin_media_id);
2324
2295
  const rate = speedOf(video.speed_shift);
2325
2296
  markerPayload = {
2326
- sourceRange: {
2327
- start: video.play_in,
2328
- end: video.play_out
2329
- },
2330
- duration: {
2331
- mode: "fixed",
2332
- value: duration
2333
- },
2334
- ...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { timeRemapping: {
2335
- kind: "linear",
2336
- mode: video.speed_shift?.mode ?? "constant",
2337
- rate
2338
- } } : {}
2297
+ inMs: video.play_in,
2298
+ outMs: video.play_out,
2299
+ ...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
2339
2300
  };
2340
2301
  } else if (part.speech !== void 0) {
2341
2302
  duration = part.speech.media_duration_ms;
2342
2303
  markerPayload = {
2343
- sourceRange: {
2344
- start: 0,
2345
- end: duration
2346
- },
2347
- duration: {
2348
- mode: "fixed",
2349
- value: duration
2350
- }
2304
+ inMs: 0,
2305
+ outMs: duration
2351
2306
  };
2352
2307
  } else if (part.caption !== void 0) {
2353
2308
  const caption = part.caption;
2354
2309
  duration = caption.initial_duration_ms;
2355
2310
  const voiceover = contentByPartId.get(caption.speech_part_id);
2356
- const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceover);
2311
+ const render = relations.find((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceover);
2357
2312
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2358
2313
  const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2359
2314
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
@@ -2375,14 +2330,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2375
2330
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2376
2331
  });
2377
2332
  markerPayload = {
2378
- sourceRange: {
2379
- start: 0,
2380
- end: duration
2381
- },
2382
- duration: {
2383
- mode: "fixed",
2384
- value: duration
2385
- }
2333
+ inMs: 0,
2334
+ outMs: duration
2386
2335
  };
2387
2336
  } else {
2388
2337
  const bgm = part.bgm;
@@ -2390,35 +2339,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2390
2339
  if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
2391
2340
  duration = fact.durationMs;
2392
2341
  markerPayload = {
2393
- sourceRange: {
2394
- start: 0,
2395
- end: duration
2396
- },
2397
- duration: { mode: "from-source" },
2398
- durationPolicy: "timeline"
2342
+ inMs: 0,
2343
+ outMs: duration,
2344
+ fillTimeline: true
2399
2345
  };
2400
2346
  }
2401
2347
  if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
2402
2348
  const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2403
2349
  if (normalizedMainRange !== void 0) markerPayload = {
2404
2350
  ...markerPayload,
2405
- targetRange: normalizedMainRange
2351
+ startMs: normalizedMainRange.start
2406
2352
  };
2407
- else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
2353
+ else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
2408
2354
  ...markerPayload,
2409
- targetRange: {
2410
- start: item.time_position.offsetMs,
2411
- end: item.time_position.offsetMs + duration
2412
- }
2413
- };
2414
- else if (item.time_position.mode === "anchored") markerPayload = {
2415
- ...markerPayload,
2416
- anchorOffset: item.time_position.offsetMs
2355
+ startMs: item.time_position.offsetMs
2417
2356
  };
2418
2357
  const marker = addEntity("sequence-marker", markerPayload);
2419
- link("track-clip", track, clip);
2420
- link("clip-marker", clip, marker);
2421
- link("marker-content", marker, content);
2358
+ link("contain", track, clip);
2359
+ link("marker", clip, marker);
2360
+ link("contain", clip, content);
2422
2361
  if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
2423
2362
  }
2424
2363
  }
@@ -2439,7 +2378,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2439
2378
  for (const part of Object.values(view.part_library ?? {})) {
2440
2379
  const speech = part.speech;
2441
2380
  if (speech?.phoneme_script === null) delete speech.phoneme_script;
2442
- if (speech !== void 0) delete speech.origin_asset_system;
2381
+ const speed = part.video_clip?.speed_shift;
2382
+ if (speed !== void 0) delete speed.mode;
2443
2383
  }
2444
2384
  return { view };
2445
2385
  };
@@ -2455,22 +2395,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2455
2395
  const sequentialOrders = [];
2456
2396
  for (const item of oldTrack.items ?? []) {
2457
2397
  const clip = rows.entities.find((row) => row.entityId === item.part_id && row.entityKind === "clip");
2458
- const binding = rows.relations.filter((row) => row.relationKind === "clip-marker" && row.endpoint0EntityId === item.part_id && rows.entities.some((entity) => entity.entityId === row.endpoint1EntityId && entity.entityKind === "sequence-marker"));
2398
+ const binding = rows.relations.filter((row) => row.relationKind === "marker" && row.endpoint0EntityId === item.part_id && rows.entities.some((entity) => entity.entityId === row.endpoint1EntityId && entity.entityKind === "sequence-marker"));
2459
2399
  const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
2460
2400
  if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
2461
2401
  const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
2462
2402
  const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2463
2403
  if (normalizedMainRange !== void 0) {
2464
- if (clip.payload.order !== void 0 || !isExactRange(marker.payload.targetRange, normalizedMainRange) || marker.payload.anchorOffset !== void 0 || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
2404
+ if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
2465
2405
  } else if (oldTrack.parts_kind === "bgm") {
2466
- if (clip.payload.order !== 0 || marker.payload.targetRange !== void 0 || marker.payload.anchorOffset !== void 0 || marker.payload.durationPolicy !== "timeline" || anchors.length !== 0) fail("Migration must preserve timeline-filling BGM semantics");
2406
+ if (clip.payload.order !== 0 || marker.payload.startMs !== void 0 || marker.payload.fillTimeline !== true || anchors.length !== 0) fail("Migration must preserve timeline-filling BGM semantics");
2467
2407
  } else if (item.time_position.mode === "sequential") {
2468
- if (marker.payload.targetRange !== void 0 || marker.payload.anchorOffset !== void 0 || typeof clip.payload.order !== "number" || !Number.isFinite(clip.payload.order) || anchors.length !== 0) fail("Migration must preserve sequential placement semantics");
2408
+ if (marker.payload.startMs !== void 0 || typeof clip.payload.order !== "number" || !Number.isFinite(clip.payload.order) || anchors.length !== 0) fail("Migration must preserve sequential placement semantics");
2469
2409
  sequentialOrders.push(clip.payload.order);
2470
2410
  } else if (item.time_position.mode === "absolute") {
2471
- const target = marker.payload.targetRange;
2472
- if (clip.payload.order !== void 0 || !isRangeStartingAt(target, item.time_position.offsetMs) || marker.payload.anchorOffset !== void 0 || anchors.length !== 0) fail("Migration must preserve absolute placement semantics");
2473
- } else if (clip.payload.order !== void 0 || marker.payload.targetRange !== void 0 || marker.payload.anchorOffset !== item.time_position.offsetMs || anchors.length !== 1 || anchors[0].endpoint1EntityId !== item.time_position.anchorPartId) fail("Migration must preserve anchored placement semantics");
2411
+ if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 0) fail("Migration must preserve absolute placement semantics");
2412
+ } else if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 1 || anchors[0].endpoint1EntityId !== item.time_position.anchorPartId) fail("Migration must preserve anchored placement semantics");
2474
2413
  }
2475
2414
  if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
2476
2415
  }
@@ -2711,7 +2650,7 @@ function requirePart(document, partId) {
2711
2650
  return part;
2712
2651
  }
2713
2652
  function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
2714
- const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceoverAudioEntityId);
2653
+ const renders = relations.filter((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceoverAudioEntityId);
2715
2654
  if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
2716
2655
  if (renders.length === 1) {
2717
2656
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
@@ -2739,7 +2678,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
2739
2678
  baseEntityIds: [script.entityId],
2740
2679
  ...phonemeScript === void 0 ? {} : { phonemeScript }
2741
2680
  });
2742
- link("phonetic-script-render", voiceoverAudioEntityId, phonetic);
2681
+ link("derived-from", voiceoverAudioEntityId, phonetic);
2743
2682
  return phonetic;
2744
2683
  }
2745
2684
  function composedAudioScript(phonetic, entities) {
@@ -2801,12 +2740,6 @@ function assertCaptionStyle(style) {
2801
2740
  "position_y"
2802
2741
  ], "Caption style");
2803
2742
  }
2804
- function isRangeStartingAt(value, expectedStart) {
2805
- return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expectedStart;
2806
- }
2807
- function isExactRange(value, expected) {
2808
- return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expected.start && value.end === expected.end;
2809
- }
2810
2743
  function requireFinite(value, label) {
2811
2744
  if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
2812
2745
  return value;
@@ -5314,4 +5247,4 @@ function serverCovers(serverVV, target) {
5314
5247
  return covers(server, target);
5315
5248
  }
5316
5249
  //#endregion
5317
- export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
5250
+ export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityGraphRevisionConflictError, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, publishEntityRows, readDocumentAudioScript, readEntityDocument, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };