@mengine/medeo-client 2.0.1-alpha.22 → 2.0.1-alpha.24
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-BB8U25jt.js → document-Ct11SMBS.js} +290 -336
- package/dist/{index-C0uUWqkz.d.ts → index-CcIoMxs1.d.ts} +12 -17
- package/dist/index.d.ts +35 -27
- package/dist/index.js +218 -377
- 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 assertValidVideoDocument, A as decodeEntityRelationRows, B as updateEntityFields, C as applyFieldChanges, Ct as speedOf, D as EntityProjectionGraph, Dt as base64ToBytes, E as projectEntityTimeline, Et as videoDocumentMirrorSchema, F as assembleEntityContent, G as hasSequence, H as isJsonObject, I as assemblePhoneticScriptContent, J as buildSpeechHostMap, K as isMediaAssetVariantKind, L as assembleScriptText, M as relationsTo, N as ScriptCompositionError, O as resolveEntityTimelineLayout, Ot as bytesToBase64, P as assembleCaptionContent, Q as VideoDocumentValidationError, R as findComposedAudioScript, S as equalJson, St as effectiveVideoClipDurationMs, T as resolveFieldPath, Tt as recordEntries, U as createEntityId, V as variantBaseEntityIds, W as createRelationId, X as fromVideoDocument, Y as derivePositionFromAbs, Z as toVideoDocument, _ as audioScriptAssetOf, _t as TIMELINE_SKELETON_DURATION_MS, a as createMirrorVideoDocument, at as findLaneTrack, b as phoneticScriptAssetOf, bt as safeDurationMs, c as DocumentMutationGuard, ct as solveVideoDocument, d as LoroEntityDraft, dt as fillMainTrackTimeGaps, et as validateVideoDocument, f as readEntityDocument, ft as reassignSpeechesToVideoClipsByTime, g as audioScriptAssetHash, gt as syncAggregatedClipsTimePosition, h as audioScriptAssetFields, ht as resolveSpeechOverlapByShiftingVideos, i as MirrorVideoDocumentAdapter, it as ensureLaneTrack, j as relationsFrom, k as EntityTimelineProjectionError, l as LORO_ENTITY_SCHEMA, lt as cascadeAfterVideoClipChanges, m as audioScriptAssetContent, mt as resolveAllSpeechOverlaps, n as createPlainMemoryAdapter, nt as videoDocumentSchema, o as createMirrorVideoDocumentAdapter, ot as hasResolvedPlacement, p as entityOrderGroups, pt as recalculateTimelineDuration, q as buildInitialVideoDocument, r as generatePartId, rt as LANE_KINDS_IN_STACK_ORDER, s as readVideoDocumentFromDraft, st as laneTrackId, t as PlainMemoryAdapter, tt as partUnionSchema, u as LoroEntityDocument, ut as arrangeMainTrackSeamlessly, v as phoneticScriptAssetContent, vt as isEmptyVideoClip, w as assertFieldChanges, wt as partUnionToDraft, x as scriptSourceAssetOf, xt as DEFAULT_UNIT_TIME_MS, y as phoneticScriptAssetHash, yt as partDurationMs, z as selectAudioScriptSegment } from "./document-Ct11SMBS.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 { ClientServerSynchronizer, DocManager } from "@mengine/sync";
|
|
@@ -402,14 +402,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
402
402
|
};
|
|
403
403
|
entities.push(track);
|
|
404
404
|
}
|
|
405
|
-
const bindings = relations.filter((row) => row.relationKind === "
|
|
405
|
+
const bindings = relations.filter((row) => row.relationKind === "contain" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
|
|
406
406
|
if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
|
|
407
407
|
if (bindings.length === 0) {
|
|
408
408
|
const relationId = createRelationId(idFactory("relation"));
|
|
409
409
|
if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
|
|
410
410
|
relations.push({
|
|
411
411
|
relationId,
|
|
412
|
-
relationKind: "
|
|
412
|
+
relationKind: "contain",
|
|
413
413
|
endpoint0EntityId: timeline.entityId,
|
|
414
414
|
endpoint1EntityId: track.entityId,
|
|
415
415
|
metadata: {},
|
|
@@ -432,10 +432,6 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
432
432
|
//#endregion
|
|
433
433
|
//#region src/entity-editor/entity-graph-http-client.ts
|
|
434
434
|
const MENGINE_API_PREFIX = "/api/mengine/v1";
|
|
435
|
-
const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
436
|
-
if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
|
|
437
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
438
|
-
} };
|
|
439
435
|
/** Browser-safe authenticated Loro client for a document's Entity graph. */
|
|
440
436
|
var EntityGraphHttpClient = class {
|
|
441
437
|
options;
|
|
@@ -485,7 +481,7 @@ function parseWireState(value, expectedDocId) {
|
|
|
485
481
|
if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
|
|
486
482
|
if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
487
483
|
const rows = parseWireRows(value.rows);
|
|
488
|
-
decodeEntityRelationRows(rows,
|
|
484
|
+
decodeEntityRelationRows(rows, {});
|
|
489
485
|
const attached = value.audio_script_entity_id;
|
|
490
486
|
if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
|
|
491
487
|
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
@@ -526,7 +522,7 @@ function parseWireRelation(value) {
|
|
|
526
522
|
}
|
|
527
523
|
function cloneAndValidateRows(rows) {
|
|
528
524
|
const clone = structuredClone(rows);
|
|
529
|
-
decodeEntityRelationRows(clone,
|
|
525
|
+
decodeEntityRelationRows(clone, {});
|
|
530
526
|
return clone;
|
|
531
527
|
}
|
|
532
528
|
function requiredString(value, field) {
|
|
@@ -550,10 +546,6 @@ async function safeReadJson(response) {
|
|
|
550
546
|
}
|
|
551
547
|
//#endregion
|
|
552
548
|
//#region src/entity-editor/media-asset-import.ts
|
|
553
|
-
const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
554
|
-
if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
|
|
555
|
-
return left - right;
|
|
556
|
-
} };
|
|
557
549
|
/**
|
|
558
550
|
* Import at the editor boundary, not at asset generation time. Existing
|
|
559
551
|
* provenance is retained.
|
|
@@ -569,17 +561,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
569
561
|
"image",
|
|
570
562
|
"video",
|
|
571
563
|
"audio",
|
|
572
|
-
"voiceover"
|
|
564
|
+
"voiceover-recording"
|
|
573
565
|
].includes(fact.kind)) throw new Error("Unsupported media kind");
|
|
574
566
|
if (fact.kind !== "image" && (!Number.isFinite(fact.durationMs) || fact.durationMs <= 0)) throw new Error(`${fact.kind} ${fact.assetId} requires its factual positive duration`);
|
|
575
|
-
if ((fact.kind === "audio" || fact.kind === "voiceover") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
576
|
-
decodeEntityRelationRows(initial,
|
|
567
|
+
if ((fact.kind === "audio" || fact.kind === "voiceover-recording") && fact.storageKey.trim() === "") throw new Error(`${fact.kind} ${fact.assetId} requires its physical storage key`);
|
|
568
|
+
decodeEntityRelationRows(initial, {});
|
|
577
569
|
assertCanonicalEditorResources(initial);
|
|
578
570
|
const entities = structuredClone([...initial.entities]);
|
|
579
571
|
const relations = structuredClone([...initial.relations]);
|
|
580
|
-
const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
|
|
572
|
+
const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
|
|
581
573
|
const entityKind = fact.kind;
|
|
582
|
-
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
574
|
+
const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
|
|
583
575
|
const durationMs = fact.kind === "image" ? null : fact.durationMs;
|
|
584
576
|
const asset = ensureAsset(entities, {
|
|
585
577
|
system,
|
|
@@ -610,12 +602,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
610
602
|
payload: { durationMs }
|
|
611
603
|
});
|
|
612
604
|
relations.push(assetLink(entityId, asset.entityId, idFactory));
|
|
613
|
-
if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
|
|
605
|
+
if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
|
|
614
606
|
const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
|
|
615
607
|
if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
|
|
616
608
|
relations.push({
|
|
617
609
|
relationId: createRelationId(idFactory("relation")),
|
|
618
|
-
relationKind: "
|
|
610
|
+
relationKind: "derived-from",
|
|
619
611
|
endpoint0EntityId: entityId,
|
|
620
612
|
endpoint1EntityId: phonetic.entityId,
|
|
621
613
|
metadata: {},
|
|
@@ -627,7 +619,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
627
619
|
entities,
|
|
628
620
|
relations
|
|
629
621
|
};
|
|
630
|
-
decodeEntityRelationRows(rows,
|
|
622
|
+
decodeEntityRelationRows(rows, {});
|
|
631
623
|
assertCanonicalEditorResources(rows);
|
|
632
624
|
return {
|
|
633
625
|
rows,
|
|
@@ -770,19 +762,19 @@ var EntityTimelineEditor = class {
|
|
|
770
762
|
return cloneRows(this.state);
|
|
771
763
|
}
|
|
772
764
|
insertClip(input) {
|
|
773
|
-
const requestedOrder = input.
|
|
765
|
+
const requestedOrder = input.order;
|
|
774
766
|
const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
|
|
775
767
|
return this.insertPlacedClip({
|
|
776
768
|
trackEntityId: input.trackEntityId,
|
|
777
769
|
contentEntityId: input.contentEntityId,
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
placement: input.
|
|
770
|
+
inMs: input.inMs,
|
|
771
|
+
outMs: input.outMs,
|
|
772
|
+
placement: input.startMs === void 0 ? {
|
|
781
773
|
kind: "sequential",
|
|
782
774
|
order
|
|
783
775
|
} : {
|
|
784
776
|
kind: "absolute",
|
|
785
|
-
|
|
777
|
+
startMs: input.startMs
|
|
786
778
|
},
|
|
787
779
|
clipPayload: input.clipPayload
|
|
788
780
|
});
|
|
@@ -794,31 +786,14 @@ var EntityTimelineEditor = class {
|
|
|
794
786
|
this.transact((draft) => {
|
|
795
787
|
const { marker } = requireClipTopology(draft, input.clipEntityId);
|
|
796
788
|
const payload = { ...cloneJsonObject(marker.payload, "Sequence Marker payload") };
|
|
797
|
-
if (input.
|
|
798
|
-
if (input.
|
|
799
|
-
if (Object.hasOwn(input, "
|
|
800
|
-
delete payload.
|
|
801
|
-
if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
replaceEntity(draft, createEntityId(input.clipEntityId), {
|
|
806
|
-
...requireEntityKind(draft, input.clipEntityId, "clip"),
|
|
807
|
-
payload: clipPayload
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
} else {
|
|
811
|
-
payload.targetRange = cloneJsonValue(input.targetRange, "targetRange");
|
|
812
|
-
const clip = requireEntityKind(draft, input.clipEntityId, "clip");
|
|
813
|
-
const clipPayload = { ...cloneJsonObject(clip.payload, "Clip payload") };
|
|
814
|
-
delete clipPayload.order;
|
|
815
|
-
replaceEntity(draft, clip.entityId, {
|
|
816
|
-
...clip,
|
|
817
|
-
payload: clipPayload
|
|
818
|
-
});
|
|
819
|
-
}
|
|
820
|
-
if (Object.hasOwn(input, "timeRemapping")) if (input.timeRemapping === void 0) delete payload.timeRemapping;
|
|
821
|
-
else payload.timeRemapping = cloneJsonValue(input.timeRemapping, "timeRemapping");
|
|
789
|
+
if (input.inMs !== void 0) payload.inMs = finiteNonNegative(input.inMs, "inMs");
|
|
790
|
+
if (input.outMs !== void 0) payload.outMs = finitePositive(input.outMs, "outMs");
|
|
791
|
+
if (Object.hasOwn(input, "startMs")) if (input.startMs === void 0) {
|
|
792
|
+
delete payload.startMs;
|
|
793
|
+
if (!hasOutgoingClipAnchor(draft, input.clipEntityId)) {}
|
|
794
|
+
} else payload.startMs = finiteNonNegative(input.startMs, "startMs");
|
|
795
|
+
if (Object.hasOwn(input, "rate")) if (input.rate === void 0) delete payload.rate;
|
|
796
|
+
else payload.rate = finitePositive(input.rate, "linear speed rate");
|
|
822
797
|
replaceEntity(draft, marker.entityId, {
|
|
823
798
|
...marker,
|
|
824
799
|
payload
|
|
@@ -929,28 +904,18 @@ var EntityTimelineEditor = class {
|
|
|
929
904
|
const content = requireSequenceContent(draft, input.contentEntityId);
|
|
930
905
|
const payload = {
|
|
931
906
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
932
|
-
|
|
933
|
-
duration: cloneJsonValue(input.duration, "duration")
|
|
907
|
+
...requireWindow(input.inMs, input.outMs)
|
|
934
908
|
};
|
|
935
|
-
delete payload.
|
|
936
|
-
delete payload.
|
|
937
|
-
if (input.
|
|
938
|
-
if (input.
|
|
909
|
+
delete payload.startMs;
|
|
910
|
+
delete payload.rate;
|
|
911
|
+
if (input.startMs !== void 0) payload.startMs = finiteNonNegative(input.startMs, "startMs");
|
|
912
|
+
if (input.rate !== void 0) payload.rate = finitePositive(input.rate, "linear speed rate");
|
|
939
913
|
replaceEntity(draft, topology.marker.entityId, {
|
|
940
914
|
...topology.marker,
|
|
941
915
|
payload
|
|
942
916
|
});
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
if (input.targetRange === void 0) clipPayload.order ??= nextSequentialOrder(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId));
|
|
946
|
-
else delete clipPayload.order;
|
|
947
|
-
replaceEntity(draft, topology.clip.entityId, {
|
|
948
|
-
...topology.clip,
|
|
949
|
-
payload: clipPayload
|
|
950
|
-
});
|
|
951
|
-
}
|
|
952
|
-
this.relinkRelation(draft, topology.markerContentRelation, {
|
|
953
|
-
endpoint0EntityId: topology.marker.entityId,
|
|
917
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
918
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
954
919
|
endpoint1EntityId: content.entityId
|
|
955
920
|
});
|
|
956
921
|
});
|
|
@@ -961,15 +926,11 @@ var EntityTimelineEditor = class {
|
|
|
961
926
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
962
927
|
replaceRows(draft, imported.rows);
|
|
963
928
|
const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
|
|
964
|
-
const
|
|
929
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
965
930
|
const clipId = this.insertPlacedClipIntoDraft(draft, {
|
|
966
931
|
trackEntityId: track.entityId,
|
|
967
932
|
contentEntityId: imported.contentEntityId,
|
|
968
|
-
|
|
969
|
-
duration: {
|
|
970
|
-
mode: "fixed",
|
|
971
|
-
value: sourceRange.end - sourceRange.start
|
|
972
|
-
},
|
|
933
|
+
...window,
|
|
973
934
|
placement: input.placement,
|
|
974
935
|
...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
|
|
975
936
|
...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
|
|
@@ -1004,15 +965,11 @@ var EntityTimelineEditor = class {
|
|
|
1004
965
|
for (const [offset, clip] of input.clips.entries()) {
|
|
1005
966
|
const imported = importMediaAsset(draft, clip.media, this.idFactory);
|
|
1006
967
|
replaceRows(draft, imported.rows);
|
|
1007
|
-
const
|
|
968
|
+
const window = requireWindow(clip.inMs, clip.outMs);
|
|
1008
969
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1009
970
|
trackEntityId: track.entityId,
|
|
1010
971
|
contentEntityId: imported.contentEntityId,
|
|
1011
|
-
|
|
1012
|
-
duration: {
|
|
1013
|
-
mode: "fixed",
|
|
1014
|
-
value: sourceRange.end - sourceRange.start
|
|
1015
|
-
},
|
|
972
|
+
...window,
|
|
1016
973
|
placement: {
|
|
1017
974
|
kind: "sequential",
|
|
1018
975
|
order: ordered.length + offset
|
|
@@ -1034,23 +991,18 @@ var EntityTimelineEditor = class {
|
|
|
1034
991
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1035
992
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1036
993
|
replaceRows(draft, imported.rows);
|
|
1037
|
-
const
|
|
994
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1038
995
|
const markerPayloadValue = {
|
|
1039
996
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1040
|
-
|
|
1041
|
-
duration: {
|
|
1042
|
-
mode: "fixed",
|
|
1043
|
-
value: sourceRange.end - sourceRange.start
|
|
1044
|
-
}
|
|
997
|
+
...window
|
|
1045
998
|
};
|
|
1046
|
-
delete markerPayloadValue.
|
|
1047
|
-
resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
|
|
999
|
+
delete markerPayloadValue.rate;
|
|
1048
1000
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1049
1001
|
...topology.marker,
|
|
1050
1002
|
payload: markerPayloadValue
|
|
1051
1003
|
});
|
|
1052
|
-
this.relinkRelation(draft, topology.
|
|
1053
|
-
endpoint0EntityId: topology.
|
|
1004
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1005
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1054
1006
|
endpoint1EntityId: createEntityId(imported.contentEntityId)
|
|
1055
1007
|
});
|
|
1056
1008
|
this.resolveOneVoiceoverOverlap(draft);
|
|
@@ -1072,42 +1024,26 @@ var EntityTimelineEditor = class {
|
|
|
1072
1024
|
setClipSpeed(input) {
|
|
1073
1025
|
this.transact((draft) => {
|
|
1074
1026
|
const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1075
|
-
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.
|
|
1027
|
+
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
|
|
1076
1028
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
|
|
1077
1029
|
const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
|
|
1078
|
-
const
|
|
1079
|
-
if (input.
|
|
1030
|
+
const priorWindow = markerWindow(initialPayload);
|
|
1031
|
+
if (input.rate === null && initialPayload.rate === void 0) return;
|
|
1080
1032
|
this.normalizeMainSequential(draft);
|
|
1081
1033
|
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1082
1034
|
const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (input.
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
mode: "fixed",
|
|
1093
|
-
value: sourceRange.end - sourceRange.start
|
|
1094
|
-
};
|
|
1095
|
-
} else {
|
|
1096
|
-
const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
|
|
1097
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1035
|
+
const window = content.entityKind === "image" && initialPayload.rate === void 0 && input.rate !== null ? {
|
|
1036
|
+
inMs: 0,
|
|
1037
|
+
outMs: markerDuration(initialPayload, priorWindow)
|
|
1038
|
+
} : priorWindow;
|
|
1039
|
+
Object.assign(payload, window);
|
|
1040
|
+
if (input.rate === null) delete payload.rate;
|
|
1041
|
+
else {
|
|
1042
|
+
const rate = finitePositive(input.rate, "linear speed rate");
|
|
1043
|
+
const duration = Math.round((window.outMs - window.inMs) / rate);
|
|
1098
1044
|
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
|
|
1099
|
-
payload.
|
|
1100
|
-
payload.duration = {
|
|
1101
|
-
mode: "fixed",
|
|
1102
|
-
value: duration
|
|
1103
|
-
};
|
|
1104
|
-
payload.timeRemapping = {
|
|
1105
|
-
kind: "linear",
|
|
1106
|
-
rate,
|
|
1107
|
-
...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
|
|
1108
|
-
};
|
|
1045
|
+
payload.rate = rate;
|
|
1109
1046
|
}
|
|
1110
|
-
resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
|
|
1111
1047
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1112
1048
|
...topology.marker,
|
|
1113
1049
|
payload
|
|
@@ -1119,21 +1055,15 @@ var EntityTimelineEditor = class {
|
|
|
1119
1055
|
this.transact((draft) => {
|
|
1120
1056
|
this.normalizeMainSequential(draft);
|
|
1121
1057
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1122
|
-
const content = requireSequenceContent(draft, otherEndpoint(topology.
|
|
1058
|
+
const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
|
|
1123
1059
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip trimming is supported only for Video or Image content");
|
|
1124
|
-
const
|
|
1060
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1125
1061
|
const payload = {
|
|
1126
1062
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1127
|
-
|
|
1063
|
+
...window
|
|
1128
1064
|
};
|
|
1129
|
-
const
|
|
1130
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1065
|
+
const duration = markerDuration(payload, window);
|
|
1131
1066
|
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Trim produces an invalid duration");
|
|
1132
|
-
payload.duration = {
|
|
1133
|
-
mode: "fixed",
|
|
1134
|
-
value: duration
|
|
1135
|
-
};
|
|
1136
|
-
resizeAbsoluteTarget(payload, duration);
|
|
1137
1067
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1138
1068
|
...topology.marker,
|
|
1139
1069
|
payload
|
|
@@ -1163,15 +1093,11 @@ var EntityTimelineEditor = class {
|
|
|
1163
1093
|
if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
|
|
1164
1094
|
const imported = importMediaAsset(draft, replacement.media, this.idFactory);
|
|
1165
1095
|
replaceRows(draft, imported.rows);
|
|
1166
|
-
const
|
|
1096
|
+
const window = requireWindow(replacement.inMs, replacement.outMs);
|
|
1167
1097
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1168
1098
|
trackEntityId: track.entityId,
|
|
1169
1099
|
contentEntityId: imported.contentEntityId,
|
|
1170
|
-
|
|
1171
|
-
duration: {
|
|
1172
|
-
mode: "fixed",
|
|
1173
|
-
value: sourceRange.end - sourceRange.start
|
|
1174
|
-
},
|
|
1100
|
+
...window,
|
|
1175
1101
|
placement: {
|
|
1176
1102
|
kind: "sequential",
|
|
1177
1103
|
order: temporaryOrder + offset
|
|
@@ -1205,10 +1131,14 @@ var EntityTimelineEditor = class {
|
|
|
1205
1131
|
deleteClip(input) {
|
|
1206
1132
|
this.transact((draft) => {
|
|
1207
1133
|
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1208
|
-
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["
|
|
1209
|
-
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["
|
|
1210
|
-
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "
|
|
1211
|
-
const removedRelationIds = new Set([
|
|
1134
|
+
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set(["contain", "marker"]));
|
|
1135
|
+
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
|
|
1136
|
+
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
|
|
1137
|
+
const removedRelationIds = new Set([
|
|
1138
|
+
topology.trackRelation.relationId,
|
|
1139
|
+
topology.clipMarkerRelation.relationId,
|
|
1140
|
+
topology.contentRelation.relationId
|
|
1141
|
+
]);
|
|
1212
1142
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
|
|
1213
1143
|
draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
|
|
1214
1144
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
|
|
@@ -1229,28 +1159,20 @@ var EntityTimelineEditor = class {
|
|
|
1229
1159
|
if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
|
|
1230
1160
|
this.applyClipPlacement(draft, childId, {
|
|
1231
1161
|
kind: "absolute",
|
|
1232
|
-
|
|
1233
|
-
start: placement.start,
|
|
1234
|
-
end: placement.start + placement.duration
|
|
1235
|
-
}
|
|
1162
|
+
startMs: placement.start
|
|
1236
1163
|
});
|
|
1237
1164
|
}
|
|
1238
1165
|
else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
|
|
1239
1166
|
for (const target of targets) this.removeClipGraph(draft, target);
|
|
1240
|
-
normalizeSequentialOrders(draft);
|
|
1241
1167
|
if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
|
|
1242
1168
|
});
|
|
1243
1169
|
}
|
|
1244
1170
|
updateClip(input) {
|
|
1245
1171
|
this.transact((draft) => {
|
|
1246
1172
|
const clip = requireEntityKind(draft, input.clipEntityId, "clip");
|
|
1247
|
-
const priorOrder = clip.payload.order;
|
|
1248
1173
|
replaceEntity(draft, clip.entityId, {
|
|
1249
1174
|
...clip,
|
|
1250
|
-
payload:
|
|
1251
|
-
...cloneJsonObject(input.payload, "Clip payload"),
|
|
1252
|
-
...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
|
|
1253
|
-
}
|
|
1175
|
+
payload: cloneJsonObject(input.payload, "Clip payload")
|
|
1254
1176
|
});
|
|
1255
1177
|
});
|
|
1256
1178
|
}
|
|
@@ -1260,13 +1182,13 @@ var EntityTimelineEditor = class {
|
|
|
1260
1182
|
const placement = input.placement ?? {
|
|
1261
1183
|
kind: "anchored",
|
|
1262
1184
|
hostClipEntityId: input.hostClipEntityId,
|
|
1263
|
-
|
|
1185
|
+
startMs: input.startMs
|
|
1264
1186
|
};
|
|
1265
|
-
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.
|
|
1187
|
+
if (input.placement !== void 0 && (input.hostClipEntityId !== void 0 || input.startMs !== void 0)) throw new Error("Specify voiceover placement or hostClipEntityId/startMs, not both");
|
|
1266
1188
|
requireVolume(input.volume);
|
|
1267
1189
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1268
1190
|
replaceRows(draft, imported.rows);
|
|
1269
|
-
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
|
|
1191
|
+
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
|
|
1270
1192
|
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1271
1193
|
this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
|
|
1272
1194
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
@@ -1276,14 +1198,8 @@ var EntityTimelineEditor = class {
|
|
|
1276
1198
|
clipEntityId: input.voiceoverClipEntityId,
|
|
1277
1199
|
trackEntityId: speechTrack.entityId,
|
|
1278
1200
|
contentEntityId: voiceover.entityId,
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
end: input.media.durationMs
|
|
1282
|
-
},
|
|
1283
|
-
duration: {
|
|
1284
|
-
mode: "fixed",
|
|
1285
|
-
value: input.media.durationMs
|
|
1286
|
-
},
|
|
1201
|
+
inMs: 0,
|
|
1202
|
+
outMs: input.media.durationMs,
|
|
1287
1203
|
placement,
|
|
1288
1204
|
clipPayload: { volume: input.volume }
|
|
1289
1205
|
});
|
|
@@ -1297,8 +1213,8 @@ var EntityTimelineEditor = class {
|
|
|
1297
1213
|
volume: input.volume
|
|
1298
1214
|
}
|
|
1299
1215
|
});
|
|
1300
|
-
this.relinkRelation(draft, topology.
|
|
1301
|
-
endpoint0EntityId: topology.
|
|
1216
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1217
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1302
1218
|
endpoint1EntityId: voiceover.entityId
|
|
1303
1219
|
});
|
|
1304
1220
|
this.applyClipPlacement(draft, topology.clip.entityId, placement);
|
|
@@ -1307,14 +1223,8 @@ var EntityTimelineEditor = class {
|
|
|
1307
1223
|
...placed.marker,
|
|
1308
1224
|
payload: {
|
|
1309
1225
|
...placed.marker.payload,
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
end: input.media.durationMs
|
|
1313
|
-
},
|
|
1314
|
-
duration: {
|
|
1315
|
-
mode: "fixed",
|
|
1316
|
-
value: input.media.durationMs
|
|
1317
|
-
}
|
|
1226
|
+
inMs: 0,
|
|
1227
|
+
outMs: input.media.durationMs
|
|
1318
1228
|
}
|
|
1319
1229
|
});
|
|
1320
1230
|
}
|
|
@@ -1363,11 +1273,8 @@ var EntityTimelineEditor = class {
|
|
|
1363
1273
|
clipEntityId: input.bgmClipEntityId,
|
|
1364
1274
|
trackEntityId: track.entityId,
|
|
1365
1275
|
contentEntityId: imported.contentEntityId,
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
end: input.media.durationMs
|
|
1369
|
-
},
|
|
1370
|
-
duration: { mode: "from-source" },
|
|
1276
|
+
inMs: 0,
|
|
1277
|
+
outMs: input.media.durationMs,
|
|
1371
1278
|
placement: {
|
|
1372
1279
|
kind: "sequential",
|
|
1373
1280
|
order: 0
|
|
@@ -1379,7 +1286,7 @@ var EntityTimelineEditor = class {
|
|
|
1379
1286
|
...topology.marker,
|
|
1380
1287
|
payload: {
|
|
1381
1288
|
...topology.marker.payload,
|
|
1382
|
-
|
|
1289
|
+
fillTimeline: true
|
|
1383
1290
|
}
|
|
1384
1291
|
});
|
|
1385
1292
|
return clipId;
|
|
@@ -1412,7 +1319,7 @@ var EntityTimelineEditor = class {
|
|
|
1412
1319
|
const assembled = new EntityProjectionGraph(draft);
|
|
1413
1320
|
for (const clip of clipsOnTrack(draft, track.entityId)) {
|
|
1414
1321
|
const topology = requireClipTopology(draft, clip.entityId);
|
|
1415
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1322
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1416
1323
|
const prior = assembled.entity(caption.entityId).payload.style;
|
|
1417
1324
|
const style = isJsonObject(prior) ? prior : {};
|
|
1418
1325
|
for (const updated of updateEntityFields(draft, caption.entityId, { style: {
|
|
@@ -1449,14 +1356,8 @@ var EntityTimelineEditor = class {
|
|
|
1449
1356
|
return this.insertPlacedClipIntoDraft(draft, {
|
|
1450
1357
|
trackEntityId: track.entityId,
|
|
1451
1358
|
contentEntityId: content.entityId,
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
end: durationMs
|
|
1455
|
-
},
|
|
1456
|
-
duration: {
|
|
1457
|
-
mode: "fixed",
|
|
1458
|
-
value: durationMs
|
|
1459
|
-
},
|
|
1359
|
+
inMs: 0,
|
|
1360
|
+
outMs: durationMs,
|
|
1460
1361
|
placement: input.placement,
|
|
1461
1362
|
...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
|
|
1462
1363
|
});
|
|
@@ -1469,8 +1370,7 @@ var EntityTimelineEditor = class {
|
|
|
1469
1370
|
const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
|
|
1470
1371
|
const markerId = this.mintEntityId(draft);
|
|
1471
1372
|
const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
|
|
1472
|
-
|
|
1473
|
-
const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
|
|
1373
|
+
const markerPlacement = applyPlacementPayload(input.placement);
|
|
1474
1374
|
const clip = {
|
|
1475
1375
|
entityId: clipId,
|
|
1476
1376
|
entityKind: "clip",
|
|
@@ -1480,13 +1380,12 @@ var EntityTimelineEditor = class {
|
|
|
1480
1380
|
entityId: markerId,
|
|
1481
1381
|
entityKind: "sequence-marker",
|
|
1482
1382
|
payload: markerPayload({
|
|
1483
|
-
|
|
1484
|
-
duration: input.duration,
|
|
1383
|
+
...requireWindow(input.inMs, input.outMs),
|
|
1485
1384
|
...markerPlacement
|
|
1486
1385
|
})
|
|
1487
1386
|
};
|
|
1488
1387
|
draft.entities.push(clip, marker);
|
|
1489
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1388
|
+
draft.relations.push(this.createRelation(draft, "contain", track.entityId, clipId), this.createRelation(draft, "marker", clipId, markerId), this.createRelation(draft, "contain", clipId, content.entityId));
|
|
1490
1389
|
if (input.placement.kind === "anchored") {
|
|
1491
1390
|
const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
|
|
1492
1391
|
if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
|
|
@@ -1496,20 +1395,7 @@ var EntityTimelineEditor = class {
|
|
|
1496
1395
|
}
|
|
1497
1396
|
ensureRoleTrack(draft, timelineEntityId, role) {
|
|
1498
1397
|
const existing = findRoleTrack(draft, timelineEntityId, role);
|
|
1499
|
-
if (existing !== void 0)
|
|
1500
|
-
if (existing.payload.order === void 0) {
|
|
1501
|
-
const replacement = {
|
|
1502
|
-
...existing,
|
|
1503
|
-
payload: {
|
|
1504
|
-
...existing.payload,
|
|
1505
|
-
order: roleOrder[role]
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
replaceEntity(draft, existing.entityId, replacement);
|
|
1509
|
-
return replacement;
|
|
1510
|
-
}
|
|
1511
|
-
return existing;
|
|
1512
|
-
}
|
|
1398
|
+
if (existing !== void 0) return existing;
|
|
1513
1399
|
const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
|
|
1514
1400
|
const track = {
|
|
1515
1401
|
entityId: this.mintEntityId(draft),
|
|
@@ -1520,7 +1406,7 @@ var EntityTimelineEditor = class {
|
|
|
1520
1406
|
}
|
|
1521
1407
|
};
|
|
1522
1408
|
draft.entities.push(track);
|
|
1523
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1409
|
+
draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
|
|
1524
1410
|
return track;
|
|
1525
1411
|
}
|
|
1526
1412
|
/** The existing pronunciation variant a voiceover consumes; composition is required. */
|
|
@@ -1531,13 +1417,13 @@ var EntityTimelineEditor = class {
|
|
|
1531
1417
|
}
|
|
1532
1418
|
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1533
1419
|
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
1534
|
-
const renders = draft.relations.filter((relation) => relation.relationKind === "
|
|
1420
|
+
const renders = draft.relations.filter((relation) => relation.relationKind === "derived-from" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
|
|
1535
1421
|
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
1536
1422
|
if (renders[0] !== void 0) {
|
|
1537
1423
|
if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
|
|
1538
1424
|
throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1539
1425
|
}
|
|
1540
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1426
|
+
draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
1541
1427
|
}
|
|
1542
1428
|
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
|
|
1543
1429
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
@@ -1572,24 +1458,18 @@ var EntityTimelineEditor = class {
|
|
|
1572
1458
|
clipEntityId: caption.captionClipEntityId,
|
|
1573
1459
|
trackEntityId: captionTrack.entityId,
|
|
1574
1460
|
contentEntityId: content.entityId,
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
end: durationMs
|
|
1578
|
-
},
|
|
1579
|
-
duration: {
|
|
1580
|
-
mode: "fixed",
|
|
1581
|
-
value: durationMs
|
|
1582
|
-
},
|
|
1461
|
+
inMs: 0,
|
|
1462
|
+
outMs: durationMs,
|
|
1583
1463
|
placement: {
|
|
1584
1464
|
kind: "anchored",
|
|
1585
1465
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1586
|
-
|
|
1466
|
+
startMs
|
|
1587
1467
|
}
|
|
1588
1468
|
});
|
|
1589
1469
|
continue;
|
|
1590
1470
|
}
|
|
1591
1471
|
const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
|
|
1592
|
-
const content = requireEntityKind(draft, otherEndpoint(topology.
|
|
1472
|
+
const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1593
1473
|
const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
|
|
1594
1474
|
const style = caption.style === void 0 ? priorStyle : {
|
|
1595
1475
|
...priorStyle,
|
|
@@ -1608,17 +1488,16 @@ var EntityTimelineEditor = class {
|
|
|
1608
1488
|
this.applyClipPlacement(draft, topology.clip.entityId, {
|
|
1609
1489
|
kind: "anchored",
|
|
1610
1490
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1611
|
-
|
|
1491
|
+
startMs
|
|
1612
1492
|
});
|
|
1613
1493
|
const placed = requireClipTopology(draft, topology.clip.entityId);
|
|
1494
|
+
const window = markerWindow(placed.marker.payload);
|
|
1495
|
+
const rate = (window.outMs - window.inMs) / durationMs;
|
|
1614
1496
|
replaceEntity(draft, placed.marker.entityId, {
|
|
1615
1497
|
...placed.marker,
|
|
1616
1498
|
payload: {
|
|
1617
1499
|
...placed.marker.payload,
|
|
1618
|
-
|
|
1619
|
-
mode: "fixed",
|
|
1620
|
-
value: durationMs
|
|
1621
|
-
}
|
|
1500
|
+
...rate === 1 ? {} : { rate }
|
|
1622
1501
|
}
|
|
1623
1502
|
});
|
|
1624
1503
|
this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
|
|
@@ -1627,7 +1506,7 @@ var EntityTimelineEditor = class {
|
|
|
1627
1506
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
1628
1507
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1629
1508
|
const track = requireEntityKind(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId), "track");
|
|
1630
|
-
const timeline = requireEntityKind(draft, otherEndpoint(
|
|
1509
|
+
const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
|
|
1631
1510
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1632
1511
|
}
|
|
1633
1512
|
replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
|
|
@@ -1644,13 +1523,13 @@ var EntityTimelineEditor = class {
|
|
|
1644
1523
|
*/
|
|
1645
1524
|
removeCaptionClipGraph(draft, clipEntityId) {
|
|
1646
1525
|
const topology = requireClipOnRole(draft, clipEntityId, "caption");
|
|
1647
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1526
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1648
1527
|
const contentRelations = incidentRelations(draft, caption.entityId);
|
|
1649
|
-
const allowed = new Set(["caption-alignment", "
|
|
1528
|
+
const allowed = new Set(["caption-alignment", "contain"]);
|
|
1650
1529
|
const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
|
|
1651
1530
|
if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
|
|
1652
1531
|
this.removeClipGraph(draft, clipEntityId);
|
|
1653
|
-
if (
|
|
1532
|
+
if (containing(draft, caption.entityId, "contained").length > 0) return;
|
|
1654
1533
|
const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
|
|
1655
1534
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1656
1535
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
|
|
@@ -1698,10 +1577,7 @@ var EntityTimelineEditor = class {
|
|
|
1698
1577
|
const overlapMs = Math.round(prior.end - current.start);
|
|
1699
1578
|
for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
|
|
1700
1579
|
kind: "absolute",
|
|
1701
|
-
|
|
1702
|
-
start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
|
|
1703
|
-
end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1704
|
-
}
|
|
1580
|
+
startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1705
1581
|
});
|
|
1706
1582
|
return;
|
|
1707
1583
|
}
|
|
@@ -1710,18 +1586,16 @@ var EntityTimelineEditor = class {
|
|
|
1710
1586
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1711
1587
|
const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
|
|
1712
1588
|
const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1713
|
-
delete
|
|
1714
|
-
delete markerPayloadValue.targetRange;
|
|
1715
|
-
delete markerPayloadValue.anchorOffset;
|
|
1589
|
+
delete markerPayloadValue.startMs;
|
|
1716
1590
|
const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
|
|
1717
1591
|
const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
|
|
1718
1592
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1719
|
-
if (placement.kind === "sequential")
|
|
1720
|
-
else if (placement.kind === "absolute") markerPayloadValue.
|
|
1593
|
+
if (placement.kind === "sequential") setClipOrder(draft, topology.clip.entityId, placement.order);
|
|
1594
|
+
else if (placement.kind === "absolute") markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
|
|
1721
1595
|
else {
|
|
1722
1596
|
const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
|
|
1723
1597
|
if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
|
|
1724
|
-
markerPayloadValue.
|
|
1598
|
+
markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
|
|
1725
1599
|
draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
|
|
1726
1600
|
}
|
|
1727
1601
|
replaceEntity(draft, topology.clip.entityId, {
|
|
@@ -1736,14 +1610,10 @@ var EntityTimelineEditor = class {
|
|
|
1736
1610
|
reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
|
|
1737
1611
|
const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
|
|
1738
1612
|
if (hosts.length === 0) {
|
|
1739
|
-
|
|
1740
|
-
if (child === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
|
|
1613
|
+
if (layout.clips.get(childClipEntityId) === void 0) throw new Error(`Clip "${childClipEntityId}" is not placed`);
|
|
1741
1614
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1742
1615
|
kind: "absolute",
|
|
1743
|
-
|
|
1744
|
-
start: absoluteStart,
|
|
1745
|
-
end: absoluteStart + child.duration
|
|
1746
|
-
}
|
|
1616
|
+
startMs: absoluteStart
|
|
1747
1617
|
});
|
|
1748
1618
|
return;
|
|
1749
1619
|
}
|
|
@@ -1751,7 +1621,7 @@ var EntityTimelineEditor = class {
|
|
|
1751
1621
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1752
1622
|
kind: "anchored",
|
|
1753
1623
|
hostClipEntityId: host.clipEntityId,
|
|
1754
|
-
|
|
1624
|
+
startMs: Math.max(0, Math.round(absoluteStart - host.start))
|
|
1755
1625
|
});
|
|
1756
1626
|
}
|
|
1757
1627
|
removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
|
|
@@ -1768,12 +1638,12 @@ var EntityTimelineEditor = class {
|
|
|
1768
1638
|
removeClipGraph(draft, clipEntityId) {
|
|
1769
1639
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1770
1640
|
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
|
|
1771
|
-
"
|
|
1772
|
-
"
|
|
1641
|
+
"contain",
|
|
1642
|
+
"marker",
|
|
1773
1643
|
"clip-anchor"
|
|
1774
1644
|
]));
|
|
1775
|
-
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["
|
|
1776
|
-
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "
|
|
1645
|
+
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["marker"]));
|
|
1646
|
+
const markerIsExclusive = incidentRelations(draft, topology.marker.entityId).filter((relation) => relation.relationKind === "marker").length === 1;
|
|
1777
1647
|
const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
|
|
1778
1648
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
|
|
1779
1649
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
@@ -1819,7 +1689,7 @@ var EntityTimelineEditor = class {
|
|
|
1819
1689
|
}
|
|
1820
1690
|
};
|
|
1821
1691
|
function validateFinalRows(rows) {
|
|
1822
|
-
decodeEntityRelationRows(rows,
|
|
1692
|
+
decodeEntityRelationRows(rows, {});
|
|
1823
1693
|
assertCanonicalEditorResources(rows);
|
|
1824
1694
|
validateTimelineHierarchy(rows);
|
|
1825
1695
|
}
|
|
@@ -1827,13 +1697,13 @@ function validateTimelineHierarchy(rows) {
|
|
|
1827
1697
|
for (const entity of rows.entities) {
|
|
1828
1698
|
if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
|
|
1829
1699
|
if (entity.entityKind !== "clip") continue;
|
|
1830
|
-
const trackRelations =
|
|
1700
|
+
const trackRelations = containing(rows, entity.entityId, "contained");
|
|
1831
1701
|
if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
|
|
1832
1702
|
requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
|
|
1833
1703
|
}
|
|
1834
1704
|
}
|
|
1835
1705
|
function requireTrackTimeline(rows, trackEntityId) {
|
|
1836
|
-
const timelineRelations =
|
|
1706
|
+
const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
|
|
1837
1707
|
if (timelineRelations.filter((relation) => {
|
|
1838
1708
|
const otherId = otherEndpoint(relation, trackEntityId);
|
|
1839
1709
|
return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
|
|
@@ -1841,20 +1711,36 @@ function requireTrackTimeline(rows, trackEntityId) {
|
|
|
1841
1711
|
}
|
|
1842
1712
|
function requireClipTopology(rows, clipEntityId) {
|
|
1843
1713
|
const clip = requireEntityKind(rows, clipEntityId, "clip");
|
|
1844
|
-
const trackRelation =
|
|
1714
|
+
const trackRelation = requireOneContain(rows, clip.entityId, "contained");
|
|
1845
1715
|
requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
|
|
1846
|
-
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "
|
|
1716
|
+
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
|
|
1847
1717
|
const marker = requireEntityKind(rows, otherEndpoint(clipMarkerRelation, clip.entityId), "sequence-marker");
|
|
1848
|
-
const
|
|
1849
|
-
requireSequenceContent(rows, otherEndpoint(
|
|
1718
|
+
const contentRelation = requireOneContain(rows, clip.entityId, "container");
|
|
1719
|
+
requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
|
|
1850
1720
|
return {
|
|
1851
1721
|
clip,
|
|
1852
1722
|
marker,
|
|
1853
1723
|
trackRelation,
|
|
1854
1724
|
clipMarkerRelation,
|
|
1855
|
-
|
|
1725
|
+
contentRelation
|
|
1856
1726
|
};
|
|
1857
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* One `contain` Relation with this entity on the stated side.
|
|
1730
|
+
*
|
|
1731
|
+
* A Clip is contained by its Track and contains its content; a Track is
|
|
1732
|
+
* contained by the Timeline and contains its Clips. Both are `contain`, so the
|
|
1733
|
+
* endpoint position is the only thing that tells them apart.
|
|
1734
|
+
*/
|
|
1735
|
+
function requireOneContain(rows, entityId, side) {
|
|
1736
|
+
const matches = containing(rows, entityId, side);
|
|
1737
|
+
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must be the ${side} of exactly one contain Relation`);
|
|
1738
|
+
return matches[0];
|
|
1739
|
+
}
|
|
1740
|
+
/** `contain` is ordered, so a lookup says which side it wants. */
|
|
1741
|
+
function containing(rows, entityId, side) {
|
|
1742
|
+
return (side === "container" ? relationsFrom : relationsTo)(rows.relations, "contain", entityId);
|
|
1743
|
+
}
|
|
1858
1744
|
function requireOneRelation(rows, entityId, relationKind) {
|
|
1859
1745
|
const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
|
|
1860
1746
|
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
|
|
@@ -1893,40 +1779,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
|
|
|
1893
1779
|
function outgoingAnchorChildren(rows, hostIds) {
|
|
1894
1780
|
return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
|
|
1895
1781
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
}
|
|
1901
|
-
if (placement.kind === "absolute") return { targetRange: cloneJsonValue(placement.targetRange, "targetRange") };
|
|
1902
|
-
return { anchorOffset: finiteNonNegative(placement.anchorOffset, "anchorOffset") };
|
|
1782
|
+
/** What the placement puts on the Marker; a sequential one puts nothing there. */
|
|
1783
|
+
function applyPlacementPayload(placement) {
|
|
1784
|
+
if (placement.kind === "sequential") return {};
|
|
1785
|
+
return { startMs: finiteNonNegative(placement.startMs, "startMs") };
|
|
1903
1786
|
}
|
|
1904
1787
|
function requireAvailableEntityId(rows, value) {
|
|
1905
1788
|
const entityId = createEntityId(value);
|
|
1906
1789
|
if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
|
|
1907
1790
|
return entityId;
|
|
1908
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Put a Clip at `order` among the Clips its Track contains.
|
|
1794
|
+
*
|
|
1795
|
+
* Where a Clip sits is the position of its membership Relation, so moving one
|
|
1796
|
+
* is moving that Relation — nothing on the Clip changes, and there are no
|
|
1797
|
+
* sibling positions left to keep in step.
|
|
1798
|
+
*/
|
|
1909
1799
|
function setClipOrder(rows, clipEntityId, order) {
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
|
|
1800
|
+
const membership = relationsTo(rows.relations, "contain", clipEntityId)[0];
|
|
1801
|
+
if (membership === void 0) throw new Error(`Clip "${clipEntityId}" is contained by no Track`);
|
|
1802
|
+
const slots = [];
|
|
1803
|
+
const siblings = [];
|
|
1804
|
+
for (const [index, relation] of rows.relations.entries()) {
|
|
1805
|
+
if (relation.relationKind !== "contain" || relation.endpoint0EntityId !== membership.endpoint0EntityId) continue;
|
|
1806
|
+
slots.push(index);
|
|
1807
|
+
siblings.push(relation);
|
|
1808
|
+
}
|
|
1809
|
+
const from = siblings.indexOf(membership);
|
|
1810
|
+
const moved = siblings.filter((relation) => relation !== membership);
|
|
1811
|
+
moved.splice(Math.max(0, Math.min(order, moved.length)), 0, membership);
|
|
1812
|
+
if (from < 0) return;
|
|
1813
|
+
slots.forEach((slot, at) => rows.relations[slot] = moved[at]);
|
|
1921
1814
|
}
|
|
1922
1815
|
function findRoleTrack(rows, timelineEntityId, role) {
|
|
1923
1816
|
const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
|
|
1924
|
-
const tracks =
|
|
1817
|
+
const tracks = containing(rows, timeline.entityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, timeline.entityId), "track")).filter((track) => track.payload.role === role);
|
|
1925
1818
|
if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
|
|
1926
1819
|
return tracks[0];
|
|
1927
1820
|
}
|
|
1928
1821
|
function clipsOnTrack(rows, trackEntityId) {
|
|
1929
|
-
return
|
|
1822
|
+
return containing(rows, trackEntityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
|
|
1930
1823
|
}
|
|
1931
1824
|
function clipRole(rows, clipEntityId) {
|
|
1932
1825
|
const topology = requireClipTopology(rows, clipEntityId);
|
|
@@ -1959,35 +1852,27 @@ function requireVolume(value) {
|
|
|
1959
1852
|
if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
|
|
1960
1853
|
return volume;
|
|
1961
1854
|
}
|
|
1962
|
-
function
|
|
1963
|
-
|
|
1964
|
-
const
|
|
1965
|
-
|
|
1966
|
-
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must be a positive whole-millisecond range`);
|
|
1855
|
+
function requireWindow(inMs, outMs, label = "Sequence Marker") {
|
|
1856
|
+
const start = finiteNonNegative(inMs, `${label} inMs`);
|
|
1857
|
+
const end = finitePositive(outMs, `${label} outMs`);
|
|
1858
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || end <= start) throw new Error(`${label} must state a positive whole-millisecond window`);
|
|
1967
1859
|
return {
|
|
1968
|
-
start,
|
|
1969
|
-
end
|
|
1860
|
+
inMs: start,
|
|
1861
|
+
outMs: end
|
|
1970
1862
|
};
|
|
1971
1863
|
}
|
|
1972
|
-
function
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
function
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
function resizeAbsoluteTarget(payload, duration) {
|
|
1985
|
-
if (payload.targetRange === void 0) return;
|
|
1986
|
-
const target = requirePositiveRange(payload.targetRange, "targetRange");
|
|
1987
|
-
payload.targetRange = {
|
|
1988
|
-
start: target.start,
|
|
1989
|
-
end: target.start + duration
|
|
1990
|
-
};
|
|
1864
|
+
function markerWindow(payload) {
|
|
1865
|
+
return requireWindow(payload.inMs, payload.outMs);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* How long the Clip runs. The window and the rate are the only stored facts;
|
|
1869
|
+
* the played length follows from them rather than being kept in step by hand.
|
|
1870
|
+
*/
|
|
1871
|
+
function markerDuration(payload, window = markerWindow(payload)) {
|
|
1872
|
+
return Math.round((window.outMs - window.inMs) / markerRate(payload));
|
|
1873
|
+
}
|
|
1874
|
+
function markerRate(payload) {
|
|
1875
|
+
return payload.rate === void 0 ? 1 : finitePositive(payload.rate, "linear speed rate");
|
|
1991
1876
|
}
|
|
1992
1877
|
function replaceRows(rows, replacement) {
|
|
1993
1878
|
rows.entities = structuredClone([...replacement.entities]);
|
|
@@ -1998,9 +1883,9 @@ function finiteNonNegative(value, label) {
|
|
|
1998
1883
|
if (result < 0) throw new Error(`${label} must be non-negative`);
|
|
1999
1884
|
return result;
|
|
2000
1885
|
}
|
|
1886
|
+
/** How many Clips the Track already holds — the index a new one lands at. */
|
|
2001
1887
|
function nextSequentialOrder(rows, trackEntityId) {
|
|
2002
|
-
|
|
2003
|
-
return orders.length === 0 ? 0 : Math.max(...orders) + 1;
|
|
1888
|
+
return relationsFrom(rows.relations, "contain", trackEntityId).length;
|
|
2004
1889
|
}
|
|
2005
1890
|
function otherEndpoint(relation, entityId) {
|
|
2006
1891
|
if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
|
|
@@ -2020,19 +1905,12 @@ function cloneJsonObject(value, label) {
|
|
|
2020
1905
|
if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
|
|
2021
1906
|
return cloned;
|
|
2022
1907
|
}
|
|
2023
|
-
function cloneJsonValue(value, label) {
|
|
2024
|
-
return cloneJsonObject({ value }, label).value;
|
|
2025
|
-
}
|
|
2026
1908
|
function cloneRows(rows) {
|
|
2027
1909
|
return {
|
|
2028
1910
|
entities: rows.entities.map((entity) => structuredClone(entity)),
|
|
2029
1911
|
relations: rows.relations.map((relation) => structuredClone(relation))
|
|
2030
1912
|
};
|
|
2031
1913
|
}
|
|
2032
|
-
const numericMarkerValidation = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
2033
|
-
if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("Sequence Marker points must use finite numeric coordinates");
|
|
2034
|
-
return left - right;
|
|
2035
|
-
} };
|
|
2036
1914
|
//#endregion
|
|
2037
1915
|
//#region src/entity-editor/legacy-entity-migration.ts
|
|
2038
1916
|
/** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
|
|
@@ -2104,7 +1982,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2104
1982
|
relations
|
|
2105
1983
|
}, {
|
|
2106
1984
|
assetId: speech.origin_speech_id,
|
|
2107
|
-
kind: "voiceover",
|
|
1985
|
+
kind: "voiceover-recording",
|
|
2108
1986
|
durationMs: speech.media_duration_ms,
|
|
2109
1987
|
storageKey: speech.audio_storage_key,
|
|
2110
1988
|
voice: {
|
|
@@ -2142,7 +2020,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2142
2020
|
...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
|
|
2143
2021
|
}, oldTrack.id);
|
|
2144
2022
|
trackIds.set(oldTrack, track);
|
|
2145
|
-
link("
|
|
2023
|
+
link("contain", timeline, track);
|
|
2146
2024
|
}
|
|
2147
2025
|
for (const oldTrack of legacy.tracks ?? []) {
|
|
2148
2026
|
const track = trackIds.get(oldTrack);
|
|
@@ -2152,7 +2030,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2152
2030
|
const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
|
|
2153
2031
|
const clip = addEntity("clip", {
|
|
2154
2032
|
...volume === void 0 ? {} : { volume },
|
|
2155
|
-
|
|
2033
|
+
order: role === "bgm" ? 0 : order
|
|
2156
2034
|
}, item.part_id);
|
|
2157
2035
|
let content = contentByPartId.get(item.part_id);
|
|
2158
2036
|
let duration;
|
|
@@ -2163,37 +2041,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2163
2041
|
const fact = facts.get(video.origin_media_id);
|
|
2164
2042
|
const rate = speedOf(video.speed_shift);
|
|
2165
2043
|
markerPayload = {
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
},
|
|
2170
|
-
duration: {
|
|
2171
|
-
mode: "fixed",
|
|
2172
|
-
value: duration
|
|
2173
|
-
},
|
|
2174
|
-
...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { timeRemapping: {
|
|
2175
|
-
kind: "linear",
|
|
2176
|
-
mode: video.speed_shift?.mode ?? "constant",
|
|
2177
|
-
rate
|
|
2178
|
-
} } : {}
|
|
2044
|
+
inMs: video.play_in,
|
|
2045
|
+
outMs: video.play_out,
|
|
2046
|
+
...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
|
|
2179
2047
|
};
|
|
2180
2048
|
} else if (part.speech !== void 0) {
|
|
2181
2049
|
duration = part.speech.media_duration_ms;
|
|
2182
2050
|
markerPayload = {
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
end: duration
|
|
2186
|
-
},
|
|
2187
|
-
duration: {
|
|
2188
|
-
mode: "fixed",
|
|
2189
|
-
value: duration
|
|
2190
|
-
}
|
|
2051
|
+
inMs: 0,
|
|
2052
|
+
outMs: duration
|
|
2191
2053
|
};
|
|
2192
2054
|
} else if (part.caption !== void 0) {
|
|
2193
2055
|
const caption = part.caption;
|
|
2194
2056
|
duration = caption.initial_duration_ms;
|
|
2195
2057
|
const voiceover = contentByPartId.get(caption.speech_part_id);
|
|
2196
|
-
const render = relations.find((relation) => relation.relationKind === "
|
|
2058
|
+
const render = relations.find((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceover);
|
|
2197
2059
|
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2198
2060
|
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2199
2061
|
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
@@ -2219,14 +2081,8 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2219
2081
|
end: caption.start_ms + duration
|
|
2220
2082
|
} });
|
|
2221
2083
|
markerPayload = {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
end: duration
|
|
2225
|
-
},
|
|
2226
|
-
duration: {
|
|
2227
|
-
mode: "fixed",
|
|
2228
|
-
value: duration
|
|
2229
|
-
}
|
|
2084
|
+
inMs: 0,
|
|
2085
|
+
outMs: duration
|
|
2230
2086
|
};
|
|
2231
2087
|
} else {
|
|
2232
2088
|
const bgm = part.bgm;
|
|
@@ -2234,35 +2090,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2234
2090
|
if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
|
|
2235
2091
|
duration = fact.durationMs;
|
|
2236
2092
|
markerPayload = {
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
},
|
|
2241
|
-
duration: { mode: "from-source" },
|
|
2242
|
-
durationPolicy: "timeline"
|
|
2093
|
+
inMs: 0,
|
|
2094
|
+
outMs: duration,
|
|
2095
|
+
fillTimeline: true
|
|
2243
2096
|
};
|
|
2244
2097
|
}
|
|
2245
2098
|
if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
|
|
2246
2099
|
const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2247
2100
|
if (normalizedMainRange !== void 0) markerPayload = {
|
|
2248
2101
|
...markerPayload,
|
|
2249
|
-
|
|
2250
|
-
};
|
|
2251
|
-
else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
|
|
2252
|
-
...markerPayload,
|
|
2253
|
-
targetRange: {
|
|
2254
|
-
start: item.time_position.offsetMs,
|
|
2255
|
-
end: item.time_position.offsetMs + duration
|
|
2256
|
-
}
|
|
2102
|
+
startMs: normalizedMainRange.start
|
|
2257
2103
|
};
|
|
2258
|
-
else if (item.time_position.mode === "anchored") markerPayload = {
|
|
2104
|
+
else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
|
|
2259
2105
|
...markerPayload,
|
|
2260
|
-
|
|
2106
|
+
startMs: item.time_position.offsetMs
|
|
2261
2107
|
};
|
|
2262
2108
|
const marker = addEntity("sequence-marker", markerPayload);
|
|
2263
|
-
link("
|
|
2264
|
-
link("
|
|
2265
|
-
link("
|
|
2109
|
+
link("contain", track, clip);
|
|
2110
|
+
link("marker", clip, marker);
|
|
2111
|
+
link("contain", clip, content);
|
|
2266
2112
|
if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
|
|
2267
2113
|
}
|
|
2268
2114
|
}
|
|
@@ -2283,6 +2129,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2283
2129
|
for (const part of Object.values(view.part_library ?? {})) {
|
|
2284
2130
|
const speech = part.speech;
|
|
2285
2131
|
if (speech?.phoneme_script === null) delete speech.phoneme_script;
|
|
2132
|
+
const speed = part.video_clip?.speed_shift;
|
|
2133
|
+
if (speed !== void 0) delete speed.mode;
|
|
2286
2134
|
}
|
|
2287
2135
|
return { view };
|
|
2288
2136
|
};
|
|
@@ -2298,22 +2146,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2298
2146
|
const sequentialOrders = [];
|
|
2299
2147
|
for (const item of oldTrack.items ?? []) {
|
|
2300
2148
|
const clip = rows.entities.find((row) => row.entityId === item.part_id && row.entityKind === "clip");
|
|
2301
|
-
const binding = rows.relations.filter((row) => row.relationKind === "
|
|
2149
|
+
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"));
|
|
2302
2150
|
const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
|
|
2303
2151
|
if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
|
|
2304
2152
|
const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
2305
2153
|
const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2306
2154
|
if (normalizedMainRange !== void 0) {
|
|
2307
|
-
if (
|
|
2155
|
+
if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
|
|
2308
2156
|
} else if (oldTrack.parts_kind === "bgm") {
|
|
2309
|
-
if (clip.payload.order !== 0 || marker.payload.
|
|
2157
|
+
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");
|
|
2310
2158
|
} else if (item.time_position.mode === "sequential") {
|
|
2311
|
-
if (marker.payload.
|
|
2159
|
+
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");
|
|
2312
2160
|
sequentialOrders.push(clip.payload.order);
|
|
2313
2161
|
} else if (item.time_position.mode === "absolute") {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
} 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");
|
|
2162
|
+
if (marker.payload.startMs !== item.time_position.offsetMs || anchors.length !== 0) fail("Migration must preserve absolute placement semantics");
|
|
2163
|
+
} 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");
|
|
2317
2164
|
}
|
|
2318
2165
|
if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
|
|
2319
2166
|
}
|
|
@@ -2554,7 +2401,7 @@ function requirePart(document, partId) {
|
|
|
2554
2401
|
return part;
|
|
2555
2402
|
}
|
|
2556
2403
|
function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2557
|
-
const renders = relations.filter((relation) => relation.relationKind === "
|
|
2404
|
+
const renders = relations.filter((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceoverAudioEntityId);
|
|
2558
2405
|
if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
|
|
2559
2406
|
if (renders.length === 1) {
|
|
2560
2407
|
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
@@ -2582,7 +2429,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
|
|
|
2582
2429
|
baseEntityIds: [script.entityId],
|
|
2583
2430
|
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2584
2431
|
});
|
|
2585
|
-
link("
|
|
2432
|
+
link("derived-from", voiceoverAudioEntityId, phonetic);
|
|
2586
2433
|
return phonetic;
|
|
2587
2434
|
}
|
|
2588
2435
|
function composedAudioScript(phonetic, entities) {
|
|
@@ -2644,12 +2491,6 @@ function assertCaptionStyle(style) {
|
|
|
2644
2491
|
"position_y"
|
|
2645
2492
|
], "Caption style");
|
|
2646
2493
|
}
|
|
2647
|
-
function isRangeStartingAt(value, expectedStart) {
|
|
2648
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expectedStart;
|
|
2649
|
-
}
|
|
2650
|
-
function isExactRange(value, expected) {
|
|
2651
|
-
return value !== null && typeof value === "object" && !Array.isArray(value) && value.start === expected.start && value.end === expected.end;
|
|
2652
|
-
}
|
|
2653
2494
|
function requireFinite(value, label) {
|
|
2654
2495
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
|
|
2655
2496
|
return value;
|