@mengine/medeo-client 2.1.1-dsl.1 → 2.1.1-dsl.11
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 +26 -11
- package/dist/{dsl-EV6kCdr6.js → dsl-Co3RI5F1.js} +1101 -531
- package/dist/dsl.d.ts +3 -3
- package/dist/dsl.js +3 -3
- package/dist/{index-B7fC7q7P.d.ts → index-DAfOkcnZ.d.ts} +23 -4
- package/dist/{index-Cu7SdKN5.d.ts → index-DSpKN_ta.d.ts} +187 -171
- package/dist/{index-yifEtZSP.d.ts → index-JWERGMrC.d.ts} +53 -56
- package/dist/index.d.ts +5 -5
- package/dist/index.js +65 -9
- 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-TXRv3wS6.js → video-draft-types-BnbfWrBx.js} +178 -111
- package/dist/{video-draft-types-c_shaRyq.d.ts → video-draft-types-GqXnsOzA.d.ts} +114 -54
- 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
|
|
@@ -12,20 +12,15 @@ function range(text, start, end) {
|
|
|
12
12
|
assertTextOffset(text, end);
|
|
13
13
|
if (start >= end) throw new Error("Expected a nonempty text range");
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
if (
|
|
19
|
-
if (key === "pauseMs" && (selected !== " " || typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)) throw new Error("pauseMs requires a nonnegative integer on one ordinary space");
|
|
15
|
+
/** A script marks only what language a run is written in; how to say it belongs to its phonetic script. */
|
|
16
|
+
function assertMark(key, value) {
|
|
17
|
+
if (key !== "language") throw new Error(`Unsupported mark: ${key}`);
|
|
18
|
+
if (value !== null && !validLanguage(value)) throw new Error("Invalid language");
|
|
20
19
|
}
|
|
21
|
-
function attributes(attrs
|
|
20
|
+
function attributes(attrs) {
|
|
22
21
|
if (!attrs) return;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
assertMark(key, value, selected);
|
|
26
|
-
result[key] = key === "pauseMs" && value === 0 ? null : value;
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
22
|
+
for (const [key, value] of Object.entries(attrs)) assertMark(key, value);
|
|
23
|
+
return { ...attrs };
|
|
29
24
|
}
|
|
30
25
|
function previewDelta(text, delta) {
|
|
31
26
|
let offset = 0;
|
|
@@ -44,7 +39,7 @@ function previewDelta(text, delta) {
|
|
|
44
39
|
].includes(key))) throw new Error("Invalid text Delta operation");
|
|
45
40
|
if ("insert" in op) {
|
|
46
41
|
if (typeof op.insert !== "string") throw new Error("Only text insertions are supported");
|
|
47
|
-
const attrs = attributes(op.attributes
|
|
42
|
+
const attrs = attributes(op.attributes);
|
|
48
43
|
text = text.slice(0, offset) + op.insert + text.slice(offset);
|
|
49
44
|
normalized.push({
|
|
50
45
|
insert: op.insert,
|
|
@@ -56,7 +51,7 @@ function previewDelta(text, delta) {
|
|
|
56
51
|
if (!Number.isSafeInteger(length) || length < 0) throw new Error("Invalid Delta length");
|
|
57
52
|
assertTextOffset(text, offset + length);
|
|
58
53
|
if ("retain" in op) {
|
|
59
|
-
const attrs = attributes(op.attributes
|
|
54
|
+
const attrs = attributes(op.attributes);
|
|
60
55
|
normalized.push({
|
|
61
56
|
retain: length,
|
|
62
57
|
...attrs ? { attributes: attrs } : {}
|
|
@@ -91,11 +86,11 @@ function prepareTextEdits(text, edits) {
|
|
|
91
86
|
break;
|
|
92
87
|
case "mark":
|
|
93
88
|
range(text, edit.start, edit.end);
|
|
94
|
-
assertMark(edit.key, edit.value
|
|
89
|
+
assertMark(edit.key, edit.value);
|
|
95
90
|
break;
|
|
96
91
|
case "unmark":
|
|
97
92
|
range(text, edit.start, edit.end);
|
|
98
|
-
assertMark(edit.key, null
|
|
93
|
+
assertMark(edit.key, null);
|
|
99
94
|
break;
|
|
100
95
|
case "delta": {
|
|
101
96
|
const result = previewDelta(text, edit.delta);
|
|
@@ -109,7 +104,28 @@ function prepareTextEdits(text, edits) {
|
|
|
109
104
|
}
|
|
110
105
|
return edit;
|
|
111
106
|
}),
|
|
112
|
-
|
|
107
|
+
text
|
|
108
|
+
};
|
|
109
|
+
}
|
|
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;
|
|
115
|
+
return first;
|
|
116
|
+
}
|
|
117
|
+
/** Read language marks off a rich-text delta. */
|
|
118
|
+
function langsFromDelta(delta) {
|
|
119
|
+
let text = "";
|
|
120
|
+
const langs = [];
|
|
121
|
+
for (const run of delta) {
|
|
122
|
+
text += run.insert;
|
|
123
|
+
const language = typeof run.attributes?.language === "string" ? run.attributes.language : void 0;
|
|
124
|
+
for (let i = 0; i < run.insert.length; i++) langs.push(language);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
text,
|
|
128
|
+
langs
|
|
113
129
|
};
|
|
114
130
|
}
|
|
115
131
|
//#endregion
|
|
@@ -122,73 +138,116 @@ function requireEntity(rows, table, id) {
|
|
|
122
138
|
return row;
|
|
123
139
|
}
|
|
124
140
|
/**
|
|
125
|
-
* Whether one relation row may stand among these rows: its fields, its derived ID, and what
|
|
126
|
-
*
|
|
127
|
-
*
|
|
141
|
+
* Whether one relation row may stand among these rows: its fields, its derived ID, and what its
|
|
142
|
+
* kind requires of each end. A row being linked need not be in `rows` yet. Relations an entity
|
|
143
|
+
* holds at most one of are fields of that entity; see {@link assertReferenceAdmitted}.
|
|
128
144
|
*/
|
|
129
145
|
function assertRelationAdmitted(rows, table, id, value) {
|
|
130
146
|
assertId(id);
|
|
131
147
|
assertRelation(table, value);
|
|
132
148
|
assertRelationIdentity(table, id, value);
|
|
133
149
|
const v = value;
|
|
134
|
-
const require = (endpoint, id) => requireEntity(rows, endpoint, id);
|
|
135
150
|
switch (table) {
|
|
136
|
-
case "clip-source-relations":
|
|
137
|
-
require("clip-entities", v.clipId);
|
|
138
|
-
if (!rows["media-entities"][v.sourceId] && !rows["caption-entities"][v.sourceId]) throw new Error("Invalid Clip source");
|
|
139
|
-
break;
|
|
140
|
-
case "time-anchor-relations": {
|
|
141
|
-
require("clip-entities", v.from);
|
|
142
|
-
if (rows["timeline-entities"][v.to]) {
|
|
143
|
-
if (v.offsetMs < 0) throw new Error("Absolute offset must be nonnegative");
|
|
144
|
-
} else require("clip-entities", v.to);
|
|
145
|
-
const visited = /* @__PURE__ */ new Set();
|
|
146
|
-
let from = v.from;
|
|
147
|
-
while (true) {
|
|
148
|
-
if (visited.has(from)) throw new Error("Time anchor cycle");
|
|
149
|
-
visited.add(from);
|
|
150
|
-
const anchor = from === v.from ? v : rows["time-anchor-relations"][`ta:${from}`];
|
|
151
|
-
if (!isRecord(anchor) || typeof anchor.to !== "string") break;
|
|
152
|
-
from = anchor.to;
|
|
153
|
-
}
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case "caption-content-relations":
|
|
157
|
-
assertCaptionEndpoints(rows, v);
|
|
158
|
-
break;
|
|
159
|
-
case "caption-alignment-relations": {
|
|
160
|
-
require("caption-entities", v.captionId);
|
|
161
|
-
const media = require("media-entities", v.mediaId);
|
|
162
|
-
if (media.kind === "image" || v.startMs >= v.endMs || v.endMs > media.durationMs) throw new Error("Invalid source alignment");
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
151
|
case "voice-timbre-relations":
|
|
166
|
-
if (
|
|
167
|
-
|
|
152
|
+
if (requireEntity(rows, "media-entities", v.recordingId).kind !== "voiceover-recording") throw new Error("Timbre requires a voiceover recording");
|
|
153
|
+
requireEntity(rows, "voice-entities", v.voiceId);
|
|
168
154
|
break;
|
|
169
155
|
case "derived-from-relations":
|
|
170
156
|
for (const end of [v.entityId, v.sourceId]) if (!entityTables.some((t) => Object.hasOwn(rows[t], end))) throw new Error("Missing provenance endpoint");
|
|
171
157
|
break;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Entity fields that hold a single-valued relation, by the entity that owns it. */
|
|
161
|
+
const referenceFields = {
|
|
162
|
+
"clip-entities": ["source", "anchor"],
|
|
163
|
+
"phonetic-script-entities": ["phoneme"],
|
|
164
|
+
"caption-entities": ["selection"]
|
|
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
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Whether a single-valued relation, stored as a field of the entity that owns it, may hold this
|
|
175
|
+
* value among these rows. The owner need not be stored yet; an absent value unlinks it.
|
|
176
|
+
* A selection's run is checked against the segment's live text by whoever holds it.
|
|
177
|
+
*/
|
|
178
|
+
function assertReferenceAdmitted(rows, table, id, field, value) {
|
|
179
|
+
assertId(id);
|
|
180
|
+
const key = `${table}.${field}`;
|
|
181
|
+
if (value === void 0) {
|
|
182
|
+
if (key === "clip-entities.source") throw new Error(`Missing required relation: ${key}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
switch (key) {
|
|
186
|
+
case "clip-entities.source":
|
|
187
|
+
assertId(value);
|
|
188
|
+
if (!Object.hasOwn(rows["media-entities"], value) && !Object.hasOwn(rows["caption-entities"], value)) throw new Error("Invalid Clip source");
|
|
189
|
+
break;
|
|
190
|
+
case "clip-entities.anchor": {
|
|
191
|
+
assertTimeAnchor(value);
|
|
192
|
+
const anchor = value;
|
|
193
|
+
if (Object.hasOwn(rows["timeline-entities"], anchor.to)) {
|
|
194
|
+
if (anchor.offsetMs < 0) throw new Error("Absolute offset must be nonnegative");
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
requireEntity(rows, "clip-entities", anchor.to);
|
|
198
|
+
const visited = new Set([id]);
|
|
199
|
+
for (let next = anchor.to; typeof next === "string";) {
|
|
200
|
+
if (visited.has(next)) throw new Error("Time anchor cycle");
|
|
201
|
+
visited.add(next);
|
|
202
|
+
const clip = rows["clip-entities"][next];
|
|
203
|
+
next = isRecord(clip) && isRecord(clip.anchor) ? clip.anchor.to : void 0;
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case "caption-entities.selection":
|
|
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);
|
|
177
213
|
break;
|
|
178
214
|
}
|
|
179
215
|
}
|
|
180
216
|
}
|
|
181
|
-
/** 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. */
|
|
182
218
|
function assertCaptionEndpoints(rows, selection) {
|
|
183
219
|
requireEntity(rows, "caption-entities", selection.captionId);
|
|
184
|
-
|
|
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);
|
|
226
|
+
}
|
|
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");
|
|
185
240
|
}
|
|
186
|
-
/** A caption selects a
|
|
187
|
-
function assertCaptionSelection(rows, selection,
|
|
241
|
+
/** A caption selects a segment of its script, or a nonempty run of it. */
|
|
242
|
+
function assertCaptionSelection(rows, selection, text) {
|
|
188
243
|
assertCaptionEndpoints(rows, selection);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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");
|
|
192
251
|
}
|
|
193
252
|
/** A caption's text comes from a selection of a script or from its own text, never both. */
|
|
194
253
|
function assertCaptionSource(caption, selected) {
|
|
@@ -261,8 +320,9 @@ function replacementPatch(before, after) {
|
|
|
261
320
|
/**
|
|
262
321
|
* Admission of one whole change, asked once before it is published. Only what the change touched
|
|
263
322
|
* is checked, so an unrelated edit still lands in a merged document that holds invalid rows.
|
|
264
|
-
*
|
|
265
|
-
*
|
|
323
|
+
* Single-valued relations are fields of their owner and are checked when the owner's field is
|
|
324
|
+
* touched. Containment and caption ranges are checked by whoever holds the tree and the script
|
|
325
|
+
* bodies, with `assertContainTree` and `assertCaptionSelection`.
|
|
266
326
|
*/
|
|
267
327
|
function assertChangeAdmitted(change) {
|
|
268
328
|
const { before, after } = change;
|
|
@@ -278,18 +338,28 @@ function assertChangeAdmitted(change) {
|
|
|
278
338
|
if (!isRecord(value)) throw new Error("Expected an entity object");
|
|
279
339
|
assertEntityPatch(table, Object.fromEntries([...fields].map((field) => [field, replacementPatch(old[field], value[field])])), old);
|
|
280
340
|
}
|
|
281
|
-
const
|
|
341
|
+
const row = value;
|
|
342
|
+
for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
|
|
343
|
+
const marker = row.seqMarker;
|
|
282
344
|
if (table === "clip-entities" && (!old || fields.has("seqMarker")) && marker?.outMs <= marker?.inMs) throw new Error("Clip window must be nonempty");
|
|
345
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) assertFillsWithWholeSource(after, marker, row.source);
|
|
283
346
|
}
|
|
284
347
|
if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
|
|
285
|
-
for (const [table, ids] of change.relations) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const value = after[table][id];
|
|
289
|
-
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
290
|
-
}
|
|
348
|
+
for (const [table, ids] of change.relations) for (const id of ids) {
|
|
349
|
+
const value = after[table][id];
|
|
350
|
+
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
291
351
|
}
|
|
292
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
|
|
355
|
+
* cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
|
|
356
|
+
*/
|
|
357
|
+
function assertFillsWithWholeSource(rows, marker, source) {
|
|
358
|
+
if (marker?.fillTimeline !== true) return;
|
|
359
|
+
const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
|
|
360
|
+
const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
|
|
361
|
+
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`);
|
|
362
|
+
}
|
|
293
363
|
//#endregion
|
|
294
364
|
//#region src/dsl/projection/types.ts
|
|
295
365
|
/** Unsupported legacy representations fail explicitly; recoverable CRDT state is returned as diagnostics. */
|
|
@@ -331,9 +401,9 @@ function resolveDslLayout(snapshot) {
|
|
|
331
401
|
const clips = [];
|
|
332
402
|
for (const child of node.children) {
|
|
333
403
|
const id = child.entityId, clip = snapshot["clip-entities"][id];
|
|
334
|
-
const sourceId =
|
|
335
|
-
const media =
|
|
336
|
-
const caption =
|
|
404
|
+
const sourceId = clip.source;
|
|
405
|
+
const media = snapshot["media-entities"][sourceId];
|
|
406
|
+
const caption = snapshot["caption-entities"][sourceId];
|
|
337
407
|
const marker = clip.seqMarker;
|
|
338
408
|
let inMs = marker.inMs, outMs = marker.outMs;
|
|
339
409
|
const present = media !== void 0 || caption !== void 0;
|
|
@@ -347,7 +417,7 @@ function resolveDslLayout(snapshot) {
|
|
|
347
417
|
const rate = marker.rate ?? 1;
|
|
348
418
|
const duration = present ? Math.max(0, Math.round((outMs - inMs) / rate)) : 0;
|
|
349
419
|
if (!Number.isSafeInteger(duration)) projectionError("time-overflow", `clip-entities/${id}`, "Duration exceeds safe integer milliseconds");
|
|
350
|
-
const anchor =
|
|
420
|
+
const anchor = clip.anchor;
|
|
351
421
|
const placement = {
|
|
352
422
|
clipId: id,
|
|
353
423
|
trackId: node.entityId,
|
|
@@ -384,7 +454,7 @@ function resolveDslLayout(snapshot) {
|
|
|
384
454
|
}
|
|
385
455
|
seen.set(current, path.length);
|
|
386
456
|
path.push(current);
|
|
387
|
-
current = snapshot["
|
|
457
|
+
current = snapshot["clip-entities"][current]?.anchor?.to;
|
|
388
458
|
}
|
|
389
459
|
for (const member of path) done.add(member);
|
|
390
460
|
}
|
|
@@ -393,7 +463,7 @@ function resolveDslLayout(snapshot) {
|
|
|
393
463
|
const position = (id) => {
|
|
394
464
|
const known = positions.get(id);
|
|
395
465
|
if (known) return known;
|
|
396
|
-
const clip = result.clips[id], anchor = snapshot["
|
|
466
|
+
const clip = result.clips[id], anchor = snapshot["clip-entities"][id]?.anchor;
|
|
397
467
|
let value;
|
|
398
468
|
if (anchor && clip.mode === "anchored" && !cycle.has(id) && result.clips[anchor.to]) {
|
|
399
469
|
const parent = position(anchor.to);
|
|
@@ -500,14 +570,6 @@ function resolveDslLayout(snapshot) {
|
|
|
500
570
|
const a = track.clips[i];
|
|
501
571
|
for (const b of track.clips.slice(i + 1)) if (overlap(a, b)) diagnose("unresolved-overlap", b.clipId, `Overlaps ${a.clipId}; fixed positions, anchors and complete windows are retained`);
|
|
502
572
|
}
|
|
503
|
-
for (const [id, relation] of Object.entries(snapshot["caption-alignment-relations"])) {
|
|
504
|
-
const media = snapshot["media-entities"][relation.mediaId];
|
|
505
|
-
if (!media || media.kind === "image" || relation.endMs <= relation.startMs || relation.endMs > media.durationMs) result.diagnostics.push({
|
|
506
|
-
code: "invalid-caption-alignment",
|
|
507
|
-
path: `caption-alignment-relations/${id}`,
|
|
508
|
-
message: "No valid source alignment; the placed display window is retained"
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
573
|
return result;
|
|
512
574
|
}
|
|
513
575
|
function overlap(a, b) {
|
|
@@ -563,9 +625,10 @@ function timelineId(s) {
|
|
|
563
625
|
/** Placement facts are authored only by explicit actions; projection never writes them back. */
|
|
564
626
|
function placementMutations(s, clipId, placement) {
|
|
565
627
|
if (placement.kind === "sequential") return [{
|
|
566
|
-
type: "
|
|
567
|
-
table: "
|
|
568
|
-
id:
|
|
628
|
+
type: "patch-entity",
|
|
629
|
+
table: "clip-entities",
|
|
630
|
+
id: clipId,
|
|
631
|
+
patch: { anchor: void 0 }
|
|
569
632
|
}];
|
|
570
633
|
const offset = placement.kind === "absolute" ? placement.startMs : placement.offsetMs;
|
|
571
634
|
if (!Number.isSafeInteger(offset) || placement.kind === "absolute" && offset < 0) throw new Error("Invalid placement time");
|
|
@@ -578,27 +641,31 @@ function placementMutations(s, clipId, placement) {
|
|
|
578
641
|
while (next && s["clip-entities"][next]) {
|
|
579
642
|
if (visited.has(next)) throw new Error("Cyclic time anchor");
|
|
580
643
|
visited.add(next);
|
|
581
|
-
next = s["
|
|
644
|
+
next = s["clip-entities"][next]?.anchor?.to;
|
|
582
645
|
}
|
|
583
646
|
fallback = Math.max(0, resolveDslLayout(s).clips[target].startMs + offset);
|
|
584
647
|
}
|
|
585
648
|
if (!Number.isSafeInteger(fallback)) throw new Error("Invalid fallback time");
|
|
586
649
|
return [{
|
|
587
|
-
type: "
|
|
588
|
-
table: "
|
|
589
|
-
|
|
590
|
-
|
|
650
|
+
type: "patch-entity",
|
|
651
|
+
table: "clip-entities",
|
|
652
|
+
id: clipId,
|
|
653
|
+
patch: { anchor: {
|
|
591
654
|
to: target,
|
|
592
655
|
offsetMs: offset,
|
|
593
656
|
fallback_abs_ms: fallback
|
|
594
|
-
}
|
|
657
|
+
} }
|
|
595
658
|
}];
|
|
596
659
|
}
|
|
660
|
+
/** Clips whose time anchor points at a Clip in `ids`, with that anchor. */
|
|
661
|
+
function anchoredTo(s, ids) {
|
|
662
|
+
return Object.entries(s["clip-entities"]).flatMap(([id, clip]) => clip.anchor && ids.has(clip.anchor.to) ? [id] : []);
|
|
663
|
+
}
|
|
597
664
|
function detachChildren(s, ids) {
|
|
598
665
|
const layout = resolveDslLayout(s);
|
|
599
|
-
return
|
|
666
|
+
return anchoredTo(s, ids).flatMap((id) => !ids.has(id) && layout.clips[id] ? placementMutations(s, id, {
|
|
600
667
|
kind: "absolute",
|
|
601
|
-
startMs: layout.clips[
|
|
668
|
+
startMs: layout.clips[id].startMs
|
|
602
669
|
}) : []);
|
|
603
670
|
}
|
|
604
671
|
function deleteClips(s, ids, policy) {
|
|
@@ -608,9 +675,9 @@ function deleteClips(s, ids, policy) {
|
|
|
608
675
|
const removed = new Set(ids);
|
|
609
676
|
if (policy === "cascade") {
|
|
610
677
|
const queue = [...ids];
|
|
611
|
-
for (let i = 0; i < queue.length; i++) for (const
|
|
612
|
-
removed.add(
|
|
613
|
-
queue.push(
|
|
678
|
+
for (let i = 0; i < queue.length; i++) for (const id of anchoredTo(s, new Set([queue[i]]))) if (!removed.has(id)) {
|
|
679
|
+
removed.add(id);
|
|
680
|
+
queue.push(id);
|
|
614
681
|
}
|
|
615
682
|
}
|
|
616
683
|
const mutations = policy === "detach" ? detachChildren(s, removed) : [];
|
|
@@ -620,14 +687,6 @@ function deleteClips(s, ids, policy) {
|
|
|
620
687
|
entityId: id
|
|
621
688
|
});
|
|
622
689
|
mutations.push({
|
|
623
|
-
type: "delete-relation",
|
|
624
|
-
table: "time-anchor-relations",
|
|
625
|
-
id: `ta:${id}`
|
|
626
|
-
}, {
|
|
627
|
-
type: "delete-relation",
|
|
628
|
-
table: "clip-source-relations",
|
|
629
|
-
id: `cs:${id}`
|
|
630
|
-
}, {
|
|
631
690
|
type: "delete-entity",
|
|
632
691
|
table: "clip-entities",
|
|
633
692
|
id
|
|
@@ -640,11 +699,10 @@ function deleteClips(s, ids, policy) {
|
|
|
640
699
|
/** Batch edits validate against preceding prepared facts without touching the live document. */
|
|
641
700
|
function updateClipsPlan(base, updates) {
|
|
642
701
|
uniqueIds(updates.map((update) => update.clipId));
|
|
643
|
-
const clips = { ...base["clip-entities"] }
|
|
702
|
+
const clips = { ...base["clip-entities"] };
|
|
644
703
|
const preview = {
|
|
645
704
|
...base,
|
|
646
|
-
"clip-entities": clips
|
|
647
|
-
"time-anchor-relations": anchors
|
|
705
|
+
"clip-entities": clips
|
|
648
706
|
};
|
|
649
707
|
const mutations = [];
|
|
650
708
|
for (const update of updates) {
|
|
@@ -660,9 +718,7 @@ function updateClipsPlan(base, updates) {
|
|
|
660
718
|
if (clip.seqMarker.fillTimeline && (windowChanged || Object.hasOwn(marker, "rate"))) throw new Error("BGM trim and speed are not enabled");
|
|
661
719
|
const next = mergeFields(clip, patch);
|
|
662
720
|
if (windowChanged) {
|
|
663
|
-
const
|
|
664
|
-
if (!sourceId) throw new Error("Missing Clip source");
|
|
665
|
-
const source = requireSource(preview, sourceId, requireTrack(preview, trackOf(preview, update.clipId)).role);
|
|
721
|
+
const source = requireSource(preview, clip.source, requireTrack(preview, trackOf(preview, update.clipId)).role);
|
|
666
722
|
validateWindow(next.seqMarker, "durationMs" in source ? source.durationMs : void 0);
|
|
667
723
|
}
|
|
668
724
|
if (Object.keys(patch).length) mutations.push({
|
|
@@ -676,8 +732,7 @@ function updateClipsPlan(base, updates) {
|
|
|
676
732
|
trackOf(preview, update.clipId);
|
|
677
733
|
const changes = placementMutations(preview, update.clipId, update.placement);
|
|
678
734
|
mutations.push(...changes);
|
|
679
|
-
for (const change of changes) if (change.type === "
|
|
680
|
-
else if (change.type === "delete-relation") delete anchors[change.id];
|
|
735
|
+
for (const change of changes) if (change.type === "patch-entity" && change.table === "clip-entities") clips[update.clipId] = mergeFields(clips[update.clipId], change.patch);
|
|
681
736
|
}
|
|
682
737
|
}
|
|
683
738
|
return mutations;
|
|
@@ -724,7 +779,10 @@ function addClipsPlan(s, input, ids) {
|
|
|
724
779
|
marker.fillTimeline = true;
|
|
725
780
|
}
|
|
726
781
|
validateWindow(marker, duration);
|
|
727
|
-
const clip = {
|
|
782
|
+
const clip = {
|
|
783
|
+
seqMarker: marker,
|
|
784
|
+
source: source.id
|
|
785
|
+
};
|
|
728
786
|
if (item.volume !== void 0) clip.volume = item.volume;
|
|
729
787
|
const id = ids[offset];
|
|
730
788
|
const result = [
|
|
@@ -735,14 +793,6 @@ function addClipsPlan(s, input, ids) {
|
|
|
735
793
|
id,
|
|
736
794
|
value: clip
|
|
737
795
|
},
|
|
738
|
-
{
|
|
739
|
-
type: "set-relation",
|
|
740
|
-
table: "clip-source-relations",
|
|
741
|
-
value: {
|
|
742
|
-
clipId: id,
|
|
743
|
-
sourceId: source.id
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
796
|
{
|
|
747
797
|
type: "insert-node",
|
|
748
798
|
entityId: id,
|
|
@@ -762,10 +812,9 @@ function moveClipsPlan(s, input) {
|
|
|
762
812
|
if (!Number.isSafeInteger(input.index) || input.index < 0 || input.index > target.length) throw new Error("Invalid insertion index");
|
|
763
813
|
for (const id of input.clipIds) {
|
|
764
814
|
trackOf(s, id);
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
requireSource(s, source.sourceId, requireTrack(s, input.trackId).role);
|
|
815
|
+
const clip = requireClip(s, id);
|
|
816
|
+
if (clip.anchor) throw new Error("Sequential reorder requires sequential Clips");
|
|
817
|
+
requireSource(s, clip.source, requireTrack(s, input.trackId).role);
|
|
769
818
|
}
|
|
770
819
|
const mutations = input.onAnchored === "keepAbsolute" ? detachChildren(s, moved) : [];
|
|
771
820
|
const order = [...target];
|
|
@@ -783,7 +832,7 @@ function replaceSequencePlan(s, input, ids) {
|
|
|
783
832
|
uniqueIds(input.clipIds);
|
|
784
833
|
if (!input.clipIds.length || !input.clips.length) throw new Error("Replacement requires nonempty runs");
|
|
785
834
|
const trackId = trackOf(s, input.clipIds[0]), order = children(s, trackId), index = order.indexOf(input.clipIds[0]);
|
|
786
|
-
if (input.clipIds.some((id, i) => order[index + i] !== id || s["
|
|
835
|
+
if (input.clipIds.some((id, i) => order[index + i] !== id || s["clip-entities"][id]?.anchor)) throw new Error("Expected an ordered contiguous sequential run");
|
|
787
836
|
if (input.clips.some((c) => c.placement && c.placement.kind !== "sequential")) throw new Error("Replacement Clips must be sequential");
|
|
788
837
|
const mutations = addClipsPlan(s, {
|
|
789
838
|
trackId,
|
|
@@ -791,7 +840,7 @@ function replaceSequencePlan(s, input, ids) {
|
|
|
791
840
|
index
|
|
792
841
|
}, ids);
|
|
793
842
|
const removed = new Set(input.clipIds);
|
|
794
|
-
const
|
|
843
|
+
const clips = { ...s["clip-entities"] };
|
|
795
844
|
if (input.onAnchored === "remap") {
|
|
796
845
|
const begin = resolveDslLayout(s).clips[input.clipIds[0]].startMs;
|
|
797
846
|
const starts = [];
|
|
@@ -803,26 +852,30 @@ function replaceSequencePlan(s, input, ids) {
|
|
|
803
852
|
const marker = creation.value.seqMarker;
|
|
804
853
|
end += Math.round((marker.outMs - marker.inMs) / (marker.rate ?? 1));
|
|
805
854
|
}
|
|
806
|
-
for (const
|
|
807
|
-
const
|
|
808
|
-
|
|
855
|
+
for (const child of anchoredTo(s, removed)) {
|
|
856
|
+
const anchor = s["clip-entities"][child].anchor;
|
|
857
|
+
const slot = input.clipIds.indexOf(anchor.to);
|
|
858
|
+
if (removed.has(child) || slot >= ids.length) continue;
|
|
809
859
|
const value = {
|
|
810
|
-
from: r.from,
|
|
811
860
|
to: ids[slot],
|
|
812
|
-
offsetMs:
|
|
813
|
-
fallback_abs_ms: Math.max(0, starts[slot] +
|
|
861
|
+
offsetMs: anchor.offsetMs,
|
|
862
|
+
fallback_abs_ms: Math.max(0, starts[slot] + anchor.offsetMs)
|
|
814
863
|
};
|
|
815
864
|
mutations.push({
|
|
816
|
-
type: "
|
|
817
|
-
table: "
|
|
818
|
-
|
|
865
|
+
type: "patch-entity",
|
|
866
|
+
table: "clip-entities",
|
|
867
|
+
id: child,
|
|
868
|
+
patch: { anchor: value }
|
|
819
869
|
});
|
|
820
|
-
|
|
870
|
+
clips[child] = {
|
|
871
|
+
...clips[child],
|
|
872
|
+
anchor: value
|
|
873
|
+
};
|
|
821
874
|
}
|
|
822
875
|
}
|
|
823
876
|
mutations.push(...deleteClips({
|
|
824
877
|
...s,
|
|
825
|
-
"
|
|
878
|
+
"clip-entities": clips
|
|
826
879
|
}, input.clipIds, "cascade"));
|
|
827
880
|
return mutations;
|
|
828
881
|
}
|
|
@@ -830,26 +883,18 @@ function replaceSourcePlan(s, clipId, input) {
|
|
|
830
883
|
if (requireClip(s, clipId).seqMarker.fillTimeline) throw new Error("Use setBgm to replace a fill Clip");
|
|
831
884
|
const source = sourcePlan(s, input, trackOf(s, clipId));
|
|
832
885
|
validateWindow(input, "durationMs" in source.source ? source.source.durationMs : void 0);
|
|
833
|
-
return [
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
patch: { seqMarker: {
|
|
886
|
+
return [...source.mutations, {
|
|
887
|
+
type: "patch-entity",
|
|
888
|
+
table: "clip-entities",
|
|
889
|
+
id: clipId,
|
|
890
|
+
patch: {
|
|
891
|
+
seqMarker: {
|
|
840
892
|
inMs: input.inMs,
|
|
841
893
|
outMs: input.outMs
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
{
|
|
845
|
-
type: "set-relation",
|
|
846
|
-
table: "clip-source-relations",
|
|
847
|
-
value: {
|
|
848
|
-
clipId,
|
|
849
|
-
sourceId: source.id
|
|
850
|
-
}
|
|
894
|
+
},
|
|
895
|
+
source: source.id
|
|
851
896
|
}
|
|
852
|
-
];
|
|
897
|
+
}];
|
|
853
898
|
}
|
|
854
899
|
const produce = new Immer({
|
|
855
900
|
autoFreeze: false,
|
|
@@ -3868,11 +3913,21 @@ function rootMap(doc, name) {
|
|
|
3868
3913
|
const value = doc.getContainerById(id);
|
|
3869
3914
|
return value?.kind() === "Map" ? value : void 0;
|
|
3870
3915
|
}
|
|
3871
|
-
function
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
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
|
+
}
|
|
3876
3931
|
}
|
|
3877
3932
|
function readContainNodes(nodes) {
|
|
3878
3933
|
const read = (nodes) => nodes.map((node) => ({
|
|
@@ -3882,19 +3937,56 @@ function readContainNodes(nodes) {
|
|
|
3882
3937
|
}));
|
|
3883
3938
|
return read(nodes);
|
|
3884
3939
|
}
|
|
3940
|
+
/** What a selection holds now: its whole segment, or the run between its Cursors on that segment's Text. */
|
|
3885
3941
|
function resolveContent(doc, content) {
|
|
3886
|
-
const text = liveText(doc,
|
|
3887
|
-
|
|
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;
|
|
3888
3955
|
const start = doc.getCursorPos(content.start)?.offset;
|
|
3889
3956
|
const end = doc.getCursorPos(content.end)?.offset;
|
|
3890
3957
|
if (start === void 0 || end === void 0 || start > end) return;
|
|
3891
3958
|
return {
|
|
3892
|
-
text:
|
|
3959
|
+
text: whole.slice(start, end),
|
|
3893
3960
|
start,
|
|
3894
3961
|
end,
|
|
3895
3962
|
bodyId: text.id
|
|
3896
3963
|
};
|
|
3897
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
|
+
}
|
|
3975
|
+
/** Optional relation fields read apart from their row: an unreadable one is dropped, the row is kept. */
|
|
3976
|
+
const optionalReferences = {
|
|
3977
|
+
"clip-entities": ["anchor"],
|
|
3978
|
+
"caption-entities": ["selection"]
|
|
3979
|
+
};
|
|
3980
|
+
/** The entity a single-valued relation field points at, by the field's path. */
|
|
3981
|
+
function referenceTargets(table, row) {
|
|
3982
|
+
const nested = (field, key) => isRecord(row[field]) ? row[field][key] : void 0;
|
|
3983
|
+
switch (table) {
|
|
3984
|
+
case "clip-entities": return [["source", row.source], ["anchor/to", nested("anchor", "to")]];
|
|
3985
|
+
case "caption-entities": return [["selection/audioScriptId", nested("selection", "audioScriptId")]];
|
|
3986
|
+
case "phonetic-script-entities": return [["phoneme/audioScriptId", nested("phoneme", "audioScriptId")]];
|
|
3987
|
+
default: return [];
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3898
3990
|
function readMedeoDsl(doc, state) {
|
|
3899
3991
|
const diagnostics = [];
|
|
3900
3992
|
const diagnostic = (code, path, error) => diagnostics.push({
|
|
@@ -3907,44 +3999,42 @@ function readMedeoDsl(doc, state) {
|
|
|
3907
3999
|
const richText = {};
|
|
3908
4000
|
for (const table of entityTables) for (const [id, raw] of Object.entries(state[table] ?? {})) try {
|
|
3909
4001
|
assertId(id);
|
|
3910
|
-
|
|
3911
|
-
const
|
|
4002
|
+
let v = raw;
|
|
4003
|
+
for (const field of optionalReferences[table] ?? []) {
|
|
4004
|
+
const value = v[field];
|
|
4005
|
+
if (!isInvalidAtomic(value)) continue;
|
|
4006
|
+
diagnostic("invalid-relation", `${table}/${id}/${field}`, value.reason);
|
|
4007
|
+
const { [field]: _dropped, ...rest } = v;
|
|
4008
|
+
v = rest;
|
|
4009
|
+
}
|
|
4010
|
+
assertEntity(table, v);
|
|
3912
4011
|
assertResourceIdentity(table, id, v);
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
}] : [])
|
|
3924
|
-
};
|
|
3925
|
-
}
|
|
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");
|
|
4020
|
+
entities[table][id] = v;
|
|
4021
|
+
for (const [key, text] of texts) richText[key] = richTextOf(text);
|
|
3926
4022
|
} catch (error) {
|
|
3927
4023
|
delete entities[table][id];
|
|
3928
4024
|
diagnostic("invalid-entity", `${table}/${id}`, error);
|
|
3929
4025
|
}
|
|
3930
4026
|
const entityIds = new Set(entityTables.flatMap((t) => Object.keys(entities[t])));
|
|
4027
|
+
for (const table of entityTables) for (const [id, row] of Object.entries(entities[table])) for (const [path, target] of referenceTargets(table, row)) if (typeof target === "string" && !entityIds.has(target)) diagnostic("dangling-reference", `${table}/${id}/${path}`, `Missing entity ${target}`);
|
|
3931
4028
|
for (const table of relationTables) for (const [id, value] of Object.entries(state[table] ?? {})) try {
|
|
3932
4029
|
if (isInvalidAtomic(value)) throw new Error(value.reason);
|
|
3933
4030
|
assertRelation(table, value);
|
|
3934
4031
|
assertRelationIdentity(table, id, value);
|
|
3935
4032
|
relations[table][id] = value;
|
|
3936
4033
|
for (const key of [
|
|
3937
|
-
"clipId",
|
|
3938
|
-
"sourceId",
|
|
3939
|
-
"from",
|
|
3940
|
-
"to",
|
|
3941
|
-
"captionId",
|
|
3942
|
-
"audioScriptId",
|
|
3943
|
-
"mediaId",
|
|
3944
4034
|
"recordingId",
|
|
3945
4035
|
"voiceId",
|
|
3946
4036
|
"entityId",
|
|
3947
|
-
"
|
|
4037
|
+
"sourceId"
|
|
3948
4038
|
]) {
|
|
3949
4039
|
const endpoint = value[key];
|
|
3950
4040
|
if (typeof endpoint === "string" && !entityIds.has(endpoint)) diagnostic("dangling-reference", `${table}/${id}/${key}`, `Missing entity ${endpoint}`);
|
|
@@ -3972,22 +4062,33 @@ function readMedeoDsl(doc, state) {
|
|
|
3972
4062
|
}];
|
|
3973
4063
|
});
|
|
3974
4064
|
const captionText = {};
|
|
3975
|
-
for (const [id, content] of Object.entries(relations["caption-content-relations"])) try {
|
|
3976
|
-
const resolved = resolveContent(doc, content);
|
|
3977
|
-
if (resolved) captionText[content.captionId] = resolved;
|
|
3978
|
-
else diagnostic("invalid-caption-content", `caption-content-relations/${id}`, "Cursor does not resolve in the current body");
|
|
3979
|
-
} catch (error) {
|
|
3980
|
-
diagnostic("invalid-caption-content", `caption-content-relations/${id}`, error);
|
|
3981
|
-
}
|
|
3982
4065
|
for (const [id, caption] of Object.entries(entities["caption-entities"])) {
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
4066
|
+
if (caption.text !== void 0) {
|
|
4067
|
+
if (Object.hasOwn(state["caption-entities"]?.[id] ?? {}, "selection")) diagnostic("multiple-caption-sources", `caption-entities/${id}`, "Inline text takes precedence over the content reference");
|
|
4068
|
+
if (richText[id]) captionText[id] = {
|
|
4069
|
+
text: caption.text,
|
|
4070
|
+
start: 0,
|
|
4071
|
+
end: caption.text.length,
|
|
4072
|
+
bodyId: richText[id].bodyId
|
|
4073
|
+
};
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
if (!caption.selection) continue;
|
|
4077
|
+
try {
|
|
4078
|
+
const resolved = resolveContent(doc, caption.selection);
|
|
4079
|
+
if (resolved) captionText[id] = resolved;
|
|
4080
|
+
else diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, "Selection does not resolve in its current segment");
|
|
4081
|
+
} catch (error) {
|
|
4082
|
+
diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, error);
|
|
4083
|
+
}
|
|
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);
|
|
3991
4092
|
}
|
|
3992
4093
|
return freezeSnapshot({
|
|
3993
4094
|
...entities,
|
|
@@ -3995,6 +4096,7 @@ function readMedeoDsl(doc, state) {
|
|
|
3995
4096
|
"contain-relations": validTree(treeNodes),
|
|
3996
4097
|
richText,
|
|
3997
4098
|
captionText,
|
|
4099
|
+
phoneticText,
|
|
3998
4100
|
diagnostics
|
|
3999
4101
|
});
|
|
4000
4102
|
}
|
|
@@ -4007,6 +4109,19 @@ function freezeSnapshot(value) {
|
|
|
4007
4109
|
}
|
|
4008
4110
|
//#endregion
|
|
4009
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);
|
|
4010
4125
|
const entities = new Set(entityTables);
|
|
4011
4126
|
const relations = new Set(relationTables);
|
|
4012
4127
|
const reserved = new Set([
|
|
@@ -4039,8 +4154,10 @@ var TransactionDraft = class {
|
|
|
4039
4154
|
entities = /* @__PURE__ */ new Map();
|
|
4040
4155
|
relations = /* @__PURE__ */ new Map();
|
|
4041
4156
|
deleted = /* @__PURE__ */ new Set();
|
|
4157
|
+
/** Texts this write edits, by {@link textKey}, as its edits so far leave them. */
|
|
4042
4158
|
textPreviews = /* @__PURE__ */ new Map();
|
|
4043
|
-
|
|
4159
|
+
/** Selections the draft wrote, by table then row; they are replayed natively in order with text edits. */
|
|
4160
|
+
dirty = /* @__PURE__ */ new Map();
|
|
4044
4161
|
removedCaptionTexts = /* @__PURE__ */ new Set();
|
|
4045
4162
|
treeChanged = false;
|
|
4046
4163
|
active = true;
|
|
@@ -4071,13 +4188,30 @@ var TransactionDraft = class {
|
|
|
4071
4188
|
close() {
|
|
4072
4189
|
this.active = false;
|
|
4073
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
|
+
}
|
|
4074
4202
|
wrap(value, path) {
|
|
4075
4203
|
if (!value || typeof value !== "object" || value instanceof Cursor) return value;
|
|
4076
4204
|
const cached = this.wrappers.get(value);
|
|
4077
4205
|
if (cached) return cached;
|
|
4078
4206
|
const proxy = new Proxy(value, {
|
|
4079
4207
|
get: (target, key) => this.run(() => {
|
|
4080
|
-
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
|
+
}
|
|
4081
4215
|
if (!path.length && key === "contain-relations" && target[key] === void 0) target[key] = [];
|
|
4082
4216
|
return this.wrap(Reflect.get(target, key), [...path, String(key)]);
|
|
4083
4217
|
}),
|
|
@@ -4132,24 +4266,27 @@ var TransactionDraft = class {
|
|
|
4132
4266
|
if (path.length === 1 && (remove || next === void 0)) this.deleted.add(lifetime);
|
|
4133
4267
|
else if (this.deleted.has(lifetime)) throw new Error("Recreate an entity in a separate transaction");
|
|
4134
4268
|
if (table === "audio-script-entities") {
|
|
4135
|
-
if (path.length === 2 && key === "
|
|
4136
|
-
if (path.length === 1 &&
|
|
4137
|
-
|
|
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
|
+
}
|
|
4138
4279
|
}
|
|
4139
4280
|
if (table === "caption-entities") {
|
|
4140
4281
|
const changesText = path.length === 1 || path.length === 2 && key === "text";
|
|
4141
4282
|
const nextText = path.length === 1 ? isRecord(next) ? next.text : void 0 : next;
|
|
4142
4283
|
if (changesText) {
|
|
4143
|
-
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");
|
|
4144
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");
|
|
4145
4286
|
if (initial?.text !== void 0 && nextText === void 0) this.removedCaptionTexts.add(rowId);
|
|
4146
4287
|
}
|
|
4147
4288
|
}
|
|
4148
|
-
if (initial && [
|
|
4149
|
-
"media-entities",
|
|
4150
|
-
"asset-entities",
|
|
4151
|
-
"voice-entities"
|
|
4152
|
-
].includes(table)) {
|
|
4289
|
+
if (initial && ["media-entities", "voice-entities"].includes(table)) {
|
|
4153
4290
|
for (const field of [
|
|
4154
4291
|
"system",
|
|
4155
4292
|
"key",
|
|
@@ -4161,7 +4298,7 @@ var TransactionDraft = class {
|
|
|
4161
4298
|
let fields = rows.get(rowId);
|
|
4162
4299
|
if (!fields) rows.set(rowId, fields = /* @__PURE__ */ new Set());
|
|
4163
4300
|
if (path.length === 1) for (const field of [...Object.keys(initial ?? {}), ...Object.keys(isRecord(next) ? next : {})]) {
|
|
4164
|
-
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;
|
|
4165
4302
|
fields.add(field);
|
|
4166
4303
|
}
|
|
4167
4304
|
else fields.add(path[2] ?? key);
|
|
@@ -4169,10 +4306,12 @@ var TransactionDraft = class {
|
|
|
4169
4306
|
let ids = this.relations.get(table);
|
|
4170
4307
|
if (!ids) this.relations.set(table, ids = /* @__PURE__ */ new Set());
|
|
4171
4308
|
ids.add(rowId);
|
|
4172
|
-
if (table === "caption-content-relations") this.dirtyCaptions.add(rowId);
|
|
4173
4309
|
}
|
|
4174
4310
|
}
|
|
4175
4311
|
};
|
|
4312
|
+
function sameSegments(a, b) {
|
|
4313
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
4314
|
+
}
|
|
4176
4315
|
//#endregion
|
|
4177
4316
|
//#region src/dsl/document/draft-operations.ts
|
|
4178
4317
|
function nodeIndex(nodes) {
|
|
@@ -4224,6 +4363,7 @@ function changeTree(nodes, mutation, rows) {
|
|
|
4224
4363
|
}
|
|
4225
4364
|
//#endregion
|
|
4226
4365
|
//#region src/dsl/document/mutations.ts
|
|
4366
|
+
const resourceTables = ["media-entities", "voice-entities"];
|
|
4227
4367
|
/** Mutations are a convenience vocabulary over the same guarded draft and native command queue. */
|
|
4228
4368
|
function applyMutations(tx, mutations) {
|
|
4229
4369
|
const rows = tx.draft;
|
|
@@ -4233,11 +4373,7 @@ function applyMutations(tx, mutations) {
|
|
|
4233
4373
|
case "create-entity": {
|
|
4234
4374
|
assertEntity(m.table, m.value);
|
|
4235
4375
|
const old = rows[m.table][m.id];
|
|
4236
|
-
if (old && !
|
|
4237
|
-
"media-entities",
|
|
4238
|
-
"voice-entities",
|
|
4239
|
-
"asset-entities"
|
|
4240
|
-
].includes(m.table)) throw new Error(`Entity already exists: ${m.id}`);
|
|
4376
|
+
if (old && !resourceTables.includes(m.table)) throw new Error(`Entity already exists: ${m.id}`);
|
|
4241
4377
|
if (old) {
|
|
4242
4378
|
assertEntityPatch(m.table, m.value, old);
|
|
4243
4379
|
applyPatch(old, m.value);
|
|
@@ -4247,12 +4383,15 @@ function applyMutations(tx, mutations) {
|
|
|
4247
4383
|
case "patch-entity": {
|
|
4248
4384
|
const current = rows[m.table][m.id];
|
|
4249
4385
|
if (!current) throw new Error(`Missing entity: ${m.id}`);
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4386
|
+
const patch = m.patch;
|
|
4387
|
+
assertEntityPatch(m.table, patch, current);
|
|
4388
|
+
const atomic = atomicFields[m.table] ?? [];
|
|
4389
|
+
const next = patchRow(m.table, current, patch);
|
|
4390
|
+
const ordinary = {};
|
|
4391
|
+
for (const key of Object.keys(patch)) if (!atomic.includes(key)) ordinary[key] = patch[key];
|
|
4392
|
+
else if (next[key] === void 0) delete current[key];
|
|
4393
|
+
else current[key] = next[key];
|
|
4394
|
+
applyPatch(current, ordinary);
|
|
4256
4395
|
break;
|
|
4257
4396
|
}
|
|
4258
4397
|
case "delete-entity":
|
|
@@ -4261,18 +4400,24 @@ function applyMutations(tx, mutations) {
|
|
|
4261
4400
|
case "set-relation": {
|
|
4262
4401
|
assertRelation(m.table, m.value);
|
|
4263
4402
|
const id = relationId(m.table, m.value);
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
rows[m.table][id] ??= {};
|
|
4267
|
-
applyPatch(rows[m.table][id], m.value);
|
|
4268
|
-
}
|
|
4403
|
+
rows[m.table][id] ??= {};
|
|
4404
|
+
applyPatch(rows[m.table][id], withSourceDigest(rows, m.table, m.value));
|
|
4269
4405
|
break;
|
|
4270
4406
|
}
|
|
4271
4407
|
case "delete-relation":
|
|
4272
4408
|
delete rows[m.table][m.id];
|
|
4273
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;
|
|
4274
4419
|
case "edit-text":
|
|
4275
|
-
tx.editText(m.audioScriptId, m.edits, m.expectedBodyId);
|
|
4420
|
+
tx.editText(m.audioScriptId, m.segmentId, m.edits, m.expectedBodyId);
|
|
4276
4421
|
break;
|
|
4277
4422
|
case "edit-caption-text":
|
|
4278
4423
|
tx.editCaptionText(m.captionId, m.edits, m.expectedBodyId);
|
|
@@ -4280,10 +4425,32 @@ function applyMutations(tx, mutations) {
|
|
|
4280
4425
|
case "set-caption-content":
|
|
4281
4426
|
tx.setCaptionContent(m);
|
|
4282
4427
|
break;
|
|
4428
|
+
case "set-phoneme":
|
|
4429
|
+
tx.setPhoneme(m);
|
|
4430
|
+
break;
|
|
4283
4431
|
default: changeTree(tx.draft["contain-relations"], m, tx.draft);
|
|
4284
4432
|
}
|
|
4285
4433
|
}
|
|
4286
4434
|
}
|
|
4435
|
+
/**
|
|
4436
|
+
* A derived-from records its source's digest (D177): the one the writer names, otherwise the
|
|
4437
|
+
* source's digest as this transaction leaves it. A source with no digest records none.
|
|
4438
|
+
*/
|
|
4439
|
+
function withSourceDigest(rows, table, value) {
|
|
4440
|
+
if (table !== "derived-from-relations") return value;
|
|
4441
|
+
const relation = value;
|
|
4442
|
+
if (relation.sourceDigest !== void 0) return value;
|
|
4443
|
+
const sourceDigest = digestOf(rows, relation.sourceId);
|
|
4444
|
+
return sourceDigest === void 0 ? value : {
|
|
4445
|
+
...relation,
|
|
4446
|
+
sourceDigest
|
|
4447
|
+
};
|
|
4448
|
+
}
|
|
4449
|
+
/** A source's digest is its file's: a PhoneticScript's or a medium's Media File `digest`. */
|
|
4450
|
+
function digestOf(rows, entityId) {
|
|
4451
|
+
const digest = (rows["phonetic-script-entities"][entityId] ?? rows["media-entities"][entityId])?.digest;
|
|
4452
|
+
return typeof digest === "string" ? digest : void 0;
|
|
4453
|
+
}
|
|
4287
4454
|
//#endregion
|
|
4288
4455
|
//#region src/dsl/document/native-text.ts
|
|
4289
4456
|
function applyTextEdits(text, edits) {
|
|
@@ -4295,11 +4462,7 @@ function applyTextEdits(text, edits) {
|
|
|
4295
4462
|
text.delete(edit.at, edit.length);
|
|
4296
4463
|
break;
|
|
4297
4464
|
case "mark":
|
|
4298
|
-
|
|
4299
|
-
start: edit.start,
|
|
4300
|
-
end: edit.end
|
|
4301
|
-
}, edit.key);
|
|
4302
|
-
else text.mark({
|
|
4465
|
+
text.mark({
|
|
4303
4466
|
start: edit.start,
|
|
4304
4467
|
end: edit.end
|
|
4305
4468
|
}, edit.key, edit.value);
|
|
@@ -4317,13 +4480,15 @@ function applyTextEdits(text, edits) {
|
|
|
4317
4480
|
}
|
|
4318
4481
|
//#endregion
|
|
4319
4482
|
//#region src/dsl/document/validation.ts
|
|
4320
|
-
/**
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
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);
|
|
4327
4492
|
}
|
|
4328
4493
|
/**
|
|
4329
4494
|
* The DSL's admission of the whole draft, then what only this document can check: containment
|
|
@@ -4355,6 +4520,14 @@ function validateDraft(tracked) {
|
|
|
4355
4520
|
}
|
|
4356
4521
|
//#endregion
|
|
4357
4522
|
//#region src/dsl/document/transact.ts
|
|
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);
|
|
4529
|
+
return isContainer(row) && row.kind() === "Map" ? row : void 0;
|
|
4530
|
+
}
|
|
4358
4531
|
/** Serialization happens before preparing any writes, so invalid/cyclic audit data cannot partly commit. */
|
|
4359
4532
|
function auditOptions(audit) {
|
|
4360
4533
|
if (!audit || typeof audit.semantic_op !== "string" || !audit.semantic_op.length || !Object.hasOwn(audit, "payload")) throw new Error("A structured DSL audit is required");
|
|
@@ -4373,8 +4546,8 @@ const nativeHookSupport = /* @__PURE__ */ new WeakMap();
|
|
|
4373
4546
|
/**
|
|
4374
4547
|
* Every write relies on the patched Mirror's `applyNative` (patches/loro-mirror@2.3.3.patch). An
|
|
4375
4548
|
* unpatched Mirror ignores the option and commits only the draft, silently dropping Text, ranges
|
|
4376
|
-
* and
|
|
4377
|
-
*
|
|
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.
|
|
4378
4551
|
*/
|
|
4379
4552
|
function assertNativeHook(mirror) {
|
|
4380
4553
|
const Impl = mirror.constructor;
|
|
@@ -4408,50 +4581,75 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4408
4581
|
mirror.setState((raw) => {
|
|
4409
4582
|
const tracked = new TransactionDraft(raw, before);
|
|
4410
4583
|
setReject((error) => tracked.fail(error));
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
if (
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
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;
|
|
4420
4605
|
};
|
|
4421
|
-
const
|
|
4422
|
-
for (const id of tracked.
|
|
4423
|
-
const
|
|
4424
|
-
if (
|
|
4425
|
-
|
|
4426
|
-
|
|
4606
|
+
const flushSelections = () => {
|
|
4607
|
+
for (const [table, id] of tracked.dirtySelections()) {
|
|
4608
|
+
const row = raw[table][id];
|
|
4609
|
+
if (!row) continue;
|
|
4610
|
+
const field = selectionField[table];
|
|
4611
|
+
const value = row[field];
|
|
4612
|
+
const old = before[table][id]?.[field];
|
|
4613
|
+
if (isInvalidAtomic(value)) {
|
|
4614
|
+
if (isInvalidAtomic(old) && old.raw === value.raw) continue;
|
|
4615
|
+
throw new Error(`Invalid ${field}`);
|
|
4427
4616
|
}
|
|
4617
|
+
if (value !== void 0) validateSelection(doc, raw, table, id, value);
|
|
4428
4618
|
steps.push({
|
|
4429
|
-
type: "
|
|
4619
|
+
type: "selection",
|
|
4620
|
+
table,
|
|
4430
4621
|
id,
|
|
4431
4622
|
value: copyInput(value)
|
|
4432
4623
|
});
|
|
4433
4624
|
}
|
|
4434
|
-
tracked.
|
|
4625
|
+
tracked.clearDirtySelections();
|
|
4435
4626
|
};
|
|
4436
|
-
const editBody = (
|
|
4627
|
+
const editBody = (target, edits, expectedBodyId) => {
|
|
4437
4628
|
tracked.run(() => {
|
|
4438
|
-
|
|
4439
|
-
const
|
|
4440
|
-
|
|
4441
|
-
|
|
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);
|
|
4442
4634
|
if (expectedBodyId !== void 0 && native?.id !== expectedBodyId) throw new Error("Stale or missing Text body");
|
|
4443
|
-
if ((
|
|
4444
|
-
const prepared = prepareTextEdits(
|
|
4445
|
-
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);
|
|
4446
4638
|
steps.push({
|
|
4447
4639
|
type: "text",
|
|
4448
|
-
|
|
4449
|
-
caption,
|
|
4640
|
+
target,
|
|
4450
4641
|
expectedBodyId: native?.id,
|
|
4451
4642
|
edits: prepared.edits
|
|
4452
4643
|
});
|
|
4453
4644
|
});
|
|
4454
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
|
+
};
|
|
4455
4653
|
const tx = Object.freeze({
|
|
4456
4654
|
get draft() {
|
|
4457
4655
|
return tracked.run(() => tracked.draft);
|
|
@@ -4459,29 +4657,95 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4459
4657
|
applyMutations(mutations) {
|
|
4460
4658
|
tracked.run(() => applyMutations(tx, mutations));
|
|
4461
4659
|
},
|
|
4462
|
-
editText(
|
|
4463
|
-
editBody(
|
|
4660
|
+
editText(audioScriptId, segmentId, edits, expectedBodyId) {
|
|
4661
|
+
editBody({
|
|
4662
|
+
audioScriptId,
|
|
4663
|
+
segmentId
|
|
4664
|
+
}, edits, expectedBodyId);
|
|
4464
4665
|
},
|
|
4465
4666
|
editCaptionText(captionId, edits, expectedBodyId) {
|
|
4466
|
-
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
|
+
});
|
|
4467
4704
|
},
|
|
4468
4705
|
setCaptionContent(selection) {
|
|
4469
4706
|
tracked.run(() => {
|
|
4470
|
-
|
|
4471
|
-
const { captionId, audioScriptId, start, end } = selection;
|
|
4707
|
+
flushSelections();
|
|
4708
|
+
const { captionId, audioScriptId, segmentId, start, end } = selection;
|
|
4472
4709
|
assertId(captionId);
|
|
4473
|
-
|
|
4474
|
-
|
|
4710
|
+
assertCaptionSelection(raw, selection, useText({
|
|
4711
|
+
audioScriptId,
|
|
4712
|
+
segmentId
|
|
4713
|
+
}));
|
|
4475
4714
|
steps.push({
|
|
4476
4715
|
type: "range",
|
|
4477
|
-
|
|
4478
|
-
|
|
4716
|
+
table: "caption-entities",
|
|
4717
|
+
id: captionId,
|
|
4718
|
+
range: {
|
|
4479
4719
|
audioScriptId,
|
|
4720
|
+
segmentId,
|
|
4480
4721
|
start,
|
|
4481
4722
|
end
|
|
4482
4723
|
}
|
|
4483
4724
|
});
|
|
4484
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
|
+
});
|
|
4485
4749
|
}
|
|
4486
4750
|
});
|
|
4487
4751
|
try {
|
|
@@ -4494,32 +4758,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4494
4758
|
}
|
|
4495
4759
|
tracked.assertValid();
|
|
4496
4760
|
if (doc.getPendingTxnLength()) throw new Error("Native writes are not allowed during transaction preparation");
|
|
4497
|
-
|
|
4761
|
+
flushSelections();
|
|
4498
4762
|
for (const id of tracked.entities.get("caption-entities")?.keys() ?? []) {
|
|
4499
4763
|
const caption = tracked.draft["caption-entities"][id];
|
|
4500
4764
|
if (caption) caption.style ??= {};
|
|
4501
4765
|
}
|
|
4502
4766
|
validateDraft(tracked);
|
|
4503
|
-
for (const step of steps) if (step.type === "text")
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
4507
|
-
for (const step of steps) {
|
|
4508
|
-
if (step.type === "range") {
|
|
4509
|
-
referenceIds.add(step.selection.captionId);
|
|
4510
|
-
changedCaptions.add(step.selection.captionId);
|
|
4511
|
-
}
|
|
4512
|
-
if (step.type === "caption") {
|
|
4513
|
-
const id = step.id.slice(3);
|
|
4514
|
-
changedCaptions.add(id);
|
|
4515
|
-
if (step.value) referenceIds.add(id);
|
|
4516
|
-
else referenceIds.delete(id);
|
|
4517
|
-
}
|
|
4518
|
-
if (step.type === "text" && step.caption) changedCaptions.add(step.scriptId);
|
|
4519
|
-
}
|
|
4520
|
-
for (const id of changedCaptions) assertCaptionSource(raw["caption-entities"][id], referenceIds.has(id));
|
|
4521
|
-
for (const step of steps) if (step.type === "range") assertCaptionEndpoints(raw, step.selection);
|
|
4522
|
-
else if (step.type === "caption" && step.value) assertCaptionEndpoints(raw, step.value);
|
|
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);
|
|
4523
4770
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
4524
4771
|
const row = raw["caption-entities"][id];
|
|
4525
4772
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -4527,8 +4774,16 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4527
4774
|
delete row.text;
|
|
4528
4775
|
}
|
|
4529
4776
|
}
|
|
4530
|
-
for (const
|
|
4531
|
-
|
|
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
|
+
}
|
|
4786
|
+
}
|
|
4532
4787
|
});
|
|
4533
4788
|
} finally {
|
|
4534
4789
|
tracked.close();
|
|
@@ -4537,44 +4792,104 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4537
4792
|
}, {
|
|
4538
4793
|
...options,
|
|
4539
4794
|
applyNative: (doc) => {
|
|
4540
|
-
const
|
|
4541
|
-
|
|
4795
|
+
const textFor = (target) => {
|
|
4796
|
+
const key = textKey(target);
|
|
4797
|
+
let text = bodies.get(key);
|
|
4542
4798
|
if (!text) {
|
|
4543
|
-
text = liveText(doc,
|
|
4544
|
-
bodies.set(
|
|
4799
|
+
text = liveText(doc, target);
|
|
4800
|
+
bodies.set(key, text);
|
|
4545
4801
|
}
|
|
4546
|
-
if (!text || text.isDeleted()) throw new Error("Prepared Text
|
|
4802
|
+
if (!text || text.isDeleted()) throw new Error("Prepared Text is missing");
|
|
4547
4803
|
return text;
|
|
4548
4804
|
};
|
|
4549
4805
|
for (const [id, value] of newCaptionTexts) {
|
|
4550
|
-
const
|
|
4551
|
-
if (value.length)
|
|
4552
|
-
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);
|
|
4553
4809
|
}
|
|
4554
|
-
for (const step of steps)
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
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);
|
|
4574
4841
|
}
|
|
4575
4842
|
}
|
|
4576
4843
|
});
|
|
4577
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
|
+
}
|
|
4578
4893
|
//#endregion
|
|
4579
4894
|
//#region src/dsl/document/medeo-dsl.ts
|
|
4580
4895
|
const contentRoots = new Set([
|
|
@@ -4665,17 +4980,23 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4665
4980
|
applyMutations(mutations, audit) {
|
|
4666
4981
|
this.transact((tx) => tx.applyMutations(mutations), audit);
|
|
4667
4982
|
}
|
|
4668
|
-
editText(scriptId, expectedBodyId, edits, audit) {
|
|
4669
|
-
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);
|
|
4670
4985
|
}
|
|
4671
4986
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
4672
4987
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
4673
4988
|
}
|
|
4674
|
-
/**
|
|
4675
|
-
|
|
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) {
|
|
4676
4994
|
this.assertActive();
|
|
4677
|
-
const text = liveText(this.doc,
|
|
4678
|
-
|
|
4995
|
+
const text = liveText(this.doc, {
|
|
4996
|
+
audioScriptId: scriptId,
|
|
4997
|
+
segmentId
|
|
4998
|
+
});
|
|
4999
|
+
if (!text) throw new Error("Missing AudioScript segment");
|
|
4679
5000
|
assertTextOffset(text.toString(), start);
|
|
4680
5001
|
assertTextOffset(text.toString(), end);
|
|
4681
5002
|
if (start >= end) throw new Error("Expected a nonempty range");
|
|
@@ -4683,6 +5004,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4683
5004
|
if (!from || !to) throw new Error("Unable to construct text range");
|
|
4684
5005
|
return {
|
|
4685
5006
|
audioScriptId: scriptId,
|
|
5007
|
+
segmentId,
|
|
4686
5008
|
start: from,
|
|
4687
5009
|
end: to
|
|
4688
5010
|
};
|
|
@@ -4732,6 +5054,132 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4732
5054
|
}
|
|
4733
5055
|
};
|
|
4734
5056
|
//#endregion
|
|
5057
|
+
//#region src/dsl/projection/audio-scripts.ts
|
|
5058
|
+
/**
|
|
5059
|
+
* The scripts a recording says. It follows derived-from from the recording; a PhoneticScript is a
|
|
5060
|
+
* variant, so its words are the base its phoneme names, and derived-from is not followed past it.
|
|
5061
|
+
* When nothing is found that way, a script read off the recording itself is what it says.
|
|
5062
|
+
*/
|
|
5063
|
+
function spokenScripts(snapshot, recordingId) {
|
|
5064
|
+
const relations = Object.values(snapshot["derived-from-relations"]);
|
|
5065
|
+
const found = /* @__PURE__ */ new Map();
|
|
5066
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5067
|
+
const visit = (id, phoneticScriptId) => {
|
|
5068
|
+
if (seen.has(id)) return;
|
|
5069
|
+
seen.add(id);
|
|
5070
|
+
if (snapshot["audio-script-entities"][id]) {
|
|
5071
|
+
if (!found.has(id)) found.set(id, phoneticScriptId);
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
5075
|
+
if (phonetic) {
|
|
5076
|
+
visit(phonetic.phoneme.audioScriptId, id);
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
for (const r of relations) if (r.entityId === id) visit(r.sourceId, phoneticScriptId);
|
|
5080
|
+
};
|
|
5081
|
+
visit(recordingId);
|
|
5082
|
+
if (!found.size) {
|
|
5083
|
+
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
|
|
5084
|
+
}
|
|
5085
|
+
return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
|
|
5086
|
+
audioScriptId,
|
|
5087
|
+
phoneticScriptId
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
5091
|
+
function toAudioScriptViews(snapshot) {
|
|
5092
|
+
const views = /* @__PURE__ */ new Map();
|
|
5093
|
+
const segmentIndex = (scriptId, segmentId) => {
|
|
5094
|
+
const index = snapshot["audio-script-entities"][scriptId]?.segments.findIndex((s) => s.id === segmentId) ?? -1;
|
|
5095
|
+
return index < 0 ? void 0 : index;
|
|
5096
|
+
};
|
|
5097
|
+
for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) views.set(id, {
|
|
5098
|
+
id,
|
|
5099
|
+
segments: script.segments.map((segment) => {
|
|
5100
|
+
const rich = snapshot.richText[segment.id];
|
|
5101
|
+
const language = rich ? markedLanguage(langsFromDelta(rich.delta)) : void 0;
|
|
5102
|
+
return language === void 0 ? {
|
|
5103
|
+
id: segment.id,
|
|
5104
|
+
text: segment.text
|
|
5105
|
+
} : {
|
|
5106
|
+
id: segment.id,
|
|
5107
|
+
text: segment.text,
|
|
5108
|
+
language
|
|
5109
|
+
};
|
|
5110
|
+
}),
|
|
5111
|
+
sourceMediaIds: [],
|
|
5112
|
+
phoneticScripts: [],
|
|
5113
|
+
speeches: [],
|
|
5114
|
+
captions: []
|
|
5115
|
+
});
|
|
5116
|
+
for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
|
|
5117
|
+
for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
|
|
5118
|
+
const { audioScriptId, segmentId, phonemeScript } = phonetic.phoneme;
|
|
5119
|
+
const { digest } = phonetic;
|
|
5120
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5121
|
+
const text = snapshot.phoneticText[id]?.text;
|
|
5122
|
+
views.get(audioScriptId)?.phoneticScripts.push({
|
|
5123
|
+
id,
|
|
5124
|
+
...segment === void 0 ? {} : { segment },
|
|
5125
|
+
...text === void 0 ? {} : { text },
|
|
5126
|
+
...phonemeScript === void 0 ? {} : { phonemeScript },
|
|
5127
|
+
...digest === void 0 ? {} : { digest }
|
|
5128
|
+
});
|
|
5129
|
+
}
|
|
5130
|
+
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
5131
|
+
for (const track of resolveDslLayout(snapshot).tracks) for (const { clipId } of track.clips) {
|
|
5132
|
+
const source = snapshot["clip-entities"][clipId]?.source;
|
|
5133
|
+
if (!source) continue;
|
|
5134
|
+
if (snapshot["media-entities"][source]?.kind === "voiceover-recording") {
|
|
5135
|
+
const voices = timbres.filter((r) => r.recordingId === source).flatMap((r) => {
|
|
5136
|
+
const voice = snapshot["voice-entities"][r.voiceId];
|
|
5137
|
+
if (!voice) return [];
|
|
5138
|
+
return [voice.name === void 0 ? {
|
|
5139
|
+
voiceId: r.voiceId,
|
|
5140
|
+
key: voice.key
|
|
5141
|
+
} : {
|
|
5142
|
+
voiceId: r.voiceId,
|
|
5143
|
+
key: voice.key,
|
|
5144
|
+
name: voice.name
|
|
5145
|
+
}];
|
|
5146
|
+
});
|
|
5147
|
+
for (const spoken of spokenScripts(snapshot, source)) {
|
|
5148
|
+
const applied = appliedSpeech(snapshot, source, spoken);
|
|
5149
|
+
views.get(spoken.audioScriptId)?.speeches.push({
|
|
5150
|
+
clipId,
|
|
5151
|
+
recordingId: source,
|
|
5152
|
+
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
5153
|
+
voices,
|
|
5154
|
+
...applied === void 0 ? {} : { applied }
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
5157
|
+
continue;
|
|
5158
|
+
}
|
|
5159
|
+
const caption = snapshot["caption-entities"][source];
|
|
5160
|
+
if (!caption?.selection || caption.text !== void 0 || !snapshot.captionText[source]) continue;
|
|
5161
|
+
const { audioScriptId, segmentId } = caption.selection;
|
|
5162
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5163
|
+
if (segment !== void 0) views.get(audioScriptId)?.captions.push({
|
|
5164
|
+
clipId,
|
|
5165
|
+
captionId: source,
|
|
5166
|
+
segment
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
5169
|
+
return [...views.values()];
|
|
5170
|
+
}
|
|
5171
|
+
/**
|
|
5172
|
+
* Whether a recording still matches what it was synthesized from: the digest its derived-from
|
|
5173
|
+
* records against the source's file digest now. An AudioScript has no file, so a recording
|
|
5174
|
+
* derived from a script directly is not judged.
|
|
5175
|
+
*/
|
|
5176
|
+
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
5177
|
+
const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
|
|
5178
|
+
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
5179
|
+
const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
5180
|
+
return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
|
|
5181
|
+
}
|
|
5182
|
+
//#endregion
|
|
4735
5183
|
//#region src/dsl/voiceover-actions.ts
|
|
4736
5184
|
function voiceoverPlan(s, input, result) {
|
|
4737
5185
|
if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
|
|
@@ -4744,16 +5192,30 @@ function voiceoverPlan(s, input, result) {
|
|
|
4744
5192
|
id: result.mediaId,
|
|
4745
5193
|
value: input.media
|
|
4746
5194
|
}];
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
5195
|
+
let segmentIds;
|
|
5196
|
+
if (input.script.segments !== void 0) {
|
|
5197
|
+
if (result.segmentIds.length !== input.script.segments.length) throw new Error("Expected one id per segment");
|
|
5198
|
+
const segments = input.script.segments;
|
|
5199
|
+
segmentIds = result.segmentIds;
|
|
5200
|
+
mutations.push({
|
|
5201
|
+
type: "create-entity",
|
|
5202
|
+
table: "audio-script-entities",
|
|
5203
|
+
id: result.audioScriptId,
|
|
5204
|
+
value: {
|
|
5205
|
+
segments: segments.map((text, i) => ({
|
|
5206
|
+
id: segmentIds[i],
|
|
5207
|
+
text
|
|
5208
|
+
})),
|
|
5209
|
+
..."defaultLanguage" in input.script && input.script.defaultLanguage !== void 0 ? { defaultLanguage: input.script.defaultLanguage } : {}
|
|
5210
|
+
}
|
|
5211
|
+
});
|
|
5212
|
+
} else {
|
|
5213
|
+
const script = s["audio-script-entities"][result.audioScriptId];
|
|
5214
|
+
if (!script) throw new Error("Missing AudioScript");
|
|
5215
|
+
segmentIds = script.segments.map((segment) => segment.id);
|
|
5216
|
+
}
|
|
5217
|
+
const phoneticScriptId = input.script.phoneticScriptId;
|
|
5218
|
+
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");
|
|
4757
5219
|
validateWindow({
|
|
4758
5220
|
inMs: 0,
|
|
4759
5221
|
outMs: input.media.durationMs
|
|
@@ -4763,7 +5225,8 @@ function voiceoverPlan(s, input, result) {
|
|
|
4763
5225
|
table: "derived-from-relations",
|
|
4764
5226
|
value: {
|
|
4765
5227
|
entityId: result.mediaId,
|
|
4766
|
-
sourceId: result.audioScriptId
|
|
5228
|
+
sourceId: phoneticScriptId ?? result.audioScriptId,
|
|
5229
|
+
...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
|
|
4767
5230
|
}
|
|
4768
5231
|
});
|
|
4769
5232
|
if (input.voice) {
|
|
@@ -4793,6 +5256,7 @@ function voiceoverPlan(s, input, result) {
|
|
|
4793
5256
|
rate: void 0,
|
|
4794
5257
|
fillTimeline: void 0
|
|
4795
5258
|
},
|
|
5259
|
+
source: result.mediaId,
|
|
4796
5260
|
...input.volume === void 0 ? {} : { volume: input.volume }
|
|
4797
5261
|
}
|
|
4798
5262
|
} : {
|
|
@@ -4804,15 +5268,9 @@ function voiceoverPlan(s, input, result) {
|
|
|
4804
5268
|
inMs: 0,
|
|
4805
5269
|
outMs: input.media.durationMs
|
|
4806
5270
|
},
|
|
5271
|
+
source: result.mediaId,
|
|
4807
5272
|
...input.volume === void 0 ? {} : { volume: input.volume }
|
|
4808
5273
|
}
|
|
4809
|
-
}, {
|
|
4810
|
-
type: "set-relation",
|
|
4811
|
-
table: "clip-source-relations",
|
|
4812
|
-
value: {
|
|
4813
|
-
clipId: result.clipId,
|
|
4814
|
-
sourceId: result.mediaId
|
|
4815
|
-
}
|
|
4816
5274
|
});
|
|
4817
5275
|
if (!existing || trackOf(s, result.clipId) !== input.trackId) mutations.push({
|
|
4818
5276
|
type: existing ? "move-node" : "insert-node",
|
|
@@ -4822,13 +5280,14 @@ function voiceoverPlan(s, input, result) {
|
|
|
4822
5280
|
mutations.push(...placementMutations(s, result.clipId, input.placement));
|
|
4823
5281
|
uniqueIds(result.captionClipIds);
|
|
4824
5282
|
const retained = new Set(result.captionClipIds);
|
|
4825
|
-
const oldCaptions =
|
|
5283
|
+
const oldCaptions = existing ? captionsShowing(s, existing.source) : [];
|
|
4826
5284
|
mutations.push(...deleteClips(s, oldCaptions.filter((id) => !retained.has(id)), "cascade"));
|
|
4827
|
-
const
|
|
5285
|
+
const placement = mutations.find((m) => m.type === "patch-entity" && m.table === "clip-entities" && m.id === result.clipId && "anchor" in m.patch);
|
|
5286
|
+
const anchor = placement?.type === "patch-entity" && placement.table === "clip-entities" ? placement.patch.anchor : void 0;
|
|
4828
5287
|
const trackClips = resolveDslLayout(s).tracks.find((track) => track.trackId === input.trackId).clips;
|
|
4829
5288
|
const slot = trackClips.findIndex((clip) => clip.clipId === result.clipId);
|
|
4830
5289
|
const predecessor = trackClips.slice(0, slot < 0 ? trackClips.length : slot).findLast((clip) => clip.mode === "sequential" && !clip.fillTimeline);
|
|
4831
|
-
const speechStart = anchor?.
|
|
5290
|
+
const speechStart = anchor?.fallback_abs_ms !== void 0 ? anchor.fallback_abs_ms : predecessor ? predecessor.startMs + predecessor.durationMs : 0;
|
|
4832
5291
|
input.captions.forEach((caption, i) => {
|
|
4833
5292
|
const clipId = result.captionClipIds[i], captionId = result.captionIds[i];
|
|
4834
5293
|
validateWindow({
|
|
@@ -4837,7 +5296,9 @@ function voiceoverPlan(s, input, result) {
|
|
|
4837
5296
|
}, input.media.durationMs);
|
|
4838
5297
|
const previous = s["clip-entities"][clipId];
|
|
4839
5298
|
if (previous && !oldCaptions.includes(clipId)) throw new Error("Cannot overwrite another use’s Caption Clip");
|
|
4840
|
-
const
|
|
5299
|
+
const segmentId = segmentIds[caption.segment];
|
|
5300
|
+
if (segmentId === void 0) throw new Error(`Missing segment ${caption.segment} of the voiceover's script`);
|
|
5301
|
+
const previousSource = previous?.source;
|
|
4841
5302
|
const style = caption.style ?? (previousSource ? s["caption-entities"][previousSource]?.style : void 0);
|
|
4842
5303
|
mutations.push({
|
|
4843
5304
|
type: "create-entity",
|
|
@@ -4848,48 +5309,38 @@ function voiceoverPlan(s, input, result) {
|
|
|
4848
5309
|
type: "set-caption-content",
|
|
4849
5310
|
captionId,
|
|
4850
5311
|
audioScriptId: result.audioScriptId,
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
table: "caption-alignment-relations",
|
|
4856
|
-
value: {
|
|
4857
|
-
captionId,
|
|
4858
|
-
mediaId: result.mediaId,
|
|
4859
|
-
startMs: caption.startMs,
|
|
4860
|
-
endMs: caption.startMs + caption.durationMs
|
|
5312
|
+
segmentId,
|
|
5313
|
+
...caption.range === void 0 ? {} : {
|
|
5314
|
+
start: caption.range.start,
|
|
5315
|
+
end: caption.range.end
|
|
4861
5316
|
}
|
|
4862
|
-
}
|
|
5317
|
+
});
|
|
5318
|
+
const anchor = {
|
|
5319
|
+
to: result.clipId,
|
|
5320
|
+
offsetMs: caption.startMs,
|
|
5321
|
+
fallback_abs_ms: speechStart + caption.startMs
|
|
5322
|
+
};
|
|
5323
|
+
const seqMarker = {
|
|
5324
|
+
inMs: 0,
|
|
5325
|
+
outMs: caption.durationMs
|
|
5326
|
+
};
|
|
5327
|
+
mutations.push(previous ? {
|
|
4863
5328
|
type: "patch-entity",
|
|
4864
5329
|
table: "clip-entities",
|
|
4865
5330
|
id: clipId,
|
|
4866
|
-
patch: {
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
5331
|
+
patch: {
|
|
5332
|
+
seqMarker,
|
|
5333
|
+
source: captionId,
|
|
5334
|
+
anchor
|
|
5335
|
+
}
|
|
4870
5336
|
} : {
|
|
4871
5337
|
type: "create-entity",
|
|
4872
5338
|
table: "clip-entities",
|
|
4873
5339
|
id: clipId,
|
|
4874
|
-
value: { seqMarker: {
|
|
4875
|
-
inMs: 0,
|
|
4876
|
-
outMs: caption.durationMs
|
|
4877
|
-
} }
|
|
4878
|
-
}, {
|
|
4879
|
-
type: "set-relation",
|
|
4880
|
-
table: "clip-source-relations",
|
|
4881
|
-
value: {
|
|
4882
|
-
clipId,
|
|
4883
|
-
sourceId: captionId
|
|
4884
|
-
}
|
|
4885
|
-
}, {
|
|
4886
|
-
type: "set-relation",
|
|
4887
|
-
table: "time-anchor-relations",
|
|
4888
5340
|
value: {
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
fallback_abs_ms: speechStart + caption.startMs
|
|
5341
|
+
seqMarker,
|
|
5342
|
+
source: captionId,
|
|
5343
|
+
anchor
|
|
4893
5344
|
}
|
|
4894
5345
|
});
|
|
4895
5346
|
if (!previous || trackOf(s, clipId) !== input.captionTrackId) mutations.push({
|
|
@@ -4900,15 +5351,44 @@ function voiceoverPlan(s, input, result) {
|
|
|
4900
5351
|
});
|
|
4901
5352
|
return mutations;
|
|
4902
5353
|
}
|
|
5354
|
+
/**
|
|
5355
|
+
* Caption Clips showing what a recording says (D183): their Caption selects the run of the segment
|
|
5356
|
+
* the PhoneticScript it was spoken from is written for, or any segment of the script it was read
|
|
5357
|
+
* from directly.
|
|
5358
|
+
*/
|
|
5359
|
+
function captionsShowing(s, recordingId) {
|
|
5360
|
+
const said = spokenScripts(s, recordingId).map(({ audioScriptId, phoneticScriptId }) => {
|
|
5361
|
+
if (phoneticScriptId === void 0) return { audioScriptId };
|
|
5362
|
+
const { segmentId } = s["phonetic-script-entities"][phoneticScriptId].phoneme;
|
|
5363
|
+
return {
|
|
5364
|
+
audioScriptId,
|
|
5365
|
+
segmentId,
|
|
5366
|
+
run: s.phoneticText[phoneticScriptId]
|
|
5367
|
+
};
|
|
5368
|
+
});
|
|
5369
|
+
return Object.entries(s["clip-entities"]).flatMap(([id, clip]) => {
|
|
5370
|
+
const selection = s["caption-entities"][clip.source]?.selection;
|
|
5371
|
+
const shown = s.captionText[clip.source];
|
|
5372
|
+
if (!selection) return [];
|
|
5373
|
+
return said.some((words) => {
|
|
5374
|
+
if (selection.audioScriptId !== words.audioScriptId) return false;
|
|
5375
|
+
if (!("segmentId" in words)) return true;
|
|
5376
|
+
if (selection.segmentId !== words.segmentId || !shown || !words.run) return false;
|
|
5377
|
+
return shown.start < words.run.end && words.run.start < shown.end;
|
|
5378
|
+
}) && requireTrack(s, trackOf(s, id)).role === "caption" ? [id] : [];
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
4903
5381
|
//#endregion
|
|
4904
5382
|
//#region src/dsl/dsl-editor.ts
|
|
4905
5383
|
/** Business edits write DSL facts in one audited transaction; layout is derived on read. */
|
|
4906
5384
|
var DslEditor = class {
|
|
4907
5385
|
dsl;
|
|
4908
5386
|
idFactory;
|
|
4909
|
-
|
|
5387
|
+
uploadText;
|
|
5388
|
+
constructor(dsl, idFactory = defaultIdFactory, uploadText) {
|
|
4910
5389
|
this.dsl = dsl;
|
|
4911
5390
|
this.idFactory = idFactory;
|
|
5391
|
+
this.uploadText = uploadText;
|
|
4912
5392
|
}
|
|
4913
5393
|
createTrack(role, options) {
|
|
4914
5394
|
const id = this.idFactory("track");
|
|
@@ -4950,11 +5430,10 @@ var DslEditor = class {
|
|
|
4950
5430
|
/** index is measured in the target's children after removing the moved Clip. */
|
|
4951
5431
|
moveClip(clipId, trackId, index, options) {
|
|
4952
5432
|
const snapshot = this.dsl.getSnapshot();
|
|
4953
|
-
requireClip(snapshot, clipId);
|
|
5433
|
+
const clip = requireClip(snapshot, clipId);
|
|
4954
5434
|
const track = snapshot["track-entities"][trackId];
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
requireSource(snapshot, source.sourceId, track.role);
|
|
5435
|
+
if (!track) throw new Error("Missing target Track or Clip source");
|
|
5436
|
+
requireSource(snapshot, clip.source, track.role);
|
|
4958
5437
|
this.write("moveClip", {
|
|
4959
5438
|
clipId,
|
|
4960
5439
|
trackId,
|
|
@@ -4969,8 +5448,7 @@ var DslEditor = class {
|
|
|
4969
5448
|
trimClip(clipId, window, options) {
|
|
4970
5449
|
const snapshot = this.dsl.getSnapshot(), clip = requireClip(snapshot, clipId);
|
|
4971
5450
|
if (clip.seqMarker.fillTimeline) throw new Error("BGM trim is not enabled");
|
|
4972
|
-
const
|
|
4973
|
-
const source = sourceId ? snapshot["media-entities"][sourceId] ?? snapshot["caption-entities"][sourceId] : void 0;
|
|
5451
|
+
const source = snapshot["media-entities"][clip.source] ?? snapshot["caption-entities"][clip.source];
|
|
4974
5452
|
if (!source) throw new Error("Cannot trim a Clip with a missing source");
|
|
4975
5453
|
const patch = {};
|
|
4976
5454
|
if (window.inMs !== void 0) patch.inMs = window.inMs;
|
|
@@ -5013,30 +5491,76 @@ var DslEditor = class {
|
|
|
5013
5491
|
patch: { volume }
|
|
5014
5492
|
}], options);
|
|
5015
5493
|
}
|
|
5016
|
-
|
|
5494
|
+
/** A script of these segments, in order; each segment gets its own id. */
|
|
5495
|
+
createAudioScript(segments, defaultLanguage, options) {
|
|
5017
5496
|
const id = this.idFactory("audio-script");
|
|
5497
|
+
const value = {
|
|
5498
|
+
segments: segments.map((text) => ({
|
|
5499
|
+
id: this.idFactory("segment"),
|
|
5500
|
+
text
|
|
5501
|
+
})),
|
|
5502
|
+
...defaultLanguage === void 0 ? {} : { defaultLanguage }
|
|
5503
|
+
};
|
|
5018
5504
|
this.write("createAudioScript", {
|
|
5019
5505
|
id,
|
|
5020
|
-
|
|
5021
|
-
defaultLanguage
|
|
5506
|
+
...value
|
|
5022
5507
|
}, [{
|
|
5023
5508
|
type: "create-entity",
|
|
5024
5509
|
table: "audio-script-entities",
|
|
5025
5510
|
id,
|
|
5026
|
-
value
|
|
5027
|
-
body,
|
|
5028
|
-
...defaultLanguage === void 0 ? {} : { defaultLanguage }
|
|
5029
|
-
}
|
|
5511
|
+
value
|
|
5030
5512
|
}], options);
|
|
5031
5513
|
return id;
|
|
5032
5514
|
}
|
|
5033
|
-
editAudioScript(scriptId, expectedBodyId, edits, options) {
|
|
5034
|
-
this.dsl.editText(scriptId, expectedBodyId, edits, audit("editAudioScript", {
|
|
5515
|
+
editAudioScript(scriptId, segmentId, expectedBodyId, edits, options) {
|
|
5516
|
+
this.dsl.editText(scriptId, segmentId, expectedBodyId, edits, audit("editAudioScript", {
|
|
5035
5517
|
scriptId,
|
|
5518
|
+
segmentId,
|
|
5036
5519
|
expectedBodyId,
|
|
5037
5520
|
edits
|
|
5038
5521
|
}, options));
|
|
5039
5522
|
}
|
|
5523
|
+
/** Adds a segment at `index`, the end by default, and returns its id. */
|
|
5524
|
+
insertScriptSegment(scriptId, text, index, options) {
|
|
5525
|
+
const segmentId = this.idFactory("segment");
|
|
5526
|
+
this.write("insertScriptSegment", {
|
|
5527
|
+
scriptId,
|
|
5528
|
+
segmentId,
|
|
5529
|
+
text,
|
|
5530
|
+
index
|
|
5531
|
+
}, [{
|
|
5532
|
+
type: "insert-segment",
|
|
5533
|
+
audioScriptId: scriptId,
|
|
5534
|
+
segmentId,
|
|
5535
|
+
text,
|
|
5536
|
+
...index === void 0 ? {} : { index }
|
|
5537
|
+
}], options);
|
|
5538
|
+
return segmentId;
|
|
5539
|
+
}
|
|
5540
|
+
/** `index` is measured after removing the segment. */
|
|
5541
|
+
moveScriptSegment(scriptId, segmentId, index, options) {
|
|
5542
|
+
this.write("moveScriptSegment", {
|
|
5543
|
+
scriptId,
|
|
5544
|
+
segmentId,
|
|
5545
|
+
index
|
|
5546
|
+
}, [{
|
|
5547
|
+
type: "move-segment",
|
|
5548
|
+
audioScriptId: scriptId,
|
|
5549
|
+
segmentId,
|
|
5550
|
+
index
|
|
5551
|
+
}], options);
|
|
5552
|
+
}
|
|
5553
|
+
/** Captions and PhoneticScripts selecting the segment keep their selection, which then shows nothing (D72). */
|
|
5554
|
+
deleteScriptSegment(scriptId, segmentId, options) {
|
|
5555
|
+
this.write("deleteScriptSegment", {
|
|
5556
|
+
scriptId,
|
|
5557
|
+
segmentId
|
|
5558
|
+
}, [{
|
|
5559
|
+
type: "delete-segment",
|
|
5560
|
+
audioScriptId: scriptId,
|
|
5561
|
+
segmentId
|
|
5562
|
+
}], options);
|
|
5563
|
+
}
|
|
5040
5564
|
createCaption(input, options) {
|
|
5041
5565
|
const id = this.idFactory("caption");
|
|
5042
5566
|
this.write("createCaption", {
|
|
@@ -5053,18 +5577,16 @@ var DslEditor = class {
|
|
|
5053
5577
|
}, ...input.text !== void 0 ? [] : [{
|
|
5054
5578
|
type: "set-caption-content",
|
|
5055
5579
|
captionId: id,
|
|
5056
|
-
|
|
5057
|
-
start: input.start,
|
|
5058
|
-
end: input.end
|
|
5580
|
+
...segmentRange(input)
|
|
5059
5581
|
}]], options);
|
|
5060
5582
|
return id;
|
|
5061
5583
|
}
|
|
5062
|
-
|
|
5584
|
+
/** Show a segment of a script, or a run of it, in place of the Caption's own text. */
|
|
5585
|
+
setCaptionContent(captionId, selection, options) {
|
|
5586
|
+
const range = segmentRange(selection);
|
|
5063
5587
|
this.write("setCaptionContent", {
|
|
5064
5588
|
captionId,
|
|
5065
|
-
|
|
5066
|
-
start,
|
|
5067
|
-
end
|
|
5589
|
+
...range
|
|
5068
5590
|
}, [{
|
|
5069
5591
|
type: "patch-entity",
|
|
5070
5592
|
table: "caption-entities",
|
|
@@ -5073,9 +5595,7 @@ var DslEditor = class {
|
|
|
5073
5595
|
}, {
|
|
5074
5596
|
type: "set-caption-content",
|
|
5075
5597
|
captionId,
|
|
5076
|
-
|
|
5077
|
-
start,
|
|
5078
|
-
end
|
|
5598
|
+
...range
|
|
5079
5599
|
}], options);
|
|
5080
5600
|
}
|
|
5081
5601
|
setCaptionText(captionId, text, options) {
|
|
@@ -5083,9 +5603,10 @@ var DslEditor = class {
|
|
|
5083
5603
|
if (!caption) throw new Error("Missing Caption");
|
|
5084
5604
|
this.dsl.transact((tx) => {
|
|
5085
5605
|
tx.applyMutations([{
|
|
5086
|
-
type: "
|
|
5087
|
-
table: "caption-
|
|
5088
|
-
id:
|
|
5606
|
+
type: "patch-entity",
|
|
5607
|
+
table: "caption-entities",
|
|
5608
|
+
id: captionId,
|
|
5609
|
+
patch: { selection: void 0 }
|
|
5089
5610
|
}]);
|
|
5090
5611
|
if (caption.text === void 0) tx.draft["caption-entities"][captionId].text = text;
|
|
5091
5612
|
else if (caption.text !== text) tx.editCaptionText(captionId, [...caption.text.length ? [{
|
|
@@ -5199,7 +5720,8 @@ var DslEditor = class {
|
|
|
5199
5720
|
const result = {
|
|
5200
5721
|
clipId: input.clipId ?? this.idFactory("clip"),
|
|
5201
5722
|
mediaId: resourceId("media", input.media.system, input.media.key),
|
|
5202
|
-
audioScriptId:
|
|
5723
|
+
audioScriptId: this.voiceoverScriptId(input.script),
|
|
5724
|
+
segmentIds: input.script.segments?.map(() => this.idFactory("segment")) ?? [],
|
|
5203
5725
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
5204
5726
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
5205
5727
|
};
|
|
@@ -5209,6 +5731,68 @@ var DslEditor = class {
|
|
|
5209
5731
|
}, voiceoverPlan(this.dsl.getSnapshot(), input, result), options);
|
|
5210
5732
|
return result;
|
|
5211
5733
|
}
|
|
5734
|
+
voiceoverScriptId(script) {
|
|
5735
|
+
if (script.audioScriptId !== void 0) return script.audioScriptId;
|
|
5736
|
+
if (script.phoneticScriptId === void 0) return this.idFactory("audio-script");
|
|
5737
|
+
const phonetic = this.dsl.getSnapshot()["phonetic-script-entities"][script.phoneticScriptId];
|
|
5738
|
+
if (!phonetic) throw new Error("Missing PhoneticScript");
|
|
5739
|
+
return phonetic.phoneme.audioScriptId;
|
|
5740
|
+
}
|
|
5741
|
+
/**
|
|
5742
|
+
* Write a PhoneticScript: create one for a segment of a script, or a run of it, or, given
|
|
5743
|
+
* `phoneticScriptId`, replace its selection and what to synthesize together (D181). A
|
|
5744
|
+
* `phonemeScript` is stored as a Memota text file first, and the file's facts are written with
|
|
5745
|
+
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
5746
|
+
*/
|
|
5747
|
+
async setPhoneticScript(input, options) {
|
|
5748
|
+
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
5749
|
+
const range = segmentRange(input);
|
|
5750
|
+
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
5751
|
+
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
5752
|
+
const cleared = {
|
|
5753
|
+
system: void 0,
|
|
5754
|
+
key: void 0,
|
|
5755
|
+
storageKey: void 0,
|
|
5756
|
+
digest: void 0
|
|
5757
|
+
};
|
|
5758
|
+
const phonemeScript = input.phonemeScript === void 0 ? {} : { phonemeScript: input.phonemeScript };
|
|
5759
|
+
this.write("setPhoneticScript", {
|
|
5760
|
+
id,
|
|
5761
|
+
...range,
|
|
5762
|
+
...phonemeScript,
|
|
5763
|
+
...cleared,
|
|
5764
|
+
...file
|
|
5765
|
+
}, [exists ? {
|
|
5766
|
+
type: "patch-entity",
|
|
5767
|
+
table: "phonetic-script-entities",
|
|
5768
|
+
id,
|
|
5769
|
+
patch: {
|
|
5770
|
+
...cleared,
|
|
5771
|
+
...file
|
|
5772
|
+
}
|
|
5773
|
+
} : {
|
|
5774
|
+
type: "create-entity",
|
|
5775
|
+
table: "phonetic-script-entities",
|
|
5776
|
+
id,
|
|
5777
|
+
value: file
|
|
5778
|
+
}, {
|
|
5779
|
+
type: "set-phoneme",
|
|
5780
|
+
phoneticScriptId: id,
|
|
5781
|
+
...range,
|
|
5782
|
+
...phonemeScript
|
|
5783
|
+
}], options);
|
|
5784
|
+
return id;
|
|
5785
|
+
}
|
|
5786
|
+
/** The file holding `text`: the one the PhoneticScript keeps while its text is unchanged, else a new upload. */
|
|
5787
|
+
async fileFor(id, text) {
|
|
5788
|
+
const current = this.dsl.getSnapshot()["phonetic-script-entities"][id];
|
|
5789
|
+
if (current?.phoneme.phonemeScript === text) {
|
|
5790
|
+
const { phoneme: _, ...file } = current;
|
|
5791
|
+
return file;
|
|
5792
|
+
}
|
|
5793
|
+
if (!this.uploadText) throw new Error("This editor stores no files");
|
|
5794
|
+
return this.uploadText(text);
|
|
5795
|
+
}
|
|
5212
5796
|
setBgm(input, options) {
|
|
5213
5797
|
const s = this.dsl.getSnapshot();
|
|
5214
5798
|
if (requireTrack(s, input.trackId).role !== "bgm") throw new Error("Expected a BGM Track");
|
|
@@ -5227,37 +5811,28 @@ var DslEditor = class {
|
|
|
5227
5811
|
fillTimeline: true
|
|
5228
5812
|
};
|
|
5229
5813
|
const volume = input.volume === void 0 ? {} : { volume: input.volume };
|
|
5230
|
-
const mutations = [
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
...volume
|
|
5242
|
-
}
|
|
5243
|
-
} : {
|
|
5244
|
-
type: "create-entity",
|
|
5245
|
-
table: "clip-entities",
|
|
5246
|
-
id,
|
|
5247
|
-
value: {
|
|
5248
|
-
seqMarker,
|
|
5249
|
-
...volume
|
|
5250
|
-
}
|
|
5251
|
-
},
|
|
5252
|
-
{
|
|
5253
|
-
type: "set-relation",
|
|
5254
|
-
table: "clip-source-relations",
|
|
5255
|
-
value: {
|
|
5256
|
-
clipId: id,
|
|
5257
|
-
sourceId: source.id
|
|
5258
|
-
}
|
|
5814
|
+
const mutations = [...source.mutations, old ? {
|
|
5815
|
+
type: "patch-entity",
|
|
5816
|
+
table: "clip-entities",
|
|
5817
|
+
id,
|
|
5818
|
+
patch: {
|
|
5819
|
+
seqMarker: {
|
|
5820
|
+
...seqMarker,
|
|
5821
|
+
rate: void 0
|
|
5822
|
+
},
|
|
5823
|
+
source: source.id,
|
|
5824
|
+
...volume
|
|
5259
5825
|
}
|
|
5260
|
-
|
|
5826
|
+
} : {
|
|
5827
|
+
type: "create-entity",
|
|
5828
|
+
table: "clip-entities",
|
|
5829
|
+
id,
|
|
5830
|
+
value: {
|
|
5831
|
+
seqMarker,
|
|
5832
|
+
source: source.id,
|
|
5833
|
+
...volume
|
|
5834
|
+
}
|
|
5835
|
+
}];
|
|
5261
5836
|
if (!old) mutations.push({
|
|
5262
5837
|
type: "insert-node",
|
|
5263
5838
|
entityId: id,
|
|
@@ -5273,6 +5848,15 @@ var DslEditor = class {
|
|
|
5273
5848
|
this.dsl.applyMutations(mutations, audit(action, payload, options));
|
|
5274
5849
|
}
|
|
5275
5850
|
};
|
|
5851
|
+
/** A selection as a write states it: a run only when both its ends are given. */
|
|
5852
|
+
function segmentRange(input) {
|
|
5853
|
+
return {
|
|
5854
|
+
audioScriptId: input.audioScriptId,
|
|
5855
|
+
segmentId: input.segmentId,
|
|
5856
|
+
...input.start === void 0 ? {} : { start: input.start },
|
|
5857
|
+
...input.end === void 0 ? {} : { end: input.end }
|
|
5858
|
+
};
|
|
5859
|
+
}
|
|
5276
5860
|
function audit(action, payload, options) {
|
|
5277
5861
|
return {
|
|
5278
5862
|
semantic_op: action,
|
|
@@ -5485,25 +6069,18 @@ function toVideoDraft(snapshot) {
|
|
|
5485
6069
|
function requireWholeAudio(media, clip, fail) {
|
|
5486
6070
|
if (media.kind === "image" || clip.inMs !== 0 || clip.outMs !== media.durationMs || clip.rate !== 1) fail("VideoDraft audio has no trim or speed fields");
|
|
5487
6071
|
}
|
|
6072
|
+
/**
|
|
6073
|
+
* What a recording says: what the selection of the PhoneticScript it was spoken from holds now,
|
|
6074
|
+
* or, for one derived from a script directly, the whole script, one segment per line.
|
|
6075
|
+
*/
|
|
5488
6076
|
function spokenText(snapshot, recordingId) {
|
|
5489
|
-
const
|
|
5490
|
-
|
|
5491
|
-
const
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
return;
|
|
5497
|
-
}
|
|
5498
|
-
for (const r of relations) if (r.entityId === id) visit(r.sourceId);
|
|
5499
|
-
};
|
|
5500
|
-
visit(recordingId);
|
|
5501
|
-
if (!scripts.size) {
|
|
5502
|
-
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) scripts.add(r.entityId);
|
|
5503
|
-
}
|
|
5504
|
-
if (scripts.size > 1) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple scripts cannot be silently concatenated into audio_script");
|
|
5505
|
-
const id = [...scripts][0];
|
|
5506
|
-
return id ? snapshot.richText[id]?.plainText ?? "" : "";
|
|
6077
|
+
const spoken = spokenScripts(snapshot, recordingId);
|
|
6078
|
+
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");
|
|
6079
|
+
const [first] = spoken;
|
|
6080
|
+
if (!first) return "";
|
|
6081
|
+
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
6082
|
+
const script = snapshot["audio-script-entities"][first.audioScriptId];
|
|
6083
|
+
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
5507
6084
|
}
|
|
5508
6085
|
//#endregion
|
|
5509
6086
|
//#region src/dsl/projection/from-video-draft.ts
|
|
@@ -5645,16 +6222,16 @@ function prepare(input, options) {
|
|
|
5645
6222
|
let script = scriptByRecording.get(recordingId);
|
|
5646
6223
|
if (script && script.text !== speech.audio_script) fail(`part_library/${id}/audio_script`, "One recording cannot acquire conflicting source scripts during import");
|
|
5647
6224
|
if (!script) {
|
|
5648
|
-
script =
|
|
5649
|
-
id: claim(mint("audio-script")),
|
|
5650
|
-
text: speech.audio_script
|
|
5651
|
-
};
|
|
6225
|
+
script = importedScript(claim(mint("audio-script")), speech.audio_script, () => mint("segment"));
|
|
5652
6226
|
scriptByRecording.set(recordingId, script);
|
|
5653
6227
|
mutations.push({
|
|
5654
6228
|
type: "create-entity",
|
|
5655
6229
|
table: "audio-script-entities",
|
|
5656
6230
|
id: script.id,
|
|
5657
|
-
value: {
|
|
6231
|
+
value: { segments: script.segments.map(({ id, text }) => ({
|
|
6232
|
+
id,
|
|
6233
|
+
text
|
|
6234
|
+
})) }
|
|
5658
6235
|
}, {
|
|
5659
6236
|
type: "set-relation",
|
|
5660
6237
|
table: "derived-from-relations",
|
|
@@ -5702,12 +6279,18 @@ function prepare(input, options) {
|
|
|
5702
6279
|
const text = caption.text;
|
|
5703
6280
|
if (typeof text !== "string") fail(`part_library/${captionId}/text`, "Expected Caption text");
|
|
5704
6281
|
const start = text.length ? script.text.indexOf(text, cursor) : -1;
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
6282
|
+
const end = start + text.length;
|
|
6283
|
+
const segment = script.segments.find((s) => start >= s.start && end <= s.start + s.text.length);
|
|
6284
|
+
if (start >= 0 && segment) {
|
|
6285
|
+
cursor = end;
|
|
6286
|
+
selections.set(captionId, text === segment.text ? {
|
|
6287
|
+
audioScriptId: script.id,
|
|
6288
|
+
segmentId: segment.id
|
|
6289
|
+
} : {
|
|
5708
6290
|
audioScriptId: script.id,
|
|
5709
|
-
|
|
5710
|
-
|
|
6291
|
+
segmentId: segment.id,
|
|
6292
|
+
start: start - segment.start,
|
|
6293
|
+
end: end - segment.start
|
|
5711
6294
|
});
|
|
5712
6295
|
} else diagnostics.push({
|
|
5713
6296
|
code: "caption-text-unmatched",
|
|
@@ -5770,28 +6353,19 @@ function prepare(input, options) {
|
|
|
5770
6353
|
captionId: sourceId,
|
|
5771
6354
|
...selected
|
|
5772
6355
|
});
|
|
5773
|
-
const speechId = caption.speech_part_id, mediaId = speechId ? mediaByPart.get(speechId) : void 0;
|
|
5774
|
-
if (mediaId && caption.start_ms !== void 0) {
|
|
5775
|
-
const startMs = milliseconds(caption.start_ms, `part_library/${id}/start_ms`), endMs = startMs + (caption.initial_duration_ms ?? outMs);
|
|
5776
|
-
const media = imported.get(mediaId);
|
|
5777
|
-
if ("durationMs" in media && endMs > startMs && endMs <= media.durationMs) mutations.push({
|
|
5778
|
-
type: "set-relation",
|
|
5779
|
-
table: "caption-alignment-relations",
|
|
5780
|
-
value: {
|
|
5781
|
-
captionId: sourceId,
|
|
5782
|
-
mediaId,
|
|
5783
|
-
startMs,
|
|
5784
|
-
endMs
|
|
5785
|
-
}
|
|
5786
|
-
});
|
|
5787
|
-
else diagnostics.push({
|
|
5788
|
-
code: "invalid-caption-alignment",
|
|
5789
|
-
path: `part_library/${id}`,
|
|
5790
|
-
message: "Source interval is invalid; display window is retained without alignment"
|
|
5791
|
-
});
|
|
5792
|
-
}
|
|
5793
6356
|
}
|
|
5794
6357
|
if (outMs <= inMs || !sourceId) fail(`part_library/${id}`, "Expected a nonempty source window");
|
|
6358
|
+
let anchor;
|
|
6359
|
+
if (placement.role !== "video_clip") {
|
|
6360
|
+
const host = hosts.get(id);
|
|
6361
|
+
const speechId = part.caption?.speech_part_id;
|
|
6362
|
+
const offsetMs = host?.offset ?? (speechId ? milliseconds(part.caption?.start_ms ?? 0, `part_library/${id}/start_ms`) : placement.abs);
|
|
6363
|
+
anchor = {
|
|
6364
|
+
to: host?.to ?? speechId ?? timelineId,
|
|
6365
|
+
offsetMs,
|
|
6366
|
+
fallback_abs_ms: placement.abs
|
|
6367
|
+
};
|
|
6368
|
+
}
|
|
5795
6369
|
mutations.push({
|
|
5796
6370
|
type: "create-entity",
|
|
5797
6371
|
table: "clip-entities",
|
|
@@ -5803,36 +6377,15 @@ function prepare(input, options) {
|
|
|
5803
6377
|
...speed ? { rate: speed.rate } : {},
|
|
5804
6378
|
...part.bgm ? { fillTimeline: true } : {}
|
|
5805
6379
|
},
|
|
6380
|
+
source: sourceId,
|
|
6381
|
+
...anchor === void 0 ? {} : { anchor },
|
|
5806
6382
|
...volume === void 0 ? {} : { volume }
|
|
5807
6383
|
}
|
|
5808
|
-
}, {
|
|
5809
|
-
type: "set-relation",
|
|
5810
|
-
table: "clip-source-relations",
|
|
5811
|
-
value: {
|
|
5812
|
-
clipId: id,
|
|
5813
|
-
sourceId
|
|
5814
|
-
}
|
|
5815
6384
|
}, {
|
|
5816
6385
|
type: "insert-node",
|
|
5817
6386
|
entityId: id,
|
|
5818
6387
|
parentId: placement.trackId
|
|
5819
6388
|
});
|
|
5820
|
-
if (placement.role !== "video_clip") {
|
|
5821
|
-
const host = hosts.get(id);
|
|
5822
|
-
const speechId = part.caption?.speech_part_id;
|
|
5823
|
-
const to = host?.to ?? speechId ?? timelineId;
|
|
5824
|
-
const offsetMs = host?.offset ?? (speechId ? milliseconds(part.caption?.start_ms ?? 0, `part_library/${id}/start_ms`) : placement.abs);
|
|
5825
|
-
mutations.push({
|
|
5826
|
-
type: "set-relation",
|
|
5827
|
-
table: "time-anchor-relations",
|
|
5828
|
-
value: {
|
|
5829
|
-
from: id,
|
|
5830
|
-
to,
|
|
5831
|
-
offsetMs,
|
|
5832
|
-
fallback_abs_ms: placement.abs
|
|
5833
|
-
}
|
|
5834
|
-
});
|
|
5835
|
-
}
|
|
5836
6389
|
}
|
|
5837
6390
|
return {
|
|
5838
6391
|
timelineId,
|
|
@@ -5848,5 +6401,22 @@ function milliseconds(value, path, positive = false, signed = false) {
|
|
|
5848
6401
|
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");
|
|
5849
6402
|
return value;
|
|
5850
6403
|
}
|
|
6404
|
+
function importedScript(id, text, mintSegment) {
|
|
6405
|
+
const segments = [];
|
|
6406
|
+
let start = 0;
|
|
6407
|
+
if (text.length) for (const line of text.split("\n")) {
|
|
6408
|
+
segments.push({
|
|
6409
|
+
id: mintSegment(),
|
|
6410
|
+
text: line,
|
|
6411
|
+
start
|
|
6412
|
+
});
|
|
6413
|
+
start += line.length + 1;
|
|
6414
|
+
}
|
|
6415
|
+
return {
|
|
6416
|
+
id,
|
|
6417
|
+
text,
|
|
6418
|
+
segments
|
|
6419
|
+
};
|
|
6420
|
+
}
|
|
5851
6421
|
//#endregion
|
|
5852
|
-
export {
|
|
6422
|
+
export { toAudioScriptViews as a, DslProjectionError as c, DslEditor as i, toVideoDraft as n, MedeoDsl as o, createInitialMedeoDsl as r, resolveDslLayout as s, fromVideoDraft as t };
|