@mengine/medeo-client 1.2.1-alpha.9 → 1.3.1-alpha.0

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 base64ToBytes, A as findLaneTrack, B as syncAggregatedClipsTimePosition, C as VideoDocumentValidationError, D as videoDocumentSchema, E as partUnionSchema, F as fillMainTrackTimeGaps, G as DEFAULT_UNIT_TIME_MS, H as isEmptyVideoClip, I as reassignSpeechesToVideoClipsByTime, J as effectiveVideoClipDurationMs, K as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, L as recalculateTimelineDuration, M as solveVideoDocument, N as cascadeAfterVideoClipChanges, O as LANE_KINDS_IN_STACK_ORDER, P as arrangeMainTrackSeamlessly, Q as videoDocumentMirrorSchema, R as resolveAllSpeechOverlaps, S as toVideoDocument, T as validateVideoDocument, U as partDurationMs, V as TIMELINE_SKELETON_DURATION_MS, W as safeDurationMs, X as partUnionToDraft, Y as speedOf, Z as recordEntries, _ as EntityTimelineProjectionError, a as applyEntityTimelineProjection, b as derivePositionFromAbs, c as writeVideoDocumentToDraft, d as resolveEntityTimelineLayout, et as bytesToBase64, f as decodeEntityRelationRows, g as hasSequence, h as createRelationId, i as MirrorVideoDocumentAdapter, j as laneTrackId, k as ensureLaneTrack, l as readVideoDocumentFromDraft, m as createEntityId, n as createPlainMemoryAdapter, o as createMirrorVideoDocument, p as isJsonObject, q as VIDEO_DOCUMENT_SCHEMA_VERSION, r as generatePartId, s as createMirrorVideoDocumentAdapter, t as PlainMemoryAdapter, u as projectEntityTimeline, v as buildInitialVideoDocument, w as assertValidVideoDocument, x as fromVideoDocument, y as buildSpeechHostMap, z as resolveSpeechOverlapByShiftingVideos } from "./document-Df6awJDH.js";
1
+ import { $ as videoDocumentMirrorSchema, A as ensureLaneTrack, B as resolveSpeechOverlapByShiftingVideos, C as toVideoDocument, D as partUnionSchema, E as validateVideoDocument, F as arrangeMainTrackSeamlessly, G as safeDurationMs, H as TIMELINE_SKELETON_DURATION_MS, I as fillMainTrackTimeGaps, J as VIDEO_DOCUMENT_SCHEMA_VERSION, K as DEFAULT_UNIT_TIME_MS, L as reassignSpeechesToVideoClipsByTime, M as laneTrackId, N as solveVideoDocument, O as videoDocumentSchema, P as cascadeAfterVideoClipChanges, Q as recordEntries, R as recalculateTimelineDuration, S as fromVideoDocument, T as assertValidVideoDocument, U as isEmptyVideoClip, V as syncAggregatedClipsTimePosition, W as partDurationMs, X as speedOf, Y as effectiveVideoClipDurationMs, Z as partUnionToDraft, _ as hasSequence, a as applyEntityTimelineProjection, b as buildSpeechHostMap, c as writeVideoDocumentToDraft, d as resolveEntityTimelineLayout, et as base64ToBytes, f as EntityTimelineProjectionError, g as createRelationId, h as createEntityId, i as MirrorVideoDocumentAdapter, j as findLaneTrack, k as LANE_KINDS_IN_STACK_ORDER, l as readVideoDocumentFromDraft, m as isJsonObject, n as createPlainMemoryAdapter, o as createMirrorVideoDocument, p as decodeEntityRelationRows, q as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, r as generatePartId, s as createMirrorVideoDocumentAdapter, t as PlainMemoryAdapter, tt as bytesToBase64, u as projectEntityTimeline, v as isMediaAssetVariantKind, w as VideoDocumentValidationError, x as derivePositionFromAbs, y as buildInitialVideoDocument, z as resolveAllSpeechOverlaps } from "./document-Caidonf8.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,149 @@ function findSseFrameBoundary(buffer) {
217
217
  return Math.min(lf, crlf);
218
218
  }
219
219
  //#endregion
