@mengine/medeo-client 2.0.1-alpha.2 → 2.0.1-alpha.21
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 +31 -0
- package/dist/{document-BSAUud6c.js → document-BXQcpnOg.js} +576 -332
- package/dist/{index-B31KeDzA.d.ts → index-CzDNr1Tb.d.ts} +297 -184
- package/dist/index.d.ts +594 -320
- package/dist/index.js +2221 -370
- package/dist/{loro-relay-doc-ssdYpuef.js → loro-relay-doc-cJSY-uau.js} +7 -2
- package/dist/relay.js +1 -1
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +4 -19
- package/package.json +9 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { u as volumeSchema } from "./shared-iWnU2osE.js";
|
|
2
|
-
import { Mirror, schema } from "loro-mirror";
|
|
2
|
+
import { Mirror, schema, validateSchema } from "loro-mirror";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
|
|
5
5
|
import { sha256 } from "@noble/hashes/sha2.js";
|
|
@@ -7,9 +7,11 @@ import { bytesToHex } from "@noble/hashes/utils.js";
|
|
|
7
7
|
import { produce } from "immer";
|
|
8
8
|
//#region src/client/base64.ts
|
|
9
9
|
function bytesToBase64(bytes) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const native = bytes.toBase64;
|
|
11
|
+
if (typeof native === "function") return native.call(bytes);
|
|
12
|
+
const chunks = [];
|
|
13
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
|
|
14
|
+
return btoa(chunks.join(""));
|
|
13
15
|
}
|
|
14
16
|
function base64ToBytes(base64) {
|
|
15
17
|
const binary = atob(base64);
|
|
@@ -47,10 +49,6 @@ function base64ToBytes(base64) {
|
|
|
47
49
|
* value, last-writer-wins). The derived `fallback_abs_ms` snapshot sits beside
|
|
48
50
|
* it as its own number field — a separate LWW unit so a projection refresh
|
|
49
51
|
* never clobbers a `time_position` edit.
|
|
50
|
-
* - meta lives in a `meta` LoroMap: the mirror root only accepts container
|
|
51
|
-
* schemas, so meta scalars can't sit at the root as bare values. The domain
|
|
52
|
-
* `VideoDocument` keeps the same `meta` wrapper, so the shapes are isomorphic
|
|
53
|
-
* and the read/seed mapping is a near-identity.
|
|
54
52
|
* - `part_library` values are nested `LoroMap`s: the value is a `LoroMap` with
|
|
55
53
|
* four mutually-exclusive optional part sub-maps (`video_clip` / `speech` /
|
|
56
54
|
* `caption` / `bgm`), each a `LoroMap` whose fields are stored as real Loro
|
|
@@ -64,16 +62,10 @@ function base64ToBytes(base64) {
|
|
|
64
62
|
* as opaque JSON-blob sub-keys via `transform`. The four sub-keys are optional,
|
|
65
63
|
* so the "exactly one part kind" union invariant is not enforced by the schema
|
|
66
64
|
* type; it is rebuilt by `draftToPartUnion` on read and gated by zod on write.
|
|
67
|
-
* - a caption's `style` is
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* other nested maps are keyed by a unique id (`part_library`) or created
|
|
72
|
-
* atomically with their parent (`partValue`'s part sub-map), so they never hit
|
|
73
|
-
* that concurrent-first-create fork and stay on plain `setContainer`. See the
|
|
74
|
-
* note on `captionPart` below.
|
|
75
|
-
* - `video_creation_settings` is an opaque JSON blob carried in a string via
|
|
76
|
-
* `transform`: a whole value, last-writer-wins (no field-level concurrent edits).
|
|
65
|
+
* - a caption's `style` is a mergeable child map for independent style fields.
|
|
66
|
+
* Creation paths materialize it with the caption, even when empty, so undoing
|
|
67
|
+
* a later style edit never removes the parent reference. Other nested maps
|
|
68
|
+
* keep their regular container identities from creation.
|
|
77
69
|
*/
|
|
78
70
|
/**
|
|
79
71
|
* JSON-blob transform for an opaque, last-writer-wins value carried in a Loro
|
|
@@ -154,16 +146,6 @@ const partValue = schema.LoroMap({
|
|
|
154
146
|
bgm: bgmPart
|
|
155
147
|
});
|
|
156
148
|
const videoDocumentMirrorSchema = schema({
|
|
157
|
-
meta: schema.LoroMap({
|
|
158
|
-
schema_version: schema.String({ required: false }),
|
|
159
|
-
draft_id: schema.String({ required: false }),
|
|
160
|
-
project_id: schema.String({ required: false }),
|
|
161
|
-
owner_id: schema.String({ required: false }),
|
|
162
|
-
thumbnail_storage_key: schema.String({ required: false }),
|
|
163
|
-
chat_session_id: schema.String({ required: false }),
|
|
164
|
-
video_creation_settings: schema.String({ required: false }).transform(jsonTransform()),
|
|
165
|
-
version: schema.Number({ required: false })
|
|
166
|
-
}),
|
|
167
149
|
timeline: schema.LoroMap({ unit_time_ms: schema.Number({ required: false }) }),
|
|
168
150
|
tracks: schema.LoroMovableList(track, (t) => t.id),
|
|
169
151
|
part_library: schema.LoroMapRecord(partValue)
|
|
@@ -213,9 +195,6 @@ function recordEntries(record) {
|
|
|
213
195
|
}
|
|
214
196
|
//#endregion
|
|
215
197
|
//#region src/document/types.ts
|
|
216
|
-
const VIDEO_DOCUMENT_SCHEMA_VERSION = "video-document/v0";
|
|
217
|
-
/** A server-derived compatibility view, never an editor write authority. */
|
|
218
|
-
const ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION = "video-document/entity-projection-v1";
|
|
219
198
|
/** The linear speed multiplier of a `speed_shift`, defaulting to 1 (original). */
|
|
220
199
|
function speedOf(speedShift) {
|
|
221
200
|
const speed = speedShift?.config?.linear?.speed;
|
|
@@ -549,12 +528,12 @@ function cascadeAfterVideoClipChanges(doc, makeEmptyPart) {
|
|
|
549
528
|
* read-view: absolute time per item, `part_aggregations`, and total duration.
|
|
550
529
|
* This is the read side of the single-directional flow — never written back.
|
|
551
530
|
*/
|
|
552
|
-
function solveVideoDocument(document) {
|
|
531
|
+
function solveVideoDocument(document, options = {}) {
|
|
553
532
|
const doc = videoDocumentToTimelineDoc(document);
|
|
554
533
|
let counter = 0;
|
|
555
534
|
const derivedFillerPartIds = /* @__PURE__ */ new Set();
|
|
556
|
-
if (
|
|
557
|
-
|
|
535
|
+
if (options.placementResolved === true) {
|
|
536
|
+
assertResolvedPlacement(document);
|
|
558
537
|
recalculateTimelineDuration(doc);
|
|
559
538
|
} else cascadeAfterVideoClipChanges(doc, () => {
|
|
560
539
|
const partId = `empty_${counter++}`;
|
|
@@ -759,6 +738,19 @@ function ensureLaneTrack(draft, kind) {
|
|
|
759
738
|
function findLaneTrack(draft, kind) {
|
|
760
739
|
return (draft.tracks ?? []).find((t) => t?.parts_kind === kind);
|
|
761
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Whether every item already states a position that can be read without running
|
|
743
|
+
* the cascade: nothing `sequential`, and every `anchored` item carrying the
|
|
744
|
+
* absolute landing its writer stamped. This is a verification predicate, not a
|
|
745
|
+
* provenance test — see `SolveVideoDocumentOptions.placementResolved`.
|
|
746
|
+
*/
|
|
747
|
+
function hasResolvedPlacement(document) {
|
|
748
|
+
return (document.tracks ?? []).every((track) => (track.items ?? []).every((item) => item.time_position.mode !== "sequential" && (item.time_position.mode !== "anchored" || Number.isSafeInteger(item.fallback_abs_ms))));
|
|
749
|
+
}
|
|
750
|
+
/** A producer that claims resolved placement must actually have resolved it. */
|
|
751
|
+
function assertResolvedPlacement(document) {
|
|
752
|
+
if (!hasResolvedPlacement(document)) throw new Error("An entity timeline projection must contain resolved placement facts");
|
|
753
|
+
}
|
|
762
754
|
//#endregion
|
|
763
755
|
//#region src/document/zod-schema.ts
|
|
764
756
|
const partKindSchema = z.enum([
|
|
@@ -859,18 +851,7 @@ const partUnionSchema = z.union([
|
|
|
859
851
|
"bgm"
|
|
860
852
|
].filter((k) => p[k] != null).length === 1;
|
|
861
853
|
}, { message: "A part_library value must have exactly one of video_clip / speech / caption / bgm" });
|
|
862
|
-
const videoDocumentMetaSchema = z.object({
|
|
863
|
-
schema_version: z.enum([VIDEO_DOCUMENT_SCHEMA_VERSION, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION]),
|
|
864
|
-
draft_id: z.string().optional(),
|
|
865
|
-
project_id: z.string().optional(),
|
|
866
|
-
owner_id: z.string().optional(),
|
|
867
|
-
thumbnail_storage_key: z.string().optional(),
|
|
868
|
-
chat_session_id: z.string().optional(),
|
|
869
|
-
video_creation_settings: z.unknown().optional(),
|
|
870
|
-
version: finiteNumber.optional()
|
|
871
|
-
}).passthrough();
|
|
872
854
|
const videoDocumentSchema = z.object({
|
|
873
|
-
meta: videoDocumentMetaSchema,
|
|
874
855
|
timeline: z.object({ unit_time_ms: finiteNumber.optional() }).passthrough().optional(),
|
|
875
856
|
tracks: z.array(trackSchema).optional(),
|
|
876
857
|
part_library: z.record(z.string(), partUnionSchema).optional()
|
|
@@ -1140,13 +1121,13 @@ function zodPath(path) {
|
|
|
1140
1121
|
//#endregion
|
|
1141
1122
|
//#region src/document/projection.ts
|
|
1142
1123
|
/**
|
|
1143
|
-
* Projection between
|
|
1144
|
-
*
|
|
1124
|
+
* Projection between authoritative `VideoDocument` and compatible content
|
|
1125
|
+
* layouts (RFC 02 §5/§7). Both directions live here:
|
|
1145
1126
|
*
|
|
1146
1127
|
* - `toVideoDocument` ingests a `VideoDraft`, deriving each item's `position`
|
|
1147
1128
|
* from the legacy absolute layout + aggregations; derived values (abs time,
|
|
1148
1129
|
* `part_aggregations`, total duration) are dropped.
|
|
1149
|
-
* - `fromVideoDocument` solves a `VideoDocument`
|
|
1130
|
+
* - `fromVideoDocument` solves a `VideoDocument` into `VideoDraftContent` via the
|
|
1150
1131
|
* timeline-core cascade, re-deriving exactly those values.
|
|
1151
1132
|
*
|
|
1152
1133
|
* Business validation lives in `validation.ts`; `fromVideoDocument` asserts a
|
|
@@ -1177,16 +1158,6 @@ function toVideoDocument(draft) {
|
|
|
1177
1158
|
...(draft.below_main_tracks ?? []).map((t) => toTrack(t, false))
|
|
1178
1159
|
];
|
|
1179
1160
|
return {
|
|
1180
|
-
meta: {
|
|
1181
|
-
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
1182
|
-
draft_id: draft.id,
|
|
1183
|
-
project_id: draft.project_id,
|
|
1184
|
-
owner_id: draft.owner_id,
|
|
1185
|
-
thumbnail_storage_key: draft.thumbnail_storage_key,
|
|
1186
|
-
chat_session_id: draft.chat_session_id,
|
|
1187
|
-
video_creation_settings: clone$1(draft.video_creation_settings),
|
|
1188
|
-
version: draft.version
|
|
1189
|
-
},
|
|
1190
1161
|
timeline: draft.timeline == null ? void 0 : { unit_time_ms: draft.timeline.unit_time_ms },
|
|
1191
1162
|
tracks,
|
|
1192
1163
|
part_library: toAuthoritativePartLibrary(draft.part_library)
|
|
@@ -1206,7 +1177,8 @@ function toAuthoritativePartLibrary(partLibrary) {
|
|
|
1206
1177
|
if (typeof part !== "object" || part == null) continue;
|
|
1207
1178
|
if (part.video_clip != null) out[partId] = { video_clip: withTrimWindowFromDuration(part.video_clip) };
|
|
1208
1179
|
else if (part.speech != null) {
|
|
1209
|
-
const {
|
|
1180
|
+
const { origin_asset_system: _originAssetSystem, ...readSpeech } = part.speech;
|
|
1181
|
+
const { duration_ms, rest } = splitDurationMs(readSpeech);
|
|
1210
1182
|
out[partId] = { speech: {
|
|
1211
1183
|
...rest,
|
|
1212
1184
|
media_duration_ms: rest.media_duration_ms ?? duration_ms
|
|
@@ -1296,8 +1268,8 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1296
1268
|
};
|
|
1297
1269
|
}
|
|
1298
1270
|
/**
|
|
1299
|
-
* Project
|
|
1300
|
-
*
|
|
1271
|
+
* Project `VideoDocument` into `VideoDraftContent`, solving each item's
|
|
1272
|
+
* absolute position, the `part_aggregations`, and
|
|
1301
1273
|
* the total duration via the timeline-core cascade.
|
|
1302
1274
|
*
|
|
1303
1275
|
* The read-view is a compatibility contract, and the IDL declares
|
|
@@ -1311,33 +1283,26 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1311
1283
|
* These two are defaultable because the projection knows their values on its own:
|
|
1312
1284
|
* absent `unit_time_ms` means "no display granularity was ever stated" and absent
|
|
1313
1285
|
* `is_hidden` means "this track was never hidden". `version` is NOT defaultable
|
|
1314
|
-
* here —
|
|
1315
|
-
*
|
|
1286
|
+
* here — it belongs to Director assembly using the server envelope update_seq.
|
|
1287
|
+
* Neither business metadata nor a revision counter belongs in content projection.
|
|
1316
1288
|
*/
|
|
1317
|
-
function fromVideoDocument(document) {
|
|
1289
|
+
function fromVideoDocument(document, options = {}) {
|
|
1318
1290
|
assertValidVideoDocument(document);
|
|
1319
|
-
const view = solveVideoDocument(document);
|
|
1291
|
+
const view = solveVideoDocument(document, options);
|
|
1320
1292
|
const tracks = document.tracks ?? [];
|
|
1321
1293
|
const mainTrack = tracks.find((t) => t.parts_kind === "video_clip");
|
|
1322
1294
|
const aboveTracks = tracks.filter((t) => t.parts_kind === "caption");
|
|
1323
1295
|
const belowTracks = tracks.filter((t) => t.parts_kind === "speech" || t.parts_kind === "bgm");
|
|
1324
1296
|
return {
|
|
1325
|
-
id: document.meta.draft_id,
|
|
1326
|
-
project_id: document.meta.project_id,
|
|
1327
|
-
owner_id: document.meta.owner_id,
|
|
1328
|
-
thumbnail_storage_key: document.meta.thumbnail_storage_key,
|
|
1329
1297
|
timeline: {
|
|
1330
1298
|
duration_ms: view.durationMs,
|
|
1331
1299
|
unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
|
|
1332
1300
|
},
|
|
1333
|
-
video_creation_settings: clone$1(document.meta.video_creation_settings),
|
|
1334
|
-
chat_session_id: document.meta.chat_session_id,
|
|
1335
1301
|
main_track: draftTrack(mainTrack, view.absByPartId),
|
|
1336
1302
|
above_main_tracks: aboveTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1337
1303
|
below_main_tracks: belowTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1338
1304
|
part_aggregations: view.aggregations,
|
|
1339
|
-
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1340
|
-
version: document.meta.version
|
|
1305
|
+
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1341
1306
|
};
|
|
1342
1307
|
}
|
|
1343
1308
|
/**
|
|
@@ -1444,7 +1409,7 @@ function clone$1(value) {
|
|
|
1444
1409
|
//#region src/document/initial-document.ts
|
|
1445
1410
|
/**
|
|
1446
1411
|
* Build the `VideoDocument` a newly created project starts from: the caller's
|
|
1447
|
-
*
|
|
1412
|
+
* no parts and one empty track per lane. Business facts remain in Director.
|
|
1448
1413
|
*
|
|
1449
1414
|
* The empty lane tracks are the reason this function exists rather than callers
|
|
1450
1415
|
* assembling a document inline. Lane tracks are otherwise minted lazily by the
|
|
@@ -1465,18 +1430,8 @@ function clone$1(value) {
|
|
|
1465
1430
|
* supplies, and total duration is derived on read (RFC 02 §6), so a new document
|
|
1466
1431
|
* has no timeline fact to state.
|
|
1467
1432
|
*/
|
|
1468
|
-
function buildInitialVideoDocument(
|
|
1433
|
+
function buildInitialVideoDocument() {
|
|
1469
1434
|
return {
|
|
1470
|
-
meta: {
|
|
1471
|
-
schema_version: VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
1472
|
-
draft_id: facts.draftId,
|
|
1473
|
-
project_id: facts.projectId,
|
|
1474
|
-
owner_id: facts.ownerId,
|
|
1475
|
-
thumbnail_storage_key: void 0,
|
|
1476
|
-
chat_session_id: facts.chatSessionId,
|
|
1477
|
-
video_creation_settings: facts.videoCreationSettings,
|
|
1478
|
-
version: void 0
|
|
1479
|
-
},
|
|
1480
1435
|
timeline: void 0,
|
|
1481
1436
|
tracks: emptyLaneTracks(),
|
|
1482
1437
|
part_library: {}
|
|
@@ -1493,36 +1448,101 @@ function emptyLaneTracks() {
|
|
|
1493
1448
|
}
|
|
1494
1449
|
//#endregion
|
|
1495
1450
|
//#region ../medeo-dsl/src/entities.ts
|
|
1451
|
+
/**
|
|
1452
|
+
* What every known entity kind declares.
|
|
1453
|
+
*
|
|
1454
|
+
* `kinds` is a multi-declaration: every entry holds at the same time, and each
|
|
1455
|
+
* is a property shared with other entity kinds — what the object *is* comes
|
|
1456
|
+
* from `entityKind` and is never repeated here. The order carries no priority,
|
|
1457
|
+
* endpoint role, or edit sequence — an operation asks whether the entity
|
|
1458
|
+
* declares the kind it needs, never what its "main type" is. An entity kind
|
|
1459
|
+
* that shares nothing declares nothing.
|
|
1460
|
+
*
|
|
1461
|
+
* `Caption` declares `Sequence` on purpose: it owns the display timing of the
|
|
1462
|
+
* one AudioScript segment it shows, which is what admits it into a Clip.
|
|
1463
|
+
* `Voice` declares no `Sequence` — it is a timbre identity, not playable
|
|
1464
|
+
* content; rendered speech is a `voiceover` entity.
|
|
1465
|
+
*
|
|
1466
|
+
* `MediaFile` says only that an entity of that kind may be bound to stored bytes,
|
|
1467
|
+
* and asks nothing of its payload. Which bytes, and which way the content
|
|
1468
|
+
* flowed, is the `from-asset` / `to-asset` Relation's answer alone.
|
|
1469
|
+
*/
|
|
1470
|
+
const ENTITY_KINDS = Object.freeze({
|
|
1471
|
+
axvideo: Object.freeze([
|
|
1472
|
+
"Container",
|
|
1473
|
+
"Sequence",
|
|
1474
|
+
"Visual",
|
|
1475
|
+
"Audible"
|
|
1476
|
+
]),
|
|
1477
|
+
timeline: Object.freeze(["Container"]),
|
|
1478
|
+
track: Object.freeze(["Container"]),
|
|
1479
|
+
clip: Object.freeze(["Container"]),
|
|
1480
|
+
asset: Object.freeze([]),
|
|
1481
|
+
video: Object.freeze([
|
|
1482
|
+
"Sequence",
|
|
1483
|
+
"Visual",
|
|
1484
|
+
"Audible",
|
|
1485
|
+
"MediaFile"
|
|
1486
|
+
]),
|
|
1487
|
+
audio: Object.freeze([
|
|
1488
|
+
"Sequence",
|
|
1489
|
+
"Audible",
|
|
1490
|
+
"MediaFile"
|
|
1491
|
+
]),
|
|
1492
|
+
voiceover: Object.freeze([
|
|
1493
|
+
"Sequence",
|
|
1494
|
+
"Audible",
|
|
1495
|
+
"MediaFile"
|
|
1496
|
+
]),
|
|
1497
|
+
image: Object.freeze([
|
|
1498
|
+
"Sequence",
|
|
1499
|
+
"Visual",
|
|
1500
|
+
"MediaFile"
|
|
1501
|
+
]),
|
|
1502
|
+
voice: Object.freeze(["Container", "IPBound"]),
|
|
1503
|
+
"sequence-marker": Object.freeze([]),
|
|
1504
|
+
viewport: Object.freeze([]),
|
|
1505
|
+
"audio-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1506
|
+
"phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1507
|
+
caption: Object.freeze([
|
|
1508
|
+
"SegmentText",
|
|
1509
|
+
"Sequence",
|
|
1510
|
+
"MediaFile"
|
|
1511
|
+
])
|
|
1512
|
+
});
|
|
1513
|
+
/** What a known kind declares; extension kinds declare nothing through this table. */
|
|
1514
|
+
function kindsOf(entityKind) {
|
|
1515
|
+
return isKnownEntityKind(entityKind) ? ENTITY_KINDS[entityKind] : [];
|
|
1516
|
+
}
|
|
1517
|
+
function declaresKind(entityKind, kind) {
|
|
1518
|
+
return kindsOf(entityKind).includes(kind);
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Sequence admission is a declaration, not a shape: a known kind is admitted
|
|
1522
|
+
* because its `kinds` say so, and a malformed payload is a separate, louder
|
|
1523
|
+
* failure than a silently non-Sequence entity. Extension kinds declare nothing
|
|
1524
|
+
* through the table and stay structurally detected.
|
|
1525
|
+
*/
|
|
1496
1526
|
function hasSequence(entity) {
|
|
1497
|
-
if (
|
|
1527
|
+
if (isReservedEntityKind(entity.entityKind)) return false;
|
|
1528
|
+
if (isKnownEntityKind(entity.entityKind)) return declaresKind(entity.entityKind, "Sequence");
|
|
1498
1529
|
return isSequenceFields(entity);
|
|
1499
1530
|
}
|
|
1500
1531
|
function isSequenceFields(value) {
|
|
1501
1532
|
if (!isRecord$1(value)) return false;
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
if (extent.kind === "bounded" && (!("end" in extent) || extent.end === void 0)) return false;
|
|
1505
|
-
if (extent.kind === "unbounded" && "end" in extent) return false;
|
|
1506
|
-
if (extent.kind !== "bounded" && extent.kind !== "unbounded") return false;
|
|
1507
|
-
if (value.sampling !== "native" && value.sampling !== "constant" && value.sampling !== "derived") return false;
|
|
1508
|
-
return "coordinateSpace" in value && value.coordinateSpace !== void 0;
|
|
1509
|
-
}
|
|
1510
|
-
function isKnownSequenceKind(kind) {
|
|
1511
|
-
return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
|
|
1533
|
+
const duration = value.durationMs;
|
|
1534
|
+
return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
|
|
1512
1535
|
}
|
|
1513
1536
|
function isMediaAssetVariantKind(kind) {
|
|
1514
|
-
return kind === "video" || kind === "image" || kind === "audio" || kind === "
|
|
1537
|
+
return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover";
|
|
1515
1538
|
}
|
|
1516
1539
|
function isKnownEntityKind(kind) {
|
|
1517
|
-
return
|
|
1540
|
+
return Object.hasOwn(ENTITY_KINDS, kind);
|
|
1518
1541
|
}
|
|
1519
1542
|
function isReservedEntityKind(kind) {
|
|
1520
1543
|
const normalized = kind.toLowerCase().replaceAll("-", "").replaceAll("_", "");
|
|
1521
1544
|
return normalized === "speech" || normalized === "videodocument";
|
|
1522
1545
|
}
|
|
1523
|
-
function isKnownNonSequenceKind(kind) {
|
|
1524
|
-
return kind === "timeline" || kind === "track" || kind === "clip" || kind === "asset" || kind === "sequence-marker" || kind === "viewport" || kind === "audio-script" || kind === "phonetic-script";
|
|
1525
|
-
}
|
|
1526
1546
|
function isRecord$1(value) {
|
|
1527
1547
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
1528
1548
|
}
|
|
@@ -1706,14 +1726,17 @@ function assembleCaptionContent(rows, captionEntityId) {
|
|
|
1706
1726
|
requireEntityKind(rows, captionEntityId, "caption");
|
|
1707
1727
|
const caption = assembleEntityContent(rows, captionEntityId);
|
|
1708
1728
|
const script = findComposedAudioScript(rows, captionEntityId, "caption");
|
|
1709
|
-
const
|
|
1729
|
+
const selection = captionSelection(caption);
|
|
1730
|
+
const composed = {
|
|
1710
1731
|
...script,
|
|
1711
1732
|
payload: caption.payload
|
|
1712
|
-
}
|
|
1733
|
+
};
|
|
1734
|
+
const segment = selectAudioScriptSegment(composed, selection);
|
|
1713
1735
|
return {
|
|
1714
1736
|
caption,
|
|
1715
1737
|
audioScript: script,
|
|
1716
1738
|
segments: [segment],
|
|
1739
|
+
segmentIndex: scriptSegments(composed).findIndex((entry) => entry.segmentId === selection.segmentId),
|
|
1717
1740
|
text: segment.text
|
|
1718
1741
|
};
|
|
1719
1742
|
}
|
|
@@ -1781,18 +1804,7 @@ function entityToRow(entity) {
|
|
|
1781
1804
|
}
|
|
1782
1805
|
//#endregion
|
|
1783
1806
|
//#region ../medeo-dsl/src/invariants.ts
|
|
1784
|
-
/**
|
|
1785
|
-
* Media variants own their Asset identity directly: a `physical-asset`
|
|
1786
|
-
* Relation may not bind them. Caption may associate with a physical text Asset
|
|
1787
|
-
* independently of its AudioScript composition.
|
|
1788
|
-
*/
|
|
1789
|
-
function validateMediaAssetIdentity(entity, index) {
|
|
1790
|
-
return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
|
|
1791
|
-
code: "media_physical_asset_forbidden",
|
|
1792
|
-
entityId: entity.entityId,
|
|
1793
|
-
message: `Media entity "${entity.entityId}" owns its Asset identity; remove its physical-asset Relation`
|
|
1794
|
-
}));
|
|
1795
|
-
}
|
|
1807
|
+
/** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
|
|
1796
1808
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
1797
1809
|
function validateEntityRelationSet(entityRefs, index, options) {
|
|
1798
1810
|
const { entities, issues } = collectRelatedEntities(entityRefs, index);
|
|
@@ -1814,7 +1826,6 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
1814
1826
|
const current = entity.current();
|
|
1815
1827
|
const entityIssues = validateEntity(entity, entityIds);
|
|
1816
1828
|
issues.push(...entityIssues);
|
|
1817
|
-
if (isMediaAssetVariantKind(current.entityKind)) issues.push(...validateMediaAssetIdentity(entity, index));
|
|
1818
1829
|
if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
|
|
1819
1830
|
if (current.entityKind === "sequence-marker") {
|
|
1820
1831
|
const marker = entity;
|
|
@@ -2039,24 +2050,25 @@ function validateMarkerSourceBounds(marker, index, compare) {
|
|
|
2039
2050
|
const content = contentEdges[0]?.other(marker)?.deref()?.current();
|
|
2040
2051
|
if (content == null || !hasSequence(content)) return [];
|
|
2041
2052
|
const sourceRange = marker.current().sourceRange;
|
|
2042
|
-
const
|
|
2043
|
-
const
|
|
2044
|
-
if (!Number.isNaN(
|
|
2053
|
+
const startVsOrigin = compare(sourceRange.start, 0);
|
|
2054
|
+
const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
|
|
2055
|
+
if (!Number.isNaN(startVsOrigin) && startVsOrigin >= 0 && (endVsDuration == null || !Number.isNaN(endVsDuration) && endVsDuration <= 0)) return [];
|
|
2045
2056
|
return [{
|
|
2046
2057
|
code: "marker_source_out_of_bounds",
|
|
2047
2058
|
entityId: marker.entityId,
|
|
2048
|
-
message: `Sequence Marker sourceRange must stay within content "${content.entityId}"
|
|
2059
|
+
message: `Sequence Marker sourceRange must stay within content "${content.entityId}" duration`
|
|
2049
2060
|
}];
|
|
2050
2061
|
}
|
|
2051
2062
|
function validateSequenceComposition(entity) {
|
|
2052
2063
|
const current = entity.current();
|
|
2053
|
-
const expected =
|
|
2054
|
-
if (expected
|
|
2055
|
-
|
|
2064
|
+
const expected = expectedDurationShape(current.entityKind);
|
|
2065
|
+
if (expected === void 0) return [];
|
|
2066
|
+
const duration = current.durationMs;
|
|
2067
|
+
if ((duration === null ? "none" : isPositiveInteger(duration) ? "own" : "invalid") === expected) return [];
|
|
2056
2068
|
return [{
|
|
2057
2069
|
code: "invalid_sequence_composition",
|
|
2058
2070
|
entityId: current.entityId,
|
|
2059
|
-
message: `${current.entityKind} must
|
|
2071
|
+
message: expected === "own" ? `${current.entityKind} must state its own durationMs in whole milliseconds` : `${current.entityKind} has no intrinsic length; durationMs must be null`
|
|
2060
2072
|
}];
|
|
2061
2073
|
}
|
|
2062
2074
|
function validateEntity(entity, entityIds = /* @__PURE__ */ new Set()) {
|
|
@@ -2087,6 +2099,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
2087
2099
|
if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
|
|
2088
2100
|
if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
2089
2101
|
for (const key of [
|
|
2102
|
+
"durationMs",
|
|
2090
2103
|
"extent",
|
|
2091
2104
|
"sampling",
|
|
2092
2105
|
"coordinateSpace",
|
|
@@ -2095,7 +2108,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
2095
2108
|
"duration",
|
|
2096
2109
|
"startMs",
|
|
2097
2110
|
"endMs"
|
|
2098
|
-
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an
|
|
2111
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
|
|
2099
2112
|
}
|
|
2100
2113
|
switch (entity.entityKind) {
|
|
2101
2114
|
case "track":
|
|
@@ -2105,19 +2118,25 @@ function validateKnownEntityPayload(entity) {
|
|
|
2105
2118
|
break;
|
|
2106
2119
|
case "video":
|
|
2107
2120
|
case "audio":
|
|
2108
|
-
|
|
2121
|
+
validateDurationPayload(value, "own", problems);
|
|
2122
|
+
break;
|
|
2109
2123
|
case "caption":
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2124
|
+
validateDurationPayload(value, "own", problems);
|
|
2125
|
+
validateCaptionPayload(value, problems);
|
|
2126
|
+
break;
|
|
2127
|
+
case "voice":
|
|
2128
|
+
validateVoicePayload(value, problems);
|
|
2114
2129
|
break;
|
|
2115
2130
|
case "image":
|
|
2116
|
-
|
|
2117
|
-
validateMediaAssetFields(value, problems);
|
|
2131
|
+
validateDurationPayload(value, "none", problems);
|
|
2118
2132
|
break;
|
|
2119
2133
|
case "axvideo":
|
|
2120
|
-
|
|
2134
|
+
for (const retired of [
|
|
2135
|
+
"extent",
|
|
2136
|
+
"sampling",
|
|
2137
|
+
"coordinateSpace",
|
|
2138
|
+
"durationMs"
|
|
2139
|
+
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
|
|
2121
2140
|
break;
|
|
2122
2141
|
case "sequence-marker":
|
|
2123
2142
|
validateMarkerPayload(value, problems);
|
|
@@ -2142,17 +2161,27 @@ function validateKnownEntityPayload(entity) {
|
|
|
2142
2161
|
}
|
|
2143
2162
|
return problems;
|
|
2144
2163
|
}
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2164
|
+
/**
|
|
2165
|
+
* Content states its own length and nothing else. The retired `extent` carried
|
|
2166
|
+
* a start as well, which had no meaning of its own and drifted into holding a
|
|
2167
|
+
* position; rejecting it keeps that mistake unwritable.
|
|
2168
|
+
*/
|
|
2169
|
+
function validateDurationPayload(value, expected, problems) {
|
|
2170
|
+
for (const retired of [
|
|
2171
|
+
"extent",
|
|
2172
|
+
"sampling",
|
|
2173
|
+
"coordinateSpace"
|
|
2174
|
+
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is retired; state durationMs instead`);
|
|
2175
|
+
if (!Object.hasOwn(value, "durationMs")) {
|
|
2176
|
+
problems.push("durationMs is required");
|
|
2177
|
+
return;
|
|
2153
2178
|
}
|
|
2154
|
-
|
|
2155
|
-
if (
|
|
2179
|
+
const duration = value.durationMs;
|
|
2180
|
+
if (expected === "none") {
|
|
2181
|
+
if (duration !== null) problems.push("durationMs must be null; this content has no intrinsic length");
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
|
|
2156
2185
|
}
|
|
2157
2186
|
function validateMarkerPayload(value, problems) {
|
|
2158
2187
|
validateRange(value.sourceRange, "sourceRange", true, problems);
|
|
@@ -2188,40 +2217,38 @@ function validateSegmentRanges(value, problems) {
|
|
|
2188
2217
|
if (typeof entry.startMs === "number" && Number.isFinite(entry.startMs) && typeof entry.endMs === "number" && Number.isFinite(entry.endMs) && entry.startMs > entry.endMs) problems.push(`segmentRanges[${index}].startMs must not exceed segmentRanges[${index}].endMs`);
|
|
2189
2218
|
}
|
|
2190
2219
|
}
|
|
2220
|
+
/** The Asset entity is the locator; it states where the bytes live and nothing else. */
|
|
2191
2221
|
function validateAssetPayload(value, problems) {
|
|
2192
|
-
|
|
2193
|
-
|
|
2222
|
+
if (value.system !== "memota" && value.system !== "memota-speech") problems.push("system must be \"memota\" or \"memota-speech\"");
|
|
2223
|
+
if (typeof value.key !== "string" || value.key.trim() === "") problems.push("key must be a non-empty string");
|
|
2194
2224
|
if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
|
|
2195
|
-
}
|
|
2196
|
-
/**
|
|
2197
|
-
* Media variants own their Asset locator directly: `external` is required and
|
|
2198
|
-
* textual domain content is owned by AudioScript; the physical-location
|
|
2199
|
-
* vocabularies cannot mix.
|
|
2200
|
-
*/
|
|
2201
|
-
function validateMediaAssetFields(value, problems) {
|
|
2202
|
-
if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
|
|
2203
|
-
else validateExternalLocator(value, problems);
|
|
2204
2225
|
validateStorageKey(value, problems);
|
|
2205
|
-
if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
|
|
2206
|
-
}
|
|
2207
|
-
function validateExternalLocator(value, problems) {
|
|
2208
|
-
const external = value.external;
|
|
2209
|
-
if (external === void 0) return;
|
|
2210
|
-
if (!isRecord(external)) {
|
|
2211
|
-
problems.push("external must be an object when present");
|
|
2212
|
-
return;
|
|
2213
|
-
}
|
|
2214
|
-
if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
|
|
2215
|
-
if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
|
|
2216
2226
|
}
|
|
2217
2227
|
function validateStorageKey(value, problems) {
|
|
2218
2228
|
if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
|
|
2219
2229
|
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Voice is a timbre identity, not playable content: it owns the voice-library
|
|
2232
|
+
* descriptor and nothing that would make it look like media or a Sequence. The
|
|
2233
|
+
* rendered voiceover is an `audio` entity bound by a `voice-timbre` Relation.
|
|
2234
|
+
*/
|
|
2220
2235
|
function validateVoicePayload(value, problems) {
|
|
2236
|
+
for (const key of [
|
|
2237
|
+
"durationMs",
|
|
2238
|
+
"extent",
|
|
2239
|
+
"sampling",
|
|
2240
|
+
"coordinateSpace",
|
|
2241
|
+
"system",
|
|
2242
|
+
"key",
|
|
2243
|
+
"storageKey"
|
|
2244
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} belongs to the rendered Audio, not to the Voice identity`);
|
|
2221
2245
|
const voice = value.voice;
|
|
2222
|
-
if (voice === void 0)
|
|
2246
|
+
if (voice === void 0) {
|
|
2247
|
+
problems.push("voice is required; a Voice entity is its voice-library identity");
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2223
2250
|
if (!isRecord(voice)) {
|
|
2224
|
-
problems.push("voice must be an object
|
|
2251
|
+
problems.push("voice must be an object");
|
|
2225
2252
|
return;
|
|
2226
2253
|
}
|
|
2227
2254
|
if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
|
|
@@ -2229,7 +2256,6 @@ function validateVoicePayload(value, problems) {
|
|
|
2229
2256
|
if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
|
|
2230
2257
|
}
|
|
2231
2258
|
function validateCaptionPayload(value, problems) {
|
|
2232
|
-
validateExternalLocator(value, problems);
|
|
2233
2259
|
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
2234
2260
|
if (Object.hasOwn(value, "selections")) problems.push("selections is forbidden; Caption requires one selection object");
|
|
2235
2261
|
const selection = value.selection;
|
|
@@ -2353,15 +2379,13 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2353
2379
|
if ([
|
|
2354
2380
|
"video",
|
|
2355
2381
|
"audio",
|
|
2356
|
-
"voice",
|
|
2357
2382
|
"image",
|
|
2358
2383
|
"caption",
|
|
2359
2384
|
"axvideo"
|
|
2360
|
-
].includes(entityKind) && /^
|
|
2385
|
+
].includes(entityKind) && /^durationMs$/.test(path)) return true;
|
|
2361
2386
|
if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
|
|
2362
2387
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
2363
|
-
if (entityKind === "asset" && /^(?:
|
|
2364
|
-
if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
|
|
2388
|
+
if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
|
|
2365
2389
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
2366
2390
|
if (entityKind === "caption" && (path.startsWith("style.") || path === "selection.segmentId")) return true;
|
|
2367
2391
|
if ([
|
|
@@ -2416,19 +2440,16 @@ function collectRelatedEntities(entityRefs, index) {
|
|
|
2416
2440
|
issues
|
|
2417
2441
|
};
|
|
2418
2442
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
if (kind === "
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
extent: "bounded",
|
|
2430
|
-
sampling: "derived"
|
|
2431
|
-
};
|
|
2443
|
+
/**
|
|
2444
|
+
* What a kind must state about its own length. An AXVideo is absent here on
|
|
2445
|
+
* purpose: its Timeline decides its length, so it stores none.
|
|
2446
|
+
*/
|
|
2447
|
+
function expectedDurationShape(kind) {
|
|
2448
|
+
if (kind === "video" || kind === "audio" || kind === "caption") return "own";
|
|
2449
|
+
if (kind === "image") return "none";
|
|
2450
|
+
}
|
|
2451
|
+
function isPositiveInteger(value) {
|
|
2452
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2432
2453
|
}
|
|
2433
2454
|
function ofKind(relations, kind) {
|
|
2434
2455
|
return relations.filter((relation) => relation.kind === kind);
|
|
@@ -2445,9 +2466,38 @@ const markerContentRelationSpec = Object.freeze({
|
|
|
2445
2466
|
validateEndpoints: (endpoints) => hasMarkerAndSequence(endpoints),
|
|
2446
2467
|
validateMetadata: isEmptyMetadata
|
|
2447
2468
|
});
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2469
|
+
/**
|
|
2470
|
+
* `from-asset(entity, asset)` — the entity was made from that stored resource.
|
|
2471
|
+
*
|
|
2472
|
+
* Eligibility is declared: the non-Asset endpoint must carry the `MediaFile` kind,
|
|
2473
|
+
* which says an entity of that kind can be bound to stored bytes and asks
|
|
2474
|
+
* nothing of its payload. The bytes stay named in exactly one place, so no
|
|
2475
|
+
* entity copies the locator into its own row and one Asset can back several
|
|
2476
|
+
* entities.
|
|
2477
|
+
*/
|
|
2478
|
+
const fromAssetRelationSpec = Object.freeze({
|
|
2479
|
+
kind: "from-asset",
|
|
2480
|
+
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2481
|
+
validateMetadata: isAssetBinding
|
|
2482
|
+
});
|
|
2483
|
+
/**
|
|
2484
|
+
* `to-asset(entity, asset)` — the entity's content was written out to that
|
|
2485
|
+
* stored resource.
|
|
2486
|
+
*
|
|
2487
|
+
* The mirror of `from-asset`, and deliberately a separate relation kind rather
|
|
2488
|
+
* than a second declared kind: eligibility is the same either way, but which
|
|
2489
|
+
* way the content flowed is what says whether an entity may still be edited.
|
|
2490
|
+
* An entity reached by `from-asset` was produced from bytes it does not own,
|
|
2491
|
+
* so changing it would contradict them. An entity with a `to-asset` owns its
|
|
2492
|
+
* content and the resource is a snapshot of it, which is what goes stale and
|
|
2493
|
+
* gets written again. A transcript that is later edited carries both at once,
|
|
2494
|
+
* and only the distinct relation kinds keep them apart.
|
|
2495
|
+
*
|
|
2496
|
+
* Writing a snapshot is host assembly; nothing a script authors creates one.
|
|
2497
|
+
*/
|
|
2498
|
+
const toAssetRelationSpec = Object.freeze({
|
|
2499
|
+
kind: "to-asset",
|
|
2500
|
+
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2451
2501
|
validateMetadata: isAssetBinding
|
|
2452
2502
|
});
|
|
2453
2503
|
/** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
|
|
@@ -2458,7 +2508,7 @@ const generatedRelationSpec = Object.freeze({
|
|
|
2458
2508
|
});
|
|
2459
2509
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
2460
2510
|
kind: "caption-alignment",
|
|
2461
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["
|
|
2511
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover"])),
|
|
2462
2512
|
validateMetadata: isCaptionAlignmentMetadata
|
|
2463
2513
|
});
|
|
2464
2514
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
@@ -2467,10 +2517,27 @@ const clipAnchorRelationSpec = Object.freeze({
|
|
|
2467
2517
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
2468
2518
|
validateMetadata: isEmptyMetadata
|
|
2469
2519
|
});
|
|
2470
|
-
/**
|
|
2520
|
+
/**
|
|
2521
|
+
* The Voiceover and the PhoneticScript it was spoken from; kinds determine
|
|
2522
|
+
* roles regardless of endpoint positions.
|
|
2523
|
+
*
|
|
2524
|
+
* Every Voiceover must carry one: which words were said, and how, is the only
|
|
2525
|
+
* thing that makes speech reproducible, and a Voiceover that cannot answer it
|
|
2526
|
+
* is refused when its plan is built.
|
|
2527
|
+
*/
|
|
2471
2528
|
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
2472
2529
|
kind: "phonetic-script-render",
|
|
2473
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["
|
|
2530
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["phonetic-script"])),
|
|
2531
|
+
validateMetadata: isEmptyMetadata
|
|
2532
|
+
});
|
|
2533
|
+
/**
|
|
2534
|
+
* The timbre identity a Voiceover was synthesized with. Voice stays an
|
|
2535
|
+
* identity: it never carries the audio itself, so the link between the two is a
|
|
2536
|
+
* Relation rather than one shared entity, and several takes can name one voice.
|
|
2537
|
+
*/
|
|
2538
|
+
const voiceTimbreRelationSpec = Object.freeze({
|
|
2539
|
+
kind: "voice-timbre",
|
|
2540
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["voice"])),
|
|
2474
2541
|
validateMetadata: isEmptyMetadata
|
|
2475
2542
|
});
|
|
2476
2543
|
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
@@ -2478,8 +2545,8 @@ const audioScriptSourceRelationSpec = Object.freeze({
|
|
|
2478
2545
|
kind: "audio-script-source",
|
|
2479
2546
|
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
2480
2547
|
"audio",
|
|
2481
|
-
"
|
|
2482
|
-
"
|
|
2548
|
+
"voiceover",
|
|
2549
|
+
"video"
|
|
2483
2550
|
])),
|
|
2484
2551
|
validateMetadata: isEmptyMetadata
|
|
2485
2552
|
});
|
|
@@ -2502,11 +2569,13 @@ const builtInRelationSpecs = Object.freeze([
|
|
|
2502
2569
|
markerContentRelationSpec,
|
|
2503
2570
|
axVideoMarkerRelationSpec,
|
|
2504
2571
|
markerTimelineRelationSpec,
|
|
2505
|
-
|
|
2572
|
+
fromAssetRelationSpec,
|
|
2573
|
+
toAssetRelationSpec,
|
|
2506
2574
|
generatedRelationSpec,
|
|
2507
2575
|
captionAlignmentRelationSpec,
|
|
2508
2576
|
clipAnchorRelationSpec,
|
|
2509
2577
|
phoneticScriptRenderRelationSpec,
|
|
2578
|
+
voiceTimbreRelationSpec,
|
|
2510
2579
|
audioScriptSourceRelationSpec,
|
|
2511
2580
|
audioScriptMarkerRelationSpec
|
|
2512
2581
|
]);
|
|
@@ -2530,13 +2599,15 @@ function hasMarkerAndSequence(endpoints) {
|
|
|
2530
2599
|
const second = endpoints[1].current();
|
|
2531
2600
|
return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
|
|
2532
2601
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
const
|
|
2536
|
-
|
|
2602
|
+
/** Exactly one end is the Asset, so the other end is unambiguously the bound entity. */
|
|
2603
|
+
function hasExactlyOneAsset(endpoints) {
|
|
2604
|
+
const kinds = endpoints.map((endpoint) => endpoint.current().entityKind);
|
|
2605
|
+
const assets = kinds.map((kind) => kind === "asset");
|
|
2606
|
+
if (assets[0] === assets[1]) return false;
|
|
2607
|
+
return declaresKind(assets[0] ? kinds[1] : kinds[0], "MediaFile");
|
|
2537
2608
|
}
|
|
2538
2609
|
function isGeneratedMedia(entity) {
|
|
2539
|
-
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "
|
|
2610
|
+
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voiceover";
|
|
2540
2611
|
}
|
|
2541
2612
|
function isEmptyMetadata(value) {
|
|
2542
2613
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -2725,7 +2796,6 @@ var InvalidEntityRelationRowsError = class extends Error {
|
|
|
2725
2796
|
this.name = "InvalidEntityRelationRowsError";
|
|
2726
2797
|
}
|
|
2727
2798
|
};
|
|
2728
|
-
/** Decodes database rows into flat entities and validates the complete relation set. */
|
|
2729
2799
|
function decodeEntityRelationRows(rows, options) {
|
|
2730
2800
|
const issues = [];
|
|
2731
2801
|
const entitiesById = /* @__PURE__ */ new Map();
|
|
@@ -2966,9 +3036,7 @@ function resolveGraphLayout(graph) {
|
|
|
2966
3036
|
if (clip.entityKind !== "clip" || graph.adjacent(clip.entityId, "track-clip").length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
|
|
2967
3037
|
const marker = graph.one(clip.entityId, "clip-marker", "sequence-marker");
|
|
2968
3038
|
const content = graph.one(marker.entityId, "marker-content");
|
|
2969
|
-
|
|
2970
|
-
const space = content.payload.coordinateSpace;
|
|
2971
|
-
if (space !== "ms" && space !== "milliseconds" && (!object(space) || space.unit !== "ms" && space.unit !== "milliseconds")) fail(`media ${content.entityId} must state millisecond coordinates for this reader`);
|
|
3039
|
+
assertTrackContent(graph, typedRole, content);
|
|
2972
3040
|
const source = range(marker.payload.sourceRange, "sourceRange");
|
|
2973
3041
|
const durationFact = marker.payload.duration;
|
|
2974
3042
|
if (!object(durationFact)) fail(`Marker ${marker.entityId} requires duration`);
|
|
@@ -3045,7 +3113,7 @@ function resolveGraphLayout(graph) {
|
|
|
3045
3113
|
let start = fact.absoluteStart ?? 0;
|
|
3046
3114
|
if (fact.mode === "anchored") {
|
|
3047
3115
|
const host = resolve(fact.anchorId);
|
|
3048
|
-
if (fact.role === "speech" && !["video_clip", "bgm"].includes(facts.get(host.clipEntityId).role)) fail("An anchored
|
|
3116
|
+
if (fact.role === "speech" && !["video_clip", "bgm"].includes(facts.get(host.clipEntityId).role)) fail("An anchored voiceover must use a visual or BGM Clip as its host");
|
|
3049
3117
|
start = host.start + fact.anchorOffset;
|
|
3050
3118
|
} else if (fact.predecessor !== void 0) {
|
|
3051
3119
|
const prior = resolve(fact.predecessor);
|
|
@@ -3087,6 +3155,32 @@ function resolveGraphLayout(graph) {
|
|
|
3087
3155
|
}))
|
|
3088
3156
|
};
|
|
3089
3157
|
}
|
|
3158
|
+
function assertTrackContent(graph, role, content) {
|
|
3159
|
+
if (role === "speech") {
|
|
3160
|
+
assertSpeechContent(graph, content);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
if (!(role === "video_clip" ? ["image", "video"] : role === "caption" ? ["caption"] : ["audio"]).includes(content.entityKind)) fail(`unsupported compatibility content kind ${content.entityKind} on ${role} Track`);
|
|
3164
|
+
}
|
|
3165
|
+
function assertSpeechContent(graph, content) {
|
|
3166
|
+
const assetLinks = graph.rows.relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3167
|
+
if (assetLinks.length !== 1 || assetLinks[0].endpoint0EntityId !== content.entityId) fail("Speech content requires one ordered from-asset binding");
|
|
3168
|
+
const asset = graph.entity(assetLinks[0].endpoint1EntityId, "asset");
|
|
3169
|
+
if (content.entityKind === "voiceover") {
|
|
3170
|
+
const renders = graph.rows.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3171
|
+
if (renders.length !== 1 || renders[0].endpoint0EntityId !== content.entityId || graph.entity(renders[0].endpoint1EntityId).entityKind !== "phonetic-script") fail("Synthesized Speech Voiceover requires one ordered PhoneticScript render");
|
|
3172
|
+
if (asset.payload.system !== "memota-speech") fail("Synthesized Speech Voiceover requires an explicit memota-speech key");
|
|
3173
|
+
return;
|
|
3174
|
+
}
|
|
3175
|
+
if (content.entityKind === "audio") {
|
|
3176
|
+
const sources = graph.rows.relations.filter((relation) => relation.relationKind === "audio-script-source" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3177
|
+
if (sources.length > 1) fail("Recorded speech has an ambiguous AudioScript source receipt");
|
|
3178
|
+
if (sources.length === 1 && (sources[0].endpoint1EntityId !== content.entityId || graph.entity(sources[0].endpoint0EntityId).entityKind !== "audio-script")) fail("Recorded Speech AudioScript source must use ordered endpoints");
|
|
3179
|
+
if (asset.payload.system !== "memota") fail("Recorded Speech Audio requires an explicit memota key");
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
fail(`unsupported compatibility content kind ${content.entityKind} on speech Track`);
|
|
3183
|
+
}
|
|
3090
3184
|
//#endregion
|
|
3091
3185
|
//#region src/document/entity-projection.ts
|
|
3092
3186
|
/**
|
|
@@ -3094,7 +3188,7 @@ function resolveGraphLayout(graph) {
|
|
|
3094
3188
|
* consulted as an editing fact. Peer IDs appear only in this derived read
|
|
3095
3189
|
* contract, reconstructed from relations; they are not stored in payloads.
|
|
3096
3190
|
*/
|
|
3097
|
-
function projectEntityTimeline(rows
|
|
3191
|
+
function projectEntityTimeline(rows) {
|
|
3098
3192
|
const graph = new EntityProjectionGraph(rows);
|
|
3099
3193
|
const layout = resolveGraphLayout(graph);
|
|
3100
3194
|
for (const track of layout.tracks) {
|
|
@@ -3128,17 +3222,21 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3128
3222
|
} else if (track.role === "speech") {
|
|
3129
3223
|
const asset = mediaAssetFacts(graph, content);
|
|
3130
3224
|
requireUntrimmedAudio(content, marker, placement);
|
|
3131
|
-
const
|
|
3132
|
-
const
|
|
3133
|
-
const
|
|
3225
|
+
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3226
|
+
const isSynthesized = content.entityKind === "voiceover";
|
|
3227
|
+
const external = externalLocator(asset, isSynthesized ? "memota-speech" : "memota");
|
|
3228
|
+
const assembled = isSynthesized ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
|
|
3229
|
+
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3230
|
+
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3134
3231
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
3135
3232
|
const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
|
|
3136
|
-
|
|
3233
|
+
const speech = {
|
|
3137
3234
|
id: clip.entityId,
|
|
3138
3235
|
kind: "speech",
|
|
3139
3236
|
media_duration_ms: placement.duration,
|
|
3140
3237
|
audio_storage_key: storageKey(asset),
|
|
3141
3238
|
origin_speech_id: external.key,
|
|
3239
|
+
origin_asset_system: external.system,
|
|
3142
3240
|
audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "audio-script-source").length > 0 ? assembleScriptText(graph.one(content.entityId, "audio-script-source", "audio-script")) : ""),
|
|
3143
3241
|
volume: volume.data,
|
|
3144
3242
|
voice: object(voice) ? {
|
|
@@ -3146,7 +3244,8 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3146
3244
|
name: typeof voice.name === "string" ? voice.name : void 0
|
|
3147
3245
|
} : void 0,
|
|
3148
3246
|
caption_ids: captionIds
|
|
3149
|
-
}
|
|
3247
|
+
};
|
|
3248
|
+
parts[clip.entityId] = { speech };
|
|
3150
3249
|
} else if (track.role === "caption") {
|
|
3151
3250
|
const assembled = assembleCaptionContent(rows, content.entityId);
|
|
3152
3251
|
const text = assembled.text;
|
|
@@ -3171,8 +3270,7 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3171
3270
|
if (marker.payload.durationPolicy !== "timeline") fail("Background Audio requires an explicit timeline-fill durationPolicy");
|
|
3172
3271
|
if (marker.payload.timeRemapping !== void 0) fail("BGM timeRemapping is not supported by this reader");
|
|
3173
3272
|
const source = range(marker.payload.sourceRange, "sourceRange");
|
|
3174
|
-
|
|
3175
|
-
if (!object(extent) || extent.start !== source.start || extent.end !== source.end) fail("Background Audio compatibility requires the complete source");
|
|
3273
|
+
if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
|
|
3176
3274
|
parts[clip.entityId] = { bgm: {
|
|
3177
3275
|
id: clip.entityId,
|
|
3178
3276
|
kind: "bgm",
|
|
@@ -3183,10 +3281,6 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3183
3281
|
}
|
|
3184
3282
|
}
|
|
3185
3283
|
const document = {
|
|
3186
|
-
meta: {
|
|
3187
|
-
...structuredClone(meta),
|
|
3188
|
-
schema_version: ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION
|
|
3189
|
-
},
|
|
3190
3284
|
timeline: graph.timeline.payload.unit_time_ms === void 0 ? void 0 : { unit_time_ms: positive(graph.timeline.payload.unit_time_ms, "unit_time_ms") },
|
|
3191
3285
|
tracks: layout.tracks.map((track) => ({
|
|
3192
3286
|
id: track.entityId,
|
|
@@ -3216,29 +3310,26 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3216
3310
|
function trackRoleOf(layout, trackEntityId) {
|
|
3217
3311
|
return layout.tracks.find((track) => track.entityId === trackEntityId)?.role;
|
|
3218
3312
|
}
|
|
3219
|
-
/**
|
|
3220
|
-
* Media facts are read from the variant itself: a media variant owns its
|
|
3221
|
-
* Asset identity and carries `external`/`storageKey` directly. Dual-entity
|
|
3222
|
-
* media graphs are rejected instead of being resolved through a legacy
|
|
3223
|
-
* `physical-asset` binding.
|
|
3224
|
-
*/
|
|
3313
|
+
/** The Asset a media entity was made from; the bytes are named there alone. */
|
|
3225
3314
|
function mediaAssetFacts(graph, content) {
|
|
3226
|
-
|
|
3227
|
-
if (!object(content.payload.external)) fail("Media " + content.entityId + " must carry its own Asset external identity");
|
|
3315
|
+
const asset = graph.one(content.entityId, "from-asset", "asset");
|
|
3228
3316
|
return {
|
|
3229
|
-
entityId: String(
|
|
3230
|
-
payload:
|
|
3317
|
+
entityId: String(asset.entityId),
|
|
3318
|
+
payload: asset.payload
|
|
3231
3319
|
};
|
|
3232
3320
|
}
|
|
3233
|
-
function externalLocator(asset, system) {
|
|
3234
|
-
const value = asset.payload.external;
|
|
3235
|
-
if (!object(value) || value.system !== system || typeof value.key !== "string" || value.key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " external key");
|
|
3236
|
-
return { key: value.key };
|
|
3237
|
-
}
|
|
3238
3321
|
function storageKey(asset) {
|
|
3239
3322
|
if (typeof asset.payload.storageKey !== "string" || asset.payload.storageKey.trim() === "") fail("Asset " + asset.entityId + " requires its factual storageKey");
|
|
3240
3323
|
return asset.payload.storageKey;
|
|
3241
3324
|
}
|
|
3325
|
+
function externalLocator(asset, system) {
|
|
3326
|
+
const { system: found, key } = asset.payload;
|
|
3327
|
+
if (found !== system || typeof key !== "string" || key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " key");
|
|
3328
|
+
return {
|
|
3329
|
+
key,
|
|
3330
|
+
system
|
|
3331
|
+
};
|
|
3332
|
+
}
|
|
3242
3333
|
function captionStyle(value) {
|
|
3243
3334
|
if (value === void 0) return void 0;
|
|
3244
3335
|
if (!object(value)) fail("Caption style must be an object");
|
|
@@ -3270,8 +3361,7 @@ function captionStyle(value) {
|
|
|
3270
3361
|
}
|
|
3271
3362
|
function requireUntrimmedAudio(content, marker, placement) {
|
|
3272
3363
|
const source = range(marker.payload.sourceRange, "sourceRange");
|
|
3273
|
-
|
|
3274
|
-
if (!object(extent) || extent.start !== source.start || extent.end !== source.end || placement.duration !== source.end - source.start || marker.payload.timeRemapping !== void 0) fail("Voice compatibility requires the complete, unremapped take");
|
|
3364
|
+
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.timeRemapping !== void 0) fail("Voiceover compatibility requires the complete, unremapped take");
|
|
3275
3365
|
}
|
|
3276
3366
|
function videoSpeed(content, marker, placement) {
|
|
3277
3367
|
const remapping = marker.payload.timeRemapping;
|
|
@@ -3329,12 +3419,11 @@ const atomicFields = new Set([
|
|
|
3329
3419
|
"sourceRange",
|
|
3330
3420
|
"targetRange",
|
|
3331
3421
|
"duration",
|
|
3332
|
-
"
|
|
3422
|
+
"durationMs",
|
|
3333
3423
|
"timeRemapping",
|
|
3334
3424
|
"textRange",
|
|
3335
3425
|
"selection",
|
|
3336
|
-
"external"
|
|
3337
|
-
"coordinateSpace"
|
|
3426
|
+
"external"
|
|
3338
3427
|
]);
|
|
3339
3428
|
const textFields = new Set(["text", "phonemeScript"]);
|
|
3340
3429
|
const orderedFields = new Set([
|
|
@@ -3561,6 +3650,73 @@ function applyNativeListChange(list, change) {
|
|
|
3561
3650
|
}
|
|
3562
3651
|
}
|
|
3563
3652
|
//#endregion
|
|
3653
|
+
//#region src/entity-crdt/audio-script-assets.ts
|
|
3654
|
+
/** A resource is a snapshot of text; the native Loro text remains the editing authority. */
|
|
3655
|
+
function audioScriptAssetContent(payload) {
|
|
3656
|
+
assembleScriptText({
|
|
3657
|
+
entityId: createEntityId("script-validation"),
|
|
3658
|
+
entityKind: "audio-script",
|
|
3659
|
+
payload
|
|
3660
|
+
});
|
|
3661
|
+
return { segments: structuredClone(payload.segments) };
|
|
3662
|
+
}
|
|
3663
|
+
function audioScriptAssetHash(payload) {
|
|
3664
|
+
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(audioScriptAssetContent(payload)))));
|
|
3665
|
+
}
|
|
3666
|
+
/**
|
|
3667
|
+
* The Asset a script was written out to. It is a snapshot the script owns, so
|
|
3668
|
+
* it is reached by `to-asset` — not `from-asset`, which would say the script
|
|
3669
|
+
* was produced from bytes it cannot change.
|
|
3670
|
+
*/
|
|
3671
|
+
function audioScriptAssetOf(rows, entityId) {
|
|
3672
|
+
return boundAsset(rows, entityId, "to-asset");
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* The Asset a script was made from — a transcript it does not own. A script
|
|
3676
|
+
* that has one never writes a snapshot of its own: the bytes it would describe
|
|
3677
|
+
* are already named, and rewriting them would contradict their source.
|
|
3678
|
+
*/
|
|
3679
|
+
function scriptSourceAssetOf(rows, entityId) {
|
|
3680
|
+
return boundAsset(rows, entityId, "from-asset");
|
|
3681
|
+
}
|
|
3682
|
+
function boundAsset(rows, entityId, relationKind) {
|
|
3683
|
+
const link = rows.relations.find((relation) => relation.relationKind === relationKind && (String(relation.endpoint0EntityId) === entityId || String(relation.endpoint1EntityId) === entityId));
|
|
3684
|
+
if (link === void 0) return void 0;
|
|
3685
|
+
const otherId = String(link.endpoint0EntityId) === entityId ? link.endpoint1EntityId : link.endpoint0EntityId;
|
|
3686
|
+
return rows.entities.find((row) => String(row.entityId) === String(otherId) && row.entityKind === "asset");
|
|
3687
|
+
}
|
|
3688
|
+
/** Internal fingerprint prevents a resource from silently describing an older text revision. */
|
|
3689
|
+
function audioScriptAssetFields(payload) {
|
|
3690
|
+
return { assetContentHash: audioScriptAssetHash(payload) };
|
|
3691
|
+
}
|
|
3692
|
+
/**
|
|
3693
|
+
* What a PhoneticScript hands to synthesis: the base AudioScript's text as the
|
|
3694
|
+
* variant pronounces it, plus the variant's own phoneme and prosody control.
|
|
3695
|
+
*
|
|
3696
|
+
* The variant's content is written by a model — deciding how words are said is
|
|
3697
|
+
* text work, not a mechanical copy — but the bytes a synthesizer reads have to
|
|
3698
|
+
* leave the entity graph to reach it, because synthesis does not handle
|
|
3699
|
+
* entities. Writing this resource down is that crossing, and it is why speech
|
|
3700
|
+
* can only come from a PhoneticScript.
|
|
3701
|
+
*/
|
|
3702
|
+
function phoneticScriptAssetContent(rows, entityId) {
|
|
3703
|
+
const assembled = assemblePhoneticScriptContent(rows, createEntityId(entityId));
|
|
3704
|
+
const own = assembled.phoneticScript.payload;
|
|
3705
|
+
return {
|
|
3706
|
+
text: assembled.text,
|
|
3707
|
+
segments: structuredClone(assembled.segments),
|
|
3708
|
+
...typeof own.phonemeScript === "string" ? { phonemeScript: own.phonemeScript } : {},
|
|
3709
|
+
...isJsonObject(own.prosody) ? { prosody: structuredClone(own.prosody) } : {}
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function phoneticScriptAssetHash(rows, entityId) {
|
|
3713
|
+
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(phoneticScriptAssetContent(rows, entityId)))));
|
|
3714
|
+
}
|
|
3715
|
+
/** The Asset a PhoneticScript was written out to, read from the graph. */
|
|
3716
|
+
function phoneticScriptAssetOf(rows, entityId) {
|
|
3717
|
+
return audioScriptAssetOf(rows, entityId);
|
|
3718
|
+
}
|
|
3719
|
+
//#endregion
|
|
3564
3720
|
//#region src/entity-crdt/ordering.ts
|
|
3565
3721
|
/** Domain order is scoped by membership; relation storage has no global order. */
|
|
3566
3722
|
function entityOrderGroups(rows) {
|
|
@@ -3816,6 +3972,29 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3816
3972
|
if (id === void 0) return [];
|
|
3817
3973
|
return this.snapshot().rows.relations.filter((row) => row.endpoint0EntityId === id || row.endpoint1EntityId === id);
|
|
3818
3974
|
}
|
|
3975
|
+
/** Read unlinked Relation records for durable effect receipts; never part of the current semantic graph. */
|
|
3976
|
+
tombstonedRelations() {
|
|
3977
|
+
const result = [];
|
|
3978
|
+
for (const key of section(this.doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
3979
|
+
const map = section(this.doc, "relations").get(key);
|
|
3980
|
+
if (!(map instanceof LoroMap) || map.get("deleted") !== true) continue;
|
|
3981
|
+
const identity = map.get("identity");
|
|
3982
|
+
if (!identity || identity.endpoints.length !== 2) throw new Error(`Invalid tombstoned Relation ${key}`);
|
|
3983
|
+
result.push({
|
|
3984
|
+
relationId: createRelationId(key),
|
|
3985
|
+
relationKind: identity.kind,
|
|
3986
|
+
endpoint0EntityId: createEntityId(identity.endpoints[0]),
|
|
3987
|
+
endpoint1EntityId: createEntityId(identity.endpoints[1]),
|
|
3988
|
+
metadata: requireMap(map, "metadata").toJSON(),
|
|
3989
|
+
trace: requireMap(map, "trace").toJSON()
|
|
3990
|
+
});
|
|
3991
|
+
}
|
|
3992
|
+
return result;
|
|
3993
|
+
}
|
|
3994
|
+
/** Resolve a queued edit target through its native identity without changing history. */
|
|
3995
|
+
resolveCurrentEntityId(id) {
|
|
3996
|
+
return currentId(this.doc, id);
|
|
3997
|
+
}
|
|
3819
3998
|
getVersion(id) {
|
|
3820
3999
|
const entity = section(this.doc, "entities").get(id);
|
|
3821
4000
|
if (!isJsonObject(entity)) return void 0;
|
|
@@ -3856,6 +4035,52 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3856
4035
|
materialize(this.doc);
|
|
3857
4036
|
this.doc.commit({ origin: MATERIALIZE_ORIGIN });
|
|
3858
4037
|
}
|
|
4038
|
+
/**
|
|
4039
|
+
* Bind a saved snapshot without rewriting text operations or resolving
|
|
4040
|
+
* selection conflicts. The script names the bytes the same way everything
|
|
4041
|
+
* else does — an Asset entity plus a Relation — and keeps only the
|
|
4042
|
+
* fingerprint that tells a stale snapshot from a current one. The Relation is
|
|
4043
|
+
* `to-asset`: these bytes are the script's own content written out, not a
|
|
4044
|
+
* source it was made from.
|
|
4045
|
+
*/
|
|
4046
|
+
bindAudioScriptAsset(entityId, assetId, expectedContentHash) {
|
|
4047
|
+
const key = family(this.doc, entityId);
|
|
4048
|
+
const map = workspace(this.doc, key);
|
|
4049
|
+
if (map.get("kind") !== "audio-script" || map.get("deleted") === true) throw new Error("Resource binding requires an active AudioScript");
|
|
4050
|
+
const payload = entityContent(this.doc, key);
|
|
4051
|
+
if (audioScriptAssetHash(payload) !== expectedContentHash) throw new Error("AudioScript changed while saving its resource");
|
|
4052
|
+
if (!assetId.trim()) throw new Error("AudioScript resource writer returned an empty asset ID");
|
|
4053
|
+
const before = this.doc.oplogVersion();
|
|
4054
|
+
const existing = readEntityDocument(this.doc, true).rows.entities.find((row) => row.entityKind === "asset" && row.payload.system === "memota" && row.payload.key === assetId);
|
|
4055
|
+
const assetEntityId = existing?.entityId ?? createEntityId(`asset_${assetId}`);
|
|
4056
|
+
if (existing === void 0) this.transact((draft) => draft.create({
|
|
4057
|
+
entityId: assetEntityId,
|
|
4058
|
+
entityKind: "asset",
|
|
4059
|
+
payload: {
|
|
4060
|
+
system: "memota",
|
|
4061
|
+
key: assetId
|
|
4062
|
+
}
|
|
4063
|
+
}));
|
|
4064
|
+
this.transact((draft) => {
|
|
4065
|
+
const current = readEntityDocument(this.doc, true).rows;
|
|
4066
|
+
if (audioScriptAssetOf(current, entityId)?.entityId !== assetEntityId) {
|
|
4067
|
+
for (const relation of current.relations) if (relation.relationKind === "to-asset" && (String(relation.endpoint0EntityId) === String(entityId) || String(relation.endpoint1EntityId) === String(entityId))) draft.unlink(String(relation.relationId));
|
|
4068
|
+
draft.link({
|
|
4069
|
+
relationId: createRelationId(`relation_to-asset_${entityId}_${assetId}`),
|
|
4070
|
+
relationKind: "to-asset",
|
|
4071
|
+
endpoint0EntityId: createEntityId(entityId),
|
|
4072
|
+
endpoint1EntityId: assetEntityId,
|
|
4073
|
+
metadata: {},
|
|
4074
|
+
trace: {}
|
|
4075
|
+
});
|
|
4076
|
+
}
|
|
4077
|
+
draft.declare(entityId, audioScriptAssetFields(payload));
|
|
4078
|
+
});
|
|
4079
|
+
return this.doc.export({
|
|
4080
|
+
mode: "update",
|
|
4081
|
+
from: before
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
3859
4084
|
canUndo() {
|
|
3860
4085
|
return this.undoManager.canUndo();
|
|
3861
4086
|
}
|
|
@@ -3869,12 +4094,12 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3869
4094
|
return this.historyEdit(() => this.undoManager.redo());
|
|
3870
4095
|
}
|
|
3871
4096
|
historyEdit(run) {
|
|
3872
|
-
const before = this.snapshot();
|
|
4097
|
+
const before = new Map(this.snapshot().rows.entities.map((row) => [family(this.doc, row.entityId), row]));
|
|
3873
4098
|
if (!run()) return false;
|
|
3874
4099
|
const after = this.snapshot();
|
|
3875
4100
|
for (const row of after.rows.entities) {
|
|
3876
4101
|
const id = family(this.doc, row.entityId);
|
|
3877
|
-
const previous = before.
|
|
4102
|
+
const previous = before.get(id);
|
|
3878
4103
|
if (previous && !equalJson(owned(previous.payload), owned(row.payload))) workspace(this.doc, id).ensureMergeableMap("changes").set(globalThis.crypto.randomUUID(), true);
|
|
3879
4104
|
}
|
|
3880
4105
|
materialize(this.doc);
|
|
@@ -4197,8 +4422,8 @@ function assertState(state) {
|
|
|
4197
4422
|
const ids = new Set(state.rows.entities.map((row) => String(row.entityId)));
|
|
4198
4423
|
for (const row of state.rows.relations) if (!ids.has(row.endpoint0EntityId) || !ids.has(row.endpoint1EntityId)) throw new Error("Relation has a missing endpoint");
|
|
4199
4424
|
}
|
|
4200
|
-
function materialize(doc) {
|
|
4201
|
-
const state = readEntityDocument(doc);
|
|
4425
|
+
function materialize(doc, allowSelectionConflicts = false) {
|
|
4426
|
+
const state = readEntityDocument(doc, allowSelectionConflicts);
|
|
4202
4427
|
const byId = section(doc, "entities");
|
|
4203
4428
|
const ids = currentIds(doc);
|
|
4204
4429
|
for (const [source, id] of ids) {
|
|
@@ -4219,13 +4444,37 @@ function materialize(doc) {
|
|
|
4219
4444
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4220
4445
|
}
|
|
4221
4446
|
//#endregion
|
|
4447
|
+
//#region src/document/document-mutation-guard.ts
|
|
4448
|
+
/**
|
|
4449
|
+
* @internal
|
|
4450
|
+
* Shared by synchronous mutation entry points for one document. The session
|
|
4451
|
+
* closes it before teardown callbacks can attempt another write.
|
|
4452
|
+
*/
|
|
4453
|
+
var DocumentMutationGuard = class {
|
|
4454
|
+
running = false;
|
|
4455
|
+
closed = false;
|
|
4456
|
+
run(mutation) {
|
|
4457
|
+
if (this.closed) throw new Error("mengine document is disposed");
|
|
4458
|
+
if (this.running) throw new Error("mengine document mutation is already running");
|
|
4459
|
+
this.running = true;
|
|
4460
|
+
try {
|
|
4461
|
+
return mutation();
|
|
4462
|
+
} finally {
|
|
4463
|
+
this.running = false;
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
close() {
|
|
4467
|
+
this.closed = true;
|
|
4468
|
+
}
|
|
4469
|
+
};
|
|
4470
|
+
//#endregion
|
|
4222
4471
|
//#region src/document/mirror-read.ts
|
|
4223
4472
|
/**
|
|
4224
4473
|
* Project the mirror state (`VideoDocumentDraft`) into the authoritative
|
|
4225
4474
|
* `VideoDocument`. The storage shape is isomorphic to the domain shape (RFC 03
|
|
4226
|
-
* §4, reference/17 §4:
|
|
4475
|
+
* §4, reference/17 §4: timeline + a single `tracks` list + part_library), so this
|
|
4227
4476
|
* is a near-identity — it reads `time_position` / `fallback_abs_ms` JSON blobs
|
|
4228
|
-
* back into structured values and
|
|
4477
|
+
* back into structured values and selects content fields. Legacy meta is ignored.
|
|
4229
4478
|
*
|
|
4230
4479
|
* It maps only authoritative facts (RFC 02 §6): `part_id` + `time_position`. The
|
|
4231
4480
|
* projection-derived `VideoDraft` read-view (absolute time, `part_aggregations`,
|
|
@@ -4242,20 +4491,8 @@ function readVideoDocumentFromDraft(draft) {
|
|
|
4242
4491
|
if (part != null) partLibrary[partId] = part;
|
|
4243
4492
|
}
|
|
4244
4493
|
const timeline = draft.timeline;
|
|
4245
|
-
const hasTimeline = timeline?.unit_time_ms != null;
|
|
4246
|
-
const meta = draft.meta;
|
|
4247
4494
|
return {
|
|
4248
|
-
|
|
4249
|
-
schema_version: meta?.schema_version === "video-document/entity-projection-v1" ? ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION : VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
4250
|
-
draft_id: emptyToUndefined(meta?.draft_id),
|
|
4251
|
-
project_id: emptyToUndefined(meta?.project_id),
|
|
4252
|
-
owner_id: emptyToUndefined(meta?.owner_id),
|
|
4253
|
-
thumbnail_storage_key: emptyToUndefined(meta?.thumbnail_storage_key),
|
|
4254
|
-
chat_session_id: emptyToUndefined(meta?.chat_session_id),
|
|
4255
|
-
video_creation_settings: meta?.video_creation_settings ?? void 0,
|
|
4256
|
-
version: meta?.version ?? void 0
|
|
4257
|
-
},
|
|
4258
|
-
timeline: hasTimeline ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4495
|
+
timeline: timeline?.unit_time_ms != null ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4259
4496
|
tracks: rowsToTracks(draft.tracks),
|
|
4260
4497
|
part_library: partLibrary
|
|
4261
4498
|
};
|
|
@@ -4279,9 +4516,6 @@ function rowToTrack(row) {
|
|
|
4279
4516
|
})
|
|
4280
4517
|
};
|
|
4281
4518
|
}
|
|
4282
|
-
function emptyToUndefined(value) {
|
|
4283
|
-
return value == null || value === "" ? void 0 : value;
|
|
4284
|
-
}
|
|
4285
4519
|
//#endregion
|
|
4286
4520
|
//#region src/document/mirror-adapter.ts
|
|
4287
4521
|
/**
|
|
@@ -4294,33 +4528,50 @@ function emptyToUndefined(value) {
|
|
|
4294
4528
|
* - `transact(edit, audit)` runs the whole op in one `mirror.setState` callback:
|
|
4295
4529
|
* one diff, one `doc.commit` carrying the audit message. The callback edits an
|
|
4296
4530
|
* immer draft, so a throw inside it discards the draft and never touches Loro
|
|
4297
|
-
* (natural rollback)
|
|
4531
|
+
* (natural rollback) without a document checkout or compensating commit.
|
|
4298
4532
|
* - mirror's `idSelector` (track items keyed by `part_id`) diffs reorders to
|
|
4299
4533
|
* real Loro `move` ops on every lane, so per-item CRDT identity survives on
|
|
4300
4534
|
* main and secondary tracks alike.
|
|
4301
4535
|
*/
|
|
4302
4536
|
var MirrorVideoDocumentAdapter = class {
|
|
4303
4537
|
doc;
|
|
4538
|
+
mutationGuard;
|
|
4304
4539
|
mirror;
|
|
4305
|
-
|
|
4540
|
+
disposed = false;
|
|
4541
|
+
constructor(doc, mutationGuard = new DocumentMutationGuard()) {
|
|
4306
4542
|
this.doc = doc;
|
|
4543
|
+
this.mutationGuard = mutationGuard;
|
|
4307
4544
|
this.mirror = new Mirror({
|
|
4308
4545
|
doc,
|
|
4309
|
-
schema: videoDocumentMirrorSchema
|
|
4546
|
+
schema: videoDocumentMirrorSchema,
|
|
4547
|
+
validateUpdates: false
|
|
4310
4548
|
});
|
|
4311
4549
|
}
|
|
4312
4550
|
snapshot() {
|
|
4313
|
-
|
|
4314
|
-
if (this.
|
|
4315
|
-
return
|
|
4551
|
+
this.assertActive();
|
|
4552
|
+
if (this.isEntityDocument()) return projectEntityTimeline(readEntityDocument(this.doc).rows);
|
|
4553
|
+
return readVideoDocumentFromDraft(this.mirror.getState());
|
|
4316
4554
|
}
|
|
4317
4555
|
/**
|
|
4318
|
-
*
|
|
4319
|
-
*
|
|
4320
|
-
*
|
|
4556
|
+
* Native Entity authority: placement is already solved by the Entity graph.
|
|
4557
|
+
*
|
|
4558
|
+
* The root check goes through `getShallowValue` first because `getMap` creates
|
|
4559
|
+
* the container on access — probing a legacy document for `medeo` would
|
|
4560
|
+
* otherwise mutate it, which a read must never do.
|
|
4321
4561
|
*/
|
|
4322
|
-
|
|
4323
|
-
|
|
4562
|
+
isEntityDocument() {
|
|
4563
|
+
const roots = this.doc.getShallowValue();
|
|
4564
|
+
if (!Object.hasOwn(roots, "medeo")) return false;
|
|
4565
|
+
return this.doc.getMap("medeo").get("schema") === LORO_ENTITY_SCHEMA;
|
|
4566
|
+
}
|
|
4567
|
+
/**
|
|
4568
|
+
* Release mirror subscriptions; do not reuse this adapter afterwards.
|
|
4569
|
+
* The caller still owns the borrowed LoroDoc.
|
|
4570
|
+
*/
|
|
4571
|
+
dispose() {
|
|
4572
|
+
if (this.disposed) return;
|
|
4573
|
+
this.disposed = true;
|
|
4574
|
+
this.mirror.dispose();
|
|
4324
4575
|
}
|
|
4325
4576
|
/**
|
|
4326
4577
|
* Apply one op as a single transaction. `edit` mutates the immer draft; mirror
|
|
@@ -4329,80 +4580,71 @@ var MirrorVideoDocumentAdapter = class {
|
|
|
4329
4580
|
* skips the commit — matching the prior "empty op leaves no audit" behavior.
|
|
4330
4581
|
*/
|
|
4331
4582
|
transact(edit, audit) {
|
|
4332
|
-
|
|
4333
|
-
this.
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4583
|
+
this.assertActive();
|
|
4584
|
+
if (this.isEntityDocument()) throw new Error("Entity documents are read-only through this adapter; edit the Medeo entities instead");
|
|
4585
|
+
this.mutationGuard.run(() => {
|
|
4586
|
+
this.mirror.setState((draft) => {
|
|
4587
|
+
edit(draft);
|
|
4588
|
+
const result = validateSchema(videoDocumentMirrorSchema, {
|
|
4589
|
+
timeline: draft.timeline,
|
|
4590
|
+
tracks: draft.tracks,
|
|
4591
|
+
part_library: draft.part_library
|
|
4592
|
+
});
|
|
4593
|
+
if (!result.valid) throw new Error(`Invalid content update: ${result.errors?.join("; ")}`);
|
|
4594
|
+
}, {
|
|
4595
|
+
origin: "mengine.semantic_editor",
|
|
4596
|
+
message: JSON.stringify({
|
|
4597
|
+
semantic_op: audit.kind,
|
|
4598
|
+
payload: audit.payload,
|
|
4599
|
+
intent: audit.intent ?? null,
|
|
4600
|
+
actor: audit.actor ?? null
|
|
4601
|
+
})
|
|
4602
|
+
});
|
|
4343
4603
|
});
|
|
4344
4604
|
}
|
|
4605
|
+
assertActive() {
|
|
4606
|
+
if (this.disposed) throw new Error("mengine document adapter is disposed");
|
|
4607
|
+
}
|
|
4345
4608
|
};
|
|
4346
4609
|
/** Build a fresh Loro doc seeded with `document` through the mirror. */
|
|
4347
4610
|
function createMirrorVideoDocument(document, options = {}) {
|
|
4348
4611
|
assertValidVideoDocument(document);
|
|
4349
4612
|
const doc = new LoroDoc();
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4613
|
+
try {
|
|
4614
|
+
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4615
|
+
const mirror = new Mirror({
|
|
4616
|
+
doc,
|
|
4617
|
+
schema: videoDocumentMirrorSchema
|
|
4618
|
+
});
|
|
4619
|
+
try {
|
|
4620
|
+
mirror.setState((draft) => {
|
|
4621
|
+
seedDraft(draft, document);
|
|
4622
|
+
}, { origin: options.origin ?? "mengine.bootstrap" });
|
|
4623
|
+
return doc;
|
|
4624
|
+
} finally {
|
|
4625
|
+
mirror.dispose();
|
|
4626
|
+
}
|
|
4627
|
+
} catch (error) {
|
|
4628
|
+
doc.free();
|
|
4629
|
+
throw error;
|
|
4630
|
+
}
|
|
4358
4631
|
}
|
|
4359
4632
|
/** Build a `MirrorVideoDocumentAdapter` over a fresh doc seeded with `document`. */
|
|
4360
4633
|
function createMirrorVideoDocumentAdapter(document, options) {
|
|
4361
4634
|
return new MirrorVideoDocumentAdapter(createMirrorVideoDocument(document, options));
|
|
4362
4635
|
}
|
|
4363
|
-
/**
|
|
4364
|
-
function
|
|
4365
|
-
if (projection.meta.schema_version !== "video-document/entity-projection-v1") throw new Error("Expected an entity timeline projection");
|
|
4366
|
-
if (!Number.isSafeInteger(entityRevision) || entityRevision < 1) throw new Error("Expected a positive entity revision");
|
|
4367
|
-
assertValidVideoDocument(projection);
|
|
4368
|
-
const from = doc.oplogVersion();
|
|
4369
|
-
new Mirror({
|
|
4370
|
-
doc,
|
|
4371
|
-
schema: videoDocumentMirrorSchema
|
|
4372
|
-
}).setState((draft) => writeVideoDocumentToDraft(draft, {
|
|
4373
|
-
...projection,
|
|
4374
|
-
meta: {
|
|
4375
|
-
...projection.meta,
|
|
4376
|
-
version: entityRevision
|
|
4377
|
-
}
|
|
4378
|
-
}), {
|
|
4379
|
-
origin: "mengine.entity_projection",
|
|
4380
|
-
message: JSON.stringify({
|
|
4381
|
-
semantic_op: "EntityTimelineProjection",
|
|
4382
|
-
entity_revision: entityRevision
|
|
4383
|
-
})
|
|
4384
|
-
});
|
|
4385
|
-
return doc.export({
|
|
4386
|
-
mode: "update",
|
|
4387
|
-
from
|
|
4388
|
-
});
|
|
4389
|
-
}
|
|
4390
|
-
/** Write a whole `VideoDocument` into a draft (seed a fresh doc / plain-memory state). */
|
|
4391
|
-
function writeVideoDocumentToDraft(draft, document) {
|
|
4392
|
-
draft.meta = {
|
|
4393
|
-
schema_version: document.meta.schema_version,
|
|
4394
|
-
draft_id: document.meta.draft_id ?? void 0,
|
|
4395
|
-
project_id: document.meta.project_id ?? void 0,
|
|
4396
|
-
owner_id: document.meta.owner_id ?? void 0,
|
|
4397
|
-
thumbnail_storage_key: document.meta.thumbnail_storage_key ?? void 0,
|
|
4398
|
-
chat_session_id: document.meta.chat_session_id ?? void 0,
|
|
4399
|
-
video_creation_settings: document.meta.video_creation_settings ?? void 0,
|
|
4400
|
-
version: document.meta.version ?? void 0
|
|
4401
|
-
};
|
|
4636
|
+
/** Write a whole `VideoDocument` into the mirror draft (seed a fresh doc / plain-memory state). */
|
|
4637
|
+
function seedDraft(draft, document) {
|
|
4402
4638
|
draft.timeline = { unit_time_ms: document.timeline?.unit_time_ms ?? void 0 };
|
|
4403
4639
|
draft.tracks = (document.tracks ?? []).map(toTrackRow);
|
|
4404
4640
|
draft.part_library = {};
|
|
4405
|
-
for (const [partId, part] of Object.entries(document.part_library ?? {}))
|
|
4641
|
+
for (const [partId, part] of Object.entries(document.part_library ?? {})) {
|
|
4642
|
+
const createdPart = part.caption != null ? { caption: {
|
|
4643
|
+
...part.caption,
|
|
4644
|
+
style: part.caption.style ?? {}
|
|
4645
|
+
} } : part;
|
|
4646
|
+
draft.part_library[partId] = partUnionToDraft(createdPart);
|
|
4647
|
+
}
|
|
4406
4648
|
}
|
|
4407
4649
|
/** Map a domain `Track` to a draft track row (no lane/lane_order — see schema). */
|
|
4408
4650
|
function toTrackRow(track) {
|
|
@@ -4486,10 +4728,12 @@ var PlainMemoryAdapter = class {
|
|
|
4486
4728
|
* use this so every minted id is appended to the current transact's list.
|
|
4487
4729
|
*/
|
|
4488
4730
|
idFactory;
|
|
4731
|
+
readOnly;
|
|
4489
4732
|
constructor(document, options) {
|
|
4490
4733
|
assertValidVideoDocument(document);
|
|
4734
|
+
this.readOnly = options?.readOnly === true;
|
|
4491
4735
|
const draft = {};
|
|
4492
|
-
|
|
4736
|
+
seedDraft(draft, document);
|
|
4493
4737
|
this.state = draft;
|
|
4494
4738
|
this.baseIdFactory = options?.idFactory ?? generatePartId;
|
|
4495
4739
|
this.idFactory = (prefix) => {
|
|
@@ -4502,7 +4746,7 @@ var PlainMemoryAdapter = class {
|
|
|
4502
4746
|
return this._journal;
|
|
4503
4747
|
}
|
|
4504
4748
|
hasContent() {
|
|
4505
|
-
return
|
|
4749
|
+
return this.state.tracks !== void 0 || this.state.timeline !== void 0;
|
|
4506
4750
|
}
|
|
4507
4751
|
snapshot() {
|
|
4508
4752
|
return readVideoDocumentFromDraft(this.state);
|
|
@@ -4513,7 +4757,7 @@ var PlainMemoryAdapter = class {
|
|
|
4513
4757
|
* no structural change — skip journal, matching mirror "no change, no commit".
|
|
4514
4758
|
*/
|
|
4515
4759
|
transact(edit, audit) {
|
|
4516
|
-
if (this.
|
|
4760
|
+
if (this.readOnly) throw new Error("Entity documents are read-only here; edit the Medeo entities instead");
|
|
4517
4761
|
this.pendingIds = [];
|
|
4518
4762
|
try {
|
|
4519
4763
|
const next = produce(this.state, edit);
|
|
@@ -4534,4 +4778,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4534
4778
|
return new PlainMemoryAdapter(document, options);
|
|
4535
4779
|
}
|
|
4536
4780
|
//#endregion
|
|
4537
|
-
export {
|
|
4781
|
+
export { validateVideoDocument as $, EntityTimelineProjectionError as A, variantBaseEntityIds as B, equalJson as C, partUnionToDraft as Ct, projectEntityTimeline as D, bytesToBase64 as Dt, resolveFieldPath as E, base64ToBytes as Et, assemblePhoneticScriptContent as F, isMediaAssetVariantKind as G, createEntityId as H, assembleScriptText as I, derivePositionFromAbs as J, buildInitialVideoDocument as K, findComposedAudioScript as L, ScriptCompositionError as M, assembleCaptionContent as N, EntityProjectionGraph as O, assembleEntityContent as P, assertValidVideoDocument as Q, selectAudioScriptSegment as R, canonicalJson as S, speedOf as St, assertFieldChanges as T, videoDocumentMirrorSchema as Tt, createRelationId as U, isJsonObject as V, hasSequence as W, toVideoDocument as X, fromVideoDocument as Y, VideoDocumentValidationError as Z, audioScriptAssetOf as _, isEmptyVideoClip as _t, createMirrorVideoDocument as a, hasResolvedPlacement as at, phoneticScriptAssetOf as b, DEFAULT_UNIT_TIME_MS as bt, DocumentMutationGuard as c, cascadeAfterVideoClipChanges as ct, LoroEntityDraft as d, reassignSpeechesToVideoClipsByTime as dt, partUnionSchema as et, readEntityDocument as f, recalculateTimelineDuration as ft, audioScriptAssetHash as g, TIMELINE_SKELETON_DURATION_MS as gt, audioScriptAssetFields as h, syncAggregatedClipsTimePosition as ht, MirrorVideoDocumentAdapter as i, findLaneTrack as it, decodeEntityRelationRows as j, resolveEntityTimelineLayout as k, LORO_ENTITY_SCHEMA as l, arrangeMainTrackSeamlessly as lt, audioScriptAssetContent as m, resolveSpeechOverlapByShiftingVideos as mt, createPlainMemoryAdapter as n, LANE_KINDS_IN_STACK_ORDER as nt, createMirrorVideoDocumentAdapter as o, laneTrackId as ot, entityOrderGroups as p, resolveAllSpeechOverlaps as pt, buildSpeechHostMap as q, generatePartId as r, ensureLaneTrack as rt, readVideoDocumentFromDraft as s, solveVideoDocument as st, PlainMemoryAdapter as t, videoDocumentSchema as tt, LoroEntityDocument as u, fillMainTrackTimeGaps as ut, phoneticScriptAssetContent as v, partDurationMs as vt, applyFieldChanges as w, recordEntries as wt, scriptSourceAssetOf as x, effectiveVideoClipDurationMs as xt, phoneticScriptAssetHash as y, safeDurationMs as yt, updateEntityFields as z };
|