@mengine/medeo-client 2.0.1-alpha.22 → 2.0.1-alpha.24

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 partUnionSchema, A as decodeEntityRelationRows, B as isJsonObject, C as applyFieldChanges, Ct as recordEntries, D as EntityProjectionGraph, E as projectEntityTimeline, Et as bytesToBase64, F as assembleScriptText, G as buildInitialVideoDocument, H as createRelationId, I as findComposedAudioScript, J as fromVideoDocument, K as buildSpeechHostMap, L as selectAudioScriptSegment, M as assembleCaptionContent, N as assembleEntityContent, O as resolveEntityTimelineLayout, P as assemblePhoneticScriptContent, Q as validateVideoDocument, R as updateEntityFields, S as equalJson, St as partUnionToDraft, T as resolveFieldPath, Tt as base64ToBytes, U as hasSequence, V as createEntityId, W as isMediaAssetVariantKind, X as VideoDocumentValidationError, Y as toVideoDocument, Z as assertValidVideoDocument, _ as audioScriptAssetOf, _t as partDurationMs, a as createMirrorVideoDocument, at as laneTrackId, b as phoneticScriptAssetOf, bt as effectiveVideoClipDurationMs, c as DocumentMutationGuard, ct as arrangeMainTrackSeamlessly, d as LoroEntityDraft, dt as recalculateTimelineDuration, et as videoDocumentSchema, f as readEntityDocument, ft as resolveAllSpeechOverlaps, g as audioScriptAssetHash, gt as isEmptyVideoClip, h as audioScriptAssetFields, ht as TIMELINE_SKELETON_DURATION_MS, i as MirrorVideoDocumentAdapter, it as hasResolvedPlacement, j as ScriptCompositionError, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as fillMainTrackTimeGaps, m as audioScriptAssetContent, mt as syncAggregatedClipsTimePosition, n as createPlainMemoryAdapter, nt as ensureLaneTrack, o as createMirrorVideoDocumentAdapter, ot as solveVideoDocument, p as entityOrderGroups, pt as resolveSpeechOverlapByShiftingVideos, q as derivePositionFromAbs, r as generatePartId, rt as findLaneTrack, s as readVideoDocumentFromDraft, st as cascadeAfterVideoClipChanges, t as PlainMemoryAdapter, tt as LANE_KINDS_IN_STACK_ORDER, u as LoroEntityDocument, ut as reassignSpeechesToVideoClipsByTime, v as phoneticScriptAssetContent, vt as safeDurationMs, w as assertFieldChanges, wt as videoDocumentMirrorSchema, x as scriptSourceAssetOf, xt as speedOf, y as phoneticScriptAssetHash, yt as DEFAULT_UNIT_TIME_MS, z as variantBaseEntityIds } from "./document-BB8U25jt.js";
1
+ import { $ as assertValidVideoDocument, A as decodeEntityRelationRows, B as updateEntityFields, C as applyFieldChanges, Ct as speedOf, D as EntityProjectionGraph, Dt as base64ToBytes, E as projectEntityTimeline, Et as videoDocumentMirrorSchema, F as assembleEntityContent, G as hasSequence, H as isJsonObject, I as assemblePhoneticScriptContent, J as buildSpeechHostMap, K as isMediaAssetVariantKind, L as assembleScriptText, M as relationsTo, N as ScriptCompositionError, O as resolveEntityTimelineLayout, Ot as bytesToBase64, P as assembleCaptionContent, Q as VideoDocumentValidationError, R as findComposedAudioScript, S as equalJson, St as effectiveVideoClipDurationMs, T as resolveFieldPath, Tt as recordEntries, U as createEntityId, V as variantBaseEntityIds, W as createRelationId, X as fromVideoDocument, Y as derivePositionFromAbs, Z as toVideoDocument, _ as audioScriptAssetOf, _t as TIMELINE_SKELETON_DURATION_MS, a as createMirrorVideoDocument, at as findLaneTrack, b as phoneticScriptAssetOf, bt as safeDurationMs, c as DocumentMutationGuard, ct as solveVideoDocument, d as LoroEntityDraft, dt as fillMainTrackTimeGaps, et as validateVideoDocument, f as readEntityDocument, ft as reassignSpeechesToVideoClipsByTime, g as audioScriptAssetHash, gt as syncAggregatedClipsTimePosition, h as audioScriptAssetFields, ht as resolveSpeechOverlapByShiftingVideos, i as MirrorVideoDocumentAdapter, it as ensureLaneTrack, j as relationsFrom, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as cascadeAfterVideoClipChanges, m as audioScriptAssetContent, mt as resolveAllSpeechOverlaps, n as createPlainMemoryAdapter, nt as videoDocumentSchema, o as createMirrorVideoDocumentAdapter, ot as hasResolvedPlacement, p as entityOrderGroups, pt as recalculateTimelineDuration, q as buildInitialVideoDocument, r as generatePartId, rt as LANE_KINDS_IN_STACK_ORDER, s as readVideoDocumentFromDraft, st as laneTrackId, t as PlainMemoryAdapter, tt as partUnionSchema, u as LoroEntityDocument, ut as arrangeMainTrackSeamlessly, v as phoneticScriptAssetContent, vt as isEmptyVideoClip, w as assertFieldChanges, wt as partUnionToDraft, x as scriptSourceAssetOf, xt as DEFAULT_UNIT_TIME_MS, y as phoneticScriptAssetHash, yt as partDurationMs, z as selectAudioScriptSegment } from "./document-Ct11SMBS.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 { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -402,14 +402,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
402
402
  };
403
403
  entities.push(track);
404
404
  }
