@mengine/medeo-client 2.1.1-dsl.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,333 @@
1
- import { $ as assertId, A as hardenCidDescriptors, B as safeStringify, C as applySchemaToInferOptions, D as deepEqual, E as decodeNestedJsonValues, F as isObject, G as valuesEqual, H as stripUndefined, I as isObjectLike, J as getChildSchema, K as CID_KEY, L as isStateAndSchemaOfType, M as inferContainerTypeFromValue, N as insertChildToMap, O as defineCidProperty, P as isArrayLike, Q as assertEntityPatch, R as isTreeID, S as applyEncode, T as containerIdToContainerType, U as tryInferContainerType, V as schemaToContainerType, W as tryUpdateToContainer, X as assertCaptionContent, Y as getMapFieldSchema, Z as assertEntity, _ as isLoroTreeSchema, at as isRecord, b as validateSchema, c as isInvalidAtomic, ct as relationTables, d as getDefaultValue, dt as assertResourceIdentity, et as assertPhoneme, f as isContainerSchema, ft as defaultIdFactory, g as isLoroMovableListSchema, h as isLoroMapSchema, it as entityTables, j as hasTransform, k as getRootContainerByType, lt as validLanguage, m as isLoroListSchema, mt as resourceId, n as textStyles, nt as assertTimeAnchor, o as medeoDslLoroShape, ot as mergeFields, p as isListLikeSchema, pt as relationId, q as getChildContainerSchema, rt as atomicFields, s as captionContentCodec, st as patchRow, t as DEFAULT_UNIT_TIME_MS, tt as assertRelation, u as schema, ut as assertRelationIdentity, v as isRootSchemaType, w as cidsEqual, x as applyDecode, y as schemaContainsIgnoreField, z as isValueOfContainerType } from "./video-draft-types-Ce8NDenQ.js";
1
+ import { sha256 } from "@noble/hashes/sha2.js";
2
2
  import { Cursor, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, isContainer, isContainerId } from "loro-crdt";
3
- import { Immer, setAutoFreeze } from "immer";
3
+ //#region ../medeo-dsl/src/ids.ts
4
+ function bytesToBase64url(bytes) {
5
+ let binary = "";
6
+ for (const byte of bytes) binary += String.fromCharCode(byte);
7
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
8
+ }
9
+ function base64urlToBytes(value) {
10
+ if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) throw new Error("Invalid base64url");
11
+ const bytes = Uint8Array.from(atob(value.replaceAll("-", "+").replaceAll("_", "/")), (c) => c.charCodeAt(0));
12
+ if (bytesToBase64url(bytes) !== value) throw new Error("Non-canonical base64url");
13
+ return bytes;
14
+ }
15
+ function tupleHash(tuple) {
16
+ return bytesToBase64url(sha256(new TextEncoder().encode(JSON.stringify(tuple))));
17
+ }
18
+ function resourceId(kind, system, key) {
19
+ return `${kind}:${tupleHash([system, key])}`;
20
+ }
21
+ function relationId(table, value) {
22
+ const v = value;
23
+ switch (table) {
24
+ case "voice-timbre-relations": return `voice-timbre:${tupleHash([v.recordingId, v.voiceId])}`;
25
+ case "derived-from-relations": return `derived-from:${tupleHash([v.entityId, v.sourceId])}`;
26
+ }
27
+ throw new Error(`Unknown relation table: ${table}`);
28
+ }
29
+ const defaultIdFactory = (kind) => `${kind}_${globalThis.crypto.randomUUID()}`;
30
+ const resourceKinds = {
31
+ "media-entities": "media",
32
+ "voice-entities": "voice"
33
+ };
34
+ /** A resource row's ID is derived from its locator, so one external resource is one entity. */
35
+ function assertResourceIdentity(table, id, value) {
36
+ const kind = resourceKinds[table];
37
+ if (kind && id !== resourceId(kind, value.system, value.key)) throw new Error("Resource identity does not match locator");
38
+ }
39
+ /** A relation row's ID is derived from its two ends. */
40
+ function assertRelationIdentity(table, id, value) {
41
+ if (id !== relationId(table, value)) throw new Error("Relation ID does not match endpoints");
42
+ }
43
+ //#endregion
44
+ //#region ../medeo-dsl/src/fields.ts
45
+ const string = (v) => typeof v === "string";
46
+ const nonempty = (v) => string(v) && v.length > 0;
47
+ const finite = (v) => typeof v === "number" && Number.isFinite(v);
48
+ const fraction = (v) => finite(v) && v >= 0 && v <= 1;
49
+ const integer = (v) => typeof v === "number" && Number.isSafeInteger(v);
50
+ const nonnegative = (v) => integer(v) && v >= 0;
51
+ const positive = (v) => integer(v) && v > 0;
52
+ const oneOf = (...values) => (v) => values.includes(v);
53
+ const required = (check) => ({ check });
54
+ const optional$1 = (check) => ({
55
+ check,
56
+ optional: true
57
+ });
58
+ const reservedIds = [
59
+ "$cid",
60
+ "__proto__",
61
+ "constructor",
62
+ "prototype"
63
+ ];
64
+ const id = (v) => nonempty(v) && !reservedIds.includes(v);
65
+ const cursor = (v) => v instanceof Cursor;
66
+ function isRecord(value) {
67
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Cursor);
68
+ }
69
+ function assertId(value) {
70
+ if (!id(value)) throw new Error("Expected a nonempty, non-reserved ID");
71
+ }
72
+ function validLanguage(value) {
73
+ if (typeof value !== "string") return false;
74
+ if (/^x(?:-[a-z0-9]{1,8})+$/i.test(value)) return true;
75
+ try {
76
+ return value.length > 0 && Intl.getCanonicalLocales(value).length === 1;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ function object(fields) {
82
+ return (v) => {
83
+ try {
84
+ checkFields(v, fields);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
89
+ };
90
+ }
91
+ function checkFields(value, fields, partial = false) {
92
+ if (!isRecord(value)) throw new Error("Expected an object");
93
+ for (const key of Object.keys(value)) if (!Object.hasOwn(fields, key)) throw new Error(`Unknown field: ${key}`);
94
+ for (const [key, field] of Object.entries(fields)) {
95
+ if (partial && !Object.hasOwn(value, key)) continue;
96
+ const v = value[key];
97
+ if (v === void 0 && field.optional) continue;
98
+ if (!field.check(v)) throw new Error(`Invalid or missing field: ${key}`);
99
+ }
100
+ }
101
+ const styleFields = {
102
+ font: optional$1(object({
103
+ system: required(oneOf("font-library")),
104
+ key: required(nonempty)
105
+ })),
106
+ fontSize: optional$1(finite),
107
+ fontColor: optional$1(string),
108
+ fontWeight: optional$1(finite),
109
+ entranceAnimation: optional$1(string),
110
+ entranceAnimationDurationMs: optional$1(nonnegative),
111
+ strokeColor: optional$1(string),
112
+ strokeWidth: optional$1(finite),
113
+ positionX: optional$1(fraction),
114
+ positionY: optional$1(fraction)
115
+ };
116
+ const seqMarkerFields = {
117
+ inMs: required(nonnegative),
118
+ outMs: required(nonnegative),
119
+ rate: optional$1((v) => finite(v) && v > 0),
120
+ fillTimeline: optional$1(oneOf(true))
121
+ };
122
+ /** A sequence marker's own fields; the window being nonempty is checked where it is written. */
123
+ function assertSeqMarker(value) {
124
+ checkFields(value, seqMarkerFields);
125
+ }
126
+ const timeAnchorFields = {
127
+ to: required(id),
128
+ offsetMs: required(integer),
129
+ fallback_abs_ms: required(nonnegative)
130
+ };
131
+ /** A time anchor's own fields; what it points at is checked by `assertReferenceAdmitted`. */
132
+ function assertTimeAnchor(value) {
133
+ checkFields(value, timeAnchorFields);
134
+ }
135
+ const selectionFields = {
136
+ audioScriptId: required(id),
137
+ segmentId: required(id),
138
+ start: optional$1(cursor),
139
+ end: optional$1(cursor)
140
+ };
141
+ /** A whole segment, or a run inside it: both ends or neither. */
142
+ const segmentSelection = (fields) => (v) => object(fields)(v) && v.start === void 0 === (v.end === void 0);
143
+ function assertCaptionContent(value) {
144
+ if (!segmentSelection(selectionFields)(value)) throw new Error("Invalid segment selection");
145
+ }
146
+ const phonemeFields = {
147
+ ...selectionFields,
148
+ phonemeScript: optional$1(string)
149
+ };
150
+ function assertPhoneme(value) {
151
+ if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
152
+ }
153
+ const segmentFields = {
154
+ id: required(id),
155
+ text: required(string),
156
+ startMs: optional$1(nonnegative),
157
+ endMs: optional$1(nonnegative)
158
+ };
159
+ /** A segment's time in the medium it was read off: both ends or neither, the end not before the start. */
160
+ const segment = (v) => {
161
+ if (!object(segmentFields)(v)) return false;
162
+ const { startMs, endMs } = v;
163
+ return startMs === void 0 ? endMs === void 0 : endMs !== void 0 && endMs >= startMs;
164
+ };
165
+ /** One segment as a script stores it; a segment added on its own is checked here, not with its row. */
166
+ function assertScriptSegment(value) {
167
+ if (!segment(value)) throw new Error("A segment is { id, text } with startMs and endMs both or neither, endMs not before startMs");
168
+ }
169
+ /** A script's segments: each with its own id, unique within the script. */
170
+ const segments = (v) => Array.isArray(v) && v.every(segment) && new Set(v.map((s) => s.id)).size === v.length;
171
+ const locator = {
172
+ system: required(oneOf("memota", "memota-speech")),
173
+ key: required(nonempty)
174
+ };
175
+ const entityFields = {
176
+ "timeline-entities": {},
177
+ "track-entities": {
178
+ role: required(oneOf("video_clip", "speech", "caption", "bgm")),
179
+ hidden: optional$1((v) => typeof v === "boolean")
180
+ },
181
+ "clip-entities": {
182
+ seqMarker: required(object(seqMarkerFields)),
183
+ volume: optional$1(finite),
184
+ source: required(id),
185
+ anchor: optional$1(object(timeAnchorFields))
186
+ },
187
+ "media-entities": {
188
+ ...locator,
189
+ kind: required(oneOf("video", "audio", "voiceover-recording", "image")),
190
+ storageKey: optional$1(nonempty),
191
+ durationMs: optional$1(positive),
192
+ digest: optional$1(nonempty)
193
+ },
194
+ "audio-script-entities": {
195
+ segments: required(segments),
196
+ defaultLanguage: optional$1(validLanguage),
197
+ system: optional$1(oneOf("memota")),
198
+ key: optional$1(nonempty),
199
+ storageKey: optional$1(nonempty),
200
+ digest: optional$1(nonempty)
201
+ },
202
+ "phonetic-script-entities": {
203
+ phoneme: optional$1(segmentSelection(phonemeFields)),
204
+ system: optional$1(oneOf("memota")),
205
+ key: optional$1(nonempty),
206
+ storageKey: optional$1(nonempty),
207
+ digest: optional$1(nonempty)
208
+ },
209
+ "caption-entities": {
210
+ selection: optional$1(segmentSelection(selectionFields)),
211
+ text: optional$1(string),
212
+ style: optional$1(object(styleFields))
213
+ },
214
+ "voice-entities": {
215
+ system: required(oneOf("voice-library")),
216
+ key: required(nonempty),
217
+ name: optional$1(string)
218
+ }
219
+ };
220
+ const entityTables = Object.keys(entityFields);
221
+ /**
222
+ * Fields stored as one encoded string: written whole, never merged field by field. A patch to one
223
+ * is merged into the current value first, then the whole value is written.
224
+ */
225
+ const atomicFields = {
226
+ "clip-entities": ["seqMarker", "anchor"],
227
+ "phonetic-script-entities": ["phoneme"],
228
+ "caption-entities": ["selection"]
229
+ };
230
+ const relationFields = {
231
+ "voice-timbre-relations": {
232
+ recordingId: required(id),
233
+ voiceId: required(id)
234
+ },
235
+ "derived-from-relations": {
236
+ entityId: required(id),
237
+ sourceId: required(id),
238
+ sourceDigest: optional$1(nonempty)
239
+ }
240
+ };
241
+ const relationTables = Object.keys(relationFields);
242
+ const fileFields = [
243
+ "system",
244
+ "key",
245
+ "storageKey",
246
+ "digest"
247
+ ];
248
+ const phonemeScriptOf = (row) => isRecord(row.phoneme) ? row.phoneme.phonemeScript : void 0;
249
+ /**
250
+ * A PhoneticScript as a write leaves it: written for a segment (D181), and its phonemeScript is raw
251
+ * content stored as its file (D178) — with text the row names that file and its digest, without
252
+ * text it has no file, and rewritten text is a new file.
253
+ */
254
+ function assertPhoneticScript(before, after) {
255
+ if (!isRecord(after.phoneme)) throw new Error("A PhoneticScript is written for a segment of its base script");
256
+ if (phonemeScriptOf(after) === void 0) {
257
+ if (fileFields.some((field) => after[field] !== void 0)) throw new Error("A PhoneticScript without text has no file");
258
+ } else if (after.system === void 0 || after.key === void 0 || after.digest === void 0) throw new Error("A PhoneticScript stores its phonemeScript as a file: name it by system and key with its digest");
259
+ if (before !== void 0 && phonemeScriptOf(after) !== phonemeScriptOf(before) && after.digest !== void 0 && after.digest === before.digest) throw new Error("A rewritten phonemeScript is stored again: write its new file");
260
+ }
261
+ /** A transcript names the file it was read from by system and key together; its facts come with that name. */
262
+ function assertTranscriptFile(row) {
263
+ if (row.system === void 0 !== (row.key === void 0)) throw new Error("A transcript names its transcription file by system and key");
264
+ if (row.key === void 0 && (row.storageKey !== void 0 || row.digest !== void 0)) throw new Error("A script without a transcription file has no file facts");
265
+ }
266
+ function assertEntity(table, value) {
267
+ checkFields(value, entityFields[table]);
268
+ if (table === "audio-script-entities") assertTranscriptFile(value);
269
+ if (table === "media-entities") {
270
+ if (value.kind === "image" && value.durationMs !== void 0) throw new Error("Image has no intrinsic duration");
271
+ if (value.kind !== "image" && !positive(value.durationMs)) throw new Error("Finite media requires durationMs");
272
+ if (["audio", "voiceover-recording"].includes(value.kind) && !nonempty(value.storageKey)) throw new Error("Audio requires storageKey");
273
+ }
274
+ }
275
+ /** Validate only touched fields, so an unrelated edit can proceed in a merged, invalid document. */
276
+ function assertEntityPatch(table, patch, current) {
277
+ if (!isRecord(patch)) throw new Error("Expected an entity patch");
278
+ if (table === "audio-script-entities" && Object.hasOwn(patch, "segments")) throw new Error("Change segments with segment mutations and edit their text through the Text adapter");
279
+ if (table === "caption-entities" && current.style !== void 0 && Object.hasOwn(patch, "style") && patch.style === void 0) throw new Error("Reset individual style fields; retain the style Map");
280
+ if (table === "caption-entities" && current.text !== void 0 && patch.text !== void 0) throw new Error("Edit Caption text through the Text adapter");
281
+ const next = patchRow(table, current, patch);
282
+ for (const key of Object.keys(patch)) {
283
+ const field = entityFields[table][key];
284
+ if (!field || !field.optional && next[key] === void 0 || next[key] !== void 0 && !field.check(next[key])) throw new Error(`Invalid patch field: ${key}`);
285
+ }
286
+ if (table === "audio-script-entities" && fileFields.some((field) => Object.hasOwn(patch, field))) {
287
+ if (current.key !== void 0 && (next.system !== current.system || next.key !== current.key)) throw new Error("A transcript's transcription file is fixed once named");
288
+ assertTranscriptFile(next);
289
+ }
290
+ if (table === "media-entities" || table === "voice-entities") {
291
+ for (const key of [
292
+ "system",
293
+ "key",
294
+ "kind"
295
+ ]) if (Object.hasOwn(patch, key) && patch[key] !== current[key]) throw new Error(`Resource identity field is immutable: ${key}`);
296
+ }
297
+ if (table === "media-entities") {
298
+ if (Object.hasOwn(patch, "durationMs")) {
299
+ if (next.kind === "image" ? next.durationMs !== void 0 : !positive(next.durationMs)) throw new Error("durationMs must match the media kind");
300
+ }
301
+ if (Object.hasOwn(patch, "storageKey") && ["audio", "voiceover-recording"].includes(next.kind) && !nonempty(next.storageKey)) throw new Error("Audio requires storageKey");
302
+ }
303
+ }
304
+ function assertRelation(table, value) {
305
+ checkFields(value, relationFields[table]);
306
+ }
307
+ /**
308
+ * The row after a patch. An atomic field merges into its current value, so a partial patch keeps
309
+ * the rest of it; an unreadable stored value is replaced instead, so writing a whole value repairs it.
310
+ */
311
+ function patchRow(table, current, patch) {
312
+ const next = mergeFields(current, patch);
313
+ for (const key of atomicFields[table] ?? []) {
314
+ const old = current[key];
315
+ const value = patch[key];
316
+ if (value !== void 0 && isRecord(old) && old.invalid === true) next[key] = isRecord(value) ? mergeFields({}, value) : value;
317
+ }
318
+ return next;
319
+ }
320
+ /** Undefined deletes; nested patches retain sibling fields and an existing empty style Map. */
321
+ function mergeFields(target, patch) {
322
+ const result = { ...target };
323
+ for (const [key, value] of Object.entries(patch)) {
324
+ if (reservedIds.includes(key)) throw new Error(`Reserved field: ${key}`);
325
+ if (value === void 0) delete result[key];
326
+ else result[key] = isRecord(value) ? mergeFields(isRecord(result[key]) ? result[key] : {}, value) : value;
327
+ }
328
+ return result;
329
+ }
330
+ //#endregion
4
331
  //#region ../medeo-dsl/src/text.ts
5
332
  function assertTextOffset(text, at) {
6
333
  if (!Number.isSafeInteger(at) || at < 0 || at > text.length) throw new Error("Invalid UTF-16 text offset");
@@ -104,42 +431,27 @@ function prepareTextEdits(text, edits) {
104
431
  }
105
432
  return edit;
106
433
  }),
107
- body: text
434
+ text
108
435
  };
109
436
  }
