@mengine/medeo-client 2.0.1-alpha.14 → 2.0.1-alpha.15

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 ensureLaneTrack, A as assembleEntityContent, B as hasSequence, C as projectEntityTimeline, Ct as bytesToBase64, D as decodeEntityRelationRows, E as EntityTimelineProjectionError, F as updateEntityFields, G as fromVideoDocument, H as buildInitialVideoDocument, I as variantBaseEntityIds, J as assertValidVideoDocument, K as toVideoDocument, L as isJsonObject, M as assembleScriptText, N as findComposedAudioScript, O as ScriptCompositionError, P as selectAudioScriptSegment, Q as LANE_KINDS_IN_STACK_ORDER, R as createEntityId, S as resolveFieldPath, St as base64ToBytes, T as resolveEntityTimelineLayout, U as buildSpeechHostMap, V as isMediaAssetVariantKind, W as derivePositionFromAbs, X as partUnionSchema, Y as validateVideoDocument, Z as videoDocumentSchema, _ as audioScriptAssetOf, _t as effectiveVideoClipDurationMs, a as createMirrorVideoDocument, at as arrangeMainTrackSeamlessly, b as applyFieldChanges, bt as recordEntries, c as DocumentMutationGuard, ct as recalculateTimelineDuration, d as LoroEntityDraft, dt as syncAggregatedClipsTimePosition, et as findLaneTrack, f as readEntityDocument, ft as TIMELINE_SKELETON_DURATION_MS, g as audioScriptAssetHash, gt as DEFAULT_UNIT_TIME_MS, h as audioScriptAssetFields, ht as safeDurationMs, i as MirrorVideoDocumentAdapter, it as cascadeAfterVideoClipChanges, j as assemblePhoneticScriptContent, k as assembleCaptionContent, l as LORO_ENTITY_SCHEMA, lt as resolveAllSpeechOverlaps, m as audioScriptAssetContent, mt as partDurationMs, n as createPlainMemoryAdapter, nt as laneTrackId, o as createMirrorVideoDocumentAdapter, ot as fillMainTrackTimeGaps, p as entityOrderGroups, pt as isEmptyVideoClip, q as VideoDocumentValidationError, r as generatePartId, rt as solveVideoDocument, s as readVideoDocumentFromDraft, st as reassignSpeechesToVideoClipsByTime, t as PlainMemoryAdapter, tt as hasResolvedPlacement, u as LoroEntityDocument, ut as resolveSpeechOverlapByShiftingVideos, v as canonicalJson, vt as speedOf, w as EntityProjectionGraph, x as assertFieldChanges, xt as videoDocumentMirrorSchema, y as equalJson, yt as partUnionToDraft, z as createRelationId } from "./document-BHHbS_17.js";
1
+ import { $ as partUnionSchema, A as decodeEntityRelationRows, B as isJsonObject, C as applyFieldChanges, Ct as recordEntries, D as EntityProjectionGraph, E as projectEntityTimeline, Et as bytesToBase64, F as assembleScriptText, G as buildInitialVideoDocument, H as createRelationId, I as findComposedAudioScript, J as fromVideoDocument, K as buildSpeechHostMap, L as selectAudioScriptSegment, M as assembleCaptionContent, N as assembleEntityContent, O as resolveEntityTimelineLayout, P as assemblePhoneticScriptContent, Q as validateVideoDocument, R as updateEntityFields, S as equalJson, St as partUnionToDraft, T as resolveFieldPath, Tt as base64ToBytes, U as hasSequence, V as createEntityId, W as isMediaAssetVariantKind, X as VideoDocumentValidationError, Y as toVideoDocument, Z as assertValidVideoDocument, _ as audioScriptAssetOf, _t as partDurationMs, a as createMirrorVideoDocument, at as laneTrackId, b as phoneticScriptAssetOf, bt as effectiveVideoClipDurationMs, c as DocumentMutationGuard, ct as arrangeMainTrackSeamlessly, d as LoroEntityDraft, dt as recalculateTimelineDuration, et as videoDocumentSchema, f as readEntityDocument, ft as resolveAllSpeechOverlaps, g as audioScriptAssetHash, gt as isEmptyVideoClip, h as audioScriptAssetFields, ht as TIMELINE_SKELETON_DURATION_MS, i as MirrorVideoDocumentAdapter, it as hasResolvedPlacement, j as ScriptCompositionError, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as fillMainTrackTimeGaps, m as audioScriptAssetContent, mt as syncAggregatedClipsTimePosition, n as createPlainMemoryAdapter, nt as ensureLaneTrack, o as createMirrorVideoDocumentAdapter, ot as solveVideoDocument, p as entityOrderGroups, pt as resolveSpeechOverlapByShiftingVideos, q as derivePositionFromAbs, r as generatePartId, rt as findLaneTrack, s as readVideoDocumentFromDraft, st as cascadeAfterVideoClipChanges, t as PlainMemoryAdapter, tt as LANE_KINDS_IN_STACK_ORDER, u as LoroEntityDocument, ut as reassignSpeechesToVideoClipsByTime, v as phoneticScriptAssetContent, vt as safeDurationMs, w as assertFieldChanges, wt as videoDocumentMirrorSchema, x as scriptSourceAssetOf, xt as speedOf, y as phoneticScriptAssetHash, yt as DEFAULT_UNIT_TIME_MS, z as variantBaseEntityIds } from "./document-CAZ_A9R8.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";
@@ -223,7 +221,8 @@ function compileEntityRows(snapshot, rows, validate) {
223
221
  }
224
222
  /** Compile an edit on an existing peer, retaining its native local undo history. */
225
223
  function applyEntityRows(editor, rows) {
226
- const before = editor.snapshot().rows;
224
+ const baseline = editor.snapshot();
225
+ const before = baseline.rows;
227
226
  const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
228
227
  const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
229
228
  const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
@@ -239,10 +238,16 @@ function applyEntityRows(editor, rows) {
239
238
  if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
240
239
  }
241
240
  }
241
+ let attached = baseline.project.audioScriptEntityId !== void 0;
242
242
  for (const row of rows.entities) {
243
243
  const previous = oldEntities.get(row.entityId);
244
- if (!previous) draft.create(row);
245
- else {
244
+ if (!previous) {
245
+ draft.create(row);
246
+ if (!attached && row.entityKind === "audio-script") {
247
+ draft.attach("audioScriptEntityId", String(row.entityId));
248
+ attached = true;
249
+ }
250
+ } else {
246
251
  if (previous.entityKind !== row.entityKind) throw new Error("Entity kind cannot change");
247
252
  if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
248
253
  }
@@ -352,23 +357,14 @@ function memotaLocator(value) {
352
357
  * explicit project attachment, multiple candidates are ambiguous. Existing
353
358
  * content IDs are reused here; Loro editing advances their immutable versions.
354
359
  */
355
- function ensureDocumentAudioScript(entities, idFactory, selectedEntityId) {
360
+ function ensureDocumentAudioScript(entities, selectedEntityId) {
356
361
  if (selectedEntityId != null) {
357
362
  if (!entities.some((row) => row.entityKind === "audio-script" && row.entityId === selectedEntityId)) throw new EntityTimelineProjectionError(`Selected AudioScript ${selectedEntityId} does not exist`);
358
363
  return selectedEntityId;
359
364
  }
360
365
  const scripts = entities.filter((row) => row.entityKind === "audio-script");
361
366
  if (scripts.length > 1) throw new EntityTimelineProjectionError("Select an AudioScript for the editor panel when multiple scripts exist");
362
- const existing = scripts[0];
363
- if (existing !== void 0) return existing.entityId;
364
- const id = createEntityId(idFactory("entity"));
365
- if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
366
- entities.push({
367
- entityId: id,
368
- entityKind: "audio-script",
369
- payload: { segments: [] }
370
- });
371
- return id;
367
+ return scripts[0]?.entityId ?? null;
372
368
  }
373
369
  /** Add only missing editor structure; existing identities and owned settings are never replaced. */
374
370
  function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}, selectedAudioScriptEntityId) {
@@ -422,7 +418,7 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
422
418
  }
423
419
  trackEntityIds[role] = track.entityId;
424
420
  }
425
- const audioScriptEntityId = ensureDocumentAudioScript(entities, idFactory, selectedAudioScriptEntityId);
421
+ const audioScriptEntityId = ensureDocumentAudioScript(entities, selectedAudioScriptEntityId);
426
422
  return {
427
423
  rows: {
428
424
  entities,
@@ -490,15 +486,16 @@ function parseWireState(value, expectedDocId) {
490
486
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
491
487
  const rows = parseWireRows(value.rows);
492
488
  decodeEntityRelationRows(rows, numericMarkerValidation$1);
493
- if (typeof value.audio_script_entity_id !== "string") throw new Error("Document AudioScript is not initialized");
489
+ const attached = value.audio_script_entity_id;
490
+ if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
494
491
  if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
495
492
  const state = {
496
493
  revision: value.revision,
497
494
  loroSnapshot: value.loro_snapshot,
498
495
  rows,
499
- audioScriptEntityId: value.audio_script_entity_id
496
+ audioScriptEntityId: attached
500
497
  };
501
- readDocumentAudioScript(state);
498
+ if (attached !== null) readDocumentAudioScript(state);
502
499
  return state;
503
500
  }
504
501
  function parseWireRows(value) {
@@ -557,30 +554,6 @@ const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right)
557
554
  if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
558
555
  return left - right;
559
556
  } };
560
- /** Import only the fixed ASR artifact; Caption entities are authored separately. */
561
- function importCaptionAsset(initial, fact, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
562
- if (!fact.assetId || fact.assetId.trim() !== fact.assetId) throw new Error("Expected a caption asset id");
563
- if (fact.storageKey !== void 0 && (!fact.storageKey || fact.storageKey.trim() !== fact.storageKey)) throw new Error("Caption asset storage key must be non-empty and trimmed");
564
- decodeEntityRelationRows(initial, numericCoordinates);
565
- assertCanonicalEditorResources(initial);
566
- const entities = structuredClone([...initial.entities]);
567
- const relations = structuredClone([...initial.relations]);
568
- const asset = ensureAsset(entities, relations, {
569
- system: "memota",
570
- key: fact.assetId,
571
- ...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey }
572
- }, idFactory);
573
- const rows = {
574
- entities,
575
- relations
576
- };
577
- decodeEntityRelationRows(rows, numericCoordinates);
578
- assertCanonicalEditorResources(rows);
579
- return {
580
- rows,
581
- assetEntityId: asset.entityId
582
- };
583
- }
584
557
  /**
585
558
  * Import at the editor boundary, not at asset generation time. Existing
586
559
  * provenance is retained.
@@ -608,7 +581,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
608
581
  const entityKind = fact.kind === "voiceover" ? "audio" : fact.kind;
609
582
  const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
610
583
  const durationMs = fact.kind === "image" ? null : fact.durationMs;
611
- const asset = ensureAsset(entities, relations, {
584
+ const asset = ensureAsset(entities, {
612
585
  system,
613
586
  key: fact.assetId,
614
587
  storageKey: fact.storageKey
@@ -652,31 +625,20 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
652
625
  };
653
626
  }
654
627
  /** One Asset per `(system, key)`: the bytes are named in exactly one row. */
