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

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-BxWxPFTj.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,26 @@ function memotaLocator(value) {
296
357
  key: value.key
297
358
  };
298
359
  }
360
+ /**
361
+ * The project AudioScript is a fixed structure identity, like the Timeline: at
362
+ * most one may exist, and a missing one is minted atomically with empty
363
+ * segments. Project initialization and legacy migration share this single
364
+ * path, so both produce the same canonical structure.
365
+ */
366
+ function ensureDocumentAudioScript(entities, idFactory) {
367
+ const scripts = entities.filter((row) => row.entityKind === "audio-script");
368
+ if (scripts.length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one AudioScript");
369
+ const existing = scripts[0];
370
+ if (existing !== void 0) return existing.entityId;
371
+ const id = createEntityId(idFactory("entity"));
372
+ if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
373
+ entities.push({
374
+ entityId: id,
375
+ entityKind: "audio-script",
376
+ payload: { segments: [] }
377
+ });
378
+ return id;
379
+ }
299
380
  /** Add only missing editor structure; existing identities and owned settings are never replaced. */
300
381
  function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}) {
301
382
  const entities = structuredClone([...initial.entities]);
@@ -348,13 +429,15 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
348
429
  }
349
430
  trackEntityIds[role] = track.entityId;
350
431
  }
432
+ const audioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
351
433
  return {
352
434
  rows: {
353
435
  entities,
354
436
  relations
355
437
  },
356
438
  timelineEntityId: timeline.entityId,
357
- trackEntityIds
439
+ trackEntityIds,
440
+ audioScriptEntityId
358
441
  };
359
442
  }
360
443
  //#endregion
@@ -364,7 +447,7 @@ const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left,
364
447
  if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
365
448
  return left < right ? -1 : left > right ? 1 : 0;
366
449
  } };
367
- /** Browser-safe authenticated CAS client for a document's Entity graph. */
450
+ /** Browser-safe authenticated Loro client for a document's Entity graph. */
368
451
  var EntityGraphHttpClient = class {
369
452
  options;
370
453
  transport;
@@ -379,30 +462,24 @@ var EntityGraphHttpClient = class {
379
462
  return this.request({ method: "GET" });
380
463
  }
381
464
  /**
382
- * Replace the complete Entity graph under the baseline revision.
465
+ * Compile edits against the original causal snapshot and publish native Loro operations.
383
466
  *
384
467
  * Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
385
468
  * transient create-then-remove never becomes a request to delete unknown
386
469
  * persisted data.
387
470
  */
388
471
  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);
472
+ if (options.migrationBaseVv !== void 0) throw new Error("Legacy entity migration is not supported by the Loro contract");
391
473
  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();
474
+ assertMediaAssetWritePolicy(baseState.rows, next);
475
+ const compiled = compileEntityRows(base64ToBytes(baseState.loroSnapshot), next);
476
+ return this.commitUpdate(bytesToBase64(compiled.update));
477
+ }
478
+ /** Send the same prepared bytes on retry; never recompile an unconfirmed edit. */
479
+ async commitUpdate(update) {
397
480
  return this.request({
398
481
  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
- })
482
+ body: JSON.stringify({ update })
406
483
  });
407
484
  }
