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

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 fromVideoDocument, A as resolveEntityTimelineLayout, At as base64ToBytes, B as assembleScriptText, C as canonicalJson, Ct as safeDurationMs, D as resolveFieldPath, Dt as partUnionToDraft, E as assertFieldChanges, Et as speedOf, F as relationsTo, G as isJsonObject, H as selectAudioScriptSegment, I as ScriptCompositionError, J as hasSequence, K as createEntityId, L as assembleCaptionContent, M as validateEntityRelationTransition, N as decodeEntityRelationRows, O as projectEntityTimeline, Ot as recordEntries, P as relationsFrom, Q as derivePositionFromAbs, R as assembleEntityContent, S as scriptSourceAssetOf, St as partDurationMs, T as applyFieldChanges, Tt as effectiveVideoClipDurationMs, U as updateEntityFields, V as findComposedAudioScript, W as variantBaseEntityIds, X as buildInitialVideoDocument, Y as isMediaAssetVariantKind, Z as buildSpeechHostMap, _ as audioScriptAssetHash, _t as resolveAllSpeechOverlaps, a as createMirrorVideoDocument, at as videoDocumentSchema, b as phoneticScriptAssetHash, bt as TIMELINE_SKELETON_DURATION_MS, c as DocumentMutationGuard, ct as findLaneTrack, d as LoroEntityDraft, dt as solveVideoDocument, et as toVideoDocument, f as readEntityDocument, ft as cascadeAfterVideoClipChanges, g as audioScriptAssetFields, gt as recalculateTimelineDuration, h as audioScriptAssetContent, ht as reassignSpeechesToVideoClipsByTime, i as MirrorVideoDocumentAdapter, it as partUnionSchema, j as EntityTimelineProjectionError, jt as bytesToBase64, k as EntityProjectionGraph, kt as videoDocumentMirrorSchema, l as LORO_ENTITY_SCHEMA, lt as hasResolvedPlacement, m as entityOrderGroups, mt as fillMainTrackTimeGaps, n as createPlainMemoryAdapter, nt as assertValidVideoDocument, o as createMirrorVideoDocumentAdapter, ot as LANE_KINDS_IN_STACK_ORDER, p as readEntityRelationRecords, pt as arrangeMainTrackSeamlessly, q as createRelationId, r as generatePartId, rt as validateVideoDocument, s as readVideoDocumentFromDraft, st as ensureLaneTrack, t as PlainMemoryAdapter, tt as VideoDocumentValidationError, u as LoroEntityDocument, ut as laneTrackId, v as audioScriptAssetOf, vt as resolveSpeechOverlapByShiftingVideos, w as equalJson, wt as DEFAULT_UNIT_TIME_MS, x as phoneticScriptAssetOf, xt as isEmptyVideoClip, y as phoneticScriptAssetContent, yt as syncAggregatedClipsTimePosition, z as assemblePhoneticScriptContent } from "./document-Bx9xf40e.js";
1
+ import { $ as assertValidVideoDocument, A as decodeEntityRelationRows, B as updateEntityFields, C as applyFieldChanges, Ct as speedOf, D as EntityProjectionGraph, Dt as base64ToBytes, E as projectEntityTimeline, Et as videoDocumentMirrorSchema, F as assembleEntityContent, G as hasSequence, H as isJsonObject, I as assemblePhoneticScriptContent, J as buildSpeechHostMap, K as isMediaAssetVariantKind, L as assembleScriptText, M as relationsTo, N as ScriptCompositionError, O as resolveEntityTimelineLayout, Ot as bytesToBase64, P as assembleCaptionContent, Q as VideoDocumentValidationError, R as findComposedAudioScript, S as equalJson, St as effectiveVideoClipDurationMs, T as resolveFieldPath, Tt as recordEntries, U as createEntityId, V as variantBaseEntityIds, W as createRelationId, X as fromVideoDocument, Y as derivePositionFromAbs, Z as toVideoDocument, _ as audioScriptAssetOf, _t as TIMELINE_SKELETON_DURATION_MS, a as createMirrorVideoDocument, at as findLaneTrack, b as phoneticScriptAssetOf, bt as safeDurationMs, c as DocumentMutationGuard, ct as solveVideoDocument, d as LoroEntityDraft, dt as fillMainTrackTimeGaps, et as validateVideoDocument, f as readEntityDocument, ft as reassignSpeechesToVideoClipsByTime, g as audioScriptAssetHash, gt as syncAggregatedClipsTimePosition, h as audioScriptAssetFields, ht as resolveSpeechOverlapByShiftingVideos, i as MirrorVideoDocumentAdapter, it as ensureLaneTrack, j as relationsFrom, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as cascadeAfterVideoClipChanges, m as audioScriptAssetContent, mt as resolveAllSpeechOverlaps, n as createPlainMemoryAdapter, nt as videoDocumentSchema, o as createMirrorVideoDocumentAdapter, ot as hasResolvedPlacement, p as entityOrderGroups, pt as recalculateTimelineDuration, q as buildInitialVideoDocument, r as generatePartId, rt as LANE_KINDS_IN_STACK_ORDER, s as readVideoDocumentFromDraft, st as laneTrackId, t as PlainMemoryAdapter, tt as partUnionSchema, u as LoroEntityDocument, ut as arrangeMainTrackSeamlessly, v as phoneticScriptAssetContent, vt as isEmptyVideoClip, w as assertFieldChanges, wt as partUnionToDraft, x as scriptSourceAssetOf, xt as DEFAULT_UNIT_TIME_MS, y as phoneticScriptAssetHash, yt as partDurationMs, z as selectAudioScriptSegment } from "./document-Ct11SMBS.js";
2
2
  import { addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, t as schemas_exports } from "./schemas.js";
