@mengine/medeo-client 2.1.1-dsl.9 → 3.0.1-caption-source.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/dist/{dsl-COyzz7Fp.js → dsl-96AZnd7O.js} +2639 -270
- package/dist/dsl.d.ts +2 -3
- package/dist/dsl.js +2 -3
- package/dist/{index-C5wDKYqC.d.ts → index-C1iiDt_Q.d.ts} +939 -27
- package/dist/{index-DAfOkcnZ.d.ts → index-CSjqNIoR.d.ts} +205 -5
- package/dist/index.d.ts +3 -5
- package/dist/index.js +829 -8
- package/dist/schemas.d.ts +21 -21
- package/dist/schemas.js +57 -21
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +37 -16
- package/package.json +7 -8
- package/dist/index-CHiPztZr.d.ts +0 -194
- package/dist/legacy.d.ts +0 -1044
- package/dist/legacy.js +0 -4349
- package/dist/shared-iWnU2osE.js +0 -39
- package/dist/storage-BW3tI_ER.js +0 -801
- package/dist/video-draft-types-BnbfWrBx.js +0 -1486
- package/dist/video-draft-types-GqXnsOzA.d.ts +0 -869
|
@@ -1,6 +1,337 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
2
|
import { Cursor, LoroDoc, LoroList, LoroMap, LoroMovableList, LoroText, LoroTree, isContainer, isContainerId } from "loro-crdt";
|
|
3
|
-
|
|
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(nonempty)
|
|
149
|
+
};
|
|
150
|
+
/** D190: omitted pronunciation uses the selected source words; an authored pronunciation is never empty. */
|
|
151
|
+
function assertPhonemeScript(value) {
|
|
152
|
+
if (value !== void 0 && !nonempty(value)) throw new Error("phonemeScript must be nonempty when provided");
|
|
153
|
+
}
|
|
154
|
+
function assertPhoneme(value) {
|
|
155
|
+
if (!segmentSelection(phonemeFields)(value)) throw new Error("Invalid phoneme");
|
|
156
|
+
}
|
|
157
|
+
const segmentFields = {
|
|
158
|
+
id: required(id),
|
|
159
|
+
text: required(string),
|
|
160
|
+
startMs: optional$1(nonnegative),
|
|
161
|
+
endMs: optional$1(nonnegative)
|
|
162
|
+
};
|
|
163
|
+
/** A segment's time in the medium it was read off: both ends or neither, the end not before the start. */
|
|
164
|
+
const segment = (v) => {
|
|
165
|
+
if (!object(segmentFields)(v)) return false;
|
|
166
|
+
const { startMs, endMs } = v;
|
|
167
|
+
return startMs === void 0 ? endMs === void 0 : endMs !== void 0 && endMs >= startMs;
|
|
168
|
+
};
|
|
169
|
+
/** One segment as a script stores it; a segment added on its own is checked here, not with its row. */
|
|
170
|
+
function assertScriptSegment(value) {
|
|
171
|
+
if (!segment(value)) throw new Error("A segment is { id, text } with startMs and endMs both or neither, endMs not before startMs");
|
|
172
|
+
}
|
|
173
|
+
/** A script's segments: each with its own id, unique within the script. */
|
|
174
|
+
const segments = (v) => Array.isArray(v) && v.every(segment) && new Set(v.map((s) => s.id)).size === v.length;
|
|
175
|
+
const locator = {
|
|
176
|
+
system: required(oneOf("memota", "memota-speech")),
|
|
177
|
+
key: required(nonempty)
|
|
178
|
+
};
|
|
179
|
+
const entityFields = {
|
|
180
|
+
"timeline-entities": {},
|
|
181
|
+
"track-entities": {
|
|
182
|
+
role: required(oneOf("video_clip", "speech", "caption", "bgm")),
|
|
183
|
+
hidden: optional$1((v) => typeof v === "boolean")
|
|
184
|
+
},
|
|
185
|
+
"clip-entities": {
|
|
186
|
+
seqMarker: required(object(seqMarkerFields)),
|
|
187
|
+
volume: optional$1(finite),
|
|
188
|
+
source: required(id),
|
|
189
|
+
anchor: optional$1(object(timeAnchorFields))
|
|
190
|
+
},
|
|
191
|
+
"media-entities": {
|
|
192
|
+
...locator,
|
|
193
|
+
kind: required(oneOf("video", "audio", "voiceover-recording", "image")),
|
|
194
|
+
storageKey: optional$1(nonempty),
|
|
195
|
+
durationMs: optional$1(positive),
|
|
196
|
+
digest: optional$1(nonempty)
|
|
197
|
+
},
|
|
198
|
+
"audio-script-entities": {
|
|
199
|
+
segments: required(segments),
|
|
200
|
+
defaultLanguage: optional$1(validLanguage),
|
|
201
|
+
system: optional$1(oneOf("memota")),
|
|
202
|
+
key: optional$1(nonempty),
|
|
203
|
+
storageKey: optional$1(nonempty),
|
|
204
|
+
digest: optional$1(nonempty)
|
|
205
|
+
},
|
|
206
|
+
"phonetic-script-entities": {
|
|
207
|
+
phoneme: optional$1(segmentSelection(phonemeFields)),
|
|
208
|
+
system: optional$1(oneOf("memota")),
|
|
209
|
+
key: optional$1(nonempty),
|
|
210
|
+
storageKey: optional$1(nonempty),
|
|
211
|
+
digest: optional$1(nonempty)
|
|
212
|
+
},
|
|
213
|
+
"caption-entities": {
|
|
214
|
+
selection: optional$1(segmentSelection(selectionFields)),
|
|
215
|
+
text: optional$1(string),
|
|
216
|
+
style: optional$1(object(styleFields))
|
|
217
|
+
},
|
|
218
|
+
"voice-entities": {
|
|
219
|
+
system: required(oneOf("voice-library")),
|
|
220
|
+
key: required(nonempty),
|
|
221
|
+
name: optional$1(string)
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const entityTables = Object.keys(entityFields);
|
|
225
|
+
/**
|
|
226
|
+
* Fields stored as one encoded string: written whole, never merged field by field. A patch to one
|
|
227
|
+
* is merged into the current value first, then the whole value is written.
|
|
228
|
+
*/
|
|
229
|
+
const atomicFields = {
|
|
230
|
+
"clip-entities": ["seqMarker", "anchor"],
|
|
231
|
+
"phonetic-script-entities": ["phoneme"],
|
|
232
|
+
"caption-entities": ["selection"]
|
|
233
|
+
};
|
|
234
|
+
const relationFields = {
|
|
235
|
+
"voice-timbre-relations": {
|
|
236
|
+
recordingId: required(id),
|
|
237
|
+
voiceId: required(id)
|
|
238
|
+
},
|
|
239
|
+
"derived-from-relations": {
|
|
240
|
+
entityId: required(id),
|
|
241
|
+
sourceId: required(id),
|
|
242
|
+
sourceDigest: optional$1(nonempty)
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const relationTables = Object.keys(relationFields);
|
|
246
|
+
const fileFields = [
|
|
247
|
+
"system",
|
|
248
|
+
"key",
|
|
249
|
+
"storageKey",
|
|
250
|
+
"digest"
|
|
251
|
+
];
|
|
252
|
+
const phonemeScriptOf = (row) => isRecord(row.phoneme) ? row.phoneme.phonemeScript : void 0;
|
|
253
|
+
/**
|
|
254
|
+
* A PhoneticScript as a write leaves it: written for a segment (D181), and its phonemeScript is raw
|
|
255
|
+
* content stored as its file (D178) — with text the row names that file and its digest, without
|
|
256
|
+
* text it has no file, and rewritten text is a new file.
|
|
257
|
+
*/
|
|
258
|
+
function assertPhoneticScript(before, after) {
|
|
259
|
+
if (!isRecord(after.phoneme)) throw new Error("A PhoneticScript is written for a segment of its base script");
|
|
260
|
+
if (phonemeScriptOf(after) === void 0) {
|
|
261
|
+
if (fileFields.some((field) => after[field] !== void 0)) throw new Error("A PhoneticScript without text has no file");
|
|
262
|
+
} 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");
|
|
263
|
+
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");
|
|
264
|
+
}
|
|
265
|
+
/** A transcript names the file it was read from by system and key together; its facts come with that name. */
|
|
266
|
+
function assertTranscriptFile(row) {
|
|
267
|
+
if (row.system === void 0 !== (row.key === void 0)) throw new Error("A transcript names its transcription file by system and key");
|
|
268
|
+
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");
|
|
269
|
+
}
|
|
270
|
+
function assertEntity(table, value) {
|
|
271
|
+
checkFields(value, entityFields[table]);
|
|
272
|
+
if (table === "audio-script-entities") assertTranscriptFile(value);
|
|
273
|
+
if (table === "media-entities") {
|
|
274
|
+
if (value.kind === "image" && value.durationMs !== void 0) throw new Error("Image has no intrinsic duration");
|
|
275
|
+
if (value.kind !== "image" && !positive(value.durationMs)) throw new Error("Finite media requires durationMs");
|
|
276
|
+
if (["audio", "voiceover-recording"].includes(value.kind) && !nonempty(value.storageKey)) throw new Error("Audio requires storageKey");
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Validate only touched fields, so an unrelated edit can proceed in a merged, invalid document. */
|
|
280
|
+
function assertEntityPatch(table, patch, current) {
|
|
281
|
+
if (!isRecord(patch)) throw new Error("Expected an entity patch");
|
|
282
|
+
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");
|
|
283
|
+
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");
|
|
284
|
+
if (table === "caption-entities" && current.text !== void 0 && patch.text !== void 0) throw new Error("Edit Caption text through the Text adapter");
|
|
285
|
+
const next = patchRow(table, current, patch);
|
|
286
|
+
for (const key of Object.keys(patch)) {
|
|
287
|
+
const field = entityFields[table][key];
|
|
288
|
+
if (!field || !field.optional && next[key] === void 0 || next[key] !== void 0 && !field.check(next[key])) throw new Error(`Invalid patch field: ${key}`);
|
|
289
|
+
}
|
|
290
|
+
if (table === "audio-script-entities" && fileFields.some((field) => Object.hasOwn(patch, field))) {
|
|
291
|
+
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");
|
|
292
|
+
assertTranscriptFile(next);
|
|
293
|
+
}
|
|
294
|
+
if (table === "media-entities" || table === "voice-entities") {
|
|
295
|
+
for (const key of [
|
|
296
|
+
"system",
|
|
297
|
+
"key",
|
|
298
|
+
"kind"
|
|
299
|
+
]) if (Object.hasOwn(patch, key) && patch[key] !== current[key]) throw new Error(`Resource identity field is immutable: ${key}`);
|
|
300
|
+
}
|
|
301
|
+
if (table === "media-entities") {
|
|
302
|
+
if (Object.hasOwn(patch, "durationMs")) {
|
|
303
|
+
if (next.kind === "image" ? next.durationMs !== void 0 : !positive(next.durationMs)) throw new Error("durationMs must match the media kind");
|
|
304
|
+
}
|
|
305
|
+
if (Object.hasOwn(patch, "storageKey") && ["audio", "voiceover-recording"].includes(next.kind) && !nonempty(next.storageKey)) throw new Error("Audio requires storageKey");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function assertRelation(table, value) {
|
|
309
|
+
checkFields(value, relationFields[table]);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* The row after a patch. An atomic field merges into its current value, so a partial patch keeps
|
|
313
|
+
* the rest of it; an unreadable stored value is replaced instead, so writing a whole value repairs it.
|
|
314
|
+
*/
|
|
315
|
+
function patchRow(table, current, patch) {
|
|
316
|
+
const next = mergeFields(current, patch);
|
|
317
|
+
for (const key of atomicFields[table] ?? []) {
|
|
318
|
+
const old = current[key];
|
|
319
|
+
const value = patch[key];
|
|
320
|
+
if (value !== void 0 && isRecord(old) && old.invalid === true) next[key] = isRecord(value) ? mergeFields({}, value) : value;
|
|
321
|
+
}
|
|
322
|
+
return next;
|
|
323
|
+
}
|
|
324
|
+
/** Undefined deletes; nested patches retain sibling fields and an existing empty style Map. */
|
|
325
|
+
function mergeFields(target, patch) {
|
|
326
|
+
const result = { ...target };
|
|
327
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
328
|
+
if (reservedIds.includes(key)) throw new Error(`Reserved field: ${key}`);
|
|
329
|
+
if (value === void 0) delete result[key];
|
|
330
|
+
else result[key] = isRecord(value) ? mergeFields(isRecord(result[key]) ? result[key] : {}, value) : value;
|
|
331
|
+
}
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
//#endregion
|
|
4
335
|
//#region ../medeo-dsl/src/text.ts
|
|
5
336
|
function assertTextOffset(text, at) {
|
|
6
337
|
if (!Number.isSafeInteger(at) || at < 0 || at > text.length) throw new Error("Invalid UTF-16 text offset");
|
|
@@ -249,9 +580,10 @@ function assertPhonemeSelection(rows, selection, text) {
|
|
|
249
580
|
assertSegmentRange(selection, text);
|
|
250
581
|
if (selection.phonemeScript !== void 0 && typeof selection.phonemeScript !== "string") throw new Error("phonemeScript is a string");
|
|
251
582
|
}
|
|
252
|
-
/**
|
|
583
|
+
/** Every new or rewritten Caption selects its words from one AudioScript segment. */
|
|
253
584
|
function assertCaptionSource(caption, selected) {
|
|
254
|
-
if (
|
|
585
|
+
if (!selected) throw new Error("Caption must select a segment of an AudioScript");
|
|
586
|
+
if (caption?.text !== void 0) throw new Error("Caption must have only one content source");
|
|
255
587
|
}
|
|
256
588
|
//#endregion
|
|
257
589
|
//#region ../medeo-dsl/src/contain.ts
|
|
@@ -311,6 +643,63 @@ function assertContainTree(rows, tree) {
|
|
|
311
643
|
}
|
|
312
644
|
}
|
|
313
645
|
//#endregion
|
|
646
|
+
//#region ../medeo-dsl/src/clip-playback.ts
|
|
647
|
+
const playable = {
|
|
648
|
+
video_clip: ["video", "image"],
|
|
649
|
+
speech: ["audio", "voiceover-recording"],
|
|
650
|
+
caption: ["caption"],
|
|
651
|
+
bgm: ["audio"]
|
|
652
|
+
};
|
|
653
|
+
function supportsClipSpeed(source) {
|
|
654
|
+
return isRecord(source) && [
|
|
655
|
+
"video",
|
|
656
|
+
"audio",
|
|
657
|
+
"voiceover-recording"
|
|
658
|
+
].includes(String(source.kind));
|
|
659
|
+
}
|
|
660
|
+
/** Content-kind admission is shared by Actions, script compilation and the final transaction. */
|
|
661
|
+
function assertClipSourceRole(role, kind) {
|
|
662
|
+
if (!playable[role]?.includes(kind)) throw new Error(`A ${role} Track does not play ${kind}`);
|
|
663
|
+
}
|
|
664
|
+
function assertClipWindow(window, duration) {
|
|
665
|
+
if (!Number.isSafeInteger(window.inMs) || !Number.isSafeInteger(window.outMs) || window.inMs < 0 || window.outMs <= window.inMs) throw new Error("Invalid Clip window");
|
|
666
|
+
if (duration !== void 0 && window.outMs > duration) throw new Error(`Invalid Clip window: ${window.outMs}ms is past its content's ${duration}ms`);
|
|
667
|
+
}
|
|
668
|
+
/** Check authored playback facts; a remote merge is still read by projection with diagnostics. */
|
|
669
|
+
function assertClipPlayback(clip, source, role) {
|
|
670
|
+
if (!isRecord(source)) throw new Error("Invalid Clip source");
|
|
671
|
+
const kind = source.kind ?? "caption";
|
|
672
|
+
const marker = clip.seqMarker;
|
|
673
|
+
assertSeqMarker(marker);
|
|
674
|
+
if (marker.outMs <= marker.inMs) throw new Error("Clip window must be nonempty");
|
|
675
|
+
if (role !== void 0) assertClipSourceRole(role, kind);
|
|
676
|
+
if (marker.rate !== void 0 && !supportsClipSpeed(source)) throw new Error("Only video and sound change speed");
|
|
677
|
+
const duration = typeof source.durationMs === "number" ? source.durationMs : void 0;
|
|
678
|
+
if (marker.fillTimeline) {
|
|
679
|
+
if (kind !== "audio") throw new Error("Only BGM audio fills the Timeline");
|
|
680
|
+
if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || marker.outMs !== duration) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${duration ?? "its length"}, no rate; the Timeline's end cuts it`);
|
|
681
|
+
}
|
|
682
|
+
assertClipWindow(marker);
|
|
683
|
+
}
|
|
684
|
+
function clipParents(tree) {
|
|
685
|
+
const result = /* @__PURE__ */ new Map();
|
|
686
|
+
for (const track of tree) for (const clip of track.children ?? []) result.set(clip.entityId, track.entityId);
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
689
|
+
/** A new parent or Track role revalidates that binding, without blocking unrelated merged rows. */
|
|
690
|
+
function assertClipPlacements(before, after, previousTree, nextTree) {
|
|
691
|
+
const previous = clipParents(previousTree);
|
|
692
|
+
for (const [id, parent] of clipParents(nextTree)) {
|
|
693
|
+
const track = after["track-entities"][parent];
|
|
694
|
+
const previousTrack = before["track-entities"][parent];
|
|
695
|
+
if (previous.get(id) === parent && (track === void 0 || isRecord(track) && isRecord(previousTrack) && previousTrack.role === track.role)) continue;
|
|
696
|
+
const role = requireEntity(after, "track-entities", parent).role;
|
|
697
|
+
const clip = after["clip-entities"][id];
|
|
698
|
+
if (!clip) continue;
|
|
699
|
+
assertClipPlayback(clip, after["media-entities"][clip.source] ?? after["caption-entities"][clip.source], role);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
//#endregion
|
|
314
703
|
//#region ../medeo-dsl/src/plan.ts
|
|
315
704
|
/** Include removed nested keys: a partial merge alone would conceal a deleted required field. */
|
|
316
705
|
function replacementPatch(before, after) {
|
|
@@ -326,6 +715,7 @@ function replacementPatch(before, after) {
|
|
|
326
715
|
*/
|
|
327
716
|
function assertChangeAdmitted(change) {
|
|
328
717
|
const { before, after } = change;
|
|
718
|
+
const parents = change.containment ? clipParents(change.containment.after) : void 0;
|
|
329
719
|
for (const [table, rows] of change.entities) for (const [id, fields] of rows) {
|
|
330
720
|
const value = after[table][id];
|
|
331
721
|
if (value === void 0) continue;
|
|
@@ -340,25 +730,17 @@ function assertChangeAdmitted(change) {
|
|
|
340
730
|
}
|
|
341
731
|
const row = value;
|
|
342
732
|
for (const field of referenceFields[table] ?? []) if (!old || fields.has(field)) assertReferenceAdmitted(after, table, id, field, row[field]);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
733
|
+
if (table === "clip-entities" && (!old || fields.has("seqMarker") || fields.has("source"))) {
|
|
734
|
+
const parent = parents?.get(id);
|
|
735
|
+
assertClipPlayback(row, after["media-entities"][row.source] ?? after["caption-entities"][row.source], parent === void 0 ? void 0 : requireEntity(after, "track-entities", parent).role);
|
|
736
|
+
}
|
|
346
737
|
}
|
|
347
738
|
if (change.entities.has("timeline-entities") && Object.keys(after["timeline-entities"]).length > 1) throw new Error("Document already has a Timeline");
|
|
348
739
|
for (const [table, ids] of change.relations) for (const id of ids) {
|
|
349
740
|
const value = after[table][id];
|
|
350
741
|
if (value !== void 0) assertRelationAdmitted(after, table, id, value);
|
|
351
742
|
}
|
|
352
|
-
|
|
353
|
-
/**
|
|
354
|
-
* A Clip that fills the Timeline plays its whole source at 1×: the player loops a short source and
|
|
355
|
-
* cuts a long one at the Timeline's end (RFC 02 §3.3), so a window or speed of its own has no meaning.
|
|
356
|
-
*/
|
|
357
|
-
function assertFillsWithWholeSource(rows, marker, source) {
|
|
358
|
-
if (marker?.fillTimeline !== true) return;
|
|
359
|
-
const media = typeof source === "string" ? rows["media-entities"][source] : void 0;
|
|
360
|
-
const durationMs = typeof media?.durationMs === "number" ? media.durationMs : void 0;
|
|
361
|
-
if (marker.inMs !== 0 || marker.rate !== void 0 && marker.rate !== 1 || durationMs !== void 0 && marker.outMs !== durationMs) throw new Error(`A Clip that fills the Timeline plays its whole source at 1×: inMs 0, outMs ${durationMs ?? "its length"}, no rate; the Timeline's end cuts it`);
|
|
743
|
+
if (change.containment) assertClipPlacements(before, after, change.containment.before, change.containment.after);
|
|
362
744
|
}
|
|
363
745
|
//#endregion
|
|
364
746
|
//#region src/dsl/projection/types.ts
|
|
@@ -594,15 +976,11 @@ function requireSource(s, id, role) {
|
|
|
594
976
|
return media;
|
|
595
977
|
}
|
|
596
978
|
function assertMediaRole(media, role) {
|
|
597
|
-
if (!media
|
|
598
|
-
|
|
599
|
-
speech: ["audio", "voiceover-recording"],
|
|
600
|
-
bgm: ["audio"],
|
|
601
|
-
caption: []
|
|
602
|
-
}[role].includes(media.kind)) throw new Error(`Source is not allowed on ${role} Track`);
|
|
979
|
+
if (!media) throw new Error(`Missing source for ${role} Track`);
|
|
980
|
+
assertClipSourceRole(role, media.kind);
|
|
603
981
|
}
|
|
604
982
|
function validateWindow(window, duration) {
|
|
605
|
-
|
|
983
|
+
assertClipWindow(window, duration);
|
|
606
984
|
}
|
|
607
985
|
function trackOf(s, id) {
|
|
608
986
|
requireClip(s, id);
|
|
@@ -873,35 +1251,1573 @@ function replaceSequencePlan(s, input, ids) {
|
|
|
873
1251
|
};
|
|
874
1252
|
}
|
|
875
1253
|
}
|
|
876
|
-
mutations.push(...deleteClips({
|
|
877
|
-
...s,
|
|
878
|
-
"clip-entities": clips
|
|
879
|
-
}, input.clipIds, "cascade"));
|
|
880
|
-
return mutations;
|
|
1254
|
+
mutations.push(...deleteClips({
|
|
1255
|
+
...s,
|
|
1256
|
+
"clip-entities": clips
|
|
1257
|
+
}, input.clipIds, "cascade"));
|
|
1258
|
+
return mutations;
|
|
1259
|
+
}
|
|
1260
|
+
function replaceSourcePlan(s, clipId, input) {
|
|
1261
|
+
if (requireClip(s, clipId).seqMarker.fillTimeline) throw new Error("Use setBgm to replace a fill Clip");
|
|
1262
|
+
const source = sourcePlan(s, input, trackOf(s, clipId));
|
|
1263
|
+
validateWindow(input, "durationMs" in source.source ? source.source.durationMs : void 0);
|
|
1264
|
+
return [...source.mutations, {
|
|
1265
|
+
type: "patch-entity",
|
|
1266
|
+
table: "clip-entities",
|
|
1267
|
+
id: clipId,
|
|
1268
|
+
patch: {
|
|
1269
|
+
seqMarker: {
|
|
1270
|
+
inMs: input.inMs,
|
|
1271
|
+
outMs: input.outMs,
|
|
1272
|
+
...supportsClipSpeed(source.source) ? {} : { rate: void 0 }
|
|
1273
|
+
},
|
|
1274
|
+
source: source.id
|
|
1275
|
+
}
|
|
1276
|
+
}];
|
|
1277
|
+
}
|
|
1278
|
+
//#endregion
|
|
1279
|
+
//#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
|
|
1280
|
+
function getMapFieldSchema(schema, key) {
|
|
1281
|
+
if (!schema) return void 0;
|
|
1282
|
+
if (Object.prototype.hasOwnProperty.call(schema.definition, key)) return schema.definition[key];
|
|
1283
|
+
if (schema.type === "loro-map" && "catchallType" in schema) return schema.catchallType;
|
|
1284
|
+
}
|
|
1285
|
+
function getChildSchema(schema, childKey) {
|
|
1286
|
+
if (!schema) return void 0;
|
|
1287
|
+
switch (schema.type) {
|
|
1288
|
+
case "schema":
|
|
1289
|
+
case "loro-map": return childKey === void 0 ? void 0 : getMapFieldSchema(schema, String(childKey));
|
|
1290
|
+
case "loro-list":
|
|
1291
|
+
case "loro-movable-list": return schema.itemSchema;
|
|
1292
|
+
case "loro-tree": return schema.nodeSchema;
|
|
1293
|
+
default: return;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
function getChildContainerSchema(schema, childKey) {
|
|
1297
|
+
const childSchema = getChildSchema(schema, childKey);
|
|
1298
|
+
if (!childSchema) return void 0;
|
|
1299
|
+
switch (childSchema.type) {
|
|
1300
|
+
case "loro-map":
|
|
1301
|
+
case "loro-list":
|
|
1302
|
+
case "loro-movable-list":
|
|
1303
|
+
case "loro-text":
|
|
1304
|
+
case "loro-tree": return childSchema;
|
|
1305
|
+
default: return;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1309
|
+
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.4/node_modules/loro-mirror/dist/constants.js
|
|
1310
|
+
const CID_KEY = "$cid";
|
|
1311
|
+
//#endregion
|
|
1312
|
+
//#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
|
|
1313
|
+
function isPlainObjectValue(value) {
|
|
1314
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof RegExp) && typeof value !== "function";
|
|
1315
|
+
}
|
|
1316
|
+
function inferContainerTypeFromValue$1(value, defaults) {
|
|
1317
|
+
switch (getContainerValueKind(value)) {
|
|
1318
|
+
case "map": return "Map";
|
|
1319
|
+
case "array": return defaults?.defaultMovableList ? "MovableList" : "List";
|
|
1320
|
+
case "string": return defaults?.defaultLoroText ? "Text" : void 0;
|
|
1321
|
+
default: return;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
function inferSchemaContainerTypeFromValue(value, defaults) {
|
|
1325
|
+
switch (inferContainerTypeFromValue$1(value, defaults)) {
|
|
1326
|
+
case "Map": return "loro-map";
|
|
1327
|
+
case "List": return "loro-list";
|
|
1328
|
+
case "MovableList": return "loro-movable-list";
|
|
1329
|
+
case "Text": return "loro-text";
|
|
1330
|
+
default: return;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
function matchesContainerType(containerType, value) {
|
|
1334
|
+
const kind = getContainerValueKind(value);
|
|
1335
|
+
switch (containerType) {
|
|
1336
|
+
case "Map": return kind === "map";
|
|
1337
|
+
case "List":
|
|
1338
|
+
case "MovableList":
|
|
1339
|
+
case "Tree": return kind === "array";
|
|
1340
|
+
case "Text": return kind === "string";
|
|
1341
|
+
default: return false;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function getContainerValueKind(value) {
|
|
1345
|
+
if (isPlainObjectValue(value)) return "map";
|
|
1346
|
+
if (Array.isArray(value)) return "array";
|
|
1347
|
+
if (typeof value === "string") return "string";
|
|
1348
|
+
return "other";
|
|
1349
|
+
}
|
|
1350
|
+
//#endregion
|
|
1351
|
+
//#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
|
|
1352
|
+
/**
|
|
1353
|
+
* Utility functions for Loro Mirror core
|
|
1354
|
+
*/
|
|
1355
|
+
/**
|
|
1356
|
+
* Check if a schema has a transform.
|
|
1357
|
+
* Distinguishes between the .transform() builder method (function) and an actual
|
|
1358
|
+
* TransformDefinition object (with decode/encode functions).
|
|
1359
|
+
*/
|
|
1360
|
+
function hasTransform(schema) {
|
|
1361
|
+
if (schema === void 0) return false;
|
|
1362
|
+
const t = schema.transform;
|
|
1363
|
+
return t != null && typeof t === "object";
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* Get the transform from a schema, or undefined if none exists.
|
|
1367
|
+
*/
|
|
1368
|
+
function getTransform(schema) {
|
|
1369
|
+
if (hasTransform(schema)) return schema.transform;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Get the effective equality strategy for a transform.
|
|
1373
|
+
* Default: "reference-equality"
|
|
1374
|
+
*/
|
|
1375
|
+
function getEqualityStrategy(schema, strategyIfNotTransformable) {
|
|
1376
|
+
const transform = getTransform(schema);
|
|
1377
|
+
if (!transform) return strategyIfNotTransformable;
|
|
1378
|
+
return transform.isEqual ?? "reference-equality";
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Apply decode transform to a CRDT value.
|
|
1382
|
+
* Null/undefined pass through as-is - transform is only called for real values.
|
|
1383
|
+
*/
|
|
1384
|
+
function applyDecode(schema, crdtValue) {
|
|
1385
|
+
if (crdtValue === null || crdtValue === void 0) return crdtValue;
|
|
1386
|
+
const transform = getTransform(schema);
|
|
1387
|
+
return transform ? transform.decode(crdtValue) : crdtValue;
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Recursively decode JSON values using schema transforms.
|
|
1391
|
+
* Mutates in-place. Used for snapshot initialization where toJSON() skips decode.
|
|
1392
|
+
*/
|
|
1393
|
+
function decodeNestedJsonValues(json, schema) {
|
|
1394
|
+
if (json === null || json === void 0 || !schema) return json;
|
|
1395
|
+
switch (schema.type) {
|
|
1396
|
+
case "loro-map":
|
|
1397
|
+
if (!isObject(json)) return json;
|
|
1398
|
+
for (const key of Object.keys(json)) {
|
|
1399
|
+
const fieldSchema = getChildSchema(schema, key);
|
|
1400
|
+
json[key] = decodeNestedJsonValues(json[key], fieldSchema);
|
|
1401
|
+
}
|
|
1402
|
+
return json;
|
|
1403
|
+
case "loro-list":
|
|
1404
|
+
case "loro-movable-list": {
|
|
1405
|
+
if (!Array.isArray(json)) return json;
|
|
1406
|
+
const itemSchema = getChildSchema(schema);
|
|
1407
|
+
for (let i = 0; i < json.length; i++) json[i] = decodeNestedJsonValues(json[i], itemSchema);
|
|
1408
|
+
return json;
|
|
1409
|
+
}
|
|
1410
|
+
case "loro-tree": {
|
|
1411
|
+
if (!Array.isArray(json)) return json;
|
|
1412
|
+
const nodeSchema = getChildSchema(schema);
|
|
1413
|
+
const walk = (nodes) => {
|
|
1414
|
+
for (const node of nodes) if (node != null && typeof node == "object") {
|
|
1415
|
+
if ("data" in node && node.data !== void 0) node.data = decodeNestedJsonValues(node.data, nodeSchema);
|
|
1416
|
+
if ("children" in node && Array.isArray(node.children)) walk(node.children);
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
walk(json);
|
|
1420
|
+
return json;
|
|
1421
|
+
}
|
|
1422
|
+
case "loro-text":
|
|
1423
|
+
case "ignore": return json;
|
|
1424
|
+
default: return applyDecode(schema, json);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
/**
|
|
1428
|
+
* Apply encode transform to a domain value.
|
|
1429
|
+
* Null/undefined pass through as-is - transform is only called for real values.
|
|
1430
|
+
*/
|
|
1431
|
+
function applyEncode(schema, domainValue) {
|
|
1432
|
+
if (domainValue === null || domainValue === void 0) return domainValue;
|
|
1433
|
+
const transform = getTransform(schema);
|
|
1434
|
+
return transform ? transform.encode(domainValue) : domainValue;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Check if two domain values are equal according to the schema's equality strategy.
|
|
1438
|
+
*
|
|
1439
|
+
* Reference equality is always checked first (performance optimization).
|
|
1440
|
+
* If refs match, returns true (no change).
|
|
1441
|
+
* If refs differ, behavior depends on isEqual setting.
|
|
1442
|
+
*/
|
|
1443
|
+
function valuesEqual(schema, oldValue, newValue, strategyIfNotTransformable) {
|
|
1444
|
+
if (oldValue === newValue) return true;
|
|
1445
|
+
const strategy = getEqualityStrategy(schema, strategyIfNotTransformable);
|
|
1446
|
+
if (strategy === "reference-equality") return false;
|
|
1447
|
+
else if (strategy === "encoded-value-equality") return applyEncode(schema, oldValue) === applyEncode(schema, newValue);
|
|
1448
|
+
else if (strategy === "deep-equality") return deepEqual(oldValue, newValue);
|
|
1449
|
+
else return strategy(oldValue, newValue);
|
|
1450
|
+
}
|
|
1451
|
+
function defineCidProperty(target, cid) {
|
|
1452
|
+
if (!isObject(target) || Object.prototype.hasOwnProperty.call(target, "$cid")) return;
|
|
1453
|
+
Object.defineProperty(target, CID_KEY, { value: cid });
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Restore the read-only `$cid` contract on a freshly produced state.
|
|
1457
|
+
*
|
|
1458
|
+
* `defineCidProperty` stamps `$cid` as `{ writable: false, enumerable: false,
|
|
1459
|
+
* configurable: false }`. Immer's `useStrictShallowCopy` preserves the descriptor, but it
|
|
1460
|
+
* deliberately rewrites every non-writable data property to `{ writable: true,
|
|
1461
|
+
* configurable: true }` so the draft stays mutable — which would leave `$cid` writable on
|
|
1462
|
+
* every object Immer copied, letting callers reassign a published `$cid`.
|
|
1463
|
+
*
|
|
1464
|
+
* Two things keep this proportional to what Immer actually copied rather than to the size
|
|
1465
|
+
* of the state:
|
|
1466
|
+
*
|
|
1467
|
+
* - `prev` is the state `produce` was called with, and Immer shares everything it did not
|
|
1468
|
+
* copy, so a subtree reference-equal to `prev` cannot have been touched.
|
|
1469
|
+
* - An already-locked `$cid` means this object was not copied this round, so its children
|
|
1470
|
+
* are the same references they were when it was last hardened and are locked too. This
|
|
1471
|
+
* is what makes a reorder cheap: moved items are shared references that land at a new
|
|
1472
|
+
* index, so `prev` no longer lines up with them, but they are still locked.
|
|
1473
|
+
*/
|
|
1474
|
+
function hardenCidDescriptors(next, prev, visited) {
|
|
1475
|
+
if (next === prev) return;
|
|
1476
|
+
if (Array.isArray(next)) {
|
|
1477
|
+
if ((visited ?? (visited = /* @__PURE__ */ new Set())).has(next)) return;
|
|
1478
|
+
visited.add(next);
|
|
1479
|
+
const prevArr = Array.isArray(prev) ? prev : void 0;
|
|
1480
|
+
for (let i = 0; i < next.length; i++) hardenCidDescriptors(next[i], prevArr?.[i], visited);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
if (!isObject(next)) return;
|
|
1484
|
+
const descriptor = Object.getOwnPropertyDescriptor(next, CID_KEY);
|
|
1485
|
+
if (descriptor) {
|
|
1486
|
+
if (!descriptor.configurable) return;
|
|
1487
|
+
if ("value" in descriptor) Object.defineProperty(next, CID_KEY, {
|
|
1488
|
+
value: descriptor.value,
|
|
1489
|
+
writable: false,
|
|
1490
|
+
enumerable: false,
|
|
1491
|
+
configurable: false
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
if ((visited ?? (visited = /* @__PURE__ */ new Set())).has(next)) return;
|
|
1495
|
+
visited.add(next);
|
|
1496
|
+
const prevObj = isObject(prev) ? prev : void 0;
|
|
1497
|
+
for (const key of Object.keys(next)) hardenCidDescriptors(next[key], prevObj?.[key], visited);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Check if a value is an object
|
|
1501
|
+
*/
|
|
1502
|
+
function isObject(value) {
|
|
1503
|
+
return isPlainObjectValue(value);
|
|
1504
|
+
}
|
|
1505
|
+
const UNSAFE_KEYS = new Set([
|
|
1506
|
+
"__proto__",
|
|
1507
|
+
"constructor",
|
|
1508
|
+
"prototype"
|
|
1509
|
+
]);
|
|
1510
|
+
/**
|
|
1511
|
+
* Recursively removes undefined values from an object.
|
|
1512
|
+
* This treats undefined values as non-existent fields.
|
|
1513
|
+
* Preserves non-enumerable properties like $cid.
|
|
1514
|
+
* Returns the original object if no undefined values are found.
|
|
1515
|
+
* Protects against prototype pollution by skipping unsafe keys.
|
|
1516
|
+
*/
|
|
1517
|
+
function stripUndefined(value) {
|
|
1518
|
+
return stripUndefinedInner(value, 0, void 0);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Depth at which we start recording the recursion path to detect cycles.
|
|
1522
|
+
*
|
|
1523
|
+
* This runs over the whole state on every `setState`, and tracking every node costs more
|
|
1524
|
+
* than the stripping itself. Mirror state is a tree whose depth follows the schema, so a
|
|
1525
|
+
* real state never gets near this; only a cycle recurses without bound. Once the path is
|
|
1526
|
+
* being recorded, any cycle revisits one of its own nodes within a single lap, so starting
|
|
1527
|
+
* late still terminates.
|
|
1528
|
+
*/
|
|
1529
|
+
const CYCLE_GUARD_DEPTH = 64;
|
|
1530
|
+
/**
|
|
1531
|
+
* Placeholder for a key that must not reach the rebuilt object: an unsafe key, or one whose
|
|
1532
|
+
* value is `undefined` and is therefore treated as a non-existent field.
|
|
1533
|
+
*/
|
|
1534
|
+
const DROPPED = Symbol("dropped");
|
|
1535
|
+
function stripUndefinedInner(value, depth, path) {
|
|
1536
|
+
if (value === void 0) return value;
|
|
1537
|
+
if (Array.isArray(value)) {
|
|
1538
|
+
if (depth >= CYCLE_GUARD_DEPTH) {
|
|
1539
|
+
if ((path ?? (path = /* @__PURE__ */ new Set())).has(value)) return value;
|
|
1540
|
+
path.add(value);
|
|
1541
|
+
}
|
|
1542
|
+
let hasChanges = false;
|
|
1543
|
+
const result = value.map((item) => {
|
|
1544
|
+
const stripped = stripUndefinedInner(item, depth + 1, path);
|
|
1545
|
+
if (stripped !== item) hasChanges = true;
|
|
1546
|
+
return stripped;
|
|
1547
|
+
});
|
|
1548
|
+
path?.delete(value);
|
|
1549
|
+
return hasChanges ? result : value;
|
|
1550
|
+
}
|
|
1551
|
+
if (isObject(value)) {
|
|
1552
|
+
if (depth >= CYCLE_GUARD_DEPTH) {
|
|
1553
|
+
if ((path ?? (path = /* @__PURE__ */ new Set())).has(value)) return value;
|
|
1554
|
+
path.add(value);
|
|
1555
|
+
}
|
|
1556
|
+
const keys = Object.keys(value);
|
|
1557
|
+
const strippedValues = Array.from({ length: keys.length });
|
|
1558
|
+
let changed = false;
|
|
1559
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1560
|
+
const key = keys[i];
|
|
1561
|
+
if (UNSAFE_KEYS.has(key)) {
|
|
1562
|
+
strippedValues[i] = DROPPED;
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
const val = value[key];
|
|
1566
|
+
if (val === void 0) {
|
|
1567
|
+
strippedValues[i] = DROPPED;
|
|
1568
|
+
changed = true;
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
const stripped = stripUndefinedInner(val, depth + 1, path);
|
|
1572
|
+
strippedValues[i] = stripped;
|
|
1573
|
+
if (stripped !== val) changed = true;
|
|
1574
|
+
}
|
|
1575
|
+
path?.delete(value);
|
|
1576
|
+
if (!changed) return value;
|
|
1577
|
+
const result = Object.create(null);
|
|
1578
|
+
const allProps = Object.getOwnPropertyNames(value);
|
|
1579
|
+
for (const key of allProps) {
|
|
1580
|
+
if (UNSAFE_KEYS.has(key)) continue;
|
|
1581
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1582
|
+
if (descriptor && !descriptor.enumerable) Object.defineProperty(result, key, descriptor);
|
|
1583
|
+
}
|
|
1584
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1585
|
+
const val = strippedValues[i];
|
|
1586
|
+
if (val === DROPPED) continue;
|
|
1587
|
+
Object.defineProperty(result, keys[i], {
|
|
1588
|
+
value: val,
|
|
1589
|
+
writable: true,
|
|
1590
|
+
enumerable: true,
|
|
1591
|
+
configurable: true
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
return result;
|
|
1595
|
+
}
|
|
1596
|
+
return value;
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Performs a deep equality check between two values
|
|
1600
|
+
*/
|
|
1601
|
+
function deepEqual(a, b) {
|
|
1602
|
+
if (a === b) return true;
|
|
1603
|
+
if (a === null || b === null || typeof a !== "object" && typeof a !== "function" || typeof b !== "object" && typeof b !== "function") return false;
|
|
1604
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1605
|
+
if (a.length !== b.length) return false;
|
|
1606
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1609
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
1610
|
+
if (a instanceof RegExp && b instanceof RegExp) return a.toString() === b.toString();
|
|
1611
|
+
if (!Array.isArray(a) && !Array.isArray(b)) {
|
|
1612
|
+
const keysA = Object.keys(a);
|
|
1613
|
+
const keysB = Object.keys(b);
|
|
1614
|
+
if (keysA.length !== keysB.length) return false;
|
|
1615
|
+
for (const key of keysA) {
|
|
1616
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
1617
|
+
if (!deepEqual(a[key], b[key])) return false;
|
|
1618
|
+
}
|
|
1619
|
+
return true;
|
|
1620
|
+
}
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
/**
|
|
1624
|
+
* Recursively compare the `$cid` markers of two state trees.
|
|
1625
|
+
*
|
|
1626
|
+
* `deepEqual` only walks enumerable keys, so it cannot see `$cid` (which is stamped as a
|
|
1627
|
+
* non-enumerable descriptor). Used by `Mirror.checkStateConsistency` so that a tampered
|
|
1628
|
+
* or stale `$cid` is reported as a divergence instead of silently sitting in the state.
|
|
1629
|
+
* Only the markers are compared here; value equality is `deepEqual`'s job.
|
|
1630
|
+
*/
|
|
1631
|
+
function cidsEqual(a, b, seen) {
|
|
1632
|
+
if (a === b) return true;
|
|
1633
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1634
|
+
if (a.length !== b.length) return false;
|
|
1635
|
+
if (markSeen(a, b, seen ?? (seen = /* @__PURE__ */ new Map())) === "seen") return true;
|
|
1636
|
+
for (let i = 0; i < a.length; i++) if (!cidsEqual(a[i], b[i], seen)) return false;
|
|
1637
|
+
return true;
|
|
1638
|
+
}
|
|
1639
|
+
if (!isObject(a) || !isObject(b)) return true;
|
|
1640
|
+
if (a["$cid"] !== b["$cid"]) return false;
|
|
1641
|
+
if (markSeen(a, b, seen ?? (seen = /* @__PURE__ */ new Map())) === "seen") return true;
|
|
1642
|
+
for (const key of Object.keys(a)) {
|
|
1643
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) continue;
|
|
1644
|
+
if (!cidsEqual(a[key], b[key], seen)) return false;
|
|
1645
|
+
}
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
function markSeen(a, b, seen) {
|
|
1649
|
+
let partners = seen.get(a);
|
|
1650
|
+
if (!partners) {
|
|
1651
|
+
partners = /* @__PURE__ */ new Set();
|
|
1652
|
+
seen.set(a, partners);
|
|
1653
|
+
}
|
|
1654
|
+
if (partners.has(b)) return "seen";
|
|
1655
|
+
partners.add(b);
|
|
1656
|
+
return "new";
|
|
1657
|
+
}
|
|
1658
|
+
function containerIdToContainerType(containerId) {
|
|
1659
|
+
const parts = containerId.split(":");
|
|
1660
|
+
return parts[parts.length - 1];
|
|
1661
|
+
}
|
|
1662
|
+
function getRootContainerByType(doc, key, type) {
|
|
1663
|
+
if (type === "Text") return doc.getText(key);
|
|
1664
|
+
else if (type === "List") return doc.getList(key);
|
|
1665
|
+
else if (type === "MovableList") return doc.getMovableList(key);
|
|
1666
|
+
else if (type === "Map") return doc.getMap(key);
|
|
1667
|
+
else if (type === "Tree") return doc.getTree(key);
|
|
1668
|
+
else throw new Error();
|
|
1669
|
+
}
|
|
1670
|
+
function insertChildToMap(containerId, key, value, inferOptions) {
|
|
1671
|
+
const ct = tryInferContainerType(value, inferOptions);
|
|
1672
|
+
if (ct) return {
|
|
1673
|
+
container: containerId,
|
|
1674
|
+
key,
|
|
1675
|
+
value,
|
|
1676
|
+
kind: "insert-container",
|
|
1677
|
+
childContainerType: ct
|
|
1678
|
+
};
|
|
1679
|
+
return {
|
|
1680
|
+
container: containerId,
|
|
1681
|
+
key,
|
|
1682
|
+
value,
|
|
1683
|
+
kind: "insert"
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
function tryUpdateToContainer(change, toUpdate, schema, inferOptions) {
|
|
1687
|
+
if (!toUpdate) return change;
|
|
1688
|
+
if (change.kind !== "insert" && change.kind !== "set") return change;
|
|
1689
|
+
const effectiveInferOptions = applySchemaToInferOptions(schema, inferOptions);
|
|
1690
|
+
const containerType = schema ? schemaToContainerType(schema) ?? tryInferContainerType(change.value, effectiveInferOptions) : tryInferContainerType(change.value, effectiveInferOptions);
|
|
1691
|
+
if (containerType == null || schema && hasTransform(schema)) {
|
|
1692
|
+
const encodedValue = applyEncode(schema, change.value);
|
|
1693
|
+
return encodedValue !== change.value ? {
|
|
1694
|
+
...change,
|
|
1695
|
+
value: encodedValue
|
|
1696
|
+
} : change;
|
|
1697
|
+
}
|
|
1698
|
+
if (change.kind === "insert") return {
|
|
1699
|
+
container: change.container,
|
|
1700
|
+
key: change.key,
|
|
1701
|
+
value: change.value,
|
|
1702
|
+
kind: "insert-container",
|
|
1703
|
+
childContainerType: containerType
|
|
1704
|
+
};
|
|
1705
|
+
if (change.kind === "set") return {
|
|
1706
|
+
container: change.container,
|
|
1707
|
+
key: change.key,
|
|
1708
|
+
value: change.value,
|
|
1709
|
+
kind: "set-container",
|
|
1710
|
+
childContainerType: containerType
|
|
1711
|
+
};
|
|
1712
|
+
return change;
|
|
1713
|
+
}
|
|
1714
|
+
function schemaToContainerType(schema) {
|
|
1715
|
+
const containerType = schema.getContainerType();
|
|
1716
|
+
return containerType === null ? void 0 : containerType;
|
|
1717
|
+
}
|
|
1718
|
+
function tryInferContainerType(value, defaults) {
|
|
1719
|
+
return inferContainerTypeFromValue$1(value, defaults);
|
|
1720
|
+
}
|
|
1721
|
+
function applySchemaToInferOptions(schema, base) {
|
|
1722
|
+
if (!schema || schema.type !== "any") return base;
|
|
1723
|
+
const next = { ...base };
|
|
1724
|
+
next.defaultLoroText = schema.options.defaultLoroText ?? false;
|
|
1725
|
+
if (schema.options.defaultMovableList !== void 0) next.defaultMovableList = schema.options.defaultMovableList;
|
|
1726
|
+
return next;
|
|
1727
|
+
}
|
|
1728
|
+
function isValueOfContainerType(containerType, value) {
|
|
1729
|
+
return matchesContainerType(containerType, value);
|
|
1730
|
+
}
|
|
1731
|
+
function inferContainerTypeFromValue(value, defaults) {
|
|
1732
|
+
return inferSchemaContainerTypeFromValue(value, defaults);
|
|
1733
|
+
}
|
|
1734
|
+
function isObjectLike(value) {
|
|
1735
|
+
return typeof value === "object";
|
|
1736
|
+
}
|
|
1737
|
+
function isArrayLike(value) {
|
|
1738
|
+
return Array.isArray(value);
|
|
1739
|
+
}
|
|
1740
|
+
function isStateAndSchemaOfType(values, stateGuard, schemaGuard) {
|
|
1741
|
+
return stateGuard(values.oldState) && stateGuard(values.newState) && (!values.schema || schemaGuard(values.schema));
|
|
1742
|
+
}
|
|
1743
|
+
function isTreeID(id) {
|
|
1744
|
+
if (!(typeof id === "string")) return false;
|
|
1745
|
+
return !!id.match(/[0-9]+@[0-9]+/);
|
|
1746
|
+
}
|
|
1747
|
+
/**
|
|
1748
|
+
* Stringify a value safely, handling non-JSON-serializable types.
|
|
1749
|
+
* Handles BigInt, Date, RegExp, functions, and custom objects.
|
|
1750
|
+
*/
|
|
1751
|
+
function safeStringify(value, indent = 2) {
|
|
1752
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1753
|
+
function replacer(val) {
|
|
1754
|
+
if (val === null || val === void 0) return val;
|
|
1755
|
+
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") return val;
|
|
1756
|
+
if (typeof val === "bigint") return `[BigInt: ${val.toString()}]`;
|
|
1757
|
+
if (typeof val === "function") return `[Function: ${val.name || "anonymous"}]`;
|
|
1758
|
+
if (typeof val === "symbol") return `[Symbol: ${val.description || ""}]`;
|
|
1759
|
+
if (typeof val === "object") {
|
|
1760
|
+
if (seen.has(val)) return "[Circular]";
|
|
1761
|
+
seen.add(val);
|
|
1762
|
+
if (val instanceof Date) return `[Date: ${val.toISOString()}]`;
|
|
1763
|
+
if (val instanceof RegExp) return `[RegExp: ${val.toString()}]`;
|
|
1764
|
+
if (val instanceof Error) return `[Error: ${val.message}]`;
|
|
1765
|
+
if (Array.isArray(val)) return val.map(replacer);
|
|
1766
|
+
const result = {};
|
|
1767
|
+
for (const key of Object.keys(val)) result[key] = replacer(val[key]);
|
|
1768
|
+
return result;
|
|
1769
|
+
}
|
|
1770
|
+
return Object.prototype.toString.call(val);
|
|
1771
|
+
}
|
|
1772
|
+
return JSON.stringify(replacer(value), null, indent);
|
|
1773
|
+
}
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#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
|
|
1776
|
+
const schemaValidationCache = /* @__PURE__ */ new WeakMap();
|
|
1777
|
+
function isCacheableValue(value) {
|
|
1778
|
+
return typeof value === "object" && value !== null;
|
|
1779
|
+
}
|
|
1780
|
+
function isSchemaValidated(schema, value) {
|
|
1781
|
+
if (!isCacheableValue(value)) return false;
|
|
1782
|
+
return schemaValidationCache.get(value)?.has(schema) ?? false;
|
|
1783
|
+
}
|
|
1784
|
+
function markSchemaValidated(schema, value) {
|
|
1785
|
+
if (!isCacheableValue(value)) return;
|
|
1786
|
+
let cache = schemaValidationCache.get(value);
|
|
1787
|
+
if (!cache) {
|
|
1788
|
+
cache = /* @__PURE__ */ new WeakSet();
|
|
1789
|
+
schemaValidationCache.set(value, cache);
|
|
1790
|
+
}
|
|
1791
|
+
cache.add(schema);
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* Type guard for LoroMapSchema
|
|
1795
|
+
*/
|
|
1796
|
+
function isLoroMapSchema(schema) {
|
|
1797
|
+
return !!schema && schema.type === "loro-map";
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* Type guard for LoroListSchema
|
|
1801
|
+
*/
|
|
1802
|
+
function isLoroListSchema(schema) {
|
|
1803
|
+
return !!schema && schema.type === "loro-list";
|
|
1804
|
+
}
|
|
1805
|
+
function isListLikeSchema(schema) {
|
|
1806
|
+
return isLoroListSchema(schema) || isLoroMovableListSchema(schema);
|
|
1807
|
+
}
|
|
1808
|
+
function isLoroMovableListSchema(schema) {
|
|
1809
|
+
return !!schema && schema.type === "loro-movable-list";
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* Type guard for RootSchemaType
|
|
1813
|
+
*/
|
|
1814
|
+
function isRootSchemaType(schema) {
|
|
1815
|
+
return !!schema && schema.type === "schema";
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Whether a schema subtree contains any `Ignore` field. Recursive schemas are
|
|
1819
|
+
* tolerated (each schema object is visited once).
|
|
1820
|
+
*/
|
|
1821
|
+
function schemaContainsIgnoreField(schema) {
|
|
1822
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1823
|
+
const walk = (s) => {
|
|
1824
|
+
if (!s || visited.has(s)) return false;
|
|
1825
|
+
visited.add(s);
|
|
1826
|
+
if (s.type === "ignore") return true;
|
|
1827
|
+
switch (s.type) {
|
|
1828
|
+
case "schema":
|
|
1829
|
+
case "loro-map": {
|
|
1830
|
+
const record = s;
|
|
1831
|
+
return Object.values(record.definition).some(walk) || walk(record.catchallType);
|
|
1832
|
+
}
|
|
1833
|
+
case "loro-list":
|
|
1834
|
+
case "loro-movable-list": return walk(s.itemSchema);
|
|
1835
|
+
case "loro-tree": return walk(s.nodeSchema);
|
|
1836
|
+
default: return false;
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
return walk(schema);
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Type guard for LoroTreeSchema
|
|
1843
|
+
*/
|
|
1844
|
+
function isLoroTreeSchema(schema) {
|
|
1845
|
+
return !!schema && schema.type === "loro-tree";
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* Check if a schema is for a Loro container
|
|
1849
|
+
*/
|
|
1850
|
+
function isContainerSchema(schema) {
|
|
1851
|
+
return !!schema && (schema.type === "loro-map" || schema.type === "loro-list" || schema.type === "loro-text" || schema.type === "loro-movable-list" || schema.type === "loro-tree");
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Validate a value against a schema
|
|
1855
|
+
*
|
|
1856
|
+
* When `options.ignoreUnknownProperties` is true, the root schema check does
|
|
1857
|
+
* not reject properties missing from the schema definition — typically
|
|
1858
|
+
* written by peers on a newer schema version. (Nested map schemas never
|
|
1859
|
+
* reject undeclared properties.)
|
|
1860
|
+
*/
|
|
1861
|
+
function validateSchema(schema, value, options) {
|
|
1862
|
+
const errors = [];
|
|
1863
|
+
const actualType = schema.type;
|
|
1864
|
+
if (schema.options.required && (value === void 0 || value === null)) {
|
|
1865
|
+
errors.push("Value is required");
|
|
1866
|
+
return {
|
|
1867
|
+
valid: false,
|
|
1868
|
+
errors
|
|
1869
|
+
};
|
|
1870
|
+
}
|
|
1871
|
+
if (value === void 0 || value === null) return { valid: true };
|
|
1872
|
+
const useCache = !options?.ignoreUnknownProperties;
|
|
1873
|
+
if (useCache && isSchemaValidated(schema, value)) return { valid: true };
|
|
1874
|
+
switch (actualType) {
|
|
1875
|
+
case "any":
|
|
1876
|
+
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");
|
|
1877
|
+
break;
|
|
1878
|
+
case "string":
|
|
1879
|
+
case "number":
|
|
1880
|
+
case "boolean":
|
|
1881
|
+
validateTransformablePrimitive(schema, value, actualType, errors);
|
|
1882
|
+
break;
|
|
1883
|
+
case "ignore": break;
|
|
1884
|
+
case "loro-text":
|
|
1885
|
+
if (typeof value !== "string") errors.push("Content must be a string");
|
|
1886
|
+
break;
|
|
1887
|
+
case "loro-map":
|
|
1888
|
+
if (!isObject(value)) errors.push("Value must be an object");
|
|
1889
|
+
else if (isLoroMapSchema(schema)) {
|
|
1890
|
+
for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
|
|
1891
|
+
const propSchema = schema.definition[key];
|
|
1892
|
+
const propValue = value[key];
|
|
1893
|
+
const result = validateSchema(propSchema, propValue);
|
|
1894
|
+
if (!result.valid && result.errors) {
|
|
1895
|
+
const prefixedErrors = result.errors.map((err) => `${key}: ${err}`);
|
|
1896
|
+
errors.push(...prefixedErrors);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
break;
|
|
1901
|
+
case "loro-movable-list":
|
|
1902
|
+
case "loro-list":
|
|
1903
|
+
if (!Array.isArray(value)) errors.push("Value must be an array");
|
|
1904
|
+
else if (isLoroListSchema(schema) || isLoroMovableListSchema(schema)) {
|
|
1905
|
+
const itemSchema = schema.itemSchema;
|
|
1906
|
+
value.forEach((item, index) => {
|
|
1907
|
+
const result = validateSchema(itemSchema, item);
|
|
1908
|
+
if (!result.valid && result.errors) {
|
|
1909
|
+
const prefixedErrors = result.errors.map((err) => `Item ${index}: ${err}`);
|
|
1910
|
+
errors.push(...prefixedErrors);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
break;
|
|
1915
|
+
case "loro-tree": {
|
|
1916
|
+
if (!Array.isArray(value)) {
|
|
1917
|
+
errors.push("Value must be an array of tree nodes");
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
if (!isLoroTreeSchema(schema)) {
|
|
1921
|
+
errors.push("Invalid tree schema");
|
|
1922
|
+
break;
|
|
1923
|
+
}
|
|
1924
|
+
const validateNode = (node, path) => {
|
|
1925
|
+
if (!isObject(node)) {
|
|
1926
|
+
errors.push(`${path}: Node must be an object`);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const n = node;
|
|
1930
|
+
if (n.id != null && typeof n.id !== "string") errors.push(`${path}: id must be a string if provided`);
|
|
1931
|
+
const dataResult = validateSchema(schema.nodeSchema, n.data);
|
|
1932
|
+
if (!dataResult.valid && dataResult.errors) errors.push(...dataResult.errors.map((e) => `${path}.data: ${e}`));
|
|
1933
|
+
if (!Array.isArray(n.children)) errors.push(`${path}: children must be an array`);
|
|
1934
|
+
else n.children.forEach((child, idx) => {
|
|
1935
|
+
validateNode(child, `${path}.children[${idx}]`);
|
|
1936
|
+
});
|
|
1937
|
+
};
|
|
1938
|
+
value.forEach((node, i) => {
|
|
1939
|
+
validateNode(node, `node[${i}]`);
|
|
1940
|
+
});
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
case "schema":
|
|
1944
|
+
if (!isObject(value)) errors.push("Value must be an object");
|
|
1945
|
+
else if (isRootSchemaType(schema)) if (!isObject(value)) errors.push("Value must be an object");
|
|
1946
|
+
else {
|
|
1947
|
+
for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
|
|
1948
|
+
const propSchema = schema.definition[key];
|
|
1949
|
+
const propValue = value[key];
|
|
1950
|
+
const result = validateSchema(propSchema, propValue);
|
|
1951
|
+
if (!result.valid && result.errors) {
|
|
1952
|
+
const prefixedErrors = result.errors.map((err) => `${key}: ${err}`);
|
|
1953
|
+
errors.push(...prefixedErrors);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
for (const key in value) if (!Object.prototype.hasOwnProperty.call(schema.definition, key)) {
|
|
1957
|
+
if (options?.ignoreUnknownProperties) continue;
|
|
1958
|
+
errors.push(`Unknown property: ${key}`);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
else errors.push(`Should be a schema, but got ${actualType}`);
|
|
1962
|
+
break;
|
|
1963
|
+
default: errors.push(`Unknown schema type: ${actualType}`);
|
|
1964
|
+
}
|
|
1965
|
+
if (schema.options.validate && typeof schema.options.validate === "function") try {
|
|
1966
|
+
const customValidation = schema.options.validate(value);
|
|
1967
|
+
if (customValidation !== true) {
|
|
1968
|
+
const errorMessage = typeof customValidation === "string" ? customValidation : "Value failed custom validation";
|
|
1969
|
+
errors.push(errorMessage);
|
|
1970
|
+
}
|
|
1971
|
+
} catch (error) {
|
|
1972
|
+
errors.push(`Validation error: ${String(error)}`);
|
|
1973
|
+
}
|
|
1974
|
+
if (errors.length === 0) {
|
|
1975
|
+
if (useCache) markSchemaValidated(schema, value);
|
|
1976
|
+
return { valid: true };
|
|
1977
|
+
}
|
|
1978
|
+
return {
|
|
1979
|
+
valid: false,
|
|
1980
|
+
errors
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1983
|
+
function validateTransformablePrimitive(schema, value, expectedType, errors) {
|
|
1984
|
+
const transform = getTransform(schema);
|
|
1985
|
+
if (transform) {
|
|
1986
|
+
if (transform.validate) try {
|
|
1987
|
+
const result = transform.validate(value);
|
|
1988
|
+
if (result !== true) errors.push(typeof result === "string" ? result : "Transform validation failed");
|
|
1989
|
+
} catch (error) {
|
|
1990
|
+
errors.push(`Transform validation error: ${String(error)}`);
|
|
1991
|
+
}
|
|
1992
|
+
if (transform.validateEncodedType) try {
|
|
1993
|
+
const encoded = applyEncode(schema, value);
|
|
1994
|
+
if (typeof encoded !== expectedType) errors.push(`Transform encode must return a ${expectedType}, got ${typeof encoded}`);
|
|
1995
|
+
} catch (error) {
|
|
1996
|
+
errors.push(`Transform encode validation error: ${String(error)}`);
|
|
1997
|
+
}
|
|
1998
|
+
} else if (typeof value !== expectedType) errors.push(`Value must be a ${expectedType}`);
|
|
1999
|
+
}
|
|
2000
|
+
/**
|
|
2001
|
+
* Get default value for a schema
|
|
2002
|
+
* Based on the schema type, it might return a plain value or a wrapped value
|
|
2003
|
+
*/
|
|
2004
|
+
function getDefaultValue(schema) {
|
|
2005
|
+
if ("defaultValue" in schema.options) return schema.options.defaultValue;
|
|
2006
|
+
switch (schema.type) {
|
|
2007
|
+
case "any": return;
|
|
2008
|
+
case "string":
|
|
2009
|
+
if (schema.options.required === false) return;
|
|
2010
|
+
if (getTransform(schema)) return;
|
|
2011
|
+
return "";
|
|
2012
|
+
case "number":
|
|
2013
|
+
if (schema.options.required === false) return;
|
|
2014
|
+
if (getTransform(schema)) return;
|
|
2015
|
+
return 0;
|
|
2016
|
+
case "boolean":
|
|
2017
|
+
if (schema.options.required === false) return;
|
|
2018
|
+
if (getTransform(schema)) return;
|
|
2019
|
+
return false;
|
|
2020
|
+
case "loro-text": {
|
|
2021
|
+
const value = schema.options.required ? "" : void 0;
|
|
2022
|
+
if (value === void 0) return void 0;
|
|
2023
|
+
return value;
|
|
2024
|
+
}
|
|
2025
|
+
case "loro-map":
|
|
2026
|
+
if (isLoroMapSchema(schema)) {
|
|
2027
|
+
const result = {};
|
|
2028
|
+
for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
|
|
2029
|
+
const value = getDefaultValue(schema.definition[key]);
|
|
2030
|
+
if (value !== void 0) result[key] = value;
|
|
2031
|
+
}
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
2034
|
+
return {};
|
|
2035
|
+
case "loro-list": return [];
|
|
2036
|
+
case "loro-tree": {
|
|
2037
|
+
const value = schema.options.required ? [] : void 0;
|
|
2038
|
+
if (value === void 0) return void 0;
|
|
2039
|
+
return value;
|
|
2040
|
+
}
|
|
2041
|
+
case "schema":
|
|
2042
|
+
if (isRootSchemaType(schema)) {
|
|
2043
|
+
const result = {};
|
|
2044
|
+
for (const key in schema.definition) if (Object.prototype.hasOwnProperty.call(schema.definition, key)) {
|
|
2045
|
+
const value = getDefaultValue(schema.definition[key]);
|
|
2046
|
+
if (value !== void 0) result[key] = value;
|
|
2047
|
+
}
|
|
2048
|
+
return result;
|
|
2049
|
+
}
|
|
2050
|
+
return {};
|
|
2051
|
+
default: return;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
//#endregion
|
|
2055
|
+
//#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
|
|
2056
|
+
/**
|
|
2057
|
+
* Create a schema definition
|
|
2058
|
+
*/
|
|
2059
|
+
function schema(definition, options) {
|
|
2060
|
+
return {
|
|
2061
|
+
type: "schema",
|
|
2062
|
+
definition,
|
|
2063
|
+
options: options || {},
|
|
2064
|
+
getContainerType() {
|
|
2065
|
+
return "Map";
|
|
2066
|
+
}
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Define a string field
|
|
2071
|
+
*/
|
|
2072
|
+
schema.String = (function(options) {
|
|
2073
|
+
const baseSchema = {
|
|
2074
|
+
type: "string",
|
|
2075
|
+
options: options || {},
|
|
2076
|
+
getContainerType: () => {
|
|
2077
|
+
return null;
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
return {
|
|
2081
|
+
...baseSchema,
|
|
2082
|
+
transform: (def) => ({
|
|
2083
|
+
...baseSchema,
|
|
2084
|
+
transform: def
|
|
2085
|
+
})
|
|
2086
|
+
};
|
|
2087
|
+
});
|
|
2088
|
+
/**
|
|
2089
|
+
* Define an any field (runtime-inferred by Mirror)
|
|
2090
|
+
*/
|
|
2091
|
+
schema.Any = function(options) {
|
|
2092
|
+
return {
|
|
2093
|
+
type: "any",
|
|
2094
|
+
options: options || {},
|
|
2095
|
+
getContainerType: () => {
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
};
|
|
2100
|
+
/**
|
|
2101
|
+
* Define a number field
|
|
2102
|
+
*/
|
|
2103
|
+
schema.Number = function(options) {
|
|
2104
|
+
const baseSchema = {
|
|
2105
|
+
type: "number",
|
|
2106
|
+
options: options || {},
|
|
2107
|
+
getContainerType: () => {
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
};
|
|
2111
|
+
return {
|
|
2112
|
+
...baseSchema,
|
|
2113
|
+
transform: (def) => ({
|
|
2114
|
+
...baseSchema,
|
|
2115
|
+
transform: def
|
|
2116
|
+
})
|
|
2117
|
+
};
|
|
2118
|
+
};
|
|
2119
|
+
/**
|
|
2120
|
+
* Define a boolean field
|
|
2121
|
+
*/
|
|
2122
|
+
schema.Boolean = function(options) {
|
|
2123
|
+
const baseSchema = {
|
|
2124
|
+
type: "boolean",
|
|
2125
|
+
options: options || {},
|
|
2126
|
+
getContainerType: () => {
|
|
2127
|
+
return null;
|
|
2128
|
+
}
|
|
2129
|
+
};
|
|
2130
|
+
return {
|
|
2131
|
+
...baseSchema,
|
|
2132
|
+
transform: (def) => ({
|
|
2133
|
+
...baseSchema,
|
|
2134
|
+
transform: def
|
|
2135
|
+
})
|
|
2136
|
+
};
|
|
2137
|
+
};
|
|
2138
|
+
/**
|
|
2139
|
+
* Define a field to be ignored (not synced with Loro)
|
|
2140
|
+
*/
|
|
2141
|
+
schema.Ignore = function(options) {
|
|
2142
|
+
return {
|
|
2143
|
+
type: "ignore",
|
|
2144
|
+
options: options || {},
|
|
2145
|
+
getContainerType: () => {
|
|
2146
|
+
return null;
|
|
2147
|
+
}
|
|
2148
|
+
};
|
|
2149
|
+
};
|
|
2150
|
+
/**
|
|
2151
|
+
* Define a Loro map
|
|
2152
|
+
*/
|
|
2153
|
+
schema.LoroMap = function(definition, options) {
|
|
2154
|
+
const baseSchema = {
|
|
2155
|
+
type: "loro-map",
|
|
2156
|
+
definition,
|
|
2157
|
+
options: options || {},
|
|
2158
|
+
getContainerType: () => {
|
|
2159
|
+
return "Map";
|
|
2160
|
+
}
|
|
2161
|
+
};
|
|
2162
|
+
const schemaWithCatchall = {
|
|
2163
|
+
...baseSchema,
|
|
2164
|
+
catchall: (catchallSchema) => {
|
|
2165
|
+
return {
|
|
2166
|
+
...baseSchema,
|
|
2167
|
+
catchallType: catchallSchema,
|
|
2168
|
+
catchall: (newCatchallSchema) => {
|
|
2169
|
+
return {
|
|
2170
|
+
...baseSchema,
|
|
2171
|
+
catchallType: newCatchallSchema,
|
|
2172
|
+
catchall: schemaWithCatchall.catchall
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
return schemaWithCatchall;
|
|
2179
|
+
};
|
|
2180
|
+
/**
|
|
2181
|
+
* Create a dynamic record schema (like zod's z.record)
|
|
2182
|
+
*/
|
|
2183
|
+
schema.LoroMapRecord = function(valueSchema, options) {
|
|
2184
|
+
return {
|
|
2185
|
+
type: "loro-map",
|
|
2186
|
+
definition: {},
|
|
2187
|
+
catchallType: valueSchema,
|
|
2188
|
+
options: options || {},
|
|
2189
|
+
getContainerType: () => {
|
|
2190
|
+
return "Map";
|
|
2191
|
+
},
|
|
2192
|
+
catchall: (newCatchallSchema) => {
|
|
2193
|
+
return schema.LoroMapRecord(newCatchallSchema, options);
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
};
|
|
2197
|
+
/**
|
|
2198
|
+
* Define a Loro list
|
|
2199
|
+
*/
|
|
2200
|
+
schema.LoroList = function(itemSchema, idSelector, options) {
|
|
2201
|
+
return {
|
|
2202
|
+
type: "loro-list",
|
|
2203
|
+
itemSchema,
|
|
2204
|
+
idSelector,
|
|
2205
|
+
options: options || {},
|
|
2206
|
+
getContainerType: () => {
|
|
2207
|
+
return "List";
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
};
|
|
2211
|
+
schema.LoroMovableList = function(itemSchema, idSelector, options) {
|
|
2212
|
+
return {
|
|
2213
|
+
type: "loro-movable-list",
|
|
2214
|
+
itemSchema,
|
|
2215
|
+
idSelector,
|
|
2216
|
+
options: options || {},
|
|
2217
|
+
getContainerType: () => {
|
|
2218
|
+
return "MovableList";
|
|
2219
|
+
}
|
|
2220
|
+
};
|
|
2221
|
+
};
|
|
2222
|
+
/**
|
|
2223
|
+
* Define a Loro text field
|
|
2224
|
+
*/
|
|
2225
|
+
schema.LoroText = function(options) {
|
|
2226
|
+
return {
|
|
2227
|
+
type: "loro-text",
|
|
2228
|
+
options: options || {},
|
|
2229
|
+
getContainerType: () => {
|
|
2230
|
+
return "Text";
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
2233
|
+
};
|
|
2234
|
+
/**
|
|
2235
|
+
* Define a Loro tree
|
|
2236
|
+
*
|
|
2237
|
+
* Each tree node has a `data` map described by `nodeSchema`.
|
|
2238
|
+
*/
|
|
2239
|
+
schema.LoroTree = function(nodeSchema, options) {
|
|
2240
|
+
return {
|
|
2241
|
+
type: "loro-tree",
|
|
2242
|
+
nodeSchema,
|
|
2243
|
+
options: options || {},
|
|
2244
|
+
getContainerType() {
|
|
2245
|
+
return "Tree";
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
};
|
|
2249
|
+
//#endregion
|
|
2250
|
+
//#region ../../node_modules/.pnpm/immer@10.2.0/node_modules/immer/dist/immer.mjs
|
|
2251
|
+
var NOTHING = Symbol.for("immer-nothing");
|
|
2252
|
+
var DRAFTABLE = Symbol.for("immer-draftable");
|
|
2253
|
+
var DRAFT_STATE = Symbol.for("immer-state");
|
|
2254
|
+
var errors = [
|
|
2255
|
+
function(plugin) {
|
|
2256
|
+
return `The plugin for '${plugin}' has not been loaded into Immer. To enable the plugin, import and call \`enable${plugin}()\` when initializing your application.`;
|
|
2257
|
+
},
|
|
2258
|
+
function(thing) {
|
|
2259
|
+
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}'`;
|
|
2260
|
+
},
|
|
2261
|
+
"This object has been frozen and should not be mutated",
|
|
2262
|
+
function(data) {
|
|
2263
|
+
return "Cannot use a proxy that has been revoked. Did you pass an object from inside an immer function to an async process? " + data;
|
|
2264
|
+
},
|
|
2265
|
+
"An immer producer returned a new value *and* modified its draft. Either return a new value *or* modify the draft.",
|
|
2266
|
+
"Immer forbids circular references",
|
|
2267
|
+
"The first or second argument to `produce` must be a function",
|
|
2268
|
+
"The third argument to `produce` must be a function or undefined",
|
|
2269
|
+
"First argument to `createDraft` must be a plain object, an array, or an immerable object",
|
|
2270
|
+
"First argument to `finishDraft` must be a draft returned by `createDraft`",
|
|
2271
|
+
function(thing) {
|
|
2272
|
+
return `'current' expects a draft, got: ${thing}`;
|
|
2273
|
+
},
|
|
2274
|
+
"Object.defineProperty() cannot be used on an Immer draft",
|
|
2275
|
+
"Object.setPrototypeOf() cannot be used on an Immer draft",
|
|
2276
|
+
"Immer only supports deleting array indices",
|
|
2277
|
+
"Immer only supports setting array indices and the 'length' property",
|
|
2278
|
+
function(thing) {
|
|
2279
|
+
return `'original' expects a draft, got: ${thing}`;
|
|
2280
|
+
}
|
|
2281
|
+
];
|
|
2282
|
+
function die(error, ...args) {
|
|
2283
|
+
{
|
|
2284
|
+
const e = errors[error];
|
|
2285
|
+
const msg = typeof e === "function" ? e.apply(null, args) : e;
|
|
2286
|
+
throw new Error(`[Immer] ${msg}`);
|
|
2287
|
+
}
|
|
2288
|
+
throw new Error(`[Immer] minified error nr: ${error}. Full error at: https://bit.ly/3cXEKWf`);
|
|
2289
|
+
}
|
|
2290
|
+
var getPrototypeOf = Object.getPrototypeOf;
|
|
2291
|
+
function isDraft(value) {
|
|
2292
|
+
return !!value && !!value[DRAFT_STATE];
|
|
2293
|
+
}
|
|
2294
|
+
function isDraftable(value) {
|
|
2295
|
+
if (!value) return false;
|
|
2296
|
+
return isPlainObject(value) || Array.isArray(value) || !!value[DRAFTABLE] || !!value.constructor?.[DRAFTABLE] || isMap(value) || isSet(value);
|
|
2297
|
+
}
|
|
2298
|
+
var objectCtorString = Object.prototype.constructor.toString();
|
|
2299
|
+
var cachedCtorStrings = /* @__PURE__ */ new WeakMap();
|
|
2300
|
+
function isPlainObject(value) {
|
|
2301
|
+
if (!value || typeof value !== "object") return false;
|
|
2302
|
+
const proto = Object.getPrototypeOf(value);
|
|
2303
|
+
if (proto === null || proto === Object.prototype) return true;
|
|
2304
|
+
const Ctor = Object.hasOwnProperty.call(proto, "constructor") && proto.constructor;
|
|
2305
|
+
if (Ctor === Object) return true;
|
|
2306
|
+
if (typeof Ctor !== "function") return false;
|
|
2307
|
+
let ctorString = cachedCtorStrings.get(Ctor);
|
|
2308
|
+
if (ctorString === void 0) {
|
|
2309
|
+
ctorString = Function.toString.call(Ctor);
|
|
2310
|
+
cachedCtorStrings.set(Ctor, ctorString);
|
|
2311
|
+
}
|
|
2312
|
+
return ctorString === objectCtorString;
|
|
2313
|
+
}
|
|
2314
|
+
function each(obj, iter, strict = true) {
|
|
2315
|
+
if (getArchtype(obj) === 0) (strict ? Reflect.ownKeys(obj) : Object.keys(obj)).forEach((key) => {
|
|
2316
|
+
iter(key, obj[key], obj);
|
|
2317
|
+
});
|
|
2318
|
+
else obj.forEach((entry, index) => iter(index, entry, obj));
|
|
2319
|
+
}
|
|
2320
|
+
function getArchtype(thing) {
|
|
2321
|
+
const state = thing[DRAFT_STATE];
|
|
2322
|
+
return state ? state.type_ : Array.isArray(thing) ? 1 : isMap(thing) ? 2 : isSet(thing) ? 3 : 0;
|
|
2323
|
+
}
|
|
2324
|
+
function has(thing, prop) {
|
|
2325
|
+
return getArchtype(thing) === 2 ? thing.has(prop) : Object.prototype.hasOwnProperty.call(thing, prop);
|
|
2326
|
+
}
|
|
2327
|
+
function set(thing, propOrOldValue, value) {
|
|
2328
|
+
const t = getArchtype(thing);
|
|
2329
|
+
if (t === 2) thing.set(propOrOldValue, value);
|
|
2330
|
+
else if (t === 3) thing.add(value);
|
|
2331
|
+
else thing[propOrOldValue] = value;
|
|
2332
|
+
}
|
|
2333
|
+
function is(x, y) {
|
|
2334
|
+
if (x === y) return x !== 0 || 1 / x === 1 / y;
|
|
2335
|
+
else return x !== x && y !== y;
|
|
2336
|
+
}
|
|
2337
|
+
function isMap(target) {
|
|
2338
|
+
return target instanceof Map;
|
|
2339
|
+
}
|
|
2340
|
+
function isSet(target) {
|
|
2341
|
+
return target instanceof Set;
|
|
2342
|
+
}
|
|
2343
|
+
function latest(state) {
|
|
2344
|
+
return state.copy_ || state.base_;
|
|
2345
|
+
}
|
|
2346
|
+
function shallowCopy(base, strict) {
|
|
2347
|
+
if (isMap(base)) return new Map(base);
|
|
2348
|
+
if (isSet(base)) return new Set(base);
|
|
2349
|
+
if (Array.isArray(base)) return Array.prototype.slice.call(base);
|
|
2350
|
+
const isPlain = isPlainObject(base);
|
|
2351
|
+
if (strict === true || strict === "class_only" && !isPlain) {
|
|
2352
|
+
const descriptors = Object.getOwnPropertyDescriptors(base);
|
|
2353
|
+
delete descriptors[DRAFT_STATE];
|
|
2354
|
+
let keys = Reflect.ownKeys(descriptors);
|
|
2355
|
+
for (let i = 0; i < keys.length; i++) {
|
|
2356
|
+
const key = keys[i];
|
|
2357
|
+
const desc = descriptors[key];
|
|
2358
|
+
if (desc.writable === false) {
|
|
2359
|
+
desc.writable = true;
|
|
2360
|
+
desc.configurable = true;
|
|
2361
|
+
}
|
|
2362
|
+
if (desc.get || desc.set) descriptors[key] = {
|
|
2363
|
+
configurable: true,
|
|
2364
|
+
writable: true,
|
|
2365
|
+
enumerable: desc.enumerable,
|
|
2366
|
+
value: base[key]
|
|
2367
|
+
};
|
|
2368
|
+
}
|
|
2369
|
+
return Object.create(getPrototypeOf(base), descriptors);
|
|
2370
|
+
} else {
|
|
2371
|
+
const proto = getPrototypeOf(base);
|
|
2372
|
+
if (proto !== null && isPlain) return { ...base };
|
|
2373
|
+
return Object.assign(Object.create(proto), base);
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
function freeze(obj, deep = false) {
|
|
2377
|
+
if (isFrozen(obj) || isDraft(obj) || !isDraftable(obj)) return obj;
|
|
2378
|
+
if (getArchtype(obj) > 1) Object.defineProperties(obj, {
|
|
2379
|
+
set: dontMutateMethodOverride,
|
|
2380
|
+
add: dontMutateMethodOverride,
|
|
2381
|
+
clear: dontMutateMethodOverride,
|
|
2382
|
+
delete: dontMutateMethodOverride
|
|
2383
|
+
});
|
|
2384
|
+
Object.freeze(obj);
|
|
2385
|
+
if (deep) Object.values(obj).forEach((value) => freeze(value, true));
|
|
2386
|
+
return obj;
|
|
2387
|
+
}
|
|
2388
|
+
function dontMutateFrozenCollections() {
|
|
2389
|
+
die(2);
|
|
2390
|
+
}
|
|
2391
|
+
var dontMutateMethodOverride = { value: dontMutateFrozenCollections };
|
|
2392
|
+
function isFrozen(obj) {
|
|
2393
|
+
if (obj === null || typeof obj !== "object") return true;
|
|
2394
|
+
return Object.isFrozen(obj);
|
|
2395
|
+
}
|
|
2396
|
+
var plugins = {};
|
|
2397
|
+
function getPlugin(pluginKey) {
|
|
2398
|
+
const plugin = plugins[pluginKey];
|
|
2399
|
+
if (!plugin) die(0, pluginKey);
|
|
2400
|
+
return plugin;
|
|
2401
|
+
}
|
|
2402
|
+
var currentScope;
|
|
2403
|
+
function getCurrentScope() {
|
|
2404
|
+
return currentScope;
|
|
2405
|
+
}
|
|
2406
|
+
function createScope(parent_, immer_) {
|
|
2407
|
+
return {
|
|
2408
|
+
drafts_: [],
|
|
2409
|
+
parent_,
|
|
2410
|
+
immer_,
|
|
2411
|
+
canAutoFreeze_: true,
|
|
2412
|
+
unfinalizedDrafts_: 0
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function usePatchesInScope(scope, patchListener) {
|
|
2416
|
+
if (patchListener) {
|
|
2417
|
+
getPlugin("Patches");
|
|
2418
|
+
scope.patches_ = [];
|
|
2419
|
+
scope.inversePatches_ = [];
|
|
2420
|
+
scope.patchListener_ = patchListener;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
function revokeScope(scope) {
|
|
2424
|
+
leaveScope(scope);
|
|
2425
|
+
scope.drafts_.forEach(revokeDraft);
|
|
2426
|
+
scope.drafts_ = null;
|
|
2427
|
+
}
|
|
2428
|
+
function leaveScope(scope) {
|
|
2429
|
+
if (scope === currentScope) currentScope = scope.parent_;
|
|
2430
|
+
}
|
|
2431
|
+
function enterScope(immer2) {
|
|
2432
|
+
return currentScope = createScope(currentScope, immer2);
|
|
2433
|
+
}
|
|
2434
|
+
function revokeDraft(draft) {
|
|
2435
|
+
const state = draft[DRAFT_STATE];
|
|
2436
|
+
if (state.type_ === 0 || state.type_ === 1) state.revoke_();
|
|
2437
|
+
else state.revoked_ = true;
|
|
2438
|
+
}
|
|
2439
|
+
function processResult(result, scope) {
|
|
2440
|
+
scope.unfinalizedDrafts_ = scope.drafts_.length;
|
|
2441
|
+
const baseDraft = scope.drafts_[0];
|
|
2442
|
+
if (result !== void 0 && result !== baseDraft) {
|
|
2443
|
+
if (baseDraft[DRAFT_STATE].modified_) {
|
|
2444
|
+
revokeScope(scope);
|
|
2445
|
+
die(4);
|
|
2446
|
+
}
|
|
2447
|
+
if (isDraftable(result)) {
|
|
2448
|
+
result = finalize(scope, result);
|
|
2449
|
+
if (!scope.parent_) maybeFreeze(scope, result);
|
|
2450
|
+
}
|
|
2451
|
+
if (scope.patches_) getPlugin("Patches").generateReplacementPatches_(baseDraft[DRAFT_STATE].base_, result, scope.patches_, scope.inversePatches_);
|
|
2452
|
+
} else result = finalize(scope, baseDraft, []);
|
|
2453
|
+
revokeScope(scope);
|
|
2454
|
+
if (scope.patches_) scope.patchListener_(scope.patches_, scope.inversePatches_);
|
|
2455
|
+
return result !== NOTHING ? result : void 0;
|
|
2456
|
+
}
|
|
2457
|
+
function finalize(rootScope, value, path) {
|
|
2458
|
+
if (isFrozen(value)) return value;
|
|
2459
|
+
const useStrictIteration = rootScope.immer_.shouldUseStrictIteration();
|
|
2460
|
+
const state = value[DRAFT_STATE];
|
|
2461
|
+
if (!state) {
|
|
2462
|
+
each(value, (key, childValue) => finalizeProperty(rootScope, state, value, key, childValue, path), useStrictIteration);
|
|
2463
|
+
return value;
|
|
2464
|
+
}
|
|
2465
|
+
if (state.scope_ !== rootScope) return value;
|
|
2466
|
+
if (!state.modified_) {
|
|
2467
|
+
maybeFreeze(rootScope, state.base_, true);
|
|
2468
|
+
return state.base_;
|
|
2469
|
+
}
|
|
2470
|
+
if (!state.finalized_) {
|
|
2471
|
+
state.finalized_ = true;
|
|
2472
|
+
state.scope_.unfinalizedDrafts_--;
|
|
2473
|
+
const result = state.copy_;
|
|
2474
|
+
let resultEach = result;
|
|
2475
|
+
let isSet2 = false;
|
|
2476
|
+
if (state.type_ === 3) {
|
|
2477
|
+
resultEach = new Set(result);
|
|
2478
|
+
result.clear();
|
|
2479
|
+
isSet2 = true;
|
|
2480
|
+
}
|
|
2481
|
+
each(resultEach, (key, childValue) => finalizeProperty(rootScope, state, result, key, childValue, path, isSet2), useStrictIteration);
|
|
2482
|
+
maybeFreeze(rootScope, result, false);
|
|
2483
|
+
if (path && rootScope.patches_) getPlugin("Patches").generatePatches_(state, path, rootScope.patches_, rootScope.inversePatches_);
|
|
2484
|
+
}
|
|
2485
|
+
return state.copy_;
|
|
2486
|
+
}
|
|
2487
|
+
function finalizeProperty(rootScope, parentState, targetObject, prop, childValue, rootPath, targetIsSet) {
|
|
2488
|
+
if (childValue == null) return;
|
|
2489
|
+
if (typeof childValue !== "object" && !targetIsSet) return;
|
|
2490
|
+
const childIsFrozen = isFrozen(childValue);
|
|
2491
|
+
if (childIsFrozen && !targetIsSet) return;
|
|
2492
|
+
if (childValue === targetObject) die(5);
|
|
2493
|
+
if (isDraft(childValue)) {
|
|
2494
|
+
const res = finalize(rootScope, childValue, rootPath && parentState && parentState.type_ !== 3 && !has(parentState.assigned_, prop) ? rootPath.concat(prop) : void 0);
|
|
2495
|
+
set(targetObject, prop, res);
|
|
2496
|
+
if (isDraft(res)) rootScope.canAutoFreeze_ = false;
|
|
2497
|
+
else return;
|
|
2498
|
+
} else if (targetIsSet) targetObject.add(childValue);
|
|
2499
|
+
if (isDraftable(childValue) && !childIsFrozen) {
|
|
2500
|
+
if (!rootScope.immer_.autoFreeze_ && rootScope.unfinalizedDrafts_ < 1) return;
|
|
2501
|
+
if (parentState && parentState.base_ && parentState.base_[prop] === childValue && childIsFrozen) return;
|
|
2502
|
+
finalize(rootScope, childValue);
|
|
2503
|
+
if ((!parentState || !parentState.scope_.parent_) && typeof prop !== "symbol" && (isMap(targetObject) ? targetObject.has(prop) : Object.prototype.propertyIsEnumerable.call(targetObject, prop))) maybeFreeze(rootScope, childValue);
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
function maybeFreeze(scope, value, deep = false) {
|
|
2507
|
+
if (!scope.parent_ && scope.immer_.autoFreeze_ && scope.canAutoFreeze_) freeze(value, deep);
|
|
2508
|
+
}
|
|
2509
|
+
function createProxyProxy(base, parent) {
|
|
2510
|
+
const isArray = Array.isArray(base);
|
|
2511
|
+
const state = {
|
|
2512
|
+
type_: isArray ? 1 : 0,
|
|
2513
|
+
scope_: parent ? parent.scope_ : getCurrentScope(),
|
|
2514
|
+
modified_: false,
|
|
2515
|
+
finalized_: false,
|
|
2516
|
+
assigned_: {},
|
|
2517
|
+
parent_: parent,
|
|
2518
|
+
base_: base,
|
|
2519
|
+
draft_: null,
|
|
2520
|
+
copy_: null,
|
|
2521
|
+
revoke_: null,
|
|
2522
|
+
isManual_: false
|
|
2523
|
+
};
|
|
2524
|
+
let target = state;
|
|
2525
|
+
let traps = objectTraps;
|
|
2526
|
+
if (isArray) {
|
|
2527
|
+
target = [state];
|
|
2528
|
+
traps = arrayTraps;
|
|
2529
|
+
}
|
|
2530
|
+
const { revoke, proxy } = Proxy.revocable(target, traps);
|
|
2531
|
+
state.draft_ = proxy;
|
|
2532
|
+
state.revoke_ = revoke;
|
|
2533
|
+
return proxy;
|
|
2534
|
+
}
|
|
2535
|
+
var objectTraps = {
|
|
2536
|
+
get(state, prop) {
|
|
2537
|
+
if (prop === DRAFT_STATE) return state;
|
|
2538
|
+
const source = latest(state);
|
|
2539
|
+
if (!has(source, prop)) return readPropFromProto(state, source, prop);
|
|
2540
|
+
const value = source[prop];
|
|
2541
|
+
if (state.finalized_ || !isDraftable(value)) return value;
|
|
2542
|
+
if (value === peek(state.base_, prop)) {
|
|
2543
|
+
prepareCopy(state);
|
|
2544
|
+
return state.copy_[prop] = createProxy(value, state);
|
|
2545
|
+
}
|
|
2546
|
+
return value;
|
|
2547
|
+
},
|
|
2548
|
+
has(state, prop) {
|
|
2549
|
+
return prop in latest(state);
|
|
2550
|
+
},
|
|
2551
|
+
ownKeys(state) {
|
|
2552
|
+
return Reflect.ownKeys(latest(state));
|
|
2553
|
+
},
|
|
2554
|
+
set(state, prop, value) {
|
|
2555
|
+
const desc = getDescriptorFromProto(latest(state), prop);
|
|
2556
|
+
if (desc?.set) {
|
|
2557
|
+
desc.set.call(state.draft_, value);
|
|
2558
|
+
return true;
|
|
2559
|
+
}
|
|
2560
|
+
if (!state.modified_) {
|
|
2561
|
+
const current2 = peek(latest(state), prop);
|
|
2562
|
+
const currentState = current2?.[DRAFT_STATE];
|
|
2563
|
+
if (currentState && currentState.base_ === value) {
|
|
2564
|
+
state.copy_[prop] = value;
|
|
2565
|
+
state.assigned_[prop] = false;
|
|
2566
|
+
return true;
|
|
2567
|
+
}
|
|
2568
|
+
if (is(value, current2) && (value !== void 0 || has(state.base_, prop))) return true;
|
|
2569
|
+
prepareCopy(state);
|
|
2570
|
+
markChanged(state);
|
|
2571
|
+
}
|
|
2572
|
+
if (state.copy_[prop] === value && (value !== void 0 || prop in state.copy_) || Number.isNaN(value) && Number.isNaN(state.copy_[prop])) return true;
|
|
2573
|
+
state.copy_[prop] = value;
|
|
2574
|
+
state.assigned_[prop] = true;
|
|
2575
|
+
return true;
|
|
2576
|
+
},
|
|
2577
|
+
deleteProperty(state, prop) {
|
|
2578
|
+
if (peek(state.base_, prop) !== void 0 || prop in state.base_) {
|
|
2579
|
+
state.assigned_[prop] = false;
|
|
2580
|
+
prepareCopy(state);
|
|
2581
|
+
markChanged(state);
|
|
2582
|
+
} else delete state.assigned_[prop];
|
|
2583
|
+
if (state.copy_) delete state.copy_[prop];
|
|
2584
|
+
return true;
|
|
2585
|
+
},
|
|
2586
|
+
getOwnPropertyDescriptor(state, prop) {
|
|
2587
|
+
const owner = latest(state);
|
|
2588
|
+
const desc = Reflect.getOwnPropertyDescriptor(owner, prop);
|
|
2589
|
+
if (!desc) return desc;
|
|
2590
|
+
return {
|
|
2591
|
+
writable: true,
|
|
2592
|
+
configurable: state.type_ !== 1 || prop !== "length",
|
|
2593
|
+
enumerable: desc.enumerable,
|
|
2594
|
+
value: owner[prop]
|
|
2595
|
+
};
|
|
2596
|
+
},
|
|
2597
|
+
defineProperty() {
|
|
2598
|
+
die(11);
|
|
2599
|
+
},
|
|
2600
|
+
getPrototypeOf(state) {
|
|
2601
|
+
return getPrototypeOf(state.base_);
|
|
2602
|
+
},
|
|
2603
|
+
setPrototypeOf() {
|
|
2604
|
+
die(12);
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
var arrayTraps = {};
|
|
2608
|
+
each(objectTraps, (key, fn) => {
|
|
2609
|
+
arrayTraps[key] = function() {
|
|
2610
|
+
arguments[0] = arguments[0][0];
|
|
2611
|
+
return fn.apply(this, arguments);
|
|
2612
|
+
};
|
|
2613
|
+
});
|
|
2614
|
+
arrayTraps.deleteProperty = function(state, prop) {
|
|
2615
|
+
if (isNaN(parseInt(prop))) die(13);
|
|
2616
|
+
return arrayTraps.set.call(this, state, prop, void 0);
|
|
2617
|
+
};
|
|
2618
|
+
arrayTraps.set = function(state, prop, value) {
|
|
2619
|
+
if (prop !== "length" && isNaN(parseInt(prop))) die(14);
|
|
2620
|
+
return objectTraps.set.call(this, state[0], prop, value, state[0]);
|
|
2621
|
+
};
|
|
2622
|
+
function peek(draft, prop) {
|
|
2623
|
+
const state = draft[DRAFT_STATE];
|
|
2624
|
+
return (state ? latest(state) : draft)[prop];
|
|
881
2625
|
}
|
|
882
|
-
function
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
2626
|
+
function readPropFromProto(state, source, prop) {
|
|
2627
|
+
const desc = getDescriptorFromProto(source, prop);
|
|
2628
|
+
return desc ? `value` in desc ? desc.value : desc.get?.call(state.draft_) : void 0;
|
|
2629
|
+
}
|
|
2630
|
+
function getDescriptorFromProto(source, prop) {
|
|
2631
|
+
if (!(prop in source)) return void 0;
|
|
2632
|
+
let proto = getPrototypeOf(source);
|
|
2633
|
+
while (proto) {
|
|
2634
|
+
const desc = Object.getOwnPropertyDescriptor(proto, prop);
|
|
2635
|
+
if (desc) return desc;
|
|
2636
|
+
proto = getPrototypeOf(proto);
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
function markChanged(state) {
|
|
2640
|
+
if (!state.modified_) {
|
|
2641
|
+
state.modified_ = true;
|
|
2642
|
+
if (state.parent_) markChanged(state.parent_);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
function prepareCopy(state) {
|
|
2646
|
+
if (!state.copy_) state.copy_ = shallowCopy(state.base_, state.scope_.immer_.useStrictShallowCopy_);
|
|
2647
|
+
}
|
|
2648
|
+
var Immer2 = class {
|
|
2649
|
+
constructor(config) {
|
|
2650
|
+
this.autoFreeze_ = true;
|
|
2651
|
+
this.useStrictShallowCopy_ = false;
|
|
2652
|
+
this.useStrictIteration_ = true;
|
|
2653
|
+
/**
|
|
2654
|
+
* The `produce` function takes a value and a "recipe function" (whose
|
|
2655
|
+
* return value often depends on the base state). The recipe function is
|
|
2656
|
+
* free to mutate its first argument however it wants. All mutations are
|
|
2657
|
+
* only ever applied to a __copy__ of the base state.
|
|
2658
|
+
*
|
|
2659
|
+
* Pass only a function to create a "curried producer" which relieves you
|
|
2660
|
+
* from passing the recipe function every time.
|
|
2661
|
+
*
|
|
2662
|
+
* Only plain objects and arrays are made mutable. All other objects are
|
|
2663
|
+
* considered uncopyable.
|
|
2664
|
+
*
|
|
2665
|
+
* Note: This function is __bound__ to its `Immer` instance.
|
|
2666
|
+
*
|
|
2667
|
+
* @param {any} base - the initial state
|
|
2668
|
+
* @param {Function} recipe - function that receives a proxy of the base state as first argument and which can be freely modified
|
|
2669
|
+
* @param {Function} patchListener - optional function that will be called with all the patches produced here
|
|
2670
|
+
* @returns {any} a new state, or the initial state if nothing was modified
|
|
2671
|
+
*/
|
|
2672
|
+
this.produce = (base, recipe, patchListener) => {
|
|
2673
|
+
if (typeof base === "function" && typeof recipe !== "function") {
|
|
2674
|
+
const defaultBase = recipe;
|
|
2675
|
+
recipe = base;
|
|
2676
|
+
const self = this;
|
|
2677
|
+
return function curriedProduce(base2 = defaultBase, ...args) {
|
|
2678
|
+
return self.produce(base2, (draft) => recipe.call(this, draft, ...args));
|
|
2679
|
+
};
|
|
2680
|
+
}
|
|
2681
|
+
if (typeof recipe !== "function") die(6);
|
|
2682
|
+
if (patchListener !== void 0 && typeof patchListener !== "function") die(7);
|
|
2683
|
+
let result;
|
|
2684
|
+
if (isDraftable(base)) {
|
|
2685
|
+
const scope = enterScope(this);
|
|
2686
|
+
const proxy = createProxy(base, void 0);
|
|
2687
|
+
let hasError = true;
|
|
2688
|
+
try {
|
|
2689
|
+
result = recipe(proxy);
|
|
2690
|
+
hasError = false;
|
|
2691
|
+
} finally {
|
|
2692
|
+
if (hasError) revokeScope(scope);
|
|
2693
|
+
else leaveScope(scope);
|
|
2694
|
+
}
|
|
2695
|
+
usePatchesInScope(scope, patchListener);
|
|
2696
|
+
return processResult(result, scope);
|
|
2697
|
+
} else if (!base || typeof base !== "object") {
|
|
2698
|
+
result = recipe(base);
|
|
2699
|
+
if (result === void 0) result = base;
|
|
2700
|
+
if (result === NOTHING) result = void 0;
|
|
2701
|
+
if (this.autoFreeze_) freeze(result, true);
|
|
2702
|
+
if (patchListener) {
|
|
2703
|
+
const p = [];
|
|
2704
|
+
const ip = [];
|
|
2705
|
+
getPlugin("Patches").generateReplacementPatches_(base, result, p, ip);
|
|
2706
|
+
patchListener(p, ip);
|
|
2707
|
+
}
|
|
2708
|
+
return result;
|
|
2709
|
+
} else die(1, base);
|
|
2710
|
+
};
|
|
2711
|
+
this.produceWithPatches = (base, recipe) => {
|
|
2712
|
+
if (typeof base === "function") return (state, ...args) => this.produceWithPatches(state, (draft) => base(draft, ...args));
|
|
2713
|
+
let patches, inversePatches;
|
|
2714
|
+
return [
|
|
2715
|
+
this.produce(base, recipe, (p, ip) => {
|
|
2716
|
+
patches = p;
|
|
2717
|
+
inversePatches = ip;
|
|
2718
|
+
}),
|
|
2719
|
+
patches,
|
|
2720
|
+
inversePatches
|
|
2721
|
+
];
|
|
2722
|
+
};
|
|
2723
|
+
if (typeof config?.autoFreeze === "boolean") this.setAutoFreeze(config.autoFreeze);
|
|
2724
|
+
if (typeof config?.useStrictShallowCopy === "boolean") this.setUseStrictShallowCopy(config.useStrictShallowCopy);
|
|
2725
|
+
if (typeof config?.useStrictIteration === "boolean") this.setUseStrictIteration(config.useStrictIteration);
|
|
2726
|
+
}
|
|
2727
|
+
createDraft(base) {
|
|
2728
|
+
if (!isDraftable(base)) die(8);
|
|
2729
|
+
if (isDraft(base)) base = current(base);
|
|
2730
|
+
const scope = enterScope(this);
|
|
2731
|
+
const proxy = createProxy(base, void 0);
|
|
2732
|
+
proxy[DRAFT_STATE].isManual_ = true;
|
|
2733
|
+
leaveScope(scope);
|
|
2734
|
+
return proxy;
|
|
2735
|
+
}
|
|
2736
|
+
finishDraft(draft, patchListener) {
|
|
2737
|
+
const state = draft && draft[DRAFT_STATE];
|
|
2738
|
+
if (!state || !state.isManual_) die(9);
|
|
2739
|
+
const { scope_: scope } = state;
|
|
2740
|
+
usePatchesInScope(scope, patchListener);
|
|
2741
|
+
return processResult(void 0, scope);
|
|
2742
|
+
}
|
|
2743
|
+
/**
|
|
2744
|
+
* Pass true to automatically freeze all copies created by Immer.
|
|
2745
|
+
*
|
|
2746
|
+
* By default, auto-freezing is enabled.
|
|
2747
|
+
*/
|
|
2748
|
+
setAutoFreeze(value) {
|
|
2749
|
+
this.autoFreeze_ = value;
|
|
2750
|
+
}
|
|
2751
|
+
/**
|
|
2752
|
+
* Pass true to enable strict shallow copy.
|
|
2753
|
+
*
|
|
2754
|
+
* By default, immer does not copy the object descriptors such as getter, setter and non-enumrable properties.
|
|
2755
|
+
*/
|
|
2756
|
+
setUseStrictShallowCopy(value) {
|
|
2757
|
+
this.useStrictShallowCopy_ = value;
|
|
2758
|
+
}
|
|
2759
|
+
/**
|
|
2760
|
+
* Pass false to use faster iteration that skips non-enumerable properties
|
|
2761
|
+
* but still handles symbols for compatibility.
|
|
2762
|
+
*
|
|
2763
|
+
* By default, strict iteration is enabled (includes all own properties).
|
|
2764
|
+
*/
|
|
2765
|
+
setUseStrictIteration(value) {
|
|
2766
|
+
this.useStrictIteration_ = value;
|
|
2767
|
+
}
|
|
2768
|
+
shouldUseStrictIteration() {
|
|
2769
|
+
return this.useStrictIteration_;
|
|
2770
|
+
}
|
|
2771
|
+
applyPatches(base, patches) {
|
|
2772
|
+
let i;
|
|
2773
|
+
for (i = patches.length - 1; i >= 0; i--) {
|
|
2774
|
+
const patch = patches[i];
|
|
2775
|
+
if (patch.path.length === 0 && patch.op === "replace") {
|
|
2776
|
+
base = patch.value;
|
|
2777
|
+
break;
|
|
2778
|
+
}
|
|
896
2779
|
}
|
|
897
|
-
|
|
2780
|
+
if (i > -1) patches = patches.slice(i + 1);
|
|
2781
|
+
const applyPatchesImpl = getPlugin("Patches").applyPatches_;
|
|
2782
|
+
if (isDraft(base)) return applyPatchesImpl(base, patches);
|
|
2783
|
+
return this.produce(base, (draft) => applyPatchesImpl(draft, patches));
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
function createProxy(value, parent) {
|
|
2787
|
+
const draft = isMap(value) ? getPlugin("MapSet").proxyMap_(value, parent) : isSet(value) ? getPlugin("MapSet").proxySet_(value, parent) : createProxyProxy(value, parent);
|
|
2788
|
+
(parent ? parent.scope_ : getCurrentScope()).drafts_.push(draft);
|
|
2789
|
+
return draft;
|
|
2790
|
+
}
|
|
2791
|
+
function current(value) {
|
|
2792
|
+
if (!isDraft(value)) die(10, value);
|
|
2793
|
+
return currentImpl(value);
|
|
2794
|
+
}
|
|
2795
|
+
function currentImpl(value) {
|
|
2796
|
+
if (!isDraftable(value) || isFrozen(value)) return value;
|
|
2797
|
+
const state = value[DRAFT_STATE];
|
|
2798
|
+
let copy;
|
|
2799
|
+
let strict = true;
|
|
2800
|
+
if (state) {
|
|
2801
|
+
if (!state.modified_) return state.base_;
|
|
2802
|
+
state.finalized_ = true;
|
|
2803
|
+
copy = shallowCopy(value, state.scope_.immer_.useStrictShallowCopy_);
|
|
2804
|
+
strict = state.scope_.immer_.shouldUseStrictIteration();
|
|
2805
|
+
} else copy = shallowCopy(value, true);
|
|
2806
|
+
each(copy, (key, childValue) => {
|
|
2807
|
+
set(copy, key, currentImpl(childValue));
|
|
2808
|
+
}, strict);
|
|
2809
|
+
if (state) state.finalized_ = false;
|
|
2810
|
+
return copy;
|
|
898
2811
|
}
|
|
899
|
-
|
|
2812
|
+
var immer = new Immer2();
|
|
2813
|
+
immer.produce;
|
|
2814
|
+
var setAutoFreeze = /* @__PURE__ */ immer.setAutoFreeze.bind(immer);
|
|
2815
|
+
const produce = new Immer2({
|
|
900
2816
|
autoFreeze: false,
|
|
901
2817
|
useStrictShallowCopy: true
|
|
902
2818
|
}).produce;
|
|
903
2819
|
//#endregion
|
|
904
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
2820
|
+
//#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
|
|
905
2821
|
/**
|
|
906
2822
|
* Deadline-based debounce timer.
|
|
907
2823
|
*
|
|
@@ -959,7 +2875,7 @@ var DebounceTimer = class {
|
|
|
959
2875
|
}
|
|
960
2876
|
};
|
|
961
2877
|
//#endregion
|
|
962
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
2878
|
+
//#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
|
|
963
2879
|
/**
|
|
964
2880
|
* Manages ephemeral patches: storage, composition, path resolution, and finalization.
|
|
965
2881
|
*/
|
|
@@ -1234,7 +3150,7 @@ var EphemeralPatchManager = class {
|
|
|
1234
3150
|
}
|
|
1235
3151
|
};
|
|
1236
3152
|
//#endregion
|
|
1237
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
3153
|
+
//#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
|
|
1238
3154
|
function normalizeTreeJson(input, options) {
|
|
1239
3155
|
if (!Array.isArray(input)) return [];
|
|
1240
3156
|
return input.map((node) => normalizeTreeNode(node, options));
|
|
@@ -1257,7 +3173,7 @@ function isPlainRecord(value) {
|
|
|
1257
3173
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1258
3174
|
}
|
|
1259
3175
|
//#endregion
|
|
1260
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
3176
|
+
//#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
|
|
1261
3177
|
function isJSONObject(v) {
|
|
1262
3178
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1263
3179
|
}
|
|
@@ -1679,7 +3595,7 @@ function isIgnoredByAncestor(id, ignoreSet, getContainerById) {
|
|
|
1679
3595
|
return false;
|
|
1680
3596
|
}
|
|
1681
3597
|
//#endregion
|
|
1682
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
3598
|
+
//#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
|
|
1683
3599
|
/**
|
|
1684
3600
|
* Finds the longest increasing subsequence of a sequence of numbers
|
|
1685
3601
|
* @param sequence The sequence of numbers
|
|
@@ -2410,7 +4326,7 @@ function diffMap(doc, oldState, newState, containerId, schema, inferOptions) {
|
|
|
2410
4326
|
return changes;
|
|
2411
4327
|
}
|
|
2412
4328
|
//#endregion
|
|
2413
|
-
//#region ../../node_modules/.pnpm/loro-mirror@2.3.3_patch_hash=a551a0e4aa4f61ec88b0d9e970a183b5ee2955107a2e0bb03b378bc510c2f395_loro-crdt@1.16.
|
|
4329
|
+
//#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
|
|
2414
4330
|
/**
|
|
2415
4331
|
* Mirror core functionality for bidirectional sync between app state and Loro CRDT
|
|
2416
4332
|
*/
|
|
@@ -3905,6 +5821,296 @@ function mergeInitialIntoBaseWithSchema(base, init, rootSchema) {
|
|
|
3905
5821
|
}
|
|
3906
5822
|
}
|
|
3907
5823
|
//#endregion
|
|
5824
|
+
//#region ../medeo-dsl/src/schema/codec.ts
|
|
5825
|
+
function isInvalidAtomic(value) {
|
|
5826
|
+
return typeof value === "object" && value !== null && "invalid" in value && value.invalid === true;
|
|
5827
|
+
}
|
|
5828
|
+
function codec(encode, decode) {
|
|
5829
|
+
return {
|
|
5830
|
+
encode: (v) => isInvalidAtomic(v) ? v.raw : encode(v),
|
|
5831
|
+
decode: (raw) => {
|
|
5832
|
+
try {
|
|
5833
|
+
return decode(raw);
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
return {
|
|
5836
|
+
invalid: true,
|
|
5837
|
+
raw,
|
|
5838
|
+
reason: String(error)
|
|
5839
|
+
};
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
};
|
|
5843
|
+
}
|
|
5844
|
+
/** A Clip's time anchor; the Clip holding it is the anchored end. */
|
|
5845
|
+
const timeAnchorCodec = codec((v) => {
|
|
5846
|
+
assertTimeAnchor(v);
|
|
5847
|
+
return JSON.stringify({
|
|
5848
|
+
to: v.to,
|
|
5849
|
+
offsetMs: v.offsetMs,
|
|
5850
|
+
fallback_abs_ms: v.fallback_abs_ms
|
|
5851
|
+
});
|
|
5852
|
+
}, (raw) => {
|
|
5853
|
+
const v = JSON.parse(raw);
|
|
5854
|
+
assertTimeAnchor(v);
|
|
5855
|
+
return v;
|
|
5856
|
+
});
|
|
5857
|
+
/** A segment selection as stored: a run's Cursors as their native bytes in unpadded base64url. */
|
|
5858
|
+
function encodeSelection(v) {
|
|
5859
|
+
return {
|
|
5860
|
+
audioScriptId: v.audioScriptId,
|
|
5861
|
+
segmentId: v.segmentId,
|
|
5862
|
+
...v.start === void 0 || v.end === void 0 ? {} : {
|
|
5863
|
+
start: bytesToBase64url(v.start.encode()),
|
|
5864
|
+
end: bytesToBase64url(v.end.encode())
|
|
5865
|
+
}
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
function decodeSelection(v) {
|
|
5869
|
+
if (!v || typeof v !== "object") throw new Error("Expected a segment selection");
|
|
5870
|
+
const { start, end, ...rest } = v;
|
|
5871
|
+
if (start === void 0 && end === void 0) return rest;
|
|
5872
|
+
if (typeof start !== "string" || typeof end !== "string") throw new Error("Expected encoded Cursor pair");
|
|
5873
|
+
return {
|
|
5874
|
+
...rest,
|
|
5875
|
+
start: Cursor.decode(base64urlToBytes(start)),
|
|
5876
|
+
end: Cursor.decode(base64urlToBytes(end))
|
|
5877
|
+
};
|
|
5878
|
+
}
|
|
5879
|
+
/** A Caption's selection. */
|
|
5880
|
+
const captionContentCodec = codec((v) => {
|
|
5881
|
+
assertCaptionContent(v);
|
|
5882
|
+
return JSON.stringify(encodeSelection(v));
|
|
5883
|
+
}, (raw) => {
|
|
5884
|
+
const value = decodeSelection(JSON.parse(raw));
|
|
5885
|
+
assertCaptionContent(value);
|
|
5886
|
+
return value;
|
|
5887
|
+
});
|
|
5888
|
+
/** A PhoneticScript's selection and what to synthesize for it, stored as one string so they move together. */
|
|
5889
|
+
const phonemeCodec = codec((v) => {
|
|
5890
|
+
assertPhoneme(v);
|
|
5891
|
+
return JSON.stringify({
|
|
5892
|
+
...encodeSelection(v),
|
|
5893
|
+
...v.phonemeScript === void 0 ? {} : { phonemeScript: v.phonemeScript }
|
|
5894
|
+
});
|
|
5895
|
+
}, (raw) => {
|
|
5896
|
+
const value = decodeSelection(JSON.parse(raw));
|
|
5897
|
+
assertPhoneme(value);
|
|
5898
|
+
return value;
|
|
5899
|
+
});
|
|
5900
|
+
/** A row-owned binding couples the selection, pronunciation and its immutable file in one LWW value. */
|
|
5901
|
+
const phoneticBindingCodec = codec((value) => {
|
|
5902
|
+
assertEntity("phonetic-script-entities", value);
|
|
5903
|
+
assertPhoneticScript(void 0, { ...value });
|
|
5904
|
+
return JSON.stringify({
|
|
5905
|
+
phoneme: JSON.parse(phonemeCodec.encode(value.phoneme)),
|
|
5906
|
+
system: value.system,
|
|
5907
|
+
key: value.key,
|
|
5908
|
+
storageKey: value.storageKey,
|
|
5909
|
+
digest: value.digest
|
|
5910
|
+
});
|
|
5911
|
+
}, (raw) => {
|
|
5912
|
+
const value = JSON.parse(raw);
|
|
5913
|
+
if (!isRecord(value)) throw new Error("Expected a PhoneticScript binding");
|
|
5914
|
+
const phoneme = phonemeCodec.decode(JSON.stringify(value.phoneme));
|
|
5915
|
+
if (isInvalidAtomic(phoneme)) throw new Error(phoneme.reason);
|
|
5916
|
+
const binding = {
|
|
5917
|
+
...value,
|
|
5918
|
+
phoneme
|
|
5919
|
+
};
|
|
5920
|
+
assertEntity("phonetic-script-entities", binding);
|
|
5921
|
+
assertPhoneticScript(void 0, binding);
|
|
5922
|
+
return binding;
|
|
5923
|
+
});
|
|
5924
|
+
/** A Clip's sequence marker is stored as one string, so it is taken or replaced whole. */
|
|
5925
|
+
const seqMarkerCodec = codec((v) => {
|
|
5926
|
+
assertSeqMarker(v);
|
|
5927
|
+
return JSON.stringify({
|
|
5928
|
+
inMs: v.inMs,
|
|
5929
|
+
outMs: v.outMs,
|
|
5930
|
+
...v.rate === void 0 ? {} : { rate: v.rate },
|
|
5931
|
+
...v.fillTimeline === void 0 ? {} : { fillTimeline: v.fillTimeline }
|
|
5932
|
+
});
|
|
5933
|
+
}, (raw) => {
|
|
5934
|
+
const v = JSON.parse(raw);
|
|
5935
|
+
assertSeqMarker(v);
|
|
5936
|
+
return v;
|
|
5937
|
+
});
|
|
5938
|
+
//#endregion
|
|
5939
|
+
//#region ../medeo-dsl/src/schema/loro-shape.ts
|
|
5940
|
+
const optional = { required: false };
|
|
5941
|
+
const mergeable = { mergeableMapChildContainers: true };
|
|
5942
|
+
const strings = (keys) => Object.fromEntries(keys.map((key) => [key, schema.String()]));
|
|
5943
|
+
const relation = (keys) => schema.LoroMapRecord(schema.LoroMap(strings(keys)), mergeable);
|
|
5944
|
+
const transform = (codec) => ({
|
|
5945
|
+
...codec,
|
|
5946
|
+
isEqual: "encoded-value-equality",
|
|
5947
|
+
validateEncodedType: true
|
|
5948
|
+
});
|
|
5949
|
+
/** One field stored as one encoded string, so it is taken or replaced whole. */
|
|
5950
|
+
const atomicValue = (codec) => schema.String().transform(transform(codec));
|
|
5951
|
+
const optionalAtomicValue = (codec) => schema.String(optional).transform(transform(codec));
|
|
5952
|
+
/**
|
|
5953
|
+
* Only direct children inherit mergeable identity. Text and ordinary entity rows keep normal
|
|
5954
|
+
* identities, so deleting a row also drops what a peer wrote into it concurrently: a relation an
|
|
5955
|
+
* entity holds at most one of is a field of that row and never outlives it.
|
|
5956
|
+
*/
|
|
5957
|
+
const medeoDslLoroShape = schema({
|
|
5958
|
+
"timeline-entities": schema.LoroMapRecord(schema.LoroMap({})),
|
|
5959
|
+
"track-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5960
|
+
role: schema.String(),
|
|
5961
|
+
hidden: schema.Boolean(optional)
|
|
5962
|
+
})),
|
|
5963
|
+
"clip-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5964
|
+
seqMarker: atomicValue(seqMarkerCodec),
|
|
5965
|
+
volume: schema.Number(optional),
|
|
5966
|
+
source: schema.String(),
|
|
5967
|
+
anchor: optionalAtomicValue(timeAnchorCodec)
|
|
5968
|
+
})),
|
|
5969
|
+
"media-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5970
|
+
kind: schema.String(),
|
|
5971
|
+
system: schema.String(),
|
|
5972
|
+
key: schema.String(),
|
|
5973
|
+
storageKey: schema.String(optional),
|
|
5974
|
+
durationMs: schema.Number(optional),
|
|
5975
|
+
digest: schema.String(optional)
|
|
5976
|
+
}), mergeable),
|
|
5977
|
+
"audio-script-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5978
|
+
segments: schema.LoroMovableList(schema.LoroMap({
|
|
5979
|
+
id: schema.String(),
|
|
5980
|
+
text: schema.LoroText(),
|
|
5981
|
+
startMs: schema.Number(optional),
|
|
5982
|
+
endMs: schema.Number(optional)
|
|
5983
|
+
}), (segment) => segment.id),
|
|
5984
|
+
defaultLanguage: schema.String(optional),
|
|
5985
|
+
system: schema.String(optional),
|
|
5986
|
+
key: schema.String(optional),
|
|
5987
|
+
storageKey: schema.String(optional),
|
|
5988
|
+
digest: schema.String(optional)
|
|
5989
|
+
})),
|
|
5990
|
+
"phonetic-script-entities": schema.LoroMapRecord(schema.LoroMap({ binding: atomicValue(phoneticBindingCodec) })),
|
|
5991
|
+
"caption-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
5992
|
+
selection: optionalAtomicValue(captionContentCodec),
|
|
5993
|
+
text: schema.LoroText(optional),
|
|
5994
|
+
style: schema.LoroMap({
|
|
5995
|
+
font: schema.LoroMap({
|
|
5996
|
+
system: schema.String(),
|
|
5997
|
+
key: schema.String()
|
|
5998
|
+
}, optional),
|
|
5999
|
+
fontSize: schema.Number(optional),
|
|
6000
|
+
fontColor: schema.String(optional),
|
|
6001
|
+
fontWeight: schema.Number(optional),
|
|
6002
|
+
entranceAnimation: schema.String(optional),
|
|
6003
|
+
entranceAnimationDurationMs: schema.Number(optional),
|
|
6004
|
+
strokeColor: schema.String(optional),
|
|
6005
|
+
strokeWidth: schema.Number(optional),
|
|
6006
|
+
positionX: schema.Number(optional),
|
|
6007
|
+
positionY: schema.Number(optional)
|
|
6008
|
+
}, {
|
|
6009
|
+
...optional,
|
|
6010
|
+
...mergeable
|
|
6011
|
+
})
|
|
6012
|
+
}, mergeable)),
|
|
6013
|
+
"voice-entities": schema.LoroMapRecord(schema.LoroMap({
|
|
6014
|
+
system: schema.String(),
|
|
6015
|
+
key: schema.String(),
|
|
6016
|
+
name: schema.String(optional)
|
|
6017
|
+
}), mergeable),
|
|
6018
|
+
"contain-relations": schema.LoroTree(schema.LoroMap({ entityId: schema.String() })),
|
|
6019
|
+
"voice-timbre-relations": relation(["recordingId", "voiceId"]),
|
|
6020
|
+
"derived-from-relations": schema.LoroMapRecord(schema.LoroMap({
|
|
6021
|
+
entityId: schema.String(),
|
|
6022
|
+
sourceId: schema.String(),
|
|
6023
|
+
sourceDigest: schema.String(optional)
|
|
6024
|
+
}), mergeable)
|
|
6025
|
+
});
|
|
6026
|
+
//#endregion
|
|
6027
|
+
//#region ../medeo-dsl/src/schema/document-format.ts
|
|
6028
|
+
/** Format discrimination does not register containers or alter the document. */
|
|
6029
|
+
function documentFormat(doc) {
|
|
6030
|
+
const roots = doc.getShallowValue();
|
|
6031
|
+
const legacy = [
|
|
6032
|
+
"medeo",
|
|
6033
|
+
"timeline",
|
|
6034
|
+
"tracks",
|
|
6035
|
+
"part_library"
|
|
6036
|
+
].some((key) => Object.hasOwn(roots, key));
|
|
6037
|
+
const dsl = Object.keys(roots).some((key) => key.endsWith("-entities") || key.endsWith("-relations"));
|
|
6038
|
+
return legacy && dsl ? "mixed" : legacy ? "legacy" : dsl ? "dsl" : "unknown";
|
|
6039
|
+
}
|
|
6040
|
+
var UnsupportedDocumentFormatError = class extends Error {
|
|
6041
|
+
code = "unsupported_document_format";
|
|
6042
|
+
constructor(message = "Expected a new DSL document; legacy, mixed or uninitialized content is not supported") {
|
|
6043
|
+
super(message);
|
|
6044
|
+
this.name = "UnsupportedDocumentFormatError";
|
|
6045
|
+
}
|
|
6046
|
+
};
|
|
6047
|
+
/** Run after initial import, before opening an editable handle. Empty new documents still have one Timeline. */
|
|
6048
|
+
function assertDslDocument(doc) {
|
|
6049
|
+
if (documentFormat(doc) !== "dsl" || doc.getMap("timeline-entities").keys().length !== 1) throw new UnsupportedDocumentFormatError();
|
|
6050
|
+
assertPhoneticBindingFormat(doc);
|
|
6051
|
+
}
|
|
6052
|
+
/** This storage cutover has no field-per-fact reader or mixed-version write path. */
|
|
6053
|
+
function assertPhoneticBindingFormat(doc) {
|
|
6054
|
+
if (!Object.hasOwn(doc.getShallowValue(), "phonetic-script-entities")) return;
|
|
6055
|
+
const table = doc.getMap("phonetic-script-entities");
|
|
6056
|
+
for (const id of table.keys()) {
|
|
6057
|
+
const row = table.get(id);
|
|
6058
|
+
if (!isContainer(row) || row.kind() !== "Map" || row.keys().some((key) => key !== "binding") || typeof row.get("binding") !== "string") throw new UnsupportedDocumentFormatError("PhoneticScript storage requires one atomic binding; old DSL documents need explicit migration");
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
//#endregion
|
|
6062
|
+
//#region ../medeo-dsl/src/schema/text-styles.ts
|
|
6063
|
+
/** How rich-text marks extend at their edges: a language runs on into text typed after it. */
|
|
6064
|
+
const textStyles = { language: { expand: "after" } };
|
|
6065
|
+
//#endregion
|
|
6066
|
+
//#region src/dsl/document/phonetic-binding.ts
|
|
6067
|
+
/**
|
|
6068
|
+
* Actions and the transaction guard keep their domain-shaped rows. The native schema stores
|
|
6069
|
+
* a PhoneticScript's facts under one binding; this lens applies draft edits to that single value.
|
|
6070
|
+
* It never reads the retired field-per-fact representation.
|
|
6071
|
+
*/
|
|
6072
|
+
function logicalDslState(native) {
|
|
6073
|
+
const phonetics = new Proxy({}, {
|
|
6074
|
+
get: (_target, key) => typeof key === "string" ? native["phonetic-script-entities"]?.[key]?.binding : void 0,
|
|
6075
|
+
set: (_target, key, value) => {
|
|
6076
|
+
if (typeof key !== "string") throw new Error("Invalid PhoneticScript key");
|
|
6077
|
+
native["phonetic-script-entities"] ??= {};
|
|
6078
|
+
const existing = native["phonetic-script-entities"][key];
|
|
6079
|
+
if (existing) existing.binding = value;
|
|
6080
|
+
else native["phonetic-script-entities"][key] = { binding: value };
|
|
6081
|
+
return true;
|
|
6082
|
+
},
|
|
6083
|
+
deleteProperty: (_target, key) => {
|
|
6084
|
+
if (typeof key === "string") delete native["phonetic-script-entities"][key];
|
|
6085
|
+
return true;
|
|
6086
|
+
},
|
|
6087
|
+
ownKeys: () => Object.keys(native["phonetic-script-entities"] ?? {}),
|
|
6088
|
+
has: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key),
|
|
6089
|
+
getOwnPropertyDescriptor: (_target, key) => typeof key === "string" && Object.hasOwn(native["phonetic-script-entities"] ?? {}, key) ? {
|
|
6090
|
+
configurable: true,
|
|
6091
|
+
enumerable: true,
|
|
6092
|
+
writable: true,
|
|
6093
|
+
value: native["phonetic-script-entities"][key].binding
|
|
6094
|
+
} : void 0
|
|
6095
|
+
});
|
|
6096
|
+
const view = {};
|
|
6097
|
+
for (const key of new Set([
|
|
6098
|
+
...Object.keys(native),
|
|
6099
|
+
...entityTables,
|
|
6100
|
+
...relationTables,
|
|
6101
|
+
"contain-relations"
|
|
6102
|
+
])) Object.defineProperty(view, key, {
|
|
6103
|
+
configurable: true,
|
|
6104
|
+
enumerable: true,
|
|
6105
|
+
get: () => key === "phonetic-script-entities" ? phonetics : Reflect.get(native, key),
|
|
6106
|
+
set: (value) => {
|
|
6107
|
+
if (key === "phonetic-script-entities") throw new Error("Cannot replace the PhoneticScript table");
|
|
6108
|
+
Reflect.set(native, key, value);
|
|
6109
|
+
}
|
|
6110
|
+
});
|
|
6111
|
+
return view;
|
|
6112
|
+
}
|
|
6113
|
+
//#endregion
|
|
3908
6114
|
//#region src/dsl/document/read.ts
|
|
3909
6115
|
/** Looking up a missing root must not create it. */
|
|
3910
6116
|
function rootMap(doc, name) {
|
|
@@ -3988,6 +6194,7 @@ function referenceTargets(table, row) {
|
|
|
3988
6194
|
}
|
|
3989
6195
|
}
|
|
3990
6196
|
function readMedeoDsl(doc, state) {
|
|
6197
|
+
const logical = logicalDslState(state);
|
|
3991
6198
|
const diagnostics = [];
|
|
3992
6199
|
const diagnostic = (code, path, error) => diagnostics.push({
|
|
3993
6200
|
code,
|
|
@@ -3997,7 +6204,7 @@ function readMedeoDsl(doc, state) {
|
|
|
3997
6204
|
const entities = Object.fromEntries(entityTables.map((t) => [t, {}]));
|
|
3998
6205
|
const relations = Object.fromEntries(relationTables.map((t) => [t, {}]));
|
|
3999
6206
|
const richText = {};
|
|
4000
|
-
for (const table of entityTables) for (const [id, raw] of Object.entries(
|
|
6207
|
+
for (const table of entityTables) for (const [id, raw] of Object.entries(logical[table] ?? {})) try {
|
|
4001
6208
|
assertId(id);
|
|
4002
6209
|
let v = raw;
|
|
4003
6210
|
for (const field of optionalReferences[table] ?? []) {
|
|
@@ -4408,7 +6615,10 @@ function applyMutations(tx, mutations) {
|
|
|
4408
6615
|
delete rows[m.table][m.id];
|
|
4409
6616
|
break;
|
|
4410
6617
|
case "insert-segment":
|
|
4411
|
-
tx.insertSegment(m.audioScriptId, m.segmentId, m.text, m.index
|
|
6618
|
+
tx.insertSegment(m.audioScriptId, m.segmentId, m.text, m.index, m.startMs === void 0 && m.endMs === void 0 ? void 0 : {
|
|
6619
|
+
startMs: m.startMs,
|
|
6620
|
+
endMs: m.endMs
|
|
6621
|
+
});
|
|
4412
6622
|
break;
|
|
4413
6623
|
case "move-segment":
|
|
4414
6624
|
tx.moveSegment(m.audioScriptId, m.segmentId, m.index);
|
|
@@ -4500,7 +6710,11 @@ function validateDraft(tracked) {
|
|
|
4500
6710
|
before,
|
|
4501
6711
|
after: state,
|
|
4502
6712
|
entities: tracked.entities,
|
|
4503
|
-
relations: tracked.relations
|
|
6713
|
+
relations: tracked.relations,
|
|
6714
|
+
containment: {
|
|
6715
|
+
before: plainTree(before["contain-relations"] ?? []),
|
|
6716
|
+
after: plainTree(state["contain-relations"] ?? [])
|
|
6717
|
+
}
|
|
4504
6718
|
});
|
|
4505
6719
|
if (!tracked.treeChanged) return;
|
|
4506
6720
|
const oldNodes = nodeIndex(before["contain-relations"] ?? []);
|
|
@@ -4577,8 +6791,11 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4577
6791
|
const steps = [];
|
|
4578
6792
|
const newCaptionTexts = /* @__PURE__ */ new Map();
|
|
4579
6793
|
const bodies = /* @__PURE__ */ new Map();
|
|
4580
|
-
const
|
|
4581
|
-
|
|
6794
|
+
const nativeBefore = mirror.getState();
|
|
6795
|
+
const before = logicalDslState(nativeBefore);
|
|
6796
|
+
const phoneticRows = /* @__PURE__ */ new Map();
|
|
6797
|
+
mirror.setState((native) => {
|
|
6798
|
+
const raw = logicalDslState(native);
|
|
4582
6799
|
const tracked = new TransactionDraft(raw, before);
|
|
4583
6800
|
setReject((error) => tracked.fail(error));
|
|
4584
6801
|
/** The current text of a Caption's own Text or a script segment, as this transaction leaves it so far. */
|
|
@@ -4666,18 +6883,21 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4666
6883
|
editCaptionText(captionId, edits, expectedBodyId) {
|
|
4667
6884
|
editBody({ captionId }, edits, expectedBodyId);
|
|
4668
6885
|
},
|
|
4669
|
-
insertSegment(audioScriptId, segmentId, text, index) {
|
|
6886
|
+
insertSegment(audioScriptId, segmentId, text, index, time) {
|
|
4670
6887
|
tracked.run(() => {
|
|
4671
6888
|
assertId(segmentId);
|
|
4672
6889
|
if (typeof text !== "string") throw new Error("Segment text is a string");
|
|
6890
|
+
const segment = {
|
|
6891
|
+
id: segmentId,
|
|
6892
|
+
text,
|
|
6893
|
+
...time
|
|
6894
|
+
};
|
|
6895
|
+
assertScriptSegment(segment);
|
|
4673
6896
|
const segments = segmentsOf(audioScriptId);
|
|
4674
6897
|
if (segments.some((segment) => segment.id === segmentId)) throw new Error(`Segment already exists: ${segmentId}`);
|
|
4675
6898
|
const at = index ?? segments.length;
|
|
4676
6899
|
if (!Number.isSafeInteger(at) || at < 0 || at > segments.length) throw new Error("Invalid segment index");
|
|
4677
|
-
segments.splice(at, 0,
|
|
4678
|
-
id: segmentId,
|
|
4679
|
-
text
|
|
4680
|
-
});
|
|
6900
|
+
segments.splice(at, 0, segment);
|
|
4681
6901
|
});
|
|
4682
6902
|
},
|
|
4683
6903
|
moveSegment(audioScriptId, segmentId, index) {
|
|
@@ -4767,6 +6987,15 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4767
6987
|
for (const step of steps) if (step.type === "text") if ("captionId" in step.target) requireEntity(raw, "caption-entities", step.target.captionId);
|
|
4768
6988
|
else requireSegment(raw, step.target.audioScriptId, step.target.segmentId);
|
|
4769
6989
|
assertSelectionsAfter(raw, before, tracked, steps);
|
|
6990
|
+
const changedPhonetics = new Set([...tracked.entities.get("phonetic-script-entities")?.keys() ?? [], ...steps.flatMap((step) => step.type !== "text" && step.table === "phonetic-script-entities" ? [step.id] : [])]);
|
|
6991
|
+
for (const id of changedPhonetics) {
|
|
6992
|
+
const row = raw["phonetic-script-entities"][id];
|
|
6993
|
+
phoneticRows.set(id, row ? copyInput(row) : void 0);
|
|
6994
|
+
if (!row) continue;
|
|
6995
|
+
const old = nativeBefore["phonetic-script-entities"][id];
|
|
6996
|
+
if (old) native["phonetic-script-entities"][id] = old;
|
|
6997
|
+
else delete native["phonetic-script-entities"][id];
|
|
6998
|
+
}
|
|
4770
6999
|
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) {
|
|
4771
7000
|
const row = raw["caption-entities"][id];
|
|
4772
7001
|
if (fields.has("text") && row?.text !== void 0 && before["caption-entities"][id]?.text === void 0) {
|
|
@@ -4775,6 +7004,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4775
7004
|
}
|
|
4776
7005
|
}
|
|
4777
7006
|
for (const table of Object.keys(selectionField)) {
|
|
7007
|
+
if (table === "phonetic-script-entities") continue;
|
|
4778
7008
|
const field = selectionField[table];
|
|
4779
7009
|
for (const [id, fields] of tracked.entities.get(table) ?? []) {
|
|
4780
7010
|
const row = raw[table][id];
|
|
@@ -4814,6 +7044,8 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4814
7044
|
applyTextEdits(text, step.edits);
|
|
4815
7045
|
continue;
|
|
4816
7046
|
}
|
|
7047
|
+
const phonetic = step.table === "phonetic-script-entities" ? phoneticRows.get(step.id) : void 0;
|
|
7048
|
+
if (step.table === "phonetic-script-entities" && !phonetic) continue;
|
|
4817
7049
|
const row = entityRow(doc, step.table, step.id);
|
|
4818
7050
|
const field = selectionField[step.table];
|
|
4819
7051
|
if (step.type === "range") {
|
|
@@ -4832,12 +7064,21 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4832
7064
|
if (!value.start || !value.end) throw new Error("Unable to construct prepared range");
|
|
4833
7065
|
}
|
|
4834
7066
|
if (step.phonemeScript !== void 0) value.phonemeScript = step.phonemeScript;
|
|
4835
|
-
if (
|
|
4836
|
-
|
|
4837
|
-
|
|
7067
|
+
if (phonetic) phonetic.phoneme = value;
|
|
7068
|
+
else {
|
|
7069
|
+
if (!row) throw new Error("Unable to construct prepared range");
|
|
7070
|
+
row.set(field, codecs[step.table].encode(value));
|
|
7071
|
+
}
|
|
7072
|
+
} else if (step.value !== void 0) if (phonetic) phonetic.phoneme = step.value;
|
|
7073
|
+
else {
|
|
4838
7074
|
if (!row) throw new Error("Prepared selection owner is missing");
|
|
4839
7075
|
row.set(field, codecs[step.table].encode(step.value));
|
|
4840
|
-
}
|
|
7076
|
+
}
|
|
7077
|
+
else if (row?.get(field) !== void 0) row.delete(field);
|
|
7078
|
+
}
|
|
7079
|
+
for (const [id, binding] of phoneticRows) {
|
|
7080
|
+
if (!binding) continue;
|
|
7081
|
+
(entityRow(doc, "phonetic-script-entities", id) ?? doc.getMap("phonetic-script-entities").setContainer(id, new LoroMap())).set("binding", phoneticBindingCodec.encode(binding));
|
|
4841
7082
|
}
|
|
4842
7083
|
}
|
|
4843
7084
|
});
|
|
@@ -4850,7 +7091,7 @@ function runTransaction(doc, mirror, edit, options, setReject) {
|
|
|
4850
7091
|
function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
4851
7092
|
const selected = new Set(Object.entries(before["caption-entities"]).filter(([, caption]) => typeof caption === "object" && caption.selection !== void 0).map(([id]) => id));
|
|
4852
7093
|
const changedCaptions = /* @__PURE__ */ new Set();
|
|
4853
|
-
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (fields.has("text")) changedCaptions.add(id);
|
|
7094
|
+
for (const [id, fields] of tracked.entities.get("caption-entities") ?? []) if (!before["caption-entities"][id] || fields.has("text")) changedCaptions.add(id);
|
|
4854
7095
|
const phonemes = /* @__PURE__ */ new Map();
|
|
4855
7096
|
for (const step of steps) {
|
|
4856
7097
|
if (step.type === "text") {
|
|
@@ -4884,7 +7125,7 @@ function assertSelectionsAfter(raw, before, tracked, steps) {
|
|
|
4884
7125
|
if (!row) continue;
|
|
4885
7126
|
const phoneme = phonemes.has(id) ? phonemes.get(id) : row.phoneme;
|
|
4886
7127
|
const old = before["phonetic-script-entities"][id];
|
|
4887
|
-
assertPhoneticScript(old, {
|
|
7128
|
+
assertPhoneticScript(old ? { ...old } : void 0, {
|
|
4888
7129
|
...row,
|
|
4889
7130
|
phoneme: isRecord(phoneme) ? phoneme : void 0
|
|
4890
7131
|
});
|
|
@@ -4923,6 +7164,7 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4923
7164
|
"tracks",
|
|
4924
7165
|
"part_library"
|
|
4925
7166
|
].some((key) => Object.hasOwn(roots, key))) throw new Error("Expected a new DSL document, not a legacy content model");
|
|
7167
|
+
assertPhoneticBindingFormat(doc);
|
|
4926
7168
|
doc.configTextStyle(textStyles);
|
|
4927
7169
|
this.mirror = new Mirror({
|
|
4928
7170
|
doc,
|
|
@@ -4986,6 +7228,11 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4986
7228
|
editCaptionText(captionId, expectedBodyId, edits, audit) {
|
|
4987
7229
|
this.transact((tx) => tx.editCaptionText(captionId, edits, expectedBodyId), audit);
|
|
4988
7230
|
}
|
|
7231
|
+
/** Check the session's write capability before an Action starts host-owned resource I/O. */
|
|
7232
|
+
assertWritable() {
|
|
7233
|
+
this.assertReadyToWrite();
|
|
7234
|
+
this.options.mutationGuard?.run(() => {});
|
|
7235
|
+
}
|
|
4989
7236
|
/**
|
|
4990
7237
|
* A run inside one segment as native Cursors. Local ranges exclude insertions at their start and
|
|
4991
7238
|
* include insertions at their end. K01 remains deferred.
|
|
@@ -4996,66 +7243,237 @@ var MedeoDsl = class MedeoDsl {
|
|
|
4996
7243
|
audioScriptId: scriptId,
|
|
4997
7244
|
segmentId
|
|
4998
7245
|
});
|
|
4999
|
-
if (!text) throw new Error("Missing AudioScript segment");
|
|
5000
|
-
assertTextOffset(text.toString(), start);
|
|
5001
|
-
assertTextOffset(text.toString(), end);
|
|
5002
|
-
if (start >= end) throw new Error("Expected a nonempty range");
|
|
5003
|
-
const from = text.getCursor(start, 0), to = text.getCursor(end, 0);
|
|
5004
|
-
if (!from || !to) throw new Error("Unable to construct text range");
|
|
5005
|
-
return {
|
|
5006
|
-
audioScriptId: scriptId,
|
|
5007
|
-
segmentId,
|
|
5008
|
-
start: from,
|
|
5009
|
-
end: to
|
|
5010
|
-
};
|
|
5011
|
-
}
|
|
5012
|
-
dispose() {
|
|
5013
|
-
if (this.disposed) return;
|
|
5014
|
-
this.disposed = true;
|
|
5015
|
-
this.unsubscribe();
|
|
5016
|
-
this.mirror.dispose();
|
|
5017
|
-
this.listeners.clear();
|
|
5018
|
-
if (this.ownsDoc) this.doc.free();
|
|
7246
|
+
if (!text) throw new Error("Missing AudioScript segment");
|
|
7247
|
+
assertTextOffset(text.toString(), start);
|
|
7248
|
+
assertTextOffset(text.toString(), end);
|
|
7249
|
+
if (start >= end) throw new Error("Expected a nonempty range");
|
|
7250
|
+
const from = text.getCursor(start, 0), to = text.getCursor(end, 0);
|
|
7251
|
+
if (!from || !to) throw new Error("Unable to construct text range");
|
|
7252
|
+
return {
|
|
7253
|
+
audioScriptId: scriptId,
|
|
7254
|
+
segmentId,
|
|
7255
|
+
start: from,
|
|
7256
|
+
end: to
|
|
7257
|
+
};
|
|
7258
|
+
}
|
|
7259
|
+
dispose() {
|
|
7260
|
+
if (this.disposed) return;
|
|
7261
|
+
this.disposed = true;
|
|
7262
|
+
this.unsubscribe();
|
|
7263
|
+
this.mirror.dispose();
|
|
7264
|
+
this.listeners.clear();
|
|
7265
|
+
if (this.ownsDoc) this.doc.free();
|
|
7266
|
+
}
|
|
7267
|
+
mutate(run) {
|
|
7268
|
+
this.assertReadyToWrite();
|
|
7269
|
+
const apply = () => {
|
|
7270
|
+
this.writing = true;
|
|
7271
|
+
try {
|
|
7272
|
+
run();
|
|
7273
|
+
} finally {
|
|
7274
|
+
this.rejectTransaction = void 0;
|
|
7275
|
+
this.writing = false;
|
|
7276
|
+
if (this.dirty) this.snapshot = void 0;
|
|
7277
|
+
this.notify();
|
|
7278
|
+
}
|
|
7279
|
+
};
|
|
7280
|
+
if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
|
|
7281
|
+
else apply();
|
|
7282
|
+
}
|
|
7283
|
+
assertReadyToWrite() {
|
|
7284
|
+
this.assertActive();
|
|
7285
|
+
if (this.writing || this.notifying) {
|
|
7286
|
+
const error = /* @__PURE__ */ new Error("A DSL mutation is already running");
|
|
7287
|
+
this.rejectTransaction?.(error);
|
|
7288
|
+
throw error;
|
|
7289
|
+
}
|
|
7290
|
+
if (this.doc.getPendingTxnLength() !== 0) throw new Error("Commit external native edits before a DSL mutation");
|
|
7291
|
+
}
|
|
7292
|
+
notify() {
|
|
7293
|
+
if (this.disposed || !this.dirty) return;
|
|
7294
|
+
this.dirty = false;
|
|
7295
|
+
this.notifying = true;
|
|
7296
|
+
try {
|
|
7297
|
+
for (const listener of this.listeners) listener(this.changeSource);
|
|
7298
|
+
} finally {
|
|
7299
|
+
this.notifying = false;
|
|
7300
|
+
}
|
|
7301
|
+
}
|
|
7302
|
+
assertActive() {
|
|
7303
|
+
if (this.disposed) throw new Error("MedeoDsl is disposed");
|
|
7304
|
+
}
|
|
7305
|
+
};
|
|
7306
|
+
//#endregion
|
|
7307
|
+
//#region src/dsl/projection/audio-scripts.ts
|
|
7308
|
+
/**
|
|
7309
|
+
* The scripts a recording says. It follows derived-from from the recording; a PhoneticScript is a
|
|
7310
|
+
* variant, so its words are the base its phoneme names, and derived-from is not followed past it.
|
|
7311
|
+
* When nothing is found that way, a script read off the recording itself is what it says.
|
|
7312
|
+
*/
|
|
7313
|
+
function spokenScripts(snapshot, recordingId) {
|
|
7314
|
+
const relations = Object.values(snapshot["derived-from-relations"]);
|
|
7315
|
+
const found = /* @__PURE__ */ new Map();
|
|
7316
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7317
|
+
const visit = (id, phoneticScriptId) => {
|
|
7318
|
+
if (snapshot["audio-script-entities"][id]) {
|
|
7319
|
+
if (!found.has(id)) found.set(id, /* @__PURE__ */ new Set());
|
|
7320
|
+
if (phoneticScriptId !== void 0) found.get(id).add(phoneticScriptId);
|
|
7321
|
+
return;
|
|
7322
|
+
}
|
|
7323
|
+
if (seen.has(id)) return;
|
|
7324
|
+
seen.add(id);
|
|
7325
|
+
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
7326
|
+
if (phonetic) {
|
|
7327
|
+
visit(phonetic.phoneme.audioScriptId, id);
|
|
7328
|
+
return;
|
|
7329
|
+
}
|
|
7330
|
+
for (const r of relations) if (r.entityId === id) visit(r.sourceId, phoneticScriptId);
|
|
7331
|
+
};
|
|
7332
|
+
visit(recordingId);
|
|
7333
|
+
if (!found.size) {
|
|
7334
|
+
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, /* @__PURE__ */ new Set());
|
|
7335
|
+
}
|
|
7336
|
+
return [...found].map(([audioScriptId, ids]) => {
|
|
7337
|
+
const phoneticScriptIds = [...ids];
|
|
7338
|
+
if (phoneticScriptIds.length === 1) return {
|
|
7339
|
+
audioScriptId,
|
|
7340
|
+
phoneticScriptId: phoneticScriptIds[0]
|
|
7341
|
+
};
|
|
7342
|
+
return phoneticScriptIds.length ? {
|
|
7343
|
+
audioScriptId,
|
|
7344
|
+
phoneticScriptIds
|
|
7345
|
+
} : { audioScriptId };
|
|
7346
|
+
});
|
|
7347
|
+
}
|
|
7348
|
+
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
7349
|
+
function toAudioScriptViews(snapshot) {
|
|
7350
|
+
const views = /* @__PURE__ */ new Map();
|
|
7351
|
+
const segmentIndex = (scriptId, segmentId) => {
|
|
7352
|
+
const index = snapshot["audio-script-entities"][scriptId]?.segments.findIndex((s) => s.id === segmentId) ?? -1;
|
|
7353
|
+
return index < 0 ? void 0 : index;
|
|
7354
|
+
};
|
|
7355
|
+
for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) views.set(id, {
|
|
7356
|
+
id,
|
|
7357
|
+
segments: script.segments.map((segment) => {
|
|
7358
|
+
const rich = snapshot.richText[segment.id];
|
|
7359
|
+
const language = rich ? markedLanguage(langsFromDelta(rich.delta)) : void 0;
|
|
7360
|
+
return {
|
|
7361
|
+
id: segment.id,
|
|
7362
|
+
text: segment.text,
|
|
7363
|
+
...language === void 0 ? {} : { language },
|
|
7364
|
+
...segment.startMs === void 0 ? {} : {
|
|
7365
|
+
startMs: segment.startMs,
|
|
7366
|
+
endMs: segment.endMs
|
|
7367
|
+
}
|
|
7368
|
+
};
|
|
7369
|
+
}),
|
|
7370
|
+
sourceMediaIds: [],
|
|
7371
|
+
phoneticScripts: [],
|
|
7372
|
+
speeches: [],
|
|
7373
|
+
captions: []
|
|
7374
|
+
});
|
|
7375
|
+
for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
|
|
7376
|
+
for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
|
|
7377
|
+
const { audioScriptId, segmentId, phonemeScript } = phonetic.phoneme;
|
|
7378
|
+
const { digest } = phonetic;
|
|
7379
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
7380
|
+
const text = snapshot.phoneticText[id]?.text;
|
|
7381
|
+
views.get(audioScriptId)?.phoneticScripts.push({
|
|
7382
|
+
id,
|
|
7383
|
+
...segment === void 0 ? {} : { segment },
|
|
7384
|
+
...text === void 0 ? {} : { text },
|
|
7385
|
+
...phonemeScript === void 0 ? {} : { phonemeScript },
|
|
7386
|
+
...digest === void 0 ? {} : { digest }
|
|
7387
|
+
});
|
|
5019
7388
|
}
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
7389
|
+
const layout = resolveDslLayout(snapshot);
|
|
7390
|
+
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
7391
|
+
for (const track of layout.tracks) for (const { clipId } of track.clips) {
|
|
7392
|
+
const source = snapshot["clip-entities"][clipId]?.source;
|
|
7393
|
+
if (!source) continue;
|
|
7394
|
+
if (snapshot["media-entities"][source]?.kind === "voiceover-recording") {
|
|
7395
|
+
const voices = timbres.filter((r) => r.recordingId === source).flatMap((r) => {
|
|
7396
|
+
const voice = snapshot["voice-entities"][r.voiceId];
|
|
7397
|
+
if (!voice) return [];
|
|
7398
|
+
return [voice.name === void 0 ? {
|
|
7399
|
+
voiceId: r.voiceId,
|
|
7400
|
+
key: voice.key
|
|
7401
|
+
} : {
|
|
7402
|
+
voiceId: r.voiceId,
|
|
7403
|
+
key: voice.key,
|
|
7404
|
+
name: voice.name
|
|
7405
|
+
}];
|
|
7406
|
+
});
|
|
7407
|
+
for (const spoken of spokenScripts(snapshot, source)) {
|
|
7408
|
+
const applied = appliedSpeech(snapshot, source, spoken);
|
|
7409
|
+
views.get(spoken.audioScriptId)?.speeches.push({
|
|
7410
|
+
clipId,
|
|
7411
|
+
recordingId: source,
|
|
7412
|
+
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
7413
|
+
...spoken.phoneticScriptIds === void 0 ? {} : { phoneticScriptIds: spoken.phoneticScriptIds },
|
|
7414
|
+
voices,
|
|
7415
|
+
...applied === void 0 ? {} : { applied }
|
|
7416
|
+
});
|
|
5037
7417
|
}
|
|
5038
|
-
|
|
5039
|
-
if (this.options.mutationGuard) this.options.mutationGuard.run(apply);
|
|
5040
|
-
else apply();
|
|
5041
|
-
}
|
|
5042
|
-
notify() {
|
|
5043
|
-
if (this.disposed || !this.dirty) return;
|
|
5044
|
-
this.dirty = false;
|
|
5045
|
-
this.notifying = true;
|
|
5046
|
-
try {
|
|
5047
|
-
for (const listener of this.listeners) listener(this.changeSource);
|
|
5048
|
-
} finally {
|
|
5049
|
-
this.notifying = false;
|
|
7418
|
+
continue;
|
|
5050
7419
|
}
|
|
7420
|
+
const caption = snapshot["caption-entities"][source];
|
|
7421
|
+
if (!caption?.selection || caption.text !== void 0 || !snapshot.captionText[source]) continue;
|
|
7422
|
+
const { audioScriptId, segmentId } = caption.selection;
|
|
7423
|
+
const segment = segmentIndex(audioScriptId, segmentId);
|
|
7424
|
+
if (segment !== void 0) views.get(audioScriptId)?.captions.push({
|
|
7425
|
+
clipId,
|
|
7426
|
+
captionId: source,
|
|
7427
|
+
segment
|
|
7428
|
+
});
|
|
5051
7429
|
}
|
|
5052
|
-
|
|
5053
|
-
|
|
7430
|
+
for (const view of views.values()) placeTranscript(snapshot, layout, view);
|
|
7431
|
+
return [...views.values()];
|
|
7432
|
+
}
|
|
7433
|
+
/**
|
|
7434
|
+
* A transcript's segments on the Timeline: a segment's time is in the medium the script was read
|
|
7435
|
+
* off. An authored Caption anchor identifies its use of that medium; otherwise the earliest
|
|
7436
|
+
* Clip holding that source moment is used. The source time is never taken from the Caption.
|
|
7437
|
+
*/
|
|
7438
|
+
function placeTranscript(snapshot, layout, view) {
|
|
7439
|
+
if (!view.sourceMediaIds.length) return;
|
|
7440
|
+
const plays = layout.tracks.flatMap((track) => track.clips).filter((clip) => view.sourceMediaIds.includes(snapshot["clip-entities"][clip.clipId]?.source ?? "")).sort((a, b) => a.startMs - b.startMs);
|
|
7441
|
+
for (const [index, segment] of view.segments.entries()) {
|
|
7442
|
+
const at = segment.startMs;
|
|
7443
|
+
if (at === void 0) continue;
|
|
7444
|
+
const anchoredUses = new Set(view.captions.filter((caption) => caption.segment === index).map((caption) => layout.clips[caption.clipId]?.anchorClipId));
|
|
7445
|
+
const candidates = plays.filter((c) => c.inMs <= at && at < c.outMs);
|
|
7446
|
+
const clip = candidates.find((c) => anchoredUses.has(c.clipId)) ?? candidates[0];
|
|
7447
|
+
if (clip) segment.timelineMs = Math.round(clip.startMs + (at - clip.inMs) / clip.rate);
|
|
5054
7448
|
}
|
|
5055
|
-
}
|
|
7449
|
+
}
|
|
7450
|
+
/**
|
|
7451
|
+
* Whether a recording still matches what it was synthesized from: the digest its derived-from
|
|
7452
|
+
* records against the source's file digest now. A recording derived from a script directly is
|
|
7453
|
+
* not judged: a written script has no file, and a transcript's file is what it was read from,
|
|
7454
|
+
* not what a take was synthesized from (D190).
|
|
7455
|
+
*/
|
|
7456
|
+
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
7457
|
+
const ids = spoken.phoneticScriptIds ?? (spoken.phoneticScriptId === void 0 ? [] : [spoken.phoneticScriptId]);
|
|
7458
|
+
if (!ids.length) return void 0;
|
|
7459
|
+
let unknown = false;
|
|
7460
|
+
for (const sourceId of ids) {
|
|
7461
|
+
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
7462
|
+
const current = snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
7463
|
+
if (recorded === void 0 || current === void 0) unknown = true;
|
|
7464
|
+
else if (recorded !== current) return false;
|
|
7465
|
+
}
|
|
7466
|
+
return unknown ? void 0 : true;
|
|
7467
|
+
}
|
|
5056
7468
|
//#endregion
|
|
5057
7469
|
//#region src/dsl/voiceover-actions.ts
|
|
5058
|
-
function
|
|
7470
|
+
function matchesPlainPhoneticOrigin(s, id, scriptId, segmentId) {
|
|
7471
|
+
const phonetic = s["phonetic-script-entities"][id];
|
|
7472
|
+
const run = s.phoneticText[id];
|
|
7473
|
+
const segment = s["audio-script-entities"][scriptId]?.segments.find((item) => item.id === segmentId);
|
|
7474
|
+
return phonetic?.phoneme.audioScriptId === scriptId && phonetic.phoneme.segmentId === segmentId && phonetic.phoneme.phonemeScript === void 0 && run?.start === 0 && run.end === segment?.text.length;
|
|
7475
|
+
}
|
|
7476
|
+
function voiceoverPlan(s, input, result, plainPhoneticIds) {
|
|
5059
7477
|
if (requireTrack(s, input.trackId).role !== "speech" || input.media.kind !== "voiceover-recording") throw new Error("Voiceover requires a speech Track and a recording");
|
|
5060
7478
|
if (input.captions.length && (!input.captionTrackId || requireTrack(s, input.captionTrackId).role !== "caption")) throw new Error("Caption Track is required");
|
|
5061
7479
|
const existing = s["clip-entities"][result.clipId];
|
|
@@ -5094,15 +7512,44 @@ function voiceoverPlan(s, input, result) {
|
|
|
5094
7512
|
inMs: 0,
|
|
5095
7513
|
outMs: input.media.durationMs
|
|
5096
7514
|
}, input.media.durationMs);
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
7515
|
+
if (phoneticScriptId !== void 0) {
|
|
7516
|
+
const recorded = Object.values(s["derived-from-relations"]).find((origin) => origin.entityId === result.mediaId && origin.sourceId === phoneticScriptId);
|
|
7517
|
+
if (recorded?.sourceDigest !== void 0 && input.sourceDigest !== void 0 && recorded.sourceDigest !== input.sourceDigest) throw new Error("A recording already has a different synthesis digest");
|
|
7518
|
+
if (!recorded || recorded.sourceDigest === void 0 && input.sourceDigest !== void 0) mutations.push({
|
|
7519
|
+
type: "set-relation",
|
|
7520
|
+
table: "derived-from-relations",
|
|
7521
|
+
value: {
|
|
7522
|
+
entityId: result.mediaId,
|
|
7523
|
+
sourceId: phoneticScriptId,
|
|
7524
|
+
...input.sourceDigest === void 0 ? {} : { sourceDigest: input.sourceDigest }
|
|
7525
|
+
}
|
|
7526
|
+
});
|
|
7527
|
+
} else {
|
|
7528
|
+
if (input.sourceDigest !== void 0) throw new Error("A synthesis digest requires its PhoneticScript");
|
|
7529
|
+
if (plainPhoneticIds.length !== segmentIds.length) throw new Error("Expected one PhoneticScript id per plain segment");
|
|
7530
|
+
segmentIds.forEach((segmentId, i) => {
|
|
7531
|
+
const id = plainPhoneticIds[i];
|
|
7532
|
+
if (s["phonetic-script-entities"][id]) {
|
|
7533
|
+
if (!matchesPlainPhoneticOrigin(s, id, result.audioScriptId, segmentId)) throw new Error("PhoneticScript id already names another synthesis origin");
|
|
7534
|
+
} else mutations.push({
|
|
7535
|
+
type: "create-entity",
|
|
7536
|
+
table: "phonetic-script-entities",
|
|
7537
|
+
id,
|
|
7538
|
+
value: { phoneme: {
|
|
7539
|
+
audioScriptId: result.audioScriptId,
|
|
7540
|
+
segmentId
|
|
7541
|
+
} }
|
|
7542
|
+
});
|
|
7543
|
+
if (!Object.values(s["derived-from-relations"]).some((origin) => origin.entityId === result.mediaId && origin.sourceId === id)) mutations.push({
|
|
7544
|
+
type: "set-relation",
|
|
7545
|
+
table: "derived-from-relations",
|
|
7546
|
+
value: {
|
|
7547
|
+
entityId: result.mediaId,
|
|
7548
|
+
sourceId: id
|
|
7549
|
+
}
|
|
7550
|
+
});
|
|
7551
|
+
});
|
|
7552
|
+
}
|
|
5106
7553
|
if (input.voice) {
|
|
5107
7554
|
const id = resourceId("voice", input.voice.system, input.voice.key);
|
|
5108
7555
|
mutations.push({
|
|
@@ -5154,7 +7601,7 @@ function voiceoverPlan(s, input, result) {
|
|
|
5154
7601
|
mutations.push(...placementMutations(s, result.clipId, input.placement));
|
|
5155
7602
|
uniqueIds(result.captionClipIds);
|
|
5156
7603
|
const retained = new Set(result.captionClipIds);
|
|
5157
|
-
const oldCaptions =
|
|
7604
|
+
const oldCaptions = existing ? captionsShowing(s, existing.source) : [];
|
|
5158
7605
|
mutations.push(...deleteClips(s, oldCaptions.filter((id) => !retained.has(id)), "cascade"));
|
|
5159
7606
|
const placement = mutations.find((m) => m.type === "patch-entity" && m.table === "clip-entities" && m.id === result.clipId && "anchor" in m.patch);
|
|
5160
7607
|
const anchor = placement?.type === "patch-entity" && placement.table === "clip-entities" ? placement.patch.anchor : void 0;
|
|
@@ -5225,6 +7672,33 @@ function voiceoverPlan(s, input, result) {
|
|
|
5225
7672
|
});
|
|
5226
7673
|
return mutations;
|
|
5227
7674
|
}
|
|
7675
|
+
/**
|
|
7676
|
+
* Caption Clips showing what a recording says (D183): their Caption selects the run of the segment
|
|
7677
|
+
* the PhoneticScript it was spoken from is written for, or any segment of the script it was read
|
|
7678
|
+
* from directly.
|
|
7679
|
+
*/
|
|
7680
|
+
function captionsShowing(s, recordingId) {
|
|
7681
|
+
const said = spokenScripts(s, recordingId).map(({ audioScriptId, phoneticScriptId }) => {
|
|
7682
|
+
if (phoneticScriptId === void 0) return { audioScriptId };
|
|
7683
|
+
const { segmentId } = s["phonetic-script-entities"][phoneticScriptId].phoneme;
|
|
7684
|
+
return {
|
|
7685
|
+
audioScriptId,
|
|
7686
|
+
segmentId,
|
|
7687
|
+
run: s.phoneticText[phoneticScriptId]
|
|
7688
|
+
};
|
|
7689
|
+
});
|
|
7690
|
+
return Object.entries(s["clip-entities"]).flatMap(([id, clip]) => {
|
|
7691
|
+
const selection = s["caption-entities"][clip.source]?.selection;
|
|
7692
|
+
const shown = s.captionText[clip.source];
|
|
7693
|
+
if (!selection) return [];
|
|
7694
|
+
return said.some((words) => {
|
|
7695
|
+
if (selection.audioScriptId !== words.audioScriptId) return false;
|
|
7696
|
+
if (!("segmentId" in words)) return true;
|
|
7697
|
+
if (selection.segmentId !== words.segmentId || !shown || !words.run) return false;
|
|
7698
|
+
return shown.start < words.run.end && words.run.start < shown.end;
|
|
7699
|
+
}) && requireTrack(s, trackOf(s, id)).role === "caption" ? [id] : [];
|
|
7700
|
+
});
|
|
7701
|
+
}
|
|
5228
7702
|
//#endregion
|
|
5229
7703
|
//#region src/dsl/dsl-editor.ts
|
|
5230
7704
|
/** Business edits write DSL facts in one audited transaction; layout is derived on read. */
|
|
@@ -5572,10 +8046,20 @@ var DslEditor = class {
|
|
|
5572
8046
|
captionClipIds: input.captions.map((c) => c.clipId ?? this.idFactory("clip")),
|
|
5573
8047
|
captionIds: input.captions.map(() => this.idFactory("caption"))
|
|
5574
8048
|
};
|
|
8049
|
+
const state = this.dsl.getSnapshot();
|
|
8050
|
+
const plainSegments = input.script.segments === void 0 ? state["audio-script-entities"][result.audioScriptId]?.segments.map((segment) => segment.id) ?? [] : result.segmentIds;
|
|
8051
|
+
const plainPhoneticIds = input.script.phoneticScriptId === void 0 ? plainSegments.map((segmentId) => {
|
|
8052
|
+
const origins = Object.values(state["derived-from-relations"]).filter((origin) => {
|
|
8053
|
+
if (origin.entityId !== result.mediaId) return false;
|
|
8054
|
+
return matchesPlainPhoneticOrigin(state, origin.sourceId, result.audioScriptId, segmentId);
|
|
8055
|
+
});
|
|
8056
|
+
if (origins.length > 1) throw new Error("Ambiguous plain recording origin");
|
|
8057
|
+
return origins[0]?.sourceId ?? this.idFactory("phonetic-script");
|
|
8058
|
+
}) : [];
|
|
5575
8059
|
this.write("applyVoiceover", {
|
|
5576
8060
|
input,
|
|
5577
8061
|
result
|
|
5578
|
-
}, voiceoverPlan(
|
|
8062
|
+
}, voiceoverPlan(state, input, result, plainPhoneticIds), options);
|
|
5579
8063
|
return result;
|
|
5580
8064
|
}
|
|
5581
8065
|
voiceoverScriptId(script) {
|
|
@@ -5592,8 +8076,12 @@ var DslEditor = class {
|
|
|
5592
8076
|
* it; an unchanged one keeps its file. Recordings already spoken from it keep their derived-from.
|
|
5593
8077
|
*/
|
|
5594
8078
|
async setPhoneticScript(input, options) {
|
|
8079
|
+
this.dsl.assertWritable();
|
|
5595
8080
|
const id = input.phoneticScriptId ?? this.idFactory("phonetic-script");
|
|
5596
8081
|
const range = segmentRange(input);
|
|
8082
|
+
assertId(id);
|
|
8083
|
+
assertSegmentRange(range, requireSegment(this.dsl.getSnapshot(), range.audioScriptId, range.segmentId).text);
|
|
8084
|
+
assertPhonemeScript(input.phonemeScript);
|
|
5597
8085
|
const file = input.phonemeScript === void 0 ? {} : await this.fileFor(id, input.phonemeScript);
|
|
5598
8086
|
const exists = Object.hasOwn(this.dsl.getSnapshot()["phonetic-script-entities"], id);
|
|
5599
8087
|
const cleared = {
|
|
@@ -5743,132 +8231,6 @@ function createInitialMedeoDsl(options = {}) {
|
|
|
5743
8231
|
}
|
|
5744
8232
|
}
|
|
5745
8233
|
//#endregion
|
|
5746
|
-
//#region src/dsl/projection/audio-scripts.ts
|
|
5747
|
-
/**
|
|
5748
|
-
* The scripts a recording says. It follows derived-from from the recording; a PhoneticScript is a
|
|
5749
|
-
* variant, so its words are the base its phoneme names, and derived-from is not followed past it.
|
|
5750
|
-
* When nothing is found that way, a script read off the recording itself is what it says.
|
|
5751
|
-
*/
|
|
5752
|
-
function spokenScripts(snapshot, recordingId) {
|
|
5753
|
-
const relations = Object.values(snapshot["derived-from-relations"]);
|
|
5754
|
-
const found = /* @__PURE__ */ new Map();
|
|
5755
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5756
|
-
const visit = (id, phoneticScriptId) => {
|
|
5757
|
-
if (seen.has(id)) return;
|
|
5758
|
-
seen.add(id);
|
|
5759
|
-
if (snapshot["audio-script-entities"][id]) {
|
|
5760
|
-
if (!found.has(id)) found.set(id, phoneticScriptId);
|
|
5761
|
-
return;
|
|
5762
|
-
}
|
|
5763
|
-
const phonetic = snapshot["phonetic-script-entities"][id];
|
|
5764
|
-
if (phonetic) {
|
|
5765
|
-
visit(phonetic.phoneme.audioScriptId, id);
|
|
5766
|
-
return;
|
|
5767
|
-
}
|
|
5768
|
-
for (const r of relations) if (r.entityId === id) visit(r.sourceId, phoneticScriptId);
|
|
5769
|
-
};
|
|
5770
|
-
visit(recordingId);
|
|
5771
|
-
if (!found.size) {
|
|
5772
|
-
for (const r of relations) if (r.sourceId === recordingId && snapshot["audio-script-entities"][r.entityId]) found.set(r.entityId, void 0);
|
|
5773
|
-
}
|
|
5774
|
-
return [...found].map(([audioScriptId, phoneticScriptId]) => phoneticScriptId === void 0 ? { audioScriptId } : {
|
|
5775
|
-
audioScriptId,
|
|
5776
|
-
phoneticScriptId
|
|
5777
|
-
});
|
|
5778
|
-
}
|
|
5779
|
-
/** Every AudioScript in the document with its PhoneticScripts, the Speech Clips saying it and the Caption Clips showing it. */
|
|
5780
|
-
function toAudioScriptViews(snapshot) {
|
|
5781
|
-
const views = /* @__PURE__ */ new Map();
|
|
5782
|
-
const segmentIndex = (scriptId, segmentId) => {
|
|
5783
|
-
const index = snapshot["audio-script-entities"][scriptId]?.segments.findIndex((s) => s.id === segmentId) ?? -1;
|
|
5784
|
-
return index < 0 ? void 0 : index;
|
|
5785
|
-
};
|
|
5786
|
-
for (const [id, script] of Object.entries(snapshot["audio-script-entities"])) views.set(id, {
|
|
5787
|
-
id,
|
|
5788
|
-
segments: script.segments.map((segment) => {
|
|
5789
|
-
const rich = snapshot.richText[segment.id];
|
|
5790
|
-
const language = rich ? markedLanguage(langsFromDelta(rich.delta)) : void 0;
|
|
5791
|
-
return language === void 0 ? {
|
|
5792
|
-
id: segment.id,
|
|
5793
|
-
text: segment.text
|
|
5794
|
-
} : {
|
|
5795
|
-
id: segment.id,
|
|
5796
|
-
text: segment.text,
|
|
5797
|
-
language
|
|
5798
|
-
};
|
|
5799
|
-
}),
|
|
5800
|
-
sourceMediaIds: [],
|
|
5801
|
-
phoneticScripts: [],
|
|
5802
|
-
speeches: [],
|
|
5803
|
-
captions: []
|
|
5804
|
-
});
|
|
5805
|
-
for (const r of Object.values(snapshot["derived-from-relations"])) if (snapshot["media-entities"][r.sourceId]) views.get(r.entityId)?.sourceMediaIds.push(r.sourceId);
|
|
5806
|
-
for (const [id, phonetic] of Object.entries(snapshot["phonetic-script-entities"])) {
|
|
5807
|
-
const { audioScriptId, segmentId, phonemeScript } = phonetic.phoneme;
|
|
5808
|
-
const { digest } = phonetic;
|
|
5809
|
-
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5810
|
-
const text = snapshot.phoneticText[id]?.text;
|
|
5811
|
-
views.get(audioScriptId)?.phoneticScripts.push({
|
|
5812
|
-
id,
|
|
5813
|
-
...segment === void 0 ? {} : { segment },
|
|
5814
|
-
...text === void 0 ? {} : { text },
|
|
5815
|
-
...phonemeScript === void 0 ? {} : { phonemeScript },
|
|
5816
|
-
...digest === void 0 ? {} : { digest }
|
|
5817
|
-
});
|
|
5818
|
-
}
|
|
5819
|
-
const timbres = Object.values(snapshot["voice-timbre-relations"]);
|
|
5820
|
-
for (const track of resolveDslLayout(snapshot).tracks) for (const { clipId } of track.clips) {
|
|
5821
|
-
const source = snapshot["clip-entities"][clipId]?.source;
|
|
5822
|
-
if (!source) continue;
|
|
5823
|
-
if (snapshot["media-entities"][source]?.kind === "voiceover-recording") {
|
|
5824
|
-
const voices = timbres.filter((r) => r.recordingId === source).flatMap((r) => {
|
|
5825
|
-
const voice = snapshot["voice-entities"][r.voiceId];
|
|
5826
|
-
if (!voice) return [];
|
|
5827
|
-
return [voice.name === void 0 ? {
|
|
5828
|
-
voiceId: r.voiceId,
|
|
5829
|
-
key: voice.key
|
|
5830
|
-
} : {
|
|
5831
|
-
voiceId: r.voiceId,
|
|
5832
|
-
key: voice.key,
|
|
5833
|
-
name: voice.name
|
|
5834
|
-
}];
|
|
5835
|
-
});
|
|
5836
|
-
for (const spoken of spokenScripts(snapshot, source)) {
|
|
5837
|
-
const applied = appliedSpeech(snapshot, source, spoken);
|
|
5838
|
-
views.get(spoken.audioScriptId)?.speeches.push({
|
|
5839
|
-
clipId,
|
|
5840
|
-
recordingId: source,
|
|
5841
|
-
...spoken.phoneticScriptId === void 0 ? {} : { phoneticScriptId: spoken.phoneticScriptId },
|
|
5842
|
-
voices,
|
|
5843
|
-
...applied === void 0 ? {} : { applied }
|
|
5844
|
-
});
|
|
5845
|
-
}
|
|
5846
|
-
continue;
|
|
5847
|
-
}
|
|
5848
|
-
const caption = snapshot["caption-entities"][source];
|
|
5849
|
-
if (!caption?.selection || caption.text !== void 0 || !snapshot.captionText[source]) continue;
|
|
5850
|
-
const { audioScriptId, segmentId } = caption.selection;
|
|
5851
|
-
const segment = segmentIndex(audioScriptId, segmentId);
|
|
5852
|
-
if (segment !== void 0) views.get(audioScriptId)?.captions.push({
|
|
5853
|
-
clipId,
|
|
5854
|
-
captionId: source,
|
|
5855
|
-
segment
|
|
5856
|
-
});
|
|
5857
|
-
}
|
|
5858
|
-
return [...views.values()];
|
|
5859
|
-
}
|
|
5860
|
-
/**
|
|
5861
|
-
* Whether a recording still matches what it was synthesized from: the digest its derived-from
|
|
5862
|
-
* records against the source's file digest now. An AudioScript has no file, so a recording
|
|
5863
|
-
* derived from a script directly is not judged.
|
|
5864
|
-
*/
|
|
5865
|
-
function appliedSpeech(snapshot, recordingId, spoken) {
|
|
5866
|
-
const sourceId = spoken.phoneticScriptId ?? spoken.audioScriptId;
|
|
5867
|
-
const recorded = Object.values(snapshot["derived-from-relations"]).find((r) => r.entityId === recordingId && r.sourceId === sourceId)?.sourceDigest;
|
|
5868
|
-
const current = spoken.phoneticScriptId === void 0 ? void 0 : snapshot["phonetic-script-entities"][sourceId]?.digest;
|
|
5869
|
-
return recorded === void 0 || current === void 0 ? void 0 : recorded === current;
|
|
5870
|
-
}
|
|
5871
|
-
//#endregion
|
|
5872
8234
|
//#region src/dsl/projection/caption-style.ts
|
|
5873
8235
|
const fields = {
|
|
5874
8236
|
fontSize: "font_size",
|
|
@@ -5905,6 +8267,9 @@ function fromDraftStyle(style) {
|
|
|
5905
8267
|
};
|
|
5906
8268
|
}
|
|
5907
8269
|
//#endregion
|
|
8270
|
+
//#region src/dsl/projection/video-draft-types.ts
|
|
8271
|
+
const DEFAULT_UNIT_TIME_MS = 33.333;
|
|
8272
|
+
//#endregion
|
|
5908
8273
|
//#region src/dsl/projection/to-video-draft.ts
|
|
5909
8274
|
/** A content read view, not an archive of authoring identities, rich-text marks or provenance. */
|
|
5910
8275
|
function toVideoDraft(snapshot) {
|
|
@@ -5940,7 +8305,8 @@ function toVideoDraft(snapshot) {
|
|
|
5940
8305
|
} }
|
|
5941
8306
|
} };
|
|
5942
8307
|
} else if (track.role === "speech") {
|
|
5943
|
-
if (!source || source.kind !== "
|
|
8308
|
+
if (!source || source.kind !== "audio" && source.kind !== "voiceover-recording") fail("Speech playback requires an Audio or VoiceoverRecording");
|
|
8309
|
+
if (source.system !== "memota" && !(source.kind === "voiceover-recording" && source.system === "memota-speech")) fail("Speech playback requires a Memota audio file");
|
|
5944
8310
|
requireWholeAudio(source, clip, fail);
|
|
5945
8311
|
if (clip.fillTimeline) fail("Legacy Speech cannot express timeline fill");
|
|
5946
8312
|
const voices = Object.values(snapshot["voice-timbre-relations"]).filter((r) => r.recordingId === sourceId).flatMap((r) => snapshot["voice-entities"][r.voiceId] ? [snapshot["voice-entities"][r.voiceId]] : []);
|
|
@@ -5957,7 +8323,7 @@ function toVideoDraft(snapshot) {
|
|
|
5957
8323
|
audio_storage_key: source.storageKey,
|
|
5958
8324
|
origin_speech_id: source.key,
|
|
5959
8325
|
volume: clip.volume,
|
|
5960
|
-
audio_script: spokenText(snapshot, sourceId),
|
|
8326
|
+
audio_script: source.kind === "voiceover-recording" ? spokenText(snapshot, sourceId) : "",
|
|
5961
8327
|
voice: voice ? {
|
|
5962
8328
|
id: voice.key,
|
|
5963
8329
|
name: voice.name
|
|
@@ -6053,6 +8419,15 @@ function spokenText(snapshot, recordingId) {
|
|
|
6053
8419
|
if (!first) return "";
|
|
6054
8420
|
if (first.phoneticScriptId !== void 0) return snapshot.phoneticText[first.phoneticScriptId]?.text ?? "";
|
|
6055
8421
|
const script = snapshot["audio-script-entities"][first.audioScriptId];
|
|
8422
|
+
if (first.phoneticScriptIds !== void 0 && script) {
|
|
8423
|
+
const ids = first.phoneticScriptIds;
|
|
8424
|
+
const selections = ids.map((id) => snapshot["phonetic-script-entities"][id]);
|
|
8425
|
+
if (!(ids.length === script.segments.length && script.segments.every((segment) => {
|
|
8426
|
+
const matching = ids.filter((id) => snapshot["phonetic-script-entities"][id]?.phoneme.segmentId === segment.id);
|
|
8427
|
+
const run = matching.length === 1 ? snapshot.phoneticText[matching[0]] : void 0;
|
|
8428
|
+
return run?.start === 0 && run.end === segment.text.length;
|
|
8429
|
+
})) || selections.some((selection) => selection.phoneme.phonemeScript !== void 0)) projectionError("ambiguous-script", `media-entities/${recordingId}`, "Multiple phonetic sources require complete plain selections of one script");
|
|
8430
|
+
}
|
|
6056
8431
|
return script ? script.segments.map((segment) => segment.text).join("\n") : "";
|
|
6057
8432
|
}
|
|
6058
8433
|
//#endregion
|
|
@@ -6265,11 +8640,7 @@ function prepare(input, options) {
|
|
|
6265
8640
|
start: start - segment.start,
|
|
6266
8641
|
end: end - segment.start
|
|
6267
8642
|
});
|
|
6268
|
-
} else
|
|
6269
|
-
code: "caption-text-unmatched",
|
|
6270
|
-
path: `part_library/${captionId}/text`,
|
|
6271
|
-
message: "No forward exact match; preserved as independent Caption text"
|
|
6272
|
-
});
|
|
8643
|
+
} else fail(`part_library/${captionId}/text`, "Caption must match a segment of its AudioScript");
|
|
6273
8644
|
}
|
|
6274
8645
|
}
|
|
6275
8646
|
const hosts = /* @__PURE__ */ new Map();
|
|
@@ -6312,16 +8683,14 @@ function prepare(input, options) {
|
|
|
6312
8683
|
if (caption.speech_part_id !== void 0 && owner.get(id) !== caption.speech_part_id) fail(`part_library/${id}/speech_part_id`, "Caption ownership must agree with ordered speech.caption_ids");
|
|
6313
8684
|
sourceId = claim(mint("caption"));
|
|
6314
8685
|
const selected = selections.get(id), style = fromDraftStyle(caption.style);
|
|
8686
|
+
if (!selected) fail(`part_library/${id}/text`, "Caption must select a segment of an AudioScript");
|
|
6315
8687
|
mutations.push({
|
|
6316
8688
|
type: "create-entity",
|
|
6317
8689
|
table: "caption-entities",
|
|
6318
8690
|
id: sourceId,
|
|
6319
|
-
value: {
|
|
6320
|
-
...style === void 0 ? {} : { style },
|
|
6321
|
-
...selected ? {} : { text: caption.text }
|
|
6322
|
-
}
|
|
8691
|
+
value: style === void 0 ? {} : { style }
|
|
6323
8692
|
});
|
|
6324
|
-
|
|
8693
|
+
mutations.push({
|
|
6325
8694
|
type: "set-caption-content",
|
|
6326
8695
|
captionId: sourceId,
|
|
6327
8696
|
...selected
|
|
@@ -6392,4 +8761,4 @@ function importedScript(id, text, mintSegment) {
|
|
|
6392
8761
|
};
|
|
6393
8762
|
}
|
|
6394
8763
|
//#endregion
|
|
6395
|
-
export { DslEditor as a,
|
|
8764
|
+
export { tupleHash as S, resolveDslLayout as _, DslEditor as a, relationId as b, MedeoDsl as c, assertPhoneticBindingFormat as d, documentFormat as f, timeAnchorCodec as g, isInvalidAtomic as h, createInitialMedeoDsl as i, UnsupportedDocumentFormatError as l, captionContentCodec as m, toVideoDraft as n, spokenScripts as o, medeoDslLoroShape as p, DEFAULT_UNIT_TIME_MS as r, toAudioScriptViews as s, fromVideoDraft as t, assertDslDocument as u, DslProjectionError as v, resourceId as x, defaultIdFactory as y };
|