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