@mengine/medeo-client 1.3.1-alpha.1 → 1.3.1-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, A as VideoDocumentValidationError, B as cascadeAfterVideoClipChanges, C as hasSequence, D as derivePositionFromAbs, E as buildSpeechHostMap, F as LANE_KINDS_IN_STACK_ORDER, G as resolveAllSpeechOverlaps, H as fillMainTrackTimeGaps, I as ensureLaneTrack, J as TIMELINE_SKELETON_DURATION_MS, K as resolveSpeechOverlapByShiftingVideos, L as findLaneTrack, M as validateVideoDocument, N as partUnionSchema, O as fromVideoDocument, P as videoDocumentSchema, Q as DEFAULT_UNIT_TIME_MS, R as laneTrackId, S as selectAudioScriptSegments, T as buildInitialVideoDocument, U as reassignSpeechesToVideoClipsByTime, V as arrangeMainTrackSeamlessly, W as recalculateTimelineDuration, X as partDurationMs, Y as isEmptyVideoClip, Z as safeDurationMs, _ as ScriptCompositionError, a as applyEntityTimelineProjection, at as videoDocumentMirrorSchema, b as assembleScriptText, c as writeVideoDocumentToDraft, d as resolveEntityTimelineLayout, et as VIDEO_DOCUMENT_SCHEMA_VERSION, f as EntityTimelineProjectionError, g as createRelationId, h as createEntityId, i as MirrorVideoDocumentAdapter, it as recordEntries, j as assertValidVideoDocument, k as toVideoDocument, l as readVideoDocumentFromDraft, m as isJsonObject, n as createPlainMemoryAdapter, nt as speedOf, o as createMirrorVideoDocument, ot as base64ToBytes, p as decodeEntityRelationRows, q as syncAggregatedClipsTimePosition, r as generatePartId, rt as partUnionToDraft, s as createMirrorVideoDocumentAdapter, st as bytesToBase64, t as PlainMemoryAdapter, tt as effectiveVideoClipDurationMs, u as projectEntityTimeline, v as assembleCaptionContent, w as isMediaAssetVariantKind, x as findComposedAudioScript, y as assemblePhoneticScriptContent, z as solveVideoDocument } from "./document-Bf200CmD.js";
1
+ import { $ as reassignSpeechesToVideoClipsByTime, A as isJsonObject, B as VideoDocumentValidationError, C as assembleEntityContent, D as selectAudioScriptSegments, E as findComposedAudioScript, F as buildInitialVideoDocument, G as LANE_KINDS_IN_STACK_ORDER, H as validateVideoDocument, I as buildSpeechHostMap, J as laneTrackId, K as ensureLaneTrack, L as derivePositionFromAbs, M as createRelationId, N as hasSequence, O as updateEntityFields, P as isMediaAssetVariantKind, Q as fillMainTrackTimeGaps, R as fromVideoDocument, S as assembleCaptionContent, T as assembleScriptText, U as partUnionSchema, V as assertValidVideoDocument, W as videoDocumentSchema, X as cascadeAfterVideoClipChanges, Y as solveVideoDocument, Z as arrangeMainTrackSeamlessly, _ as EntityProjectionGraph, _t as bytesToBase64, a as applyEntityTimelineProjection, at as isEmptyVideoClip, b as decodeEntityRelationRows, c as writeVideoDocumentToDraft, ct as DEFAULT_UNIT_TIME_MS, d as LoroEntityDocument, dt as effectiveVideoClipDurationMs, et as recalculateTimelineDuration, f as LoroEntityDraft, ft as speedOf, g as projectEntityTimeline, gt as base64ToBytes, h as equalJson, ht as videoDocumentMirrorSchema, i as MirrorVideoDocumentAdapter, it as TIMELINE_SKELETON_DURATION_MS, j as createEntityId, k as variantBaseEntityIds, l as readVideoDocumentFromDraft, lt as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, m as entityOrderGroups, mt as recordEntries, n as createPlainMemoryAdapter, nt as resolveSpeechOverlapByShiftingVideos, o as createMirrorVideoDocument, ot as partDurationMs, p as readEntityDocument, pt as partUnionToDraft, q as findLaneTrack, r as generatePartId, rt as syncAggregatedClipsTimePosition, s as createMirrorVideoDocumentAdapter, st as safeDurationMs, t as PlainMemoryAdapter, tt as resolveAllSpeechOverlaps, u as LORO_ENTITY_SCHEMA, ut as VIDEO_DOCUMENT_SCHEMA_VERSION, v as resolveEntityTimelineLayout, w as assemblePhoneticScriptContent, x as ScriptCompositionError, y as EntityTimelineProjectionError, z as toVideoDocument } from "./document-CkbNx-mi.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, VersionVector } from "loro-crdt";
4
4
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -217,6 +217,67 @@ function findSseFrameBoundary(buffer) {
217
217
  return Math.min(lf, crlf);
218
218
  }