110
- /** An empty body has no segments; otherwise each line, including a trailing empty one, is a segment. */
111
- function lines(body) {
112
- if (body === "") return [];
113
- const result = [];
114
- let start = 0;
115
- for (const text of body.split("\n")) {
116
- result.push({
117
- text,
118
- start,
119
- end: start + text.length
120
- });
121
- start += text.length + 1;
122
- }
123
- return result;
124
- }
125
- /** The language a whole line carries: one shared mark on every unit, else none. */
126
- function lineLanguage(text, line) {
127
- if (line.start === line.end) return void 0;
128
- const first = text.langs[line.start];
129
- for (let i = line.start + 1; i < line.end; i++) if (text.langs[i] !== first) return void 0;
437
+ /** The language a whole segment carries: one shared mark on every unit, else none. */
438
+ function markedLanguage(text) {
439
+ if (!text.text.length) return void 0;
440
+ const first = text.langs[0];
441
+ for (let i = 1; i < text.text.length; i++) if (text.langs[i] !== first) return void 0;
130
442
  return first;
131
443
  }
132
444
  /** Read language marks off a rich-text delta. */
133
445
  function langsFromDelta(delta) {
134
- let body = "";
446
+ let text = "";
135
447
  const langs = [];
136
448
  for (const run of delta) {
137
- body += run.insert;
449
+ text += run.insert;
138
450
  const language = typeof run.attributes?.language === "string" ? run.attributes.language : void 0;
139
451
  for (let i = 0; i < run.insert.length; i++) langs.push(language);
140
452
  }
141
453
  return {
142
- body,
454
+ text,
143
455
  langs
144
456
  };
145
457
  }
@@ -178,16 +490,23 @@ const referenceFields = {
178
490
  "phonetic-script-entities": ["phoneme"],
179
491
  "caption-entities": ["selection"]
180
492
  };
493
+ /** The segment a selection names: refused when its script or the segment is missing. */
494
+ function requireSegment(rows, audioScriptId, segmentId) {
495
+ const script = requireEntity(rows, "audio-script-entities", audioScriptId);
496
+ const segment = Array.isArray(script.segments) ? script.segments.find((s) => isRecord(s) && s.id === segmentId) : void 0;
497
+ if (!segment) throw new Error(`Missing segment ${String(segmentId)} of ${String(audioScriptId)}`);
498
+ return segment;
499
+ }
181
500
  /**
182
501
  * Whether a single-valued relation, stored as a field of the entity that owns it, may hold this
183
502
  * value among these rows. The owner need not be stored yet; an absent value unlinks it.
184
- * A caption selection's range is checked against the live body by whoever holds it.
503
+ * A selection's run is checked against the segment's live text by whoever holds it.
185
504
  */
186
505
  function assertReferenceAdmitted(rows, table, id, field, value) {
187
506
  assertId(id);
188
507
  const key = `${table}.${field}`;
189
508
  if (value === void 0) {
190
- if (key === "clip-entities.source" || key === "phonetic-script-entities.phoneme") throw new Error(`Missing required relation: ${key}`);
509
+ if (key === "clip-entities.source") throw new Error(`Missing required relation: ${key}`);
191
510
  return;
192
511
  }
193
512
  switch (key) {
@@ -213,26 +532,49 @@ function assertReferenceAdmitted(rows, table, id, field, value) {
213
532
  break;
214
533
  }
215
534
  case "caption-entities.selection":
216
- assertCaptionContent(value);
217
- requireEntity(rows, "audio-script-entities", value.audioScriptId);
218
- break;
219
- case "phonetic-script-entities.phoneme":
220
- assertPhoneme(value);
221
- requireEntity(rows, "audio-script-entities", value.audioScriptId);
535
+ case "phonetic-script-entities.phoneme": {
536
+ if (key === "caption-entities.selection") assertCaptionContent(value);
537
+ else assertPhoneme(value);
538
+ const { audioScriptId, segmentId } = value;
539
+ requireSegment(rows, audioScriptId, segmentId);
222
540
  break;
541
+ }
223
542
  }
224
543
  }
225
- /** Both ends of a caption's selection exist: the caption and the script it selects from. */
544
+ /** Both ends of a caption's selection exist: the caption and the segment it selects. */
226
545
  function assertCaptionEndpoints(rows, selection) {
227
546
  requireEntity(rows, "caption-entities", selection.captionId);
228
- requireEntity(rows, "audio-script-entities", selection.audioScriptId);
547
+ requireSegment(rows, selection.audioScriptId, selection.segmentId);
548
+ }
549
+ /** Both ends of a phoneme exist: the PhoneticScript and the segment it is written for. */
550
+ function assertPhonemeEndpoints(rows, selection) {
551
+ requireEntity(rows, "phonetic-script-entities", selection.phoneticScriptId);
552
+ requireSegment(rows, selection.audioScriptId, selection.segmentId);
553
+ }
554
+ /**
555
+ * A selection of a segment's current text: the whole segment, or a run of it in UTF-16 offsets
556
+ * with both ends stated. What it selects is never empty.
557
+ */
558
+ function assertSegmentRange(range, text) {
559
+ if (range.start === void 0 !== (range.end === void 0)) throw new Error("A run inside a segment states both its start and its end");
560
+ if (range.start === void 0 || range.end === void 0) {
561
+ if (!text.length) throw new Error("An empty segment cannot be selected");
562
+ return;
563
+ }
564
+ assertTextOffset(text, range.start);
565
+ assertTextOffset(text, range.end);
566
+ if (range.start >= range.end) throw new Error("Expected a nonempty range in the current segment");
229
567
  }
230
- /** A caption selects a nonempty run of its script's current body, in UTF-16 offsets. */
231
- function assertCaptionSelection(rows, selection, body) {
568
+ /** A caption selects a segment of its script, or a nonempty run of it. */
569
+ function assertCaptionSelection(rows, selection, text) {
232
570
  assertCaptionEndpoints(rows, selection);
233
- assertTextOffset(body, selection.start);
234
- assertTextOffset(body, selection.end);
235
- if (selection.start >= selection.end) throw new Error("Expected a nonempty range in the current body");
571
+ assertSegmentRange(selection, text);
572
+ }
573
+ /** A PhoneticScript is written for a segment of its script, or a nonempty run of it, as a Caption selects one. */
574
+ function assertPhonemeSelection(rows, selection, text) {
575
+ assertPhonemeEndpoints(rows, selection);
576
+ assertSegmentRange(selection, text);
577
+ if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
236
578
  }
237
579
  /** A caption's text comes from a selection of a script or from its own text, never both. */
238
580
  function assertCaptionSource(caption, selected) {
@@ -858,35 +1200,1572 @@ function replaceSequencePlan(s, input, ids) {
858
1200
  };
859
1201
  }
860
1202
  }
861
- mutations.push(...deleteClips({
862
- ...s,
863
- "clip-entities": clips
864
- }, input.clipIds, "cascade"));
865
- return mutations;
1203
+ mutations.push(...deleteClips({
1204
+ ...s,
1205
+ "clip-entities": clips
1206
+ }, input.clipIds, "cascade"));
1207
+ return mutations;
1208
+ }
1209
+ function replaceSourcePlan(s, clipId, input) {
1210
+ if (requireClip(s, clipId).seqMarker.fillTimeline) throw new Error("Use setBgm to replace a fill Clip");
1211
+ const source = sourcePlan(s, input, trackOf(s, clipId));
1212
+ validateWindow(input, "durationMs" in source.source ? source.source.durationMs : void 0);
1213
+ return [...source.mutations, {
1214
+ type: "patch-entity",
1215
+ table: "clip-entities",
1216
+ id: clipId,
1217
+ patch: {
1218
+ seqMarker: {
1219
+ inMs: input.inMs,
1220
+ outMs: input.outMs
1221
+ },
1222
+ source: source.id
1223
+ }
1224
+ }];
1225
+ }
1226
+ //#endregion
1227
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/resolver.js
1228
+ function getMapFieldSchema(schema, key) {
1229
+ if (!schema) return void 0;
1230
+ if (Object.prototype.hasOwnProperty.call(schema.definition, key)) return schema.definition[key];
1231
+ if (schema.type === "loro-map" && "catchallType" in schema) return schema.catchallType;
1232
+ }
1233
+ function getChildSchema(schema, childKey) {
1234
+ if (!schema) return void 0;
1235
+ switch (schema.type) {
1236
+ case "schema":
1237
+ case "loro-map": return childKey === void 0 ? void 0 : getMapFieldSchema(schema, String(childKey));
1238
+ case "loro-list":
1239
+ case "loro-movable-list": return schema.itemSchema;
1240
+ case "loro-tree": return schema.nodeSchema;
1241
+ default: return;
1242
+ }
1243
+ }
1244
+ function getChildContainerSchema(schema, childKey) {
1245
+ const childSchema = getChildSchema(schema, childKey);
1246
+ if (!childSchema) return void 0;
1247
+ switch (childSchema.type) {
1248
+ case "loro-map":
1249
+ case "loro-list":
1250
+ case "loro-movable-list":
1251
+ case "loro-text":
1252
+ case "loro-tree": return childSchema;
1253
+ default: return;
1254
+ }
1255
+ }
1256
+ //#endregion
1257
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/constants.js
1258
+ const CID_KEY = "$cid";
1259
+ //#endregion
1260
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/container-inference.js
1261
+ function isPlainObjectValue(value) {
1262
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && typeof value !== "function";
1263
+ }
1264
+ function inferContainerTypeFromValue$1(value, defaults) {
1265
+ switch (getContainerValueKind(value)) {
1266
+ case "map": return "Map";
1267
+ case "array": return defaults?.defaultMovableList ? "MovableList" : "List";
1268
+ case "string": return defaults?.defaultLoroText ? "Text" : void 0;
1269
+ default: return;
1270
+ }
1271
+ }
1272
+ function inferSchemaContainerTypeFromValue(value, defaults) {
1273
+ switch (inferContainerTypeFromValue$1(value, defaults)) {
1274
+ case "Map": return "loro-map";
1275
+ case "List": return "loro-list";
1276
+ case "MovableList": return "loro-movable-list";
1277
+ case "Text": return "loro-text";
1278
+ default: return;
1279
+ }
1280
+ }
1281
+ function matchesContainerType(containerType, value) {
1282
+ const kind = getContainerValueKind(value);
1283
+ switch (containerType) {
1284
+ case "Map": return kind === "map";
1285
+ case "List":
1286
+ case "MovableList":
1287
+ case "Tree": return kind === "array";
1288
+ case "Text": return kind === "string";
1289
+ default: return false;
1290
+ }
1291
+ }
1292
+ function getContainerValueKind(value) {
1293
+ if (isPlainObjectValue(value)) return "map";
1294
+ if (Array.isArray(value)) return "array";
1295
+ if (typeof value === "string") return "string";
1296
+ return "other";
1297
+ }
1298
+ //#endregion
1299
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/utils.js
1300
+ /**
1301
+ * Utility functions for Loro Mirror core
1302
+ */
1303
+ /**
1304
+ * Check if a schema has a transform.
1305
+ * Distinguishes between the .transform() builder method (function) and an actual
1306
+ * TransformDefinition object (with decode/encode functions).
1307
+ */
1308
+ function hasTransform(schema) {
1309
+ if (schema === void 0) return false;
1310
+ const t = schema.transform;
1311
+ return t != null && typeof t === "object";
1312
+ }
1313
+ /**
1314
+ * Get the transform from a schema, or undefined if none exists.
1315
+ */
1316
+ function getTransform(schema) {
1317
+ if (hasTransform(schema)) return schema.transform;
1318
+ }
1319
+ /**
1320
+ * Get the effective equality strategy for a transform.
1321
+ * Default: "reference-equality"
1322
+ */
1323
+ function getEqualityStrategy(schema, strategyIfNotTransformable) {
1324
+ const transform = getTransform(schema);
1325
+ if (!transform) return strategyIfNotTransformable;
1326
+ return transform.isEqual ?? "reference-equality";
1327
+ }
1328
+ /**
1329
+ * Apply decode transform to a CRDT value.
1330
+ * Null/undefined pass through as-is - transform is only called for real values.
1331
+ */
1332
+ function applyDecode(schema, crdtValue) {
1333
+ if (crdtValue === null || crdtValue === void 0) return crdtValue;
1334
+ const transform = getTransform(schema);
1335
+ return transform ? transform.decode(crdtValue) : crdtValue;
1336
+ }
1337
+ /**
1338
+ * Recursively decode JSON values using schema transforms.
1339
+ * Mutates in-place. Used for snapshot initialization where toJSON() skips decode.
1340
+ */
1341
+ function decodeNestedJsonValues(json, schema) {
1342
+ if (json === null || json === void 0 || !schema) return json;
1343
+ switch (schema.type) {
1344
+ case "loro-map":
1345
+ if (!isObject(json)) return json;
1346
+ for (const key of Object.keys(json)) {
1347
+ const fieldSchema = getChildSchema(schema, key);
1348
+ json[key] = decodeNestedJsonValues(json[key], fieldSchema);
1349
+ }
1350
+ return json;
1351
+ case "loro-list":
1352
+ case "loro-movable-list": {
1353
+ if (!Array.isArray(json)) return json;
1354
+ const itemSchema = getChildSchema(schema);
1355
+ for (let i = 0; i < json.length; i++) json[i] = decodeNestedJsonValues(json[i], itemSchema);
1356
+ return json;
1357
+ }
1358
+ case "loro-tree": {
1359
+ if (!Array.isArray(json)) return json;
1360
+ const nodeSchema = getChildSchema(schema);
1361
+ const walk = (nodes) => {
1362
+ for (const node of nodes) if (node != null && typeof node == "object") {
1363
+ if ("data" in node && node.data !== void 0) node.data = decodeNestedJsonValues(node.data, nodeSchema);
1364
+ if ("children" in node && Array.isArray(node.children)) walk(node.children);
1365
+ }
1366
+ };
1367
+ walk(json);
1368
+ return json;
1369
+ }
1370
+ case "loro-text":
1371
+ case "ignore": return json;
1372
+ default: return applyDecode(schema, json);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Apply encode transform to a domain value.
1377
+ * Null/undefined pass through as-is - transform is only called for real values.
1378
+ */
1379
+ function applyEncode(schema, domainValue) {
1380
+ if (domainValue === null || domainValue === void 0) return domainValue;
1381
+ const transform = getTransform(schema);
1382
+ return transform ? transform.encode(domainValue) : domainValue;
1383
+ }
1384
+ /**
1385
+ * Check if two domain values are equal according to the schema's equality strategy.
1386
+ *
1387
+ * Reference equality is always checked first (performance optimization).
1388
+ * If refs match, returns true (no change).
1389
+ * If refs differ, behavior depends on isEqual setting.
1390
+ */
1391
+ function valuesEqual(schema, oldValue, newValue, strategyIfNotTransformable) {
1392
+ if (oldValue === newValue) return true;
1393
+ const strategy = getEqualityStrategy(schema, strategyIfNotTransformable);
1394
+ if (strategy === "reference-equality") return false;
1395
+ else if (strategy === "encoded-value-equality") return applyEncode(schema, oldValue) === applyEncode(schema, newValue);
1396
+ else if (strategy === "deep-equality") return deepEqual(oldValue, newValue);
1397
+ else return strategy(oldValue, newValue);
1398
+ }
1399
+ function defineCidProperty(target, cid) {
1400
+ if (!isObject(target) || Object.prototype.hasOwnProperty.call(target, "$cid")) return;
1401
+ Object.defineProperty(target, CID_KEY, { value: cid });
1402
+ }
1403
+ /**
1404
+ * Restore the read-only `$cid` contract on a freshly produced state.
1405
+ *
1406
+ * `defineCidProperty` stamps `$cid` as `{ writable: false, enumerable: false,
1407
+ * configurable: false }`. Immer's `useStrictShallowCopy` preserves the descriptor, but it
1408
+ * deliberately rewrites every non-writable data property to `{ writable: true,
1409
+ * configurable: true }` so the draft stays mutable — which would leave `$cid` writable on
1410
+ * every object Immer copied, letting callers reassign a published `$cid`.
1411
+ *
1412
+ * Two things keep this proportional to what Immer actually copied rather than to the size
1413
+ * of the state:
1414
+ *
1415
+ * - `prev` is the state `produce` was called with, and Immer shares everything it did not
1416
+ * copy, so a subtree reference-equal to `prev` cannot have been touched.
1417
+ * - An already-locked `$cid` means this object was not copied this round, so its children
1418
+ * are the same references they were when it was last hardened and are locked too. This
1419
+ * is what makes a reorder cheap: moved items are shared references that land at a new
1420
+ * index, so `prev` no longer lines up with them, but they are still locked.
1421
+ */
1422
+ function hardenCidDescriptors(next, prev, visited) {
1423
+ if (next === prev) return;
1424
+ if (Array.isArray(next)) {
1425
+ if ((visited ?? (visited = /* @__PURE__ */ new Set())).has(next)) return;
1426
+ visited.add(next);
1427
+ const prevArr = Array.isArray(prev) ? prev : void 0;
1428
+ for (let i = 0; i < next.length; i++) hardenCidDescriptors(next[i], prevArr?.[i], visited);
1429
+ return;
1430
+ }
1431
+ if (!isObject(next)) return;
1432
+ const descriptor = Object.getOwnPropertyDescriptor(next, CID_KEY);
1433
+ if (descriptor) {
1434
+ if (!descriptor.configurable) return;
1435
+ if ("value" in descriptor) Object.defineProperty(next, CID_KEY, {
1436
+ value: descriptor.value,
1437
+ writable: false,
1438
+ enumerable: false,
1439
+ configurable: false
1440
+ });
1441
+ }
1442
+ if ((visited ?? (visited = /* @__PURE__ */ new Set())).has(next)) return;
1443
+ visited.add(next);
1444
+ const prevObj = isObject(prev) ? prev : void 0;
1445
+ for (const key of Object.keys(next)) hardenCidDescriptors(next[key], prevObj?.[key], visited);
1446
+ }
1447
+ /**
1448
+ * Check if a value is an object
1449
+ */
1450
+ function isObject(value) {
1451
+ return isPlainObjectValue(value);
1452
+ }
1453
+ const UNSAFE_KEYS = new Set([
1454
+ "__proto__",
1455
+ "constructor",
1456
+ "prototype"
1457
+ ]);
1458
+ /**
1459
+ * Recursively removes undefined values from an object.
1460
+ * This treats undefined values as non-existent fields.
1461
+ * Preserves non-enumerable properties like $cid.
1462
+ * Returns the original object if no undefined values are found.
1463
+ * Protects against prototype pollution by skipping unsafe keys.
1464
+ */
1465
+ function stripUndefined(value) {
1466
+ return stripUndefinedInner(value, 0, void 0);
1467
+ }
1468
+ /**
1469
+ * Depth at which we start recording the recursion path to detect cycles.
1470
+ *
1471
+ * This runs over the whole state on every `setState`, and tracking every node costs more
1472
+ * than the stripping itself. Mirror state is a tree whose depth follows the schema, so a
1473
+ * real state never gets near this; only a cycle recurses without bound. Once the path is
1474
+ * being recorded, any cycle revisits one of its own nodes within a single lap, so starting
1475
+ * late still terminates.
1476
+ */
1477
+ const CYCLE_GUARD_DEPTH = 64;
1478
+ /**
1479
+ * Placeholder for a key that must not reach the rebuilt object: an unsafe key, or one whose
1480
+ * value is `undefined` and is therefore treated as a non-existent field.
1481
+ */
1482
+ const DROPPED = Symbol("dropped");
1483
+ function stripUndefinedInner(value, depth, path) {
1484
+ if (value === void 0) return value;
1485
+ if (Array.isArray(value)) {
1486
+ if (depth >= CYCLE_GUARD_DEPTH) {
1487
+ if ((path ?? (path = /* @__PURE__ */ new Set())).has(value)) return value;
1488
+ path.add(value);
1489
+ }
1490
+ let hasChanges = false;
1491
+ const result = value.map((item) => {
1492
+ const stripped = stripUndefinedInner(item, depth + 1, path);
1493
+ if (stripped !== item) hasChanges = true;
1494
+ return stripped;
1495
+ });
1496
+ path?.delete(value);
1497
+ return hasChanges ? result : value;
1498
+ }
1499
+ if (isObject(value)) {
1500
+ if (depth >= CYCLE_GUARD_DEPTH) {
1501
+ if ((path ?? (path = /* @__PURE__ */ new Set())).has(value)) return value;
1502
+ path.add(value);
1503
+ }
1504
+ const keys = Object.keys(value);
1505
+ const strippedValues = Array.from({ length: keys.length });
1506
+ let changed = false;
1507
+ for (let i = 0; i < keys.length; i++) {
1508
+ const key = keys[i];
1509
+ if (UNSAFE_KEYS.has(key)) {
1510
+ strippedValues[i] = DROPPED;
1511
+ continue;
1512
+ }
1513
+ const val = value[key];
1514
+ if (val === void 0) {
1515
+ strippedValues[i] = DROPPED;
1516
+ changed = true;
1517
+ continue;
1518
+ }
1519
+ const stripped = stripUndefinedInner(val, depth + 1, path);
1520
+ strippedValues[i] = stripped;
1521
+ if (stripped !== val) changed = true;
1522
+ }
1523
+ path?.delete(value);
1524
+ if (!changed) return value;
1525
+ const result = Object.create(null);
1526
+ const allProps = Object.getOwnPropertyNames(value);
1527
+ for (const key of allProps) {
1528
+ if (UNSAFE_KEYS.has(key)) continue;
1529
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1530
+ if (descriptor && !descriptor.enumerable) Object.defineProperty(result, key, descriptor);
1531
+ }
1532
+ for (let i = 0; i < keys.length; i++) {
1533
+ const val = strippedValues[i];
1534
+ if (val === DROPPED) continue;
1535
+ Object.defineProperty(result, keys[i], {
1536
+ value: val,
1537
+ writable: true,
1538
+ enumerable: true,
1539
+ configurable: true
1540
+ });
1541
+ }
1542
+ return result;
1543
+ }
1544
+ return value;
1545
+ }
1546
+ /**
1547
+ * Performs a deep equality check between two values
1548
+ */
1549
+ function deepEqual(a, b) {
1550
+ if (a === b) return true;
1551
+ if (a === null || b === null || typeof a !== "object" && typeof a !== "function" || typeof b !== "object" && typeof b !== "function") return false;
1552
+ if (Array.isArray(a) && Array.isArray(b)) {
1553
+ if (a.length !== b.length) return false;
1554
+ for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
1555
+ return true;
1556
+ }
1557
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
1558
+ if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();
1559
+ if (!Array.isArray(a) && !Array.isArray(b)) {
1560
+ const keysA = Object.keys(a);
1561
+ const keysB = Object.keys(b);
1562
+ if (keysA.length !== keysB.length) return false;
1563
+ for (const key of keysA) {
1564
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
1565
+ if (!deepEqual(a[key], b[key])) return false;
1566
+ }
1567
+ return true;
1568
+ }
1569
+ return false;
1570
+ }
1571
+ /**
1572
+ * Recursively compare the `$cid` markers of two state trees.
1573
+ *
1574
+ * `deepEqual` only walks enumerable keys, so it cannot see `$cid` (which is stamped as a
1575
+ * non-enumerable descriptor). Used by `Mirror.checkStateConsistency` so that a tampered
1576
+ * or stale `$cid` is reported as a divergence instead of silently sitting in the state.
1577
+ * Only the markers are compared here; value equality is `deepEqual`'s job.
1578
+ */
1579
+ function cidsEqual(a, b, seen) {
1580
+ if (a === b) return true;
1581
+ if (Array.isArray(a) && Array.isArray(b)) {
1582
+ if (a.length !== b.length) return false;
1583
+ if (markSeen(a, b, seen ?? (seen = /* @__PURE__ */ new Map())) === "seen") return true;
1584
+ for (let i = 0; i < a.length; i++) if (!cidsEqual(a[i], b[i], seen)) return false;
1585
+ return true;
1586
+ }
1587
+ if (!isObject(a) || !isObject(b)) return true;
1588
+ if (a["$cid"] !== b["$cid"]) return false;
1589
+ if (markSeen(a, b, seen ?? (seen = /* @__PURE__ */ new Map())) === "seen") return true;
1590
+ for (const key of Object.keys(a)) {
1591
+ if (!Object.prototype.hasOwnProperty.call(b, key)) continue;
1592
+ if (!cidsEqual(a[key], b[key], seen)) return false;
1593
+ }
1594
+ return true;
1595
+ }
1596
+ function markSeen(a, b, seen) {
1597
+ let partners = seen.get(a);
1598
+ if (!partners) {
1599
+ partners = /* @__PURE__ */ new Set();
1600
+ seen.set(a, partners);
1601
+ }
1602
+ if (partners.has(b)) return "seen";
1603
+ partners.add(b);
1604
+ return "new";
1605
+ }
1606
+ function containerIdToContainerType(containerId) {
1607
+ const parts = containerId.split(":");
1608
+ return parts[parts.length - 1];
1609
+ }
1610
+ function getRootContainerByType(doc, key, type) {
1611
+ if (type === "Text") return doc.getText(key);
1612
+ else if (type === "List") return doc.getList(key);
1613
+ else if (type === "MovableList") return doc.getMovableList(key);
1614
+ else if (type === "Map") return doc.getMap(key);
1615
+ else if (type === "Tree") return doc.getTree(key);
1616
+ else throw new Error();
1617
+ }
1618
+ function insertChildToMap(containerId, key, value, inferOptions) {
1619
+ const ct = tryInferContainerType(value, inferOptions);
1620
+ if (ct) return {
1621
+ container: containerId,
1622
+ key,
1623
+ value,
1624
+ kind: "insert-container",
1625
+ childContainerType: ct
1626
+ };
1627
+ return {
1628
+ container: containerId,
1629
+ key,
1630
+ value,
1631
+ kind: "insert"
1632
+ };
1633
+ }
1634
+ function tryUpdateToContainer(change, toUpdate, schema, inferOptions) {
1635
+ if (!toUpdate) return change;
1636
+ if (change.kind !== "insert" && change.kind !== "set") return change;
1637
+ const effectiveInferOptions = applySchemaToInferOptions(schema, inferOptions);
1638
+ const containerType = schema ? schemaToContainerType(schema) ?? tryInferContainerType(change.value, effectiveInferOptions) : tryInferContainerType(change.value, effectiveInferOptions);
1639
+ if (containerType == null || schema && hasTransform(schema)) {
1640
+ const encodedValue = applyEncode(schema, change.value);
1641
+ return encodedValue !== change.value ? {
1642
+ ...change,
1643
+ value: encodedValue
1644
+ } : change;
1645
+ }
1646
+ if (change.kind === "insert") return {
1647
+ container: change.container,
1648
+ key: change.key,
1649
+ value: change.value,
1650
+ kind: "insert-container",
1651
+ childContainerType: containerType
1652
+ };
1653
+ if (change.kind === "set") return {
1654
+ container: change.container,
1655
+ key: change.key,
1656
+ value: change.value,
1657
+ kind: "set-container",
1658
+ childContainerType: containerType
1659
+ };
1660
+ return change;
1661
+ }
1662
+ function schemaToContainerType(schema) {
1663
+ const containerType = schema.getContainerType();
1664
+ return containerType === null ? void 0 : containerType;
1665
+ }
1666
+ function tryInferContainerType(value, defaults) {
1667
+ return inferContainerTypeFromValue$1(value, defaults);
1668
+ }
1669
+ function applySchemaToInferOptions(schema, base) {
1670
+ if (!schema || schema.type !== "any") return base;
1671
+ const next = { ...base };
1672
+ next.defaultLoroText = schema.options.defaultLoroText ?? false;
1673
+ if (schema.options.defaultMovableList !== void 0) next.defaultMovableList = schema.options.defaultMovableList;
1674
+ return next;
1675
+ }
1676
+ function isValueOfContainerType(containerType, value) {
1677
+ return matchesContainerType(containerType, value);
1678
+ }
1679
+ function inferContainerTypeFromValue(value, defaults) {
1680
+ return inferSchemaContainerTypeFromValue(value, defaults);
1681
+ }
1682
+ function isObjectLike(value) {
1683
+ return typeof value === "object";
1684
+ }
1685
+ function isArrayLike(value) {
1686
+ return Array.isArray(value);
1687
+ }
1688
+ function isStateAndSchemaOfType(values, stateGuard, schemaGuard) {
1689
+ return stateGuard(values.oldState) && stateGuard(values.newState) && (!values.schema || schemaGuard(values.schema));
1690
+ }
1691
+ function isTreeID(id) {
1692
+ if (!(typeof id === "string")) return false;
1693
+ return !!id.match(/[0-9]+@[0-9]+/);
1694
+ }
1695
+ /**
1696
+ * Stringify a value safely, handling non-JSON-serializable types.
1697
+ * Handles BigInt, Date, RegExp, functions, and custom objects.
1698
+ */
1699
+ function safeStringify(value, indent = 2) {
1700
+ const seen = /* @__PURE__ */ new WeakSet();
1701
+ function replacer(val) {
1702
+ if (val === null || val === void 0) return val;
1703
+ if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") return val;
1704
+ if (typeof val === "bigint") return `[BigInt: ${val.toString()}]`;
1705
+ if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
1706
+ if (typeof val === "symbol") return `[Symbol: ${val.description || ""}]`;
1707
+ if (typeof val === "object") {
1708
+ if (seen.has(val)) return "[Circular]";
1709
+ seen.add(val);
1710
+ if (val instanceof Date) return `[Date: ${val.toISOString()}]`;
1711
+ if (val instanceof RegExp) return `[RegExp: ${val.toString()}]`;
1712
+ if (val instanceof Error) return `[Error: ${val.message}]`;
1713
+ if (Array.isArray(val)) return val.map(replacer);
1714
+ const result = {};
1715
+ for (const key of Object.keys(val)) result[key] = replacer(val[key]);
1716
+ return result;
1717
+ }
1718
+ return Object.prototype.toString.call(val);
1719
+ }
1720
+ return JSON.stringify(replacer(value), null, indent);
1721
+ }
1722
+ //#endregion
1723
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/validators.js
1724
+ const schemaValidationCache = /* @__PURE__ */ new WeakMap();
1725
+ function isCacheableValue(value) {
1726
+ return typeof value === "object" && value !== null;
1727
+ }
1728
+ function isSchemaValidated(schema, value) {
1729
+ if (!isCacheableValue(value)) return false;
1730
+ return schemaValidationCache.get(value)?.has(schema) ?? false;
1731
+ }
1732
+ function markSchemaValidated(schema, value) {
1733
+ if (!isCacheableValue(value)) return;
1734
+ let cache = schemaValidationCache.get(value);
1735
+ if (!cache) {
1736
+ cache = /* @__PURE__ */ new WeakSet();
1737
+ schemaValidationCache.set(value, cache);
1738
+ }
1739
+ cache.add(schema);
1740
+ }
1741
+ /**
1742
+ * Type guard for LoroMapSchema
1743
+ */
1744
+ function isLoroMapSchema(schema) {
1745
+ return !!schema && schema.type === "loro-map";
1746
+ }
1747
+ /**
1748
+ * Type guard for LoroListSchema
1749
+ */
1750
+ function isLoroListSchema(schema) {
1751
+ return !!schema && schema.type === "loro-list";
1752
+ }
1753
+ function isListLikeSchema(schema) {
1754
+ return isLoroListSchema(schema) || isLoroMovableListSchema(schema);
1755
+ }
1756
+ function isLoroMovableListSchema(schema) {
1757
+ return !!schema && schema.type === "loro-movable-list";
1758
+ }
1759
+ /**
1760
+ * Type guard for RootSchemaType
1761
+ */
1762
+ function isRootSchemaType(schema) {
1763
+ return !!schema && schema.type === "schema";
1764
+ }
1765
+ /**
1766
+ * Whether a schema subtree contains any `Ignore` field. Recursive schemas are
1767
+ * tolerated (each schema object is visited once).
1768
+ */
1769
+ function schemaContainsIgnoreField(schema) {
1770
+ const visited = /* @__PURE__ */ new Set();
1771
+ const walk = (s) => {
1772
+ if (!s || visited.has(s)) return false;
1773
+ visited.add(s);
1774
+ if (s.type === "ignore") return true;
1775
+ switch (s.type) {
1776
+ case "schema":
1777
+ case "loro-map": {
1778
+ const record = s;
1779
+ return Object.values(record.definition).some(walk) || walk(record.catchallType);
1780
+ }
1781
+ case "loro-list":
1782
+ case "loro-movable-list": return walk(s.itemSchema);
1783
+ case "loro-tree": return walk(s.nodeSchema);
1784
+ default: return false;
1785
+ }
1786
+ };
1787
+ return walk(schema);
1788
+ }
1789
+ /**
1790
+ * Type guard for LoroTreeSchema
1791
+ */
1792
+ function isLoroTreeSchema(schema) {
1793
+ return !!schema && schema.type === "loro-tree";
1794
+ }
1795
+ /**
1796
+ * Check if a schema is for a Loro container
1797
+ */
1798
+ function isContainerSchema(schema) {
1799
+ return !!schema && (schema.type === "loro-map" || schema.type === "loro-list" || schema.type === "loro-text" || schema.type === "loro-movable-list" || schema.type === "loro-tree");
1800
+ }
1801
+ /**
1802
+ * Validate a value against a schema
1803
+ *
1804
+ * When `options.ignoreUnknownProperties` is true, the root schema check does
1805
+ * not reject properties missing from the schema definition — typically
1806
+ * written by peers on a newer schema version. (Nested map schemas never
1807
+ * reject undeclared properties.)
1808
+ */
1809
+ function validateSchema(schema, value, options) {
1810
+ const errors = [];
1811
+ const actualType = schema.type;
1812
+ if (schema.options.required && (value === void 0 || value === null)) {
1813
+ errors.push("Value is required");
1814
+ return {
1815
+ valid: false,
1816
+ errors
1817
+ };
1818
+ }
1819
+ if (value === void 0 || value === null) return { valid: true };
1820
+ const useCache = !options?.ignoreUnknownProperties;
1821
+ if (useCache && isSchemaValidated(schema, value)) return { valid: true };
1822
+ switch (actualType) {
1823
+ case "any":
1824
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && value !== null && value !== void 0 && !Array.isArray(value) && !isObject(value)) errors.push("Value must be JSON-like");
1825
+ break;
1826
+ case "string":
1827
+ case "number":
1828
+ case "boolean":
1829
+ validateTransformablePrimitive(schema, value, actualType, errors);
1830
+ break;
1831
+ case "ignore": break;
1832
+ case "loro-text":
1833
+ if (typeof value !== "string") errors.push("Content must be a string");
1834
+ break;
1835
+ case "loro-map":
1836
+ if (!isObject(value)) errors.push("Value must be an object");
1837
+ else if (isLoroMapSchema(schema)) {
1838
+ for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1839
+ const propSchema = schema.definition[key];
1840
+ const propValue = value[key];
1841
+ const result = validateSchema(propSchema, propValue);
1842
+ if (!result.valid && result.errors) {
1843
+ const prefixedErrors = result.errors.map((err) => `${key}: ${err}`);
1844
+ errors.push(...prefixedErrors);
1845
+ }
1846
+ }
1847
+ }
1848
+ break;
1849
+ case "loro-movable-list":
1850
+ case "loro-list":
1851
+ if (!Array.isArray(value)) errors.push("Value must be an array");
1852
+ else if (isLoroListSchema(schema) || isLoroMovableListSchema(schema)) {
1853
+ const itemSchema = schema.itemSchema;
1854
+ value.forEach((item, index) => {
1855
+ const result = validateSchema(itemSchema, item);
1856
+ if (!result.valid && result.errors) {
1857
+ const prefixedErrors = result.errors.map((err) => `Item ${index}: ${err}`);
1858
+ errors.push(...prefixedErrors);
1859
+ }
1860
+ });
1861
+ }
1862
+ break;
1863
+ case "loro-tree": {
1864
+ if (!Array.isArray(value)) {
1865
+ errors.push("Value must be an array of tree nodes");
1866
+ break;
1867
+ }
1868
+ if (!isLoroTreeSchema(schema)) {
1869
+ errors.push("Invalid tree schema");
1870
+ break;
1871
+ }
1872
+ const validateNode = (node, path) => {
1873
+ if (!isObject(node)) {
1874
+ errors.push(`${path}: Node must be an object`);
1875
+ return;
1876
+ }
1877
+ const n = node;
1878
+ if (n.id != null && typeof n.id !== "string") errors.push(`${path}: id must be a string if provided`);
1879
+ const dataResult = validateSchema(schema.nodeSchema, n.data);
1880
+ if (!dataResult.valid && dataResult.errors) errors.push(...dataResult.errors.map((e) => `${path}.data: ${e}`));
1881
+ if (!Array.isArray(n.children)) errors.push(`${path}: children must be an array`);
1882
+ else n.children.forEach((child, idx) => {
1883
+ validateNode(child, `${path}.children[${idx}]`);
1884
+ });
1885
+ };
1886
+ value.forEach((node, i) => {
1887
+ validateNode(node, `node[${i}]`);
1888
+ });
1889
+ break;
1890
+ }
1891
+ case "schema":
1892
+ if (!isObject(value)) errors.push("Value must be an object");
1893
+ else if (isRootSchemaType(schema)) if (!isObject(value)) errors.push("Value must be an object");
1894
+ else {
1895
+ for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1896
+ const propSchema = schema.definition[key];
1897
+ const propValue = value[key];
1898
+ const result = validateSchema(propSchema, propValue);
1899
+ if (!result.valid && result.errors) {
1900
+ const prefixedErrors = result.errors.map((err) => `${key}: ${err}`);
1901
+ errors.push(...prefixedErrors);
1902
+ }
1903
+ }
1904
+ for (const key in value) if (!Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1905
+ if (options?.ignoreUnknownProperties) continue;
1906
+ errors.push(`Unknown property: ${key}`);
1907
+ }
1908
+ }
1909
+ else errors.push(`Should be a schema, but got ${actualType}`);
1910
+ break;
1911
+ default: errors.push(`Unknown schema type: ${actualType}`);
1912
+ }
1913
+ if (schema.options.validate && typeof schema.options.validate === "function") try {
1914
+ const customValidation = schema.options.validate(value);
1915
+ if (customValidation !== true) {
1916
+ const errorMessage = typeof customValidation === "string" ? customValidation : "Value failed custom validation";
1917
+ errors.push(errorMessage);
1918
+ }
1919
+ } catch (error) {
1920
+ errors.push(`Validation error: ${String(error)}`);
1921
+ }
1922
+ if (errors.length === 0) {
1923
+ if (useCache) markSchemaValidated(schema, value);
1924
+ return { valid: true };
1925
+ }
1926
+ return {
1927
+ valid: false,
1928
+ errors
1929
+ };
1930
+ }
1931
+ function validateTransformablePrimitive(schema, value, expectedType, errors) {
1932
+ const transform = getTransform(schema);
1933
+ if (transform) {
1934
+ if (transform.validate) try {
1935
+ const result = transform.validate(value);
1936
+ if (result !== true) errors.push(typeof result === "string" ? result : "Transform validation failed");
1937
+ } catch (error) {
1938
+ errors.push(`Transform validation error: ${String(error)}`);
1939
+ }
1940
+ if (transform.validateEncodedType) try {
1941
+ const encoded = applyEncode(schema, value);
1942
+ if (typeof encoded !== expectedType) errors.push(`Transform encode must return a ${expectedType}, got ${typeof encoded}`);
1943
+ } catch (error) {
1944
+ errors.push(`Transform encode validation error: ${String(error)}`);
1945
+ }
1946
+ } else if (typeof value !== expectedType) errors.push(`Value must be a ${expectedType}`);
1947
+ }
1948
+ /**
1949
+ * Get default value for a schema
1950
+ * Based on the schema type, it might return a plain value or a wrapped value
1951
+ */
1952
+ function getDefaultValue(schema) {
1953
+ if ("defaultValue" in schema.options) return schema.options.defaultValue;
1954
+ switch (schema.type) {
1955
+ case "any": return;
1956
+ case "string":
1957
+ if (schema.options.required === false) return;
1958
+ if (getTransform(schema)) return;
1959
+ return "";
1960
+ case "number":
1961
+ if (schema.options.required === false) return;
1962
+ if (getTransform(schema)) return;
1963
+ return 0;
1964
+ case "boolean":
1965
+ if (schema.options.required === false) return;
1966
+ if (getTransform(schema)) return;
1967
+ return false;
1968
+ case "loro-text": {
1969
+ const value = schema.options.required ? "" : void 0;
1970
+ if (value === void 0) return void 0;
1971
+ return value;
1972
+ }
1973
+ case "loro-map":
1974
+ if (isLoroMapSchema(schema)) {
1975
+ const result = {};
1976
+ for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1977
+ const value = getDefaultValue(schema.definition[key]);
1978
+ if (value !== void 0) result[key] = value;
1979
+ }
1980
+ return result;
1981
+ }
1982
+ return {};
1983
+ case "loro-list": return [];
1984
+ case "loro-tree": {
1985
+ const value = schema.options.required ? [] : void 0;
1986
+ if (value === void 0) return void 0;
1987
+ return value;
1988
+ }
1989
+ case "schema":
1990
+ if (isRootSchemaType(schema)) {
1991
+ const result = {};
1992
+ for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
1993
+ const value = getDefaultValue(schema.definition[key]);
1994
+ if (value !== void 0) result[key] = value;
1995
+ }
1996
+ return result;
1997
+ }
1998
+ return {};
1999
+ default: return;
2000
+ }
2001
+ }
2002
+ //#endregion
2003
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/schema/index.js
2004
+ /**
2005
+ * Create a schema definition
2006
+ */
2007
+ function schema(definition, options) {
2008
+ return {
2009
+ type: "schema",
2010
+ definition,
2011
+ options: options || {},
2012
+ getContainerType() {
2013
+ return "Map";
2014
+ }
2015
+ };
2016
+ }
2017
+ /**
2018
+ * Define a string field
2019
+ */
2020
+ schema.String = (function(options) {
2021
+ const baseSchema = {
2022
+ type: "string",
2023
+ options: options || {},
2024
+ getContainerType: () => {
2025
+ return null;
2026
+ }
2027
+ };
2028
+ return {
2029
+ ...baseSchema,
2030
+ transform: (def) => ({
2031
+ ...baseSchema,
2032
+ transform: def
2033
+ })
2034
+ };
2035
+ });
2036
+ /**
2037
+ * Define an any field (runtime-inferred by Mirror)
2038
+ */
2039
+ schema.Any = function(options) {
2040
+ return {
2041
+ type: "any",
2042
+ options: options || {},
2043
+ getContainerType: () => {
2044
+ return null;
2045
+ }
2046
+ };
2047
+ };
2048
+ /**
2049
+ * Define a number field
2050
+ */
2051
+ schema.Number = function(options) {
2052
+ const baseSchema = {
2053
+ type: "number",
2054
+ options: options || {},
2055
+ getContainerType: () => {
2056
+ return null;
2057
+ }
2058
+ };
2059
+ return {
2060
+ ...baseSchema,
2061
+ transform: (def) => ({
2062
+ ...baseSchema,
2063
+ transform: def
2064
+ })
2065
+ };
2066
+ };
2067
+ /**
2068
+ * Define a boolean field
2069
+ */
2070
+ schema.Boolean = function(options) {
2071
+ const baseSchema = {
2072
+ type: "boolean",
2073
+ options: options || {},
2074
+ getContainerType: () => {
2075
+ return null;
2076
+ }
2077
+ };
2078
+ return {
2079
+ ...baseSchema,
2080
+ transform: (def) => ({
2081
+ ...baseSchema,
2082
+ transform: def
2083
+ })
2084
+ };
2085
+ };
2086
+ /**
2087
+ * Define a field to be ignored (not synced with Loro)
2088
+ */
2089
+ schema.Ignore = function(options) {
2090
+ return {
2091
+ type: "ignore",
2092
+ options: options || {},
2093
+ getContainerType: () => {
2094
+ return null;
2095
+ }
2096
+ };
2097
+ };
2098
+ /**
2099
+ * Define a Loro map
2100
+ */
2101
+ schema.LoroMap = function(definition, options) {
2102
+ const baseSchema = {
2103
+ type: "loro-map",
2104
+ definition,
2105
+ options: options || {},
2106
+ getContainerType: () => {
2107
+ return "Map";
2108
+ }
2109
+ };
2110
+ const schemaWithCatchall = {
2111
+ ...baseSchema,
2112
+ catchall: (catchallSchema) => {
2113
+ return {
2114
+ ...baseSchema,
2115
+ catchallType: catchallSchema,
2116
+ catchall: (newCatchallSchema) => {
2117
+ return {
2118
+ ...baseSchema,
2119
+ catchallType: newCatchallSchema,
2120
+ catchall: schemaWithCatchall.catchall
2121
+ };
2122
+ }
2123
+ };
2124
+ }
2125
+ };
2126
+ return schemaWithCatchall;
2127
+ };
2128
+ /**
2129
+ * Create a dynamic record schema (like zod's z.record)
2130
+ */
2131
+ schema.LoroMapRecord = function(valueSchema, options) {
2132
+ return {
2133
+ type: "loro-map",
2134
+ definition: {},
2135
+ catchallType: valueSchema,
2136
+ options: options || {},
2137
+ getContainerType: () => {
2138
+ return "Map";
2139
+ },
2140
+ catchall: (newCatchallSchema) => {
2141
+ return schema.LoroMapRecord(newCatchallSchema, options);
2142
+ }
2143
+ };
2144
+ };
2145
+ /**
2146
+ * Define a Loro list
2147
+ */
2148
+ schema.LoroList = function(itemSchema, idSelector, options) {
2149
+ return {
2150
+ type: "loro-list",
2151
+ itemSchema,
2152
+ idSelector,
2153
+ options: options || {},
2154
+ getContainerType: () => {
2155
+ return "List";
2156
+ }
2157
+ };
2158
+ };
2159
+ schema.LoroMovableList = function(itemSchema, idSelector, options) {
2160
+ return {
2161
+ type: "loro-movable-list",
2162
+ itemSchema,
2163
+ idSelector,
2164
+ options: options || {},
2165
+ getContainerType: () => {
2166
+ return "MovableList";
2167
+ }
2168
+ };
2169
+ };
2170
+ /**
2171
+ * Define a Loro text field
2172
+ */
2173
+ schema.LoroText = function(options) {
2174
+ return {
2175
+ type: "loro-text",
2176
+ options: options || {},
2177
+ getContainerType: () => {
2178
+ return "Text";
2179
+ }
2180
+ };
2181
+ };
2182
+ /**
2183
+ * Define a Loro tree
2184
+ *
2185
+ * Each tree node has a `data` map described by `nodeSchema`.
2186
+ */
2187
+ schema.LoroTree = function(nodeSchema, options) {
2188
+ return {
2189
+ type: "loro-tree",
2190
+ nodeSchema,
2191
+ options: options || {},
2192
+ getContainerType() {
2193
+ return "Tree";
2194
+ }
2195
+ };
2196
+ };
2197
+ //#endregion
2198
+ //#region ../../node_modules/.pnpm/immer@10.2.0/node_modules/immer/dist/immer.mjs
2199
+ var NOTHING = Symbol.for("immer-nothing");
2200
+ var DRAFTABLE = Symbol.for("immer-draftable");
2201
+ var DRAFT_STATE = Symbol.for("immer-state");
2202
+ var errors = [
2203
+ function(plugin) {
2204
+ return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`;
2205
+ },
2206
+ function(thing) {
2207
+ return `produce can only be called on things that are draftable: plain objects, arrays, Map, Set or classes that are marked with '[immerable]: true'. Got '${thing}'`;
2208
+ },
2209
+ "This object has been frozen and should not be mutated",
2210
+ function(data) {
2211
+ return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data;
2212
+ },
2213
+ "An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
2214
+ "Immer forbids circular references",
2215
+ "The first or second argument to `produce` must be a function",
2216
+ "The third argument to `produce` must be a function or undefined",
2217
+ "First argument to `createDraft` must be a plain object, an array, or an immerable object",
2218
+ "First argument to `finishDraft` must be a draft returned by `createDraft`",
2219
+ function(thing) {
2220
+ return `'current' expects a draft, got: ${thing}`;
2221
+ },
2222
+ "Object.defineProperty() cannot be used on an Immer draft",
2223
+ "Object.setPrototypeOf() cannot be used on an Immer draft",
2224
+ "Immer only supports deleting array indices",
2225
+ "Immer only supports setting array indices and the 'length' property",
2226
+ function(thing) {
2227
+ return `'original' expects a draft, got: ${thing}`;
2228
+ }
2229
+ ];
2230
+ function die(error, ...args) {
2231
+ {
2232
+ const e = errors[error];
2233
+ const msg = typeof e === "function" ? e.apply(null, args) : e;
2234
+ throw new Error(`[Immer] ${msg}`);
2235
+ }
2236
+ throw new Error(`[Immer] minified error nr: ${error}. Full error at: https://bit.ly/3cXEKWf`);
2237
+ }
2238
+ var getPrototypeOf = Object.getPrototypeOf;
2239
+ function isDraft(value) {
2240
+ return !!value && !!value[DRAFT_STATE];
2241
+ }
2242
+ function isDraftable(value) {
2243
+ if (!value) return false;
2244
+ return isPlainObject(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!value.constructor?.[DRAFTABLE] || isMap(value) || isSet(value);
2245
+ }
2246
+ var objectCtorString = Object.prototype.constructor.toString();
2247
+ var cachedCtorStrings = /* @__PURE__ */ new WeakMap();
2248
+ function isPlainObject(value) {
2249
+ if (!value || typeof value !== "object") return false;
2250
+ const proto = Object.getPrototypeOf(value);
2251
+ if (proto === null || proto === Object.prototype) return true;
2252
+ const Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor;
2253
+ if (Ctor === Object) return true;
2254
+ if (typeof Ctor !== "function") return false;
2255
+ let ctorString = cachedCtorStrings.get(Ctor);
2256
+ if (ctorString === void 0) {
2257
+ ctorString = Function.toString.call(Ctor);
2258
+ cachedCtorStrings.set(Ctor, ctorString);
2259
+ }
2260
+ return ctorString === objectCtorString;
2261
+ }
2262
+ function each(obj, iter, strict = true) {
2263
+ if (getArchtype(obj) === 0) (strict ? Reflect.ownKeys(obj) : Object.keys(obj)).forEach((key) => {
2264
+ iter(key, obj[key], obj);
2265
+ });
2266
+ else obj.forEach((entry, index) => iter(index, entry, obj));
2267
+ }
2268
+ function getArchtype(thing) {
2269
+ const state = thing[DRAFT_STATE];
2270
+ return state ? state.type_ : Array.isArray(thing) ? 1 : isMap(thing) ? 2 : isSet(thing) ? 3 : 0;
2271
+ }
2272
+ function has(thing, prop) {
2273
+ return getArchtype(thing) === 2 ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop);
2274
+ }
2275
+ function set(thing, propOrOldValue, value) {
2276
+ const t = getArchtype(thing);
2277
+ if (t === 2) thing.set(propOrOldValue, value);
2278
+ else if (t === 3) thing.add(value);
2279
+ else thing[propOrOldValue] = value;
2280
+ }
2281
+ function is(x, y) {
2282
+ if (x === y) return x !== 0 || 1 / x === 1 / y;
2283
+ else return x !== x && y !== y;
2284
+ }
2285
+ function isMap(target) {
2286
+ return target instanceof Map;
2287
+ }
2288
+ function isSet(target) {
2289
+ return target instanceof Set;
2290
+ }
2291
+ function latest(state) {
2292
+ return state.copy_ || state.base_;
2293
+ }
2294
+ function shallowCopy(base, strict) {
2295
+ if (isMap(base)) return new Map(base);
2296
+ if (isSet(base)) return new Set(base);
2297
+ if (Array.isArray(base)) return Array.prototype.slice.call(base);
2298
+ const isPlain = isPlainObject(base);
2299
+ if (strict === true || strict === "class_only" && !isPlain) {
2300
+ const descriptors = Object.getOwnPropertyDescriptors(base);
2301
+ delete descriptors[DRAFT_STATE];
2302
+ let keys = Reflect.ownKeys(descriptors);
2303
+ for (let i = 0; i < keys.length; i++) {
2304
+ const key = keys[i];
2305
+ const desc = descriptors[key];
2306
+ if (desc.writable === false) {
2307
+ desc.writable = true;
2308
+ desc.configurable = true;
2309
+ }
2310
+ if (desc.get || desc.set) descriptors[key] = {
2311
+ configurable: true,
2312
+ writable: true,
2313
+ enumerable: desc.enumerable,
2314
+ value: base[key]
2315
+ };
2316
+ }
2317
+ return Object.create(getPrototypeOf(base), descriptors);
2318
+ } else {
2319
+ const proto = getPrototypeOf(base);
2320
+ if (proto !== null && isPlain) return { ...base };
2321
+ return Object.assign(Object.create(proto), base);
2322
+ }
2323
+ }
2324
+ function freeze(obj, deep = false) {
2325
+ if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj;
2326
+ if (getArchtype(obj) > 1) Object.defineProperties(obj, {
2327
+ set: dontMutateMethodOverride,
2328
+ add: dontMutateMethodOverride,
2329
+ clear: dontMutateMethodOverride,
2330
+ delete: dontMutateMethodOverride
2331
+ });
2332
+ Object.freeze(obj);
2333
+ if (deep) Object.values(obj).forEach((value) => freeze(value, true));
2334
+ return obj;
2335
+ }
2336
+ function dontMutateFrozenCollections() {
2337
+ die(2);
2338
+ }
2339
+ var dontMutateMethodOverride = { value: dontMutateFrozenCollections };
2340
+ function isFrozen(obj) {
2341
+ if (obj === null || typeof obj !== "object") return true;
2342
+ return Object.isFrozen(obj);
2343
+ }
2344
+ var plugins = {};
2345
+ function getPlugin(pluginKey) {
2346
+ const plugin = plugins[pluginKey];
2347
+ if (!plugin) die(0, pluginKey);
2348
+ return plugin;
2349
+ }
2350
+ var currentScope;
2351
+ function getCurrentScope() {
2352
+ return currentScope;
2353
+ }
2354
+ function createScope(parent_, immer_) {
2355
+ return {
2356
+ drafts_: [],
2357
+ parent_,
2358
+ immer_,
2359
+ canAutoFreeze_: true,
2360
+ unfinalizedDrafts_: 0
2361
+ };
2362
+ }
2363
+ function usePatchesInScope(scope, patchListener) {
2364
+ if (patchListener) {
2365
+ getPlugin("Patches");
2366
+ scope.patches_ = [];
2367
+ scope.inversePatches_ = [];
2368
+ scope.patchListener_ = patchListener;
2369
+ }
2370
+ }
2371
+ function revokeScope(scope) {
2372
+ leaveScope(scope);
2373
+ scope.drafts_.forEach(revokeDraft);
2374
+ scope.drafts_ = null;
2375
+ }
2376
+ function leaveScope(scope) {
2377
+ if (scope === currentScope) currentScope = scope.parent_;
2378
+ }
2379
+ function enterScope(immer2) {
2380
+ return currentScope = createScope(currentScope, immer2);
2381
+ }
2382
+ function revokeDraft(draft) {
2383
+ const state = draft[DRAFT_STATE];
2384
+ if (state.type_ === 0 || state.type_ === 1) state.revoke_();
2385
+ else state.revoked_ = true;
2386
+ }
2387
+ function processResult(result, scope) {
2388
+ scope.unfinalizedDrafts_ = scope.drafts_.length;
2389
+ const baseDraft = scope.drafts_[0];
2390
+ if (result !== void 0 && result !== baseDraft) {
2391
+ if (baseDraft[DRAFT_STATE].modified_) {
2392
+ revokeScope(scope);
2393
+ die(4);
2394
+ }
2395
+ if (isDraftable(result)) {
2396
+ result = finalize(scope, result);
2397
+ if (!scope.parent_) maybeFreeze(scope, result);
2398
+ }
2399
+ if (scope.patches_) getPlugin("Patches").generateReplacementPatches_(baseDraft[DRAFT_STATE].base_, result, scope.patches_, scope.inversePatches_);
2400
+ } else result = finalize(scope, baseDraft, []);
2401
+ revokeScope(scope);
2402
+ if (scope.patches_) scope.patchListener_(scope.patches_, scope.inversePatches_);
2403
+ return result !== NOTHING ? result : void 0;
2404
+ }
2405
+ function finalize(rootScope, value, path) {
2406
+ if (isFrozen(value)) return value;
2407
+ const useStrictIteration = rootScope.immer_.shouldUseStrictIteration();
2408
+ const state = value[DRAFT_STATE];
2409
+ if (!state) {
2410
+ each(value, (key, childValue) => finalizeProperty(rootScope, state, value, key, childValue, path), useStrictIteration);
2411
+ return value;
2412
+ }
2413
+ if (state.scope_ !== rootScope) return value;
2414
+ if (!state.modified_) {
2415
+ maybeFreeze(rootScope, state.base_, true);
2416
+ return state.base_;
2417
+ }
2418
+ if (!state.finalized_) {
2419
+ state.finalized_ = true;
2420
+ state.scope_.unfinalizedDrafts_--;
2421
+ const result = state.copy_;
2422
+ let resultEach = result;
2423
+ let isSet2 = false;
2424
+ if (state.type_ === 3) {
2425
+ resultEach = new Set(result);
2426
+ result.clear();
2427
+ isSet2 = true;
2428
+ }
2429
+ each(resultEach, (key, childValue) => finalizeProperty(rootScope, state, result, key, childValue, path, isSet2), useStrictIteration);
2430
+ maybeFreeze(rootScope, result, false);
2431
+ if (path && rootScope.patches_) getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_);
2432
+ }
2433
+ return state.copy_;
2434
+ }
2435
+ function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath, targetIsSet) {
2436
+ if (childValue == null) return;
2437
+ if (typeof childValue !== "object" && !targetIsSet) return;
2438
+ const childIsFrozen = isFrozen(childValue);
2439
+ if (childIsFrozen && !targetIsSet) return;
2440
+ if (childValue === targetObject) die(5);
2441
+ if (isDraft(childValue)) {
2442
+ const res = finalize(rootScope, childValue, rootPath && parentState && parentState.type_ !== 3 && !has(parentState.assigned_, prop) ? rootPath.concat(prop) : void 0);
2443
+ set(targetObject, prop, res);
2444
+ if (isDraft(res)) rootScope.canAutoFreeze_ = false;
2445
+ else return;
2446
+ } else if (targetIsSet) targetObject.add(childValue);
2447
+ if (isDraftable(childValue) && !childIsFrozen) {
2448
+ if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) return;
2449
+ if (parentState && parentState.base_ && parentState.base_[prop] === childValue && childIsFrozen) return;
2450
+ finalize(rootScope, childValue);
2451
+ if ((!parentState || !parentState.scope_.parent_) && typeof prop !== "symbol" && (isMap(targetObject) ? targetObject.has(prop) : Object.prototype.propertyIsEnumerable.call(targetObject, prop))) maybeFreeze(rootScope, childValue);
2452
+ }
2453
+ }
2454
+ function maybeFreeze(scope, value, deep = false) {
2455
+ if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) freeze(value, deep);
2456
+ }
2457
+ function createProxyProxy(base, parent) {
2458
+ const isArray = Array.isArray(base);
2459
+ const state = {
2460
+ type_: isArray ? 1 : 0,
2461
+ scope_: parent ? parent.scope_ : getCurrentScope(),
2462
+ modified_: false,
2463
+ finalized_: false,
2464
+ assigned_: {},
2465
+ parent_: parent,
2466
+ base_: base,
2467
+ draft_: null,
2468
+ copy_: null,
2469
+ revoke_: null,
2470
+ isManual_: false
2471
+ };
2472
+ let target = state;
2473
+ let traps = objectTraps;
2474
+ if (isArray) {
2475
+ target = [state];
2476
+ traps = arrayTraps;
2477
+ }
2478
+ const { revoke, proxy } = Proxy.revocable(target, traps);
2479
+ state.draft_ = proxy;
2480
+ state.revoke_ = revoke;
2481
+ return proxy;
2482
+ }
2483
+ var objectTraps = {
2484
+ get(state, prop) {
2485
+ if (prop === DRAFT_STATE) return state;
2486
+ const source = latest(state);
2487
+ if (!has(source, prop)) return readPropFromProto(state, source, prop);
2488
+ const value = source[prop];
2489
+ if (state.finalized_ || !isDraftable(value)) return value;
2490
+ if (value === peek(state.base_, prop)) {
2491
+ prepareCopy(state);
2492
+ return state.copy_[prop] = createProxy(value, state);
2493
+ }
2494
+ return value;
2495
+ },
2496
+ has(state, prop) {
2497
+ return prop in latest(state);
2498
+ },
2499
+ ownKeys(state) {
2500
+ return Reflect.ownKeys(latest(state));
2501
+ },
2502
+ set(state, prop, value) {
2503
+ const desc = getDescriptorFromProto(latest(state), prop);
2504
+ if (desc?.set) {
2505
+ desc.set.call(state.draft_, value);
2506
+ return true;
2507
+ }
2508
+ if (!state.modified_) {
2509
+ const current2 = peek(latest(state), prop);
2510
+ const currentState = current2?.[DRAFT_STATE];
2511
+ if (currentState && currentState.base_ === value) {
2512
+ state.copy_[prop] = value;
2513
+ state.assigned_[prop] = false;
2514
+ return true;
2515
+ }
2516
+ if (is(value, current2) && (value !== void 0 || has(state.base_, prop))) return true;
2517
+ prepareCopy(state);
2518
+ markChanged(state);
2519
+ }
2520
+ if (state.copy_[prop] === value && (value !== void 0 || prop in state.copy_) || Number.isNaN(value) && Number.isNaN(state.copy_[prop])) return true;
2521
+ state.copy_[prop] = value;
2522
+ state.assigned_[prop] = true;
2523
+ return true;
2524
+ },
2525
+ deleteProperty(state, prop) {
2526
+ if (peek(state.base_, prop) !== void 0 || prop in state.base_) {
2527
+ state.assigned_[prop] = false;
2528
+ prepareCopy(state);
2529
+ markChanged(state);
2530
+ } else delete state.assigned_[prop];
2531
+ if (state.copy_) delete state.copy_[prop];
2532
+ return true;
2533
+ },
2534
+ getOwnPropertyDescriptor(state, prop) {
2535
+ const owner = latest(state);
2536
+ const desc = Reflect.getOwnPropertyDescriptor(owner, prop);
2537
+ if (!desc) return desc;
2538
+ return {
2539
+ writable: true,
2540
+ configurable: state.type_ !== 1 || prop !== "length",
2541
+ enumerable: desc.enumerable,
2542
+ value: owner[prop]
2543
+ };
2544
+ },
2545
+ defineProperty() {
2546
+ die(11);
2547
+ },
2548
+ getPrototypeOf(state) {
2549
+ return getPrototypeOf(state.base_);
2550
+ },
2551
+ setPrototypeOf() {
2552
+ die(12);
2553
+ }
2554
+ };
2555
+ var arrayTraps = {};
2556
+ each(objectTraps, (key, fn) => {
2557
+ arrayTraps[key] = function() {
2558
+ arguments[0] = arguments[0][0];
2559
+ return fn.apply(this, arguments);
2560
+ };
2561
+ });
2562
+ arrayTraps.deleteProperty = function(state, prop) {
2563
+ if (isNaN(parseInt(prop))) die(13);
2564
+ return arrayTraps.set.call(this, state, prop, void 0);
2565
+ };
2566
+ arrayTraps.set = function(state, prop, value) {
2567
+ if (prop !== "length" && isNaN(parseInt(prop))) die(14);
2568
+ return objectTraps.set.call(this, state[0], prop, value, state[0]);
2569
+ };
2570
+ function peek(draft, prop) {
2571
+ const state = draft[DRAFT_STATE];
2572
+ return (state ? latest(state) : draft)[prop];
866
2573
  }
