@mengine/medeo-client 2.0.1-alpha.23 → 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,8 +1,6 @@
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 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
- import { sha256 } from "@noble/hashes/sha2.js";
5
- import { bytesToHex } from "@noble/hashes/utils.js";
6
4
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
7
5
  import { DisposableSet, EventBus, Task } from "@mengine/utils";
8
6
  import { BaseDocStorage, DummyConnection } from "@mengine/storage";
@@ -230,7 +228,6 @@ function applyEntityRows(editor, rows) {
230
228
  const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
231
229
  const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
232
230
  if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
233
- validateEntityRelationTransition(before, rows);
234
231
  return {
235
232
  update: editor.transact((draft) => {
236
233
  for (const row of before.relations) {
@@ -238,7 +235,7 @@ function applyEntityRows(editor, rows) {
238
235
  if (!next) draft.unlink(row.relationId);
239
236
  else {
240
237
  if (next.relationKind !== row.relationKind || next.endpoint0EntityId !== row.endpoint0EntityId || next.endpoint1EntityId !== row.endpoint1EntityId) throw new Error("Relation identity cannot change kind or endpoint positions");
241
- if (!equalJson(row.trace, next.trace)) draft.patchRelationTrace(row.relationId, next.trace);
238
+ if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
242
239
  }
243
240
  }
244
241
  let attached = baseline.project.audioScriptEntityId !== void 0;
@@ -255,22 +252,7 @@ function applyEntityRows(editor, rows) {
255
252
  if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
256
253
  }
257
254
  }
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
- }
255
+ for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
274
256
  const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
275
257
  for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
276
258
  const next = afterOrder.get(group) ?? [];
@@ -420,14 +402,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
420
402
  };
421
403
  entities.push(track);
422
404
  }
423
- 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));
424
406
  if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