655
- function ensureAsset(entities, relations, locator, idFactory) {
628
+ function ensureAsset(entities, locator, idFactory) {
656
629
  const found = entities.find((row) => row.entityKind === "asset" && row.payload.system === locator.system && row.payload.key === locator.key);
657
630
  if (found !== void 0) {
658
631
  if (locator.storageKey === void 0 || found.payload.storageKey === locator.storageKey) return found;
659
632
  if (found.payload.storageKey !== void 0) throw new Error(`Media facts conflict with existing storage key for ${locator.key}`);
660
- const replacement = {
661
- entityId: createEntityId(idFactory("entity")),
662
- entityKind: "asset",
633
+ const filled = {
634
+ ...found,
663
635
  payload: {
664
636
  ...found.payload,
665
637
  storageKey: locator.storageKey
666
638
  }
667
639
  };
668
- if (entities.some((row) => row.entityId === replacement.entityId)) throw new Error(`Duplicate entity id ${replacement.entityId}`);
669
- const bindings = relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === found.entityId || relation.endpoint1EntityId === found.entityId)).sort((left, right) => String(left.relationId).localeCompare(String(right.relationId)));
670
- const replaced = new Set(bindings.map((relation) => relation.relationId));
671
- const nextBindings = bindings.map((relation) => ({
672
- ...relation,
673
- relationId: createRelationId(idFactory("relation")),
674
- endpoint0EntityId: relation.endpoint0EntityId === found.entityId ? replacement.entityId : relation.endpoint0EntityId,
675
- endpoint1EntityId: relation.endpoint1EntityId === found.entityId ? replacement.entityId : relation.endpoint1EntityId
676
- }));
677
- relations.splice(0, relations.length, ...relations.filter((relation) => !replaced.has(relation.relationId)), ...nextBindings);
678
- replaceEntity$1(entities, found.entityId, replacement);
679
- return replacement;
640
+ replaceEntity$1(entities, found.entityId, filled);
641
+ return filled;
680
642
  }
681
643
  const asset = {
682
644
  entityId: createEntityId(idFactory("entity")),
@@ -785,7 +747,7 @@ const defaultIdFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()
785
747
  * Each mutation is applied to a detached graph, validated as a complete DSL set,
786
748
  * and committed only after validation succeeds.
787
749
  */
788
- var EntityTimelineEditor = class EntityTimelineEditor {
750
+ var EntityTimelineEditor = class {
789
751
  idFactory;
790
752
  state;
791
753
  constructor(initialRows, idFactory = defaultIdFactory) {
@@ -1096,65 +1058,49 @@ var EntityTimelineEditor = class EntityTimelineEditor {
1096
1058
  });
1097
1059
  }
1098
1060
  setClipSpeed(input) {
1099
- this.transact((draft) => this.setClipSpeedIntoDraft(draft, input));
1100
- }
1101
- /** Apply one typed Director speed effect as a single graph transaction. */
1102
- setVideoClipSpeeds(input) {
1103
- if (input.clips.length === 0) throw new Error("At least one Video Clip speed is required");
1104
- if (new Set(input.clips.map((clip) => clip.videoClipEntityId)).size !== input.clips.length) throw new Error("Video Clip speed targets must be unique");
1105
1061
  this.transact((draft) => {
1106
- for (const clip of input.clips) {
1107
- const rate = speedModeRate(clip.mode);
1108
- this.setClipSpeedIntoDraft(draft, {
1109
- clipEntityId: clip.videoClipEntityId,
1110
- timeRemapping: rate === 1 ? null : {
1111
- kind: "linear",
1112
- rate,
1113
- mode: "constant"
1114
- }
1115
- });
1116
- }
1117
- });
1118
- }
1119
- setClipSpeedIntoDraft(draft, input) {
1120
- const topology = requireCanonicalClipOnRole(draft, input.clipEntityId, "video_clip");
1121
- const content = requireSequenceContent(draft, topology.markerContentRelation.endpoint1EntityId);
1122
- if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
1123
- const initialPayload = cloneJsonObject(topology.marker.payload, "Sequence Marker payload");
1124
- const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
1125
- if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
1126
- const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1127
- let sourceRange = priorSource;
1128
- if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
1129
- start: 0,
1130
- end: markerDuration(initialPayload, priorSource)
1131
- };
1132
- if (input.timeRemapping === null) {
1133
- delete payload.timeRemapping;
1134
- payload.sourceRange = sourceRange;
1135
- payload.duration = {
1136
- mode: "fixed",
1137
- value: sourceRange.end - sourceRange.start
1138
- };
1139
- } else {
1140
- const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
1141
- const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1142
- if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
1143
- payload.sourceRange = sourceRange;
1144
- payload.duration = {
1145
- mode: "fixed",
1146
- value: duration
1147
- };
1148
- payload.timeRemapping = {
1149
- kind: "linear",
1150
- rate,
1151
- ...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
1062
+ const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
1063
+ const content = requireSequenceContent(draft, otherEndpoint(initialTopology.markerContentRelation, initialTopology.marker.entityId));
1064
+ if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
1065
+ const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
1066
+ const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
1067
+ if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
1068
+ this.normalizeMainSequential(draft);
1069
+ const topology = requireClipTopology(draft, input.clipEntityId);
1070
+ const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
1071
+ let sourceRange = priorSource;
1072
+ if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
1073
+ start: 0,
1074
+ end: markerDuration(initialPayload, priorSource)
1152
1075
  };
1153
- }
1154
- resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
1155
- replaceEntity(draft, topology.marker.entityId, {
1156
- ...topology.marker,
1157
- payload
1076
+ if (input.timeRemapping === null) {
1077
+ delete payload.timeRemapping;
1078
+ payload.sourceRange = sourceRange;
1079
+ payload.duration = {
1080
+ mode: "fixed",
1081
+ value: sourceRange.end - sourceRange.start
1082
+ };
1083
+ } else {
1084
+ const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
1085
+ const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
1086
+ if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
1087
+ payload.sourceRange = sourceRange;
1088
+ payload.duration = {
1089
+ mode: "fixed",
1090
+ value: duration
1091
+ };
1092
+ payload.timeRemapping = {
1093
+ kind: "linear",
1094
+ rate,
1095
+ ...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
1096
+ };
1097
+ }
1098
+ resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
1099
+ replaceEntity(draft, topology.marker.entityId, {
1100
+ ...topology.marker,
1101
+ payload
1102
+ });
1103
+ this.resolveOneVoiceoverOverlap(draft);
1158
1104
  });
1159
1105
  }
1160
1106
  trimClip(input) {
@@ -1504,447 +1450,12 @@ var EntityTimelineEditor = class EntityTimelineEditor {
1504
1450
  });
1505
1451
  });
1506
1452
  }