867
- function replaceSourcePlan(s, clipId, input) {
868
- if (requireClip(s, clipId).seqMarker.fillTimeline) throw new Error("Use setBgm to replace a fill Clip");
869
- const source = sourcePlan(s, input, trackOf(s, clipId));
870
- validateWindow(input, "durationMs" in source.source ? source.source.durationMs : void 0);
871
- return [...source.mutations, {
872
- type: "patch-entity",
873
- table: "clip-entities",
874
- id: clipId,
875
- patch: {
876
- seqMarker: {
877
- inMs: input.inMs,
878
- outMs: input.outMs
879
- },
880
- source: source.id
2574
+ function readPropFromProto(state, source, prop) {
2575
+ const desc = getDescriptorFromProto(source, prop);
2576
+ return desc ? `value` in desc ? desc.value : desc.get?.call(state.draft_) : void 0;
2577
+ }
2578
+ function getDescriptorFromProto(source, prop) {
2579
+ if (!(prop in source)) return void 0;
2580
+ let proto = getPrototypeOf(source);
2581
+ while (proto) {
2582
+ const desc = Object.getOwnPropertyDescriptor(proto, prop);
2583
+ if (desc) return desc;
2584
+ proto = getPrototypeOf(proto);
2585
+ }
2586
+ }
2587
+ function markChanged(state) {
2588
+ if (!state.modified_) {
2589
+ state.modified_ = true;
2590
+ if (state.parent_) markChanged(state.parent_);
2591
+ }
2592
+ }
2593
+ function prepareCopy(state) {
2594
+ if (!state.copy_) state.copy_ = shallowCopy(state.base_, state.scope_.immer_.useStrictShallowCopy_);
2595
+ }
2596
+ var Immer2 = class {
2597
+ constructor(config) {
2598
+ this.autoFreeze_ = true;
2599
+ this.useStrictShallowCopy_ = false;
2600
+ this.useStrictIteration_ = true;
2601
+ /**
2602
+ * The `produce` function takes a value and a "recipe function" (whose
2603
+ * return value often depends on the base state). The recipe function is
2604
+ * free to mutate its first argument however it wants. All mutations are
2605
+ * only ever applied to a __copy__ of the base state.
2606
+ *
2607
+ * Pass only a function to create a "curried producer" which relieves you
2608
+ * from passing the recipe function every time.
2609
+ *
2610
+ * Only plain objects and arrays are made mutable. All other objects are
2611
+ * considered uncopyable.
2612
+ *
2613
+ * Note: This function is __bound__ to its `Immer` instance.
2614
+ *
2615
+ * @param {any} base - the initial state
2616
+ * @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
2617
+ * @param {Function} patchListener - optional function that will be called with all the patches produced here
2618
+ * @returns {any} a new state, or the initial state if nothing was modified
2619
+ */
2620
+ this.produce = (base, recipe, patchListener) => {
2621
+ if (typeof base === "function" && typeof recipe !== "function") {
2622
+ const defaultBase = recipe;
2623
+ recipe = base;
2624
+ const self = this;
2625
+ return function curriedProduce(base2 = defaultBase, ...args) {
2626
+ return self.produce(base2, (draft) => recipe.call(this, draft, ...args));
2627
+ };
2628
+ }
2629
+ if (typeof recipe !== "function") die(6);
2630
+ if (patchListener !== void 0 && typeof patchListener !== "function") die(7);
2631
+ let result;
2632
+ if (isDraftable(base)) {
2633
+ const scope = enterScope(this);
2634
+ const proxy = createProxy(base, void 0);
2635
+ let hasError = true;
2636
+ try {
2637
+ result = recipe(proxy);
2638
+ hasError = false;
2639
+ } finally {
2640
+ if (hasError) revokeScope(scope);
2641
+ else leaveScope(scope);
2642
+ }
2643
+ usePatchesInScope(scope, patchListener);
2644
+ return processResult(result, scope);
2645
+ } else if (!base || typeof base !== "object") {
2646
+ result = recipe(base);
2647
+ if (result === void 0) result = base;
2648
+ if (result === NOTHING) result = void 0;
2649
+ if (this.autoFreeze_) freeze(result, true);
2650
+ if (patchListener) {
2651
+ const p = [];
2652
+ const ip = [];
2653
+ getPlugin("Patches").generateReplacementPatches_(base, result, p, ip);
2654
+ patchListener(p, ip);
2655
+ }
2656
+ return result;
2657
+ } else die(1, base);
2658
+ };
2659
+ this.produceWithPatches = (base, recipe) => {
2660
+ if (typeof base === "function") return (state, ...args) => this.produceWithPatches(state, (draft) => base(draft, ...args));
2661
+ let patches, inversePatches;
2662
+ return [
2663
+ this.produce(base, recipe, (p, ip) => {
2664
+ patches = p;
2665
+ inversePatches = ip;
2666
+ }),
2667
+ patches,
2668
+ inversePatches
2669
+ ];
2670
+ };
2671
+ if (typeof config?.autoFreeze === "boolean") this.setAutoFreeze(config.autoFreeze);
2672
+ if (typeof config?.useStrictShallowCopy === "boolean") this.setUseStrictShallowCopy(config.useStrictShallowCopy);
2673
+ if (typeof config?.useStrictIteration === "boolean") this.setUseStrictIteration(config.useStrictIteration);
2674
+ }
2675
+ createDraft(base) {
2676
+ if (!isDraftable(base)) die(8);
2677
+ if (isDraft(base)) base = current(base);
2678
+ const scope = enterScope(this);
2679
+ const proxy = createProxy(base, void 0);
2680
+ proxy[DRAFT_STATE].isManual_ = true;
2681
+ leaveScope(scope);
2682
+ return proxy;
2683
+ }
2684
+ finishDraft(draft, patchListener) {
2685
+ const state = draft && draft[DRAFT_STATE];
2686
+ if (!state || !state.isManual_) die(9);
2687
+ const { scope_: scope } = state;
2688
+ usePatchesInScope(scope, patchListener);
2689
+ return processResult(void 0, scope);
2690
+ }
2691
+ /**
2692
+ * Pass true to automatically freeze all copies created by Immer.
2693
+ *
2694
+ * By default, auto-freezing is enabled.
2695
+ */
2696
+ setAutoFreeze(value) {
2697
+ this.autoFreeze_ = value;
2698
+ }
2699
+ /**
2700
+ * Pass true to enable strict shallow copy.
2701
+ *
2702
+ * By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
2703
+ */
2704
+ setUseStrictShallowCopy(value) {
2705
+ this.useStrictShallowCopy_ = value;
2706
+ }
2707
+ /**
2708
+ * Pass false to use faster iteration that skips non-enumerable properties
2709
+ * but still handles symbols for compatibility.
2710
+ *
2711
+ * By default, strict iteration is enabled (includes all own properties).
2712
+ */
2713
+ setUseStrictIteration(value) {
2714
+ this.useStrictIteration_ = value;
2715
+ }
2716
+ shouldUseStrictIteration() {
2717
+ return this.useStrictIteration_;
2718
+ }
2719
+ applyPatches(base, patches) {
2720
+ let i;
2721
+ for (i = patches.length - 1; i >= 0; i--) {
2722
+ const patch = patches[i];
2723
+ if (patch.path.length === 0 && patch.op === "replace") {
2724
+ base = patch.value;
2725
+ break;
2726
+ }
881
2727
  }
882
- }];
2728
+ if (i > -1) patches = patches.slice(i + 1);
2729
+ const applyPatchesImpl = getPlugin("Patches").applyPatches_;
2730
+ if (isDraft(base)) return applyPatchesImpl(base, patches);
2731
+ return this.produce(base, (draft) => applyPatchesImpl(draft, patches));
2732
+ }
2733
+ };
2734
+ function createProxy(value, parent) {
2735
+ const draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : createProxyProxy(value, parent);
2736
+ (parent ? parent.scope_ : getCurrentScope()).drafts_.push(draft);
2737
+ return draft;
2738
+ }
2739
+ function current(value) {
2740
+ if (!isDraft(value)) die(10, value);
2741
+ return currentImpl(value);
2742
+ }
2743
+ function currentImpl(value) {
2744
+ if (!isDraftable(value) || isFrozen(value)) return value;
2745
+ const state = value[DRAFT_STATE];
2746
+ let copy;
2747
+ let strict = true;
2748
+ if (state) {
2749
+ if (!state.modified_) return state.base_;
2750
+ state.finalized_ = true;
2751
+ copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_);
2752
+ strict = state.scope_.immer_.shouldUseStrictIteration();
2753
+ } else copy = shallowCopy(value, true);
2754
+ each(copy, (key, childValue) => {
2755
+ set(copy, key, currentImpl(childValue));
2756
+ }, strict);
2757
+ if (state) state.finalized_ = false;
2758
+ return copy;
883
2759
  }
884
- const produce = new Immer({
2760
+ var immer = new Immer2();
2761
+ immer.produce;
2762
+ var setAutoFreeze = /* @__PURE__ */ immer.setAutoFreeze.bind(immer);
2763
+ const produce = new Immer2({
885
2764
  autoFreeze: false,
886
2765
  useStrictShallowCopy: true
887
2766
  }).produce;
888
2767
  //#endregion
889
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/debounce-timer.js
2768
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/debounce-timer.js
890
2769
  /**
891
2770
  * Deadline-based debounce timer.
892
2771
  *
@@ -944,7 +2823,7 @@ var DebounceTimer = class {
944
2823
  }
945
2824
  };
946
2825
  //#endregion
947
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/ephemeral.js
2826
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/ephemeral.js
948
2827
  /**
949
2828
  * Manages ephemeral patches: storage, composition, path resolution, and finalization.
950
2829
  */
@@ -1219,7 +3098,7 @@ var EphemeralPatchManager = class {
1219
3098
  }
1220
3099
  };
1221
3100
  //#endregion
1222
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/tree-utils.js
3101
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/tree-utils.js
1223
3102
  function normalizeTreeJson(input, options) {
1224
3103
  if (!Array.isArray(input)) return [];
1225
3104
  return input.map((node) => normalizeTreeNode(node, options));
@@ -1242,7 +3121,7 @@ function isPlainRecord(value) {
1242
3121
  return typeof value === "object" && value !== null && !Array.isArray(value);
1243
3122
  }
1244
3123
  //#endregion
1245
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/loroEventApply.js
3124
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/loroEventApply.js
1246
3125
  function isJSONObject(v) {
1247
3126
  return typeof v === "object" && v !== null && !Array.isArray(v);
1248
3127
  }
@@ -1664,7 +3543,7 @@ function isIgnoredByAncestor(id, ignoreSet, getContainerById) {
1664
3543
  return false;
1665
3544
  }
1666
3545
  //#endregion
1667
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/diff.js
3546
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/diff.js
1668
3547
  /**
1669
3548
  * Finds the longest increasing subsequence of a sequence of numbers
1670
3549
  * @param sequence The sequence of numbers
@@ -2395,7 +4274,7 @@ function diffMap(doc, oldState, newState, containerId, schema, inferOptions) {
2395
4274
  return changes;
2396
4275
  }
2397
4276
  //#endregion
2398
- //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.1/node_modules/loro-mirror/dist/core/mirror.js
4277
+ //#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/core/mirror.js
2399
4278
  /**
2400
4279
  * Mirror core functionality for bidirectional sync between app state and Loro CRDT
2401
4280
  */
@@ -3890,6 +5769,220 @@ function mergeInitialIntoBaseWithSchema(base, init, rootSchema) {
3890
5769
  }
3891
5770
  }
3892
5771
  //#endregion
5772
+ //#region ../medeo-dsl/src/schema/codec.ts
5773
+ function isInvalidAtomic(value) {
5774
+ return typeof value === "object" && value !== null && "invalid" in value && value.invalid === true;
5775
+ }
5776
+ function codec(encode, decode) {
5777
+ return {
5778
+ encode: (v) => isInvalidAtomic(v) ? v.raw : encode(v),
5779
+ decode: (raw) => {
5780
+ try {
5781
+ return decode(raw);
5782
+ } catch (error) {
5783
+ return {
5784
+ invalid: true,
5785
+ raw,
5786
+ reason: String(error)
5787
+ };
5788
+ }
5789
+ }
5790
+ };
5791
+ }
5792
+ /** A Clip's time anchor; the Clip holding it is the anchored end. */
5793
+ const timeAnchorCodec = codec((v) => {
5794
+ assertTimeAnchor(v);
5795
+ return JSON.stringify({
5796
+ to: v.to,
5797
+ offsetMs: v.offsetMs,
5798
+ fallback_abs_ms: v.fallback_abs_ms
5799
+ });
5800
+ }, (raw) => {
5801
+ const v = JSON.parse(raw);
5802
+ assertTimeAnchor(v);
5803
+ return v;
5804
+ });
5805
+ /** A segment selection as stored: a run's Cursors as their native bytes in unpadded base64url. */
5806
+ function encodeSelection(v) {
5807
+ return {
5808
+ audioScriptId: v.audioScriptId,
5809
+ segmentId: v.segmentId,
5810
+ ...v.start === void 0 || v.end === void 0 ? {} : {
5811
+ start: bytesToBase64url(v.start.encode()),
5812
+ end: bytesToBase64url(v.end.encode())
5813
+ }
5814
+ };
5815
+ }
5816
+ function decodeSelection(v) {
5817
+ if (!v || typeof v !== "object") throw new Error("Expected a segment selection");
5818
+ const { start, end, ...rest } = v;
5819
+ if (start === void 0 && end === void 0) return rest;
5820
+ if (typeof start !== "string" || typeof end !== "string") throw new Error("Expected encoded Cursor pair");
5821
+ return {
5822
+ ...rest,
5823
+ start: Cursor.decode(base64urlToBytes(start)),
5824
+ end: Cursor.decode(base64urlToBytes(end))
5825
+ };
5826
+ }
5827
+ /** A Caption's selection. */
5828
+ const captionContentCodec = codec((v) => {
5829
+ assertCaptionContent(v);
5830
+ return JSON.stringify(encodeSelection(v));
5831
+ }, (raw) => {
5832
+ const value = decodeSelection(JSON.parse(raw));
5833
+ assertCaptionContent(value);
5834
+ return value;
5835
+ });
5836
+ /** A PhoneticScript's selection and what to synthesize for it, stored as one string so they move together. */
5837
+ const phonemeCodec = codec((v) => {
5838
+ assertPhoneme(v);
5839
+ return JSON.stringify({
5840
+ ...encodeSelection(v),
5841
+ ...v.phonemeScript === void 0 ? {} : { phonemeScript: v.phonemeScript }
5842
+ });
5843
+ }, (raw) => {
5844
+ const value = decodeSelection(JSON.parse(raw));
5845
+ assertPhoneme(value);
5846
+ return value;
5847
+ });
5848
+ /** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
5849
+ const seqMarkerCodec = codec((v) => {
5850
+ assertSeqMarker(v);
5851
+ return JSON.stringify({
5852
+ inMs: v.inMs,
5853
+ outMs: v.outMs,
5854
+ ...v.rate === void 0 ? {} : { rate: v.rate },
5855
+ ...v.fillTimeline === void 0 ? {} : { fillTimeline: v.fillTimeline }
5856
+ });
5857
+ }, (raw) => {
5858
+ const v = JSON.parse(raw);
5859
+ assertSeqMarker(v);
5860
+ return v;
5861
+ });
5862
+ //#endregion
5863
+ //#region ../medeo-dsl/src/schema/loro-shape.ts
5864
+ const optional = { required: false };
5865
+ const mergeable = { mergeableMapChildContainers: true };
5866
+ const strings = (keys) => Object.fromEntries(keys.map((key) => [key, schema.String()]));
5867
+ const relation = (keys) => schema.LoroMapRecord(schema.LoroMap(strings(keys)), mergeable);
5868
+ const transform = (codec) => ({
5869
+ ...codec,
5870
+ isEqual: "encoded-value-equality",
5871
+ validateEncodedType: true
5872
+ });
5873
+ /** One field stored as one encoded string, so it is taken or replaced whole. */
5874
+ const atomicValue = (codec) => schema.String().transform(transform(codec));
5875
+ const optionalAtomicValue = (codec) => schema.String(optional).transform(transform(codec));
5876
+ /**
5877
+ * Only direct children inherit mergeable identity. Text and ordinary entity rows keep normal
5878
+ * identities, so deleting a row also drops what a peer wrote into it concurrently: a relation an
5879
+ * entity holds at most one of is a field of that row and never outlives it.
5880
+ */
5881
+ const medeoDslLoroShape = schema({
5882
+ "timeline-entities": schema.LoroMapRecord(schema.LoroMap({})),
5883
+ "track-entities": schema.LoroMapRecord(schema.LoroMap({
5884
+ role: schema.String(),
5885
+ hidden: schema.Boolean(optional)
5886
+ })),
5887
+ "clip-entities": schema.LoroMapRecord(schema.LoroMap({
5888
+ seqMarker: atomicValue(seqMarkerCodec),
5889
+ volume: schema.Number(optional),
5890
+ source: schema.String(),
5891
+ anchor: optionalAtomicValue(timeAnchorCodec)
5892
+ })),
5893
+ "media-entities": schema.LoroMapRecord(schema.LoroMap({
5894
+ kind: schema.String(),
5895
+ system: schema.String(),
5896
+ key: schema.String(),
5897
+ storageKey: schema.String(optional),
5898
+ durationMs: schema.Number(optional),
5899
+ digest: schema.String(optional)
5900
+ }), mergeable),
5901
+ "audio-script-entities": schema.LoroMapRecord(schema.LoroMap({
5902
+ segments: schema.LoroMovableList(schema.LoroMap({
5903
+ id: schema.String(),
5904
+ text: schema.LoroText(),
5905
+ startMs: schema.Number(optional),
5906
+ endMs: schema.Number(optional)
5907
+ }), (segment) => segment.id),
5908
+ defaultLanguage: schema.String(optional),
5909
+ system: schema.String(optional),
5910
+ key: schema.String(optional),
5911
+ storageKey: schema.String(optional),
5912
+ digest: schema.String(optional)
5913
+ })),
5914
+ "phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({
5915
+ phoneme: atomicValue(phonemeCodec),
5916
+ system: schema.String(optional),
5917
+ key: schema.String(optional),
5918
+ storageKey: schema.String(optional),
5919
+ digest: schema.String(optional)
5920
+ })),
5921
+ "caption-entities": schema.LoroMapRecord(schema.LoroMap({
5922
+ selection: optionalAtomicValue(captionContentCodec),
5923
+ text: schema.LoroText(optional),
5924
+ style: schema.LoroMap({
5925
+ font: schema.LoroMap({
5926
+ system: schema.String(),
5927
+ key: schema.String()
5928
+ }, optional),
5929
+ fontSize: schema.Number(optional),
5930
+ fontColor: schema.String(optional),
5931
+ fontWeight: schema.Number(optional),
5932
+ entranceAnimation: schema.String(optional),
5933
+ entranceAnimationDurationMs: schema.Number(optional),
5934
+ strokeColor: schema.String(optional),
5935
+ strokeWidth: schema.Number(optional),
5936
+ positionX: schema.Number(optional),
5937
+ positionY: schema.Number(optional)
5938
+ }, {
5939
+ ...optional,
5940
+ ...mergeable
5941
+ })
5942
+ }, mergeable)),
5943
+ "voice-entities": schema.LoroMapRecord(schema.LoroMap({
5944
+ system: schema.String(),
5945
+ key: schema.String(),
5946
+ name: schema.String(optional)
5947
+ }), mergeable),
5948
+ "contain-relations": schema.LoroTree(schema.LoroMap({ entityId: schema.String() })),
5949
+ "voice-timbre-relations": relation(["recordingId", "voiceId"]),
5950
+ "derived-from-relations": schema.LoroMapRecord(schema.LoroMap({
5951
+ entityId: schema.String(),
5952
+ sourceId: schema.String(),
5953
+ sourceDigest: schema.String(optional)
5954
+ }), mergeable)
5955
+ });
5956
+ //#endregion
5957
+ //#region ../medeo-dsl/src/schema/document-format.ts
5958
+ /** Format discrimination does not register containers or alter the document. */
5959
+ function documentFormat(doc) {
5960
+ const roots = doc.getShallowValue();
5961
+ const legacy = [
5962
+ "medeo",
5963
+ "timeline",
5964
+ "tracks",
5965
+ "part_library"
5966
+ ].some((key) => Object.hasOwn(roots, key));
5967
+ const dsl = Object.keys(roots).some((key) => key.endsWith("-entities") || key.endsWith("-relations"));
5968
+ return legacy && dsl ? "mixed" : legacy ? "legacy" : dsl ? "dsl" : "unknown";
5969
+ }
5970
+ var UnsupportedDocumentFormatError = class extends Error {
5971
+ code = "unsupported_document_format";
5972
+ constructor() {
5973
+ super("Expected a new DSL document; legacy, mixed or uninitialized content is not supported");
5974
+ this.name = "UnsupportedDocumentFormatError";
5975
+ }
5976
+ };
5977
+ /** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
5978
+ function assertDslDocument(doc) {
5979
+ if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
5980
+ }
5981
+ //#endregion
5982
+ //#region ../medeo-dsl/src/schema/text-styles.ts
5983
+ /** How rich-text marks extend at their edges: a language runs on into text typed after it. */
5984
+ const textStyles = { language: { expand: "after" } };
5985
+ //#endregion
3893
5986
  //#region src/dsl/document/read.ts