220
+ //#region src/entity-editor/editor-resources.ts
221
+ const roleOrder$1 = {
222
+ caption: 0,
223
+ video_clip: 1,
224
+ speech: 2,
225
+ bgm: 3
226
+ };
227
+ /**
228
+ * MEngine editor policy, not a cardinality restriction on the general-purpose DSL.
229
+ *
230
+ * Media variants share one entity identity with their Asset: every
231
+ * Image/Video/Audio/Voice Entity must carry its own external locator, every
232
+ * external asset id has exactly one claimant, and no media variant may be
233
+ * bound through a legacy `physical-asset` Relation. Dual-entity media graphs
234
+ * are rejected, not migrated. Caption inline text keeps its inline Asset and
235
+ * `physical-asset` binding.
236
+ */
237
+ function assertCanonicalEditorResources(rows) {
238
+ if (rows.entities.filter((row) => row.entityKind === "timeline").length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one Timeline");
239
+ for (const role of Object.keys(roleOrder$1)) if (rows.entities.filter((row) => row.entityKind === "track" && row.payload.role === role).length > 1) throw new EntityTimelineProjectionError(`Editor has multiple ${role} Tracks`);
240
+ const identityByAssetKey = /* @__PURE__ */ new Map();
241
+ const kinds = new Map(rows.entities.map((row) => [String(row.entityId), row.entityKind]));
242
+ for (const entity of rows.entities) {
243
+ if (isMediaAssetVariantKind(entity.entityKind) && memotaLocator(entity.payload.external) === void 0) throw new EntityTimelineProjectionError(`Media Entity ${entity.entityId} must carry its own Asset external identity`);
244
+ if (entity.entityKind !== "asset" && !isMediaAssetVariantKind(entity.entityKind)) continue;
245
+ const external = entity.payload.external;
246
+ if (!isJsonObject(external) || external.system !== "memota" && external.system !== "memota-speech" || typeof external.key !== "string") continue;
247
+ const key = JSON.stringify([external.system, external.key]);
248
+ const previous = identityByAssetKey.get(key);
249
+ if (previous !== void 0 && previous !== entity.entityId) throw new EntityTimelineProjectionError(`Duplicate Asset for ${key}; reuse ${previous}`);
250
+ identityByAssetKey.set(key, entity.entityId);
251
+ }
252
+ for (const relation of rows.relations) {
253
+ if (relation.relationKind !== "physical-asset") continue;
254
+ const mediaEndpoint = [relation.endpoint0EntityId, relation.endpoint1EntityId].find((endpoint) => isMediaAssetVariantKind(kinds.get(String(endpoint)) ?? ""));
255
+ if (mediaEndpoint !== void 0) throw new EntityTimelineProjectionError(`Cannot bind media Entity ${mediaEndpoint} through physical-asset Relation ${relation.relationId}; media owns its Asset identity directly`);
256
+ }
257
+ }
258
+ /**
259
+ * Commit-transition policy shared by the editor, the HTTP commit boundary, the
260
+ * host tool, and the entity stores. It compares the authoritative before and
261
+ * after rows of one CAS commit and enforces the single-identity write rules
262
+ * on the complete after state, with no exemption for historical graphs:
263
+ *
264
+ * 1. every Image/Video/Audio/Voice Entity carries its own Asset `external`
265
+ * identity;
266
+ * 2. no `physical-asset` Relation may bind a media variant (Caption inline
267
+ * text keeps its inline Asset and binding);
268
+ * 3. a self-carried media identity is immutable — it cannot be rewritten to
269
+ * another asset id.
270
+ *
271
+ * Dual-entity media graphs are rejected instead of being migrated or merged
272
+ * implicitly. Deleting media is explicit and allowed.
273
+ */
274
+ function assertMediaAssetWritePolicy(before, after) {
275
+ const beforeEntities = new Map(before.entities.map((row) => [String(row.entityId), row]));
276
+ const afterKinds = new Map(after.entities.map((row) => [String(row.entityId), row.entityKind]));
277
+ for (const row of after.entities) {
278
+ if (!isMediaAssetVariantKind(row.entityKind)) continue;
279
+ if (memotaLocator(row.payload.external) === void 0) throw new EntityTimelineProjectionError(`Media Entity ${row.entityId} must carry its own Asset external identity`);
280
+ }
281
+ for (const relation of after.relations) {
282
+ if (relation.relationKind !== "physical-asset") continue;
283
+ const mediaEndpoint = [relation.endpoint0EntityId, relation.endpoint1EntityId].find((endpoint) => isMediaAssetVariantKind(afterKinds.get(String(endpoint)) ?? ""));
284
+ if (mediaEndpoint !== void 0) throw new EntityTimelineProjectionError(`Cannot bind media Entity ${mediaEndpoint} through physical-asset Relation ${relation.relationId}; media owns its Asset identity directly`);
285
+ }
286
+ for (const row of after.entities) {
287
+ if (!isMediaAssetVariantKind(row.entityKind)) continue;
288
+ const priorExternal = memotaLocator(beforeEntities.get(String(row.entityId))?.payload.external);
289
+ if (priorExternal === void 0) continue;
290
+ const nextExternal = memotaLocator(row.payload.external);
291
+ if (nextExternal !== void 0 && (nextExternal.system !== priorExternal.system || nextExternal.key !== priorExternal.key)) throw new EntityTimelineProjectionError(`Media Entity ${row.entityId} cannot rewrite its Asset external identity; create a new Entity instead`);
292
+ }
293
+ }
294
+ function memotaLocator(value) {
295
+ if (!isJsonObject(value) || value.system !== "memota" && value.system !== "memota-speech" || typeof value.key !== "string" || value.key.trim() === "") return;
296
+ return {
297
+ system: value.system,
298
+ key: value.key
299
+ };
300
+ }
301
+ /** Add only missing editor structure; existing identities and owned settings are never replaced. */
302
+ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}) {
303
+ const entities = structuredClone([...initial.entities]);
304
+ const relations = structuredClone([...initial.relations]);
305
+ const roots = entities.filter((row) => row.entityKind === "timeline");
306
+ if (roots.length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one Timeline");
307
+ const mintEntity = () => {
308
+ const id = createEntityId(idFactory("entity"));
309
+ if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
310
+ return id;
311
+ };
312
+ let timeline = roots[0];
313
+ if (timeline === void 0) {
314
+ timeline = {
315
+ entityId: mintEntity(),
316
+ entityKind: "timeline",
317
+ payload: structuredClone(timelinePayload)
318
+ };
319
+ entities.push(timeline);
320
+ }
321
+ const trackEntityIds = {};
322
+ for (const role of Object.keys(roleOrder$1)) {
323
+ const matches = entities.filter((row) => row.entityKind === "track" && row.payload.role === role);
324
+ if (matches.length > 1) throw new EntityTimelineProjectionError(`Editor has multiple ${role} Tracks`);
325
+ let track = matches[0];
326
+ if (track === void 0) {
327
+ track = {
328
+ entityId: mintEntity(),
329
+ entityKind: "track",
330
+ payload: {
331
+ role,
332
+ order: roleOrder$1[role]
333
+ }
334
+ };
335
+ entities.push(track);
336
+ }
337
+ const bindings = relations.filter((row) => row.relationKind === "timeline-track" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
338
+ if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
339
+ if (bindings.length === 0) {
340
+ const relationId = createRelationId(idFactory("relation"));
341
+ if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
342
+ relations.push({
343
+ relationId,
344
+ relationKind: "timeline-track",
345
+ endpoint0EntityId: timeline.entityId,
346
+ endpoint1EntityId: track.entityId,
347
+ metadata: {},
348
+ trace: {}
349
+ });
350
+ }
351
+ trackEntityIds[role] = track.entityId;
352
+ }
353
+ return {
354
+ rows: {
355
+ entities,
356
+ relations
357
+ },
358
+ timelineEntityId: timeline.entityId,
359
+ trackEntityIds
360
+ };
361
+ }
362
+ //#endregion
220
363
  //#region src/entity-editor/entity-graph-http-client.ts
221
364
  const MENGINE_API_PREFIX = "/api/mengine/v1";
222
365
  const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left, right) => {
@@ -248,6 +391,7 @@ var EntityGraphHttpClient = class {
248
391
  if (!isNonNegativeSafeInteger(baseState.revision)) throw new Error("Entity graph revision must be a non-negative safe integer");
249
392
  const baseRows = cloneAndValidateRows(baseState.rows);
250
393
  const next = cloneAndValidateRows(nextRows);
394
+ assertMediaAssetWritePolicy(baseRows, next);
251
395
  const nextEntityIds = new Set(next.entities.map((row) => row.entityId));
252
396
  const nextRelationIds = new Set(next.relations.map((row) => row.relationId));
253
397
  const deletedEntityIds = baseRows.entities.map((row) => row.entityId).filter((entityId) => !nextEntityIds.has(entityId)).sort();
@@ -356,8 +500,18 @@ const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right)
356
500
  if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
357
501
  return left - right;
358
502
  } };