408
485
  async request(init) {
@@ -420,10 +497,16 @@ function parseWireState(value, expectedDocId) {
420
497
  if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
421
498
  const rows = parseWireRows(value.rows);
422
499
  decodeEntityRelationRows(rows, numericMarkerValidation$1);
423
- return {
500
+ if (typeof value.audio_script_entity_id !== "string") throw new Error("Document AudioScript is not initialized");
501
+ if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
502
+ const state = {
424
503
  revision: value.revision,
425
- rows
504
+ loroSnapshot: value.loro_snapshot,
505
+ rows,
506
+ audioScriptEntityId: value.audio_script_entity_id
426
507
  };
508
+ readDocumentAudioScript(state);
509
+ return state;
427
510
  }
428
511
  function parseWireRows(value) {
429
512
  if (!isRecord(value) || !Array.isArray(value.entities) || !Array.isArray(value.relations)) throw new Error("Invalid entity-state response rows");
@@ -456,23 +539,6 @@ function cloneAndValidateRows(rows) {
456
539
  decodeEntityRelationRows(clone, numericMarkerValidation$1);
457
540
  return clone;
458
541
  }
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
542
  function requiredString(value, field) {
477
543
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`Invalid ${field} in entity-state response`);
478
544
  return value;
@@ -616,8 +682,12 @@ function requireComposedAudioScript(rows, phoneticScriptEntityId) {
616
682
  function captionSelections(selections) {
617
683
  return structuredClone(selections);
618
684
  }
619
- function assertSelectionNamesScriptSegments(rows, audioScriptEntityId, selections) {
620
- selectAudioScriptSegments(requireEntityKind(rows, audioScriptEntityId, "audio-script"), selections);
685
+ function captionBaseIds(ids) {
686
+ if (!Array.isArray(ids) || ids.length === 0) throw new Error("Caption requires non-empty baseEntityIds");
687
+ return ids.map(createEntityId);
688
+ }
689
+ function assertCaptionScript(rows, captionId, scriptId) {
690
+ if (assembleCaptionContent(rows, captionId).audioScript.entityId !== scriptId) throw new Error("Voiceover Caption must compose the same AudioScript as the PhoneticScript");
621
691
  }
622
692
  const roleOrder = {
623
693
  caption: 0,
@@ -1281,28 +1351,23 @@ var EntityTimelineEditor = class {
1281
1351
  const track = findRoleTrack(draft, input.timelineEntityId, "caption");
1282
1352
  if (track === void 0) return;
1283
1353
  const patch = cloneJsonObject(input.style, "Caption style");
1354
+ const assembled = new EntityProjectionGraph(draft);
1284
1355
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1285
1356
  const topology = requireClipTopology(draft, clip.entityId);
1286
1357
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1287
- const prior = caption.payload.style;
1358
+ const prior = assembled.entity(caption.entityId).payload.style;
1288
1359
  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
- });
1360
+ for (const updated of updateEntityFields(draft, caption.entityId, { style: {
1361
+ ...style,
1362
+ ...patch
1363
+ } })) replaceEntity(draft, updated.entityId, updated);
1299
1364
  }
1300
1365
  });
1301
1366
  }
1302
1367
  /**
1303
1368
  * 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;
1369
+ * entity stores baseEntityIds, selection, optional style, and inherent cue-bounded
1370
+ * Sequence. This helper does not copy base text. The Clip and its display Marker describe this use;
1306
1371
  * moving or stretching the returned Clip later changes the Clip/Marker,
1307
1372
  * never the Caption entity or the AudioScript.
1308
1373
  */
@@ -1310,8 +1375,6 @@ var EntityTimelineEditor = class {
1310
1375
  return this.transact((draft) => {
1311
1376
  const durationMs = finitePositive(input.durationMs, "Caption durationMs");
1312
1377
  const selections = captionSelections(input.selections);
1313
- const script = requireEntityKind(draft, input.audioScriptEntityId, "audio-script");
1314
- assertSelectionNamesScriptSegments(draft, script.entityId, selections);
1315
1378
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
1316
1379
  const content = {
1317
1380
  entityId: this.mintEntityId(draft),
@@ -1324,12 +1387,13 @@ var EntityTimelineEditor = class {
1324
1387
  },
1325
1388
  sampling: "native",
1326
1389
  coordinateSpace: "milliseconds",
1390
+ baseEntityIds: captionBaseIds(input.baseEntityIds),
1327
1391
  selections,
1328
1392
  ...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
1329
1393
  }
1330
1394
  };
1331
1395
  draft.entities.push(content);
1332
- draft.relations.push(this.createRelation(draft, "caption-composition", content.entityId, script.entityId, { segmentAlignment: [] }));
1396
+ assembleCaptionContent(draft, content.entityId);
1333
1397
  return this.insertPlacedClipIntoDraft(draft, {
1334
1398
  trackEntityId: track.entityId,
1335
1399
  contentEntityId: content.entityId,
@@ -1434,7 +1498,7 @@ var EntityTimelineEditor = class {
1434
1498
  const startMs = finiteNonNegative(caption.startMs, "Caption startMs");
1435
1499
  const durationMs = finitePositive(caption.durationMs, "Caption durationMs");
1436
1500
  const selections = captionSelections(caption.selections);
1437
- assertSelectionNamesScriptSegments(draft, audioScriptEntityId, selections);
1501
+ const baseEntityIds = captionBaseIds(caption.baseEntityIds);
1438
1502
  if (draft.entities.find((entity) => entity.entityId === caption.captionClipEntityId) === void 0) {
1439
1503
  const content = {
1440
1504
  entityId: this.mintEntityId(draft),
@@ -1447,12 +1511,14 @@ var EntityTimelineEditor = class {
1447
1511
  },
1448
1512
  sampling: "native",
1449
1513
  coordinateSpace: "milliseconds",
1514
+ baseEntityIds,
1450
1515
  selections,
1451
1516
  ...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
1452
1517
  }
1453
1518
  };
1454
1519
  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: {
1520
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1521
+ draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
1456
1522
  start: startMs,
1457
1523
  end: startMs + durationMs
1458
1524
  } }));
@@ -1487,10 +1553,12 @@ var EntityTimelineEditor = class {
1487
1553
  ...content,
1488
1554
  payload: {
1489
1555
  ...content.payload,
1556
+ baseEntityIds,
1490
1557
  selections,
1491
1558
  ...Object.keys(style).length === 0 ? {} : { style }
1492
1559
  }
1493
1560
  });
1561
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1494
1562
  this.applyClipPlacement(draft, topology.clip.entityId, {
1495
1563
  kind: "anchored",
1496
1564
  hostClipEntityId: voiceoverClipEntityId,
@@ -1507,7 +1575,7 @@ var EntityTimelineEditor = class {
1507
1575
  }
1508
1576
  }
1509
1577
  });
1510
- this.replaceCaptionSemanticRelations(draft, content.entityId, voiceEntityId, audioScriptEntityId, startMs, durationMs);
1578
+ this.replaceCaptionAlignment(draft, content.entityId, voiceEntityId, startMs, durationMs);
1511
1579
  }
