@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/README.md +43 -11
- package/dist/{document-Df6awJDH.js → document-Caidonf8.js} +214 -66
- package/dist/{index-CbuWTReu.d.ts → index-WxMNUbOa.d.ts} +9 -5
- package/dist/index.d.ts +90 -19
- package/dist/index.js +271 -86
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
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
|
-
/**
|
|
360
|
-
|
|
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
|
|
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
|
|
531
|
+
return isMediaAssetVariantKind(row.entityKind) && isJsonObject(external) && external.system === system && external.key === fact.assetId;
|
|
376
532
|
});
|
|
377
|
-
if (
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
579
|
+
assertCanonicalEditorResources(rows);
|
|
464
580
|
return {
|
|
465
581
|
rows,
|
|
466
|
-
|
|
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
|
-
|
|
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
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
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.
|
|
31
|
-
"@mengine/
|
|
32
|
-
"@mengine/
|
|
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",
|