359
- /** Import at the editor boundary, not at asset generation time. Existing provenance is retained. */
360
- function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, selection = {}) {
503
+ /**
504
+ * Import at the editor boundary, not at asset generation time. Existing
505
+ * provenance is retained.
506
+ *
507
+ * A media variant is a logical facet of its Asset and shares one entity
508
+ * identity: the import mints exactly one entity of the media kind that carries
509
+ * the external locator itself, and deduplicates only against existing variants
510
+ * that already carry the same `(system, key)`. Legacy dual-entity graphs
511
+ * (`asset` row plus a `physical-asset` binding) are not reused and are
512
+ * rejected by the canonical resource policy instead of being migrated.
513
+ */
514
+ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
361
515
  if (!fact.assetId || fact.assetId.trim() !== fact.assetId) throw new Error("Expected an asset id");
362
516
  if (![
363
517
  "image",
@@ -368,59 +522,32 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
368
522
  if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
369
523
  if ((fact.kind === "audio" || fact.kind === "voice") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
370
524
  decodeEntityRelationRows(initial, numericCoordinates);
525
+ assertCanonicalEditorResources(initial);
371
526
  const entities = structuredClone([...initial.entities]);
372
527
  const relations = structuredClone([...initial.relations]);
373
- const assets = entities.filter((row) => {
528
+ const system = fact.kind === "voice" ? "memota-speech" : "memota";
529
+ const existing = entities.filter((row) => {
374
530
  const external = row.payload.external;
375
- return row.entityKind === "asset" && isJsonObject(external) && external.system === (fact.kind === "voice" ? "memota-speech" : "memota") && external.key === fact.assetId && (selection.assetEntityId === void 0 || row.entityId === selection.assetEntityId);
531
+ return isMediaAssetVariantKind(row.entityKind) && isJsonObject(external) && external.system === system && external.key === fact.assetId;
376
532
  });
377
- if (selection.assetEntityId !== void 0 && assets.length !== 1) throw new Error("Selected Asset does not match the recalled asset facts");
378
- if (assets.length > 1) throw new Error(`Ambiguous Asset bindings for ${fact.assetId}; select assetEntityId explicitly`);
379
- const mintEntity = () => {
380
- const id = createEntityId(idFactory("entity"));
381
- if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
382
- return id;
383
- };
384
- let asset = assets[0];
385
- if (asset === void 0) {
386
- asset = {
387
- entityId: mintEntity(),
388
- entityKind: "asset",
389
- payload: {
390
- external: {
391
- system: fact.kind === "voice" ? "memota-speech" : "memota",
392
- key: fact.assetId
393
- },
394
- ...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey }
395
- }
533
+ if (existing.length > 1) throw new Error(`Duplicate media identities for ${fact.assetId}; reuse ${existing[0].entityId}`);
534
+ if (existing[0] !== void 0) {
535
+ const variant = requireMatchingVariant(existing[0], fact);
536
+ replaceEntity$1(entities, variant.entityId, variant);
537
+ return {
538
+ rows: {
539
+ entities,
540
+ relations
541
+ },
542
+ contentEntityId: variant.entityId
396
543
  };
397
- entities.push(asset);
398
- } else if (fact.storageKey !== void 0) {
399
- if (asset.payload.storageKey !== void 0 && asset.payload.storageKey !== fact.storageKey) throw new Error(`Media facts conflict with existing storage key for ${fact.assetId}`);
400
- if (asset.payload.storageKey === void 0) {
401
- asset = {
402
- ...asset,
403
- payload: {
404
- ...asset.payload,
405
- storageKey: fact.storageKey
406
- }
407
- };
408
- const assetIndex = entities.findIndex((row) => row.entityId === asset.entityId);
409
- entities[assetIndex] = asset;
410
- }
411
544
  }
412
- const bindings = relations.filter((row) => row.relationKind === "physical-asset" && (row.endpoint0EntityId === asset.entityId || row.endpoint1EntityId === asset.entityId) && (selection.contentEntityId === void 0 || (row.endpoint0EntityId === asset.entityId ? row.endpoint1EntityId : row.endpoint0EntityId) === selection.contentEntityId));
413
- const contentIds = new Set(bindings.map((row) => row.endpoint0EntityId === asset.entityId ? row.endpoint1EntityId : row.endpoint0EntityId));
414
- if (selection.contentEntityId !== void 0 && contentIds.size !== 1) throw new Error("Selected media Entity is not bound to the selected Asset");
415
- if (contentIds.size > 1) throw new Error(`Ambiguous media bindings for ${fact.assetId}; select contentEntityId explicitly`);
416
- let content;
417
- if (bindings[0] !== void 0) {
418
- const relation = bindings[0];
419
- content = entities.find((row) => row.entityId === (relation.endpoint0EntityId === asset.entityId ? relation.endpoint1EntityId : relation.endpoint0EntityId));
420
- const extent = content?.payload.extent;
421
- if (content?.entityKind !== fact.kind || fact.kind !== "image" && (!isJsonObject(extent) || extent.start !== 0 || extent.end !== fact.durationMs) || fact.kind === "voice" && !sameVoice(content.payload.voice, fact.voice)) throw new Error(`Media facts conflict with existing binding for ${fact.assetId}`);
422
- } else {
423
- const payload = {
545
+ const entityId = createEntityId(idFactory("entity"));
546
+ if (entities.some((row) => row.entityId === entityId)) throw new Error(`Duplicate entity id ${entityId}`);
547
+ const variant = {
548
+ entityId,
549
+ entityKind: fact.kind,
550
+ payload: {
424
551
  extent: fact.kind === "image" ? {
425
552
  kind: "unbounded",
426
553
  start: 0
@@ -431,43 +558,52 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
431
558
  },
432
559
  sampling: fact.kind === "image" ? "constant" : "native",
433
560
  coordinateSpace: "milliseconds",
434
- ...fact.kind === "voice" ? { voice: {
561
+ external: {
562
+ system,
563
+ key: fact.assetId
564
+ },
565
+ ...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey },
566
+ ...fact.kind === "voice" && fact.voice !== void 0 ? { voice: {
435
567
  system: fact.voice.system,
436
568
  key: fact.voice.key,
437
569
  ...fact.voice.name === void 0 ? {} : { name: fact.voice.name }
438
570
  } } : {}
439
- };
440
- const createdContent = {
441
- entityId: mintEntity(),
442
- entityKind: fact.kind,
443
- payload
444
- };
445
- content = createdContent;
446
- entities.push(createdContent);
447
- const relationId = createRelationId(idFactory("relation"));
448
- if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
449
- relations.push({
450
- relationId,
451
- relationKind: "physical-asset",
452
- endpoint0EntityId: createdContent.entityId,
453
- endpoint1EntityId: asset.entityId,
454
- metadata: {},
455
- trace: {}
456
- });
457
- }
571
+ }
572
+ };
573
+ entities.push(variant);
458
574
  const rows = {
459
575
  entities,
460
576
  relations
461
577
  };
462
578
  decodeEntityRelationRows(rows, numericCoordinates);
463
- if (content === void 0) throw new Error(`Media Entity could not be resolved for ${fact.assetId}`);
579
+ assertCanonicalEditorResources(rows);
464
580
  return {
465
581
  rows,
466
- assetEntityId: asset.entityId,
467
- contentEntityId: content.entityId
582
+ contentEntityId: entityId
468
583
  };
469
584
  }
585
+ function requireMatchingVariant(row, fact) {
586
+ const extent = row.payload.extent;
587
+ if (row.entityKind !== fact.kind || fact.kind !== "image" && (!isJsonObject(extent) || extent.start !== 0 || extent.end !== fact.durationMs) || fact.kind === "voice" && !sameVoice(row.payload.voice, fact.voice)) throw new Error(`Media facts conflict with existing identity for ${fact.assetId}`);
588
+ if (fact.storageKey !== void 0 && row.payload.storageKey !== fact.storageKey) {
589
+ if (row.payload.storageKey !== void 0) throw new Error(`Media facts conflict with existing storage key for ${fact.assetId}`);
590
+ return {
591
+ ...row,
592
+ payload: {
593
+ ...row.payload,
594
+ storageKey: fact.storageKey
595
+ }
596
+ };
597
+ }
598
+ return row;
599
+ }
600
+ function replaceEntity$1(entities, entityId, replacement) {
601
+ const index = entities.findIndex((row) => row.entityId === entityId);
602
+ if (index < 0) throw new Error(`Entity id ${entityId} does not exist`);
603
+ entities[index] = replacement;
604
+ }
470
605
  function sameVoice(left, right) {
606
+ if (right === void 0) return left === void 0;
471
607
  return isJsonObject(left) && left.system === right.system && left.key === right.key && left.name === right.name && Object.keys(left).every((key) => key === "system" || key === "key" || key === "name");
472
608
  }
473
609
  //#endregion
@@ -983,9 +1119,13 @@ var EntityTimelineEditor = class {
983
1119
  upsertVoiceoverTake(input) {
984
1120
  return this.transact((draft) => {
985
1121
  this.normalizeMainSequential(draft);
986
- finiteNonNegative(input.anchorOffset, "Voiceover anchorOffset");
1122
+ const placement = input.placement ?? {
1123
+ kind: "anchored",
1124
+ hostClipEntityId: input.hostClipEntityId,
1125
+ anchorOffset: input.anchorOffset
1126
+ };
1127
+ if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify Voice placement or hostClipEntityId/anchorOffset, not both");
987
1128
  requireVolume(input.volume);
988
- requireClipOnRole(draft, input.hostClipEntityId, "video_clip");
989
1129
  const imported = importMediaAsset(draft, input.media, this.idFactory);
990
1130
  replaceRows(draft, imported.rows);
991
1131
  const voice = requireEntityKind(draft, imported.contentEntityId, "voice");
@@ -1005,11 +1145,7 @@ var EntityTimelineEditor = class {
1005
1145
  mode: "fixed",
1006
1146
  value: input.media.durationMs
1007
1147
  },
1008
- placement: {
1009
- kind: "anchored",
1010
- hostClipEntityId: input.hostClipEntityId,
1011
- anchorOffset: input.anchorOffset
1012
- },
1148
+ placement,
1013
1149
  clipPayload: { volume: input.volume }
1014
1150
  });
1015
1151
  else {
@@ -1026,11 +1162,7 @@ var EntityTimelineEditor = class {
1026
1162
  endpoint0EntityId: topology.marker.entityId,
1027
1163
  endpoint1EntityId: voice.entityId
1028
1164
  });
1029
- this.applyClipPlacement(draft, topology.clip.entityId, {
1030
- kind: "anchored",
1031
- hostClipEntityId: input.hostClipEntityId,
1032
- anchorOffset: input.anchorOffset
1033
- });
1165
+ this.applyClipPlacement(draft, topology.clip.entityId, placement);
1034
1166
  const placed = requireClipTopology(draft, topology.clip.entityId);
1035
1167
  replaceEntity(draft, placed.marker.entityId, {
1036
1168
  ...placed.marker,
@@ -1154,6 +1286,57 @@ var EntityTimelineEditor = class {
1154
1286
  }
1155
1287
  });
1156
1288
  }
1289
+ /**
1290
+ * Place an independent Caption: its own content entity, inline text Asset,
1291
+ * and Clip with a display Marker. No Voice, provenance, or alignment is
1292
+ * required — the placement describes only this use. Moving or stretching
1293
+ * the returned Clip later changes the Clip/Marker, never the Caption entity.
1294
+ */
1295
+ insertCaptionClip(input) {
1296
+ return this.transact((draft) => {
1297
+ const durationMs = finitePositive(input.durationMs, "Caption durationMs");
1298
+ const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
1299
+ const content = {
1300
+ entityId: this.mintEntityId(draft),
1301
+ entityKind: "caption",
1302
+ payload: {
1303
+ extent: {
1304
+ kind: "bounded",
1305
+ start: 0,
1306
+ end: durationMs
1307
+ },
1308
+ sampling: "native",
1309
+ coordinateSpace: "milliseconds",
1310
+ text: input.text,
1311
+ ...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
1312
+ }
1313
+ };
1314
+ const asset = {
1315
+ entityId: this.mintEntityId(draft),
1316
+ entityKind: "asset",
1317
+ payload: { inline: {
1318
+ mediaType: "text/plain",
1319
+ text: input.text
1320
+ } }
1321
+ };
1322
+ draft.entities.push(content, asset);
1323
+ draft.relations.push(this.createRelation(draft, "physical-asset", content.entityId, asset.entityId));
1324
+ return this.insertPlacedClipIntoDraft(draft, {
1325
+ trackEntityId: track.entityId,
1326
+ contentEntityId: content.entityId,
1327
+ sourceRange: {
1328
+ start: 0,
1329
+ end: durationMs
1330
+ },
1331
+ duration: {
1332
+ mode: "fixed",
1333
+ value: durationMs
1334
+ },
1335
+ placement: input.placement,
1336
+ ...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
1337
+ });
1338
+ });
1339
+ }
1157
1340
  insertPlacedClipIntoDraft(draft, input) {
1158
1341
  const track = requireEntityKind(draft, input.trackEntityId, "track");
1159
1342
  requireTrackTimeline(draft, track.entityId);
@@ -1518,6 +1701,7 @@ var EntityTimelineEditor = class {
1518
1701
  const draft = cloneRows(this.state);
1519
1702
  const result = edit(draft);
1520
1703
  validateFinalRows(draft);
1704
+ assertMediaAssetWritePolicy(this.state, draft);
1521
1705
  this.state = draft;
1522
1706
  return result;
1523
1707
  }
@@ -1554,6 +1738,7 @@ var EntityTimelineEditor = class {
1554
1738
  };
1555
1739
  function validateFinalRows(rows) {
1556
1740
  decodeEntityRelationRows(rows, numericMarkerValidation);
1741
+ assertCanonicalEditorResources(rows);
1557
1742
  validateTimelineHierarchy(rows);
1558
1743
  }
1559
1744
  function validateTimelineHierarchy(rows) {
@@ -4519,4 +4704,4 @@ function serverCovers(serverVV, target) {
4519
4704
  return covers(server, target);
4520
4705
  }
4521
4706
  //#endregion
4522
- 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, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assertEntityProjectionPreservesLegacy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureLaneTrack, fillMainTrackTimeGaps, 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, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
4707
+ 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, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, 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, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Tt as VideoDraft, c as MirrorVideoDocumentAdapter } from "./index-CbuWTReu.js";
1
+ import { Et as VideoDraft, c as MirrorVideoDocumentAdapter } from "./index-WxMNUbOa.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 { $ as base64ToBytes, et as bytesToBase64, q as VIDEO_DOCUMENT_SCHEMA_VERSION, s as createMirrorVideoDocumentAdapter } from "./document-Df6awJDH.js";
1
+ import { J as VIDEO_DOCUMENT_SCHEMA_VERSION, et as base64ToBytes, s as createMirrorVideoDocumentAdapter, tt as bytesToBase64 } from "./document-Caidonf8.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.2.1-alpha.9",
3
+ "version": "1.3.1-alpha.0",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,9 @@
27
27
  "immer": "^10.2.0",
28
28
  "loro-mirror": "^2.2.0",
29
29
  "zod": "^4.4.3",
30
- "@mengine/storage": "1.2.1-alpha.9",
31
- "@mengine/sync": "1.2.1-alpha.9",
32
- "@mengine/utils": "1.2.1-alpha.9"
30
+ "@mengine/storage": "1.3.1-alpha.0",
31
+ "@mengine/utils": "1.3.1-alpha.0",
32
+ "@mengine/sync": "1.3.1-alpha.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^25.9.1",