219
219
  //#endregion
220
+ //#region src/entity-crdt/compile-rows.ts
221
+ /** Lower a detached ordinary-object edit against its actual causal Loro baseline. */
222
+ function compileEntityRows(snapshot, rows, validate) {
223
+ const doc = new LoroDoc();
224
+ doc.import(snapshot);
225
+ return applyEntityRows(new LoroEntityDocument(doc, validate), rows);
226
+ }
227
+ /** Compile an edit on an existing peer, retaining its native local undo history. */
228
+ function applyEntityRows(editor, rows) {
229
+ const before = editor.snapshot().rows;
230
+ const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
231
+ const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
232
+ const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
233
+ const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
234
+ if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
235
+ return {
236
+ update: editor.transact((draft) => {
237
+ for (const row of before.relations) {
238
+ const next = newRelations.get(row.relationId);
239
+ if (!next) draft.unlink(row.relationId);
240
+ else {
241
+ if (next.relationKind !== row.relationKind || next.endpoint0EntityId !== row.endpoint0EntityId || next.endpoint1EntityId !== row.endpoint1EntityId) throw new Error("Relation identity cannot change kind or endpoint positions");
242
+ if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
243
+ }
244
+ }
245
+ for (const row of rows.entities) {
246
+ const previous = oldEntities.get(row.entityId);
247
+ if (!previous) draft.create(row);
248
+ else {
249
+ if (previous.entityKind !== row.entityKind) throw new Error("Entity kind cannot change");
250
+ if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
251
+ }
252
+ }
253
+ for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
254
+ const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
255
+ for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
256
+ const next = afterOrder.get(group) ?? [];
257
+ if (!equalJson(beforeOrder.get(group) ?? [], next)) draft.order(group, next);
258
+ }
259
+ for (const row of before.entities) if (!newEntities.has(row.entityId)) draft.delete(row.entityId);
260
+ }),
261
+ state: editor.snapshot()
262
+ };
263
+ }
264
+ //#endregion
265
+ //#region src/entity-editor/document-audio-script.ts
266
+ /** Read only the attached version and preserve every segment; never aggregate other scripts. */
267
+ function readDocumentAudioScript(state) {
268
+ const id = state.audioScriptEntityId;
269
+ if (id === null) throw new EntityTimelineProjectionError("Document AudioScript is not initialized");
270
+ if (typeof id !== "string" || id.trim() !== id || !id) throw new EntityTimelineProjectionError("Invalid document AudioScript identity");
271
+ const entity = state.rows.entities.find((row) => row.entityId === id);
272
+ if (!entity || entity.entityKind !== "audio-script") throw new EntityTimelineProjectionError(`Document AudioScript ${id} must name the project AudioScript`);
273
+ const content = assembleEntityContent(state.rows, createEntityId(id));
274
+ assembleScriptText(content);
275
+ return {
276
+ entityId: id,
277
+ segments: structuredClone(content.payload.segments)
278
+ };
279
+ }
280
+ //#endregion
220
281
  //#region src/entity-editor/editor-resources.ts
221
282
  const roleOrder$1 = {
222
283
  caption: 0,
@@ -296,6 +357,25 @@ function memotaLocator(value) {
296
357
  key: value.key
297
358
  };
298
359
  }