405
- const bindings = relations.filter((row) => row.relationKind === "timeline-track" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
405
+ const bindings = relations.filter((row) => row.relationKind === "contain" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
406
406
  if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
407
407
  if (bindings.length === 0) {
408
408
  const relationId = createRelationId(idFactory("relation"));
409
409
  if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
410
410
  relations.push({
411
411
  relationId,
412
- relationKind: "timeline-track",
412
+ relationKind: "contain",
413
413
  endpoint0EntityId: timeline.entityId,
414
414
  endpoint1EntityId: track.entityId,
415
415
  metadata: {},
@@ -432,10 +432,6 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
432
432
  //#endregion
433
433
  //#region src/entity-editor/entity-graph-http-client.ts
434
434
  const MENGINE_API_PREFIX = "/api/mengine/v1";
435
- const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left, right) => {
436
- if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
437
- return left < right ? -1 : left > right ? 1 : 0;
438
- } };
439
435
  /** Browser-safe authenticated Loro client for a document's Entity graph. */
440
436
  var EntityGraphHttpClient = class {
441
437
  options;
@@ -485,7 +481,7 @@ function parseWireState(value, expectedDocId) {
485
481
  if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
486
482
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
487
483
  const rows = parseWireRows(value.rows);
488
- decodeEntityRelationRows(rows, numericMarkerValidation$1);
484
+ decodeEntityRelationRows(rows, {});
489
485
  const attached = value.audio_script_entity_id;
490
486
  if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
491
487
  if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
@@ -526,7 +522,7 @@ function parseWireRelation(value) {
526
522
  }
527
523
  function cloneAndValidateRows(rows) {
528
524
  const clone = structuredClone(rows);
529
- decodeEntityRelationRows(clone, numericMarkerValidation$1);
525
+ decodeEntityRelationRows(clone, {});
530
526
  return clone;
531
527
  }
532
528
  function requiredString(value, field) {
@@ -550,10 +546,6 @@ async function safeReadJson(response) {
550
546
  }
551
547
  //#endregion
552
548
  //#region src/entity-editor/media-asset-import.ts
553
- const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right) => {
554
- if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
555
- return left - right;
556
- } };
557
549
  /**
558
550
  * Import at the editor boundary, not at asset generation time. Existing
559
551
  * provenance is retained.
@@ -569,17 +561,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
569
561
  "image",
570
562
  "video",
571
563
  "audio",
572
- "voiceover"
564
+ "voiceover-recording"
573
565
  ].includes(fact.kind)) throw new Error("Unsupported media kind");
574
566
  if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
575
- if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
576
- decodeEntityRelationRows(initial, numericCoordinates);
567
+ if ((fact.kind === "audio" || fact.kind === "voiceover-recording") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
568
+ decodeEntityRelationRows(initial, {});
577
569
  assertCanonicalEditorResources(initial);
578
570
  const entities = structuredClone([...initial.entities]);
579
571
  const relations = structuredClone([...initial.relations]);
580
- const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
572
+ const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
581
573
  const entityKind = fact.kind;
582
- const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
574
+ const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
583
575
  const durationMs = fact.kind === "image" ? null : fact.durationMs;
584
576
  const asset = ensureAsset(entities, {
585
577
  system,
@@ -610,12 +602,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
610
602
  payload: { durationMs }
611
603
  });
612
604
  relations.push(assetLink(entityId, asset.entityId, idFactory));
613
- if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
605
+ if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
614
606
  const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
615
607
  if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
616
608
  relations.push({
617
609
  relationId: createRelationId(idFactory("relation")),
618
- relationKind: "phonetic-script-render",
610
+ relationKind: "derived-from",
619
611
  endpoint0EntityId: entityId,
620
612
  endpoint1EntityId: phonetic.entityId,
621
613
  metadata: {},
@@ -627,7 +619,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
627
619
  entities,
628
620
  relations
629
621
  };
630
- decodeEntityRelationRows(rows, numericCoordinates);
622
+ decodeEntityRelationRows(rows, {});
631
623
  assertCanonicalEditorResources(rows);
632
624
  return {
633
625
  rows,
@@ -770,19 +762,19 @@ var EntityTimelineEditor = class {
770
762
  return cloneRows(this.state);
771
763
  }
772
764
  insertClip(input) {
773
- const requestedOrder = input.clipPayload?.order;
765
+ const requestedOrder = input.order;
774
766
  const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
775
767
  return this.insertPlacedClip({
776
768
  trackEntityId: input.trackEntityId,
777
769
  contentEntityId: input.contentEntityId,
778
- sourceRange: input.sourceRange,
779
- duration: input.duration,
780
- placement: input.targetRange === void 0 ? {
770
+ inMs: input.inMs,
771
+ outMs: input.outMs,
772
+ placement: input.startMs === void 0 ? {
781
773
  kind: "sequential",
782
774
  order
783
775
  } : {
784
776
  kind: "absolute",
785
- targetRange: input.targetRange
777
+ startMs: input.startMs
786
778
  },
787
779
  clipPayload: input.clipPayload
788
780
  });
@@ -794,31 +786,14 @@ var EntityTimelineEditor = class {
794
786
  this.transact((draft) => {
795
787
  const { marker } = requireClipTopology(draft, input.clipEntityId);
796
788
  const payload = { ...cloneJsonObject(marker.payload, "Sequence Marker payload") };
797
- if (input.sourceRange !== void 0) payload.sourceRange = cloneJsonValue(input.sourceRange, "sourceRange");
798
- if (input.duration !== void 0) payload.duration = cloneJsonValue(input.duration, "duration");
799
- if (Object.hasOwn(input, "targetRange")) if (input.targetRange === void 0) {
800
- delete payload.targetRange;
801
- if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {
802
- const trackId = otherEndpoint(requireOneRelation(draft, createEntityId(input.clipEntityId), "track-clip"), input.clipEntityId);
803
- const clipPayload = { ...cloneJsonObject(requireEntityKind(draft, input.clipEntityId, "clip").payload, "Clip payload") };
804
- clipPayload.order ??= nextSequentialOrder(draft, trackId);
805
- replaceEntity(draft, createEntityId(input.clipEntityId), {
806
- ...requireEntityKind(draft, input.clipEntityId, "clip"),
807
- payload: clipPayload
808
- });
809
- }
810
- } else {
811
- payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
812
- const clip = requireEntityKind(draft, input.clipEntityId, "clip");
813
- const clipPayload = { ...cloneJsonObject(clip.payload, "Clip payload") };
814
- delete clipPayload.order;
815
- replaceEntity(draft, clip.entityId, {
816
- ...clip,
817
- payload: clipPayload
818
- });
819
- }
820
- if (Object.hasOwn(input, "timeRemapping")) if (input.timeRemapping === void 0) delete payload.timeRemapping;
821
- else payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
789
+ if (input.inMs !== void 0) payload.inMs = finiteNonNegative(input.inMs, "inMs");
790
+ if (input.outMs !== void 0) payload.outMs = finitePositive(input.outMs, "outMs");
791
+ if (Object.hasOwn(input, "startMs")) if (input.startMs === void 0) {
792
+ delete payload.startMs;
793
+ if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {}
794
+ } else payload.startMs = finiteNonNegative(input.startMs, "startMs");
795
+ if (Object.hasOwn(input, "rate")) if (input.rate === void 0) delete payload.rate;
796
+ else payload.rate = finitePositive(input.rate, "linear speed rate");
822
797
  replaceEntity(draft, marker.entityId, {
823
798
  ...marker,
824
799
  payload
@@ -929,28 +904,18 @@ var EntityTimelineEditor = class {
929
904
  const content = requireSequenceContent(draft, input.contentEntityId);
930
905
  const payload = {
931
906
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
932
- sourceRange: cloneJsonValue(input.sourceRange, "sourceRange"),
933
- duration: cloneJsonValue(input.duration, "duration")
907
+ ...requireWindow(input.inMs, input.outMs)
934
908
  };
935
- delete payload.targetRange;
936
- delete payload.timeRemapping;
937
- if (input.targetRange !== void 0) payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
938
- if (input.timeRemapping !== void 0) payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
909
+ delete payload.startMs;
910
+ delete payload.rate;
911
+ if (input.startMs !== void 0) payload.startMs = finiteNonNegative(input.startMs, "startMs");
912
+ if (input.rate !== void 0) payload.rate = finitePositive(input.rate, "linear speed rate");
939
913
  replaceEntity(draft, topology.marker.entityId, {
940
914
  ...topology.marker,
941
915
  payload
942
916
  });
943
- if (!hasOutgoingClipAnchor(draft, topology.clip.entityId)) {
944
- const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
945
- if (input.targetRange === void 0) clipPayload.order ??= nextSequentialOrder(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId));
946
- else delete clipPayload.order;
947
- replaceEntity(draft, topology.clip.entityId, {
948
- ...topology.clip,
949
- payload: clipPayload
950
- });
951
- }
952
- this.relinkRelation(draft, topology.markerContentRelation, {
953
- endpoint0EntityId: topology.marker.entityId,
917
+ this.relinkRelation(draft, topology.contentRelation, {
918
+ endpoint0EntityId: topology.clip.entityId,
954
919
  endpoint1EntityId: content.entityId
955
920
  });
956
921
  });
@@ -961,15 +926,11 @@ var EntityTimelineEditor = class {
961
926
  const imported = importMediaAsset(draft, input.media, this.idFactory);
962
927
  replaceRows(draft, imported.rows);
963
928
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
964
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
929
+ const window = requireWindow(input.inMs, input.outMs);
965
930
  const clipId = this.insertPlacedClipIntoDraft(draft, {
966
931
  trackEntityId: track.entityId,
967
932
  contentEntityId: imported.contentEntityId,
968
- sourceRange,
969
- duration: {
970
- mode: "fixed",
971
- value: sourceRange.end - sourceRange.start
972
- },
933
+ ...window,
973
934
  placement: input.placement,
974
935
  ...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
975
936
  ...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
@@ -1004,15 +965,11 @@ var EntityTimelineEditor = class {
1004
965
  for (const [offset, clip] of input.clips.entries()) {
1005
966
  const imported = importMediaAsset(draft, clip.media, this.idFactory);
1006
967
  replaceRows(draft, imported.rows);
1007
- const sourceRange = requirePositiveRange(clip.sourceRange, "sourceRange");
968
+ const window = requireWindow(clip.inMs, clip.outMs);
1008
969
  created.push(this.insertPlacedClipIntoDraft(draft, {
1009
970
  trackEntityId: track.entityId,
1010
971
  contentEntityId: imported.contentEntityId,
1011
- sourceRange,
1012
- duration: {
1013
- mode: "fixed",
1014
- value: sourceRange.end - sourceRange.start
1015
- },
972
+ ...window,
1016
973
  placement: {
1017
974
  kind: "sequential",
1018
975
  order: ordered.length + offset
@@ -1034,23 +991,18 @@ var EntityTimelineEditor = class {
1034
991
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1035
992
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1036
993
  replaceRows(draft, imported.rows);
1037
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
994
+ const window = requireWindow(input.inMs, input.outMs);
1038
995
  const markerPayloadValue = {
1039
996
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1040
- sourceRange,
1041
- duration: {
1042
- mode: "fixed",
1043
- value: sourceRange.end - sourceRange.start
1044
- }
997
+ ...window
1045
998
  };
1046
- delete markerPayloadValue.timeRemapping;
1047
- resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
999
+ delete markerPayloadValue.rate;
1048
1000
  replaceEntity(draft, topology.marker.entityId, {
1049
1001
  ...topology.marker,
1050
1002
  payload: markerPayloadValue
1051
1003
  });
1052
- this.relinkRelation(draft, topology.markerContentRelation, {
1053
- endpoint0EntityId: topology.marker.entityId,
1004
+ this.relinkRelation(draft, topology.contentRelation, {
1005
+ endpoint0EntityId: topology.clip.entityId,
1054
1006
  endpoint1EntityId: createEntityId(imported.contentEntityId)
1055
1007
  });
1056
1008
  this.resolveOneVoiceoverOverlap(draft);
@@ -1072,42 +1024,26 @@ var EntityTimelineEditor = class {
1072
1024
  setClipSpeed(input) {
1073
1025
  this.transact((draft) => {
1074
1026
  const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1075
- const content = requireSequenceContent(draft, otherEndpoint(initialTopology.markerContentRelation, initialTopology.marker.entityId));
1027
+ const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
1076
1028
  if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
1077
1029
  const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
1078
- const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
1079
- if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
1030
+ const priorWindow = markerWindow(initialPayload);
1031
+ if (input.rate === null && initialPayload.rate === void 0) return;
1080
1032
  this.normalizeMainSequential(draft);
1081
1033
  const topology = requireClipTopology(draft, input.clipEntityId);
1082
1034
  const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1083
- let sourceRange = priorSource;
1084
- if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
1085
- start: 0,
1086
- end: markerDuration(initialPayload, priorSource)
1087
- };
1088
- if (input.timeRemapping === null) {
1089
- delete payload.timeRemapping;
1090
- payload.sourceRange = sourceRange;
1091
- payload.duration = {
1092
- mode: "fixed",
1093
- value: sourceRange.end - sourceRange.start
1094
- };
1095
- } else {
1096
- const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
1097
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1035
+ const window = content.entityKind === "image" && initialPayload.rate === void 0 && input.rate !== null ? {
1036
+ inMs: 0,
1037
+ outMs: markerDuration(initialPayload, priorWindow)
1038
+ } : priorWindow;
1039
+ Object.assign(payload, window);
1040
+ if (input.rate === null) delete payload.rate;
1041
+ else {
1042
+ const rate = finitePositive(input.rate, "linear speed rate");
1043
+ const duration = Math.round((window.outMs - window.inMs) / rate);
1098
1044
  if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
1099
- payload.sourceRange = sourceRange;
1100
- payload.duration = {
1101
- mode: "fixed",
1102
- value: duration
1103
- };
1104
- payload.timeRemapping = {
1105
- kind: "linear",
1106
- rate,
1107
- ...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
1108
- };
1045
+ payload.rate = rate;
1109
1046
  }
1110
- resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
1111
1047
  replaceEntity(draft, topology.marker.entityId, {
1112
1048
  ...topology.marker,
1113
1049
  payload
@@ -1119,21 +1055,15 @@ var EntityTimelineEditor = class {
1119
1055
  this.transact((draft) => {
1120
1056
  this.normalizeMainSequential(draft);
1121
1057
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1122
- const content = requireSequenceContent(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId));
1058
+ const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
1123
1059
  if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip trimming is supported only for Video or Image content");
1124
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
1060
+ const window = requireWindow(input.inMs, input.outMs);
1125
1061
  const payload = {
1126
1062
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1127
- sourceRange
1063
+ ...window
1128
1064
  };
1129
- const rate = linearRate(payload.timeRemapping);
1130
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1065
+ const duration = markerDuration(payload, window);
1131
1066
  if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Trim produces an invalid duration");
1132
- payload.duration = {
1133
- mode: "fixed",
1134
- value: duration
1135
- };
1136
- resizeAbsoluteTarget(payload, duration);
1137
1067
  replaceEntity(draft, topology.marker.entityId, {
1138
1068
  ...topology.marker,
1139
1069
  payload
@@ -1163,15 +1093,11 @@ var EntityTimelineEditor = class {
1163
1093
  if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
1164
1094
  const imported = importMediaAsset(draft, replacement.media, this.idFactory);
1165
1095
  replaceRows(draft, imported.rows);
1166
- const sourceRange = requirePositiveRange(replacement.sourceRange, "sourceRange");
1096
+ const window = requireWindow(replacement.inMs, replacement.outMs);
1167
1097
  created.push(this.insertPlacedClipIntoDraft(draft, {
1168
1098
  trackEntityId: track.entityId,
1169
1099
  contentEntityId: imported.contentEntityId,
1170
- sourceRange,
1171
- duration: {
1172
- mode: "fixed",
1173
- value: sourceRange.end - sourceRange.start
1174
- },
1100
+ ...window,
1175
1101
  placement: {
1176
1102
  kind: "sequential",
1177
1103
  order: temporaryOrder + offset
@@ -1205,10 +1131,14 @@ var EntityTimelineEditor = class {
1205
1131
  deleteClip(input) {
1206
1132
  this.transact((draft) => {
1207
1133
  const topology = requireClipTopology(draft, input.clipEntityId);
1208
- assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["track-clip", "clip-marker"]));
1209
- assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["clip-marker", "marker-content"]));
1210
- const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "clip-marker").length === 1;
1211
- const removedRelationIds = new Set([topology.trackRelation.relationId, topology.clipMarkerRelation.relationId]);
1134
+ assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["contain", "marker"]));
1135
+ assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
1136
+ const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
1137
+ const removedRelationIds = new Set([
1138
+ topology.trackRelation.relationId,
1139
+ topology.clipMarkerRelation.relationId,
1140
+ topology.contentRelation.relationId
1141
+ ]);
1212
1142
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
1213
1143
  draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
1214
1144
  draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
@@ -1229,28 +1159,20 @@ var EntityTimelineEditor = class {
1229
1159
  if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
1230
1160
  this.applyClipPlacement(draft, childId, {
1231
1161
  kind: "absolute",
1232
- targetRange: {
1233
- start: placement.start,
1234
- end: placement.start + placement.duration
1235
- }
1162
+ startMs: placement.start
1236
1163
  });
1237
1164
  }
1238
1165
  else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
1239
1166
  for (const target of targets) this.removeClipGraph(draft, target);
1240
- normalizeSequentialOrders(draft);
1241
1167
  if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
1242
1168
  });
1243
1169
  }
1244
1170
  updateClip(input) {
1245
1171
  this.transact((draft) => {
1246
1172
  const clip = requireEntityKind(draft, input.clipEntityId, "clip");
1247
- const priorOrder = clip.payload.order;
1248
1173
  replaceEntity(draft, clip.entityId, {
1249
1174
  ...clip,
1250
- payload: {
1251
- ...cloneJsonObject(input.payload, "Clip payload"),
1252
- ...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
1253
- }
1175
+ payload: cloneJsonObject(input.payload, "Clip payload")
1254
1176
  });
1255
1177
  });
1256
1178
  }
@@ -1260,13 +1182,13 @@ var EntityTimelineEditor = class {
1260
1182
  const placement = input.placement ?? {
1261
1183
  kind: "anchored",
1262
1184
  hostClipEntityId: input.hostClipEntityId,
1263
- anchorOffset: input.anchorOffset
1185
+ startMs: input.startMs
1264
1186
  };
1265
- if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/anchorOffset, not both");
1187
+ if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.startMs !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/startMs, not both");
1266
1188
  requireVolume(input.volume);
1267
1189
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1268
1190
  replaceRows(draft, imported.rows);
1269
- const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
1191
+ const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
1270
1192
  const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
1271
1193
  this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
1272
1194
  const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
@@ -1276,14 +1198,8 @@ var EntityTimelineEditor = class {
1276
1198
  clipEntityId: input.voiceoverClipEntityId,
1277
1199
  trackEntityId: speechTrack.entityId,
1278
1200
  contentEntityId: voiceover.entityId,
1279
- sourceRange: {
1280
- start: 0,
1281
- end: input.media.durationMs
1282
- },
1283
- duration: {
1284
- mode: "fixed",
1285
- value: input.media.durationMs
1286
- },
1201
+ inMs: 0,
1202
+ outMs: input.media.durationMs,
1287
1203
  placement,
1288
1204
  clipPayload: { volume: input.volume }
1289
1205
  });
@@ -1297,8 +1213,8 @@ var EntityTimelineEditor = class {
1297
1213
  volume: input.volume
1298
1214
  }
1299
1215
  });
1300
- this.relinkRelation(draft, topology.markerContentRelation, {
1301
- endpoint0EntityId: topology.marker.entityId,
1216
+ this.relinkRelation(draft, topology.contentRelation, {
1217
+ endpoint0EntityId: topology.clip.entityId,
1302
1218
  endpoint1EntityId: voiceover.entityId
1303
1219
  });
1304
1220
  this.applyClipPlacement(draft, topology.clip.entityId, placement);
@@ -1307,14 +1223,8 @@ var EntityTimelineEditor = class {
1307
1223
  ...placed.marker,
1308
1224
  payload: {
1309
1225
  ...placed.marker.payload,
1310
- sourceRange: {
1311
- start: 0,
1312
- end: input.media.durationMs
1313
- },
1314
- duration: {
1315
- mode: "fixed",
1316
- value: input.media.durationMs
1317
- }
1226
+ inMs: 0,
1227
+ outMs: input.media.durationMs
1318
1228
  }
1319
1229
  });
1320
1230
  }
@@ -1363,11 +1273,8 @@ var EntityTimelineEditor = class {
1363
1273
  clipEntityId: input.bgmClipEntityId,
1364
1274
  trackEntityId: track.entityId,
1365
1275
  contentEntityId: imported.contentEntityId,
1366
- sourceRange: {
1367
- start: 0,
1368
- end: input.media.durationMs
1369
- },
1370
- duration: { mode: "from-source" },
1276
+ inMs: 0,
1277
+ outMs: input.media.durationMs,
1371
1278
  placement: {
1372
1279
  kind: "sequential",
1373
1280
  order: 0
@@ -1379,7 +1286,7 @@ var EntityTimelineEditor = class {
1379
1286
  ...topology.marker,
1380
1287
  payload: {
1381
1288
  ...topology.marker.payload,
1382
- durationPolicy: "timeline"
1289
+ fillTimeline: true
1383
1290
  }
1384
1291
  });
1385
1292
  return clipId;
@@ -1412,7 +1319,7 @@ var EntityTimelineEditor = class {
1412
1319
  const assembled = new EntityProjectionGraph(draft);
1413
1320
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1414
1321
  const topology = requireClipTopology(draft, clip.entityId);
1415
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1322
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1416
1323
  const prior = assembled.entity(caption.entityId).payload.style;
1417
1324
  const style = isJsonObject(prior) ? prior : {};
1418
1325
  for (const updated of updateEntityFields(draft, caption.entityId, { style: {
@@ -1449,14 +1356,8 @@ var EntityTimelineEditor = class {
1449
1356
  return this.insertPlacedClipIntoDraft(draft, {
1450
1357
  trackEntityId: track.entityId,
1451
1358
  contentEntityId: content.entityId,
1452
- sourceRange: {
1453
- start: 0,
1454
- end: durationMs
1455
- },
1456
- duration: {
1457
- mode: "fixed",
1458
- value: durationMs
1459
- },
1359
+ inMs: 0,
1360
+ outMs: durationMs,
1460
1361
  placement: input.placement,
1461
1362
  ...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
1462
1363
  });
@@ -1469,8 +1370,7 @@ var EntityTimelineEditor = class {
1469
1370
  const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
1470
1371
  const markerId = this.mintEntityId(draft);
1471
1372
  const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
1472
- delete clipPayload.order;
1473
- const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
1373
+ const markerPlacement = applyPlacementPayload(input.placement);
1474
1374
  const clip = {
1475
1375
  entityId: clipId,
1476
1376
  entityKind: "clip",
@@ -1480,13 +1380,12 @@ var EntityTimelineEditor = class {
1480
1380
  entityId: markerId,
1481
1381
  entityKind: "sequence-marker",
1482
1382
  payload: markerPayload({
1483
- sourceRange: input.sourceRange,
1484
- duration: input.duration,
1383
+ ...requireWindow(input.inMs, input.outMs),
1485
1384
  ...markerPlacement
1486
1385
  })
1487
1386
  };
1488
1387
  draft.entities.push(clip, marker);
1489
- 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));
1388
+ draft.relations.push(this.createRelation(draft, "contain", track.entityId, clipId), this.createRelation(draft, "marker", clipId, markerId), this.createRelation(draft, "contain", clipId, content.entityId));
1490
1389
  if (input.placement.kind === "anchored") {
1491
1390
  const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
1492
1391
  if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
@@ -1496,20 +1395,7 @@ var EntityTimelineEditor = class {
1496
1395
  }
1497
1396
  ensureRoleTrack(draft, timelineEntityId, role) {
1498
1397
  const existing = findRoleTrack(draft, timelineEntityId, role);
1499
- if (existing !== void 0) {
1500
- if (existing.payload.order === void 0) {
1501
- const replacement = {
1502
- ...existing,
1503
- payload: {
1504
- ...existing.payload,
1505
- order: roleOrder[role]
1506
- }
1507
- };
1508
- replaceEntity(draft, existing.entityId, replacement);
1509
- return replacement;
1510
- }
1511
- return existing;
1512
- }
1398
+ if (existing !== void 0) return existing;
1513
1399
  const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
1514
1400
  const track = {
1515
1401
  entityId: this.mintEntityId(draft),
@@ -1520,7 +1406,7 @@ var EntityTimelineEditor = class {
1520
1406
  }
1521
1407
  };
1522
1408
  draft.entities.push(track);
1523
- draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
1409
+ draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
1524
1410
  return track;
1525
1411
  }
1526
1412
  /** The existing pronunciation variant a voiceover consumes; composition is required. */
@@ -1531,13 +1417,13 @@ var EntityTimelineEditor = class {
1531
1417
  }
1532
1418
  /** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
1533
1419
  ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
1534
- const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
1420
+ const renders = draft.relations.filter((relation) => relation.relationKind === "derived-from" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
1535
1421
  if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
1536
1422
  if (renders[0] !== void 0) {
1537
1423
  if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
1538
1424
  throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
1539
1425
  }
1540
- draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
1426
+ draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
1541
1427
  }
1542
1428
  reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
1543
1429
  const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
@@ -1572,24 +1458,18 @@ var EntityTimelineEditor = class {
1572
1458
  clipEntityId: caption.captionClipEntityId,
1573
1459
  trackEntityId: captionTrack.entityId,
1574
1460
  contentEntityId: content.entityId,
1575
- sourceRange: {
1576
- start: 0,
1577
- end: durationMs
1578
- },
1579
- duration: {
1580
- mode: "fixed",
1581
- value: durationMs
1582
- },
1461
+ inMs: 0,
1462
+ outMs: durationMs,
1583
1463
  placement: {
1584
1464
  kind: "anchored",
1585
1465
  hostClipEntityId: voiceoverClipEntityId,
1586
- anchorOffset: startMs
1466
+ startMs
1587
1467
  }
1588
1468
  });
1589
1469
  continue;
1590
1470
  }
1591
1471
  const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
1592
- const content = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1472
+ const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1593
1473
  const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
1594
1474
  const style = caption.style === void 0 ? priorStyle : {
1595
1475
  ...priorStyle,
@@ -1608,17 +1488,16 @@ var EntityTimelineEditor = class {
1608
1488
  this.applyClipPlacement(draft, topology.clip.entityId, {
1609
1489
  kind: "anchored",
1610
1490
  hostClipEntityId: voiceoverClipEntityId,
1611
- anchorOffset: startMs
1491
+ startMs
1612
1492
  });
1613
1493
  const placed = requireClipTopology(draft, topology.clip.entityId);
1494
+ const window = markerWindow(placed.marker.payload);
1495
+ const rate = (window.outMs - window.inMs) / durationMs;
1614
1496
  replaceEntity(draft, placed.marker.entityId, {
1615
1497
  ...placed.marker,
1616
1498
  payload: {
1617
1499
  ...placed.marker.payload,
1618
- duration: {
1619
- mode: "fixed",
1620
- value: durationMs
1621
- }
1500
+ ...rate === 1 ? {} : { rate }
1622
1501
  }
1623
1502
  });
1624
1503
  this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
@@ -1627,7 +1506,7 @@ var EntityTimelineEditor = class {
1627
1506
  ensureRoleTrackForClip(draft, clipEntityId, role) {
1628
1507
  const topology = requireClipTopology(draft, clipEntityId);
1629
1508
  const track = requireEntityKind(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId), "track");
1630
- const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
1509
+ const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
1631
1510
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1632
1511
  }
1633
1512
  replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
@@ -1644,13 +1523,13 @@ var EntityTimelineEditor = class {
1644
1523
  */
1645
1524
  removeCaptionClipGraph(draft, clipEntityId) {
1646
1525
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1647
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1526
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1648
1527
  const contentRelations = incidentRelations(draft, caption.entityId);
1649
- const allowed = new Set(["caption-alignment", "marker-content"]);
1528
+ const allowed = new Set(["caption-alignment", "contain"]);
1650
1529
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1651
1530
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1652
1531
  this.removeClipGraph(draft, clipEntityId);
1653
- if (incidentRelations(draft, caption.entityId).some((relation) => relation.relationKind === "marker-content")) return;
1532
+ if (containing(draft, caption.entityId, "contained").length > 0) return;
1654
1533
  const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
1655
1534
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1656
1535
  draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
@@ -1698,10 +1577,7 @@ var EntityTimelineEditor = class {
1698
1577
  const overlapMs = Math.round(prior.end - current.start);
1699
1578
  for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
1700
1579
  kind: "absolute",
1701
- targetRange: {
1702
- start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
1703
- end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
1704
- }
1580
+ startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
1705
1581
  });
1706
1582
  return;
1707
1583
  }
@@ -1710,18 +1586,16 @@ var EntityTimelineEditor = class {
1710
1586
  const topology = requireClipTopology(draft, clipEntityId);
1711
1587
  const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
1712
1588
  const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1713
- delete clipPayload.order;
1714
- delete markerPayloadValue.targetRange;
1715
- delete markerPayloadValue.anchorOffset;
1589
+ delete markerPayloadValue.startMs;
1716
1590
  const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
1717
1591
  const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
1718
1592
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1719
- if (placement.kind === "sequential") clipPayload.order = finiteNumber(placement.order, "Clip order");
1720
- else if (placement.kind === "absolute") markerPayloadValue.targetRange = cloneJsonValue(placement.targetRange, "targetRange");
1593
+ if (placement.kind === "sequential") setClipOrder(draft, topology.clip.entityId, placement.order);
1594
+ else if (placement.kind === "absolute") markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1721
1595
  else {
1722
1596
  const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
1723
1597
  if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
1724
- markerPayloadValue.anchorOffset = finiteNonNegative(placement.anchorOffset, "anchorOffset");
1598
+ markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1725
1599
  draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
1726
1600
  }
1727
1601
  replaceEntity(draft, topology.clip.entityId, {
@@ -1736,14 +1610,10 @@ var EntityTimelineEditor = class {
1736
1610
  reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
1737
1611
  const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
1738
1612
  if (hosts.length === 0) {
1739
- const child = layout.clips.get(childClipEntityId);
1740
- if (child === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
1613
+ if (layout.clips.get(childClipEntityId) === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
1741
1614
  this.applyClipPlacement(draft, childClipEntityId, {
1742
1615
  kind: "absolute",
1743
- targetRange: {
1744
- start: absoluteStart,
1745
- end: absoluteStart + child.duration
1746
- }
1616
+ startMs: absoluteStart
1747
1617
  });
1748
1618
  return;
1749
1619
  }
@@ -1751,7 +1621,7 @@ var EntityTimelineEditor = class {
1751
1621
  this.applyClipPlacement(draft, childClipEntityId, {
1752
1622
  kind: "anchored",
1753
1623
  hostClipEntityId: host.clipEntityId,
1754
- anchorOffset: Math.max(0, Math.round(absoluteStart - host.start))
1624
+ startMs: Math.max(0, Math.round(absoluteStart - host.start))
1755
1625
  });
1756
1626
  }
1757
1627
  removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
@@ -1768,12 +1638,12 @@ var EntityTimelineEditor = class {
1768
1638
  removeClipGraph(draft, clipEntityId) {
1769
1639
  const topology = requireClipTopology(draft, clipEntityId);
1770
1640
  assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
1771
- "track-clip",
1772
- "clip-marker",
1641
+ "contain",
1642
+ "marker",
1773
1643
  "clip-anchor"
1774
1644
  ]));
1775
- assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["clip-marker", "marker-content"]));
1776
- const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "clip-marker").length === 1;
1645
+ assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
1646
+ const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
1777
1647
  const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
1778
1648
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
1779
1649
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
@@ -1819,7 +1689,7 @@ var EntityTimelineEditor = class {
1819
1689
  }
1820
1690
  };
1821
1691
  function validateFinalRows(rows) {
1822
- decodeEntityRelationRows(rows, numericMarkerValidation);
1692
+ decodeEntityRelationRows(rows, {});
1823
1693
  assertCanonicalEditorResources(rows);
1824
1694
  validateTimelineHierarchy(rows);
1825
1695
  }
@@ -1827,13 +1697,13 @@ function validateTimelineHierarchy(rows) {
1827
1697
  for (const entity of rows.entities) {
1828
1698
  if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
1829
1699
  if (entity.entityKind !== "clip") continue;
1830
- const trackRelations = incidentRelations(rows, entity.entityId).filter((relation) => relation.relationKind === "track-clip");
1700
+ const trackRelations = containing(rows, entity.entityId, "contained");
1831
1701
  if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
1832
1702
  requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
1833
1703
  }
1834
1704
  }
1835
1705
  function requireTrackTimeline(rows, trackEntityId) {
1836
- const timelineRelations = incidentRelations(rows, trackEntityId).filter((relation) => relation.relationKind === "timeline-track");
1706
+ const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
1837
1707
  if (timelineRelations.filter((relation) => {
1838
1708
  const otherId = otherEndpoint(relation, trackEntityId);
1839
1709
  return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
@@ -1841,20 +1711,36 @@ function requireTrackTimeline(rows, trackEntityId) {
1841
1711
  }
1842
1712
  function requireClipTopology(rows, clipEntityId) {
1843
1713
  const clip = requireEntityKind(rows, clipEntityId, "clip");
1844
- const trackRelation = requireOneRelation(rows, clip.entityId, "track-clip");
1714
+ const trackRelation = requireOneContain(rows, clip.entityId, "contained");
1845
1715
  requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
1846
- const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "clip-marker");
1716
+ const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
1847
1717
  const marker = requireEntityKind(rows, otherEndpoint(clipMarkerRelation, clip.entityId), "sequence-marker");
1848
- const markerContentRelation = requireOneRelation(rows, marker.entityId, "marker-content");
1849
- requireSequenceContent(rows, otherEndpoint(markerContentRelation, marker.entityId));
1718
+ const contentRelation = requireOneContain(rows, clip.entityId, "container");
1719
+ requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
1850
1720
  return {
1851
1721
  clip,
1852
1722
  marker,
1853
1723
  trackRelation,
1854
1724
  clipMarkerRelation,
1855
- markerContentRelation
1725
+ contentRelation
1856
1726
  };
1857
1727
  }
1728
+ /**
1729
+ * One `contain` Relation with this entity on the stated side.
1730
+ *
1731
+ * A Clip is contained by its Track and contains its content; a Track is
1732
+ * contained by the Timeline and contains its Clips. Both are `contain`, so the
1733
+ * endpoint position is the only thing that tells them apart.
1734
+ */
1735
+ function requireOneContain(rows, entityId, side) {
1736
+ const matches = containing(rows, entityId, side);
1737
+ if (matches.length !== 1) throw new Error(`Entity "${entityId}" must be the ${side} of exactly one contain Relation`);
1738
+ return matches[0];
1739
+ }
1740
+ /** `contain` is ordered, so a lookup says which side it wants. */
1741
+ function containing(rows, entityId, side) {
1742
+ return (side === "container" ? relationsFrom : relationsTo)(rows.relations, "contain", entityId);
1743
+ }
1858
1744
  function requireOneRelation(rows, entityId, relationKind) {
1859
1745
  const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
1860
1746
  if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
@@ -1893,40 +1779,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
1893
1779
  function outgoingAnchorChildren(rows, hostIds) {
1894
1780
  return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
1895
1781
  }
1896
- function applyPlacementPayload(clipPayload, placement) {
1897
- if (placement.kind === "sequential") {
1898
- clipPayload.order = finiteNumber(placement.order, "Clip order");
1899
- return {};
1900
- }
1901
- if (placement.kind === "absolute") return { targetRange: cloneJsonValue(placement.targetRange, "targetRange") };
1902
- return { anchorOffset: finiteNonNegative(placement.anchorOffset, "anchorOffset") };
1782
+ /** What the placement puts on the Marker; a sequential one puts nothing there. */
1783
+ function applyPlacementPayload(placement) {
1784
+ if (placement.kind === "sequential") return {};
1785
+ return { startMs: finiteNonNegative(placement.startMs, "startMs") };
1903
1786
  }
1904
1787
  function requireAvailableEntityId(rows, value) {
1905
1788
  const entityId = createEntityId(value);
1906
1789
  if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
1907
1790
  return entityId;
1908
1791
  }
1792
+ /**
1793
+ * Put a Clip at `order` among the Clips its Track contains.
1794
+ *
1795
+ * Where a Clip sits is the position of its membership Relation, so moving one
1796
+ * is moving that Relation — nothing on the Clip changes, and there are no
1797
+ * sibling positions left to keep in step.
1798
+ */
1909
1799
  function setClipOrder(rows, clipEntityId, order) {
1910
- const clip = requireEntityKind(rows, clipEntityId, "clip");
1911
- replaceEntity(rows, clip.entityId, {
1912
- ...clip,
1913
- payload: {
1914
- ...clip.payload,
1915
- order
1916
- }
1917
- });
1918
- }
1919
- function normalizeSequentialOrders(rows) {
1920
- 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));
1800
+ const membership = relationsTo(rows.relations, "contain", clipEntityId)[0];
1801
+ if (membership === void 0) throw new Error(`Clip "${clipEntityId}" is contained by no Track`);
1802
+ const slots = [];
1803
+ const siblings = [];
1804
+ for (const [index, relation] of rows.relations.entries()) {
1805
+ if (relation.relationKind !== "contain" || relation.endpoint0EntityId !== membership.endpoint0EntityId) continue;
1806
+ slots.push(index);
1807
+ siblings.push(relation);
1808
+ }
1809
+ const from = siblings.indexOf(membership);
1810
+ const moved = siblings.filter((relation) => relation !== membership);
1811
+ moved.splice(Math.max(0, Math.min(order, moved.length)), 0, membership);
1812
+ if (from < 0) return;
1813
+ slots.forEach((slot, at) => rows.relations[slot] = moved[at]);
1921
1814
  }
1922
1815
  function findRoleTrack(rows, timelineEntityId, role) {
1923
1816
  const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
1924
- 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);
1817
+ const tracks = containing(rows, timeline.entityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
1925
1818
  if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
1926
1819
  return tracks[0];
1927
1820
  }
1928
1821
  function clipsOnTrack(rows, trackEntityId) {
1929
- return incidentRelations(rows, trackEntityId).filter((relation) => relation.relationKind === "track-clip").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
1822
+ return containing(rows, trackEntityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
1930
1823
  }
1931
1824
  function clipRole(rows, clipEntityId) {
1932
1825
  const topology = requireClipTopology(rows, clipEntityId);
@@ -1959,35 +1852,27 @@ function requireVolume(value) {
1959
1852
  if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
1960
1853
  return volume;
1961
1854
  }
1962
- function requirePositiveRange(value, label) {
1963
- if (!isJsonObject(value)) throw new Error(`${label} must be an object`);
1964
- const start = finiteNonNegative(value.start, `${label}.start`);
1965
- const end = finitePositive(value.end, `${label}.end`);
1966
- if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must be a positive whole-millisecond range`);
1855
+ function requireWindow(inMs, outMs, label = "Sequence Marker") {
1856
+ const start = finiteNonNegative(inMs, `${label} inMs`);
1857
+ const end = finitePositive(outMs, `${label} outMs`);
1858
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must state a positive whole-millisecond window`);
1967
1859
  return {
1968
- start,
1969
- end
1860
+ inMs: start,
1861
+ outMs: end
1970
1862
  };
1971
1863
  }
1972
- function markerDuration(payload, sourceRange) {
1973
- const duration = payload.duration;
1974
- if (!isJsonObject(duration)) throw new Error("Sequence Marker duration must be an object");
1975
- if (duration.mode === "from-source") return sourceRange.end - sourceRange.start;
1976
- if (duration.mode !== "fixed") throw new Error("Sequence Marker duration mode is unsupported");
1977
- return finitePositive(duration.value, "Sequence Marker fixed duration");
1978
- }
1979
- function linearRate(value) {
1980
- if (value === void 0) return 1;
1981
- if (!isJsonObject(value) || value.kind !== "linear") throw new Error("Only linear Clip time remapping is supported");
1982
- return finitePositive(value.rate, "linear speed rate");
1983
- }
1984
- function resizeAbsoluteTarget(payload, duration) {
1985
- if (payload.targetRange === void 0) return;
1986
- const target = requirePositiveRange(payload.targetRange, "targetRange");
1987
- payload.targetRange = {
1988
- start: target.start,
1989
- end: target.start + duration
1990
- };
1864
+ function markerWindow(payload) {
1865
+ return requireWindow(payload.inMs, payload.outMs);
1866
+ }
1867
+ /**
1868
+ * How long the Clip runs. The window and the rate are the only stored facts;
1869
+ * the played length follows from them rather than being kept in step by hand.
1870
+ */
1871
+ function markerDuration(payload, window = markerWindow(payload)) {
1872
+ return Math.round((window.outMs - window.inMs) / markerRate(payload));
1873
+ }
1874
+ function markerRate(payload) {
1875
+ return payload.rate === void 0 ? 1 : finitePositive(payload.rate, "linear speed rate");
1991
1876
  }
1992
1877
  function replaceRows(rows, replacement) {
1993
1878
  rows.entities = structuredClone([...replacement.entities]);
@@ -1998,9 +1883,9 @@ function finiteNonNegative(value, label) {
1998
1883
  if (result < 0) throw new Error(`${label} must be non-negative`);
1999
1884
  return result;
2000
1885
  }
1886
+ /** How many Clips the Track already holds — the index a new one lands at. */
2001
1887
  function nextSequentialOrder(rows, trackEntityId) {
2002
- 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));
2003
- return orders.length === 0 ? 0 : Math.max(...orders) + 1;
1888
+ return relationsFrom(rows.relations, "contain", trackEntityId).length;
2004
1889
  }
2005
1890
  function otherEndpoint(relation, entityId) {
2006
1891
  if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
@@ -2020,19 +1905,12 @@ function cloneJsonObject(value, label) {
2020
1905
  if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
2021
1906
  return cloned;
2022
1907
  }
2023
- function cloneJsonValue(value, label) {
2024
- return cloneJsonObject({ value }, label).value;
2025
- }
2026
1908
  function cloneRows(rows) {
2027
1909
  return {
2028
1910
  entities: rows.entities.map((entity) => structuredClone(entity)),
2029
1911
  relations: rows.relations.map((relation) => structuredClone(relation))
2030
1912
  };
2031
1913
  }
2032
- const numericMarkerValidation = { compareMarkerPoints: (_marker, _range, left, right) => {
2033
- if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
2034
- return left - right;
2035
- } };
2036
1914
  //#endregion
2037
1915
  //#region src/entity-editor/legacy-entity-migration.ts
2038
1916
  /** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
@@ -2104,7 +1982,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2104
1982
  relations
2105
1983
  }, {
2106
1984
  assetId: speech.origin_speech_id,
2107
- kind: "voiceover",
1985
+ kind: "voiceover-recording",
2108
1986
  durationMs: speech.media_duration_ms,
2109
1987
  storageKey: speech.audio_storage_key,
2110
1988
  voice: {
@@ -2142,7 +2020,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2142
2020
  ...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
2143
2021
  }, oldTrack.id);
2144
2022
  trackIds.set(oldTrack, track);
2145
- link("timeline-track", timeline, track);
2023
+ link("contain", timeline, track);
2146
2024
  }
2147
2025
  for (const oldTrack of legacy.tracks ?? []) {
2148
2026
  const track = trackIds.get(oldTrack);
@@ -2152,7 +2030,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2152
2030
  const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
2153
2031
  const clip = addEntity("clip", {
2154
2032
  ...volume === void 0 ? {} : { volume },
2155
- ...item.time_position.mode === "sequential" && !(role === "video_clip" && normalizedMainRanges !== void 0) || role === "bgm" ? { order: role === "bgm" ? 0 : order } : {}
2033
+ order: role === "bgm" ? 0 : order
2156
2034
  }, item.part_id);
2157
2035
  let content = contentByPartId.get(item.part_id);
2158
2036
  let duration;
@@ -2163,37 +2041,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2163
2041
  const fact = facts.get(video.origin_media_id);
2164
2042
  const rate = speedOf(video.speed_shift);
2165
2043
  markerPayload = {
2166
- sourceRange: {
2167
- start: video.play_in,
2168
- end: video.play_out
2169
- },
2170
- duration: {
2171
- mode: "fixed",
2172
- value: duration
2173
- },
2174
- ...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { timeRemapping: {
2175
- kind: "linear",
2176
- mode: video.speed_shift?.mode ?? "constant",
2177
- rate
2178
- } } : {}
2044
+ inMs: video.play_in,
2045
+ outMs: video.play_out,
2046
+ ...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
2179
2047
  };
2180
2048
  } else if (part.speech !== void 0) {
2181
2049
  duration = part.speech.media_duration_ms;
2182
2050
  markerPayload = {
2183
- sourceRange: {
2184
- start: 0,
2185
- end: duration
2186
- },
2187
- duration: {
2188
- mode: "fixed",
2189
- value: duration
2190
- }
2051
+ inMs: 0,
2052
+ outMs: duration
2191
2053
  };
2192
2054
  } else if (part.caption !== void 0) {
2193
2055
  const caption = part.caption;
2194
2056
  duration = caption.initial_duration_ms;
2195
2057
  const voiceover = contentByPartId.get(caption.speech_part_id);
2196
- const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceover);
2058
+ const render = relations.find((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceover);
2197
2059
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2198
2060
  const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2199
2061
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
@@ -2219,14 +2081,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2219
2081
  end: caption.start_ms + duration
2220
2082
  } });
2221
2083
  markerPayload = {
2222
- sourceRange: {
2223
- start: 0,
2224
- end: duration
2225
- },
2226
- duration: {
2227
- mode: "fixed",
2228
- value: duration
2229
- }
2084
+ inMs: 0,
2085
+ outMs: duration
2230
2086
  };
2231
2087
  } else {
2232
2088
  const bgm = part.bgm;
@@ -2234,35 +2090,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2234
2090
  if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
2235
2091
  duration = fact.durationMs;
2236
2092
  markerPayload = {
2237
- sourceRange: {
2238
- start: 0,
2239
- end: duration
2240
- },
2241
- duration: { mode: "from-source" },
2242
- durationPolicy: "timeline"
2093
+ inMs: 0,
2094
+ outMs: duration,
2095
+ fillTimeline: true
2243
2096
  };
2244
2097
  }
2245
2098
  if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
2246
2099
  const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2247
2100
  if (normalizedMainRange !== void 0) markerPayload = {
2248
2101
  ...markerPayload,
2249
- targetRange: normalizedMainRange
2250
- };
2251
- else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
2252
- ...markerPayload,
2253
- targetRange: {
2254
- start: item.time_position.offsetMs,
2255
- end: item.time_position.offsetMs + duration
2256
- }
2102
+ startMs: normalizedMainRange.start
2257
2103
  };
2258
- else if (item.time_position.mode === "anchored") markerPayload = {
2104
+ else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
2259
2105
  ...markerPayload,
2260
- anchorOffset: item.time_position.offsetMs
2106
+ startMs: item.time_position.offsetMs
2261
2107
  };
2262
2108
  const marker = addEntity("sequence-marker", markerPayload);
2263
- link("track-clip", track, clip);
2264
- link("clip-marker", clip, marker);
2265
- link("marker-content", marker, content);
2109
+ link("contain", track, clip);
2110
+ link("marker", clip, marker);
2111
+ link("contain", clip, content);
2266
2112
  if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
2267
2113
  }
2268
2114
  }
@@ -2283,6 +2129,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2283
2129
  for (const part of Object.values(view.part_library ?? {})) {
2284
2130
  const speech = part.speech;
2285
2131
  if (speech?.phoneme_script === null) delete speech.phoneme_script;
2132
+ const speed = part.video_clip?.speed_shift;
2133
+ if (speed !== void 0) delete speed.mode;
2286
2134
  }
2287
2135
  return { view };
2288
2136
  };
@@ -2298,22 +2146,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2298
2146
  const sequentialOrders = [];
2299
2147
  for (const item of oldTrack.items ?? []) {
2300
2148
  const clip = rows.entities.find((row) => row.entityId === item.part_id && row.entityKind === "clip");
2301
- 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"));
2149
+ 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"));
2302
2150
  const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
2303
2151
  if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
2304
2152
  const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
2305
2153
  const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2306
2154
  if (normalizedMainRange !== void 0) {
2307
- 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");
2155
+ if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
2308
2156
  } else if (oldTrack.parts_kind === "bgm") {
2309
- 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");
2157
+ 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");
2310
2158
  } else if (item.time_position.mode === "sequential") {
2311
- 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");
2159
+ 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");
2312
2160
  sequentialOrders.push(clip.payload.order);
2313
2161
  } else if (item.time_position.mode === "absolute") {
2314
- const target = marker.payload.targetRange;
2315
- 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");
2316
- } 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");
2162
+ if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 0) fail("Migration must preserve absolute placement semantics");
2163
+ } 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");
2317
2164
  }
2318
2165
  if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
2319
2166
  }
@@ -2554,7 +2401,7 @@ function requirePart(document, partId) {
2554
2401
  return part;
2555
2402
  }
2556
2403
  function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
2557
- const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceoverAudioEntityId);
2404
+ const renders = relations.filter((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceoverAudioEntityId);
2558
2405
  if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
2559
2406
  if (renders.length === 1) {
2560
2407
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
@@ -2582,7 +2429,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
2582
2429
  baseEntityIds: [script.entityId],
2583
2430
  ...phonemeScript === void 0 ? {} : { phonemeScript }
2584
2431
  });
2585
- link("phonetic-script-render", voiceoverAudioEntityId, phonetic);
2432
+ link("derived-from", voiceoverAudioEntityId, phonetic);
2586
2433
  return phonetic;
2587
2434
  }
2588
2435
  function composedAudioScript(phonetic, entities) {
@@ -2644,12 +2491,6 @@ function assertCaptionStyle(style) {
2644
2491
  "position_y"
2645
2492
  ], "Caption style");
2646
2493
  }
2647
- function isRangeStartingAt(value, expectedStart) {
2648
- return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expectedStart;
2649
- }
2650
- function isExactRange(value, expected) {
2651
- return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expected.start && value.end === expected.end;
2652
- }
2653
2494
  function requireFinite(value, label) {
2654
2495
  if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
2655
2496
  return value;