@mengine/medeo-client 2.0.1-alpha.6 → 2.0.1-alpha.8
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-N-8GwtOX.js → document-DdwzfZ1g.js} +259 -211
- package/dist/{index-B31KeDzA.d.ts → index-CDrPUhBH.d.ts} +276 -181
- package/dist/index.d.ts +113 -143
- package/dist/index.js +469 -164
- 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 +7 -7
|
@@ -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)
|
|
@@ -1296,8 +1267,8 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1296
1267
|
};
|
|
1297
1268
|
}
|
|
1298
1269
|
/**
|
|
1299
|
-
* Project
|
|
1300
|
-
*
|
|
1270
|
+
* Project `VideoDocument` into `VideoDraftContent`, solving each item's
|
|
1271
|
+
* absolute position, the `part_aggregations`, and
|
|
1301
1272
|
* the total duration via the timeline-core cascade.
|
|
1302
1273
|
*
|
|
1303
1274
|
* The read-view is a compatibility contract, and the IDL declares
|
|
@@ -1311,33 +1282,26 @@ function deriveItem(item, isMain, speechHost, partLibrary) {
|
|
|
1311
1282
|
* These two are defaultable because the projection knows their values on its own:
|
|
1312
1283
|
* absent `unit_time_ms` means "no display granularity was ever stated" and absent
|
|
1313
1284
|
* `is_hidden` means "this track was never hidden". `version` is NOT defaultable
|
|
1314
|
-
* here —
|
|
1315
|
-
*
|
|
1285
|
+
* here — it belongs to Director assembly using the server envelope update_seq.
|
|
1286
|
+
* Neither business metadata nor a revision counter belongs in content projection.
|
|
1316
1287
|
*/
|
|
1317
|
-
function fromVideoDocument(document) {
|
|
1288
|
+
function fromVideoDocument(document, options = {}) {
|
|
1318
1289
|
assertValidVideoDocument(document);
|
|
1319
|
-
const view = solveVideoDocument(document);
|
|
1290
|
+
const view = solveVideoDocument(document, options);
|
|
1320
1291
|
const tracks = document.tracks ?? [];
|
|
1321
1292
|
const mainTrack = tracks.find((t) => t.parts_kind === "video_clip");
|
|
1322
1293
|
const aboveTracks = tracks.filter((t) => t.parts_kind === "caption");
|
|
1323
1294
|
const belowTracks = tracks.filter((t) => t.parts_kind === "speech" || t.parts_kind === "bgm");
|
|
1324
1295
|
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
1296
|
timeline: {
|
|
1330
1297
|
duration_ms: view.durationMs,
|
|
1331
1298
|
unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
|
|
1332
1299
|
},
|
|
1333
|
-
video_creation_settings: clone$1(document.meta.video_creation_settings),
|
|
1334
|
-
chat_session_id: document.meta.chat_session_id,
|
|
1335
1300
|
main_track: draftTrack(mainTrack, view.absByPartId),
|
|
1336
1301
|
above_main_tracks: aboveTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1337
1302
|
below_main_tracks: belowTracks.map((t) => draftTrack(t, view.absByPartId)),
|
|
1338
1303
|
part_aggregations: view.aggregations,
|
|
1339
|
-
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1340
|
-
version: document.meta.version
|
|
1304
|
+
part_library: toReadViewPartLibrary(view.partLibrary, view.durationMs, view.derivedFillerPartIds)
|
|
1341
1305
|
};
|
|
1342
1306
|
}
|
|
1343
1307
|
/**
|
|
@@ -1444,7 +1408,7 @@ function clone$1(value) {
|
|
|
1444
1408
|
//#region src/document/initial-document.ts
|
|
1445
1409
|
/**
|
|
1446
1410
|
* Build the `VideoDocument` a newly created project starts from: the caller's
|
|
1447
|
-
*
|
|
1411
|
+
* no parts and one empty track per lane. Business facts remain in Director.
|
|
1448
1412
|
*
|
|
1449
1413
|
* The empty lane tracks are the reason this function exists rather than callers
|
|
1450
1414
|
* assembling a document inline. Lane tracks are otherwise minted lazily by the
|
|
@@ -1465,18 +1429,8 @@ function clone$1(value) {
|
|
|
1465
1429
|
* supplies, and total duration is derived on read (RFC 02 §6), so a new document
|
|
1466
1430
|
* has no timeline fact to state.
|
|
1467
1431
|
*/
|
|
1468
|
-
function buildInitialVideoDocument(
|
|
1432
|
+
function buildInitialVideoDocument() {
|
|
1469
1433
|
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
1434
|
timeline: void 0,
|
|
1481
1435
|
tracks: emptyLaneTracks(),
|
|
1482
1436
|
part_library: {}
|
|
@@ -1493,8 +1447,63 @@ function emptyLaneTracks() {
|
|
|
1493
1447
|
}
|
|
1494
1448
|
//#endregion
|
|
1495
1449
|
//#region ../medeo-dsl/src/entities.ts
|
|
1450
|
+
/**
|
|
1451
|
+
* The declared trait set of every known kind.
|
|
1452
|
+
*
|
|
1453
|
+
* `caption` carries `sequence` on purpose: a Caption owns the display timing of
|
|
1454
|
+
* the one AudioScript segment it shows, which is what admits it into a Clip.
|
|
1455
|
+
* `voice` carries no media or sequence trait — it is a timbre identity, not
|
|
1456
|
+
* playable content; a rendered voiceover is an `audio` entity.
|
|
1457
|
+
*/
|
|
1458
|
+
const ENTITY_TRAIT_KINDS = Object.freeze({
|
|
1459
|
+
axvideo: Object.freeze([
|
|
1460
|
+
"container",
|
|
1461
|
+
"sequence",
|
|
1462
|
+
"visual",
|
|
1463
|
+
"audible"
|
|
1464
|
+
]),
|
|
1465
|
+
timeline: Object.freeze(["container"]),
|
|
1466
|
+
track: Object.freeze(["container", "sequence"]),
|
|
1467
|
+
clip: Object.freeze(["container"]),
|
|
1468
|
+
asset: Object.freeze([]),
|
|
1469
|
+
video: Object.freeze([
|
|
1470
|
+
"media-file",
|
|
1471
|
+
"sequence",
|
|
1472
|
+
"visual",
|
|
1473
|
+
"audible"
|
|
1474
|
+
]),
|
|
1475
|
+
audio: Object.freeze([
|
|
1476
|
+
"media-file",
|
|
1477
|
+
"sequence",
|
|
1478
|
+
"audible"
|
|
1479
|
+
]),
|
|
1480
|
+
image: Object.freeze([
|
|
1481
|
+
"media-file",
|
|
1482
|
+
"sequence",
|
|
1483
|
+
"visual"
|
|
1484
|
+
]),
|
|
1485
|
+
voice: Object.freeze(["container", "ip-bound"]),
|
|
1486
|
+
"sequence-marker": Object.freeze([]),
|
|
1487
|
+
viewport: Object.freeze([]),
|
|
1488
|
+
"audio-script": Object.freeze(["segment-text"]),
|
|
1489
|
+
"phonetic-script": Object.freeze(["segment-text"]),
|
|
1490
|
+
caption: Object.freeze(["segment-text", "sequence"])
|
|
1491
|
+
});
|
|
1492
|
+
/** Declared traits of a known kind; extension kinds declare none through this table. */
|
|
1493
|
+
function traitKindsOf(entityKind) {
|
|
1494
|
+
return isKnownEntityKind(entityKind) ? ENTITY_TRAIT_KINDS[entityKind] : [];
|
|
1495
|
+
}
|
|
1496
|
+
function hasTraitKind(entityKind, trait) {
|
|
1497
|
+
return traitKindsOf(entityKind).includes(trait);
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Sequence admission asks the declared traits first, so a kind that does not
|
|
1501
|
+
* declare `sequence` can never buy its way in by carrying the fields. Extension
|
|
1502
|
+
* kinds declare no traits and stay structurally detected.
|
|
1503
|
+
*/
|
|
1496
1504
|
function hasSequence(entity) {
|
|
1497
|
-
if (
|
|
1505
|
+
if (isReservedEntityKind(entity.entityKind)) return false;
|
|
1506
|
+
if (isKnownEntityKind(entity.entityKind) && !hasTraitKind(entity.entityKind, "sequence")) return false;
|
|
1498
1507
|
return isSequenceFields(entity);
|
|
1499
1508
|
}
|
|
1500
1509
|
function isSequenceFields(value) {
|
|
@@ -1507,22 +1516,16 @@ function isSequenceFields(value) {
|
|
|
1507
1516
|
if (value.sampling !== "native" && value.sampling !== "constant" && value.sampling !== "derived") return false;
|
|
1508
1517
|
return "coordinateSpace" in value && value.coordinateSpace !== void 0;
|
|
1509
1518
|
}
|
|
1510
|
-
function isKnownSequenceKind(kind) {
|
|
1511
|
-
return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
|
|
1512
|
-
}
|
|
1513
1519
|
function isMediaAssetVariantKind(kind) {
|
|
1514
|
-
return kind === "video" || kind === "image" || kind === "audio"
|
|
1520
|
+
return kind === "video" || kind === "image" || kind === "audio";
|
|
1515
1521
|
}
|
|
1516
1522
|
function isKnownEntityKind(kind) {
|
|
1517
|
-
return
|
|
1523
|
+
return Object.hasOwn(ENTITY_TRAIT_KINDS, kind);
|
|
1518
1524
|
}
|
|
1519
1525
|
function isReservedEntityKind(kind) {
|
|
1520
1526
|
const normalized = kind.toLowerCase().replaceAll("-", "").replaceAll("_", "");
|
|
1521
1527
|
return normalized === "speech" || normalized === "videodocument";
|
|
1522
1528
|
}
|
|
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
1529
|
function isRecord$1(value) {
|
|
1527
1530
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
1528
1531
|
}
|
|
@@ -2105,12 +2108,15 @@ function validateKnownEntityPayload(entity) {
|
|
|
2105
2108
|
break;
|
|
2106
2109
|
case "video":
|
|
2107
2110
|
case "audio":
|
|
2108
|
-
|
|
2111
|
+
validateSequencePayload(value, "bounded", "native", problems);
|
|
2112
|
+
validateMediaAssetFields(value, problems);
|
|
2113
|
+
break;
|
|
2109
2114
|
case "caption":
|
|
2110
2115
|
validateSequencePayload(value, "bounded", "native", problems);
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2116
|
+
validateCaptionPayload(value, problems);
|
|
2117
|
+
break;
|
|
2118
|
+
case "voice":
|
|
2119
|
+
validateVoicePayload(value, problems);
|
|
2114
2120
|
break;
|
|
2115
2121
|
case "image":
|
|
2116
2122
|
validateSequencePayload(value, "unbounded", "constant", problems);
|
|
@@ -2217,11 +2223,26 @@ function validateExternalLocator(value, problems) {
|
|
|
2217
2223
|
function validateStorageKey(value, problems) {
|
|
2218
2224
|
if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
|
|
2219
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Voice is a timbre identity, not playable content: it owns the voice-library
|
|
2228
|
+
* descriptor and nothing that would make it look like media or a Sequence. The
|
|
2229
|
+
* rendered voiceover is an `audio` entity bound by a `voice-timbre` Relation.
|
|
2230
|
+
*/
|
|
2220
2231
|
function validateVoicePayload(value, problems) {
|
|
2232
|
+
for (const key of [
|
|
2233
|
+
"extent",
|
|
2234
|
+
"sampling",
|
|
2235
|
+
"coordinateSpace",
|
|
2236
|
+
"external",
|
|
2237
|
+
"storageKey"
|
|
2238
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} belongs to the rendered Audio, not to the Voice identity`);
|
|
2221
2239
|
const voice = value.voice;
|
|
2222
|
-
if (voice === void 0)
|
|
2240
|
+
if (voice === void 0) {
|
|
2241
|
+
problems.push("voice is required; a Voice entity is its voice-library identity");
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2223
2244
|
if (!isRecord(voice)) {
|
|
2224
|
-
problems.push("voice must be an object
|
|
2245
|
+
problems.push("voice must be an object");
|
|
2225
2246
|
return;
|
|
2226
2247
|
}
|
|
2227
2248
|
if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
|
|
@@ -2353,7 +2374,6 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2353
2374
|
if ([
|
|
2354
2375
|
"video",
|
|
2355
2376
|
"audio",
|
|
2356
|
-
"voice",
|
|
2357
2377
|
"image",
|
|
2358
2378
|
"caption",
|
|
2359
2379
|
"axvideo"
|
|
@@ -2417,7 +2437,7 @@ function collectRelatedEntities(entityRefs, index) {
|
|
|
2417
2437
|
};
|
|
2418
2438
|
}
|
|
2419
2439
|
function expectedSequenceShape(kind) {
|
|
2420
|
-
if (kind === "video" || kind === "audio" || kind === "
|
|
2440
|
+
if (kind === "video" || kind === "audio" || kind === "caption") return {
|
|
2421
2441
|
extent: "bounded",
|
|
2422
2442
|
sampling: "native"
|
|
2423
2443
|
};
|
|
@@ -2458,7 +2478,7 @@ const generatedRelationSpec = Object.freeze({
|
|
|
2458
2478
|
});
|
|
2459
2479
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
2460
2480
|
kind: "caption-alignment",
|
|
2461
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"
|
|
2481
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"])),
|
|
2462
2482
|
validateMetadata: isCaptionAlignmentMetadata
|
|
2463
2483
|
});
|
|
2464
2484
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
@@ -2467,20 +2487,29 @@ const clipAnchorRelationSpec = Object.freeze({
|
|
|
2467
2487
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
2468
2488
|
validateMetadata: isEmptyMetadata
|
|
2469
2489
|
});
|
|
2470
|
-
/**
|
|
2490
|
+
/**
|
|
2491
|
+
* The rendered voiceover Audio and the PhoneticScript it was synthesized from;
|
|
2492
|
+
* kinds determine roles regardless of endpoint positions.
|
|
2493
|
+
*/
|
|
2471
2494
|
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
2472
2495
|
kind: "phonetic-script-render",
|
|
2473
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["
|
|
2496
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio"]), new Set(["phonetic-script"])),
|
|
2497
|
+
validateMetadata: isEmptyMetadata
|
|
2498
|
+
});
|
|
2499
|
+
/**
|
|
2500
|
+
* The timbre identity a rendered voiceover Audio was synthesized with. Voice
|
|
2501
|
+
* stays an identity: it never carries the audio itself, so the link between the
|
|
2502
|
+
* two is a Relation rather than one shared entity.
|
|
2503
|
+
*/
|
|
2504
|
+
const voiceTimbreRelationSpec = Object.freeze({
|
|
2505
|
+
kind: "voice-timbre",
|
|
2506
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio"]), new Set(["voice"])),
|
|
2474
2507
|
validateMetadata: isEmptyMetadata
|
|
2475
2508
|
});
|
|
2476
2509
|
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
2477
2510
|
const audioScriptSourceRelationSpec = Object.freeze({
|
|
2478
2511
|
kind: "audio-script-source",
|
|
2479
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
2480
|
-
"audio",
|
|
2481
|
-
"video",
|
|
2482
|
-
"voice"
|
|
2483
|
-
])),
|
|
2512
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
|
|
2484
2513
|
validateMetadata: isEmptyMetadata
|
|
2485
2514
|
});
|
|
2486
2515
|
/**
|
|
@@ -2507,6 +2536,7 @@ const builtInRelationSpecs = Object.freeze([
|
|
|
2507
2536
|
captionAlignmentRelationSpec,
|
|
2508
2537
|
clipAnchorRelationSpec,
|
|
2509
2538
|
phoneticScriptRenderRelationSpec,
|
|
2539
|
+
voiceTimbreRelationSpec,
|
|
2510
2540
|
audioScriptSourceRelationSpec,
|
|
2511
2541
|
audioScriptMarkerRelationSpec
|
|
2512
2542
|
]);
|
|
@@ -2536,7 +2566,7 @@ function hasAssetAndSequence(endpoints) {
|
|
|
2536
2566
|
return first.entityKind === "asset" && hasSequence(second) || second.entityKind === "asset" && hasSequence(first);
|
|
2537
2567
|
}
|
|
2538
2568
|
function isGeneratedMedia(entity) {
|
|
2539
|
-
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio"
|
|
2569
|
+
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio";
|
|
2540
2570
|
}
|
|
2541
2571
|
function isEmptyMetadata(value) {
|
|
2542
2572
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -2966,7 +2996,7 @@ function resolveGraphLayout(graph) {
|
|
|
2966
2996
|
if (clip.entityKind !== "clip" || graph.adjacent(clip.entityId, "track-clip").length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
|
|
2967
2997
|
const marker = graph.one(clip.entityId, "clip-marker", "sequence-marker");
|
|
2968
2998
|
const content = graph.one(marker.entityId, "marker-content");
|
|
2969
|
-
if (!(typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["
|
|
2999
|
+
if (!(typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["audio"] : typedRole === "caption" ? ["caption"] : ["audio"]).includes(content.entityKind)) fail(`unsupported compatibility content kind ${content.entityKind} on ${typedRole} Track`);
|
|
2970
3000
|
const space = content.payload.coordinateSpace;
|
|
2971
3001
|
if (space !== "ms" && space !== "milliseconds" && (!object(space) || space.unit !== "ms" && space.unit !== "milliseconds")) fail(`media ${content.entityId} must state millisecond coordinates for this reader`);
|
|
2972
3002
|
const source = range(marker.payload.sourceRange, "sourceRange");
|
|
@@ -3045,7 +3075,7 @@ function resolveGraphLayout(graph) {
|
|
|
3045
3075
|
let start = fact.absoluteStart ?? 0;
|
|
3046
3076
|
if (fact.mode === "anchored") {
|
|
3047
3077
|
const host = resolve(fact.anchorId);
|
|
3048
|
-
if (fact.role === "speech" && !["video_clip", "bgm"].includes(facts.get(host.clipEntityId).role)) fail("An anchored
|
|
3078
|
+
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
3079
|
start = host.start + fact.anchorOffset;
|
|
3050
3080
|
} else if (fact.predecessor !== void 0) {
|
|
3051
3081
|
const prior = resolve(fact.predecessor);
|
|
@@ -3094,7 +3124,7 @@ function resolveGraphLayout(graph) {
|
|
|
3094
3124
|
* consulted as an editing fact. Peer IDs appear only in this derived read
|
|
3095
3125
|
* contract, reconstructed from relations; they are not stored in payloads.
|
|
3096
3126
|
*/
|
|
3097
|
-
function projectEntityTimeline(rows
|
|
3127
|
+
function projectEntityTimeline(rows) {
|
|
3098
3128
|
const graph = new EntityProjectionGraph(rows);
|
|
3099
3129
|
const layout = resolveGraphLayout(graph);
|
|
3100
3130
|
for (const track of layout.tracks) {
|
|
@@ -3129,8 +3159,11 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3129
3159
|
const asset = mediaAssetFacts(graph, content);
|
|
3130
3160
|
requireUntrimmedAudio(content, marker, placement);
|
|
3131
3161
|
const external = externalLocator(asset, "memota-speech");
|
|
3132
|
-
const
|
|
3133
|
-
const
|
|
3162
|
+
const renderEdges = graph.adjacent(content.entityId, "phonetic-script-render");
|
|
3163
|
+
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3164
|
+
const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
|
|
3165
|
+
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3166
|
+
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3134
3167
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
3135
3168
|
const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
|
|
3136
3169
|
parts[clip.entityId] = { speech: {
|
|
@@ -3183,10 +3216,6 @@ function projectEntityTimeline(rows, meta) {
|
|
|
3183
3216
|
}
|
|
3184
3217
|
}
|
|
3185
3218
|
const document = {
|
|
3186
|
-
meta: {
|
|
3187
|
-
...structuredClone(meta),
|
|
3188
|
-
schema_version: ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION
|
|
3189
|
-
},
|
|
3190
3219
|
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
3220
|
tracks: layout.tracks.map((track) => ({
|
|
3192
3221
|
id: track.entityId,
|
|
@@ -4271,13 +4300,37 @@ function materialize(doc, allowSelectionConflicts = false) {
|
|
|
4271
4300
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4272
4301
|
}
|
|
4273
4302
|
//#endregion
|
|
4303
|
+
//#region src/document/document-mutation-guard.ts
|
|
4304
|
+
/**
|
|
4305
|
+
* @internal
|
|
4306
|
+
* Shared by synchronous mutation entry points for one document. The session
|
|
4307
|
+
* closes it before teardown callbacks can attempt another write.
|
|
4308
|
+
*/
|
|
4309
|
+
var DocumentMutationGuard = class {
|
|
4310
|
+
running = false;
|
|
4311
|
+
closed = false;
|
|
4312
|
+
run(mutation) {
|
|
4313
|
+
if (this.closed) throw new Error("mengine document is disposed");
|
|
4314
|
+
if (this.running) throw new Error("mengine document mutation is already running");
|
|
4315
|
+
this.running = true;
|
|
4316
|
+
try {
|
|
4317
|
+
return mutation();
|
|
4318
|
+
} finally {
|
|
4319
|
+
this.running = false;
|
|
4320
|
+
}
|
|
4321
|
+
}
|
|
4322
|
+
close() {
|
|
4323
|
+
this.closed = true;
|
|
4324
|
+
}
|
|
4325
|
+
};
|
|
4326
|
+
//#endregion
|
|
4274
4327
|
//#region src/document/mirror-read.ts
|
|
4275
4328
|
/**
|
|
4276
4329
|
* Project the mirror state (`VideoDocumentDraft`) into the authoritative
|
|
4277
4330
|
* `VideoDocument`. The storage shape is isomorphic to the domain shape (RFC 03
|
|
4278
|
-
* §4, reference/17 §4:
|
|
4331
|
+
* §4, reference/17 §4: timeline + a single `tracks` list + part_library), so this
|
|
4279
4332
|
* is a near-identity — it reads `time_position` / `fallback_abs_ms` JSON blobs
|
|
4280
|
-
* back into structured values and
|
|
4333
|
+
* back into structured values and selects content fields. Legacy meta is ignored.
|
|
4281
4334
|
*
|
|
4282
4335
|
* It maps only authoritative facts (RFC 02 §6): `part_id` + `time_position`. The
|
|
4283
4336
|
* projection-derived `VideoDraft` read-view (absolute time, `part_aggregations`,
|
|
@@ -4294,20 +4347,8 @@ function readVideoDocumentFromDraft(draft) {
|
|
|
4294
4347
|
if (part != null) partLibrary[partId] = part;
|
|
4295
4348
|
}
|
|
4296
4349
|
const timeline = draft.timeline;
|
|
4297
|
-
const hasTimeline = timeline?.unit_time_ms != null;
|
|
4298
|
-
const meta = draft.meta;
|
|
4299
4350
|
return {
|
|
4300
|
-
|
|
4301
|
-
schema_version: meta?.schema_version === "video-document/entity-projection-v1" ? ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION : VIDEO_DOCUMENT_SCHEMA_VERSION,
|
|
4302
|
-
draft_id: emptyToUndefined(meta?.draft_id),
|
|
4303
|
-
project_id: emptyToUndefined(meta?.project_id),
|
|
4304
|
-
owner_id: emptyToUndefined(meta?.owner_id),
|
|
4305
|
-
thumbnail_storage_key: emptyToUndefined(meta?.thumbnail_storage_key),
|
|
4306
|
-
chat_session_id: emptyToUndefined(meta?.chat_session_id),
|
|
4307
|
-
video_creation_settings: meta?.video_creation_settings ?? void 0,
|
|
4308
|
-
version: meta?.version ?? void 0
|
|
4309
|
-
},
|
|
4310
|
-
timeline: hasTimeline ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4351
|
+
timeline: timeline?.unit_time_ms != null ? { unit_time_ms: timeline.unit_time_ms } : void 0,
|
|
4311
4352
|
tracks: rowsToTracks(draft.tracks),
|
|
4312
4353
|
part_library: partLibrary
|
|
4313
4354
|
};
|
|
@@ -4331,9 +4372,6 @@ function rowToTrack(row) {
|
|
|
4331
4372
|
})
|
|
4332
4373
|
};
|
|
4333
4374
|
}
|
|
4334
|
-
function emptyToUndefined(value) {
|
|
4335
|
-
return value == null || value === "" ? void 0 : value;
|
|
4336
|
-
}
|
|
4337
4375
|
//#endregion
|
|
4338
4376
|
//#region src/document/mirror-adapter.ts
|
|
4339
4377
|
/**
|
|
@@ -4346,33 +4384,50 @@ function emptyToUndefined(value) {
|
|
|
4346
4384
|
* - `transact(edit, audit)` runs the whole op in one `mirror.setState` callback:
|
|
4347
4385
|
* one diff, one `doc.commit` carrying the audit message. The callback edits an
|
|
4348
4386
|
* immer draft, so a throw inside it discards the draft and never touches Loro
|
|
4349
|
-
* (natural rollback)
|
|
4387
|
+
* (natural rollback) without a document checkout or compensating commit.
|
|
4350
4388
|
* - mirror's `idSelector` (track items keyed by `part_id`) diffs reorders to
|
|
4351
4389
|
* real Loro `move` ops on every lane, so per-item CRDT identity survives on
|
|
4352
4390
|
* main and secondary tracks alike.
|
|
4353
4391
|
*/
|
|
4354
4392
|
var MirrorVideoDocumentAdapter = class {
|
|
4355
4393
|
doc;
|
|
4394
|
+
mutationGuard;
|
|
4356
4395
|
mirror;
|
|
4357
|
-
|
|
4396
|
+
disposed = false;
|
|
4397
|
+
constructor(doc, mutationGuard = new DocumentMutationGuard()) {
|
|
4358
4398
|
this.doc = doc;
|
|
4399
|
+
this.mutationGuard = mutationGuard;
|
|
4359
4400
|
this.mirror = new Mirror({
|
|
4360
4401
|
doc,
|
|
4361
|
-
schema: videoDocumentMirrorSchema
|
|
4402
|
+
schema: videoDocumentMirrorSchema,
|
|
4403
|
+
validateUpdates: false
|
|
4362
4404
|
});
|
|
4363
4405
|
}
|
|
4364
4406
|
snapshot() {
|
|
4365
|
-
|
|
4366
|
-
if (this.
|
|
4367
|
-
return
|
|
4407
|
+
this.assertActive();
|
|
4408
|
+
if (this.isEntityDocument()) return projectEntityTimeline(readEntityDocument(this.doc).rows);
|
|
4409
|
+
return readVideoDocumentFromDraft(this.mirror.getState());
|
|
4368
4410
|
}
|
|
4369
4411
|
/**
|
|
4370
|
-
*
|
|
4371
|
-
*
|
|
4372
|
-
*
|
|
4412
|
+
* Native Entity authority: placement is already solved by the Entity graph.
|
|
4413
|
+
*
|
|
4414
|
+
* The root check goes through `getShallowValue` first because `getMap` creates
|
|
4415
|
+
* the container on access — probing a legacy document for `medeo` would
|
|
4416
|
+
* otherwise mutate it, which a read must never do.
|
|
4373
4417
|
*/
|
|
4374
|
-
|
|
4375
|
-
|
|
4418
|
+
isEntityDocument() {
|
|
4419
|
+
const roots = this.doc.getShallowValue();
|
|
4420
|
+
if (!Object.hasOwn(roots, "medeo")) return false;
|
|
4421
|
+
return this.doc.getMap("medeo").get("schema") === LORO_ENTITY_SCHEMA;
|
|
4422
|
+
}
|
|
4423
|
+
/**
|
|
4424
|
+
* Release mirror subscriptions; do not reuse this adapter afterwards.
|
|
4425
|
+
* The caller still owns the borrowed LoroDoc.
|
|
4426
|
+
*/
|
|
4427
|
+
dispose() {
|
|
4428
|
+
if (this.disposed) return;
|
|
4429
|
+
this.disposed = true;
|
|
4430
|
+
this.mirror.dispose();
|
|
4376
4431
|
}
|
|
4377
4432
|
/**
|
|
4378
4433
|
* Apply one op as a single transaction. `edit` mutates the immer draft; mirror
|
|
@@ -4381,80 +4436,71 @@ var MirrorVideoDocumentAdapter = class {
|
|
|
4381
4436
|
* skips the commit — matching the prior "empty op leaves no audit" behavior.
|
|
4382
4437
|
*/
|
|
4383
4438
|
transact(edit, audit) {
|
|
4384
|
-
|
|
4385
|
-
this.
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4439
|
+
this.assertActive();
|
|
4440
|
+
if (this.isEntityDocument()) throw new Error("Entity documents are read-only through this adapter; edit the Medeo entities instead");
|
|
4441
|
+
this.mutationGuard.run(() => {
|
|
4442
|
+
this.mirror.setState((draft) => {
|
|
4443
|
+
edit(draft);
|
|
4444
|
+
const result = validateSchema(videoDocumentMirrorSchema, {
|
|
4445
|
+
timeline: draft.timeline,
|
|
4446
|
+
tracks: draft.tracks,
|
|
4447
|
+
part_library: draft.part_library
|
|
4448
|
+
});
|
|
4449
|
+
if (!result.valid) throw new Error(`Invalid content update: ${result.errors?.join("; ")}`);
|
|
4450
|
+
}, {
|
|
4451
|
+
origin: "mengine.semantic_editor",
|
|
4452
|
+
message: JSON.stringify({
|
|
4453
|
+
semantic_op: audit.kind,
|
|
4454
|
+
payload: audit.payload,
|
|
4455
|
+
intent: audit.intent ?? null,
|
|
4456
|
+
actor: audit.actor ?? null
|
|
4457
|
+
})
|
|
4458
|
+
});
|
|
4395
4459
|
});
|
|
4396
4460
|
}
|
|
4461
|
+
assertActive() {
|
|
4462
|
+
if (this.disposed) throw new Error("mengine document adapter is disposed");
|
|
4463
|
+
}
|
|
4397
4464
|
};
|
|
4398
4465
|
/** Build a fresh Loro doc seeded with `document` through the mirror. */
|
|
4399
4466
|
function createMirrorVideoDocument(document, options = {}) {
|
|
4400
4467
|
assertValidVideoDocument(document);
|
|
4401
4468
|
const doc = new LoroDoc();
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4469
|
+
try {
|
|
4470
|
+
if (options.peerId != null) doc.setPeerId(options.peerId);
|
|
4471
|
+
const mirror = new Mirror({
|
|
4472
|
+
doc,
|
|
4473
|
+
schema: videoDocumentMirrorSchema
|
|
4474
|
+
});
|
|
4475
|
+
try {
|
|
4476
|
+
mirror.setState((draft) => {
|
|
4477
|
+
seedDraft(draft, document);
|
|
4478
|
+
}, { origin: options.origin ?? "mengine.bootstrap" });
|
|
4479
|
+
return doc;
|
|
4480
|
+
} finally {
|
|
4481
|
+
mirror.dispose();
|
|
4482
|
+
}
|
|
4483
|
+
} catch (error) {
|
|
4484
|
+
doc.free();
|
|
4485
|
+
throw error;
|
|
4486
|
+
}
|
|
4410
4487
|
}
|
|
4411
4488
|
/** Build a `MirrorVideoDocumentAdapter` over a fresh doc seeded with `document`. */
|
|
4412
4489
|
function createMirrorVideoDocumentAdapter(document, options) {
|
|
4413
4490
|
return new MirrorVideoDocumentAdapter(createMirrorVideoDocument(document, options));
|
|
4414
4491
|
}
|
|
4415
|
-
/**
|
|
4416
|
-
function
|
|
4417
|
-
if (projection.meta.schema_version !== "video-document/entity-projection-v1") throw new Error("Expected an entity timeline projection");
|
|
4418
|
-
if (!Number.isSafeInteger(entityRevision) || entityRevision < 1) throw new Error("Expected a positive entity revision");
|
|
4419
|
-
assertValidVideoDocument(projection);
|
|
4420
|
-
const from = doc.oplogVersion();
|
|
4421
|
-
new Mirror({
|
|
4422
|
-
doc,
|
|
4423
|
-
schema: videoDocumentMirrorSchema
|
|
4424
|
-
}).setState((draft) => writeVideoDocumentToDraft(draft, {
|
|
4425
|
-
...projection,
|
|
4426
|
-
meta: {
|
|
4427
|
-
...projection.meta,
|
|
4428
|
-
version: entityRevision
|
|
4429
|
-
}
|
|
4430
|
-
}), {
|
|
4431
|
-
origin: "mengine.entity_projection",
|
|
4432
|
-
message: JSON.stringify({
|
|
4433
|
-
semantic_op: "EntityTimelineProjection",
|
|
4434
|
-
entity_revision: entityRevision
|
|
4435
|
-
})
|
|
4436
|
-
});
|
|
4437
|
-
return doc.export({
|
|
4438
|
-
mode: "update",
|
|
4439
|
-
from
|
|
4440
|
-
});
|
|
4441
|
-
}
|
|
4442
|
-
/** Write a whole `VideoDocument` into a draft (seed a fresh doc / plain-memory state). */
|
|
4443
|
-
function writeVideoDocumentToDraft(draft, document) {
|
|
4444
|
-
draft.meta = {
|
|
4445
|
-
schema_version: document.meta.schema_version,
|
|
4446
|
-
draft_id: document.meta.draft_id ?? void 0,
|
|
4447
|
-
project_id: document.meta.project_id ?? void 0,
|
|
4448
|
-
owner_id: document.meta.owner_id ?? void 0,
|
|
4449
|
-
thumbnail_storage_key: document.meta.thumbnail_storage_key ?? void 0,
|
|
4450
|
-
chat_session_id: document.meta.chat_session_id ?? void 0,
|
|
4451
|
-
video_creation_settings: document.meta.video_creation_settings ?? void 0,
|
|
4452
|
-
version: document.meta.version ?? void 0
|
|
4453
|
-
};
|
|
4492
|
+
/** Write a whole `VideoDocument` into the mirror draft (seed a fresh doc / plain-memory state). */
|
|
4493
|
+
function seedDraft(draft, document) {
|
|
4454
4494
|
draft.timeline = { unit_time_ms: document.timeline?.unit_time_ms ?? void 0 };
|
|
4455
4495
|
draft.tracks = (document.tracks ?? []).map(toTrackRow);
|
|
4456
4496
|
draft.part_library = {};
|
|
4457
|
-
for (const [partId, part] of Object.entries(document.part_library ?? {}))
|
|
4497
|
+
for (const [partId, part] of Object.entries(document.part_library ?? {})) {
|
|
4498
|
+
const createdPart = part.caption != null ? { caption: {
|
|
4499
|
+
...part.caption,
|
|
4500
|
+
style: part.caption.style ?? {}
|
|
4501
|
+
} } : part;
|
|
4502
|
+
draft.part_library[partId] = partUnionToDraft(createdPart);
|
|
4503
|
+
}
|
|
4458
4504
|
}
|
|
4459
4505
|
/** Map a domain `Track` to a draft track row (no lane/lane_order — see schema). */
|
|
4460
4506
|
function toTrackRow(track) {
|
|
@@ -4538,10 +4584,12 @@ var PlainMemoryAdapter = class {
|
|
|
4538
4584
|
* use this so every minted id is appended to the current transact's list.
|
|
4539
4585
|
*/
|
|
4540
4586
|
idFactory;
|
|
4587
|
+
readOnly;
|
|
4541
4588
|
constructor(document, options) {
|
|
4542
4589
|
assertValidVideoDocument(document);
|
|
4590
|
+
this.readOnly = options?.readOnly === true;
|
|
4543
4591
|
const draft = {};
|
|
4544
|
-
|
|
4592
|
+
seedDraft(draft, document);
|
|
4545
4593
|
this.state = draft;
|
|
4546
4594
|
this.baseIdFactory = options?.idFactory ?? generatePartId;
|
|
4547
4595
|
this.idFactory = (prefix) => {
|
|
@@ -4554,7 +4602,7 @@ var PlainMemoryAdapter = class {
|
|
|
4554
4602
|
return this._journal;
|
|
4555
4603
|
}
|
|
4556
4604
|
hasContent() {
|
|
4557
|
-
return
|
|
4605
|
+
return this.state.tracks !== void 0 || this.state.timeline !== void 0;
|
|
4558
4606
|
}
|
|
4559
4607
|
snapshot() {
|
|
4560
4608
|
return readVideoDocumentFromDraft(this.state);
|
|
@@ -4565,7 +4613,7 @@ var PlainMemoryAdapter = class {
|
|
|
4565
4613
|
* no structural change — skip journal, matching mirror "no change, no commit".
|
|
4566
4614
|
*/
|
|
4567
4615
|
transact(edit, audit) {
|
|
4568
|
-
if (this.
|
|
4616
|
+
if (this.readOnly) throw new Error("Entity documents are read-only here; edit the Medeo entities instead");
|
|
4569
4617
|
this.pendingIds = [];
|
|
4570
4618
|
try {
|
|
4571
4619
|
const next = produce(this.state, edit);
|
|
@@ -4586,4 +4634,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4586
4634
|
return new PlainMemoryAdapter(document, options);
|
|
4587
4635
|
}
|
|
4588
4636
|
//#endregion
|
|
4589
|
-
export {
|
|
4637
|
+
export { findLaneTrack as $, assemblePhoneticScriptContent as A, isMediaAssetVariantKind as B, EntityProjectionGraph as C, ScriptCompositionError as D, decodeEntityRelationRows as E, variantBaseEntityIds as F, toVideoDocument as G, buildSpeechHostMap as H, isJsonObject as I, validateVideoDocument as J, VideoDocumentValidationError as K, createEntityId as L, findComposedAudioScript as M, selectAudioScriptSegment as N, assembleCaptionContent as O, updateEntityFields as P, ensureLaneTrack as Q, createRelationId as R, projectEntityTimeline as S, bytesToBase64 as St, EntityTimelineProjectionError as T, derivePositionFromAbs as U, buildInitialVideoDocument as V, fromVideoDocument as W, videoDocumentSchema as X, partUnionSchema as Y, LANE_KINDS_IN_STACK_ORDER as Z, hasAudioScriptAsset as _, speedOf as _t, createMirrorVideoDocument as a, fillMainTrackTimeGaps as at, assertFieldChanges as b, videoDocumentMirrorSchema as bt, DocumentMutationGuard as c, resolveAllSpeechOverlaps as ct, LoroEntityDraft as d, TIMELINE_SKELETON_DURATION_MS as dt, hasResolvedPlacement as et, readEntityDocument as f, isEmptyVideoClip as ft, audioScriptAssetHash as g, effectiveVideoClipDurationMs as gt, audioScriptAssetFields as h, DEFAULT_UNIT_TIME_MS as ht, MirrorVideoDocumentAdapter as i, arrangeMainTrackSeamlessly as it, assembleScriptText as j, assembleEntityContent as k, LORO_ENTITY_SCHEMA as l, resolveSpeechOverlapByShiftingVideos as lt, audioScriptAssetContent as m, safeDurationMs as mt, createPlainMemoryAdapter as n, solveVideoDocument as nt, createMirrorVideoDocumentAdapter as o, reassignSpeechesToVideoClipsByTime as ot, entityOrderGroups as p, partDurationMs as pt, assertValidVideoDocument as q, generatePartId as r, cascadeAfterVideoClipChanges as rt, readVideoDocumentFromDraft as s, recalculateTimelineDuration as st, PlainMemoryAdapter as t, laneTrackId as tt, LoroEntityDocument as u, syncAggregatedClipsTimePosition as ut, equalJson as v, partUnionToDraft as vt, resolveEntityTimelineLayout as w, resolveFieldPath as x, base64ToBytes as xt, applyFieldChanges as y, recordEntries as yt, hasSequence as z };
|