@mengine/medeo-client 1.3.1-alpha.0 → 1.3.1-alpha.2
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 +26 -11
- package/dist/{document-Caidonf8.js → document-BLYcbv8E.js} +378 -109
- package/dist/{index-WxMNUbOa.d.ts → index-Dxxu0Mi1.d.ts} +36 -9
- package/dist/index.d.ts +82 -16
- package/dist/index.js +199 -133
- 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 isEmptyVideoClip, A as buildSpeechHostMap, B as ensureLaneTrack, C as variantBaseEntityIds, D as hasSequence, E as createRelationId, F as assertValidVideoDocument, G as arrangeMainTrackSeamlessly, H as laneTrackId, I as validateVideoDocument, J as recalculateTimelineDuration, K as fillMainTrackTimeGaps, L as partUnionSchema, M as fromVideoDocument, N as toVideoDocument, O as isMediaAssetVariantKind, P as VideoDocumentValidationError, Q as TIMELINE_SKELETON_DURATION_MS, R as videoDocumentSchema, S as updateEntityFields, T as createEntityId, U as solveVideoDocument, V as findLaneTrack, W as cascadeAfterVideoClipChanges, X as resolveSpeechOverlapByShiftingVideos, Y as resolveAllSpeechOverlaps, Z as syncAggregatedClipsTimePosition, _ as assembleEntityContent, a as applyEntityTimelineProjection, at as effectiveVideoClipDurationMs, b as findComposedAudioScript, c as writeVideoDocumentToDraft, ct as recordEntries, d as EntityProjectionGraph, dt as bytesToBase64, et as partDurationMs, f as resolveEntityTimelineLayout, g as assembleCaptionContent, h as ScriptCompositionError, i as MirrorVideoDocumentAdapter, it as VIDEO_DOCUMENT_SCHEMA_VERSION, j as derivePositionFromAbs, k as buildInitialVideoDocument, l as readVideoDocumentFromDraft, lt as videoDocumentMirrorSchema, m as decodeEntityRelationRows, n as createPlainMemoryAdapter, nt as DEFAULT_UNIT_TIME_MS, o as createMirrorVideoDocument, ot as speedOf, p as EntityTimelineProjectionError, q as reassignSpeechesToVideoClipsByTime, r as generatePartId, rt as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, s as createMirrorVideoDocumentAdapter, st as partUnionToDraft, t as PlainMemoryAdapter, tt as safeDurationMs, u as projectEntityTimeline, ut as base64ToBytes, v as assemblePhoneticScriptContent, w as isJsonObject, x as selectAudioScriptSegments, y as assembleScriptText, z as LANE_KINDS_IN_STACK_ORDER } from "./document-BLYcbv8E.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";
|
|
@@ -231,8 +231,7 @@ const roleOrder$1 = {
|
|
|
231
231
|
* Image/Video/Audio/Voice Entity must carry its own external locator, every
|
|
232
232
|
* external asset id has exactly one claimant, and no media variant may be
|
|
233
233
|
* bound through a legacy `physical-asset` Relation. Dual-entity media graphs
|
|
234
|
-
* are rejected, not migrated. Caption
|
|
235
|
-
* `physical-asset` binding.
|
|
234
|
+
* are rejected, not migrated. Caption composes AudioScript text and has no physical text Asset.
|
|
236
235
|
*/
|
|
237
236
|
function assertCanonicalEditorResources(rows) {
|
|
238
237
|
if (rows.entities.filter((row) => row.entityKind === "timeline").length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one Timeline");
|
|
@@ -263,8 +262,7 @@ function assertCanonicalEditorResources(rows) {
|
|
|
263
262
|
*
|
|
264
263
|
* 1. every Image/Video/Audio/Voice Entity carries its own Asset `external`
|
|
265
264
|
* identity;
|
|
266
|
-
* 2. no `physical-asset` Relation may bind a media variant
|
|
267
|
-
* text keeps its inline Asset and binding);
|
|
265
|
+
* 2. no `physical-asset` Relation may bind a media variant or Caption;
|
|
268
266
|
* 3. a self-carried media identity is immutable — it cannot be rewritten to
|
|
269
267
|
* another asset id.
|
|
270
268
|
*
|
|
@@ -608,6 +606,23 @@ function sameVoice(left, right) {
|
|
|
608
606
|
}
|
|
609
607
|
//#endregion
|
|
610
608
|
//#region src/entity-editor/entity-timeline-editor.ts
|
|
609
|
+
/**
|
|
610
|
+
* Resolve the AudioScript a PhoneticScript composes. The composition is the
|
|
611
|
+
* required text source: a variant without it has no content and fails here.
|
|
612
|
+
*/
|
|
613
|
+
function requireComposedAudioScript(rows, phoneticScriptEntityId) {
|
|
614
|
+
return assemblePhoneticScriptContent(rows, phoneticScriptEntityId).audioScript.entityId;
|
|
615
|
+
}
|
|
616
|
+
function captionSelections(selections) {
|
|
617
|
+
return structuredClone(selections);
|
|
618
|
+
}
|
|
619
|
+
function captionBaseIds(ids) {
|
|
620
|
+
if (!Array.isArray(ids) || ids.length === 0) throw new Error("Caption requires non-empty baseEntityIds");
|
|
621
|
+
return ids.map(createEntityId);
|
|
622
|
+
}
|
|
623
|
+
function assertCaptionScript(rows, captionId, scriptId) {
|
|
624
|
+
if (assembleCaptionContent(rows, captionId).audioScript.entityId !== scriptId) throw new Error("Voiceover Caption must compose the same AudioScript as the PhoneticScript");
|
|
625
|
+
}
|
|
611
626
|
const roleOrder = {
|
|
612
627
|
caption: 0,
|
|
613
628
|
video_clip: 1,
|
|
@@ -1129,7 +1144,8 @@ var EntityTimelineEditor = class {
|
|
|
1129
1144
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1130
1145
|
replaceRows(draft, imported.rows);
|
|
1131
1146
|
const voice = requireEntityKind(draft, imported.contentEntityId, "voice");
|
|
1132
|
-
const
|
|
1147
|
+
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1148
|
+
this.ensurePhoneticRender(draft, voice.entityId, phonetic.entityId);
|
|
1133
1149
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
1134
1150
|
const existing = draft.entities.find((row) => row.entityId === input.voiceoverClipEntityId);
|
|
1135
1151
|
let voiceoverClipEntityId;
|
|
@@ -1179,12 +1195,13 @@ var EntityTimelineEditor = class {
|
|
|
1179
1195
|
}
|
|
1180
1196
|
});
|
|
1181
1197
|
}
|
|
1182
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voice.entityId,
|
|
1198
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voice.entityId, phonetic.entityId, input.captions);
|
|
1183
1199
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1184
1200
|
return {
|
|
1185
1201
|
voiceoverClipEntityId,
|
|
1186
1202
|
voiceEntityId: voice.entityId,
|
|
1187
|
-
|
|
1203
|
+
phoneticScriptEntityId: phonetic.entityId,
|
|
1204
|
+
audioScriptEntityId: requireComposedAudioScript(draft, phonetic.entityId),
|
|
1188
1205
|
captionClipEntityIds: input.captions.map((caption) => caption.captionClipEntityId)
|
|
1189
1206
|
};
|
|
1190
1207
|
});
|
|
@@ -1268,33 +1285,30 @@ var EntityTimelineEditor = class {
|
|
|
1268
1285
|
const track = findRoleTrack(draft, input.timelineEntityId, "caption");
|
|
1269
1286
|
if (track === void 0) return;
|
|
1270
1287
|
const patch = cloneJsonObject(input.style, "Caption style");
|
|
1288
|
+
const assembled = new EntityProjectionGraph(draft);
|
|
1271
1289
|
for (const clip of clipsOnTrack(draft, track.entityId)) {
|
|
1272
1290
|
const topology = requireClipTopology(draft, clip.entityId);
|
|
1273
1291
|
const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
|
|
1274
|
-
const prior = caption.payload.style;
|
|
1292
|
+
const prior = assembled.entity(caption.entityId).payload.style;
|
|
1275
1293
|
const style = isJsonObject(prior) ? prior : {};
|
|
1276
|
-
|
|
1277
|
-
...
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
style: {
|
|
1281
|
-
...style,
|
|
1282
|
-
...patch
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
});
|
|
1294
|
+
for (const updated of updateEntityFields(draft, caption.entityId, { style: {
|
|
1295
|
+
...style,
|
|
1296
|
+
...patch
|
|
1297
|
+
} })) replaceEntity(draft, updated.entityId, updated);
|
|
1286
1298
|
}
|
|
1287
1299
|
});
|
|
1288
1300
|
}
|
|
1289
1301
|
/**
|
|
1290
|
-
* Place
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
* the returned Clip later changes the Clip/Marker,
|
|
1302
|
+
* Place a Caption as a composed variant of an existing AudioScript: the
|
|
1303
|
+
* entity stores baseEntityIds, selection, optional style, and inherent cue-bounded
|
|
1304
|
+
* Sequence. This helper does not copy base text. The Clip and its display Marker describe this use;
|
|
1305
|
+
* moving or stretching the returned Clip later changes the Clip/Marker,
|
|
1306
|
+
* never the Caption entity or the AudioScript.
|
|
1294
1307
|
*/
|
|
1295
1308
|
insertCaptionClip(input) {
|
|
1296
1309
|
return this.transact((draft) => {
|
|
1297
1310
|
const durationMs = finitePositive(input.durationMs, "Caption durationMs");
|
|
1311
|
+
const selections = captionSelections(input.selections);
|
|
1298
1312
|
const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
|
|
1299
1313
|
const content = {
|
|
1300
1314
|
entityId: this.mintEntityId(draft),
|
|
@@ -1307,20 +1321,13 @@ var EntityTimelineEditor = class {
|
|
|
1307
1321
|
},
|
|
1308
1322
|
sampling: "native",
|
|
1309
1323
|
coordinateSpace: "milliseconds",
|
|
1310
|
-
|
|
1324
|
+
baseEntityIds: captionBaseIds(input.baseEntityIds),
|
|
1325
|
+
selections,
|
|
1311
1326
|
...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
|
|
1312
1327
|
}
|
|
1313
1328
|
};
|
|
1314
|
-
|
|
1315
|
-
|
|
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));
|
|
1329
|
+
draft.entities.push(content);
|
|
1330
|
+
assembleCaptionContent(draft, content.entityId);
|
|
1324
1331
|
return this.insertPlacedClipIntoDraft(draft, {
|
|
1325
1332
|
trackEntityId: track.entityId,
|
|
1326
1333
|
contentEntityId: content.entityId,
|
|
@@ -1398,34 +1405,24 @@ var EntityTimelineEditor = class {
|
|
|
1398
1405
|
draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
|
|
1399
1406
|
return track;
|
|
1400
1407
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1408
|
+
/** The existing pronunciation variant a Voice consumes; composition is required. */
|
|
1409
|
+
requireComposedPhoneticScript(draft, phoneticScriptEntityId) {
|
|
1410
|
+
const phonetic = requireEntityKind(draft, phoneticScriptEntityId, "phonetic-script");
|
|
1411
|
+
requireComposedAudioScript(draft, phonetic.entityId);
|
|
1412
|
+
return phonetic;
|
|
1413
|
+
}
|
|
1414
|
+
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1415
|
+
ensurePhoneticRender(draft, voiceEntityId, phoneticScriptEntityId) {
|
|
1416
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceEntityId || relation.endpoint1EntityId === voiceEntityId));
|
|
1417
|
+
if (renders.length > 1) throw new Error(`Voice "${voiceEntityId}" has multiple PhoneticScript render relations`);
|
|
1404
1418
|
if (renders[0] !== void 0) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
...script,
|
|
1408
|
-
payload: { segments: [{
|
|
1409
|
-
segmentId: "segment-0",
|
|
1410
|
-
text
|
|
1411
|
-
}] }
|
|
1412
|
-
};
|
|
1413
|
-
replaceEntity(draft, script.entityId, replacement);
|
|
1414
|
-
return replacement;
|
|
1419
|
+
if (otherEndpoint(renders[0], voiceEntityId) === phoneticScriptEntityId) return;
|
|
1420
|
+
throw new Error(`Voice "${voiceEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1415
1421
|
}
|
|
1416
|
-
|
|
1417
|
-
entityId: this.mintEntityId(draft),
|
|
1418
|
-
entityKind: "audio-script",
|
|
1419
|
-
payload: { segments: [{
|
|
1420
|
-
segmentId: "segment-0",
|
|
1421
|
-
text
|
|
1422
|
-
}] }
|
|
1423
|
-
};
|
|
1424
|
-
draft.entities.push(script);
|
|
1425
|
-
draft.relations.push(this.createRelation(draft, "audio-script-render", voiceEntityId, script.entityId));
|
|
1426
|
-
return script;
|
|
1422
|
+
draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceEntityId, phoneticScriptEntityId));
|
|
1427
1423
|
}
|
|
1428
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceEntityId,
|
|
1424
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceEntityId, phoneticScriptEntityId, captions) {
|
|
1425
|
+
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
1429
1426
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
1430
1427
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
1431
1428
|
const existing = directAnchorChildren(draft, voiceoverClipEntityId).filter((clipId) => clipRole(draft, clipId) === "caption");
|
|
@@ -1434,6 +1431,8 @@ var EntityTimelineEditor = class {
|
|
|
1434
1431
|
for (const caption of captions) {
|
|
1435
1432
|
const startMs = finiteNonNegative(caption.startMs, "Caption startMs");
|
|
1436
1433
|
const durationMs = finitePositive(caption.durationMs, "Caption durationMs");
|
|
1434
|
+
const selections = captionSelections(caption.selections);
|
|
1435
|
+
const baseEntityIds = captionBaseIds(caption.baseEntityIds);
|
|
1437
1436
|
if (draft.entities.find((entity) => entity.entityId === caption.captionClipEntityId) === void 0) {
|
|
1438
1437
|
const content = {
|
|
1439
1438
|
entityId: this.mintEntityId(draft),
|
|
@@ -1446,23 +1445,17 @@ var EntityTimelineEditor = class {
|
|
|
1446
1445
|
},
|
|
1447
1446
|
sampling: "native",
|
|
1448
1447
|
coordinateSpace: "milliseconds",
|
|
1449
|
-
|
|
1448
|
+
baseEntityIds,
|
|
1449
|
+
selections,
|
|
1450
1450
|
...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
|
|
1451
1451
|
}
|
|
1452
1452
|
};
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
payload: { inline: {
|
|
1457
|
-
mediaType: "text/plain",
|
|
1458
|
-
text: caption.text
|
|
1459
|
-
} }
|
|
1460
|
-
};
|
|
1461
|
-
draft.entities.push(content, asset);
|
|
1462
|
-
draft.relations.push(this.createRelation(draft, "physical-asset", content.entityId, asset.entityId), this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
|
|
1453
|
+
draft.entities.push(content);
|
|
1454
|
+
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1455
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
|
|
1463
1456
|
start: startMs,
|
|
1464
1457
|
end: startMs + durationMs
|
|
1465
|
-
} })
|
|
1458
|
+
} }));
|
|
1466
1459
|
this.insertPlacedClipIntoDraft(draft, {
|
|
1467
1460
|
clipEntityId: caption.captionClipEntityId,
|
|
1468
1461
|
trackEntityId: captionTrack.entityId,
|
|
@@ -1494,27 +1487,12 @@ var EntityTimelineEditor = class {
|
|
|
1494
1487
|
...content,
|
|
1495
1488
|
payload: {
|
|
1496
1489
|
...content.payload,
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
start: 0,
|
|
1500
|
-
end: durationMs
|
|
1501
|
-
},
|
|
1502
|
-
text: caption.text,
|
|
1490
|
+
baseEntityIds,
|
|
1491
|
+
selections,
|
|
1503
1492
|
...Object.keys(style).length === 0 ? {} : { style }
|
|
1504
1493
|
}
|
|
1505
1494
|
});
|
|
1506
|
-
|
|
1507
|
-
if (asset.payload.inline === void 0) throw new Error(`Caption "${caption.captionClipEntityId}" is not inline`);
|
|
1508
|
-
replaceEntity(draft, asset.entityId, {
|
|
1509
|
-
...asset,
|
|
1510
|
-
payload: {
|
|
1511
|
-
...asset.payload,
|
|
1512
|
-
inline: {
|
|
1513
|
-
mediaType: "text/plain",
|
|
1514
|
-
text: caption.text
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
});
|
|
1495
|
+
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1518
1496
|
this.applyClipPlacement(draft, topology.clip.entityId, {
|
|
1519
1497
|
kind: "anchored",
|
|
1520
1498
|
hostClipEntityId: voiceoverClipEntityId,
|
|
@@ -1525,17 +1503,13 @@ var EntityTimelineEditor = class {
|
|
|
1525
1503
|
...placed.marker,
|
|
1526
1504
|
payload: {
|
|
1527
1505
|
...placed.marker.payload,
|
|
1528
|
-
sourceRange: {
|
|
1529
|
-
start: 0,
|
|
1530
|
-
end: durationMs
|
|
1531
|
-
},
|
|
1532
1506
|
duration: {
|
|
1533
1507
|
mode: "fixed",
|
|
1534
1508
|
value: durationMs
|
|
1535
1509
|
}
|
|
1536
1510
|
}
|
|
1537
1511
|
});
|
|
1538
|
-
this.
|
|
1512
|
+
this.replaceCaptionAlignment(draft, content.entityId, voiceEntityId, startMs, durationMs);
|
|
1539
1513
|
}
|
|
1540
1514
|
}
|
|
1541
1515
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
@@ -1544,34 +1518,30 @@ var EntityTimelineEditor = class {
|
|
|
1544
1518
|
const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
|
|
1545
1519
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1546
1520
|
}
|
|
1547
|
-
|
|
1548
|
-
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment"
|
|
1521
|
+
replaceCaptionAlignment(draft, captionEntityId, voiceEntityId, startMs, durationMs) {
|
|
1522
|
+
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
|
|
1549
1523
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1550
1524
|
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceEntityId, { alignment: {
|
|
1551
1525
|
start: startMs,
|
|
1552
1526
|
end: startMs + durationMs
|
|
1553
|
-
} })
|
|
1527
|
+
} }));
|
|
1554
1528
|
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Remove one placed Caption use. The composed AudioScript outlives the
|
|
1531
|
+
* caption: it may anchor further captions and remains the text owner.
|
|
1532
|
+
*/
|
|
1555
1533
|
removeCaptionClipGraph(draft, clipEntityId) {
|
|
1556
1534
|
const topology = requireClipOnRole(draft, clipEntityId, "caption");
|
|
1557
1535
|
const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
|
|
1558
|
-
const asset = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, caption.entityId, "physical-asset"), caption.entityId), "asset");
|
|
1559
1536
|
const contentRelations = incidentRelations(draft, caption.entityId);
|
|
1560
|
-
const allowed = new Set([
|
|
1561
|
-
"physical-asset",
|
|
1562
|
-
"caption-alignment",
|
|
1563
|
-
"caption-provenance",
|
|
1564
|
-
"marker-content"
|
|
1565
|
-
]);
|
|
1537
|
+
const allowed = new Set(["caption-alignment", "marker-content"]);
|
|
1566
1538
|
const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
|
|
1567
1539
|
if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
|
|
1568
1540
|
this.removeClipGraph(draft, clipEntityId);
|
|
1569
1541
|
if (incidentRelations(draft, caption.entityId).some((relation) => relation.relationKind === "marker-content")) return;
|
|
1570
1542
|
const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
|
|
1571
|
-
const assetIncidents = incidentRelations(draft, asset.entityId);
|
|
1572
|
-
const deleteAsset = asset.payload.inline !== void 0 && assetIncidents.every((relation) => removed.has(relation.relationId));
|
|
1573
1543
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1574
|
-
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId
|
|
1544
|
+
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
|
|
1575
1545
|
}
|
|
1576
1546
|
replaceAnchorHost(draft, childClipEntityId, hostClipEntityId) {
|
|
1577
1547
|
const anchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === childClipEntityId);
|
|
@@ -1992,7 +1962,6 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
1992
1962
|
facts.set(fact.assetId, fact);
|
|
1993
1963
|
}
|
|
1994
1964
|
const contentByPartId = /* @__PURE__ */ new Map();
|
|
1995
|
-
const scriptBySpeechClipId = /* @__PURE__ */ new Map();
|
|
1996
1965
|
for (const track of legacy.tracks ?? []) for (const item of track.items ?? []) {
|
|
1997
1966
|
const part = requirePart(legacy, item.part_id);
|
|
1998
1967
|
if (part.video_clip !== void 0) {
|
|
@@ -2026,7 +1995,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2026
1995
|
entities = [...imported.rows.entities];
|
|
2027
1996
|
relations = [...imported.rows.relations];
|
|
2028
1997
|
contentByPartId.set(item.part_id, imported.contentEntityId);
|
|
2029
|
-
|
|
1998
|
+
const phonemeScript = speech.phoneme_script;
|
|
1999
|
+
ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link);
|
|
2030
2000
|
} else if (part.bgm !== void 0) {
|
|
2031
2001
|
const bgm = part.bgm;
|
|
2032
2002
|
const fact = facts.get(bgm.origin_media_id);
|
|
@@ -2101,6 +2071,22 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2101
2071
|
} else if (part.caption !== void 0) {
|
|
2102
2072
|
const caption = part.caption;
|
|
2103
2073
|
duration = caption.initial_duration_ms;
|
|
2074
|
+
const voice = contentByPartId.get(caption.speech_part_id);
|
|
2075
|
+
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voice);
|
|
2076
|
+
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2077
|
+
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2078
|
+
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
2079
|
+
const sourceText = scriptText(script);
|
|
2080
|
+
const captionText = caption.text;
|
|
2081
|
+
const offset = sourceText.indexOf(captionText);
|
|
2082
|
+
if (!captionText.trim() || offset < 0 || sourceText.indexOf(captionText, offset + 1) >= 0) fail("Legacy Caption needs explicit unambiguous AudioScript selections before cutover");
|
|
2083
|
+
const selections = [{
|
|
2084
|
+
segmentId: "segment-0",
|
|
2085
|
+
...sourceText === captionText ? {} : { textRange: {
|
|
2086
|
+
start: Array.from(sourceText.slice(0, offset)).length,
|
|
2087
|
+
end: Array.from(sourceText.slice(0, offset + captionText.length)).length
|
|
2088
|
+
} }
|
|
2089
|
+
}];
|
|
2104
2090
|
content = addEntity("caption", {
|
|
2105
2091
|
extent: {
|
|
2106
2092
|
kind: "bounded",
|
|
@@ -2109,21 +2095,14 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2109
2095
|
},
|
|
2110
2096
|
sampling: "native",
|
|
2111
2097
|
coordinateSpace: "milliseconds",
|
|
2112
|
-
|
|
2098
|
+
baseEntityIds: [script.entityId],
|
|
2099
|
+
selections,
|
|
2113
2100
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2114
2101
|
});
|
|
2115
|
-
const asset = addEntity("asset", { inline: {
|
|
2116
|
-
mediaType: "text/plain",
|
|
2117
|
-
text: caption.text
|
|
2118
|
-
} });
|
|
2119
|
-
link("physical-asset", content, asset);
|
|
2120
|
-
const voice = contentByPartId.get(caption.speech_part_id);
|
|
2121
|
-
const script = scriptBySpeechClipId.get(caption.speech_part_id);
|
|
2122
2102
|
link("caption-alignment", content, voice, { alignment: {
|
|
2123
2103
|
start: caption.start_ms,
|
|
2124
2104
|
end: caption.start_ms + duration
|
|
2125
2105
|
} });
|
|
2126
|
-
link("caption-provenance", content, script, { segmentAlignment: [] });
|
|
2127
2106
|
markerPayload = {
|
|
2128
2107
|
sourceRange: {
|
|
2129
2108
|
start: 0,
|
|
@@ -2339,7 +2318,7 @@ function assertPartSupported(id, part, role) {
|
|
|
2339
2318
|
if (speech.voice?.name !== void 0 && typeof speech.voice.name !== "string") fail("Migration requires a string voice name when present");
|
|
2340
2319
|
if (!Array.isArray(speech.caption_ids) || speech.caption_ids.some((captionId) => !trimmed(captionId))) fail("Migration requires explicit Speech caption identities");
|
|
2341
2320
|
if (new Set(speech.caption_ids).size !== speech.caption_ids.length) fail("Migration requires unique Speech caption identities");
|
|
2342
|
-
if (phonemeScript !== void 0 && phonemeScript !== null) fail("
|
|
2321
|
+
if (phonemeScript !== void 0 && phonemeScript !== null && typeof phonemeScript !== "string") fail("Migration requires a string phoneme_script when present");
|
|
2343
2322
|
assertKnownFields(speech.voice, ["id", "name"], "voice");
|
|
2344
2323
|
assertKnownFields(speech, [
|
|
2345
2324
|
"id",
|
|
@@ -2463,20 +2442,31 @@ function requirePart(document, partId) {
|
|
|
2463
2442
|
if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
|
|
2464
2443
|
return part;
|
|
2465
2444
|
}
|
|
2466
|
-
function
|
|
2467
|
-
const renders = relations.filter((relation) => relation.relationKind === "
|
|
2468
|
-
if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous
|
|
2445
|
+
function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link) {
|
|
2446
|
+
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceEntityId);
|
|
2447
|
+
if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous PhoneticScript provenance`);
|
|
2469
2448
|
if (renders.length === 1) {
|
|
2470
|
-
const
|
|
2471
|
-
if (
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2449
|
+
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
2450
|
+
if (phonetic === void 0) fail(`Voice ${voiceEntityId} has a dangling PhoneticScript render`);
|
|
2451
|
+
const script = composedAudioScript(phonetic, entities);
|
|
2452
|
+
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the legacy Speech script`);
|
|
2453
|
+
return phonetic.entityId;
|
|
2454
|
+
}
|
|
2455
|
+
const phonetic = addEntity("phonetic-script", {
|
|
2456
|
+
baseEntityIds: [addEntity("audio-script", { segments: [{
|
|
2457
|
+
segmentId: "segment-0",
|
|
2458
|
+
text: audioScriptText
|
|
2459
|
+
}] })],
|
|
2460
|
+
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2461
|
+
});
|
|
2462
|
+
link("phonetic-script-render", voiceEntityId, phonetic);
|
|
2463
|
+
return phonetic;
|
|
2464
|
+
}
|
|
2465
|
+
function composedAudioScript(phonetic, entities) {
|
|
2466
|
+
return findComposedAudioScript({
|
|
2467
|
+
entities,
|
|
2468
|
+
relations: []
|
|
2469
|
+
}, phonetic.entityId, "phonetic-script");
|
|
2480
2470
|
}
|
|
2481
2471
|
function scriptText(script) {
|
|
2482
2472
|
const segments = script.payload.segments;
|
|
@@ -2566,6 +2556,82 @@ function fail(message) {
|
|
|
2566
2556
|
throw new EntityTimelineProjectionError(message);
|
|
2567
2557
|
}
|
|
2568
2558
|
//#endregion
|
|
2559
|
+
//#region src/entity-editor/caption-content.ts
|
|
2560
|
+
/** Author Caption content from semantic intent; callers never reconcile variant bases or assembled fields. */
|
|
2561
|
+
function prepareCaptionContent(rows, input) {
|
|
2562
|
+
const entityId = createEntityId(input.contentEntityId);
|
|
2563
|
+
const scriptId = createEntityId(input.audioScriptEntityId);
|
|
2564
|
+
if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) throw new Error("Caption duration must be positive");
|
|
2565
|
+
const previous = rows.entities.find((row) => row.entityId === entityId);
|
|
2566
|
+
if (previous !== void 0 && previous.entityKind !== "caption") throw new Error("Caption content identity has a different kind");
|
|
2567
|
+
let baseEntityIds = [scriptId];
|
|
2568
|
+
if (previous !== void 0) {
|
|
2569
|
+
const source = assembleCaptionContent(rows, entityId).audioScript.entityId;
|
|
2570
|
+
const bases = variantBaseEntityIds(previous);
|
|
2571
|
+
if (source !== scriptId && !bases.includes(source)) throw new Error("Replacing an indirectly composed AudioScript requires an explicit composition change");
|
|
2572
|
+
baseEntityIds = bases.map((id) => id === source ? scriptId : id);
|
|
2573
|
+
}
|
|
2574
|
+
const candidate = {
|
|
2575
|
+
entityId,
|
|
2576
|
+
entityKind: "caption",
|
|
2577
|
+
payload: {
|
|
2578
|
+
...previous?.payload,
|
|
2579
|
+
baseEntityIds,
|
|
2580
|
+
extent: previous?.payload.extent ?? {
|
|
2581
|
+
kind: "bounded",
|
|
2582
|
+
start: 0,
|
|
2583
|
+
end: input.durationMs
|
|
2584
|
+
},
|
|
2585
|
+
sampling: previous?.payload.sampling ?? "native",
|
|
2586
|
+
coordinateSpace: previous?.payload.coordinateSpace ?? "milliseconds"
|
|
2587
|
+
}
|
|
2588
|
+
};
|
|
2589
|
+
const graph = {
|
|
2590
|
+
...rows,
|
|
2591
|
+
entities: [...rows.entities.filter((row) => row.entityId !== entityId), candidate]
|
|
2592
|
+
};
|
|
2593
|
+
const textSource = {
|
|
2594
|
+
...assembleEntityContent(graph, entityId),
|
|
2595
|
+
entityKind: "audio-script"
|
|
2596
|
+
};
|
|
2597
|
+
const selected = {
|
|
2598
|
+
...candidate,
|
|
2599
|
+
payload: {
|
|
2600
|
+
...candidate.payload,
|
|
2601
|
+
selections: structuredClone(input.selections ?? selectCaptionText(textSource, input.text))
|
|
2602
|
+
}
|
|
2603
|
+
};
|
|
2604
|
+
if (assembleCaptionContent({
|
|
2605
|
+
...graph,
|
|
2606
|
+
entities: graph.entities.map((row) => row.entityId === entityId ? selected : row)
|
|
2607
|
+
}, entityId).text !== input.text) throw new Error("Caption review text differs from its assembled variant content");
|
|
2608
|
+
return selected;
|
|
2609
|
+
}
|
|
2610
|
+
function selectCaptionText(script, text) {
|
|
2611
|
+
if (!text.trim()) throw new Error("Caption text selection must not be blank");
|
|
2612
|
+
const source = script;
|
|
2613
|
+
const fullText = assembleScriptText(source);
|
|
2614
|
+
const segments = source.payload.segments;
|
|
2615
|
+
if (text === fullText) return segments.map((segment) => ({ segmentId: segment.segmentId }));
|
|
2616
|
+
const matches = [];
|
|
2617
|
+
for (const segment of segments) {
|
|
2618
|
+
let offset = segment.text.indexOf(text);
|
|
2619
|
+
while (offset !== -1) {
|
|
2620
|
+
const start = Array.from(segment.text.slice(0, offset)).length;
|
|
2621
|
+
matches.push({
|
|
2622
|
+
segmentId: segment.segmentId,
|
|
2623
|
+
textRange: {
|
|
2624
|
+
start,
|
|
2625
|
+
end: start + Array.from(text).length
|
|
2626
|
+
}
|
|
2627
|
+
});
|
|
2628
|
+
offset = segment.text.indexOf(text, offset + 1);
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
if (matches.length !== 1) throw new Error("Caption text must select a unique AudioScript range; provide explicit selections");
|
|
2632
|
+
return matches;
|
|
2633
|
+
}
|
|
2634
|
+
//#endregion
|
|
2569
2635
|
//#region src/editor/snapshot-utils.ts
|
|
2570
2636
|
function isMap(value) {
|
|
2571
2637
|
return value instanceof Map;
|
|
@@ -4704,4 +4770,4 @@ function serverCovers(serverVV, target) {
|
|
|
4704
4770
|
return covers(server, target);
|
|
4705
4771
|
}
|
|
4706
4772
|
//#endregion
|
|
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 };
|
|
4773
|
+
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, assembleEntityContent, 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, prepareCaptionContent, 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 };
|
package/dist/testing.d.ts
CHANGED
package/dist/testing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dt as bytesToBase64, it as VIDEO_DOCUMENT_SCHEMA_VERSION, s as createMirrorVideoDocumentAdapter, ut as base64ToBytes } from "./document-BLYcbv8E.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.
|
|
3
|
+
"version": "1.3.1-alpha.2",
|
|
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.3.1-alpha.
|
|
31
|
-
"@mengine/
|
|
32
|
-
"@mengine/
|
|
30
|
+
"@mengine/storage": "1.3.1-alpha.2",
|
|
31
|
+
"@mengine/sync": "1.3.1-alpha.2",
|
|
32
|
+
"@mengine/utils": "1.3.1-alpha.2"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^25.9.1",
|