@mengine/medeo-client 2.0.1-alpha.23 → 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-7Vi1AeQc.js → document-Ct11SMBS.js} +315 -482
- package/dist/{index-mGObzOh4.d.ts → index-CcIoMxs1.d.ts} +88 -376
- package/dist/index.d.ts +39 -77
- package/dist/index.js +241 -557
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
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
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
5
|
-
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
6
4
|
import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
|
|
7
5
|
import { DisposableSet, EventBus, Task } from "@mengine/utils";
|
|
8
6
|
import { BaseDocStorage, DummyConnection } from "@mengine/storage";
|
|
@@ -230,7 +228,6 @@ function applyEntityRows(editor, rows) {
|
|
|
230
228
|
const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
231
229
|
const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
|
|
232
230
|
if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
|
|
233
|
-
validateEntityRelationTransition(before, rows);
|
|
234
231
|
return {
|
|
235
232
|
update: editor.transact((draft) => {
|
|
236
233
|
for (const row of before.relations) {
|
|
@@ -238,7 +235,7 @@ function applyEntityRows(editor, rows) {
|
|
|
238
235
|
if (!next) draft.unlink(row.relationId);
|
|
239
236
|
else {
|
|
240
237
|
if (next.relationKind !== row.relationKind || next.endpoint0EntityId !== row.endpoint0EntityId || next.endpoint1EntityId !== row.endpoint1EntityId) throw new Error("Relation identity cannot change kind or endpoint positions");
|
|
241
|
-
if (!equalJson(row.trace, next.trace)) draft.
|
|
238
|
+
if (!equalJson(row.metadata, next.metadata) || !equalJson(row.trace, next.trace)) draft.patchRelation(row.relationId, next.metadata, next.trace);
|
|
242
239
|
}
|
|
243
240
|
}
|
|
244
241
|
let attached = baseline.project.audioScriptEntityId !== void 0;
|
|
@@ -255,22 +252,7 @@ function applyEntityRows(editor, rows) {
|
|
|
255
252
|
if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
|
-
for (const row of rows.relations)
|
|
259
|
-
if (oldRelations.has(row.relationId)) continue;
|
|
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
|
-
}
|
|
255
|
+
for (const row of rows.relations) if (!oldRelations.has(row.relationId)) draft.link(row);
|
|
274
256
|
const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
|
|
275
257
|
for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
|
|
276
258
|
const next = afterOrder.get(group) ?? [];
|
|
@@ -420,14 +402,14 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
420
402
|
};
|
|
421
403
|
entities.push(track);
|
|
422
404
|
}
|
|
423
|
-
const bindings = relations.filter((row) => row.relationKind === "
|
|
405
|
+
const bindings = relations.filter((row) => row.relationKind === "contain" && (row.endpoint0EntityId === track.entityId || row.endpoint1EntityId === track.entityId));
|
|
424
406
|
if (bindings.some((row) => row.endpoint0EntityId !== timeline.entityId && row.endpoint1EntityId !== timeline.entityId)) throw new EntityTimelineProjectionError(`Track ${track.entityId} belongs to another Timeline`);
|
|
425
407
|
if (bindings.length === 0) {
|
|
426
408
|
const relationId = createRelationId(idFactory("relation"));
|
|
427
409
|
if (relations.some((row) => row.relationId === relationId)) throw new Error(`Duplicate relation id ${relationId}`);
|
|
428
410
|
relations.push({
|
|
429
411
|
relationId,
|
|
430
|
-
relationKind: "
|
|
412
|
+
relationKind: "contain",
|
|
431
413
|
endpoint0EntityId: timeline.entityId,
|
|
432
414
|
endpoint1EntityId: track.entityId,
|
|
433
415
|
metadata: {},
|
|
@@ -450,10 +432,6 @@ function ensureEditorFoundation(initial, idFactory = (prefix) => `${prefix}_${gl
|
|
|
450
432
|
//#endregion
|
|
451
433
|
//#region src/entity-editor/entity-graph-http-client.ts
|
|
452
434
|
const MENGINE_API_PREFIX = "/api/mengine/v1";
|
|
453
|
-
const numericMarkerValidation$1 = { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
454
|
-
if (typeof left !== "number" || !Number.isFinite(left) || typeof right !== "number" || !Number.isFinite(right)) throw new Error("entity-state requires finite numeric Marker coordinates");
|
|
455
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
456
|
-
} };
|
|
457
435
|
/** Browser-safe authenticated Loro client for a document's Entity graph. */
|
|
458
436
|
var EntityGraphHttpClient = class {
|
|
459
437
|
options;
|
|
@@ -503,7 +481,7 @@ function parseWireState(value, expectedDocId) {
|
|
|
503
481
|
if (!isRecord(value) || typeof value.doc_id !== "string" || !isNonNegativeSafeInteger(value.revision)) throw new Error("Invalid entity-state response envelope");
|
|
504
482
|
if (value.doc_id !== expectedDocId) throw new Error(`Entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
505
483
|
const rows = parseWireRows(value.rows);
|
|
506
|
-
decodeEntityRelationRows(rows,
|
|
484
|
+
decodeEntityRelationRows(rows, {});
|
|
507
485
|
const attached = value.audio_script_entity_id;
|
|
508
486
|
if (attached !== null && typeof attached !== "string") throw new Error("Invalid document AudioScript identity");
|
|
509
487
|
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
@@ -544,7 +522,7 @@ function parseWireRelation(value) {
|
|
|
544
522
|
}
|
|
545
523
|
function cloneAndValidateRows(rows) {
|
|
546
524
|
const clone = structuredClone(rows);
|
|
547
|
-
decodeEntityRelationRows(clone,
|
|
525
|
+
decodeEntityRelationRows(clone, {});
|
|
548
526
|
return clone;
|
|
549
527
|
}
|
|
550
528
|
function requiredString(value, field) {
|
|
@@ -568,10 +546,6 @@ async function safeReadJson(response) {
|
|
|
568
546
|
}
|
|
569
547
|
//#endregion
|
|
570
548
|
//#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
549
|
/**
|
|
576
550
|
* Import at the editor boundary, not at asset generation time. Existing
|
|
577
551
|
* provenance is retained.
|
|
@@ -587,17 +561,17 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
587
561
|
"image",
|
|
588
562
|
"video",
|
|
589
563
|
"audio",
|
|
590
|
-
"voiceover"
|
|
564
|
+
"voiceover-recording"
|
|
591
565
|
].includes(fact.kind)) throw new Error("Unsupported media kind");
|
|
592
566
|
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,
|
|
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, {});
|
|
595
569
|
assertCanonicalEditorResources(initial);
|
|
596
570
|
const entities = structuredClone([...initial.entities]);
|
|
597
571
|
const relations = structuredClone([...initial.relations]);
|
|
598
|
-
const system = fact.kind === "voiceover" ? "memota-speech" : "memota";
|
|
572
|
+
const system = fact.kind === "voiceover-recording" ? "memota-speech" : "memota";
|
|
599
573
|
const entityKind = fact.kind;
|
|
600
|
-
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
574
|
+
const timbre = fact.kind === "voiceover-recording" ? fact.voice : void 0;
|
|
601
575
|
const durationMs = fact.kind === "image" ? null : fact.durationMs;
|
|
602
576
|
const asset = ensureAsset(entities, {
|
|
603
577
|
system,
|
|
@@ -628,12 +602,12 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
628
602
|
payload: { durationMs }
|
|
629
603
|
});
|
|
630
604
|
relations.push(assetLink(entityId, asset.entityId, idFactory));
|
|
631
|
-
if (fact.kind === "voiceover" && fact.phoneticScriptEntityId !== void 0) {
|
|
605
|
+
if (fact.kind === "voiceover-recording" && fact.phoneticScriptEntityId !== void 0) {
|
|
632
606
|
const phonetic = entities.find((row) => String(row.entityId) === fact.phoneticScriptEntityId);
|
|
633
607
|
if (phonetic?.entityKind !== "phonetic-script") throw new Error(`Voiceover ${fact.assetId} requires the PhoneticScript it was spoken from`);
|
|
634
608
|
relations.push({
|
|
635
609
|
relationId: createRelationId(idFactory("relation")),
|
|
636
|
-
relationKind: "
|
|
610
|
+
relationKind: "derived-from",
|
|
637
611
|
endpoint0EntityId: entityId,
|
|
638
612
|
endpoint1EntityId: phonetic.entityId,
|
|
639
613
|
metadata: {},
|
|
@@ -645,7 +619,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
645
619
|
entities,
|
|
646
620
|
relations
|
|
647
621
|
};
|
|
648
|
-
decodeEntityRelationRows(rows,
|
|
622
|
+
decodeEntityRelationRows(rows, {});
|
|
649
623
|
assertCanonicalEditorResources(rows);
|
|
650
624
|
return {
|
|
651
625
|
rows,
|
|
@@ -788,19 +762,19 @@ var EntityTimelineEditor = class {
|
|
|
788
762
|
return cloneRows(this.state);
|
|
789
763
|
}
|
|
790
764
|
insertClip(input) {
|
|
791
|
-
const requestedOrder = input.
|
|
765
|
+
const requestedOrder = input.order;
|
|
792
766
|
const order = typeof requestedOrder === "number" ? requestedOrder : nextSequentialOrder(this.state, createEntityId(input.trackEntityId));
|
|
793
767
|
return this.insertPlacedClip({
|
|
794
768
|
trackEntityId: input.trackEntityId,
|
|
795
769
|
contentEntityId: input.contentEntityId,
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
placement: input.
|
|
770
|
+
inMs: input.inMs,
|
|
771
|
+
outMs: input.outMs,
|
|
772
|
+
placement: input.startMs === void 0 ? {
|
|
799
773
|
kind: "sequential",
|
|
800
774
|
order
|
|
801
775
|
} : {
|
|
802
776
|
kind: "absolute",
|
|
803
|
-
|
|
777
|
+
startMs: input.startMs
|
|
804
778
|
},
|
|
805
779
|
clipPayload: input.clipPayload
|
|
806
780
|
});
|
|
@@ -812,31 +786,14 @@ var EntityTimelineEditor = class {
|
|
|
812
786
|
this.transact((draft) => {
|
|
813
787
|
const { marker } = requireClipTopology(draft, input.clipEntityId);
|
|
814
788
|
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");
|
|
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");
|
|
840
797
|
replaceEntity(draft, marker.entityId, {
|
|
841
798
|
...marker,
|
|
842
799
|
payload
|
|
@@ -947,28 +904,18 @@ var EntityTimelineEditor = class {
|
|
|
947
904
|
const content = requireSequenceContent(draft, input.contentEntityId);
|
|
948
905
|
const payload = {
|
|
949
906
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
950
|
-
|
|
951
|
-
duration: cloneJsonValue(input.duration, "duration")
|
|
907
|
+
...requireWindow(input.inMs, input.outMs)
|
|
952
908
|
};
|
|
953
|
-
delete payload.
|
|
954
|
-
delete payload.
|
|
955
|
-
if (input.
|
|
956
|
-
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");
|
|
957
913
|
replaceEntity(draft, topology.marker.entityId, {
|
|
958
914
|
...topology.marker,
|
|
959
915
|
payload
|
|
960
916
|
});
|
|
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,
|
|
917
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
918
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
972
919
|
endpoint1EntityId: content.entityId
|
|
973
920
|
});
|
|
974
921
|
});
|
|
@@ -979,15 +926,11 @@ var EntityTimelineEditor = class {
|
|
|
979
926
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
980
927
|
replaceRows(draft, imported.rows);
|
|
981
928
|
const track = this.ensureRoleTrack(draft, input.timelineEntityId, "video_clip");
|
|
982
|
-
const
|
|
929
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
983
930
|
const clipId = this.insertPlacedClipIntoDraft(draft, {
|
|
984
931
|
trackEntityId: track.entityId,
|
|
985
932
|
contentEntityId: imported.contentEntityId,
|
|
986
|
-
|
|
987
|
-
duration: {
|
|
988
|
-
mode: "fixed",
|
|
989
|
-
value: sourceRange.end - sourceRange.start
|
|
990
|
-
},
|
|
933
|
+
...window,
|
|
991
934
|
placement: input.placement,
|
|
992
935
|
...input.clipEntityId === void 0 ? {} : { clipEntityId: input.clipEntityId },
|
|
993
936
|
...input.volume === void 0 ? {} : { clipPayload: { volume: requireVolume(input.volume) } }
|
|
@@ -1022,15 +965,11 @@ var EntityTimelineEditor = class {
|
|
|
1022
965
|
for (const [offset, clip] of input.clips.entries()) {
|
|
1023
966
|
const imported = importMediaAsset(draft, clip.media, this.idFactory);
|
|
1024
967
|
replaceRows(draft, imported.rows);
|
|
1025
|
-
const
|
|
968
|
+
const window = requireWindow(clip.inMs, clip.outMs);
|
|
1026
969
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1027
970
|
trackEntityId: track.entityId,
|
|
1028
971
|
contentEntityId: imported.contentEntityId,
|
|
1029
|
-
|
|
1030
|
-
duration: {
|
|
1031
|
-
mode: "fixed",
|
|
1032
|
-
value: sourceRange.end - sourceRange.start
|
|
1033
|
-
},
|
|
972
|
+
...window,
|
|
1034
973
|
placement: {
|
|
1035
974
|
kind: "sequential",
|
|
1036
975
|
order: ordered.length + offset
|
|
@@ -1052,23 +991,18 @@ var EntityTimelineEditor = class {
|
|
|
1052
991
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1053
992
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1054
993
|
replaceRows(draft, imported.rows);
|
|
1055
|
-
const
|
|
994
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1056
995
|
const markerPayloadValue = {
|
|
1057
996
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1058
|
-
|
|
1059
|
-
duration: {
|
|
1060
|
-
mode: "fixed",
|
|
1061
|
-
value: sourceRange.end - sourceRange.start
|
|
1062
|
-
}
|
|
997
|
+
...window
|
|
1063
998
|
};
|
|
1064
|
-
delete markerPayloadValue.
|
|
1065
|
-
resizeAbsoluteTarget(markerPayloadValue, sourceRange.end - sourceRange.start);
|
|
999
|
+
delete markerPayloadValue.rate;
|
|
1066
1000
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1067
1001
|
...topology.marker,
|
|
1068
1002
|
payload: markerPayloadValue
|
|
1069
1003
|
});
|
|
1070
|
-
this.relinkRelation(draft, topology.
|
|
1071
|
-
endpoint0EntityId: topology.
|
|
1004
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1005
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1072
1006
|
endpoint1EntityId: createEntityId(imported.contentEntityId)
|
|
1073
1007
|
});
|
|
1074
1008
|
this.resolveOneVoiceoverOverlap(draft);
|
|
@@ -1090,42 +1024,26 @@ var EntityTimelineEditor = class {
|
|
|
1090
1024
|
setClipSpeed(input) {
|
|
1091
1025
|
this.transact((draft) => {
|
|
1092
1026
|
const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1093
|
-
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.
|
|
1027
|
+
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.contentRelation, initialTopology.clip.entityId));
|
|
1094
1028
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
|
|
1095
1029
|
const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
|
|
1096
|
-
const
|
|
1097
|
-
if (input.
|
|
1030
|
+
const priorWindow = markerWindow(initialPayload);
|
|
1031
|
+
if (input.rate === null && initialPayload.rate === void 0) return;
|
|
1098
1032
|
this.normalizeMainSequential(draft);
|
|
1099
1033
|
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1100
1034
|
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);
|
|
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);
|
|
1116
1044
|
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
|
-
};
|
|
1045
|
+
payload.rate = rate;
|
|
1127
1046
|
}
|
|
1128
|
-
resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
|
|
1129
1047
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1130
1048
|
...topology.marker,
|
|
1131
1049
|
payload
|
|
@@ -1137,21 +1055,15 @@ var EntityTimelineEditor = class {
|
|
|
1137
1055
|
this.transact((draft) => {
|
|
1138
1056
|
this.normalizeMainSequential(draft);
|
|
1139
1057
|
const topology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1140
|
-
const content = requireSequenceContent(draft, otherEndpoint(topology.
|
|
1058
|
+
const content = requireSequenceContent(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId));
|
|
1141
1059
|
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip trimming is supported only for Video or Image content");
|
|
1142
|
-
const
|
|
1060
|
+
const window = requireWindow(input.inMs, input.outMs);
|
|
1143
1061
|
const payload = {
|
|
1144
1062
|
...cloneJsonObject(topology.marker.payload, "Sequence Marker payload"),
|
|
1145
|
-
|
|
1063
|
+
...window
|
|
1146
1064
|
};
|
|
1147
|
-
const
|
|
1148
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1065
|
+
const duration = markerDuration(payload, window);
|
|
1149
1066
|
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
1067
|
replaceEntity(draft, topology.marker.entityId, {
|
|
1156
1068
|
...topology.marker,
|
|
1157
1069
|
payload
|
|
@@ -1181,15 +1093,11 @@ var EntityTimelineEditor = class {
|
|
|
1181
1093
|
if (replacement.clipEntityId !== void 0 && oldIds.has(replacement.clipEntityId)) throw new Error("A replacement Clip id cannot reuse an Entity being removed");
|
|
1182
1094
|
const imported = importMediaAsset(draft, replacement.media, this.idFactory);
|
|
1183
1095
|
replaceRows(draft, imported.rows);
|
|
1184
|
-
const
|
|
1096
|
+
const window = requireWindow(replacement.inMs, replacement.outMs);
|
|
1185
1097
|
created.push(this.insertPlacedClipIntoDraft(draft, {
|
|
1186
1098
|
trackEntityId: track.entityId,
|
|
1187
1099
|
contentEntityId: imported.contentEntityId,
|
|
1188
|
-
|
|
1189
|
-
duration: {
|
|
1190
|
-
mode: "fixed",
|
|
1191
|
-
value: sourceRange.end - sourceRange.start
|
|
1192
|
-
},
|
|
1100
|
+
...window,
|
|
1193
1101
|
placement: {
|
|
1194
1102
|
kind: "sequential",
|
|
1195
1103
|
order: temporaryOrder + offset
|
|
@@ -1223,10 +1131,14 @@ var EntityTimelineEditor = class {
|
|
|
1223
1131
|
deleteClip(input) {
|
|
1224
1132
|
this.transact((draft) => {
|
|
1225
1133
|
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([
|
|
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
|
+
]);
|
|
1230
1142
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removedRelationIds.add(relation.relationId);
|
|
1231
1143
|
draft.relations = draft.relations.filter((relation) => !removedRelationIds.has(relation.relationId));
|
|
1232
1144
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== topology.clip.entityId && (!markerIsExclusive || entity.entityId !== topology.marker.entityId));
|
|
@@ -1247,28 +1159,20 @@ var EntityTimelineEditor = class {
|
|
|
1247
1159
|
if (placement === void 0) throw new Error(`Anchored child Clip "${childId}" is not placed`);
|
|
1248
1160
|
this.applyClipPlacement(draft, childId, {
|
|
1249
1161
|
kind: "absolute",
|
|
1250
|
-
|
|
1251
|
-
start: placement.start,
|
|
1252
|
-
end: placement.start + placement.duration
|
|
1253
|
-
}
|
|
1162
|
+
startMs: placement.start
|
|
1254
1163
|
});
|
|
1255
1164
|
}
|
|
1256
1165
|
else for (const target of targets) this.removeAnchoredDescendants(draft, target, targets);
|
|
1257
1166
|
for (const target of targets) this.removeClipGraph(draft, target);
|
|
1258
|
-
normalizeSequentialOrders(draft);
|
|
1259
1167
|
if (includesVisual) this.resolveOneVoiceoverOverlap(draft);
|
|
1260
1168
|
});
|
|
1261
1169
|
}
|
|
1262
1170
|
updateClip(input) {
|
|
1263
1171
|
this.transact((draft) => {
|
|
1264
1172
|
const clip = requireEntityKind(draft, input.clipEntityId, "clip");
|
|
1265
|
-
const priorOrder = clip.payload.order;
|
|
1266
1173
|
replaceEntity(draft, clip.entityId, {
|
|
1267
1174
|
...clip,
|
|
1268
|
-
payload:
|
|
1269
|
-
...cloneJsonObject(input.payload, "Clip payload"),
|
|
1270
|
-
...priorOrder === void 0 || Object.hasOwn(input.payload, "order") ? {} : { order: priorOrder }
|
|
1271
|
-
}
|
|
1175
|
+
payload: cloneJsonObject(input.payload, "Clip payload")
|
|
1272
1176
|
});
|
|
1273
1177
|
});
|
|
1274
1178
|
}
|
|
@@ -1278,13 +1182,13 @@ var EntityTimelineEditor = class {
|
|
|
1278
1182
|
const placement = input.placement ?? {
|
|
1279
1183
|
kind: "anchored",
|
|
1280
1184
|
hostClipEntityId: input.hostClipEntityId,
|
|
1281
|
-
|
|
1185
|
+
startMs: input.startMs
|
|
1282
1186
|
};
|
|
1283
|
-
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");
|
|
1284
1188
|
requireVolume(input.volume);
|
|
1285
1189
|
const imported = importMediaAsset(draft, input.media, this.idFactory);
|
|
1286
1190
|
replaceRows(draft, imported.rows);
|
|
1287
|
-
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover");
|
|
1191
|
+
const voiceover = requireEntityKind(draft, imported.contentEntityId, "voiceover-recording");
|
|
1288
1192
|
const phonetic = this.requireComposedPhoneticScript(draft, input.phoneticScriptEntityId);
|
|
1289
1193
|
this.ensurePhoneticRender(draft, voiceover.entityId, phonetic.entityId);
|
|
1290
1194
|
const speechTrack = this.ensureRoleTrack(draft, input.timelineEntityId, "speech");
|
|
@@ -1294,14 +1198,8 @@ var EntityTimelineEditor = class {
|
|
|
1294
1198
|
clipEntityId: input.voiceoverClipEntityId,
|
|
1295
1199
|
trackEntityId: speechTrack.entityId,
|
|
1296
1200
|
contentEntityId: voiceover.entityId,
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
end: input.media.durationMs
|
|
1300
|
-
},
|
|
1301
|
-
duration: {
|
|
1302
|
-
mode: "fixed",
|
|
1303
|
-
value: input.media.durationMs
|
|
1304
|
-
},
|
|
1201
|
+
inMs: 0,
|
|
1202
|
+
outMs: input.media.durationMs,
|
|
1305
1203
|
placement,
|
|
1306
1204
|
clipPayload: { volume: input.volume }
|
|
1307
1205
|
});
|
|
@@ -1315,8 +1213,8 @@ var EntityTimelineEditor = class {
|
|
|
1315
1213
|
volume: input.volume
|
|
1316
1214
|
}
|
|
1317
1215
|
});
|
|
1318
|
-
this.relinkRelation(draft, topology.
|
|
1319
|
-
endpoint0EntityId: topology.
|
|
1216
|
+
this.relinkRelation(draft, topology.contentRelation, {
|
|
1217
|
+
endpoint0EntityId: topology.clip.entityId,
|
|
1320
1218
|
endpoint1EntityId: voiceover.entityId
|
|
1321
1219
|
});
|
|
1322
1220
|
this.applyClipPlacement(draft, topology.clip.entityId, placement);
|
|
@@ -1325,18 +1223,12 @@ var EntityTimelineEditor = class {
|
|
|
1325
1223
|
...placed.marker,
|
|
1326
1224
|
payload: {
|
|
1327
1225
|
...placed.marker.payload,
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
end: input.media.durationMs
|
|
1331
|
-
},
|
|
1332
|
-
duration: {
|
|
1333
|
-
mode: "fixed",
|
|
1334
|
-
value: input.media.durationMs
|
|
1335
|
-
}
|
|
1226
|
+
inMs: 0,
|
|
1227
|
+
outMs: input.media.durationMs
|
|
1336
1228
|
}
|
|
1337
1229
|
});
|
|
1338
1230
|
}
|
|
1339
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phonetic.entityId, input.captions);
|
|
1231
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceover.entityId, phonetic.entityId, input.captions);
|
|
1340
1232
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1341
1233
|
return {
|
|
1342
1234
|
voiceoverClipEntityId,
|
|
@@ -1381,11 +1273,8 @@ var EntityTimelineEditor = class {
|
|
|
1381
1273
|
clipEntityId: input.bgmClipEntityId,
|
|
1382
1274
|
trackEntityId: track.entityId,
|
|
1383
1275
|
contentEntityId: imported.contentEntityId,
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
end: input.media.durationMs
|
|
1387
|
-
},
|
|
1388
|
-
duration: { mode: "from-source" },
|
|
1276
|
+
inMs: 0,
|
|
1277
|
+
outMs: input.media.durationMs,
|
|
1389
1278
|
placement: {
|
|
1390
1279
|
kind: "sequential",
|
|
1391
1280
|
order: 0
|
|
@@ -1397,7 +1286,7 @@ var EntityTimelineEditor = class {
|
|
|
1397
1286
|
...topology.marker,
|
|
1398
1287
|
payload: {
|
|
1399
1288
|
...topology.marker.payload,
|
|
1400
|
-
|
|
1289
|
+
fillTimeline: true
|
|
1401
1290
|
}
|
|
1402
1291
|
});
|
|
1403
1292
|
return clipId;
|
|
@@ -1430,7 +1319,7 @@ var EntityTimelineEditor = class {
|
|
|
1430
1319
|
const assembled = new EntityProjectionGraph(draft);
|
|
1431
1320
|
for (const clip of clipsOnTrack(draft, track.entityId)) {
|
|
1432
1321
|
const topology = requireClipTopology(draft, clip.entityId);
|
|
1433
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1322
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1434
1323
|
const prior = assembled.entity(caption.entityId).payload.style;
|
|
1435
1324
|
const style = isJsonObject(prior) ? prior : {};
|
|
1436
1325
|
for (const updated of updateEntityFields(draft, caption.entityId, { style: {
|
|
@@ -1467,14 +1356,8 @@ var EntityTimelineEditor = class {
|
|
|
1467
1356
|
return this.insertPlacedClipIntoDraft(draft, {
|
|
1468
1357
|
trackEntityId: track.entityId,
|
|
1469
1358
|
contentEntityId: content.entityId,
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
end: durationMs
|
|
1473
|
-
},
|
|
1474
|
-
duration: {
|
|
1475
|
-
mode: "fixed",
|
|
1476
|
-
value: durationMs
|
|
1477
|
-
},
|
|
1359
|
+
inMs: 0,
|
|
1360
|
+
outMs: durationMs,
|
|
1478
1361
|
placement: input.placement,
|
|
1479
1362
|
...input.captionClipEntityId === void 0 ? {} : { clipEntityId: input.captionClipEntityId }
|
|
1480
1363
|
});
|
|
@@ -1487,8 +1370,7 @@ var EntityTimelineEditor = class {
|
|
|
1487
1370
|
const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
|
|
1488
1371
|
const markerId = this.mintEntityId(draft);
|
|
1489
1372
|
const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
|
|
1490
|
-
|
|
1491
|
-
const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
|
|
1373
|
+
const markerPlacement = applyPlacementPayload(input.placement);
|
|
1492
1374
|
const clip = {
|
|
1493
1375
|
entityId: clipId,
|
|
1494
1376
|
entityKind: "clip",
|
|
@@ -1498,13 +1380,12 @@ var EntityTimelineEditor = class {
|
|
|
1498
1380
|
entityId: markerId,
|
|
1499
1381
|
entityKind: "sequence-marker",
|
|
1500
1382
|
payload: markerPayload({
|
|
1501
|
-
|
|
1502
|
-
duration: input.duration,
|
|
1383
|
+
...requireWindow(input.inMs, input.outMs),
|
|
1503
1384
|
...markerPlacement
|
|
1504
1385
|
})
|
|
1505
1386
|
};
|
|
1506
1387
|
draft.entities.push(clip, marker);
|
|
1507
|
-
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));
|
|
1508
1389
|
if (input.placement.kind === "anchored") {
|
|
1509
1390
|
const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
|
|
1510
1391
|
if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
|
|
@@ -1514,20 +1395,7 @@ var EntityTimelineEditor = class {
|
|
|
1514
1395
|
}
|
|
1515
1396
|
ensureRoleTrack(draft, timelineEntityId, role) {
|
|
1516
1397
|
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
|
-
}
|
|
1398
|
+
if (existing !== void 0) return existing;
|
|
1531
1399
|
const timeline = requireEntityKind(draft, timelineEntityId, "timeline");
|
|
1532
1400
|
const track = {
|
|
1533
1401
|
entityId: this.mintEntityId(draft),
|
|
@@ -1538,7 +1406,7 @@ var EntityTimelineEditor = class {
|
|
|
1538
1406
|
}
|
|
1539
1407
|
};
|
|
1540
1408
|
draft.entities.push(track);
|
|
1541
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1409
|
+
draft.relations.push(this.createRelation(draft, "contain", timeline.entityId, track.entityId));
|
|
1542
1410
|
return track;
|
|
1543
1411
|
}
|
|
1544
1412
|
/** The existing pronunciation variant a voiceover consumes; composition is required. */
|
|
@@ -1547,17 +1415,17 @@ var EntityTimelineEditor = class {
|
|
|
1547
1415
|
requireComposedAudioScript(draft, phonetic.entityId);
|
|
1548
1416
|
return phonetic;
|
|
1549
1417
|
}
|
|
1550
|
-
/** Preserve the factual synthesis input
|
|
1418
|
+
/** Preserve the factual synthesis input, regardless of persisted endpoint positions. */
|
|
1551
1419
|
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
1552
|
-
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));
|
|
1553
1421
|
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
1554
1422
|
if (renders[0] !== void 0) {
|
|
1555
1423
|
if (otherEndpoint(renders[0], voiceoverAudioEntityId) === phoneticScriptEntityId) return;
|
|
1556
1424
|
throw new Error(`Voiceover "${voiceoverAudioEntityId}" already has a different PhoneticScript synthesis input`);
|
|
1557
1425
|
}
|
|
1558
|
-
draft.relations.push(this.createRelation(draft, "
|
|
1426
|
+
draft.relations.push(this.createRelation(draft, "derived-from", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
1559
1427
|
}
|
|
1560
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
|
|
1428
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, voiceoverAudioEntityId, phoneticScriptEntityId, captions) {
|
|
1561
1429
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
1562
1430
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
1563
1431
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
@@ -1582,28 +1450,26 @@ var EntityTimelineEditor = class {
|
|
|
1582
1450
|
};
|
|
1583
1451
|
draft.entities.push(content);
|
|
1584
1452
|
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
1453
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
|
|
1454
|
+
start: startMs,
|
|
1455
|
+
end: startMs + durationMs
|
|
1456
|
+
} }));
|
|
1585
1457
|
this.insertPlacedClipIntoDraft(draft, {
|
|
1586
1458
|
clipEntityId: caption.captionClipEntityId,
|
|
1587
1459
|
trackEntityId: captionTrack.entityId,
|
|
1588
1460
|
contentEntityId: content.entityId,
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
end: durationMs
|
|
1592
|
-
},
|
|
1593
|
-
duration: {
|
|
1594
|
-
mode: "fixed",
|
|
1595
|
-
value: durationMs
|
|
1596
|
-
},
|
|
1461
|
+
inMs: 0,
|
|
1462
|
+
outMs: durationMs,
|
|
1597
1463
|
placement: {
|
|
1598
1464
|
kind: "anchored",
|
|
1599
1465
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1600
|
-
|
|
1466
|
+
startMs
|
|
1601
1467
|
}
|
|
1602
1468
|
});
|
|
1603
1469
|
continue;
|
|
1604
1470
|
}
|
|
1605
1471
|
const topology = requireClipOnRole(draft, caption.captionClipEntityId, "caption");
|
|
1606
|
-
const content = requireEntityKind(draft, otherEndpoint(topology.
|
|
1472
|
+
const content = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1607
1473
|
const priorStyle = isJsonObject(content.payload.style) ? content.payload.style : {};
|
|
1608
1474
|
const style = caption.style === void 0 ? priorStyle : {
|
|
1609
1475
|
...priorStyle,
|
|
@@ -1622,40 +1488,48 @@ var EntityTimelineEditor = class {
|
|
|
1622
1488
|
this.applyClipPlacement(draft, topology.clip.entityId, {
|
|
1623
1489
|
kind: "anchored",
|
|
1624
1490
|
hostClipEntityId: voiceoverClipEntityId,
|
|
1625
|
-
|
|
1491
|
+
startMs
|
|
1626
1492
|
});
|
|
1627
1493
|
const placed = requireClipTopology(draft, topology.clip.entityId);
|
|
1494
|
+
const window = markerWindow(placed.marker.payload);
|
|
1495
|
+
const rate = (window.outMs - window.inMs) / durationMs;
|
|
1628
1496
|
replaceEntity(draft, placed.marker.entityId, {
|
|
1629
1497
|
...placed.marker,
|
|
1630
1498
|
payload: {
|
|
1631
1499
|
...placed.marker.payload,
|
|
1632
|
-
|
|
1633
|
-
mode: "fixed",
|
|
1634
|
-
value: durationMs
|
|
1635
|
-
}
|
|
1500
|
+
...rate === 1 ? {} : { rate }
|
|
1636
1501
|
}
|
|
1637
1502
|
});
|
|
1503
|
+
this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
|
|
1638
1504
|
}
|
|
1639
1505
|
}
|
|
1640
1506
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
1641
1507
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1642
1508
|
const track = requireEntityKind(draft, otherEndpoint(topology.trackRelation, topology.clip.entityId), "track");
|
|
1643
|
-
const timeline = requireEntityKind(draft, otherEndpoint(
|
|
1509
|
+
const timeline = requireEntityKind(draft, otherEndpoint(requireOneContain(draft, track.entityId, "contained"), track.entityId), "timeline");
|
|
1644
1510
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
1645
1511
|
}
|
|
1512
|
+
replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
|
|
1513
|
+
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
|
|
1514
|
+
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1515
|
+
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
|
|
1516
|
+
start: startMs,
|
|
1517
|
+
end: startMs + durationMs
|
|
1518
|
+
} }));
|
|
1519
|
+
}
|
|
1646
1520
|
/**
|
|
1647
1521
|
* Remove one placed Caption use. The composed AudioScript outlives the
|
|
1648
1522
|
* caption: it may anchor further captions and remains the text owner.
|
|
1649
1523
|
*/
|
|
1650
1524
|
removeCaptionClipGraph(draft, clipEntityId) {
|
|
1651
1525
|
const topology = requireClipOnRole(draft, clipEntityId, "caption");
|
|
1652
|
-
const caption = requireEntityKind(draft, otherEndpoint(topology.
|
|
1526
|
+
const caption = requireEntityKind(draft, otherEndpoint(topology.contentRelation, topology.clip.entityId), "caption");
|
|
1653
1527
|
const contentRelations = incidentRelations(draft, caption.entityId);
|
|
1654
|
-
const allowed = new Set(["caption-alignment", "
|
|
1528
|
+
const allowed = new Set(["caption-alignment", "contain"]);
|
|
1655
1529
|
const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
|
|
1656
1530
|
if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
|
|
1657
1531
|
this.removeClipGraph(draft, clipEntityId);
|
|
1658
|
-
if (
|
|
1532
|
+
if (containing(draft, caption.entityId, "contained").length > 0) return;
|
|
1659
1533
|
const removed = new Set(incidentRelations(draft, caption.entityId).map((relation) => relation.relationId));
|
|
1660
1534
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1661
1535
|
draft.entities = draft.entities.filter((entity) => entity.entityId !== caption.entityId);
|
|
@@ -1703,10 +1577,7 @@ var EntityTimelineEditor = class {
|
|
|
1703
1577
|
const overlapMs = Math.round(prior.end - current.start);
|
|
1704
1578
|
for (const [visualIndex, clip] of videoTrack.clips.entries()) this.applyClipPlacement(draft, clip.clipEntityId, {
|
|
1705
1579
|
kind: "absolute",
|
|
1706
|
-
|
|
1707
|
-
start: clip.start + (visualIndex >= hostIndex ? overlapMs : 0),
|
|
1708
|
-
end: clip.start + clip.duration + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1709
|
-
}
|
|
1580
|
+
startMs: clip.start + (visualIndex >= hostIndex ? overlapMs : 0)
|
|
1710
1581
|
});
|
|
1711
1582
|
return;
|
|
1712
1583
|
}
|
|
@@ -1715,18 +1586,16 @@ var EntityTimelineEditor = class {
|
|
|
1715
1586
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1716
1587
|
const clipPayload = { ...cloneJsonObject(topology.clip.payload, "Clip payload") };
|
|
1717
1588
|
const markerPayloadValue = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1718
|
-
delete
|
|
1719
|
-
delete markerPayloadValue.targetRange;
|
|
1720
|
-
delete markerPayloadValue.anchorOffset;
|
|
1589
|
+
delete markerPayloadValue.startMs;
|
|
1721
1590
|
const outgoingAnchors = draft.relations.filter((relation) => relation.relationKind === "clip-anchor" && relation.endpoint0EntityId === topology.clip.entityId);
|
|
1722
1591
|
const removed = new Set(outgoingAnchors.map((relation) => relation.relationId));
|
|
1723
1592
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
1724
|
-
if (placement.kind === "sequential")
|
|
1725
|
-
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");
|
|
1726
1595
|
else {
|
|
1727
1596
|
const host = requireEntityKind(draft, placement.hostClipEntityId, "clip");
|
|
1728
1597
|
if (host.entityId === topology.clip.entityId) throw new Error("A Clip cannot anchor to itself");
|
|
1729
|
-
markerPayloadValue.
|
|
1598
|
+
markerPayloadValue.startMs = finiteNonNegative(placement.startMs, "startMs");
|
|
1730
1599
|
draft.relations.push(this.createRelation(draft, "clip-anchor", topology.clip.entityId, host.entityId));
|
|
1731
1600
|
}
|
|
1732
1601
|
replaceEntity(draft, topology.clip.entityId, {
|
|
@@ -1741,14 +1610,10 @@ var EntityTimelineEditor = class {
|
|
|
1741
1610
|
reanchorAtAbsolute(draft, childClipEntityId, absoluteStart, layout) {
|
|
1742
1611
|
const hosts = layout.tracks.find((track) => track.role === "video_clip")?.clips.filter((clip) => clip.mode === "sequential") ?? [];
|
|
1743
1612
|
if (hosts.length === 0) {
|
|
1744
|
-
|
|
1745
|
-
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`);
|
|
1746
1614
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1747
1615
|
kind: "absolute",
|
|
1748
|
-
|
|
1749
|
-
start: absoluteStart,
|
|
1750
|
-
end: absoluteStart + child.duration
|
|
1751
|
-
}
|
|
1616
|
+
startMs: absoluteStart
|
|
1752
1617
|
});
|
|
1753
1618
|
return;
|
|
1754
1619
|
}
|
|
@@ -1756,7 +1621,7 @@ var EntityTimelineEditor = class {
|
|
|
1756
1621
|
this.applyClipPlacement(draft, childClipEntityId, {
|
|
1757
1622
|
kind: "anchored",
|
|
1758
1623
|
hostClipEntityId: host.clipEntityId,
|
|
1759
|
-
|
|
1624
|
+
startMs: Math.max(0, Math.round(absoluteStart - host.start))
|
|
1760
1625
|
});
|
|
1761
1626
|
}
|
|
1762
1627
|
removeAnchoredDescendants(draft, hostClipEntityId, scheduled) {
|
|
@@ -1773,12 +1638,12 @@ var EntityTimelineEditor = class {
|
|
|
1773
1638
|
removeClipGraph(draft, clipEntityId) {
|
|
1774
1639
|
const topology = requireClipTopology(draft, clipEntityId);
|
|
1775
1640
|
assertOnlyStructuralIncidents(draft, topology.clip.entityId, new Set([
|
|
1776
|
-
"
|
|
1777
|
-
"
|
|
1641
|
+
"contain",
|
|
1642
|
+
"marker",
|
|
1778
1643
|
"clip-anchor"
|
|
1779
1644
|
]));
|
|
1780
|
-
assertOnlyStructuralIncidents(draft, topology.marker.entityId, new Set(["
|
|
1781
|
-
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;
|
|
1782
1647
|
const removed = new Set(incidentRelations(draft, topology.clip.entityId).map((relation) => relation.relationId));
|
|
1783
1648
|
if (markerIsExclusive) for (const relation of incidentRelations(draft, topology.marker.entityId)) removed.add(relation.relationId);
|
|
1784
1649
|
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
@@ -1824,7 +1689,7 @@ var EntityTimelineEditor = class {
|
|
|
1824
1689
|
}
|
|
1825
1690
|
};
|
|
1826
1691
|
function validateFinalRows(rows) {
|
|
1827
|
-
decodeEntityRelationRows(rows,
|
|
1692
|
+
decodeEntityRelationRows(rows, {});
|
|
1828
1693
|
assertCanonicalEditorResources(rows);
|
|
1829
1694
|
validateTimelineHierarchy(rows);
|
|
1830
1695
|
}
|
|
@@ -1832,13 +1697,13 @@ function validateTimelineHierarchy(rows) {
|
|
|
1832
1697
|
for (const entity of rows.entities) {
|
|
1833
1698
|
if (entity.entityKind === "track") requireTrackTimeline(rows, entity.entityId);
|
|
1834
1699
|
if (entity.entityKind !== "clip") continue;
|
|
1835
|
-
const trackRelations =
|
|
1700
|
+
const trackRelations = containing(rows, entity.entityId, "contained");
|
|
1836
1701
|
if (trackRelations.length !== 1) throw new Error(`Clip Entity "${entity.entityId}" must belong to exactly one Track`);
|
|
1837
1702
|
requireEntityKind(rows, otherEndpoint(trackRelations[0], entity.entityId), "track");
|
|
1838
1703
|
}
|
|
1839
1704
|
}
|
|
1840
1705
|
function requireTrackTimeline(rows, trackEntityId) {
|
|
1841
|
-
const timelineRelations =
|
|
1706
|
+
const timelineRelations = containing(rows, createEntityId(trackEntityId), "contained");
|
|
1842
1707
|
if (timelineRelations.filter((relation) => {
|
|
1843
1708
|
const otherId = otherEndpoint(relation, trackEntityId);
|
|
1844
1709
|
return rows.entities.some((entity) => entity.entityId === otherId && entity.entityKind === "timeline");
|
|
@@ -1846,20 +1711,36 @@ function requireTrackTimeline(rows, trackEntityId) {
|
|
|
1846
1711
|
}
|
|
1847
1712
|
function requireClipTopology(rows, clipEntityId) {
|
|
1848
1713
|
const clip = requireEntityKind(rows, clipEntityId, "clip");
|
|
1849
|
-
const trackRelation =
|
|
1714
|
+
const trackRelation = requireOneContain(rows, clip.entityId, "contained");
|
|
1850
1715
|
requireEntityKind(rows, otherEndpoint(trackRelation, clip.entityId), "track");
|
|
1851
|
-
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "
|
|
1716
|
+
const clipMarkerRelation = requireOneRelation(rows, clip.entityId, "marker");
|
|
1852
1717
|
const marker = requireEntityKind(rows, otherEndpoint(clipMarkerRelation, clip.entityId), "sequence-marker");
|
|
1853
|
-
const
|
|
1854
|
-
requireSequenceContent(rows, otherEndpoint(
|
|
1718
|
+
const contentRelation = requireOneContain(rows, clip.entityId, "container");
|
|
1719
|
+
requireSequenceContent(rows, otherEndpoint(contentRelation, clip.entityId));
|
|
1855
1720
|
return {
|
|
1856
1721
|
clip,
|
|
1857
1722
|
marker,
|
|
1858
1723
|
trackRelation,
|
|
1859
1724
|
clipMarkerRelation,
|
|
1860
|
-
|
|
1725
|
+
contentRelation
|
|
1861
1726
|
};
|
|
1862
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
|
+
}
|
|
1863
1744
|
function requireOneRelation(rows, entityId, relationKind) {
|
|
1864
1745
|
const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
|
|
1865
1746
|
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
|
|
@@ -1898,40 +1779,47 @@ function hasOutgoingClipAnchor(rows, clipEntityId) {
|
|
|
1898
1779
|
function outgoingAnchorChildren(rows, hostIds) {
|
|
1899
1780
|
return rows.relations.filter((relation) => relation.relationKind === "clip-anchor" && hostIds.has(relation.endpoint1EntityId)).map((relation) => String(relation.endpoint0EntityId));
|
|
1900
1781
|
}
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
}
|
|
1906
|
-
if (placement.kind === "absolute") return { targetRange: cloneJsonValue(placement.targetRange, "targetRange") };
|
|
1907
|
-
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") };
|
|
1908
1786
|
}
|
|
1909
1787
|
function requireAvailableEntityId(rows, value) {
|
|
1910
1788
|
const entityId = createEntityId(value);
|
|
1911
1789
|
if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
|
|
1912
1790
|
return entityId;
|
|
1913
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
|
+
*/
|
|
1914
1799
|
function setClipOrder(rows, clipEntityId, order) {
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
}
|
|
1924
|
-
|
|
1925
|
-
|
|
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]);
|
|
1926
1814
|
}
|
|
1927
1815
|
function findRoleTrack(rows, timelineEntityId, role) {
|
|
1928
1816
|
const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
|
|
1929
|
-
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);
|
|
1930
1818
|
if (tracks.length > 1) throw new Error(`Timeline "${timelineEntityId}" has multiple ${role} Tracks`);
|
|
1931
1819
|
return tracks[0];
|
|
1932
1820
|
}
|
|
1933
1821
|
function clipsOnTrack(rows, trackEntityId) {
|
|
1934
|
-
return
|
|
1822
|
+
return containing(rows, trackEntityId, "container").map((relation) => requireEntityKind(rows, otherEndpoint(relation, trackEntityId), "clip"));
|
|
1935
1823
|
}
|
|
1936
1824
|
function clipRole(rows, clipEntityId) {
|
|
1937
1825
|
const topology = requireClipTopology(rows, clipEntityId);
|
|
@@ -1964,35 +1852,27 @@ function requireVolume(value) {
|
|
|
1964
1852
|
if (volume < -60 || volume > 20) throw new Error("Clip volume must be between -60 and 20 dB");
|
|
1965
1853
|
return volume;
|
|
1966
1854
|
}
|
|
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`);
|
|
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`);
|
|
1972
1859
|
return {
|
|
1973
|
-
start,
|
|
1974
|
-
end
|
|
1860
|
+
inMs: start,
|
|
1861
|
+
outMs: end
|
|
1975
1862
|
};
|
|
1976
1863
|
}
|
|
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
|
-
};
|
|
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");
|
|
1996
1876
|
}
|
|
1997
1877
|
function replaceRows(rows, replacement) {
|
|
1998
1878
|
rows.entities = structuredClone([...replacement.entities]);
|
|
@@ -2003,9 +1883,9 @@ function finiteNonNegative(value, label) {
|
|
|
2003
1883
|
if (result < 0) throw new Error(`${label} must be non-negative`);
|
|
2004
1884
|
return result;
|
|
2005
1885
|
}
|
|
1886
|
+
/** How many Clips the Track already holds — the index a new one lands at. */
|
|
2006
1887
|
function nextSequentialOrder(rows, trackEntityId) {
|
|
2007
|
-
|
|
2008
|
-
return orders.length === 0 ? 0 : Math.max(...orders) + 1;
|
|
1888
|
+
return relationsFrom(rows.relations, "contain", trackEntityId).length;
|
|
2009
1889
|
}
|
|
2010
1890
|
function otherEndpoint(relation, entityId) {
|
|
2011
1891
|
if (relation.endpoint0EntityId === entityId) return relation.endpoint1EntityId;
|
|
@@ -2025,174 +1905,12 @@ function cloneJsonObject(value, label) {
|
|
|
2025
1905
|
if (!isJsonObject(cloned)) throw new Error(`${label} must contain only JSON values`);
|
|
2026
1906
|
return cloned;
|
|
2027
1907
|
}
|
|
2028
|
-
function cloneJsonValue(value, label) {
|
|
2029
|
-
return cloneJsonObject({ value }, label).value;
|
|
2030
|
-
}
|
|
2031
1908
|
function cloneRows(rows) {
|
|
2032
1909
|
return {
|
|
2033
1910
|
entities: rows.entities.map((entity) => structuredClone(entity)),
|
|
2034
1911
|
relations: rows.relations.map((relation) => structuredClone(relation))
|
|
2035
1912
|
};
|
|
2036
1913
|
}
|
|
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
|
-
//#endregion
|
|
2042
|
-
//#region src/entity-editor/revise-audio-script.ts
|
|
2043
|
-
/** Revise text as an independent authored document, never as a version-family mutation. */
|
|
2044
|
-
function reviseAudioScript(rows, input) {
|
|
2045
|
-
if (typeof input.editId !== "string" || !input.editId.trim()) throw new Error("editId is required");
|
|
2046
|
-
if (!Array.isArray(input.edits) || !input.edits.length) throw new Error("At least one segment edit is required");
|
|
2047
|
-
const edits = /* @__PURE__ */ new Map();
|
|
2048
|
-
for (const edit of input.edits) {
|
|
2049
|
-
if (!edit.segmentId?.trim() || typeof edit.text !== "string" || !edit.text.trim()) throw new Error("Segment text must be nonempty");
|
|
2050
|
-
if (edits.has(edit.segmentId)) throw new Error("Duplicate segment edit");
|
|
2051
|
-
edits.set(edit.segmentId, edit.text);
|
|
2052
|
-
}
|
|
2053
|
-
const signature = hash({
|
|
2054
|
-
...input,
|
|
2055
|
-
captionUsages: input.captionUsages ?? []
|
|
2056
|
-
});
|
|
2057
|
-
const prior = rows.relations.filter((r) => r.relationKind === "generated" && r.trace.command === "revise-audio-script" && r.trace.editId === input.editId);
|
|
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
|
-
}
|
|
2077
|
-
const source = rows.entities.find((row) => row.entityId === input.sourceAudioScriptEntityId);
|
|
2078
|
-
if (source?.entityKind !== "audio-script") throw new Error("AudioScript source is missing");
|
|
2079
|
-
assembleScriptText({
|
|
2080
|
-
...source,
|
|
2081
|
-
entityKind: "audio-script"
|
|
2082
|
-
});
|
|
2083
|
-
const payload = structuredClone(source.payload);
|
|
2084
|
-
if (!Array.isArray(payload.segments)) throw new Error("AudioScript must own its segments");
|
|
2085
|
-
const segmentIds = new Set(payload.segments.filter(isJsonObject).map((segment) => segment.segmentId));
|
|
2086
|
-
for (const segmentId of edits.keys()) if (!segmentIds.has(segmentId)) throw new Error(`Unknown segment ${segmentId}`);
|
|
2087
|
-
const changedSegments = /* @__PURE__ */ new Set();
|
|
2088
|
-
payload.segments = payload.segments.map((segment) => {
|
|
2089
|
-
if (segment === null || typeof segment !== "object" || Array.isArray(segment)) throw new Error("Invalid segment");
|
|
2090
|
-
const text = edits.get(String(segment.segmentId)) ?? segment.text;
|
|
2091
|
-
if (text !== segment.text) changedSegments.add(String(segment.segmentId));
|
|
2092
|
-
return {
|
|
2093
|
-
...segment,
|
|
2094
|
-
text
|
|
2095
|
-
};
|
|
2096
|
-
});
|
|
2097
|
-
if (!changedSegments.size) return {
|
|
2098
|
-
rows,
|
|
2099
|
-
audioScriptEntityId: source.entityId,
|
|
2100
|
-
captionUsages: [],
|
|
2101
|
-
changed: false
|
|
2102
|
-
};
|
|
2103
|
-
delete payload.assetContentHash;
|
|
2104
|
-
const id = hash(input.editId);
|
|
2105
|
-
const audioScriptEntityId = createEntityId(`entity_script_revision_${id}`);
|
|
2106
|
-
const result = {
|
|
2107
|
-
entities: [...rows.entities, {
|
|
2108
|
-
entityId: audioScriptEntityId,
|
|
2109
|
-
entityKind: "audio-script",
|
|
2110
|
-
payload
|
|
2111
|
-
}],
|
|
2112
|
-
relations: [...rows.relations, {
|
|
2113
|
-
relationId: createRelationId(`relation_script_revision_${id}`),
|
|
2114
|
-
relationKind: "generated",
|
|
2115
|
-
endpoint0EntityId: audioScriptEntityId,
|
|
2116
|
-
endpoint1EntityId: source.entityId,
|
|
2117
|
-
metadata: {},
|
|
2118
|
-
trace: {
|
|
2119
|
-
command: "revise-audio-script",
|
|
2120
|
-
editId: input.editId,
|
|
2121
|
-
signature
|
|
2122
|
-
}
|
|
2123
|
-
}]
|
|
2124
|
-
};
|
|
2125
|
-
const entities = [...result.entities];
|
|
2126
|
-
let relations = [...result.relations];
|
|
2127
|
-
const captionUsages = [];
|
|
2128
|
-
const selectedMarkers = /* @__PURE__ */ new Set();
|
|
2129
|
-
for (const [index, usage] of (input.captionUsages ?? []).entries()) {
|
|
2130
|
-
if (selectedMarkers.has(usage.markerEntityId)) throw new Error("Duplicate caption usage");
|
|
2131
|
-
selectedMarkers.add(usage.markerEntityId);
|
|
2132
|
-
const caption = rows.entities.find((row) => row.entityId === usage.captionEntityId);
|
|
2133
|
-
if (caption?.entityKind !== "caption") throw new Error("Caption is missing");
|
|
2134
|
-
const assembled = assembleCaptionContent(rows, caption.entityId);
|
|
2135
|
-
if (assembled.audioScript.entityId !== source.entityId) throw new Error("Caption does not use the source AudioScript");
|
|
2136
|
-
const selection = assembled.caption.payload.selection;
|
|
2137
|
-
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
|
-
const bases = caption.payload.baseEntityIds;
|
|
2139
|
-
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 === "clip-marker" && r.endpoint0EntityId === usage.clipEntityId);
|
|
2141
|
-
const markerContents = rows.relations.filter((r) => r.relationKind === "marker-content" && r.endpoint0EntityId === usage.markerEntityId);
|
|
2142
|
-
const markerOwners = rows.relations.filter((r) => (r.relationKind === "clip-marker" || r.relationKind === "axvideo-marker") && r.endpoint1EntityId === usage.markerEntityId);
|
|
2143
|
-
if (clipMarkers.length !== 1 || clipMarkers[0].endpoint1EntityId !== usage.markerEntityId || markerOwners.length !== 1 || markerContents.length !== 1 || markerContents[0].endpoint1EntityId !== usage.captionEntityId) throw new Error("Caption usage conflict");
|
|
2144
|
-
const newCaptionId = createEntityId(`entity_caption_revision_${id}_${index}`);
|
|
2145
|
-
const clone = {
|
|
2146
|
-
...caption,
|
|
2147
|
-
entityId: newCaptionId,
|
|
2148
|
-
payload: {
|
|
2149
|
-
...structuredClone(caption.payload),
|
|
2150
|
-
baseEntityIds: bases.map((base) => base === source.entityId ? audioScriptEntityId : base),
|
|
2151
|
-
...usage.selection === void 0 ? {} : { selection: structuredClone(usage.selection) }
|
|
2152
|
-
}
|
|
2153
|
-
};
|
|
2154
|
-
entities.push(clone);
|
|
2155
|
-
assembleCaptionContent({
|
|
2156
|
-
entities,
|
|
2157
|
-
relations
|
|
2158
|
-
}, newCaptionId);
|
|
2159
|
-
relations = relations.filter((r) => r.relationId !== markerContents[0].relationId);
|
|
2160
|
-
relations.push({
|
|
2161
|
-
...markerContents[0],
|
|
2162
|
-
relationId: createRelationId(`relation_caption_revision_${id}_${index}`),
|
|
2163
|
-
endpoint1EntityId: newCaptionId,
|
|
2164
|
-
trace: {
|
|
2165
|
-
command: "revise-audio-script",
|
|
2166
|
-
editId: input.editId,
|
|
2167
|
-
usageIndex: index
|
|
2168
|
-
}
|
|
2169
|
-
});
|
|
2170
|
-
captionUsages.push({
|
|
2171
|
-
clipEntityId: usage.clipEntityId,
|
|
2172
|
-
markerEntityId: usage.markerEntityId,
|
|
2173
|
-
previousCaptionEntityId: usage.captionEntityId,
|
|
2174
|
-
captionEntityId: newCaptionId
|
|
2175
|
-
});
|
|
2176
|
-
}
|
|
2177
|
-
const revised = {
|
|
2178
|
-
entities,
|
|
2179
|
-
relations
|
|
2180
|
-
};
|
|
2181
|
-
decodeEntityRelationRows(revised, { compareMarkerPoints: (_marker, _range, left, right) => {
|
|
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);
|
|
2186
|
-
return {
|
|
2187
|
-
rows: revised,
|
|
2188
|
-
audioScriptEntityId,
|
|
2189
|
-
captionUsages,
|
|
2190
|
-
changed: true
|
|
2191
|
-
};
|
|
2192
|
-
}
|
|
2193
|
-
function hash(value) {
|
|
2194
|
-
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
|
|
2195
|
-
}
|
|
2196
1914
|
//#endregion
|
|
2197
1915
|
//#region src/entity-editor/legacy-entity-migration.ts
|
|
2198
1916
|
/** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
|
|
@@ -2264,7 +1982,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2264
1982
|
relations
|
|
2265
1983
|
}, {
|
|
2266
1984
|
assetId: speech.origin_speech_id,
|
|
2267
|
-
kind: "voiceover",
|
|
1985
|
+
kind: "voiceover-recording",
|
|
2268
1986
|
durationMs: speech.media_duration_ms,
|
|
2269
1987
|
storageKey: speech.audio_storage_key,
|
|
2270
1988
|
voice: {
|
|
@@ -2302,7 +2020,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2302
2020
|
...oldTrack.is_hidden === void 0 ? {} : { hidden: oldTrack.is_hidden }
|
|
2303
2021
|
}, oldTrack.id);
|
|
2304
2022
|
trackIds.set(oldTrack, track);
|
|
2305
|
-
link("
|
|
2023
|
+
link("contain", timeline, track);
|
|
2306
2024
|
}
|
|
2307
2025
|
for (const oldTrack of legacy.tracks ?? []) {
|
|
2308
2026
|
const track = trackIds.get(oldTrack);
|
|
@@ -2312,7 +2030,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2312
2030
|
const volume = part.video_clip?.volume ?? part.speech?.volume ?? part.bgm?.volume;
|
|
2313
2031
|
const clip = addEntity("clip", {
|
|
2314
2032
|
...volume === void 0 ? {} : { volume },
|
|
2315
|
-
|
|
2033
|
+
order: role === "bgm" ? 0 : order
|
|
2316
2034
|
}, item.part_id);
|
|
2317
2035
|
let content = contentByPartId.get(item.part_id);
|
|
2318
2036
|
let duration;
|
|
@@ -2323,37 +2041,21 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2323
2041
|
const fact = facts.get(video.origin_media_id);
|
|
2324
2042
|
const rate = speedOf(video.speed_shift);
|
|
2325
2043
|
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
|
-
} } : {}
|
|
2044
|
+
inMs: video.play_in,
|
|
2045
|
+
outMs: video.play_out,
|
|
2046
|
+
...(fact.kind === "image" || fact.kind === "video") && video.speed_shift !== void 0 ? { rate } : {}
|
|
2339
2047
|
};
|
|
2340
2048
|
} else if (part.speech !== void 0) {
|
|
2341
2049
|
duration = part.speech.media_duration_ms;
|
|
2342
2050
|
markerPayload = {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
end: duration
|
|
2346
|
-
},
|
|
2347
|
-
duration: {
|
|
2348
|
-
mode: "fixed",
|
|
2349
|
-
value: duration
|
|
2350
|
-
}
|
|
2051
|
+
inMs: 0,
|
|
2052
|
+
outMs: duration
|
|
2351
2053
|
};
|
|
2352
2054
|
} else if (part.caption !== void 0) {
|
|
2353
2055
|
const caption = part.caption;
|
|
2354
2056
|
duration = caption.initial_duration_ms;
|
|
2355
2057
|
const voiceover = contentByPartId.get(caption.speech_part_id);
|
|
2356
|
-
const render = relations.find((relation) => relation.relationKind === "
|
|
2058
|
+
const render = relations.find((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceover);
|
|
2357
2059
|
const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
|
|
2358
2060
|
const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
|
|
2359
2061
|
if (script === void 0) fail("Legacy Caption has no composed AudioScript");
|
|
@@ -2374,15 +2076,13 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2374
2076
|
selection,
|
|
2375
2077
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
2376
2078
|
});
|
|
2079
|
+
link("caption-alignment", content, voiceover, { alignment: {
|
|
2080
|
+
start: caption.start_ms,
|
|
2081
|
+
end: caption.start_ms + duration
|
|
2082
|
+
} });
|
|
2377
2083
|
markerPayload = {
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
end: duration
|
|
2381
|
-
},
|
|
2382
|
-
duration: {
|
|
2383
|
-
mode: "fixed",
|
|
2384
|
-
value: duration
|
|
2385
|
-
}
|
|
2084
|
+
inMs: 0,
|
|
2085
|
+
outMs: duration
|
|
2386
2086
|
};
|
|
2387
2087
|
} else {
|
|
2388
2088
|
const bgm = part.bgm;
|
|
@@ -2390,35 +2090,25 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
2390
2090
|
if (fact.kind !== "audio") fail(`Legacy BGM ${item.part_id} requires an audio fact`);
|
|
2391
2091
|
duration = fact.durationMs;
|
|
2392
2092
|
markerPayload = {
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
},
|
|
2397
|
-
duration: { mode: "from-source" },
|
|
2398
|
-
durationPolicy: "timeline"
|
|
2093
|
+
inMs: 0,
|
|
2094
|
+
outMs: duration,
|
|
2095
|
+
fillTimeline: true
|
|
2399
2096
|
};
|
|
2400
2097
|
}
|
|
2401
2098
|
if (content === void 0) fail(`Migration could not resolve content for ${item.part_id}`);
|
|
2402
2099
|
const normalizedMainRange = role === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2403
2100
|
if (normalizedMainRange !== void 0) markerPayload = {
|
|
2404
2101
|
...markerPayload,
|
|
2405
|
-
|
|
2102
|
+
startMs: normalizedMainRange.start
|
|
2406
2103
|
};
|
|
2407
|
-
else if (item.time_position.mode === "absolute" && role !== "bgm") markerPayload = {
|
|
2104
|
+
else if (item.time_position.mode === "absolute" && role !== "bgm" || item.time_position.mode === "anchored") markerPayload = {
|
|
2408
2105
|
...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
|
|
2106
|
+
startMs: item.time_position.offsetMs
|
|
2417
2107
|
};
|
|
2418
2108
|
const marker = addEntity("sequence-marker", markerPayload);
|
|
2419
|
-
link("
|
|
2420
|
-
link("
|
|
2421
|
-
link("
|
|
2109
|
+
link("contain", track, clip);
|
|
2110
|
+
link("marker", clip, marker);
|
|
2111
|
+
link("contain", clip, content);
|
|
2422
2112
|
if (item.time_position.mode === "anchored") link("clip-anchor", clip, item.time_position.anchorPartId);
|
|
2423
2113
|
}
|
|
2424
2114
|
}
|
|
@@ -2439,7 +2129,8 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2439
2129
|
for (const part of Object.values(view.part_library ?? {})) {
|
|
2440
2130
|
const speech = part.speech;
|
|
2441
2131
|
if (speech?.phoneme_script === null) delete speech.phoneme_script;
|
|
2442
|
-
|
|
2132
|
+
const speed = part.video_clip?.speed_shift;
|
|
2133
|
+
if (speed !== void 0) delete speed.mode;
|
|
2443
2134
|
}
|
|
2444
2135
|
return { view };
|
|
2445
2136
|
};
|
|
@@ -2455,22 +2146,21 @@ function assertEntityProjectionPreservesLegacy(legacy, projection, rows) {
|
|
|
2455
2146
|
const sequentialOrders = [];
|
|
2456
2147
|
for (const item of oldTrack.items ?? []) {
|
|
2457
2148
|
const clip = rows.entities.find((row) => row.entityId === item.part_id && row.entityKind === "clip");
|
|
2458
|
-
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"));
|
|
2459
2150
|
const marker = rows.entities.find((row) => row.entityId === binding[0]?.endpoint1EntityId && row.entityKind === "sequence-marker");
|
|
2460
2151
|
if (clip === void 0 || binding.length !== 1 || marker === void 0) fail("Migration must preserve Clip/Marker placement identity");
|
|
2461
2152
|
const anchors = rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
2462
2153
|
const normalizedMainRange = oldTrack.parts_kind === "video_clip" ? normalizedMainRanges?.get(item.part_id) : void 0;
|
|
2463
2154
|
if (normalizedMainRange !== void 0) {
|
|
2464
|
-
if (
|
|
2155
|
+
if (marker.payload.startMs !== normalizedMainRange.start || anchors.length !== 0) fail("Migration must preserve the legacy overlap-normalized main Track");
|
|
2465
2156
|
} else if (oldTrack.parts_kind === "bgm") {
|
|
2466
|
-
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");
|
|
2467
2158
|
} else if (item.time_position.mode === "sequential") {
|
|
2468
|
-
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");
|
|
2469
2160
|
sequentialOrders.push(clip.payload.order);
|
|
2470
2161
|
} 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");
|
|
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");
|
|
2474
2164
|
}
|
|
2475
2165
|
if (sequentialOrders.some((order, index) => index > 0 && order <= sequentialOrders[index - 1])) fail("Migration must preserve sequential Clip order");
|
|
2476
2166
|
}
|
|
@@ -2711,7 +2401,7 @@ function requirePart(document, partId) {
|
|
|
2711
2401
|
return part;
|
|
2712
2402
|
}
|
|
2713
2403
|
function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phonemeScript, entities, relations, addEntity, link, documentAudioScriptEntityId) {
|
|
2714
|
-
const renders = relations.filter((relation) => relation.relationKind === "
|
|
2404
|
+
const renders = relations.filter((relation) => relation.relationKind === "derived-from" && relation.endpoint0EntityId === voiceoverAudioEntityId);
|
|
2715
2405
|
if (renders.length > 1) fail(`Voiceover ${voiceoverAudioEntityId} has ambiguous PhoneticScript provenance`);
|
|
2716
2406
|
if (renders.length === 1) {
|
|
2717
2407
|
const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
|
|
@@ -2739,7 +2429,7 @@ function ensureSpeechPhoneticChain(voiceoverAudioEntityId, audioScriptText, phon
|
|
|
2739
2429
|
baseEntityIds: [script.entityId],
|
|
2740
2430
|
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
2741
2431
|
});
|
|
2742
|
-
link("
|
|
2432
|
+
link("derived-from", voiceoverAudioEntityId, phonetic);
|
|
2743
2433
|
return phonetic;
|
|
2744
2434
|
}
|
|
2745
2435
|
function composedAudioScript(phonetic, entities) {
|
|
@@ -2801,12 +2491,6 @@ function assertCaptionStyle(style) {
|
|
|
2801
2491
|
"position_y"
|
|
2802
2492
|
], "Caption style");
|
|
2803
2493
|
}
|
|
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
2494
|
function requireFinite(value, label) {
|
|
2811
2495
|
if (typeof value !== "number" || !Number.isFinite(value)) fail(`${label} must be finite`);
|
|
2812
2496
|
return value;
|
|
@@ -5314,4 +4998,4 @@ function serverCovers(serverVV, target) {
|
|
|
5314
4998
|
return covers(server, target);
|
|
5315
4999
|
}
|
|
5316
5000
|
//#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,
|
|
5001
|
+
export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|