@mengine/medeo-client 3.0.1-tool-guardrails.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -5
- package/dist/{dsl-96AZnd7O.js → dsl-C0-G8JVT.js} +252 -329
- package/dist/dsl.d.ts +2 -2
- package/dist/dsl.js +2 -2
- package/dist/{index-C1iiDt_Q.d.ts → index-BOPcHqIj.d.ts} +47 -29
- package/dist/{index-CSjqNIoR.d.ts → index-C_wwGt5k.d.ts} +3 -7
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -16
- package/dist/testing.d.ts +1 -2
- package/dist/testing.js +14 -35
- package/package.json +4 -6
- package/dist/schemas.d.ts +0 -606
- package/dist/schemas.js +0 -478
|
@@ -145,12 +145,8 @@ function assertCaptionContent(value) {
|
|
|
145
145
|
}
|
|
146
146
|
const phonemeFields = {
|
|
147
147
|
...selectionFields,
|
|
148
|
-
phonemeScript: optional$1(
|
|
148
|
+
phonemeScript: optional$1(string)
|
|
149
149
|
};
|
|
150
|
-
/** D190: omitted pronunciation uses the selected source words; an authored pronunciation is never empty. */
|
|
151
|
-
function assertPhonemeScript(value) {
|
|
152
|
-
if (value !== void 0 && !nonempty(value)) throw new Error("phonemeScript must be nonempty when provided");
|
|
153
|
-
}
|
|
154
150
|
function assertPhoneme(value) {
|
|
155
151
|
if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
|
|
156
152
|
}
|
|
@@ -173,9 +169,39 @@ function assertScriptSegment(value) {
|
|
|
173
169
|
/** A script's segments: each with its own id, unique within the script. */
|
|
174
170
|
const segments = (v) => Array.isArray(v) && v.every(segment) && new Set(v.map((s) => s.id)).size === v.length;
|
|
175
171
|
const locator = {
|
|
176
|
-
system: required(oneOf("memota", "memota-speech")),
|
|
172
|
+
system: required(oneOf("memota", "memota-speech", "placeholder")),
|
|
177
173
|
key: required(nonempty)
|
|
178
174
|
};
|
|
175
|
+
/**
|
|
176
|
+
* A placeholder medium is an Image with no file, standing in where a legacy VideoDraft placed a
|
|
177
|
+
* visual clip the old editor drew as a blank (D196). The blank one stands in for a clip placed
|
|
178
|
+
* without media (`origin_media_id: ''`); one keyed by a memota media id stands in for media the
|
|
179
|
+
* clip named but Director found missing, so the id survives. Importing keeps the clips' time on
|
|
180
|
+
* them instead of dropping them. Only migration authors them.
|
|
181
|
+
*/
|
|
182
|
+
const BLANK_PLACEHOLDER = {
|
|
183
|
+
system: "placeholder",
|
|
184
|
+
key: "blank",
|
|
185
|
+
kind: "image"
|
|
186
|
+
};
|
|
187
|
+
/**
|
|
188
|
+
* Only a memota media id may key a missing-media placeholder: visual parts name media and nothing
|
|
189
|
+
* else, and a key of this shape can never be mistaken for a file.
|
|
190
|
+
*/
|
|
191
|
+
const MISSING_MEDIA_KEY = /^media_[0-9A-Za-z]+$/;
|
|
192
|
+
/** The placeholder for a memota media id Director found missing. */
|
|
193
|
+
function missingMediaPlaceholder(mediaId) {
|
|
194
|
+
return {
|
|
195
|
+
system: "placeholder",
|
|
196
|
+
key: mediaId,
|
|
197
|
+
kind: "image"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assertPlaceholder(media) {
|
|
201
|
+
if (media.system !== "placeholder") return;
|
|
202
|
+
if (media.kind !== "image" || media.key !== BLANK_PLACEHOLDER.key && !(typeof media.key === "string" && MISSING_MEDIA_KEY.test(media.key))) throw new Error("A placeholder is the blank Image or one standing in for a missing memota media id");
|
|
203
|
+
if (media.storageKey !== void 0 || media.digest !== void 0) throw new Error("A placeholder has no file");
|
|
204
|
+
}
|
|
179
205
|
const entityFields = {
|
|
180
206
|
"timeline-entities": {},
|
|
181
207
|
"track-entities": {
|
|
@@ -271,6 +297,7 @@ function assertEntity(table, value) {
|
|
|
271
297
|
checkFields(value, entityFields[table]);
|
|
272
298
|
if (table === "audio-script-entities") assertTranscriptFile(value);
|
|
273
299
|
if (table === "media-entities") {
|
|
300
|
+
assertPlaceholder(value);
|
|
274
301
|
if (value.kind === "image" && value.durationMs !== void 0) throw new Error("Image has no intrinsic duration");
|
|
275
302
|
if (value.kind !== "image" && !positive(value.durationMs)) throw new Error("Finite media requires durationMs");
|
|
276
303
|
if (["audio", "voiceover-recording"].includes(value.kind) && !nonempty(value.storageKey)) throw new Error("Audio requires storageKey");
|
|
@@ -299,6 +326,7 @@ function assertEntityPatch(table, patch, current) {
|
|
|
299
326
|
]) if (Object.hasOwn(patch, key) && patch[key] !== current[key]) throw new Error(`Resource identity field is immutable: ${key}`);
|
|
300
327
|
}
|
|
301
328
|
if (table === "media-entities") {
|
|
329
|
+
assertPlaceholder(next);
|
|
302
330
|
if (Object.hasOwn(patch, "durationMs")) {
|
|
303
331
|
if (next.kind === "image" ? next.durationMs !== void 0 : !positive(next.durationMs)) throw new Error("durationMs must match the media kind");
|
|
304
332
|
}
|
|
@@ -580,10 +608,9 @@ function assertPhonemeSelection(rows, selection, text) {
|
|
|
580
608
|
assertSegmentRange(selection, text);
|
|
581
609
|
if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
|
|
582
610
|
}
|
|
583
|
-
/**
|
|
611
|
+
/** A caption's text comes from a selection of a script or from its own text, never both. */
|
|
584
612
|
function assertCaptionSource(caption, selected) {
|
|
585
|
-
if (
|
|
586
|
-
if (caption?.text !== void 0) throw new Error("Caption must have only one content source");
|
|
613
|
+
if (caption?.text !== void 0 && selected) throw new Error("Caption must have only one content source");
|
|
587
614
|
}
|
|
588
615
|
//#endregion
|
|
589
616
|
//#region ../medeo-dsl/src/contain.ts
|
|
@@ -643,63 +670,6 @@ function assertContainTree(rows, tree) {
|
|
|
643
670
|
}
|
|
644
671
|
}
|
|
645
672
|
//#endregion
|
|
646
|
-
//#region ../medeo-dsl/src/clip-playback.ts
|
|
647
|
-
const playable = {
|
|
648
|
-
video_clip: ["video", "image"],
|
|
649
|
-
speech: ["audio", "voiceover-recording"],
|
|
650
|
-
caption: ["caption"],
|
|
651
|
-
bgm: ["audio"]
|
|
652
|
-
};
|
|
653
|
-
function supportsClipSpeed(source) {
|
|
654
|
-
return isRecord(source) && [
|
|
655
|
-
"video",
|
|
656
|
-
"audio",
|
|
657
|
-
"voiceover-recording"
|
|
658
|
-
].includes(String(source.kind));
|
|
659
|
-
}
|
|
660
|
-
/** Content-kind admission is shared by Actions, script compilation and the final transaction. */
|
|
661
|
-
function assertClipSourceRole(role, kind) {
|
|
662
|
-
if (!playable[role]?.includes(kind)) throw new Error(`A ${role} Track does not play ${kind}`);
|
|
663
|
-
}
|
|
664
|
-
function assertClipWindow(window, duration) {
|
|
665
|
-
if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs) throw new Error("Invalid Clip window");
|
|
666
|
-
if (duration !== void 0 && window.outMs > duration) throw new Error(`Invalid Clip window: ${window.outMs}ms is past its content's ${duration}ms`);
|
|
667
|
-
}
|
|
668
|
-
/** Check authored playback facts; a remote merge is still read by projection with diagnostics. */
|
|
669
|
-
function assertClipPlayback(clip, source, role) {
|
|
670
|
-
if (!isRecord(source)) throw new Error("Invalid Clip source");
|
|
671
|
-
const kind = source.kind ?? "caption";
|
|
672
|
-
const marker = clip.seqMarker;
|
|
673
|
-
assertSeqMarker(marker);
|
|
674
|
-
if (marker.outMs <= marker.inMs) throw new Error("Clip window must be nonempty");
|
|
675
|
-
if (role !== void 0) assertClipSourceRole(role, kind);
|
|
676
|
-
if (marker.rate !== void 0 && !supportsClipSpeed(source)) throw new Error("Only video and sound change speed");
|
|
677
|
-
const duration = typeof source.durationMs === "number" ? source.durationMs : void 0;
|
|
678
|
-
if (marker.fillTimeline) {
|
|
679
|
-
if (kind !== "audio") throw new Error("Only BGM audio fills the Timeline");
|
|
680
|
-
if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || marker.outMs !== duration) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${duration ?? "its length"}, no rate; the Timeline's end cuts it`);
|
|
681
|
-
}
|
|
682
|
-
assertClipWindow(marker);
|
|
683
|
-
}
|
|
684
|
-
function clipParents(tree) {
|
|
685
|
-
const result = /* @__PURE__ */ new Map();
|
|
686
|
-
for (const track of tree) for (const clip of track.children ?? []) result.set(clip.entityId, track.entityId);
|
|
687
|
-
return result;
|
|
688
|
-
}
|
|
689
|
-
/** A new parent or Track role revalidates that binding, without blocking unrelated merged rows. */
|
|
690
|
-
function assertClipPlacements(before, after, previousTree, nextTree) {
|
|
691
|
-
const previous = clipParents(previousTree);
|
|
692
|
-
for (const [id, parent] of clipParents(nextTree)) {
|
|
693
|
-
const track = after["track-entities"][parent];
|
|
694
|
-
const previousTrack = before["track-entities"][parent];
|
|
695
|
-
if (previous.get(id) === parent && (track === void 0 || isRecord(track) && isRecord(previousTrack) && previousTrack.role === track.role)) continue;
|
|
696
|
-
const role = requireEntity(after, "track-entities", parent).role;
|
|
697
|
-
const clip = after["clip-entities"][id];
|
|
698
|
-
if (!clip) continue;
|
|
699
|
-
assertClipPlayback(clip, after["media-entities"][clip.source] ?? after["caption-entities"][clip.source], role);
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
//#endregion
|
|
703
673
|
//#region ../medeo-dsl/src/plan.ts
|
|
704
674
|
/** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
|
|
705
675
|
function replacementPatch(before, after) {
|
|
@@ -715,7 +685,6 @@ function replacementPatch(before, after) {
|
|
|
715
685
|
*/
|
|
716
686
|
function assertChangeAdmitted(change) {
|
|
717
687
|
const { before, after } = change;
|
|
718
|
-
const parents = change.containment ? clipParents(change.containment.after) : void 0;
|
|
719
688
|
for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
|
|
720
689
|
const value = after[table][id];
|
|
721
690
|
if (value === void 0) continue;
|
|
@@ -730,17 +699,25 @@ function assertChangeAdmitted(change) {
|
|
|
730
699
|
}
|
|
731
700
|
const row = value;
|
|
732
701
|
for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
}
|
|
702
|
+
const marker = row.seqMarker;
|
|
703
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker")) && marker?.outMs <= marker?.inMs) throw new Error("Clip window must be nonempty");
|
|
704
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) assertFillsWithWholeSource(after, marker, row.source);
|
|
737
705
|
}
|
|
738
706
|
if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
|
|
739
707
|
for (const [table, ids] of change.relations) for (const id of ids) {
|
|
740
708
|
const value = after[table][id];
|
|
741
709
|
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
742
710
|
}
|
|
743
|
-
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
|
|
714
|
+
* cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
|
|
715
|
+
*/
|
|
716
|
+
function assertFillsWithWholeSource(rows, marker, source) {
|
|
717
|
+
if (marker?.fillTimeline !== true) return;
|
|
718
|
+
const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
|
|
719
|
+
const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
|
|
720
|
+
if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || durationMs !== void 0 && marker.outMs !== durationMs) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${durationMs ?? "its length"}, no rate; the Timeline's end cuts it`);
|
|
744
721
|
}
|
|
745
722
|
//#endregion
|
|
746
723
|
//#region src/dsl/projection/types.ts
|
|
@@ -976,11 +953,15 @@ function requireSource(s, id, role) {
|
|
|
976
953
|
return media;
|
|
977
954
|
}
|
|
978
955
|
function assertMediaRole(media, role) {
|
|
979
|
-
if (!media
|
|
980
|
-
|
|
956
|
+
if (!media || !{
|
|
957
|
+
video_clip: ["video", "image"],
|
|
958
|
+
speech: ["audio", "voiceover-recording"],
|
|
959
|
+
bgm: ["audio"],
|
|
960
|
+
caption: []
|
|
961
|
+
}[role].includes(media.kind)) throw new Error(`Source is not allowed on ${role} Track`);
|
|
981
962
|
}
|
|
982
963
|
function validateWindow(window, duration) {
|
|
983
|
-
|
|
964
|
+
if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs || duration !== void 0 && window.outMs > duration) throw new Error("Invalid Clip window");
|
|
984
965
|
}
|
|
985
966
|
function trackOf(s, id) {
|
|
986
967
|
requireClip(s, id);
|
|
@@ -1268,8 +1249,7 @@ function replaceSourcePlan(s, clipId, input) {
|
|
|
1268
1249
|
patch: {
|
|
1269
1250
|
seqMarker: {
|
|
1270
1251
|
inMs: input.inMs,
|
|
1271
|
-
outMs: input.outMs
|
|
1272
|
-
...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
|
|
1252
|
+
outMs: input.outMs
|
|
1273
1253
|
},
|
|
1274
1254
|
source: source.id
|
|
1275
1255
|
}
|
|
@@ -5897,30 +5877,6 @@ const phonemeCodec = codec((v) => {
|
|
|
5897
5877
|
assertPhoneme(value);
|
|
5898
5878
|
return value;
|
|
5899
5879
|
});
|
|
5900
|
-
/** A row-owned binding couples the selection, pronunciation and its immutable file in one LWW value. */
|
|
5901
|
-
const phoneticBindingCodec = codec((value) => {
|
|
5902
|
-
assertEntity("phonetic-script-entities", value);
|
|
5903
|
-
assertPhoneticScript(void 0, { ...value });
|
|
5904
|
-
return JSON.stringify({
|
|
5905
|
-
phoneme: JSON.parse(phonemeCodec.encode(value.phoneme)),
|
|
5906
|
-
system: value.system,
|
|
5907
|
-
key: value.key,
|
|
5908
|
-
storageKey: value.storageKey,
|
|
5909
|
-
digest: value.digest
|
|
5910
|
-
});
|
|
5911
|
-
}, (raw) => {
|
|
5912
|
-
const value = JSON.parse(raw);
|
|
5913
|
-
if (!isRecord(value)) throw new Error("Expected a PhoneticScript binding");
|
|
5914
|
-
const phoneme = phonemeCodec.decode(JSON.stringify(value.phoneme));
|
|
5915
|
-
if (isInvalidAtomic(phoneme)) throw new Error(phoneme.reason);
|
|
5916
|
-
const binding = {
|
|
5917
|
-
...value,
|
|
5918
|
-
phoneme
|
|
5919
|
-
};
|
|
5920
|
-
assertEntity("phonetic-script-entities", binding);
|
|
5921
|
-
assertPhoneticScript(void 0, binding);
|
|
5922
|
-
return binding;
|
|
5923
|
-
});
|
|
5924
5880
|
/** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
|
|
5925
5881
|
const seqMarkerCodec = codec((v) => {
|
|
5926
5882
|
assertSeqMarker(v);
|
|
@@ -5987,7 +5943,13 @@ const medeoDslLoroShape = schema({
|
|
|
5987
5943
|
storageKey: schema.String(optional),
|
|
5988
5944
|
digest: schema.String(optional)
|
|
5989
5945
|
})),
|
|
5990
|
-
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5946
|
+
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5947
|
+
phoneme: atomicValue(phonemeCodec),
|
|
5948
|
+
system: schema.String(optional),
|
|
5949
|
+
key: schema.String(optional),
|
|
5950
|
+
storageKey: schema.String(optional),
|
|
5951
|
+
digest: schema.String(optional)
|
|
5952
|
+
})),
|
|
5991
5953
|
"caption-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5992
5954
|
selection: optionalAtomicValue(captionContentCodec),
|
|
5993
5955
|
text: schema.LoroText(optional),
|
|
@@ -6039,78 +6001,20 @@ function documentFormat(doc) {
|
|
|
6039
6001
|
}
|
|
6040
6002
|
var UnsupportedDocumentFormatError = class extends Error {
|
|
6041
6003
|
code = "unsupported_document_format";
|
|
6042
|
-
constructor(
|
|
6043
|
-
super(
|
|
6004
|
+
constructor() {
|
|
6005
|
+
super("Expected a new DSL document; legacy, mixed or uninitialized content is not supported");
|
|
6044
6006
|
this.name = "UnsupportedDocumentFormatError";
|
|
6045
6007
|
}
|
|
6046
6008
|
};
|
|
6047
6009
|
/** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
|
|
6048
6010
|
function assertDslDocument(doc) {
|
|
6049
6011
|
if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
|
|
6050
|
-
assertPhoneticBindingFormat(doc);
|
|
6051
|
-
}
|
|
6052
|
-
/** This storage cutover has no field-per-fact reader or mixed-version write path. */
|
|
6053
|
-
function assertPhoneticBindingFormat(doc) {
|
|
6054
|
-
if (!Object.hasOwn(doc.getShallowValue(), "phonetic-script-entities")) return;
|
|
6055
|
-
const table = doc.getMap("phonetic-script-entities");
|
|
6056
|
-
for (const id of table.keys()) {
|
|
6057
|
-
const row = table.get(id);
|
|
6058
|
-
if (!isContainer(row) || row.kind() !== "Map" || row.keys().some((key) => key !== "binding") || typeof row.get("binding") !== "string") throw new UnsupportedDocumentFormatError("PhoneticScript storage requires one atomic binding; old DSL documents need explicit migration");
|
|
6059
|
-
}
|
|
6060
6012
|
}
|
|
6061
6013
|
//#endregion
|
|
6062
6014
|
//#region ../medeo-dsl/src/schema/text-styles.ts
|
|
6063
6015
|
/** How rich-text marks extend at their edges: a language runs on into text typed after it. */
|
|
6064
6016
|
const textStyles = { language: { expand: "after" } };
|
|
6065
6017
|
//#endregion
|
|
6066
|
-
//#region src/dsl/document/phonetic-binding.ts
|
|
6067
|
-
/**
|
|
6068
|
-
* Actions and the transaction guard keep their domain-shaped rows. The native schema stores
|
|
6069
|
-
* a PhoneticScript's facts under one binding; this lens applies draft edits to that single value.
|
|
6070
|
-
* It never reads the retired field-per-fact representation.
|
|
6071
|
-
*/
|
|
6072
|
-
function logicalDslState(native) {
|
|
6073
|
-
const phonetics = new Proxy({}, {
|
|
6074
|
-
get: (_target, key) => typeof key === "string" ? native["phonetic-script-entities"]?.[key]?.binding : void 0,
|
|
6075
|
-
set: (_target, key, value) => {
|
|
6076
|
-
if (typeof key !== "string") throw new Error("Invalid PhoneticScript key");
|
|
6077
|
-
native["phonetic-script-entities"] ??= {};
|
|
6078
|
-
const existing = native["phonetic-script-entities"][key];
|
|
6079
|
-
if (existing) existing.binding = value;
|
|
6080
|
-
else native["phonetic-script-entities"][key] = { binding: value };
|
|
6081
|
-
return true;
|
|
6082
|
-
},
|
|
6083
|
-
deleteProperty: (_target, key) => {
|
|
6084
|
-
if (typeof key === "string") delete native["phonetic-script-entities"][key];
|
|
6085
|
-
return true;
|
|
6086
|
-
},
|
|
6087
|
-
ownKeys: () => Object.keys(native["phonetic-script-entities"] ?? {}),
|
|
6088
|
-
has: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key),
|
|
6089
|
-
getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key) ? {
|
|
6090
|
-
configurable: true,
|
|
6091
|
-
enumerable: true,
|
|
6092
|
-
writable: true,
|
|
6093
|
-
value: native["phonetic-script-entities"][key].binding
|
|
6094
|
-
} : void 0
|
|
6095
|
-
});
|
|
6096
|
-
const view = {};
|
|
6097
|
-
for (const key of new Set([
|
|
6098
|
-
...Object.keys(native),
|
|
6099
|
-
...entityTables,
|
|
6100
|
-
...relationTables,
|
|
6101
|
-
"contain-relations"
|
|
6102
|
-
])) Object.defineProperty(view, key, {
|
|
6103
|
-
configurable: true,
|
|
6104
|
-
enumerable: true,
|
|
6105
|
-
get: () => key === "phonetic-script-entities" ? phonetics : Reflect.get(native, key),
|
|
6106
|
-
set: (value) => {
|
|
6107
|
-
if (key === "phonetic-script-entities") throw new Error("Cannot replace the PhoneticScript table");
|
|
6108
|
-
Reflect.set(native, key, value);
|
|
6109
|
-
}
|
|
6110
|
-
});
|
|
6111
|
-
return view;
|
|
6112
|
-
}
|
|
6113
|
-
//#endregion
|
|
6114
6018
|
//#region src/dsl/document/read.ts
|
|
6115
6019
|
/** Looking up a missing root must not create it. */
|
|
6116
6020
|
function rootMap(doc, name) {
|
|
@@ -6194,7 +6098,6 @@ function referenceTargets(table, row) {
|
|
|
6194
6098
|
}
|
|
6195
6099
|
}
|
|
6196
6100
|
function readMedeoDsl(doc, state) {
|
|
6197
|
-
const logical = logicalDslState(state);
|
|
6198
6101
|
const diagnostics = [];
|
|
6199
6102
|
const diagnostic = (code, path, error) => diagnostics.push({
|
|
6200
6103
|
code,
|
|
@@ -6204,7 +6107,7 @@ function readMedeoDsl(doc, state) {
|
|
|
6204
6107
|
const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
|
|
6205
6108
|
const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
|
|
6206
6109
|
const richText = {};
|
|
6207
|
-
for (const table of entityTables) for (const [id, raw] of Object.entries(
|
|
6110
|
+
for (const table of entityTables) for (const [id, raw] of Object.entries(state[table] ?? {})) try {
|
|
6208
6111
|
assertId(id);
|
|
6209
6112
|
let v = raw;
|
|
6210
6113
|
for (const field of optionalReferences[table] ?? []) {
|
|
@@ -6710,11 +6613,7 @@ function validateDraft(tracked) {
|
|
|
6710
6613
|
before,
|
|
6711
6614
|
after: state,
|
|
6712
6615
|
entities: tracked.entities,
|
|
6713
|
-
relations: tracked.relations
|
|
6714
|
-
containment: {
|
|
6715
|
-
before: plainTree(before["contain-relations"] ?? []),
|
|
6716
|
-
after: plainTree(state["contain-relations"] ?? [])
|
|
6717
|
-
}
|
|
6616
|
+
relations: tracked.relations
|
|
6718
6617
|
});
|
|
6719
6618
|
if (!tracked.treeChanged) return;
|
|
6720
6619
|
const oldNodes = nodeIndex(before["contain-relations"] ?? []);
|
|
@@ -6791,11 +6690,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6791
6690
|
const steps = [];
|
|
6792
6691
|
const newCaptionTexts = /* @__PURE__ */ new Map();
|
|
6793
6692
|
const bodies = /* @__PURE__ */ new Map();
|
|
6794
|
-
const
|
|
6795
|
-
|
|
6796
|
-
const phoneticRows = /* @__PURE__ */ new Map();
|
|
6797
|
-
mirror.setState((native) => {
|
|
6798
|
-
const raw = logicalDslState(native);
|
|
6693
|
+
const before = mirror.getState();
|
|
6694
|
+
mirror.setState((raw) => {
|
|
6799
6695
|
const tracked = new TransactionDraft(raw, before);
|
|
6800
6696
|
setReject((error) => tracked.fail(error));
|
|
6801
6697
|
/** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
|
|
@@ -6987,15 +6883,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6987
6883
|
for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
|
|
6988
6884
|
else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
|
|
6989
6885
|
assertSelectionsAfter(raw, before, tracked, steps);
|
|
6990
|
-
const changedPhonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...steps.flatMap((step) => step.type !== "text" && step.table === "phonetic-script-entities" ? [step.id] : [])]);
|
|
6991
|
-
for (const id of changedPhonetics) {
|
|
6992
|
-
const row = raw["phonetic-script-entities"][id];
|
|
6993
|
-
phoneticRows.set(id, row ? copyInput(row) : void 0);
|
|
6994
|
-
if (!row) continue;
|
|
6995
|
-
const old = nativeBefore["phonetic-script-entities"][id];
|
|
6996
|
-
if (old) native["phonetic-script-entities"][id] = old;
|
|
6997
|
-
else delete native["phonetic-script-entities"][id];
|
|
6998
|
-
}
|
|
6999
6886
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
7000
6887
|
const row = raw["caption-entities"][id];
|
|
7001
6888
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -7004,7 +6891,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
7004
6891
|
}
|
|
7005
6892
|
}
|
|
7006
6893
|
for (const table of Object.keys(selectionField)) {
|
|
7007
|
-
if (table === "phonetic-script-entities") continue;
|
|
7008
6894
|
const field = selectionField[table];
|
|
7009
6895
|
for (const [id, fields] of tracked.entities.get(table) ?? []) {
|
|
7010
6896
|
const row = raw[table][id];
|
|
@@ -7044,8 +6930,6 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
7044
6930
|
applyTextEdits(text, step.edits);
|
|
7045
6931
|
continue;
|
|
7046
6932
|
}
|
|
7047
|
-
const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
|
|
7048
|
-
if (step.table === "phonetic-script-entities" && !phonetic) continue;
|
|
7049
6933
|
const row = entityRow(doc, step.table, step.id);
|
|
7050
6934
|
const field = selectionField[step.table];
|
|
7051
6935
|
if (step.type === "range") {
|
|
@@ -7064,21 +6948,12 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
7064
6948
|
if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
|
|
7065
6949
|
}
|
|
7066
6950
|
if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
|
|
7067
|
-
if (
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
row.set(field, codecs[step.table].encode(value));
|
|
7071
|
-
}
|
|
7072
|
-
} else if (step.value !== void 0) if (phonetic) phonetic.phoneme = step.value;
|
|
7073
|
-
else {
|
|
6951
|
+
if (!row) throw new Error("Unable to construct prepared range");
|
|
6952
|
+
row.set(field, codecs[step.table].encode(value));
|
|
6953
|
+
} else if (step.value !== void 0) {
|
|
7074
6954
|
if (!row) throw new Error("Prepared selection owner is missing");
|
|
7075
6955
|
row.set(field, codecs[step.table].encode(step.value));
|
|
7076
|
-
}
|
|
7077
|
-
else if (row?.get(field) !== void 0) row.delete(field);
|
|
7078
|
-
}
|
|
7079
|
-
for (const [id, binding] of phoneticRows) {
|
|
7080
|
-
if (!binding) continue;
|
|
7081
|
-
(entityRow(doc, "phonetic-script-entities", id) ?? doc.getMap("phonetic-script-entities").setContainer(id, new LoroMap())).set("binding", phoneticBindingCodec.encode(binding));
|
|
6956
|
+
} else if (row?.get(field) !== void 0) row.delete(field);
|
|
7082
6957
|
}
|
|
7083
6958
|
}
|
|
7084
6959
|
});
|
|
@@ -7091,7 +6966,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
7091
6966
|
function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
7092
6967
|
const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
|
|
7093
6968
|
const changedCaptions = /* @__PURE__ */ new Set();
|
|
7094
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (
|
|
6969
|
+
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
7095
6970
|
const phonemes = /* @__PURE__ */ new Map();
|
|
7096
6971
|
for (const step of steps) {
|
|
7097
6972
|
if (step.type === "text") {
|
|
@@ -7125,7 +7000,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
|
7125
7000
|
if (!row) continue;
|
|
7126
7001
|
const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
|
|
7127
7002
|
const old = before["phonetic-script-entities"][id];
|
|
7128
|
-
assertPhoneticScript(old
|
|
7003
|
+
assertPhoneticScript(old, {
|
|
7129
7004
|
...row,
|
|
7130
7005
|
phoneme: isRecord(phoneme) ? phoneme : void 0
|
|
7131
7006
|
});
|
|
@@ -7164,7 +7039,6 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7164
7039
|
"tracks",
|
|
7165
7040
|
"part_library"
|
|
7166
7041
|
].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
|
|
7167
|
-
assertPhoneticBindingFormat(doc);
|
|
7168
7042
|
doc.configTextStyle(textStyles);
|
|
7169
7043
|
this.mirror = new Mirror({
|
|
7170
7044
|
doc,
|
|
@@ -7228,11 +7102,6 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7228
7102
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
7229
7103
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
7230
7104
|
}
|
|
7231
|
-
/** Check the session's write capability before an Action starts host-owned resource I/O. */
|
|
7232
|
-
assertWritable() {
|
|
7233
|
-
this.assertReadyToWrite();
|
|
7234
|
-
this.options.mutationGuard?.run(() => {});
|
|
7235
|
-
}
|
|
7236
7105
|
/**
|
|
7237
7106
|
* A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
|
|
7238
7107
|
* include insertions at their end. K01 remains deferred.
|
|
@@ -7265,7 +7134,13 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7265
7134
|
if (this.ownsDoc) this.doc.free();
|
|
7266
7135
|
}
|
|
7267
7136
|
mutate(run) {
|
|
7268
|
-
this.
|
|
7137
|
+
this.assertActive();
|
|
7138
|
+
if (this.writing || this.notifying) {
|
|
7139
|
+
const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
|
|
7140
|
+
this.rejectTransaction?.(error);
|
|
7141
|
+
throw error;
|
|
7142
|
+
}
|
|
7143
|
+
if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
|
|
7269
7144
|
const apply = () => {
|
|
7270
7145
|
this.writing = true;
|
|
7271
7146
|
try {
|
|
@@ -7280,15 +7155,6 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7280
7155
|
if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
|
|
7281
7156
|
else apply();
|
|
7282
7157
|
}
|
|
7283
|
-
assertReadyToWrite() {
|
|
7284
|
-
this.assertActive();
|
|
7285
|
-
if (this.writing || this.notifying) {
|
|
7286
|
-
const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
|
|
7287
|
-
this.rejectTransaction?.(error);
|
|
7288
|
-
throw error;
|
|
7289
|
-
}
|
|
7290
|
-
if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
|
|
7291
|
-
}
|
|
7292
7158
|
notify() {
|
|
7293
7159
|
if (this.disposed || !this.dirty) return;
|
|
7294
7160
|
this.dirty = false;
|
|
@@ -7315,13 +7181,12 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7315
7181
|
const found = /* @__PURE__ */ new Map();
|
|
7316
7182
|
const seen = /* @__PURE__ */ new Set();
|
|
7317
7183
|
const visit = (id, phoneticScriptId) => {
|
|
7184
|
+
if (seen.has(id)) return;
|
|
7185
|
+
seen.add(id);
|
|
7318
7186
|
if (snapshot["audio-script-entities"][id]) {
|
|
7319
|
-
if (!found.has(id)) found.set(id,
|
|
7320
|
-
if (phoneticScriptId !== void 0) found.get(id).add(phoneticScriptId);
|
|
7187
|
+
if (!found.has(id)) found.set(id, phoneticScriptId);
|
|
7321
7188
|
return;
|
|
7322
7189
|
}
|
|
7323
|
-
if (seen.has(id)) return;
|
|
7324
|
-
seen.add(id);
|
|
7325
7190
|
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
7326
7191
|
if (phonetic) {
|
|
7327
7192
|
visit(phonetic.phoneme.audioScriptId, id);
|
|
@@ -7331,18 +7196,11 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7331
7196
|
};
|
|
7332
7197
|
visit(recordingId);
|
|
7333
7198
|
if (!found.size) {
|
|
7334
|
-
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId,
|
|
7199
|
+
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
|
|
7335
7200
|
}
|
|
7336
|
-
return [...found].map(([audioScriptId,
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
audioScriptId,
|
|
7340
|
-
phoneticScriptId: phoneticScriptIds[0]
|
|
7341
|
-
};
|
|
7342
|
-
return phoneticScriptIds.length ? {
|
|
7343
|
-
audioScriptId,
|
|
7344
|
-
phoneticScriptIds
|
|
7345
|
-
} : { audioScriptId };
|
|
7201
|
+
return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
|
|
7202
|
+
audioScriptId,
|
|
7203
|
+
phoneticScriptId
|
|
7346
7204
|
});
|
|
7347
7205
|
}
|
|
7348
7206
|
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
@@ -7387,6 +7245,7 @@ function toAudioScriptViews(snapshot) {
|
|
|
7387
7245
|
});
|
|
7388
7246
|
}
|
|
7389
7247
|
const layout = resolveDslLayout(snapshot);
|
|
7248
|
+
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7390
7249
|
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
7391
7250
|
for (const track of layout.tracks) for (const { clipId } of track.clips) {
|
|
7392
7251
|
const source = snapshot["clip-entities"][clipId]?.source;
|
|
@@ -7410,7 +7269,6 @@ function toAudioScriptViews(snapshot) {
|
|
|
7410
7269
|
clipId,
|
|
7411
7270
|
recordingId: source,
|
|
7412
7271
|
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
7413
|
-
...spoken.phoneticScriptIds === void 0 ? {} : { phoneticScriptIds: spoken.phoneticScriptIds },
|
|
7414
7272
|
voices,
|
|
7415
7273
|
...applied === void 0 ? {} : { applied }
|
|
7416
7274
|
});
|
|
@@ -7427,23 +7285,19 @@ function toAudioScriptViews(snapshot) {
|
|
|
7427
7285
|
segment
|
|
7428
7286
|
});
|
|
7429
7287
|
}
|
|
7430
|
-
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7431
7288
|
return [...views.values()];
|
|
7432
7289
|
}
|
|
7433
7290
|
/**
|
|
7434
7291
|
* A transcript's segments on the Timeline: a segment's time is in the medium the script was read
|
|
7435
|
-
* off
|
|
7436
|
-
* Clip holding that source moment is used. The source time is never taken from the Caption.
|
|
7292
|
+
* off, so it lands where the earliest Clip playing that medium holds that moment in its window.
|
|
7437
7293
|
*/
|
|
7438
7294
|
function placeTranscript(snapshot, layout, view) {
|
|
7439
7295
|
if (!view.sourceMediaIds.length) return;
|
|
7440
7296
|
const plays = layout.tracks.flatMap((track) => track.clips).filter((clip) => view.sourceMediaIds.includes(snapshot["clip-entities"][clip.clipId]?.source ?? "")).sort((a, b) => a.startMs - b.startMs);
|
|
7441
|
-
for (const
|
|
7297
|
+
for (const segment of view.segments) {
|
|
7442
7298
|
const at = segment.startMs;
|
|
7443
7299
|
if (at === void 0) continue;
|
|
7444
|
-
const
|
|
7445
|
-
const candidates = plays.filter((c) => c.inMs <= at && at < c.outMs);
|
|
7446
|
-
const clip = candidates.find((c) => anchoredUses.has(c.clipId)) ?? candidates[0];
|
|
7300
|
+
const clip = plays.find((c) => c.inMs <= at && at < c.outMs);
|
|
7447
7301
|
if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
|
|
7448
7302
|
}
|
|
7449
7303
|
}
|
|
@@ -7454,26 +7308,14 @@ function placeTranscript(snapshot, layout, view) {
|
|
|
7454
7308
|
* not what a take was synthesized from (D190).
|
|
7455
7309
|
*/
|
|
7456
7310
|
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
7457
|
-
const
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
7462
|
-
const current = snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
7463
|
-
if (recorded === void 0 || current === void 0) unknown = true;
|
|
7464
|
-
else if (recorded !== current) return false;
|
|
7465
|
-
}
|
|
7466
|
-
return unknown ? void 0 : true;
|
|
7311
|
+
const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
|
|
7312
|
+
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
7313
|
+
const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
7314
|
+
return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
|
|
7467
7315
|
}
|
|
7468
7316
|
//#endregion
|
|
7469
7317
|
//#region src/dsl/voiceover-actions.ts
|
|
7470
|
-
function
|
|
7471
|
-
const phonetic = s["phonetic-script-entities"][id];
|
|
7472
|
-
const run = s.phoneticText[id];
|
|
7473
|
-
const segment = s["audio-script-entities"][scriptId]?.segments.find((item) => item.id === segmentId);
|
|
7474
|
-
return phonetic?.phoneme.audioScriptId === scriptId && phonetic.phoneme.segmentId === segmentId && phonetic.phoneme.phonemeScript === void 0 && run?.start === 0 && run.end === segment?.text.length;
|
|
7475
|
-
}
|
|
7476
|
-
function voiceoverPlan(s, input, result, plainPhoneticIds) {
|
|
7318
|
+
function voiceoverPlan(s, input, result) {
|
|
7477
7319
|
if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
|
|
7478
7320
|
if (input.captions.length && (!input.captionTrackId || requireTrack(s, input.captionTrackId).role !== "caption")) throw new Error("Caption Track is required");
|
|
7479
7321
|
const existing = s["clip-entities"][result.clipId];
|
|
@@ -7512,44 +7354,15 @@ function voiceoverPlan(s, input, result, plainPhoneticIds) {
|
|
|
7512
7354
|
inMs: 0,
|
|
7513
7355
|
outMs: input.media.durationMs
|
|
7514
7356
|
}, input.media.durationMs);
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
|
|
7525
|
-
}
|
|
7526
|
-
});
|
|
7527
|
-
} else {
|
|
7528
|
-
if (input.sourceDigest !== void 0) throw new Error("A synthesis digest requires its PhoneticScript");
|
|
7529
|
-
if (plainPhoneticIds.length !== segmentIds.length) throw new Error("Expected one PhoneticScript id per plain segment");
|
|
7530
|
-
segmentIds.forEach((segmentId, i) => {
|
|
7531
|
-
const id = plainPhoneticIds[i];
|
|
7532
|
-
if (s["phonetic-script-entities"][id]) {
|
|
7533
|
-
if (!matchesPlainPhoneticOrigin(s, id, result.audioScriptId, segmentId)) throw new Error("PhoneticScript id already names another synthesis origin");
|
|
7534
|
-
} else mutations.push({
|
|
7535
|
-
type: "create-entity",
|
|
7536
|
-
table: "phonetic-script-entities",
|
|
7537
|
-
id,
|
|
7538
|
-
value: { phoneme: {
|
|
7539
|
-
audioScriptId: result.audioScriptId,
|
|
7540
|
-
segmentId
|
|
7541
|
-
} }
|
|
7542
|
-
});
|
|
7543
|
-
if (!Object.values(s["derived-from-relations"]).some((origin) => origin.entityId === result.mediaId && origin.sourceId === id)) mutations.push({
|
|
7544
|
-
type: "set-relation",
|
|
7545
|
-
table: "derived-from-relations",
|
|
7546
|
-
value: {
|
|
7547
|
-
entityId: result.mediaId,
|
|
7548
|
-
sourceId: id
|
|
7549
|
-
}
|
|
7550
|
-
});
|
|
7551
|
-
});
|
|
7552
|
-
}
|
|
7357
|
+
mutations.push({
|
|
7358
|
+
type: "set-relation",
|
|
7359
|
+
table: "derived-from-relations",
|
|
7360
|
+
value: {
|
|
7361
|
+
entityId: result.mediaId,
|
|
7362
|
+
sourceId: phoneticScriptId ?? result.audioScriptId,
|
|
7363
|
+
...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
|
|
7364
|
+
}
|
|
7365
|
+
});
|
|
7553
7366
|
if (input.voice) {
|
|
7554
7367
|
const id = resourceId("voice", input.voice.system, input.voice.key);
|
|
7555
7368
|
mutations.push({
|
|
@@ -8046,20 +7859,10 @@ var DslEditor = class {
|
|
|
8046
7859
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
8047
7860
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
8048
7861
|
};
|
|
8049
|
-
const state = this.dsl.getSnapshot();
|
|
8050
|
-
const plainSegments = input.script.segments === void 0 ? state["audio-script-entities"][result.audioScriptId]?.segments.map((segment) => segment.id) ?? [] : result.segmentIds;
|
|
8051
|
-
const plainPhoneticIds = input.script.phoneticScriptId === void 0 ? plainSegments.map((segmentId) => {
|
|
8052
|
-
const origins = Object.values(state["derived-from-relations"]).filter((origin) => {
|
|
8053
|
-
if (origin.entityId !== result.mediaId) return false;
|
|
8054
|
-
return matchesPlainPhoneticOrigin(state, origin.sourceId, result.audioScriptId, segmentId);
|
|
8055
|
-
});
|
|
8056
|
-
if (origins.length > 1) throw new Error("Ambiguous plain recording origin");
|
|
8057
|
-
return origins[0]?.sourceId ?? this.idFactory("phonetic-script");
|
|
8058
|
-
}) : [];
|
|
8059
7862
|
this.write("applyVoiceover", {
|
|
8060
7863
|
input,
|
|
8061
7864
|
result
|
|
8062
|
-
}, voiceoverPlan(
|
|
7865
|
+
}, voiceoverPlan(this.dsl.getSnapshot(), input, result), options);
|
|
8063
7866
|
return result;
|
|
8064
7867
|
}
|
|
8065
7868
|
voiceoverScriptId(script) {
|
|
@@ -8076,12 +7879,8 @@ var DslEditor = class {
|
|
|
8076
7879
|
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
8077
7880
|
*/
|
|
8078
7881
|
async setPhoneticScript(input, options) {
|
|
8079
|
-
this.dsl.assertWritable();
|
|
8080
7882
|
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
8081
7883
|
const range = segmentRange(input);
|
|
8082
|
-
assertId(id);
|
|
8083
|
-
assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
|
|
8084
|
-
assertPhonemeScript(input.phonemeScript);
|
|
8085
7884
|
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
8086
7885
|
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
8087
7886
|
const cleared = {
|
|
@@ -8290,13 +8089,14 @@ function toVideoDraft(snapshot) {
|
|
|
8290
8089
|
duration_ms: clip.durationMs
|
|
8291
8090
|
};
|
|
8292
8091
|
if (track.role === "video_clip") {
|
|
8293
|
-
|
|
8092
|
+
const placeholder = source?.system === "placeholder";
|
|
8093
|
+
if (!source || !["image", "video"].includes(source.kind) || !(source.system === "memota" || placeholder)) fail("Visual playback requires a memota Image or Video, or a placeholder Image");
|
|
8294
8094
|
if (clip.fillTimeline) fail("VideoDraft cannot express timeline-filling visual content");
|
|
8295
8095
|
parts[clip.clipId] = { video_clip: {
|
|
8296
8096
|
...common,
|
|
8297
8097
|
play_in: clip.inMs,
|
|
8298
8098
|
play_out: clip.outMs,
|
|
8299
|
-
origin_media_id: source.key,
|
|
8099
|
+
origin_media_id: placeholder && source.key === BLANK_PLACEHOLDER.key ? "" : source.key,
|
|
8300
8100
|
volume: clip.volume,
|
|
8301
8101
|
...snapshot["clip-entities"][clip.clipId].seqMarker.rate === void 0 ? {} : { speed_shift: {
|
|
8302
8102
|
category: "linear",
|
|
@@ -8305,8 +8105,7 @@ function toVideoDraft(snapshot) {
|
|
|
8305
8105
|
} }
|
|
8306
8106
|
} };
|
|
8307
8107
|
} else if (track.role === "speech") {
|
|
8308
|
-
if (!source || source.kind !== "
|
|
8309
|
-
if (source.system !== "memota" && !(source.kind === "voiceover-recording" && source.system === "memota-speech")) fail("Speech playback requires a Memota audio file");
|
|
8108
|
+
if (!source || source.kind !== "voiceover-recording" || source.system !== "memota-speech") fail("Legacy Speech requires a memota-speech VoiceoverRecording");
|
|
8310
8109
|
requireWholeAudio(source, clip, fail);
|
|
8311
8110
|
if (clip.fillTimeline) fail("Legacy Speech cannot express timeline fill");
|
|
8312
8111
|
const voices = Object.values(snapshot["voice-timbre-relations"]).filter((r) => r.recordingId === sourceId).flatMap((r) => snapshot["voice-entities"][r.voiceId] ? [snapshot["voice-entities"][r.voiceId]] : []);
|
|
@@ -8323,7 +8122,7 @@ function toVideoDraft(snapshot) {
|
|
|
8323
8122
|
audio_storage_key: source.storageKey,
|
|
8324
8123
|
origin_speech_id: source.key,
|
|
8325
8124
|
volume: clip.volume,
|
|
8326
|
-
audio_script:
|
|
8125
|
+
audio_script: spokenText(snapshot, sourceId),
|
|
8327
8126
|
voice: voice ? {
|
|
8328
8127
|
id: voice.key,
|
|
8329
8128
|
name: voice.name
|
|
@@ -8419,15 +8218,6 @@ function spokenText(snapshot, recordingId) {
|
|
|
8419
8218
|
if (!first) return "";
|
|
8420
8219
|
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
8421
8220
|
const script = snapshot["audio-script-entities"][first.audioScriptId];
|
|
8422
|
-
if (first.phoneticScriptIds !== void 0 && script) {
|
|
8423
|
-
const ids = first.phoneticScriptIds;
|
|
8424
|
-
const selections = ids.map((id) => snapshot["phonetic-script-entities"][id]);
|
|
8425
|
-
if (!(ids.length === script.segments.length && script.segments.every((segment) => {
|
|
8426
|
-
const matching = ids.filter((id) => snapshot["phonetic-script-entities"][id]?.phoneme.segmentId === segment.id);
|
|
8427
|
-
const run = matching.length === 1 ? snapshot.phoneticText[matching[0]] : void 0;
|
|
8428
|
-
return run?.start === 0 && run.end === segment.text.length;
|
|
8429
|
-
})) || selections.some((selection) => selection.phoneme.phonemeScript !== void 0)) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple phonetic sources require complete plain selections of one script");
|
|
8430
|
-
}
|
|
8431
8221
|
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
8432
8222
|
}
|
|
8433
8223
|
//#endregion
|
|
@@ -8518,6 +8308,7 @@ function prepare(input, options) {
|
|
|
8518
8308
|
if (facts.has(id)) fail("mediaFacts", `Duplicate facts for ${fact.system}/${fact.key}`);
|
|
8519
8309
|
facts.set(id, fact);
|
|
8520
8310
|
}
|
|
8311
|
+
for (const fact of facts.values()) if (fact.system === "placeholder" && facts.has(resourceId("media", "memota", fact.key))) fail("mediaFacts", `Media cannot be both present and missing: ${fact.key}`);
|
|
8521
8312
|
const register = (media) => {
|
|
8522
8313
|
const id = resourceId("media", media.system, media.key), old = imported.get(id);
|
|
8523
8314
|
if (old) {
|
|
@@ -8543,6 +8334,15 @@ function prepare(input, options) {
|
|
|
8543
8334
|
const mediaByPart = /* @__PURE__ */ new Map(), scriptByRecording = /* @__PURE__ */ new Map(), scriptsByPart = /* @__PURE__ */ new Map();
|
|
8544
8335
|
const voices = /* @__PURE__ */ new Set();
|
|
8545
8336
|
for (const [id, p] of Object.entries(library)) if (p.video_clip) {
|
|
8337
|
+
if (p.video_clip.origin_media_id === "") {
|
|
8338
|
+
mediaByPart.set(id, register(BLANK_PLACEHOLDER));
|
|
8339
|
+
continue;
|
|
8340
|
+
}
|
|
8341
|
+
const missing = p.video_clip.origin_media_id && facts.get(resourceId("media", "placeholder", p.video_clip.origin_media_id));
|
|
8342
|
+
if (missing) {
|
|
8343
|
+
mediaByPart.set(id, register(missing));
|
|
8344
|
+
continue;
|
|
8345
|
+
}
|
|
8546
8346
|
const fact = factFor("memota", p.video_clip.origin_media_id);
|
|
8547
8347
|
if (fact.kind !== "image" && fact.kind !== "video") fail(`part_library/${id}`, "Expected Image or Video facts");
|
|
8548
8348
|
mediaByPart.set(id, register(fact));
|
|
@@ -8640,7 +8440,11 @@ function prepare(input, options) {
|
|
|
8640
8440
|
start: start - segment.start,
|
|
8641
8441
|
end: end - segment.start
|
|
8642
8442
|
});
|
|
8643
|
-
} else
|
|
8443
|
+
} else diagnostics.push({
|
|
8444
|
+
code: "caption-text-unmatched",
|
|
8445
|
+
path: `part_library/${captionId}/text`,
|
|
8446
|
+
message: "No forward exact match; preserved as independent Caption text"
|
|
8447
|
+
});
|
|
8644
8448
|
}
|
|
8645
8449
|
}
|
|
8646
8450
|
const hosts = /* @__PURE__ */ new Map();
|
|
@@ -8683,14 +8487,16 @@ function prepare(input, options) {
|
|
|
8683
8487
|
if (caption.speech_part_id !== void 0 && owner.get(id) !== caption.speech_part_id) fail(`part_library/${id}/speech_part_id`, "Caption ownership must agree with ordered speech.caption_ids");
|
|
8684
8488
|
sourceId = claim(mint("caption"));
|
|
8685
8489
|
const selected = selections.get(id), style = fromDraftStyle(caption.style);
|
|
8686
|
-
if (!selected) fail(`part_library/${id}/text`, "Caption must select a segment of an AudioScript");
|
|
8687
8490
|
mutations.push({
|
|
8688
8491
|
type: "create-entity",
|
|
8689
8492
|
table: "caption-entities",
|
|
8690
8493
|
id: sourceId,
|
|
8691
|
-
value:
|
|
8494
|
+
value: {
|
|
8495
|
+
...style === void 0 ? {} : { style },
|
|
8496
|
+
...selected ? {} : { text: caption.text }
|
|
8497
|
+
}
|
|
8692
8498
|
});
|
|
8693
|
-
mutations.push({
|
|
8499
|
+
if (selected) mutations.push({
|
|
8694
8500
|
type: "set-caption-content",
|
|
8695
8501
|
captionId: sourceId,
|
|
8696
8502
|
...selected
|
|
@@ -8760,5 +8566,122 @@ function importedScript(id, text, mintSegment) {
|
|
|
8760
8566
|
segments
|
|
8761
8567
|
};
|
|
8762
8568
|
}
|
|
8569
|
+
function normalizeLegacyVideoDraft(input, mediaFacts = []) {
|
|
8570
|
+
const draft = structuredClone(input);
|
|
8571
|
+
const diagnostics = [];
|
|
8572
|
+
const note = (code, path, message) => diagnostics.push({
|
|
8573
|
+
code,
|
|
8574
|
+
path,
|
|
8575
|
+
message
|
|
8576
|
+
});
|
|
8577
|
+
const library = draft.part_library ?? {};
|
|
8578
|
+
const tracks = [
|
|
8579
|
+
...draft.above_main_tracks ?? [],
|
|
8580
|
+
...draft.main_track ? [draft.main_track] : [],
|
|
8581
|
+
...draft.below_main_tracks ?? []
|
|
8582
|
+
];
|
|
8583
|
+
const facts = new Map(mediaFacts.map((fact) => [resourceId("media", fact.system, fact.key), fact]));
|
|
8584
|
+
const round = (owner, field, path) => {
|
|
8585
|
+
const fields = owner;
|
|
8586
|
+
const value = fields[field];
|
|
8587
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Number.isSafeInteger(value)) return;
|
|
8588
|
+
const rounded = Math.round(value);
|
|
8589
|
+
fields[field] = rounded;
|
|
8590
|
+
note("legacy-time-rounded", `${path}/${field}`, `${value} → ${rounded}`);
|
|
8591
|
+
};
|
|
8592
|
+
for (const track of tracks) for (const item of track.items ?? []) round(item, "abs_time_position", `tracks/${track.id}/${item.part_id}`);
|
|
8593
|
+
for (const [id, part] of Object.entries(library)) {
|
|
8594
|
+
const path = `part_library/${id}`;
|
|
8595
|
+
if (part.video_clip) for (const f of [
|
|
8596
|
+
"play_in",
|
|
8597
|
+
"play_out",
|
|
8598
|
+
"duration_ms"
|
|
8599
|
+
]) round(part.video_clip, f, path);
|
|
8600
|
+
if (part.speech) for (const f of ["duration_ms", "media_duration_ms"]) round(part.speech, f, path);
|
|
8601
|
+
if (part.caption) for (const f of [
|
|
8602
|
+
"duration_ms",
|
|
8603
|
+
"initial_duration_ms",
|
|
8604
|
+
"start_ms"
|
|
8605
|
+
]) round(part.caption, f, path);
|
|
8606
|
+
if (part.bgm) round(part.bgm, "duration_ms", path);
|
|
8607
|
+
}
|
|
8608
|
+
for (const aggregation of draft.part_aggregations ?? []) for (const attachment of aggregation.attachments ?? []) round(attachment, "relative_time_position", `part_aggregations/${aggregation.body_part_id}/${attachment.part_id}`);
|
|
8609
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
8610
|
+
const drop = (id, why) => {
|
|
8611
|
+
dropped.add(id);
|
|
8612
|
+
note("legacy-part-dropped", `part_library/${id}`, why);
|
|
8613
|
+
};
|
|
8614
|
+
for (const [id, part] of Object.entries(library)) {
|
|
8615
|
+
const path = `part_library/${id}`;
|
|
8616
|
+
const clip = part.video_clip;
|
|
8617
|
+
const sound = part.bgm ?? part.speech;
|
|
8618
|
+
const fact = part.bgm ? facts.get(resourceId("media", "memota", part.bgm.origin_media_id ?? "")) : part.speech && facts.get(resourceId("media", "memota-speech", part.speech.origin_speech_id ?? ""));
|
|
8619
|
+
if (sound && fact && "storageKey" in fact && fact.storageKey && fact.storageKey !== sound.audio_storage_key) {
|
|
8620
|
+
note("legacy-storage-key-current", `${path}/audio_storage_key`, `${sound.audio_storage_key} → ${fact.storageKey}`);
|
|
8621
|
+
sound.audio_storage_key = fact.storageKey;
|
|
8622
|
+
}
|
|
8623
|
+
if (!clip) continue;
|
|
8624
|
+
const rate = clip.speed_shift?.config?.linear?.speed ?? 1;
|
|
8625
|
+
const inMs = clip.play_in ?? 0;
|
|
8626
|
+
const outMs = clip.play_out ?? (clip.duration_ms === void 0 ? void 0 : inMs + Math.round(clip.duration_ms * rate));
|
|
8627
|
+
const source = facts.get(resourceId("media", "memota", clip.origin_media_id ?? ""));
|
|
8628
|
+
let windowEnd = outMs;
|
|
8629
|
+
if (source?.kind === "video" && outMs !== void 0 && outMs > source.durationMs) {
|
|
8630
|
+
windowEnd = source.durationMs;
|
|
8631
|
+
if (windowEnd > inMs) {
|
|
8632
|
+
note("legacy-window-cut", `${path}/play_out`, `${outMs} → ${windowEnd} (source length)`);
|
|
8633
|
+
clip.play_out = windowEnd;
|
|
8634
|
+
if (clip.duration_ms !== void 0) clip.duration_ms = Math.max(0, Math.round((windowEnd - inMs) / rate));
|
|
8635
|
+
}
|
|
8636
|
+
}
|
|
8637
|
+
if (windowEnd === void 0) continue;
|
|
8638
|
+
const length = windowEnd - inMs;
|
|
8639
|
+
if (length <= 0) drop(id, `Empty window ${inMs}–${windowEnd}`);
|
|
8640
|
+
else if (clip.origin_media_id === "" && length / rate < 40) drop(id, `Placeholder sliver of ${length / rate}ms`);
|
|
8641
|
+
}
|
|
8642
|
+
for (const id of dropped) delete library[id];
|
|
8643
|
+
for (const track of tracks) track.items = track.items?.filter((item) => !dropped.has(item.part_id ?? ""));
|
|
8644
|
+
const abs = /* @__PURE__ */ new Map();
|
|
8645
|
+
for (const track of tracks) for (const item of track.items ?? []) abs.set(item.part_id ?? "", item.abs_time_position ?? 0);
|
|
8646
|
+
const mainOrder = new Map((draft.main_track?.items ?? []).map((item, index) => [item.part_id ?? "", index]));
|
|
8647
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
8648
|
+
for (const aggregation of draft.part_aggregations ?? []) {
|
|
8649
|
+
const host = aggregation.body_part_id ?? "";
|
|
8650
|
+
for (const attachment of aggregation.attachments ?? []) {
|
|
8651
|
+
const speech = attachment.part_id ?? "";
|
|
8652
|
+
const path = `part_aggregations/${host}/${speech}`;
|
|
8653
|
+
if (!library[speech]?.speech) note("legacy-attachment-dropped", path, "Not a placed Speech");
|
|
8654
|
+
else if (!library[host]?.video_clip || !mainOrder.has(host)) note("legacy-attachment-dropped", path, "Host is not a placed visual clip");
|
|
8655
|
+
else candidates.set(speech, [...candidates.get(speech) ?? [], {
|
|
8656
|
+
host,
|
|
8657
|
+
offset: attachment.relative_time_position ?? 0
|
|
8658
|
+
}]);
|
|
8659
|
+
}
|
|
8660
|
+
}
|
|
8661
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
8662
|
+
for (const [speech, list] of candidates) {
|
|
8663
|
+
if (list.length === 1) {
|
|
8664
|
+
chosen.set(speech, list[0]);
|
|
8665
|
+
continue;
|
|
8666
|
+
}
|
|
8667
|
+
const playing = list.filter((c) => (abs.get(c.host) ?? 0) + c.offset === abs.get(speech)).sort((a, b) => mainOrder.get(a.host) - mainOrder.get(b.host));
|
|
8668
|
+
const pick = playing[0] ?? list[0];
|
|
8669
|
+
chosen.set(speech, pick);
|
|
8670
|
+
note(playing.length ? "legacy-speech-host-chosen" : "legacy-speech-host-unmatched", `part_aggregations/${pick.host}/${speech}`, `${list.length} hosts: ${list.map((c) => c.host).join(", ")}`);
|
|
8671
|
+
}
|
|
8672
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
8673
|
+
for (const [speech, { host, offset }] of chosen) byHost.set(host, [...byHost.get(host) ?? [], {
|
|
8674
|
+
part_id: speech,
|
|
8675
|
+
relative_time_position: offset
|
|
8676
|
+
}]);
|
|
8677
|
+
if (draft.part_aggregations) draft.part_aggregations = [...new Set(draft.part_aggregations.map((a) => a.body_part_id ?? ""))].filter((host) => byHost.has(host)).map((host) => ({
|
|
8678
|
+
body_part_id: host,
|
|
8679
|
+
attachments: byHost.get(host)
|
|
8680
|
+
}));
|
|
8681
|
+
return {
|
|
8682
|
+
draft,
|
|
8683
|
+
diagnostics
|
|
8684
|
+
};
|
|
8685
|
+
}
|
|
8763
8686
|
//#endregion
|
|
8764
|
-
export { tupleHash as S,
|
|
8687
|
+
export { tupleHash as C, resourceId as S, DslProjectionError as _, createInitialMedeoDsl as a, defaultIdFactory as b, MedeoDsl as c, documentFormat as d, medeoDslLoroShape as f, resolveDslLayout as g, timeAnchorCodec as h, DEFAULT_UNIT_TIME_MS as i, UnsupportedDocumentFormatError as l, isInvalidAtomic as m, fromVideoDraft as n, DslEditor as o, captionContentCodec as p, toVideoDraft as r, toAudioScriptViews as s, normalizeLegacyVideoDraft as t, assertDslDocument as u, BLANK_PLACEHOLDER as v, relationId as x, missingMediaPlaceholder as y };
|