@mengine/medeo-client 3.0.0 → 3.0.1-caption-source.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-Cpk8WxcZ.js → dsl-96AZnd7O.js} +326 -89
- package/dist/dsl.d.ts +2 -2
- package/dist/dsl.js +2 -2
- package/dist/{index-DuM7I4Ku.d.ts → index-C1iiDt_Q.d.ts} +28 -18
- package/dist/{index-Dldi944U.d.ts → index-CSjqNIoR.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 +8 -6
|
@@ -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
|
}
|
|
@@ -576,9 +580,10 @@ function assertPhonemeSelection(rows, selection, text) {
|
|
|
576
580
|
assertSegmentRange(selection, text);
|
|
577
581
|
if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
|
|
578
582
|
}
|
|
579
|
-
/**
|
|
583
|
+
/** Every new or rewritten Caption selects its words from one AudioScript segment. */
|
|
580
584
|
function assertCaptionSource(caption, selected) {
|
|
581
|
-
if (
|
|
585
|
+
if (!selected) throw new Error("Caption must select a segment of an AudioScript");
|
|
586
|
+
if (caption?.text !== void 0) throw new Error("Caption must have only one content source");
|
|
582
587
|
}
|
|
583
588
|
//#endregion
|
|
584
589
|
//#region ../medeo-dsl/src/contain.ts
|
|
@@ -638,6 +643,63 @@ function assertContainTree(rows, tree) {
|
|
|
638
643
|
}
|
|
639
644
|
}
|
|
640
645
|
//#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
|
|
641
703
|
//#region ../medeo-dsl/src/plan.ts
|
|
642
704
|
/** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
|
|
643
705
|
function replacementPatch(before, after) {
|
|
@@ -653,6 +715,7 @@ function replacementPatch(before, after) {
|
|
|
653
715
|
*/
|
|
654
716
|
function assertChangeAdmitted(change) {
|
|
655
717
|
const { before, after } = change;
|
|
718
|
+
const parents = change.containment ? clipParents(change.containment.after) : void 0;
|
|
656
719
|
for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
|
|
657
720
|
const value = after[table][id];
|
|
658
721
|
if (value === void 0) continue;
|
|
@@ -667,25 +730,17 @@ function assertChangeAdmitted(change) {
|
|
|
667
730
|
}
|
|
668
731
|
const row = value;
|
|
669
732
|
for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
733
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) {
|
|
734
|
+
const parent = parents?.get(id);
|
|
735
|
+
assertClipPlayback(row, after["media-entities"][row.source] ?? after["caption-entities"][row.source], parent === void 0 ? void 0 : requireEntity(after, "track-entities", parent).role);
|
|
736
|
+
}
|
|
673
737
|
}
|
|
674
738
|
if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
|
|
675
739
|
for (const [table, ids] of change.relations) for (const id of ids) {
|
|
676
740
|
const value = after[table][id];
|
|
677
741
|
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
678
742
|
}
|
|
679
|
-
|
|
680
|
-
/**
|
|
681
|
-
* A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
|
|
682
|
-
* cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
|
|
683
|
-
*/
|
|
684
|
-
function assertFillsWithWholeSource(rows, marker, source) {
|
|
685
|
-
if (marker?.fillTimeline !== true) return;
|
|
686
|
-
const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
|
|
687
|
-
const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
|
|
688
|
-
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`);
|
|
743
|
+
if (change.containment) assertClipPlacements(before, after, change.containment.before, change.containment.after);
|
|
689
744
|
}
|
|
690
745
|
//#endregion
|
|
691
746
|
//#region src/dsl/projection/types.ts
|
|
@@ -921,15 +976,11 @@ function requireSource(s, id, role) {
|
|
|
921
976
|
return media;
|
|
922
977
|
}
|
|
923
978
|
function assertMediaRole(media, role) {
|
|
924
|
-
if (!media
|
|
925
|
-
|
|
926
|
-
speech: ["audio", "voiceover-recording"],
|
|
927
|
-
bgm: ["audio"],
|
|
928
|
-
caption: []
|
|
929
|
-
}[role].includes(media.kind)) throw new Error(`Source is not allowed on ${role} Track`);
|
|
979
|
+
if (!media) throw new Error(`Missing source for ${role} Track`);
|
|
980
|
+
assertClipSourceRole(role, media.kind);
|
|
930
981
|
}
|
|
931
982
|
function validateWindow(window, duration) {
|
|
932
|
-
|
|
983
|
+
assertClipWindow(window, duration);
|
|
933
984
|
}
|
|
934
985
|
function trackOf(s, id) {
|
|
935
986
|
requireClip(s, id);
|
|
@@ -1217,7 +1268,8 @@ function replaceSourcePlan(s, clipId, input) {
|
|
|
1217
1268
|
patch: {
|
|
1218
1269
|
seqMarker: {
|
|
1219
1270
|
inMs: input.inMs,
|
|
1220
|
-
outMs: input.outMs
|
|
1271
|
+
outMs: input.outMs,
|
|
1272
|
+
...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
|
|
1221
1273
|
},
|
|
1222
1274
|
source: source.id
|
|
1223
1275
|
}
|
|
@@ -5845,6 +5897,30 @@ const phonemeCodec = codec((v) => {
|
|
|
5845
5897
|
assertPhoneme(value);
|
|
5846
5898
|
return value;
|
|
5847
5899
|
});
|
|
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
|
+
});
|
|
5848
5924
|
/** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
|
|
5849
5925
|
const seqMarkerCodec = codec((v) => {
|
|
5850
5926
|
assertSeqMarker(v);
|
|
@@ -5911,13 +5987,7 @@ const medeoDslLoroShape = schema({
|
|
|
5911
5987
|
storageKey: schema.String(optional),
|
|
5912
5988
|
digest: schema.String(optional)
|
|
5913
5989
|
})),
|
|
5914
|
-
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5915
|
-
phoneme: atomicValue(phonemeCodec),
|
|
5916
|
-
system: schema.String(optional),
|
|
5917
|
-
key: schema.String(optional),
|
|
5918
|
-
storageKey: schema.String(optional),
|
|
5919
|
-
digest: schema.String(optional)
|
|
5920
|
-
})),
|
|
5990
|
+
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ binding: atomicValue(phoneticBindingCodec) })),
|
|
5921
5991
|
"caption-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5922
5992
|
selection: optionalAtomicValue(captionContentCodec),
|
|
5923
5993
|
text: schema.LoroText(optional),
|
|
@@ -5969,20 +6039,78 @@ function documentFormat(doc) {
|
|
|
5969
6039
|
}
|
|
5970
6040
|
var UnsupportedDocumentFormatError = class extends Error {
|
|
5971
6041
|
code = "unsupported_document_format";
|
|
5972
|
-
constructor() {
|
|
5973
|
-
super(
|
|
6042
|
+
constructor(message = "Expected a new DSL document; legacy, mixed or uninitialized content is not supported") {
|
|
6043
|
+
super(message);
|
|
5974
6044
|
this.name = "UnsupportedDocumentFormatError";
|
|
5975
6045
|
}
|
|
5976
6046
|
};
|
|
5977
6047
|
/** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
|
|
5978
6048
|
function assertDslDocument(doc) {
|
|
5979
6049
|
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
|
+
}
|
|
5980
6060
|
}
|
|
5981
6061
|
//#endregion
|
|
5982
6062
|
//#region ../medeo-dsl/src/schema/text-styles.ts
|
|
5983
6063
|
/** How rich-text marks extend at their edges: a language runs on into text typed after it. */
|
|
5984
6064
|
const textStyles = { language: { expand: "after" } };
|
|
5985
6065
|
//#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
|
|
5986
6114
|
//#region src/dsl/document/read.ts
|
|
5987
6115
|
/** Looking up a missing root must not create it. */
|
|
5988
6116
|
function rootMap(doc, name) {
|
|
@@ -6066,6 +6194,7 @@ function referenceTargets(table, row) {
|
|
|
6066
6194
|
}
|
|
6067
6195
|
}
|
|
6068
6196
|
function readMedeoDsl(doc, state) {
|
|
6197
|
+
const logical = logicalDslState(state);
|
|
6069
6198
|
const diagnostics = [];
|
|
6070
6199
|
const diagnostic = (code, path, error) => diagnostics.push({
|
|
6071
6200
|
code,
|
|
@@ -6075,7 +6204,7 @@ function readMedeoDsl(doc, state) {
|
|
|
6075
6204
|
const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
|
|
6076
6205
|
const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
|
|
6077
6206
|
const richText = {};
|
|
6078
|
-
for (const table of entityTables) for (const [id, raw] of Object.entries(
|
|
6207
|
+
for (const table of entityTables) for (const [id, raw] of Object.entries(logical[table] ?? {})) try {
|
|
6079
6208
|
assertId(id);
|
|
6080
6209
|
let v = raw;
|
|
6081
6210
|
for (const field of optionalReferences[table] ?? []) {
|
|
@@ -6581,7 +6710,11 @@ function validateDraft(tracked) {
|
|
|
6581
6710
|
before,
|
|
6582
6711
|
after: state,
|
|
6583
6712
|
entities: tracked.entities,
|
|
6584
|
-
relations: tracked.relations
|
|
6713
|
+
relations: tracked.relations,
|
|
6714
|
+
containment: {
|
|
6715
|
+
before: plainTree(before["contain-relations"] ?? []),
|
|
6716
|
+
after: plainTree(state["contain-relations"] ?? [])
|
|
6717
|
+
}
|
|
6585
6718
|
});
|
|
6586
6719
|
if (!tracked.treeChanged) return;
|
|
6587
6720
|
const oldNodes = nodeIndex(before["contain-relations"] ?? []);
|
|
@@ -6658,8 +6791,11 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6658
6791
|
const steps = [];
|
|
6659
6792
|
const newCaptionTexts = /* @__PURE__ */ new Map();
|
|
6660
6793
|
const bodies = /* @__PURE__ */ new Map();
|
|
6661
|
-
const
|
|
6662
|
-
|
|
6794
|
+
const nativeBefore = mirror.getState();
|
|
6795
|
+
const before = logicalDslState(nativeBefore);
|
|
6796
|
+
const phoneticRows = /* @__PURE__ */ new Map();
|
|
6797
|
+
mirror.setState((native) => {
|
|
6798
|
+
const raw = logicalDslState(native);
|
|
6663
6799
|
const tracked = new TransactionDraft(raw, before);
|
|
6664
6800
|
setReject((error) => tracked.fail(error));
|
|
6665
6801
|
/** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
|
|
@@ -6851,6 +6987,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6851
6987
|
for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
|
|
6852
6988
|
else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
|
|
6853
6989
|
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
|
+
}
|
|
6854
6999
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
6855
7000
|
const row = raw["caption-entities"][id];
|
|
6856
7001
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -6859,6 +7004,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6859
7004
|
}
|
|
6860
7005
|
}
|
|
6861
7006
|
for (const table of Object.keys(selectionField)) {
|
|
7007
|
+
if (table === "phonetic-script-entities") continue;
|
|
6862
7008
|
const field = selectionField[table];
|
|
6863
7009
|
for (const [id, fields] of tracked.entities.get(table) ?? []) {
|
|
6864
7010
|
const row = raw[table][id];
|
|
@@ -6898,6 +7044,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6898
7044
|
applyTextEdits(text, step.edits);
|
|
6899
7045
|
continue;
|
|
6900
7046
|
}
|
|
7047
|
+
const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
|
|
7048
|
+
if (step.table === "phonetic-script-entities" && !phonetic) continue;
|
|
6901
7049
|
const row = entityRow(doc, step.table, step.id);
|
|
6902
7050
|
const field = selectionField[step.table];
|
|
6903
7051
|
if (step.type === "range") {
|
|
@@ -6916,12 +7064,21 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6916
7064
|
if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
|
|
6917
7065
|
}
|
|
6918
7066
|
if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
|
|
6919
|
-
if (
|
|
6920
|
-
|
|
6921
|
-
|
|
7067
|
+
if (phonetic) phonetic.phoneme = value;
|
|
7068
|
+
else {
|
|
7069
|
+
if (!row) throw new Error("Unable to construct prepared range");
|
|
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 {
|
|
6922
7074
|
if (!row) throw new Error("Prepared selection owner is missing");
|
|
6923
7075
|
row.set(field, codecs[step.table].encode(step.value));
|
|
6924
|
-
}
|
|
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));
|
|
6925
7082
|
}
|
|
6926
7083
|
}
|
|
6927
7084
|
});
|
|
@@ -6934,7 +7091,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
6934
7091
|
function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
6935
7092
|
const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
|
|
6936
7093
|
const changedCaptions = /* @__PURE__ */ new Set();
|
|
6937
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
7094
|
+
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (!before["caption-entities"][id] || fields.has("text")) changedCaptions.add(id);
|
|
6938
7095
|
const phonemes = /* @__PURE__ */ new Map();
|
|
6939
7096
|
for (const step of steps) {
|
|
6940
7097
|
if (step.type === "text") {
|
|
@@ -6968,7 +7125,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
|
6968
7125
|
if (!row) continue;
|
|
6969
7126
|
const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
|
|
6970
7127
|
const old = before["phonetic-script-entities"][id];
|
|
6971
|
-
assertPhoneticScript(old, {
|
|
7128
|
+
assertPhoneticScript(old ? { ...old } : void 0, {
|
|
6972
7129
|
...row,
|
|
6973
7130
|
phoneme: isRecord(phoneme) ? phoneme : void 0
|
|
6974
7131
|
});
|
|
@@ -7007,6 +7164,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7007
7164
|
"tracks",
|
|
7008
7165
|
"part_library"
|
|
7009
7166
|
].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
|
|
7167
|
+
assertPhoneticBindingFormat(doc);
|
|
7010
7168
|
doc.configTextStyle(textStyles);
|
|
7011
7169
|
this.mirror = new Mirror({
|
|
7012
7170
|
doc,
|
|
@@ -7070,6 +7228,11 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7070
7228
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
7071
7229
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
7072
7230
|
}
|
|
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
|
+
}
|
|
7073
7236
|
/**
|
|
7074
7237
|
* A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
|
|
7075
7238
|
* include insertions at their end. K01 remains deferred.
|
|
@@ -7102,13 +7265,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7102
7265
|
if (this.ownsDoc) this.doc.free();
|
|
7103
7266
|
}
|
|
7104
7267
|
mutate(run) {
|
|
7105
|
-
this.
|
|
7106
|
-
if (this.writing || this.notifying) {
|
|
7107
|
-
const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
|
|
7108
|
-
this.rejectTransaction?.(error);
|
|
7109
|
-
throw error;
|
|
7110
|
-
}
|
|
7111
|
-
if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
|
|
7268
|
+
this.assertReadyToWrite();
|
|
7112
7269
|
const apply = () => {
|
|
7113
7270
|
this.writing = true;
|
|
7114
7271
|
try {
|
|
@@ -7123,6 +7280,15 @@ var MedeoDsl = class MedeoDsl {
|
|
|
7123
7280
|
if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
|
|
7124
7281
|
else apply();
|
|
7125
7282
|
}
|
|
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
|
+
}
|
|
7126
7292
|
notify() {
|
|
7127
7293
|
if (this.disposed || !this.dirty) return;
|
|
7128
7294
|
this.dirty = false;
|
|
@@ -7149,12 +7315,13 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7149
7315
|
const found = /* @__PURE__ */ new Map();
|
|
7150
7316
|
const seen = /* @__PURE__ */ new Set();
|
|
7151
7317
|
const visit = (id, phoneticScriptId) => {
|
|
7152
|
-
if (seen.has(id)) return;
|
|
7153
|
-
seen.add(id);
|
|
7154
7318
|
if (snapshot["audio-script-entities"][id]) {
|
|
7155
|
-
if (!found.has(id)) found.set(id,
|
|
7319
|
+
if (!found.has(id)) found.set(id, /* @__PURE__ */ new Set());
|
|
7320
|
+
if (phoneticScriptId !== void 0) found.get(id).add(phoneticScriptId);
|
|
7156
7321
|
return;
|
|
7157
7322
|
}
|
|
7323
|
+
if (seen.has(id)) return;
|
|
7324
|
+
seen.add(id);
|
|
7158
7325
|
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
7159
7326
|
if (phonetic) {
|
|
7160
7327
|
visit(phonetic.phoneme.audioScriptId, id);
|
|
@@ -7164,11 +7331,18 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
7164
7331
|
};
|
|
7165
7332
|
visit(recordingId);
|
|
7166
7333
|
if (!found.size) {
|
|
7167
|
-
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId,
|
|
7334
|
+
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, /* @__PURE__ */ new Set());
|
|
7168
7335
|
}
|
|
7169
|
-
return [...found].map(([audioScriptId,
|
|
7170
|
-
|
|
7171
|
-
|
|
7336
|
+
return [...found].map(([audioScriptId, ids]) => {
|
|
7337
|
+
const phoneticScriptIds = [...ids];
|
|
7338
|
+
if (phoneticScriptIds.length === 1) return {
|
|
7339
|
+
audioScriptId,
|
|
7340
|
+
phoneticScriptId: phoneticScriptIds[0]
|
|
7341
|
+
};
|
|
7342
|
+
return phoneticScriptIds.length ? {
|
|
7343
|
+
audioScriptId,
|
|
7344
|
+
phoneticScriptIds
|
|
7345
|
+
} : { audioScriptId };
|
|
7172
7346
|
});
|
|
7173
7347
|
}
|
|
7174
7348
|
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
@@ -7213,7 +7387,6 @@ function toAudioScriptViews(snapshot) {
|
|
|
7213
7387
|
});
|
|
7214
7388
|
}
|
|
7215
7389
|
const layout = resolveDslLayout(snapshot);
|
|
7216
|
-
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7217
7390
|
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
7218
7391
|
for (const track of layout.tracks) for (const { clipId } of track.clips) {
|
|
7219
7392
|
const source = snapshot["clip-entities"][clipId]?.source;
|
|
@@ -7237,6 +7410,7 @@ function toAudioScriptViews(snapshot) {
|
|
|
7237
7410
|
clipId,
|
|
7238
7411
|
recordingId: source,
|
|
7239
7412
|
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
7413
|
+
...spoken.phoneticScriptIds === void 0 ? {} : { phoneticScriptIds: spoken.phoneticScriptIds },
|
|
7240
7414
|
voices,
|
|
7241
7415
|
...applied === void 0 ? {} : { applied }
|
|
7242
7416
|
});
|
|
@@ -7253,19 +7427,23 @@ function toAudioScriptViews(snapshot) {
|
|
|
7253
7427
|
segment
|
|
7254
7428
|
});
|
|
7255
7429
|
}
|
|
7430
|
+
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7256
7431
|
return [...views.values()];
|
|
7257
7432
|
}
|
|
7258
7433
|
/**
|
|
7259
7434
|
* A transcript's segments on the Timeline: a segment's time is in the medium the script was read
|
|
7260
|
-
* off
|
|
7435
|
+
* off. An authored Caption anchor identifies its use of that medium; otherwise the earliest
|
|
7436
|
+
* Clip holding that source moment is used. The source time is never taken from the Caption.
|
|
7261
7437
|
*/
|
|
7262
7438
|
function placeTranscript(snapshot, layout, view) {
|
|
7263
7439
|
if (!view.sourceMediaIds.length) return;
|
|
7264
7440
|
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);
|
|
7265
|
-
for (const segment of view.segments) {
|
|
7441
|
+
for (const [index, segment] of view.segments.entries()) {
|
|
7266
7442
|
const at = segment.startMs;
|
|
7267
7443
|
if (at === void 0) continue;
|
|
7268
|
-
const
|
|
7444
|
+
const anchoredUses = new Set(view.captions.filter((caption) => caption.segment === index).map((caption) => layout.clips[caption.clipId]?.anchorClipId));
|
|
7445
|
+
const candidates = plays.filter((c) => c.inMs <= at && at < c.outMs);
|
|
7446
|
+
const clip = candidates.find((c) => anchoredUses.has(c.clipId)) ?? candidates[0];
|
|
7269
7447
|
if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
|
|
7270
7448
|
}
|
|
7271
7449
|
}
|
|
@@ -7276,14 +7454,26 @@ function placeTranscript(snapshot, layout, view) {
|
|
|
7276
7454
|
* not what a take was synthesized from (D190).
|
|
7277
7455
|
*/
|
|
7278
7456
|
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
7279
|
-
const
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
|
|
7457
|
+
const ids = spoken.phoneticScriptIds ?? (spoken.phoneticScriptId === void 0 ? [] : [spoken.phoneticScriptId]);
|
|
7458
|
+
if (!ids.length) return void 0;
|
|
7459
|
+
let unknown = false;
|
|
7460
|
+
for (const sourceId of ids) {
|
|
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;
|
|
7283
7467
|
}
|
|
7284
7468
|
//#endregion
|
|
7285
7469
|
//#region src/dsl/voiceover-actions.ts
|
|
7286
|
-
function
|
|
7470
|
+
function matchesPlainPhoneticOrigin(s, id, scriptId, segmentId) {
|
|
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) {
|
|
7287
7477
|
if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
|
|
7288
7478
|
if (input.captions.length && (!input.captionTrackId || requireTrack(s, input.captionTrackId).role !== "caption")) throw new Error("Caption Track is required");
|
|
7289
7479
|
const existing = s["clip-entities"][result.clipId];
|
|
@@ -7322,15 +7512,44 @@ function voiceoverPlan(s, input, result) {
|
|
|
7322
7512
|
inMs: 0,
|
|
7323
7513
|
outMs: input.media.durationMs
|
|
7324
7514
|
}, input.media.durationMs);
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7515
|
+
if (phoneticScriptId !== void 0) {
|
|
7516
|
+
const recorded = Object.values(s["derived-from-relations"]).find((origin) => origin.entityId === result.mediaId && origin.sourceId === phoneticScriptId);
|
|
7517
|
+
if (recorded?.sourceDigest !== void 0 && input.sourceDigest !== void 0 && recorded.sourceDigest !== input.sourceDigest) throw new Error("A recording already has a different synthesis digest");
|
|
7518
|
+
if (!recorded || recorded.sourceDigest === void 0 && input.sourceDigest !== void 0) mutations.push({
|
|
7519
|
+
type: "set-relation",
|
|
7520
|
+
table: "derived-from-relations",
|
|
7521
|
+
value: {
|
|
7522
|
+
entityId: result.mediaId,
|
|
7523
|
+
sourceId: phoneticScriptId,
|
|
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
|
+
}
|
|
7334
7553
|
if (input.voice) {
|
|
7335
7554
|
const id = resourceId("voice", input.voice.system, input.voice.key);
|
|
7336
7555
|
mutations.push({
|
|
@@ -7827,10 +8046,20 @@ var DslEditor = class {
|
|
|
7827
8046
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
7828
8047
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
7829
8048
|
};
|
|
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
|
+
}) : [];
|
|
7830
8059
|
this.write("applyVoiceover", {
|
|
7831
8060
|
input,
|
|
7832
8061
|
result
|
|
7833
|
-
}, voiceoverPlan(
|
|
8062
|
+
}, voiceoverPlan(state, input, result, plainPhoneticIds), options);
|
|
7834
8063
|
return result;
|
|
7835
8064
|
}
|
|
7836
8065
|
voiceoverScriptId(script) {
|
|
@@ -7847,8 +8076,12 @@ var DslEditor = class {
|
|
|
7847
8076
|
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
7848
8077
|
*/
|
|
7849
8078
|
async setPhoneticScript(input, options) {
|
|
8079
|
+
this.dsl.assertWritable();
|
|
7850
8080
|
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
7851
8081
|
const range = segmentRange(input);
|
|
8082
|
+
assertId(id);
|
|
8083
|
+
assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
|
|
8084
|
+
assertPhonemeScript(input.phonemeScript);
|
|
7852
8085
|
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
7853
8086
|
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
7854
8087
|
const cleared = {
|
|
@@ -8072,7 +8305,8 @@ function toVideoDraft(snapshot) {
|
|
|
8072
8305
|
} }
|
|
8073
8306
|
} };
|
|
8074
8307
|
} else if (track.role === "speech") {
|
|
8075
|
-
if (!source || source.kind !== "
|
|
8308
|
+
if (!source || source.kind !== "audio" && source.kind !== "voiceover-recording") fail("Speech playback requires an Audio or VoiceoverRecording");
|
|
8309
|
+
if (source.system !== "memota" && !(source.kind === "voiceover-recording" && source.system === "memota-speech")) fail("Speech playback requires a Memota audio file");
|
|
8076
8310
|
requireWholeAudio(source, clip, fail);
|
|
8077
8311
|
if (clip.fillTimeline) fail("Legacy Speech cannot express timeline fill");
|
|
8078
8312
|
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]] : []);
|
|
@@ -8089,7 +8323,7 @@ function toVideoDraft(snapshot) {
|
|
|
8089
8323
|
audio_storage_key: source.storageKey,
|
|
8090
8324
|
origin_speech_id: source.key,
|
|
8091
8325
|
volume: clip.volume,
|
|
8092
|
-
audio_script: spokenText(snapshot, sourceId),
|
|
8326
|
+
audio_script: source.kind === "voiceover-recording" ? spokenText(snapshot, sourceId) : "",
|
|
8093
8327
|
voice: voice ? {
|
|
8094
8328
|
id: voice.key,
|
|
8095
8329
|
name: voice.name
|
|
@@ -8185,6 +8419,15 @@ function spokenText(snapshot, recordingId) {
|
|
|
8185
8419
|
if (!first) return "";
|
|
8186
8420
|
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
8187
8421
|
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
|
+
}
|
|
8188
8431
|
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
8189
8432
|
}
|
|
8190
8433
|
//#endregion
|
|
@@ -8397,11 +8640,7 @@ function prepare(input, options) {
|
|
|
8397
8640
|
start: start - segment.start,
|
|
8398
8641
|
end: end - segment.start
|
|
8399
8642
|
});
|
|
8400
|
-
} else
|
|
8401
|
-
code: "caption-text-unmatched",
|
|
8402
|
-
path: `part_library/${captionId}/text`,
|
|
8403
|
-
message: "No forward exact match; preserved as independent Caption text"
|
|
8404
|
-
});
|
|
8643
|
+
} else fail(`part_library/${captionId}/text`, "Caption must match a segment of its AudioScript");
|
|
8405
8644
|
}
|
|
8406
8645
|
}
|
|
8407
8646
|
const hosts = /* @__PURE__ */ new Map();
|
|
@@ -8444,16 +8683,14 @@ function prepare(input, options) {
|
|
|
8444
8683
|
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");
|
|
8445
8684
|
sourceId = claim(mint("caption"));
|
|
8446
8685
|
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");
|
|
8447
8687
|
mutations.push({
|
|
8448
8688
|
type: "create-entity",
|
|
8449
8689
|
table: "caption-entities",
|
|
8450
8690
|
id: sourceId,
|
|
8451
|
-
value: {
|
|
8452
|
-
...style === void 0 ? {} : { style },
|
|
8453
|
-
...selected ? {} : { text: caption.text }
|
|
8454
|
-
}
|
|
8691
|
+
value: style === void 0 ? {} : { style }
|
|
8455
8692
|
});
|
|
8456
|
-
|
|
8693
|
+
mutations.push({
|
|
8457
8694
|
type: "set-caption-content",
|
|
8458
8695
|
captionId: sourceId,
|
|
8459
8696
|
...selected
|
|
@@ -8524,4 +8761,4 @@ function importedScript(id, text, mintSegment) {
|
|
|
8524
8761
|
};
|
|
8525
8762
|
}
|
|
8526
8763
|
//#endregion
|
|
8527
|
-
export {
|
|
8764
|
+
export { tupleHash as S, resolveDslLayout as _, DslEditor as a, relationId as b, MedeoDsl as c, assertPhoneticBindingFormat as d, documentFormat as f, timeAnchorCodec as g, isInvalidAtomic as h, createInitialMedeoDsl as i, UnsupportedDocumentFormatError as l, captionContentCodec as m, toVideoDraft as n, spokenScripts as o, medeoDslLoroShape as p, DEFAULT_UNIT_TIME_MS as r, toAudioScriptViews as s, fromVideoDraft as t, assertDslDocument as u, DslProjectionError as v, resourceId as x, defaultIdFactory as y };
|