@mengine/medeo-client 1.3.1-alpha.0 → 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 +26 -11
- package/dist/{document-Caidonf8.js → document-BxWxPFTj.js} +1056 -122
- package/dist/{index-WxMNUbOa.d.ts → index-Bs60N9Hl.d.ts} +36 -9
- package/dist/index.d.ts +215 -20
- package/dist/index.js +313 -168
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +5 -4
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,
|
|
@@ -231,8 +292,7 @@ const roleOrder$1 = {
|
|
|
231
292
|
* Image/Video/Audio/Voice Entity must carry its own external locator, every
|
|
232
293
|
* external asset id has exactly one claimant, and no media variant may be
|
|
233
294
|
* bound through a legacy `physical-asset` Relation. Dual-entity media graphs
|
|
234
|
-
* are rejected, not migrated. Caption
|
|
235
|
-
* `physical-asset` binding.
|
|
295
|
+
* are rejected, not migrated. Caption composes AudioScript text and has no physical text Asset.
|
|
236
296
|
*/
|
|
237
297
|
function assertCanonicalEditorResources(rows) {
|
|
238
298
|
if (rows.entities.filter((row) => row.entityKind === "timeline").length > 1) throw new EntityTimelineProjectionError("Editor requires exactly one Timeline");
|
|
@@ -263,8 +323,7 @@ function assertCanonicalEditorResources(rows) {
|
|
|
263
323
|
*
|
|
264
324
|
* 1. every Image/Video/Audio/Voice Entity carries its own Asset `external`
|
|
265
325
|
* identity;
|
|
266
|
-
* 2. no `physical-asset` Relation may bind a media variant
|
|
267
|
-
* text keeps its inline Asset and binding);
|
|
326
|
+
* 2. no `physical-asset` Relation may bind a media variant or Caption;
|
|
268
327
|
* 3. a self-carried media identity is immutable — it cannot be rewritten to
|
|
269
328
|
* another asset id.
|
|
270
329
|
*
|
|
@@ -298,6 +357,26 @@ function memotaLocator(value) {
|
|
|
298
357
|
key: value.key
|
|
299
358
|
};
|
|
300
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
|
+
}
|
|
301
380
|
/** Add only missing editor structure; existing identities and owned settings are never replaced. */
|
|
302
381
|
function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`, timelinePayload = {}) {
|
|
303
382
|
const entities = structuredClone([...initial.entities]);
|
|
@@ -350,13 +429,15 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
350
429
|
}
|
|
351
430
|
trackEntityIds[role] = track.entityId;
|
|
352
431
|
}
|
|
432
|
+
const audioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
|
|
353
433
|
return {
|
|
354
434
|
rows: {
|
|
355
435
|
entities,
|
|
356
436
|
relations
|
|
357
437
|
},
|
|
358
438
|
timelineEntityId: timeline.entityId,
|
|
359
|
-
trackEntityIds
|
|
439
|
+
trackEntityIds,
|
|
440
|
+
audioScriptEntityId
|
|
360
441
|
};
|
|
361
442
|
}
|
|
362
443
|
//#endregion
|
|
@@ -366,7 +447,7 @@ const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left,
|
|
|
366
447
|
if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
|
|
367
448
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
368
449
|
} };
|
|
369
|
-
/** Browser-safe authenticated
|
|
450
|
+
/** Browser-safe authenticated Loro client for a document's Entity graph. */
|
|
370
451
|
var EntityGraphHttpClient = class {
|
|
371
452
|
options;
|
|
372
453
|
transport;
|
|
@@ -381,30 +462,24 @@ var EntityGraphHttpClient = class {
|
|
|
381
462
|
return this.request({ method: "GET" });
|
|
382
463
|
}
|
|
383
464
|
/**
|
|
384
|
-
*
|
|
465
|
+
* Compile edits against the original causal snapshot and publish native Loro operations.
|
|
385
466
|
*
|
|
386
467
|
* Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
|
|
387
468
|
* transient create-then-remove never becomes a request to delete unknown
|
|
388
469
|
* persisted data.
|
|
389
470
|
*/
|
|
390
471
|
async commit(baseState, nextRows, options = {}) {
|
|
391
|
-
if (
|
|
392
|
-
const baseRows = cloneAndValidateRows(baseState.rows);
|
|
472
|
+
if (options.migrationBaseVv !== void 0) throw new Error("Legacy entity migration is not supported by the Loro contract");
|
|
393
473
|
const next = cloneAndValidateRows(nextRows);
|
|
394
|
-
assertMediaAssetWritePolicy(
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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) {
|
|
399
480
|
return this.request({
|
|
400
481
|
method: "POST",
|
|
401
|
-
body: JSON.stringify({
|
|
402
|
-
expected_revision: baseState.revision,
|
|
403
|
-
rows: toWireRows(next),
|
|
404
|
-
deleted_entity_ids: deletedEntityIds,
|
|
405
|
-
deleted_relation_ids: deletedRelationIds,
|
|
406
|
-
...options.migrationBaseVv === void 0 ? {} : { migration_base_vv: options.migrationBaseVv }
|
|
407
|
-
})
|
|
482
|
+
body: JSON.stringify({ update })
|
|
408
483
|
});
|
|
409
484
|
}
|
|
410
485
|
async request(init) {
|
|
@@ -422,10 +497,16 @@ function parseWireState(value, expectedDocId) {
|
|
|
422
497
|
if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
423
498
|
const rows = parseWireRows(value.rows);
|
|
424
499
|
decodeEntityRelationRows(rows, numericMarkerValidation$1);
|
|
425
|
-
|
|
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 = {
|
|
426
503
|
revision: value.revision,
|
|
427
|
-
|
|
504
|
+
loroSnapshot: value.loro_snapshot,
|
|
505
|
+
rows,
|
|
506
|
+
audioScriptEntityId: value.audio_script_entity_id
|
|
428
507
|
};
|
|
508
|
+
readDocumentAudioScript(state);
|
|
509
|
+
return state;
|
|
429
510
|
}
|
|
430
511
|
function parseWireRows(value) {
|
|
431
512
|
if (!isRecord(value) || !Array.isArray(value.entities) || !Array.isArray(value.relations)) throw new Error("Invalid entity-state response rows");
|
|
@@ -458,23 +539,6 @@ function cloneAndValidateRows(rows) {
|
|
|
458
539
|
decodeEntityRelationRows(clone, numericMarkerValidation$1);
|
|
459
540
|
return clone;
|
|
460
541
|
}
|
|
461
|
-
function toWireRows(rows) {
|
|
462
|
-
return {
|
|
463
|
-
entities: rows.entities.map((row) => ({
|
|
464
|
-
entity_id: row.entityId,
|
|
465
|
-
entity_kind: row.entityKind,
|
|
466
|
-
payload: row.payload
|
|
467
|
-
})),
|
|
468
|
-
relations: rows.relations.map((row) => ({
|
|
469
|
-
relation_id: row.relationId,
|
|
470
|
-
relation_kind: row.relationKind,
|
|
471
|
-
endpoint_0_entity_id: row.endpoint0EntityId,
|
|
472
|
-
endpoint_1_entity_id: row.endpoint1EntityId,
|
|
473
|
-
metadata: row.metadata,
|
|
474
|
-
trace: row.trace
|
|
475
|
-
}))
|
|
476
|
-
};
|
|
477
|
-
}
|
|
478
542
|
function requiredString(value, field) {
|
|
479
543
|
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`Invalid ${field} in entity-state response`);
|
|
480
544
|
return value;
|
|
@@ -608,6 +672,23 @@ function sameVoice(left, right) {
|
|
|
608
672
|
}
|
|
609
673
|
//#endregion
|
|
610
674
|
//#region src/entity-editor/entity-timeline-editor.ts
|
|
675
|
+
/**
|
|
676
|
+
* Resolve the AudioScript a PhoneticScript composes. The composition is the
|
|
677
|
+
* required text source: a variant without it has no content and fails here.
|
|
678
|
+
*/
|
|
679
|
+
function requireComposedAudioScript(rows, phoneticScriptEntityId) {
|
|
680
|
+
return assemblePhoneticScriptContent(rows, phoneticScriptEntityId).audioScript.entityId;
|
|
681
|
+
}
|
|
682
|
+
function captionSelections(selections) {
|
|
683
|
+
return structuredClone(selections);
|
|
684
|
+
}
|
|
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");
|
|
691
|
+
}
|
|
611
692
|
const roleOrder = {
|
|
612
693
|
caption: 0,
|
|
613
694
|
video_clip: 1,
|
|
@@ -1129,7 +1210,8 @@ var EntityTimelineEditor = class {
|
|
|
1129
1210
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1130
1211
|
replaceRows(draft, imported.rows);
|
|
1131
1212
|
const voice = requireEntityKind(draft, imported.contentEntityId, "voice");
|
|
1132
|
-
const
|
|
1213
|
+
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1214
|
+
this.ensurePhoneticRender(draft, voice.entityId, phonetic.entityId);
|
|
1133
1215
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
1134
1216
|
const existing = draft.entities.find((row) => row.entityId === input.voiceoverClipEntityId);
|
|
1135
1217
|
let voiceoverClipEntityId;
|
|
@@ -1179,12 +1261,13 @@ var EntityTimelineEditor = class {
|
|
|
1179
1261
|
}
|
|
1180
1262
|
});
|
|
1181
1263
|
}
|
|
1182
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voice.entityId,
|
|
1264
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voice.entityId, phonetic.entityId, input.captions);
|
|
1183
1265
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1184
1266
|
return {
|
|
1185
1267
|
voiceoverClipEntityId,
|
|
1186
1268
|
voiceEntityId: voice.entityId,
|
|
1187
|
-
|
|
1269
|
+
phoneticScriptEntityId: phonetic.entityId,
|
|
1270
|
+
audioScriptEntityId: requireComposedAudioScript(draft, phonetic.entityId),
|
|
1188
1271
|
captionClipEntityIds: input.captions.map((caption) => caption.captionClipEntityId)
|
|
1189
1272
|
};
|
|
1190
1273
|
});
|
|
@@ -1268,33 +1351,30 @@ var EntityTimelineEditor = class {
|
|
|
1268
1351
|
const track = findRoleTrack(draft, input.timelineEntityId, "caption");
|
|
1269
1352
|
if (track === void 0) return;
|
|
1270
1353
|
const patch = cloneJsonObject(input.style, "Caption style");
|
|
1354
|
+
const assembled = new EntityProjectionGraph(draft);
|
|
1271
1355
|
for (const clip of clipsOnTrack(draft, track.entityId)) {
|
|
1272
1356
|
const topology = requireClipTopology(draft, clip.entityId);
|
|
1273
1357
|
const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
|
|
1274
|
-
const prior = caption.payload.style;
|
|
1358
|
+
const prior = assembled.entity(caption.entityId).payload.style;
|
|
1275
1359
|
const style = isJsonObject(prior) ? prior : {};
|
|
1276
|
-
|
|
1277
|
-
...
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
style: {
|
|
1281
|
-
...style,
|
|
1282
|
-
...patch
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
});
|
|
1360
|
+
for (const updated of updateEntityFields(draft, caption.entityId, { style: {
|
|
1361
|
+
...style,
|
|
1362
|
+
...patch
|
|
1363
|
+
} })) replaceEntity(draft, updated.entityId, updated);
|
|
1286
1364
|
}
|
|
1287
1365
|
});
|
|
1288
1366
|
}
|
|
1289
1367
|
/**
|
|
1290
|
-
* Place
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
* the returned Clip later changes the Clip/Marker,
|
|
1368
|
+
* Place a Caption as a composed variant of an existing AudioScript: the
|
|
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;
|
|
1371
|
+
* moving or stretching the returned Clip later changes the Clip/Marker,
|
|
1372
|
+
* never the Caption entity or the AudioScript.
|
|
1294
1373
|
*/
|
|
1295
1374
|
insertCaptionClip(input) {
|
|
1296
1375
|
return this.transact((draft) => {
|
|
1297
1376
|
const durationMs = finitePositive(input.durationMs, "Caption durationMs");
|
|
1377
|
+
const selections = captionSelections(input.selections);
|
|
1298
1378
|
const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
|
|
1299
1379
|
const content = {
|
|
1300
1380
|
entityId: this.mintEntityId(draft),
|
|
@@ -1307,20 +1387,13 @@ var EntityTimelineEditor = class {
|
|
|
1307
1387
|
},
|
|
1308
1388
|
sampling: "native",
|
|
1309
1389
|
coordinateSpace: "milliseconds",
|
|
1310
|
-
|
|
1390
|
+
baseEntityIds: captionBaseIds(input.baseEntityIds),
|
|
1391
|
+
selections,
|
|
1311
1392
|
...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
|
|
1312
1393
|
}
|
|
1313
1394
|
};
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
entityKind: "asset",
|
|
1317
|
-
payload: { inline: {
|
|
1318
|
-
mediaType: "text/plain",
|
|
1319
|
-
text: input.text
|
|
1320
|
-
} }
|
|
1321
|
-
};
|
|
1322
|
-
draft.entities.push(content, asset);
|
|
1323
|
-
draft.relations.push(this.createRelation(draft, "physical-asset", content.entityId, asset.entityId));
|
|
1395
|
+
draft.entities.push(content);
|
|
1396
|
+
assembleCaptionContent(draft, content.entityId);
|
|
1324
1397
|
return this.insertPlacedClipIntoDraft(draft, {
|
|
1325
1398
|
trackEntityId: track.entityId,
|
|
1326
1399
|
contentEntityId: content.entityId,
|
|
@@ -1398,34 +1471,24 @@ var EntityTimelineEditor = class {
|
|
|
1398
1471
|
draft.relations.push(this.createRelation(draft, "timeline-track", timeline.entityId, track.entityId));
|
|
1399
1472
|
return track;
|
|
1400
1473
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1474
|
+
/** The existing pronunciation variant a Voice consumes; composition is required. */
|
|
1475
|
+
requireComposedPhoneticScript(draft, phoneticScriptEntityId) {
|
|
1476
|
+
const phonetic = requireEntityKind(draft, phoneticScriptEntityId, "phonetic-script");
|
|
1477
|
+
requireComposedAudioScript(draft, phonetic.entityId);
|
|
1478
|
+
return phonetic;
|
|
1479
|
+
}
|
|
1480
|
+
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1481
|
+
ensurePhoneticRender(draft, voiceEntityId, phoneticScriptEntityId) {
|
|
1482
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceEntityId || relation.endpoint1EntityId === voiceEntityId));
|
|
1483
|
+
if (renders.length > 1) throw new Error(`Voice "${voiceEntityId}" has multiple PhoneticScript render relations`);
|
|
1404
1484
|
if (renders[0] !== void 0) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
...script,
|
|
1408
|
-
payload: { segments: [{
|
|
1409
|
-
segmentId: "segment-0",
|
|
1410
|
-
text
|
|
1411
|
-
}] }
|
|
1412
|
-
};
|
|
1413
|
-
replaceEntity(draft, script.entityId, replacement);
|
|
1414
|
-
return replacement;
|
|
1485
|
+
if (otherEndpoint(renders[0], voiceEntityId) === phoneticScriptEntityId) return;
|
|
1486
|
+
throw new Error(`Voice "${voiceEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1415
1487
|
}
|
|
1416
|
-
|
|
1417
|
-
entityId: this.mintEntityId(draft),
|
|
1418
|
-
entityKind: "audio-script",
|
|
1419
|
-
payload: { segments: [{
|
|
1420
|
-
segmentId: "segment-0",
|
|
1421
|
-
text
|
|
1422
|
-
}] }
|
|
1423
|
-
};
|
|
1424
|
-
draft.entities.push(script);
|
|
1425
|
-
draft.relations.push(this.createRelation(draft, "audio-script-render", voiceEntityId, script.entityId));
|
|
1426
|
-
return script;
|
|
1488
|
+
draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceEntityId, phoneticScriptEntityId));
|
|
1427
1489
|
}
|
|
1428
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceEntityId,
|
|
1490
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceEntityId, phoneticScriptEntityId, captions) {
|
|
1491
|
+
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
1429
1492
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
1430
1493
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
1431
1494
|
const existing = directAnchorChildren(draft, voiceoverClipEntityId).filter((clipId) => clipRole(draft, clipId) === "caption");
|
|
@@ -1434,6 +1497,8 @@ var EntityTimelineEditor = class {
|
|
|
1434
1497
|
for (const caption of captions) {
|
|
1435
1498
|
const startMs = finiteNonNegative(caption.startMs, "Caption startMs");
|
|
1436
1499
|
const durationMs = finitePositive(caption.durationMs, "Caption durationMs");
|
|
1500
|
+
const selections = captionSelections(caption.selections);
|
|
1501
|
+
const baseEntityIds = captionBaseIds(caption.baseEntityIds);
|
|
1437
1502
|
if (draft.entities.find((entity) => entity.entityId === caption.captionClipEntityId) === void 0) {
|
|
1438
1503
|
const content = {
|
|
1439
1504
|
entityId: this.mintEntityId(draft),
|
|
@@ -1446,23 +1511,17 @@ var EntityTimelineEditor = class {
|
|
|
1446
1511
|
},
|
|
1447
1512
|
sampling: "native",
|
|
1448
1513
|
coordinateSpace: "milliseconds",
|
|
1449
|
-
|
|
1514
|
+
baseEntityIds,
|
|
1515
|
+
selections,
|
|
1450
1516
|
...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
|
|
1451
1517
|
}
|
|
1452
1518
|
};
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
payload: { inline: {
|
|
1457
|
-
mediaType: "text/plain",
|
|
1458
|
-
text: caption.text
|
|
1459
|
-
} }
|
|
1460
|
-
};
|
|
1461
|
-
draft.entities.push(content, asset);
|
|
1462
|
-
draft.relations.push(this.createRelation(draft, "physical-asset", content.entityId, asset.entityId), this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
|
|
1519
|
+
draft.entities.push(content);
|
|
1520
|
+
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1521
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
|
|
1463
1522
|
start: startMs,
|
|
1464
1523
|
end: startMs + durationMs
|
|
1465
|
-
} })
|
|
1524
|
+
} }));
|
|
1466
1525
|
this.insertPlacedClipIntoDraft(draft, {
|
|
1467
1526
|
clipEntityId: caption.captionClipEntityId,
|
|
1468
1527
|
trackEntityId: captionTrack.entityId,
|
|
@@ -1494,27 +1553,12 @@ var EntityTimelineEditor = class {
|
|
|
1494
1553
|
...content,
|
|
1495
1554
|
payload: {
|
|
1496
1555
|
...content.payload,
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
start: 0,
|
|
1500
|
-
end: durationMs
|
|
1501
|
-
},
|
|
1502
|
-
text: caption.text,
|
|
1556
|
+
baseEntityIds,
|
|
1557
|
+
selections,
|
|
1503
1558
|
...Object.keys(style).length === 0 ? {} : { style }
|
|
1504
1559
|
}
|
|
1505
1560
|
});
|
|
1506
|
-
|
|
1507
|
-
if (asset.payload.inline === void 0) throw new Error(`Caption "${caption.captionClipEntityId}" is not inline`);
|
|
1508
|
-
replaceEntity(draft, asset.entityId, {
|
|
1509
|
-
...asset,
|
|
1510
|
-
payload: {
|
|
1511
|
-
...asset.payload,
|
|
1512
|
-
inline: {
|
|
1513
|
-
mediaType: "text/plain",
|
|
1514
|
-
text: caption.text
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
});
|
|
1561
|
+
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1518
1562
|
this.applyClipPlacement(draft, topology.clip.entityId, {
|
|
1519
1563
|
kind: "anchored",
|
|
1520
1564
|
hostClipEntityId: voiceoverClipEntityId,
|
|
@@ -1525,17 +1569,13 @@ var EntityTimelineEditor = class {
|
|
|
1525
1569
|
...placed.marker,
|
|
1526
1570
|
payload: {
|
|
1527
1571
|
...placed.marker.payload,
|
|
1528
|
-
sourceRange: {
|
|
1529
|
-
start: 0,
|
|
1530
|
-
end: durationMs
|
|
1531
|
-
},
|
|
1532
1572
|
duration: {
|
|
1533
1573
|
mode: "fixed",
|
|
1534
1574
|
value: durationMs
|
|
1535
1575
|
}
|
|
1536
1576
|
}
|
|
1537
1577
|
});
|
|
1538
|
-
this.
|
|
1578
|
+
this.replaceCaptionAlignment(draft, content.entityId, voiceEntityId, startMs, durationMs);
|
|
1539
1579
|
}
|
|
1540
1580
|
}
|
|
1541
1581
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
@@ -1544,34 +1584,30 @@ var EntityTimelineEditor = class {
|
|
|
1544
1584
|
const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
|
|
1545
1585
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1546
1586
|
}
|
|
1547
|
-
|
|
1548
|
-
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));
|
|
1549
1589
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1550
1590
|
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceEntityId, { alignment: {
|
|
1551
1591
|
start: startMs,
|
|
1552
1592
|
end: startMs + durationMs
|
|
1553
|
-
} })
|
|
1593
|
+
} }));
|
|
1554
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Remove one placed Caption use. The composed AudioScript outlives the
|
|
1597
|
+
* caption: it may anchor further captions and remains the text owner.
|
|
1598
|
+
*/
|
|
1555
1599
|
removeCaptionClipGraph(draft, clipEntityId) {
|
|
1556
1600
|
const topology = requireClipOnRole(draft, clipEntityId, "caption");
|
|
1557
1601
|
const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
|
|
1558
|
-
const asset = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, caption.entityId, "physical-asset"), caption.entityId), "asset");
|
|
1559
1602
|
const contentRelations = incidentRelations(draft, caption.entityId);
|
|
1560
|
-
const allowed = new Set([
|
|
1561
|
-
"physical-asset",
|
|
1562
|
-
"caption-alignment",
|
|
1563
|
-
"caption-provenance",
|
|
1564
|
-
"marker-content"
|
|
1565
|
-
]);
|
|
1603
|
+
const allowed = new Set(["caption-alignment", "marker-content"]);
|
|
1566
1604
|
const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
|
|
1567
1605
|
if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
|
|
1568
1606
|
this.removeClipGraph(draft, clipEntityId);
|
|
1569
1607
|
if (incidentRelations(draft, caption.entityId).some((relation) => relation.relationKind === "marker-content")) return;
|
|
1570
1608
|
const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
|
|
1571
|
-
const assetIncidents = incidentRelations(draft, asset.entityId);
|
|
1572
|
-
const deleteAsset = asset.payload.inline !== void 0 && assetIncidents.every((relation) => removed.has(relation.relationId));
|
|
1573
1609
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1574
|
-
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId
|
|
1610
|
+
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
|
|
1575
1611
|
}
|
|
1576
1612
|
replaceAnchorHost(draft, childClipEntityId, hostClipEntityId) {
|
|
1577
1613
|
const anchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === childClipEntityId);
|
|
@@ -1964,6 +2000,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
1964
2000
|
const normalizedMainRanges = overlapNormalizedMainRanges(legacy);
|
|
1965
2001
|
let entities = structuredClone([...initial.entities]);
|
|
1966
2002
|
let relations = structuredClone([...initial.relations]);
|
|
2003
|
+
const documentAudioScriptEntityId = ensureDocumentAudioScript(entities, idFactory);
|
|
1967
2004
|
const addEntity = (kind, payload, existingId) => {
|
|
1968
2005
|
const entityId = createEntityId(existingId ?? idFactory("entity"));
|
|
1969
2006
|
if (entities.some((row) => row.entityId === entityId)) fail(`Migration entity identity collision: ${entityId}`);
|
|
@@ -1992,7 +2029,6 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
1992
2029
|
facts.set(fact.assetId, fact);
|
|
1993
2030
|
}
|
|
1994
2031
|
const contentByPartId = /* @__PURE__ */ new Map();
|
|
1995
|
-
const scriptBySpeechClipId = /* @__PURE__ */ new Map();
|
|
1996
2032
|
for (const track of legacy.tracks ?? []) for (const item of track.items ?? []) {
|
|
1997
2033
|
const part = requirePart(legacy, item.part_id);
|
|
1998
2034
|
if (part.video_clip !== void 0) {
|
|
@@ -2026,7 +2062,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2026
2062
|
entities = [...imported.rows.entities];
|
|
2027
2063
|
relations = [...imported.rows.relations];
|
|
2028
2064
|
contentByPartId.set(item.part_id, imported.contentEntityId);
|
|
2029
|
-
|
|
2065
|
+
const phonemeScript = speech.phoneme_script;
|
|
2066
|
+
ensureSpeechPhoneticChain(imported.contentEntityId, speech.audio_script, typeof phonemeScript === "string" ? phonemeScript : void 0, entities, relations, addEntity, link, documentAudioScriptEntityId);
|
|
2030
2067
|
} else if (part.bgm !== void 0) {
|
|
2031
2068
|
const bgm = part.bgm;
|
|
2032
2069
|
const fact = facts.get(bgm.origin_media_id);
|
|
@@ -2101,6 +2138,22 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2101
2138
|
} else if (part.caption !== void 0) {
|
|
2102
2139
|
const caption = part.caption;
|
|
2103
2140
|
duration = caption.initial_duration_ms;
|
|
2141
|
+
const voice = contentByPartId.get(caption.speech_part_id);
|
|
2142
|
+
const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voice);
|
|
2143
|
+
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2144
|
+
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2145
|
+
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
2146
|
+
const sourceText = scriptText(script);
|
|
2147
|
+
const captionText = caption.text;
|
|
2148
|
+
const offset = sourceText.indexOf(captionText);
|
|
2149
|
+
if (!captionText.trim() || offset < 0 || sourceText.indexOf(captionText, offset + 1) >= 0) fail("Legacy Caption needs explicit unambiguous AudioScript selections before cutover");
|
|
2150
|
+
const selections = [{
|
|
2151
|
+
segmentId: "segment-0",
|
|
2152
|
+
...sourceText === captionText ? {} : { textRange: {
|
|
2153
|
+
start: Array.from(sourceText.slice(0, offset)).length,
|
|
2154
|
+
end: Array.from(sourceText.slice(0, offset + captionText.length)).length
|
|
2155
|
+
} }
|
|
2156
|
+
}];
|
|
2104
2157
|
content = addEntity("caption", {
|
|
2105
2158
|
extent: {
|
|
2106
2159
|
kind: "bounded",
|
|
@@ -2109,21 +2162,14 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2109
2162
|
},
|
|
2110
2163
|
sampling: "native",
|
|
2111
2164
|
coordinateSpace: "milliseconds",
|
|
2112
|
-
|
|
2165
|
+
baseEntityIds: [script.entityId],
|
|
2166
|
+
selections,
|
|
2113
2167
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2114
2168
|
});
|
|
2115
|
-
const asset = addEntity("asset", { inline: {
|
|
2116
|
-
mediaType: "text/plain",
|
|
2117
|
-
text: caption.text
|
|
2118
|
-
} });
|
|
2119
|
-
link("physical-asset", content, asset);
|
|
2120
|
-
const voice = contentByPartId.get(caption.speech_part_id);
|
|
2121
|
-
const script = scriptBySpeechClipId.get(caption.speech_part_id);
|
|
2122
2169
|
link("caption-alignment", content, voice, { alignment: {
|
|
2123
2170
|
start: caption.start_ms,
|
|
2124
2171
|
end: caption.start_ms + duration
|
|
2125
2172
|
} });
|
|
2126
|
-
link("caption-provenance", content, script, { segmentAlignment: [] });
|
|
2127
2173
|
markerPayload = {
|
|
2128
2174
|
sourceRange: {
|
|
2129
2175
|
start: 0,
|
|
@@ -2339,7 +2385,7 @@ function assertPartSupported(id, part, role) {
|
|
|
2339
2385
|
if (speech.voice?.name !== void 0 && typeof speech.voice.name !== "string") fail("Migration requires a string voice name when present");
|
|
2340
2386
|
if (!Array.isArray(speech.caption_ids) || speech.caption_ids.some((captionId) => !trimmed(captionId))) fail("Migration requires explicit Speech caption identities");
|
|
2341
2387
|
if (new Set(speech.caption_ids).size !== speech.caption_ids.length) fail("Migration requires unique Speech caption identities");
|
|
2342
|
-
if (phonemeScript !== void 0 && phonemeScript !== null) fail("
|
|
2388
|
+
if (phonemeScript !== void 0 && phonemeScript !== null && typeof phonemeScript !== "string") fail("Migration requires a string phoneme_script when present");
|
|
2343
2389
|
assertKnownFields(speech.voice, ["id", "name"], "voice");
|
|
2344
2390
|
assertKnownFields(speech, [
|
|
2345
2391
|
"id",
|
|
@@ -2463,20 +2509,43 @@ function requirePart(document, partId) {
|
|
|
2463
2509
|
if (part === void 0) fail(`Placed Clip ${partId} has no part payload`);
|
|
2464
2510
|
return part;
|
|
2465
2511
|
}
|
|
2466
|
-
function
|
|
2467
|
-
const renders = relations.filter((relation) => relation.relationKind === "
|
|
2468
|
-
if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous
|
|
2512
|
+
function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2513
|
+
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voiceEntityId);
|
|
2514
|
+
if (renders.length > 1) fail(`Voice ${voiceEntityId} has ambiguous PhoneticScript provenance`);
|
|
2469
2515
|
if (renders.length === 1) {
|
|
2470
|
-
const
|
|
2471
|
-
if (
|
|
2472
|
-
|
|
2516
|
+
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
2517
|
+
if (phonetic === void 0) fail(`Voice ${voiceEntityId} has a dangling PhoneticScript render`);
|
|
2518
|
+
const script = composedAudioScript(phonetic, entities);
|
|
2519
|
+
if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the legacy Speech script`);
|
|
2520
|
+
return phonetic.entityId;
|
|
2521
|
+
}
|
|
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
|
+
};
|
|
2473
2536
|
}
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
}
|
|
2478
|
-
link("
|
|
2479
|
-
return
|
|
2537
|
+
const phonetic = addEntity("phonetic-script", {
|
|
2538
|
+
baseEntityIds: [script.entityId],
|
|
2539
|
+
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2540
|
+
});
|
|
2541
|
+
link("phonetic-script-render", voiceEntityId, phonetic);
|
|
2542
|
+
return phonetic;
|
|
2543
|
+
}
|
|
2544
|
+
function composedAudioScript(phonetic, entities) {
|
|
2545
|
+
return findComposedAudioScript({
|
|
2546
|
+
entities,
|
|
2547
|
+
relations: []
|
|
2548
|
+
}, phonetic.entityId, "phonetic-script");
|
|
2480
2549
|
}
|
|
2481
2550
|
function scriptText(script) {
|
|
2482
2551
|
const segments = script.payload.segments;
|
|
@@ -2566,6 +2635,82 @@ function fail(message) {
|
|
|
2566
2635
|
throw new EntityTimelineProjectionError(message);
|
|
2567
2636
|
}
|
|
2568
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
|
|
2569
2714
|
//#region src/editor/snapshot-utils.ts
|
|
2570
2715
|
function isMap(value) {
|
|
2571
2716
|
return value instanceof Map;
|
|
@@ -4704,4 +4849,4 @@ function serverCovers(serverVV, target) {
|
|
|
4704
4849
|
return covers(server, target);
|
|
4705
4850
|
}
|
|
4706
4851
|
//#endregion
|
|
4707
|
-
export { DEFAULT_UNIT_TIME_MS, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
|
|
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 };
|