3894
5987
  /** Looking up a missing root must not create it. */
3895
5988
  function rootMap(doc, name) {
@@ -3898,11 +5991,21 @@ function rootMap(doc, name) {
3898
5991
  const value = doc.getContainerById(id);
3899
5992
  return value?.kind() === "Map" ? value : void 0;
3900
5993
  }
3901
- function liveText(doc, scriptId, caption = false) {
3902
- const row = rootMap(doc, caption ? "caption-entities" : "audio-script-entities")?.get(scriptId);
3903
- if (!isContainer(row) || row.kind() !== "Map") return;
3904
- const body = row.get(caption ? "text" : "body");
3905
- return isContainer(body) && body.kind() === "Text" && !body.isDeleted() ? body : void 0;
5994
+ function asMap(value) {
5995
+ return isContainer(value) && value.kind() === "Map" ? value : void 0;
5996
+ }
5997
+ function asText(value) {
5998
+ return isContainer(value) && value.kind() === "Text" && !value.isDeleted() ? value : void 0;
5999
+ }
6000
+ /** The live Text a target names: a Caption's own, or a script segment's. */
6001
+ function liveText(doc, target) {
6002
+ if ("captionId" in target) return asText(asMap(rootMap(doc, "caption-entities")?.get(target.captionId))?.get("text"));
6003
+ const segments = asMap(rootMap(doc, "audio-script-entities")?.get(target.audioScriptId))?.get("segments");
6004
+ if (!isContainer(segments) || segments.kind() !== "MovableList") return;
6005
+ for (const item of segments.toArray()) {
6006
+ const segment = asMap(item);
6007
+ if (segment?.get("id") === target.segmentId) return asText(segment.get("text"));
6008
+ }
3906
6009
  }
3907
6010
  function readContainNodes(nodes) {
3908
6011
  const read = (nodes) => nodes.map((node) => ({
@@ -3912,19 +6015,41 @@ function readContainNodes(nodes) {
3912
6015
  }));
3913
6016
  return read(nodes);
3914
6017
  }
6018
+ /** What a selection holds now: its whole segment, or the run between its Cursors on that segment's Text. */
3915
6019
  function resolveContent(doc, content) {
3916
- const text = liveText(doc, content.audioScriptId);
3917
- if (!text || content.start.containerId() !== text.id || content.end.containerId() !== text.id) return;
6020
+ const text = liveText(doc, {
6021
+ audioScriptId: content.audioScriptId,
6022
+ segmentId: content.segmentId
6023
+ });
6024
+ if (!text) return;
6025
+ const whole = text.toString();
6026
+ if (content.start === void 0 || content.end === void 0) return {
6027
+ text: whole,
6028
+ start: 0,
6029
+ end: whole.length,
6030
+ bodyId: text.id
6031
+ };
6032
+ if (content.start.containerId() !== text.id || content.end.containerId() !== text.id) return;
3918
6033
  const start = doc.getCursorPos(content.start)?.offset;
3919
6034
  const end = doc.getCursorPos(content.end)?.offset;
3920
6035
  if (start === void 0 || end === void 0 || start > end) return;
3921
6036
  return {
3922
- text: text.toString().slice(start, end),
6037
+ text: whole.slice(start, end),
3923
6038
  start,
3924
6039
  end,
3925
6040
  bodyId: text.id
3926
6041
  };
3927
6042
  }
6043
+ function richTextOf(text) {
6044
+ return {
6045
+ bodyId: text.id,
6046
+ plainText: text.toString(),
6047
+ delta: text.toDelta().flatMap((run) => typeof run.insert === "string" ? [{
6048
+ insert: run.insert,
6049
+ ...run.attributes ? { attributes: run.attributes } : {}
6050
+ }] : [])
6051
+ };
6052
+ }
3928
6053
  /** Optional relation fields read apart from their row: an unreadable one is dropped, the row is kept. */
3929
6054
  const optionalReferences = {
3930
6055
  "clip-entities": ["anchor"],
@@ -3962,19 +6087,16 @@ function readMedeoDsl(doc, state) {
3962
6087
  }
3963
6088
  assertEntity(table, v);
3964
6089
  assertResourceIdentity(table, id, v);
6090
+ if (table === "phonetic-script-entities") assertPhoneticScript(void 0, v);
6091
+ const texts = [];
6092
+ if (table === "audio-script-entities") for (const segment of v.segments) texts.push([segment.id, liveText(doc, {
6093
+ audioScriptId: id,
6094
+ segmentId: segment.id
6095
+ })]);
6096
+ if (table === "caption-entities" && v.text !== void 0) texts.push([id, liveText(doc, { captionId: id })]);
6097
+ for (const [, text] of texts) if (!text) throw new Error("Missing live LoroText");
3965
6098
  entities[table][id] = v;
3966
- if (table === "audio-script-entities" || table === "caption-entities" && v.text !== void 0) {
3967
- const text = liveText(doc, id, table === "caption-entities");
3968
- if (!text) throw new Error("Missing live LoroText body");
3969
- richText[id] = {
3970
- bodyId: text.id,
3971
- plainText: text.toString(),
3972
- delta: text.toDelta().flatMap((run) => typeof run.insert === "string" ? [{
3973
- insert: run.insert,
3974
- ...run.attributes ? { attributes: run.attributes } : {}
3975
- }] : [])
3976
- };
3977
- }
6099
+ for (const [key, text] of texts) richText[key] = richTextOf(text);
3978
6100
  } catch (error) {
3979
6101
  delete entities[table][id];
3980
6102
  diagnostic("invalid-entity", `${table}/${id}`, error);
@@ -4033,17 +6155,26 @@ function readMedeoDsl(doc, state) {
4033
6155
  try {
4034
6156
  const resolved = resolveContent(doc, caption.selection);
4035
6157
  if (resolved) captionText[id] = resolved;
4036
- else diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, "Cursor does not resolve in the current body");
6158
+ else diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, "Selection does not resolve in its current segment");
4037
6159
  } catch (error) {
4038
6160
  diagnostic("invalid-caption-content", `caption-entities/${id}/selection`, error);
4039
6161
  }
4040
6162
  }
6163
+ const phoneticText = {};
6164
+ for (const [id, phonetic] of Object.entries(entities["phonetic-script-entities"])) try {
6165
+ const resolved = resolveContent(doc, phonetic.phoneme);
6166
+ if (resolved) phoneticText[id] = resolved;
6167
+ else diagnostic("invalid-phoneme", `phonetic-script-entities/${id}/phoneme`, "Selection does not resolve in its current segment");
6168
+ } catch (error) {
6169
+ diagnostic("invalid-phoneme", `phonetic-script-entities/${id}/phoneme`, error);
6170
+ }
4041
6171
  return freezeSnapshot({
4042
6172
  ...entities,
4043
6173
  ...relations,
4044
6174
  "contain-relations": validTree(treeNodes),
4045
6175
  richText,
4046
6176
  captionText,
6177
+ phoneticText,
4047
6178
  diagnostics
4048
6179
  });
4049
6180
  }
@@ -4056,6 +6187,19 @@ function freezeSnapshot(value) {
4056
6187
  }
4057
6188
  //#endregion
4058
6189
  //#region src/dsl/document/draft.ts
6190
+ function textKey(target) {
6191
+ return "captionId" in target ? JSON.stringify(["caption", target.captionId]) : JSON.stringify([
6192
+ "segment",
6193
+ target.audioScriptId,
6194
+ target.segmentId
6195
+ ]);
6196
+ }
6197
+ /** Rows holding a segment selection, by the field that holds it (D181). */
6198
+ const selectionField = {
6199
+ "caption-entities": "selection",
6200
+ "phonetic-script-entities": "phoneme"
6201
+ };
6202
+ const isSelectionTable = (table) => Object.hasOwn(selectionField, table);
4059
6203
  const entities = new Set(entityTables);
4060
6204
  const relations = new Set(relationTables);
4061
6205
  const reserved = new Set([
@@ -4088,9 +6232,10 @@ var TransactionDraft = class {
4088
6232
  entities = /* @__PURE__ */ new Map();
4089
6233
  relations = /* @__PURE__ */ new Map();
4090
6234
  deleted = /* @__PURE__ */ new Set();
6235
+ /** Texts this write edits, by {@link textKey}, as its edits so far leave them. */
4091
6236
  textPreviews = /* @__PURE__ */ new Map();
4092
- /** Captions whose selection the draft wrote; they are replayed natively in order with text edits. */
4093
- dirtyCaptions = /* @__PURE__ */ new Set();
6237
+ /** Selections the draft wrote, by table then row; they are replayed natively in order with text edits. */
6238
+ dirty = /* @__PURE__ */ new Map();
4094
6239
  removedCaptionTexts = /* @__PURE__ */ new Set();
4095
6240
  treeChanged = false;
4096
6241
  active = true;
@@ -4121,13 +6266,30 @@ var TransactionDraft = class {
4121
6266
  close() {
4122
6267
  this.active = false;
4123
6268
  }
6269
+ dirtySelections() {
6270
+ return [...this.dirty].flatMap(([table, ids]) => [...ids].map((id) => [table, id]));
6271
+ }
6272
+ clearDirtySelections() {
6273
+ this.dirty.clear();
6274
+ }
6275
+ /** Whether this write edits any segment of a script. */
6276
+ usesScript(scriptId) {
6277
+ const prefix = JSON.stringify(["segment", scriptId]).slice(0, -1) + ",";
6278
+ return [...this.textPreviews.keys()].some((key) => key.startsWith(prefix));
6279
+ }
4124
6280
  wrap(value, path) {
4125
6281
  if (!value || typeof value !== "object" || value instanceof Cursor) return value;
4126
6282
  const cached = this.wrappers.get(value);
4127
6283
  if (cached) return cached;
4128
6284
  const proxy = new Proxy(value, {
4129
6285
  get: (target, key) => this.run(() => {
4130
- if ((path[0] === "audio-script-entities" && key === "body" || path[0] === "caption-entities" && key === "text") && path.length === 2 && this.textPreviews.has(path[1])) return this.textPreviews.get(path[1]);
6286
+ if (key === "text") {
6287
+ 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({
6288
+ audioScriptId: path[1],
6289
+ segmentId: String(target.id)
6290
+ })) : void 0;
6291
+ if (preview !== void 0) return preview;
6292
+ }
4131
6293
  if (!path.length && key === "contain-relations" && target[key] === void 0) target[key] = [];
4132
6294
  return this.wrap(Reflect.get(target, key), [...path, String(key)]);
4133
6295
  }),
@@ -4182,16 +6344,22 @@ var TransactionDraft = class {
4182
6344
  if (path.length === 1 && (remove || next === void 0)) this.deleted.add(lifetime);
4183
6345
  else if (this.deleted.has(lifetime)) throw new Error("Recreate an entity in a separate transaction");
4184
6346
  if (table === "audio-script-entities") {
4185
- if (path.length === 2 && key === "body" && (initial || this.textPreviews.has(rowId))) throw new Error("Edit body through the Text adapter");
4186
- if (path.length === 1 && initial && isRecord(next) && next.body !== initial.body) throw new Error("Edit body through the Text adapter");
4187
- if (path.length === 1 && this.textPreviews.has(rowId)) throw new Error("Cannot delete or replace a body used in this transaction");
6347
+ 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");
6348
+ if (path.length === 1 && this.usesScript(rowId)) throw new Error("Cannot delete or replace a Text used in this transaction");
6349
+ }
6350
+ if (isSelectionTable(table)) {
6351
+ const field = selectionField[table];
6352
+ if (path.length === 1 && isRecord(next) && Object.hasOwn(next, field) || path.length === 2 && key === field || path[2] === field) {
6353
+ let ids = this.dirty.get(table);
6354
+ if (!ids) this.dirty.set(table, ids = /* @__PURE__ */ new Set());
6355
+ ids.add(rowId);
6356
+ }
4188
6357
  }
4189
6358
  if (table === "caption-entities") {
4190
- if (path.length === 1 && isRecord(next) && Object.hasOwn(next, "selection") || path.length === 2 && key === "selection" || path[2] === "selection") this.dirtyCaptions.add(rowId);
4191
6359
  const changesText = path.length === 1 || path.length === 2 && key === "text";
4192
6360
  const nextText = path.length === 1 ? isRecord(next) ? next.text : void 0 : next;
4193
6361
  if (changesText) {
4194
- if (this.textPreviews.has(rowId)) throw new Error("Cannot delete or replace a body used in this transaction");
6362
+ if (this.textPreviews.has(textKey({ captionId: rowId }))) throw new Error("Cannot delete or replace a Text used in this transaction");
4195
6363
  if (nextText !== void 0 && (this.removedCaptionTexts.has(rowId) || initial?.text !== void 0 && nextText !== initial.text)) throw new Error("Edit Caption text through the Text adapter");
4196
6364
  if (initial?.text !== void 0 && nextText === void 0) this.removedCaptionTexts.add(rowId);
4197
6365
  }
@@ -4208,7 +6376,7 @@ var TransactionDraft = class {
4208
6376
  let fields = rows.get(rowId);
4209
6377
  if (!fields) rows.set(rowId, fields = /* @__PURE__ */ new Set());
4210
6378
  if (path.length === 1) for (const field of [...Object.keys(initial ?? {}), ...Object.keys(isRecord(next) ? next : {})]) {
4211
- if ((field === "body" && table === "audio-script-entities" || field === "text" && table === "caption-entities") && initial && isRecord(next) && next[field] === initial[field]) continue;
6379
+ if (initial && isRecord(next) && (field === "segments" && table === "audio-script-entities" && sameSegments(next[field], initial[field]) || field === "text" && table === "caption-entities" && next[field] === initial[field])) continue;
4212
6380
  fields.add(field);
4213
6381
  }
4214
6382
  else fields.add(path[2] ?? key);
@@ -4219,6 +6387,9 @@ var TransactionDraft = class {
4219
6387
  }
4220
6388
  }
4221
6389
  };
6390
+ function sameSegments(a, b) {
6391
+ return JSON.stringify(a) === JSON.stringify(b);
6392
+ }
4222
6393
  //#endregion
4223
6394
  //#region src/dsl/document/draft-operations.ts
4224
6395
  function nodeIndex(nodes) {
@@ -4314,8 +6485,20 @@ function applyMutations(tx, mutations) {
4314
6485
  case "delete-relation":
4315
6486
  delete rows[m.table][m.id];
4316
6487
  break;
6488
+ case "insert-segment":
6489
+ tx.insertSegment(m.audioScriptId, m.segmentId, m.text, m.index, m.startMs === void 0 && m.endMs === void 0 ? void 0 : {
6490
+ startMs: m.startMs,
6491
+ endMs: m.endMs
6492
+ });
6493
+ break;
6494
+ case "move-segment":
6495
+ tx.moveSegment(m.audioScriptId, m.segmentId, m.index);
6496
+ break;
6497
+ case "delete-segment":
6498
+ tx.deleteSegment(m.audioScriptId, m.segmentId);
6499
+ break;
4317
6500
  case "edit-text":
4318
- tx.editText(m.audioScriptId, m.edits, m.expectedBodyId);
6501
+ tx.editText(m.audioScriptId, m.segmentId, m.edits, m.expectedBodyId);
4319
6502
  break;
4320
6503
  case "edit-caption-text":
4321
6504
  tx.editCaptionText(m.captionId, m.edits, m.expectedBodyId);
@@ -4323,6 +6506,9 @@ function applyMutations(tx, mutations) {
4323
6506
  case "set-caption-content":
4324
6507
  tx.setCaptionContent(m);
4325
6508
  break;
6509
+ case "set-phoneme":
6510
+ tx.setPhoneme(m);
6511
+ break;
4326
6512
  default: changeTree(tx.draft["contain-relations"], m, tx.draft);
4327
6513
  }
4328
6514
  }
@@ -4375,12 +6561,15 @@ function applyTextEdits(text, edits) {
4375
6561
  }
4376
6562
  //#endregion
4377
6563
  //#region src/dsl/document/validation.ts
4378
- /** A caption selection by the DSL's rules, which must also select a nonempty run of the live body. */
4379
- function validateSelection(doc, state, captionId, value) {
4380
- assertReferenceAdmitted(state, "caption-entities", captionId, "selection", value);
4381
- const resolved = resolveContent(doc, value);
4382
- if (!resolved || resolved.start === resolved.end) throw new Error("Expected a nonempty range in the current body");
4383
- captionContentCodec.encode(value);
6564
+ /**
6565
+ * A selection the draft wrote whole, by the DSL's rules: its segment must exist and what it
6566
+ * selects must not be empty. A run's Cursors must lie on that segment's live Text.
6567
+ */
6568
+ function validateSelection(doc, state, table, id, value) {
6569
+ assertReferenceAdmitted(state, table, id, selectionField[table], value);
6570
+ const selection = value;
6571
+ 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");
6572
+ (table === "caption-entities" ? captionContentCodec : phonemeCodec).encode(selection);
4384
6573
  }
4385
6574
  /**
4386
6575
  * The DSL's admission of the whole draft, then what only this document can check: containment
@@ -4412,8 +6601,12 @@ function validateDraft(tracked) {
4412
6601
  }
4413
6602
  //#endregion
4414
6603
  //#region src/dsl/document/transact.ts
4415
- function captionRow(doc, captionId) {
4416
- const row = doc.getMap("caption-entities").get(captionId);
6604
+ const codecs = {
6605
+ "caption-entities": captionContentCodec,
6606
+ "phonetic-script-entities": phonemeCodec
6607
+ };
6608
+ function entityRow(doc, table, id) {
6609
+ const row = doc.getMap(table).get(id);
4417
6610
  return isContainer(row) && row.kind() === "Map" ? row : void 0;
4418
6611
  }
4419
6612
  /** Serialization happens before preparing any writes, so invalid/cyclic audit data cannot partly commit. */
@@ -4434,8 +6627,8 @@ const nativeHookSupport = /* @__PURE__ */ new WeakMap();
4434
6627
  /**
4435
6628
  * Every write relies on the patched Mirror's `applyNative` (patches/loro-mirror@2.3.3.patch). An
4436
6629
  * unpatched Mirror ignores the option and commits only the draft, silently dropping Text, ranges
4437
- * and Caption selections. The patch runs the hook even for an empty draft without committing, so
4438
- * probe each Mirror class once on a scratch document and refuse before the first real write.
6630
+ * and selections. The patch runs the hook even for an empty draft without committing, so probe
6631
+ * each Mirror class once on a scratch document and refuse before the first real write.
4439
6632
  */
4440
6633
  function assertNativeHook(mirror) {
4441
6634
  const Impl = mirror.constructor;
@@ -4469,57 +6662,75 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4469
6662
  mirror.setState((raw) => {
4470
6663
  const tracked = new TransactionDraft(raw, before);
4471
6664
  setReject((error) => tracked.fail(error));
4472
- const useBody = (scriptId, caption = false) => {
4473
- assertId(scriptId);
4474
- const entity = requireEntity(tracked.draft, caption ? "caption-entities" : "audio-script-entities", scriptId);
4475
- if (tracked.deleted.has(scriptId)) throw new Error("Cannot use a deleted body");
4476
- const body = entity[caption ? "text" : "body"];
4477
- if (typeof body !== "string") throw new Error("Missing Text body");
4478
- if (caption && tracked.removedCaptionTexts.has(scriptId)) throw new Error("Cannot edit a removed Text");
4479
- tracked.textPreviews.set(scriptId, body);
4480
- return body;
6665
+ /** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
6666
+ const useText = (target) => {
6667
+ let text;
6668
+ if ("captionId" in target) {
6669
+ assertId(target.captionId);
6670
+ const entity = requireEntity(tracked.draft, "caption-entities", target.captionId);
6671
+ if (tracked.deleted.has(target.captionId)) throw new Error("Cannot use a deleted Text");
6672
+ if (tracked.removedCaptionTexts.has(target.captionId)) throw new Error("Cannot edit a removed Text");
6673
+ text = entity.text;
6674
+ } else {
6675
+ assertId(target.audioScriptId);
6676
+ if (tracked.deleted.has(target.audioScriptId)) throw new Error("Cannot use a deleted Text");
6677
+ text = requireSegment(tracked.draft, target.audioScriptId, target.segmentId).text;
6678
+ }
6679
+ if (typeof text !== "string") throw new Error("Missing Text");
6680
+ tracked.textPreviews.set(textKey(target), text);
6681
+ return text;
4481
6682
  };
4482
- const flushCaptions = () => {
4483
- for (const id of tracked.dirtyCaptions) {
4484
- const row = raw["caption-entities"][id];
6683
+ const textBefore = (target) => {
6684
+ if ("captionId" in target) return before["caption-entities"][target.captionId]?.text;
6685
+ return before["audio-script-entities"][target.audioScriptId]?.segments?.find((segment) => segment.id === target.segmentId)?.text;
6686
+ };
6687
+ const flushSelections = () => {
6688
+ for (const [table, id] of tracked.dirtySelections()) {
6689
+ const row = raw[table][id];
4485
6690
  if (!row) continue;
4486
- const value = row.selection;
4487
- const old = before["caption-entities"][id]?.selection;
6691
+ const field = selectionField[table];
6692
+ const value = row[field];
6693
+ const old = before[table][id]?.[field];
4488
6694
  if (isInvalidAtomic(value)) {
4489
6695
  if (isInvalidAtomic(old) && old.raw === value.raw) continue;
4490
- throw new Error("Invalid Caption selection");
4491
- }
4492
- if (value !== void 0) {
4493
- validateSelection(doc, raw, id, value);
4494
- useBody(value.audioScriptId);
6696
+ throw new Error(`Invalid ${field}`);
4495
6697
  }
6698
+ if (value !== void 0) validateSelection(doc, raw, table, id, value);
4496
6699
  steps.push({
4497
- type: "caption",
6700
+ type: "selection",
6701
+ table,
4498
6702
  id,
4499
6703
  value: copyInput(value)
4500
6704
  });
4501
6705
  }
4502
- tracked.dirtyCaptions.clear();
6706
+ tracked.clearDirtySelections();
4503
6707
  };
4504
- const editBody = (scriptId, edits, expectedBodyId, caption) => {
6708
+ const editBody = (target, edits, expectedBodyId) => {
4505
6709
  tracked.run(() => {
4506
- flushCaptions();
4507
- const body = useBody(scriptId, caption);
4508
- if (!bodies.has(scriptId)) bodies.set(scriptId, liveText(doc, scriptId, caption));
4509
- const native = bodies.get(scriptId);
6710
+ flushSelections();
6711
+ const text = useText(target);
6712
+ const key = textKey(target);
6713
+ if (!bodies.has(key)) bodies.set(key, liveText(doc, target));
6714
+ const native = bodies.get(key);
4510
6715
  if (expectedBodyId !== void 0 && native?.id !== expectedBodyId) throw new Error("Stale or missing Text body");
4511
- if ((caption ? before["caption-entities"][scriptId]?.text : before["audio-script-entities"][scriptId]?.body) !== void 0 && !native) throw new Error("Missing live Text body");
4512
- const prepared = prepareTextEdits(body, copyInput(edits));
4513
- tracked.textPreviews.set(scriptId, prepared.body);
6716
+ if (textBefore(target) !== void 0 && !native) throw new Error("Missing live Text body");
6717
+ const prepared = prepareTextEdits(text, copyInput(edits));
6718
+ tracked.textPreviews.set(key, prepared.text);
4514
6719
  steps.push({
4515
6720
  type: "text",
4516
- scriptId,
4517
- caption,
6721
+ target,
4518
6722
  expectedBodyId: native?.id,
4519
6723
  edits: prepared.edits
4520
6724
  });
4521
6725
  });
4522
6726
  };
6727
+ /** A script's segments as this transaction leaves them; never a Mirror-tracked write. */
6728
+ const segmentsOf = (audioScriptId) => {
6729
+ assertId(audioScriptId);
6730
+ requireEntity(tracked.draft, "audio-script-entities", audioScriptId);
6731
+ if (tracked.deleted.has(audioScriptId)) throw new Error("Cannot change a deleted AudioScript");
6732
+ return raw["audio-script-entities"][audioScriptId].segments;
6733
+ };
4523
6734
  const tx = Object.freeze({
4524
6735
  get draft() {
4525
6736
  return tracked.run(() => tracked.draft);
@@ -4527,29 +6738,98 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4527
6738
  applyMutations(mutations) {
4528
6739
  tracked.run(() => applyMutations(tx, mutations));
4529
6740
  },
4530
- editText(scriptId, edits, expectedBodyId) {
4531
- editBody(scriptId, edits, expectedBodyId, false);
6741
+ editText(audioScriptId, segmentId, edits, expectedBodyId) {
6742
+ editBody({
6743
+ audioScriptId,
6744
+ segmentId
6745
+ }, edits, expectedBodyId);
4532
6746
  },
4533
6747
  editCaptionText(captionId, edits, expectedBodyId) {
4534
- editBody(captionId, edits, expectedBodyId, true);
6748
+ editBody({ captionId }, edits, expectedBodyId);
6749
+ },
6750
+ insertSegment(audioScriptId, segmentId, text, index, time) {
6751
+ tracked.run(() => {
6752
+ assertId(segmentId);
6753
+ if (typeof text !== "string") throw new Error("Segment text is a string");
6754
+ const segment = {
6755
+ id: segmentId,
6756
+ text,
6757
+ ...time
6758
+ };
6759
+ assertScriptSegment(segment);
6760
+ const segments = segmentsOf(audioScriptId);
6761
+ if (segments.some((segment) => segment.id === segmentId)) throw new Error(`Segment already exists: ${segmentId}`);
6762
+ const at = index ?? segments.length;
6763
+ if (!Number.isSafeInteger(at) || at < 0 || at > segments.length) throw new Error("Invalid segment index");
6764
+ segments.splice(at, 0, segment);
6765
+ });
6766
+ },
6767
+ moveSegment(audioScriptId, segmentId, index) {
6768
+ tracked.run(() => {
6769
+ const segments = segmentsOf(audioScriptId);
6770
+ const from = segments.findIndex((segment) => segment.id === segmentId);
6771
+ if (from < 0) throw new Error(`Missing segment ${segmentId} of ${audioScriptId}`);
6772
+ if (!Number.isSafeInteger(index) || index < 0 || index >= segments.length) throw new Error("Invalid segment index");
6773
+ const [segment] = segments.splice(from, 1);
6774
+ segments.splice(index, 0, segment);
6775
+ });
6776
+ },
6777
+ deleteSegment(audioScriptId, segmentId) {
6778
+ tracked.run(() => {
6779
+ const segments = segmentsOf(audioScriptId);
6780
+ const at = segments.findIndex((segment) => segment.id === segmentId);
6781
+ if (at < 0) throw new Error(`Missing segment ${segmentId} of ${audioScriptId}`);
6782
+ if (tracked.textPreviews.has(textKey({
6783
+ audioScriptId,
6784
+ segmentId
6785
+ }))) throw new Error("Cannot delete a segment whose text this transaction edits");
6786
+ segments.splice(at, 1);
6787
+ });
4535
6788
  },
4536
6789
  setCaptionContent(selection) {
4537
6790
  tracked.run(() => {
4538
- flushCaptions();
4539
- const { captionId, audioScriptId, start, end } = selection;
6791
+ flushSelections();
6792
+ const { captionId, audioScriptId, segmentId, start, end } = selection;
4540
6793
  assertId(captionId);
4541
- assertCaptionEndpoints(raw, selection);
4542
- assertCaptionSelection(raw, selection, useBody(audioScriptId));
6794
+ assertCaptionSelection(raw, selection, useText({
6795
+ audioScriptId,
6796
+ segmentId
6797
+ }));
4543
6798
  steps.push({
4544
6799
  type: "range",
4545
- selection: {
4546
- captionId,
6800
+ table: "caption-entities",
6801
+ id: captionId,
6802
+ range: {
4547
6803
  audioScriptId,
6804
+ segmentId,
4548
6805
  start,
4549
6806
  end
4550
6807
  }
4551
6808
  });
4552
6809
  });
6810
+ },
6811
+ setPhoneme(selection) {
6812
+ tracked.run(() => {
6813
+ flushSelections();
6814
+ const { phoneticScriptId, audioScriptId, segmentId, start, end, phonemeScript } = selection;
6815
+ assertId(phoneticScriptId);
6816
+ assertPhonemeSelection(raw, selection, useText({
6817
+ audioScriptId,
6818
+ segmentId
6819
+ }));
6820
+ steps.push({
6821
+ type: "range",
6822
+ table: "phonetic-script-entities",
6823
+ id: phoneticScriptId,
6824
+ range: {
6825
+ audioScriptId,
6826
+ segmentId,
6827
+ start,
6828
+ end
6829
+ },
6830
+ ...phonemeScript === void 0 ? {} : { phonemeScript }
6831
+ });
6832
+ });
4553
6833
  }
4554
6834
  });
4555
6835
  try {
@@ -4562,34 +6842,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4562
6842
  }
4563
6843
  tracked.assertValid();
4564
6844
  if (doc.getPendingTxnLength()) throw new Error("Native writes are not allowed during transaction preparation");
4565
- flushCaptions();
6845
+ flushSelections();
4566
6846
  for (const id of tracked.entities.get("caption-entities")?.keys() ?? []) {
4567
6847
  const caption = tracked.draft["caption-entities"][id];
4568
6848
  if (caption) caption.style ??= {};
4569
6849
  }
4570
6850
  validateDraft(tracked);
4571
- for (const step of steps) if (step.type === "text") requireEntity(raw, step.caption ? "caption-entities" : "audio-script-entities", step.scriptId);
4572
- const referenceIds = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
4573
- const changedCaptions = /* @__PURE__ */ new Set();
4574
- for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
4575
- for (const step of steps) {
4576
- if (step.type === "range") {
4577
- referenceIds.add(step.selection.captionId);
4578
- changedCaptions.add(step.selection.captionId);
4579
- }
4580
- if (step.type === "caption") {
4581
- changedCaptions.add(step.id);
4582
- if (step.value) referenceIds.add(step.id);
4583
- else referenceIds.delete(step.id);
4584
- }
4585
- if (step.type === "text" && step.caption) changedCaptions.add(step.scriptId);
4586
- }
4587
- for (const id of changedCaptions) if (raw["caption-entities"][id]) assertCaptionSource(raw["caption-entities"][id], referenceIds.has(id));
4588
- for (const step of steps) if (step.type === "range") assertCaptionEndpoints(raw, step.selection);
4589
- else if (step.type === "caption" && step.value) assertCaptionEndpoints(raw, {
4590
- captionId: step.id,
4591
- audioScriptId: step.value.audioScriptId
4592
- });
6851
+ for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
6852
+ else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
6853
+ assertSelectionsAfter(raw, before, tracked, steps);
4593
6854
  for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
4594
6855
  const row = raw["caption-entities"][id];
4595
6856
  if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
@@ -4597,12 +6858,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4597
6858
  delete row.text;
4598
6859
  }
4599
6860
  }
4600
- for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
4601
- const row = raw["caption-entities"][id];
4602
- if (!row || !fields.has("selection")) continue;
4603
- const old = before["caption-entities"][id];
4604
- if (old?.selection === void 0) delete row.selection;
4605
- else row.selection = old.selection;
6861
+ for (const table of Object.keys(selectionField)) {
6862
+ const field = selectionField[table];
6863
+ for (const [id, fields] of tracked.entities.get(table) ?? []) {
6864
+ const row = raw[table][id];
6865
+ if (!row || !fields.has(field)) continue;
6866
+ const old = before[table][id]?.[field];
6867
+ if (old === void 0) delete row[field];
6868
+ else row[field] = old;
6869
+ }
4606
6870
  }
4607
6871
  });
4608
6872
  } finally {
@@ -4612,45 +6876,104 @@ function runTransaction(doc, mirror, edit, options, setReject) {
4612
6876
  }, {
4613
6877
  ...options,
4614
6878
  applyNative: (doc) => {
4615
- const bodyFor = (scriptId, caption = false) => {
4616
- let text = bodies.get(scriptId);
6879
+ const textFor = (target) => {
6880
+ const key = textKey(target);
6881
+ let text = bodies.get(key);
4617
6882
  if (!text) {
4618
- text = liveText(doc, scriptId, caption);
4619
- bodies.set(scriptId, text);
6883
+ text = liveText(doc, target);
6884
+ bodies.set(key, text);
4620
6885
  }
4621
- if (!text || text.isDeleted()) throw new Error("Prepared Text body is missing");
6886
+ if (!text || text.isDeleted()) throw new Error("Prepared Text is missing");
4622
6887
  return text;
4623
6888
  };
4624
6889
  for (const [id, value] of newCaptionTexts) {
4625
- const body = doc.getMap("caption-entities").get(id).setContainer("text", new LoroText());
4626
- if (value.length) body.insert(0, value);
4627
- bodies.set(id, body);
6890
+ const text = doc.getMap("caption-entities").get(id).setContainer("text", new LoroText());
6891
+ if (value.length) text.insert(0, value);
6892
+ bodies.set(textKey({ captionId: id }), text);
4628
6893
  }
4629
- for (const step of steps) if (step.type === "text") {
4630
- const text = bodyFor(step.scriptId, step.caption);
4631
- if (step.expectedBodyId && text.id !== step.expectedBodyId) throw new Error("Stale Text body");
4632
- applyTextEdits(text, step.edits);
4633
- } else if (step.type === "range") {
4634
- const { captionId, audioScriptId, start, end } = step.selection;
4635
- const text = bodyFor(audioScriptId);
4636
- const from = text?.getCursor(start, 0), to = text?.getCursor(end, 0);
4637
- const row = captionRow(doc, captionId);
4638
- if (!from || !to || !row) throw new Error("Unable to construct prepared range");
4639
- row.set("selection", captionContentCodec.encode({
4640
- audioScriptId,
4641
- start: from,
4642
- end: to
4643
- }));
4644
- } else {
4645
- const row = captionRow(doc, step.id);
4646
- if (step.value !== void 0) {
4647
- if (!row) throw new Error("Prepared Caption is missing");
4648
- row.set("selection", captionContentCodec.encode(step.value));
4649
- } else if (row?.get("selection") !== void 0) row.delete("selection");
6894
+ for (const step of steps) {
6895
+ if (step.type === "text") {
6896
+ const text = textFor(step.target);
6897
+ if (step.expectedBodyId && text.id !== step.expectedBodyId) throw new Error("Stale Text body");
6898
+ applyTextEdits(text, step.edits);
6899
+ continue;
6900
+ }
6901
+ const row = entityRow(doc, step.table, step.id);
6902
+ const field = selectionField[step.table];
6903
+ if (step.type === "range") {
6904
+ const { audioScriptId, segmentId, start, end } = step.range;
6905
+ const value = {
6906
+ audioScriptId,
6907
+ segmentId
6908
+ };
6909
+ if (start !== void 0 && end !== void 0) {
6910
+ const text = textFor({
6911
+ audioScriptId,
6912
+ segmentId
6913
+ });
6914
+ value.start = text.getCursor(start, 0);
6915
+ value.end = text.getCursor(end, 0);
6916
+ if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
6917
+ }
6918
+ if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
6919
+ if (!row) throw new Error("Unable to construct prepared range");
6920
+ row.set(field, codecs[step.table].encode(value));
6921
+ } else if (step.value !== void 0) {
6922
+ if (!row) throw new Error("Prepared selection owner is missing");
6923
+ row.set(field, codecs[step.table].encode(step.value));
6924
+ } else if (row?.get(field) !== void 0) row.delete(field);
4650
6925
  }
4651
6926
  }
4652
6927
  });
4653
6928
  }
6929
+ /**
6930
+ * What only the whole write can say about selections, with the native steps counted in order:
6931
+ * each Caption keeps one source of words, each selection's segment still exists, and every
6932
+ * PhoneticScript the write touched is written for a segment and stores its text as a file.
6933
+ */
6934
+ function assertSelectionsAfter(raw, before, tracked, steps) {
6935
+ const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
6936
+ const changedCaptions = /* @__PURE__ */ new Set();
6937
+ for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
6938
+ const phonemes = /* @__PURE__ */ new Map();
6939
+ for (const step of steps) {
6940
+ if (step.type === "text") {
6941
+ if ("captionId" in step.target) changedCaptions.add(step.target.captionId);
6942
+ continue;
6943
+ }
6944
+ const value = step.type === "range" ? {
6945
+ ...step.range,
6946
+ ...step.phonemeScript === void 0 ? {} : { phonemeScript: step.phonemeScript }
6947
+ } : step.value;
6948
+ if (step.table === "caption-entities") {
6949
+ changedCaptions.add(step.id);
6950
+ if (value) selected.add(step.id);
6951
+ else selected.delete(step.id);
6952
+ if (value) assertCaptionEndpoints(raw, {
6953
+ captionId: step.id,
6954
+ ...value
6955
+ });
6956
+ } else {
6957
+ phonemes.set(step.id, value);
6958
+ if (value) assertPhonemeEndpoints(raw, {
6959
+ phoneticScriptId: step.id,
6960
+ ...value
6961
+ });
6962
+ }
6963
+ }
6964
+ for (const id of changedCaptions) if (raw["caption-entities"][id]) assertCaptionSource(raw["caption-entities"][id], selected.has(id));
6965
+ const phonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...phonemes.keys()]);
6966
+ for (const id of phonetics) {
6967
+ const row = raw["phonetic-script-entities"][id];
6968
+ if (!row) continue;
6969
+ const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
6970
+ const old = before["phonetic-script-entities"][id];
6971
+ assertPhoneticScript(old, {
6972
+ ...row,
6973
+ phoneme: isRecord(phoneme) ? phoneme : void 0
6974
+ });
6975
+ }
6976
+ }
4654
6977
  //#endregion
4655
6978
  //#region src/dsl/document/medeo-dsl.ts
4656
6979
  const contentRoots = new Set([
@@ -4741,17 +7064,23 @@ var MedeoDsl = class MedeoDsl {
4741
7064
  applyMutations(mutations, audit) {
4742
7065
  this.transact((tx) => tx.applyMutations(mutations), audit);
4743
7066
  }
4744
- editText(scriptId, expectedBodyId, edits, audit) {
4745
- this.transact((tx) => tx.editText(scriptId, edits, expectedBodyId), audit);
7067
+ editText(scriptId, segmentId, expectedBodyId, edits, audit) {
7068
+ this.transact((tx) => tx.editText(scriptId, segmentId, edits, expectedBodyId), audit);
4746
7069
  }
4747
7070
  editCaptionText(captionId, expectedBodyId, edits, audit) {
4748
7071
  this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
4749
7072
  }
4750
- /** Local ranges exclude insertions at their start and include insertions at their end. K01 remains deferred. */
4751
- textRange(scriptId, start, end) {
7073
+ /**
7074
+ * A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
7075
+ * include insertions at their end. K01 remains deferred.
7076
+ */
7077
+ textRange(scriptId, segmentId, start, end) {
4752
7078
  this.assertActive();
4753
- const text = liveText(this.doc, scriptId);
4754
- if (!text) throw new Error("Missing AudioScript body");
7079
+ const text = liveText(this.doc, {
7080
+ audioScriptId: scriptId,
7081
+ segmentId
7082
+ });
7083
+ if (!text) throw new Error("Missing AudioScript segment");
4755
7084
  assertTextOffset(text.toString(), start);
4756
7085
  assertTextOffset(text.toString(), end);
4757
7086
  if (start >= end) throw new Error("Expected a nonempty range");
@@ -4759,6 +7088,7 @@ var MedeoDsl = class MedeoDsl {
4759
7088
  if (!from || !to) throw new Error("Unable to construct text range");
4760
7089
  return {
4761
7090
  audioScriptId: scriptId,
7091
+ segmentId,
4762
7092
  start: from,
4763
7093
  end: to
4764
7094
  };
@@ -4771,42 +7101,186 @@ var MedeoDsl = class MedeoDsl {
4771
7101
  this.listeners.clear();
4772
7102
  if (this.ownsDoc) this.doc.free();
4773
7103
  }
4774
- mutate(run) {
4775
- this.assertActive();
4776
- if (this.writing || this.notifying) {
4777
- const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
4778
- this.rejectTransaction?.(error);
4779
- throw error;
4780
- }
4781
- if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
4782
- const apply = () => {
4783
- this.writing = true;
4784
- try {
4785
- run();
4786
- } finally {
4787
- this.rejectTransaction = void 0;
4788
- this.writing = false;
4789
- if (this.dirty) this.snapshot = void 0;
4790
- this.notify();
7104
+ mutate(run) {
7105
+ this.assertActive();
7106
+ if (this.writing || this.notifying) {
7107
+ const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
7108
+ this.rejectTransaction?.(error);
7109
+ throw error;
7110
+ }
7111
+ if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
7112
+ const apply = () => {
7113
+ this.writing = true;
7114
+ try {
7115
+ run();
7116
+ } finally {
7117
+ this.rejectTransaction = void 0;
7118
+ this.writing = false;
7119
+ if (this.dirty) this.snapshot = void 0;
7120
+ this.notify();
7121
+ }
7122
+ };
7123
+ if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
7124
+ else apply();
7125
+ }
7126
+ notify() {
7127
+ if (this.disposed || !this.dirty) return;
7128
+ this.dirty = false;
7129
+ this.notifying = true;
7130
+ try {
7131
+ for (const listener of this.listeners) listener(this.changeSource);
7132
+ } finally {
7133
+ this.notifying = false;
7134
+ }
7135
+ }
7136
+ assertActive() {
7137
+ if (this.disposed) throw new Error("MedeoDsl is disposed");
7138
+ }
7139
+ };
7140
+ //#endregion
7141
+ //#region src/dsl/projection/audio-scripts.ts
7142
+ /**
7143
+ * The scripts a recording says. It follows derived-from from the recording; a PhoneticScript is a
7144
+ * variant, so its words are the base its phoneme names, and derived-from is not followed past it.
7145
+ * When nothing is found that way, a script read off the recording itself is what it says.
7146
+ */
7147
+ function spokenScripts(snapshot, recordingId) {
7148
+ const relations = Object.values(snapshot["derived-from-relations"]);
7149
+ const found = /* @__PURE__ */ new Map();
7150
+ const seen = /* @__PURE__ */ new Set();
7151
+ const visit = (id, phoneticScriptId) => {
7152
+ if (seen.has(id)) return;
7153
+ seen.add(id);
7154
+ if (snapshot["audio-script-entities"][id]) {
7155
+ if (!found.has(id)) found.set(id, phoneticScriptId);
7156
+ return;
7157
+ }
7158
+ const phonetic = snapshot["phonetic-script-entities"][id];
7159
+ if (phonetic) {
7160
+ visit(phonetic.phoneme.audioScriptId, id);
7161
+ return;
7162
+ }
7163
+ for (const r of relations) if (r.entityId === id) visit(r.sourceId, phoneticScriptId);
7164
+ };
7165
+ visit(recordingId);
7166
+ if (!found.size) {
7167
+ for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
7168
+ }
7169
+ return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
7170
+ audioScriptId,
7171
+ phoneticScriptId
7172
+ });
7173
+ }
7174
+ /** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
7175
+ function toAudioScriptViews(snapshot) {
7176
+ const views = /* @__PURE__ */ new Map();
7177
+ const segmentIndex = (scriptId, segmentId) => {
7178
+ const index = snapshot["audio-script-entities"][scriptId]?.segments.findIndex((s) => s.id === segmentId) ?? -1;
7179
+ return index < 0 ? void 0 : index;
7180
+ };
7181
+ for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) views.set(id, {
7182
+ id,
7183
+ segments: script.segments.map((segment) => {
7184
+ const rich = snapshot.richText[segment.id];
7185
+ const language = rich ? markedLanguage(langsFromDelta(rich.delta)) : void 0;
7186
+ return {
7187
+ id: segment.id,
7188
+ text: segment.text,
7189
+ ...language === void 0 ? {} : { language },
7190
+ ...segment.startMs === void 0 ? {} : {
7191
+ startMs: segment.startMs,
7192
+ endMs: segment.endMs
7193
+ }
7194
+ };
7195
+ }),
7196
+ sourceMediaIds: [],
7197
+ phoneticScripts: [],
7198
+ speeches: [],
7199
+ captions: []
7200
+ });
7201
+ for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
7202
+ for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
7203
+ const { audioScriptId, segmentId, phonemeScript } = phonetic.phoneme;
7204
+ const { digest } = phonetic;
7205
+ const segment = segmentIndex(audioScriptId, segmentId);
7206
+ const text = snapshot.phoneticText[id]?.text;
7207
+ views.get(audioScriptId)?.phoneticScripts.push({
7208
+ id,
7209
+ ...segment === void 0 ? {} : { segment },
7210
+ ...text === void 0 ? {} : { text },
7211
+ ...phonemeScript === void 0 ? {} : { phonemeScript },
7212
+ ...digest === void 0 ? {} : { digest }
7213
+ });
7214
+ }
7215
+ const layout = resolveDslLayout(snapshot);
7216
+ for (const view of views.values()) placeTranscript(snapshot, layout, view);
7217
+ const timbres = Object.values(snapshot["voice-timbre-relations"]);
7218
+ for (const track of layout.tracks) for (const { clipId } of track.clips) {
7219
+ const source = snapshot["clip-entities"][clipId]?.source;
7220
+ if (!source) continue;
7221
+ if (snapshot["media-entities"][source]?.kind === "voiceover-recording") {
7222
+ const voices = timbres.filter((r) => r.recordingId === source).flatMap((r) => {
7223
+ const voice = snapshot["voice-entities"][r.voiceId];
7224
+ if (!voice) return [];
7225
+ return [voice.name === void 0 ? {
7226
+ voiceId: r.voiceId,
7227
+ key: voice.key
7228
+ } : {
7229
+ voiceId: r.voiceId,
7230
+ key: voice.key,
7231
+ name: voice.name
7232
+ }];
7233
+ });
7234
+ for (const spoken of spokenScripts(snapshot, source)) {
7235
+ const applied = appliedSpeech(snapshot, source, spoken);
7236
+ views.get(spoken.audioScriptId)?.speeches.push({
7237
+ clipId,
7238
+ recordingId: source,
7239
+ ...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
7240
+ voices,
7241
+ ...applied === void 0 ? {} : { applied }
7242
+ });
4791
7243
  }
4792
- };
4793
- if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
4794
- else apply();
4795
- }
4796
- notify() {
4797
- if (this.disposed || !this.dirty) return;
4798
- this.dirty = false;
4799
- this.notifying = true;
4800
- try {
4801
- for (const listener of this.listeners) listener(this.changeSource);
4802
- } finally {
4803
- this.notifying = false;
7244
+ continue;
4804
7245
  }
7246
+ const caption = snapshot["caption-entities"][source];
7247
+ if (!caption?.selection || caption.text !== void 0 || !snapshot.captionText[source]) continue;
7248
+ const { audioScriptId, segmentId } = caption.selection;
7249
+ const segment = segmentIndex(audioScriptId, segmentId);
7250
+ if (segment !== void 0) views.get(audioScriptId)?.captions.push({
7251
+ clipId,
7252
+ captionId: source,
7253
+ segment
7254
+ });
4805
7255
  }
4806
- assertActive() {
4807
- if (this.disposed) throw new Error("MedeoDsl is disposed");
7256
+ return [...views.values()];
7257
+ }
7258
+ /**
7259
+ * A transcript's segments on the Timeline: a segment's time is in the medium the script was read
7260
+ * off, so it lands where the earliest Clip playing that medium holds that moment in its window.
7261
+ */
7262
+ function placeTranscript(snapshot, layout, view) {
7263
+ if (!view.sourceMediaIds.length) return;
7264
+ const plays = layout.tracks.flatMap((track) => track.clips).filter((clip) => view.sourceMediaIds.includes(snapshot["clip-entities"][clip.clipId]?.source ?? "")).sort((a, b) => a.startMs - b.startMs);
7265
+ for (const segment of view.segments) {
7266
+ const at = segment.startMs;
7267
+ if (at === void 0) continue;
7268
+ const clip = plays.find((c) => c.inMs <= at && at < c.outMs);
7269
+ if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
4808
7270
  }
4809
- };
7271
+ }
7272
+ /**
7273
+ * Whether a recording still matches what it was synthesized from: the digest its derived-from
7274
+ * records against the source's file digest now. A recording derived from a script directly is
7275
+ * not judged: a written script has no file, and a transcript's file is what it was read from,
7276
+ * not what a take was synthesized from (D190).
7277
+ */
7278
+ function appliedSpeech(snapshot, recordingId, spoken) {
7279
+ const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
7280
+ const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
7281
+ const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
7282
+ return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
7283
+ }
4810
7284
  //#endregion
4811
7285
  //#region src/dsl/voiceover-actions.ts
4812
7286
  function voiceoverPlan(s, input, result) {
@@ -4820,16 +7294,28 @@ function voiceoverPlan(s, input, result) {
4820
7294
  id: result.mediaId,
4821
7295
  value: input.media
4822
7296
  }];
4823
- if (input.script.body !== void 0) mutations.push({
4824
- type: "create-entity",
4825
- table: "audio-script-entities",
4826
- id: result.audioScriptId,
4827
- value: {
4828
- body: input.script.body,
4829
- ..."defaultLanguage" in input.script && input.script.defaultLanguage !== void 0 ? { defaultLanguage: input.script.defaultLanguage } : {}
4830
- }
4831
- });
4832
- else if (!s["audio-script-entities"][result.audioScriptId]) throw new Error("Missing AudioScript");
7297
+ let segmentIds;
7298
+ if (input.script.segments !== void 0) {
7299
+ if (result.segmentIds.length !== input.script.segments.length) throw new Error("Expected one id per segment");
7300
+ const segments = input.script.segments;
7301
+ segmentIds = result.segmentIds;
7302
+ mutations.push({
7303
+ type: "create-entity",
7304
+ table: "audio-script-entities",
7305
+ id: result.audioScriptId,
7306
+ value: {
7307
+ segments: segments.map((text, i) => ({
7308
+ id: segmentIds[i],
7309
+ text
7310
+ })),
7311
+ ..."defaultLanguage" in input.script && input.script.defaultLanguage !== void 0 ? { defaultLanguage: input.script.defaultLanguage } : {}
7312
+ }
7313
+ });
7314
+ } else {
7315
+ const script = s["audio-script-entities"][result.audioScriptId];
7316
+ if (!script) throw new Error("Missing AudioScript");
7317
+ segmentIds = script.segments.map((segment) => segment.id);
7318
+ }
4833
7319
  const phoneticScriptId = input.script.phoneticScriptId;
4834
7320
  if (phoneticScriptId !== void 0 && s["phonetic-script-entities"][phoneticScriptId]?.phoneme.audioScriptId !== result.audioScriptId) throw new Error("The PhoneticScript must name this AudioScript as its base");
4835
7321
  validateWindow({
@@ -4896,7 +7382,7 @@ function voiceoverPlan(s, input, result) {
4896
7382
  mutations.push(...placementMutations(s, result.clipId, input.placement));
4897
7383
  uniqueIds(result.captionClipIds);
4898
7384
  const retained = new Set(result.captionClipIds);
4899
- const oldCaptions = anchoredTo(s, new Set([result.clipId])).filter((id) => requireTrack(s, trackOf(s, id)).role === "caption");
7385
+ const oldCaptions = existing ? captionsShowing(s, existing.source) : [];
4900
7386
  mutations.push(...deleteClips(s, oldCaptions.filter((id) => !retained.has(id)), "cascade"));
4901
7387
  const placement = mutations.find((m) => m.type === "patch-entity" && m.table === "clip-entities" && m.id === result.clipId && "anchor" in m.patch);
4902
7388
  const anchor = placement?.type === "patch-entity" && placement.table === "clip-entities" ? placement.patch.anchor : void 0;
@@ -4912,6 +7398,8 @@ function voiceoverPlan(s, input, result) {
4912
7398
  }, input.media.durationMs);
4913
7399
  const previous = s["clip-entities"][clipId];
4914
7400
  if (previous && !oldCaptions.includes(clipId)) throw new Error("Cannot overwrite another use’s Caption Clip");
7401
+ const segmentId = segmentIds[caption.segment];
7402
+ if (segmentId === void 0) throw new Error(`Missing segment ${caption.segment} of the voiceover's script`);
4915
7403
  const previousSource = previous?.source;
4916
7404
  const style = caption.style ?? (previousSource ? s["caption-entities"][previousSource]?.style : void 0);
4917
7405
  mutations.push({
@@ -4923,8 +7411,11 @@ function voiceoverPlan(s, input, result) {
4923
7411
  type: "set-caption-content",
4924
7412
  captionId,
4925
7413
  audioScriptId: result.audioScriptId,
4926
- start: caption.range.start,
4927
- end: caption.range.end
7414
+ segmentId,
7415
+ ...caption.range === void 0 ? {} : {
7416
+ start: caption.range.start,
7417
+ end: caption.range.end
7418
+ }
4928
7419
  });
4929
7420
  const anchor = {
4930
7421
  to: result.clipId,
@@ -4962,6 +7453,33 @@ function voiceoverPlan(s, input, result) {
4962
7453
  });
4963
7454
  return mutations;
4964
7455
  }
7456
+ /**
7457
+ * Caption Clips showing what a recording says (D183): their Caption selects the run of the segment
7458
+ * the PhoneticScript it was spoken from is written for, or any segment of the script it was read
7459
+ * from directly.
7460
+ */
7461
+ function captionsShowing(s, recordingId) {
7462
+ const said = spokenScripts(s, recordingId).map(({ audioScriptId, phoneticScriptId }) => {
7463
+ if (phoneticScriptId === void 0) return { audioScriptId };
7464
+ const { segmentId } = s["phonetic-script-entities"][phoneticScriptId].phoneme;
7465
+ return {
7466
+ audioScriptId,
7467
+ segmentId,
7468
+ run: s.phoneticText[phoneticScriptId]
7469
+ };
7470
+ });
7471
+ return Object.entries(s["clip-entities"]).flatMap(([id, clip]) => {
7472
+ const selection = s["caption-entities"][clip.source]?.selection;
7473
+ const shown = s.captionText[clip.source];
7474
+ if (!selection) return [];
7475
+ return said.some((words) => {
7476
+ if (selection.audioScriptId !== words.audioScriptId) return false;
7477
+ if (!("segmentId" in words)) return true;
7478
+ if (selection.segmentId !== words.segmentId || !shown || !words.run) return false;
7479
+ return shown.start < words.run.end && words.run.start < shown.end;
7480
+ }) && requireTrack(s, trackOf(s, id)).role === "caption" ? [id] : [];
7481
+ });
7482
+ }
4965
7483
  //#endregion
4966
7484
  //#region src/dsl/dsl-editor.ts
4967
7485
  /** Business edits write DSL facts in one audited transaction; layout is derived on read. */
@@ -5075,30 +7593,76 @@ var DslEditor = class {
5075
7593
  patch: { volume }
5076
7594
  }], options);
5077
7595
  }
5078
- createAudioScript(body, defaultLanguage, options) {
7596
+ /** A script of these segments, in order; each segment gets its own id. */
7597
+ createAudioScript(segments, defaultLanguage, options) {
5079
7598
  const id = this.idFactory("audio-script");
7599
+ const value = {
7600
+ segments: segments.map((text) => ({
7601
+ id: this.idFactory("segment"),
7602
+ text
7603
+ })),
7604
+ ...defaultLanguage === void 0 ? {} : { defaultLanguage }
7605
+ };
5080
7606
  this.write("createAudioScript", {
5081
7607
  id,
5082
- body,
5083
- defaultLanguage
7608
+ ...value
5084
7609
  }, [{
5085
7610
  type: "create-entity",
5086
7611
  table: "audio-script-entities",
5087
7612
  id,
5088
- value: {
5089
- body,
5090
- ...defaultLanguage === void 0 ? {} : { defaultLanguage }
5091
- }
7613
+ value
5092
7614
  }], options);
5093
7615
  return id;
5094
7616
  }
5095
- editAudioScript(scriptId, expectedBodyId, edits, options) {
5096
- this.dsl.editText(scriptId, expectedBodyId, edits, audit("editAudioScript", {
7617
+ editAudioScript(scriptId, segmentId, expectedBodyId, edits, options) {
7618
+ this.dsl.editText(scriptId, segmentId, expectedBodyId, edits, audit("editAudioScript", {
5097
7619
  scriptId,
7620
+ segmentId,
5098
7621
  expectedBodyId,
5099
7622
  edits
5100
7623
  }, options));
5101
7624
  }
7625
+ /** Adds a segment at `index`, the end by default, and returns its id. */
7626
+ insertScriptSegment(scriptId, text, index, options) {
7627
+ const segmentId = this.idFactory("segment");
7628
+ this.write("insertScriptSegment", {
7629
+ scriptId,
7630
+ segmentId,
7631
+ text,
7632
+ index
7633
+ }, [{
7634
+ type: "insert-segment",
7635
+ audioScriptId: scriptId,
7636
+ segmentId,
7637
+ text,
7638
+ ...index === void 0 ? {} : { index }
7639
+ }], options);
7640
+ return segmentId;
7641
+ }
7642
+ /** `index` is measured after removing the segment. */
7643
+ moveScriptSegment(scriptId, segmentId, index, options) {
7644
+ this.write("moveScriptSegment", {
7645
+ scriptId,
7646
+ segmentId,
7647
+ index
7648
+ }, [{
7649
+ type: "move-segment",
7650
+ audioScriptId: scriptId,
7651
+ segmentId,
7652
+ index
7653
+ }], options);
7654
+ }
7655
+ /** Captions and PhoneticScripts selecting the segment keep their selection, which then shows nothing (D72). */
7656
+ deleteScriptSegment(scriptId, segmentId, options) {
7657
+ this.write("deleteScriptSegment", {
7658
+ scriptId,
7659
+ segmentId
7660
+ }, [{
7661
+ type: "delete-segment",
7662
+ audioScriptId: scriptId,
7663
+ segmentId
7664
+ }], options);
7665
+ }
5102
7666
  createCaption(input, options) {
5103
7667
  const id = this.idFactory("caption");
5104
7668
  this.write("createCaption", {
@@ -5115,18 +7679,16 @@ var DslEditor = class {
5115
7679
  }, ...input.text !== void 0 ? [] : [{
5116
7680
  type: "set-caption-content",
5117
7681
  captionId: id,
5118
- audioScriptId: input.audioScriptId,
5119
- start: input.start,
5120
- end: input.end
7682
+ ...segmentRange(input)
5121
7683
  }]], options);
5122
7684
  return id;
5123
7685
  }
5124
- setCaptionContent(captionId, audioScriptId, start, end, options) {
7686
+ /** Show a segment of a script, or a run of it, in place of the Caption's own text. */
7687
+ setCaptionContent(captionId, selection, options) {
7688
+ const range = segmentRange(selection);
5125
7689
  this.write("setCaptionContent", {
5126
7690
  captionId,
5127
- audioScriptId,
5128
- start,
5129
- end
7691
+ ...range
5130
7692
  }, [{
5131
7693
  type: "patch-entity",
5132
7694
  table: "caption-entities",
@@ -5135,9 +7697,7 @@ var DslEditor = class {
5135
7697
  }, {
5136
7698
  type: "set-caption-content",
5137
7699
  captionId,
5138
- audioScriptId,
5139
- start,
5140
- end
7700
+ ...range
5141
7701
  }], options);
5142
7702
  }
5143
7703
  setCaptionText(captionId, text, options) {
@@ -5263,6 +7823,7 @@ var DslEditor = class {
5263
7823
  clipId: input.clipId ?? this.idFactory("clip"),
5264
7824
  mediaId: resourceId("media", input.media.system, input.media.key),
5265
7825
  audioScriptId: this.voiceoverScriptId(input.script),
7826
+ segmentIds: input.script.segments?.map(() => this.idFactory("segment")) ?? [],
5266
7827
  captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
5267
7828
  captionIds: input.captions.map(() => this.idFactory("caption"))
5268
7829
  };
@@ -5280,17 +7841,14 @@ var DslEditor = class {
5280
7841
  return phonetic.phoneme.audioScriptId;
5281
7842
  }
5282
7843
  /**
5283
- * Write a PhoneticScript: create one for `audioScriptId`, or, given `phoneticScriptId`, replace its
5284
- * base and pronunciation together. A `phonemeScript` is stored as a Memota text file first, and
5285
- * the file's facts are written with it; an unchanged one keeps its file. Recordings already spoken
5286
- * from it keep their derived-from.
7844
+ * Write a PhoneticScript: create one for a segment of a script, or a run of it, or, given
7845
+ * `phoneticScriptId`, replace its selection and what to synthesize together (D181). A
7846
+ * `phonemeScript` is stored as a Memota text file first, and the file's facts are written with
7847
+ * it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
5287
7848
  */
5288
7849
  async setPhoneticScript(input, options) {
5289
7850
  const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
5290
- const phoneme = {
5291
- audioScriptId: input.audioScriptId,
5292
- ...input.phonemeScript === void 0 ? {} : { phonemeScript: input.phonemeScript }
5293
- };
7851
+ const range = segmentRange(input);
5294
7852
  const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
5295
7853
  const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
5296
7854
  const cleared = {
@@ -5299,9 +7857,11 @@ var DslEditor = class {
5299
7857
  storageKey: void 0,
5300
7858
  digest: void 0
5301
7859
  };
7860
+ const phonemeScript = input.phonemeScript === void 0 ? {} : { phonemeScript: input.phonemeScript };
5302
7861
  this.write("setPhoneticScript", {
5303
7862
  id,
5304
- phoneme,
7863
+ ...range,
7864
+ ...phonemeScript,
5305
7865
  ...cleared,
5306
7866
  ...file
5307
7867
  }, [exists ? {
@@ -5309,10 +7869,6 @@ var DslEditor = class {
5309
7869
  table: "phonetic-script-entities",
5310
7870
  id,
5311
7871
  patch: {
5312
- phoneme: {
5313
- ...phoneme,
5314
- phonemeScript: input.phonemeScript
5315
- },
5316
7872
  ...cleared,
5317
7873
  ...file
5318
7874
  }
@@ -5320,10 +7876,12 @@ var DslEditor = class {
5320
7876
  type: "create-entity",
5321
7877
  table: "phonetic-script-entities",
5322
7878
  id,
5323
- value: {
5324
- phoneme,
5325
- ...file
5326
- }
7879
+ value: file
7880
+ }, {
7881
+ type: "set-phoneme",
7882
+ phoneticScriptId: id,
7883
+ ...range,
7884
+ ...phonemeScript
5327
7885
  }], options);
5328
7886
  return id;
5329
7887
  }
@@ -5392,6 +7950,15 @@ var DslEditor = class {
5392
7950
  this.dsl.applyMutations(mutations, audit(action, payload, options));
5393
7951
  }
5394
7952
  };
7953
+ /** A selection as a write states it: a run only when both its ends are given. */
7954
+ function segmentRange(input) {
7955
+ return {
7956
+ audioScriptId: input.audioScriptId,
7957
+ segmentId: input.segmentId,
7958
+ ...input.start === void 0 ? {} : { start: input.start },
7959
+ ...input.end === void 0 ? {} : { end: input.end }
7960
+ };
7961
+ }
5395
7962
  function audit(action, payload, options) {
5396
7963
  return {
5397
7964
  semantic_op: action,
@@ -5431,131 +7998,6 @@ function createInitialMedeoDsl(options = {}) {
5431
7998
  }
5432
7999
  }
5433
8000
  //#endregion
5434
- //#region src/dsl/projection/audio-scripts.ts
5435
- /**
5436
- * The scripts a recording says. It follows derived-from from the recording; a PhoneticScript is a
5437
- * variant, so its words are the base its phoneme names, and derived-from is not followed past it.
5438
- * When nothing is found that way, a script read off the recording itself is what it says.
5439
- */
5440
- function spokenScripts(snapshot, recordingId) {
5441
- const relations = Object.values(snapshot["derived-from-relations"]);
5442
- const found = /* @__PURE__ */ new Map();
5443
- const seen = /* @__PURE__ */ new Set();
5444
- const visit = (id, phoneticScriptId) => {
5445
- if (seen.has(id)) return;
5446
- seen.add(id);
5447
- if (snapshot["audio-script-entities"][id]) {
5448
- if (!found.has(id)) found.set(id, phoneticScriptId);
5449
- return;
5450
- }
5451
- const phonetic = snapshot["phonetic-script-entities"][id];
5452
- if (phonetic) {
5453
- visit(phonetic.phoneme.audioScriptId, id);
5454
- return;
5455
- }
5456
- for (const r of relations) if (r.entityId === id) visit(r.sourceId, phoneticScriptId);
5457
- };
5458
- visit(recordingId);
5459
- if (!found.size) {
5460
- for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
5461
- }
5462
- return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
5463
- audioScriptId,
5464
- phoneticScriptId
5465
- });
5466
- }
5467
- /** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
5468
- function toAudioScriptViews(snapshot) {
5469
- const views = /* @__PURE__ */ new Map();
5470
- const bodies = /* @__PURE__ */ new Map();
5471
- for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) {
5472
- const rich = snapshot.richText[id];
5473
- const text = rich ? langsFromDelta(rich.delta) : {
5474
- body: script.body,
5475
- langs: []
5476
- };
5477
- bodies.set(id, text.body);
5478
- views.set(id, {
5479
- id,
5480
- segments: lines(text.body).map((line) => {
5481
- const language = lineLanguage(text, line);
5482
- return language === void 0 ? { text: line.text } : {
5483
- text: line.text,
5484
- language
5485
- };
5486
- }),
5487
- sourceMediaIds: [],
5488
- phoneticScripts: [],
5489
- speeches: [],
5490
- captions: []
5491
- });
5492
- }
5493
- for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
5494
- for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
5495
- const { phonemeScript } = phonetic.phoneme;
5496
- const { digest } = phonetic;
5497
- views.get(phonetic.phoneme.audioScriptId)?.phoneticScripts.push({
5498
- id,
5499
- ...phonemeScript === void 0 ? {} : { phonemeScript },
5500
- ...digest === void 0 ? {} : { digest }
5501
- });
5502
- }
5503
- const timbres = Object.values(snapshot["voice-timbre-relations"]);
5504
- for (const track of resolveDslLayout(snapshot).tracks) for (const { clipId } of track.clips) {
5505
- const source = snapshot["clip-entities"][clipId]?.source;
5506
- if (!source) continue;
5507
- if (snapshot["media-entities"][source]?.kind === "voiceover-recording") {
5508
- const voices = timbres.filter((r) => r.recordingId === source).flatMap((r) => {
5509
- const voice = snapshot["voice-entities"][r.voiceId];
5510
- if (!voice) return [];
5511
- return [voice.name === void 0 ? {
5512
- voiceId: r.voiceId,
5513
- key: voice.key
5514
- } : {
5515
- voiceId: r.voiceId,
5516
- key: voice.key,
5517
- name: voice.name
5518
- }];
5519
- });
5520
- for (const spoken of spokenScripts(snapshot, source)) {
5521
- const applied = appliedSpeech(snapshot, source, spoken);
5522
- views.get(spoken.audioScriptId)?.speeches.push({
5523
- clipId,
5524
- recordingId: source,
5525
- ...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
5526
- voices,
5527
- ...applied === void 0 ? {} : { applied }
5528
- });
5529
- }
5530
- continue;
5531
- }
5532
- const caption = snapshot["caption-entities"][source];
5533
- const shown = snapshot.captionText[source];
5534
- if (!caption?.selection || caption.text !== void 0 || !shown) continue;
5535
- const scriptId = caption.selection.audioScriptId;
5536
- const body = bodies.get(scriptId);
5537
- if (body === void 0) continue;
5538
- const segment = lines(body).findIndex((line) => shown.start >= line.start && shown.end <= line.end);
5539
- if (segment >= 0) views.get(scriptId)?.captions.push({
5540
- clipId,
5541
- captionId: source,
5542
- segment
5543
- });
5544
- }
5545
- return [...views.values()];
5546
- }
5547
- /**
5548
- * Whether a recording still matches what it was synthesized from: the digest its derived-from
5549
- * records against the source's file digest now. An AudioScript has no file, so a recording
5550
- * derived from a script directly is not judged.
5551
- */
5552
- function appliedSpeech(snapshot, recordingId, spoken) {
5553
- const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
5554
- const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
5555
- const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
5556
- return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
5557
- }
5558
- //#endregion
5559
8001
  //#region src/dsl/projection/caption-style.ts
5560
8002
  const fields = {
5561
8003
  fontSize: "font_size",
@@ -5592,6 +8034,9 @@ function fromDraftStyle(style) {
5592
8034
  };
5593
8035
  }
5594
8036
  //#endregion
8037
+ //#region src/dsl/projection/video-draft-types.ts
8038
+ const DEFAULT_UNIT_TIME_MS = 33.333;
8039
+ //#endregion
5595
8040
  //#region src/dsl/projection/to-video-draft.ts
5596
8041
  /** A content read view, not an archive of authoring identities, rich-text marks or provenance. */
5597
8042
  function toVideoDraft(snapshot) {
@@ -5644,7 +8089,7 @@ function toVideoDraft(snapshot) {
5644
8089
  audio_storage_key: source.storageKey,
5645
8090
  origin_speech_id: source.key,
5646
8091
  volume: clip.volume,
5647
- audio_script: spokenText(snapshot, sourceId, captionIds),
8092
+ audio_script: spokenText(snapshot, sourceId),
5648
8093
  voice: voice ? {
5649
8094
  id: voice.key,
5650
8095
  name: voice.name
@@ -5730,29 +8175,17 @@ function requireWholeAudio(media, clip, fail) {
5730
8175
  if (media.kind === "image" || clip.inMs !== 0 || clip.outMs !== media.durationMs || clip.rate !== 1) fail("VideoDraft audio has no trim or speed fields");
5731
8176
  }
5732
8177
  /**
5733
- * The words a speech Clip says. A script voiced in several takes is said one part per take, and
5734
- * the Captions following the Clip select that part: its words are the segments they select, in
5735
- * script order. A Clip no Caption of its script follows says the whole script.
8178
+ * What a recording says: what the selection of the PhoneticScript it was spoken from holds now,
8179
+ * or, for one derived from a script directly, the whole script, one segment per line.
5736
8180
  */
5737
- function spokenText(snapshot, recordingId, captionClipIds) {
5738
- const scripts = new Set(spokenScripts(snapshot, recordingId).map((spoken) => spoken.audioScriptId));
5739
- if (scripts.size > 1) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple scripts cannot be silently concatenated into audio_script");
5740
- const id = [...scripts][0];
5741
- if (!id) return "";
5742
- const body = snapshot.richText[id]?.plainText ?? "";
5743
- const segments = lines(body);
5744
- const said = /* @__PURE__ */ new Set();
5745
- for (const clipId of captionClipIds) {
5746
- const captionId = snapshot["clip-entities"][clipId]?.source;
5747
- const caption = captionId === void 0 ? void 0 : snapshot["caption-entities"][captionId];
5748
- const shown = captionId === void 0 ? void 0 : snapshot.captionText[captionId];
5749
- if (caption?.selection?.audioScriptId !== id || caption.text !== void 0 || !shown) continue;
5750
- segments.forEach((line, index) => {
5751
- if (shown.start < Math.max(line.end, line.start + 1) && shown.end > line.start) said.add(index);
5752
- });
5753
- }
5754
- if (!said.size) return body;
5755
- return [...said].sort((a, b) => a - b).map((index) => segments[index].text).join("\n");
8181
+ function spokenText(snapshot, recordingId) {
8182
+ const spoken = spokenScripts(snapshot, recordingId);
8183
+ 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");
8184
+ const [first] = spoken;
8185
+ if (!first) return "";
8186
+ if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
8187
+ const script = snapshot["audio-script-entities"][first.audioScriptId];
8188
+ return script ? script.segments.map((segment) => segment.text).join("\n") : "";
5756
8189
  }
5757
8190
  //#endregion
5758
8191
  //#region src/dsl/projection/from-video-draft.ts
@@ -5894,16 +8327,16 @@ function prepare(input, options) {
5894
8327
  let script = scriptByRecording.get(recordingId);
5895
8328
  if (script && script.text !== speech.audio_script) fail(`part_library/${id}/audio_script`, "One recording cannot acquire conflicting source scripts during import");
5896
8329
  if (!script) {
5897
- script = {
5898
- id: claim(mint("audio-script")),
5899
- text: speech.audio_script
5900
- };
8330
+ script = importedScript(claim(mint("audio-script")), speech.audio_script, () => mint("segment"));
5901
8331
  scriptByRecording.set(recordingId, script);
5902
8332
  mutations.push({
5903
8333
  type: "create-entity",
5904
8334
  table: "audio-script-entities",
5905
8335
  id: script.id,
5906
- value: { body: script.text }
8336
+ value: { segments: script.segments.map(({ id, text }) => ({
8337
+ id,
8338
+ text
8339
+ })) }
5907
8340
  }, {
5908
8341
  type: "set-relation",
5909
8342
  table: "derived-from-relations",
@@ -5951,12 +8384,18 @@ function prepare(input, options) {
5951
8384
  const text = caption.text;
5952
8385
  if (typeof text !== "string") fail(`part_library/${captionId}/text`, "Expected Caption text");
5953
8386
  const start = text.length ? script.text.indexOf(text, cursor) : -1;
5954
- if (start >= 0) {
5955
- cursor = start + text.length;
5956
- selections.set(captionId, {
8387
+ const end = start + text.length;
8388
+ const segment = script.segments.find((s) => start >= s.start && end <= s.start + s.text.length);
8389
+ if (start >= 0 && segment) {
8390
+ cursor = end;
8391
+ selections.set(captionId, text === segment.text ? {
8392
+ audioScriptId: script.id,
8393
+ segmentId: segment.id
8394
+ } : {
5957
8395
  audioScriptId: script.id,
5958
- start,
5959
- end: cursor
8396
+ segmentId: segment.id,
8397
+ start: start - segment.start,
8398
+ end: end - segment.start
5960
8399
  });
5961
8400
  } else diagnostics.push({
5962
8401
  code: "caption-text-unmatched",
@@ -6067,5 +8506,22 @@ function milliseconds(value, path, positive = false, signed = false) {
6067
8506
  if (typeof value !== "number" || !Number.isSafeInteger(value) || !signed && value < 0 || positive && value <= 0) projectionError("invalid-time", path, "Expected safe integer milliseconds in the valid range");
6068
8507
  return value;
6069
8508
  }
8509
+ function importedScript(id, text, mintSegment) {
8510
+ const segments = [];
8511
+ let start = 0;
8512
+ if (text.length) for (const line of text.split("\n")) {
8513
+ segments.push({
8514
+ id: mintSegment(),
8515
+ text: line,
8516
+ start
8517
+ });
8518
+ start += line.length + 1;
8519
+ }
8520
+ return {
8521
+ id,
8522
+ text,
8523
+ segments
8524
+ };
8525
+ }
6070
8526
  //#endregion
6071
- export { DslEditor as a, DslProjectionError as c, createInitialMedeoDsl as i, toVideoDraft as n, MedeoDsl as o, toAudioScriptViews as r, resolveDslLayout as s, fromVideoDraft as t };
8527
+ export { defaultIdFactory as _, DslEditor as a, tupleHash as b, UnsupportedDocumentFormatError as c, medeoDslLoroShape as d, captionContentCodec as f, DslProjectionError as g, resolveDslLayout as h, createInitialMedeoDsl as i, assertDslDocument as l, timeAnchorCodec as m, toVideoDraft as n, toAudioScriptViews as o, isInvalidAtomic as p, DEFAULT_UNIT_TIME_MS as r, MedeoDsl as s, fromVideoDraft as t, documentFormat as u, relationId as v, resourceId as y };