@mengine/medeo-client 3.1.0 → 3.1.1-dsl.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 +4 -0
- package/dist/{dsl-C0-G8JVT.js → dsl-Xm5NW58k.js} +327 -90
- package/dist/dsl.d.ts +2 -2
- package/dist/dsl.js +2 -2
- package/dist/{index-BOPcHqIj.d.ts → index-CQ8h3u9S.d.ts} +29 -19
- package/dist/{index-C_wwGt5k.d.ts → index-D48u9I25.d.ts} +7 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +16 -3
- package/dist/schemas.d.ts +606 -0
- package/dist/schemas.js +478 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.js +35 -14
- package/package.json +6 -4
|
@@ -145,8 +145,12 @@ function assertCaptionContent(value) {
|
|
|
145
145
|
}
|
|
146
146
|
const phonemeFields = {
|
|
147
147
|
...selectionFields,
|
|
148
|
-
phonemeScript: optional$1(
|
|
148
|
+
phonemeScript: optional$1(nonempty)
|
|
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
|
+
}
|
|
150
154
|
function assertPhoneme(value) {
|
|
151
155
|
if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
|
|
152
156
|
}
|
|
@@ -174,7 +178,7 @@ const locator = {
|
|
|
174
178
|
};
|
|
175
179
|
/**
|
|
176
180
|
* 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 (
|
|
181
|
+
* visual clip the old editor drew as a blank (D205). The blank one stands in for a clip placed
|
|
178
182
|
* without media (`origin_media_id: ''`); one keyed by a memota media id stands in for media the
|
|
179
183
|
* clip named but Director found missing, so the id survives. Importing keeps the clips' time on
|
|
180
184
|
* them instead of dropping them. Only migration authors them.
|
|
@@ -608,9 +612,10 @@ function assertPhonemeSelection(rows, selection, text) {
|
|
|
608
612
|
assertSegmentRange(selection, text);
|
|
609
613
|
if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
|
|
610
614
|
}
|
|
611
|
-
/**
|
|
615
|
+
/** Every new or rewritten Caption selects its words from one AudioScript segment. */
|
|
612
616
|
function assertCaptionSource(caption, selected) {
|
|
613
|
-
if (
|
|
617
|
+
if (!selected) throw new Error("Caption must select a segment of an AudioScript");
|
|
618
|
+
if (caption?.text !== void 0) throw new Error("Caption must have only one content source");
|
|
614
619
|
}
|
|
615
620
|
//#endregion
|
|
616
621
|
//#region ../medeo-dsl/src/contain.ts
|
|
@@ -670,6 +675,63 @@ function assertContainTree(rows, tree) {
|
|
|
670
675
|
}
|
|
671
676
|
}
|
|
672
677
|
//#endregion
|
|
678
|
+
//#region ../medeo-dsl/src/clip-playback.ts
|
|
679
|
+
const playable = {
|
|
680
|
+
video_clip: ["video", "image"],
|
|
681
|
+
speech: ["audio", "voiceover-recording"],
|
|
682
|
+
caption: ["caption"],
|
|
683
|
+
bgm: ["audio"]
|
|
684
|
+
};
|
|
685
|
+
function supportsClipSpeed(source) {
|
|
686
|
+
return isRecord(source) && [
|
|
687
|
+
"video",
|
|
688
|
+
"audio",
|
|
689
|
+
"voiceover-recording"
|
|
690
|
+
].includes(String(source.kind));
|
|
691
|
+
}
|
|
692
|
+
/** Content-kind admission is shared by Actions, script compilation and the final transaction. */
|
|
693
|
+
function assertClipSourceRole(role, kind) {
|
|
694
|
+
if (!playable[role]?.includes(kind)) throw new Error(`A ${role} Track does not play ${kind}`);
|
|
695
|
+
}
|
|
696
|
+
function assertClipWindow(window, duration) {
|
|
697
|
+
if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs) throw new Error("Invalid Clip window");
|
|
698
|
+
if (duration !== void 0 && window.outMs > duration) throw new Error(`Invalid Clip window: ${window.outMs}ms is past its content's ${duration}ms`);
|
|
699
|
+
}
|
|
700
|
+
/** Check authored playback facts; a remote merge is still read by projection with diagnostics. */
|
|
701
|
+
function assertClipPlayback(clip, source, role) {
|
|
702
|
+
if (!isRecord(source)) throw new Error("Invalid Clip source");
|
|
703
|
+
const kind = source.kind ?? "caption";
|
|
704
|
+
const marker = clip.seqMarker;
|
|
705
|
+
assertSeqMarker(marker);
|
|
706
|
+
if (marker.outMs <= marker.inMs) throw new Error("Clip window must be nonempty");
|
|
707
|
+
if (role !== void 0) assertClipSourceRole(role, kind);
|
|
708
|
+
if (marker.rate !== void 0 && !supportsClipSpeed(source)) throw new Error("Only video and sound change speed");
|
|
709
|
+
const duration = typeof source.durationMs === "number" ? source.durationMs : void 0;
|
|
710
|
+
if (marker.fillTimeline) {
|
|
711
|
+
if (kind !== "audio") throw new Error("Only BGM audio fills the Timeline");
|
|
712
|
+
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`);
|
|
713
|
+
}
|
|
714
|
+
assertClipWindow(marker);
|
|
715
|
+
}
|
|
716
|
+
function clipParents(tree) {
|
|
717
|
+
const result = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const track of tree) for (const clip of track.children ?? []) result.set(clip.entityId, track.entityId);
|
|
719
|
+
return result;
|
|
720
|
+
}
|
|
721
|
+
/** A new parent or Track role revalidates that binding, without blocking unrelated merged rows. */
|
|
722
|
+
function assertClipPlacements(before, after, previousTree, nextTree) {
|
|
723
|
+
const previous = clipParents(previousTree);
|
|
724
|
+
for (const [id, parent] of clipParents(nextTree)) {
|
|
725
|
+
const track = after["track-entities"][parent];
|
|
726
|
+
const previousTrack = before["track-entities"][parent];
|
|
727
|
+
if (previous.get(id) === parent && (track === void 0 || isRecord(track) && isRecord(previousTrack) && previousTrack.role === track.role)) continue;
|
|
728
|
+
const role = requireEntity(after, "track-entities", parent).role;
|
|
729
|
+
const clip = after["clip-entities"][id];
|
|
730
|
+
if (!clip) continue;
|
|
731
|
+
assertClipPlayback(clip, after["media-entities"][clip.source] ?? after["caption-entities"][clip.source], role);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
673
735
|
//#region ../medeo-dsl/src/plan.ts
|
|
674
736
|
/** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
|
|
675
737
|
function replacementPatch(before, after) {
|
|
@@ -685,6 +747,7 @@ function replacementPatch(before, after) {
|
|
|
685
747
|
*/
|
|
686
748
|
function assertChangeAdmitted(change) {
|
|
687
749
|
const { before, after } = change;
|
|
750
|
+
const parents = change.containment ? clipParents(change.containment.after) : void 0;
|
|
688
751
|
for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
|
|
689
752
|
const value = after[table][id];
|
|
690
753
|
if (value === void 0) continue;
|
|
@@ -699,25 +762,17 @@ function assertChangeAdmitted(change) {
|
|
|
699
762
|
}
|
|
700
763
|
const row = value;
|
|
701
764
|
for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
765
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) {
|
|
766
|
+
const parent = parents?.get(id);
|
|
767
|
+
assertClipPlayback(row, after["media-entities"][row.source] ?? after["caption-entities"][row.source], parent === void 0 ? void 0 : requireEntity(after, "track-entities", parent).role);
|
|
768
|
+
}
|
|
705
769
|
}
|
|
706
770
|
if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
|
|
707
771
|
for (const [table, ids] of change.relations) for (const id of ids) {
|
|
708
772
|
const value = after[table][id];
|
|
709
773
|
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
710
774
|
}
|
|
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`);
|
|
775
|
+
if (change.containment) assertClipPlacements(before, after, change.containment.before, change.containment.after);
|
|
721
776
|
}
|
|
722
777
|
//#endregion
|
|
723
778
|
//#region src/dsl/projection/types.ts
|
|
@@ -953,15 +1008,11 @@ function requireSource(s, id, role) {
|
|
|
953
1008
|
return media;
|
|
954
1009
|
}
|
|
955
1010
|
function assertMediaRole(media, role) {
|
|
956
|
-
if (!media
|
|
957
|
-
|
|
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`);
|
|
1011
|
+
if (!media) throw new Error(`Missing source for ${role} Track`);
|
|
1012
|
+
assertClipSourceRole(role, media.kind);
|
|
962
1013
|
}
|
|
963
1014
|
function validateWindow(window, duration) {
|
|
964
|
-
|
|
1015
|
+
assertClipWindow(window, duration);
|
|
965
1016
|
}
|
|
966
1017
|
function trackOf(s, id) {
|
|
967
1018
|
requireClip(s, id);
|
|
@@ -1249,7 +1300,8 @@ function replaceSourcePlan(s, clipId, input) {
|
|
|
1249
1300
|
patch: {
|
|
1250
1301
|
seqMarker: {
|
|
1251
1302
|
inMs: input.inMs,
|
|
1252
|
-
outMs: input.outMs
|
|
1303
|
+
outMs: input.outMs,
|
|
1304
|
+
...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
|
|
1253
1305
|
},
|
|
1254
1306
|
source: source.id
|
|
1255
1307
|
}
|
|
@@ -5877,6 +5929,30 @@ const phonemeCodec = codec((v) => {
|
|
|
5877
5929
|
assertPhoneme(value);
|
|
5878
5930
|
return value;
|
|
5879
5931
|
});
|
|
5932
|
+
/** A row-owned binding couples the selection, pronunciation and its immutable file in one LWW value. */
|
|
5933
|
+
const phoneticBindingCodec = codec((value) => {
|
|
5934
|
+
assertEntity("phonetic-script-entities", value);
|
|
5935
|
+
assertPhoneticScript(void 0, { ...value });
|
|
5936
|
+
return JSON.stringify({
|
|
5937
|
+
phoneme: JSON.parse(phonemeCodec.encode(value.phoneme)),
|
|
5938
|
+
system: value.system,
|
|
5939
|
+
key: value.key,
|
|
5940
|
+
storageKey: value.storageKey,
|
|
5941
|
+
digest: value.digest
|
|
5942
|
+
});
|
|
5943
|
+
}, (raw) => {
|
|
5944
|
+
const value = JSON.parse(raw);
|
|
5945
|
+
if (!isRecord(value)) throw new Error("Expected a PhoneticScript binding");
|
|
5946
|
+
const phoneme = phonemeCodec.decode(JSON.stringify(value.phoneme));
|
|
5947
|
+
if (isInvalidAtomic(phoneme)) throw new Error(phoneme.reason);
|
|
5948
|
+
const binding = {
|
|
5949
|
+
...value,
|
|
5950
|
+
phoneme
|
|
5951
|
+
};
|
|
5952
|
+
assertEntity("phonetic-script-entities", binding);
|
|
5953
|
+
assertPhoneticScript(void 0, binding);
|
|
5954
|
+
return binding;
|
|
5955
|
+
});
|
|
5880
5956
|
/** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
|
|
5881
5957
|
const seqMarkerCodec = codec((v) => {
|
|
5882
5958
|
assertSeqMarker(v);
|
|
@@ -5943,13 +6019,7 @@ const medeoDslLoroShape = schema({
|
|
|
5943
6019
|
storageKey: schema.String(optional),
|
|
5944
6020
|
digest: schema.String(optional)
|
|
5945
6021
|
})),
|
|
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
|
-
})),
|
|
6022
|
+
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ binding: atomicValue(phoneticBindingCodec) })),
|
|
5953
6023
|
"caption-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5954
6024
|
selection: optionalAtomicValue(captionContentCodec),
|
|
5955
6025
|
text: schema.LoroText(optional),
|
|
@@ -6001,20 +6071,78 @@ function documentFormat(doc) {
|
|
|
6001
6071
|
}
|
|
6002
6072
|
var UnsupportedDocumentFormatError = class extends Error {
|
|
6003
6073
|
code = "unsupported_document_format";
|
|
6004
|
-
constructor() {
|
|
6005
|
-
super(
|
|
6074
|
+
constructor(message = "Expected a new DSL document; legacy, mixed or uninitialized content is not supported") {
|
|
6075
|
+
super(message);
|
|
6006
6076
|
this.name = "UnsupportedDocumentFormatError";
|
|
6007
6077
|
}
|
|
6008
6078
|
};
|
|
6009
6079
|
/** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
|
|
6010
6080
|
function assertDslDocument(doc) {
|
|
6011
6081
|
if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
|
|
6082
|
+
assertPhoneticBindingFormat(doc);
|
|
6083
|
+
}
|
|
6084
|
+
/** This storage cutover has no field-per-fact reader or mixed-version write path. */
|
|
6085
|
+
function assertPhoneticBindingFormat(doc) {
|
|
6086
|
+
if (!Object.hasOwn(doc.getShallowValue(), "phonetic-script-entities")) return;
|
|
6087
|
+
const table = doc.getMap("phonetic-script-entities");
|
|
6088
|
+
for (const id of table.keys()) {
|
|
6089
|
+
const row = table.get(id);
|
|
6090
|
+
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");
|
|
6091
|
+
}
|
|
6012
6092
|
}
|
|
6013
6093
|
//#endregion
|
|
6014
6094
|
//#region ../medeo-dsl/src/schema/text-styles.ts
|
|
6015
6095
|
/** How rich-text marks extend at their edges: a language runs on into text typed after it. */
|
|
6016
6096
|
const textStyles = { language: { expand: "after" } };
|
|
6017
6097
|
//#endregion
|
|
6098
|
+
//#region src/dsl/document/phonetic-binding.ts
|
|
6099
|
+
/**
|
|
6100
|
+
* Actions and the transaction guard keep their domain-shaped rows. The native schema stores
|
|
6101
|
+
* a PhoneticScript's facts under one binding; this lens applies draft edits to that single value.
|
|
6102
|
+
* It never reads the retired field-per-fact representation.
|
|
6103
|
+
*/
|
|
6104
|
+
function logicalDslState(native) {
|
|
6105
|
+
const phonetics = new Proxy({}, {
|
|
6106
|
+
get: (_target, key) => typeof key === "string" ? native["phonetic-script-entities"]?.[key]?.binding : void 0,
|
|
6107
|
+
set: (_target, key, value) => {
|
|
6108
|
+
if (typeof key !== "string") throw new Error("Invalid PhoneticScript key");
|
|
6109
|
+
native["phonetic-script-entities"] ??= {};
|
|
6110
|
+
const existing = native["phonetic-script-entities"][key];
|
|
6111
|
+
if (existing) existing.binding = value;
|
|
6112
|
+
else native["phonetic-script-entities"][key] = { binding: value };
|
|
6113
|
+
return true;
|
|
6114
|
+
},
|
|
6115
|
+
deleteProperty: (_target, key) => {
|
|
6116
|
+
if (typeof key === "string") delete native["phonetic-script-entities"][key];
|
|
6117
|
+
return true;
|
|
6118
|
+
},
|
|
6119
|
+
ownKeys: () => Object.keys(native["phonetic-script-entities"] ?? {}),
|
|
6120
|
+
has: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key),
|
|
6121
|
+
getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key) ? {
|
|
6122
|
+
configurable: true,
|
|
6123
|
+
enumerable: true,
|
|
6124
|
+
writable: true,
|
|
6125
|
+
value: native["phonetic-script-entities"][key].binding
|
|
6126
|
+
} : void 0
|
|
6127
|
+
});
|
|
6128
|
+
const view = {};
|
|
6129
|
+
for (const key of new Set([
|
|
6130
|
+
...Object.keys(native),
|
|
6131
|
+
...entityTables,
|
|
6132
|
+
...relationTables,
|
|
6133
|
+
"contain-relations"
|
|
6134
|
+
])) Object.defineProperty(view, key, {
|
|
6135
|
+
configurable: true,
|
|
6136
|
+
enumerable: true,
|
|
6137
|
+
get: () => key === "phonetic-script-entities" ? phonetics : Reflect.get(native, key),
|
|
6138
|
+
set: (value) => {
|
|
6139
|
+
if (key === "phonetic-script-entities") throw new Error("Cannot replace the PhoneticScript table");
|
|
6140
|
+
Reflect.set(native, key, value);
|
|
6141
|
+
}
|
|
6142
|
+
});
|
|
6143
|
+
return view;
|
|
6144
|
+
}
|
|
6145
|
+
//#endregion
|
|
6018
6146
|
//#region src/dsl/document/read.ts
|
|
6019
6147
|
/** Looking up a missing root must not create it. */
|
|
6020
6148
|
function rootMap(doc, name) {
|
|
@@ -6098,6 +6226,7 @@ function referenceTargets(table, row) {
|
|
|
6098
6226
|
}
|
|
6099
6227
|
}
|
|
6100
6228
|
function readMedeoDsl(doc, state) {
|
|
6229
|
+
const logical = logicalDslState(state);
|
|
6101
6230
|
const diagnostics = [];
|
|
6102
6231
|
const diagnostic = (code, path, error) => diagnostics.push({
|
|
6103
6232
|
code,
|
|
@@ -6107,7 +6236,7 @@ function readMedeoDsl(doc, state) {
|
|
|
6107
6236
|
const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
|
|
6108
6237
|
const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
|
|
6109
6238
|
const richText = {};
|
|
6110
|
-
for (const table of entityTables) for (const [id, raw] of Object.entries(
|
|
6239
|
+
for (const table of entityTables) for (const [id, raw] of Object.entries(logical[table] ?? {})) try {
|
|
6111
6240
|
assertId(id);
|
|
6112
6241
|
let v = raw;
|
|
6113
6242
|
for (const field of optionalReferences[table] ?? []) {
|
|
@@ -6613,7 +6742,11 @@ function validateDraft(tracked) {
|
|
|
6613
6742
|
before,
|
|
6614
6743
|
after: state,
|
|
6615
6744
|
entities: tracked.entities,
|
|
6616
|
-
relations: tracked.relations
|
|
6745
|
+
relations: tracked.relations,
|
|
6746
|
+
containment: {
|
|
6747
|
+
before: plainTree(before["contain-relations"] ?? []),
|
|
6748
|
+
after: plainTree(state["contain-relations"] ?? [])
|
|
6749
|
+
}
|
|
6617
6750
|
});
|
|
6618
6751
|
if (!tracked.treeChanged) return;
|
|
6619
6752
|
const oldNodes = nodeIndex(before["contain-relations"] ?? []);
|
|
@@ -6690,8 +6823,11 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6690
6823
|
const steps = [];
|
|
6691
6824
|
const newCaptionTexts = /* @__PURE__ */ new Map();
|
|
6692
6825
|
const bodies = /* @__PURE__ */ new Map();
|
|
6693
|
-
const
|
|
6694
|
-
|
|
6826
|
+
const nativeBefore = mirror.getState();
|
|
6827
|
+
const before = logicalDslState(nativeBefore);
|
|
6828
|
+
const phoneticRows = /* @__PURE__ */ new Map();
|
|
6829
|
+
mirror.setState((native) => {
|
|
6830
|
+
const raw = logicalDslState(native);
|
|
6695
6831
|
const tracked = new TransactionDraft(raw, before);
|
|
6696
6832
|
setReject((error) => tracked.fail(error));
|
|
6697
6833
|
/** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
|
|
@@ -6883,6 +7019,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6883
7019
|
for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
|
|
6884
7020
|
else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
|
|
6885
7021
|
assertSelectionsAfter(raw, before, tracked, steps);
|
|
7022
|
+
const changedPhonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...steps.flatMap((step) => step.type !== "text" && step.table === "phonetic-script-entities" ? [step.id] : [])]);
|
|
7023
|
+
for (const id of changedPhonetics) {
|
|
7024
|
+
const row = raw["phonetic-script-entities"][id];
|
|
7025
|
+
phoneticRows.set(id, row ? copyInput(row) : void 0);
|
|
7026
|
+
if (!row) continue;
|
|
7027
|
+
const old = nativeBefore["phonetic-script-entities"][id];
|
|
7028
|
+
if (old) native["phonetic-script-entities"][id] = old;
|
|
7029
|
+
else delete native["phonetic-script-entities"][id];
|
|
7030
|
+
}
|
|
6886
7031
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
6887
7032
|
const row = raw["caption-entities"][id];
|
|
6888
7033
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -6891,6 +7036,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6891
7036
|
}
|
|
6892
7037
|
}
|
|
6893
7038
|
for (const table of Object.keys(selectionField)) {
|
|
7039
|
+
if (table === "phonetic-script-entities") continue;
|
|
6894
7040
|
const field = selectionField[table];
|
|
6895
7041
|
for (const [id, fields] of tracked.entities.get(table) ?? []) {
|
|
6896
7042
|
const row = raw[table][id];
|
|
@@ -6930,6 +7076,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6930
7076
|
applyTextEdits(text, step.edits);
|
|
6931
7077
|
continue;
|
|
6932
7078
|
}
|
|
7079
|
+
const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
|
|
7080
|
+
if (step.table === "phonetic-script-entities" && !phonetic) continue;
|
|
6933
7081
|
const row = entityRow(doc, step.table, step.id);
|
|
6934
7082
|
const field = selectionField[step.table];
|
|
6935
7083
|
if (step.type === "range") {
|
|
@@ -6948,12 +7096,21 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6948
7096
|
if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
|
|
6949
7097
|
}
|
|
6950
7098
|
if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
|
|
6951
|
-
if (
|
|
6952
|
-
|
|
6953
|
-
|
|
7099
|
+
if (phonetic) phonetic.phoneme = value;
|
|
7100
|
+
else {
|
|
7101
|
+
if (!row) throw new Error("Unable to construct prepared range");
|
|
7102
|
+
row.set(field, codecs[step.table].encode(value));
|
|
7103
|
+
}
|
|
7104
|
+
} else if (step.value !== void 0) if (phonetic) phonetic.phoneme = step.value;
|
|
7105
|
+
else {
|
|
6954
7106
|
if (!row) throw new Error("Prepared selection owner is missing");
|
|
6955
7107
|
row.set(field, codecs[step.table].encode(step.value));
|
|
6956
|
-
}
|
|
7108
|
+
}
|
|
7109
|
+
else if (row?.get(field) !== void 0) row.delete(field);
|
|
7110
|
+
}
|
|
7111
|
+
for (const [id, binding] of phoneticRows) {
|
|
7112
|
+
if (!binding) continue;
|
|
7113
|
+
(entityRow(doc, "phonetic-script-entities", id) ?? doc.getMap("phonetic-script-entities").setContainer(id, new LoroMap())).set("binding", phoneticBindingCodec.encode(binding));
|
|
6957
7114
|
}
|
|
6958
7115
|
}
|
|
6959
7116
|
});
|
|
@@ -6966,7 +7123,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6966
7123
|
function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
6967
7124
|
const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
|
|
6968
7125
|
const changedCaptions = /* @__PURE__ */ new Set();
|
|
6969
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
7126
|
+
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (!before["caption-entities"][id] || fields.has("text")) changedCaptions.add(id);
|
|
6970
7127
|
const phonemes = /* @__PURE__ */ new Map();
|
|
6971
7128
|
for (const step of steps) {
|
|
6972
7129
|
if (step.type === "text") {
|
|
@@ -7000,7 +7157,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
|
7000
7157
|
if (!row) continue;
|
|
7001
7158
|
const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
|
|
7002
7159
|
const old = before["phonetic-script-entities"][id];
|
|
7003
|
-
assertPhoneticScript(old, {
|
|
7160
|
+
assertPhoneticScript(old ? { ...old } : void 0, {
|
|
7004
7161
|
...row,
|
|
7005
7162
|
phoneme: isRecord(phoneme) ? phoneme : void 0
|
|
7006
7163
|
});
|
|
@@ -7039,6 +7196,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7039
7196
|
"tracks",
|
|
7040
7197
|
"part_library"
|
|
7041
7198
|
].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
|
|
7199
|
+
assertPhoneticBindingFormat(doc);
|
|
7042
7200
|
doc.configTextStyle(textStyles);
|
|
7043
7201
|
this.mirror = new Mirror({
|
|
7044
7202
|
doc,
|
|
@@ -7102,6 +7260,11 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7102
7260
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
7103
7261
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
7104
7262
|
}
|
|
7263
|
+
/** Check the session's write capability before an Action starts host-owned resource I/O. */
|
|
7264
|
+
assertWritable() {
|
|
7265
|
+
this.assertReadyToWrite();
|
|
7266
|
+
this.options.mutationGuard?.run(() => {});
|
|
7267
|
+
}
|
|
7105
7268
|
/**
|
|
7106
7269
|
* A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
|
|
7107
7270
|
* include insertions at their end. K01 remains deferred.
|
|
@@ -7134,13 +7297,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7134
7297
|
if (this.ownsDoc) this.doc.free();
|
|
7135
7298
|
}
|
|
7136
7299
|
mutate(run) {
|
|
7137
|
-
this.
|
|
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");
|
|
7300
|
+
this.assertReadyToWrite();
|
|
7144
7301
|
const apply = () => {
|
|
7145
7302
|
this.writing = true;
|
|
7146
7303
|
try {
|
|
@@ -7155,6 +7312,15 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7155
7312
|
if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
|
|
7156
7313
|
else apply();
|
|
7157
7314
|
}
|
|
7315
|
+
assertReadyToWrite() {
|
|
7316
|
+
this.assertActive();
|
|
7317
|
+
if (this.writing || this.notifying) {
|
|
7318
|
+
const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
|
|
7319
|
+
this.rejectTransaction?.(error);
|
|
7320
|
+
throw error;
|
|
7321
|
+
}
|
|
7322
|
+
if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
|
|
7323
|
+
}
|
|
7158
7324
|
notify() {
|
|
7159
7325
|
if (this.disposed || !this.dirty) return;
|
|
7160
7326
|
this.dirty = false;
|
|
@@ -7181,12 +7347,13 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7181
7347
|
const found = /* @__PURE__ */ new Map();
|
|
7182
7348
|
const seen = /* @__PURE__ */ new Set();
|
|
7183
7349
|
const visit = (id, phoneticScriptId) => {
|
|
7184
|
-
if (seen.has(id)) return;
|
|
7185
|
-
seen.add(id);
|
|
7186
7350
|
if (snapshot["audio-script-entities"][id]) {
|
|
7187
|
-
if (!found.has(id)) found.set(id,
|
|
7351
|
+
if (!found.has(id)) found.set(id, /* @__PURE__ */ new Set());
|
|
7352
|
+
if (phoneticScriptId !== void 0) found.get(id).add(phoneticScriptId);
|
|
7188
7353
|
return;
|
|
7189
7354
|
}
|
|
7355
|
+
if (seen.has(id)) return;
|
|
7356
|
+
seen.add(id);
|
|
7190
7357
|
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
7191
7358
|
if (phonetic) {
|
|
7192
7359
|
visit(phonetic.phoneme.audioScriptId, id);
|
|
@@ -7196,11 +7363,18 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7196
7363
|
};
|
|
7197
7364
|
visit(recordingId);
|
|
7198
7365
|
if (!found.size) {
|
|
7199
|
-
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId,
|
|
7366
|
+
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, /* @__PURE__ */ new Set());
|
|
7200
7367
|
}
|
|
7201
|
-
return [...found].map(([audioScriptId,
|
|
7202
|
-
|
|
7203
|
-
|
|
7368
|
+
return [...found].map(([audioScriptId, ids]) => {
|
|
7369
|
+
const phoneticScriptIds = [...ids];
|
|
7370
|
+
if (phoneticScriptIds.length === 1) return {
|
|
7371
|
+
audioScriptId,
|
|
7372
|
+
phoneticScriptId: phoneticScriptIds[0]
|
|
7373
|
+
};
|
|
7374
|
+
return phoneticScriptIds.length ? {
|
|
7375
|
+
audioScriptId,
|
|
7376
|
+
phoneticScriptIds
|
|
7377
|
+
} : { audioScriptId };
|
|
7204
7378
|
});
|
|
7205
7379
|
}
|
|
7206
7380
|
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
@@ -7245,7 +7419,6 @@ function toAudioScriptViews(snapshot) {
|
|
|
7245
7419
|
});
|
|
7246
7420
|
}
|
|
7247
7421
|
const layout = resolveDslLayout(snapshot);
|
|
7248
|
-
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7249
7422
|
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
7250
7423
|
for (const track of layout.tracks) for (const { clipId } of track.clips) {
|
|
7251
7424
|
const source = snapshot["clip-entities"][clipId]?.source;
|
|
@@ -7269,6 +7442,7 @@ function toAudioScriptViews(snapshot) {
|
|
|
7269
7442
|
clipId,
|
|
7270
7443
|
recordingId: source,
|
|
7271
7444
|
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
7445
|
+
...spoken.phoneticScriptIds === void 0 ? {} : { phoneticScriptIds: spoken.phoneticScriptIds },
|
|
7272
7446
|
voices,
|
|
7273
7447
|
...applied === void 0 ? {} : { applied }
|
|
7274
7448
|
});
|
|
@@ -7285,19 +7459,23 @@ function toAudioScriptViews(snapshot) {
|
|
|
7285
7459
|
segment
|
|
7286
7460
|
});
|
|
7287
7461
|
}
|
|
7462
|
+
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7288
7463
|
return [...views.values()];
|
|
7289
7464
|
}
|
|
7290
7465
|
/**
|
|
7291
7466
|
* A transcript's segments on the Timeline: a segment's time is in the medium the script was read
|
|
7292
|
-
* off
|
|
7467
|
+
* off. An authored Caption anchor identifies its use of that medium; otherwise the earliest
|
|
7468
|
+
* Clip holding that source moment is used. The source time is never taken from the Caption.
|
|
7293
7469
|
*/
|
|
7294
7470
|
function placeTranscript(snapshot, layout, view) {
|
|
7295
7471
|
if (!view.sourceMediaIds.length) return;
|
|
7296
7472
|
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);
|
|
7297
|
-
for (const segment of view.segments) {
|
|
7473
|
+
for (const [index, segment] of view.segments.entries()) {
|
|
7298
7474
|
const at = segment.startMs;
|
|
7299
7475
|
if (at === void 0) continue;
|
|
7300
|
-
const
|
|
7476
|
+
const anchoredUses = new Set(view.captions.filter((caption) => caption.segment === index).map((caption) => layout.clips[caption.clipId]?.anchorClipId));
|
|
7477
|
+
const candidates = plays.filter((c) => c.inMs <= at && at < c.outMs);
|
|
7478
|
+
const clip = candidates.find((c) => anchoredUses.has(c.clipId)) ?? candidates[0];
|
|
7301
7479
|
if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
|
|
7302
7480
|
}
|
|
7303
7481
|
}
|
|
@@ -7308,14 +7486,26 @@ function placeTranscript(snapshot, layout, view) {
|
|
|
7308
7486
|
* not what a take was synthesized from (D190).
|
|
7309
7487
|
*/
|
|
7310
7488
|
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
7311
|
-
const
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7489
|
+
const ids = spoken.phoneticScriptIds ?? (spoken.phoneticScriptId === void 0 ? [] : [spoken.phoneticScriptId]);
|
|
7490
|
+
if (!ids.length) return void 0;
|
|
7491
|
+
let unknown = false;
|
|
7492
|
+
for (const sourceId of ids) {
|
|
7493
|
+
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
7494
|
+
const current = snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
7495
|
+
if (recorded === void 0 || current === void 0) unknown = true;
|
|
7496
|
+
else if (recorded !== current) return false;
|
|
7497
|
+
}
|
|
7498
|
+
return unknown ? void 0 : true;
|
|
7315
7499
|
}
|
|
7316
7500
|
//#endregion
|
|
7317
7501
|
//#region src/dsl/voiceover-actions.ts
|
|
7318
|
-
function
|
|
7502
|
+
function matchesPlainPhoneticOrigin(s, id, scriptId, segmentId) {
|
|
7503
|
+
const phonetic = s["phonetic-script-entities"][id];
|
|
7504
|
+
const run = s.phoneticText[id];
|
|
7505
|
+
const segment = s["audio-script-entities"][scriptId]?.segments.find((item) => item.id === segmentId);
|
|
7506
|
+
return phonetic?.phoneme.audioScriptId === scriptId && phonetic.phoneme.segmentId === segmentId && phonetic.phoneme.phonemeScript === void 0 && run?.start === 0 && run.end === segment?.text.length;
|
|
7507
|
+
}
|
|
7508
|
+
function voiceoverPlan(s, input, result, plainPhoneticIds) {
|
|
7319
7509
|
if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
|
|
7320
7510
|
if (input.captions.length && (!input.captionTrackId || requireTrack(s, input.captionTrackId).role !== "caption")) throw new Error("Caption Track is required");
|
|
7321
7511
|
const existing = s["clip-entities"][result.clipId];
|
|
@@ -7354,15 +7544,44 @@ function voiceoverPlan(s, input, result) {
|
|
|
7354
7544
|
inMs: 0,
|
|
7355
7545
|
outMs: input.media.durationMs
|
|
7356
7546
|
}, input.media.durationMs);
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7547
|
+
if (phoneticScriptId !== void 0) {
|
|
7548
|
+
const recorded = Object.values(s["derived-from-relations"]).find((origin) => origin.entityId === result.mediaId && origin.sourceId === phoneticScriptId);
|
|
7549
|
+
if (recorded?.sourceDigest !== void 0 && input.sourceDigest !== void 0 && recorded.sourceDigest !== input.sourceDigest) throw new Error("A recording already has a different synthesis digest");
|
|
7550
|
+
if (!recorded || recorded.sourceDigest === void 0 && input.sourceDigest !== void 0) mutations.push({
|
|
7551
|
+
type: "set-relation",
|
|
7552
|
+
table: "derived-from-relations",
|
|
7553
|
+
value: {
|
|
7554
|
+
entityId: result.mediaId,
|
|
7555
|
+
sourceId: phoneticScriptId,
|
|
7556
|
+
...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
|
|
7557
|
+
}
|
|
7558
|
+
});
|
|
7559
|
+
} else {
|
|
7560
|
+
if (input.sourceDigest !== void 0) throw new Error("A synthesis digest requires its PhoneticScript");
|
|
7561
|
+
if (plainPhoneticIds.length !== segmentIds.length) throw new Error("Expected one PhoneticScript id per plain segment");
|
|
7562
|
+
segmentIds.forEach((segmentId, i) => {
|
|
7563
|
+
const id = plainPhoneticIds[i];
|
|
7564
|
+
if (s["phonetic-script-entities"][id]) {
|
|
7565
|
+
if (!matchesPlainPhoneticOrigin(s, id, result.audioScriptId, segmentId)) throw new Error("PhoneticScript id already names another synthesis origin");
|
|
7566
|
+
} else mutations.push({
|
|
7567
|
+
type: "create-entity",
|
|
7568
|
+
table: "phonetic-script-entities",
|
|
7569
|
+
id,
|
|
7570
|
+
value: { phoneme: {
|
|
7571
|
+
audioScriptId: result.audioScriptId,
|
|
7572
|
+
segmentId
|
|
7573
|
+
} }
|
|
7574
|
+
});
|
|
7575
|
+
if (!Object.values(s["derived-from-relations"]).some((origin) => origin.entityId === result.mediaId && origin.sourceId === id)) mutations.push({
|
|
7576
|
+
type: "set-relation",
|
|
7577
|
+
table: "derived-from-relations",
|
|
7578
|
+
value: {
|
|
7579
|
+
entityId: result.mediaId,
|
|
7580
|
+
sourceId: id
|
|
7581
|
+
}
|
|
7582
|
+
});
|
|
7583
|
+
});
|
|
7584
|
+
}
|
|
7366
7585
|
if (input.voice) {
|
|
7367
7586
|
const id = resourceId("voice", input.voice.system, input.voice.key);
|
|
7368
7587
|
mutations.push({
|
|
@@ -7859,10 +8078,20 @@ var DslEditor = class {
|
|
|
7859
8078
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
7860
8079
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
7861
8080
|
};
|
|
8081
|
+
const state = this.dsl.getSnapshot();
|
|
8082
|
+
const plainSegments = input.script.segments === void 0 ? state["audio-script-entities"][result.audioScriptId]?.segments.map((segment) => segment.id) ?? [] : result.segmentIds;
|
|
8083
|
+
const plainPhoneticIds = input.script.phoneticScriptId === void 0 ? plainSegments.map((segmentId) => {
|
|
8084
|
+
const origins = Object.values(state["derived-from-relations"]).filter((origin) => {
|
|
8085
|
+
if (origin.entityId !== result.mediaId) return false;
|
|
8086
|
+
return matchesPlainPhoneticOrigin(state, origin.sourceId, result.audioScriptId, segmentId);
|
|
8087
|
+
});
|
|
8088
|
+
if (origins.length > 1) throw new Error("Ambiguous plain recording origin");
|
|
8089
|
+
return origins[0]?.sourceId ?? this.idFactory("phonetic-script");
|
|
8090
|
+
}) : [];
|
|
7862
8091
|
this.write("applyVoiceover", {
|
|
7863
8092
|
input,
|
|
7864
8093
|
result
|
|
7865
|
-
}, voiceoverPlan(
|
|
8094
|
+
}, voiceoverPlan(state, input, result, plainPhoneticIds), options);
|
|
7866
8095
|
return result;
|
|
7867
8096
|
}
|
|
7868
8097
|
voiceoverScriptId(script) {
|
|
@@ -7879,8 +8108,12 @@ var DslEditor = class {
|
|
|
7879
8108
|
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
7880
8109
|
*/
|
|
7881
8110
|
async setPhoneticScript(input, options) {
|
|
8111
|
+
this.dsl.assertWritable();
|
|
7882
8112
|
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
7883
8113
|
const range = segmentRange(input);
|
|
8114
|
+
assertId(id);
|
|
8115
|
+
assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
|
|
8116
|
+
assertPhonemeScript(input.phonemeScript);
|
|
7884
8117
|
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
7885
8118
|
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
7886
8119
|
const cleared = {
|
|
@@ -8105,7 +8338,8 @@ function toVideoDraft(snapshot) {
|
|
|
8105
8338
|
} }
|
|
8106
8339
|
} };
|
|
8107
8340
|
} else if (track.role === "speech") {
|
|
8108
|
-
if (!source || source.kind !== "
|
|
8341
|
+
if (!source || source.kind !== "audio" && source.kind !== "voiceover-recording") fail("Speech playback requires an Audio or VoiceoverRecording");
|
|
8342
|
+
if (source.system !== "memota" && !(source.kind === "voiceover-recording" && source.system === "memota-speech")) fail("Speech playback requires a Memota audio file");
|
|
8109
8343
|
requireWholeAudio(source, clip, fail);
|
|
8110
8344
|
if (clip.fillTimeline) fail("Legacy Speech cannot express timeline fill");
|
|
8111
8345
|
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]] : []);
|
|
@@ -8122,7 +8356,7 @@ function toVideoDraft(snapshot) {
|
|
|
8122
8356
|
audio_storage_key: source.storageKey,
|
|
8123
8357
|
origin_speech_id: source.key,
|
|
8124
8358
|
volume: clip.volume,
|
|
8125
|
-
audio_script: spokenText(snapshot, sourceId),
|
|
8359
|
+
audio_script: source.kind === "voiceover-recording" ? spokenText(snapshot, sourceId) : "",
|
|
8126
8360
|
voice: voice ? {
|
|
8127
8361
|
id: voice.key,
|
|
8128
8362
|
name: voice.name
|
|
@@ -8218,6 +8452,15 @@ function spokenText(snapshot, recordingId) {
|
|
|
8218
8452
|
if (!first) return "";
|
|
8219
8453
|
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
8220
8454
|
const script = snapshot["audio-script-entities"][first.audioScriptId];
|
|
8455
|
+
if (first.phoneticScriptIds !== void 0 && script) {
|
|
8456
|
+
const ids = first.phoneticScriptIds;
|
|
8457
|
+
const selections = ids.map((id) => snapshot["phonetic-script-entities"][id]);
|
|
8458
|
+
if (!(ids.length === script.segments.length && script.segments.every((segment) => {
|
|
8459
|
+
const matching = ids.filter((id) => snapshot["phonetic-script-entities"][id]?.phoneme.segmentId === segment.id);
|
|
8460
|
+
const run = matching.length === 1 ? snapshot.phoneticText[matching[0]] : void 0;
|
|
8461
|
+
return run?.start === 0 && run.end === segment.text.length;
|
|
8462
|
+
})) || selections.some((selection) => selection.phoneme.phonemeScript !== void 0)) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple phonetic sources require complete plain selections of one script");
|
|
8463
|
+
}
|
|
8221
8464
|
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
8222
8465
|
}
|
|
8223
8466
|
//#endregion
|
|
@@ -8440,11 +8683,7 @@ function prepare(input, options) {
|
|
|
8440
8683
|
start: start - segment.start,
|
|
8441
8684
|
end: end - segment.start
|
|
8442
8685
|
});
|
|
8443
|
-
} else
|
|
8444
|
-
code: "caption-text-unmatched",
|
|
8445
|
-
path: `part_library/${captionId}/text`,
|
|
8446
|
-
message: "No forward exact match; preserved as independent Caption text"
|
|
8447
|
-
});
|
|
8686
|
+
} else fail(`part_library/${captionId}/text`, "Caption must match a segment of its AudioScript");
|
|
8448
8687
|
}
|
|
8449
8688
|
}
|
|
8450
8689
|
const hosts = /* @__PURE__ */ new Map();
|
|
@@ -8487,16 +8726,14 @@ function prepare(input, options) {
|
|
|
8487
8726
|
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");
|
|
8488
8727
|
sourceId = claim(mint("caption"));
|
|
8489
8728
|
const selected = selections.get(id), style = fromDraftStyle(caption.style);
|
|
8729
|
+
if (!selected) fail(`part_library/${id}/text`, "Caption must select a segment of an AudioScript");
|
|
8490
8730
|
mutations.push({
|
|
8491
8731
|
type: "create-entity",
|
|
8492
8732
|
table: "caption-entities",
|
|
8493
8733
|
id: sourceId,
|
|
8494
|
-
value: {
|
|
8495
|
-
...style === void 0 ? {} : { style },
|
|
8496
|
-
...selected ? {} : { text: caption.text }
|
|
8497
|
-
}
|
|
8734
|
+
value: style === void 0 ? {} : { style }
|
|
8498
8735
|
});
|
|
8499
|
-
|
|
8736
|
+
mutations.push({
|
|
8500
8737
|
type: "set-caption-content",
|
|
8501
8738
|
captionId: sourceId,
|
|
8502
8739
|
...selected
|
|
@@ -8684,4 +8921,4 @@ function normalizeLegacyVideoDraft(input, mediaFacts = []) {
|
|
|
8684
8921
|
};
|
|
8685
8922
|
}
|
|
8686
8923
|
//#endregion
|
|
8687
|
-
export {
|
|
8924
|
+
export { relationId as C, defaultIdFactory as S, tupleHash as T, timeAnchorCodec as _, createInitialMedeoDsl as a, BLANK_PLACEHOLDER as b, toAudioScriptViews as c, assertDslDocument as d, assertPhoneticBindingFormat as f, isInvalidAtomic as g, captionContentCodec as h, DEFAULT_UNIT_TIME_MS as i, MedeoDsl as l, medeoDslLoroShape as m, fromVideoDraft as n, DslEditor as o, documentFormat as p, toVideoDraft as r, spokenScripts as s, normalizeLegacyVideoDraft as t, UnsupportedDocumentFormatError as u, resolveDslLayout as v, resourceId as w, missingMediaPlaceholder as x, DslProjectionError as y };
|