1512
1580
  }
1513
1581
  ensureRoleTrackForClip(draft, clipEntityId, role) {
@@ -1516,13 +1584,13 @@ var EntityTimelineEditor = class {
1516
1584
  const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
1517
1585
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1518
1586
  }
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));
1587
+ replaceCaptionAlignment(draft, captionEntityId, voiceEntityId, startMs, durationMs) {
1588
+ const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
1521
1589
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1522
1590
  draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceEntityId, { alignment: {
1523
1591
  start: startMs,
1524
1592
  end: startMs + durationMs
1525
- } }), this.createRelation(draft, "caption-composition", captionEntityId, scriptEntityId, { segmentAlignment: [] }));
1593
+ } }));
1526
1594
  }
1527
1595
  /**
1528
1596
  * Remove one placed Caption use. The composed AudioScript outlives the
@@ -1532,11 +1600,7 @@ var EntityTimelineEditor = class {
1532
1600
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1533
1601
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1534
1602
  const contentRelations = incidentRelations(draft, caption.entityId);
1535
- const allowed = new Set([
1536
- "caption-composition",
1537
- "caption-alignment",
1538
- "marker-content"
1539
- ]);
1603
+ const allowed = new Set(["caption-alignment", "marker-content"]);
1540
1604
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1541
1605
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1542
1606
  this.removeClipGraph(draft, clipEntityId);
@@ -1936,6 +2000,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
1936
2000
  const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
1937
2001
  let entities = structuredClone([...initial.entities]);
1938
2002
  let relations = structuredClone([...initial.relations]);
2003
+ const documentAudioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
1939
2004
  const addEntity = (kind, payload, existingId) => {
1940
2005
  const entityId = createEntityId(existingId ?? idFactory("entity"));
1941
2006
  if (entities.some((row) => row.entityId === entityId)) fail(`Migration entity identity collision: ${entityId}`);
@@ -1998,7 +2063,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
1998
2063
  relations = [...imported.rows.relations];
1999
2064
  contentByPartId.set(item.part_id, imported.contentEntityId);
2000
2065
  const phonemeScript = speech.phoneme_script;
2001
- ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link);
2066
+ ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link, documentAudioScriptEntityId);
2002
2067
  } else if (part.bgm !== void 0) {
2003
2068
  const bgm = part.bgm;
2004
2069
  const fact = facts.get(bgm.origin_media_id);
@@ -2076,7 +2141,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2076
2141
  const voice = contentByPartId.get(caption.speech_part_id);
2077
2142
  const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voice);
2078
2143
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2079
- const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities, relations);
2144
+ const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2080
2145
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
2081
2146
  const sourceText = scriptText(script);
2082
2147
  const captionText = caption.text;
@@ -2097,10 +2162,10 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2097
2162
  },
2098
2163
  sampling: "native",
2099
2164
  coordinateSpace: "milliseconds",
2165
+ baseEntityIds: [script.entityId],
2100
2166
  selections,
2101
2167
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2102
2168
  });
2103
- link("caption-composition", content, script.entityId, { segmentAlignment: [] });
2104
2169
  link("caption-alignment", content, voice, { alignment: {
2105
2170
  start: caption.start_ms,
2106
2171
  end: caption.start_ms + duration
@@ -2444,31 +2509,43 @@ function requirePart(document, partId) {
2444
2509
  if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
2445
2510
  return part;
2446
2511
  }
2447
- function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link) {
2512
+ function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
2448
2513
  const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceEntityId);
2449
2514
  if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous PhoneticScript provenance`);
2450
2515
  if (renders.length === 1) {
2451
2516
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
2452
2517
  if (phonetic === void 0) fail(`Voice ${voiceEntityId} has a dangling PhoneticScript render`);
2453
- const script = composedAudioScript(phonetic, entities, relations);
2518
+ const script = composedAudioScript(phonetic, entities);
2454
2519
  if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the legacy Speech script`);
2455
2520
  return phonetic.entityId;
2456
2521
  }
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: [] });
2522
+ const script = entities.find((entity) => entity.entityId === documentAudioScriptEntityId);
2523
+ if (script === void 0 || script.entityKind !== "audio-script") fail("Migration must carry the fixed document AudioScript");
2524
+ const segments = script.payload.segments;
2525
+ if (Array.isArray(segments) && segments.length > 0) {
2526
+ if (scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the project AudioScript text`);
2527
+ } else {
2528
+ const scriptIndex = entities.findIndex((row) => row.entityId === script.entityId);
2529
+ entities[scriptIndex] = {
2530
+ ...script,
2531
+ payload: { segments: [{
2532
+ segmentId: "segment-0",
2533
+ text: audioScriptText
2534
+ }] }
2535
+ };
2536
+ }
2537
+ const phonetic = addEntity("phonetic-script", {
2538
+ baseEntityIds: [script.entityId],
2539
+ ...phonemeScript === void 0 ? {} : { phonemeScript }
2540
+ });
2463
2541
  link("phonetic-script-render", voiceEntityId, phonetic);
2464
2542
  return phonetic;
2465
2543
  }
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);
2544
+ function composedAudioScript(phonetic, entities) {
2545
+ return findComposedAudioScript({
2546
+ entities,
2547
+ relations: []
2548
+ }, phonetic.entityId, "phonetic-script");
2472
2549
  }
2473
2550
  function scriptText(script) {
2474
2551
  const segments = script.payload.segments;
@@ -2558,6 +2635,82 @@ function fail(message) {
2558
2635
  throw new EntityTimelineProjectionError(message);
2559
2636
  }
2560
2637
  //#endregion
2638
+ //#region src/entity-editor/caption-content.ts
2639
+ /** Author Caption content from semantic intent; callers never reconcile variant bases or assembled fields. */
2640
+ function prepareCaptionContent(rows, input) {
2641
+ const entityId = createEntityId(input.contentEntityId);
2642
+ const scriptId = createEntityId(input.audioScriptEntityId);
2643
+ if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) throw new Error("Caption duration must be positive");
2644
+ const previous = rows.entities.find((row) => row.entityId === entityId);
2645
+ if (previous !== void 0 && previous.entityKind !== "caption") throw new Error("Caption content identity has a different kind");
2646
+ let baseEntityIds = [scriptId];
2647
+ if (previous !== void 0) {
2648
+ const source = assembleCaptionContent(rows, entityId).audioScript.entityId;
2649
+ const bases = variantBaseEntityIds(previous);
2650
+ if (source !== scriptId && !bases.includes(source)) throw new Error("Replacing an indirectly composed AudioScript requires an explicit composition change");
2651
+ baseEntityIds = bases.map((id) => id === source ? scriptId : id);
2652
+ }
2653
+ const candidate = {
2654
+ entityId,
2655
+ entityKind: "caption",
2656
+ payload: {
2657
+ ...previous?.payload,
2658
+ baseEntityIds,
2659
+ extent: previous?.payload.extent ?? {
2660
+ kind: "bounded",
2661
+ start: 0,
2662
+ end: input.durationMs
2663
+ },
2664
+ sampling: previous?.payload.sampling ?? "native",
2665
+ coordinateSpace: previous?.payload.coordinateSpace ?? "milliseconds"
2666
+ }
2667
+ };
2668
+ const graph = {
2669
+ ...rows,
2670
+ entities: [...rows.entities.filter((row) => row.entityId !== entityId), candidate]
2671
+ };
2672
+ const textSource = {
2673
+ ...assembleEntityContent(graph, entityId),
2674
+ entityKind: "audio-script"
2675
+ };
2676
+ const selected = {
2677
+ ...candidate,
2678
+ payload: {
2679
+ ...candidate.payload,
2680
+ selections: structuredClone(input.selections ?? selectCaptionText(textSource, input.text))
2681
+ }
2682
+ };
2683
+ if (assembleCaptionContent({
2684
+ ...graph,
2685
+ entities: graph.entities.map((row) => row.entityId === entityId ? selected : row)
2686
+ }, entityId).text !== input.text) throw new Error("Caption review text differs from its assembled variant content");
2687
+ return selected;
2688
+ }
2689
+ function selectCaptionText(script, text) {
2690
+ if (!text.trim()) throw new Error("Caption text selection must not be blank");
2691
+ const source = script;
2692
+ const fullText = assembleScriptText(source);
2693
+ const segments = source.payload.segments;
2694
+ if (text === fullText) return segments.map((segment) => ({ segmentId: segment.segmentId }));
2695
+ const matches = [];
2696
+ for (const segment of segments) {
2697
+ let offset = segment.text.indexOf(text);
2698
+ while (offset !== -1) {
2699
+ const start = Array.from(segment.text.slice(0, offset)).length;
2700
+ matches.push({
2701
+ segmentId: segment.segmentId,
2702
+ textRange: {
2703
+ start,
2704
+ end: start + Array.from(text).length
2705
+ }
2706
+ });
2707
+ offset = segment.text.indexOf(text, offset + 1);
2708
+ }
2709
+ }
2710
+ if (matches.length !== 1) throw new Error("Caption text must select a unique AudioScript range; provide explicit selections");
2711
+ return matches;
2712
+ }
2713
+ //#endregion
2561
2714
  //#region src/editor/snapshot-utils.ts
2562
2715
  function isMap(value) {
2563
2716
  return value instanceof Map;
@@ -4696,4 +4849,4 @@ function serverCovers(serverVV, target) {
4696
4849
  return covers(server, target);
4697
4850
  }
4698
4851
  //#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 };
4852
+ 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-BxWxPFTj.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.10",
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.10",
32
+ "@mengine/sync": "1.3.1-alpha.10",
33
+ "@mengine/utils": "1.3.1-alpha.10"
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"