425
407
  if (bindings.length === 0) {
426
408
  const relationId = createRelationId(idFactory("relation"));
427
409
  if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
428
410
  relations.push({
429
411
  relationId,
430
- relationKind: "timeline-track",
412
+ relationKind: "contain",
431
413
  endpoint0EntityId: timeline.entityId,
432
414
  endpoint1EntityId: track.entityId,
433
415
  metadata: {},
@@ -450,10 +432,6 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
450
432
  //#endregion
451
433
  //#region src/entity-editor/entity-graph-http-client.ts
452
434
  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
- } };
457
435
  /** Browser-safe authenticated Loro client for a document's Entity graph. */
458
436
  var EntityGraphHttpClient = class {
459
437
  options;
@@ -503,7 +481,7 @@ function parseWireState(value, expectedDocId) {
503
481
  if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
504
482
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
505
483
  const rows = parseWireRows(value.rows);
506
- decodeEntityRelationRows(rows, numericMarkerValidation$1);
484
+ decodeEntityRelationRows(rows, {});
507
485
  const attached = value.audio_script_entity_id;
508
486
  if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
509
487
  if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
@@ -544,7 +522,7 @@ function parseWireRelation(value) {
544
522
  }
545
523
  function cloneAndValidateRows(rows) {
546
524
  const clone = structuredClone(rows);
547
- decodeEntityRelationRows(clone, numericMarkerValidation$1);
525
+ decodeEntityRelationRows(clone, {});
548
526
  return clone;
549
527
  }
550
528
  function requiredString(value, field) {
@@ -568,10 +546,6 @@ async function safeReadJson(response) {
568
546
  }
569
547
  //#endregion
570
548
  //#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
549
  /**
576
550
  * Import at the editor boundary, not at asset generation time. Existing
577
551
  * provenance is retained.
@@ -587,17 +561,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
587
561
  "image",
588
562
  "video",
589
563
  "audio",
590
- "voiceover"
564
+ "voiceover-recording"
591
565
  ].includes(fact.kind)) throw new Error("Unsupported media kind");
592
566
  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);
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, {});
595
569
  assertCanonicalEditorResources(initial);
596
570
  const entities = structuredClone([...initial.entities]);
597
571
  const relations = structuredClone([...initial.relations]);
598
- const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
572
+ const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
599
573
  const entityKind = fact.kind;
600
- const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
574
+ const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
601
575
  const durationMs = fact.kind === "image" ? null : fact.durationMs;
602
576
  const asset = ensureAsset(entities, {
603
577
  system,
@@ -628,12 +602,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
628
602
  payload: { durationMs }
629
603
  });
630
604
  relations.push(assetLink(entityId, asset.entityId, idFactory));
631
- if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
605
+ if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
632
606
  const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
633
607
  if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
634
608
  relations.push({
635
609
  relationId: createRelationId(idFactory("relation")),
636
- relationKind: "phonetic-script-render",
610
+ relationKind: "derived-from",
637
611
  endpoint0EntityId: entityId,
638
612
  endpoint1EntityId: phonetic.entityId,
639
613
  metadata: {},
@@ -645,7 +619,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
645
619
  entities,
646
620
  relations
647
621
  };
648
- decodeEntityRelationRows(rows, numericCoordinates);
622
+ decodeEntityRelationRows(rows, {});
649
623
  assertCanonicalEditorResources(rows);
650
624
  return {
651
625
  rows,
@@ -788,19 +762,19 @@ var EntityTimelineEditor = class {
788
762
  return cloneRows(this.state);
789
763
  }
790
764
  insertClip(input) {
791
- const requestedOrder = input.clipPayload?.order;
765
+ const requestedOrder = input.order;
792
766
  const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
793
767
  return this.insertPlacedClip({
794
768
  trackEntityId: input.trackEntityId,
795
769
  contentEntityId: input.contentEntityId,
796
- sourceRange: input.sourceRange,
797
- duration: input.duration,
798
- placement: input.targetRange === void 0 ? {
770
+ inMs: input.inMs,
771
+ outMs: input.outMs,
772
+ placement: input.startMs === void 0 ? {
799
773
  kind: "sequential",
800
774
  order
801
775
  } : {
802
776
  kind: "absolute",
803
- targetRange: input.targetRange
777
+ startMs: input.startMs
804
778
  },
805
779
  clipPayload: input.clipPayload
806
780
  });
@@ -812,31 +786,14 @@ var EntityTimelineEditor = class {
812
786
  this.transact((draft) => {
813
787
  const { marker } = requireClipTopology(draft, input.clipEntityId);
814
788
  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");
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");
840
797
  replaceEntity(draft, marker.entityId, {
841
798
  ...marker,
842
799
  payload
@@ -947,28 +904,18 @@ var EntityTimelineEditor = class {
947
904
  const content = requireSequenceContent(draft, input.contentEntityId);
948
905
  const payload = {
949
906
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
950
- sourceRange: cloneJsonValue(input.sourceRange, "sourceRange"),
951
- duration: cloneJsonValue(input.duration, "duration")
907
+ ...requireWindow(input.inMs, input.outMs)
952
908
  };
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");
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");
957
913
  replaceEntity(draft, topology.marker.entityId, {
958
914
  ...topology.marker,
959
915
  payload
960
916
  });
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,
917
+ this.relinkRelation(draft, topology.contentRelation, {
918
+ endpoint0EntityId: topology.clip.entityId,
972
919
  endpoint1EntityId: content.entityId
973
920
  });
974
921
  });
@@ -979,15 +926,11 @@ var EntityTimelineEditor = class {
979
926
  const imported = importMediaAsset(draft, input.media, this.idFactory);
980
927
  replaceRows(draft, imported.rows);
981
928
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
982
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
929
+ const window = requireWindow(input.inMs, input.outMs);
983
930
  const clipId = this.insertPlacedClipIntoDraft(draft, {
984
931
  trackEntityId: track.entityId,
985
932
  contentEntityId: imported.contentEntityId,
986
- sourceRange,
987
- duration: {
988
- mode: "fixed",
989
- value: sourceRange.end - sourceRange.start
990
- },
933
+ ...window,
991
934
  placement: input.placement,
992
935
  ...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
993
936
  ...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
@@ -1022,15 +965,11 @@ var EntityTimelineEditor = class {
1022
965
  for (const [offset, clip] of input.clips.entries()) {
1023
966
  const imported = importMediaAsset(draft, clip.media, this.idFactory);
1024
967
  replaceRows(draft, imported.rows);
1025
- const sourceRange = requirePositiveRange(clip.sourceRange, "sourceRange");
968
+ const window = requireWindow(clip.inMs, clip.outMs);
1026
969
  created.push(this.insertPlacedClipIntoDraft(draft, {
1027
970
  trackEntityId: track.entityId,
1028
971
  contentEntityId: imported.contentEntityId,
1029
- sourceRange,
1030
- duration: {
1031
- mode: "fixed",
1032
- value: sourceRange.end - sourceRange.start
1033
- },
972
+ ...window,
1034
973
  placement: {
1035
974
  kind: "sequential",
1036
975
  order: ordered.length + offset
@@ -1052,23 +991,18 @@ var EntityTimelineEditor = class {
1052
991
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1053
992
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1054
993
  replaceRows(draft, imported.rows);
1055
- const sourceRange = requirePositiveRange(input.sourceRange, "sourceRange");
994
+ const window = requireWindow(input.inMs, input.outMs);
1056
995
  const markerPayloadValue = {
1057
996
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1058
- sourceRange,
1059
- duration: {
1060
- mode: "fixed",
1061
- value: sourceRange.end - sourceRange.start
1062
- }
997
+ ...window
1063
998
  };
1064
- delete markerPayloadValue.timeRemapping;
1065
- resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
999
+ delete markerPayloadValue.rate;
1066
1000
  replaceEntity(draft, topology.marker.entityId, {
1067
1001
  ...topology.marker,
1068
1002
  payload: markerPayloadValue
1069
1003
  });
1070
- this.relinkRelation(draft, topology.markerContentRelation, {
1071
- endpoint0EntityId: topology.marker.entityId,
1004
+ this.relinkRelation(draft, topology.contentRelation, {
1005
+ endpoint0EntityId: topology.clip.entityId,
1072
1006
  endpoint1EntityId: createEntityId(imported.contentEntityId)
1073
1007
  });
1074
1008
  this.resolveOneVoiceoverOverlap(draft);
@@ -1090,42 +1024,26 @@ var EntityTimelineEditor = class {
1090
1024
  setClipSpeed(input) {
1091
1025
  this.transact((draft) => {
1092
1026
  const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1093
- const content = requireSequenceContent(draft, otherEndpoint(initialTopology.markerContentRelation, initialTopology.marker.entityId));
1027
+ const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
1094
1028
  if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
1095
1029
  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;
1030
+ const priorWindow = markerWindow(initialPayload);
1031
+ if (input.rate === null && initialPayload.rate === void 0) return;
1098
1032
  this.normalizeMainSequential(draft);
1099
1033
  const topology = requireClipTopology(draft, input.clipEntityId);
1100
1034
  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);
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);
1116
1044
  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
- };
1045
+ payload.rate = rate;
1127
1046
  }
1128
- resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
1129
1047
  replaceEntity(draft, topology.marker.entityId, {
1130
1048
  ...topology.marker,
1131
1049
  payload
@@ -1137,21 +1055,15 @@ var EntityTimelineEditor = class {
1137
1055
  this.transact((draft) => {
1138
1056
  this.normalizeMainSequential(draft);
1139
1057
  const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1140
- const content = requireSequenceContent(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId));
1058
+ const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
1141
1059
  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");
1060
+ const window = requireWindow(input.inMs, input.outMs);
1143
1061
  const payload = {
1144
1062
  ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
1145
- sourceRange
1063
+ ...window
1146
1064
  };
1147
- const rate = linearRate(payload.timeRemapping);
1148
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1065
+ const duration = markerDuration(payload, window);
1149
1066
  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
1067
  replaceEntity(draft, topology.marker.entityId, {
1156
1068
  ...topology.marker,
1157
1069
  payload
@@ -1181,15 +1093,11 @@ var EntityTimelineEditor = class {
1181
1093
  if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
1182
1094
  const imported = importMediaAsset(draft, replacement.media, this.idFactory);
1183
1095
  replaceRows(draft, imported.rows);
1184
- const sourceRange = requirePositiveRange(replacement.sourceRange, "sourceRange");
1096
+ const window = requireWindow(replacement.inMs, replacement.outMs);
1185
1097
  created.push(this.insertPlacedClipIntoDraft(draft, {
1186
1098
  trackEntityId: track.entityId,
1187
1099
  contentEntityId: imported.contentEntityId,
1188
- sourceRange,
1189
- duration: {
1190
- mode: "fixed",
1191
- value: sourceRange.end - sourceRange.start
1192
- },
1100
+ ...window,
1193
1101
  placement: {
1194
1102
  kind: "sequential",
1195
1103
  order: temporaryOrder + offset
@@ -1223,10 +1131,14 @@ var EntityTimelineEditor = class {
1223
1131
  deleteClip(input) {
1224
1132
  this.transact((draft) => {
1225
1133
  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]);
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
+ ]);
1230
1142
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
1231
1143
  draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
1232
1144
  draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
@@ -1247,28 +1159,20 @@ var EntityTimelineEditor = class {
1247
1159
  if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
1248
1160
  this.applyClipPlacement(draft, childId, {
1249
1161
  kind: "absolute",
1250
- targetRange: {
1251
- start: placement.start,
1252
- end: placement.start + placement.duration
1253
- }
1162
+ startMs: placement.start
1254
1163
  });
1255
1164
  }
1256
1165
  else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
1257
1166
  for (const target of targets) this.removeClipGraph(draft, target);
1258
- normalizeSequentialOrders(draft);
1259
1167
  if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
1260
1168
  });
1261
1169
  }
1262
1170
  updateClip(input) {
1263
1171
  this.transact((draft) => {
1264
1172
  const clip = requireEntityKind(draft, input.clipEntityId, "clip");
1265
- const priorOrder = clip.payload.order;
1266
1173
  replaceEntity(draft, clip.entityId, {
1267
1174
  ...clip,
1268
- payload: {
1269
- ...cloneJsonObject(input.payload, "Clip payload"),
1270
- ...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
1271
- }
1175
+ payload: cloneJsonObject(input.payload, "Clip payload")
1272
1176
  });
1273
1177
  });
1274
1178
  }
@@ -1278,13 +1182,13 @@ var EntityTimelineEditor = class {
1278
1182
  const placement = input.placement ?? {
1279
1183
  kind: "anchored",
1280
1184
  hostClipEntityId: input.hostClipEntityId,
1281
- anchorOffset: input.anchorOffset
1185
+ startMs: input.startMs
1282
1186
  };
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");
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");
1284
1188
  requireVolume(input.volume);
1285
1189
  const imported = importMediaAsset(draft, input.media, this.idFactory);
1286
1190
  replaceRows(draft, imported.rows);
1287
- const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
1191
+ const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
1288
1192
  const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
1289
1193
  this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
1290
1194
  const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
@@ -1294,14 +1198,8 @@ var EntityTimelineEditor = class {
1294
1198
  clipEntityId: input.voiceoverClipEntityId,
1295
1199
  trackEntityId: speechTrack.entityId,
1296
1200
  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
- },
1201
+ inMs: 0,
1202
+ outMs: input.media.durationMs,
1305
1203
  placement,
1306
1204
  clipPayload: { volume: input.volume }
1307
1205
  });
@@ -1315,8 +1213,8 @@ var EntityTimelineEditor = class {
1315
1213
  volume: input.volume
1316
1214
  }
1317
1215
  });
1318
- this.relinkRelation(draft, topology.markerContentRelation, {
1319
- endpoint0EntityId: topology.marker.entityId,
1216
+ this.relinkRelation(draft, topology.contentRelation, {
1217
+ endpoint0EntityId: topology.clip.entityId,
1320
1218
  endpoint1EntityId: voiceover.entityId
1321
1219
  });
1322
1220
  this.applyClipPlacement(draft, topology.clip.entityId, placement);
@@ -1325,18 +1223,12 @@ var EntityTimelineEditor = class {
1325
1223
  ...placed.marker,
1326
1224
  payload: {
1327
1225
  ...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
- }
1226
+ inMs: 0,
1227
+ outMs: input.media.durationMs
1336
1228
  }
1337
1229
  });
1338
1230
  }
1339
- this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phonetic.entityId, input.captions);
1231
+ this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceover.entityId, phonetic.entityId, input.captions);
1340
1232
  this.resolveOneVoiceoverOverlap(draft);
1341
1233
  return {
1342
1234
  voiceoverClipEntityId,
@@ -1381,11 +1273,8 @@ var EntityTimelineEditor = class {
1381
1273
  clipEntityId: input.bgmClipEntityId,
1382
1274
  trackEntityId: track.entityId,
1383
1275
  contentEntityId: imported.contentEntityId,
1384
- sourceRange: {
1385
- start: 0,
1386
- end: input.media.durationMs
1387
- },
1388
- duration: { mode: "from-source" },
1276
+ inMs: 0,
1277
+ outMs: input.media.durationMs,
1389
1278
  placement: {
1390
1279
  kind: "sequential",
1391
1280
  order: 0
@@ -1397,7 +1286,7 @@ var EntityTimelineEditor = class {
1397
1286
  ...topology.marker,
1398
1287
  payload: {
1399
1288
  ...topology.marker.payload,
1400
- durationPolicy: "timeline"
1289
+ fillTimeline: true
1401
1290
  }
1402
1291
  });
1403
1292
  return clipId;
@@ -1430,7 +1319,7 @@ var EntityTimelineEditor = class {
1430
1319
  const assembled = new EntityProjectionGraph(draft);
1431
1320
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1432
1321
  const topology = requireClipTopology(draft, clip.entityId);
1433
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1322
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1434
1323
  const prior = assembled.entity(caption.entityId).payload.style;
1435
1324
  const style = isJsonObject(prior) ? prior : {};
1436
1325
  for (const updated of updateEntityFields(draft, caption.entityId, { style: {
@@ -1467,14 +1356,8 @@ var EntityTimelineEditor = class {
1467
1356
  return this.insertPlacedClipIntoDraft(draft, {
1468
1357
  trackEntityId: track.entityId,
1469
1358
  contentEntityId: content.entityId,
1470
- sourceRange: {
1471
- start: 0,
1472
- end: durationMs
1473
- },
1474
- duration: {
1475
- mode: "fixed",
1476
- value: durationMs
1477
- },
1359
+ inMs: 0,
1360
+ outMs: durationMs,
1478
1361
  placement: input.placement,
1479
1362
  ...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
1480
1363
  });
@@ -1487,8 +1370,7 @@ var EntityTimelineEditor = class {
1487
1370
  const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
1488
1371
  const markerId = this.mintEntityId(draft);
1489
1372
  const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
1490
- delete clipPayload.order;
1491
- const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
1373
+ const markerPlacement = applyPlacementPayload(input.placement);
1492
1374
  const clip = {
1493
1375
  entityId: clipId,
1494
1376
  entityKind: "clip",
@@ -1498,13 +1380,12 @@ var EntityTimelineEditor = class {
1498
1380
  entityId: markerId,
1499
1381
  entityKind: "sequence-marker",
1500
1382
  payload: markerPayload({
1501
- sourceRange: input.sourceRange,
1502
- duration: input.duration,
1383
+ ...requireWindow(input.inMs, input.outMs),
1503
1384
  ...markerPlacement
1504
1385
  })
1505
1386
  };
1506
1387
  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));
1388
+ draft.relations.push(this.createRelation(draft, "contain", track.entityId, clipId), this.createRelation(draft, "marker", clipId, markerId), this.createRelation(draft, "contain", clipId, content.entityId));
1508
1389
  if (input.placement.kind === "anchored") {
1509
1390
  const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
1510
1391
  if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
@@ -1514,20 +1395,7 @@ var EntityTimelineEditor = class {
1514
1395
  }
1515
1396
  ensureRoleTrack(draft, timelineEntityId, role) {
1516
1397
  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
- }
1398
+ if (existing !== void 0) return existing;
1531
1399
  const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
1532
1400
  const track = {
1533
1401
  entityId: this.mintEntityId(draft),
@@ -1538,7 +1406,7 @@ var EntityTimelineEditor = class {
1538
1406
  }
1539
1407
  };
1540
1408
  draft.entities.push(track);
1541
- draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
1409
+ draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
1542
1410
  return track;
1543
1411
  }
1544
1412
  /** The existing pronunciation variant a voiceover consumes; composition is required. */
@@ -1547,17 +1415,17 @@ var EntityTimelineEditor = class {
1547
1415
  requireComposedAudioScript(draft, phonetic.entityId);
1548
1416
  return phonetic;
1549
1417
  }
1550
- /** Preserve the factual synthesis input as `[rendered Audio, PhoneticScript]`. */
1418
+ /** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
1551
1419
  ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
1552
- 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));
1553
1421
  if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
1554
1422
  if (renders[0] !== void 0) {
1555
1423
  if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
1556
1424
  throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
1557
1425
  }
1558
- draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
1426
+ draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
1559
1427
  }
1560
- reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
1428
+ reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
1561
1429
  const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
1562
1430
  const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
1563
1431
  if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
@@ -1582,28 +1450,26 @@ var EntityTimelineEditor = class {
1582
1450
  };
1583
1451
  draft.entities.push(content);
1584
1452
  assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1453
+ draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
1454
+ start: startMs,
1455
+ end: startMs + durationMs
1456
+ } }));
1585
1457
  this.insertPlacedClipIntoDraft(draft, {
1586
1458
  clipEntityId: caption.captionClipEntityId,
1587
1459
  trackEntityId: captionTrack.entityId,
1588
1460
  contentEntityId: content.entityId,
1589
- sourceRange: {
1590
- start: 0,
1591
- end: durationMs
1592
- },
1593
- duration: {
1594
- mode: "fixed",
1595
- value: durationMs
1596
- },
1461
+ inMs: 0,
1462
+ outMs: durationMs,
1597
1463
  placement: {
1598
1464
  kind: "anchored",
1599
1465
  hostClipEntityId: voiceoverClipEntityId,
1600
- anchorOffset: startMs
1466
+ startMs
1601
1467
  }
1602
1468
  });
1603
1469
  continue;
1604
1470
  }
1605
1471
  const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
1606
- const content = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1472
+ const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1607
1473
  const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
1608
1474
  const style = caption.style === void 0 ? priorStyle : {
1609
1475
  ...priorStyle,
@@ -1622,40 +1488,48 @@ var EntityTimelineEditor = class {
1622
1488
  this.applyClipPlacement(draft, topology.clip.entityId, {
1623
1489
  kind: "anchored",
1624
1490
  hostClipEntityId: voiceoverClipEntityId,
1625
- anchorOffset: startMs
1491
+ startMs
1626
1492
  });
1627
1493
  const placed = requireClipTopology(draft, topology.clip.entityId);
1494
+ const window = markerWindow(placed.marker.payload);
1495
+ const rate = (window.outMs - window.inMs) / durationMs;
1628
1496
  replaceEntity(draft, placed.marker.entityId, {
1629
1497
  ...placed.marker,
1630
1498
  payload: {
1631
1499
  ...placed.marker.payload,
1632
- duration: {
1633
- mode: "fixed",
1634
- value: durationMs
1635
- }
1500
+ ...rate === 1 ? {} : { rate }
1636
1501
  }
1637
1502
  });
1503
+ this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
1638
1504
  }
1639
1505
  }
1640
1506
  ensureRoleTrackForClip(draft, clipEntityId, role) {
1641
1507
  const topology = requireClipTopology(draft, clipEntityId);
1642
1508
  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");
1509
+ const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
1644
1510
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1645
1511
  }
1512
+ replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
1513
+ const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
1514
+ draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1515
+ draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
1516
+ start: startMs,
1517
+ end: startMs + durationMs
1518
+ } }));
1519
+ }
1646
1520
  /**
1647
1521
  * Remove one placed Caption use. The composed AudioScript outlives the
1648
1522
  * caption: it may anchor further captions and remains the text owner.
1649
1523
  */
1650
1524
  removeCaptionClipGraph(draft, clipEntityId) {
1651
1525
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1652
- const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1526
+ const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
1653
1527
  const contentRelations = incidentRelations(draft, caption.entityId);
1654
- const allowed = new Set(["caption-alignment", "marker-content"]);
1528
+ const allowed = new Set(["caption-alignment", "contain"]);
1655
1529
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1656
1530
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1657
1531
  this.removeClipGraph(draft, clipEntityId);
1658
- if (incidentRelations(draft, caption.entityId).some((relation) => relation.relationKind === "marker-content")) return;
1532
+ if (containing(draft, caption.entityId, "contained").length > 0) return;
1659
1533
  const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
1660
1534
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1661
1535
  draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
@@ -1703,10 +1577,7 @@ var EntityTimelineEditor = class {
1703
1577
  const overlapMs = Math.round(prior.end - current.start);
1704
1578
  for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
1705
1579
  kind: "absolute",
1706
- targetRange: {
1707
- start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
1708
- end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
1709
- }
1580
+ startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
1710
1581
  });
1711
1582
  return;
1712
1583
  }
@@ -1715,18 +1586,16 @@ var EntityTimelineEditor = class {
1715
1586
  const topology = requireClipTopology(draft, clipEntityId);
1716
1587
  const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
1717
1588
  const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1718
- delete clipPayload.order;
1719
- delete markerPayloadValue.targetRange;
1720
- delete markerPayloadValue.anchorOffset;
1589
+ delete markerPayloadValue.startMs;
1721
1590
  const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
1722
1591
  const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
1723
1592
  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");
1593
+ if (placement.kind === "sequential") setClipOrder(draft, topology.clip.entityId, placement.order);
1594
+ else if (placement.kind === "absolute") markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1726
1595
  else {
1727
1596
  const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
1728
1597
  if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
1729
- markerPayloadValue.anchorOffset = finiteNonNegative(placement.anchorOffset, "anchorOffset");
1598
+ markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
1730
1599
  draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
1731
1600
  }
1732
1601
  replaceEntity(draft, topology.clip.entityId, {
@@ -1741,14 +1610,10 @@ var EntityTimelineEditor = class {
1741
1610
  reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
1742
1611
  const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
1743
1612
  if (hosts.length === 0) {
1744
- const child = layout.clips.get(childClipEntityId);
1745
- 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`);
1746
1614
  this.applyClipPlacement(draft, childClipEntityId, {
1747
1615
  kind: "absolute",
1748
- targetRange: {
1749
- start: absoluteStart,
1750
- end: absoluteStart + child.duration
1751
- }
1616
+ startMs: absoluteStart
1752
1617
  });
1753
1618
  return;
1754
1619
  }
@@ -1756,7 +1621,7 @@ var EntityTimelineEditor = class {
1756
1621
  this.applyClipPlacement(draft, childClipEntityId, {
1757
1622
  kind: "anchored",
1758
1623
  hostClipEntityId: host.clipEntityId,
1759
- anchorOffset: Math.max(0, Math.round(absoluteStart - host.start))
1624
+ startMs: Math.max(0, Math.round(absoluteStart - host.start))
1760
1625
  });
1761
1626
  }
1762
1627
  removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
@@ -1773,12 +1638,12 @@ var EntityTimelineEditor = class {
1773
1638
  removeClipGraph(draft, clipEntityId) {
1774
1639
  const topology = requireClipTopology(draft, clipEntityId);
1775
1640
  assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
1776
- "track-clip",
1777
- "clip-marker",
1641
+ "contain",
1642
+ "marker",
1778
1643
  "clip-anchor"
1779
1644
  ]));
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;
1645
+ assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
1646
+ const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
1782
1647
  const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
1783
1648
  if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
1784
1649
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
@@ -1824,7 +1689,7 @@ var EntityTimelineEditor = class {
1824
1689
  }
1825
1690
  };
1826
1691
  function validateFinalRows(rows) {
1827
- decodeEntityRelationRows(rows, numericMarkerValidation);
1692
+ decodeEntityRelationRows(rows, {});
1828
1693
  assertCanonicalEditorResources(rows);
1829
1694
  validateTimelineHierarchy(rows);
1830
1695
  }
@@ -1832,13 +1697,13 @@ function validateTimelineHierarchy(rows) {
1832
1697
  for (const entity of rows.entities) {
1833
1698
  if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
1834
1699
  if (entity.entityKind !== "clip") continue;
1835
- const trackRelations = incidentRelations(rows, entity.entityId).filter((relation) => relation.relationKind === "track-clip");
1700
+ const trackRelations = containing(rows, entity.entityId, "contained");
1836
1701
  if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
1837
1702
  requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
1838
1703
  }
1839
1704
  }
1840
1705
  function requireTrackTimeline(rows, trackEntityId) {
1841
- const timelineRelations = incidentRelations(rows, trackEntityId).filter((relation) => relation.relationKind === "timeline-track");
1706
+ const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
1842
1707
  if (timelineRelations.filter((relation) => {
1843
1708
  const otherId = otherEndpoint(relation, trackEntityId);
1844
1709
  return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
@@ -1846,20 +1711,36 @@ function requireTrackTimeline(rows, trackEntityId) {
1846
1711
  }
1847
1712
  function requireClipTopology(rows, clipEntityId) {
1848
1713
  const clip = requireEntityKind(rows, clipEntityId, "clip");
1849
- const trackRelation = requireOneRelation(rows, clip.entityId, "track-clip");
1714
+ const trackRelation = requireOneContain(rows, clip.entityId, "contained");
1850
1715
  requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
1851
- const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "clip-marker");
1716
+ const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
1852
1717
  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));
1718
+ const contentRelation = requireOneContain(rows, clip.entityId, "container");
1719
+ requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
1855
1720
  return {
1856
1721
  clip,
1857
1722
  marker,
1858
1723
  trackRelation,
1859
1724
  clipMarkerRelation,
1860
- markerContentRelation
1725
+ contentRelation
1861
1726
  };
1862
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
+ }
1863
1744
  function requireOneRelation(rows, entityId, relationKind) {
1864
1745
  const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
1865
1746
  if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
@@ -1898,40 +1779,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
1898
1779
  function outgoingAnchorChildren(rows, hostIds) {
1899
1780
  return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
1900
1781
  }
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") };
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") };
1908
1786
  }
1909
1787
  function requireAvailableEntityId(rows, value) {
1910
1788
  const entityId = createEntityId(value);
1911
1789
  if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
1912
1790
  return entityId;
1913
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
+ */
1914
1799
  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));
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]);
1926
1814
  }
1927
1815
  function findRoleTrack(rows, timelineEntityId, role) {
1928
1816
  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);
1817
+ const tracks = containing(rows, timeline.entityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
1930
1818
  if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
1931
1819
  return tracks[0];
1932
1820
  }
1933
1821
  function clipsOnTrack(rows, trackEntityId) {
1934
- 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"));
1935
1823
  }
1936
1824
  function clipRole(rows, clipEntityId) {
1937
1825
  const topology = requireClipTopology(rows, clipEntityId);
@@ -1964,35 +1852,27 @@ function requireVolume(value) {
1964
1852
  if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
1965
1853
  return volume;
1966
1854
  }
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`);
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`);
1972
1859
  return {
1973
- start,
1974
- end
1860
+ inMs: start,
1861
+ outMs: end
1975
1862
  };
1976
1863
  }
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
- };
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");
1996
1876
  }
1997
1877
  function replaceRows(rows, replacement) {
1998
1878
  rows.entities = structuredClone([...replacement.entities]);
@@ -2003,9 +1883,9 @@ function finiteNonNegative(value, label) {
2003
1883
  if (result < 0) throw new Error(`${label} must be non-negative`);
2004
1884
  return result;
2005
1885
  }
1886
+ /** How many Clips the Track already holds — the index a new one lands at. */
2006
1887
  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;
1888
+ return relationsFrom(rows.relations, "contain", trackEntityId).length;
2009
1889
  }
2010
1890
  function otherEndpoint(relation, entityId) {
2011
1891
  if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
@@ -2025,174 +1905,12 @@ function cloneJsonObject(value, label) {
2025
1905
  if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
2026
1906
  return cloned;
2027
1907
  }
2028
- function cloneJsonValue(value, label) {
2029
- return cloneJsonObject({ value }, label).value;
2030
- }
2031
1908
  function cloneRows(rows) {
2032
1909
  return {
2033
1910
  entities: rows.entities.map((entity) => structuredClone(entity)),
2034
1911
  relations: rows.relations.map((relation) => structuredClone(relation))
2035
1912
  };
2036
1913
  }
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
- //#endregion
2042
- //#region src/entity-editor/revise-audio-script.ts
2043
- /** Revise text as an independent authored document, never as a version-family mutation. */
2044
- function reviseAudioScript(rows, input) {
2045
- if (typeof input.editId !== "string" || !input.editId.trim()) throw new Error("editId is required");
2046
- if (!Array.isArray(input.edits) || !input.edits.length) throw new Error("At least one segment edit is required");
2047
- const edits = /* @__PURE__ */ new Map();
2048
- for (const edit of input.edits) {
2049
- if (!edit.segmentId?.trim() || typeof edit.text !== "string" || !edit.text.trim()) throw new Error("Segment text must be nonempty");
2050
- if (edits.has(edit.segmentId)) throw new Error("Duplicate segment edit");
2051
- edits.set(edit.segmentId, edit.text);
2052
- }
2053
- const signature = hash({
2054
- ...input,
2055
- captionUsages: input.captionUsages ?? []
2056
- });
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
- }
2077
- const source = rows.entities.find((row) => row.entityId === input.sourceAudioScriptEntityId);
2078
- if (source?.entityKind !== "audio-script") throw new Error("AudioScript source is missing");
2079
- assembleScriptText({
2080
- ...source,
2081
- entityKind: "audio-script"
2082
- });
2083
- const payload = structuredClone(source.payload);
2084
- if (!Array.isArray(payload.segments)) throw new Error("AudioScript must own its segments");
2085
- const segmentIds = new Set(payload.segments.filter(isJsonObject).map((segment) => segment.segmentId));
2086
- for (const segmentId of edits.keys()) if (!segmentIds.has(segmentId)) throw new Error(`Unknown segment ${segmentId}`);
2087
- const changedSegments = /* @__PURE__ */ new Set();
2088
- payload.segments = payload.segments.map((segment) => {
2089
- if (segment === null || typeof segment !== "object" || Array.isArray(segment)) throw new Error("Invalid segment");
2090
- const text = edits.get(String(segment.segmentId)) ?? segment.text;
2091
- if (text !== segment.text) changedSegments.add(String(segment.segmentId));
2092
- return {
2093
- ...segment,
2094
- text
2095
- };
2096
- });
2097
- if (!changedSegments.size) return {
2098
- rows,
2099
- audioScriptEntityId: source.entityId,
2100
- captionUsages: [],
2101
- changed: false
2102
- };
2103
- delete payload.assetContentHash;
2104
- const id = hash(input.editId);
2105
- 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
- }]
2124
- };
2125
- const entities = [...result.entities];
2126
- let relations = [...result.relations];
2127
- const captionUsages = [];
2128
- const selectedMarkers = /* @__PURE__ */ new Set();
2129
- for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2130
- if (selectedMarkers.has(usage.markerEntityId)) throw new Error("Duplicate caption usage");
2131
- selectedMarkers.add(usage.markerEntityId);
2132
- const caption = rows.entities.find((row) => row.entityId === usage.captionEntityId);
2133
- if (caption?.entityKind !== "caption") throw new Error("Caption is missing");
2134
- const assembled = assembleCaptionContent(rows, caption.entityId);
2135
- if (assembled.audioScript.entityId !== source.entityId) throw new Error("Caption does not use the source AudioScript");
2136
- const selection = assembled.caption.payload.selection;
2137
- 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
- const bases = caption.payload.baseEntityIds;
2139
- 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");
2144
- 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],
2162
- relationId: createRelationId(`relation_caption_revision_${id}_${index}`),
2163
- endpoint1EntityId: newCaptionId,
2164
- trace: {
2165
- command: "revise-audio-script",
2166
- editId: input.editId,
2167
- usageIndex: index
2168
- }
2169
- });
2170
- captionUsages.push({
2171
- clipEntityId: usage.clipEntityId,
2172
- markerEntityId: usage.markerEntityId,
2173
- previousCaptionEntityId: usage.captionEntityId,
2174
- captionEntityId: newCaptionId
2175
- });
2176
- }
2177
- const revised = {
2178
- entities,
2179
- relations
2180
- };
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);
2186
- return {
2187
- rows: revised,
2188
- audioScriptEntityId,
2189
- captionUsages,
2190
- changed: true
2191
- };
2192
- }
2193
- function hash(value) {
2194
- return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
2195
- }
2196
1914
  //#endregion
2197
1915
  //#region src/entity-editor/legacy-entity-migration.ts
2198
1916
  /** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
@@ -2264,7 +1982,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2264
1982
  relations
2265
1983
  }, {
2266
1984
  assetId: speech.origin_speech_id,
2267
- kind: "voiceover",
1985
+ kind: "voiceover-recording",
2268
1986
  durationMs: speech.media_duration_ms,
2269
1987
  storageKey: speech.audio_storage_key,
2270
1988
  voice: {
@@ -2302,7 +2020,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2302
2020
  ...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
2303
2021
  }, oldTrack.id);
2304
2022
  trackIds.set(oldTrack, track);
2305
- link("timeline-track", timeline, track);
2023
+ link("contain", timeline, track);
2306
2024
  }
2307
2025
  for (const oldTrack of legacy.tracks ?? []) {
2308
2026
  const track = trackIds.get(oldTrack);
@@ -2312,7 +2030,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2312
2030
  const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
2313
2031
  const clip = addEntity("clip", {
2314
2032
  ...volume === void 0 ? {} : { volume },
2315
- ...item.time_position.mode === "sequential" && !(role === "video_clip" && normalizedMainRanges !== void 0) || role === "bgm" ? { order: role === "bgm" ? 0 : order } : {}
2033
+ order: role === "bgm" ? 0 : order
2316
2034
  }, item.part_id);
2317
2035
  let content = contentByPartId.get(item.part_id);
2318
2036
  let duration;
@@ -2323,37 +2041,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2323
2041
  const fact = facts.get(video.origin_media_id);
2324
2042
  const rate = speedOf(video.speed_shift);
2325
2043
  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
- } } : {}
2044
+ inMs: video.play_in,
2045
+ outMs: video.play_out,
2046
+ ...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
2339
2047
  };
2340
2048
  } else if (part.speech !== void 0) {
2341
2049
  duration = part.speech.media_duration_ms;
2342
2050
  markerPayload = {
2343
- sourceRange: {
2344
- start: 0,
2345
- end: duration
2346
- },
2347
- duration: {
2348
- mode: "fixed",
2349
- value: duration
2350
- }
2051
+ inMs: 0,
2052
+ outMs: duration
2351
2053
  };
2352
2054
  } else if (part.caption !== void 0) {
2353
2055
  const caption = part.caption;
2354
2056
  duration = caption.initial_duration_ms;
2355
2057
  const voiceover = contentByPartId.get(caption.speech_part_id);
2356
- 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);
2357
2059
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2358
2060
  const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2359
2061
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
@@ -2374,15 +2076,13 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2374
2076
  selection,
2375
2077
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2376
2078
  });
2079
+ link("caption-alignment", content, voiceover, { alignment: {
2080
+ start: caption.start_ms,
2081
+ end: caption.start_ms + duration
2082
+ } });
2377
2083
  markerPayload = {
2378
- sourceRange: {
2379
- start: 0,
2380
- end: duration
2381
- },
2382
- duration: {
2383
- mode: "fixed",
2384
- value: duration
2385
- }
2084
+ inMs: 0,
2085
+ outMs: duration
2386
2086
  };
2387
2087
  } else {
2388
2088
  const bgm = part.bgm;
@@ -2390,35 +2090,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2390
2090
  if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
2391
2091
  duration = fact.durationMs;
2392
2092
  markerPayload = {
2393
- sourceRange: {
2394
- start: 0,
2395
- end: duration
2396
- },
2397
- duration: { mode: "from-source" },
2398
- durationPolicy: "timeline"
2093
+ inMs: 0,
2094
+ outMs: duration,
2095
+ fillTimeline: true
2399
2096
  };
2400
2097
  }
2401
2098
  if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
2402
2099
  const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2403
2100
  if (normalizedMainRange !== void 0) markerPayload = {
2404
2101
  ...markerPayload,
2405
- targetRange: normalizedMainRange
2102
+ startMs: normalizedMainRange.start
2406
2103
  };
2407
- else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
2104
+ else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
2408
2105
  ...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
2106
+ startMs: item.time_position.offsetMs
2417
2107
  };
2418
2108
  const marker = addEntity("sequence-marker", markerPayload);
2419
- link("track-clip", track, clip);
2420
- link("clip-marker", clip, marker);
2421
- link("marker-content", marker, content);
2109
+ link("contain", track, clip);
2110
+ link("marker", clip, marker);
2111
+ link("contain", clip, content);
2422
2112
  if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
2423
2113
  }
2424
2114
  }
@@ -2439,7 +2129,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2439
2129
  for (const part of Object.values(view.part_library ?? {})) {
2440
2130
  const speech = part.speech;
2441
2131
  if (speech?.phoneme_script === null) delete speech.phoneme_script;
2442
- if (speech !== void 0) delete speech.origin_asset_system;
2132
+ const speed = part.video_clip?.speed_shift;
2133
+ if (speed !== void 0) delete speed.mode;
2443
2134
  }
2444
2135
  return { view };
2445
2136
  };
@@ -2455,22 +2146,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
2455
2146
  const sequentialOrders = [];
2456
2147
  for (const item of oldTrack.items ?? []) {
2457
2148
  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"));
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"));
2459
2150
  const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
2460
2151
  if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
2461
2152
  const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
2462
2153
  const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
2463
2154
  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");
2155
+ if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
2465
2156
  } 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");
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");
2467
2158
  } 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");
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");
2469
2160
  sequentialOrders.push(clip.payload.order);
2470
2161
  } 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");
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");
2474
2164
  }
2475
2165
  if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
2476
2166
  }
@@ -2711,7 +2401,7 @@ function requirePart(document, partId) {
2711
2401
  return part;
2712
2402
  }
2713
2403
  function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
2714
- 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);
2715
2405
  if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
2716
2406
  if (renders.length === 1) {
2717
2407
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
@@ -2739,7 +2429,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
2739
2429
  baseEntityIds: [script.entityId],
2740
2430
  ...phonemeScript === void 0 ? {} : { phonemeScript }
2741
2431
  });
2742
- link("phonetic-script-render", voiceoverAudioEntityId, phonetic);
2432
+ link("derived-from", voiceoverAudioEntityId, phonetic);
2743
2433
  return phonetic;
2744
2434
  }
2745
2435
  function composedAudioScript(phonetic, entities) {
@@ -2801,12 +2491,6 @@ function assertCaptionStyle(style) {
2801
2491
  "position_y"
2802
2492
  ], "Caption style");
2803
2493
  }
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
2494
  function requireFinite(value, label) {
2811
2495
  if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
2812
2496
  return value;
@@ -5314,4 +4998,4 @@ function serverCovers(serverVV, target) {
5314
4998
  return covers(server, target);
5315
4999
  }
5316
5000
  //#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 };
5001
+ 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, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };