@mengine/medeo-client 2.0.1-alpha.23 → 2.0.1-alpha.25
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/dist/{document-7Vi1AeQc.js → document-Bx9xf40e.js} +380 -402
- package/dist/{index-mGObzOh4.d.ts → index-BYv9FPSS.d.ts} +14 -37
- package/dist/index.d.ts +79 -51
- package/dist/index.js +412 -479
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as fromVideoDocument, A as resolveEntityTimelineLayout, At as base64ToBytes, B as assembleScriptText, C as canonicalJson, Ct as safeDurationMs, D as resolveFieldPath, Dt as partUnionToDraft, E as assertFieldChanges, Et as speedOf, F as relationsTo, G as isJsonObject, H as selectAudioScriptSegment, I as ScriptCompositionError, J as hasSequence, K as createEntityId, L as assembleCaptionContent, M as validateEntityRelationTransition, N as decodeEntityRelationRows, O as projectEntityTimeline, Ot as recordEntries, P as relationsFrom, Q as derivePositionFromAbs, R as assembleEntityContent, S as scriptSourceAssetOf, St as partDurationMs, T as applyFieldChanges, Tt as effectiveVideoClipDurationMs, U as updateEntityFields, V as findComposedAudioScript, W as variantBaseEntityIds, X as buildInitialVideoDocument, Y as isMediaAssetVariantKind, Z as buildSpeechHostMap, _ as audioScriptAssetHash, _t as resolveAllSpeechOverlaps, a as createMirrorVideoDocument, at as videoDocumentSchema, b as phoneticScriptAssetHash, bt as TIMELINE_SKELETON_DURATION_MS, c as DocumentMutationGuard, ct as findLaneTrack, d as LoroEntityDraft, dt as solveVideoDocument, et as toVideoDocument, f as readEntityDocument, ft as cascadeAfterVideoClipChanges, g as audioScriptAssetFields, gt as recalculateTimelineDuration, h as audioScriptAssetContent, ht as reassignSpeechesToVideoClipsByTime, i as MirrorVideoDocumentAdapter, it as partUnionSchema, j as EntityTimelineProjectionError, jt as bytesToBase64, k as EntityProjectionGraph, kt as videoDocumentMirrorSchema, l as LORO_ENTITY_SCHEMA, lt as hasResolvedPlacement, m as entityOrderGroups, mt as fillMainTrackTimeGaps, n as createPlainMemoryAdapter, nt as assertValidVideoDocument, o as createMirrorVideoDocumentAdapter, ot as LANE_KINDS_IN_STACK_ORDER, p as readEntityRelationRecords, pt as arrangeMainTrackSeamlessly, q as createRelationId, r as generatePartId, rt as validateVideoDocument, s as readVideoDocumentFromDraft, st as ensureLaneTrack, t as PlainMemoryAdapter, tt as VideoDocumentValidationError, u as LoroEntityDocument, ut as laneTrackId, v as audioScriptAssetOf, vt as resolveSpeechOverlapByShiftingVideos, w as equalJson, wt as DEFAULT_UNIT_TIME_MS, x as phoneticScriptAssetOf, xt as isEmptyVideoClip, y as phoneticScriptAssetContent, yt as syncAggregatedClipsTimePosition, z as assemblePhoneticScriptContent } from "./document-Bx9xf40e.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, UndoManager, VersionVector } from "loro-crdt";
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -216,19 +216,34 @@ function findSseFrameBoundary(buffer) {
|
|
|
216
216
|
//#endregion
|
|
217
217
|
//#region src/entity-crdt/compile-rows.ts
|
|
218
218
|
/** Lower a detached ordinary-object edit against its actual causal Loro baseline. */
|
|
219
|
-
function compileEntityRows(snapshot, rows, validate) {
|
|
219
|
+
function compileEntityRows(snapshot, rows, validate, options = {}) {
|
|
220
220
|
const doc = new LoroDoc();
|
|
221
221
|
doc.import(snapshot);
|
|
222
|
-
return applyEntityRows(new LoroEntityDocument(doc, validate), rows);
|
|
222
|
+
return applyEntityRows(new LoroEntityDocument(doc, validate), rows, options);
|
|
223
|
+
}
|
|
224
|
+
/** Publish final rows as imported causal operations, outside the caller's local undo history. */
|
|
225
|
+
function publishEntityRows(editor, rows, options = {}) {
|
|
226
|
+
const from = editor.doc.oplogVersion();
|
|
227
|
+
const compiled = compileEntityRows(editor.doc.export({ mode: "snapshot" }), rows, void 0, options);
|
|
228
|
+
editor.import(compiled.update);
|
|
229
|
+
return {
|
|
230
|
+
update: editor.doc.export({
|
|
231
|
+
mode: "update",
|
|
232
|
+
from
|
|
233
|
+
}),
|
|
234
|
+
state: editor.snapshot()
|
|
235
|
+
};
|
|
223
236
|
}
|
|
224
237
|
/** Compile an edit on an existing peer, retaining its native local undo history. */
|
|
225
|
-
function applyEntityRows(editor, rows) {
|
|
238
|
+
function applyEntityRows(editor, rows, options = {}) {
|
|
226
239
|
const baseline = editor.snapshot();
|
|
227
240
|
const before = baseline.rows;
|
|
228
241
|
const oldEntities = new Map(before.entities.map((row) => [row.entityId, row]));
|
|
229
242
|
const newEntities = new Map(rows.entities.map((row) => [row.entityId, row]));
|
|
230
243
|
const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
231
244
|
const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
|
|
245
|
+
const oldClipContent = new Map(before.relations.filter((row) => row.relationKind === "contain" && oldEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
|
|
246
|
+
const newClipContent = new Map(rows.relations.filter((row) => row.relationKind === "contain" && newEntities.get(row.endpoint0EntityId)?.entityKind === "clip").map((row) => [row.endpoint0EntityId, row.relationId]));
|
|
232
247
|
if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
|
|
233
248
|
validateEntityRelationTransition(before, rows);
|
|
234
249
|
return {
|
|
@@ -255,28 +270,16 @@ function applyEntityRows(editor, rows) {
|
|
|
255
270
|
if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
|
-
for (const row of rows.relations)
|
|
259
|
-
|
|
260
|
-
if (row.relationKind === "generated") draft.linkGenerated({
|
|
261
|
-
relationId: row.relationId,
|
|
262
|
-
outputEntityId: row.endpoint0EntityId,
|
|
263
|
-
inputEntityId: row.endpoint1EntityId,
|
|
264
|
-
trace: row.trace
|
|
265
|
-
});
|
|
266
|
-
else if (row.relationKind === "clip-anchor") draft.linkClipAnchor({
|
|
267
|
-
relationId: row.relationId,
|
|
268
|
-
childClipEntityId: row.endpoint0EntityId,
|
|
269
|
-
hostClipEntityId: row.endpoint1EntityId,
|
|
270
|
-
trace: row.trace
|
|
271
|
-
});
|
|
272
|
-
else draft.link(row);
|
|
273
|
-
}
|
|
273
|
+
for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
|
|
274
|
+
for (const [clipId, relationId] of newClipContent) if (oldClipContent.get(clipId) !== relationId) draft.selectClipContent(relationId);
|
|
274
275
|
const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
|
|
275
276
|
for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
|
|
276
277
|
const next = afterOrder.get(group) ?? [];
|
|
277
278
|
if (!equalJson(beforeOrder.get(group) ?? [], next)) draft.order(group, next);
|
|
278
279
|
}
|
|
279
280
|
for (const row of before.entities) if (!newEntities.has(row.entityId)) draft.delete(row.entityId);
|
|
281
|
+
for (const [slot, entityId] of Object.entries(options.projectAttachments ?? {})) draft.attach(slot, entityId);
|
|
282
|
+
for (const [clipEntityId, phoneticScriptEntityId] of Object.entries(options.phoneticScriptSelections ?? {})) draft.selectPhoneticScript(clipEntityId, phoneticScriptEntityId);
|
|
280
283
|
}),
|
|
281
284
|
state: editor.snapshot()
|
|
282
285
|
};
|
|
@@ -420,14 +423,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
420
423
|
};
|
|
421
424
|
entities.push(track);
|
|
422
425
|
}
|
|
423
|
-
const bindings = relations.filter((row) => row.relationKind === "
|
|
426
|
+
const bindings = relations.filter((row) => row.relationKind === "contain" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
|
|
424
427
|
if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
|
|
425
428
|
if (bindings.length === 0) {
|
|
426
429
|
const relationId = createRelationId(idFactory("relation"));
|
|
427
430
|
if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
|
|
428
431
|
relations.push({
|
|
429
432
|
relationId,
|
|
430
|
-
relationKind: "
|
|
433
|
+
relationKind: "contain",
|
|
431
434
|
endpoint0EntityId: timeline.entityId,
|
|
432
435
|
endpoint1EntityId: track.entityId,
|
|
433
436
|
metadata: {},
|
|
@@ -450,10 +453,16 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
450
453
|
//#endregion
|
|
451
454
|
//#region src/entity-editor/entity-graph-http-client.ts
|
|
452
455
|
const MENGINE_API_PREFIX = "/api/mengine/v1";
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
456
|
+
var EntityGraphRevisionConflictError = class extends MengineHttpRequestError {
|
|
457
|
+
expectedRevision;
|
|
458
|
+
currentRevision;
|
|
459
|
+
constructor(expectedRevision, currentRevision, payload) {
|
|
460
|
+
super(409, payload);
|
|
461
|
+
this.expectedRevision = expectedRevision;
|
|
462
|
+
this.currentRevision = currentRevision;
|
|
463
|
+
this.name = "EntityGraphRevisionConflictError";
|
|
464
|
+
}
|
|
465
|
+
};
|
|
457
466
|
/** Browser-safe authenticated Loro client for a document's Entity graph. */
|
|
458
467
|
var EntityGraphHttpClient = class {
|
|
459
468
|
options;
|
|
@@ -470,6 +479,8 @@ var EntityGraphHttpClient = class {
|
|
|
470
479
|
}
|
|
471
480
|
/**
|
|
472
481
|
* Compile edits against the original causal snapshot and publish native Loro operations.
|
|
482
|
+
* The base revision is sent as an optimistic precondition, so a stale adoption
|
|
483
|
+
* fails loudly instead of letting Loro choose between concurrent selections.
|
|
473
484
|
*
|
|
474
485
|
* Deletion intent is derived from baseline IDs omitted from `nextRows`, so a
|
|
475
486
|
* transient create-then-remove never becomes a request to delete unknown
|
|
@@ -480,13 +491,17 @@ var EntityGraphHttpClient = class {
|
|
|
480
491
|
const next = cloneAndValidateRows(nextRows);
|
|
481
492
|
assertMediaAssetWritePolicy(baseState.rows, next);
|
|
482
493
|
const compiled = compileEntityRows(base64ToBytes(baseState.loroSnapshot), next);
|
|
483
|
-
return this.commitUpdate(bytesToBase64(compiled.update));
|
|
494
|
+
return this.commitUpdate(bytesToBase64(compiled.update), baseState.revision);
|
|
484
495
|
}
|
|
485
|
-
/** Send
|
|
486
|
-
async commitUpdate(update) {
|
|
496
|
+
/** Send prepared bytes directly; omit `expectedRevision` for immutable fact publication. */
|
|
497
|
+
async commitUpdate(update, expectedRevision) {
|
|
498
|
+
if (expectedRevision !== void 0 && !isNonNegativeSafeInteger(expectedRevision)) throw new Error("expectedRevision must be a nonnegative safe integer");
|
|
487
499
|
return this.request({
|
|
488
500
|
method: "POST",
|
|
489
|
-
body: JSON.stringify({
|
|
501
|
+
body: JSON.stringify({
|
|
502
|
+
update,
|
|
503
|
+
...expectedRevision === void 0 ? {} : { expected_revision: expectedRevision }
|
|
504
|
+
})
|
|
490
505
|
});
|
|
491
506
|
}
|
|
492
507
|
async request(init) {
|
|
@@ -495,15 +510,26 @@ var EntityGraphHttpClient = class {
|
|
|
495
510
|
headers: this.transport.headers()
|
|
496
511
|
});
|
|
497
512
|
const payload = await safeReadJson(response);
|
|
498
|
-
if (!response.ok) throw new MengineHttpRequestError(response.status, payload);
|
|
513
|
+
if (!response.ok) throw parseRevisionConflict(response.status, payload) ?? new MengineHttpRequestError(response.status, payload);
|
|
499
514
|
return parseWireState(payload, this.options.docId);
|
|
500
515
|
}
|
|
501
516
|
};
|
|
517
|
+
function parseRevisionConflict(status, value) {
|
|
518
|
+
if (status !== 409 || !isRecord(value) || !isRecord(value.error) || value.error.code !== "revision_conflict") return;
|
|
519
|
+
const error = value.error;
|
|
520
|
+
if (typeof error.message !== "string" || !isNonNegativeSafeInteger(error.expected_revision) || !isNonNegativeSafeInteger(error.current_revision)) return;
|
|
521
|
+
return new EntityGraphRevisionConflictError(error.expected_revision, error.current_revision, { error: {
|
|
522
|
+
code: "revision_conflict",
|
|
523
|
+
message: error.message,
|
|
524
|
+
expected_revision: error.expected_revision,
|
|
525
|
+
current_revision: error.current_revision
|
|
526
|
+
} });
|
|
527
|
+
}
|
|
502
528
|
function parseWireState(value, expectedDocId) {
|
|
503
529
|
if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
|
|
504
530
|
if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
505
531
|
const rows = parseWireRows(value.rows);
|
|
506
|
-
decodeEntityRelationRows(rows,
|
|
532
|
+
decodeEntityRelationRows(rows, {});
|
|
507
533
|
const attached = value.audio_script_entity_id;
|
|
508
534
|
if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
|
|
509
535
|
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
@@ -544,7 +570,7 @@ function parseWireRelation(value) {
|
|
|
544
570
|
}
|
|
545
571
|
function cloneAndValidateRows(rows) {
|
|
546
572
|
const clone = structuredClone(rows);
|
|
547
|
-
decodeEntityRelationRows(clone,
|
|
573
|
+
decodeEntityRelationRows(clone, {});
|
|
548
574
|
return clone;
|
|
549
575
|
}
|
|
550
576
|
function requiredString(value, field) {
|
|
@@ -568,10 +594,6 @@ async function safeReadJson(response) {
|
|
|
568
594
|
}
|
|
569
595
|
//#endregion
|
|
570
596
|
//#region src/entity-editor/media-asset-import.ts
|
|
571
|
-
const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
572
|
-
if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
|
|
573
|
-
return left - right;
|
|
574
|
-
} };
|
|
575
597
|
/**
|
|
576
598
|
* Import at the editor boundary, not at asset generation time. Existing
|
|
577
599
|
* provenance is retained.
|
|
@@ -587,17 +609,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
587
609
|
"image",
|
|
588
610
|
"video",
|
|
589
611
|
"audio",
|
|
590
|
-
"voiceover"
|
|
612
|
+
"voiceover-recording"
|
|
591
613
|
].includes(fact.kind)) throw new Error("Unsupported media kind");
|
|
592
614
|
if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
|
|
593
|
-
if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
594
|
-
decodeEntityRelationRows(initial,
|
|
615
|
+
if ((fact.kind === "audio" || fact.kind === "voiceover-recording") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
616
|
+
decodeEntityRelationRows(initial, {});
|
|
595
617
|
assertCanonicalEditorResources(initial);
|
|
596
618
|
const entities = structuredClone([...initial.entities]);
|
|
597
619
|
const relations = structuredClone([...initial.relations]);
|
|
598
|
-
const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
|
|
620
|
+
const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
|
|
599
621
|
const entityKind = fact.kind;
|
|
600
|
-
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
622
|
+
const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
|
|
601
623
|
const durationMs = fact.kind === "image" ? null : fact.durationMs;
|
|
602
624
|
const asset = ensureAsset(entities, {
|
|
603
625
|
system,
|
|
@@ -628,12 +650,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
628
650
|
payload: { durationMs }
|
|
629
651
|
});
|
|
630
652
|
relations.push(assetLink(entityId, asset.entityId, idFactory));
|
|
631
|
-
if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
|
|
653
|
+
if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
|
|
632
654
|
const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
|
|
633
655
|
if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
|
|
634
656
|
relations.push({
|
|
635
657
|
relationId: createRelationId(idFactory("relation")),
|
|
636
|
-
relationKind: "
|
|
658
|
+
relationKind: "derived-from",
|
|
637
659
|
endpoint0EntityId: entityId,
|
|
638
660
|
endpoint1EntityId: phonetic.entityId,
|
|
639
661
|
metadata: {},
|
|
@@ -645,7 +667,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
645
667
|
entities,
|
|
646
668
|
relations
|
|
647
669
|
};
|
|
648
|
-
decodeEntityRelationRows(rows,
|
|
670
|
+
decodeEntityRelationRows(rows, {});
|
|
649
671
|
assertCanonicalEditorResources(rows);
|
|
650
672
|
return {
|
|
651
673
|
rows,
|
|
@@ -788,19 +810,19 @@ var EntityTimelineEditor = class {
|
|
|
788
810
|
return cloneRows(this.state);
|
|
789
811
|
}
|
|
790
812
|
insertClip(input) {
|
|
791
|
-
const requestedOrder = input.
|
|
813
|
+
const requestedOrder = input.order;
|
|
792
814
|
const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
|
|
793
815
|
return this.insertPlacedClip({
|
|
794
816
|
trackEntityId: input.trackEntityId,
|
|
795
817
|
contentEntityId: input.contentEntityId,
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
placement: input.
|
|
818
|
+
inMs: input.inMs,
|
|
819
|
+
outMs: input.outMs,
|
|
820
|
+
placement: input.startMs === void 0 ? {
|
|
799
821
|
kind: "sequential",
|
|
800
822
|
order
|
|
801
823
|
} : {
|
|
802
824
|
kind: "absolute",
|
|
803
|
-
|
|
825
|
+
startMs: input.startMs
|
|
804
826
|
},
|
|
805
827
|
clipPayload: input.clipPayload
|
|
806
828
|
});
|
|
@@ -812,31 +834,14 @@ var EntityTimelineEditor = class {
|
|
|
812
834
|
this.transact((draft) => {
|
|
813
835
|
const { marker } = requireClipTopology(draft, input.clipEntityId);
|
|
814
836
|
const payload = { ...cloneJsonObject(marker.payload, "Sequence Marker payload") };
|
|
815
|
-
if (input.
|
|
816
|
-
if (input.
|
|
817
|
-
if (Object.hasOwn(input, "
|
|
818
|
-
delete payload.
|
|
819
|
-
if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
replaceEntity(draft, createEntityId(input.clipEntityId), {
|
|
824
|
-
...requireEntityKind(draft, input.clipEntityId, "clip"),
|
|
825
|
-
payload: clipPayload
|
|
826
|
-
});
|
|
827
|
-
}
|
|
828
|
-
} else {
|
|
829
|
-
payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
|
|
830
|
-
const clip = requireEntityKind(draft, input.clipEntityId, "clip");
|
|
831
|
-
const clipPayload = { ...cloneJsonObject(clip.payload, "Clip payload") };
|
|
832
|
-
delete clipPayload.order;
|
|
833
|
-
replaceEntity(draft, clip.entityId, {
|
|
834
|
-
...clip,
|
|
835
|
-
payload: clipPayload
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
if (Object.hasOwn(input, "timeRemapping")) if (input.timeRemapping === void 0) delete payload.timeRemapping;
|
|
839
|
-
else payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
|
|
837
|
+
if (input.inMs !== void 0) payload.inMs = finiteNonNegative(input.inMs, "inMs");
|
|
838
|
+
if (input.outMs !== void 0) payload.outMs = finitePositive(input.outMs, "outMs");
|
|
839
|
+
if (Object.hasOwn(input, "startMs")) if (input.startMs === void 0) {
|
|
840
|
+
delete payload.startMs;
|
|
841
|
+
if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {}
|
|
842
|
+
} else payload.startMs = finiteNonNegative(input.startMs, "startMs");
|
|
843
|
+
if (Object.hasOwn(input, "rate")) if (input.rate === void 0) delete payload.rate;
|
|
844
|
+
else payload.rate = finitePositive(input.rate, "linear speed rate");
|
|
840
845
|
replaceEntity(draft, marker.entityId, {
|
|
841
846
|
...marker,
|
|
842
847
|
payload
|
|
@@ -947,28 +952,18 @@ var EntityTimelineEditor = class {
|
|
|
947
952
|
const content = requireSequenceContent(draft, input.contentEntityId);
|
|
948
953
|
const payload = {
|
|
949
954
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
950
|
-
|
|
951
|
-
duration: cloneJsonValue(input.duration, "duration")
|
|
955
|
+
...requireWindow(input.inMs, input.outMs)
|
|
952
956
|
};
|
|
953
|
-
delete payload.
|
|
954
|
-
delete payload.
|
|
955
|
-
if (input.
|
|
956
|
-
if (input.
|
|
957
|
+
delete payload.startMs;
|
|
958
|
+
delete payload.rate;
|
|
959
|
+
if (input.startMs !== void 0) payload.startMs = finiteNonNegative(input.startMs, "startMs");
|
|
960
|
+
if (input.rate !== void 0) payload.rate = finitePositive(input.rate, "linear speed rate");
|
|
957
961
|
replaceEntity(draft, topology.marker.entityId, {
|
|
958
962
|
...topology.marker,
|
|
959
963
|
payload
|
|
960
964
|
});
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
if (input.targetRange === void 0) clipPayload.order ??= nextSequentialOrder(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId));
|
|
964
|
-
else delete clipPayload.order;
|
|
965
|
-
replaceEntity(draft, topology.clip.entityId, {
|
|
966
|
-
...topology.clip,
|
|
967
|
-
payload: clipPayload
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
this.relinkRelation(draft, topology.markerContentRelation, {
|
|
971
|
-
endpoint0EntityId: topology.marker.entityId,
|
|
965
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
966
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
972
967
|
endpoint1EntityId: content.entityId
|
|
973
968
|
});
|
|
974
969
|
});
|
|
@@ -979,15 +974,11 @@ var EntityTimelineEditor = class {
|
|
|
979
974
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
980
975
|
replaceRows(draft, imported.rows);
|
|
981
976
|
const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
|
|
982
|
-
const
|
|
977
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
983
978
|
const clipId = this.insertPlacedClipIntoDraft(draft, {
|
|
984
979
|
trackEntityId: track.entityId,
|
|
985
980
|
contentEntityId: imported.contentEntityId,
|
|
986
|
-
|
|
987
|
-
duration: {
|
|
988
|
-
mode: "fixed",
|
|
989
|
-
value: sourceRange.end - sourceRange.start
|
|
990
|
-
},
|
|
981
|
+
...window,
|
|
991
982
|
placement: input.placement,
|
|
992
983
|
...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
|
|
993
984
|
...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
|
|
@@ -1022,15 +1013,11 @@ var EntityTimelineEditor = class {
|
|
|
1022
1013
|
for (const [offset, clip] of input.clips.entries()) {
|
|
1023
1014
|
const imported = importMediaAsset(draft, clip.media, this.idFactory);
|
|
1024
1015
|
replaceRows(draft, imported.rows);
|
|
1025
|
-
const
|
|
1016
|
+
const window = requireWindow(clip.inMs, clip.outMs);
|
|
1026
1017
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1027
1018
|
trackEntityId: track.entityId,
|
|
1028
1019
|
contentEntityId: imported.contentEntityId,
|
|
1029
|
-
|
|
1030
|
-
duration: {
|
|
1031
|
-
mode: "fixed",
|
|
1032
|
-
value: sourceRange.end - sourceRange.start
|
|
1033
|
-
},
|
|
1020
|
+
...window,
|
|
1034
1021
|
placement: {
|
|
1035
1022
|
kind: "sequential",
|
|
1036
1023
|
order: ordered.length + offset
|
|
@@ -1052,23 +1039,18 @@ var EntityTimelineEditor = class {
|
|
|
1052
1039
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1053
1040
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1054
1041
|
replaceRows(draft, imported.rows);
|
|
1055
|
-
const
|
|
1042
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1056
1043
|
const markerPayloadValue = {
|
|
1057
1044
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1058
|
-
|
|
1059
|
-
duration: {
|
|
1060
|
-
mode: "fixed",
|
|
1061
|
-
value: sourceRange.end - sourceRange.start
|
|
1062
|
-
}
|
|
1045
|
+
...window
|
|
1063
1046
|
};
|
|
1064
|
-
delete markerPayloadValue.
|
|
1065
|
-
resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
|
|
1047
|
+
delete markerPayloadValue.rate;
|
|
1066
1048
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1067
1049
|
...topology.marker,
|
|
1068
1050
|
payload: markerPayloadValue
|
|
1069
1051
|
});
|
|
1070
|
-
this.relinkRelation(draft, topology.
|
|
1071
|
-
endpoint0EntityId: topology.
|
|
1052
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1053
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1072
1054
|
endpoint1EntityId: createEntityId(imported.contentEntityId)
|
|
1073
1055
|
});
|
|
1074
1056
|
this.resolveOneVoiceoverOverlap(draft);
|
|
@@ -1090,42 +1072,26 @@ var EntityTimelineEditor = class {
|
|
|
1090
1072
|
setClipSpeed(input) {
|
|
1091
1073
|
this.transact((draft) => {
|
|
1092
1074
|
const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1093
|
-
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.
|
|
1075
|
+
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
|
|
1094
1076
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
|
|
1095
1077
|
const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
|
|
1096
|
-
const
|
|
1097
|
-
if (input.
|
|
1078
|
+
const priorWindow = markerWindow(initialPayload);
|
|
1079
|
+
if (input.rate === null && initialPayload.rate === void 0) return;
|
|
1098
1080
|
this.normalizeMainSequential(draft);
|
|
1099
1081
|
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1100
1082
|
const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
if (input.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
mode: "fixed",
|
|
1111
|
-
value: sourceRange.end - sourceRange.start
|
|
1112
|
-
};
|
|
1113
|
-
} else {
|
|
1114
|
-
const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
|
|
1115
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1083
|
+
const window = content.entityKind === "image" && initialPayload.rate === void 0 && input.rate !== null ? {
|
|
1084
|
+
inMs: 0,
|
|
1085
|
+
outMs: markerDuration(initialPayload, priorWindow)
|
|
1086
|
+
} : priorWindow;
|
|
1087
|
+
Object.assign(payload, window);
|
|
1088
|
+
if (input.rate === null) delete payload.rate;
|
|
1089
|
+
else {
|
|
1090
|
+
const rate = finitePositive(input.rate, "linear speed rate");
|
|
1091
|
+
const duration = Math.round((window.outMs - window.inMs) / rate);
|
|
1116
1092
|
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
|
|
1117
|
-
payload.
|
|
1118
|
-
payload.duration = {
|
|
1119
|
-
mode: "fixed",
|
|
1120
|
-
value: duration
|
|
1121
|
-
};
|
|
1122
|
-
payload.timeRemapping = {
|
|
1123
|
-
kind: "linear",
|
|
1124
|
-
rate,
|
|
1125
|
-
...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
|
|
1126
|
-
};
|
|
1093
|
+
payload.rate = rate;
|
|
1127
1094
|
}
|
|
1128
|
-
resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
|
|
1129
1095
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1130
1096
|
...topology.marker,
|
|
1131
1097
|
payload
|
|
@@ -1137,21 +1103,15 @@ var EntityTimelineEditor = class {
|
|
|
1137
1103
|
this.transact((draft) => {
|
|
1138
1104
|
this.normalizeMainSequential(draft);
|
|
1139
1105
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1140
|
-
const content = requireSequenceContent(draft, otherEndpoint(topology.
|
|
1106
|
+
const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
|
|
1141
1107
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip trimming is supported only for Video or Image content");
|
|
1142
|
-
const
|
|
1108
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1143
1109
|
const payload = {
|
|
1144
1110
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1145
|
-
|
|
1111
|
+
...window
|
|
1146
1112
|
};
|
|
1147
|
-
const
|
|
1148
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1113
|
+
const duration = markerDuration(payload, window);
|
|
1149
1114
|
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Trim produces an invalid duration");
|
|
1150
|
-
payload.duration = {
|
|
1151
|
-
mode: "fixed",
|
|
1152
|
-
value: duration
|
|
1153
|
-
};
|
|
1154
|
-
resizeAbsoluteTarget(payload, duration);
|
|
1155
1115
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1156
1116
|
...topology.marker,
|
|
1157
1117
|
payload
|
|
@@ -1181,15 +1141,11 @@ var EntityTimelineEditor = class {
|
|
|
1181
1141
|
if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
|
|
1182
1142
|
const imported = importMediaAsset(draft, replacement.media, this.idFactory);
|
|
1183
1143
|
replaceRows(draft, imported.rows);
|
|
1184
|
-
const
|
|
1144
|
+
const window = requireWindow(replacement.inMs, replacement.outMs);
|
|
1185
1145
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1186
1146
|
trackEntityId: track.entityId,
|
|
1187
1147
|
contentEntityId: imported.contentEntityId,
|
|
1188
|
-
|
|
1189
|
-
duration: {
|
|
1190
|
-
mode: "fixed",
|
|
1191
|
-
value: sourceRange.end - sourceRange.start
|
|
1192
|
-
},
|
|
1148
|
+
...window,
|
|
1193
1149
|
placement: {
|
|
1194
1150
|
kind: "sequential",
|
|
1195
1151
|
order: temporaryOrder + offset
|
|
@@ -1223,10 +1179,14 @@ var EntityTimelineEditor = class {
|
|
|
1223
1179
|
deleteClip(input) {
|
|
1224
1180
|
this.transact((draft) => {
|
|
1225
1181
|
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1226
|
-
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["
|
|
1227
|
-
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["
|
|
1228
|
-
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "
|
|
1229
|
-
const removedRelationIds = new Set([
|
|
1182
|
+
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["contain", "marker"]));
|
|
1183
|
+
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
|
|
1184
|
+
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
|
|
1185
|
+
const removedRelationIds = new Set([
|
|
1186
|
+
topology.trackRelation.relationId,
|
|
1187
|
+
topology.clipMarkerRelation.relationId,
|
|
1188
|
+
topology.contentRelation.relationId
|
|
1189
|
+
]);
|
|
1230
1190
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
|
|
1231
1191
|
draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
|
|
1232
1192
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
|
|
@@ -1247,28 +1207,20 @@ var EntityTimelineEditor = class {
|
|
|
1247
1207
|
if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
|
|
1248
1208
|
this.applyClipPlacement(draft, childId, {
|
|
1249
1209
|
kind: "absolute",
|
|
1250
|
-
|
|
1251
|
-
start: placement.start,
|
|
1252
|
-
end: placement.start + placement.duration
|
|
1253
|
-
}
|
|
1210
|
+
startMs: placement.start
|
|
1254
1211
|
});
|
|
1255
1212
|
}
|
|
1256
1213
|
else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
|
|
1257
1214
|
for (const target of targets) this.removeClipGraph(draft, target);
|
|
1258
|
-
normalizeSequentialOrders(draft);
|
|
1259
1215
|
if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
|
|
1260
1216
|
});
|
|
1261
1217
|
}
|
|
1262
1218
|
updateClip(input) {
|
|
1263
1219
|
this.transact((draft) => {
|
|
1264
1220
|
const clip = requireEntityKind(draft, input.clipEntityId, "clip");
|
|
1265
|
-
const priorOrder = clip.payload.order;
|
|
1266
1221
|
replaceEntity(draft, clip.entityId, {
|
|
1267
1222
|
...clip,
|
|
1268
|
-
payload:
|
|
1269
|
-
...cloneJsonObject(input.payload, "Clip payload"),
|
|
1270
|
-
...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
|
|
1271
|
-
}
|
|
1223
|
+
payload: cloneJsonObject(input.payload, "Clip payload")
|
|
1272
1224
|
});
|
|
1273
1225
|
});
|
|
1274
1226
|
}
|
|
@@ -1278,13 +1230,13 @@ var EntityTimelineEditor = class {
|
|
|
1278
1230
|
const placement = input.placement ?? {
|
|
1279
1231
|
kind: "anchored",
|
|
1280
1232
|
hostClipEntityId: input.hostClipEntityId,
|
|
1281
|
-
|
|
1233
|
+
startMs: input.startMs
|
|
1282
1234
|
};
|
|
1283
|
-
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.
|
|
1235
|
+
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.startMs !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/startMs, not both");
|
|
1284
1236
|
requireVolume(input.volume);
|
|
1285
1237
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1286
1238
|
replaceRows(draft, imported.rows);
|
|
1287
|
-
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
|
|
1239
|
+
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
|
|
1288
1240
|
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1289
1241
|
this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
|
|
1290
1242
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
@@ -1294,14 +1246,8 @@ var EntityTimelineEditor = class {
|
|
|
1294
1246
|
clipEntityId: input.voiceoverClipEntityId,
|
|
1295
1247
|
trackEntityId: speechTrack.entityId,
|
|
1296
1248
|
contentEntityId: voiceover.entityId,
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
end: input.media.durationMs
|
|
1300
|
-
},
|
|
1301
|
-
duration: {
|
|
1302
|
-
mode: "fixed",
|
|
1303
|
-
value: input.media.durationMs
|
|
1304
|
-
},
|
|
1249
|
+
inMs: 0,
|
|
1250
|
+
outMs: input.media.durationMs,
|
|
1305
1251
|
placement,
|
|
1306
1252
|
clipPayload: { volume: input.volume }
|
|
1307
1253
|
});
|
|
@@ -1315,8 +1261,8 @@ var EntityTimelineEditor = class {
|
|
|
1315
1261
|
volume: input.volume
|
|
1316
1262
|
}
|
|
1317
1263
|
});
|
|
1318
|
-
this.relinkRelation(draft, topology.
|
|
1319
|
-
endpoint0EntityId: topology.
|
|
1264
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1265
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1320
1266
|
endpoint1EntityId: voiceover.entityId
|
|
1321
1267
|
});
|
|
1322
1268
|
this.applyClipPlacement(draft, topology.clip.entityId, placement);
|
|
@@ -1325,14 +1271,8 @@ var EntityTimelineEditor = class {
|
|
|
1325
1271
|
...placed.marker,
|
|
1326
1272
|
payload: {
|
|
1327
1273
|
...placed.marker.payload,
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
end: input.media.durationMs
|
|
1331
|
-
},
|
|
1332
|
-
duration: {
|
|
1333
|
-
mode: "fixed",
|
|
1334
|
-
value: input.media.durationMs
|
|
1335
|
-
}
|
|
1274
|
+
inMs: 0,
|
|
1275
|
+
outMs: input.media.durationMs
|
|
1336
1276
|
}
|
|
1337
1277
|
});
|
|
1338
1278
|
}
|
|
@@ -1381,11 +1321,8 @@ var EntityTimelineEditor = class {
|
|
|
1381
1321
|
clipEntityId: input.bgmClipEntityId,
|
|
1382
1322
|
trackEntityId: track.entityId,
|
|
1383
1323
|
contentEntityId: imported.contentEntityId,
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
end: input.media.durationMs
|
|
1387
|
-
},
|
|
1388
|
-
duration: { mode: "from-source" },
|
|
1324
|
+
inMs: 0,
|
|
1325
|
+
outMs: input.media.durationMs,
|
|
1389
1326
|
placement: {
|
|
1390
1327
|
kind: "sequential",
|
|
1391
1328
|
order: 0
|
|
@@ -1397,7 +1334,7 @@ var EntityTimelineEditor = class {
|
|
|
1397
1334
|
...topology.marker,
|
|
1398
1335
|
payload: {
|
|
1399
1336
|
...topology.marker.payload,
|
|
1400
|
-
|
|
1337
|
+
fillTimeline: true
|
|
1401
1338
|
}
|
|
1402
1339
|
});
|
|
1403
1340
|
return clipId;
|
|
@@ -1430,7 +1367,7 @@ var EntityTimelineEditor = class {
|
|
|
1430
1367
|
const assembled = new EntityProjectionGraph(draft);
|
|
1431
1368
|
for (const clip of clipsOnTrack(draft, track.entityId)) {
|
|
1432
1369
|
const topology = requireClipTopology(draft, clip.entityId);
|
|
1433
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1370
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1434
1371
|
const prior = assembled.entity(caption.entityId).payload.style;
|
|
1435
1372
|
const style = isJsonObject(prior) ? prior : {};
|
|
1436
1373
|
for (const updated of updateEntityFields(draft, caption.entityId, { style: {
|
|
@@ -1467,14 +1404,8 @@ var EntityTimelineEditor = class {
|
|
|
1467
1404
|
return this.insertPlacedClipIntoDraft(draft, {
|
|
1468
1405
|
trackEntityId: track.entityId,
|
|
1469
1406
|
contentEntityId: content.entityId,
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
end: durationMs
|
|
1473
|
-
},
|
|
1474
|
-
duration: {
|
|
1475
|
-
mode: "fixed",
|
|
1476
|
-
value: durationMs
|
|
1477
|
-
},
|
|
1407
|
+
inMs: 0,
|
|
1408
|
+
outMs: durationMs,
|
|
1478
1409
|
placement: input.placement,
|
|
1479
1410
|
...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
|
|
1480
1411
|
});
|
|
@@ -1487,8 +1418,7 @@ var EntityTimelineEditor = class {
|
|
|
1487
1418
|
const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
|
|
1488
1419
|
const markerId = this.mintEntityId(draft);
|
|
1489
1420
|
const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
|
|
1490
|
-
|
|
1491
|
-
const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
|
|
1421
|
+
const markerPlacement = applyPlacementPayload(input.placement);
|
|
1492
1422
|
const clip = {
|
|
1493
1423
|
entityId: clipId,
|
|
1494
1424
|
entityKind: "clip",
|
|
@@ -1498,13 +1428,12 @@ var EntityTimelineEditor = class {
|
|
|
1498
1428
|
entityId: markerId,
|
|
1499
1429
|
entityKind: "sequence-marker",
|
|
1500
1430
|
payload: markerPayload({
|
|
1501
|
-
|
|
1502
|
-
duration: input.duration,
|
|
1431
|
+
...requireWindow(input.inMs, input.outMs),
|
|
1503
1432
|
...markerPlacement
|
|
1504
1433
|
})
|
|
1505
1434
|
};
|
|
1506
1435
|
draft.entities.push(clip, marker);
|
|
1507
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1436
|
+
draft.relations.push(this.createRelation(draft, "contain", track.entityId, clipId), this.createRelation(draft, "marker", clipId, markerId), this.createRelation(draft, "contain", clipId, content.entityId));
|
|
1508
1437
|
if (input.placement.kind === "anchored") {
|
|
1509
1438
|
const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
|
|
1510
1439
|
if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
|
|
@@ -1514,20 +1443,7 @@ var EntityTimelineEditor = class {
|
|
|
1514
1443
|
}
|
|
1515
1444
|
ensureRoleTrack(draft, timelineEntityId, role) {
|
|
1516
1445
|
const existing = findRoleTrack(draft, timelineEntityId, role);
|
|
1517
|
-
if (existing !== void 0)
|
|
1518
|
-
if (existing.payload.order === void 0) {
|
|
1519
|
-
const replacement = {
|
|
1520
|
-
...existing,
|
|
1521
|
-
payload: {
|
|
1522
|
-
...existing.payload,
|
|
1523
|
-
order: roleOrder[role]
|
|
1524
|
-
}
|
|
1525
|
-
};
|
|
1526
|
-
replaceEntity(draft, existing.entityId, replacement);
|
|
1527
|
-
return replacement;
|
|
1528
|
-
}
|
|
1529
|
-
return existing;
|
|
1530
|
-
}
|
|
1446
|
+
if (existing !== void 0) return existing;
|
|
1531
1447
|
const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
|
|
1532
1448
|
const track = {
|
|
1533
1449
|
entityId: this.mintEntityId(draft),
|
|
@@ -1538,7 +1454,7 @@ var EntityTimelineEditor = class {
|
|
|
1538
1454
|
}
|
|
1539
1455
|
};
|
|
1540
1456
|
draft.entities.push(track);
|
|
1541
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1457
|
+
draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
|
|
1542
1458
|
return track;
|
|
1543
1459
|
}
|
|
1544
1460
|
/** The existing pronunciation variant a voiceover consumes; composition is required. */
|
|
@@ -1549,13 +1465,13 @@ var EntityTimelineEditor = class {
|
|
|
1549
1465
|
}
|
|
1550
1466
|
/** Preserve the factual synthesis input as `[rendered Audio, PhoneticScript]`. */
|
|
1551
1467
|
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
1552
|
-
const renders = draft.relations.filter((relation) => relation.relationKind === "
|
|
1468
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "derived-from" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
|
|
1553
1469
|
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
1554
1470
|
if (renders[0] !== void 0) {
|
|
1555
1471
|
if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
|
|
1556
1472
|
throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1557
1473
|
}
|
|
1558
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1474
|
+
draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
1559
1475
|
}
|
|
1560
1476
|
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
|
|
1561
1477
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
@@ -1586,24 +1502,18 @@ var EntityTimelineEditor = class {
|
|
|
1586
1502
|
clipEntityId: caption.captionClipEntityId,
|
|
1587
1503
|
trackEntityId: captionTrack.entityId,
|
|
1588
1504
|
contentEntityId: content.entityId,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
end: durationMs
|
|
1592
|
-
},
|
|
1593
|
-
duration: {
|
|
1594
|
-
mode: "fixed",
|
|
1595
|
-
value: durationMs
|
|
1596
|
-
},
|
|
1505
|
+
inMs: 0,
|
|
1506
|
+
outMs: durationMs,
|
|
1597
1507
|
placement: {
|
|
1598
1508
|
kind: "anchored",
|
|
1599
1509
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1600
|
-
|
|
1510
|
+
startMs
|
|
1601
1511
|
}
|
|
1602
1512
|
});
|
|
1603
1513
|
continue;
|
|
1604
1514
|
}
|
|
1605
1515
|
const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
|
|
1606
|
-
const content = requireEntityKind(draft, otherEndpoint(topology.
|
|
1516
|
+
const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1607
1517
|
const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
|
|
1608
1518
|
const style = caption.style === void 0 ? priorStyle : {
|
|
1609
1519
|
...priorStyle,
|
|
@@ -1622,17 +1532,16 @@ var EntityTimelineEditor = class {
|
|
|
1622
1532
|
this.applyClipPlacement(draft, topology.clip.entityId, {
|
|
1623
1533
|
kind: "anchored",
|
|
1624
1534
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1625
|
-
|
|
1535
|
+
startMs
|
|
1626
1536
|
});
|
|
1627
1537
|
const placed = requireClipTopology(draft, topology.clip.entityId);
|
|
1538
|
+
const window = markerWindow(placed.marker.payload);
|
|
1539
|
+
const rate = (window.outMs - window.inMs) / durationMs;
|
|
1628
1540
|
replaceEntity(draft, placed.marker.entityId, {
|
|
1629
1541
|
...placed.marker,
|
|
1630
1542
|
payload: {
|
|
1631
1543
|
...placed.marker.payload,
|
|
1632
|
-
|
|
1633
|
-
mode: "fixed",
|
|
1634
|
-
value: durationMs
|
|
1635
|
-
}
|
|
1544
|
+
...rate === 1 ? {} : { rate }
|
|
1636
1545
|
}
|
|
1637
1546
|
});
|
|
1638
1547
|
}
|
|
@@ -1640,7 +1549,7 @@ var EntityTimelineEditor = class {
|
|
|
1640
1549
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
1641
1550
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1642
1551
|
const track = requireEntityKind(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId), "track");
|
|
1643
|
-
const timeline = requireEntityKind(draft, otherEndpoint(
|
|
1552
|
+
const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
|
|
1644
1553
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1645
1554
|
}
|
|
1646
1555
|
/**
|
|
@@ -1649,13 +1558,13 @@ var EntityTimelineEditor = class {
|
|
|
1649
1558
|
*/
|
|
1650
1559
|
removeCaptionClipGraph(draft, clipEntityId) {
|
|
1651
1560
|
const topology = requireClipOnRole(draft, clipEntityId, "caption");
|
|
1652
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1561
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1653
1562
|
const contentRelations = incidentRelations(draft, caption.entityId);
|
|
1654
|
-
const allowed = new Set(["caption-alignment", "
|
|
1563
|
+
const allowed = new Set(["caption-alignment", "contain"]);
|
|
1655
1564
|
const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
|
|
1656
1565
|
if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
|
|
1657
1566
|
this.removeClipGraph(draft, clipEntityId);
|
|
1658
|
-
if (
|
|
1567
|
+
if (containing(draft, caption.entityId, "contained").length > 0) return;
|
|
1659
1568
|
const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
|
|
1660
1569
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1661
1570
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
|
|
@@ -1703,10 +1612,7 @@ var EntityTimelineEditor = class {
|
|
|
1703
1612
|
const overlapMs = Math.round(prior.end - current.start);
|
|
1704
1613
|
for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
|
|
1705
1614
|
kind: "absolute",
|
|
1706
|
-
|
|
1707
|
-
start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
|
|
1708
|
-
end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1709
|
-
}
|
|
1615
|
+
startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1710
1616
|
});
|
|
1711
1617
|
return;
|
|
1712
1618
|
}
|
|
@@ -1715,18 +1621,16 @@ var EntityTimelineEditor = class {
|
|
|
1715
1621
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1716
1622
|
const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
|
|
1717
1623
|
const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1718
|
-
delete
|
|
1719
|
-
delete markerPayloadValue.targetRange;
|
|
1720
|
-
delete markerPayloadValue.anchorOffset;
|
|
1624
|
+
delete markerPayloadValue.startMs;
|
|
1721
1625
|
const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
|
|
1722
1626
|
const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
|
|
1723
1627
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1724
|
-
if (placement.kind === "sequential")
|
|
1725
|
-
else if (placement.kind === "absolute") markerPayloadValue.
|
|
1628
|
+
if (placement.kind === "sequential") setClipOrder(draft, topology.clip.entityId, placement.order);
|
|
1629
|
+
else if (placement.kind === "absolute") markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
|
|
1726
1630
|
else {
|
|
1727
1631
|
const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
|
|
1728
1632
|
if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
|
|
1729
|
-
markerPayloadValue.
|
|
1633
|
+
markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
|
|
1730
1634
|
draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
|
|
1731
1635
|
}
|
|
1732
1636
|
replaceEntity(draft, topology.clip.entityId, {
|
|
@@ -1741,14 +1645,10 @@ var EntityTimelineEditor = class {
|
|
|
1741
1645
|
reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
|
|
1742
1646
|
const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
|
|
1743
1647
|
if (hosts.length === 0) {
|
|
1744
|
-
|
|
1745
|
-
if (child === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
|
|
1648
|
+
if (layout.clips.get(childClipEntityId) === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
|
|
1746
1649
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1747
1650
|
kind: "absolute",
|
|
1748
|
-
|
|
1749
|
-
start: absoluteStart,
|
|
1750
|
-
end: absoluteStart + child.duration
|
|
1751
|
-
}
|
|
1651
|
+
startMs: absoluteStart
|
|
1752
1652
|
});
|
|
1753
1653
|
return;
|
|
1754
1654
|
}
|
|
@@ -1756,7 +1656,7 @@ var EntityTimelineEditor = class {
|
|
|
1756
1656
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1757
1657
|
kind: "anchored",
|
|
1758
1658
|
hostClipEntityId: host.clipEntityId,
|
|
1759
|
-
|
|
1659
|
+
startMs: Math.max(0, Math.round(absoluteStart - host.start))
|
|
1760
1660
|
});
|
|
1761
1661
|
}
|
|
1762
1662
|
removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
|
|
@@ -1773,12 +1673,12 @@ var EntityTimelineEditor = class {
|
|
|
1773
1673
|
removeClipGraph(draft, clipEntityId) {
|
|
1774
1674
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1775
1675
|
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
|
|
1776
|
-
"
|
|
1777
|
-
"
|
|
1676
|
+
"contain",
|
|
1677
|
+
"marker",
|
|
1778
1678
|
"clip-anchor"
|
|
1779
1679
|
]));
|
|
1780
|
-
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["
|
|
1781
|
-
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "
|
|
1680
|
+
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
|
|
1681
|
+
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
|
|
1782
1682
|
const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
|
|
1783
1683
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
|
|
1784
1684
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
@@ -1824,7 +1724,7 @@ var EntityTimelineEditor = class {
|
|
|
1824
1724
|
}
|
|
1825
1725
|
};
|
|
1826
1726
|
function validateFinalRows(rows) {
|
|
1827
|
-
decodeEntityRelationRows(rows,
|
|
1727
|
+
decodeEntityRelationRows(rows, {});
|
|
1828
1728
|
assertCanonicalEditorResources(rows);
|
|
1829
1729
|
validateTimelineHierarchy(rows);
|
|
1830
1730
|
}
|
|
@@ -1832,13 +1732,13 @@ function validateTimelineHierarchy(rows) {
|
|
|
1832
1732
|
for (const entity of rows.entities) {
|
|
1833
1733
|
if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
|
|
1834
1734
|
if (entity.entityKind !== "clip") continue;
|
|
1835
|
-
const trackRelations =
|
|
1735
|
+
const trackRelations = containing(rows, entity.entityId, "contained");
|
|
1836
1736
|
if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
|
|
1837
1737
|
requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
|
|
1838
1738
|
}
|
|
1839
1739
|
}
|
|
1840
1740
|
function requireTrackTimeline(rows, trackEntityId) {
|
|
1841
|
-
const timelineRelations =
|
|
1741
|
+
const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
|
|
1842
1742
|
if (timelineRelations.filter((relation) => {
|
|
1843
1743
|
const otherId = otherEndpoint(relation, trackEntityId);
|
|
1844
1744
|
return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
|
|
@@ -1846,20 +1746,36 @@ function requireTrackTimeline(rows, trackEntityId) {
|
|
|
1846
1746
|
}
|
|
1847
1747
|
function requireClipTopology(rows, clipEntityId) {
|
|
1848
1748
|
const clip = requireEntityKind(rows, clipEntityId, "clip");
|
|
1849
|
-
const trackRelation =
|
|
1749
|
+
const trackRelation = requireOneContain(rows, clip.entityId, "contained");
|
|
1850
1750
|
requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
|
|
1851
|
-
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "
|
|
1751
|
+
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
|
|
1852
1752
|
const marker = requireEntityKind(rows, otherEndpoint(clipMarkerRelation, clip.entityId), "sequence-marker");
|
|
1853
|
-
const
|
|
1854
|
-
requireSequenceContent(rows, otherEndpoint(
|
|
1753
|
+
const contentRelation = requireOneContain(rows, clip.entityId, "container");
|
|
1754
|
+
requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
|
|
1855
1755
|
return {
|
|
1856
1756
|
clip,
|
|
1857
1757
|
marker,
|
|
1858
1758
|
trackRelation,
|
|
1859
1759
|
clipMarkerRelation,
|
|
1860
|
-
|
|
1760
|
+
contentRelation
|
|
1861
1761
|
};
|
|
1862
1762
|
}
|
|
1763
|
+
/**
|
|
1764
|
+
* One `contain` Relation with this entity on the stated side.
|
|
1765
|
+
*
|
|
1766
|
+
* A Clip is contained by its Track and contains its content; a Track is
|
|
1767
|
+
* contained by the Timeline and contains its Clips. Both are `contain`, so the
|
|
1768
|
+
* endpoint position is the only thing that tells them apart.
|
|
1769
|
+
*/
|
|
1770
|
+
function requireOneContain(rows, entityId, side) {
|
|
1771
|
+
const matches = containing(rows, entityId, side);
|
|
1772
|
+
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must be the ${side} of exactly one contain Relation`);
|
|
1773
|
+
return matches[0];
|
|
1774
|
+
}
|
|
1775
|
+
/** `contain` is ordered, so a lookup says which side it wants. */
|
|
1776
|
+
function containing(rows, entityId, side) {
|
|
1777
|
+
return (side === "container" ? relationsFrom : relationsTo)(rows.relations, "contain", entityId);
|
|
1778
|
+
}
|
|
1863
1779
|
function requireOneRelation(rows, entityId, relationKind) {
|
|
1864
1780
|
const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
|
|
1865
1781
|
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
|
|
@@ -1898,40 +1814,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
|
|
|
1898
1814
|
function outgoingAnchorChildren(rows, hostIds) {
|
|
1899
1815
|
return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
|
|
1900
1816
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
1906
|
-
if (placement.kind === "absolute") return { targetRange: cloneJsonValue(placement.targetRange, "targetRange") };
|
|
1907
|
-
return { anchorOffset: finiteNonNegative(placement.anchorOffset, "anchorOffset") };
|
|
1817
|
+
/** What the placement puts on the Marker; a sequential one puts nothing there. */
|
|
1818
|
+
function applyPlacementPayload(placement) {
|
|
1819
|
+
if (placement.kind === "sequential") return {};
|
|
1820
|
+
return { startMs: finiteNonNegative(placement.startMs, "startMs") };
|
|
1908
1821
|
}
|
|
1909
1822
|
function requireAvailableEntityId(rows, value) {
|
|
1910
1823
|
const entityId = createEntityId(value);
|
|
1911
1824
|
if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
|
|
1912
1825
|
return entityId;
|
|
1913
1826
|
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Put a Clip at `order` among the Clips its Track contains.
|
|
1829
|
+
*
|
|
1830
|
+
* Where a Clip sits is the position of its membership Relation, so moving one
|
|
1831
|
+
* is moving that Relation — nothing on the Clip changes, and there are no
|
|
1832
|
+
* sibling positions left to keep in step.
|
|
1833
|
+
*/
|
|
1914
1834
|
function setClipOrder(rows, clipEntityId, order) {
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
|
|
1835
|
+
const membership = relationsTo(rows.relations, "contain", clipEntityId)[0];
|
|
1836
|
+
if (membership === void 0) throw new Error(`Clip "${clipEntityId}" is contained by no Track`);
|
|
1837
|
+
const slots = [];
|
|
1838
|
+
const siblings = [];
|
|
1839
|
+
for (const [index, relation] of rows.relations.entries()) {
|
|
1840
|
+
if (relation.relationKind !== "contain" || relation.endpoint0EntityId !== membership.endpoint0EntityId) continue;
|
|
1841
|
+
slots.push(index);
|
|
1842
|
+
siblings.push(relation);
|
|
1843
|
+
}
|
|
1844
|
+
const from = siblings.indexOf(membership);
|
|
1845
|
+
const moved = siblings.filter((relation) => relation !== membership);
|
|
1846
|
+
moved.splice(Math.max(0, Math.min(order, moved.length)), 0, membership);
|
|
1847
|
+
if (from < 0) return;
|
|
1848
|
+
slots.forEach((slot, at) => rows.relations[slot] = moved[at]);
|
|
1926
1849
|
}
|
|
1927
1850
|
function findRoleTrack(rows, timelineEntityId, role) {
|
|
1928
1851
|
const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
|
|
1929
|
-
const tracks =
|
|
1852
|
+
const tracks = containing(rows, timeline.entityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
|
|
1930
1853
|
if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
|
|
1931
1854
|
return tracks[0];
|
|
1932
1855
|
}
|
|
1933
1856
|
function clipsOnTrack(rows, trackEntityId) {
|
|
1934
|
-
return
|
|
1857
|
+
return containing(rows, trackEntityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
|
|
1935
1858
|
}
|
|
1936
1859
|
function clipRole(rows, clipEntityId) {
|
|
1937
1860
|
const topology = requireClipTopology(rows, clipEntityId);
|
|
@@ -1964,35 +1887,27 @@ function requireVolume(value) {
|
|
|
1964
1887
|
if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
|
|
1965
1888
|
return volume;
|
|
1966
1889
|
}
|
|
1967
|
-
function
|
|
1968
|
-
|
|
1969
|
-
const
|
|
1970
|
-
|
|
1971
|
-
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must be a positive whole-millisecond range`);
|
|
1890
|
+
function requireWindow(inMs, outMs, label = "Sequence Marker") {
|
|
1891
|
+
const start = finiteNonNegative(inMs, `${label} inMs`);
|
|
1892
|
+
const end = finitePositive(outMs, `${label} outMs`);
|
|
1893
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must state a positive whole-millisecond window`);
|
|
1972
1894
|
return {
|
|
1973
|
-
start,
|
|
1974
|
-
end
|
|
1895
|
+
inMs: start,
|
|
1896
|
+
outMs: end
|
|
1975
1897
|
};
|
|
1976
1898
|
}
|
|
1977
|
-
function
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
function
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
function resizeAbsoluteTarget(payload, duration) {
|
|
1990
|
-
if (payload.targetRange === void 0) return;
|
|
1991
|
-
const target = requirePositiveRange(payload.targetRange, "targetRange");
|
|
1992
|
-
payload.targetRange = {
|
|
1993
|
-
start: target.start,
|
|
1994
|
-
end: target.start + duration
|
|
1995
|
-
};
|
|
1899
|
+
function markerWindow(payload) {
|
|
1900
|
+
return requireWindow(payload.inMs, payload.outMs);
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* How long the Clip runs. The window and the rate are the only stored facts;
|
|
1904
|
+
* the played length follows from them rather than being kept in step by hand.
|
|
1905
|
+
*/
|
|
1906
|
+
function markerDuration(payload, window = markerWindow(payload)) {
|
|
1907
|
+
return Math.round((window.outMs - window.inMs) / markerRate(payload));
|
|
1908
|
+
}
|
|
1909
|
+
function markerRate(payload) {
|
|
1910
|
+
return payload.rate === void 0 ? 1 : finitePositive(payload.rate, "linear speed rate");
|
|
1996
1911
|
}
|
|
1997
1912
|
function replaceRows(rows, replacement) {
|
|
1998
1913
|
rows.entities = structuredClone([...replacement.entities]);
|
|
@@ -2003,9 +1918,9 @@ function finiteNonNegative(value, label) {
|
|
|
2003
1918
|
if (result < 0) throw new Error(`${label} must be non-negative`);
|
|
2004
1919
|
return result;
|
|
2005
1920
|
}
|
|
1921
|
+
/** How many Clips the Track already holds — the index a new one lands at. */
|
|
2006
1922
|
function nextSequentialOrder(rows, trackEntityId) {
|
|
2007
|
-
|
|
2008
|
-
return orders.length === 0 ? 0 : Math.max(...orders) + 1;
|
|
1923
|
+
return relationsFrom(rows.relations, "contain", trackEntityId).length;
|
|
2009
1924
|
}
|
|
2010
1925
|
function otherEndpoint(relation, entityId) {
|
|
2011
1926
|
if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
|
|
@@ -2025,19 +1940,12 @@ function cloneJsonObject(value, label) {
|
|
|
2025
1940
|
if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
|
|
2026
1941
|
return cloned;
|
|
2027
1942
|
}
|
|
2028
|
-
function cloneJsonValue(value, label) {
|
|
2029
|
-
return cloneJsonObject({ value }, label).value;
|
|
2030
|
-
}
|
|
2031
1943
|
function cloneRows(rows) {
|
|
2032
1944
|
return {
|
|
2033
1945
|
entities: rows.entities.map((entity) => structuredClone(entity)),
|
|
2034
1946
|
relations: rows.relations.map((relation) => structuredClone(relation))
|
|
2035
1947
|
};
|
|
2036
1948
|
}
|
|
2037
|
-
const numericMarkerValidation = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
2038
|
-
if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
|
|
2039
|
-
return left - right;
|
|
2040
|
-
} };
|
|
2041
1949
|
//#endregion
|
|
2042
1950
|
//#region src/entity-editor/revise-audio-script.ts
|
|
2043
1951
|
/** Revise text as an independent authored document, never as a version-family mutation. */
|
|
@@ -2054,26 +1962,8 @@ function reviseAudioScript(rows, input) {
|
|
|
2054
1962
|
...input,
|
|
2055
1963
|
captionUsages: input.captionUsages ?? []
|
|
2056
1964
|
});
|
|
2057
|
-
const prior = rows.relations.filter((r) => r.relationKind === "
|
|
2058
|
-
if (prior.length)
|
|
2059
|
-
if (prior.length !== 1 || prior[0].trace.signature !== signature) throw new Error("AudioScript edit identity conflict");
|
|
2060
|
-
const captionUsages = (input.captionUsages ?? []).map((usage, index) => {
|
|
2061
|
-
const edge = rows.relations.find((r) => r.relationKind === "marker-content" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId && r.trace.usageIndex === index);
|
|
2062
|
-
if (!edge || edge.endpoint0EntityId !== usage.markerEntityId) throw new Error("Caption usage conflict after revision");
|
|
2063
|
-
return {
|
|
2064
|
-
clipEntityId: usage.clipEntityId,
|
|
2065
|
-
markerEntityId: usage.markerEntityId,
|
|
2066
|
-
previousCaptionEntityId: usage.captionEntityId,
|
|
2067
|
-
captionEntityId: String(edge.endpoint1EntityId)
|
|
2068
|
-
};
|
|
2069
|
-
});
|
|
2070
|
-
return {
|
|
2071
|
-
rows,
|
|
2072
|
-
audioScriptEntityId: String(prior[0].endpoint0EntityId),
|
|
2073
|
-
captionUsages,
|
|
2074
|
-
changed: false
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
1965
|
+
const prior = rows.relations.filter((r) => r.relationKind === "derived-from" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId);
|
|
1966
|
+
if (prior.length && (prior.length !== 1 || prior[0].trace.signature !== signature)) throw new Error("AudioScript edit identity conflict");
|
|
2077
1967
|
const source = rows.entities.find((row) => row.entityId === input.sourceAudioScriptEntityId);
|
|
2078
1968
|
if (source?.entityKind !== "audio-script") throw new Error("AudioScript source is missing");
|
|
2079
1969
|
assembleScriptText({
|
|
@@ -2094,39 +1984,37 @@ function reviseAudioScript(rows, input) {
|
|
|
2094
1984
|
text
|
|
2095
1985
|
};
|
|
2096
1986
|
});
|
|
2097
|
-
if (!changedSegments.size)
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
1987
|
+
if (!changedSegments.size) {
|
|
1988
|
+
if (prior.length) throw new Error("AudioScript edit identity conflict");
|
|
1989
|
+
return {
|
|
1990
|
+
rows,
|
|
1991
|
+
audioScriptEntityId: source.entityId,
|
|
1992
|
+
captionUsages: [],
|
|
1993
|
+
changed: false
|
|
1994
|
+
};
|
|
1995
|
+
}
|
|
2103
1996
|
delete payload.assetContentHash;
|
|
2104
1997
|
const id = hash(input.editId);
|
|
2105
1998
|
const audioScriptEntityId = createEntityId(`entity_script_revision_${id}`);
|
|
2106
|
-
const
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
}
|
|
2123
|
-
}]
|
|
1999
|
+
const audioScript = {
|
|
2000
|
+
entityId: audioScriptEntityId,
|
|
2001
|
+
entityKind: "audio-script",
|
|
2002
|
+
payload
|
|
2003
|
+
};
|
|
2004
|
+
const provenance = {
|
|
2005
|
+
relationId: createRelationId(`relation_script_revision_${id}`),
|
|
2006
|
+
relationKind: "derived-from",
|
|
2007
|
+
endpoint0EntityId: audioScriptEntityId,
|
|
2008
|
+
endpoint1EntityId: source.entityId,
|
|
2009
|
+
metadata: {},
|
|
2010
|
+
trace: {
|
|
2011
|
+
command: "revise-audio-script",
|
|
2012
|
+
editId: input.editId,
|
|
2013
|
+
signature
|
|
2014
|
+
}
|
|
2124
2015
|
};
|
|
2125
|
-
const entities = [...result.entities];
|
|
2126
|
-
let relations = [...result.relations];
|
|
2127
|
-
const captionUsages = [];
|
|
2128
2016
|
const selectedMarkers = /* @__PURE__ */ new Set();
|
|
2129
|
-
|
|
2017
|
+
const planCaption = (usage, index, targetAudioScriptEntityId) => {
|
|
2130
2018
|
if (selectedMarkers.has(usage.markerEntityId)) throw new Error("Duplicate caption usage");
|
|
2131
2019
|
selectedMarkers.add(usage.markerEntityId);
|
|
2132
2020
|
const caption = rows.entities.find((row) => row.entityId === usage.captionEntityId);
|
|
@@ -2137,52 +2025,131 @@ function reviseAudioScript(rows, input) {
|
|
|
2137
2025
|
if (isJsonObject(selection) && selection.textRange !== void 0 && typeof selection.segmentId === "string" && changedSegments.has(selection.segmentId) && usage.selection === void 0) throw new Error("Changed partial Caption requires an explicit selection");
|
|
2138
2026
|
const bases = caption.payload.baseEntityIds;
|
|
2139
2027
|
if (!Array.isArray(bases) || !bases.includes(source.entityId)) throw new Error("Caption requires a direct AudioScript base");
|
|
2140
|
-
const clipMarkers = rows.relations.filter((r) => r.relationKind === "
|
|
2141
|
-
const
|
|
2142
|
-
const markerOwners = rows.relations.filter((r) =>
|
|
2143
|
-
if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 ||
|
|
2028
|
+
const clipMarkers = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint0EntityId === usage.clipEntityId);
|
|
2029
|
+
const clipContents = rows.relations.filter((r) => r.relationKind === "contain" && r.endpoint0EntityId === usage.clipEntityId);
|
|
2030
|
+
const markerOwners = rows.relations.filter((r) => r.relationKind === "marker" && r.endpoint1EntityId === usage.markerEntityId);
|
|
2031
|
+
if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 || clipContents.length !== 1) throw new Error("Caption usage conflict");
|
|
2144
2032
|
const newCaptionId = createEntityId(`entity_caption_revision_${id}_${index}`);
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
relations
|
|
2158
|
-
}, newCaptionId);
|
|
2159
|
-
relations = relations.filter((r) => r.relationId !== markerContents[0].relationId);
|
|
2160
|
-
relations.push({
|
|
2161
|
-
...markerContents[0],
|
|
2033
|
+
return {
|
|
2034
|
+
clone: {
|
|
2035
|
+
...caption,
|
|
2036
|
+
entityId: newCaptionId,
|
|
2037
|
+
payload: {
|
|
2038
|
+
...structuredClone(caption.payload),
|
|
2039
|
+
baseEntityIds: bases.map((base) => base === source.entityId ? targetAudioScriptEntityId : base),
|
|
2040
|
+
...usage.selection === void 0 ? {} : { selection: structuredClone(usage.selection) }
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
2043
|
+
clipContent: clipContents[0],
|
|
2044
|
+
newCaptionId,
|
|
2162
2045
|
relationId: createRelationId(`relation_caption_revision_${id}_${index}`),
|
|
2163
|
-
endpoint1EntityId: newCaptionId,
|
|
2164
2046
|
trace: {
|
|
2165
2047
|
command: "revise-audio-script",
|
|
2166
2048
|
editId: input.editId,
|
|
2167
2049
|
usageIndex: index
|
|
2168
2050
|
}
|
|
2051
|
+
};
|
|
2052
|
+
};
|
|
2053
|
+
if (prior.length) {
|
|
2054
|
+
const currentAudioScriptEntityId = prior[0].endpoint0EntityId;
|
|
2055
|
+
if (!equalJson(prior[0], {
|
|
2056
|
+
...provenance,
|
|
2057
|
+
relationId: prior[0].relationId,
|
|
2058
|
+
endpoint0EntityId: currentAudioScriptEntityId
|
|
2059
|
+
})) throw new Error("AudioScript edit identity conflict");
|
|
2060
|
+
const preservedScripts = rows.entities.filter((row) => row.entityId === currentAudioScriptEntityId);
|
|
2061
|
+
const preservedScript = preservedScripts[0]?.entityKind === "audio-script" ? {
|
|
2062
|
+
...preservedScripts[0],
|
|
2063
|
+
payload: Object.fromEntries(Object.entries(preservedScripts[0].payload).filter(([key]) => key !== "assetContentHash"))
|
|
2064
|
+
} : preservedScripts[0];
|
|
2065
|
+
if (preservedScripts.length !== 1 || !equalJson(preservedScript, {
|
|
2066
|
+
...audioScript,
|
|
2067
|
+
entityId: currentAudioScriptEntityId
|
|
2068
|
+
})) throw new Error("AudioScript edit identity conflict");
|
|
2069
|
+
let relations = [...rows.relations];
|
|
2070
|
+
let changed = false;
|
|
2071
|
+
const captionUsages = [];
|
|
2072
|
+
for (const [index, usage] of (input.captionUsages ?? []).entries()) {
|
|
2073
|
+
const plan = planCaption(usage, index, currentAudioScriptEntityId);
|
|
2074
|
+
const preservedCaptions = rows.entities.filter((row) => row.entityId === plan.newCaptionId);
|
|
2075
|
+
if (preservedCaptions.length !== 1 || !equalJson(preservedCaptions[0], plan.clone)) throw new Error("Caption usage conflict after revision");
|
|
2076
|
+
const plannedEdge = {
|
|
2077
|
+
relationId: plan.relationId,
|
|
2078
|
+
relationKind: "contain",
|
|
2079
|
+
endpoint0EntityId: plan.clipContent.endpoint0EntityId,
|
|
2080
|
+
endpoint1EntityId: plan.newCaptionId,
|
|
2081
|
+
metadata: structuredClone(plan.clipContent.metadata),
|
|
2082
|
+
trace: plan.trace
|
|
2083
|
+
};
|
|
2084
|
+
if (plan.clipContent.relationId === plan.relationId) {
|
|
2085
|
+
if (!equalJson(plan.clipContent, plannedEdge)) throw new Error("Caption usage conflict after revision");
|
|
2086
|
+
} else {
|
|
2087
|
+
if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict after revision");
|
|
2088
|
+
relations = relations.filter((row) => row.relationId !== plan.clipContent.relationId);
|
|
2089
|
+
relations.push(plannedEdge);
|
|
2090
|
+
changed = true;
|
|
2091
|
+
}
|
|
2092
|
+
captionUsages.push({
|
|
2093
|
+
clipEntityId: usage.clipEntityId,
|
|
2094
|
+
markerEntityId: usage.markerEntityId,
|
|
2095
|
+
previousCaptionEntityId: usage.captionEntityId,
|
|
2096
|
+
captionEntityId: plan.newCaptionId
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
if (!changed) return {
|
|
2100
|
+
rows,
|
|
2101
|
+
audioScriptEntityId: currentAudioScriptEntityId,
|
|
2102
|
+
captionUsages,
|
|
2103
|
+
changed: false
|
|
2104
|
+
};
|
|
2105
|
+
const revised = {
|
|
2106
|
+
entities: rows.entities,
|
|
2107
|
+
relations
|
|
2108
|
+
};
|
|
2109
|
+
decodeAndValidate(rows, revised);
|
|
2110
|
+
return {
|
|
2111
|
+
rows: revised,
|
|
2112
|
+
audioScriptEntityId: currentAudioScriptEntityId,
|
|
2113
|
+
captionUsages,
|
|
2114
|
+
changed: true
|
|
2115
|
+
};
|
|
2116
|
+
}
|
|
2117
|
+
const result = {
|
|
2118
|
+
entities: [...rows.entities, audioScript],
|
|
2119
|
+
relations: [...rows.relations, provenance]
|
|
2120
|
+
};
|
|
2121
|
+
const entities = [...result.entities];
|
|
2122
|
+
let relations = [...result.relations];
|
|
2123
|
+
const captionUsages = [];
|
|
2124
|
+
for (const [index, usage] of (input.captionUsages ?? []).entries()) {
|
|
2125
|
+
const plan = planCaption(usage, index, audioScriptEntityId);
|
|
2126
|
+
if (plan.clipContent.endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict");
|
|
2127
|
+
entities.push(plan.clone);
|
|
2128
|
+
assembleCaptionContent({
|
|
2129
|
+
entities,
|
|
2130
|
+
relations
|
|
2131
|
+
}, plan.newCaptionId);
|
|
2132
|
+
relations = relations.filter((r) => r.relationId !== plan.clipContent.relationId);
|
|
2133
|
+
relations.push({
|
|
2134
|
+
relationId: plan.relationId,
|
|
2135
|
+
relationKind: "contain",
|
|
2136
|
+
endpoint0EntityId: plan.clipContent.endpoint0EntityId,
|
|
2137
|
+
endpoint1EntityId: plan.newCaptionId,
|
|
2138
|
+
metadata: structuredClone(plan.clipContent.metadata),
|
|
2139
|
+
trace: plan.trace
|
|
2169
2140
|
});
|
|
2170
2141
|
captionUsages.push({
|
|
2171
2142
|
clipEntityId: usage.clipEntityId,
|
|
2172
2143
|
markerEntityId: usage.markerEntityId,
|
|
2173
2144
|
previousCaptionEntityId: usage.captionEntityId,
|
|
2174
|
-
captionEntityId: newCaptionId
|
|
2145
|
+
captionEntityId: plan.newCaptionId
|
|
2175
2146
|
});
|
|
2176
2147
|
}
|
|
2177
2148
|
const revised = {
|
|
2178
2149
|
entities,
|
|
2179
2150
|
relations
|
|
2180
2151
|
};
|
|
2181
|
-
|
|
2182
|
-
if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
|
|
2183
|
-
return left - right;
|
|
2184
|
-
} });
|
|
2185
|
-
validateEntityRelationTransition(rows, revised);
|
|
2152
|
+
decodeAndValidate(rows, revised);
|
|
2186
2153
|
return {
|
|
2187
2154
|
rows: revised,
|
|
2188
2155
|
audioScriptEntityId,
|
|
@@ -2190,6 +2157,10 @@ function reviseAudioScript(rows, input) {
|
|
|
2190
2157
|
changed: true
|
|
2191
2158
|
};
|
|
2192
2159
|
}
|
|
2160
|
+
function decodeAndValidate(previous, revised) {
|
|
2161
|
+
decodeEntityRelationRows(revised, {});
|
|
2162
|
+
validateEntityRelationTransition(previous, revised);
|
|
2163
|
+
}
|
|
2193
2164
|
function hash(value) {
|
|
2194
2165
|
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
|
|
2195
2166
|
}
|
|
@@ -2264,7 +2235,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2264
2235
|
relations
|
|
2265
2236
|
}, {
|
|
2266
2237
|
assetId: speech.origin_speech_id,
|
|
2267
|
-
kind: "voiceover",
|
|
2238
|
+
kind: "voiceover-recording",
|
|
2268
2239
|
durationMs: speech.media_duration_ms,
|
|
2269
2240
|
storageKey: speech.audio_storage_key,
|
|
2270
2241
|
voice: {
|
|
@@ -2302,7 +2273,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2302
2273
|
...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
|
|
2303
2274
|
}, oldTrack.id);
|
|
2304
2275
|
trackIds.set(oldTrack, track);
|
|
2305
|
-
link("
|
|
2276
|
+
link("contain", timeline, track);
|
|
2306
2277
|
}
|
|
2307
2278
|
for (const oldTrack of legacy.tracks ?? []) {
|
|
2308
2279
|
const track = trackIds.get(oldTrack);
|
|
@@ -2312,7 +2283,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2312
2283
|
const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
|
|
2313
2284
|
const clip = addEntity("clip", {
|
|
2314
2285
|
...volume === void 0 ? {} : { volume },
|
|
2315
|
-
|
|
2286
|
+
order: role === "bgm" ? 0 : order
|
|
2316
2287
|
}, item.part_id);
|
|
2317
2288
|
let content = contentByPartId.get(item.part_id);
|
|
2318
2289
|
let duration;
|
|
@@ -2323,37 +2294,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2323
2294
|
const fact = facts.get(video.origin_media_id);
|
|
2324
2295
|
const rate = speedOf(video.speed_shift);
|
|
2325
2296
|
markerPayload = {
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
},
|
|
2330
|
-
duration: {
|
|
2331
|
-
mode: "fixed",
|
|
2332
|
-
value: duration
|
|
2333
|
-
},
|
|
2334
|
-
...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { timeRemapping: {
|
|
2335
|
-
kind: "linear",
|
|
2336
|
-
mode: video.speed_shift?.mode ?? "constant",
|
|
2337
|
-
rate
|
|
2338
|
-
} } : {}
|
|
2297
|
+
inMs: video.play_in,
|
|
2298
|
+
outMs: video.play_out,
|
|
2299
|
+
...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
|
|
2339
2300
|
};
|
|
2340
2301
|
} else if (part.speech !== void 0) {
|
|
2341
2302
|
duration = part.speech.media_duration_ms;
|
|
2342
2303
|
markerPayload = {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
end: duration
|
|
2346
|
-
},
|
|
2347
|
-
duration: {
|
|
2348
|
-
mode: "fixed",
|
|
2349
|
-
value: duration
|
|
2350
|
-
}
|
|
2304
|
+
inMs: 0,
|
|
2305
|
+
outMs: duration
|
|
2351
2306
|
};
|
|
2352
2307
|
} else if (part.caption !== void 0) {
|
|
2353
2308
|
const caption = part.caption;
|
|
2354
2309
|
duration = caption.initial_duration_ms;
|
|
2355
2310
|
const voiceover = contentByPartId.get(caption.speech_part_id);
|
|
2356
|
-
const render = relations.find((relation) => relation.relationKind === "
|
|
2311
|
+
const render = relations.find((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceover);
|
|
2357
2312
|
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2358
2313
|
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2359
2314
|
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
@@ -2375,14 +2330,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2375
2330
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2376
2331
|
});
|
|
2377
2332
|
markerPayload = {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
end: duration
|
|
2381
|
-
},
|
|
2382
|
-
duration: {
|
|
2383
|
-
mode: "fixed",
|
|
2384
|
-
value: duration
|
|
2385
|
-
}
|
|
2333
|
+
inMs: 0,
|
|
2334
|
+
outMs: duration
|
|
2386
2335
|
};
|
|
2387
2336
|
} else {
|
|
2388
2337
|
const bgm = part.bgm;
|
|
@@ -2390,35 +2339,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2390
2339
|
if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
|
|
2391
2340
|
duration = fact.durationMs;
|
|
2392
2341
|
markerPayload = {
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
},
|
|
2397
|
-
duration: { mode: "from-source" },
|
|
2398
|
-
durationPolicy: "timeline"
|
|
2342
|
+
inMs: 0,
|
|
2343
|
+
outMs: duration,
|
|
2344
|
+
fillTimeline: true
|
|
2399
2345
|
};
|
|
2400
2346
|
}
|
|
2401
2347
|
if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
|
|
2402
2348
|
const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2403
2349
|
if (normalizedMainRange !== void 0) markerPayload = {
|
|
2404
2350
|
...markerPayload,
|
|
2405
|
-
|
|
2351
|
+
startMs: normalizedMainRange.start
|
|
2406
2352
|
};
|
|
2407
|
-
else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
|
|
2353
|
+
else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
|
|
2408
2354
|
...markerPayload,
|
|
2409
|
-
|
|
2410
|
-
start: item.time_position.offsetMs,
|
|
2411
|
-
end: item.time_position.offsetMs + duration
|
|
2412
|
-
}
|
|
2413
|
-
};
|
|
2414
|
-
else if (item.time_position.mode === "anchored") markerPayload = {
|
|
2415
|
-
...markerPayload,
|
|
2416
|
-
anchorOffset: item.time_position.offsetMs
|
|
2355
|
+
startMs: item.time_position.offsetMs
|
|
2417
2356
|
};
|
|
2418
2357
|
const marker = addEntity("sequence-marker", markerPayload);
|
|
2419
|
-
link("
|
|
2420
|
-
link("
|
|
2421
|
-
link("
|
|
2358
|
+
link("contain", track, clip);
|
|
2359
|
+
link("marker", clip, marker);
|
|
2360
|
+
link("contain", clip, content);
|
|
2422
2361
|
if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
|
|
2423
2362
|
}
|
|
2424
2363
|
}
|
|
@@ -2439,7 +2378,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2439
2378
|
for (const part of Object.values(view.part_library ?? {})) {
|
|
2440
2379
|
const speech = part.speech;
|
|
2441
2380
|
if (speech?.phoneme_script === null) delete speech.phoneme_script;
|
|
2442
|
-
|
|
2381
|
+
const speed = part.video_clip?.speed_shift;
|
|
2382
|
+
if (speed !== void 0) delete speed.mode;
|
|
2443
2383
|
}
|
|
2444
2384
|
return { view };
|
|
2445
2385
|
};
|
|
@@ -2455,22 +2395,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2455
2395
|
const sequentialOrders = [];
|
|
2456
2396
|
for (const item of oldTrack.items ?? []) {
|
|
2457
2397
|
const clip = rows.entities.find((row) => row.entityId === item.part_id && row.entityKind === "clip");
|
|
2458
|
-
const binding = rows.relations.filter((row) => row.relationKind === "
|
|
2398
|
+
const binding = rows.relations.filter((row) => row.relationKind === "marker" && row.endpoint0EntityId === item.part_id && rows.entities.some((entity) => entity.entityId === row.endpoint1EntityId && entity.entityKind === "sequence-marker"));
|
|
2459
2399
|
const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
|
|
2460
2400
|
if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
|
|
2461
2401
|
const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
2462
2402
|
const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2463
2403
|
if (normalizedMainRange !== void 0) {
|
|
2464
|
-
if (
|
|
2404
|
+
if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
|
|
2465
2405
|
} else if (oldTrack.parts_kind === "bgm") {
|
|
2466
|
-
if (clip.payload.order !== 0 || marker.payload.
|
|
2406
|
+
if (clip.payload.order !== 0 || marker.payload.startMs !== void 0 || marker.payload.fillTimeline !== true || anchors.length !== 0) fail("Migration must preserve timeline-filling BGM semantics");
|
|
2467
2407
|
} else if (item.time_position.mode === "sequential") {
|
|
2468
|
-
if (marker.payload.
|
|
2408
|
+
if (marker.payload.startMs !== void 0 || typeof clip.payload.order !== "number" || !Number.isFinite(clip.payload.order) || anchors.length !== 0) fail("Migration must preserve sequential placement semantics");
|
|
2469
2409
|
sequentialOrders.push(clip.payload.order);
|
|
2470
2410
|
} else if (item.time_position.mode === "absolute") {
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
} else if (clip.payload.order !== void 0 || marker.payload.targetRange !== void 0 || marker.payload.anchorOffset !== item.time_position.offsetMs || anchors.length !== 1 || anchors[0].endpoint1EntityId !== item.time_position.anchorPartId) fail("Migration must preserve anchored placement semantics");
|
|
2411
|
+
if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 0) fail("Migration must preserve absolute placement semantics");
|
|
2412
|
+
} else if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 1 || anchors[0].endpoint1EntityId !== item.time_position.anchorPartId) fail("Migration must preserve anchored placement semantics");
|
|
2474
2413
|
}
|
|
2475
2414
|
if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
|
|
2476
2415
|
}
|
|
@@ -2711,7 +2650,7 @@ function requirePart(document, partId) {
|
|
|
2711
2650
|
return part;
|
|
2712
2651
|
}
|
|
2713
2652
|
function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2714
|
-
const renders = relations.filter((relation) => relation.relationKind === "
|
|
2653
|
+
const renders = relations.filter((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceoverAudioEntityId);
|
|
2715
2654
|
if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
|
|
2716
2655
|
if (renders.length === 1) {
|
|
2717
2656
|
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
@@ -2739,7 +2678,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
|
|
|
2739
2678
|
baseEntityIds: [script.entityId],
|
|
2740
2679
|
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2741
2680
|
});
|
|
2742
|
-
link("
|
|
2681
|
+
link("derived-from", voiceoverAudioEntityId, phonetic);
|
|
2743
2682
|
return phonetic;
|
|
2744
2683
|
}
|
|
2745
2684
|
function composedAudioScript(phonetic, entities) {
|
|
@@ -2801,12 +2740,6 @@ function assertCaptionStyle(style) {
|
|
|
2801
2740
|
"position_y"
|
|
2802
2741
|
], "Caption style");
|
|
2803
2742
|
}
|
|
2804
|
-
function isRangeStartingAt(value, expectedStart) {
|
|
2805
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expectedStart;
|
|
2806
|
-
}
|
|
2807
|
-
function isExactRange(value, expected) {
|
|
2808
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expected.start && value.end === expected.end;
|
|
2809
|
-
}
|
|
2810
2743
|
function requireFinite(value, label) {
|
|
2811
2744
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
|
|
2812
2745
|
return value;
|
|
@@ -5314,4 +5247,4 @@ function serverCovers(serverVV, target) {
|
|
|
5314
5247
|
return covers(server, target);
|
|
5315
5248
|
}
|
|
5316
5249
|
//#endregion
|
|
5317
|
-
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, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|
|
5250
|
+
export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityGraphRevisionConflictError, 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, publishEntityRows, readDocumentAudioScript, readEntityDocument, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|