360
+ /**
361
+ * Bootstrap an empty script when the foundation has no candidate. Without an
362
+ * explicit project attachment, multiple candidates are ambiguous. Existing
363
+ * content IDs are reused here; Loro editing advances their immutable versions.
364
+ */
365
+ function ensureDocumentAudioScript(entities, idFactory) {
366
+ const scripts = entities.filter((row) => row.entityKind === "audio-script");
367
+ if (scripts.length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one AudioScript");
368
+ const existing = scripts[0];
369
+ if (existing !== void 0) return existing.entityId;
370
+ const id = createEntityId(idFactory("entity"));
371
+ if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
372
+ entities.push({
373
+ entityId: id,
374
+ entityKind: "audio-script",
375
+ payload: { segments: [] }
376
+ });
377
+ return id;
378
+ }
299
379
  /** Add only missing editor structure; existing identities and owned settings are never replaced. */
300
380
  function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}) {
301
381
  const entities = structuredClone([...initial.entities]);
@@ -348,13 +428,15 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
348
428
  }
349
429
  trackEntityIds[role] = track.entityId;
350
430
  }
431
+ const audioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
351
432
  return {
352
433
  rows: {
353
434
  entities,
354
435
  relations
355
436
  },
356
437
  timelineEntityId: timeline.entityId,
357
- trackEntityIds
438
+ trackEntityIds,
439
+ audioScriptEntityId
358
440
  };
359
441
  }
360
442
  //#endregion
@@ -364,7 +446,7 @@ const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left,
364
446
  if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
365
447
  return left < right ? -1 : left > right ? 1 : 0;
366
448
  } };
