@mengine/medeo-client 2.1.1-dsl.8 → 2.1.1-dsl.9
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 +10 -9
- package/dist/{dsl-DbYqFFOb.js → dsl-COyzz7Fp.js} +622 -298
- package/dist/dsl.d.ts +2 -2
- package/dist/dsl.js +2 -2
- package/dist/{index-DHcMMn2M.d.ts → index-C5wDKYqC.d.ts} +60 -35
- package/dist/{index-Cx5DojM1.d.ts → index-CHiPztZr.d.ts} +22 -7
- package/dist/{index-Dpl4bT94.d.ts → index-DAfOkcnZ.d.ts} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -2
- package/dist/legacy.d.ts +2 -2
- package/dist/legacy.js +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +4 -7
- package/dist/{video-draft-types-Ce8NDenQ.js → video-draft-types-BnbfWrBx.js} +64 -42
- package/dist/{video-draft-types-Bq15tPBP.d.ts → video-draft-types-GqXnsOzA.d.ts} +69 -21
- package/package.json +4 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as assertEntityPatch, A as getRootContainerByType, B as isValueOfContainerType, C as applyEncode, D as decodeNestedJsonValues, E as containerIdToContainerType, F as isArrayLike, G as tryUpdateToContainer, H as schemaToContainerType, I as isObject, J as getChildContainerSchema, K as valuesEqual, L as isObjectLike, M as hasTransform, N as inferContainerTypeFromValue, O as deepEqual, P as insertChildToMap, Q as assertEntity, R as isStateAndSchemaOfType, S as applyDecode, T as cidsEqual, U as stripUndefined, V as safeStringify, W as tryInferContainerType, X as getMapFieldSchema, Y as getChildSchema, Z as assertCaptionContent, _ as isLoroMovableListSchema, at as atomicFields, b as schemaContainsIgnoreField, c as isInvalidAtomic, ct as mergeFields, d as schema, dt as validLanguage, et as assertId, f as getDefaultValue, ft as assertRelationIdentity, g as isLoroMapSchema, gt as resourceId, h as isLoroListSchema, ht as relationId, it as assertTimeAnchor, j as hardenCidDescriptors, k as defineCidProperty, l as phonemeCodec, lt as patchRow, m as isListLikeSchema, mt as defaultIdFactory, n as textStyles, nt as assertPhoneticScript, o as medeoDslLoroShape, ot as entityTables, p as isContainerSchema, pt as assertResourceIdentity, q as CID_KEY, rt as assertRelation, s as captionContentCodec, st as isRecord, t as DEFAULT_UNIT_TIME_MS, tt as assertPhoneme, ut as relationTables, v as isLoroTreeSchema, w as applySchemaToInferOptions, x as validateSchema, y as isRootSchemaType, z as isTreeID } from "./video-draft-types-BnbfWrBx.js";
|
|
2
2
|
import { Cursor, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, isContainer, isContainerId } from "loro-crdt";
|
|
3
3
|
import { Immer, setAutoFreeze } from "immer";
|
|
4
4
|
//#region ../medeo-dsl/src/text.ts
|
|
@@ -104,42 +104,27 @@ function prepareTextEdits(text, edits) {
|
|
|
104
104
|
}
|
|
105
105
|
return edit;
|
|
106
106
|
}),
|
|
107
|
-
|
|
107
|
+
text
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
/**
|
|
111
|
-
function
|
|
112
|
-
if (
|
|
113
|
-
const
|
|
114
|
-
let
|
|
115
|
-
for (const text of body.split("\n")) {
|
|
116
|
-
result.push({
|
|
117
|
-
text,
|
|
118
|
-
start,
|
|
119
|
-
end: start + text.length
|
|
120
|
-
});
|
|
121
|
-
start += text.length + 1;
|
|
122
|
-
}
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
/** The language a whole line carries: one shared mark on every unit, else none. */
|
|
126
|
-
function lineLanguage(text, line) {
|
|
127
|
-
if (line.start === line.end) return void 0;
|
|
128
|
-
const first = text.langs[line.start];
|
|
129
|
-
for (let i = line.start + 1; i < line.end; i++) if (text.langs[i] !== first) return void 0;
|
|
110
|
+
/** The language a whole segment carries: one shared mark on every unit, else none. */
|
|
111
|
+
function markedLanguage(text) {
|
|
112
|
+
if (!text.text.length) return void 0;
|
|
113
|
+
const first = text.langs[0];
|
|
114
|
+
for (let i = 1; i < text.text.length; i++) if (text.langs[i] !== first) return void 0;
|
|
130
115
|
return first;
|
|
131
116
|
}
|
|
132
117
|
/** Read language marks off a rich-text delta. */
|
|
133
118
|
function langsFromDelta(delta) {
|
|
134
|
-
let
|
|
119
|
+
let text = "";
|
|
135
120
|
const langs = [];
|
|
136
121
|
for (const run of delta) {
|
|
137
|
-
|
|
122
|
+
text += run.insert;
|
|
138
123
|
const language = typeof run.attributes?.language === "string" ? run.attributes.language : void 0;
|
|
139
124
|
for (let i = 0; i < run.insert.length; i++) langs.push(language);
|
|
140
125
|
}
|
|
141
126
|
return {
|
|
142
|
-
|
|
127
|
+
text,
|
|
143
128
|
langs
|
|
144
129
|
};
|
|
145
130
|
}
|
|
@@ -178,16 +163,23 @@ const referenceFields = {
|
|
|
178
163
|
"phonetic-script-entities": ["phoneme"],
|
|
179
164
|
"caption-entities": ["selection"]
|
|
180
165
|
};
|
|
166
|
+
/** The segment a selection names: refused when its script or the segment is missing. */
|
|
167
|
+
function requireSegment(rows, audioScriptId, segmentId) {
|
|
168
|
+
const script = requireEntity(rows, "audio-script-entities", audioScriptId);
|
|
169
|
+
const segment = Array.isArray(script.segments) ? script.segments.find((s) => isRecord(s) && s.id === segmentId) : void 0;
|
|
170
|
+
if (!segment) throw new Error(`Missing segment ${String(segmentId)} of ${String(audioScriptId)}`);
|
|
171
|
+
return segment;
|
|
172
|
+
}
|
|
181
173
|
/**
|
|
182
174
|
* Whether a single-valued relation, stored as a field of the entity that owns it, may hold this
|
|
183
175
|
* value among these rows. The owner need not be stored yet; an absent value unlinks it.
|
|
184
|
-
* A
|
|
176
|
+
* A selection's run is checked against the segment's live text by whoever holds it.
|
|
185
177
|
*/
|
|
186
178
|
function assertReferenceAdmitted(rows, table, id, field, value) {
|
|
187
179
|
assertId(id);
|
|
188
180
|
const key = `${table}.${field}`;
|
|
189
181
|
if (value === void 0) {
|
|
190
|
-
if (key === "clip-entities.source"
|
|
182
|
+
if (key === "clip-entities.source") throw new Error(`Missing required relation: ${key}`);
|
|
191
183
|
return;
|
|
192
184
|
}
|
|
193
185
|
switch (key) {
|
|
@@ -213,26 +205,49 @@ function assertReferenceAdmitted(rows, table, id, field, value) {
|
|
|
213
205
|
break;
|
|
214
206
|
}
|
|
215
207
|
case "caption-entities.selection":
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
requireEntity(rows, "audio-script-entities", value.audioScriptId);
|
|
208
|
+
case "phonetic-script-entities.phoneme": {
|
|
209
|
+
if (key === "caption-entities.selection") assertCaptionContent(value);
|
|
210
|
+
else assertPhoneme(value);
|
|
211
|
+
const { audioScriptId, segmentId } = value;
|
|
212
|
+
requireSegment(rows, audioScriptId, segmentId);
|
|
222
213
|
break;
|
|
214
|
+
}
|
|
223
215
|
}
|
|
224
216
|
}
|
|
225
|
-
/** Both ends of a caption's selection exist: the caption and the
|
|
217
|
+
/** Both ends of a caption's selection exist: the caption and the segment it selects. */
|
|
226
218
|
function assertCaptionEndpoints(rows, selection) {
|
|
227
219
|
requireEntity(rows, "caption-entities", selection.captionId);
|
|
228
|
-
|
|
220
|
+
requireSegment(rows, selection.audioScriptId, selection.segmentId);
|
|
221
|
+
}
|
|
222
|
+
/** Both ends of a phoneme exist: the PhoneticScript and the segment it is written for. */
|
|
223
|
+
function assertPhonemeEndpoints(rows, selection) {
|
|
224
|
+
requireEntity(rows, "phonetic-script-entities", selection.phoneticScriptId);
|
|
225
|
+
requireSegment(rows, selection.audioScriptId, selection.segmentId);
|
|
229
226
|
}
|
|
230
|
-
/**
|
|
231
|
-
|
|
227
|
+
/**
|
|
228
|
+
* A selection of a segment's current text: the whole segment, or a run of it in UTF-16 offsets
|
|
229
|
+
* with both ends stated. What it selects is never empty.
|
|
230
|
+
*/
|
|
231
|
+
function assertSegmentRange(range, text) {
|
|
232
|
+
if (range.start === void 0 !== (range.end === void 0)) throw new Error("A run inside a segment states both its start and its end");
|
|
233
|
+
if (range.start === void 0 || range.end === void 0) {
|
|
234
|
+
if (!text.length) throw new Error("An empty segment cannot be selected");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
assertTextOffset(text, range.start);
|
|
238
|
+
assertTextOffset(text, range.end);
|
|
239
|
+
if (range.start >= range.end) throw new Error("Expected a nonempty range in the current segment");
|
|
240
|
+
}
|
|
241
|
+
/** A caption selects a segment of its script, or a nonempty run of it. */
|
|
242
|
+
function assertCaptionSelection(rows, selection, text) {
|
|
232
243
|
assertCaptionEndpoints(rows, selection);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
244
|
+
assertSegmentRange(selection, text);
|
|
245
|
+
}
|
|
246
|
+
/** A PhoneticScript is written for a segment of its script, or a nonempty run of it, as a Caption selects one. */
|
|
247
|
+
function assertPhonemeSelection(rows, selection, text) {
|
|
248
|
+
assertPhonemeEndpoints(rows, selection);
|
|
249
|
+
assertSegmentRange(selection, text);
|
|
250
|
+
if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
|
|
236
251
|
}
|
|
237
252
|
/** A caption's text comes from a selection of a script or from its own text, never both. */
|
|
238
253
|
function assertCaptionSource(caption, selected) {
|
|
@@ -3898,11 +3913,21 @@ function rootMap(doc, name) {
|
|
|
3898
3913
|
const value = doc.getContainerById(id);
|
|
3899
3914
|
return value?.kind() === "Map" ? value : void 0;
|
|
3900
3915
|
}
|
|
3901
|
-
function
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
return isContainer(
|
|
3916
|
+
function asMap(value) {
|
|
3917
|
+
return isContainer(value) && value.kind() === "Map" ? value : void 0;
|
|
3918
|
+
}
|
|
3919
|
+
function asText(value) {
|
|
3920
|
+
return isContainer(value) && value.kind() === "Text" && !value.isDeleted() ? value : void 0;
|
|
3921
|
+
}
|
|
3922
|
+
/** The live Text a target names: a Caption's own, or a script segment's. */
|
|
3923
|
+
function liveText(doc, target) {
|
|
3924
|
+
if ("captionId" in target) return asText(asMap(rootMap(doc, "caption-entities")?.get(target.captionId))?.get("text"));
|
|
3925
|
+
const segments = asMap(rootMap(doc, "audio-script-entities")?.get(target.audioScriptId))?.get("segments");
|
|
3926
|
+
if (!isContainer(segments) || segments.kind() !== "MovableList") return;
|
|
3927
|
+
for (const item of segments.toArray()) {
|
|
3928
|
+
const segment = asMap(item);
|
|
3929
|
+
if (segment?.get("id") === target.segmentId) return asText(segment.get("text"));
|
|
3930
|
+
}
|
|
3906
3931
|
}
|
|
3907
3932
|
function readContainNodes(nodes) {
|
|
3908
3933
|
const read = (nodes) => nodes.map((node) => ({
|
|
@@ -3912,19 +3937,41 @@ function readContainNodes(nodes) {
|
|
|
3912
3937
|
}));
|
|
3913
3938
|
return read(nodes);
|
|
3914
3939
|
}
|
|
3940
|
+
/** What a selection holds now: its whole segment, or the run between its Cursors on that segment's Text. */
|
|
3915
3941
|
function resolveContent(doc, content) {
|
|
3916
|
-
const text = liveText(doc,
|
|
3917
|
-
|
|
3942
|
+
const text = liveText(doc, {
|
|
3943
|
+
audioScriptId: content.audioScriptId,
|
|
3944
|
+
segmentId: content.segmentId
|
|
3945
|
+
});
|
|
3946
|
+
if (!text) return;
|
|
3947
|
+
const whole = text.toString();
|
|
3948
|
+
if (content.start === void 0 || content.end === void 0) return {
|
|
3949
|
+
text: whole,
|
|
3950
|
+
start: 0,
|
|
3951
|
+
end: whole.length,
|
|
3952
|
+
bodyId: text.id
|
|
3953
|
+
};
|
|
3954
|
+
if (content.start.containerId() !== text.id || content.end.containerId() !== text.id) return;
|
|
3918
3955
|
const start = doc.getCursorPos(content.start)?.offset;
|
|
3919
3956
|
const end = doc.getCursorPos(content.end)?.offset;
|
|
3920
3957
|
if (start === void 0 || end === void 0 || start > end) return;
|
|
3921
3958
|
return {
|
|
3922
|
-
text:
|
|
3959
|
+
text: whole.slice(start, end),
|
|
3923
3960
|
start,
|
|
3924
3961
|
end,
|
|
3925
3962
|
bodyId: text.id
|
|
3926
3963
|
};
|
|
3927
3964
|
}
|
|
3965
|
+
function richTextOf(text) {
|
|
3966
|
+
return {
|
|
3967
|
+
bodyId: text.id,
|
|
3968
|
+
plainText: text.toString(),
|
|
3969
|
+
delta: text.toDelta().flatMap((run) => typeof run.insert === "string" ? [{
|
|
3970
|
+
insert: run.insert,
|
|
3971
|
+
...run.attributes ? { attributes: run.attributes } : {}
|
|
3972
|
+
}] : [])
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3928
3975
|
/** Optional relation fields read apart from their row: an unreadable one is dropped, the row is kept. */
|
|
3929
3976
|
const optionalReferences = {
|
|
3930
3977
|
"clip-entities": ["anchor"],
|
|
@@ -3962,19 +4009,16 @@ function readMedeoDsl(doc, state) {
|
|
|
3962
4009
|
}
|
|
3963
4010
|
assertEntity(table, v);
|
|
3964
4011
|
assertResourceIdentity(table, id, v);
|
|
4012
|
+
if (table === "phonetic-script-entities") assertPhoneticScript(void 0, v);
|
|
4013
|
+
const texts = [];
|
|
4014
|
+
if (table === "audio-script-entities") for (const segment of v.segments) texts.push([segment.id, liveText(doc, {
|
|
4015
|
+
audioScriptId: id,
|
|
4016
|
+
segmentId: segment.id
|
|
4017
|
+
})]);
|
|
4018
|
+
if (table === "caption-entities" && v.text !== void 0) texts.push([id, liveText(doc, { captionId: id })]);
|
|
4019
|
+
for (const [, text] of texts) if (!text) throw new Error("Missing live LoroText");
|
|
3965
4020
|
entities[table][id] = v;
|
|
3966
|
-
|
|
3967
|
-
const text = liveText(doc, id, table === "caption-entities");
|
|
3968
|
-
if (!text) throw new Error("Missing live LoroText body");
|
|
3969
|
-
richText[id] = {
|
|
3970
|
-
bodyId: text.id,
|
|
3971
|
-
plainText: text.toString(),
|
|
3972
|
-
delta: text.toDelta().flatMap((run) => typeof run.insert === "string" ? [{
|
|
3973
|
-
insert: run.insert,
|
|
3974
|
-
...run.attributes ? { attributes: run.attributes } : {}
|
|
3975
|
-
}] : [])
|
|
3976
|
-
};
|
|
3977
|
-
}
|
|
4021
|
+
for (const [key, text] of texts) richText[key] = richTextOf(text);
|
|
3978
4022
|
} catch (error) {
|
|
3979
4023
|
delete entities[table][id];
|
|
3980
4024
|
diagnostic("invalid-entity", `${table}/${id}`, error);
|
|
@@ -4033,17 +4077,26 @@ function readMedeoDsl(doc, state) {
|
|
|
4033
4077
|
try {
|
|
4034
4078
|
const resolved = resolveContent(doc, caption.selection);
|
|
4035
4079
|
if (resolved) captionText[id] = resolved;
|
|
4036
|
-
else diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, "
|
|
4080
|
+
else diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, "Selection does not resolve in its current segment");
|
|
4037
4081
|
} catch (error) {
|
|
4038
4082
|
diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, error);
|
|
4039
4083
|
}
|
|
4040
4084
|
}
|
|
4085
|
+
const phoneticText = {};
|
|
4086
|
+
for (const [id, phonetic] of Object.entries(entities["phonetic-script-entities"])) try {
|
|
4087
|
+
const resolved = resolveContent(doc, phonetic.phoneme);
|
|
4088
|
+
if (resolved) phoneticText[id] = resolved;
|
|
4089
|
+
else diagnostic("invalid-phoneme", `phonetic-script-entities/${id}/phoneme`, "Selection does not resolve in its current segment");
|
|
4090
|
+
} catch (error) {
|
|
4091
|
+
diagnostic("invalid-phoneme", `phonetic-script-entities/${id}/phoneme`, error);
|
|
4092
|
+
}
|
|
4041
4093
|
return freezeSnapshot({
|
|
4042
4094
|
...entities,
|
|
4043
4095
|
...relations,
|
|
4044
4096
|
"contain-relations": validTree(treeNodes),
|
|
4045
4097
|
richText,
|
|
4046
4098
|
captionText,
|
|
4099
|
+
phoneticText,
|
|
4047
4100
|
diagnostics
|
|
4048
4101
|
});
|
|
4049
4102
|
}
|
|
@@ -4056,6 +4109,19 @@ function freezeSnapshot(value) {
|
|
|
4056
4109
|
}
|
|
4057
4110
|
//#endregion
|
|
4058
4111
|
//#region src/dsl/document/draft.ts
|
|
4112
|
+
function textKey(target) {
|
|
4113
|
+
return "captionId" in target ? JSON.stringify(["caption", target.captionId]) : JSON.stringify([
|
|
4114
|
+
"segment",
|
|
4115
|
+
target.audioScriptId,
|
|
4116
|
+
target.segmentId
|
|
4117
|
+
]);
|
|
4118
|
+
}
|
|
4119
|
+
/** Rows holding a segment selection, by the field that holds it (D181). */
|
|
4120
|
+
const selectionField = {
|
|
4121
|
+
"caption-entities": "selection",
|
|
4122
|
+
"phonetic-script-entities": "phoneme"
|
|
4123
|
+
};
|
|
4124
|
+
const isSelectionTable = (table) => Object.hasOwn(selectionField, table);
|
|
4059
4125
|
const entities = new Set(entityTables);
|
|
4060
4126
|
const relations = new Set(relationTables);
|
|
4061
4127
|
const reserved = new Set([
|
|
@@ -4088,9 +4154,10 @@ var TransactionDraft = class {
|
|
|
4088
4154
|
entities = /* @__PURE__ */ new Map();
|
|
4089
4155
|
relations = /* @__PURE__ */ new Map();
|
|
4090
4156
|
deleted = /* @__PURE__ */ new Set();
|
|
4157
|
+
/** Texts this write edits, by {@link textKey}, as its edits so far leave them. */
|
|
4091
4158
|
textPreviews = /* @__PURE__ */ new Map();
|
|
4092
|
-
/**
|
|
4093
|
-
|
|
4159
|
+
/** Selections the draft wrote, by table then row; they are replayed natively in order with text edits. */
|
|
4160
|
+
dirty = /* @__PURE__ */ new Map();
|
|
4094
4161
|
removedCaptionTexts = /* @__PURE__ */ new Set();
|
|
4095
4162
|
treeChanged = false;
|
|
4096
4163
|
active = true;
|
|
@@ -4121,13 +4188,30 @@ var TransactionDraft = class {
|
|
|
4121
4188
|
close() {
|
|
4122
4189
|
this.active = false;
|
|
4123
4190
|
}
|
|
4191
|
+
dirtySelections() {
|
|
4192
|
+
return [...this.dirty].flatMap(([table, ids]) => [...ids].map((id) => [table, id]));
|
|
4193
|
+
}
|
|
4194
|
+
clearDirtySelections() {
|
|
4195
|
+
this.dirty.clear();
|
|
4196
|
+
}
|
|
4197
|
+
/** Whether this write edits any segment of a script. */
|
|
4198
|
+
usesScript(scriptId) {
|
|
4199
|
+
const prefix = JSON.stringify(["segment", scriptId]).slice(0, -1) + ",";
|
|
4200
|
+
return [...this.textPreviews.keys()].some((key) => key.startsWith(prefix));
|
|
4201
|
+
}
|
|
4124
4202
|
wrap(value, path) {
|
|
4125
4203
|
if (!value || typeof value !== "object" || value instanceof Cursor) return value;
|
|
4126
4204
|
const cached = this.wrappers.get(value);
|
|
4127
4205
|
if (cached) return cached;
|
|
4128
4206
|
const proxy = new Proxy(value, {
|
|
4129
4207
|
get: (target, key) => this.run(() => {
|
|
4130
|
-
if (
|
|
4208
|
+
if (key === "text") {
|
|
4209
|
+
const preview = path[0] === "caption-entities" && path.length === 2 ? this.textPreviews.get(textKey({ captionId: path[1] })) : path[0] === "audio-script-entities" && path[2] === "segments" && path.length === 4 ? this.textPreviews.get(textKey({
|
|
4210
|
+
audioScriptId: path[1],
|
|
4211
|
+
segmentId: String(target.id)
|
|
4212
|
+
})) : void 0;
|
|
4213
|
+
if (preview !== void 0) return preview;
|
|
4214
|
+
}
|
|
4131
4215
|
if (!path.length && key === "contain-relations" && target[key] === void 0) target[key] = [];
|
|
4132
4216
|
return this.wrap(Reflect.get(target, key), [...path, String(key)]);
|
|
4133
4217
|
}),
|
|
@@ -4182,16 +4266,22 @@ var TransactionDraft = class {
|
|
|
4182
4266
|
if (path.length === 1 && (remove || next === void 0)) this.deleted.add(lifetime);
|
|
4183
4267
|
else if (this.deleted.has(lifetime)) throw new Error("Recreate an entity in a separate transaction");
|
|
4184
4268
|
if (table === "audio-script-entities") {
|
|
4185
|
-
if (path.length === 2 && key === "
|
|
4186
|
-
if (path.length === 1 &&
|
|
4187
|
-
|
|
4269
|
+
if ((path.length === 2 && key === "segments" || path[2] === "segments" || path.length === 1 && initial && isRecord(next) && !sameSegments(next.segments, initial.segments)) && (initial || this.usesScript(rowId))) throw new Error("Change segments with segment mutations and edit their text through the Text adapter");
|
|
4270
|
+
if (path.length === 1 && this.usesScript(rowId)) throw new Error("Cannot delete or replace a Text used in this transaction");
|
|
4271
|
+
}
|
|
4272
|
+
if (isSelectionTable(table)) {
|
|
4273
|
+
const field = selectionField[table];
|
|
4274
|
+
if (path.length === 1 && isRecord(next) && Object.hasOwn(next, field) || path.length === 2 && key === field || path[2] === field) {
|
|
4275
|
+
let ids = this.dirty.get(table);
|
|
4276
|
+
if (!ids) this.dirty.set(table, ids = /* @__PURE__ */ new Set());
|
|
4277
|
+
ids.add(rowId);
|
|
4278
|
+
}
|
|
4188
4279
|
}
|
|
4189
4280
|
if (table === "caption-entities") {
|
|
4190
|
-
if (path.length === 1 && isRecord(next) && Object.hasOwn(next, "selection") || path.length === 2 && key === "selection" || path[2] === "selection") this.dirtyCaptions.add(rowId);
|
|
4191
4281
|
const changesText = path.length === 1 || path.length === 2 && key === "text";
|
|
4192
4282
|
const nextText = path.length === 1 ? isRecord(next) ? next.text : void 0 : next;
|
|
4193
4283
|
if (changesText) {
|
|
4194
|
-
if (this.textPreviews.has(rowId)) throw new Error("Cannot delete or replace a
|
|
4284
|
+
if (this.textPreviews.has(textKey({ captionId: rowId }))) throw new Error("Cannot delete or replace a Text used in this transaction");
|
|
4195
4285
|
if (nextText !== void 0 && (this.removedCaptionTexts.has(rowId) || initial?.text !== void 0 && nextText !== initial.text)) throw new Error("Edit Caption text through the Text adapter");
|
|
4196
4286
|
if (initial?.text !== void 0 && nextText === void 0) this.removedCaptionTexts.add(rowId);
|
|
4197
4287
|
}
|
|
@@ -4208,7 +4298,7 @@ var TransactionDraft = class {
|
|
|
4208
4298
|
let fields = rows.get(rowId);
|
|
4209
4299
|
if (!fields) rows.set(rowId, fields = /* @__PURE__ */ new Set());
|
|
4210
4300
|
if (path.length === 1) for (const field of [...Object.keys(initial ?? {}), ...Object.keys(isRecord(next) ? next : {})]) {
|
|
4211
|
-
if ((field === "
|
|
4301
|
+
if (initial && isRecord(next) && (field === "segments" && table === "audio-script-entities" && sameSegments(next[field], initial[field]) || field === "text" && table === "caption-entities" && next[field] === initial[field])) continue;
|
|
4212
4302
|
fields.add(field);
|
|
4213
4303
|
}
|
|
4214
4304
|
else fields.add(path[2] ?? key);
|
|
@@ -4219,6 +4309,9 @@ var TransactionDraft = class {
|
|
|
4219
4309
|
}
|
|
4220
4310
|
}
|
|
4221
4311
|
};
|
|
4312
|
+
function sameSegments(a, b) {
|
|
4313
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
4314
|
+
}
|
|
4222
4315
|
//#endregion
|
|
4223
4316
|
//#region src/dsl/document/draft-operations.ts
|
|
4224
4317
|
function nodeIndex(nodes) {
|
|
@@ -4314,8 +4407,17 @@ function applyMutations(tx, mutations) {
|
|
|
4314
4407
|
case "delete-relation":
|
|
4315
4408
|
delete rows[m.table][m.id];
|
|
4316
4409
|
break;
|
|
4410
|
+
case "insert-segment":
|
|
4411
|
+
tx.insertSegment(m.audioScriptId, m.segmentId, m.text, m.index);
|
|
4412
|
+
break;
|
|
4413
|
+
case "move-segment":
|
|
4414
|
+
tx.moveSegment(m.audioScriptId, m.segmentId, m.index);
|
|
4415
|
+
break;
|
|
4416
|
+
case "delete-segment":
|
|
4417
|
+
tx.deleteSegment(m.audioScriptId, m.segmentId);
|
|
4418
|
+
break;
|
|
4317
4419
|
case "edit-text":
|
|
4318
|
-
tx.editText(m.audioScriptId, m.edits, m.expectedBodyId);
|
|
4420
|
+
tx.editText(m.audioScriptId, m.segmentId, m.edits, m.expectedBodyId);
|
|
4319
4421
|
break;
|
|
4320
4422
|
case "edit-caption-text":
|
|
4321
4423
|
tx.editCaptionText(m.captionId, m.edits, m.expectedBodyId);
|
|
@@ -4323,6 +4425,9 @@ function applyMutations(tx, mutations) {
|
|
|
4323
4425
|
case "set-caption-content":
|
|
4324
4426
|
tx.setCaptionContent(m);
|
|
4325
4427
|
break;
|
|
4428
|
+
case "set-phoneme":
|
|
4429
|
+
tx.setPhoneme(m);
|
|
4430
|
+
break;
|
|
4326
4431
|
default: changeTree(tx.draft["contain-relations"], m, tx.draft);
|
|
4327
4432
|
}
|
|
4328
4433
|
}
|
|
@@ -4375,12 +4480,15 @@ function applyTextEdits(text, edits) {
|
|
|
4375
4480
|
}
|
|
4376
4481
|
//#endregion
|
|
4377
4482
|
//#region src/dsl/document/validation.ts
|
|
4378
|
-
/**
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4483
|
+
/**
|
|
4484
|
+
* A selection the draft wrote whole, by the DSL's rules: its segment must exist and what it
|
|
4485
|
+
* selects must not be empty. A run's Cursors must lie on that segment's live Text.
|
|
4486
|
+
*/
|
|
4487
|
+
function validateSelection(doc, state, table, id, value) {
|
|
4488
|
+
assertReferenceAdmitted(state, table, id, selectionField[table], value);
|
|
4489
|
+
const selection = value;
|
|
4490
|
+
if (!(selection.start === void 0 ? requireSegment(state, selection.audioScriptId, selection.segmentId).text : resolveContent(doc, selection)?.text)) throw new Error("Expected a nonempty selection of the current segment");
|
|
4491
|
+
(table === "caption-entities" ? captionContentCodec : phonemeCodec).encode(selection);
|
|
4384
4492
|
}
|
|
4385
4493
|
/**
|
|
4386
4494
|
* The DSL's admission of the whole draft, then what only this document can check: containment
|
|
@@ -4412,8 +4520,12 @@ function validateDraft(tracked) {
|
|
|
4412
4520
|
}
|
|
4413
4521
|
//#endregion
|
|
4414
4522
|
//#region src/dsl/document/transact.ts
|
|
4415
|
-
|
|
4416
|
-
|
|
4523
|
+
const codecs = {
|
|
4524
|
+
"caption-entities": captionContentCodec,
|
|
4525
|
+
"phonetic-script-entities": phonemeCodec
|
|
4526
|
+
};
|
|
4527
|
+
function entityRow(doc, table, id) {
|
|
4528
|
+
const row = doc.getMap(table).get(id);
|
|
4417
4529
|
return isContainer(row) && row.kind() === "Map" ? row : void 0;
|
|
4418
4530
|
}
|
|
4419
4531
|
/** Serialization happens before preparing any writes, so invalid/cyclic audit data cannot partly commit. */
|
|
@@ -4434,8 +4546,8 @@ const nativeHookSupport = /* @__PURE__ */ new WeakMap();
|
|
|
4434
4546
|
/**
|
|
4435
4547
|
* Every write relies on the patched Mirror's `applyNative` (patches/loro-mirror@2.3.3.patch). An
|
|
4436
4548
|
* unpatched Mirror ignores the option and commits only the draft, silently dropping Text, ranges
|
|
4437
|
-
* and
|
|
4438
|
-
*
|
|
4549
|
+
* and selections. The patch runs the hook even for an empty draft without committing, so probe
|
|
4550
|
+
* each Mirror class once on a scratch document and refuse before the first real write.
|
|
4439
4551
|
*/
|
|
4440
4552
|
function assertNativeHook(mirror) {
|
|
4441
4553
|
const Impl = mirror.constructor;
|
|
@@ -4469,57 +4581,75 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4469
4581
|
mirror.setState((raw) => {
|
|
4470
4582
|
const tracked = new TransactionDraft(raw, before);
|
|
4471
4583
|
setReject((error) => tracked.fail(error));
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
if (
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4584
|
+
/** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
|
|
4585
|
+
const useText = (target) => {
|
|
4586
|
+
let text;
|
|
4587
|
+
if ("captionId" in target) {
|
|
4588
|
+
assertId(target.captionId);
|
|
4589
|
+
const entity = requireEntity(tracked.draft, "caption-entities", target.captionId);
|
|
4590
|
+
if (tracked.deleted.has(target.captionId)) throw new Error("Cannot use a deleted Text");
|
|
4591
|
+
if (tracked.removedCaptionTexts.has(target.captionId)) throw new Error("Cannot edit a removed Text");
|
|
4592
|
+
text = entity.text;
|
|
4593
|
+
} else {
|
|
4594
|
+
assertId(target.audioScriptId);
|
|
4595
|
+
if (tracked.deleted.has(target.audioScriptId)) throw new Error("Cannot use a deleted Text");
|
|
4596
|
+
text = requireSegment(tracked.draft, target.audioScriptId, target.segmentId).text;
|
|
4597
|
+
}
|
|
4598
|
+
if (typeof text !== "string") throw new Error("Missing Text");
|
|
4599
|
+
tracked.textPreviews.set(textKey(target), text);
|
|
4600
|
+
return text;
|
|
4601
|
+
};
|
|
4602
|
+
const textBefore = (target) => {
|
|
4603
|
+
if ("captionId" in target) return before["caption-entities"][target.captionId]?.text;
|
|
4604
|
+
return before["audio-script-entities"][target.audioScriptId]?.segments?.find((segment) => segment.id === target.segmentId)?.text;
|
|
4481
4605
|
};
|
|
4482
|
-
const
|
|
4483
|
-
for (const id of tracked.
|
|
4484
|
-
const row = raw[
|
|
4606
|
+
const flushSelections = () => {
|
|
4607
|
+
for (const [table, id] of tracked.dirtySelections()) {
|
|
4608
|
+
const row = raw[table][id];
|
|
4485
4609
|
if (!row) continue;
|
|
4486
|
-
const
|
|
4487
|
-
const
|
|
4610
|
+
const field = selectionField[table];
|
|
4611
|
+
const value = row[field];
|
|
4612
|
+
const old = before[table][id]?.[field];
|
|
4488
4613
|
if (isInvalidAtomic(value)) {
|
|
4489
4614
|
if (isInvalidAtomic(old) && old.raw === value.raw) continue;
|
|
4490
|
-
throw new Error(
|
|
4491
|
-
}
|
|
4492
|
-
if (value !== void 0) {
|
|
4493
|
-
validateSelection(doc, raw, id, value);
|
|
4494
|
-
useBody(value.audioScriptId);
|
|
4615
|
+
throw new Error(`Invalid ${field}`);
|
|
4495
4616
|
}
|
|
4617
|
+
if (value !== void 0) validateSelection(doc, raw, table, id, value);
|
|
4496
4618
|
steps.push({
|
|
4497
|
-
type: "
|
|
4619
|
+
type: "selection",
|
|
4620
|
+
table,
|
|
4498
4621
|
id,
|
|
4499
4622
|
value: copyInput(value)
|
|
4500
4623
|
});
|
|
4501
4624
|
}
|
|
4502
|
-
tracked.
|
|
4625
|
+
tracked.clearDirtySelections();
|
|
4503
4626
|
};
|
|
4504
|
-
const editBody = (
|
|
4627
|
+
const editBody = (target, edits, expectedBodyId) => {
|
|
4505
4628
|
tracked.run(() => {
|
|
4506
|
-
|
|
4507
|
-
const
|
|
4508
|
-
|
|
4509
|
-
|
|
4629
|
+
flushSelections();
|
|
4630
|
+
const text = useText(target);
|
|
4631
|
+
const key = textKey(target);
|
|
4632
|
+
if (!bodies.has(key)) bodies.set(key, liveText(doc, target));
|
|
4633
|
+
const native = bodies.get(key);
|
|
4510
4634
|
if (expectedBodyId !== void 0 && native?.id !== expectedBodyId) throw new Error("Stale or missing Text body");
|
|
4511
|
-
if ((
|
|
4512
|
-
const prepared = prepareTextEdits(
|
|
4513
|
-
tracked.textPreviews.set(
|
|
4635
|
+
if (textBefore(target) !== void 0 && !native) throw new Error("Missing live Text body");
|
|
4636
|
+
const prepared = prepareTextEdits(text, copyInput(edits));
|
|
4637
|
+
tracked.textPreviews.set(key, prepared.text);
|
|
4514
4638
|
steps.push({
|
|
4515
4639
|
type: "text",
|
|
4516
|
-
|
|
4517
|
-
caption,
|
|
4640
|
+
target,
|
|
4518
4641
|
expectedBodyId: native?.id,
|
|
4519
4642
|
edits: prepared.edits
|
|
4520
4643
|
});
|
|
4521
4644
|
});
|
|
4522
4645
|
};
|
|
4646
|
+
/** A script's segments as this transaction leaves them; never a Mirror-tracked write. */
|
|
4647
|
+
const segmentsOf = (audioScriptId) => {
|
|
4648
|
+
assertId(audioScriptId);
|
|
4649
|
+
requireEntity(tracked.draft, "audio-script-entities", audioScriptId);
|
|
4650
|
+
if (tracked.deleted.has(audioScriptId)) throw new Error("Cannot change a deleted AudioScript");
|
|
4651
|
+
return raw["audio-script-entities"][audioScriptId].segments;
|
|
4652
|
+
};
|
|
4523
4653
|
const tx = Object.freeze({
|
|
4524
4654
|
get draft() {
|
|
4525
4655
|
return tracked.run(() => tracked.draft);
|
|
@@ -4527,29 +4657,95 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4527
4657
|
applyMutations(mutations) {
|
|
4528
4658
|
tracked.run(() => applyMutations(tx, mutations));
|
|
4529
4659
|
},
|
|
4530
|
-
editText(
|
|
4531
|
-
editBody(
|
|
4660
|
+
editText(audioScriptId, segmentId, edits, expectedBodyId) {
|
|
4661
|
+
editBody({
|
|
4662
|
+
audioScriptId,
|
|
4663
|
+
segmentId
|
|
4664
|
+
}, edits, expectedBodyId);
|
|
4532
4665
|
},
|
|
4533
4666
|
editCaptionText(captionId, edits, expectedBodyId) {
|
|
4534
|
-
editBody(captionId, edits, expectedBodyId
|
|
4667
|
+
editBody({ captionId }, edits, expectedBodyId);
|
|
4668
|
+
},
|
|
4669
|
+
insertSegment(audioScriptId, segmentId, text, index) {
|
|
4670
|
+
tracked.run(() => {
|
|
4671
|
+
assertId(segmentId);
|
|
4672
|
+
if (typeof text !== "string") throw new Error("Segment text is a string");
|
|
4673
|
+
const segments = segmentsOf(audioScriptId);
|
|
4674
|
+
if (segments.some((segment) => segment.id === segmentId)) throw new Error(`Segment already exists: ${segmentId}`);
|
|
4675
|
+
const at = index ?? segments.length;
|
|
4676
|
+
if (!Number.isSafeInteger(at) || at < 0 || at > segments.length) throw new Error("Invalid segment index");
|
|
4677
|
+
segments.splice(at, 0, {
|
|
4678
|
+
id: segmentId,
|
|
4679
|
+
text
|
|
4680
|
+
});
|
|
4681
|
+
});
|
|
4682
|
+
},
|
|
4683
|
+
moveSegment(audioScriptId, segmentId, index) {
|
|
4684
|
+
tracked.run(() => {
|
|
4685
|
+
const segments = segmentsOf(audioScriptId);
|
|
4686
|
+
const from = segments.findIndex((segment) => segment.id === segmentId);
|
|
4687
|
+
if (from < 0) throw new Error(`Missing segment ${segmentId} of ${audioScriptId}`);
|
|
4688
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= segments.length) throw new Error("Invalid segment index");
|
|
4689
|
+
const [segment] = segments.splice(from, 1);
|
|
4690
|
+
segments.splice(index, 0, segment);
|
|
4691
|
+
});
|
|
4692
|
+
},
|
|
4693
|
+
deleteSegment(audioScriptId, segmentId) {
|
|
4694
|
+
tracked.run(() => {
|
|
4695
|
+
const segments = segmentsOf(audioScriptId);
|
|
4696
|
+
const at = segments.findIndex((segment) => segment.id === segmentId);
|
|
4697
|
+
if (at < 0) throw new Error(`Missing segment ${segmentId} of ${audioScriptId}`);
|
|
4698
|
+
if (tracked.textPreviews.has(textKey({
|
|
4699
|
+
audioScriptId,
|
|
4700
|
+
segmentId
|
|
4701
|
+
}))) throw new Error("Cannot delete a segment whose text this transaction edits");
|
|
4702
|
+
segments.splice(at, 1);
|
|
4703
|
+
});
|
|
4535
4704
|
},
|
|
4536
4705
|
setCaptionContent(selection) {
|
|
4537
4706
|
tracked.run(() => {
|
|
4538
|
-
|
|
4539
|
-
const { captionId, audioScriptId, start, end } = selection;
|
|
4707
|
+
flushSelections();
|
|
4708
|
+
const { captionId, audioScriptId, segmentId, start, end } = selection;
|
|
4540
4709
|
assertId(captionId);
|
|
4541
|
-
|
|
4542
|
-
|
|
4710
|
+
assertCaptionSelection(raw, selection, useText({
|
|
4711
|
+
audioScriptId,
|
|
4712
|
+
segmentId
|
|
4713
|
+
}));
|
|
4543
4714
|
steps.push({
|
|
4544
4715
|
type: "range",
|
|
4545
|
-
|
|
4546
|
-
|
|
4716
|
+
table: "caption-entities",
|
|
4717
|
+
id: captionId,
|
|
4718
|
+
range: {
|
|
4547
4719
|
audioScriptId,
|
|
4720
|
+
segmentId,
|
|
4548
4721
|
start,
|
|
4549
4722
|
end
|
|
4550
4723
|
}
|
|
4551
4724
|
});
|
|
4552
4725
|
});
|
|
4726
|
+
},
|
|
4727
|
+
setPhoneme(selection) {
|
|
4728
|
+
tracked.run(() => {
|
|
4729
|
+
flushSelections();
|
|
4730
|
+
const { phoneticScriptId, audioScriptId, segmentId, start, end, phonemeScript } = selection;
|
|
4731
|
+
assertId(phoneticScriptId);
|
|
4732
|
+
assertPhonemeSelection(raw, selection, useText({
|
|
4733
|
+
audioScriptId,
|
|
4734
|
+
segmentId
|
|
4735
|
+
}));
|
|
4736
|
+
steps.push({
|
|
4737
|
+
type: "range",
|
|
4738
|
+
table: "phonetic-script-entities",
|
|
4739
|
+
id: phoneticScriptId,
|
|
4740
|
+
range: {
|
|
4741
|
+
audioScriptId,
|
|
4742
|
+
segmentId,
|
|
4743
|
+
start,
|
|
4744
|
+
end
|
|
4745
|
+
},
|
|
4746
|
+
...phonemeScript === void 0 ? {} : { phonemeScript }
|
|
4747
|
+
});
|
|
4748
|
+
});
|
|
4553
4749
|
}
|
|
4554
4750
|
});
|
|
4555
4751
|
try {
|
|
@@ -4562,34 +4758,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4562
4758
|
}
|
|
4563
4759
|
tracked.assertValid();
|
|
4564
4760
|
if (doc.getPendingTxnLength()) throw new Error("Native writes are not allowed during transaction preparation");
|
|
4565
|
-
|
|
4761
|
+
flushSelections();
|
|
4566
4762
|
for (const id of tracked.entities.get("caption-entities")?.keys() ?? []) {
|
|
4567
4763
|
const caption = tracked.draft["caption-entities"][id];
|
|
4568
4764
|
if (caption) caption.style ??= {};
|
|
4569
4765
|
}
|
|
4570
4766
|
validateDraft(tracked);
|
|
4571
|
-
for (const step of steps) if (step.type === "text")
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
4575
|
-
for (const step of steps) {
|
|
4576
|
-
if (step.type === "range") {
|
|
4577
|
-
referenceIds.add(step.selection.captionId);
|
|
4578
|
-
changedCaptions.add(step.selection.captionId);
|
|
4579
|
-
}
|
|
4580
|
-
if (step.type === "caption") {
|
|
4581
|
-
changedCaptions.add(step.id);
|
|
4582
|
-
if (step.value) referenceIds.add(step.id);
|
|
4583
|
-
else referenceIds.delete(step.id);
|
|
4584
|
-
}
|
|
4585
|
-
if (step.type === "text" && step.caption) changedCaptions.add(step.scriptId);
|
|
4586
|
-
}
|
|
4587
|
-
for (const id of changedCaptions) if (raw["caption-entities"][id]) assertCaptionSource(raw["caption-entities"][id], referenceIds.has(id));
|
|
4588
|
-
for (const step of steps) if (step.type === "range") assertCaptionEndpoints(raw, step.selection);
|
|
4589
|
-
else if (step.type === "caption" && step.value) assertCaptionEndpoints(raw, {
|
|
4590
|
-
captionId: step.id,
|
|
4591
|
-
audioScriptId: step.value.audioScriptId
|
|
4592
|
-
});
|
|
4767
|
+
for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
|
|
4768
|
+
else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
|
|
4769
|
+
assertSelectionsAfter(raw, before, tracked, steps);
|
|
4593
4770
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
4594
4771
|
const row = raw["caption-entities"][id];
|
|
4595
4772
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -4597,12 +4774,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4597
4774
|
delete row.text;
|
|
4598
4775
|
}
|
|
4599
4776
|
}
|
|
4600
|
-
for (const
|
|
4601
|
-
const
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4777
|
+
for (const table of Object.keys(selectionField)) {
|
|
4778
|
+
const field = selectionField[table];
|
|
4779
|
+
for (const [id, fields] of tracked.entities.get(table) ?? []) {
|
|
4780
|
+
const row = raw[table][id];
|
|
4781
|
+
if (!row || !fields.has(field)) continue;
|
|
4782
|
+
const old = before[table][id]?.[field];
|
|
4783
|
+
if (old === void 0) delete row[field];
|
|
4784
|
+
else row[field] = old;
|
|
4785
|
+
}
|
|
4606
4786
|
}
|
|
4607
4787
|
});
|
|
4608
4788
|
} finally {
|
|
@@ -4612,45 +4792,104 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4612
4792
|
}, {
|
|
4613
4793
|
...options,
|
|
4614
4794
|
applyNative: (doc) => {
|
|
4615
|
-
const
|
|
4616
|
-
|
|
4795
|
+
const textFor = (target) => {
|
|
4796
|
+
const key = textKey(target);
|
|
4797
|
+
let text = bodies.get(key);
|
|
4617
4798
|
if (!text) {
|
|
4618
|
-
text = liveText(doc,
|
|
4619
|
-
bodies.set(
|
|
4799
|
+
text = liveText(doc, target);
|
|
4800
|
+
bodies.set(key, text);
|
|
4620
4801
|
}
|
|
4621
|
-
if (!text || text.isDeleted()) throw new Error("Prepared Text
|
|
4802
|
+
if (!text || text.isDeleted()) throw new Error("Prepared Text is missing");
|
|
4622
4803
|
return text;
|
|
4623
4804
|
};
|
|
4624
4805
|
for (const [id, value] of newCaptionTexts) {
|
|
4625
|
-
const
|
|
4626
|
-
if (value.length)
|
|
4627
|
-
bodies.set(id,
|
|
4806
|
+
const text = doc.getMap("caption-entities").get(id).setContainer("text", new LoroText());
|
|
4807
|
+
if (value.length) text.insert(0, value);
|
|
4808
|
+
bodies.set(textKey({ captionId: id }), text);
|
|
4628
4809
|
}
|
|
4629
|
-
for (const step of steps)
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
const
|
|
4637
|
-
const
|
|
4638
|
-
if (
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4810
|
+
for (const step of steps) {
|
|
4811
|
+
if (step.type === "text") {
|
|
4812
|
+
const text = textFor(step.target);
|
|
4813
|
+
if (step.expectedBodyId && text.id !== step.expectedBodyId) throw new Error("Stale Text body");
|
|
4814
|
+
applyTextEdits(text, step.edits);
|
|
4815
|
+
continue;
|
|
4816
|
+
}
|
|
4817
|
+
const row = entityRow(doc, step.table, step.id);
|
|
4818
|
+
const field = selectionField[step.table];
|
|
4819
|
+
if (step.type === "range") {
|
|
4820
|
+
const { audioScriptId, segmentId, start, end } = step.range;
|
|
4821
|
+
const value = {
|
|
4822
|
+
audioScriptId,
|
|
4823
|
+
segmentId
|
|
4824
|
+
};
|
|
4825
|
+
if (start !== void 0 && end !== void 0) {
|
|
4826
|
+
const text = textFor({
|
|
4827
|
+
audioScriptId,
|
|
4828
|
+
segmentId
|
|
4829
|
+
});
|
|
4830
|
+
value.start = text.getCursor(start, 0);
|
|
4831
|
+
value.end = text.getCursor(end, 0);
|
|
4832
|
+
if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
|
|
4833
|
+
}
|
|
4834
|
+
if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
|
|
4835
|
+
if (!row) throw new Error("Unable to construct prepared range");
|
|
4836
|
+
row.set(field, codecs[step.table].encode(value));
|
|
4837
|
+
} else if (step.value !== void 0) {
|
|
4838
|
+
if (!row) throw new Error("Prepared selection owner is missing");
|
|
4839
|
+
row.set(field, codecs[step.table].encode(step.value));
|
|
4840
|
+
} else if (row?.get(field) !== void 0) row.delete(field);
|
|
4650
4841
|
}
|
|
4651
4842
|
}
|
|
4652
4843
|
});
|
|
4653
4844
|
}
|
|
4845
|
+
/**
|
|
4846
|
+
* What only the whole write can say about selections, with the native steps counted in order:
|
|
4847
|
+
* each Caption keeps one source of words, each selection's segment still exists, and every
|
|
4848
|
+
* PhoneticScript the write touched is written for a segment and stores its text as a file.
|
|
4849
|
+
*/
|
|
4850
|
+
function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
4851
|
+
const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
|
|
4852
|
+
const changedCaptions = /* @__PURE__ */ new Set();
|
|
4853
|
+
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
4854
|
+
const phonemes = /* @__PURE__ */ new Map();
|
|
4855
|
+
for (const step of steps) {
|
|
4856
|
+
if (step.type === "text") {
|
|
4857
|
+
if ("captionId" in step.target) changedCaptions.add(step.target.captionId);
|
|
4858
|
+
continue;
|
|
4859
|
+
}
|
|
4860
|
+
const value = step.type === "range" ? {
|
|
4861
|
+
...step.range,
|
|
4862
|
+
...step.phonemeScript === void 0 ? {} : { phonemeScript: step.phonemeScript }
|
|
4863
|
+
} : step.value;
|
|
4864
|
+
if (step.table === "caption-entities") {
|
|
4865
|
+
changedCaptions.add(step.id);
|
|
4866
|
+
if (value) selected.add(step.id);
|
|
4867
|
+
else selected.delete(step.id);
|
|
4868
|
+
if (value) assertCaptionEndpoints(raw, {
|
|
4869
|
+
captionId: step.id,
|
|
4870
|
+
...value
|
|
4871
|
+
});
|
|
4872
|
+
} else {
|
|
4873
|
+
phonemes.set(step.id, value);
|
|
4874
|
+
if (value) assertPhonemeEndpoints(raw, {
|
|
4875
|
+
phoneticScriptId: step.id,
|
|
4876
|
+
...value
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
for (const id of changedCaptions) if (raw["caption-entities"][id]) assertCaptionSource(raw["caption-entities"][id], selected.has(id));
|
|
4881
|
+
const phonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...phonemes.keys()]);
|
|
4882
|
+
for (const id of phonetics) {
|
|
4883
|
+
const row = raw["phonetic-script-entities"][id];
|
|
4884
|
+
if (!row) continue;
|
|
4885
|
+
const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
|
|
4886
|
+
const old = before["phonetic-script-entities"][id];
|
|
4887
|
+
assertPhoneticScript(old, {
|
|
4888
|
+
...row,
|
|
4889
|
+
phoneme: isRecord(phoneme) ? phoneme : void 0
|
|
4890
|
+
});
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4654
4893
|
//#endregion
|
|
4655
4894
|
//#region src/dsl/document/medeo-dsl.ts
|
|
4656
4895
|
const contentRoots = new Set([
|
|
@@ -4741,17 +4980,23 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4741
4980
|
applyMutations(mutations, audit) {
|
|
4742
4981
|
this.transact((tx) => tx.applyMutations(mutations), audit);
|
|
4743
4982
|
}
|
|
4744
|
-
editText(scriptId, expectedBodyId, edits, audit) {
|
|
4745
|
-
this.transact((tx) => tx.editText(scriptId, edits, expectedBodyId), audit);
|
|
4983
|
+
editText(scriptId, segmentId, expectedBodyId, edits, audit) {
|
|
4984
|
+
this.transact((tx) => tx.editText(scriptId, segmentId, edits, expectedBodyId), audit);
|
|
4746
4985
|
}
|
|
4747
4986
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
4748
4987
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
4749
4988
|
}
|
|
4750
|
-
/**
|
|
4751
|
-
|
|
4989
|
+
/**
|
|
4990
|
+
* A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
|
|
4991
|
+
* include insertions at their end. K01 remains deferred.
|
|
4992
|
+
*/
|
|
4993
|
+
textRange(scriptId, segmentId, start, end) {
|
|
4752
4994
|
this.assertActive();
|
|
4753
|
-
const text = liveText(this.doc,
|
|
4754
|
-
|
|
4995
|
+
const text = liveText(this.doc, {
|
|
4996
|
+
audioScriptId: scriptId,
|
|
4997
|
+
segmentId
|
|
4998
|
+
});
|
|
4999
|
+
if (!text) throw new Error("Missing AudioScript segment");
|
|
4755
5000
|
assertTextOffset(text.toString(), start);
|
|
4756
5001
|
assertTextOffset(text.toString(), end);
|
|
4757
5002
|
if (start >= end) throw new Error("Expected a nonempty range");
|
|
@@ -4759,6 +5004,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4759
5004
|
if (!from || !to) throw new Error("Unable to construct text range");
|
|
4760
5005
|
return {
|
|
4761
5006
|
audioScriptId: scriptId,
|
|
5007
|
+
segmentId,
|
|
4762
5008
|
start: from,
|
|
4763
5009
|
end: to
|
|
4764
5010
|
};
|
|
@@ -4820,16 +5066,28 @@ function voiceoverPlan(s, input, result) {
|
|
|
4820
5066
|
id: result.mediaId,
|
|
4821
5067
|
value: input.media
|
|
4822
5068
|
}];
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
5069
|
+
let segmentIds;
|
|
5070
|
+
if (input.script.segments !== void 0) {
|
|
5071
|
+
if (result.segmentIds.length !== input.script.segments.length) throw new Error("Expected one id per segment");
|
|
5072
|
+
const segments = input.script.segments;
|
|
5073
|
+
segmentIds = result.segmentIds;
|
|
5074
|
+
mutations.push({
|
|
5075
|
+
type: "create-entity",
|
|
5076
|
+
table: "audio-script-entities",
|
|
5077
|
+
id: result.audioScriptId,
|
|
5078
|
+
value: {
|
|
5079
|
+
segments: segments.map((text, i) => ({
|
|
5080
|
+
id: segmentIds[i],
|
|
5081
|
+
text
|
|
5082
|
+
})),
|
|
5083
|
+
..."defaultLanguage" in input.script && input.script.defaultLanguage !== void 0 ? { defaultLanguage: input.script.defaultLanguage } : {}
|
|
5084
|
+
}
|
|
5085
|
+
});
|
|
5086
|
+
} else {
|
|
5087
|
+
const script = s["audio-script-entities"][result.audioScriptId];
|
|
5088
|
+
if (!script) throw new Error("Missing AudioScript");
|
|
5089
|
+
segmentIds = script.segments.map((segment) => segment.id);
|
|
5090
|
+
}
|
|
4833
5091
|
const phoneticScriptId = input.script.phoneticScriptId;
|
|
4834
5092
|
if (phoneticScriptId !== void 0 && s["phonetic-script-entities"][phoneticScriptId]?.phoneme.audioScriptId !== result.audioScriptId) throw new Error("The PhoneticScript must name this AudioScript as its base");
|
|
4835
5093
|
validateWindow({
|
|
@@ -4912,6 +5170,8 @@ function voiceoverPlan(s, input, result) {
|
|
|
4912
5170
|
}, input.media.durationMs);
|
|
4913
5171
|
const previous = s["clip-entities"][clipId];
|
|
4914
5172
|
if (previous && !oldCaptions.includes(clipId)) throw new Error("Cannot overwrite another use’s Caption Clip");
|
|
5173
|
+
const segmentId = segmentIds[caption.segment];
|
|
5174
|
+
if (segmentId === void 0) throw new Error(`Missing segment ${caption.segment} of the voiceover's script`);
|
|
4915
5175
|
const previousSource = previous?.source;
|
|
4916
5176
|
const style = caption.style ?? (previousSource ? s["caption-entities"][previousSource]?.style : void 0);
|
|
4917
5177
|
mutations.push({
|
|
@@ -4923,8 +5183,11 @@ function voiceoverPlan(s, input, result) {
|
|
|
4923
5183
|
type: "set-caption-content",
|
|
4924
5184
|
captionId,
|
|
4925
5185
|
audioScriptId: result.audioScriptId,
|
|
4926
|
-
|
|
4927
|
-
|
|
5186
|
+
segmentId,
|
|
5187
|
+
...caption.range === void 0 ? {} : {
|
|
5188
|
+
start: caption.range.start,
|
|
5189
|
+
end: caption.range.end
|
|
5190
|
+
}
|
|
4928
5191
|
});
|
|
4929
5192
|
const anchor = {
|
|
4930
5193
|
to: result.clipId,
|
|
@@ -5075,30 +5338,76 @@ var DslEditor = class {
|
|
|
5075
5338
|
patch: { volume }
|
|
5076
5339
|
}], options);
|
|
5077
5340
|
}
|
|
5078
|
-
|
|
5341
|
+
/** A script of these segments, in order; each segment gets its own id. */
|
|
5342
|
+
createAudioScript(segments, defaultLanguage, options) {
|
|
5079
5343
|
const id = this.idFactory("audio-script");
|
|
5344
|
+
const value = {
|
|
5345
|
+
segments: segments.map((text) => ({
|
|
5346
|
+
id: this.idFactory("segment"),
|
|
5347
|
+
text
|
|
5348
|
+
})),
|
|
5349
|
+
...defaultLanguage === void 0 ? {} : { defaultLanguage }
|
|
5350
|
+
};
|
|
5080
5351
|
this.write("createAudioScript", {
|
|
5081
5352
|
id,
|
|
5082
|
-
|
|
5083
|
-
defaultLanguage
|
|
5353
|
+
...value
|
|
5084
5354
|
}, [{
|
|
5085
5355
|
type: "create-entity",
|
|
5086
5356
|
table: "audio-script-entities",
|
|
5087
5357
|
id,
|
|
5088
|
-
value
|
|
5089
|
-
body,
|
|
5090
|
-
...defaultLanguage === void 0 ? {} : { defaultLanguage }
|
|
5091
|
-
}
|
|
5358
|
+
value
|
|
5092
5359
|
}], options);
|
|
5093
5360
|
return id;
|
|
5094
5361
|
}
|
|
5095
|
-
editAudioScript(scriptId, expectedBodyId, edits, options) {
|
|
5096
|
-
this.dsl.editText(scriptId, expectedBodyId, edits, audit("editAudioScript", {
|
|
5362
|
+
editAudioScript(scriptId, segmentId, expectedBodyId, edits, options) {
|
|
5363
|
+
this.dsl.editText(scriptId, segmentId, expectedBodyId, edits, audit("editAudioScript", {
|
|
5097
5364
|
scriptId,
|
|
5365
|
+
segmentId,
|
|
5098
5366
|
expectedBodyId,
|
|
5099
5367
|
edits
|
|
5100
5368
|
}, options));
|
|
5101
5369
|
}
|
|
5370
|
+
/** Adds a segment at `index`, the end by default, and returns its id. */
|
|
5371
|
+
insertScriptSegment(scriptId, text, index, options) {
|
|
5372
|
+
const segmentId = this.idFactory("segment");
|
|
5373
|
+
this.write("insertScriptSegment", {
|
|
5374
|
+
scriptId,
|
|
5375
|
+
segmentId,
|
|
5376
|
+
text,
|
|
5377
|
+
index
|
|
5378
|
+
}, [{
|
|
5379
|
+
type: "insert-segment",
|
|
5380
|
+
audioScriptId: scriptId,
|
|
5381
|
+
segmentId,
|
|
5382
|
+
text,
|
|
5383
|
+
...index === void 0 ? {} : { index }
|
|
5384
|
+
}], options);
|
|
5385
|
+
return segmentId;
|
|
5386
|
+
}
|
|
5387
|
+
/** `index` is measured after removing the segment. */
|
|
5388
|
+
moveScriptSegment(scriptId, segmentId, index, options) {
|
|
5389
|
+
this.write("moveScriptSegment", {
|
|
5390
|
+
scriptId,
|
|
5391
|
+
segmentId,
|
|
5392
|
+
index
|
|
5393
|
+
}, [{
|
|
5394
|
+
type: "move-segment",
|
|
5395
|
+
audioScriptId: scriptId,
|
|
5396
|
+
segmentId,
|
|
5397
|
+
index
|
|
5398
|
+
}], options);
|
|
5399
|
+
}
|
|
5400
|
+
/** Captions and PhoneticScripts selecting the segment keep their selection, which then shows nothing (D72). */
|
|
5401
|
+
deleteScriptSegment(scriptId, segmentId, options) {
|
|
5402
|
+
this.write("deleteScriptSegment", {
|
|
5403
|
+
scriptId,
|
|
5404
|
+
segmentId
|
|
5405
|
+
}, [{
|
|
5406
|
+
type: "delete-segment",
|
|
5407
|
+
audioScriptId: scriptId,
|
|
5408
|
+
segmentId
|
|
5409
|
+
}], options);
|
|
5410
|
+
}
|
|
5102
5411
|
createCaption(input, options) {
|
|
5103
5412
|
const id = this.idFactory("caption");
|
|
5104
5413
|
this.write("createCaption", {
|
|
@@ -5115,18 +5424,16 @@ var DslEditor = class {
|
|
|
5115
5424
|
}, ...input.text !== void 0 ? [] : [{
|
|
5116
5425
|
type: "set-caption-content",
|
|
5117
5426
|
captionId: id,
|
|
5118
|
-
|
|
5119
|
-
start: input.start,
|
|
5120
|
-
end: input.end
|
|
5427
|
+
...segmentRange(input)
|
|
5121
5428
|
}]], options);
|
|
5122
5429
|
return id;
|
|
5123
5430
|
}
|
|
5124
|
-
|
|
5431
|
+
/** Show a segment of a script, or a run of it, in place of the Caption's own text. */
|
|
5432
|
+
setCaptionContent(captionId, selection, options) {
|
|
5433
|
+
const range = segmentRange(selection);
|
|
5125
5434
|
this.write("setCaptionContent", {
|
|
5126
5435
|
captionId,
|
|
5127
|
-
|
|
5128
|
-
start,
|
|
5129
|
-
end
|
|
5436
|
+
...range
|
|
5130
5437
|
}, [{
|
|
5131
5438
|
type: "patch-entity",
|
|
5132
5439
|
table: "caption-entities",
|
|
@@ -5135,9 +5442,7 @@ var DslEditor = class {
|
|
|
5135
5442
|
}, {
|
|
5136
5443
|
type: "set-caption-content",
|
|
5137
5444
|
captionId,
|
|
5138
|
-
|
|
5139
|
-
start,
|
|
5140
|
-
end
|
|
5445
|
+
...range
|
|
5141
5446
|
}], options);
|
|
5142
5447
|
}
|
|
5143
5448
|
setCaptionText(captionId, text, options) {
|
|
@@ -5263,6 +5568,7 @@ var DslEditor = class {
|
|
|
5263
5568
|
clipId: input.clipId ?? this.idFactory("clip"),
|
|
5264
5569
|
mediaId: resourceId("media", input.media.system, input.media.key),
|
|
5265
5570
|
audioScriptId: this.voiceoverScriptId(input.script),
|
|
5571
|
+
segmentIds: input.script.segments?.map(() => this.idFactory("segment")) ?? [],
|
|
5266
5572
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
5267
5573
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
5268
5574
|
};
|
|
@@ -5280,17 +5586,14 @@ var DslEditor = class {
|
|
|
5280
5586
|
return phonetic.phoneme.audioScriptId;
|
|
5281
5587
|
}
|
|
5282
5588
|
/**
|
|
5283
|
-
* Write a PhoneticScript: create one for
|
|
5284
|
-
*
|
|
5285
|
-
*
|
|
5286
|
-
* from it keep their derived-from.
|
|
5589
|
+
* Write a PhoneticScript: create one for a segment of a script, or a run of it, or, given
|
|
5590
|
+
* `phoneticScriptId`, replace its selection and what to synthesize together (D181). A
|
|
5591
|
+
* `phonemeScript` is stored as a Memota text file first, and the file's facts are written with
|
|
5592
|
+
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
5287
5593
|
*/
|
|
5288
5594
|
async setPhoneticScript(input, options) {
|
|
5289
5595
|
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
5290
|
-
const
|
|
5291
|
-
audioScriptId: input.audioScriptId,
|
|
5292
|
-
...input.phonemeScript === void 0 ? {} : { phonemeScript: input.phonemeScript }
|
|
5293
|
-
};
|
|
5596
|
+
const range = segmentRange(input);
|
|
5294
5597
|
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
5295
5598
|
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
5296
5599
|
const cleared = {
|
|
@@ -5299,9 +5602,11 @@ var DslEditor = class {
|
|
|
5299
5602
|
storageKey: void 0,
|
|
5300
5603
|
digest: void 0
|
|
5301
5604
|
};
|
|
5605
|
+
const phonemeScript = input.phonemeScript === void 0 ? {} : { phonemeScript: input.phonemeScript };
|
|
5302
5606
|
this.write("setPhoneticScript", {
|
|
5303
5607
|
id,
|
|
5304
|
-
|
|
5608
|
+
...range,
|
|
5609
|
+
...phonemeScript,
|
|
5305
5610
|
...cleared,
|
|
5306
5611
|
...file
|
|
5307
5612
|
}, [exists ? {
|
|
@@ -5309,10 +5614,6 @@ var DslEditor = class {
|
|
|
5309
5614
|
table: "phonetic-script-entities",
|
|
5310
5615
|
id,
|
|
5311
5616
|
patch: {
|
|
5312
|
-
phoneme: {
|
|
5313
|
-
...phoneme,
|
|
5314
|
-
phonemeScript: input.phonemeScript
|
|
5315
|
-
},
|
|
5316
5617
|
...cleared,
|
|
5317
5618
|
...file
|
|
5318
5619
|
}
|
|
@@ -5320,10 +5621,12 @@ var DslEditor = class {
|
|
|
5320
5621
|
type: "create-entity",
|
|
5321
5622
|
table: "phonetic-script-entities",
|
|
5322
5623
|
id,
|
|
5323
|
-
value:
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5624
|
+
value: file
|
|
5625
|
+
}, {
|
|
5626
|
+
type: "set-phoneme",
|
|
5627
|
+
phoneticScriptId: id,
|
|
5628
|
+
...range,
|
|
5629
|
+
...phonemeScript
|
|
5327
5630
|
}], options);
|
|
5328
5631
|
return id;
|
|
5329
5632
|
}
|
|
@@ -5392,6 +5695,15 @@ var DslEditor = class {
|
|
|
5392
5695
|
this.dsl.applyMutations(mutations, audit(action, payload, options));
|
|
5393
5696
|
}
|
|
5394
5697
|
};
|
|
5698
|
+
/** A selection as a write states it: a run only when both its ends are given. */
|
|
5699
|
+
function segmentRange(input) {
|
|
5700
|
+
return {
|
|
5701
|
+
audioScriptId: input.audioScriptId,
|
|
5702
|
+
segmentId: input.segmentId,
|
|
5703
|
+
...input.start === void 0 ? {} : { start: input.start },
|
|
5704
|
+
...input.end === void 0 ? {} : { end: input.end }
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5395
5707
|
function audit(action, payload, options) {
|
|
5396
5708
|
return {
|
|
5397
5709
|
semantic_op: action,
|
|
@@ -5467,35 +5779,39 @@ function spokenScripts(snapshot, recordingId) {
|
|
|
5467
5779
|
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
5468
5780
|
function toAudioScriptViews(snapshot) {
|
|
5469
5781
|
const views = /* @__PURE__ */ new Map();
|
|
5470
|
-
const
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
}
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
}
|
|
5782
|
+
const segmentIndex = (scriptId, segmentId) => {
|
|
5783
|
+
const index = snapshot["audio-script-entities"][scriptId]?.segments.findIndex((s) => s.id === segmentId) ?? -1;
|
|
5784
|
+
return index < 0 ? void 0 : index;
|
|
5785
|
+
};
|
|
5786
|
+
for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) views.set(id, {
|
|
5787
|
+
id,
|
|
5788
|
+
segments: script.segments.map((segment) => {
|
|
5789
|
+
const rich = snapshot.richText[segment.id];
|
|
5790
|
+
const language = rich ? markedLanguage(langsFromDelta(rich.delta)) : void 0;
|
|
5791
|
+
return language === void 0 ? {
|
|
5792
|
+
id: segment.id,
|
|
5793
|
+
text: segment.text
|
|
5794
|
+
} : {
|
|
5795
|
+
id: segment.id,
|
|
5796
|
+
text: segment.text,
|
|
5797
|
+
language
|
|
5798
|
+
};
|
|
5799
|
+
}),
|
|
5800
|
+
sourceMediaIds: [],
|
|
5801
|
+
phoneticScripts: [],
|
|
5802
|
+
speeches: [],
|
|
5803
|
+
captions: []
|
|
5804
|
+
});
|
|
5493
5805
|
for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
|
|
5494
5806
|
for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
|
|
5495
|
-
const { phonemeScript } = phonetic.phoneme;
|
|
5807
|
+
const { audioScriptId, segmentId, phonemeScript } = phonetic.phoneme;
|
|
5496
5808
|
const { digest } = phonetic;
|
|
5497
|
-
|
|
5809
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5810
|
+
const text = snapshot.phoneticText[id]?.text;
|
|
5811
|
+
views.get(audioScriptId)?.phoneticScripts.push({
|
|
5498
5812
|
id,
|
|
5813
|
+
...segment === void 0 ? {} : { segment },
|
|
5814
|
+
...text === void 0 ? {} : { text },
|
|
5499
5815
|
...phonemeScript === void 0 ? {} : { phonemeScript },
|
|
5500
5816
|
...digest === void 0 ? {} : { digest }
|
|
5501
5817
|
});
|
|
@@ -5530,13 +5846,10 @@ function toAudioScriptViews(snapshot) {
|
|
|
5530
5846
|
continue;
|
|
5531
5847
|
}
|
|
5532
5848
|
const caption = snapshot["caption-entities"][source];
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
const
|
|
5536
|
-
|
|
5537
|
-
if (body === void 0) continue;
|
|
5538
|
-
const segment = lines(body).findIndex((line) => shown.start >= line.start && shown.end <= line.end);
|
|
5539
|
-
if (segment >= 0) views.get(scriptId)?.captions.push({
|
|
5849
|
+
if (!caption?.selection || caption.text !== void 0 || !snapshot.captionText[source]) continue;
|
|
5850
|
+
const { audioScriptId, segmentId } = caption.selection;
|
|
5851
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5852
|
+
if (segment !== void 0) views.get(audioScriptId)?.captions.push({
|
|
5540
5853
|
clipId,
|
|
5541
5854
|
captionId: source,
|
|
5542
5855
|
segment
|
|
@@ -5644,7 +5957,7 @@ function toVideoDraft(snapshot) {
|
|
|
5644
5957
|
audio_storage_key: source.storageKey,
|
|
5645
5958
|
origin_speech_id: source.key,
|
|
5646
5959
|
volume: clip.volume,
|
|
5647
|
-
audio_script: spokenText(snapshot, sourceId
|
|
5960
|
+
audio_script: spokenText(snapshot, sourceId),
|
|
5648
5961
|
voice: voice ? {
|
|
5649
5962
|
id: voice.key,
|
|
5650
5963
|
name: voice.name
|
|
@@ -5730,29 +6043,17 @@ function requireWholeAudio(media, clip, fail) {
|
|
|
5730
6043
|
if (media.kind === "image" || clip.inMs !== 0 || clip.outMs !== media.durationMs || clip.rate !== 1) fail("VideoDraft audio has no trim or speed fields");
|
|
5731
6044
|
}
|
|
5732
6045
|
/**
|
|
5733
|
-
*
|
|
5734
|
-
*
|
|
5735
|
-
* script order. A Clip no Caption of its script follows says the whole script.
|
|
6046
|
+
* What a recording says: what the selection of the PhoneticScript it was spoken from holds now,
|
|
6047
|
+
* or, for one derived from a script directly, the whole script, one segment per line.
|
|
5736
6048
|
*/
|
|
5737
|
-
function spokenText(snapshot, recordingId
|
|
5738
|
-
const
|
|
5739
|
-
if (
|
|
5740
|
-
const
|
|
5741
|
-
if (!
|
|
5742
|
-
|
|
5743
|
-
const
|
|
5744
|
-
|
|
5745
|
-
for (const clipId of captionClipIds) {
|
|
5746
|
-
const captionId = snapshot["clip-entities"][clipId]?.source;
|
|
5747
|
-
const caption = captionId === void 0 ? void 0 : snapshot["caption-entities"][captionId];
|
|
5748
|
-
const shown = captionId === void 0 ? void 0 : snapshot.captionText[captionId];
|
|
5749
|
-
if (caption?.selection?.audioScriptId !== id || caption.text !== void 0 || !shown) continue;
|
|
5750
|
-
segments.forEach((line, index) => {
|
|
5751
|
-
if (shown.start < Math.max(line.end, line.start + 1) && shown.end > line.start) said.add(index);
|
|
5752
|
-
});
|
|
5753
|
-
}
|
|
5754
|
-
if (!said.size) return body;
|
|
5755
|
-
return [...said].sort((a, b) => a - b).map((index) => segments[index].text).join("\n");
|
|
6049
|
+
function spokenText(snapshot, recordingId) {
|
|
6050
|
+
const spoken = spokenScripts(snapshot, recordingId);
|
|
6051
|
+
if (new Set(spoken.map((s) => s.phoneticScriptId ?? s.audioScriptId)).size > 1) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple scripts cannot be silently concatenated into audio_script");
|
|
6052
|
+
const [first] = spoken;
|
|
6053
|
+
if (!first) return "";
|
|
6054
|
+
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
6055
|
+
const script = snapshot["audio-script-entities"][first.audioScriptId];
|
|
6056
|
+
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
5756
6057
|
}
|
|
5757
6058
|
//#endregion
|
|
5758
6059
|
//#region src/dsl/projection/from-video-draft.ts
|
|
@@ -5894,16 +6195,16 @@ function prepare(input, options) {
|
|
|
5894
6195
|
let script = scriptByRecording.get(recordingId);
|
|
5895
6196
|
if (script && script.text !== speech.audio_script) fail(`part_library/${id}/audio_script`, "One recording cannot acquire conflicting source scripts during import");
|
|
5896
6197
|
if (!script) {
|
|
5897
|
-
script =
|
|
5898
|
-
id: claim(mint("audio-script")),
|
|
5899
|
-
text: speech.audio_script
|
|
5900
|
-
};
|
|
6198
|
+
script = importedScript(claim(mint("audio-script")), speech.audio_script, () => mint("segment"));
|
|
5901
6199
|
scriptByRecording.set(recordingId, script);
|
|
5902
6200
|
mutations.push({
|
|
5903
6201
|
type: "create-entity",
|
|
5904
6202
|
table: "audio-script-entities",
|
|
5905
6203
|
id: script.id,
|
|
5906
|
-
value: {
|
|
6204
|
+
value: { segments: script.segments.map(({ id, text }) => ({
|
|
6205
|
+
id,
|
|
6206
|
+
text
|
|
6207
|
+
})) }
|
|
5907
6208
|
}, {
|
|
5908
6209
|
type: "set-relation",
|
|
5909
6210
|
table: "derived-from-relations",
|
|
@@ -5951,12 +6252,18 @@ function prepare(input, options) {
|
|
|
5951
6252
|
const text = caption.text;
|
|
5952
6253
|
if (typeof text !== "string") fail(`part_library/${captionId}/text`, "Expected Caption text");
|
|
5953
6254
|
const start = text.length ? script.text.indexOf(text, cursor) : -1;
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
6255
|
+
const end = start + text.length;
|
|
6256
|
+
const segment = script.segments.find((s) => start >= s.start && end <= s.start + s.text.length);
|
|
6257
|
+
if (start >= 0 && segment) {
|
|
6258
|
+
cursor = end;
|
|
6259
|
+
selections.set(captionId, text === segment.text ? {
|
|
6260
|
+
audioScriptId: script.id,
|
|
6261
|
+
segmentId: segment.id
|
|
6262
|
+
} : {
|
|
5957
6263
|
audioScriptId: script.id,
|
|
5958
|
-
|
|
5959
|
-
|
|
6264
|
+
segmentId: segment.id,
|
|
6265
|
+
start: start - segment.start,
|
|
6266
|
+
end: end - segment.start
|
|
5960
6267
|
});
|
|
5961
6268
|
} else diagnostics.push({
|
|
5962
6269
|
code: "caption-text-unmatched",
|
|
@@ -6067,5 +6374,22 @@ function milliseconds(value, path, positive = false, signed = false) {
|
|
|
6067
6374
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || !signed && value < 0 || positive && value <= 0) projectionError("invalid-time", path, "Expected safe integer milliseconds in the valid range");
|
|
6068
6375
|
return value;
|
|
6069
6376
|
}
|
|
6377
|
+
function importedScript(id, text, mintSegment) {
|
|
6378
|
+
const segments = [];
|
|
6379
|
+
let start = 0;
|
|
6380
|
+
if (text.length) for (const line of text.split("\n")) {
|
|
6381
|
+
segments.push({
|
|
6382
|
+
id: mintSegment(),
|
|
6383
|
+
text: line,
|
|
6384
|
+
start
|
|
6385
|
+
});
|
|
6386
|
+
start += line.length + 1;
|
|
6387
|
+
}
|
|
6388
|
+
return {
|
|
6389
|
+
id,
|
|
6390
|
+
text,
|
|
6391
|
+
segments
|
|
6392
|
+
};
|
|
6393
|
+
}
|
|
6070
6394
|
//#endregion
|
|
6071
6395
|
export { DslEditor as a, DslProjectionError as c, createInitialMedeoDsl as i, toVideoDraft as n, MedeoDsl as o, toAudioScriptViews as r, resolveDslLayout as s, fromVideoDraft as t };
|