1507
- #applyCaptionAssets(envelope) {
1508
- return this.transact((draft) => {
1509
- const input = envelope.input;
1510
- const effectId = requireTrimmed(input.effectId, "Caption effect id");
1511
- const identityScope = stableEffectScope(captionAssetsCommand, effectId, envelope.baselineFingerprint);
1512
- const segments = validateAsrSegments(input.segments);
1513
- const source = requireEntityKind(draft, input.source.entityId, input.source.entityKind);
1514
- const track = requireCanonicalCaptionTrack(draft, input.timelineEntityId, input.trackEntityId);
1515
- const signature = envelope.signature;
1516
- const replay = resolveCaptionAssetsReplay(draft, input, effectId, signature, segments, source.entityId, track.entityId);
1517
- if (replay !== void 0) return replay;
1518
- const imported = importCaptionAsset(draft, input.captionAsset, stableEffectIdFactory(identityScope, "caption-asset"));
1519
- if (imported.assetEntityId !== envelope.decisions.captionAsset.resultAssetEntityId) throw new Error("Frozen Caption Asset decision no longer matches the causal baseline");
1520
- replaceRows(draft, imported.rows);
1521
- const scriptId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, "audio-script"));
1522
- const scriptMarkerId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, "audio-script-marker"));
1523
- const asrStartMs = Math.min(...segments.map((segment) => segment.startMs));
1524
- const asrEndMs = Math.max(...segments.map((segment) => segment.endMs));
1525
- draft.entities.push({
1526
- entityId: scriptId,
1527
- entityKind: "audio-script",
1528
- payload: { segments: segments.map(({ segmentId, text, language }) => ({
1529
- segmentId,
1530
- text,
1531
- ...language === void 0 ? {} : { language }
1532
- })) }
1533
- });
1534
- draft.entities.push({
1535
- entityId: scriptMarkerId,
1536
- entityKind: "sequence-marker",
1537
- payload: {
1538
- sourceRange: {
1539
- start: asrStartMs,
1540
- end: asrEndMs
1541
- },
1542
- duration: {
1543
- mode: "fixed",
1544
- value: asrEndMs - asrStartMs
1545
- },
1546
- segmentRanges: segments.map(({ segmentId, startMs, endMs }) => ({
1547
- segmentId,
1548
- startMs,
1549
- endMs
1550
- }))
1551
- }
1552
- });
1553
- draft.relations.push(this.createRelation(draft, "audio-script-source", scriptId, source.entityId, {}, effectTrace(effectId, signature), stableEffectId("relation", identityScope, "audio-script-source")), this.createRelation(draft, "audio-script-marker", scriptId, scriptMarkerId, {}, effectTrace(effectId, signature), stableEffectId("relation", identityScope, "audio-script-marker")));
1554
- const captionEntityIds = [];
1555
- const captionClipEntityIds = [];
1556
- for (const [index, segment] of segments.entries()) {
1557
- const durationMs = segment.endMs - segment.startMs;
1558
- const target = envelope.decisions.targetRanges[index];
1559
- if (target?.segmentId !== segment.segmentId) throw new Error("Frozen Caption placement order is invalid");
1560
- const targetRange = {
1561
- start: target.start,
1562
- end: target.end
1563
- };
1564
- const captionId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, `caption:${segment.segmentId}`));
1565
- draft.entities.push({
1566
- entityId: captionId,
1567
- entityKind: "caption",
1568
- payload: {
1569
- durationMs,
1570
- baseEntityIds: [scriptId],
1571
- selection: { segmentId: segment.segmentId }
1572
- }
1573
- });
1574
- const trace = effectTrace(effectId, signature, segment.segmentId);
1575
- draft.relations.push(this.createRelation(draft, "from-asset", captionId, createEntityId(imported.assetEntityId), {}, trace, stableEffectId("relation", identityScope, `caption-asset:${segment.segmentId}`)));
1576
- const clipId = this.insertPlacedClipIntoDraft(draft, {
1577
- clipEntityId: stableEffectId("entity", identityScope, `caption-clip:${segment.segmentId}`),
1578
- markerEntityId: stableEffectId("entity", identityScope, `caption-marker:${segment.segmentId}`),
1579
- trackEntityId: track.entityId,
1580
- contentEntityId: captionId,
1581
- sourceRange: {
1582
- start: 0,
1583
- end: durationMs
1584
- },
1585
- duration: {
1586
- mode: "fixed",
1587
- value: targetRange.end - targetRange.start
1588
- },
1589
- placement: {
1590
- kind: "absolute",
1591
- targetRange
1592
- },
1593
- relationTrace: trace,
1594
- relationIds: {
1595
- trackClip: stableEffectId("relation", identityScope, `track-clip:${segment.segmentId}`),
1596
- clipMarker: stableEffectId("relation", identityScope, `clip-marker:${segment.segmentId}`),
1597
- markerContent: stableEffectId("relation", identityScope, `marker-content:${segment.segmentId}`)
1598
- }
1599
- });
1600
- captionEntityIds.push(captionId);
1601
- captionClipEntityIds.push(clipId);
1602
- }
1603
- return {
1604
- audioScriptEntityId: scriptId,
1605
- audioScriptMarkerEntityId: scriptMarkerId,
1606
- captionAssetEntityId: imported.assetEntityId,
1607
- captionEntityIds,
1608
- captionClipEntityIds
1609
- };
1610
- });
1611
- }
1612
- static compileCaptionAssetsEffect(base, envelope) {
1613
- assertCaptionAssetsEnvelope(envelope);
1614
- const input = envelope.input;
1615
- const effectId = requireTrimmed(input.effectId, "Caption effect id");
1616
- const segments = validateAsrSegments(input.segments);
1617
- if (effectId !== envelope.effectId || captionEffectSignature(input, segments) !== envelope.signature) throw new Error("Caption effect envelope signature does not match its canonical input");
1618
- const current = cloneRows(base.snapshot().rows);
1619
- const replay = resolveCaptionAssetsReplay(current, input, effectId, envelope.signature, segments, createEntityId(input.source.entityId), createEntityId(input.trackEntityId));
1620
- if (replay !== void 0) return {
1621
- result: replay,
1622
- replayed: true,
1623
- rows: current
1624
- };
1625
- const fingerprint = captionBaselineFingerprint(base);
1626
- if (fingerprint !== envelope.baselineFingerprint) throw new Error("Caption effect baseline changed; independently prepared envelopes on divergent baselines are unsupported");
1627
- if (canonicalJson(resolveCaptionEffectDecisions(current, input, stableEffectScope(captionAssetsCommand, effectId, fingerprint), segments)) !== canonicalJson(envelope.decisions)) throw new Error("Caption effect decisions no longer match the frozen causal baseline");
1628
- const doc = base.doc.fork();
1629
- doc.setPeerId(stableEffectPeerId(captionAssetsCommand, effectId, fingerprint));
1630
- const staged = new LoroEntityDocument(doc);
1631
- const editor = new EntityTimelineEditor(staged.snapshot().rows, () => {
1632
- throw new Error("Caption effects must use effect-derived identities");
1633
- });
1634
- const result = editor.#applyCaptionAssets(envelope);
1635
- const applied = applyEntityRows(staged, editor.rows());
1636
- return {
1637
- result,
1638
- replayed: false,
1639
- update: applied.update,
1640
- rows: applied.state.rows
1641
- };
1642
- }
1643
- #applyGeneratedVisuals(envelope) {
1644
- return this.transact((draft) => {
1645
- const { input } = envelope;
1646
- const identityScope = stableEffectScope(generatedVisualsCommand, envelope.effectId, envelope.baselineFingerprint);
1647
- const anchorInput = visualInsertionAnchor(input);
1648
- const anchor = requireCanonicalClipOnRole(draft, anchorInput.clipEntityId, "video_clip");
1649
- if (anchor.trackRelation.endpoint0EntityId !== envelope.decisions.trackEntityId || anchor.trackRelation.endpoint1EntityId !== anchor.clip.entityId || anchorInput.position !== envelope.decisions.anchorPosition) throw new Error("Frozen visual insertion anchor is no longer canonical");
1650
- const ordered = resolveEntityTimelineLayout(draft).tracks.find((candidate) => candidate.entityId === envelope.decisions.trackEntityId)?.clips.map((clip) => clip.clipEntityId);
1651
- const anchorIndex = ordered?.indexOf(anchor.clip.entityId) ?? -1;
1652
- if (ordered === void 0 || anchorIndex < 0 || anchorIndex !== envelope.decisions.anchorOrder) throw new Error("Frozen visual insertion order no longer matches the causal baseline");
1653
- const clipEntityIds = [];
1654
- const mediaEntityIds = [];
1655
- for (const [index, clip] of input.clips.entries()) {
1656
- const imported = importMediaAsset(draft, clip.media, stableEffectIdFactory(identityScope, `visual:${index}:media`));
1657
- replaceRows(draft, imported.rows);
1658
- const media = requireSequenceContent(draft, imported.contentEntityId);
1659
- if (media.entityKind !== "video" && media.entityKind !== "image") throw new Error("Generated visual content must be Video or Image");
1660
- const durationMs = safeWholeMs(clip.durationMs, "Generated visual durationMs");
1661
- if (durationMs === 0 || clip.media.kind === "video" && durationMs > clip.media.durationMs) throw new Error("Generated visual duration must fit its immutable media Sequence");
1662
- const trace = generatedVisualTrace(envelope, index, clip.generationJobId);
1663
- const clipId = this.insertPlacedClipIntoDraft(draft, {
1664
- clipEntityId: stableEffectId("entity", identityScope, `visual:${index}:clip`),
1665
- markerEntityId: stableEffectId("entity", identityScope, `visual:${index}:marker`),
1666
- trackEntityId: envelope.decisions.trackEntityId,
1667
- contentEntityId: media.entityId,
1668
- sourceRange: {
1669
- start: 0,
1670
- end: durationMs
1671
- },
1672
- duration: {
1673
- mode: "fixed",
1674
- value: durationMs
1675
- },
1676
- placement: {
1677
- kind: "sequential",
1678
- order: envelope.decisions.insertionOrders[index]
1679
- },
1680
- ...clip.volume === void 0 ? {} : { clipPayload: { volume: clip.volume } },
1681
- relationTrace: trace,
1682
- relationIds: {
1683
- trackClip: stableEffectId("relation", identityScope, `visual:${index}:track-clip`),
1684
- clipMarker: stableEffectId("relation", identityScope, `visual:${index}:clip-marker`),
1685
- markerContent: stableEffectId("relation", identityScope, `visual:${index}:marker-content`)
1686
- }
1687
- });
1688
- for (const [sourceIndex, sourceId] of clip.sourceMediaEntityIds.entries()) {
1689
- const source = requireSequenceContent(draft, sourceId);
1690
- if (![
1691
- "video",
1692
- "image",
1693
- "audio"
1694
- ].includes(source.entityKind)) throw new Error("Generated visual lineage requires Video, Image, or Audio inputs");
1695
- draft.relations.push(this.createRelation(draft, "generated", media.entityId, source.entityId, {}, generatedVisualTrace(envelope, index, clip.generationJobId, sourceIndex), stableEffectId("relation", identityScope, `visual:${index}:generated:${sourceIndex}`)));
1696
- }
1697
- clipEntityIds.push(clipId);
1698
- mediaEntityIds.push(media.entityId);
1699
- }
1700
- return {
1701
- clipEntityIds,
1702
- mediaEntityIds
1703
- };
1704
- });
1705
- }
1706
- static compileGeneratedVisualEffect(base, envelope) {
1707
- assertGeneratedVisualEnvelope(envelope);
1708
- consumeEffectEnvelope(consumedGeneratedVisualEnvelopes, envelope, "Generated visual");
1709
- const current = cloneRows(base.snapshot().rows);
1710
- const replay = resolveGeneratedVisualReplay(current, envelope);
1711
- if (replay !== void 0) return {
1712
- result: replay,
1713
- replayed: true,
1714
- rows: current
1715
- };
1716
- const fingerprint = captionBaselineFingerprint(base);
1717
- if (fingerprint !== envelope.baselineFingerprint) throw new Error("Generated visual effect baseline changed; prepare a new effect from the authority");
1718
- if (canonicalJson(resolveGeneratedVisualDecisions(current, envelope.input)) !== canonicalJson(envelope.decisions)) throw new Error("Generated visual decisions no longer match the frozen causal baseline");
1719
- const doc = base.doc.fork();
1720
- doc.setPeerId(stableEffectPeerId(generatedVisualsCommand, envelope.effectId, fingerprint));
1721
- const staged = new LoroEntityDocument(doc);
1722
- const editor = new EntityTimelineEditor(staged.snapshot().rows, () => {
1723
- throw new Error("Generated visual effects must use effect-derived identities");
1724
- });
1725
- const result = editor.#applyGeneratedVisuals(envelope);
1726
- const applied = applyEntityRows(staged, editor.rows());
1727
- return {
1728
- result,
1729
- replayed: false,
1730
- update: applied.update,
1731
- rows: applied.state.rows
1732
- };
1733
- }
1734
- #applySpeechAssets(envelope) {
1735
- return this.transact((draft) => {
1736
- const { input } = envelope;
1737
- const value = input.value;
1738
- const identityScope = stableEffectScope(speechAssetsCommand, envelope.effectId, envelope.baselineFingerprint);
1739
- const speechTrack = ensureEffectRoleTrack(draft, value.timelineEntityId, "speech", envelope.decisions.speechTrackEntityId, identityScope);
1740
- const captionTrack = value.items.some((item) => item.captions.length > 0) ? ensureEffectRoleTrack(draft, value.timelineEntityId, "caption", envelope.decisions.captionTrackEntityId, identityScope) : void 0;
1741
- const speechClipEntityIds = [];
1742
- const speechAudioEntityIds = [];
1743
- const audioScriptEntityIds = [];
1744
- const phoneticScriptEntityIds = [];
1745
- const captionClipEntityIds = [];
1746
- for (const [index, item] of value.items.entries()) {
1747
- const trace = speechEffectTrace(envelope, index);
1748
- const scripts = materializeSpeechSource(draft, item, identityScope, index, trace);
1749
- const imported = importMediaAsset(draft, item.media, stableEffectIdFactory(identityScope, `speech:${index}:media`));
1750
- replaceRows(draft, imported.rows);
1751
- const speech = requireEntityKind(draft, imported.contentEntityId, "audio");
1752
- if (imported.voiceEntityId === void 0 || item.media.voice === void 0) throw new Error("Generated speech requires an explicit Voice identity");
1753
- if (incidentRelations(draft, speech.entityId).filter((relation) => relation.relationKind === "phonetic-script-render").length > 0) throw new Error(`Speech Audio "${speech.entityId}" already has render lineage`);
1754
- draft.relations.push(this.createRelation(draft, "phonetic-script-render", speech.entityId, scripts.phonetic.entityId, {}, {
1755
- ...trace,
1756
- role: "speech-render"
1757
- }, stableEffectId("relation", identityScope, `speech:${index}:render`)));
1758
- const startMs = envelope.decisions.targetStartsMs[index];
1759
- let speechClipId;
1760
- if (!("targetSpeechClipEntityId" in item)) speechClipId = this.insertPlacedClipIntoDraft(draft, {
1761
- clipEntityId: stableEffectId("entity", identityScope, `speech:${index}:clip`),
1762
- markerEntityId: stableEffectId("entity", identityScope, `speech:${index}:marker`),
1763
- trackEntityId: speechTrack.entityId,
1764
- contentEntityId: speech.entityId,
1765
- sourceRange: {
1766
- start: 0,
1767
- end: item.media.durationMs
1768
- },
1769
- duration: {
1770
- mode: "fixed",
1771
- value: item.media.durationMs
1772
- },
1773
- placement: {
1774
- kind: "absolute",
1775
- targetRange: {
1776
- start: startMs,
1777
- end: startMs + item.media.durationMs
1778
- }
1779
- },
1780
- clipPayload: { volume: requireVolume(item.volume ?? 0) },
1781
- relationTrace: trace,
1782
- relationIds: {
1783
- trackClip: stableEffectId("relation", identityScope, `speech:${index}:track-clip`),
1784
- clipMarker: stableEffectId("relation", identityScope, `speech:${index}:clip-marker`),
1785
- markerContent: stableEffectId("relation", identityScope, `speech:${index}:marker-content`)
1786
- }
1787
- });
1788
- else {
1789
- const target = requireCanonicalClipOnRole(draft, item.targetSpeechClipEntityId, "speech");
1790
- if (target.trackRelation.endpoint0EntityId !== speechTrack.entityId) throw new Error("Replacement Speech occurrence is not on the frozen Speech Track");
1791
- speechClipId = target.clip.entityId;
1792
- replaceEntity(draft, target.clip.entityId, {
1793
- ...target.clip,
1794
- payload: {
1795
- ...target.clip.payload,
1796
- volume: requireVolume(item.volume ?? 0)
1797
- }
1798
- });
1799
- replaceRelationWithTrace(draft, target.markerContentRelation, target.marker.entityId, speech.entityId, {
1800
- ...trace,
1801
- role: "speech-content"
1802
- }, stableEffectId("relation", identityScope, `speech:${index}:marker-content`));
1803
- const markerPayload = { ...cloneJsonObject(target.marker.payload, "Speech Marker payload") };
1804
- markerPayload.sourceRange = {
1805
- start: 0,
1806
- end: item.media.durationMs
1807
- };
1808
- markerPayload.duration = {
1809
- mode: "fixed",
1810
- value: item.media.durationMs
1811
- };
1812
- resizeAbsoluteTarget(markerPayload, item.media.durationMs);
1813
- replaceEntity(draft, target.marker.entityId, {
1814
- ...target.marker,
1815
- payload: markerPayload
1816
- });
1817
- }
1818
- const currentCaptionIds = "targetCaptionClipEntityIds" in item ? item.targetCaptionClipEntityIds : [];
1819
- const itemCaptionIds = [];
1820
- for (const [captionIndex, caption] of item.captions.entries()) {
1821
- const durationMs = safeWholeMs(caption.endMs, "Speech Caption endMs") - safeWholeMs(caption.startMs, "Speech Caption startMs");
1822
- if (durationMs <= 0) throw new Error("Speech Caption must have positive duration");
1823
- const captionEntity = {
1824
- entityId: requireAvailableEntityId(draft, stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}`)),
1825
- entityKind: "caption",
1826
- payload: {
1827
- baseEntityIds: [scripts.audioScript.entityId],
1828
- selection: captionSelection(caption.selection),
1829
- durationMs
1830
- }
1831
- };
1832
- draft.entities.push(captionEntity);
1833
- assembleCaptionContent(draft, captionEntity.entityId);
1834
- const captionTrace = {
1835
- ...trace,
1836
- role: "caption-content",
1837
- captionIndex
1838
- };
1839
- let captionClipId;
1840
- const existingCaptionId = currentCaptionIds[captionIndex];
1841
- if (!("targetSpeechClipEntityId" in item) || existingCaptionId === void 0) captionClipId = this.insertPlacedClipIntoDraft(draft, {
1842
- clipEntityId: stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}:clip`),
1843
- markerEntityId: stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}:marker`),
1844
- trackEntityId: captionTrack.entityId,
1845
- contentEntityId: captionEntity.entityId,
1846
- sourceRange: {
1847
- start: 0,
1848
- end: durationMs
1849
- },
1850
- duration: {
1851
- mode: "fixed",
1852
- value: durationMs
1853
- },
1854
- placement: {
1855
- kind: "absolute",
1856
- targetRange: {
1857
- start: startMs + caption.startMs,
1858
- end: startMs + caption.endMs
1859
- }
1860
- },
1861
- relationTrace: captionTrace,
1862
- relationIds: {
1863
- trackClip: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:track-clip`),
1864
- clipMarker: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:clip-marker`),
1865
- markerContent: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:marker-content`)
1866
- }
1867
- });
1868
- else {
1869
- const targetCaption = requireCanonicalClipOnRole(draft, existingCaptionId, "caption");
1870
- if (targetCaption.trackRelation.endpoint0EntityId !== captionTrack.entityId) throw new Error("Replacement Caption occurrence is not on the frozen Caption Track");
1871
- captionClipId = targetCaption.clip.entityId;
1872
- replaceRelationWithTrace(draft, targetCaption.markerContentRelation, targetCaption.marker.entityId, captionEntity.entityId, captionTrace, stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:marker-content`));
1873
- replaceEntity(draft, targetCaption.marker.entityId, {
1874
- ...targetCaption.marker,
1875
- payload: {
1876
- ...targetCaption.marker.payload,
1877
- sourceRange: {
1878
- start: 0,
1879
- end: durationMs
1880
- },
1881
- duration: {
1882
- mode: "fixed",
1883
- value: durationMs
1884
- },
1885
- targetRange: {
1886
- start: startMs + caption.startMs,
1887
- end: startMs + caption.endMs
1888
- }
1889
- }
1890
- });
1891
- }
1892
- itemCaptionIds.push(captionClipId);
1893
- }
1894
- for (const retiredCaptionId of currentCaptionIds.slice(item.captions.length)) {
1895
- const retired = requireCanonicalClipOnRole(draft, retiredCaptionId, "caption");
1896
- if (retired.trackRelation.endpoint0EntityId !== envelope.decisions.captionTrackEntityId) throw new Error("Retired Caption occurrence is not on the frozen Caption Track");
1897
- retireClipOccurrence(draft, retired);
1898
- }
1899
- speechClipEntityIds.push(speechClipId);
1900
- speechAudioEntityIds.push(speech.entityId);
1901
- audioScriptEntityIds.push(scripts.audioScript.entityId);
1902
- phoneticScriptEntityIds.push(scripts.phonetic.entityId);
1903
- captionClipEntityIds.push(itemCaptionIds);
1904
- }
1905
- return {
1906
- speechClipEntityIds,
1907
- speechAudioEntityIds,
1908
- audioScriptEntityIds,
1909
- phoneticScriptEntityIds,
1910
- captionClipEntityIds
1911
- };
1912
- });
1913
- }
1914
- static compileSpeechAssetsEffect(base, envelope) {
1915
- assertSpeechAssetsEnvelope(envelope);
1916
- consumeEffectEnvelope(consumedSpeechEnvelopes, envelope, "Speech");
1917
- const current = cloneRows(base.snapshot().rows);
1918
- const replay = resolveSpeechAssetsReplay(current, envelope);
1919
- if (replay !== void 0) return {
1920
- result: replay,
1921
- replayed: true,
1922
- rows: current
1923
- };
1924
- const fingerprint = captionBaselineFingerprint(base);
1925
- if (fingerprint !== envelope.baselineFingerprint) throw new Error("Speech effect baseline changed; prepare a new effect from the authority");
1926
- if (canonicalJson(resolveSpeechEffectDecisions(current, envelope.input, envelope.effectId, fingerprint)) !== canonicalJson(envelope.decisions)) throw new Error("Speech effect decisions no longer match the frozen causal baseline");
1927
- const doc = base.doc.fork();
1928
- doc.setPeerId(stableEffectPeerId(speechAssetsCommand, envelope.effectId, fingerprint));
1929
- const staged = new LoroEntityDocument(doc);
1930
- const editor = new EntityTimelineEditor(staged.snapshot().rows, stableEffectIdFactory(stableEffectScope(speechAssetsCommand, envelope.effectId, fingerprint), "speech-editor"));
1931
- editor.#applySpeechAssets(envelope);
1932
- const applied = applyEntityRows(staged, editor.rows());
1933
- const result = resolveSpeechAssetsReplay(cloneRows(applied.state.rows), envelope);
1934
- if (result === void 0) throw new Error("Committed Speech effect is missing its complete receipt");
1935
- return {
1936
- result,
1937
- replayed: false,
1938
- update: applied.update,
1939
- rows: applied.state.rows
1940
- };
1941
- }
1942
1453
  insertPlacedClipIntoDraft(draft, input) {
1943
1454
  const track = requireEntityKind(draft, input.trackEntityId, "track");
1944
1455
  requireTrackTimeline(draft, track.entityId);
1945
1456
  const content = requireSequenceContent(draft, input.contentEntityId);
1946
1457
  const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
1947
- const markerId = input.markerEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.markerEntityId);
1458
+ const markerId = this.mintEntityId(draft);
1948
1459
  const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
1949
1460
  delete clipPayload.order;
1950
1461
  const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
@@ -1963,7 +1474,7 @@ var EntityTimelineEditor = class EntityTimelineEditor {
1963
1474
  })
1964
1475
  };
1965
1476
  draft.entities.push(clip, marker);
1966
- draft.relations.push(this.createRelation(draft, "track-clip", track.entityId, clipId, {}, input.relationTrace, input.relationIds?.trackClip), this.createRelation(draft, "clip-marker", clipId, markerId, {}, input.relationTrace, input.relationIds?.clipMarker), this.createRelation(draft, "marker-content", markerId, content.entityId, {}, input.relationTrace, input.relationIds?.markerContent));
1477
+ 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));
1967
1478
  if (input.placement.kind === "anchored") {
1968
1479
  const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
1969
1480
  if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
@@ -2269,14 +1780,14 @@ var EntityTimelineEditor = class EntityTimelineEditor {
2269
1780
  if (draft.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
2270
1781
  return entityId;
2271
1782
  }
2272
- createRelation(draft, relationKind, endpoint0EntityId, endpoint1EntityId, metadata = {}, trace = {}, requestedRelationId) {
1783
+ createRelation(draft, relationKind, endpoint0EntityId, endpoint1EntityId, metadata = {}) {
2273
1784
  return {
2274
- relationId: requestedRelationId === void 0 ? this.mintRelationId(draft) : requireAvailableRelationId(draft, requestedRelationId),
1785
+ relationId: this.mintRelationId(draft),
2275
1786
  relationKind,
2276
1787
  endpoint0EntityId,
2277
1788
  endpoint1EntityId,
2278
1789
  metadata,
2279
- trace
1790
+ trace: {}
2280
1791
  };
2281
1792
  }
2282
1793
  relinkRelation(draft, relation, endpoints) {
@@ -2295,859 +1806,6 @@ var EntityTimelineEditor = class EntityTimelineEditor {
2295
1806
  return relationId;
2296
1807
  }
2297
1808
  };
2298
- /** Freeze every state-dependent choice once in the sole FE tool-effect handler. */
2299
- function prepareCaptionAssetsEffect(base, rawInput) {
2300
- const effectId = requireTrimmed(rawInput.effectId, "Caption effect id");
2301
- const segments = validateAsrSegments(rawInput.segments);
2302
- const input = structuredClone({
2303
- ...rawInput,
2304
- effectId,
2305
- segments
2306
- });
2307
- const rows = cloneRows(base.snapshot().rows);
2308
- const signature = captionEffectSignature(input, segments);
2309
- const baselineFingerprint = captionBaselineFingerprint(base);
2310
- return deepFreeze({
2311
- version: 1,
2312
- baselineFingerprint,
2313
- effectId,
2314
- signature,
2315
- input,
2316
- decisions: resolveCaptionEffectDecisions(rows, input, stableEffectScope(captionAssetsCommand, effectId, baselineFingerprint), segments)
2317
- });
2318
- }
2319
- /**
2320
- * Compile only a frozen envelope on its exact causal baseline. Collaborators
2321
- * receive the resulting Loro update, never the raw effect.
2322
- */
2323
- function compileCaptionAssetsEffect(base, envelope) {
2324
- return EntityTimelineEditor.compileCaptionAssetsEffect(base, envelope);
2325
- }
2326
- const captionAssetsCommand = "apply-caption-assets";
2327
- const generatedVisualsCommand = "add-generated-visuals";
2328
- const speechAssetsCommand = "apply-speech-assets";
2329
- const consumedGeneratedVisualEnvelopes = /* @__PURE__ */ new Set();
2330
- const consumedSpeechEnvelopes = /* @__PURE__ */ new Set();
2331
- function consumeEffectEnvelope(consumed, envelope, label) {
2332
- const key = `${envelope.baselineFingerprint}\u0000${envelope.effectId}\u0000${envelope.signature}`;
2333
- if (consumed.has(key)) throw new Error(`${label} effect envelope is one-shot; retain and retry the compiled Loro update bytes`);
2334
- consumed.add(key);
2335
- }
2336
- /** Freeze the explicit insertion anchor and lineage on one causal baseline. */
2337
- function prepareGeneratedVisualEffect(base, rawInput) {
2338
- const effectId = requireTrimmed(rawInput.effectId, "Generated visual effect id");
2339
- if (rawInput.clips.length === 0) throw new Error("Generated visual effect requires at least one clip");
2340
- const clips = rawInput.clips.map((clip, index) => {
2341
- const generationJobId = requireTrimmed(clip.generationJobId, `Generated visual ${index} generation job id`);
2342
- const durationMs = safeWholeMs(clip.durationMs, `Generated visual ${index} durationMs`);
2343
- if (durationMs === 0 || clip.media.kind === "video" && durationMs > clip.media.durationMs) throw new Error(`Generated visual ${index} duration must fit its immutable media Sequence`);
2344
- if (clip.sourceMediaEntityIds.length === 0) throw new Error(`Generated visual ${index} requires explicit source lineage`);
2345
- const sourceMediaEntityIds = clip.sourceMediaEntityIds.map((sourceId) => requireTrimmed(sourceId, `Generated visual ${index} source media id`));
2346
- if (new Set(sourceMediaEntityIds).size !== sourceMediaEntityIds.length) throw new Error(`Generated visual ${index} source lineage must be unique`);
2347
- return {
2348
- ...structuredClone(clip),
2349
- durationMs,
2350
- generationJobId,
2351
- sourceMediaEntityIds
2352
- };
2353
- });
2354
- const input = structuredClone({
2355
- ...rawInput,
2356
- effectId,
2357
- clips
2358
- });
2359
- const rows = cloneRows(base.snapshot().rows);
2360
- for (const [index, clip] of input.clips.entries()) for (const sourceId of clip.sourceMediaEntityIds) {
2361
- const source = requireSequenceContent(rows, sourceId);
2362
- if (![
2363
- "video",
2364
- "image",
2365
- "audio"
2366
- ].includes(source.entityKind)) throw new Error(`Generated visual ${index} source must be Video, Image, or Audio`);
2367
- }
2368
- return deepFreeze({
2369
- version: 1,
2370
- baselineFingerprint: captionBaselineFingerprint(base),
2371
- effectId,
2372
- signature: generatedVisualSignature(input),
2373
- input,
2374
- decisions: resolveGeneratedVisualDecisions(rows, input)
2375
- });
2376
- }
2377
- function prepareContinueVideoEffect(base, input) {
2378
- const rows = cloneRows(base.snapshot().rows);
2379
- requireEntityKind(rows, input.sourceMediaEntityId, "video");
2380
- requireEntityKind(rows, input.tailFrameEntityId, "image");
2381
- return prepareGeneratedVisualEffect(base, {
2382
- effectId: input.effectId,
2383
- timelineEntityId: input.timelineEntityId,
2384
- afterClipEntityId: input.afterClipEntityId,
2385
- clips: [{
2386
- media: input.media,
2387
- durationMs: input.media.durationMs,
2388
- generationJobId: input.effectId,
2389
- sourceMediaEntityIds: [input.sourceMediaEntityId, input.tailFrameEntityId]
2390
- }]
2391
- });
2392
- }
2393
- /** Compile once at the FE authority; retain the returned update bytes for retry and collaborators. */
2394
- function compileGeneratedVisualEffect(base, envelope) {
2395
- return EntityTimelineEditor.compileGeneratedVisualEffect(base, envelope);
2396
- }
2397
- function prepareSpeechAssetsEffect(base, rawInput) {
2398
- for (const item of rawInput.value.items) if (item.source.kind === "audio-script") if (item.source.audioScriptEntityId === void 0) audioScriptSegmentsFromAsr(item.source.segments, false);
2399
- else validateResolvedAsrSegments(item.source.segments);
2400
- else validateScriptSegments(item.source.segments);
2401
- const value = structuredClone(rawInput.value);
2402
- const effectId = requireTrimmed(value.effectId, "Speech effect id");
2403
- if (value.items.length === 0) throw new Error("Speech effect requires at least one item");
2404
- if (new Set(value.items.map((item) => item.media.assetId)).size !== value.items.length) throw new Error("Speech effect media assets must be unique");
2405
- const input = structuredClone({
2406
- ...rawInput,
2407
- value: {
2408
- ...value,
2409
- effectId
2410
- }
2411
- });
2412
- const baselineFingerprint = captionBaselineFingerprint(base);
2413
- return deepFreeze({
2414
- version: 1,
2415
- baselineFingerprint,
2416
- effectId,
2417
- signature: speechEffectSignature(input),
2418
- input,
2419
- decisions: resolveSpeechEffectDecisions(cloneRows(base.snapshot().rows), input, effectId, baselineFingerprint)
2420
- });
2421
- }
2422
- /** Compile once at the FE authority; retain the returned update bytes for retry and collaborators. */
2423
- function compileSpeechAssetsEffect(base, envelope) {
2424
- return EntityTimelineEditor.compileSpeechAssetsEffect(base, envelope);
2425
- }
2426
- function validateAsrSegments(segments) {
2427
- if (segments.length === 0) throw new Error("Caption assets require at least one ASR segment");
2428
- const ids = /* @__PURE__ */ new Set();
2429
- return segments.map((segment) => {
2430
- const segmentId = requireTrimmed(segment.segmentId, "ASR segment id");
2431
- if (ids.has(segmentId)) throw new Error(`Duplicate ASR segment id "${segmentId}"`);
2432
- ids.add(segmentId);
2433
- if (typeof segment.text !== "string" || segment.text.trim() === "") throw new Error(`ASR segment "${segmentId}" requires visible text`);
2434
- const startMs = safeWholeMs(segment.startMs, `ASR segment "${segmentId}" startMs`);
2435
- const endMs = safeWholeMs(segment.endMs, `ASR segment "${segmentId}" endMs`);
2436
- if (endMs <= startMs) throw new Error(`ASR segment "${segmentId}" must have positive duration`);
2437
- const language = segment.language === void 0 ? void 0 : requireTrimmed(segment.language, `ASR segment "${segmentId}" language`);
2438
- return {
2439
- segmentId,
2440
- text: segment.text,
2441
- startMs,
2442
- endMs,
2443
- ...language === void 0 ? {} : { language }
2444
- };
2445
- });
2446
- }
2447
- function requireCanonicalCaptionTrack(rows, timelineEntityId, trackEntityId) {
2448
- const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
2449
- const track = requireEntityKind(rows, trackEntityId, "track");
2450
- if (track.payload.role !== "caption") throw new Error(`Track "${track.entityId}" must have role "caption"`);
2451
- const bindings = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
2452
- if (bindings.length !== 1) throw new Error(`Caption Track "${track.entityId}" must have one unambiguous Timeline`);
2453
- const binding = bindings[0];
2454
- if (binding.endpoint0EntityId !== timeline.entityId || binding.endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
2455
- return track;
2456
- }
2457
- function resolveSourceOccurrence(rows, input, sourceEntityId) {
2458
- const clip = requireEntityKind(rows, input.sourceOccurrence.clipEntityId, "clip");
2459
- const marker = requireEntityKind(rows, input.sourceOccurrence.markerEntityId, "sequence-marker");
2460
- const clipMarkers = incidentRelations(rows, clip.entityId).filter((relation) => relation.relationKind === "clip-marker");
2461
- if (clipMarkers.length !== 1 || clipMarkers[0].endpoint0EntityId !== clip.entityId || clipMarkers[0].endpoint1EntityId !== marker.entityId) throw new Error("Source occurrence requires one ordered clip-marker [Clip, SequenceMarker]");
2462
- const contents = incidentRelations(rows, marker.entityId).filter((relation) => relation.relationKind === "marker-content");
2463
- if (contents.length !== 1 || contents[0].endpoint0EntityId !== marker.entityId || contents[0].endpoint1EntityId !== sourceEntityId) throw new Error("Source occurrence requires one ordered marker-content [SequenceMarker, source Entity]");
2464
- const trackBindings = incidentRelations(rows, clip.entityId).filter((relation) => relation.relationKind === "track-clip");
2465
- if (trackBindings.length !== 1 || trackBindings[0].endpoint1EntityId !== clip.entityId) throw new Error("Source occurrence requires one ordered track-clip [Track, Clip]");
2466
- const sourceTrack = requireEntityKind(rows, trackBindings[0].endpoint0EntityId, "track");
2467
- const timelineBindings = incidentRelations(rows, sourceTrack.entityId).filter((relation) => relation.relationKind === "timeline-track");
2468
- if (timelineBindings.length !== 1 || timelineBindings[0].endpoint0EntityId !== input.timelineEntityId || timelineBindings[0].endpoint1EntityId !== sourceTrack.entityId) throw new Error("Source occurrence must be placed on the selected Timeline through [Timeline, Track]");
2469
- const placement = resolveEntityTimelineLayout(rows).clips.get(clip.entityId);
2470
- if (placement === void 0 || placement.markerEntityId !== marker.entityId || placement.contentEntityId !== sourceEntityId) throw new Error("Source occurrence is not an unambiguous placed use of the selected source Entity");
2471
- const sourceRange = requirePositiveRange(marker.payload.sourceRange, "Source occurrence sourceRange");
2472
- return {
2473
- sourceRange,
2474
- timelineStart: placement.start,
2475
- rate: (sourceRange.end - sourceRange.start) / placement.duration
2476
- };
2477
- }
2478
- function occurrenceTargetRange(occurrence, segment) {
2479
- if (segment.startMs < occurrence.sourceRange.start || segment.endMs > occurrence.sourceRange.end) throw new Error(`ASR segment "${segment.segmentId}" is outside the selected source occurrence`);
2480
- const start = Math.round(occurrence.timelineStart + (segment.startMs - occurrence.sourceRange.start) / occurrence.rate);
2481
- const end = Math.round(occurrence.timelineStart + (segment.endMs - occurrence.sourceRange.start) / occurrence.rate);
2482
- if (end <= start) throw new Error(`ASR segment "${segment.segmentId}" collapses under the occurrence time mapping`);
2483
- return {
2484
- start,
2485
- end
2486
- };
2487
- }
2488
- function resolveCaptionEffectDecisions(rows, input, identityScope, segments) {
2489
- const source = requireEntityKind(rows, input.source.entityId, input.source.entityKind);
2490
- requireCanonicalCaptionTrack(rows, input.timelineEntityId, input.trackEntityId);
2491
- const occurrence = resolveSourceOccurrence(rows, input, source.entityId);
2492
- return {
2493
- sourceOccurrence: {
2494
- sourceStartMs: occurrence.sourceRange.start,
2495
- sourceEndMs: occurrence.sourceRange.end,
2496
- timelineStartMs: occurrence.timelineStart,
2497
- rate: occurrence.rate
2498
- },
2499
- targetRanges: segments.map((segment) => ({
2500
- segmentId: segment.segmentId,
2501
- ...occurrenceTargetRange(occurrence, segment)
2502
- })),
2503
- captionAsset: resolveCaptionAssetDecision(rows, input, identityScope)
2504
- };
2505
- }
2506
- function resolveCaptionAssetDecision(rows, input, identityScope) {
2507
- const assetId = requireTrimmed(input.captionAsset.assetId, "Caption asset id");
2508
- const storageKey = input.captionAsset.storageKey === void 0 ? void 0 : requireTrimmed(input.captionAsset.storageKey, "Caption asset storage key");
2509
- const matches = rows.entities.filter((row) => row.entityKind === "asset" && row.payload.system === "memota" && row.payload.key === assetId);
2510
- if (matches.length > 1) throw new Error(`Caption Asset "${assetId}" is ambiguous`);
2511
- const existing = matches[0];
2512
- if (existing === void 0) return {
2513
- mode: "create",
2514
- resultAssetEntityId: stableEffectId("entity", identityScope, "caption-asset:entity:0"),
2515
- reboundRelationIds: []
2516
- };
2517
- if (storageKey === void 0 || existing.payload.storageKey === storageKey) return {
2518
- mode: "reuse",
2519
- priorAssetEntityId: existing.entityId,
2520
- resultAssetEntityId: existing.entityId,
2521
- reboundRelationIds: []
2522
- };
2523
- if (existing.payload.storageKey !== void 0) throw new Error(`Caption Asset "${assetId}" conflicts with its existing storage key`);
2524
- const bindings = rows.relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === existing.entityId || relation.endpoint1EntityId === existing.entityId)).map((relation) => String(relation.relationId)).sort();
2525
- return {
2526
- mode: "replace",
2527
- priorAssetEntityId: existing.entityId,
2528
- resultAssetEntityId: stableEffectId("entity", identityScope, "caption-asset:entity:0"),
2529
- reboundRelationIds: bindings
2530
- };
2531
- }
2532
- function captionBaselineFingerprint(base) {
2533
- const causal = bytesToHex(base.doc.oplogVersion().encode());
2534
- return bytesToHex(sha256(new TextEncoder().encode(canonicalJson({
2535
- causal,
2536
- state: base.snapshot()
2537
- }))));
2538
- }
2539
- function resolveSpeechEffectDecisions(rows, input, effectId, baselineFingerprint) {
2540
- const { value } = input;
2541
- requireEntityKind(rows, value.timelineEntityId, "timeline");
2542
- for (const item of value.items) if (item.source.kind === "phonetic-script") requirePersistedPhoneticSourceScript(rows, item.source);
2543
- else if (item.source.audioScriptEntityId !== void 0) requireAssetlessAsrSourceScript(rows, item.source);
2544
- const scope = stableEffectScope(speechAssetsCommand, effectId, baselineFingerprint);
2545
- const speechTrackEntityId = resolveEffectTrackId(rows, value.timelineEntityId, "speech", scope);
2546
- const captionTrackEntityId = value.items.some((item) => item.captions.length > 0 || "targetCaptionClipEntityIds" in item && item.targetCaptionClipEntityIds.length > 0) ? resolveEffectTrackId(rows, value.timelineEntityId, "caption", scope) : void 0;
2547
- if (input.mode === "add") return {
2548
- speechTrackEntityId,
2549
- ...captionTrackEntityId === void 0 ? {} : { captionTrackEntityId },
2550
- targetStartsMs: input.value.items.map((item) => safeWholeMs(item.placementStartMs, "Speech placement startMs"))
2551
- };
2552
- const targets = input.value.items.map((item) => item.targetSpeechClipEntityId);
2553
- if (new Set(targets).size !== targets.length) throw new Error("Replacement Speech targets must be unique");
2554
- const captionTargets = input.value.items.flatMap((item) => item.targetCaptionClipEntityIds);
2555
- if (new Set(captionTargets).size !== captionTargets.length) throw new Error("Replacement Caption targets must be unique across the effect");
2556
- const layout = resolveEntityTimelineLayout(rows);
2557
- const starts = input.value.items.map((item) => {
2558
- const target = requireCanonicalClipOnRole(rows, item.targetSpeechClipEntityId, "speech");
2559
- if (target.trackRelation.endpoint0EntityId !== speechTrackEntityId || target.trackRelation.endpoint1EntityId !== target.clip.entityId) throw new Error("Replacement Speech target must use ordered Track/Clip endpoints");
2560
- for (const captionId of item.targetCaptionClipEntityIds) {
2561
- const caption = requireCanonicalClipOnRole(rows, captionId, "caption");
2562
- if (captionTrackEntityId === void 0 || caption.trackRelation.endpoint0EntityId !== captionTrackEntityId || caption.trackRelation.endpoint1EntityId !== caption.clip.entityId) throw new Error("Replacement Caption target must use ordered Track/Clip endpoints");
2563
- }
2564
- const placed = layout.clips.get(target.clip.entityId);
2565
- if (placed === void 0) throw new Error(`Replacement Speech Clip "${target.clip.entityId}" is not placed`);
2566
- return placed.start;
2567
- });
2568
- return {
2569
- speechTrackEntityId,
2570
- ...captionTrackEntityId === void 0 ? {} : { captionTrackEntityId },
2571
- targetStartsMs: starts
2572
- };
2573
- }
2574
- function resolveEffectTrackId(rows, timelineEntityId, role, identityScope) {
2575
- if (rows.relations.filter((relation) => relation.relationKind === "timeline-track" && relation.endpoint1EntityId === timelineEntityId).length > 0) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
2576
- const candidates = rows.relations.filter((relation) => relation.relationKind === "timeline-track" && relation.endpoint0EntityId === timelineEntityId).map((relation) => requireEntityKind(rows, relation.endpoint1EntityId, "track")).filter((track) => track.payload.role === role);
2577
- if (candidates.length > 1) throw new Error(`${role} Track is ambiguous`);
2578
- return candidates[0]?.entityId ?? createEntityId(stableEffectId("entity", identityScope, `track:${role}`));
2579
- }
2580
- function ensureEffectRoleTrack(rows, timelineEntityId, role, expectedTrackId, identityScope) {
2581
- const resolved = resolveEffectTrackId(rows, timelineEntityId, role, identityScope);
2582
- if (resolved !== expectedTrackId) throw new Error(`Frozen ${role} Track decision changed`);
2583
- if (rows.entities.find((entity) => entity.entityId === resolved) !== void 0) return requireEntityKind(rows, resolved, "track");
2584
- const track = {
2585
- entityId: createEntityId(expectedTrackId),
2586
- entityKind: "track",
2587
- payload: {
2588
- role,
2589
- order: roleOrder[role]
2590
- }
2591
- };
2592
- rows.entities.push(track);
2593
- rows.relations.push({
2594
- relationId: createRelationId(stableEffectId("relation", identityScope, `timeline-track:${role}`)),
2595
- relationKind: "timeline-track",
2596
- endpoint0EntityId: createEntityId(timelineEntityId),
2597
- endpoint1EntityId: track.entityId,
2598
- metadata: {},
2599
- trace: {}
2600
- });
2601
- return track;
2602
- }
2603
- function materializeSpeechSource(rows, item, identityScope, itemIndex, trace) {
2604
- const source = item.source;
2605
- const segments = source.kind === "audio-script" ? audioScriptSegmentsFromAsr(source.segments, source.audioScriptEntityId !== void 0) : validateScriptSegments(source.segments);
2606
- const requestedScriptId = source.audioScriptEntityId === void 0 ? stableEffectId("entity", identityScope, `speech:${itemIndex}:audio-script`) : requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
2607
- const existingScript = rows.entities.find((entity) => entity.entityId === requestedScriptId);
2608
- let audioScript;
2609
- if (existingScript === void 0) {
2610
- if (source.kind === "phonetic-script") throw new Error(`Phonetic Speech source AudioScript "${requestedScriptId}" must already exist with an Asset`);
2611
- audioScript = {
2612
- entityId: requireAvailableEntityId(rows, requestedScriptId),
2613
- entityKind: "audio-script",
2614
- payload: { segments }
2615
- };
2616
- rows.entities.push(audioScript);
2617
- } else {
2618
- audioScript = requireEntityKind(rows, requestedScriptId, "audio-script");
2619
- if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${requestedScriptId}" conflicts with the immutable Speech source`);
2620
- if (source.kind === "phonetic-script") requireCanonicalAudioScriptAsset(rows, audioScript.entityId);
2621
- else requireNoAudioScriptAsset(rows, audioScript.entityId, "ASR-composed");
2622
- }
2623
- if (source.kind === "audio-script") {
2624
- const timed = validateResolvedAsrSegments(source.segments);
2625
- if (source.sourceMediaEntityIds.length === 0) throw new Error("Audio Speech source requires media lineage");
2626
- for (const [sourceIndex, sourceId] of source.sourceMediaEntityIds.entries()) {
2627
- const media = requireSequenceContent(rows, sourceId);
2628
- if (media.entityKind !== "audio" && media.entityKind !== "video") throw new Error("audio-script-source requires Audio or Video input");
2629
- const existing = rows.relations.filter((relation) => relation.relationKind === "audio-script-source" && (relation.endpoint0EntityId === audioScript.entityId || relation.endpoint1EntityId === audioScript.entityId));
2630
- if (existing.some((relation) => relation.endpoint0EntityId !== audioScript.entityId)) throw new Error("audio-script-source must use ordered endpoints [AudioScript, source media]");
2631
- if (!existing.some((relation) => relation.endpoint1EntityId === media.entityId)) rows.relations.push({
2632
- relationId: createRelationId(stableEffectId("relation", identityScope, `speech:${itemIndex}:audio-source:${sourceIndex}`)),
2633
- relationKind: "audio-script-source",
2634
- endpoint0EntityId: audioScript.entityId,
2635
- endpoint1EntityId: media.entityId,
2636
- metadata: {},
2637
- trace: {
2638
- ...trace,
2639
- role: "audio-script-source",
2640
- sourceIndex
2641
- }
2642
- });
2643
- }
2644
- const markerLinks = incidentRelations(rows, audioScript.entityId).filter((relation) => relation.relationKind === "audio-script-marker");
2645
- if (markerLinks.some((relation) => relation.endpoint0EntityId !== audioScript.entityId)) throw new Error("audio-script-marker must use ordered endpoints [AudioScript, annotation SequenceMarker]");
2646
- if (markerLinks.length > 1) throw new Error(`AudioScript "${audioScript.entityId}" has ambiguous timing annotations`);
2647
- const segmentRanges = timed.map((segment) => ({
2648
- segmentId: segment.segmentId,
2649
- startMs: segment.startMs,
2650
- endMs: segment.endMs
2651
- }));
2652
- if (markerLinks[0] !== void 0) {
2653
- if (canonicalJson(requireEntityKind(rows, markerLinks[0].endpoint1EntityId, "sequence-marker").payload.segmentRanges) !== canonicalJson(segmentRanges)) throw new Error(`AudioScript "${audioScript.entityId}" timing conflicts with the immutable Speech source`);
2654
- } else {
2655
- const markerId = stableEffectId("entity", identityScope, `speech:${itemIndex}:audio-script-marker`);
2656
- rows.entities.push({
2657
- entityId: createEntityId(markerId),
2658
- entityKind: "sequence-marker",
2659
- payload: {
2660
- sourceRange: {
2661
- start: source.sourceStartMs,
2662
- end: source.sourceStartMs + item.media.durationMs
2663
- },
2664
- duration: {
2665
- mode: "fixed",
2666
- value: item.media.durationMs
2667
- },
2668
- segmentRanges
2669
- }
2670
- });
2671
- rows.relations.push({
2672
- relationId: createRelationId(stableEffectId("relation", identityScope, `speech:${itemIndex}:script-marker`)),
2673
- relationKind: "audio-script-marker",
2674
- endpoint0EntityId: audioScript.entityId,
2675
- endpoint1EntityId: createEntityId(markerId),
2676
- metadata: {},
2677
- trace: {
2678
- ...trace,
2679
- role: "audio-script-marker"
2680
- }
2681
- });
2682
- }
2683
- }
2684
- const phoneticId = source.kind === "phonetic-script" ? requireTrimmed(source.phoneticScriptEntityId, "PhoneticScript entity id") : stableEffectId("entity", identityScope, `speech:${itemIndex}:phonetic-script`);
2685
- const existingPhonetic = rows.entities.find((entity) => entity.entityId === phoneticId);
2686
- const phoneticPayload = {
2687
- baseEntityIds: [audioScript.entityId],
2688
- ...source.kind === "phonetic-script" && source.phonemeScript !== void 0 ? { phonemeScript: source.phonemeScript } : {}
2689
- };
2690
- let phonetic;
2691
- if (existingPhonetic === void 0) {
2692
- phonetic = {
2693
- entityId: requireAvailableEntityId(rows, phoneticId),
2694
- entityKind: "phonetic-script",
2695
- payload: phoneticPayload
2696
- };
2697
- rows.entities.push(phonetic);
2698
- } else {
2699
- phonetic = requireEntityKind(rows, phoneticId, "phonetic-script");
2700
- if (canonicalJson(phonetic.payload) !== canonicalJson(phoneticPayload)) throw new Error(`PhoneticScript "${phoneticId}" conflicts with the immutable Speech source`);
2701
- }
2702
- assemblePhoneticScriptContent(rows, phonetic.entityId);
2703
- return {
2704
- audioScript,
2705
- phonetic
2706
- };
2707
- }
2708
- function requirePersistedPhoneticSourceScript(rows, source) {
2709
- const scriptId = requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
2710
- const audioScript = requireEntityKind(rows, scriptId, "audio-script");
2711
- const segments = validateScriptSegments(source.segments);
2712
- if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${scriptId}" conflicts with the immutable Speech source`);
2713
- requireCanonicalAudioScriptAsset(rows, audioScript.entityId);
2714
- return audioScript;
2715
- }
2716
- function requireAssetlessAsrSourceScript(rows, source) {
2717
- const scriptId = requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
2718
- const audioScript = requireEntityKind(rows, scriptId, "audio-script");
2719
- const segments = audioScriptSegmentsFromAsr(source.segments, true);
2720
- if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${scriptId}" conflicts with the immutable ASR source`);
2721
- requireNoAudioScriptAsset(rows, audioScript.entityId, "ASR-composed");
2722
- return audioScript;
2723
- }
2724
- function requireNoAudioScriptAsset(rows, audioScriptEntityId, label) {
2725
- if (incidentRelations(rows, audioScriptEntityId).filter((relation) => relation.relationKind === "from-asset").length > 0) throw new Error(`${label} AudioScript "${audioScriptEntityId}" must not have an Asset binding`);
2726
- }
2727
- function requireCanonicalAudioScriptAsset(rows, audioScriptEntityId) {
2728
- const bindings = incidentRelations(rows, audioScriptEntityId).filter((relation) => relation.relationKind === "from-asset");
2729
- if (bindings.length !== 1) throw new Error(`AudioScript "${audioScriptEntityId}" requires exactly one Asset binding`);
2730
- const binding = bindings[0];
2731
- if (binding.endpoint0EntityId !== audioScriptEntityId) throw new Error("from-asset must use ordered endpoints [AudioScript, Asset]");
2732
- const asset = requireEntityKind(rows, binding.endpoint1EntityId, "asset");
2733
- if (asset.payload.system !== "memota" || typeof asset.payload.key !== "string" || asset.payload.key.length === 0 || asset.payload.key.trim() !== asset.payload.key) throw new Error(`AudioScript Asset "${asset.entityId}" must state a canonical memota locator`);
2734
- return asset;
2735
- }
2736
- function validateScriptSegments(segments) {
2737
- if (segments.length === 0) throw new Error("Speech source requires at least one script segment");
2738
- const ids = /* @__PURE__ */ new Set();
2739
- return Array.from({ length: segments.length }, (_, index) => {
2740
- if (!Object.hasOwn(segments, index)) throw new Error("Speech source segments must not be sparse");
2741
- const value = segments[index];
2742
- const segment = strictJsonObject(value, `Speech source segment ${index}`);
2743
- if (typeof segment.segmentId !== "string") throw new Error("Speech source segment id must be a string");
2744
- const segmentId = requireTrimmed(segment.segmentId, "Speech source segment id");
2745
- if (ids.has(segmentId)) throw new Error(`Duplicate Speech source segment id "${segmentId}"`);
2746
- ids.add(segmentId);
2747
- if (typeof segment.text !== "string" || segment.text.trim() === "") throw new Error("Speech source text is required");
2748
- if (segment.language !== void 0 && typeof segment.language !== "string") throw new Error("Speech source language must be a string");
2749
- return {
2750
- ...segment,
2751
- segmentId,
2752
- text: segment.text,
2753
- ...segment.language === void 0 ? {} : { language: requireTrimmed(segment.language, "Speech source language") }
2754
- };
2755
- });
2756
- }
2757
- function validateResolvedAsrSegments(segments) {
2758
- return validateScriptSegments(segments).map((segment) => {
2759
- const segmentId = segment.segmentId;
2760
- const startMs = safeWholeMs(segment.startMs, `ASR segment "${segmentId}" startMs`);
2761
- const endMs = safeWholeMs(segment.endMs, `ASR segment "${segmentId}" endMs`);
2762
- if (endMs <= startMs) throw new Error(`ASR segment "${segmentId}" must have positive duration`);
2763
- return {
2764
- ...segment,
2765
- segmentId,
2766
- text: segment.text,
2767
- startMs,
2768
- endMs
2769
- };
2770
- });
2771
- }
2772
- function audioScriptSegmentsFromAsr(segments, preserveResolvedFields) {
2773
- return validateResolvedAsrSegments(segments).map((timed) => {
2774
- if (!preserveResolvedFields) {
2775
- const allowed = new Set([
2776
- "segmentId",
2777
- "text",
2778
- "language",
2779
- "startMs",
2780
- "endMs"
2781
- ]);
2782
- const extras = Object.keys(timed).filter((key) => !allowed.has(key));
2783
- if (extras.length > 0) throw new Error(`New ASR segment must not contain host-resolved fields: ${extras.join(", ")}`);
2784
- return {
2785
- segmentId: timed.segmentId,
2786
- text: timed.text,
2787
- ...timed.language === void 0 ? {} : { language: timed.language }
2788
- };
2789
- }
2790
- const segment = { ...timed };
2791
- delete segment.startMs;
2792
- delete segment.endMs;
2793
- return segment;
2794
- });
2795
- }
2796
- function strictJsonObject(value, label) {
2797
- const clone = strictJsonValue(value, label, /* @__PURE__ */ new WeakSet());
2798
- if (!isJsonObject(clone)) throw new Error(`${label} must be a plain JSON object`);
2799
- return clone;
2800
- }
2801
- function strictJsonValue(value, label, seen) {
2802
- if (value === null || typeof value === "string" || typeof value === "boolean") return value;
2803
- if (typeof value === "number") {
2804
- if (!Number.isFinite(value)) throw new Error(`${label} must not contain non-finite JSON numbers`);
2805
- return value;
2806
- }
2807
- if (Array.isArray(value)) {
2808
- if (seen.has(value)) throw new Error(`${label} must not contain cyclic JSON values`);
2809
- seen.add(value);
2810
- const result = [];
2811
- for (let index = 0; index < value.length; index += 1) {
2812
- if (!Object.hasOwn(value, index)) throw new Error(`${label} must not contain sparse JSON arrays`);
2813
- result.push(strictJsonValue(value[index], `${label}[${index}]`, seen));
2814
- }
2815
- seen.delete(value);
2816
- return result;
2817
- }
2818
- if (typeof value !== "object" || value === void 0) throw new Error(`${label} must contain only JSON values`);
2819
- const prototype = Object.getPrototypeOf(value);
2820
- if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must not contain objects with custom prototypes`);
2821
- if (seen.has(value)) throw new Error(`${label} must not contain cyclic JSON values`);
2822
- seen.add(value);
2823
- const result = {};
2824
- for (const [key, child] of Object.entries(value)) {
2825
- if (child === void 0) throw new Error(`${label}.${key} must not be undefined`);
2826
- result[key] = strictJsonValue(child, `${label}.${key}`, seen);
2827
- }
2828
- seen.delete(value);
2829
- return result;
2830
- }
2831
- function replaceRelationWithTrace(rows, previous, endpoint0EntityId, endpoint1EntityId, trace, relationId) {
2832
- rows.relations = rows.relations.filter((relation) => relation.relationId !== previous.relationId);
2833
- rows.relations.push({
2834
- relationId: requireAvailableRelationId(rows, relationId),
2835
- relationKind: previous.relationKind,
2836
- endpoint0EntityId,
2837
- endpoint1EntityId,
2838
- metadata: previous.metadata,
2839
- trace
2840
- });
2841
- }
2842
- /** Retire one editor use while retaining its immutable content row and Loro history. */
2843
- function retireClipOccurrence(rows, topology) {
2844
- const removedRelations = new Set([
2845
- topology.trackRelation.relationId,
2846
- topology.clipMarkerRelation.relationId,
2847
- topology.markerContentRelation.relationId
2848
- ]);
2849
- rows.relations = rows.relations.filter((relation) => !removedRelations.has(relation.relationId));
2850
- rows.entities = rows.entities.filter((entity) => entity.entityId !== topology.clip.entityId && entity.entityId !== topology.marker.entityId);
2851
- }
2852
- function speechEffectSignature(input) {
2853
- return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(input))));
2854
- }
2855
- function speechEffectTrace(envelope, itemIndex) {
2856
- return {
2857
- command: speechAssetsCommand,
2858
- effectId: envelope.effectId,
2859
- signature: envelope.signature,
2860
- mode: envelope.input.mode,
2861
- itemIndex
2862
- };
2863
- }
2864
- function assertSpeechAssetsEnvelope(value) {
2865
- if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || speechEffectSignature(value.input) !== value.signature || value.effectId !== value.input.value.effectId) throw new Error("Speech effect requires a canonical frozen envelope");
2866
- }
2867
- function resolveSpeechAssetsReplay(rows, envelope) {
2868
- const tagged = rows.relations.filter((relation) => relation.trace.command === speechAssetsCommand && relation.trace.effectId === envelope.effectId);
2869
- if (tagged.length === 0) return void 0;
2870
- if (tagged.some((relation) => relation.trace.signature !== envelope.signature)) throw new Error(`Speech effect "${envelope.effectId}" was already applied with different input`);
2871
- const result = {
2872
- speechClipEntityIds: [],
2873
- speechAudioEntityIds: [],
2874
- audioScriptEntityIds: [],
2875
- phoneticScriptEntityIds: [],
2876
- captionClipEntityIds: []
2877
- };
2878
- for (const [index, item] of envelope.input.value.items.entries()) {
2879
- const relations = tagged.filter((relation) => relation.trace.itemIndex === index);
2880
- const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render");
2881
- if (renders.length !== 1) throw new Error(`Speech effect item ${index} has an incomplete render receipt`);
2882
- const render = renders[0];
2883
- const audio = requireEntityKind(rows, render.endpoint0EntityId, "audio");
2884
- const phonetic = requireEntityKind(rows, render.endpoint1EntityId, "phonetic-script");
2885
- const audioScriptId = requireComposedAudioScript(rows, phonetic.entityId);
2886
- const contentRelations = relations.filter((relation) => relation.relationKind === "marker-content");
2887
- const speechContent = contentRelations.filter((relation) => relation.trace.role !== "caption-content");
2888
- if (speechContent.length !== 1 || speechContent[0].endpoint1EntityId !== audio.entityId) throw new Error(`Speech effect item ${index} has an incomplete Speech occurrence receipt`);
2889
- const speechClip = requireSingleClipForMarker(rows, requireEntityKind(rows, speechContent[0].endpoint0EntityId, "sequence-marker").entityId, "speech");
2890
- const captionIds = [];
2891
- for (let captionIndex = 0; captionIndex < item.captions.length; captionIndex += 1) {
2892
- const matches = contentRelations.filter((relation) => relation.trace.role === "caption-content" && relation.trace.captionIndex === captionIndex);
2893
- if (matches.length !== 1) throw new Error(`Speech effect item ${index} Caption receipt is incomplete`);
2894
- if (assembleCaptionContent(rows, requireEntityKind(rows, matches[0].endpoint1EntityId, "caption").entityId).audioScript.entityId !== audioScriptId) throw new Error("Speech Caption must retain the same AudioScript base");
2895
- captionIds.push(requireSingleClipForMarker(rows, matches[0].endpoint0EntityId, "caption").entityId);
2896
- }
2897
- result.speechClipEntityIds.push(speechClip.entityId);
2898
- result.speechAudioEntityIds.push(audio.entityId);
2899
- result.audioScriptEntityIds.push(audioScriptId);
2900
- result.phoneticScriptEntityIds.push(phonetic.entityId);
2901
- result.captionClipEntityIds.push(captionIds);
2902
- }
2903
- return result;
2904
- }
2905
- function requireSingleClipForMarker(rows, markerEntityId, role) {
2906
- const links = incidentRelations(rows, markerEntityId).filter((relation) => relation.relationKind === "clip-marker");
2907
- if (links.length !== 1 || links[0].endpoint1EntityId !== markerEntityId) throw new Error("clip-marker must use one ordered [Clip, SequenceMarker] relation");
2908
- return requireClipOnRole(rows, links[0].endpoint0EntityId, role).clip;
2909
- }
2910
- function resolveGeneratedVisualDecisions(rows, input) {
2911
- const timeline = requireEntityKind(rows, input.timelineEntityId, "timeline");
2912
- const anchorInput = visualInsertionAnchor(input);
2913
- const anchor = requireCanonicalClipOnRole(rows, anchorInput.clipEntityId, "video_clip");
2914
- const track = requireEntityKind(rows, anchor.trackRelation.endpoint0EntityId, "track");
2915
- if (anchor.trackRelation.endpoint1EntityId !== anchor.clip.entityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
2916
- const timelineLinks = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
2917
- if (timelineLinks.length !== 1) throw new Error(`Video Track "${track.entityId}" must have one unambiguous Timeline`);
2918
- const timelineLink = timelineLinks[0];
2919
- if (timelineLink.endpoint0EntityId !== timeline.entityId || timelineLink.endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
2920
- const resolvedTrack = resolveEntityTimelineLayout(rows).tracks.find((candidate) => candidate.entityId === track.entityId);
2921
- const anchorOrder = resolvedTrack?.clips.findIndex((clip) => clip.clipEntityId === anchor.clip.entityId) ?? -1;
2922
- if (anchorOrder < 0) throw new Error(`Visual anchor Clip "${anchor.clip.entityId}" is not placed on its Track`);
2923
- if (resolvedTrack.clips.some((clip) => clip.mode !== "sequential")) throw new Error("Generated visual insertion requires a fully sequential Video Track");
2924
- const insertionIndex = anchorOrder + (anchorInput.position === "after" ? 1 : 0);
2925
- return {
2926
- trackEntityId: track.entityId,
2927
- anchorOrder,
2928
- anchorPosition: anchorInput.position,
2929
- insertionOrders: visualInsertionOrders(rows, resolvedTrack.clips.map((clip) => clip.clipEntityId), insertionIndex, input.clips.length)
2930
- };
2931
- }
2932
- function visualInsertionOrders(rows, orderedClipIds, insertionIndex, count) {
2933
- const orders = orderedClipIds.map((clipId) => {
2934
- const order = requireEntityKind(rows, clipId, "clip").payload.order;
2935
- if (typeof order !== "number" || !Number.isFinite(order)) throw new Error(`Sequential Clip "${clipId}" requires a finite order`);
2936
- return order;
2937
- });
2938
- if (orders.some((order, index) => index > 0 && order <= orders[index - 1])) throw new Error("Generated visual insertion requires unambiguous increasing Clip orders");
2939
- const lower = orders[insertionIndex - 1];
2940
- const upper = orders[insertionIndex];
2941
- if (lower === void 0) return Array.from({ length: count }, (_, index) => upper - count + index);
2942
- if (upper === void 0) return Array.from({ length: count }, (_, index) => lower + index + 1);
2943
- const gap = (upper - lower) / (count + 1);
2944
- const result = Array.from({ length: count }, (_, index) => lower + gap * (index + 1));
2945
- if (result.some((order, index) => !Number.isFinite(order) || order <= (index === 0 ? lower : result[index - 1]) || order >= upper)) throw new Error("Clip order precision is insufficient for generated visual insertion");
2946
- return result;
2947
- }
2948
- function visualInsertionAnchor(input) {
2949
- const before = input.beforeClipEntityId;
2950
- const after = input.afterClipEntityId;
2951
- if (before === void 0 === (after === void 0)) throw new Error("Generated visual effect requires exactly one before or after Clip anchor");
2952
- return before === void 0 ? {
2953
- position: "after",
2954
- clipEntityId: requireTrimmed(after, "After Clip anchor")
2955
- } : {
2956
- position: "before",
2957
- clipEntityId: requireTrimmed(before, "Before Clip anchor")
2958
- };
2959
- }
2960
- function generatedVisualSignature(input) {
2961
- return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(input))));
2962
- }
2963
- function assertGeneratedVisualEnvelope(value) {
2964
- if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || requireTrimmed(value.baselineFingerprint, "Generated visual baseline fingerprint") !== value.baselineFingerprint || requireTrimmed(value.effectId, "Generated visual effect id") !== value.effectId || generatedVisualSignature(value.input) !== value.signature) throw new Error("Generated visual effect requires a canonical frozen envelope");
2965
- }
2966
- function generatedVisualTrace(envelope, itemIndex, generationJobId, sourceIndex) {
2967
- return {
2968
- command: generatedVisualsCommand,
2969
- effectId: envelope.effectId,
2970
- signature: envelope.signature,
2971
- itemIndex,
2972
- generationJobId,
2973
- ...sourceIndex === void 0 ? {} : { sourceIndex }
2974
- };
2975
- }
2976
- function resolveGeneratedVisualReplay(rows, envelope) {
2977
- const tagged = rows.relations.filter((relation) => relation.trace.command === generatedVisualsCommand && relation.trace.effectId === envelope.effectId);
2978
- if (tagged.length === 0) return void 0;
2979
- if (tagged.some((relation) => relation.trace.signature !== envelope.signature)) throw new Error(`Generated visual effect "${envelope.effectId}" was already applied with different input`);
2980
- const clipEntityIds = [];
2981
- const mediaEntityIds = [];
2982
- let expectedCount = 0;
2983
- for (const [index, clipInput] of envelope.input.clips.entries()) {
2984
- const item = tagged.filter((relation) => relation.trace.itemIndex === index);
2985
- const trackLink = requireGeneratedVisualRelation(item, "track-clip", index);
2986
- if (trackLink.endpoint0EntityId !== envelope.decisions.trackEntityId || requireEntityKind(rows, trackLink.endpoint1EntityId, "clip").entityId !== trackLink.endpoint1EntityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
2987
- const clipEntityId = trackLink.endpoint1EntityId;
2988
- const markerLink = requireGeneratedVisualRelation(item, "clip-marker", index);
2989
- if (markerLink.endpoint0EntityId !== clipEntityId) throw new Error("clip-marker must use ordered endpoints [Clip, SequenceMarker]");
2990
- const marker = requireEntityKind(rows, markerLink.endpoint1EntityId, "sequence-marker");
2991
- const contentLink = requireGeneratedVisualRelation(item, "marker-content", index);
2992
- if (contentLink.endpoint0EntityId !== marker.entityId) throw new Error("marker-content must use ordered endpoints [SequenceMarker, media]");
2993
- const media = requireSequenceContent(rows, contentLink.endpoint1EntityId);
2994
- if (media.entityKind !== "video" && media.entityKind !== "image") throw new Error("Generated visual receipt must reference Video or Image output");
2995
- const lineage = item.filter((relation) => relation.relationKind === "generated");
2996
- if (lineage.length !== clipInput.sourceMediaEntityIds.length) throw new Error(`Generated visual ${index} has incomplete source lineage`);
2997
- for (const [sourceIndex, sourceId] of clipInput.sourceMediaEntityIds.entries()) {
2998
- const matches = lineage.filter((relation) => relation.trace.sourceIndex === sourceIndex);
2999
- if (matches.length !== 1 || matches[0].endpoint0EntityId !== media.entityId || matches[0].endpoint1EntityId !== sourceId) throw new Error("generated must use ordered endpoints [output media, input media]");
3000
- }
3001
- expectedCount += 3 + clipInput.sourceMediaEntityIds.length;
3002
- clipEntityIds.push(clipEntityId);
3003
- mediaEntityIds.push(media.entityId);
3004
- }
3005
- if (tagged.length !== expectedCount) throw new Error(`Generated visual effect "${envelope.effectId}" has an ambiguous relation receipt`);
3006
- return {
3007
- clipEntityIds,
3008
- mediaEntityIds
3009
- };
3010
- }
3011
- function requireGeneratedVisualRelation(relations, kind, itemIndex) {
3012
- const matches = relations.filter((relation) => relation.relationKind === kind);
3013
- if (matches.length !== 1) throw new Error(`Generated visual ${itemIndex} must have exactly one ${kind} Relation`);
3014
- return matches[0];
3015
- }
3016
- function assertCaptionAssetsEnvelope(value) {
3017
- if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || requireTrimmed(value.baselineFingerprint, "Caption baseline fingerprint") !== value.baselineFingerprint || requireTrimmed(value.signature, "Caption effect signature") !== value.signature) throw new Error("Caption effect requires a canonical frozen envelope");
3018
- }
3019
- function deepFreeze(value) {
3020
- if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
3021
- for (const child of Object.values(value)) deepFreeze(child);
3022
- Object.freeze(value);
3023
- }
3024
- return value;
3025
- }
3026
- function captionEffectSignature(input, segments) {
3027
- const value = JSON.stringify({
3028
- timelineEntityId: input.timelineEntityId,
3029
- trackEntityId: input.trackEntityId,
3030
- source: {
3031
- entityId: input.source.entityId,
3032
- entityKind: input.source.entityKind
3033
- },
3034
- sourceOccurrence: {
3035
- clipEntityId: input.sourceOccurrence.clipEntityId,
3036
- markerEntityId: input.sourceOccurrence.markerEntityId
3037
- },
3038
- captionAsset: {
3039
- assetId: input.captionAsset.assetId,
3040
- ...input.captionAsset.storageKey === void 0 ? {} : { storageKey: input.captionAsset.storageKey }
3041
- },
3042
- segments
3043
- });
3044
- return bytesToHex(sha256(new TextEncoder().encode(value)));
3045
- }
3046
- function effectTrace(effectId, signature, segmentId) {
3047
- return {
3048
- command: captionAssetsCommand,
3049
- effectId,
3050
- signature,
3051
- ...segmentId === void 0 ? {} : { segmentId }
3052
- };
3053
- }
3054
- function stableEffectId(kind, effectId, localKey) {
3055
- return `${kind}_effect_${bytesToHex(sha256(new TextEncoder().encode(`${captionAssetsCommand}\u0000${effectId}\u0000${kind}\u0000${localKey}`))).slice(0, 40)}`;
3056
- }
3057
- function stableEffectScope(command, effectId, baselineFingerprint) {
3058
- return `${command}\u0000${effectId}\u0000${baselineFingerprint}`;
3059
- }
3060
- function stableEffectIdFactory(effectId, namespace) {
3061
- const counters = {
3062
- entity: 0,
3063
- relation: 0
3064
- };
3065
- return (kind) => stableEffectId(kind, effectId, `${namespace}:${kind}:${counters[kind]++}`);
3066
- }
3067
- function stableEffectPeerId(command, effectId, baselineFingerprint) {
3068
- const digest = sha256(new TextEncoder().encode(`${command}\u0000${effectId}\u0000${baselineFingerprint}\u0000loro-peer`));
3069
- return BigInt(`0x${bytesToHex(digest).slice(0, 15)}`).toString();
3070
- }
3071
- function isCaptionEffectRelation(relation, effectId) {
3072
- return relation.trace.command === captionAssetsCommand && relation.trace.effectId === effectId;
3073
- }
3074
- function resolveCaptionAssetsReplay(rows, input, effectId, signature, segments, sourceEntityId, trackEntityId) {
3075
- const tagged = rows.relations.filter((relation) => isCaptionEffectRelation(relation, effectId));
3076
- if (tagged.length === 0) return void 0;
3077
- if (tagged.some((relation) => relation.trace.signature !== signature)) throw new Error(`Caption effect "${effectId}" was already applied with different input`);
3078
- const sourceRelations = tagged.filter((relation) => relation.relationKind === "audio-script-source");
3079
- if (sourceRelations.length !== 1) throw new Error(`Caption effect "${effectId}" has an ambiguous source receipt`);
3080
- const sourceRelation = sourceRelations[0];
3081
- if (sourceRelation.endpoint1EntityId !== sourceEntityId) throw new Error("audio-script-source must use ordered endpoints [AudioScript, source media]");
3082
- const script = requireEntityKind(rows, sourceRelation.endpoint0EntityId, "audio-script");
3083
- if (!sameScriptSegments(script.payload.segments, segments)) throw new Error(`Caption effect "${effectId}" AudioScript no longer matches its immutable ASR result`);
3084
- const scriptMarkerRelations = tagged.filter((relation) => relation.relationKind === "audio-script-marker");
3085
- if (scriptMarkerRelations.length !== 1 || scriptMarkerRelations[0].endpoint0EntityId !== script.entityId) throw new Error("audio-script-marker must use ordered endpoints [AudioScript, annotation SequenceMarker]");
3086
- const scriptMarker = requireEntityKind(rows, scriptMarkerRelations[0].endpoint1EntityId, "sequence-marker");
3087
- if (!sameSegmentRanges(scriptMarker.payload.segmentRanges, segments)) throw new Error(`Caption effect "${effectId}" annotation Marker no longer matches its immutable ASR timing`);
3088
- let assetEntityId;
3089
- const captionEntityIds = [];
3090
- const captionClipEntityIds = [];
3091
- for (const segment of segments) {
3092
- const relations = tagged.filter((relation) => relation.trace.segmentId === segment.segmentId);
3093
- const assetLink = requireTaggedRelation(relations, "from-asset", segment.segmentId);
3094
- const caption = requireEntityKind(rows, assetLink.endpoint0EntityId, "caption");
3095
- const asset = requireEntityKind(rows, assetLink.endpoint1EntityId, "asset");
3096
- if (assetEntityId !== void 0 && asset.entityId !== assetEntityId) throw new Error(`Caption effect "${effectId}" references multiple Caption Assets`);
3097
- assetEntityId = asset.entityId;
3098
- if (asset.payload.system !== "memota" || asset.payload.key !== input.captionAsset.assetId || input.captionAsset.storageKey !== void 0 && asset.payload.storageKey !== input.captionAsset.storageKey) throw new Error(`Caption effect "${effectId}" Caption Asset does not match its receipt`);
3099
- const selection = caption.payload.selection;
3100
- const bases = caption.payload.baseEntityIds;
3101
- if (caption.payload.durationMs !== segment.endMs - segment.startMs || !isJsonObject(selection) || selection.segmentId !== segment.segmentId || !Array.isArray(bases) || bases.length !== 1 || bases[0] !== script.entityId) throw new Error(`Caption effect "${effectId}" Caption "${caption.entityId}" conflicts with its ASR segment`);
3102
- const contentLink = requireTaggedRelation(relations, "marker-content", segment.segmentId);
3103
- if (contentLink.endpoint1EntityId !== caption.entityId) throw new Error("marker-content must use ordered endpoints [SequenceMarker, Caption]");
3104
- const marker = requireEntityKind(rows, contentLink.endpoint0EntityId, "sequence-marker");
3105
- const markerLink = requireTaggedRelation(relations, "clip-marker", segment.segmentId);
3106
- if (markerLink.endpoint1EntityId !== marker.entityId) throw new Error("clip-marker must use ordered endpoints [Clip, SequenceMarker]");
3107
- const clip = requireEntityKind(rows, markerLink.endpoint0EntityId, "clip");
3108
- const trackLink = requireTaggedRelation(relations, "track-clip", segment.segmentId);
3109
- if (trackLink.endpoint0EntityId !== trackEntityId || trackLink.endpoint1EntityId !== clip.entityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
3110
- captionEntityIds.push(caption.entityId);
3111
- captionClipEntityIds.push(clip.entityId);
3112
- }
3113
- if (assetEntityId === void 0 || tagged.length !== 2 + segments.length * 4) throw new Error(`Caption effect "${effectId}" has an incomplete or ambiguous relation receipt`);
3114
- return {
3115
- audioScriptEntityId: script.entityId,
3116
- audioScriptMarkerEntityId: scriptMarker.entityId,
3117
- captionAssetEntityId: assetEntityId,
3118
- captionEntityIds,
3119
- captionClipEntityIds
3120
- };
3121
- }
3122
- function requireTaggedRelation(relations, kind, segmentId) {
3123
- const matches = relations.filter((relation) => relation.relationKind === kind);
3124
- if (matches.length !== 1) throw new Error(`ASR segment "${segmentId}" must have exactly one ${kind} Relation`);
3125
- return matches[0];
3126
- }
3127
- function sameScriptSegments(value, segments) {
3128
- if (!Array.isArray(value) || value.length !== segments.length) return false;
3129
- return value.every((entry, index) => {
3130
- if (!isJsonObject(entry)) return false;
3131
- const expected = segments[index];
3132
- return entry.segmentId === expected.segmentId && entry.text === expected.text && entry.language === expected.language && Object.keys(entry).every((key) => key === "segmentId" || key === "text" || key === "language");
3133
- });
3134
- }
3135
- function sameSegmentRanges(value, segments) {
3136
- if (!Array.isArray(value) || value.length !== segments.length) return false;
3137
- return value.every((entry, index) => {
3138
- if (!isJsonObject(entry)) return false;
3139
- const expected = segments[index];
3140
- return entry.segmentId === expected.segmentId && entry.startMs === expected.startMs && entry.endMs === expected.endMs && Object.keys(entry).every((key) => key === "segmentId" || key === "startMs" || key === "endMs");
3141
- });
3142
- }
3143
- function requireTrimmed(value, label) {
3144
- if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be non-empty and trimmed`);
3145
- return value;
3146
- }
3147
- function safeWholeMs(value, label) {
3148
- if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative whole millisecond`);
3149
- return value;
3150
- }
3151
1809
  function validateFinalRows(rows) {
3152
1810
  decodeEntityRelationRows(rows, numericMarkerValidation);
3153
1811
  assertCanonicalEditorResources(rows);
@@ -3185,16 +1843,6 @@ function requireClipTopology(rows, clipEntityId) {
3185
1843
  markerContentRelation
3186
1844
  };
3187
1845
  }
3188
- function requireCanonicalClipOnRole(rows, clipEntityId, role) {
3189
- const topology = requireClipTopology(rows, clipEntityId);
3190
- if (topology.trackRelation.endpoint1EntityId !== topology.clip.entityId || topology.clipMarkerRelation.endpoint0EntityId !== topology.clip.entityId || topology.clipMarkerRelation.endpoint1EntityId !== topology.marker.entityId || topology.markerContentRelation.endpoint0EntityId !== topology.marker.entityId) throw new Error("Occurrence must use ordered Track→Clip→SequenceMarker→content endpoints");
3191
- const track = requireEntityKind(rows, topology.trackRelation.endpoint0EntityId, "track");
3192
- if (track.payload.role !== role) throw new Error(`Clip "${clipEntityId}" must belong to the ${role} Track`);
3193
- const timelineLinks = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
3194
- if (timelineLinks.length !== 1 || timelineLinks[0].endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use one ordered [Timeline, Track] relation");
3195
- requireEntityKind(rows, timelineLinks[0].endpoint0EntityId, "timeline");
3196
- return topology;
3197
- }
3198
1846
  function requireOneRelation(rows, entityId, relationKind) {
3199
1847
  const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
3200
1848
  if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
@@ -3246,11 +1894,6 @@ function requireAvailableEntityId(rows, value) {
3246
1894
  if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
3247
1895
  return entityId;
3248
1896
  }
3249
- function requireAvailableRelationId(rows, value) {
3250
- const relationId = createRelationId(value);
3251
- if (rows.relations.some((relation) => relation.relationId === relationId)) throw new Error(`Relation id "${relationId}" already exists`);
3252
- return relationId;
3253
- }
3254
1897
  function setClipOrder(rows, clipEntityId, order) {
3255
1898
  const clip = requireEntityKind(rows, clipEntityId, "clip");
3256
1899
  replaceEntity(rows, clip.entityId, {
@@ -3326,15 +1969,6 @@ function linearRate(value) {
3326
1969
  if (!isJsonObject(value) || value.kind !== "linear") throw new Error("Only linear Clip time remapping is supported");
3327
1970
  return finitePositive(value.rate, "linear speed rate");
3328
1971
  }
3329
- function speedModeRate(mode) {
3330
- switch (mode) {
3331
- case "0.5x": return .5;
3332
- case "0.75x": return .75;
3333
- case "1.0x": return 1;
3334
- case "1.5x": return 1.5;
3335
- case "2.0x": return 2;
3336
- }
3337
- }
3338
1972
  function resizeAbsoluteTarget(payload, duration) {
3339
1973
  if (payload.targetRange === void 0) return;
3340
1974
  const target = requirePositiveRange(payload.targetRange, "targetRange");
@@ -3400,7 +2034,15 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
3400
2034
  const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
3401
2035
  let entities = structuredClone([...initial.entities]);
3402
2036
  let relations = structuredClone([...initial.relations]);
3403
- const documentAudioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
2037
+ const documentAudioScriptEntityId = ensureDocumentAudioScript(entities) ?? (() => {
2038
+ const id = createEntityId(idFactory("entity"));
2039
+ entities.push({
2040
+ entityId: id,
2041
+ entityKind: "audio-script",
2042
+ payload: { segments: [] }
2043
+ });
2044
+ return String(id);
2045
+ })();
3404
2046
  const addEntity = (kind, payload, existingId) => {
3405
2047
  const entityId = createEntityId(existingId ?? idFactory("entity"));
3406
2048
  if (entities.some((row) => row.entityId === entityId)) fail(`Migration entity identity collision: ${entityId}`);
@@ -6503,4 +5145,4 @@ function serverCovers(serverVV, target) {
6503
5145
  return covers(server, target);
6504
5146
  }
6505
5147
  //#endregion
6506
- 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, compileCaptionAssetsEffect, compileEntityRows, compileGeneratedVisualEffect, compileSpeechAssetsEffect, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importCaptionAsset, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, prepareCaptionAssetsEffect, prepareCaptionContent, prepareContinueVideoEffect, prepareGeneratedVisualEffect, prepareSpeechAssetsEffect, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
5148
+ 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 };