367
- /** Browser-safe authenticated CAS client for a document's Entity graph. */
449
+ /** Browser-safe authenticated Loro client for a document's Entity graph. */
368
450
  var EntityGraphHttpClient = class {
369
451
  options;
370
452
  transport;
@@ -379,30 +461,24 @@ var EntityGraphHttpClient = class {
379
461
  return this.request({ method: "GET" });
380
462
  }
381
463
  /**
382
- * Replace the complete Entity graph under the baseline revision.
464
+ * Compile edits against the original causal snapshot and publish native Loro operations.
383
465
  *
384
466
  * Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
385
467
  * transient create-then-remove never becomes a request to delete unknown
386
468
  * persisted data.
387
469
  */
388
470
  async commit(baseState, nextRows, options = {}) {
389
- if (!isNonNegativeSafeInteger(baseState.revision)) throw new Error("Entity graph revision must be a non-negative safe integer");
390
- const baseRows = cloneAndValidateRows(baseState.rows);
471
+ if (options.migrationBaseVv !== void 0) throw new Error("Legacy entity migration is not supported by the Loro contract");
391
472
  const next = cloneAndValidateRows(nextRows);
392
- assertMediaAssetWritePolicy(baseRows, next);
393
- const nextEntityIds = new Set(next.entities.map((row) => row.entityId));
394
- const nextRelationIds = new Set(next.relations.map((row) => row.relationId));
395
- const deletedEntityIds = baseRows.entities.map((row) => row.entityId).filter((entityId) => !nextEntityIds.has(entityId)).sort();
396
- const deletedRelationIds = baseRows.relations.map((row) => row.relationId).filter((relationId) => !nextRelationIds.has(relationId)).sort();
473
+ assertMediaAssetWritePolicy(baseState.rows, next);
474
+ const compiled = compileEntityRows(base64ToBytes(baseState.loroSnapshot), next);
475
+ return this.commitUpdate(bytesToBase64(compiled.update));
476
+ }
477
+ /** Send the same prepared bytes on retry; never recompile an unconfirmed edit. */
478
+ async commitUpdate(update) {
397
479
  return this.request({
398
480
  method: "POST",
399
- body: JSON.stringify({
400
- expected_revision: baseState.revision,
401
- rows: toWireRows(next),
402
- deleted_entity_ids: deletedEntityIds,
403
- deleted_relation_ids: deletedRelationIds,
404
- ...options.migrationBaseVv === void 0 ? {} : { migration_base_vv: options.migrationBaseVv }
405
- })
481
+ body: JSON.stringify({ update })
406
482
  });
407
483
  }
408
484
  async request(init) {
@@ -420,10 +496,16 @@ function parseWireState(value, expectedDocId) {
420
496
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
421
497
  const rows = parseWireRows(value.rows);
422
498
  decodeEntityRelationRows(rows, numericMarkerValidation$1);
423
- return {
499
+ if (typeof value.audio_script_entity_id !== "string") throw new Error("Document AudioScript is not initialized");
500
+ if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
501
+ const state = {
424
502
  revision: value.revision,
425
- rows
503
+ loroSnapshot: value.loro_snapshot,
504
+ rows,
505
+ audioScriptEntityId: value.audio_script_entity_id
426
506
  };
507
+ readDocumentAudioScript(state);
508
+ return state;
427
509
  }
428
510
  function parseWireRows(value) {
429
511
  if (!isRecord(value) || !Array.isArray(value.entities) || !Array.isArray(value.relations)) throw new Error("Invalid entity-state response rows");
@@ -456,23 +538,6 @@ function cloneAndValidateRows(rows) {
456
538
  decodeEntityRelationRows(clone, numericMarkerValidation$1);
457
539
  return clone;
458
540
  }
459
- function toWireRows(rows) {
460
- return {
461
- entities: rows.entities.map((row) => ({
462
- entity_id: row.entityId,
463
- entity_kind: row.entityKind,
464
- payload: row.payload
465
- })),
466
- relations: rows.relations.map((row) => ({
467
- relation_id: row.relationId,
468
- relation_kind: row.relationKind,
469
- endpoint_0_entity_id: row.endpoint0EntityId,
470
- endpoint_1_entity_id: row.endpoint1EntityId,
471
- metadata: row.metadata,
472
- trace: row.trace
473
- }))
474
- };
475
- }
476
541
  function requiredString(value, field) {
477
542
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`Invalid ${field} in entity-state response`);
478
543
  return value;
@@ -616,8 +681,12 @@ function requireComposedAudioScript(rows, phoneticScriptEntityId) {
616
681
  function captionSelections(selections) {
617
682
  return structuredClone(selections);
618
683
  }
619
- function assertSelectionNamesScriptSegments(rows, audioScriptEntityId, selections) {
620
- selectAudioScriptSegments(requireEntityKind(rows, audioScriptEntityId, "audio-script"), selections);
684
+ function captionBaseIds(ids) {
685
+ if (!Array.isArray(ids) || ids.length === 0) throw new Error("Caption requires non-empty baseEntityIds");
686
+ return ids.map(createEntityId);
687
+ }
688
+ function assertCaptionScript(rows, captionId, scriptId) {
689
+ if (assembleCaptionContent(rows, captionId).audioScript.entityId !== scriptId) throw new Error("Voiceover Caption must compose the same AudioScript as the PhoneticScript");
621
690
  }
622
691
  const roleOrder = {
623
692
  caption: 0,
@@ -1281,28 +1350,23 @@ var EntityTimelineEditor = class {
1281
1350
  const track = findRoleTrack(draft, input.timelineEntityId, "caption");
1282
1351
  if (track === void 0) return;
1283
1352
  const patch = cloneJsonObject(input.style, "Caption style");
1353
+ const assembled = new EntityProjectionGraph(draft);
1284
1354
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1285
1355
  const topology = requireClipTopology(draft, clip.entityId);
1286
1356
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1287
- const prior = caption.payload.style;
1357
+ const prior = assembled.entity(caption.entityId).payload.style;
1288
1358
  const style = isJsonObject(prior) ? prior : {};
1289
- replaceEntity(draft, caption.entityId, {
1290
- ...caption,
1291
- payload: {
1292
- ...caption.payload,
1293
- style: {
1294
- ...style,
1295
- ...patch
1296
- }
1297
- }
1298
- });
1359
+ for (const updated of updateEntityFields(draft, caption.entityId, { style: {
1360
+ ...style,
1361
+ ...patch
1362
+ } })) replaceEntity(draft, updated.entityId, updated);
1299
1363
  }
1300
1364
  });
1301
1365
  }
1302
1366
  /**
1303
1367
  * Place a Caption as a composed variant of an existing AudioScript: the
1304
- * entity stores only its selection, style, and inherent cue-bounded
1305
- * Sequence — never text. The Clip and its display Marker describe this use;
1368
+ * entity stores baseEntityIds, selection, optional style, and inherent cue-bounded
1369
+ * Sequence. This helper does not copy base text. The Clip and its display Marker describe this use;
1306
1370
  * moving or stretching the returned Clip later changes the Clip/Marker,
1307
1371
  * never the Caption entity or the AudioScript.
1308
1372
  */
@@ -1310,8 +1374,6 @@ var EntityTimelineEditor = class {
1310
1374
  return this.transact((draft) => {
1311
1375
  const durationMs = finitePositive(input.durationMs, "Caption durationMs");
1312
1376
  const selections = captionSelections(input.selections);
1313
- const script = requireEntityKind(draft, input.audioScriptEntityId, "audio-script");
1314
- assertSelectionNamesScriptSegments(draft, script.entityId, selections);
1315
1377
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
1316
1378
  const content = {
1317
1379
  entityId: this.mintEntityId(draft),
@@ -1324,12 +1386,13 @@ var EntityTimelineEditor = class {
1324
1386
  },
1325
1387
  sampling: "native",
1326
1388
  coordinateSpace: "milliseconds",
1389
+ baseEntityIds: captionBaseIds(input.baseEntityIds),
1327
1390
  selections,
1328
1391
  ...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
1329
1392
  }
1330
1393
  };
1331
1394
  draft.entities.push(content);
1332
- draft.relations.push(this.createRelation(draft, "caption-composition", content.entityId, script.entityId, { segmentAlignment: [] }));
1395
+ assembleCaptionContent(draft, content.entityId);
1333
1396
  return this.insertPlacedClipIntoDraft(draft, {
1334
1397
  trackEntityId: track.entityId,
1335
1398
  contentEntityId: content.entityId,
@@ -1434,7 +1497,7 @@ var EntityTimelineEditor = class {
1434
1497
  const startMs = finiteNonNegative(caption.startMs, "Caption startMs");
1435
1498
  const durationMs = finitePositive(caption.durationMs, "Caption durationMs");
1436
1499
  const selections = captionSelections(caption.selections);
1437
- assertSelectionNamesScriptSegments(draft, audioScriptEntityId, selections);
1500
+ const baseEntityIds = captionBaseIds(caption.baseEntityIds);
1438
1501
  if (draft.entities.find((entity) => entity.entityId === caption.captionClipEntityId) === void 0) {
1439
1502
  const content = {
1440
1503
  entityId: this.mintEntityId(draft),
@@ -1447,12 +1510,14 @@ var EntityTimelineEditor = class {
1447
1510
  },
1448
1511
  sampling: "native",
1449
1512
  coordinateSpace: "milliseconds",
1513
+ baseEntityIds,
1450
1514
  selections,
1451
1515
  ...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
1452
1516
  }
1453
1517
  };
1454
1518
  draft.entities.push(content);
1455
- draft.relations.push(this.createRelation(draft, "caption-composition", content.entityId, audioScriptEntityId, { segmentAlignment: [] }), this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
1519
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1520
+ draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
1456
1521
  start: startMs,
1457
1522
  end: startMs + durationMs
1458
1523
  } }));
@@ -1487,10 +1552,12 @@ var EntityTimelineEditor = class {
1487
1552
  ...content,
1488
1553
  payload: {
1489
1554
  ...content.payload,
1555
+ baseEntityIds,
1490
1556
  selections,
1491
1557
  ...Object.keys(style).length === 0 ? {} : { style }
1492
1558
  }
1493
1559
  });
1560
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1494
1561
  this.applyClipPlacement(draft, topology.clip.entityId, {
1495
1562
  kind: "anchored",
1496
1563
  hostClipEntityId: voiceoverClipEntityId,
@@ -1507,7 +1574,7 @@ var EntityTimelineEditor = class {
1507
1574
  }
1508
1575
  }
1509
1576
  });
1510
- this.replaceCaptionSemanticRelations(draft, content.entityId, voiceEntityId, audioScriptEntityId, startMs, durationMs);
1577
+ this.replaceCaptionAlignment(draft, content.entityId, voiceEntityId, startMs, durationMs);
1511
1578
  }
1512
1579
  }
1513
1580
  ensureRoleTrackForClip(draft, clipEntityId, role) {
@@ -1516,13 +1583,13 @@ var EntityTimelineEditor = class {
1516
1583
  const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
1517
1584
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1518
1585
  }
1519
- replaceCaptionSemanticRelations(draft, captionEntityId, voiceEntityId, scriptEntityId, startMs, durationMs) {
1520
- const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment" || relation.relationKind === "caption-composition").map((relation) => relation.relationId));
1586
+ replaceCaptionAlignment(draft, captionEntityId, voiceEntityId, startMs, durationMs) {
1587
+ const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
1521
1588
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1522
1589
  draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceEntityId, { alignment: {
1523
1590
  start: startMs,
1524
1591
  end: startMs + durationMs
1525
- } }), this.createRelation(draft, "caption-composition", captionEntityId, scriptEntityId, { segmentAlignment: [] }));
1592
+ } }));
1526
1593
  }