3
3
  import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
4
- import { sha256 } from "@noble/hashes/sha2.js";
5
- import { bytesToHex } from "@noble/hashes/utils.js";
6
4
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
7
5
  import { DisposableSet, EventBus, Task } from "@mengine/utils";
8
6
  import { BaseDocStorage, DummyConnection } from "@mengine/storage";
@@ -216,36 +214,20 @@ function findSseFrameBoundary(buffer) {
216
214
  //#endregion
217
215
  //#region src/entity-crdt/compile-rows.ts
218
216
  /** Lower a detached ordinary-object edit against its actual causal Loro baseline. */
219
- function compileEntityRows(snapshot, rows, validate, options = {}) {
217
+ function compileEntityRows(snapshot, rows, validate) {
220
218
  const doc = new LoroDoc();
221
219
  doc.import(snapshot);
222
- return applyEntityRows(new LoroEntityDocument(doc, validate), rows, options);
223
- }
224
- /** Publish final rows as imported causal operations, outside the caller's local undo history. */
225
- function publishEntityRows(editor, rows, options = {}) {
226
- const from = editor.doc.oplogVersion();
227
- const compiled = compileEntityRows(editor.doc.export({ mode: "snapshot" }), rows, void 0, options);
228
- editor.import(compiled.update);
229
- return {
230
- update: editor.doc.export({
231
- mode: "update",
232
- from
233
- }),
234
- state: editor.snapshot()
235
- };
220
+ return applyEntityRows(new LoroEntityDocument(doc, validate), rows);
236
221
  }
237
222
  /** Compile an edit on an existing peer, retaining its native local undo history. */
238
- function applyEntityRows(editor, rows, options = {}) {
223
+ function applyEntityRows(editor, rows) {
239
224
  const baseline = editor.snapshot();
240
225
  const before = baseline.rows;
241
226
  const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
242
227
  const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
243
228
  const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
244
229
  const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
245
- const oldClipContent = new Map(before.relations.filter((row) => row.relationKind === "contain" && oldEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
246
- const newClipContent = new Map(rows.relations.filter((row) => row.relationKind === "contain" && newEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
247
230
  if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
248
- validateEntityRelationTransition(before, rows);
249
231
  return {
250
232
  update: editor.transact((draft) => {
251
233
  for (const row of before.relations) {
@@ -253,7 +235,7 @@ function applyEntityRows(editor, rows, options = {}) {
253
235
  if (!next) draft.unlink(row.relationId);
254
236
  else {
255
237
  if (next.relationKind !== row.relationKind || next.endpoint0EntityId !== row.endpoint0EntityId || next.endpoint1EntityId !== row.endpoint1EntityId) throw new Error("Relation identity cannot change kind or endpoint positions");
256
- if (!equalJson(row.trace, next.trace)) draft.patchRelationTrace(row.relationId, next.trace);
238
+ if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
257
239
  }
258
240
  }
259
241
  let attached = baseline.project.audioScriptEntityId !== void 0;
@@ -271,15 +253,12 @@ function applyEntityRows(editor, rows, options = {}) {
271
253
  }
272
254
  }
273
255
  for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
274
- for (const [clipId, relationId] of newClipContent) if (oldClipContent.get(clipId) !== relationId) draft.selectClipContent(relationId);
275
256
  const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
276
257
  for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
277
258
  const next = afterOrder.get(group) ?? [];
278
259
  if (!equalJson(beforeOrder.get(group) ?? [], next)) draft.order(group, next);
279
260
  }
280
261
  for (const row of before.entities) if (!newEntities.has(row.entityId)) draft.delete(row.entityId);
281
- for (const [slot, entityId] of Object.entries(options.projectAttachments ?? {})) draft.attach(slot, entityId);
282
- for (const [clipEntityId, phoneticScriptEntityId] of Object.entries(options.phoneticScriptSelections ?? {})) draft.selectPhoneticScript(clipEntityId, phoneticScriptEntityId);
283
262
  }),
284
263
  state: editor.snapshot()
285
264
  };
@@ -453,16 +432,6 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
453
432
  //#endregion
454
433
  //#region src/entity-editor/entity-graph-http-client.ts
455
434
  const MENGINE_API_PREFIX = "/api/mengine/v1";
456
- var EntityGraphRevisionConflictError = class extends MengineHttpRequestError {
457
- expectedRevision;
458
- currentRevision;
459
- constructor(expectedRevision, currentRevision, payload) {
460
- super(409, payload);
461
- this.expectedRevision = expectedRevision;
462
- this.currentRevision = currentRevision;
463
- this.name = "EntityGraphRevisionConflictError";
464
- }
465
- };
466
435
  /** Browser-safe authenticated Loro client for a document's Entity graph. */
467
436
  var EntityGraphHttpClient = class {
468
437
  options;
@@ -479,8 +448,6 @@ var EntityGraphHttpClient = class {
479
448
  }
480
449
  /**
481
450
  * Compile edits against the original causal snapshot and publish native Loro operations.
482
- * The base revision is sent as an optimistic precondition, so a stale adoption
483
- * fails loudly instead of letting Loro choose between concurrent selections.
484
451
  *
485
452
  * Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
486
453
  * transient create-then-remove never becomes a request to delete unknown
@@ -491,17 +458,13 @@ var EntityGraphHttpClient = class {
491
458
  const next = cloneAndValidateRows(nextRows);
492
459
  assertMediaAssetWritePolicy(baseState.rows, next);
493
460
  const compiled = compileEntityRows(base64ToBytes(baseState.loroSnapshot), next);
494
- return this.commitUpdate(bytesToBase64(compiled.update), baseState.revision);
461
+ return this.commitUpdate(bytesToBase64(compiled.update));
495
462
  }
496
- /** Send prepared bytes directly; omit `expectedRevision` for immutable fact publication. */
497
- async commitUpdate(update, expectedRevision) {
498
- if (expectedRevision !== void 0 && !isNonNegativeSafeInteger(expectedRevision)) throw new Error("expectedRevision must be a nonnegative safe integer");
463
+ /** Send the same prepared bytes on retry; never recompile an unconfirmed edit. */
464
+ async commitUpdate(update) {
499
465
  return this.request({
500
466
  method: "POST",
501
- body: JSON.stringify({
502
- update,
503
- ...expectedRevision === void 0 ? {} : { expected_revision: expectedRevision }
504
- })
467
+ body: JSON.stringify({ update })
505
468
  });
506
469
  }
507
470
  async request(init) {
@@ -510,21 +473,10 @@ var EntityGraphHttpClient = class {
510
473
  headers: this.transport.headers()
511
474
  });
512
475
  const payload = await safeReadJson(response);
513
- if (!response.ok) throw parseRevisionConflict(response.status, payload) ?? new MengineHttpRequestError(response.status, payload);
476
+ if (!response.ok) throw new MengineHttpRequestError(response.status, payload);
514
477
  return parseWireState(payload, this.options.docId);
515
478
  }
516
479
  };
517
- function parseRevisionConflict(status, value) {
518
- if (status !== 409 || !isRecord(value) || !isRecord(value.error) || value.error.code !== "revision_conflict") return;
519
- const error = value.error;
520
- if (typeof error.message !== "string" || !isNonNegativeSafeInteger(error.expected_revision) || !isNonNegativeSafeInteger(error.current_revision)) return;
521
- return new EntityGraphRevisionConflictError(error.expected_revision, error.current_revision, { error: {
522
- code: "revision_conflict",
523
- message: error.message,
524
- expected_revision: error.expected_revision,
525
- current_revision: error.current_revision
526
- } });
527
- }
528
480
  function parseWireState(value, expectedDocId) {
529
481
  if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
530
482
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
@@ -1276,7 +1228,7 @@ var EntityTimelineEditor = class {
1276
1228
  }
1277
1229
  });
1278
1230
  }
1279
- this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phonetic.entityId, input.captions);
1231
+ this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceover.entityId, phonetic.entityId, input.captions);
1280
1232
  this.resolveOneVoiceoverOverlap(draft);
1281
1233
  return {
1282
1234
  voiceoverClipEntityId,
@@ -1463,7 +1415,7 @@ var EntityTimelineEditor = class {
1463
1415
  requireComposedAudioScript(draft, phonetic.entityId);
1464
1416
  return phonetic;
1465
1417
  }
1466
- /** Preserve the factual synthesis input as `[rendered Audio, PhoneticScript]`. */
1418
+ /** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
1467
1419
  ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
1468
1420
  const renders = draft.relations.filter((relation) => relation.relationKind === "derived-from" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
1469
1421
  if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
@@ -1473,7 +1425,7 @@ var EntityTimelineEditor = class {
1473
1425
  }
1474
1426
  draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
1475
1427
  }
1476
- reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
1428
+ reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
1477
1429
  const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
1478
1430
  const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
1479
1431
  if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
@@ -1498,6 +1450,10 @@ var EntityTimelineEditor = class {
1498
1450
  };
1499
1451
  draft.entities.push(content);
1500
1452
  assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1453
+ draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
1454
+ start: startMs,
1455
+ end: startMs + durationMs
1456
+ } }));
1501
1457
  this.insertPlacedClipIntoDraft(draft, {
1502
1458
  clipEntityId: caption.captionClipEntityId,
1503
1459
  trackEntityId: captionTrack.entityId,
@@ -1544,6 +1500,7 @@ var EntityTimelineEditor = class {
1544
1500
  ...rate === 1 ? {} : { rate }
1545
1501
  }
1546
1502
  });
1503
+ this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
1547
1504
  }
1548
1505
  }
1549
1506
  ensureRoleTrackForClip(draft, clipEntityId, role) {
@@ -1552,6 +1509,14 @@ var EntityTimelineEditor = class {
1552
1509
  const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
1553
1510
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1554
1511
  }
1512
+ replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
1513
+ const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
1514
+ draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1515
+ draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
1516
+ start: startMs,
1517
+ end: startMs + durationMs
1518
+ } }));
1519
+ }
1555
1520
  /**
1556
1521
  * Remove one placed Caption use. The composed AudioScript outlives the
1557
1522
  * caption: it may anchor further captions and remains the text owner.
@@ -1947,224 +1912,6 @@ function cloneRows(rows) {
1947
1912
  };
1948
1913
  }
1949
1914
  //#endregion
1950
- //#region src/entity-editor/revise-audio-script.ts
1951
- /** Revise text as an independent authored document, never as a version-family mutation. */
1952
- function reviseAudioScript(rows, input) {
1953
- if (typeof input.editId !== "string" || !input.editId.trim()) throw new Error("editId is required");
1954
- if (!Array.isArray(input.edits) || !input.edits.length) throw new Error("At least one segment edit is required");
1955
- const edits = /* @__PURE__ */ new Map();
1956
- for (const edit of input.edits) {
1957
- if (!edit.segmentId?.trim() || typeof edit.text !== "string" || !edit.text.trim()) throw new Error("Segment text must be nonempty");
1958
- if (edits.has(edit.segmentId)) throw new Error("Duplicate segment edit");
1959
- edits.set(edit.segmentId, edit.text);
1960
- }
1961
- const signature = hash({
1962
- ...input,
1963
- captionUsages: input.captionUsages ?? []
1964
- });
1965
- const prior = rows.relations.filter((r) => r.relationKind === "derived-from" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId);
1966
- if (prior.length && (prior.length !== 1 || prior[0].trace.signature !== signature)) throw new Error("AudioScript edit identity conflict");
1967
- const source = rows.entities.find((row) => row.entityId === input.sourceAudioScriptEntityId);
1968
- if (source?.entityKind !== "audio-script") throw new Error("AudioScript source is missing");
1969
- assembleScriptText({
1970
- ...source,
1971
- entityKind: "audio-script"
1972
- });
1973
- const payload = structuredClone(source.payload);
1974
- if (!Array.isArray(payload.segments)) throw new Error("AudioScript must own its segments");
1975
- const segmentIds = new Set(payload.segments.filter(isJsonObject).map((segment) => segment.segmentId));
1976
- for (const segmentId of edits.keys()) if (!segmentIds.has(segmentId)) throw new Error(`Unknown segment ${segmentId}`);
1977
- const changedSegments = /* @__PURE__ */ new Set();
1978
- payload.segments = payload.segments.map((segment) => {
1979
- if (segment === null || typeof segment !== "object" || Array.isArray(segment)) throw new Error("Invalid segment");
1980
- const text = edits.get(String(segment.segmentId)) ?? segment.text;
1981
- if (text !== segment.text) changedSegments.add(String(segment.segmentId));
1982
- return {
1983
- ...segment,
1984
- text
1985
- };
1986
- });
1987
- if (!changedSegments.size) {
1988
- if (prior.length) throw new Error("AudioScript edit identity conflict");
1989
- return {
1990
- rows,
1991
- audioScriptEntityId: source.entityId,
1992
- captionUsages: [],
1993
- changed: false
1994
- };
1995
- }
1996
- delete payload.assetContentHash;
1997
- const id = hash(input.editId);
1998
- const audioScriptEntityId = createEntityId(`entity_script_revision_${id}`);
1999
- const audioScript = {
2000
- entityId: audioScriptEntityId,
2001
- entityKind: "audio-script",
2002
- payload
2003
- };
2004
- const provenance = {
2005
- relationId: createRelationId(`relation_script_revision_${id}`),
2006
- relationKind: "derived-from",
2007
- endpoint0EntityId: audioScriptEntityId,
2008
- endpoint1EntityId: source.entityId,
2009
- metadata: {},
2010
- trace: {
2011
- command: "revise-audio-script",
2012
- editId: input.editId,
2013
- signature
2014
- }
2015
- };
2016
- const selectedMarkers = /* @__PURE__ */ new Set();
2017
- const planCaption = (usage, index, targetAudioScriptEntityId) => {
2018
- if (selectedMarkers.has(usage.markerEntityId)) throw new Error("Duplicate caption usage");
2019
- selectedMarkers.add(usage.markerEntityId);
2020
- const caption = rows.entities.find((row) => row.entityId === usage.captionEntityId);
2021
- if (caption?.entityKind !== "caption") throw new Error("Caption is missing");
2022
- const assembled = assembleCaptionContent(rows, caption.entityId);
2023
- if (assembled.audioScript.entityId !== source.entityId) throw new Error("Caption does not use the source AudioScript");
2024
- const selection = assembled.caption.payload.selection;
2025
- if (isJsonObject(selection) && selection.textRange !== void 0 && typeof selection.segmentId === "string" && changedSegments.has(selection.segmentId) && usage.selection === void 0) throw new Error("Changed partial Caption requires an explicit selection");
2026
- const bases = caption.payload.baseEntityIds;
2027
- if (!Array.isArray(bases) || !bases.includes(source.entityId)) throw new Error("Caption requires a direct AudioScript base");
2028
- const clipMarkers = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint0EntityId === usage.clipEntityId);
2029
- const clipContents = rows.relations.filter((r) => r.relationKind === "contain" && r.endpoint0EntityId === usage.clipEntityId);
2030
- const markerOwners = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint1EntityId === usage.markerEntityId);
2031
- if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 || clipContents.length !== 1) throw new Error("Caption usage conflict");
2032
- const newCaptionId = createEntityId(`entity_caption_revision_${id}_${index}`);
2033
- return {
2034
- clone: {
2035
- ...caption,
2036
- entityId: newCaptionId,
2037
- payload: {
2038
- ...structuredClone(caption.payload),
2039
- baseEntityIds: bases.map((base) => base === source.entityId ? targetAudioScriptEntityId : base),
2040
- ...usage.selection === void 0 ? {} : { selection: structuredClone(usage.selection) }
2041
- }
2042
- },
2043
- clipContent: clipContents[0],
2044
- newCaptionId,
2045
- relationId: createRelationId(`relation_caption_revision_${id}_${index}`),
2046
- trace: {
2047
- command: "revise-audio-script",
2048
- editId: input.editId,
2049
- usageIndex: index
2050
- }
2051
- };
2052
- };
2053
- if (prior.length) {
2054
- const currentAudioScriptEntityId = prior[0].endpoint0EntityId;
2055
- if (!equalJson(prior[0], {
2056
- ...provenance,
2057
- relationId: prior[0].relationId,
2058
- endpoint0EntityId: currentAudioScriptEntityId
2059
- })) throw new Error("AudioScript edit identity conflict");
2060
- const preservedScripts = rows.entities.filter((row) => row.entityId === currentAudioScriptEntityId);
2061
- const preservedScript = preservedScripts[0]?.entityKind === "audio-script" ? {
2062
- ...preservedScripts[0],
2063
- payload: Object.fromEntries(Object.entries(preservedScripts[0].payload).filter(([key]) => key !== "assetContentHash"))
2064
- } : preservedScripts[0];
2065
- if (preservedScripts.length !== 1 || !equalJson(preservedScript, {
2066
- ...audioScript,
2067
- entityId: currentAudioScriptEntityId
2068
- })) throw new Error("AudioScript edit identity conflict");
2069
- let relations = [...rows.relations];
2070
- let changed = false;
2071
- const captionUsages = [];
2072
- for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2073
- const plan = planCaption(usage, index, currentAudioScriptEntityId);
2074
- const preservedCaptions = rows.entities.filter((row) => row.entityId === plan.newCaptionId);
2075
- if (preservedCaptions.length !== 1 || !equalJson(preservedCaptions[0], plan.clone)) throw new Error("Caption usage conflict after revision");
2076
- const plannedEdge = {
2077
- relationId: plan.relationId,
2078
- relationKind: "contain",
2079
- endpoint0EntityId: plan.clipContent.endpoint0EntityId,
2080
- endpoint1EntityId: plan.newCaptionId,
2081
- metadata: structuredClone(plan.clipContent.metadata),
2082
- trace: plan.trace
2083
- };
2084
- if (plan.clipContent.relationId === plan.relationId) {
2085
- if (!equalJson(plan.clipContent, plannedEdge)) throw new Error("Caption usage conflict after revision");
2086
- } else {
2087
- if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict after revision");
2088
- relations = relations.filter((row) => row.relationId !== plan.clipContent.relationId);
2089
- relations.push(plannedEdge);
2090
- changed = true;
2091
- }
2092
- captionUsages.push({
2093
- clipEntityId: usage.clipEntityId,
2094
- markerEntityId: usage.markerEntityId,
2095
- previousCaptionEntityId: usage.captionEntityId,
2096
- captionEntityId: plan.newCaptionId
2097
- });
2098
- }
2099
- if (!changed) return {
2100
- rows,
2101
- audioScriptEntityId: currentAudioScriptEntityId,
2102
- captionUsages,
2103
- changed: false
2104
- };
2105
- const revised = {
2106
- entities: rows.entities,
2107
- relations
2108
- };
2109
- decodeAndValidate(rows, revised);
2110
- return {
2111
- rows: revised,
2112
- audioScriptEntityId: currentAudioScriptEntityId,
2113
- captionUsages,
2114
- changed: true
2115
- };
2116
- }
2117
- const result = {
2118
- entities: [...rows.entities, audioScript],
2119
- relations: [...rows.relations, provenance]
2120
- };
2121
- const entities = [...result.entities];
2122
- let relations = [...result.relations];
2123
- const captionUsages = [];
2124
- for (const [index, usage] of (input.captionUsages ?? []).entries()) {
2125
- const plan = planCaption(usage, index, audioScriptEntityId);
2126
- if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict");
2127
- entities.push(plan.clone);
2128
- assembleCaptionContent({
2129
- entities,
2130
- relations
2131
- }, plan.newCaptionId);
2132
- relations = relations.filter((r) => r.relationId !== plan.clipContent.relationId);
2133
- relations.push({
2134
- relationId: plan.relationId,
2135
- relationKind: "contain",
2136
- endpoint0EntityId: plan.clipContent.endpoint0EntityId,
2137
- endpoint1EntityId: plan.newCaptionId,
2138
- metadata: structuredClone(plan.clipContent.metadata),
2139
- trace: plan.trace
2140
- });
2141
- captionUsages.push({
2142
- clipEntityId: usage.clipEntityId,
2143
- markerEntityId: usage.markerEntityId,
2144
- previousCaptionEntityId: usage.captionEntityId,
2145
- captionEntityId: plan.newCaptionId
2146
- });
2147
- }
2148
- const revised = {
2149
- entities,
2150
- relations
2151
- };
2152
- decodeAndValidate(rows, revised);
2153
- return {
2154
- rows: revised,
2155
- audioScriptEntityId,
2156
- captionUsages,
2157
- changed: true
2158
- };
2159
- }
2160
- function decodeAndValidate(previous, revised) {
2161
- decodeEntityRelationRows(revised, {});
2162
- validateEntityRelationTransition(previous, revised);
2163
- }
2164
- function hash(value) {
2165
- return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
2166
- }
2167
- //#endregion
2168
1915
  //#region src/entity-editor/legacy-entity-migration.ts
