@mengine/medeo-client 2.0.1-alpha.2 → 2.0.1-alpha.20
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 +31 -0
- package/dist/{document-BSAUud6c.js → document-yPiwEmkJ.js} +521 -328
- package/dist/{index-B31KeDzA.d.ts → index-C0uUWqkz.d.ts} +278 -183
- package/dist/index.d.ts +196 -168
- package/dist/index.js +590 -282
- package/dist/{loro-relay-doc-ssdYpuef.js → loro-relay-doc-cJSY-uau.js} +7 -2
- package/dist/relay.js +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +4 -19
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as partUnionSchema, A as decodeEntityRelationRows, B as isJsonObject, C as applyFieldChanges, Ct as recordEntries, D as EntityProjectionGraph, E as projectEntityTimeline, Et as bytesToBase64, F as assembleScriptText, G as buildInitialVideoDocument, H as createRelationId, I as findComposedAudioScript, J as fromVideoDocument, K as buildSpeechHostMap, L as selectAudioScriptSegment, M as assembleCaptionContent, N as assembleEntityContent, O as resolveEntityTimelineLayout, P as assemblePhoneticScriptContent, Q as validateVideoDocument, R as updateEntityFields, S as equalJson, St as partUnionToDraft, T as resolveFieldPath, Tt as base64ToBytes, U as hasSequence, V as createEntityId, W as isMediaAssetVariantKind, X as VideoDocumentValidationError, Y as toVideoDocument, Z as assertValidVideoDocument, _ as audioScriptAssetOf, _t as partDurationMs, a as createMirrorVideoDocument, at as laneTrackId, b as phoneticScriptAssetOf, bt as effectiveVideoClipDurationMs, c as DocumentMutationGuard, ct as arrangeMainTrackSeamlessly, d as LoroEntityDraft, dt as recalculateTimelineDuration, et as videoDocumentSchema, f as readEntityDocument, ft as resolveAllSpeechOverlaps, g as audioScriptAssetHash, gt as isEmptyVideoClip, h as audioScriptAssetFields, ht as TIMELINE_SKELETON_DURATION_MS, i as MirrorVideoDocumentAdapter, it as hasResolvedPlacement, j as ScriptCompositionError, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as fillMainTrackTimeGaps, m as audioScriptAssetContent, mt as syncAggregatedClipsTimePosition, n as createPlainMemoryAdapter, nt as ensureLaneTrack, o as createMirrorVideoDocumentAdapter, ot as solveVideoDocument, p as entityOrderGroups, pt as resolveSpeechOverlapByShiftingVideos, q as derivePositionFromAbs, r as generatePartId, rt as findLaneTrack, s as readVideoDocumentFromDraft, st as cascadeAfterVideoClipChanges, t as PlainMemoryAdapter, tt as LANE_KINDS_IN_STACK_ORDER, u as LoroEntityDocument, ut as reassignSpeechesToVideoClipsByTime, v as phoneticScriptAssetContent, vt as safeDurationMs, w as assertFieldChanges, wt as videoDocumentMirrorSchema, x as scriptSourceAssetOf, xt as speedOf, y as phoneticScriptAssetHash, yt as DEFAULT_UNIT_TIME_MS, z as variantBaseEntityIds } from "./document-yPiwEmkJ.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
|
-
import { LoroDoc, VersionVector } from "loro-crdt";
|
|
3
|
+
import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
|
|
4
4
|
import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
|
|
5
5
|
import { DisposableSet, EventBus, Task } from "@mengine/utils";
|
|
6
6
|
import { BaseDocStorage, DummyConnection } from "@mengine/storage";
|
|
@@ -60,12 +60,6 @@ var MengineHttpClient = class {
|
|
|
60
60
|
async fetchSnapshot() {
|
|
61
61
|
return await this.requestJson("snapshot");
|
|
62
62
|
}
|
|
63
|
-
async bootstrapSnapshot(snapshot) {
|
|
64
|
-
return await this.requestJson("bootstrap", {
|
|
65
|
-
method: "POST",
|
|
66
|
-
body: JSON.stringify({ snapshot: bytesToBase64(snapshot) })
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
63
|
/**
|
|
70
64
|
* Loro VV-diff pull: send the caller's oplog `VersionVector.encode` as `from`
|
|
71
65
|
* (omit for a full pull) and receive exactly the updates it is missing plus
|
|
@@ -168,9 +162,6 @@ async function safeReadJson$1(response) {
|
|
|
168
162
|
//#endregion
|
|
169
163
|
//#region src/client/sse.ts
|
|
170
164
|
async function readMengineEventStream(options) {
|
|
171
|
-
const onUpdate = (event) => {
|
|
172
|
-
options.onUpdate(event);
|
|
173
|
-
};
|
|
174
165
|
const response = await options.client.fetch(options.client.eventsUrl(), {
|
|
175
166
|
headers: options.client.headers(),
|
|
176
167
|
signal: options.signal
|
|
@@ -189,15 +180,19 @@ async function readMengineEventStream(options) {
|
|
|
189
180
|
while (splitAt >= 0) {
|
|
190
181
|
const frame = buffer.slice(0, splitAt);
|
|
191
182
|
buffer = buffer.slice(buffer[splitAt] === "\r" ? splitAt + 4 : splitAt + 2);
|
|
192
|
-
handleSseFrame(frame,
|
|
183
|
+
handleSseFrame(frame, options);
|
|
193
184
|
splitAt = findSseFrameBoundary(buffer);
|
|
194
185
|
}
|
|
195
186
|
}
|
|
196
187
|
}
|
|
197
|
-
function handleSseFrame(frame,
|
|
188
|
+
function handleSseFrame(frame, options) {
|
|
198
189
|
const event = parseSseFrame(frame);
|
|
190
|
+
if (event.event === "resync") {
|
|
191
|
+
options.onResync?.();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
199
194
|
if (event.event !== "update" || event.data.length === 0) return;
|
|
200
|
-
onUpdate(JSON.parse(event.data.join("\n")));
|
|
195
|
+
options.onUpdate(JSON.parse(event.data.join("\n")));
|
|
201
196
|
}
|
|
202
197
|
function parseSseFrame(frame) {
|
|
203
198
|
let event = null;
|
|
@@ -226,7 +221,8 @@ function compileEntityRows(snapshot, rows, validate) {
|
|
|
226
221
|
}
|
|
227
222
|
/** Compile an edit on an existing peer, retaining its native local undo history. */
|
|
228
223
|
function applyEntityRows(editor, rows) {
|
|
229
|
-
const
|
|
224
|
+
const baseline = editor.snapshot();
|
|
225
|
+
const before = baseline.rows;
|
|
230
226
|
const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
|
|
231
227
|
const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
|
|
232
228
|
const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
@@ -242,10 +238,16 @@ function applyEntityRows(editor, rows) {
|
|
|
242
238
|
if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
|
|
243
239
|
}
|
|
244
240
|
}
|
|
241
|
+
let attached = baseline.project.audioScriptEntityId !== void 0;
|
|
245
242
|
for (const row of rows.entities) {
|
|
246
243
|
const previous = oldEntities.get(row.entityId);
|
|
247
|
-
if (!previous)
|
|
248
|
-
|
|
244
|
+
if (!previous) {
|
|
245
|
+
draft.create(row);
|
|
246
|
+
if (!attached && row.entityKind === "audio-script") {
|
|
247
|
+
draft.attach("audioScriptEntityId", String(row.entityId));
|
|
248
|
+
attached = true;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
249
251
|
if (previous.entityKind !== row.entityKind) throw new Error("Entity kind cannot change");
|
|
250
252
|
if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
|
|
251
253
|
}
|
|
@@ -288,66 +290,59 @@ const roleOrder$1 = {
|
|
|
288
290
|
/**
|
|
289
291
|
* MEngine editor policy, not a cardinality restriction on the general-purpose DSL.
|
|
290
292
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
* are rejected, not migrated. Caption composes AudioScript text and may associate with a physical text Asset.
|
|
293
|
+
* Resources live on Asset entities and are reached through `from-asset`: no
|
|
294
|
+
* other entity carries a locator, one Asset claims each `(system, key)` pair,
|
|
295
|
+
* and media reaches at most one Asset. Media whose bytes are not named through
|
|
296
|
+
* that Relation is rejected rather than guessed at.
|
|
296
297
|
*/
|
|
297
298
|
function assertCanonicalEditorResources(rows) {
|
|
298
299
|
if (rows.entities.filter((row) => row.entityKind === "timeline").length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one Timeline");
|
|
299
300
|
for (const role of Object.keys(roleOrder$1)) if (rows.entities.filter((row) => row.entityKind === "track" && row.payload.role === role).length > 1) throw new EntityTimelineProjectionError(`Editor has multiple ${role} Tracks`);
|
|
300
301
|
const identityByAssetKey = /* @__PURE__ */ new Map();
|
|
301
|
-
const kinds = new Map(rows.entities.map((row) => [String(row.entityId), row.entityKind]));
|
|
302
302
|
for (const entity of rows.entities) {
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
303
|
+
if (entity.entityKind === "asset") {
|
|
304
|
+
const locator = memotaLocator(entity.payload);
|
|
305
|
+
if (locator === void 0) throw new EntityTimelineProjectionError(`Asset ${entity.entityId} must state where its bytes live`);
|
|
306
|
+
const key = JSON.stringify([locator.system, locator.key]);
|
|
307
|
+
const previous = identityByAssetKey.get(key);
|
|
308
|
+
if (previous !== void 0 && previous !== entity.entityId) throw new EntityTimelineProjectionError(`Duplicate Asset for ${key}; reuse ${previous}`);
|
|
309
|
+
identityByAssetKey.set(key, entity.entityId);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (memotaLocator(entity.payload) !== void 0) throw new EntityTimelineProjectionError(`Entity ${entity.entityId} must not carry a locator; link its Asset with from-asset`);
|
|
311
313
|
}
|
|
312
|
-
for (const
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
if (mediaEndpoint !== void 0) throw new EntityTimelineProjectionError(`Cannot bind media Entity ${mediaEndpoint} through physical-asset Relation ${relation.relationId}; media owns its Asset identity directly`);
|
|
314
|
+
for (const entity of rows.entities) {
|
|
315
|
+
if (!isMediaAssetVariantKind(entity.entityKind)) continue;
|
|
316
|
+
if (assetIdsOf(rows, entity.entityId).length !== 1) throw new EntityTimelineProjectionError(`Media Entity ${entity.entityId} requires exactly one from-asset Asset`);
|
|
316
317
|
}
|
|
317
318
|
}
|
|
319
|
+
/** The Asset ids one entity reaches, in row order. */
|
|
320
|
+
function assetIdsOf(rows, entityId) {
|
|
321
|
+
const assets = new Set(rows.entities.filter((row) => row.entityKind === "asset").map((row) => String(row.entityId)));
|
|
322
|
+
return rows.relations.filter((relation) => relation.relationKind === "from-asset").map((relation) => String(relation.endpoint0EntityId) === entityId ? String(relation.endpoint1EntityId) : String(relation.endpoint1EntityId) === entityId ? String(relation.endpoint0EntityId) : void 0).filter((id) => id !== void 0 && assets.has(id));
|
|
323
|
+
}
|
|
318
324
|
/**
|
|
319
325
|
* Commit-transition policy shared by the editor, the HTTP commit boundary, the
|
|
320
326
|
* host tool, and the entity stores. It compares the authoritative before and
|
|
321
|
-
* after rows of one CAS commit and enforces the
|
|
322
|
-
*
|
|
327
|
+
* after rows of one CAS commit and enforces the resource rules on the complete
|
|
328
|
+
* after state, with no exemption for historical graphs:
|
|
323
329
|
*
|
|
324
|
-
* 1. every
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* another asset id.
|
|
330
|
+
* 1. every media Entity reaches exactly one Asset through `from-asset`;
|
|
331
|
+
* 2. no entity other than an Asset carries a locator;
|
|
332
|
+
* 3. an Asset's locator is immutable — pointing it at other bytes would
|
|
333
|
+
* silently rewrite every entity already made from it.
|
|
329
334
|
*
|
|
330
|
-
*
|
|
331
|
-
* implicitly. Deleting media is explicit and allowed.
|
|
335
|
+
* Deleting media is explicit and allowed.
|
|
332
336
|
*/
|
|
333
337
|
function assertMediaAssetWritePolicy(before, after) {
|
|
334
338
|
const beforeEntities = new Map(before.entities.map((row) => [String(row.entityId), row]));
|
|
335
|
-
|
|
339
|
+
assertCanonicalEditorResources(after);
|
|
336
340
|
for (const row of after.entities) {
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
const mediaEndpoint = [relation.endpoint0EntityId, relation.endpoint1EntityId].find((endpoint) => isMediaAssetVariantKind(afterKinds.get(String(endpoint)) ?? ""));
|
|
343
|
-
if (mediaEndpoint !== void 0) throw new EntityTimelineProjectionError(`Cannot bind media Entity ${mediaEndpoint} through physical-asset Relation ${relation.relationId}; media owns its Asset identity directly`);
|
|
344
|
-
}
|
|
345
|
-
for (const row of after.entities) {
|
|
346
|
-
if (!isMediaAssetVariantKind(row.entityKind)) continue;
|
|
347
|
-
const priorExternal = memotaLocator(beforeEntities.get(String(row.entityId))?.payload.external);
|
|
348
|
-
if (priorExternal === void 0) continue;
|
|
349
|
-
const nextExternal = memotaLocator(row.payload.external);
|
|
350
|
-
if (nextExternal !== void 0 && (nextExternal.system !== priorExternal.system || nextExternal.key !== priorExternal.key)) throw new EntityTimelineProjectionError(`Media Entity ${row.entityId} cannot rewrite its Asset external identity; create a new Entity instead`);
|
|
341
|
+
if (row.entityKind !== "asset") continue;
|
|
342
|
+
const prior = memotaLocator(beforeEntities.get(String(row.entityId))?.payload);
|
|
343
|
+
if (prior === void 0) continue;
|
|
344
|
+
const next = memotaLocator(row.payload);
|
|
345
|
+
if (next !== void 0 && (next.system !== prior.system || next.key !== prior.key)) throw new EntityTimelineProjectionError(`Asset ${row.entityId} cannot be pointed at other bytes; create a new Asset instead`);
|
|
351
346
|
}
|
|
352
347
|
}
|
|
353
348
|
function memotaLocator(value) {
|
|
@@ -362,23 +357,14 @@ function memotaLocator(value) {
|
|
|
362
357
|
* explicit project attachment, multiple candidates are ambiguous. Existing
|
|
363
358
|
* content IDs are reused here; Loro editing advances their immutable versions.
|
|
364
359
|
*/
|
|
365
|
-
function ensureDocumentAudioScript(entities,
|
|
360
|
+
function ensureDocumentAudioScript(entities, selectedEntityId) {
|
|
366
361
|
if (selectedEntityId != null) {
|
|
367
362
|
if (!entities.some((row) => row.entityKind === "audio-script" && row.entityId === selectedEntityId)) throw new EntityTimelineProjectionError(`Selected AudioScript ${selectedEntityId} does not exist`);
|
|
368
363
|
return selectedEntityId;
|
|
369
364
|
}
|
|
370
365
|
const scripts = entities.filter((row) => row.entityKind === "audio-script");
|
|
371
366
|
if (scripts.length > 1) throw new EntityTimelineProjectionError("Select an AudioScript for the editor panel when multiple scripts exist");
|
|
372
|
-
|
|
373
|
-
if (existing !== void 0) return existing.entityId;
|
|
374
|
-
const id = createEntityId(idFactory("entity"));
|
|
375
|
-
if (entities.some((row) => row.entityId === id)) throw new Error(`Duplicate entity id ${id}`);
|
|
376
|
-
entities.push({
|
|
377
|
-
entityId: id,
|
|
378
|
-
entityKind: "audio-script",
|
|
379
|
-
payload: { segments: [] }
|
|
380
|
-
});
|
|
381
|
-
return id;
|
|
367
|
+
return scripts[0]?.entityId ?? null;
|
|
382
368
|
}
|
|
383
369
|
/** Add only missing editor structure; existing identities and owned settings are never replaced. */
|
|
384
370
|
function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}, selectedAudioScriptEntityId) {
|
|
@@ -432,7 +418,7 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
432
418
|
}
|
|
433
419
|
trackEntityIds[role] = track.entityId;
|
|
434
420
|
}
|
|
435
|
-
const audioScriptEntityId = ensureDocumentAudioScript(entities,
|
|
421
|
+
const audioScriptEntityId = ensureDocumentAudioScript(entities, selectedAudioScriptEntityId);
|
|
436
422
|
return {
|
|
437
423
|
rows: {
|
|
438
424
|
entities,
|
|
@@ -500,15 +486,16 @@ function parseWireState(value, expectedDocId) {
|
|
|
500
486
|
if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
501
487
|
const rows = parseWireRows(value.rows);
|
|
502
488
|
decodeEntityRelationRows(rows, numericMarkerValidation$1);
|
|
503
|
-
|
|
489
|
+
const attached = value.audio_script_entity_id;
|
|
490
|
+
if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
|
|
504
491
|
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
505
492
|
const state = {
|
|
506
493
|
revision: value.revision,
|
|
507
494
|
loroSnapshot: value.loro_snapshot,
|
|
508
495
|
rows,
|
|
509
|
-
audioScriptEntityId:
|
|
496
|
+
audioScriptEntityId: attached
|
|
510
497
|
};
|
|
511
|
-
readDocumentAudioScript(state);
|
|
498
|
+
if (attached !== null) readDocumentAudioScript(state);
|
|
512
499
|
return state;
|
|
513
500
|
}
|
|
514
501
|
function parseWireRows(value) {
|
|
@@ -571,12 +558,10 @@ const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right)
|
|
|
571
558
|
* Import at the editor boundary, not at asset generation time. Existing
|
|
572
559
|
* provenance is retained.
|
|
573
560
|
*
|
|
574
|
-
*
|
|
575
|
-
*
|
|
576
|
-
* the
|
|
577
|
-
*
|
|
578
|
-
* (`asset` row plus a `physical-asset` binding) are not reused and are
|
|
579
|
-
* rejected by the canonical resource policy instead of being migrated.
|
|
561
|
+
* The bytes are named once, on an Asset entity, and the media entity reaches
|
|
562
|
+
* them through a `from-asset` Relation. Re-importing the same `(system, key)`
|
|
563
|
+
* reuses that Asset and the media already made from it, so one stored resource
|
|
564
|
+
* is never copied into several rows.
|
|
580
565
|
*/
|
|
581
566
|
function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
|
|
582
567
|
if (!fact.assetId || fact.assetId.trim() !== fact.assetId) throw new Error("Expected an asset id");
|
|
@@ -584,60 +569,60 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
584
569
|
"image",
|
|
585
570
|
"video",
|
|
586
571
|
"audio",
|
|
587
|
-
"
|
|
572
|
+
"voiceover"
|
|
588
573
|
].includes(fact.kind)) throw new Error("Unsupported media kind");
|
|
589
574
|
if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
|
|
590
|
-
if ((fact.kind === "audio" || fact.kind === "
|
|
575
|
+
if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
591
576
|
decodeEntityRelationRows(initial, numericCoordinates);
|
|
592
577
|
assertCanonicalEditorResources(initial);
|
|
593
578
|
const entities = structuredClone([...initial.entities]);
|
|
594
579
|
const relations = structuredClone([...initial.relations]);
|
|
595
|
-
const system = fact.kind === "
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
580
|
+
const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
|
|
581
|
+
const entityKind = fact.kind;
|
|
582
|
+
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
583
|
+
const durationMs = fact.kind === "image" ? null : fact.durationMs;
|
|
584
|
+
const asset = ensureAsset(entities, {
|
|
585
|
+
system,
|
|
586
|
+
key: fact.assetId,
|
|
587
|
+
storageKey: fact.storageKey
|
|
588
|
+
}, idFactory);
|
|
589
|
+
const madeFromAsset = entities.filter((row) => row.entityId !== asset.entityId && assetIdOf(relations, row.entityId) === asset.entityId);
|
|
590
|
+
if (madeFromAsset.length > 1) throw new Error(`Duplicate media identities for ${fact.assetId}; reuse ${madeFromAsset[0].entityId}`);
|
|
591
|
+
const existing = madeFromAsset[0];
|
|
592
|
+
if (existing !== void 0) {
|
|
593
|
+
if (existing.entityKind !== entityKind || existing.payload.durationMs !== durationMs) throw new Error(`Media facts conflict with existing identity for ${fact.assetId}`);
|
|
594
|
+
const voiceEntityId = bindVoiceTimbre(entities, relations, existing.entityId, timbre, idFactory);
|
|
604
595
|
return {
|
|
605
596
|
rows: {
|
|
606
597
|
entities,
|
|
607
598
|
relations
|
|
608
599
|
},
|
|
609
|
-
contentEntityId:
|
|
600
|
+
contentEntityId: existing.entityId,
|
|
601
|
+
assetEntityId: asset.entityId,
|
|
602
|
+
...voiceEntityId
|
|
610
603
|
};
|
|
611
604
|
}
|
|
612
605
|
const entityId = createEntityId(idFactory("entity"));
|
|
613
606
|
if (entities.some((row) => row.entityId === entityId)) throw new Error(`Duplicate entity id ${entityId}`);
|
|
614
|
-
|
|
607
|
+
entities.push({
|
|
615
608
|
entityId,
|
|
616
|
-
entityKind
|
|
617
|
-
payload: {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
...fact.kind === "voice" && fact.voice !== void 0 ? { voice: {
|
|
634
|
-
system: fact.voice.system,
|
|
635
|
-
key: fact.voice.key,
|
|
636
|
-
...fact.voice.name === void 0 ? {} : { name: fact.voice.name }
|
|
637
|
-
} } : {}
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
entities.push(variant);
|
|
609
|
+
entityKind,
|
|
610
|
+
payload: { durationMs }
|
|
611
|
+
});
|
|
612
|
+
relations.push(assetLink(entityId, asset.entityId, idFactory));
|
|
613
|
+
if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
|
|
614
|
+
const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
|
|
615
|
+
if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
|
|
616
|
+
relations.push({
|
|
617
|
+
relationId: createRelationId(idFactory("relation")),
|
|
618
|
+
relationKind: "phonetic-script-render",
|
|
619
|
+
endpoint0EntityId: entityId,
|
|
620
|
+
endpoint1EntityId: phonetic.entityId,
|
|
621
|
+
metadata: {},
|
|
622
|
+
trace: {}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const voiceEntityId = bindVoiceTimbre(entities, relations, entityId, timbre, idFactory);
|
|
641
626
|
const rows = {
|
|
642
627
|
entities,
|
|
643
628
|
relations
|
|
@@ -646,33 +631,102 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
646
631
|
assertCanonicalEditorResources(rows);
|
|
647
632
|
return {
|
|
648
633
|
rows,
|
|
649
|
-
contentEntityId: entityId
|
|
634
|
+
contentEntityId: entityId,
|
|
635
|
+
assetEntityId: asset.entityId,
|
|
636
|
+
...voiceEntityId
|
|
650
637
|
};
|
|
651
638
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
if (
|
|
657
|
-
|
|
658
|
-
|
|
639
|
+
/** One Asset per `(system, key)`: the bytes are named in exactly one row. */
|
|
640
|
+
function ensureAsset(entities, locator, idFactory) {
|
|
641
|
+
const found = entities.find((row) => row.entityKind === "asset" && row.payload.system === locator.system && row.payload.key === locator.key);
|
|
642
|
+
if (found !== void 0) {
|
|
643
|
+
if (locator.storageKey === void 0 || found.payload.storageKey === locator.storageKey) return found;
|
|
644
|
+
if (found.payload.storageKey !== void 0) throw new Error(`Media facts conflict with existing storage key for ${locator.key}`);
|
|
645
|
+
const filled = {
|
|
646
|
+
...found,
|
|
659
647
|
payload: {
|
|
660
|
-
...
|
|
661
|
-
storageKey:
|
|
648
|
+
...found.payload,
|
|
649
|
+
storageKey: locator.storageKey
|
|
662
650
|
}
|
|
663
651
|
};
|
|
652
|
+
replaceEntity$1(entities, found.entityId, filled);
|
|
653
|
+
return filled;
|
|
664
654
|
}
|
|
665
|
-
|
|
655
|
+
const asset = {
|
|
656
|
+
entityId: createEntityId(idFactory("entity")),
|
|
657
|
+
entityKind: "asset",
|
|
658
|
+
payload: {
|
|
659
|
+
system: locator.system,
|
|
660
|
+
key: locator.key,
|
|
661
|
+
...locator.storageKey === void 0 ? {} : { storageKey: locator.storageKey }
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
entities.push(asset);
|
|
665
|
+
return asset;
|
|
666
|
+
}
|
|
667
|
+
function assetLink(entityId, assetEntityId, idFactory) {
|
|
668
|
+
return {
|
|
669
|
+
relationId: createRelationId(idFactory("relation")),
|
|
670
|
+
relationKind: "from-asset",
|
|
671
|
+
endpoint0EntityId: createEntityId(entityId),
|
|
672
|
+
endpoint1EntityId: createEntityId(assetEntityId),
|
|
673
|
+
metadata: {},
|
|
674
|
+
trace: {}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
function assetIdOf(relations, entityId) {
|
|
678
|
+
const link = relations.find((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === entityId || relation.endpoint1EntityId === entityId));
|
|
679
|
+
if (link === void 0) return void 0;
|
|
680
|
+
return String(link.endpoint0EntityId === entityId ? link.endpoint1EntityId : link.endpoint0EntityId);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Attach the timbre a synthesized voiceover was rendered with. One Voice
|
|
684
|
+
* identity is shared by every take naming the same library entry, so an
|
|
685
|
+
* existing row is reused rather than duplicated. Recorded media declares no
|
|
686
|
+
* timbre and keeps no link.
|
|
687
|
+
*/
|
|
688
|
+
function bindVoiceTimbre(entities, relations, audioEntityId, descriptor, idFactory) {
|
|
689
|
+
const linked = relations.filter((relation) => relation.relationKind === "voice-timbre" && (relation.endpoint0EntityId === audioEntityId || relation.endpoint1EntityId === audioEntityId));
|
|
690
|
+
if (linked.length > 1) throw new Error(`Voiceover ${audioEntityId} has multiple Voice timbre relations`);
|
|
691
|
+
if (descriptor === void 0) {
|
|
692
|
+
if (linked.length > 0) throw new Error(`Recorded media ${audioEntityId} cannot keep a synthesized Voice timbre`);
|
|
693
|
+
return {};
|
|
694
|
+
}
|
|
695
|
+
const voice = entities.find((row) => row.entityKind === "voice" && sameVoiceDescriptor(row.payload.voice, descriptor)) ?? appendVoiceIdentity(entities, descriptor, idFactory);
|
|
696
|
+
const existing = linked[0];
|
|
697
|
+
if (existing === void 0) relations.push({
|
|
698
|
+
relationId: createRelationId(idFactory("relation")),
|
|
699
|
+
relationKind: "voice-timbre",
|
|
700
|
+
endpoint0EntityId: createEntityId(audioEntityId),
|
|
701
|
+
endpoint1EntityId: createEntityId(voice.entityId),
|
|
702
|
+
metadata: {},
|
|
703
|
+
trace: {}
|
|
704
|
+
});
|
|
705
|
+
else if (existing.endpoint0EntityId !== audioEntityId || existing.endpoint1EntityId !== voice.entityId) throw new Error(`Voiceover ${audioEntityId} already carries a different Voice timbre`);
|
|
706
|
+
return { voiceEntityId: voice.entityId };
|
|
707
|
+
}
|
|
708
|
+
function appendVoiceIdentity(entities, descriptor, idFactory) {
|
|
709
|
+
const voice = {
|
|
710
|
+
entityId: createEntityId(idFactory("entity")),
|
|
711
|
+
entityKind: "voice",
|
|
712
|
+
payload: { voice: {
|
|
713
|
+
system: descriptor.system,
|
|
714
|
+
key: descriptor.key,
|
|
715
|
+
...descriptor.name === void 0 ? {} : { name: descriptor.name }
|
|
716
|
+
} }
|
|
717
|
+
};
|
|
718
|
+
entities.push(voice);
|
|
719
|
+
return voice;
|
|
720
|
+
}
|
|
721
|
+
/** Two takes share a Voice identity only when they name the same library entry. */
|
|
722
|
+
function sameVoiceDescriptor(stored, descriptor) {
|
|
723
|
+
return isJsonObject(stored) && stored.system === descriptor.system && stored.key === descriptor.key && stored.name === descriptor.name;
|
|
666
724
|
}
|
|
667
725
|
function replaceEntity$1(entities, entityId, replacement) {
|
|
668
726
|
const index = entities.findIndex((row) => row.entityId === entityId);
|
|
669
727
|
if (index < 0) throw new Error(`Entity id ${entityId} does not exist`);
|
|
670
728
|
entities[index] = replacement;
|
|
671
729
|
}
|
|
672
|
-
function sameVoice(left, right) {
|
|
673
|
-
if (right === void 0) return left === void 0;
|
|
674
|
-
return isJsonObject(left) && left.system === right.system && left.key === right.key && left.name === right.name && Object.keys(left).every((key) => key === "system" || key === "key" || key === "name");
|
|
675
|
-
}
|
|
676
730
|
//#endregion
|
|
677
731
|
//#region src/entity-editor/entity-timeline-editor.ts
|
|
678
732
|
/**
|
|
@@ -1208,20 +1262,20 @@ var EntityTimelineEditor = class {
|
|
|
1208
1262
|
hostClipEntityId: input.hostClipEntityId,
|
|
1209
1263
|
anchorOffset: input.anchorOffset
|
|
1210
1264
|
};
|
|
1211
|
-
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify
|
|
1265
|
+
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.anchorOffset !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/anchorOffset, not both");
|
|
1212
1266
|
requireVolume(input.volume);
|
|
1213
1267
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1214
1268
|
replaceRows(draft, imported.rows);
|
|
1215
|
-
const
|
|
1269
|
+
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
|
|
1216
1270
|
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1217
|
-
this.ensurePhoneticRender(draft,
|
|
1271
|
+
this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
|
|
1218
1272
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
1219
1273
|
const existing = draft.entities.find((row) => row.entityId === input.voiceoverClipEntityId);
|
|
1220
1274
|
let voiceoverClipEntityId;
|
|
1221
1275
|
if (existing === void 0) voiceoverClipEntityId = this.insertPlacedClipIntoDraft(draft, {
|
|
1222
1276
|
clipEntityId: input.voiceoverClipEntityId,
|
|
1223
1277
|
trackEntityId: speechTrack.entityId,
|
|
1224
|
-
contentEntityId:
|
|
1278
|
+
contentEntityId: voiceover.entityId,
|
|
1225
1279
|
sourceRange: {
|
|
1226
1280
|
start: 0,
|
|
1227
1281
|
end: input.media.durationMs
|
|
@@ -1245,7 +1299,7 @@ var EntityTimelineEditor = class {
|
|
|
1245
1299
|
});
|
|
1246
1300
|
this.relinkRelation(draft, topology.markerContentRelation, {
|
|
1247
1301
|
endpoint0EntityId: topology.marker.entityId,
|
|
1248
|
-
endpoint1EntityId:
|
|
1302
|
+
endpoint1EntityId: voiceover.entityId
|
|
1249
1303
|
});
|
|
1250
1304
|
this.applyClipPlacement(draft, topology.clip.entityId, placement);
|
|
1251
1305
|
const placed = requireClipTopology(draft, topology.clip.entityId);
|
|
@@ -1264,11 +1318,12 @@ var EntityTimelineEditor = class {
|
|
|
1264
1318
|
}
|
|
1265
1319
|
});
|
|
1266
1320
|
}
|
|
1267
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1321
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceover.entityId, phonetic.entityId, input.captions);
|
|
1268
1322
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1269
1323
|
return {
|
|
1270
1324
|
voiceoverClipEntityId,
|
|
1271
|
-
|
|
1325
|
+
voiceoverAudioEntityId: voiceover.entityId,
|
|
1326
|
+
...imported.voiceEntityId === void 0 ? {} : { voiceEntityId: imported.voiceEntityId },
|
|
1272
1327
|
phoneticScriptEntityId: phonetic.entityId,
|
|
1273
1328
|
audioScriptEntityId: requireComposedAudioScript(draft, phonetic.entityId),
|
|
1274
1329
|
captionClipEntityIds: input.captions.map((caption) => caption.captionClipEntityId)
|
|
@@ -1383,13 +1438,7 @@ var EntityTimelineEditor = class {
|
|
|
1383
1438
|
entityId: input.captionEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.captionEntityId),
|
|
1384
1439
|
entityKind: "caption",
|
|
1385
1440
|
payload: {
|
|
1386
|
-
|
|
1387
|
-
kind: "bounded",
|
|
1388
|
-
start: 0,
|
|
1389
|
-
end: durationMs
|
|
1390
|
-
},
|
|
1391
|
-
sampling: "native",
|
|
1392
|
-
coordinateSpace: "milliseconds",
|
|
1441
|
+
durationMs,
|
|
1393
1442
|
baseEntityIds: captionBaseIds(input.baseEntityIds),
|
|
1394
1443
|
selection,
|
|
1395
1444
|
...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
|
|
@@ -1474,23 +1523,23 @@ var EntityTimelineEditor = class {
|
|
|
1474
1523
|
draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
|
|
1475
1524
|
return track;
|
|
1476
1525
|
}
|
|
1477
|
-
/** The existing pronunciation variant a
|
|
1526
|
+
/** The existing pronunciation variant a voiceover consumes; composition is required. */
|
|
1478
1527
|
requireComposedPhoneticScript(draft, phoneticScriptEntityId) {
|
|
1479
1528
|
const phonetic = requireEntityKind(draft, phoneticScriptEntityId, "phonetic-script");
|
|
1480
1529
|
requireComposedAudioScript(draft, phonetic.entityId);
|
|
1481
1530
|
return phonetic;
|
|
1482
1531
|
}
|
|
1483
1532
|
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1484
|
-
ensurePhoneticRender(draft,
|
|
1485
|
-
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId ===
|
|
1486
|
-
if (renders.length > 1) throw new Error(`
|
|
1533
|
+
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
1534
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
|
|
1535
|
+
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
1487
1536
|
if (renders[0] !== void 0) {
|
|
1488
|
-
if (otherEndpoint(renders[0],
|
|
1489
|
-
throw new Error(`
|
|
1537
|
+
if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
|
|
1538
|
+
throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1490
1539
|
}
|
|
1491
|
-
draft.relations.push(this.createRelation(draft, "phonetic-script-render",
|
|
1540
|
+
draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
1492
1541
|
}
|
|
1493
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1542
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
|
|
1494
1543
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
1495
1544
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
1496
1545
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
@@ -1507,13 +1556,7 @@ var EntityTimelineEditor = class {
|
|
|
1507
1556
|
entityId: this.mintEntityId(draft),
|
|
1508
1557
|
entityKind: "caption",
|
|
1509
1558
|
payload: {
|
|
1510
|
-
|
|
1511
|
-
kind: "bounded",
|
|
1512
|
-
start: 0,
|
|
1513
|
-
end: durationMs
|
|
1514
|
-
},
|
|
1515
|
-
sampling: "native",
|
|
1516
|
-
coordinateSpace: "milliseconds",
|
|
1559
|
+
durationMs,
|
|
1517
1560
|
baseEntityIds,
|
|
1518
1561
|
selection,
|
|
1519
1562
|
...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
|
|
@@ -1521,7 +1564,7 @@ var EntityTimelineEditor = class {
|
|
|
1521
1564
|
};
|
|
1522
1565
|
draft.entities.push(content);
|
|
1523
1566
|
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1524
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId,
|
|
1567
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
|
|
1525
1568
|
start: startMs,
|
|
1526
1569
|
end: startMs + durationMs
|
|
1527
1570
|
} }));
|
|
@@ -1578,7 +1621,7 @@ var EntityTimelineEditor = class {
|
|
|
1578
1621
|
}
|
|
1579
1622
|
}
|
|
1580
1623
|
});
|
|
1581
|
-
this.replaceCaptionAlignment(draft, content.entityId,
|
|
1624
|
+
this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
|
|
1582
1625
|
}
|
|
1583
1626
|
}
|
|
1584
1627
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
@@ -1587,10 +1630,10 @@ var EntityTimelineEditor = class {
|
|
|
1587
1630
|
const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
|
|
1588
1631
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1589
1632
|
}
|
|
1590
|
-
replaceCaptionAlignment(draft, captionEntityId,
|
|
1633
|
+
replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
|
|
1591
1634
|
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
|
|
1592
1635
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1593
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId,
|
|
1636
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
|
|
1594
1637
|
start: startMs,
|
|
1595
1638
|
end: startMs + durationMs
|
|
1596
1639
|
} }));
|
|
@@ -1999,11 +2042,19 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
1999
2042
|
}, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
|
|
2000
2043
|
assertSupportedLegacy(legacy);
|
|
2001
2044
|
if (initial.entities.some((row) => row.entityKind === "timeline")) fail("Timeline already exists; migration is not an editing operation");
|
|
2002
|
-
if (legacy
|
|
2045
|
+
if (hasResolvedPlacement(legacy)) fail("An entity projection cannot be migrated as legacy state");
|
|
2003
2046
|
const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
|
|
2004
2047
|
let entities = structuredClone([...initial.entities]);
|
|
2005
2048
|
let relations = structuredClone([...initial.relations]);
|
|
2006
|
-
const documentAudioScriptEntityId = ensureDocumentAudioScript(entities
|
|
2049
|
+
const documentAudioScriptEntityId = ensureDocumentAudioScript(entities) ?? (() => {
|
|
2050
|
+
const id = createEntityId(idFactory("entity"));
|
|
2051
|
+
entities.push({
|
|
2052
|
+
entityId: id,
|
|
2053
|
+
entityKind: "audio-script",
|
|
2054
|
+
payload: { segments: [] }
|
|
2055
|
+
});
|
|
2056
|
+
return String(id);
|
|
2057
|
+
})();
|
|
2007
2058
|
const addEntity = (kind, payload, existingId) => {
|
|
2008
2059
|
const entityId = createEntityId(existingId ?? idFactory("entity"));
|
|
2009
2060
|
if (entities.some((row) => row.entityId === entityId)) fail(`Migration entity identity collision: ${entityId}`);
|
|
@@ -2053,7 +2104,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2053
2104
|
relations
|
|
2054
2105
|
}, {
|
|
2055
2106
|
assetId: speech.origin_speech_id,
|
|
2056
|
-
kind: "
|
|
2107
|
+
kind: "voiceover",
|
|
2057
2108
|
durationMs: speech.media_duration_ms,
|
|
2058
2109
|
storageKey: speech.audio_storage_key,
|
|
2059
2110
|
voice: {
|
|
@@ -2141,8 +2192,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2141
2192
|
} else if (part.caption !== void 0) {
|
|
2142
2193
|
const caption = part.caption;
|
|
2143
2194
|
duration = caption.initial_duration_ms;
|
|
2144
|
-
const
|
|
2145
|
-
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId ===
|
|
2195
|
+
const voiceover = contentByPartId.get(caption.speech_part_id);
|
|
2196
|
+
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceover);
|
|
2146
2197
|
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2147
2198
|
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2148
2199
|
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
@@ -2158,18 +2209,12 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2158
2209
|
} }
|
|
2159
2210
|
};
|
|
2160
2211
|
content = addEntity("caption", {
|
|
2161
|
-
|
|
2162
|
-
kind: "bounded",
|
|
2163
|
-
start: 0,
|
|
2164
|
-
end: duration
|
|
2165
|
-
},
|
|
2166
|
-
sampling: "native",
|
|
2167
|
-
coordinateSpace: "milliseconds",
|
|
2212
|
+
durationMs: duration,
|
|
2168
2213
|
baseEntityIds: [script.entityId],
|
|
2169
2214
|
selection,
|
|
2170
2215
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2171
2216
|
});
|
|
2172
|
-
link("caption-alignment", content,
|
|
2217
|
+
link("caption-alignment", content, voiceover, { alignment: {
|
|
2173
2218
|
start: caption.start_ms,
|
|
2174
2219
|
end: caption.start_ms + duration
|
|
2175
2220
|
} });
|
|
@@ -2225,27 +2270,23 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2225
2270
|
entities,
|
|
2226
2271
|
relations
|
|
2227
2272
|
};
|
|
2228
|
-
assertEntityProjectionPreservesLegacy(legacy, projectEntityTimeline(rows
|
|
2273
|
+
assertEntityProjectionPreservesLegacy(legacy, projectEntityTimeline(rows), rows);
|
|
2229
2274
|
return rows;
|
|
2230
2275
|
}
|
|
2231
2276
|
/** Server gate: a cutover may change representation, never the existing render facts. */
|
|
2232
2277
|
function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
2233
2278
|
assertSupportedLegacy(legacy);
|
|
2234
|
-
if (projection
|
|
2279
|
+
if (!hasResolvedPlacement(projection)) fail("Expected an entity projection for migration");
|
|
2235
2280
|
const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
|
|
2236
|
-
const compatibilityFacts = (document) => {
|
|
2237
|
-
const
|
|
2281
|
+
const compatibilityFacts = (document, placementResolved) => {
|
|
2282
|
+
const view = fromVideoDocument(document, { placementResolved });
|
|
2238
2283
|
for (const part of Object.values(view.part_library ?? {})) {
|
|
2239
2284
|
const speech = part.speech;
|
|
2240
2285
|
if (speech?.phoneme_script === null) delete speech.phoneme_script;
|
|
2241
2286
|
}
|
|
2242
|
-
|
|
2243
|
-
return {
|
|
2244
|
-
meta,
|
|
2245
|
-
view
|
|
2246
|
-
};
|
|
2287
|
+
return { view };
|
|
2247
2288
|
};
|
|
2248
|
-
if (canonical(compatibilityFacts(legacy)) !== canonical(compatibilityFacts(projection))) fail("Entity migration must preserve all existing timeline facts; edit after cutover");
|
|
2289
|
+
if (canonical(compatibilityFacts(legacy, false)) !== canonical(compatibilityFacts(projection, true))) fail("Entity migration must preserve all existing timeline facts; edit after cutover");
|
|
2249
2290
|
const trackOrders = [];
|
|
2250
2291
|
for (const oldTrack of legacy.tracks ?? []) {
|
|
2251
2292
|
const candidates = rows.entities.filter((row) => row.entityKind === "track" && (oldTrack.id === void 0 ? row.payload.role === oldTrack.parts_kind : row.entityId === oldTrack.id));
|
|
@@ -2512,21 +2553,21 @@ function requirePart(document, partId) {
|
|
|
2512
2553
|
if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
|
|
2513
2554
|
return part;
|
|
2514
2555
|
}
|
|
2515
|
-
function ensureSpeechPhoneticChain(
|
|
2516
|
-
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId ===
|
|
2517
|
-
if (renders.length > 1) fail(`
|
|
2556
|
+
function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2557
|
+
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceoverAudioEntityId);
|
|
2558
|
+
if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
|
|
2518
2559
|
if (renders.length === 1) {
|
|
2519
2560
|
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
2520
|
-
if (phonetic === void 0) fail(`
|
|
2561
|
+
if (phonetic === void 0) fail(`Voiceover ${voiceoverAudioEntityId} has a dangling PhoneticScript render`);
|
|
2521
2562
|
const script = composedAudioScript(phonetic, entities);
|
|
2522
|
-
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`
|
|
2563
|
+
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voiceover ${voiceoverAudioEntityId} conflicts with the legacy Speech script`);
|
|
2523
2564
|
return phonetic.entityId;
|
|
2524
2565
|
}
|
|
2525
2566
|
const script = entities.find((entity) => entity.entityId === documentAudioScriptEntityId);
|
|
2526
2567
|
if (script === void 0 || script.entityKind !== "audio-script") fail("Migration must carry the fixed document AudioScript");
|
|
2527
2568
|
const segments = script.payload.segments;
|
|
2528
2569
|
if (Array.isArray(segments) && segments.length > 0) {
|
|
2529
|
-
if (scriptText(script) !== audioScriptText) fail(`
|
|
2570
|
+
if (scriptText(script) !== audioScriptText) fail(`Voiceover ${voiceoverAudioEntityId} conflicts with the project AudioScript text`);
|
|
2530
2571
|
} else {
|
|
2531
2572
|
const scriptIndex = entities.findIndex((row) => row.entityId === script.entityId);
|
|
2532
2573
|
entities[scriptIndex] = {
|
|
@@ -2541,7 +2582,7 @@ function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript
|
|
|
2541
2582
|
baseEntityIds: [script.entityId],
|
|
2542
2583
|
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2543
2584
|
});
|
|
2544
|
-
link("phonetic-script-render",
|
|
2585
|
+
link("phonetic-script-render", voiceoverAudioEntityId, phonetic);
|
|
2545
2586
|
return phonetic;
|
|
2546
2587
|
}
|
|
2547
2588
|
function composedAudioScript(phonetic, entities) {
|
|
@@ -2659,13 +2700,7 @@ function prepareCaptionContent(rows, input) {
|
|
|
2659
2700
|
payload: {
|
|
2660
2701
|
...previous?.payload,
|
|
2661
2702
|
baseEntityIds,
|
|
2662
|
-
|
|
2663
|
-
kind: "bounded",
|
|
2664
|
-
start: 0,
|
|
2665
|
-
end: input.durationMs
|
|
2666
|
-
},
|
|
2667
|
-
sampling: previous?.payload.sampling ?? "native",
|
|
2668
|
-
coordinateSpace: previous?.payload.coordinateSpace ?? "milliseconds"
|
|
2703
|
+
durationMs: previous?.payload.durationMs ?? input.durationMs
|
|
2669
2704
|
}
|
|
2670
2705
|
};
|
|
2671
2706
|
const graph = {
|
|
@@ -3648,7 +3683,7 @@ function writeSpeechAssets(draft, assets) {
|
|
|
3648
3683
|
if (caption == null) continue;
|
|
3649
3684
|
const priorCaption = draft.part_library?.[captionId]?.caption;
|
|
3650
3685
|
setPart(draft, captionId, { caption: {
|
|
3651
|
-
...priorCaption,
|
|
3686
|
+
...priorCaption ?? { style: {} },
|
|
3652
3687
|
id: captionId,
|
|
3653
3688
|
kind: "caption",
|
|
3654
3689
|
initial_duration_ms: caption.duration_ms,
|
|
@@ -4057,6 +4092,10 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4057
4092
|
client;
|
|
4058
4093
|
doc;
|
|
4059
4094
|
adapter;
|
|
4095
|
+
/** Whether this document's timeline is owned by the Entity graph. */
|
|
4096
|
+
isEntityDocument() {
|
|
4097
|
+
return this.adapter.isEntityDocument();
|
|
4098
|
+
}
|
|
4060
4099
|
editor;
|
|
4061
4100
|
/**
|
|
4062
4101
|
* The version the server is known to hold. Starts empty (nothing confirmed)
|
|
@@ -4075,17 +4114,24 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4075
4114
|
* Fetches the snapshot up front rather than starting empty and converging: the
|
|
4076
4115
|
* agent's first act is to read the document, so there is no useful state before
|
|
4077
4116
|
* the snapshot lands. This also fails fast and loudly on a document that does
|
|
4078
|
-
* not exist
|
|
4079
|
-
* waiting forever for content that will never arrive.
|
|
4117
|
+
* not exist or whose first snapshot is incomplete or invalid.
|
|
4080
4118
|
*/
|
|
4081
4119
|
static async open(options) {
|
|
4082
|
-
const doc = new LoroDoc();
|
|
4083
|
-
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4084
4120
|
const response = await options.client.fetchSnapshot();
|
|
4085
|
-
doc
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4121
|
+
const doc = new LoroDoc();
|
|
4122
|
+
let manualSyncDoc;
|
|
4123
|
+
try {
|
|
4124
|
+
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4125
|
+
if (doc.import(base64ToBytes(response.snapshot)).pending != null) throw new Error("Initial mengine snapshot has missing dependencies");
|
|
4126
|
+
manualSyncDoc = new ManualSyncDoc(options.client, doc);
|
|
4127
|
+
assertValidVideoDocument(manualSyncDoc.snapshot());
|
|
4128
|
+
manualSyncDoc.watermark = manualSyncDoc.serverVVFrom(response.version.server_vv) ?? doc.oplogVersion();
|
|
4129
|
+
return manualSyncDoc;
|
|
4130
|
+
} catch (error) {
|
|
4131
|
+
manualSyncDoc?.adapter.dispose();
|
|
4132
|
+
doc.free();
|
|
4133
|
+
throw error;
|
|
4134
|
+
}
|
|
4089
4135
|
}
|
|
4090
4136
|
/** Current document read model (authoritative shape). */
|
|
4091
4137
|
snapshot() {
|
|
@@ -4104,9 +4150,9 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4104
4150
|
editorPeerId() {
|
|
4105
4151
|
return this.doc.peerIdStr;
|
|
4106
4152
|
}
|
|
4107
|
-
/** Current
|
|
4108
|
-
|
|
4109
|
-
return fromVideoDocument(this.adapter.snapshot());
|
|
4153
|
+
/** Current content projected into the compatible track and part read shape. */
|
|
4154
|
+
content() {
|
|
4155
|
+
return fromVideoDocument(this.adapter.snapshot(), { placementResolved: this.isEntityDocument() });
|
|
4110
4156
|
}
|
|
4111
4157
|
/**
|
|
4112
4158
|
* Replay a deterministic sandbox journal into this document.
|
|
@@ -4126,9 +4172,8 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4126
4172
|
*
|
|
4127
4173
|
* This pair replaces the legacy integer-version comparison that
|
|
4128
4174
|
* `DraftVersionDetector` used to tell the LLM "the draft was modified
|
|
4129
|
-
* externally, reload before editing".
|
|
4130
|
-
*
|
|
4131
|
-
* vectors restores the same capability.
|
|
4175
|
+
* externally, reload before editing". Comparing Loro version vectors covers
|
|
4176
|
+
* all collaborative edits without a business revision field in the document.
|
|
4132
4177
|
*
|
|
4133
4178
|
* Same capability, not a stronger one: like the legacy detector, this only
|
|
4134
4179
|
* reports what changed between two moments the caller chose to sample.
|
|
@@ -4212,6 +4257,7 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4212
4257
|
if (serverVV != null) this.watermark = serverVV;
|
|
4213
4258
|
return {
|
|
4214
4259
|
kind: response.kind,
|
|
4260
|
+
mengineUpdateSeq: response.version?.update_seq ?? response.update_seq ?? void 0,
|
|
4215
4261
|
updateSeq: response.update_seq ?? void 0,
|
|
4216
4262
|
collaborated: serverVV != null && !covers(localVersion, serverVV)
|
|
4217
4263
|
};
|
|
@@ -4250,22 +4296,24 @@ var ManualSyncDoc = class ManualSyncDoc {
|
|
|
4250
4296
|
* recovers whatever the stream missed while it was down. Recovery therefore
|
|
4251
4297
|
* lives in the synchronizer (keyed on the real doc version), not here.
|
|
4252
4298
|
*
|
|
4253
|
-
*
|
|
4299
|
+
* Update frames are forwarded via `onUpdate`, resync hints via `onResync`; this
|
|
4254
4300
|
* connection keeps no version cursor and does no catch-up of its own. Dedup is
|
|
4255
4301
|
* unnecessary because `LoroDoc.import` is idempotent by OpId/VV.
|
|
4256
4302
|
*/
|
|
4257
4303
|
var SseConnection = class {
|
|
4258
4304
|
client;
|
|
4259
4305
|
onUpdate;
|
|
4306
|
+
onResync;
|
|
4260
4307
|
reconnectDelayMs;
|
|
4261
4308
|
inner = void 0;
|
|
4262
4309
|
event = new EventBus();
|
|
4263
4310
|
streamTask = null;
|
|
4264
4311
|
_status = "idle";
|
|
4265
4312
|
_error;
|
|
4266
|
-
constructor(client, onUpdate, reconnectDelayMs) {
|
|
4313
|
+
constructor(client, onUpdate, onResync, reconnectDelayMs) {
|
|
4267
4314
|
this.client = client;
|
|
4268
4315
|
this.onUpdate = onUpdate;
|
|
4316
|
+
this.onResync = onResync;
|
|
4269
4317
|
this.reconnectDelayMs = reconnectDelayMs;
|
|
4270
4318
|
}
|
|
4271
4319
|
get status() {
|
|
@@ -4326,10 +4374,12 @@ var SseConnection = class {
|
|
|
4326
4374
|
client: this.client,
|
|
4327
4375
|
signal,
|
|
4328
4376
|
onOpen: () => this.setStatus("connected"),
|
|
4377
|
+
onResync: this.onResync,
|
|
4329
4378
|
onUpdate: (event) => {
|
|
4330
4379
|
for (const update of event.updates) this.onUpdate(update);
|
|
4331
4380
|
}
|
|
4332
4381
|
});
|
|
4382
|
+
if (!signal.aborted) this.setStatus("connecting", /* @__PURE__ */ new Error("SSE stream closed"));
|
|
4333
4383
|
} catch (error) {
|
|
4334
4384
|
if (signal.aborted) break;
|
|
4335
4385
|
this.setStatus("connecting", error instanceof Error ? error : new Error(String(error)));
|
|
@@ -4374,7 +4424,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4374
4424
|
this.options = options;
|
|
4375
4425
|
this.client = options.client;
|
|
4376
4426
|
this.docId = options.docId;
|
|
4377
|
-
this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), options.sseReconnectDelayMs ?? 500);
|
|
4427
|
+
this.connection = new SseConnection(this.client, (update) => this.emitUpdate(update), () => this.events.emit("resync"), options.sseReconnectDelayMs ?? 500);
|
|
4378
4428
|
}
|
|
4379
4429
|
get isReadonly() {
|
|
4380
4430
|
return this.options.readonlyMode ?? false;
|
|
@@ -4382,6 +4432,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4382
4432
|
async getDoc(docId) {
|
|
4383
4433
|
this.assertDocId(docId);
|
|
4384
4434
|
const snapshot = await this.client.fetchSnapshot();
|
|
4435
|
+
this.reportServerVersion(snapshot.version?.server_vv);
|
|
4385
4436
|
const now = /* @__PURE__ */ new Date();
|
|
4386
4437
|
return {
|
|
4387
4438
|
docId,
|
|
@@ -4393,6 +4444,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4393
4444
|
async getDocDiff(docId, knownVersion) {
|
|
4394
4445
|
this.assertDocId(docId);
|
|
4395
4446
|
const response = await this.client.sync(knownVersion);
|
|
4447
|
+
this.reportServerVersion(response.server_vv);
|
|
4396
4448
|
return {
|
|
4397
4449
|
docId,
|
|
4398
4450
|
missing: base64ToBytes(response.update),
|
|
@@ -4423,7 +4475,7 @@ var MedeoHttpDocStorage = class {
|
|
|
4423
4475
|
if (update.data.byteLength === 0) return {};
|
|
4424
4476
|
try {
|
|
4425
4477
|
const response = await this.client.pushUpdate(update.data);
|
|
4426
|
-
const serverVV =
|
|
4478
|
+
const serverVV = this.reportServerVersion(response.version?.server_vv);
|
|
4427
4479
|
this.events.emit("pushOutcome", {
|
|
4428
4480
|
kind: response.kind,
|
|
4429
4481
|
updateSeq: response.update_seq ?? void 0,
|
|
@@ -4451,6 +4503,19 @@ var MedeoHttpDocStorage = class {
|
|
|
4451
4503
|
subscribePushOutcome(callback) {
|
|
4452
4504
|
return this.events.on("pushOutcome", callback);
|
|
4453
4505
|
}
|
|
4506
|
+
/** Observe live hints that require VV catch-up without replacing the SSE connection. */
|
|
4507
|
+
subscribeResync(callback) {
|
|
4508
|
+
return this.events.on("resync", callback);
|
|
4509
|
+
}
|
|
4510
|
+
/** Authoritative HTTP reads and accepted pushes, never inferred from SSE bytes. */
|
|
4511
|
+
subscribeServerVersion(callback) {
|
|
4512
|
+
return this.events.on("serverVersion", callback);
|
|
4513
|
+
}
|
|
4514
|
+
reportServerVersion(encoded) {
|
|
4515
|
+
const version = decodeServerVV(encoded);
|
|
4516
|
+
if (version != null) this.events.emit("serverVersion", version);
|
|
4517
|
+
return version;
|
|
4518
|
+
}
|
|
4454
4519
|
async deleteDoc(_docId) {
|
|
4455
4520
|
throw new Error("MedeoHttpDocStorage does not support deleteDoc");
|
|
4456
4521
|
}
|
|
@@ -4567,6 +4632,118 @@ var MemoryDocStorage = class extends BaseDocStorage {
|
|
|
4567
4632
|
}
|
|
4568
4633
|
};
|
|
4569
4634
|
//#endregion
|
|
4635
|
+
//#region src/session/document-undo-manager.ts
|
|
4636
|
+
/**
|
|
4637
|
+
* Tracks the shared LoroDoc independently of semantic editing. Undo/redo use
|
|
4638
|
+
* the same mutation guard as the adapter and publish settled availability.
|
|
4639
|
+
*/
|
|
4640
|
+
var DocumentUndoManager = class {
|
|
4641
|
+
doc;
|
|
4642
|
+
mutationGuard;
|
|
4643
|
+
onChange;
|
|
4644
|
+
manager;
|
|
4645
|
+
unsubscribe;
|
|
4646
|
+
disposed = false;
|
|
4647
|
+
notificationQueued = false;
|
|
4648
|
+
published = {
|
|
4649
|
+
canUndo: false,
|
|
4650
|
+
canRedo: false
|
|
4651
|
+
};
|
|
4652
|
+
activeSource = null;
|
|
4653
|
+
constructor(doc, mutationGuard, onChange) {
|
|
4654
|
+
this.doc = doc;
|
|
4655
|
+
this.mutationGuard = mutationGuard;
|
|
4656
|
+
this.onChange = onChange;
|
|
4657
|
+
const peer = doc.peerIdStr;
|
|
4658
|
+
this.manager = new UndoManager(doc, {
|
|
4659
|
+
mergeInterval: 0,
|
|
4660
|
+
maxUndoSteps: 100,
|
|
4661
|
+
excludeOriginPrefixes: ["mengine.bootstrap", "mengine.initialize"],
|
|
4662
|
+
onPush: (_isUndo, range) => {
|
|
4663
|
+
this.scheduleNotification();
|
|
4664
|
+
return {
|
|
4665
|
+
value: this.activeSource ?? {
|
|
4666
|
+
peer,
|
|
4667
|
+
counter: range.start
|
|
4668
|
+
},
|
|
4669
|
+
cursors: []
|
|
4670
|
+
};
|
|
4671
|
+
},
|
|
4672
|
+
onPop: () => this.scheduleNotification()
|
|
4673
|
+
});
|
|
4674
|
+
this.unsubscribe = doc.subscribe(() => this.scheduleNotification());
|
|
4675
|
+
}
|
|
4676
|
+
getState() {
|
|
4677
|
+
this.assertActive();
|
|
4678
|
+
return {
|
|
4679
|
+
canUndo: this.manager.canUndo(),
|
|
4680
|
+
canRedo: this.manager.canRedo()
|
|
4681
|
+
};
|
|
4682
|
+
}
|
|
4683
|
+
undo(options) {
|
|
4684
|
+
return this.applyUndoRedo("Undo", options);
|
|
4685
|
+
}
|
|
4686
|
+
redo(options) {
|
|
4687
|
+
return this.applyUndoRedo("Redo", options);
|
|
4688
|
+
}
|
|
4689
|
+
beginGroup() {
|
|
4690
|
+
this.assertActive();
|
|
4691
|
+
this.mutationGuard.run(() => this.manager.groupStart());
|
|
4692
|
+
}
|
|
4693
|
+
endGroup() {
|
|
4694
|
+
this.assertActive();
|
|
4695
|
+
this.mutationGuard.run(() => this.manager.groupEnd());
|
|
4696
|
+
this.scheduleNotification();
|
|
4697
|
+
}
|
|
4698
|
+
applyUndoRedo(kind, options) {
|
|
4699
|
+
this.assertActive();
|
|
4700
|
+
return this.mutationGuard.run(() => {
|
|
4701
|
+
if (kind === "Undo" ? !this.manager.canUndo() : !this.manager.canRedo()) return false;
|
|
4702
|
+
this.activeSource = kind === "Undo" ? this.manager.topUndoValue() : this.manager.topRedoValue();
|
|
4703
|
+
try {
|
|
4704
|
+
this.doc.setNextCommitOptions({ message: JSON.stringify({
|
|
4705
|
+
semantic_op: kind,
|
|
4706
|
+
payload: { source: this.activeSource },
|
|
4707
|
+
intent: options?.intent ?? null,
|
|
4708
|
+
actor: options?.actor ?? null
|
|
4709
|
+
}) });
|
|
4710
|
+
const before = this.doc.oplogFrontiers();
|
|
4711
|
+
return (kind === "Undo" ? this.manager.undo() : this.manager.redo()) && this.doc.cmpWithFrontiers(before) !== 0;
|
|
4712
|
+
} finally {
|
|
4713
|
+
this.doc.clearNextCommitOptions();
|
|
4714
|
+
this.activeSource = null;
|
|
4715
|
+
this.scheduleNotification();
|
|
4716
|
+
}
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4719
|
+
dispose() {
|
|
4720
|
+
if (this.disposed) return;
|
|
4721
|
+
this.disposed = true;
|
|
4722
|
+
this.unsubscribe();
|
|
4723
|
+
this.manager.setOnPush(void 0);
|
|
4724
|
+
this.manager.setOnPop(void 0);
|
|
4725
|
+
this.manager.free();
|
|
4726
|
+
}
|
|
4727
|
+
scheduleNotification() {
|
|
4728
|
+
if (this.notificationQueued || this.disposed) return;
|
|
4729
|
+
this.notificationQueued = true;
|
|
4730
|
+
queueMicrotask(() => {
|
|
4731
|
+
this.notificationQueued = false;
|
|
4732
|
+
if (!this.disposed) this.publish();
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
publish() {
|
|
4736
|
+
if (this.disposed) return;
|
|
4737
|
+
const state = this.getState();
|
|
4738
|
+
if (state.canUndo === this.published.canUndo && state.canRedo === this.published.canRedo) return;
|
|
4739
|
+
this.published = state;
|
|
4740
|
+
this.onChange();
|
|
4741
|
+
}
|
|
4742
|
+
assertActive() {
|
|
4743
|
+
if (this.disposed) throw new Error("mengine doc undo manager is disposed");
|
|
4744
|
+
}
|
|
4745
|
+
};
|
|
4746
|
+
//#endregion
|
|
4570
4747
|
//#region src/session/mengine-doc-session.ts
|
|
4571
4748
|
/**
|
|
4572
4749
|
* Raised by {@link MengineDocSession.waitForServerAck} when the local edits it
|
|
@@ -4602,13 +4779,9 @@ var MengineAckFailedError = class extends Error {
|
|
|
4602
4779
|
* A live editing session for one Medeo document — the single entry point clients
|
|
4603
4780
|
* (FE draft driver, Agent, embeds) use to open, edit, and observe a document.
|
|
4604
4781
|
*
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4607
|
-
*
|
|
4608
|
-
* ├─ ClientServerSynchronizer ── DocManager ── LoroDoc
|
|
4609
|
-
* MedeoHttpDocStorage┘ │
|
|
4610
|
-
* (remote = Rust mengine-server) ▼
|
|
4611
|
-
* MirrorVideoDocumentAdapter + SemanticEditor
|
|
4782
|
+
* SemanticEditor writes through MirrorVideoDocumentAdapter directly, while
|
|
4783
|
+
* DocumentUndoManager observes the same LoroDoc independently. Both mutation
|
|
4784
|
+
* paths use the session's DocumentMutationGuard, closed before teardown.
|
|
4612
4785
|
*
|
|
4613
4786
|
* `DocManager` owns the `LoroDoc`: local edits committed on the adapter are
|
|
4614
4787
|
* picked up via `subscribeLocalUpdates`, saved to local storage, then pushed to
|
|
@@ -4629,11 +4802,20 @@ var MengineDocSession = class {
|
|
|
4629
4802
|
manager;
|
|
4630
4803
|
events = new EventBus();
|
|
4631
4804
|
disposables = new DisposableSet();
|
|
4805
|
+
mutationGuard = new DocumentMutationGuard();
|
|
4632
4806
|
adapterValue = null;
|
|
4633
4807
|
editorValue = null;
|
|
4808
|
+
undoManagerValue = null;
|
|
4634
4809
|
started = false;
|
|
4810
|
+
startTask = null;
|
|
4811
|
+
destroyed = false;
|
|
4812
|
+
lifetime = null;
|
|
4813
|
+
hasConnected = false;
|
|
4814
|
+
initialVersionValue = null;
|
|
4815
|
+
localVersionValue = null;
|
|
4816
|
+
pushErrorValue = null;
|
|
4635
4817
|
/**
|
|
4636
|
-
* Latest server oplog version seen on
|
|
4818
|
+
* Latest server oplog version seen on an authoritative HTTP response. Lets `waitForServerAck`
|
|
4637
4819
|
* return without waiting when the server is already known to cover the local
|
|
4638
4820
|
* doc (e.g. nothing was edited since the last confirmed push).
|
|
4639
4821
|
*/
|
|
@@ -4662,14 +4844,14 @@ var MengineDocSession = class {
|
|
|
4662
4844
|
return this.server.subscribePushOutcome(callback);
|
|
4663
4845
|
}
|
|
4664
4846
|
/**
|
|
4665
|
-
* Resolve once the server has durably accepted the local
|
|
4847
|
+
* Resolve once the server has durably accepted the local commits as of
|
|
4666
4848
|
* *now* — the capability an Agent tool needs to answer "did my edit land?"
|
|
4667
4849
|
* before reporting success.
|
|
4668
4850
|
*
|
|
4669
4851
|
* Call it right after the editor ops whose durability matters. It snapshots the
|
|
4670
|
-
* local
|
|
4671
|
-
*
|
|
4672
|
-
*
|
|
4852
|
+
* latest local commit's version (or the loaded baseline before any edit) and
|
|
4853
|
+
* resolves when an authoritative server version covers it. Remote-only
|
|
4854
|
+
* imports do not extend this target, matching the displayed confirmation state.
|
|
4673
4855
|
*
|
|
4674
4856
|
* Version coverage, not byte identity, is the acceptance test — for two reasons:
|
|
4675
4857
|
* the ops may reach the server inside a merged blob rather than as the exact
|
|
@@ -4684,31 +4866,39 @@ var MengineDocSession = class {
|
|
|
4684
4866
|
* @throws {MengineAckFailedError} on timeout, rejection, or transport failure.
|
|
4685
4867
|
*/
|
|
4686
4868
|
async waitForServerAck(options = {}) {
|
|
4687
|
-
if (this.adapterValue == null) throw new Error("mengine doc session is not started");
|
|
4869
|
+
if (this.adapterValue == null || this.lifetime == null) throw new Error("mengine doc session is not started");
|
|
4688
4870
|
const timeoutMs = options.timeoutMs ?? 15e3;
|
|
4689
|
-
const target = this.manager.connectDoc(this.docId).version();
|
|
4871
|
+
const target = this.confirmationTarget() ?? this.manager.connectDoc(this.docId).version();
|
|
4872
|
+
const signal = this.lifetime.signal;
|
|
4690
4873
|
if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) return;
|
|
4691
4874
|
await new Promise((resolve, reject) => {
|
|
4692
|
-
|
|
4875
|
+
const cleanup = [];
|
|
4693
4876
|
let done = false;
|
|
4694
4877
|
const settle = (error) => {
|
|
4695
4878
|
if (done) return;
|
|
4696
4879
|
done = true;
|
|
4697
4880
|
clearTimeout(timer);
|
|
4698
|
-
off();
|
|
4881
|
+
cleanup.forEach((off) => off());
|
|
4699
4882
|
if (error == null) resolve();
|
|
4700
4883
|
else reject(error);
|
|
4701
4884
|
};
|
|
4702
4885
|
const timer = setTimeout(() => {
|
|
4703
4886
|
settle(new MengineAckFailedError("timeout", void 0, void 0, `mengine did not acknowledge the update within ${timeoutMs}ms`));
|
|
4704
4887
|
}, timeoutMs);
|
|
4705
|
-
|
|
4888
|
+
const onAbort = () => settle(new MengineAckFailedError("failed", void 0, void 0, "mengine doc session destroyed"));
|
|
4889
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
4890
|
+
cleanup.push(() => signal.removeEventListener("abort", onAbort));
|
|
4891
|
+
cleanup.push(this.events.on("syncState", () => {
|
|
4892
|
+
if (this.serverVVValue != null && serverCovers(this.serverVVValue, target)) settle(void 0);
|
|
4893
|
+
}));
|
|
4894
|
+
cleanup.push(this.server.subscribePushOutcome((outcome) => {
|
|
4706
4895
|
if (outcome.kind === "rejected" || outcome.kind === "failed") {
|
|
4707
4896
|
settle(new MengineAckFailedError(outcome.kind, outcome.code, outcome.error, outcome.error?.message ?? `mengine push ${outcome.kind}`));
|
|
4708
4897
|
return;
|
|
4709
4898
|
}
|
|
4710
|
-
|
|
4711
|
-
|
|
4899
|
+
}));
|
|
4900
|
+
if (signal.aborted) onAbort();
|
|
4901
|
+
else this.synchronizer.requestSync(this.docId);
|
|
4712
4902
|
});
|
|
4713
4903
|
}
|
|
4714
4904
|
/**
|
|
@@ -4744,44 +4934,154 @@ var MengineDocSession = class {
|
|
|
4744
4934
|
if (this.adapterValue == null) throw new Error("mengine doc session is not started");
|
|
4745
4935
|
return this.adapterValue.snapshot();
|
|
4746
4936
|
}
|
|
4937
|
+
/** Current-session undo/redo availability. Loading and closed sessions report false. */
|
|
4938
|
+
getUndoState() {
|
|
4939
|
+
return this.undoManagerValue?.getState() ?? {
|
|
4940
|
+
canUndo: false,
|
|
4941
|
+
canRedo: false
|
|
4942
|
+
};
|
|
4943
|
+
}
|
|
4944
|
+
/**
|
|
4945
|
+
* Supplies current availability immediately, then coalesces stack changes in a
|
|
4946
|
+
* microtask. Session teardown publishes empty availability synchronously.
|
|
4947
|
+
*/
|
|
4948
|
+
subscribeUndoState(cb) {
|
|
4949
|
+
const notify = () => cb(this.getUndoState());
|
|
4950
|
+
const off = this.destroyed ? () => {} : this.events.on("undoState", notify);
|
|
4951
|
+
notify();
|
|
4952
|
+
return off;
|
|
4953
|
+
}
|
|
4954
|
+
/**
|
|
4955
|
+
* Undo a local edit immediately; server confirmation remains asynchronous.
|
|
4956
|
+
* On a shared LWW field this writes its prior value, potentially overwriting a
|
|
4957
|
+
* later remote edit to that same field. Returns false if no update is produced.
|
|
4958
|
+
*/
|
|
4959
|
+
undo(options) {
|
|
4960
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4961
|
+
return this.undoManagerValue.undo(options);
|
|
4962
|
+
}
|
|
4963
|
+
/**
|
|
4964
|
+
* Restore the state visible before undo without rerunning external side effects.
|
|
4965
|
+
* Returns false if no update is produced; durable confirmation is separate.
|
|
4966
|
+
*/
|
|
4967
|
+
redo(options) {
|
|
4968
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4969
|
+
return this.undoManagerValue.redo(options);
|
|
4970
|
+
}
|
|
4971
|
+
/**
|
|
4972
|
+
* Combine existing semantic operations (e.g. trim a clip and adjust its volume)
|
|
4973
|
+
* or repeated commits during one drag gesture into a single undo step. A gesture
|
|
4974
|
+
* that only previews locally and commits once on release needs no group.
|
|
4975
|
+
* Each commit is immediately visible locally and enters sync independently;
|
|
4976
|
+
* a later failure does not roll it back.
|
|
4977
|
+
* Always pair with endUndoGroup in a finally block. Conflicting remote imports
|
|
4978
|
+
* can end/split the group early, and unrelated local edits would join it.
|
|
4979
|
+
* Complete asynchronous asset preparation before opening the group.
|
|
4980
|
+
*/
|
|
4981
|
+
beginUndoGroup() {
|
|
4982
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4983
|
+
this.undoManagerValue.beginGroup();
|
|
4984
|
+
}
|
|
4985
|
+
/** End a gesture, including one already split by a conflicting remote import. */
|
|
4986
|
+
endUndoGroup() {
|
|
4987
|
+
if (this.undoManagerValue == null) throw new Error("mengine doc undo manager is not ready");
|
|
4988
|
+
this.undoManagerValue.endGroup();
|
|
4989
|
+
}
|
|
4747
4990
|
/**
|
|
4748
4991
|
* Start the sync stack and connect the document.
|
|
4749
4992
|
*
|
|
4750
|
-
* Returns
|
|
4751
|
-
*
|
|
4752
|
-
*
|
|
4993
|
+
* Returns after DocManager loads a local snapshot, or after the first remote
|
|
4994
|
+
* sync when no local snapshot exists. A valid empty document is ready too.
|
|
4995
|
+
* Cached documents can open before SSE connects; subsequent remote changes
|
|
4996
|
+
* surface through `subscribe`.
|
|
4753
4997
|
*/
|
|
4754
|
-
|
|
4755
|
-
if (this.
|
|
4998
|
+
start() {
|
|
4999
|
+
if (this.destroyed) return Promise.reject(/* @__PURE__ */ new Error("mengine doc session destroyed"));
|
|
5000
|
+
if (this.startTask != null) return this.startTask;
|
|
4756
5001
|
this.started = true;
|
|
4757
|
-
this.
|
|
4758
|
-
this.
|
|
5002
|
+
this.lifetime = new AbortController();
|
|
5003
|
+
this.startTask = this.open().catch((error) => {
|
|
5004
|
+
const wasDestroyed = this.destroyed;
|
|
5005
|
+
this.destroy();
|
|
5006
|
+
if (wasDestroyed) throw new Error("mengine doc session destroyed");
|
|
5007
|
+
throw error;
|
|
5008
|
+
});
|
|
5009
|
+
return this.startTask;
|
|
5010
|
+
}
|
|
5011
|
+
async open() {
|
|
5012
|
+
this.disposables.add(this.server.subscribeResync(() => this.synchronizer.requestSync(this.docId)));
|
|
5013
|
+
this.disposables.add(this.server.subscribeServerVersion((version) => {
|
|
5014
|
+
this.serverVVValue = version;
|
|
5015
|
+
if (this.getSyncState().confirmation === "confirmed") this.pushErrorValue = null;
|
|
5016
|
+
this.emitSyncState();
|
|
5017
|
+
}));
|
|
5018
|
+
this.disposables.add(this.server.subscribePushOutcome((outcome) => {
|
|
5019
|
+
this.pushErrorValue = outcome.kind === "failed" || outcome.kind === "rejected" ? outcome.error ?? /* @__PURE__ */ new Error(`mengine push ${outcome.kind}`) : null;
|
|
5020
|
+
this.emitSyncState();
|
|
5021
|
+
}));
|
|
5022
|
+
this.disposables.add(this.server.connection.onStatusChanged((status) => {
|
|
5023
|
+
if (status === "connected") this.hasConnected = true;
|
|
5024
|
+
this.emitSyncState();
|
|
5025
|
+
}));
|
|
4759
5026
|
this.disposables.add(() => this.local.connection.disconnect());
|
|
4760
5027
|
this.disposables.add(() => this.server.connection.disconnect());
|
|
5028
|
+
this.local.connection.connect();
|
|
5029
|
+
await this.local.connection.waitForConnected().abortOn(this.lifetime.signal);
|
|
5030
|
+
if (this.destroyed) throw new Error("mengine doc session destroyed");
|
|
5031
|
+
this.server.connection.connect();
|
|
4761
5032
|
this.synchronizer.start();
|
|
4762
5033
|
this.manager.start();
|
|
4763
5034
|
this.disposables.add(() => this.synchronizer.stop());
|
|
4764
5035
|
this.disposables.add(() => this.manager.stop());
|
|
4765
5036
|
const doc = this.manager.connectDoc(this.docId);
|
|
4766
|
-
if (this.options.peerId != null) doc.setPeerId(this.options.peerId);
|
|
4767
|
-
this.adapterValue = new MirrorVideoDocumentAdapter(doc);
|
|
4768
|
-
this.editorValue = new SemanticEditor(this.adapterValue);
|
|
4769
5037
|
this.disposables.add(() => this.manager.disconnectDoc(this.docId));
|
|
5038
|
+
if (this.options.peerId != null) doc.setPeerId(this.options.peerId);
|
|
5039
|
+
this.adapterValue = new MirrorVideoDocumentAdapter(doc, this.mutationGuard);
|
|
4770
5040
|
const unsubscribe = doc.subscribe((event) => {
|
|
4771
5041
|
if (this.adapterValue == null) return;
|
|
5042
|
+
if (event.by === "local") this.localVersionValue = doc.version();
|
|
5043
|
+
this.emitSyncState();
|
|
4772
5044
|
this.events.emit("update", {
|
|
4773
5045
|
source: event.by === "local" ? "local" : "remote",
|
|
4774
5046
|
snapshot: this.adapterValue.snapshot()
|
|
4775
5047
|
});
|
|
4776
5048
|
});
|
|
4777
5049
|
this.disposables.add(unsubscribe);
|
|
4778
|
-
this.disposables.add(this.manager.onDocStateChange(this.docId, (state) =>
|
|
4779
|
-
|
|
4780
|
-
|
|
5050
|
+
this.disposables.add(this.manager.onDocStateChange(this.docId, (state) => {
|
|
5051
|
+
this.events.emit("state", state);
|
|
5052
|
+
this.emitSyncState();
|
|
4781
5053
|
}));
|
|
4782
|
-
await this.
|
|
5054
|
+
await this.waitForInitialLoad();
|
|
5055
|
+
if (this.destroyed) throw new Error("mengine doc session destroyed");
|
|
5056
|
+
assertValidVideoDocument(this.snapshot());
|
|
5057
|
+
this.undoManagerValue = new DocumentUndoManager(doc, this.mutationGuard, () => this.events.emit("undoState"));
|
|
5058
|
+
this.editorValue = new SemanticEditor(this.adapterValue);
|
|
5059
|
+
this.initialVersionValue = doc.version();
|
|
5060
|
+
this.emitSyncState();
|
|
4783
5061
|
return this.snapshot();
|
|
4784
5062
|
}
|
|
5063
|
+
getSyncState() {
|
|
5064
|
+
const state = this.getState();
|
|
5065
|
+
const target = this.confirmationTarget();
|
|
5066
|
+
const confirmation = this.serverVVValue == null || target == null ? this.localVersionValue == null ? "unknown" : "pending" : serverCovers(this.serverVVValue, target) ? "confirmed" : "pending";
|
|
5067
|
+
const connection = !this.started ? "disconnected" : this.server.connection.status === "connected" ? "connected" : this.hasConnected || this.server.connection.error != null ? "disconnected" : "connecting";
|
|
5068
|
+
return {
|
|
5069
|
+
confirmation,
|
|
5070
|
+
phase: connection === "disconnected" ? "offline" : this.pushErrorValue != null || state.error != null ? "failed" : connection === "connecting" ? "connecting" : state.syncing ? "syncing" : "idle"
|
|
5071
|
+
};
|
|
5072
|
+
}
|
|
5073
|
+
confirmationTarget() {
|
|
5074
|
+
return this.localVersionValue ?? this.initialVersionValue;
|
|
5075
|
+
}
|
|
5076
|
+
/** Immediately supplies current state; unsubscribe when the host changes docs. */
|
|
5077
|
+
subscribeSyncState(cb) {
|
|
5078
|
+
const off = this.events.on("syncState", cb);
|
|
5079
|
+
cb(this.getSyncState());
|
|
5080
|
+
return off;
|
|
5081
|
+
}
|
|
5082
|
+
emitSyncState() {
|
|
5083
|
+
this.events.emit("syncState", this.getSyncState());
|
|
5084
|
+
}
|
|
4785
5085
|
subscribe(cb) {
|
|
4786
5086
|
return this.events.on("update", cb);
|
|
4787
5087
|
}
|
|
@@ -4792,40 +5092,48 @@ var MengineDocSession = class {
|
|
|
4792
5092
|
return this.manager.getDocState(this.docId);
|
|
4793
5093
|
}
|
|
4794
5094
|
destroy() {
|
|
5095
|
+
if (this.destroyed) return;
|
|
5096
|
+
this.destroyed = true;
|
|
5097
|
+
this.mutationGuard.close();
|
|
5098
|
+
this.started = false;
|
|
5099
|
+
this.lifetime?.abort();
|
|
5100
|
+
this.undoManagerValue?.dispose();
|
|
5101
|
+
this.undoManagerValue = null;
|
|
5102
|
+
this.events.emit("undoState");
|
|
4795
5103
|
this.disposables.dispose();
|
|
5104
|
+
this.emitSyncState();
|
|
4796
5105
|
this.events.clearAll();
|
|
5106
|
+
this.adapterValue?.dispose();
|
|
4797
5107
|
this.adapterValue = null;
|
|
4798
5108
|
this.editorValue = null;
|
|
4799
5109
|
this.started = false;
|
|
4800
5110
|
}
|
|
4801
|
-
/**
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
5111
|
+
/** Local absence needs a remote sync; empty business content does not. */
|
|
5112
|
+
async waitForInitialLoad() {
|
|
5113
|
+
const signal = this.lifetime.signal;
|
|
5114
|
+
await new Promise((resolve, reject) => {
|
|
5115
|
+
let off = () => {};
|
|
5116
|
+
let offConnection = () => {};
|
|
5117
|
+
const finish = (error) => {
|
|
5118
|
+
off();
|
|
5119
|
+
offConnection();
|
|
5120
|
+
signal.removeEventListener("abort", onAbort);
|
|
5121
|
+
if (error != null) reject(error);
|
|
5122
|
+
else resolve();
|
|
5123
|
+
};
|
|
5124
|
+
const onAbort = () => finish(/* @__PURE__ */ new Error("mengine doc session destroyed"));
|
|
5125
|
+
const check = (state) => {
|
|
5126
|
+
if (state.loaded && (state.loadedFromLocal || state.synced)) finish();
|
|
5127
|
+
else if (state.error != null) finish(new Error(state.error));
|
|
5128
|
+
else if (state.loaded && this.server.connection.error != null) finish(this.server.connection.error);
|
|
5129
|
+
};
|
|
5130
|
+
off = this.onStateChange(check);
|
|
5131
|
+
offConnection = this.server.connection.onStatusChanged(() => check(this.getState()));
|
|
5132
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5133
|
+
if (signal.aborted) onAbort();
|
|
5134
|
+
else check(this.getState());
|
|
4819
5135
|
});
|
|
4820
5136
|
}
|
|
4821
|
-
hasContent() {
|
|
4822
|
-
if (this.adapterValue == null) return false;
|
|
4823
|
-
try {
|
|
4824
|
-
return this.adapterValue.hasContent();
|
|
4825
|
-
} catch {
|
|
4826
|
-
return false;
|
|
4827
|
-
}
|
|
4828
|
-
}
|
|
4829
5137
|
};
|
|
4830
5138
|
/**
|
|
4831
5139
|
* True when the *encoded* server version `serverVV` includes everything in
|
|
@@ -4849,4 +5157,4 @@ function serverCovers(serverVV, target) {
|
|
|
4849
5157
|
return covers(server, target);
|
|
4850
5158
|
}
|
|
4851
5159
|
//#endregion
|
|
4852
|
-
export { DEFAULT_UNIT_TIME_MS,
|
|
5160
|
+
export { DEFAULT_UNIT_TIME_MS, 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, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, 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, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|