1527
1594
  /**
1528
1595
  * Remove one placed Caption use. The composed AudioScript outlives the
@@ -1532,11 +1599,7 @@ var EntityTimelineEditor = class {
1532
1599
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1533
1600
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1534
1601
  const contentRelations = incidentRelations(draft, caption.entityId);
1535
- const allowed = new Set([
1536
- "caption-composition",
1537
- "caption-alignment",
1538
- "marker-content"
1539
- ]);
1602
+ const allowed = new Set(["caption-alignment", "marker-content"]);
1540
1603
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1541
1604
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1542
1605
  this.removeClipGraph(draft, clipEntityId);
@@ -1936,6 +1999,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
1936
1999
  const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
1937
2000
  let entities = structuredClone([...initial.entities]);
1938
2001
  let relations = structuredClone([...initial.relations]);
2002
+ const documentAudioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
1939
2003
  const addEntity = (kind, payload, existingId) => {
1940
2004
  const entityId = createEntityId(existingId ?? idFactory("entity"));
1941
2005
  if (entities.some((row) => row.entityId === entityId)) fail(`Migration entity identity collision: ${entityId}`);
@@ -1998,7 +2062,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
1998
2062
  relations = [...imported.rows.relations];
1999
2063
  contentByPartId.set(item.part_id, imported.contentEntityId);
2000
2064
  const phonemeScript = speech.phoneme_script;
2001
- ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link);
2065
+ ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link, documentAudioScriptEntityId);
2002
2066
  } else if (part.bgm !== void 0) {
2003
2067
  const bgm = part.bgm;
2004
2068
  const fact = facts.get(bgm.origin_media_id);
@@ -2076,7 +2140,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2076
2140
  const voice = contentByPartId.get(caption.speech_part_id);
2077
2141
  const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voice);
2078
2142
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2079
- const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities, relations);
2143
+ const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2080
2144
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
2081
2145
  const sourceText = scriptText(script);
2082
2146
  const captionText = caption.text;
@@ -2097,10 +2161,10 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2097
2161
  },
2098
2162
  sampling: "native",
2099
2163
  coordinateSpace: "milliseconds",
2164
+ baseEntityIds: [script.entityId],
2100
2165
  selections,
2101
2166
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2102
2167
  });
2103
- link("caption-composition", content, script.entityId, { segmentAlignment: [] });
2104
2168
  link("caption-alignment", content, voice, { alignment: {
2105
2169
  start: caption.start_ms,
2106
2170
  end: caption.start_ms + duration
@@ -2444,31 +2508,43 @@ function requirePart(document, partId) {
2444
2508
  if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
2445
2509
  return part;
2446
2510
  }
2447
- function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link) {
2511
+ function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
2448
2512
  const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceEntityId);
2449
2513
  if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous PhoneticScript provenance`);
2450
2514
  if (renders.length === 1) {
2451
2515
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
2452
2516
  if (phonetic === void 0) fail(`Voice ${voiceEntityId} has a dangling PhoneticScript render`);
2453
- const script = composedAudioScript(phonetic, entities, relations);
2517
+ const script = composedAudioScript(phonetic, entities);
2454
2518
  if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the legacy Speech script`);
2455
2519
  return phonetic.entityId;
2456
2520
  }
2457
- const script = addEntity("audio-script", { segments: [{
2458
- segmentId: "segment-0",
2459
- text: audioScriptText
2460
- }] });
2461
- const phonetic = addEntity("phonetic-script", phonemeScript === void 0 ? {} : { phonemeScript });
2462
- link("phonetic-script-composition", phonetic, script, { segmentAlignment: [] });
2521
+ const script = entities.find((entity) => entity.entityId === documentAudioScriptEntityId);
2522
+ if (script === void 0 || script.entityKind !== "audio-script") fail("Migration must carry the fixed document AudioScript");
2523
+ const segments = script.payload.segments;
2524
+ if (Array.isArray(segments) && segments.length > 0) {
2525
+ if (scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the project AudioScript text`);
2526
+ } else {
2527
+ const scriptIndex = entities.findIndex((row) => row.entityId === script.entityId);
2528
+ entities[scriptIndex] = {
2529
+ ...script,
2530
+ payload: { segments: [{
2531
+ segmentId: "segment-0",
2532
+ text: audioScriptText
2533
+ }] }
2534
+ };
2535
+ }
2536
+ const phonetic = addEntity("phonetic-script", {
2537
+ baseEntityIds: [script.entityId],
2538
+ ...phonemeScript === void 0 ? {} : { phonemeScript }
2539
+ });
2463
2540
  link("phonetic-script-render", voiceEntityId, phonetic);
2464
2541
  return phonetic;
2465
2542
  }
2466
- function composedAudioScript(phonetic, entities, relations) {
2467
- const edges = relations.filter((relation) => relation.relationKind === "phonetic-script-composition" && (relation.endpoint0EntityId === phonetic.entityId || relation.endpoint1EntityId === phonetic.entityId));
2468
- if (edges.length !== 1) return void 0;
2469
- const edge = edges[0];
2470
- const otherId = edge.endpoint0EntityId === phonetic.entityId ? edge.endpoint1EntityId : edge.endpoint0EntityId;
2471
- return entities.find((entity) => entity.entityId === otherId);
2543
+ function composedAudioScript(phonetic, entities) {
2544
+ return findComposedAudioScript({
2545
+ entities,
2546
+ relations: []
2547
+ }, phonetic.entityId, "phonetic-script");
2472
2548
  }
2473
2549
  function scriptText(script) {
2474
2550
  const segments = script.payload.segments;
@@ -2558,6 +2634,82 @@ function fail(message) {
2558
2634
  throw new EntityTimelineProjectionError(message);
2559
2635
  }
2560
2636
  //#endregion
2637
+ //#region src/entity-editor/caption-content.ts
2638
+ /** Author Caption content from semantic intent; callers never reconcile variant bases or assembled fields. */
2639
+ function prepareCaptionContent(rows, input) {
2640
+ const entityId = createEntityId(input.contentEntityId);
2641
+ const scriptId = createEntityId(input.audioScriptEntityId);
2642
+ if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) throw new Error("Caption duration must be positive");
2643
+ const previous = rows.entities.find((row) => row.entityId === entityId);
2644
+ if (previous !== void 0 && previous.entityKind !== "caption") throw new Error("Caption content identity has a different kind");
2645
+ let baseEntityIds = [scriptId];
2646
+ if (previous !== void 0) {
2647
+ const source = assembleCaptionContent(rows, entityId).audioScript.entityId;
2648
+ const bases = variantBaseEntityIds(previous);
2649
+ if (source !== scriptId && !bases.includes(source)) throw new Error("Replacing an indirectly composed AudioScript requires an explicit composition change");
2650
+ baseEntityIds = bases.map((id) => id === source ? scriptId : id);
2651
+ }
2652
+ const candidate = {
2653
+ entityId,
2654
+ entityKind: "caption",
2655
+ payload: {
2656
+ ...previous?.payload,
2657
+ baseEntityIds,
2658
+ extent: previous?.payload.extent ?? {
2659
+ kind: "bounded",
2660
+ start: 0,
2661
+ end: input.durationMs
2662
+ },
2663
+ sampling: previous?.payload.sampling ?? "native",
2664
+ coordinateSpace: previous?.payload.coordinateSpace ?? "milliseconds"
2665
+ }
2666
+ };
2667
+ const graph = {
2668
+ ...rows,
2669
+ entities: [...rows.entities.filter((row) => row.entityId !== entityId), candidate]
2670
+ };
2671
+ const textSource = {
2672
+ ...assembleEntityContent(graph, entityId),
2673
+ entityKind: "audio-script"
2674
+ };
2675
+ const selected = {
2676
+ ...candidate,
2677
+ payload: {
2678
+ ...candidate.payload,
2679
+ selections: structuredClone(input.selections ?? selectCaptionText(textSource, input.text))
2680
+ }
2681
+ };
2682
+ if (assembleCaptionContent({
2683
+ ...graph,
2684
+ entities: graph.entities.map((row) => row.entityId === entityId ? selected : row)
2685
+ }, entityId).text !== input.text) throw new Error("Caption review text differs from its assembled variant content");
2686
+ return selected;
2687
+ }
2688
+ function selectCaptionText(script, text) {
2689
+ if (!text.trim()) throw new Error("Caption text selection must not be blank");
2690
+ const source = script;
2691
+ const fullText = assembleScriptText(source);
2692
+ const segments = source.payload.segments;
2693
+ if (text === fullText) return segments.map((segment) => ({ segmentId: segment.segmentId }));
2694
+ const matches = [];
2695
+ for (const segment of segments) {
2696
+ let offset = segment.text.indexOf(text);
2697
+ while (offset !== -1) {
2698
+ const start = Array.from(segment.text.slice(0, offset)).length;
2699
+ matches.push({
2700
+ segmentId: segment.segmentId,
2701
+ textRange: {
2702
+ start,
2703
+ end: start + Array.from(text).length
2704
+ }
2705
+ });
2706
+ offset = segment.text.indexOf(text, offset + 1);
2707
+ }
2708
+ }
2709
+ if (matches.length !== 1) throw new Error("Caption text must select a unique AudioScript range; provide explicit selections");
2710
+ return matches;
2711
+ }
2712
+ //#endregion
2561
2713
  //#region src/editor/snapshot-utils.ts
