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