@mengine/medeo-client 2.0.1-alpha.21 → 2.0.1-alpha.23
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-BXQcpnOg.js → document-7Vi1AeQc.js} +130 -59
- package/dist/{index-CzDNr1Tb.d.ts → index-mGObzOh4.d.ts} +340 -75
- package/dist/index.d.ts +213 -413
- package/dist/index.js +231 -1617
- 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 VideoDocumentValidationError, A as resolveEntityTimelineLayout, B as selectAudioScriptSegment, C as canonicalJson, Ct as effectiveVideoClipDurationMs, D as resolveFieldPath, Dt as videoDocumentMirrorSchema, E as assertFieldChanges, Et as recordEntries, F as assembleCaptionContent, G as createRelationId, H as variantBaseEntityIds, I as assembleEntityContent, J as buildInitialVideoDocument, K as hasSequence, L as assemblePhoneticScriptContent, M as validateEntityRelationTransition, N as decodeEntityRelationRows, O as projectEntityTimeline, Ot as base64ToBytes, P as ScriptCompositionError, Q as toVideoDocument, R as assembleScriptText, S as scriptSourceAssetOf, St as DEFAULT_UNIT_TIME_MS, T as applyFieldChanges, Tt as partUnionToDraft, U as isJsonObject, V as updateEntityFields, W as createEntityId, X as derivePositionFromAbs, Y as buildSpeechHostMap, Z as fromVideoDocument, _ as audioScriptAssetHash, _t as syncAggregatedClipsTimePosition, a as createMirrorVideoDocument, at as ensureLaneTrack, b as phoneticScriptAssetHash, bt as partDurationMs, c as DocumentMutationGuard, ct as laneTrackId, d as LoroEntityDraft, dt as arrangeMainTrackSeamlessly, et as assertValidVideoDocument, f as readEntityDocument, ft as fillMainTrackTimeGaps, g as audioScriptAssetFields, gt as resolveSpeechOverlapByShiftingVideos, h as audioScriptAssetContent, ht as resolveAllSpeechOverlaps, i as MirrorVideoDocumentAdapter, it as LANE_KINDS_IN_STACK_ORDER, j as EntityTimelineProjectionError, k as EntityProjectionGraph, kt as bytesToBase64, l as LORO_ENTITY_SCHEMA, lt as solveVideoDocument, m as entityOrderGroups, mt as recalculateTimelineDuration, n as createPlainMemoryAdapter, nt as partUnionSchema, o as createMirrorVideoDocumentAdapter, ot as findLaneTrack, p as readEntityRelationRecords, pt as reassignSpeechesToVideoClipsByTime, q as isMediaAssetVariantKind, r as generatePartId, rt as videoDocumentSchema, s as readVideoDocumentFromDraft, st as hasResolvedPlacement, t as PlainMemoryAdapter, tt as validateVideoDocument, u as LoroEntityDocument, ut as cascadeAfterVideoClipChanges, v as audioScriptAssetOf, vt as TIMELINE_SKELETON_DURATION_MS, w as equalJson, wt as speedOf, x as phoneticScriptAssetOf, xt as safeDurationMs, y as phoneticScriptAssetContent, yt as isEmptyVideoClip, z as findComposedAudioScript } from "./document-7Vi1AeQc.js";
|
|
2
2
|
import { addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, t as schemas_exports } from "./schemas.js";
|
|
3
3
|
import { LoroDoc, UndoManager, VersionVector } from "loro-crdt";
|
|
4
4
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -230,6 +230,7 @@ function applyEntityRows(editor, rows) {
|
|
|
230
230
|
const oldRelations = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
231
231
|
const newRelations = new Map(rows.relations.map((row) => [row.relationId, row]));
|
|
232
232
|
if (newEntities.size !== rows.entities.length || newRelations.size !== rows.relations.length) throw new Error("Duplicate domain identity");
|
|
233
|
+
validateEntityRelationTransition(before, rows);
|
|
233
234
|
return {
|
|
234
235
|
update: editor.transact((draft) => {
|
|
235
236
|
for (const row of before.relations) {
|
|
@@ -237,7 +238,7 @@ function applyEntityRows(editor, rows) {
|
|
|
237
238
|
if (!next) draft.unlink(row.relationId);
|
|
238
239
|
else {
|
|
239
240
|
if (next.relationKind !== row.relationKind || next.endpoint0EntityId !== row.endpoint0EntityId || next.endpoint1EntityId !== row.endpoint1EntityId) throw new Error("Relation identity cannot change kind or endpoint positions");
|
|
240
|
-
if (!equalJson(row.
|
|
241
|
+
if (!equalJson(row.trace, next.trace)) draft.patchRelationTrace(row.relationId, next.trace);
|
|
241
242
|
}
|
|
242
243
|
}
|
|
243
244
|
let attached = baseline.project.audioScriptEntityId !== void 0;
|
|
@@ -254,7 +255,22 @@ function applyEntityRows(editor, rows) {
|
|
|
254
255
|
if (!equalJson(previous.payload, row.payload)) draft.replaceOwned(row.entityId, row.payload);
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
for (const row of rows.relations)
|
|
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
|
+
}
|
|
258
274
|
const beforeOrder = entityOrderGroups(before), afterOrder = entityOrderGroups(rows);
|
|
259
275
|
for (const group of new Set([...beforeOrder.keys(), ...afterOrder.keys()])) {
|
|
260
276
|
const next = afterOrder.get(group) ?? [];
|
|
@@ -556,30 +572,6 @@ const numericCoordinates = { compareMarkerPoints: (_marker, _range, left, right)
|
|
|
556
572
|
if (typeof left !== "number" || typeof right !== "number" || !Number.isFinite(left) || !Number.isFinite(right)) throw new Error("Media editor requires numeric coordinates");
|
|
557
573
|
return left - right;
|
|
558
574
|
} };
|
|
559
|
-
/** Import only the fixed ASR artifact; Caption entities are authored separately. */
|
|
560
|
-
function importCaptionAsset(initial, fact, idFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()}`) {
|
|
561
|
-
if (!fact.assetId || fact.assetId.trim() !== fact.assetId) throw new Error("Expected a caption asset id");
|
|
562
|
-
if (fact.storageKey !== void 0 && (!fact.storageKey || fact.storageKey.trim() !== fact.storageKey)) throw new Error("Caption asset storage key must be non-empty and trimmed");
|
|
563
|
-
decodeEntityRelationRows(initial, numericCoordinates);
|
|
564
|
-
assertCanonicalEditorResources(initial);
|
|
565
|
-
const entities = structuredClone([...initial.entities]);
|
|
566
|
-
const relations = structuredClone([...initial.relations]);
|
|
567
|
-
const asset = ensureAsset(entities, relations, {
|
|
568
|
-
system: "memota",
|
|
569
|
-
key: fact.assetId,
|
|
570
|
-
...fact.storageKey === void 0 ? {} : { storageKey: fact.storageKey }
|
|
571
|
-
}, idFactory);
|
|
572
|
-
const rows = {
|
|
573
|
-
entities,
|
|
574
|
-
relations
|
|
575
|
-
};
|
|
576
|
-
decodeEntityRelationRows(rows, numericCoordinates);
|
|
577
|
-
assertCanonicalEditorResources(rows);
|
|
578
|
-
return {
|
|
579
|
-
rows,
|
|
580
|
-
assetEntityId: asset.entityId
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
575
|
/**
|
|
584
576
|
* Import at the editor boundary, not at asset generation time. Existing
|
|
585
577
|
* provenance is retained.
|
|
@@ -607,7 +599,7 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
607
599
|
const entityKind = fact.kind;
|
|
608
600
|
const timbre = fact.kind === "voiceover" ? fact.voice : void 0;
|
|
609
601
|
const durationMs = fact.kind === "image" ? null : fact.durationMs;
|
|
610
|
-
const asset = ensureAsset(entities,
|
|
602
|
+
const asset = ensureAsset(entities, {
|
|
611
603
|
system,
|
|
612
604
|
key: fact.assetId,
|
|
613
605
|
storageKey: fact.storageKey
|
|
@@ -663,31 +655,20 @@ function importMediaAsset(initial, fact, idFactory = (prefix) => `${prefix}_${gl
|
|
|
663
655
|
};
|
|
664
656
|
}
|
|
665
657
|
/** One Asset per `(system, key)`: the bytes are named in exactly one row. */
|
|
666
|
-
function ensureAsset(entities,
|
|
658
|
+
function ensureAsset(entities, locator, idFactory) {
|
|
667
659
|
const found = entities.find((row) => row.entityKind === "asset" && row.payload.system === locator.system && row.payload.key === locator.key);
|
|
668
660
|
if (found !== void 0) {
|
|
669
661
|
if (locator.storageKey === void 0 || found.payload.storageKey === locator.storageKey) return found;
|
|
670
662
|
if (found.payload.storageKey !== void 0) throw new Error(`Media facts conflict with existing storage key for ${locator.key}`);
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
entityKind: "asset",
|
|
663
|
+
const filled = {
|
|
664
|
+
...found,
|
|
674
665
|
payload: {
|
|
675
666
|
...found.payload,
|
|
676
667
|
storageKey: locator.storageKey
|
|
677
668
|
}
|
|
678
669
|
};
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const replaced = new Set(bindings.map((relation) => relation.relationId));
|
|
682
|
-
const nextBindings = bindings.map((relation) => ({
|
|
683
|
-
...relation,
|
|
684
|
-
relationId: createRelationId(idFactory("relation")),
|
|
685
|
-
endpoint0EntityId: relation.endpoint0EntityId === found.entityId ? replacement.entityId : relation.endpoint0EntityId,
|
|
686
|
-
endpoint1EntityId: relation.endpoint1EntityId === found.entityId ? replacement.entityId : relation.endpoint1EntityId
|
|
687
|
-
}));
|
|
688
|
-
relations.splice(0, relations.length, ...relations.filter((relation) => !replaced.has(relation.relationId)), ...nextBindings);
|
|
689
|
-
replaceEntity$1(entities, found.entityId, replacement);
|
|
690
|
-
return replacement;
|
|
670
|
+
replaceEntity$1(entities, found.entityId, filled);
|
|
671
|
+
return filled;
|
|
691
672
|
}
|
|
692
673
|
const asset = {
|
|
693
674
|
entityId: createEntityId(idFactory("entity")),
|
|
@@ -796,7 +777,7 @@ const defaultIdFactory = (prefix) => `${prefix}_${globalThis.crypto.randomUUID()
|
|
|
796
777
|
* Each mutation is applied to a detached graph, validated as a complete DSL set,
|
|
797
778
|
* and committed only after validation succeeds.
|
|
798
779
|
*/
|
|
799
|
-
var EntityTimelineEditor = class
|
|
780
|
+
var EntityTimelineEditor = class {
|
|
800
781
|
idFactory;
|
|
801
782
|
state;
|
|
802
783
|
constructor(initialRows, idFactory = defaultIdFactory) {
|
|
@@ -1107,65 +1088,49 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
1107
1088
|
});
|
|
1108
1089
|
}
|
|
1109
1090
|
setClipSpeed(input) {
|
|
1110
|
-
this.transact((draft) => this.setClipSpeedIntoDraft(draft, input));
|
|
1111
|
-
}
|
|
1112
|
-
/** Apply one typed Director speed effect as a single graph transaction. */
|
|
1113
|
-
setVideoClipSpeeds(input) {
|
|
1114
|
-
if (input.clips.length === 0) throw new Error("At least one Video Clip speed is required");
|
|
1115
|
-
if (new Set(input.clips.map((clip) => clip.videoClipEntityId)).size !== input.clips.length) throw new Error("Video Clip speed targets must be unique");
|
|
1116
1091
|
this.transact((draft) => {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
setClipSpeedIntoDraft(draft, input) {
|
|
1131
|
-
const topology = requireCanonicalClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1132
|
-
const content = requireSequenceContent(draft, topology.markerContentRelation.endpoint1EntityId);
|
|
1133
|
-
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
|
|
1134
|
-
const initialPayload = cloneJsonObject(topology.marker.payload, "Sequence Marker payload");
|
|
1135
|
-
const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
|
|
1136
|
-
if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
|
|
1137
|
-
const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1138
|
-
let sourceRange = priorSource;
|
|
1139
|
-
if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
|
|
1140
|
-
start: 0,
|
|
1141
|
-
end: markerDuration(initialPayload, priorSource)
|
|
1142
|
-
};
|
|
1143
|
-
if (input.timeRemapping === null) {
|
|
1144
|
-
delete payload.timeRemapping;
|
|
1145
|
-
payload.sourceRange = sourceRange;
|
|
1146
|
-
payload.duration = {
|
|
1147
|
-
mode: "fixed",
|
|
1148
|
-
value: sourceRange.end - sourceRange.start
|
|
1149
|
-
};
|
|
1150
|
-
} else {
|
|
1151
|
-
const rate = finitePositive(input.timeRemapping.rate, "linear speed rate");
|
|
1152
|
-
const duration = Math.round((sourceRange.end - sourceRange.start) / rate);
|
|
1153
|
-
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
|
|
1154
|
-
payload.sourceRange = sourceRange;
|
|
1155
|
-
payload.duration = {
|
|
1156
|
-
mode: "fixed",
|
|
1157
|
-
value: duration
|
|
1158
|
-
};
|
|
1159
|
-
payload.timeRemapping = {
|
|
1160
|
-
kind: "linear",
|
|
1161
|
-
rate,
|
|
1162
|
-
...input.timeRemapping.mode === void 0 ? {} : { mode: input.timeRemapping.mode }
|
|
1092
|
+
const initialTopology = requireClipOnRole(draft, input.clipEntityId, "video_clip");
|
|
1093
|
+
const content = requireSequenceContent(draft, otherEndpoint(initialTopology.markerContentRelation, initialTopology.marker.entityId));
|
|
1094
|
+
if (content.entityKind !== "video" && content.entityKind !== "image") throw new Error("Clip speed is supported only for Video or Image content");
|
|
1095
|
+
const initialPayload = cloneJsonObject(initialTopology.marker.payload, "Sequence Marker payload");
|
|
1096
|
+
const priorSource = requirePositiveRange(initialPayload.sourceRange, "sourceRange");
|
|
1097
|
+
if (input.timeRemapping === null && initialPayload.timeRemapping === void 0) return;
|
|
1098
|
+
this.normalizeMainSequential(draft);
|
|
1099
|
+
const topology = requireClipTopology(draft, input.clipEntityId);
|
|
1100
|
+
const payload = { ...cloneJsonObject(topology.marker.payload, "Sequence Marker payload") };
|
|
1101
|
+
let sourceRange = priorSource;
|
|
1102
|
+
if (content.entityKind === "image" && initialPayload.timeRemapping === void 0 && input.timeRemapping !== null) sourceRange = {
|
|
1103
|
+
start: 0,
|
|
1104
|
+
end: markerDuration(initialPayload, priorSource)
|
|
1163
1105
|
};
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1106
|
+
if (input.timeRemapping === null) {
|
|
1107
|
+
delete payload.timeRemapping;
|
|
1108
|
+
payload.sourceRange = sourceRange;
|
|
1109
|
+
payload.duration = {
|
|
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);
|
|
1116
|
+
if (duration <= 0 || !Number.isSafeInteger(duration)) throw new Error("Linear speed produces an invalid duration");
|
|
1117
|
+
payload.sourceRange = sourceRange;
|
|
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
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
resizeAbsoluteTarget(payload, markerDuration(payload, sourceRange));
|
|
1129
|
+
replaceEntity(draft, topology.marker.entityId, {
|
|
1130
|
+
...topology.marker,
|
|
1131
|
+
payload
|
|
1132
|
+
});
|
|
1133
|
+
this.resolveOneVoiceoverOverlap(draft);
|
|
1169
1134
|
});
|
|
1170
1135
|
}
|
|
1171
1136
|
trimClip(input) {
|
|
@@ -1371,7 +1336,7 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
1371
1336
|
}
|
|
1372
1337
|
});
|
|
1373
1338
|
}
|
|
1374
|
-
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1339
|
+
this.reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phonetic.entityId, input.captions);
|
|
1375
1340
|
this.resolveOneVoiceoverOverlap(draft);
|
|
1376
1341
|
return {
|
|
1377
1342
|
voiceoverClipEntityId,
|
|
@@ -1515,481 +1480,12 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
1515
1480
|
});
|
|
1516
1481
|
});
|
|
1517
1482
|
}
|
|
1518
|
-
#applyCaptionAssets(envelope) {
|
|
1519
|
-
return this.transact((draft) => {
|
|
1520
|
-
const input = envelope.input;
|
|
1521
|
-
const effectId = requireTrimmed(input.effectId, "Caption effect id");
|
|
1522
|
-
const identityScope = stableEffectScope(captionAssetsCommand, effectId, envelope.baselineFingerprint);
|
|
1523
|
-
const segments = validateAsrSegments(input.segments);
|
|
1524
|
-
const source = requireEntityKind(draft, input.source.entityId, input.source.entityKind);
|
|
1525
|
-
const track = requireCanonicalCaptionTrack(draft, input.timelineEntityId, input.trackEntityId);
|
|
1526
|
-
const signature = envelope.signature;
|
|
1527
|
-
const replay = resolveCaptionAssetsReplay(draft, input, effectId, signature, segments, source.entityId, track.entityId);
|
|
1528
|
-
if (replay !== void 0) return replay;
|
|
1529
|
-
const imported = importCaptionAsset(draft, input.captionAsset, stableEffectIdFactory(identityScope, "caption-asset"));
|
|
1530
|
-
if (imported.assetEntityId !== envelope.decisions.captionAsset.resultAssetEntityId) throw new Error("Frozen Caption Asset decision no longer matches the causal baseline");
|
|
1531
|
-
replaceRows(draft, imported.rows);
|
|
1532
|
-
const scriptId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, "audio-script"));
|
|
1533
|
-
const scriptMarkerId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, "audio-script-marker"));
|
|
1534
|
-
const asrStartMs = Math.min(...segments.map((segment) => segment.startMs));
|
|
1535
|
-
const asrEndMs = Math.max(...segments.map((segment) => segment.endMs));
|
|
1536
|
-
draft.entities.push({
|
|
1537
|
-
entityId: scriptId,
|
|
1538
|
-
entityKind: "audio-script",
|
|
1539
|
-
payload: { segments: segments.map(({ segmentId, text, language }) => ({
|
|
1540
|
-
segmentId,
|
|
1541
|
-
text,
|
|
1542
|
-
...language === void 0 ? {} : { language }
|
|
1543
|
-
})) }
|
|
1544
|
-
});
|
|
1545
|
-
draft.entities.push({
|
|
1546
|
-
entityId: scriptMarkerId,
|
|
1547
|
-
entityKind: "sequence-marker",
|
|
1548
|
-
payload: {
|
|
1549
|
-
sourceRange: {
|
|
1550
|
-
start: asrStartMs,
|
|
1551
|
-
end: asrEndMs
|
|
1552
|
-
},
|
|
1553
|
-
duration: {
|
|
1554
|
-
mode: "fixed",
|
|
1555
|
-
value: asrEndMs - asrStartMs
|
|
1556
|
-
},
|
|
1557
|
-
segmentRanges: segments.map(({ segmentId, startMs, endMs }) => ({
|
|
1558
|
-
segmentId,
|
|
1559
|
-
startMs,
|
|
1560
|
-
endMs
|
|
1561
|
-
}))
|
|
1562
|
-
}
|
|
1563
|
-
});
|
|
1564
|
-
draft.relations.push(this.createRelation(draft, "audio-script-source", scriptId, source.entityId, {}, effectTrace(effectId, signature), stableEffectId("relation", identityScope, "audio-script-source")), this.createRelation(draft, "audio-script-marker", scriptId, scriptMarkerId, {}, effectTrace(effectId, signature), stableEffectId("relation", identityScope, "audio-script-marker")));
|
|
1565
|
-
const captionEntityIds = [];
|
|
1566
|
-
const captionClipEntityIds = [];
|
|
1567
|
-
for (const [index, segment] of segments.entries()) {
|
|
1568
|
-
const durationMs = segment.endMs - segment.startMs;
|
|
1569
|
-
const target = envelope.decisions.targetRanges[index];
|
|
1570
|
-
if (target?.segmentId !== segment.segmentId) throw new Error("Frozen Caption placement order is invalid");
|
|
1571
|
-
const targetRange = {
|
|
1572
|
-
start: target.start,
|
|
1573
|
-
end: target.end
|
|
1574
|
-
};
|
|
1575
|
-
const captionId = requireAvailableEntityId(draft, stableEffectId("entity", identityScope, `caption:${segment.segmentId}`));
|
|
1576
|
-
draft.entities.push({
|
|
1577
|
-
entityId: captionId,
|
|
1578
|
-
entityKind: "caption",
|
|
1579
|
-
payload: {
|
|
1580
|
-
durationMs,
|
|
1581
|
-
baseEntityIds: [scriptId],
|
|
1582
|
-
selection: { segmentId: segment.segmentId }
|
|
1583
|
-
}
|
|
1584
|
-
});
|
|
1585
|
-
const trace = effectTrace(effectId, signature, segment.segmentId);
|
|
1586
|
-
draft.relations.push(this.createRelation(draft, "from-asset", captionId, createEntityId(imported.assetEntityId), {}, trace, stableEffectId("relation", identityScope, `caption-asset:${segment.segmentId}`)));
|
|
1587
|
-
const clipId = this.insertPlacedClipIntoDraft(draft, {
|
|
1588
|
-
clipEntityId: stableEffectId("entity", identityScope, `caption-clip:${segment.segmentId}`),
|
|
1589
|
-
markerEntityId: stableEffectId("entity", identityScope, `caption-marker:${segment.segmentId}`),
|
|
1590
|
-
trackEntityId: track.entityId,
|
|
1591
|
-
contentEntityId: captionId,
|
|
1592
|
-
sourceRange: {
|
|
1593
|
-
start: 0,
|
|
1594
|
-
end: durationMs
|
|
1595
|
-
},
|
|
1596
|
-
duration: {
|
|
1597
|
-
mode: "fixed",
|
|
1598
|
-
value: targetRange.end - targetRange.start
|
|
1599
|
-
},
|
|
1600
|
-
placement: {
|
|
1601
|
-
kind: "absolute",
|
|
1602
|
-
targetRange
|
|
1603
|
-
},
|
|
1604
|
-
relationTrace: trace,
|
|
1605
|
-
relationIds: {
|
|
1606
|
-
trackClip: stableEffectId("relation", identityScope, `track-clip:${segment.segmentId}`),
|
|
1607
|
-
clipMarker: stableEffectId("relation", identityScope, `clip-marker:${segment.segmentId}`),
|
|
1608
|
-
markerContent: stableEffectId("relation", identityScope, `marker-content:${segment.segmentId}`)
|
|
1609
|
-
}
|
|
1610
|
-
});
|
|
1611
|
-
captionEntityIds.push(captionId);
|
|
1612
|
-
captionClipEntityIds.push(clipId);
|
|
1613
|
-
}
|
|
1614
|
-
return {
|
|
1615
|
-
audioScriptEntityId: scriptId,
|
|
1616
|
-
audioScriptMarkerEntityId: scriptMarkerId,
|
|
1617
|
-
captionAssetEntityId: imported.assetEntityId,
|
|
1618
|
-
captionEntityIds,
|
|
1619
|
-
captionClipEntityIds
|
|
1620
|
-
};
|
|
1621
|
-
});
|
|
1622
|
-
}
|
|
1623
|
-
static compileCaptionAssetsEffect(base, envelope) {
|
|
1624
|
-
assertCaptionAssetsEnvelope(envelope);
|
|
1625
|
-
const input = envelope.input;
|
|
1626
|
-
const effectId = requireTrimmed(input.effectId, "Caption effect id");
|
|
1627
|
-
const segments = validateAsrSegments(input.segments);
|
|
1628
|
-
if (effectId !== envelope.effectId || captionEffectSignature(input, segments) !== envelope.signature) throw new Error("Caption effect envelope signature does not match its canonical input");
|
|
1629
|
-
const current = cloneRows(base.snapshot().rows);
|
|
1630
|
-
const replay = resolveCaptionAssetsReplay(current, input, effectId, envelope.signature, segments, createEntityId(input.source.entityId), createEntityId(input.trackEntityId));
|
|
1631
|
-
if (replay !== void 0) return {
|
|
1632
|
-
result: replay,
|
|
1633
|
-
replayed: true,
|
|
1634
|
-
rows: current
|
|
1635
|
-
};
|
|
1636
|
-
const fingerprint = captionBaselineFingerprint(base);
|
|
1637
|
-
if (fingerprint !== envelope.baselineFingerprint) throw new Error("Caption effect baseline changed; independently prepared envelopes on divergent baselines are unsupported");
|
|
1638
|
-
if (canonicalJson(resolveCaptionEffectDecisions(current, input, stableEffectScope(captionAssetsCommand, effectId, fingerprint), segments)) !== canonicalJson(envelope.decisions)) throw new Error("Caption effect decisions no longer match the frozen causal baseline");
|
|
1639
|
-
const doc = base.doc.fork();
|
|
1640
|
-
doc.setPeerId(stableEffectPeerId(captionAssetsCommand, effectId, fingerprint));
|
|
1641
|
-
const staged = new LoroEntityDocument(doc);
|
|
1642
|
-
const editor = new EntityTimelineEditor(staged.snapshot().rows, () => {
|
|
1643
|
-
throw new Error("Caption effects must use effect-derived identities");
|
|
1644
|
-
});
|
|
1645
|
-
const result = editor.#applyCaptionAssets(envelope);
|
|
1646
|
-
const applied = applyEntityRows(staged, editor.rows());
|
|
1647
|
-
return {
|
|
1648
|
-
result,
|
|
1649
|
-
replayed: false,
|
|
1650
|
-
update: applied.update,
|
|
1651
|
-
rows: applied.state.rows
|
|
1652
|
-
};
|
|
1653
|
-
}
|
|
1654
|
-
#applyGeneratedVisuals(envelope) {
|
|
1655
|
-
return this.transact((draft) => {
|
|
1656
|
-
const { input } = envelope;
|
|
1657
|
-
const identityScope = stableEffectScope(generatedVisualsCommand, envelope.effectId, envelope.baselineFingerprint);
|
|
1658
|
-
if (canonicalJson(resolveGeneratedVisualDecisions(draft, input, identityScope)) !== canonicalJson(envelope.decisions)) throw new Error("Frozen visual insertion decision no longer matches the causal baseline");
|
|
1659
|
-
ensureEffectRoleTrack(draft, input.timelineEntityId, "video_clip", envelope.decisions.trackEntityId, identityScope);
|
|
1660
|
-
const clipEntityIds = [];
|
|
1661
|
-
const mediaEntityIds = [];
|
|
1662
|
-
for (const [index, clip] of input.clips.entries()) {
|
|
1663
|
-
const imported = importMediaAsset(draft, clip.media, stableEffectIdFactory(identityScope, `visual:${index}:media`));
|
|
1664
|
-
replaceRows(draft, imported.rows);
|
|
1665
|
-
const media = requireSequenceContent(draft, imported.contentEntityId);
|
|
1666
|
-
if (media.entityKind !== "video" && media.entityKind !== "image") throw new Error("Generated visual content must be Video or Image");
|
|
1667
|
-
const durationMs = safeWholeMs(clip.durationMs, "Generated visual durationMs");
|
|
1668
|
-
if (durationMs === 0 || clip.media.kind === "video" && durationMs > clip.media.durationMs) throw new Error("Generated visual duration must fit its immutable media Sequence");
|
|
1669
|
-
const trace = generatedVisualTrace(envelope, index, clip.generationJobId);
|
|
1670
|
-
const clipId = this.insertPlacedClipIntoDraft(draft, {
|
|
1671
|
-
clipEntityId: stableEffectId("entity", identityScope, `visual:${index}:clip`),
|
|
1672
|
-
markerEntityId: stableEffectId("entity", identityScope, `visual:${index}:marker`),
|
|
1673
|
-
trackEntityId: envelope.decisions.trackEntityId,
|
|
1674
|
-
contentEntityId: media.entityId,
|
|
1675
|
-
sourceRange: {
|
|
1676
|
-
start: 0,
|
|
1677
|
-
end: durationMs
|
|
1678
|
-
},
|
|
1679
|
-
duration: {
|
|
1680
|
-
mode: "fixed",
|
|
1681
|
-
value: durationMs
|
|
1682
|
-
},
|
|
1683
|
-
placement: {
|
|
1684
|
-
kind: "sequential",
|
|
1685
|
-
order: envelope.decisions.insertionOrders[index]
|
|
1686
|
-
},
|
|
1687
|
-
...clip.volume === void 0 ? {} : { clipPayload: { volume: clip.volume } },
|
|
1688
|
-
relationTrace: trace,
|
|
1689
|
-
relationIds: {
|
|
1690
|
-
trackClip: stableEffectId("relation", identityScope, `visual:${index}:track-clip`),
|
|
1691
|
-
clipMarker: stableEffectId("relation", identityScope, `visual:${index}:clip-marker`),
|
|
1692
|
-
markerContent: stableEffectId("relation", identityScope, `visual:${index}:marker-content`)
|
|
1693
|
-
}
|
|
1694
|
-
});
|
|
1695
|
-
for (const [sourceIndex, sourceId] of clip.sourceMediaEntityIds.entries()) {
|
|
1696
|
-
const source = requireSequenceContent(draft, sourceId);
|
|
1697
|
-
if (![
|
|
1698
|
-
"video",
|
|
1699
|
-
"image",
|
|
1700
|
-
"audio"
|
|
1701
|
-
].includes(source.entityKind)) throw new Error("Generated visual lineage requires Video, Image, or Audio inputs");
|
|
1702
|
-
draft.relations.push(this.createRelation(draft, "generated", media.entityId, source.entityId, {}, generatedVisualTrace(envelope, index, clip.generationJobId, sourceIndex), stableEffectId("relation", identityScope, `visual:${index}:generated:${sourceIndex}`)));
|
|
1703
|
-
}
|
|
1704
|
-
clipEntityIds.push(clipId);
|
|
1705
|
-
mediaEntityIds.push(media.entityId);
|
|
1706
|
-
}
|
|
1707
|
-
return {
|
|
1708
|
-
clipEntityIds,
|
|
1709
|
-
mediaEntityIds
|
|
1710
|
-
};
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
static compileGeneratedVisualEffect(base, envelope) {
|
|
1714
|
-
assertGeneratedVisualEnvelope(envelope);
|
|
1715
|
-
consumeEffectEnvelope(consumedGeneratedVisualEnvelopes, envelope, "Generated visual");
|
|
1716
|
-
const current = cloneRows(base.snapshot().rows);
|
|
1717
|
-
const replay = resolveGeneratedVisualReplay(current, envelope);
|
|
1718
|
-
if (replay !== void 0) return {
|
|
1719
|
-
result: replay,
|
|
1720
|
-
replayed: true,
|
|
1721
|
-
rows: current
|
|
1722
|
-
};
|
|
1723
|
-
const fingerprint = captionBaselineFingerprint(base);
|
|
1724
|
-
if (fingerprint !== envelope.baselineFingerprint) throw new Error("Generated visual effect baseline changed; prepare a new effect from the authority");
|
|
1725
|
-
if (canonicalJson(resolveGeneratedVisualDecisions(current, envelope.input, stableEffectScope(generatedVisualsCommand, envelope.effectId, envelope.baselineFingerprint))) !== canonicalJson(envelope.decisions)) throw new Error("Generated visual decisions no longer match the frozen causal baseline");
|
|
1726
|
-
const doc = base.doc.fork();
|
|
1727
|
-
doc.setPeerId(stableEffectPeerId(generatedVisualsCommand, envelope.effectId, fingerprint));
|
|
1728
|
-
const staged = new LoroEntityDocument(doc);
|
|
1729
|
-
const editor = new EntityTimelineEditor(staged.snapshot().rows, () => {
|
|
1730
|
-
throw new Error("Generated visual effects must use effect-derived identities");
|
|
1731
|
-
});
|
|
1732
|
-
const result = editor.#applyGeneratedVisuals(envelope);
|
|
1733
|
-
const applied = applyEntityRows(staged, editor.rows());
|
|
1734
|
-
return {
|
|
1735
|
-
result,
|
|
1736
|
-
replayed: false,
|
|
1737
|
-
update: applied.update,
|
|
1738
|
-
rows: applied.state.rows
|
|
1739
|
-
};
|
|
1740
|
-
}
|
|
1741
|
-
#applySpeechAssets(envelope) {
|
|
1742
|
-
return this.transact((draft) => {
|
|
1743
|
-
const { input } = envelope;
|
|
1744
|
-
const value = input.value;
|
|
1745
|
-
const identityScope = stableEffectScope(speechAssetsCommand, envelope.effectId, envelope.baselineFingerprint);
|
|
1746
|
-
const speechTrack = ensureEffectRoleTrack(draft, value.timelineEntityId, "speech", envelope.decisions.speechTrackEntityId, identityScope);
|
|
1747
|
-
const captionTrack = value.items.some((item) => item.captions.length > 0) ? ensureEffectRoleTrack(draft, value.timelineEntityId, "caption", envelope.decisions.captionTrackEntityId, identityScope) : void 0;
|
|
1748
|
-
const speechClipEntityIds = [];
|
|
1749
|
-
const speechAudioEntityIds = [];
|
|
1750
|
-
const audioScriptEntityIds = [];
|
|
1751
|
-
const phoneticScriptEntityIds = [];
|
|
1752
|
-
const captionClipEntityIds = [];
|
|
1753
|
-
for (const [index, item] of value.items.entries()) {
|
|
1754
|
-
const trace = speechEffectTrace(envelope, index);
|
|
1755
|
-
const imported = importMediaAsset(draft, item.renderMode === "synthesized" ? (({ phoneticScriptEntityId: _phoneticScriptEntityId, ...fact }) => fact)(item.media) : item.media, stableEffectIdFactory(identityScope, `speech:${index}:media`));
|
|
1756
|
-
replaceRows(draft, imported.rows);
|
|
1757
|
-
const speech = item.renderMode === "recorded" ? requireEntityKind(draft, imported.contentEntityId, "audio") : requireEntityKind(draft, imported.contentEntityId, "voiceover");
|
|
1758
|
-
const scripts = materializeSpeechSource(draft, item, identityScope, index, trace, speech.entityId);
|
|
1759
|
-
const existingRender = incidentRelations(draft, speech.entityId).filter((relation) => relation.relationKind === "phonetic-script-render");
|
|
1760
|
-
if (item.renderMode === "synthesized") {
|
|
1761
|
-
if (imported.voiceEntityId === void 0 || item.media.voice === void 0) throw new Error("Synthesized speech requires an explicit Voice identity");
|
|
1762
|
-
if (item.media.phoneticScriptEntityId !== void 0 && item.media.phoneticScriptEntityId !== scripts.phonetic.entityId) throw new Error("Synthesized speech media conflicts with its PhoneticScript source");
|
|
1763
|
-
requireSynthesizedSpeechIdentity(draft, requireEntityKind(draft, speech.entityId, "voiceover"), item.media);
|
|
1764
|
-
if (existingRender.length > 1) throw new Error(`Speech Audio "${speech.entityId}" has ambiguous render lineage`);
|
|
1765
|
-
const renderId = stableEffectId("relation", identityScope, `speech:${index}:render`);
|
|
1766
|
-
const renderTrace = {
|
|
1767
|
-
...trace,
|
|
1768
|
-
role: "speech-render"
|
|
1769
|
-
};
|
|
1770
|
-
if (existingRender.length === 1) {
|
|
1771
|
-
if (existingRender[0].endpoint0EntityId !== speech.entityId || existingRender[0].endpoint1EntityId !== scripts.phonetic.entityId) throw new Error(`Speech Audio "${speech.entityId}" has conflicting render lineage`);
|
|
1772
|
-
replaceRelationWithTrace(draft, existingRender[0], speech.entityId, scripts.phonetic.entityId, renderTrace, renderId);
|
|
1773
|
-
} else draft.relations.push(this.createRelation(draft, "phonetic-script-render", speech.entityId, scripts.phonetic.entityId, {}, renderTrace, renderId));
|
|
1774
|
-
} else {
|
|
1775
|
-
const timbre = incidentRelations(draft, speech.entityId).filter((relation) => relation.relationKind === "voice-timbre");
|
|
1776
|
-
if (existingRender.length > 0 || timbre.length > 0) throw new Error("Recorded speech must not have Voice or render lineage");
|
|
1777
|
-
}
|
|
1778
|
-
const startMs = envelope.decisions.targetStartsMs[index];
|
|
1779
|
-
const recordedSpeechClipId = envelope.decisions.recordedSpeechClipEntityIds[index];
|
|
1780
|
-
let speechClipId;
|
|
1781
|
-
if (!("targetSpeechClipEntityId" in item) && recordedSpeechClipId === null) speechClipId = this.insertPlacedClipIntoDraft(draft, {
|
|
1782
|
-
clipEntityId: stableEffectId("entity", identityScope, `speech:${index}:clip`),
|
|
1783
|
-
markerEntityId: stableEffectId("entity", identityScope, `speech:${index}:marker`),
|
|
1784
|
-
trackEntityId: speechTrack.entityId,
|
|
1785
|
-
contentEntityId: speech.entityId,
|
|
1786
|
-
sourceRange: {
|
|
1787
|
-
start: 0,
|
|
1788
|
-
end: item.media.durationMs
|
|
1789
|
-
},
|
|
1790
|
-
duration: {
|
|
1791
|
-
mode: "fixed",
|
|
1792
|
-
value: item.media.durationMs
|
|
1793
|
-
},
|
|
1794
|
-
placement: {
|
|
1795
|
-
kind: "absolute",
|
|
1796
|
-
targetRange: {
|
|
1797
|
-
start: startMs,
|
|
1798
|
-
end: startMs + item.media.durationMs
|
|
1799
|
-
}
|
|
1800
|
-
},
|
|
1801
|
-
clipPayload: { volume: requireVolume(item.volume ?? 0) },
|
|
1802
|
-
relationTrace: {
|
|
1803
|
-
...trace,
|
|
1804
|
-
role: "speech-content"
|
|
1805
|
-
},
|
|
1806
|
-
relationIds: {
|
|
1807
|
-
trackClip: stableEffectId("relation", identityScope, `speech:${index}:track-clip`),
|
|
1808
|
-
clipMarker: stableEffectId("relation", identityScope, `speech:${index}:clip-marker`),
|
|
1809
|
-
markerContent: stableEffectId("relation", identityScope, `speech:${index}:marker-content`)
|
|
1810
|
-
}
|
|
1811
|
-
});
|
|
1812
|
-
else if (!("targetSpeechClipEntityId" in item)) {
|
|
1813
|
-
if (item.renderMode !== "recorded" || typeof recordedSpeechClipId !== "string") throw new Error("Only recorded speech may reuse a raw Audio occurrence");
|
|
1814
|
-
const target = requireCanonicalClipOnRole(draft, recordedSpeechClipId, "speech");
|
|
1815
|
-
if (target.markerContentRelation.endpoint1EntityId !== speech.entityId) throw new Error("Recorded speech occurrence no longer uses the frozen raw Audio");
|
|
1816
|
-
speechClipId = target.clip.entityId;
|
|
1817
|
-
const currentVolume = typeof target.clip.payload.volume === "number" ? target.clip.payload.volume : 0;
|
|
1818
|
-
replaceEntity(draft, target.clip.entityId, {
|
|
1819
|
-
...target.clip,
|
|
1820
|
-
payload: {
|
|
1821
|
-
...target.clip.payload,
|
|
1822
|
-
volume: requireVolume(item.volume ?? currentVolume)
|
|
1823
|
-
}
|
|
1824
|
-
});
|
|
1825
|
-
replaceRelationWithTrace(draft, target.markerContentRelation, target.marker.entityId, speech.entityId, {
|
|
1826
|
-
...trace,
|
|
1827
|
-
role: "speech-content"
|
|
1828
|
-
}, stableEffectId("relation", identityScope, `speech:${index}:marker-content`));
|
|
1829
|
-
} else {
|
|
1830
|
-
const target = requireCanonicalClipOnRole(draft, item.targetSpeechClipEntityId, "speech");
|
|
1831
|
-
if (target.trackRelation.endpoint0EntityId !== speechTrack.entityId) throw new Error("Replacement Speech occurrence is not on the frozen Speech Track");
|
|
1832
|
-
speechClipId = target.clip.entityId;
|
|
1833
|
-
replaceEntity(draft, target.clip.entityId, {
|
|
1834
|
-
...target.clip,
|
|
1835
|
-
payload: {
|
|
1836
|
-
...target.clip.payload,
|
|
1837
|
-
volume: requireVolume(item.volume ?? 0)
|
|
1838
|
-
}
|
|
1839
|
-
});
|
|
1840
|
-
replaceRelationWithTrace(draft, target.markerContentRelation, target.marker.entityId, speech.entityId, {
|
|
1841
|
-
...trace,
|
|
1842
|
-
role: "speech-content"
|
|
1843
|
-
}, stableEffectId("relation", identityScope, `speech:${index}:marker-content`));
|
|
1844
|
-
const markerPayload = { ...cloneJsonObject(target.marker.payload, "Speech Marker payload") };
|
|
1845
|
-
markerPayload.sourceRange = {
|
|
1846
|
-
start: 0,
|
|
1847
|
-
end: item.media.durationMs
|
|
1848
|
-
};
|
|
1849
|
-
markerPayload.duration = {
|
|
1850
|
-
mode: "fixed",
|
|
1851
|
-
value: item.media.durationMs
|
|
1852
|
-
};
|
|
1853
|
-
resizeAbsoluteTarget(markerPayload, item.media.durationMs);
|
|
1854
|
-
replaceEntity(draft, target.marker.entityId, {
|
|
1855
|
-
...target.marker,
|
|
1856
|
-
payload: markerPayload
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
const currentCaptionIds = "targetCaptionClipEntityIds" in item ? item.targetCaptionClipEntityIds : [];
|
|
1860
|
-
const itemCaptionIds = [];
|
|
1861
|
-
for (const [captionIndex, caption] of item.captions.entries()) {
|
|
1862
|
-
const durationMs = safeWholeMs(caption.endMs, "Speech Caption endMs") - safeWholeMs(caption.startMs, "Speech Caption startMs");
|
|
1863
|
-
if (durationMs <= 0) throw new Error("Speech Caption must have positive duration");
|
|
1864
|
-
const captionEntity = {
|
|
1865
|
-
entityId: requireAvailableEntityId(draft, stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}`)),
|
|
1866
|
-
entityKind: "caption",
|
|
1867
|
-
payload: {
|
|
1868
|
-
baseEntityIds: [scripts.audioScript.entityId],
|
|
1869
|
-
selection: captionSelection(caption.selection),
|
|
1870
|
-
durationMs
|
|
1871
|
-
}
|
|
1872
|
-
};
|
|
1873
|
-
draft.entities.push(captionEntity);
|
|
1874
|
-
assembleCaptionContent(draft, captionEntity.entityId);
|
|
1875
|
-
const captionTrace = {
|
|
1876
|
-
...trace,
|
|
1877
|
-
role: "caption-content",
|
|
1878
|
-
captionIndex
|
|
1879
|
-
};
|
|
1880
|
-
let captionClipId;
|
|
1881
|
-
const existingCaptionId = currentCaptionIds[captionIndex];
|
|
1882
|
-
if (!("targetSpeechClipEntityId" in item) || existingCaptionId === void 0) captionClipId = this.insertPlacedClipIntoDraft(draft, {
|
|
1883
|
-
clipEntityId: stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}:clip`),
|
|
1884
|
-
markerEntityId: stableEffectId("entity", identityScope, `speech:${index}:caption:${captionIndex}:marker`),
|
|
1885
|
-
trackEntityId: captionTrack.entityId,
|
|
1886
|
-
contentEntityId: captionEntity.entityId,
|
|
1887
|
-
sourceRange: {
|
|
1888
|
-
start: 0,
|
|
1889
|
-
end: durationMs
|
|
1890
|
-
},
|
|
1891
|
-
duration: {
|
|
1892
|
-
mode: "fixed",
|
|
1893
|
-
value: durationMs
|
|
1894
|
-
},
|
|
1895
|
-
placement: {
|
|
1896
|
-
kind: "absolute",
|
|
1897
|
-
targetRange: {
|
|
1898
|
-
start: startMs + caption.startMs,
|
|
1899
|
-
end: startMs + caption.endMs
|
|
1900
|
-
}
|
|
1901
|
-
},
|
|
1902
|
-
relationTrace: captionTrace,
|
|
1903
|
-
relationIds: {
|
|
1904
|
-
trackClip: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:track-clip`),
|
|
1905
|
-
clipMarker: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:clip-marker`),
|
|
1906
|
-
markerContent: stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:marker-content`)
|
|
1907
|
-
}
|
|
1908
|
-
});
|
|
1909
|
-
else {
|
|
1910
|
-
const targetCaption = requireCanonicalClipOnRole(draft, existingCaptionId, "caption");
|
|
1911
|
-
if (targetCaption.trackRelation.endpoint0EntityId !== captionTrack.entityId) throw new Error("Replacement Caption occurrence is not on the frozen Caption Track");
|
|
1912
|
-
captionClipId = targetCaption.clip.entityId;
|
|
1913
|
-
replaceRelationWithTrace(draft, targetCaption.markerContentRelation, targetCaption.marker.entityId, captionEntity.entityId, captionTrace, stableEffectId("relation", identityScope, `speech:${index}:caption:${captionIndex}:marker-content`));
|
|
1914
|
-
replaceEntity(draft, targetCaption.marker.entityId, {
|
|
1915
|
-
...targetCaption.marker,
|
|
1916
|
-
payload: {
|
|
1917
|
-
...targetCaption.marker.payload,
|
|
1918
|
-
sourceRange: {
|
|
1919
|
-
start: 0,
|
|
1920
|
-
end: durationMs
|
|
1921
|
-
},
|
|
1922
|
-
duration: {
|
|
1923
|
-
mode: "fixed",
|
|
1924
|
-
value: durationMs
|
|
1925
|
-
},
|
|
1926
|
-
targetRange: {
|
|
1927
|
-
start: startMs + caption.startMs,
|
|
1928
|
-
end: startMs + caption.endMs
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
itemCaptionIds.push(captionClipId);
|
|
1934
|
-
}
|
|
1935
|
-
for (const retiredCaptionId of currentCaptionIds.slice(item.captions.length)) {
|
|
1936
|
-
const retired = requireCanonicalClipOnRole(draft, retiredCaptionId, "caption");
|
|
1937
|
-
if (retired.trackRelation.endpoint0EntityId !== envelope.decisions.captionTrackEntityId) throw new Error("Retired Caption occurrence is not on the frozen Caption Track");
|
|
1938
|
-
retireClipOccurrence(draft, retired);
|
|
1939
|
-
}
|
|
1940
|
-
speechClipEntityIds.push(speechClipId);
|
|
1941
|
-
speechAudioEntityIds.push(speech.entityId);
|
|
1942
|
-
audioScriptEntityIds.push(scripts.audioScript.entityId);
|
|
1943
|
-
phoneticScriptEntityIds.push(scripts.phonetic?.entityId ?? null);
|
|
1944
|
-
captionClipEntityIds.push(itemCaptionIds);
|
|
1945
|
-
}
|
|
1946
|
-
return {
|
|
1947
|
-
speechClipEntityIds,
|
|
1948
|
-
speechAudioEntityIds,
|
|
1949
|
-
audioScriptEntityIds,
|
|
1950
|
-
phoneticScriptEntityIds,
|
|
1951
|
-
captionClipEntityIds
|
|
1952
|
-
};
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
static compileSpeechAssetsEffect(base, envelope) {
|
|
1956
|
-
assertSpeechAssetsEnvelope(envelope);
|
|
1957
|
-
consumeEffectEnvelope(consumedSpeechEnvelopes, envelope, "Speech");
|
|
1958
|
-
const current = cloneRows(base.snapshot().rows);
|
|
1959
|
-
const replay = resolveSpeechAssetsReplay(current, envelope, {
|
|
1960
|
-
tombstonedRelations: base.tombstonedRelations(),
|
|
1961
|
-
getVersion: (id) => base.getVersion(id),
|
|
1962
|
-
resolveCurrentEntityId: (id) => base.resolveCurrentEntityId(id)
|
|
1963
|
-
});
|
|
1964
|
-
if (replay !== void 0) return {
|
|
1965
|
-
result: replay,
|
|
1966
|
-
replayed: true,
|
|
1967
|
-
rows: current
|
|
1968
|
-
};
|
|
1969
|
-
const fingerprint = captionBaselineFingerprint(base);
|
|
1970
|
-
if (fingerprint !== envelope.baselineFingerprint) throw new Error("Speech effect baseline changed; prepare a new effect from the authority");
|
|
1971
|
-
if (canonicalJson(resolveSpeechEffectDecisions(current, envelope.input, envelope.effectId, fingerprint)) !== canonicalJson(envelope.decisions)) throw new Error("Speech effect decisions no longer match the frozen causal baseline");
|
|
1972
|
-
const doc = base.doc.fork();
|
|
1973
|
-
doc.setPeerId(stableEffectPeerId(speechAssetsCommand, envelope.effectId, fingerprint));
|
|
1974
|
-
const staged = new LoroEntityDocument(doc);
|
|
1975
|
-
const editor = new EntityTimelineEditor(staged.snapshot().rows, stableEffectIdFactory(stableEffectScope(speechAssetsCommand, envelope.effectId, fingerprint), "speech-editor"));
|
|
1976
|
-
editor.#applySpeechAssets(envelope);
|
|
1977
|
-
const applied = applyEntityRows(staged, editor.rows());
|
|
1978
|
-
const result = resolveSpeechAssetsReplay(cloneRows(applied.state.rows), envelope);
|
|
1979
|
-
if (result === void 0) throw new Error("Committed Speech effect is missing its complete receipt");
|
|
1980
|
-
return {
|
|
1981
|
-
result,
|
|
1982
|
-
replayed: false,
|
|
1983
|
-
update: applied.update,
|
|
1984
|
-
rows: applied.state.rows
|
|
1985
|
-
};
|
|
1986
|
-
}
|
|
1987
1483
|
insertPlacedClipIntoDraft(draft, input) {
|
|
1988
1484
|
const track = requireEntityKind(draft, input.trackEntityId, "track");
|
|
1989
1485
|
requireTrackTimeline(draft, track.entityId);
|
|
1990
1486
|
const content = requireSequenceContent(draft, input.contentEntityId);
|
|
1991
1487
|
const clipId = input.clipEntityId === void 0 ? this.mintEntityId(draft) : requireAvailableEntityId(draft, input.clipEntityId);
|
|
1992
|
-
const markerId =
|
|
1488
|
+
const markerId = this.mintEntityId(draft);
|
|
1993
1489
|
const clipPayload = { ...cloneJsonObject(input.clipPayload ?? {}, "Clip payload") };
|
|
1994
1490
|
delete clipPayload.order;
|
|
1995
1491
|
const markerPlacement = applyPlacementPayload(clipPayload, input.placement);
|
|
@@ -2008,7 +1504,7 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2008
1504
|
})
|
|
2009
1505
|
};
|
|
2010
1506
|
draft.entities.push(clip, marker);
|
|
2011
|
-
draft.relations.push(this.createRelation(draft, "track-clip", track.entityId, clipId
|
|
1507
|
+
draft.relations.push(this.createRelation(draft, "track-clip", track.entityId, clipId), this.createRelation(draft, "clip-marker", clipId, markerId), this.createRelation(draft, "marker-content", markerId, content.entityId));
|
|
2012
1508
|
if (input.placement.kind === "anchored") {
|
|
2013
1509
|
const host = requireEntityKind(draft, input.placement.hostClipEntityId, "clip");
|
|
2014
1510
|
if (host.entityId === clipId) throw new Error("A Clip cannot anchor to itself");
|
|
@@ -2051,7 +1547,7 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2051
1547
|
requireComposedAudioScript(draft, phonetic.entityId);
|
|
2052
1548
|
return phonetic;
|
|
2053
1549
|
}
|
|
2054
|
-
/** Preserve the factual synthesis input
|
|
1550
|
+
/** Preserve the factual synthesis input as `[rendered Audio, PhoneticScript]`. */
|
|
2055
1551
|
ensurePhoneticRender(draft, voiceoverAudioEntityId, phoneticScriptEntityId) {
|
|
2056
1552
|
const renders = draft.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === voiceoverAudioEntityId || relation.endpoint1EntityId === voiceoverAudioEntityId));
|
|
2057
1553
|
if (renders.length > 1) throw new Error(`Voiceover "${voiceoverAudioEntityId}" has multiple PhoneticScript render relations`);
|
|
@@ -2061,7 +1557,7 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2061
1557
|
}
|
|
2062
1558
|
draft.relations.push(this.createRelation(draft, "phonetic-script-render", voiceoverAudioEntityId, phoneticScriptEntityId));
|
|
2063
1559
|
}
|
|
2064
|
-
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId,
|
|
1560
|
+
reconcileVoiceoverCaptions(draft, voiceoverClipEntityId, phoneticScriptEntityId, captions) {
|
|
2065
1561
|
const audioScriptEntityId = requireComposedAudioScript(draft, phoneticScriptEntityId);
|
|
2066
1562
|
const requested = new Set(captions.map((caption) => caption.captionClipEntityId));
|
|
2067
1563
|
if (requested.size !== captions.length) throw new Error("Caption Clip ids must be unique within a voiceover take");
|
|
@@ -2086,10 +1582,6 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2086
1582
|
};
|
|
2087
1583
|
draft.entities.push(content);
|
|
2088
1584
|
assertCaptionScript(draft, content.entityId, audioScriptEntityId);
|
|
2089
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceoverAudioEntityId, { alignment: {
|
|
2090
|
-
start: startMs,
|
|
2091
|
-
end: startMs + durationMs
|
|
2092
|
-
} }));
|
|
2093
1585
|
this.insertPlacedClipIntoDraft(draft, {
|
|
2094
1586
|
clipEntityId: caption.captionClipEntityId,
|
|
2095
1587
|
trackEntityId: captionTrack.entityId,
|
|
@@ -2143,7 +1635,6 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2143
1635
|
}
|
|
2144
1636
|
}
|
|
2145
1637
|
});
|
|
2146
|
-
this.replaceCaptionAlignment(draft, content.entityId, voiceoverAudioEntityId, startMs, durationMs);
|
|
2147
1638
|
}
|
|
2148
1639
|
}
|
|
2149
1640
|
ensureRoleTrackForClip(draft, clipEntityId, role) {
|
|
@@ -2152,14 +1643,6 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2152
1643
|
const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
|
|
2153
1644
|
return this.ensureRoleTrack(draft, timeline.entityId, role);
|
|
2154
1645
|
}
|
|
2155
|
-
replaceCaptionAlignment(draft, captionEntityId, voiceoverAudioEntityId, startMs, durationMs) {
|
|
2156
|
-
const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
|
|
2157
|
-
draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
|
|
2158
|
-
draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceoverAudioEntityId, { alignment: {
|
|
2159
|
-
start: startMs,
|
|
2160
|
-
end: startMs + durationMs
|
|
2161
|
-
} }));
|
|
2162
|
-
}
|
|
2163
1646
|
/**
|
|
2164
1647
|
* Remove one placed Caption use. The composed AudioScript outlives the
|
|
2165
1648
|
* caption: it may anchor further captions and remains the text owner.
|
|
@@ -2314,14 +1797,14 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2314
1797
|
if (draft.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
|
|
2315
1798
|
return entityId;
|
|
2316
1799
|
}
|
|
2317
|
-
createRelation(draft, relationKind, endpoint0EntityId, endpoint1EntityId, metadata = {}
|
|
1800
|
+
createRelation(draft, relationKind, endpoint0EntityId, endpoint1EntityId, metadata = {}) {
|
|
2318
1801
|
return {
|
|
2319
|
-
relationId:
|
|
1802
|
+
relationId: this.mintRelationId(draft),
|
|
2320
1803
|
relationKind,
|
|
2321
1804
|
endpoint0EntityId,
|
|
2322
1805
|
endpoint1EntityId,
|
|
2323
1806
|
metadata,
|
|
2324
|
-
trace
|
|
1807
|
+
trace: {}
|
|
2325
1808
|
};
|
|
2326
1809
|
}
|
|
2327
1810
|
relinkRelation(draft, relation, endpoints) {
|
|
@@ -2340,994 +1823,6 @@ var EntityTimelineEditor = class EntityTimelineEditor {
|
|
|
2340
1823
|
return relationId;
|
|
2341
1824
|
}
|
|
2342
1825
|
};
|
|
2343
|
-
/** Freeze every state-dependent choice once in the sole FE tool-effect handler. */
|
|
2344
|
-
function prepareCaptionAssetsEffect(base, rawInput) {
|
|
2345
|
-
const effectId = requireTrimmed(rawInput.effectId, "Caption effect id");
|
|
2346
|
-
const segments = validateAsrSegments(rawInput.segments);
|
|
2347
|
-
const input = structuredClone({
|
|
2348
|
-
...rawInput,
|
|
2349
|
-
effectId,
|
|
2350
|
-
segments
|
|
2351
|
-
});
|
|
2352
|
-
const rows = cloneRows(base.snapshot().rows);
|
|
2353
|
-
const signature = captionEffectSignature(input, segments);
|
|
2354
|
-
const baselineFingerprint = captionBaselineFingerprint(base);
|
|
2355
|
-
return deepFreeze({
|
|
2356
|
-
version: 1,
|
|
2357
|
-
baselineFingerprint,
|
|
2358
|
-
effectId,
|
|
2359
|
-
signature,
|
|
2360
|
-
input,
|
|
2361
|
-
decisions: resolveCaptionEffectDecisions(rows, input, stableEffectScope(captionAssetsCommand, effectId, baselineFingerprint), segments)
|
|
2362
|
-
});
|
|
2363
|
-
}
|
|
2364
|
-
/**
|
|
2365
|
-
* Compile only a frozen envelope on its exact causal baseline. Collaborators
|
|
2366
|
-
* receive the resulting Loro update, never the raw effect.
|
|
2367
|
-
*/
|
|
2368
|
-
function compileCaptionAssetsEffect(base, envelope) {
|
|
2369
|
-
return EntityTimelineEditor.compileCaptionAssetsEffect(base, envelope);
|
|
2370
|
-
}
|
|
2371
|
-
const captionAssetsCommand = "apply-caption-assets";
|
|
2372
|
-
const generatedVisualsCommand = "add-generated-visuals";
|
|
2373
|
-
const speechAssetsCommand = "apply-speech-assets";
|
|
2374
|
-
const consumedGeneratedVisualEnvelopes = /* @__PURE__ */ new Set();
|
|
2375
|
-
const consumedSpeechEnvelopes = /* @__PURE__ */ new Set();
|
|
2376
|
-
function consumeEffectEnvelope(consumed, envelope, label) {
|
|
2377
|
-
const key = `${envelope.baselineFingerprint}\u0000${envelope.effectId}\u0000${envelope.signature}`;
|
|
2378
|
-
if (consumed.has(key)) throw new Error(`${label} effect envelope is one-shot; retain and retry the compiled Loro update bytes`);
|
|
2379
|
-
consumed.add(key);
|
|
2380
|
-
}
|
|
2381
|
-
/** Freeze the insertion position and lineage on one causal baseline. */
|
|
2382
|
-
function prepareGeneratedVisualEffect(base, rawInput) {
|
|
2383
|
-
const effectId = requireTrimmed(rawInput.effectId, "Generated visual effect id");
|
|
2384
|
-
if (rawInput.clips.length === 0) throw new Error("Generated visual effect requires at least one clip");
|
|
2385
|
-
const clips = rawInput.clips.map((clip, index) => {
|
|
2386
|
-
const generationJobId = requireTrimmed(clip.generationJobId, `Generated visual ${index} generation job id`);
|
|
2387
|
-
const durationMs = safeWholeMs(clip.durationMs, `Generated visual ${index} durationMs`);
|
|
2388
|
-
if (durationMs === 0 || clip.media.kind === "video" && durationMs > clip.media.durationMs) throw new Error(`Generated visual ${index} duration must fit its immutable media Sequence`);
|
|
2389
|
-
if (clip.sourceMediaEntityIds.length === 0) throw new Error(`Generated visual ${index} requires explicit source lineage`);
|
|
2390
|
-
const sourceMediaEntityIds = clip.sourceMediaEntityIds.map((sourceId) => requireTrimmed(sourceId, `Generated visual ${index} source media id`));
|
|
2391
|
-
if (new Set(sourceMediaEntityIds).size !== sourceMediaEntityIds.length) throw new Error(`Generated visual ${index} source lineage must be unique`);
|
|
2392
|
-
return {
|
|
2393
|
-
...structuredClone(clip),
|
|
2394
|
-
durationMs,
|
|
2395
|
-
generationJobId,
|
|
2396
|
-
sourceMediaEntityIds
|
|
2397
|
-
};
|
|
2398
|
-
});
|
|
2399
|
-
const input = structuredClone({
|
|
2400
|
-
...rawInput,
|
|
2401
|
-
effectId,
|
|
2402
|
-
clips
|
|
2403
|
-
});
|
|
2404
|
-
const rows = cloneRows(base.snapshot().rows);
|
|
2405
|
-
for (const [index, clip] of input.clips.entries()) for (const sourceId of clip.sourceMediaEntityIds) {
|
|
2406
|
-
const source = requireSequenceContent(rows, sourceId);
|
|
2407
|
-
if (![
|
|
2408
|
-
"video",
|
|
2409
|
-
"image",
|
|
2410
|
-
"audio"
|
|
2411
|
-
].includes(source.entityKind)) throw new Error(`Generated visual ${index} source must be Video, Image, or Audio`);
|
|
2412
|
-
}
|
|
2413
|
-
const baselineFingerprint = captionBaselineFingerprint(base);
|
|
2414
|
-
return deepFreeze({
|
|
2415
|
-
version: 1,
|
|
2416
|
-
baselineFingerprint,
|
|
2417
|
-
effectId,
|
|
2418
|
-
signature: generatedVisualSignature(input),
|
|
2419
|
-
input,
|
|
2420
|
-
decisions: resolveGeneratedVisualDecisions(rows, input, stableEffectScope(generatedVisualsCommand, effectId, baselineFingerprint))
|
|
2421
|
-
});
|
|
2422
|
-
}
|
|
2423
|
-
function prepareContinueVideoEffect(base, input) {
|
|
2424
|
-
const rows = cloneRows(base.snapshot().rows);
|
|
2425
|
-
requireEntityKind(rows, input.sourceMediaEntityId, "video");
|
|
2426
|
-
requireEntityKind(rows, input.tailFrameEntityId, "image");
|
|
2427
|
-
return prepareGeneratedVisualEffect(base, {
|
|
2428
|
-
effectId: input.effectId,
|
|
2429
|
-
timelineEntityId: input.timelineEntityId,
|
|
2430
|
-
afterClipEntityId: input.afterClipEntityId,
|
|
2431
|
-
clips: [{
|
|
2432
|
-
media: input.media,
|
|
2433
|
-
durationMs: input.media.durationMs,
|
|
2434
|
-
generationJobId: input.effectId,
|
|
2435
|
-
sourceMediaEntityIds: [input.sourceMediaEntityId, input.tailFrameEntityId]
|
|
2436
|
-
}]
|
|
2437
|
-
});
|
|
2438
|
-
}
|
|
2439
|
-
/** Compile once at the FE authority; retain the returned update bytes for retry and collaborators. */
|
|
2440
|
-
function compileGeneratedVisualEffect(base, envelope) {
|
|
2441
|
-
return EntityTimelineEditor.compileGeneratedVisualEffect(base, envelope);
|
|
2442
|
-
}
|
|
2443
|
-
function prepareSpeechAssetsEffect(base, rawInput) {
|
|
2444
|
-
for (const item of rawInput.value.items) {
|
|
2445
|
-
assertSpeechRenderContract(rawInput.mode, item);
|
|
2446
|
-
if (item.source.kind === "audio-script") if (item.source.audioScriptEntityId === void 0) audioScriptSegmentsFromAsr(item.source.segments, false);
|
|
2447
|
-
else validateResolvedAsrSegments(item.source.segments);
|
|
2448
|
-
else validateScriptSegments(item.source.segments);
|
|
2449
|
-
}
|
|
2450
|
-
const value = structuredClone(rawInput.value);
|
|
2451
|
-
const effectId = requireTrimmed(value.effectId, "Speech effect id");
|
|
2452
|
-
if (value.items.length === 0) throw new Error("Speech effect requires at least one item");
|
|
2453
|
-
if (new Set(value.items.map((item) => item.media.assetId)).size !== value.items.length) throw new Error("Speech effect media assets must be unique");
|
|
2454
|
-
const input = structuredClone({
|
|
2455
|
-
...rawInput,
|
|
2456
|
-
value: {
|
|
2457
|
-
...value,
|
|
2458
|
-
effectId
|
|
2459
|
-
}
|
|
2460
|
-
});
|
|
2461
|
-
const baselineFingerprint = captionBaselineFingerprint(base);
|
|
2462
|
-
return deepFreeze({
|
|
2463
|
-
version: 1,
|
|
2464
|
-
baselineFingerprint,
|
|
2465
|
-
effectId,
|
|
2466
|
-
signature: speechEffectSignature(input),
|
|
2467
|
-
input,
|
|
2468
|
-
decisions: resolveSpeechEffectDecisions(cloneRows(base.snapshot().rows), input, effectId, baselineFingerprint)
|
|
2469
|
-
});
|
|
2470
|
-
}
|
|
2471
|
-
function assertSpeechRenderContract(mode, item) {
|
|
2472
|
-
if (item.renderMode === "recorded") {
|
|
2473
|
-
if (mode !== "add") throw new Error("Recorded speech cannot replace a synthesized Voice");
|
|
2474
|
-
if (item.media.kind !== "audio") throw new Error("Recorded speech requires raw Audio media");
|
|
2475
|
-
if ("voice" in item.media && item.media.voice !== void 0) throw new Error("Recorded speech must not declare a Voice");
|
|
2476
|
-
if (item.source.kind !== "audio-script") throw new Error("Recorded speech requires an AudioScript source");
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
if (item.renderMode !== "synthesized") throw new Error("Speech renderMode must be recorded or synthesized");
|
|
2480
|
-
if (item.media.kind !== "voiceover") throw new Error("Synthesized speech requires voiceover media");
|
|
2481
|
-
if (item.media.voice === void 0) throw new Error("Synthesized speech requires an explicit Voice identity");
|
|
2482
|
-
}
|
|
2483
|
-
/** Compile once at the FE authority; retain the returned update bytes for retry and collaborators. */
|
|
2484
|
-
function compileSpeechAssetsEffect(base, envelope) {
|
|
2485
|
-
return EntityTimelineEditor.compileSpeechAssetsEffect(base, envelope);
|
|
2486
|
-
}
|
|
2487
|
-
function validateAsrSegments(segments) {
|
|
2488
|
-
if (segments.length === 0) throw new Error("Caption assets require at least one ASR segment");
|
|
2489
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2490
|
-
return segments.map((segment) => {
|
|
2491
|
-
const segmentId = requireTrimmed(segment.segmentId, "ASR segment id");
|
|
2492
|
-
if (ids.has(segmentId)) throw new Error(`Duplicate ASR segment id "${segmentId}"`);
|
|
2493
|
-
ids.add(segmentId);
|
|
2494
|
-
if (typeof segment.text !== "string" || segment.text.trim() === "") throw new Error(`ASR segment "${segmentId}" requires visible text`);
|
|
2495
|
-
const startMs = safeWholeMs(segment.startMs, `ASR segment "${segmentId}" startMs`);
|
|
2496
|
-
const endMs = safeWholeMs(segment.endMs, `ASR segment "${segmentId}" endMs`);
|
|
2497
|
-
if (endMs <= startMs) throw new Error(`ASR segment "${segmentId}" must have positive duration`);
|
|
2498
|
-
const language = segment.language === void 0 ? void 0 : requireTrimmed(segment.language, `ASR segment "${segmentId}" language`);
|
|
2499
|
-
return {
|
|
2500
|
-
segmentId,
|
|
2501
|
-
text: segment.text,
|
|
2502
|
-
startMs,
|
|
2503
|
-
endMs,
|
|
2504
|
-
...language === void 0 ? {} : { language }
|
|
2505
|
-
};
|
|
2506
|
-
});
|
|
2507
|
-
}
|
|
2508
|
-
function requireCanonicalCaptionTrack(rows, timelineEntityId, trackEntityId) {
|
|
2509
|
-
const timeline = requireEntityKind(rows, timelineEntityId, "timeline");
|
|
2510
|
-
const track = requireEntityKind(rows, trackEntityId, "track");
|
|
2511
|
-
if (track.payload.role !== "caption") throw new Error(`Track "${track.entityId}" must have role "caption"`);
|
|
2512
|
-
const bindings = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
|
|
2513
|
-
if (bindings.length !== 1) throw new Error(`Caption Track "${track.entityId}" must have one unambiguous Timeline`);
|
|
2514
|
-
const binding = bindings[0];
|
|
2515
|
-
if (binding.endpoint0EntityId !== timeline.entityId || binding.endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
|
|
2516
|
-
return track;
|
|
2517
|
-
}
|
|
2518
|
-
function resolveSourceOccurrence(rows, input, sourceEntityId) {
|
|
2519
|
-
const clip = requireEntityKind(rows, input.sourceOccurrence.clipEntityId, "clip");
|
|
2520
|
-
const marker = requireEntityKind(rows, input.sourceOccurrence.markerEntityId, "sequence-marker");
|
|
2521
|
-
const clipMarkers = incidentRelations(rows, clip.entityId).filter((relation) => relation.relationKind === "clip-marker");
|
|
2522
|
-
if (clipMarkers.length !== 1 || clipMarkers[0].endpoint0EntityId !== clip.entityId || clipMarkers[0].endpoint1EntityId !== marker.entityId) throw new Error("Source occurrence requires one ordered clip-marker [Clip, SequenceMarker]");
|
|
2523
|
-
const contents = incidentRelations(rows, marker.entityId).filter((relation) => relation.relationKind === "marker-content");
|
|
2524
|
-
if (contents.length !== 1 || contents[0].endpoint0EntityId !== marker.entityId || contents[0].endpoint1EntityId !== sourceEntityId) throw new Error("Source occurrence requires one ordered marker-content [SequenceMarker, source Entity]");
|
|
2525
|
-
const trackBindings = incidentRelations(rows, clip.entityId).filter((relation) => relation.relationKind === "track-clip");
|
|
2526
|
-
if (trackBindings.length !== 1 || trackBindings[0].endpoint1EntityId !== clip.entityId) throw new Error("Source occurrence requires one ordered track-clip [Track, Clip]");
|
|
2527
|
-
const sourceTrack = requireEntityKind(rows, trackBindings[0].endpoint0EntityId, "track");
|
|
2528
|
-
const timelineBindings = incidentRelations(rows, sourceTrack.entityId).filter((relation) => relation.relationKind === "timeline-track");
|
|
2529
|
-
if (timelineBindings.length !== 1 || timelineBindings[0].endpoint0EntityId !== input.timelineEntityId || timelineBindings[0].endpoint1EntityId !== sourceTrack.entityId) throw new Error("Source occurrence must be placed on the selected Timeline through [Timeline, Track]");
|
|
2530
|
-
const placement = resolveEntityTimelineLayout(rows).clips.get(clip.entityId);
|
|
2531
|
-
if (placement === void 0 || placement.markerEntityId !== marker.entityId || placement.contentEntityId !== sourceEntityId) throw new Error("Source occurrence is not an unambiguous placed use of the selected source Entity");
|
|
2532
|
-
const sourceRange = requirePositiveRange(marker.payload.sourceRange, "Source occurrence sourceRange");
|
|
2533
|
-
return {
|
|
2534
|
-
sourceRange,
|
|
2535
|
-
timelineStart: placement.start,
|
|
2536
|
-
rate: (sourceRange.end - sourceRange.start) / placement.duration
|
|
2537
|
-
};
|
|
2538
|
-
}
|
|
2539
|
-
function occurrenceTargetRange(occurrence, segment) {
|
|
2540
|
-
if (segment.startMs < occurrence.sourceRange.start || segment.endMs > occurrence.sourceRange.end) throw new Error(`ASR segment "${segment.segmentId}" is outside the selected source occurrence`);
|
|
2541
|
-
const start = Math.round(occurrence.timelineStart + (segment.startMs - occurrence.sourceRange.start) / occurrence.rate);
|
|
2542
|
-
const end = Math.round(occurrence.timelineStart + (segment.endMs - occurrence.sourceRange.start) / occurrence.rate);
|
|
2543
|
-
if (end <= start) throw new Error(`ASR segment "${segment.segmentId}" collapses under the occurrence time mapping`);
|
|
2544
|
-
return {
|
|
2545
|
-
start,
|
|
2546
|
-
end
|
|
2547
|
-
};
|
|
2548
|
-
}
|
|
2549
|
-
function resolveCaptionEffectDecisions(rows, input, identityScope, segments) {
|
|
2550
|
-
const source = requireEntityKind(rows, input.source.entityId, input.source.entityKind);
|
|
2551
|
-
requireCanonicalCaptionTrack(rows, input.timelineEntityId, input.trackEntityId);
|
|
2552
|
-
const occurrence = resolveSourceOccurrence(rows, input, source.entityId);
|
|
2553
|
-
return {
|
|
2554
|
-
sourceOccurrence: {
|
|
2555
|
-
sourceStartMs: occurrence.sourceRange.start,
|
|
2556
|
-
sourceEndMs: occurrence.sourceRange.end,
|
|
2557
|
-
timelineStartMs: occurrence.timelineStart,
|
|
2558
|
-
rate: occurrence.rate
|
|
2559
|
-
},
|
|
2560
|
-
targetRanges: segments.map((segment) => ({
|
|
2561
|
-
segmentId: segment.segmentId,
|
|
2562
|
-
...occurrenceTargetRange(occurrence, segment)
|
|
2563
|
-
})),
|
|
2564
|
-
captionAsset: resolveCaptionAssetDecision(rows, input, identityScope)
|
|
2565
|
-
};
|
|
2566
|
-
}
|
|
2567
|
-
function resolveCaptionAssetDecision(rows, input, identityScope) {
|
|
2568
|
-
const assetId = requireTrimmed(input.captionAsset.assetId, "Caption asset id");
|
|
2569
|
-
const storageKey = input.captionAsset.storageKey === void 0 ? void 0 : requireTrimmed(input.captionAsset.storageKey, "Caption asset storage key");
|
|
2570
|
-
const matches = rows.entities.filter((row) => row.entityKind === "asset" && row.payload.system === "memota" && row.payload.key === assetId);
|
|
2571
|
-
if (matches.length > 1) throw new Error(`Caption Asset "${assetId}" is ambiguous`);
|
|
2572
|
-
const existing = matches[0];
|
|
2573
|
-
if (existing === void 0) return {
|
|
2574
|
-
mode: "create",
|
|
2575
|
-
resultAssetEntityId: stableEffectId("entity", identityScope, "caption-asset:entity:0"),
|
|
2576
|
-
reboundRelationIds: []
|
|
2577
|
-
};
|
|
2578
|
-
if (storageKey === void 0 || existing.payload.storageKey === storageKey) return {
|
|
2579
|
-
mode: "reuse",
|
|
2580
|
-
priorAssetEntityId: existing.entityId,
|
|
2581
|
-
resultAssetEntityId: existing.entityId,
|
|
2582
|
-
reboundRelationIds: []
|
|
2583
|
-
};
|
|
2584
|
-
if (existing.payload.storageKey !== void 0) throw new Error(`Caption Asset "${assetId}" conflicts with its existing storage key`);
|
|
2585
|
-
const bindings = rows.relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === existing.entityId || relation.endpoint1EntityId === existing.entityId)).map((relation) => String(relation.relationId)).sort();
|
|
2586
|
-
return {
|
|
2587
|
-
mode: "replace",
|
|
2588
|
-
priorAssetEntityId: existing.entityId,
|
|
2589
|
-
resultAssetEntityId: stableEffectId("entity", identityScope, "caption-asset:entity:0"),
|
|
2590
|
-
reboundRelationIds: bindings
|
|
2591
|
-
};
|
|
2592
|
-
}
|
|
2593
|
-
function captionBaselineFingerprint(base) {
|
|
2594
|
-
const causal = bytesToHex(base.doc.oplogVersion().encode());
|
|
2595
|
-
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson({
|
|
2596
|
-
causal,
|
|
2597
|
-
state: base.snapshot()
|
|
2598
|
-
}))));
|
|
2599
|
-
}
|
|
2600
|
-
function resolveSpeechEffectDecisions(rows, input, effectId, baselineFingerprint) {
|
|
2601
|
-
const { value } = input;
|
|
2602
|
-
requireEntityKind(rows, value.timelineEntityId, "timeline");
|
|
2603
|
-
for (const item of value.items) if (item.source.kind === "phonetic-script") requirePersistedPhoneticSourceScript(rows, item.source);
|
|
2604
|
-
else if (item.source.audioScriptEntityId !== void 0) requireAssetlessAsrSourceScript(rows, item.source);
|
|
2605
|
-
const scope = stableEffectScope(speechAssetsCommand, effectId, baselineFingerprint);
|
|
2606
|
-
const speechTrackEntityId = resolveEffectTrackId(rows, value.timelineEntityId, "speech", scope);
|
|
2607
|
-
const captionTrackEntityId = value.items.some((item) => item.captions.length > 0 || "targetCaptionClipEntityIds" in item && item.targetCaptionClipEntityIds.length > 0) ? resolveEffectTrackId(rows, value.timelineEntityId, "caption", scope) : void 0;
|
|
2608
|
-
if (input.mode === "add") {
|
|
2609
|
-
const targetStartsMs = input.value.items.map((item) => safeWholeMs(item.placementStartMs, "Speech placement startMs"));
|
|
2610
|
-
return {
|
|
2611
|
-
speechTrackEntityId,
|
|
2612
|
-
...captionTrackEntityId === void 0 ? {} : { captionTrackEntityId },
|
|
2613
|
-
targetStartsMs,
|
|
2614
|
-
recordedSpeechClipEntityIds: input.value.items.map((item, index) => item.renderMode === "recorded" ? resolveRecordedSpeechOccurrence(rows, item, speechTrackEntityId, targetStartsMs[index]) : null)
|
|
2615
|
-
};
|
|
2616
|
-
}
|
|
2617
|
-
const targets = input.value.items.map((item) => item.targetSpeechClipEntityId);
|
|
2618
|
-
if (new Set(targets).size !== targets.length) throw new Error("Replacement Speech targets must be unique");
|
|
2619
|
-
const captionTargets = input.value.items.flatMap((item) => item.targetCaptionClipEntityIds);
|
|
2620
|
-
if (new Set(captionTargets).size !== captionTargets.length) throw new Error("Replacement Caption targets must be unique across the effect");
|
|
2621
|
-
const layout = resolveEntityTimelineLayout(rows);
|
|
2622
|
-
const starts = input.value.items.map((item) => {
|
|
2623
|
-
const target = requireCanonicalClipOnRole(rows, item.targetSpeechClipEntityId, "speech");
|
|
2624
|
-
if (target.trackRelation.endpoint0EntityId !== speechTrackEntityId || target.trackRelation.endpoint1EntityId !== target.clip.entityId) throw new Error("Replacement Speech target must use ordered Track/Clip endpoints");
|
|
2625
|
-
for (const captionId of item.targetCaptionClipEntityIds) {
|
|
2626
|
-
const caption = requireCanonicalClipOnRole(rows, captionId, "caption");
|
|
2627
|
-
if (captionTrackEntityId === void 0 || caption.trackRelation.endpoint0EntityId !== captionTrackEntityId || caption.trackRelation.endpoint1EntityId !== caption.clip.entityId) throw new Error("Replacement Caption target must use ordered Track/Clip endpoints");
|
|
2628
|
-
}
|
|
2629
|
-
const placed = layout.clips.get(target.clip.entityId);
|
|
2630
|
-
if (placed === void 0) throw new Error(`Replacement Speech Clip "${target.clip.entityId}" is not placed`);
|
|
2631
|
-
return placed.start;
|
|
2632
|
-
});
|
|
2633
|
-
return {
|
|
2634
|
-
speechTrackEntityId,
|
|
2635
|
-
...captionTrackEntityId === void 0 ? {} : { captionTrackEntityId },
|
|
2636
|
-
targetStartsMs: starts,
|
|
2637
|
-
recordedSpeechClipEntityIds: input.value.items.map(() => null)
|
|
2638
|
-
};
|
|
2639
|
-
}
|
|
2640
|
-
function resolveRecordedSpeechOccurrence(rows, item, speechTrackEntityId, startMs) {
|
|
2641
|
-
if (item.source.sourceMediaEntityIds.length !== 1) throw new Error("Recorded speech requires exactly one raw Audio source");
|
|
2642
|
-
const audio = requireEntityKind(rows, item.source.sourceMediaEntityIds[0], "audio");
|
|
2643
|
-
requireRecordedAudioAsset(rows, audio, item.media);
|
|
2644
|
-
const candidates = [...resolveEntityTimelineLayout(rows).clips.values()].filter((placement) => placement.trackEntityId === speechTrackEntityId && placement.contentEntityId === audio.entityId);
|
|
2645
|
-
if (candidates.length > 1) throw new Error("Recorded speech Audio occurrence is ambiguous");
|
|
2646
|
-
const existing = candidates[0];
|
|
2647
|
-
if (existing === void 0) return null;
|
|
2648
|
-
if (existing.start !== startMs || existing.duration !== item.media.durationMs) throw new Error("Recorded speech Audio occurrence conflicts with the requested placement");
|
|
2649
|
-
const topology = requireCanonicalClipOnRole(rows, existing.clipEntityId, "speech");
|
|
2650
|
-
const sourceRange = requirePositiveRange(topology.marker.payload.sourceRange, "Recorded Speech sourceRange");
|
|
2651
|
-
if (sourceRange.start !== 0 || sourceRange.end !== item.media.durationMs || topology.marker.payload.timeRemapping !== void 0) throw new Error("Recorded speech requires the complete, unremapped raw Audio occurrence");
|
|
2652
|
-
return existing.clipEntityId;
|
|
2653
|
-
}
|
|
2654
|
-
function requireRecordedAudioAsset(rows, audio, media) {
|
|
2655
|
-
const bindings = incidentRelations(rows, audio.entityId).filter((relation) => relation.relationKind === "from-asset");
|
|
2656
|
-
if (bindings.length !== 1 || bindings[0].endpoint0EntityId !== audio.entityId) throw new Error("Recorded Audio requires one ordered from-asset binding");
|
|
2657
|
-
const asset = requireEntityKind(rows, bindings[0].endpoint1EntityId, "asset");
|
|
2658
|
-
if (asset.payload.system !== "memota" || asset.payload.key !== media.assetId || asset.payload.storageKey !== media.storageKey || audio.payload.durationMs !== media.durationMs) throw new Error("Recorded speech media must match its canonical memota Audio source");
|
|
2659
|
-
}
|
|
2660
|
-
function requireSynthesizedSpeechIdentity(rows, audio, media) {
|
|
2661
|
-
const bindings = incidentRelations(rows, audio.entityId).filter((relation) => relation.relationKind === "from-asset");
|
|
2662
|
-
if (bindings.length !== 1 || bindings[0].endpoint0EntityId !== audio.entityId) throw new Error("Synthesized speech requires one ordered from-asset binding");
|
|
2663
|
-
const asset = requireEntityKind(rows, bindings[0].endpoint1EntityId, "asset");
|
|
2664
|
-
if (asset.payload.system !== "memota-speech" || asset.payload.key !== media.assetId || asset.payload.storageKey !== media.storageKey || audio.payload.durationMs !== media.durationMs) throw new Error("Synthesized speech media must match its canonical memota-speech Voiceover");
|
|
2665
|
-
const timbre = incidentRelations(rows, audio.entityId).filter((relation) => relation.relationKind === "voice-timbre");
|
|
2666
|
-
if (timbre.length !== 1 || timbre[0].endpoint0EntityId !== audio.entityId) throw new Error("Synthesized speech requires one ordered Voice identity");
|
|
2667
|
-
if (canonicalJson(requireEntityKind(rows, timbre[0].endpoint1EntityId, "voice").payload.voice) !== canonicalJson(media.voice)) throw new Error("Synthesized speech Voice conflicts with its trusted media fact");
|
|
2668
|
-
}
|
|
2669
|
-
function resolveEffectTrackId(rows, timelineEntityId, role, identityScope) {
|
|
2670
|
-
if (rows.relations.filter((relation) => relation.relationKind === "timeline-track" && relation.endpoint1EntityId === timelineEntityId).length > 0) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
|
|
2671
|
-
const candidates = rows.relations.filter((relation) => relation.relationKind === "timeline-track" && relation.endpoint0EntityId === timelineEntityId).map((relation) => requireEntityKind(rows, relation.endpoint1EntityId, "track")).filter((track) => track.payload.role === role);
|
|
2672
|
-
if (candidates.length > 1) throw new Error(`${role} Track is ambiguous`);
|
|
2673
|
-
return candidates[0]?.entityId ?? createEntityId(stableEffectId("entity", identityScope, `track:${role}`));
|
|
2674
|
-
}
|
|
2675
|
-
function ensureEffectRoleTrack(rows, timelineEntityId, role, expectedTrackId, identityScope) {
|
|
2676
|
-
const resolved = resolveEffectTrackId(rows, timelineEntityId, role, identityScope);
|
|
2677
|
-
if (resolved !== expectedTrackId) throw new Error(`Frozen ${role} Track decision changed`);
|
|
2678
|
-
if (rows.entities.find((entity) => entity.entityId === resolved) !== void 0) return requireEntityKind(rows, resolved, "track");
|
|
2679
|
-
const track = {
|
|
2680
|
-
entityId: createEntityId(expectedTrackId),
|
|
2681
|
-
entityKind: "track",
|
|
2682
|
-
payload: {
|
|
2683
|
-
role,
|
|
2684
|
-
order: roleOrder[role]
|
|
2685
|
-
}
|
|
2686
|
-
};
|
|
2687
|
-
rows.entities.push(track);
|
|
2688
|
-
rows.relations.push({
|
|
2689
|
-
relationId: createRelationId(stableEffectId("relation", identityScope, `timeline-track:${role}`)),
|
|
2690
|
-
relationKind: "timeline-track",
|
|
2691
|
-
endpoint0EntityId: createEntityId(timelineEntityId),
|
|
2692
|
-
endpoint1EntityId: track.entityId,
|
|
2693
|
-
metadata: {},
|
|
2694
|
-
trace: {}
|
|
2695
|
-
});
|
|
2696
|
-
return track;
|
|
2697
|
-
}
|
|
2698
|
-
function materializeSpeechSource(rows, item, identityScope, itemIndex, trace, speechAudioEntityId) {
|
|
2699
|
-
const source = item.source;
|
|
2700
|
-
const segments = source.kind === "audio-script" ? audioScriptSegmentsFromAsr(source.segments, source.audioScriptEntityId !== void 0) : validateScriptSegments(source.segments);
|
|
2701
|
-
const requestedScriptId = source.audioScriptEntityId === void 0 ? stableEffectId("entity", identityScope, `speech:${itemIndex}:audio-script`) : requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
|
|
2702
|
-
const existingScript = rows.entities.find((entity) => entity.entityId === requestedScriptId);
|
|
2703
|
-
let audioScript;
|
|
2704
|
-
if (existingScript === void 0) {
|
|
2705
|
-
if (source.kind === "phonetic-script") throw new Error(`Phonetic Speech source AudioScript "${requestedScriptId}" must already exist with an Asset`);
|
|
2706
|
-
audioScript = {
|
|
2707
|
-
entityId: requireAvailableEntityId(rows, requestedScriptId),
|
|
2708
|
-
entityKind: "audio-script",
|
|
2709
|
-
payload: { segments }
|
|
2710
|
-
};
|
|
2711
|
-
rows.entities.push(audioScript);
|
|
2712
|
-
} else {
|
|
2713
|
-
audioScript = requireEntityKind(rows, requestedScriptId, "audio-script");
|
|
2714
|
-
if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${requestedScriptId}" conflicts with the immutable Speech source`);
|
|
2715
|
-
if (source.kind === "phonetic-script") requireCanonicalAudioScriptAsset(rows, audioScript.entityId);
|
|
2716
|
-
else requireNoAudioScriptAsset(rows, audioScript.entityId, "ASR-composed");
|
|
2717
|
-
}
|
|
2718
|
-
if (source.kind === "audio-script") {
|
|
2719
|
-
if (item.renderMode === "recorded" && (source.sourceMediaEntityIds.length !== 1 || source.sourceMediaEntityIds[0] !== speechAudioEntityId)) throw new Error("Recorded speech Audio must be its sole AudioScript source");
|
|
2720
|
-
const timed = validateResolvedAsrSegments(source.segments);
|
|
2721
|
-
if (source.sourceMediaEntityIds.length === 0) throw new Error("Audio Speech source requires media lineage");
|
|
2722
|
-
if (item.renderMode === "recorded") {
|
|
2723
|
-
const selected = new Set(source.sourceMediaEntityIds);
|
|
2724
|
-
rows.relations = rows.relations.filter((relation) => relation.relationKind !== "audio-script-source" || !selected.has(relation.endpoint1EntityId));
|
|
2725
|
-
}
|
|
2726
|
-
for (const [sourceIndex, sourceId] of source.sourceMediaEntityIds.entries()) {
|
|
2727
|
-
const media = requireSequenceContent(rows, sourceId);
|
|
2728
|
-
if (media.entityKind !== "audio" && media.entityKind !== "video") throw new Error("audio-script-source requires Audio or Video input");
|
|
2729
|
-
const existing = rows.relations.filter((relation) => relation.relationKind === "audio-script-source" && (relation.endpoint0EntityId === audioScript.entityId || relation.endpoint1EntityId === audioScript.entityId));
|
|
2730
|
-
if (existing.some((relation) => relation.endpoint0EntityId !== audioScript.entityId)) throw new Error("audio-script-source must use ordered endpoints [AudioScript, source media]");
|
|
2731
|
-
if (!existing.some((relation) => relation.endpoint1EntityId === media.entityId)) rows.relations.push({
|
|
2732
|
-
relationId: createRelationId(stableEffectId("relation", identityScope, `speech:${itemIndex}:audio-source:${sourceIndex}`)),
|
|
2733
|
-
relationKind: "audio-script-source",
|
|
2734
|
-
endpoint0EntityId: audioScript.entityId,
|
|
2735
|
-
endpoint1EntityId: media.entityId,
|
|
2736
|
-
metadata: {},
|
|
2737
|
-
trace: {
|
|
2738
|
-
...trace,
|
|
2739
|
-
role: "audio-script-source",
|
|
2740
|
-
sourceIndex
|
|
2741
|
-
}
|
|
2742
|
-
});
|
|
2743
|
-
}
|
|
2744
|
-
const markerLinks = incidentRelations(rows, audioScript.entityId).filter((relation) => relation.relationKind === "audio-script-marker");
|
|
2745
|
-
if (markerLinks.some((relation) => relation.endpoint0EntityId !== audioScript.entityId)) throw new Error("audio-script-marker must use ordered endpoints [AudioScript, annotation SequenceMarker]");
|
|
2746
|
-
if (markerLinks.length > 1) throw new Error(`AudioScript "${audioScript.entityId}" has ambiguous timing annotations`);
|
|
2747
|
-
const segmentRanges = timed.map((segment) => ({
|
|
2748
|
-
segmentId: segment.segmentId,
|
|
2749
|
-
startMs: segment.startMs,
|
|
2750
|
-
endMs: segment.endMs
|
|
2751
|
-
}));
|
|
2752
|
-
if (markerLinks[0] !== void 0) {
|
|
2753
|
-
if (canonicalJson(requireEntityKind(rows, markerLinks[0].endpoint1EntityId, "sequence-marker").payload.segmentRanges) !== canonicalJson(segmentRanges)) throw new Error(`AudioScript "${audioScript.entityId}" timing conflicts with the immutable Speech source`);
|
|
2754
|
-
} else {
|
|
2755
|
-
const markerId = stableEffectId("entity", identityScope, `speech:${itemIndex}:audio-script-marker`);
|
|
2756
|
-
rows.entities.push({
|
|
2757
|
-
entityId: createEntityId(markerId),
|
|
2758
|
-
entityKind: "sequence-marker",
|
|
2759
|
-
payload: {
|
|
2760
|
-
sourceRange: {
|
|
2761
|
-
start: source.sourceStartMs,
|
|
2762
|
-
end: source.sourceStartMs + item.media.durationMs
|
|
2763
|
-
},
|
|
2764
|
-
duration: {
|
|
2765
|
-
mode: "fixed",
|
|
2766
|
-
value: item.media.durationMs
|
|
2767
|
-
},
|
|
2768
|
-
segmentRanges
|
|
2769
|
-
}
|
|
2770
|
-
});
|
|
2771
|
-
rows.relations.push({
|
|
2772
|
-
relationId: createRelationId(stableEffectId("relation", identityScope, `speech:${itemIndex}:script-marker`)),
|
|
2773
|
-
relationKind: "audio-script-marker",
|
|
2774
|
-
endpoint0EntityId: audioScript.entityId,
|
|
2775
|
-
endpoint1EntityId: createEntityId(markerId),
|
|
2776
|
-
metadata: {},
|
|
2777
|
-
trace: {
|
|
2778
|
-
...trace,
|
|
2779
|
-
role: "audio-script-marker"
|
|
2780
|
-
}
|
|
2781
|
-
});
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
if (item.renderMode === "recorded") return { audioScript };
|
|
2785
|
-
const phoneticId = source.kind === "phonetic-script" ? requireTrimmed(source.phoneticScriptEntityId, "PhoneticScript entity id") : stableEffectId("entity", identityScope, `speech:${itemIndex}:phonetic-script`);
|
|
2786
|
-
const existingPhonetic = rows.entities.find((entity) => entity.entityId === phoneticId);
|
|
2787
|
-
const phoneticPayload = {
|
|
2788
|
-
baseEntityIds: [audioScript.entityId],
|
|
2789
|
-
...source.kind === "phonetic-script" && source.phonemeScript !== void 0 ? { phonemeScript: source.phonemeScript } : {}
|
|
2790
|
-
};
|
|
2791
|
-
let phonetic;
|
|
2792
|
-
if (existingPhonetic === void 0) {
|
|
2793
|
-
phonetic = {
|
|
2794
|
-
entityId: requireAvailableEntityId(rows, phoneticId),
|
|
2795
|
-
entityKind: "phonetic-script",
|
|
2796
|
-
payload: phoneticPayload
|
|
2797
|
-
};
|
|
2798
|
-
rows.entities.push(phonetic);
|
|
2799
|
-
} else {
|
|
2800
|
-
phonetic = requireEntityKind(rows, phoneticId, "phonetic-script");
|
|
2801
|
-
if (canonicalJson(phonetic.payload) !== canonicalJson(phoneticPayload)) throw new Error(`PhoneticScript "${phoneticId}" conflicts with the immutable Speech source`);
|
|
2802
|
-
}
|
|
2803
|
-
assemblePhoneticScriptContent(rows, phonetic.entityId);
|
|
2804
|
-
return {
|
|
2805
|
-
audioScript,
|
|
2806
|
-
phonetic
|
|
2807
|
-
};
|
|
2808
|
-
}
|
|
2809
|
-
function requirePersistedPhoneticSourceScript(rows, source) {
|
|
2810
|
-
const scriptId = requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
|
|
2811
|
-
const audioScript = requireEntityKind(rows, scriptId, "audio-script");
|
|
2812
|
-
const segments = validateScriptSegments(source.segments);
|
|
2813
|
-
if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${scriptId}" conflicts with the immutable Speech source`);
|
|
2814
|
-
requireCanonicalAudioScriptAsset(rows, audioScript.entityId);
|
|
2815
|
-
return audioScript;
|
|
2816
|
-
}
|
|
2817
|
-
function requireAssetlessAsrSourceScript(rows, source) {
|
|
2818
|
-
const scriptId = requireTrimmed(source.audioScriptEntityId, "AudioScript entity id");
|
|
2819
|
-
const audioScript = requireEntityKind(rows, scriptId, "audio-script");
|
|
2820
|
-
const segments = audioScriptSegmentsFromAsr(source.segments, true);
|
|
2821
|
-
if (canonicalJson(audioScript.payload.segments) !== canonicalJson(segments)) throw new Error(`AudioScript "${scriptId}" conflicts with the immutable ASR source`);
|
|
2822
|
-
requireNoAudioScriptAsset(rows, audioScript.entityId, "ASR-composed");
|
|
2823
|
-
return audioScript;
|
|
2824
|
-
}
|
|
2825
|
-
function requireNoAudioScriptAsset(rows, audioScriptEntityId, label) {
|
|
2826
|
-
if (incidentRelations(rows, audioScriptEntityId).filter((relation) => relation.relationKind === "to-asset").length > 0) throw new Error(`${label} AudioScript "${audioScriptEntityId}" must not have an authored Asset snapshot`);
|
|
2827
|
-
}
|
|
2828
|
-
function requireCanonicalAudioScriptAsset(rows, audioScriptEntityId) {
|
|
2829
|
-
const bindings = incidentRelations(rows, audioScriptEntityId).filter((relation) => relation.relationKind === "to-asset");
|
|
2830
|
-
if (bindings.length !== 1) throw new Error(`AudioScript "${audioScriptEntityId}" requires exactly one Asset binding`);
|
|
2831
|
-
const binding = bindings[0];
|
|
2832
|
-
if (binding.endpoint0EntityId !== audioScriptEntityId) throw new Error("to-asset must use ordered endpoints [AudioScript, Asset]");
|
|
2833
|
-
const asset = requireEntityKind(rows, binding.endpoint1EntityId, "asset");
|
|
2834
|
-
if (asset.payload.system !== "memota" || typeof asset.payload.key !== "string" || asset.payload.key.length === 0 || asset.payload.key.trim() !== asset.payload.key) throw new Error(`AudioScript Asset "${asset.entityId}" must state a canonical memota locator`);
|
|
2835
|
-
return asset;
|
|
2836
|
-
}
|
|
2837
|
-
function validateScriptSegments(segments) {
|
|
2838
|
-
if (segments.length === 0) throw new Error("Speech source requires at least one script segment");
|
|
2839
|
-
const ids = /* @__PURE__ */ new Set();
|
|
2840
|
-
return Array.from({ length: segments.length }, (_, index) => {
|
|
2841
|
-
if (!Object.hasOwn(segments, index)) throw new Error("Speech source segments must not be sparse");
|
|
2842
|
-
const value = segments[index];
|
|
2843
|
-
const segment = strictJsonObject(value, `Speech source segment ${index}`);
|
|
2844
|
-
if (typeof segment.segmentId !== "string") throw new Error("Speech source segment id must be a string");
|
|
2845
|
-
const segmentId = requireTrimmed(segment.segmentId, "Speech source segment id");
|
|
2846
|
-
if (ids.has(segmentId)) throw new Error(`Duplicate Speech source segment id "${segmentId}"`);
|
|
2847
|
-
ids.add(segmentId);
|
|
2848
|
-
if (typeof segment.text !== "string" || segment.text.trim() === "") throw new Error("Speech source text is required");
|
|
2849
|
-
if (segment.language !== void 0 && typeof segment.language !== "string") throw new Error("Speech source language must be a string");
|
|
2850
|
-
return {
|
|
2851
|
-
...segment,
|
|
2852
|
-
segmentId,
|
|
2853
|
-
text: segment.text,
|
|
2854
|
-
...segment.language === void 0 ? {} : { language: requireTrimmed(segment.language, "Speech source language") }
|
|
2855
|
-
};
|
|
2856
|
-
});
|
|
2857
|
-
}
|
|
2858
|
-
function validateResolvedAsrSegments(segments) {
|
|
2859
|
-
return validateScriptSegments(segments).map((segment) => {
|
|
2860
|
-
const segmentId = segment.segmentId;
|
|
2861
|
-
const startMs = safeWholeMs(segment.startMs, `ASR segment "${segmentId}" startMs`);
|
|
2862
|
-
const endMs = safeWholeMs(segment.endMs, `ASR segment "${segmentId}" endMs`);
|
|
2863
|
-
if (endMs <= startMs) throw new Error(`ASR segment "${segmentId}" must have positive duration`);
|
|
2864
|
-
return {
|
|
2865
|
-
...segment,
|
|
2866
|
-
segmentId,
|
|
2867
|
-
text: segment.text,
|
|
2868
|
-
startMs,
|
|
2869
|
-
endMs
|
|
2870
|
-
};
|
|
2871
|
-
});
|
|
2872
|
-
}
|
|
2873
|
-
function audioScriptSegmentsFromAsr(segments, preserveResolvedFields) {
|
|
2874
|
-
return validateResolvedAsrSegments(segments).map((timed) => {
|
|
2875
|
-
if (!preserveResolvedFields) {
|
|
2876
|
-
const allowed = new Set([
|
|
2877
|
-
"segmentId",
|
|
2878
|
-
"text",
|
|
2879
|
-
"language",
|
|
2880
|
-
"startMs",
|
|
2881
|
-
"endMs"
|
|
2882
|
-
]);
|
|
2883
|
-
const extras = Object.keys(timed).filter((key) => !allowed.has(key));
|
|
2884
|
-
if (extras.length > 0) throw new Error(`New ASR segment must not contain host-resolved fields: ${extras.join(", ")}`);
|
|
2885
|
-
return {
|
|
2886
|
-
segmentId: timed.segmentId,
|
|
2887
|
-
text: timed.text,
|
|
2888
|
-
...timed.language === void 0 ? {} : { language: timed.language }
|
|
2889
|
-
};
|
|
2890
|
-
}
|
|
2891
|
-
const segment = { ...timed };
|
|
2892
|
-
delete segment.startMs;
|
|
2893
|
-
delete segment.endMs;
|
|
2894
|
-
return segment;
|
|
2895
|
-
});
|
|
2896
|
-
}
|
|
2897
|
-
function strictJsonObject(value, label) {
|
|
2898
|
-
const clone = strictJsonValue(value, label, /* @__PURE__ */ new WeakSet());
|
|
2899
|
-
if (!isJsonObject(clone)) throw new Error(`${label} must be a plain JSON object`);
|
|
2900
|
-
return clone;
|
|
2901
|
-
}
|
|
2902
|
-
function strictJsonValue(value, label, seen) {
|
|
2903
|
-
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
2904
|
-
if (typeof value === "number") {
|
|
2905
|
-
if (!Number.isFinite(value)) throw new Error(`${label} must not contain non-finite JSON numbers`);
|
|
2906
|
-
return value;
|
|
2907
|
-
}
|
|
2908
|
-
if (Array.isArray(value)) {
|
|
2909
|
-
if (seen.has(value)) throw new Error(`${label} must not contain cyclic JSON values`);
|
|
2910
|
-
seen.add(value);
|
|
2911
|
-
const result = [];
|
|
2912
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
2913
|
-
if (!Object.hasOwn(value, index)) throw new Error(`${label} must not contain sparse JSON arrays`);
|
|
2914
|
-
result.push(strictJsonValue(value[index], `${label}[${index}]`, seen));
|
|
2915
|
-
}
|
|
2916
|
-
seen.delete(value);
|
|
2917
|
-
return result;
|
|
2918
|
-
}
|
|
2919
|
-
if (typeof value !== "object" || value === void 0) throw new Error(`${label} must contain only JSON values`);
|
|
2920
|
-
const prototype = Object.getPrototypeOf(value);
|
|
2921
|
-
if (prototype !== Object.prototype && prototype !== null) throw new Error(`${label} must not contain objects with custom prototypes`);
|
|
2922
|
-
if (seen.has(value)) throw new Error(`${label} must not contain cyclic JSON values`);
|
|
2923
|
-
seen.add(value);
|
|
2924
|
-
const result = {};
|
|
2925
|
-
for (const [key, child] of Object.entries(value)) {
|
|
2926
|
-
if (child === void 0) throw new Error(`${label}.${key} must not be undefined`);
|
|
2927
|
-
result[key] = strictJsonValue(child, `${label}.${key}`, seen);
|
|
2928
|
-
}
|
|
2929
|
-
seen.delete(value);
|
|
2930
|
-
return result;
|
|
2931
|
-
}
|
|
2932
|
-
function replaceRelationWithTrace(rows, previous, endpoint0EntityId, endpoint1EntityId, trace, relationId) {
|
|
2933
|
-
rows.relations = rows.relations.filter((relation) => relation.relationId !== previous.relationId);
|
|
2934
|
-
rows.relations.push({
|
|
2935
|
-
relationId: requireAvailableRelationId(rows, relationId),
|
|
2936
|
-
relationKind: previous.relationKind,
|
|
2937
|
-
endpoint0EntityId,
|
|
2938
|
-
endpoint1EntityId,
|
|
2939
|
-
metadata: previous.metadata,
|
|
2940
|
-
trace
|
|
2941
|
-
});
|
|
2942
|
-
}
|
|
2943
|
-
/** Retire one editor use while retaining its immutable content row and Loro history. */
|
|
2944
|
-
function retireClipOccurrence(rows, topology) {
|
|
2945
|
-
const removedRelations = new Set([
|
|
2946
|
-
topology.trackRelation.relationId,
|
|
2947
|
-
topology.clipMarkerRelation.relationId,
|
|
2948
|
-
topology.markerContentRelation.relationId
|
|
2949
|
-
]);
|
|
2950
|
-
rows.relations = rows.relations.filter((relation) => !removedRelations.has(relation.relationId));
|
|
2951
|
-
rows.entities = rows.entities.filter((entity) => entity.entityId !== topology.clip.entityId && entity.entityId !== topology.marker.entityId);
|
|
2952
|
-
}
|
|
2953
|
-
function speechEffectSignature(input) {
|
|
2954
|
-
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(input))));
|
|
2955
|
-
}
|
|
2956
|
-
function speechEffectTrace(envelope, itemIndex) {
|
|
2957
|
-
return {
|
|
2958
|
-
command: speechAssetsCommand,
|
|
2959
|
-
effectId: envelope.effectId,
|
|
2960
|
-
signature: envelope.signature,
|
|
2961
|
-
mode: envelope.input.mode,
|
|
2962
|
-
itemIndex
|
|
2963
|
-
};
|
|
2964
|
-
}
|
|
2965
|
-
function assertSpeechAssetsEnvelope(value) {
|
|
2966
|
-
if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || speechEffectSignature(value.input) !== value.signature || value.effectId !== value.input.value.effectId) throw new Error("Speech effect requires a canonical frozen envelope");
|
|
2967
|
-
for (const item of value.input.value.items) assertSpeechRenderContract(value.input.mode, item);
|
|
2968
|
-
}
|
|
2969
|
-
function resolveSpeechAssetsReplay(rows, envelope, history) {
|
|
2970
|
-
const currentTagged = rows.relations.filter((relation) => relation.trace.command === speechAssetsCommand && relation.trace.effectId === envelope.effectId);
|
|
2971
|
-
const historicalTagged = (history?.tombstonedRelations ?? []).filter((relation) => relation.trace.command === speechAssetsCommand && relation.trace.effectId === envelope.effectId);
|
|
2972
|
-
const tagged = [...currentTagged, ...historicalTagged];
|
|
2973
|
-
const historicalIds = new Set(historicalTagged.map((relation) => relation.relationId));
|
|
2974
|
-
if (tagged.length === 0) return void 0;
|
|
2975
|
-
if (tagged.some((relation) => relation.trace.signature !== envelope.signature)) throw new Error(`Speech effect "${envelope.effectId}" was already applied with different input`);
|
|
2976
|
-
const result = {
|
|
2977
|
-
speechClipEntityIds: [],
|
|
2978
|
-
speechAudioEntityIds: [],
|
|
2979
|
-
audioScriptEntityIds: [],
|
|
2980
|
-
phoneticScriptEntityIds: [],
|
|
2981
|
-
captionClipEntityIds: []
|
|
2982
|
-
};
|
|
2983
|
-
for (const [index, item] of envelope.input.value.items.entries()) {
|
|
2984
|
-
const relations = tagged.filter((relation) => relation.trace.itemIndex === index);
|
|
2985
|
-
const sourceReceipts = relations.filter((relation) => relation.relationKind === "audio-script-source");
|
|
2986
|
-
const contentRelations = relations.filter((relation) => relation.relationKind === "marker-content");
|
|
2987
|
-
const speechContent = contentRelations.filter((relation) => relation.trace.role === "speech-content");
|
|
2988
|
-
const captionContent = contentRelations.filter((relation) => relation.trace.role === "caption-content");
|
|
2989
|
-
const receiptMarkers = relations.filter((relation) => relation.relationKind === "audio-script-marker");
|
|
2990
|
-
const renders = relations.filter((relation) => relation.relationKind === "phonetic-script-render");
|
|
2991
|
-
if (speechContent.length > 1) throw new Error(`Speech effect item ${index} has an ambiguous Speech occurrence receipt`);
|
|
2992
|
-
if (speechContent.length === 0) throw new Error(`Speech effect item ${index} has an incomplete Speech occurrence receipt`);
|
|
2993
|
-
if (contentRelations.length !== speechContent.length + captionContent.length) throw new Error(`Speech effect item ${index} has an unrecognized content receipt`);
|
|
2994
|
-
const captionReceipts = item.captions.map((_, captionIndex) => captionContent.filter((relation) => relation.trace.captionIndex === captionIndex));
|
|
2995
|
-
if (captionContent.some((relation) => !Number.isInteger(relation.trace.captionIndex) || relation.trace.captionIndex < 0 || relation.trace.captionIndex >= item.captions.length) || captionReceipts.some((matches) => matches.length > 1)) throw new Error(`Speech effect item ${index} has an ambiguous Caption receipt`);
|
|
2996
|
-
const sourceIndexes = sourceReceipts.map((relation) => relation.trace.sourceIndex);
|
|
2997
|
-
const expectedSourceCount = item.source.kind === "audio-script" ? item.source.sourceMediaEntityIds.length : void 0;
|
|
2998
|
-
if ((expectedSourceCount === void 0 ? sourceReceipts.length > 0 : sourceIndexes.some((sourceIndex) => !Number.isInteger(sourceIndex) || sourceIndex < 0 || sourceIndex >= expectedSourceCount)) || new Set(sourceIndexes).size !== sourceIndexes.length) throw new Error(`Speech effect item ${index} has an ambiguous AudioScript source receipt`);
|
|
2999
|
-
if (receiptMarkers.length > 1) throw new Error("Speech has an ambiguous AudioScript receipt");
|
|
3000
|
-
if (item.renderMode === "recorded") {
|
|
3001
|
-
if (sourceReceipts.length > 1) throw new Error("Recorded speech has an ambiguous AudioScript source receipt");
|
|
3002
|
-
if (sourceReceipts.length === 0) throw new Error("Recorded speech has an incomplete AudioScript source receipt");
|
|
3003
|
-
if (renders.length !== 0) throw new Error("Recorded speech must not have render lineage");
|
|
3004
|
-
} else if (renders.length > 1) throw new Error(`Speech effect item ${index} has an ambiguous render receipt`);
|
|
3005
|
-
else if (renders.length === 0) throw new Error(`Speech effect item ${index} has an incomplete render receipt`);
|
|
3006
|
-
const historicalRecorded = item.renderMode === "recorded" && (speechContent.some((relation) => historicalIds.has(relation.relationId)) || sourceReceipts.some((relation) => historicalIds.has(relation.relationId)));
|
|
3007
|
-
let audio;
|
|
3008
|
-
let speechClip;
|
|
3009
|
-
const speechReceipt = speechContent[0];
|
|
3010
|
-
const speechReceiptIsHistorical = historicalIds.has(speechReceipt.relationId);
|
|
3011
|
-
const speechKind = item.renderMode === "recorded" ? "audio" : "voiceover";
|
|
3012
|
-
if (speechReceiptIsHistorical) {
|
|
3013
|
-
requireHistoricalEntity(history?.getVersion(speechReceipt.endpoint1EntityId), speechReceipt.endpoint1EntityId, speechKind);
|
|
3014
|
-
requireHistoricalEntity(history?.getVersion(speechReceipt.endpoint0EntityId), speechReceipt.endpoint0EntityId, "sequence-marker");
|
|
3015
|
-
}
|
|
3016
|
-
const currentSpeechId = speechReceiptIsHistorical ? history.resolveCurrentEntityId(speechReceipt.endpoint1EntityId) : speechReceipt.endpoint1EntityId;
|
|
3017
|
-
const currentMarkerId = speechReceiptIsHistorical ? history.resolveCurrentEntityId(speechReceipt.endpoint0EntityId) : speechReceipt.endpoint0EntityId;
|
|
3018
|
-
audio = requireEntityKind(rows, currentSpeechId, speechKind);
|
|
3019
|
-
const speechMarker = requireEntityKind(rows, currentMarkerId, "sequence-marker");
|
|
3020
|
-
speechClip = requireSingleClipForMarker(rows, speechMarker.entityId, "speech");
|
|
3021
|
-
const currentSpeechContent = requireOneRelation(rows, speechMarker.entityId, "marker-content");
|
|
3022
|
-
if (currentSpeechContent.endpoint0EntityId !== speechMarker.entityId || currentSpeechContent.endpoint1EntityId !== audio.entityId) throw new Error("Speech occurrence receipt no longer resolves to its current content");
|
|
3023
|
-
let audioScriptId;
|
|
3024
|
-
let phoneticScriptEntityId;
|
|
3025
|
-
if (item.renderMode === "synthesized") {
|
|
3026
|
-
const voiceover = requireEntityKind(rows, audio.entityId, "voiceover");
|
|
3027
|
-
requireSynthesizedSpeechIdentity(rows, voiceover, item.media);
|
|
3028
|
-
if (renders[0].endpoint0EntityId !== audio.entityId) throw new Error(`Speech effect item ${index} has an incomplete render receipt`);
|
|
3029
|
-
const phonetic = requireEntityKind(rows, renders[0].endpoint1EntityId, "phonetic-script");
|
|
3030
|
-
const currentRenders = incidentRelations(rows, voiceover.entityId).filter((relation) => relation.relationKind === "phonetic-script-render");
|
|
3031
|
-
if (currentRenders.length !== 1 || currentRenders[0].endpoint0EntityId !== voiceover.entityId || currentRenders[0].endpoint1EntityId !== phonetic.entityId) throw new Error("Synthesized speech has ambiguous or conflicting current render lineage");
|
|
3032
|
-
audioScriptId = requireComposedAudioScript(rows, phonetic.entityId);
|
|
3033
|
-
phoneticScriptEntityId = phonetic.entityId;
|
|
3034
|
-
} else {
|
|
3035
|
-
audio = requireEntityKind(rows, audio.entityId, "audio");
|
|
3036
|
-
requireRecordedAudioAsset(rows, audio, item.media);
|
|
3037
|
-
if (sourceReceipts[0].endpoint1EntityId !== audio.entityId) throw new Error("Recorded speech has an incomplete AudioScript source receipt");
|
|
3038
|
-
if (item.source.audioScriptEntityId !== void 0 && sourceReceipts[0].endpoint0EntityId !== item.source.audioScriptEntityId) throw new Error("Recorded speech receipt does not match the requested AudioScript identity");
|
|
3039
|
-
const receiptScriptId = sourceReceipts[0].endpoint0EntityId;
|
|
3040
|
-
const sourceReceiptIsHistorical = historicalIds.has(sourceReceipts[0].relationId);
|
|
3041
|
-
const audioScript = sourceReceiptIsHistorical ? requireHistoricalEntity(history?.getVersion(receiptScriptId), receiptScriptId, "audio-script") : requireEntityKind(rows, receiptScriptId, "audio-script");
|
|
3042
|
-
const expected = audioScriptSegmentsFromAsr(item.source.segments, item.source.audioScriptEntityId !== void 0);
|
|
3043
|
-
if (canonicalJson(audioScript.payload.segments) !== canonicalJson(expected)) throw new Error("Recorded speech AudioScript conflicts with its immutable ASR source");
|
|
3044
|
-
const currentScriptId = sourceReceiptIsHistorical ? createEntityId(history.resolveCurrentEntityId(receiptScriptId)) : audioScript.entityId;
|
|
3045
|
-
requireNoAudioScriptAsset(rows, currentScriptId, "ASR-composed");
|
|
3046
|
-
if (receiptMarkers.length === 1) {
|
|
3047
|
-
const markerScriptId = historicalIds.has(receiptMarkers[0].relationId) ? audioScript.entityId : currentScriptId;
|
|
3048
|
-
if (receiptMarkers[0].endpoint0EntityId !== markerScriptId) throw new Error("Recorded speech marker receipt conflicts with its AudioScript source receipt");
|
|
3049
|
-
}
|
|
3050
|
-
const sources = incidentRelations(rows, audio.entityId).filter((relation) => relation.relationKind === "audio-script-source");
|
|
3051
|
-
if (sources.length > 1 || sources.length === 1 && (sources[0].endpoint1EntityId !== audio.entityId || requireEntityKind(rows, sources[0].endpoint0EntityId, "audio-script").entityKind !== "audio-script")) throw new Error("Recorded speech has ambiguous or reversed current AudioScript source lineage");
|
|
3052
|
-
if (!historicalRecorded && (sources.length !== 1 || sources[0].endpoint0EntityId !== currentScriptId)) throw new Error("Recorded speech has an incomplete AudioScript source receipt");
|
|
3053
|
-
if (incidentRelations(rows, audio.entityId).some((relation) => relation.relationKind === "voice-timbre" || relation.relationKind === "phonetic-script-render")) throw new Error("Recorded speech must not have Voice or render lineage");
|
|
3054
|
-
audioScriptId = currentScriptId;
|
|
3055
|
-
phoneticScriptEntityId = null;
|
|
3056
|
-
}
|
|
3057
|
-
const captionIds = [];
|
|
3058
|
-
for (let captionIndex = 0; captionIndex < item.captions.length; captionIndex += 1) {
|
|
3059
|
-
const matches = captionReceipts[captionIndex];
|
|
3060
|
-
if (matches.length !== 1) throw new Error(`Speech effect item ${index} Caption receipt is incomplete`);
|
|
3061
|
-
if (assembleCaptionContent(rows, requireEntityKind(rows, matches[0].endpoint1EntityId, "caption").entityId).audioScript.entityId !== audioScriptId) throw new Error("Speech Caption must retain the same AudioScript base");
|
|
3062
|
-
captionIds.push(requireSingleClipForMarker(rows, matches[0].endpoint0EntityId, "caption").entityId);
|
|
3063
|
-
}
|
|
3064
|
-
result.speechClipEntityIds.push(speechClip.entityId);
|
|
3065
|
-
result.speechAudioEntityIds.push(audio.entityId);
|
|
3066
|
-
result.audioScriptEntityIds.push(audioScriptId);
|
|
3067
|
-
result.phoneticScriptEntityIds.push(phoneticScriptEntityId);
|
|
3068
|
-
result.captionClipEntityIds.push(captionIds);
|
|
3069
|
-
}
|
|
3070
|
-
return result;
|
|
3071
|
-
}
|
|
3072
|
-
function requireSingleClipForMarker(rows, markerEntityId, role) {
|
|
3073
|
-
const links = incidentRelations(rows, markerEntityId).filter((relation) => relation.relationKind === "clip-marker");
|
|
3074
|
-
if (links.length !== 1 || links[0].endpoint1EntityId !== markerEntityId) throw new Error("clip-marker must use one ordered [Clip, SequenceMarker] relation");
|
|
3075
|
-
return requireClipOnRole(rows, links[0].endpoint0EntityId, role).clip;
|
|
3076
|
-
}
|
|
3077
|
-
function resolveGeneratedVisualDecisions(rows, input, identityScope) {
|
|
3078
|
-
const timeline = requireEntityKind(rows, input.timelineEntityId, "timeline");
|
|
3079
|
-
const anchorInput = visualInsertionAnchor(input);
|
|
3080
|
-
if (anchorInput.position === "track_start") {
|
|
3081
|
-
const trackEntityId = resolveEffectTrackId(rows, timeline.entityId, "video_clip", identityScope);
|
|
3082
|
-
const clips = resolveEntityTimelineLayout(rows).tracks.find((candidate) => candidate.entityId === trackEntityId)?.clips ?? [];
|
|
3083
|
-
if (clips.some((clip) => clip.mode !== "sequential")) throw new Error("Generated visual insertion requires a fully sequential Video Track");
|
|
3084
|
-
return {
|
|
3085
|
-
trackEntityId,
|
|
3086
|
-
anchorOrder: null,
|
|
3087
|
-
anchorPosition: "track_start",
|
|
3088
|
-
insertionOrders: visualInsertionOrders(rows, clips.map((clip) => clip.clipEntityId), 0, input.clips.length)
|
|
3089
|
-
};
|
|
3090
|
-
}
|
|
3091
|
-
const anchor = requireCanonicalClipOnRole(rows, anchorInput.clipEntityId, "video_clip");
|
|
3092
|
-
const track = requireEntityKind(rows, anchor.trackRelation.endpoint0EntityId, "track");
|
|
3093
|
-
if (anchor.trackRelation.endpoint1EntityId !== anchor.clip.entityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
|
|
3094
|
-
const timelineLinks = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
|
|
3095
|
-
if (timelineLinks.length !== 1) throw new Error(`Video Track "${track.entityId}" must have one unambiguous Timeline`);
|
|
3096
|
-
const timelineLink = timelineLinks[0];
|
|
3097
|
-
if (timelineLink.endpoint0EntityId !== timeline.entityId || timelineLink.endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use ordered endpoints [Timeline, Track]");
|
|
3098
|
-
const resolvedTrack = resolveEntityTimelineLayout(rows).tracks.find((candidate) => candidate.entityId === track.entityId);
|
|
3099
|
-
const anchorOrder = resolvedTrack?.clips.findIndex((clip) => clip.clipEntityId === anchor.clip.entityId) ?? -1;
|
|
3100
|
-
if (anchorOrder < 0) throw new Error(`Visual anchor Clip "${anchor.clip.entityId}" is not placed on its Track`);
|
|
3101
|
-
if (resolvedTrack.clips.some((clip) => clip.mode !== "sequential")) throw new Error("Generated visual insertion requires a fully sequential Video Track");
|
|
3102
|
-
const insertionIndex = anchorOrder + (anchorInput.position === "after" ? 1 : 0);
|
|
3103
|
-
return {
|
|
3104
|
-
trackEntityId: track.entityId,
|
|
3105
|
-
anchorOrder,
|
|
3106
|
-
anchorPosition: anchorInput.position,
|
|
3107
|
-
insertionOrders: visualInsertionOrders(rows, resolvedTrack.clips.map((clip) => clip.clipEntityId), insertionIndex, input.clips.length)
|
|
3108
|
-
};
|
|
3109
|
-
}
|
|
3110
|
-
function visualInsertionOrders(rows, orderedClipIds, insertionIndex, count) {
|
|
3111
|
-
const orders = orderedClipIds.map((clipId) => {
|
|
3112
|
-
const order = requireEntityKind(rows, clipId, "clip").payload.order;
|
|
3113
|
-
if (typeof order !== "number" || !Number.isFinite(order)) throw new Error(`Sequential Clip "${clipId}" requires a finite order`);
|
|
3114
|
-
return order;
|
|
3115
|
-
});
|
|
3116
|
-
if (orders.some((order, index) => index > 0 && order <= orders[index - 1])) throw new Error("Generated visual insertion requires unambiguous increasing Clip orders");
|
|
3117
|
-
const lower = orders[insertionIndex - 1];
|
|
3118
|
-
const upper = orders[insertionIndex];
|
|
3119
|
-
if (lower === void 0 && upper === void 0) return Array.from({ length: count }, (_, index) => index);
|
|
3120
|
-
if (lower === void 0) return Array.from({ length: count }, (_, index) => upper - count + index);
|
|
3121
|
-
if (upper === void 0) return Array.from({ length: count }, (_, index) => lower + index + 1);
|
|
3122
|
-
const gap = (upper - lower) / (count + 1);
|
|
3123
|
-
const result = Array.from({ length: count }, (_, index) => lower + gap * (index + 1));
|
|
3124
|
-
if (result.some((order, index) => !Number.isFinite(order) || order <= (index === 0 ? lower : result[index - 1]) || order >= upper)) throw new Error("Clip order precision is insufficient for generated visual insertion");
|
|
3125
|
-
return result;
|
|
3126
|
-
}
|
|
3127
|
-
function visualInsertionAnchor(input) {
|
|
3128
|
-
const before = input.beforeClipEntityId;
|
|
3129
|
-
const after = input.afterClipEntityId;
|
|
3130
|
-
if (before !== void 0 && after !== void 0) throw new Error("Generated visual effect cannot set both Clip anchors");
|
|
3131
|
-
if (before === void 0 && after === void 0) return { position: "track_start" };
|
|
3132
|
-
return before === void 0 ? {
|
|
3133
|
-
position: "after",
|
|
3134
|
-
clipEntityId: requireTrimmed(after, "After Clip anchor")
|
|
3135
|
-
} : {
|
|
3136
|
-
position: "before",
|
|
3137
|
-
clipEntityId: requireTrimmed(before, "Before Clip anchor")
|
|
3138
|
-
};
|
|
3139
|
-
}
|
|
3140
|
-
function generatedVisualSignature(input) {
|
|
3141
|
-
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(input))));
|
|
3142
|
-
}
|
|
3143
|
-
function assertGeneratedVisualEnvelope(value) {
|
|
3144
|
-
if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || requireTrimmed(value.baselineFingerprint, "Generated visual baseline fingerprint") !== value.baselineFingerprint || requireTrimmed(value.effectId, "Generated visual effect id") !== value.effectId || generatedVisualSignature(value.input) !== value.signature) throw new Error("Generated visual effect requires a canonical frozen envelope");
|
|
3145
|
-
}
|
|
3146
|
-
function generatedVisualTrace(envelope, itemIndex, generationJobId, sourceIndex) {
|
|
3147
|
-
return {
|
|
3148
|
-
command: generatedVisualsCommand,
|
|
3149
|
-
effectId: envelope.effectId,
|
|
3150
|
-
signature: envelope.signature,
|
|
3151
|
-
itemIndex,
|
|
3152
|
-
generationJobId,
|
|
3153
|
-
...sourceIndex === void 0 ? {} : { sourceIndex }
|
|
3154
|
-
};
|
|
3155
|
-
}
|
|
3156
|
-
function resolveGeneratedVisualReplay(rows, envelope) {
|
|
3157
|
-
const tagged = rows.relations.filter((relation) => relation.trace.command === generatedVisualsCommand && relation.trace.effectId === envelope.effectId);
|
|
3158
|
-
if (tagged.length === 0) return void 0;
|
|
3159
|
-
if (tagged.some((relation) => relation.trace.signature !== envelope.signature)) throw new Error(`Generated visual effect "${envelope.effectId}" was already applied with different input`);
|
|
3160
|
-
const clipEntityIds = [];
|
|
3161
|
-
const mediaEntityIds = [];
|
|
3162
|
-
let expectedCount = 0;
|
|
3163
|
-
for (const [index, clipInput] of envelope.input.clips.entries()) {
|
|
3164
|
-
const item = tagged.filter((relation) => relation.trace.itemIndex === index);
|
|
3165
|
-
const trackLink = requireGeneratedVisualRelation(item, "track-clip", index);
|
|
3166
|
-
if (trackLink.endpoint0EntityId !== envelope.decisions.trackEntityId || requireEntityKind(rows, trackLink.endpoint1EntityId, "clip").entityId !== trackLink.endpoint1EntityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
|
|
3167
|
-
const clipEntityId = trackLink.endpoint1EntityId;
|
|
3168
|
-
const markerLink = requireGeneratedVisualRelation(item, "clip-marker", index);
|
|
3169
|
-
if (markerLink.endpoint0EntityId !== clipEntityId) throw new Error("clip-marker must use ordered endpoints [Clip, SequenceMarker]");
|
|
3170
|
-
const marker = requireEntityKind(rows, markerLink.endpoint1EntityId, "sequence-marker");
|
|
3171
|
-
const contentLink = requireGeneratedVisualRelation(item, "marker-content", index);
|
|
3172
|
-
if (contentLink.endpoint0EntityId !== marker.entityId) throw new Error("marker-content must use ordered endpoints [SequenceMarker, media]");
|
|
3173
|
-
const media = requireSequenceContent(rows, contentLink.endpoint1EntityId);
|
|
3174
|
-
if (media.entityKind !== "video" && media.entityKind !== "image") throw new Error("Generated visual receipt must reference Video or Image output");
|
|
3175
|
-
const lineage = item.filter((relation) => relation.relationKind === "generated");
|
|
3176
|
-
if (lineage.length !== clipInput.sourceMediaEntityIds.length) throw new Error(`Generated visual ${index} has incomplete source lineage`);
|
|
3177
|
-
for (const [sourceIndex, sourceId] of clipInput.sourceMediaEntityIds.entries()) {
|
|
3178
|
-
const matches = lineage.filter((relation) => relation.trace.sourceIndex === sourceIndex);
|
|
3179
|
-
if (matches.length !== 1 || matches[0].endpoint0EntityId !== media.entityId || matches[0].endpoint1EntityId !== sourceId) throw new Error("generated must use ordered endpoints [output media, input media]");
|
|
3180
|
-
}
|
|
3181
|
-
expectedCount += 3 + clipInput.sourceMediaEntityIds.length;
|
|
3182
|
-
clipEntityIds.push(clipEntityId);
|
|
3183
|
-
mediaEntityIds.push(media.entityId);
|
|
3184
|
-
}
|
|
3185
|
-
if (tagged.length !== expectedCount) throw new Error(`Generated visual effect "${envelope.effectId}" has an ambiguous relation receipt`);
|
|
3186
|
-
return {
|
|
3187
|
-
clipEntityIds,
|
|
3188
|
-
mediaEntityIds
|
|
3189
|
-
};
|
|
3190
|
-
}
|
|
3191
|
-
function requireGeneratedVisualRelation(relations, kind, itemIndex) {
|
|
3192
|
-
const matches = relations.filter((relation) => relation.relationKind === kind);
|
|
3193
|
-
if (matches.length !== 1) throw new Error(`Generated visual ${itemIndex} must have exactly one ${kind} Relation`);
|
|
3194
|
-
return matches[0];
|
|
3195
|
-
}
|
|
3196
|
-
function assertCaptionAssetsEnvelope(value) {
|
|
3197
|
-
if (value.version !== 1 || !Object.isFrozen(value) || !Object.isFrozen(value.input) || !Object.isFrozen(value.decisions) || requireTrimmed(value.baselineFingerprint, "Caption baseline fingerprint") !== value.baselineFingerprint || requireTrimmed(value.signature, "Caption effect signature") !== value.signature) throw new Error("Caption effect requires a canonical frozen envelope");
|
|
3198
|
-
}
|
|
3199
|
-
function deepFreeze(value) {
|
|
3200
|
-
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
3201
|
-
for (const child of Object.values(value)) deepFreeze(child);
|
|
3202
|
-
Object.freeze(value);
|
|
3203
|
-
}
|
|
3204
|
-
return value;
|
|
3205
|
-
}
|
|
3206
|
-
function captionEffectSignature(input, segments) {
|
|
3207
|
-
const value = JSON.stringify({
|
|
3208
|
-
timelineEntityId: input.timelineEntityId,
|
|
3209
|
-
trackEntityId: input.trackEntityId,
|
|
3210
|
-
source: {
|
|
3211
|
-
entityId: input.source.entityId,
|
|
3212
|
-
entityKind: input.source.entityKind
|
|
3213
|
-
},
|
|
3214
|
-
sourceOccurrence: {
|
|
3215
|
-
clipEntityId: input.sourceOccurrence.clipEntityId,
|
|
3216
|
-
markerEntityId: input.sourceOccurrence.markerEntityId
|
|
3217
|
-
},
|
|
3218
|
-
captionAsset: {
|
|
3219
|
-
assetId: input.captionAsset.assetId,
|
|
3220
|
-
...input.captionAsset.storageKey === void 0 ? {} : { storageKey: input.captionAsset.storageKey }
|
|
3221
|
-
},
|
|
3222
|
-
segments
|
|
3223
|
-
});
|
|
3224
|
-
return bytesToHex(sha256(new TextEncoder().encode(value)));
|
|
3225
|
-
}
|
|
3226
|
-
function effectTrace(effectId, signature, segmentId) {
|
|
3227
|
-
return {
|
|
3228
|
-
command: captionAssetsCommand,
|
|
3229
|
-
effectId,
|
|
3230
|
-
signature,
|
|
3231
|
-
...segmentId === void 0 ? {} : { segmentId }
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
function stableEffectId(kind, effectId, localKey) {
|
|
3235
|
-
return `${kind}_effect_${bytesToHex(sha256(new TextEncoder().encode(`${captionAssetsCommand}\u0000${effectId}\u0000${kind}\u0000${localKey}`))).slice(0, 40)}`;
|
|
3236
|
-
}
|
|
3237
|
-
function stableEffectScope(command, effectId, baselineFingerprint) {
|
|
3238
|
-
return `${command}\u0000${effectId}\u0000${baselineFingerprint}`;
|
|
3239
|
-
}
|
|
3240
|
-
function stableEffectIdFactory(effectId, namespace) {
|
|
3241
|
-
const counters = {
|
|
3242
|
-
entity: 0,
|
|
3243
|
-
relation: 0
|
|
3244
|
-
};
|
|
3245
|
-
return (kind) => stableEffectId(kind, effectId, `${namespace}:${kind}:${counters[kind]++}`);
|
|
3246
|
-
}
|
|
3247
|
-
function stableEffectPeerId(command, effectId, baselineFingerprint) {
|
|
3248
|
-
const digest = sha256(new TextEncoder().encode(`${command}\u0000${effectId}\u0000${baselineFingerprint}\u0000loro-peer`));
|
|
3249
|
-
return BigInt(`0x${bytesToHex(digest).slice(0, 15)}`).toString();
|
|
3250
|
-
}
|
|
3251
|
-
function isCaptionEffectRelation(relation, effectId) {
|
|
3252
|
-
return relation.trace.command === captionAssetsCommand && relation.trace.effectId === effectId;
|
|
3253
|
-
}
|
|
3254
|
-
function resolveCaptionAssetsReplay(rows, input, effectId, signature, segments, sourceEntityId, trackEntityId) {
|
|
3255
|
-
const tagged = rows.relations.filter((relation) => isCaptionEffectRelation(relation, effectId));
|
|
3256
|
-
if (tagged.length === 0) return void 0;
|
|
3257
|
-
if (tagged.some((relation) => relation.trace.signature !== signature)) throw new Error(`Caption effect "${effectId}" was already applied with different input`);
|
|
3258
|
-
const sourceRelations = tagged.filter((relation) => relation.relationKind === "audio-script-source");
|
|
3259
|
-
if (sourceRelations.length !== 1) throw new Error(`Caption effect "${effectId}" has an ambiguous source receipt`);
|
|
3260
|
-
const sourceRelation = sourceRelations[0];
|
|
3261
|
-
if (sourceRelation.endpoint1EntityId !== sourceEntityId) throw new Error("audio-script-source must use ordered endpoints [AudioScript, source media]");
|
|
3262
|
-
const script = requireEntityKind(rows, sourceRelation.endpoint0EntityId, "audio-script");
|
|
3263
|
-
if (!sameScriptSegments(script.payload.segments, segments)) throw new Error(`Caption effect "${effectId}" AudioScript no longer matches its immutable ASR result`);
|
|
3264
|
-
const scriptMarkerRelations = tagged.filter((relation) => relation.relationKind === "audio-script-marker");
|
|
3265
|
-
if (scriptMarkerRelations.length !== 1 || scriptMarkerRelations[0].endpoint0EntityId !== script.entityId) throw new Error("audio-script-marker must use ordered endpoints [AudioScript, annotation SequenceMarker]");
|
|
3266
|
-
const scriptMarker = requireEntityKind(rows, scriptMarkerRelations[0].endpoint1EntityId, "sequence-marker");
|
|
3267
|
-
if (!sameSegmentRanges(scriptMarker.payload.segmentRanges, segments)) throw new Error(`Caption effect "${effectId}" annotation Marker no longer matches its immutable ASR timing`);
|
|
3268
|
-
let assetEntityId;
|
|
3269
|
-
const captionEntityIds = [];
|
|
3270
|
-
const captionClipEntityIds = [];
|
|
3271
|
-
for (const segment of segments) {
|
|
3272
|
-
const relations = tagged.filter((relation) => relation.trace.segmentId === segment.segmentId);
|
|
3273
|
-
const assetLink = requireTaggedRelation(relations, "from-asset", segment.segmentId);
|
|
3274
|
-
const caption = requireEntityKind(rows, assetLink.endpoint0EntityId, "caption");
|
|
3275
|
-
const asset = requireEntityKind(rows, assetLink.endpoint1EntityId, "asset");
|
|
3276
|
-
if (assetEntityId !== void 0 && asset.entityId !== assetEntityId) throw new Error(`Caption effect "${effectId}" references multiple Caption Assets`);
|
|
3277
|
-
assetEntityId = asset.entityId;
|
|
3278
|
-
if (asset.payload.system !== "memota" || asset.payload.key !== input.captionAsset.assetId || input.captionAsset.storageKey !== void 0 && asset.payload.storageKey !== input.captionAsset.storageKey) throw new Error(`Caption effect "${effectId}" Caption Asset does not match its receipt`);
|
|
3279
|
-
const selection = caption.payload.selection;
|
|
3280
|
-
const bases = caption.payload.baseEntityIds;
|
|
3281
|
-
if (caption.payload.durationMs !== segment.endMs - segment.startMs || !isJsonObject(selection) || selection.segmentId !== segment.segmentId || !Array.isArray(bases) || bases.length !== 1 || bases[0] !== script.entityId) throw new Error(`Caption effect "${effectId}" Caption "${caption.entityId}" conflicts with its ASR segment`);
|
|
3282
|
-
const contentLink = requireTaggedRelation(relations, "marker-content", segment.segmentId);
|
|
3283
|
-
if (contentLink.endpoint1EntityId !== caption.entityId) throw new Error("marker-content must use ordered endpoints [SequenceMarker, Caption]");
|
|
3284
|
-
const marker = requireEntityKind(rows, contentLink.endpoint0EntityId, "sequence-marker");
|
|
3285
|
-
const markerLink = requireTaggedRelation(relations, "clip-marker", segment.segmentId);
|
|
3286
|
-
if (markerLink.endpoint1EntityId !== marker.entityId) throw new Error("clip-marker must use ordered endpoints [Clip, SequenceMarker]");
|
|
3287
|
-
const clip = requireEntityKind(rows, markerLink.endpoint0EntityId, "clip");
|
|
3288
|
-
const trackLink = requireTaggedRelation(relations, "track-clip", segment.segmentId);
|
|
3289
|
-
if (trackLink.endpoint0EntityId !== trackEntityId || trackLink.endpoint1EntityId !== clip.entityId) throw new Error("track-clip must use ordered endpoints [Track, Clip]");
|
|
3290
|
-
captionEntityIds.push(caption.entityId);
|
|
3291
|
-
captionClipEntityIds.push(clip.entityId);
|
|
3292
|
-
}
|
|
3293
|
-
if (assetEntityId === void 0 || tagged.length !== 2 + segments.length * 4) throw new Error(`Caption effect "${effectId}" has an incomplete or ambiguous relation receipt`);
|
|
3294
|
-
return {
|
|
3295
|
-
audioScriptEntityId: script.entityId,
|
|
3296
|
-
audioScriptMarkerEntityId: scriptMarker.entityId,
|
|
3297
|
-
captionAssetEntityId: assetEntityId,
|
|
3298
|
-
captionEntityIds,
|
|
3299
|
-
captionClipEntityIds
|
|
3300
|
-
};
|
|
3301
|
-
}
|
|
3302
|
-
function requireTaggedRelation(relations, kind, segmentId) {
|
|
3303
|
-
const matches = relations.filter((relation) => relation.relationKind === kind);
|
|
3304
|
-
if (matches.length !== 1) throw new Error(`ASR segment "${segmentId}" must have exactly one ${kind} Relation`);
|
|
3305
|
-
return matches[0];
|
|
3306
|
-
}
|
|
3307
|
-
function sameScriptSegments(value, segments) {
|
|
3308
|
-
if (!Array.isArray(value) || value.length !== segments.length) return false;
|
|
3309
|
-
return value.every((entry, index) => {
|
|
3310
|
-
if (!isJsonObject(entry)) return false;
|
|
3311
|
-
const expected = segments[index];
|
|
3312
|
-
return entry.segmentId === expected.segmentId && entry.text === expected.text && entry.language === expected.language && Object.keys(entry).every((key) => key === "segmentId" || key === "text" || key === "language");
|
|
3313
|
-
});
|
|
3314
|
-
}
|
|
3315
|
-
function sameSegmentRanges(value, segments) {
|
|
3316
|
-
if (!Array.isArray(value) || value.length !== segments.length) return false;
|
|
3317
|
-
return value.every((entry, index) => {
|
|
3318
|
-
if (!isJsonObject(entry)) return false;
|
|
3319
|
-
const expected = segments[index];
|
|
3320
|
-
return entry.segmentId === expected.segmentId && entry.startMs === expected.startMs && entry.endMs === expected.endMs && Object.keys(entry).every((key) => key === "segmentId" || key === "startMs" || key === "endMs");
|
|
3321
|
-
});
|
|
3322
|
-
}
|
|
3323
|
-
function requireTrimmed(value, label) {
|
|
3324
|
-
if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new Error(`${label} must be non-empty and trimmed`);
|
|
3325
|
-
return value;
|
|
3326
|
-
}
|
|
3327
|
-
function safeWholeMs(value, label) {
|
|
3328
|
-
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label} must be a non-negative whole millisecond`);
|
|
3329
|
-
return value;
|
|
3330
|
-
}
|
|
3331
1826
|
function validateFinalRows(rows) {
|
|
3332
1827
|
decodeEntityRelationRows(rows, numericMarkerValidation);
|
|
3333
1828
|
assertCanonicalEditorResources(rows);
|
|
@@ -3365,16 +1860,6 @@ function requireClipTopology(rows, clipEntityId) {
|
|
|
3365
1860
|
markerContentRelation
|
|
3366
1861
|
};
|
|
3367
1862
|
}
|
|
3368
|
-
function requireCanonicalClipOnRole(rows, clipEntityId, role) {
|
|
3369
|
-
const topology = requireClipTopology(rows, clipEntityId);
|
|
3370
|
-
if (topology.trackRelation.endpoint1EntityId !== topology.clip.entityId || topology.clipMarkerRelation.endpoint0EntityId !== topology.clip.entityId || topology.clipMarkerRelation.endpoint1EntityId !== topology.marker.entityId || topology.markerContentRelation.endpoint0EntityId !== topology.marker.entityId) throw new Error("Occurrence must use ordered Track→Clip→SequenceMarker→content endpoints");
|
|
3371
|
-
const track = requireEntityKind(rows, topology.trackRelation.endpoint0EntityId, "track");
|
|
3372
|
-
if (track.payload.role !== role) throw new Error(`Clip "${clipEntityId}" must belong to the ${role} Track`);
|
|
3373
|
-
const timelineLinks = incidentRelations(rows, track.entityId).filter((relation) => relation.relationKind === "timeline-track");
|
|
3374
|
-
if (timelineLinks.length !== 1 || timelineLinks[0].endpoint1EntityId !== track.entityId) throw new Error("timeline-track must use one ordered [Timeline, Track] relation");
|
|
3375
|
-
requireEntityKind(rows, timelineLinks[0].endpoint0EntityId, "timeline");
|
|
3376
|
-
return topology;
|
|
3377
|
-
}
|
|
3378
1863
|
function requireOneRelation(rows, entityId, relationKind) {
|
|
3379
1864
|
const matches = incidentRelations(rows, entityId).filter((relation) => relation.relationKind === relationKind);
|
|
3380
1865
|
if (matches.length !== 1) throw new Error(`Entity "${entityId}" must have exactly one ${relationKind} Relation`);
|
|
@@ -3387,11 +1872,6 @@ function requireEntityKind(rows, entityIdValue, entityKind) {
|
|
|
3387
1872
|
if (entity.entityKind !== entityKind) throw new Error(`Entity "${entityId}" must have kind "${entityKind}", got "${entity.entityKind}"`);
|
|
3388
1873
|
return entity;
|
|
3389
1874
|
}
|
|
3390
|
-
function requireHistoricalEntity(entity, entityId, entityKind) {
|
|
3391
|
-
if (entity === void 0) throw new Error(`Historical Entity id "${entityId}" does not exist`);
|
|
3392
|
-
if (entity.entityKind !== entityKind) throw new Error(`Historical Entity "${entityId}" must have kind "${entityKind}", got "${entity.entityKind}"`);
|
|
3393
|
-
return entity;
|
|
3394
|
-
}
|
|
3395
1875
|
function requireSequenceContent(rows, entityIdValue) {
|
|
3396
1876
|
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(entityIdValue)) throw new Error("Timeline content must reference an existing Sequence media Entity id; a URL is not allowed");
|
|
3397
1877
|
const entityId = createEntityId(entityIdValue);
|
|
@@ -3431,11 +1911,6 @@ function requireAvailableEntityId(rows, value) {
|
|
|
3431
1911
|
if (rows.entities.some((entity) => entity.entityId === entityId)) throw new Error(`Entity id "${entityId}" already exists`);
|
|
3432
1912
|
return entityId;
|
|
3433
1913
|
}
|
|
3434
|
-
function requireAvailableRelationId(rows, value) {
|
|
3435
|
-
const relationId = createRelationId(value);
|
|
3436
|
-
if (rows.relations.some((relation) => relation.relationId === relationId)) throw new Error(`Relation id "${relationId}" already exists`);
|
|
3437
|
-
return relationId;
|
|
3438
|
-
}
|
|
3439
1914
|
function setClipOrder(rows, clipEntityId, order) {
|
|
3440
1915
|
const clip = requireEntityKind(rows, clipEntityId, "clip");
|
|
3441
1916
|
replaceEntity(rows, clip.entityId, {
|
|
@@ -3511,15 +1986,6 @@ function linearRate(value) {
|
|
|
3511
1986
|
if (!isJsonObject(value) || value.kind !== "linear") throw new Error("Only linear Clip time remapping is supported");
|
|
3512
1987
|
return finitePositive(value.rate, "linear speed rate");
|
|
3513
1988
|
}
|
|
3514
|
-
function speedModeRate(mode) {
|
|
3515
|
-
switch (mode) {
|
|
3516
|
-
case "0.5x": return .5;
|
|
3517
|
-
case "0.75x": return .75;
|
|
3518
|
-
case "1.0x": return 1;
|
|
3519
|
-
case "1.5x": return 1.5;
|
|
3520
|
-
case "2.0x": return 2;
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3523
1989
|
function resizeAbsoluteTarget(payload, duration) {
|
|
3524
1990
|
if (payload.targetRange === void 0) return;
|
|
3525
1991
|
const target = requirePositiveRange(payload.targetRange, "targetRange");
|
|
@@ -3573,6 +2039,161 @@ const numericMarkerValidation = { compareMarkerPoints: (_marker, _range, left, r
|
|
|
3573
2039
|
return left - right;
|
|
3574
2040
|
} };
|
|
3575
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
|
+
//#endregion
|
|
3576
2197
|
//#region src/entity-editor/legacy-entity-migration.ts
|
|
3577
2198
|
/** One-time import only. Ordinary editor operations must never reverse-diff a legacy document. */
|
|
3578
2199
|
function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
@@ -3753,10 +2374,6 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
|
|
|
3753
2374
|
selection,
|
|
3754
2375
|
...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
|
|
3755
2376
|
});
|
|
3756
|
-
link("caption-alignment", content, voiceover, { alignment: {
|
|
3757
|
-
start: caption.start_ms,
|
|
3758
|
-
end: caption.start_ms + duration
|
|
3759
|
-
} });
|
|
3760
2377
|
markerPayload = {
|
|
3761
2378
|
sourceRange: {
|
|
3762
2379
|
start: 0,
|
|
@@ -3962,7 +2579,6 @@ function assertPartSupported(id, part, role) {
|
|
|
3962
2579
|
} else if (part.speech !== void 0) {
|
|
3963
2580
|
const speech = part.speech;
|
|
3964
2581
|
const phonemeScript = speech.phoneme_script;
|
|
3965
|
-
const originAssetSystem = speech.origin_asset_system;
|
|
3966
2582
|
if (speech.id !== id || speech.kind !== "speech" || !trimmed(speech.origin_speech_id) || !trimmed(speech.audio_storage_key) || typeof speech.audio_script !== "string") fail("Migration requires complete Speech identity and physical take facts");
|
|
3967
2583
|
requirePositiveMilliseconds(speech.media_duration_ms, "Speech duration");
|
|
3968
2584
|
requireFinite(speech.volume, "Speech volume");
|
|
@@ -3971,7 +2587,6 @@ function assertPartSupported(id, part, role) {
|
|
|
3971
2587
|
if (!Array.isArray(speech.caption_ids) || speech.caption_ids.some((captionId) => !trimmed(captionId))) fail("Migration requires explicit Speech caption identities");
|
|
3972
2588
|
if (new Set(speech.caption_ids).size !== speech.caption_ids.length) fail("Migration requires unique Speech caption identities");
|
|
3973
2589
|
if (phonemeScript !== void 0 && phonemeScript !== null && typeof phonemeScript !== "string") fail("Migration requires a string phoneme_script when present");
|
|
3974
|
-
if (originAssetSystem !== void 0 && originAssetSystem !== "memota-speech") fail("Legacy synthesized Speech requires a memota-speech origin asset");
|
|
3975
2590
|
assertKnownFields(speech.voice, ["id", "name"], "voice");
|
|
3976
2591
|
assertKnownFields(speech, [
|
|
3977
2592
|
"id",
|
|
@@ -3981,7 +2596,6 @@ function assertPartSupported(id, part, role) {
|
|
|
3981
2596
|
"volume",
|
|
3982
2597
|
"audio_storage_key",
|
|
3983
2598
|
"origin_speech_id",
|
|
3984
|
-
"origin_asset_system",
|
|
3985
2599
|
"voice",
|
|
3986
2600
|
"caption_ids",
|
|
3987
2601
|
"phoneme_script"
|
|
@@ -6700,4 +5314,4 @@ function serverCovers(serverVV, target) {
|
|
|
6700
5314
|
return covers(server, target);
|
|
6701
5315
|
}
|
|
6702
5316
|
//#endregion
|
|
6703
|
-
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,
|
|
5317
|
+
export { DEFAULT_UNIT_TIME_MS, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, LORO_ENTITY_SCHEMA, LoroEntityDocument, LoroEntityDraft, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, ValidationError, VideoDocumentValidationError, applyEntityRows, applyFieldChanges, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertFieldChanges, assertMediaAssetWritePolicy, assertValidVideoDocument, audioScriptAssetContent, audioScriptAssetFields, audioScriptAssetHash, audioScriptAssetOf, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, compileEntityRows, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, phoneticScriptAssetContent, phoneticScriptAssetHash, phoneticScriptAssetOf, prepareCaptionContent, projectEntityTimeline, readDocumentAudioScript, readEntityDocument, readEntityRelationRecords, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveFieldPath, resolveSpeechOverlapByShiftingVideos, reviseAudioScript, safeDurationMs, schemas_exports as schemas, scriptSourceAssetOf, selectAudioScriptSegment, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema };
|