2562
2714
  function isMap(value) {
2563
2715
  return value instanceof Map;
@@ -4696,4 +4848,4 @@ function serverCovers(serverVV, target) {
4696
4848
  return covers(server, target);
4697
4849
  }
4698
4850
  //#endregion
4699
- export { DEFAULT_UNIT_TIME_MS, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, 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, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
4851
+ export { DEFAULT_UNIT_TIME_MS, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, 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, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityRows, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, 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, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ot as VideoDraft, c as MirrorVideoDocumentAdapter } from "./index-PYtGeXNk.js";
1
+ import { c as MirrorVideoDocumentAdapter, kt as VideoDraft } from "./index-Bs60N9Hl.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 { et as VIDEO_DOCUMENT_SCHEMA_VERSION, ot as base64ToBytes, s as createMirrorVideoDocumentAdapter, st as bytesToBase64 } from "./document-Bf200CmD.js";
1
+ import { _t as bytesToBase64, gt as base64ToBytes, s as createMirrorVideoDocumentAdapter, ut as VIDEO_DOCUMENT_SCHEMA_VERSION } from "./document-CkbNx-mi.js";
2
2
  import { t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.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": "1.3.1-alpha.1",
3
+ "version": "1.3.1-alpha.11",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,12 +24,13 @@
24
24
  "registry": "https://registry.npmjs.org/"
25
25
  },
26
26
  "dependencies": {
27
+ "@noble/hashes": "^2.4.0",
27
28
  "immer": "^10.2.0",
28
29
  "loro-mirror": "^2.2.0",
29
30
  "zod": "^4.4.3",
30
- "@mengine/utils": "1.3.1-alpha.1",
31
- "@mengine/storage": "1.3.1-alpha.1",
32
- "@mengine/sync": "1.3.1-alpha.1"
31
+ "@mengine/storage": "1.3.1-alpha.11",
32
+ "@mengine/sync": "1.3.1-alpha.11",
33
+ "@mengine/utils": "1.3.1-alpha.11"
33
34
  },
34
35
  "devDependencies": {
35
36
  "@types/node": "^25.9.1",
@@ -39,8 +40,8 @@
39
40
  "typescript": "^6.0.3",
40
41
  "vite-plugin-wasm": "^3.6.0",
41
42
  "vite-plus": "^0.1.23",
42
- "@mengine/medeo-dsl": "0.0.0",
43
- "@mengine/idl-codegen": "0.0.0"
43
+ "@mengine/idl-codegen": "0.0.0",
44
+ "@mengine/medeo-dsl": "0.0.0"
44
45
  },
45
46
  "peerDependencies": {
46
47
  "loro-crdt": "^1.13.6"