2169
1916
  /** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
2170
1917
  function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
@@ -2329,6 +2076,10 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2329
2076
  selection,
2330
2077
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2331
2078
  });
2079
+ link("caption-alignment", content, voiceover, { alignment: {
2080
+ start: caption.start_ms,
2081
+ end: caption.start_ms + duration
2082
+ } });
2332
2083
  markerPayload = {
2333
2084
  inMs: 0,
2334
2085
  outMs: duration
@@ -5247,4 +4998,4 @@ function serverCovers(serverVV, target) {
5247
4998
  return covers(server, target);
5248
4999
  }
5249
5000
  //#endregion
5250
- export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityGraphRevisionConflictError, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, publishEntityRows, readDocumentAudioScript, readEntityDocument, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
5001
+ export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { c as MirrorVideoDocumentAdapter, kt as VideoDraft } from "./index-BYv9FPSS.js";
1
+ import { c as MirrorVideoDocumentAdapter, kt as VideoDraft } from "./index-CcIoMxs1.js";
2
2
 
3
3
  //#region src/testing/in-memory-mengine-server.d.ts
4
4
  /**
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { At as base64ToBytes, jt as bytesToBase64, o as createMirrorVideoDocumentAdapter } from "./document-Bx9xf40e.js";
1
+ import { Dt as base64ToBytes, Ot as bytesToBase64, o as createMirrorVideoDocumentAdapter } from "./document-Ct11SMBS.js";
2
2
  import { t as classifyUpdate } from "./loro-relay-doc-cJSY-uau.js";
3
3
  import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
4
4
  //#region src/testing/in-memory-mengine-server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "2.0.1-alpha.25",
3
+ "version": "2.0.1-alpha.26",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,9 +28,9 @@
28
28
  "immer": "^10.2.0",
29
29
  "loro-mirror": "^2.3.3",
30
30
  "zod": "^4.4.3",
31
- "@mengine/storage": "2.0.1-alpha.25",
32
- "@mengine/sync": "2.0.1-alpha.25",
33
- "@mengine/utils": "2.0.1-alpha.25"
31
+ "@mengine/storage": "2.0.1-alpha.26",
32
+ "@mengine/utils": "2.0.1-alpha.26",
33
+ "@mengine/sync": "2.0.1-alpha.26"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^25.9.1",