@mengine/medeo-tool 1.4.1-alpha.4 → 2.0.1-alpha.10
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 +9 -0
- package/dist/{entity-contract-DQ56Ihrh.d.mts → entity-contract-Cf3AiSe7.d.mts} +46 -44
- package/dist/{entity-sandbox-OArq9NSH.mjs → entity-sandbox-BTR2cRl1.mjs} +274 -170
- package/dist/entity-sandbox-BTR2cRl1.mjs.map +1 -0
- package/dist/index.d.mts +27 -10
- package/dist/index.mjs +120 -130
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +43 -45
- package/dist/worker-entry.d.mts +2 -1
- package/dist/worker-entry.mjs +100 -93
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/entity-sandbox-OArq9NSH.mjs.map +0 -1
|
@@ -1,35 +1,94 @@
|
|
|
1
1
|
import { applyFieldChanges, assertCanonicalEditorResources, assertFieldChanges, ensureEditorFoundation, importMediaAsset, readDocumentAudioScript } from "@mengine/medeo-client";
|
|
2
2
|
//#region ../medeo-dsl/src/entities.ts
|
|
3
|
+
/**
|
|
4
|
+
* What every known entity kind declares.
|
|
5
|
+
*
|
|
6
|
+
* `kinds` is a multi-declaration: every entry holds at the same time, and each
|
|
7
|
+
* is a property shared with other entity kinds — what the object *is* comes
|
|
8
|
+
* from `entityKind` and is never repeated here. The order carries no priority,
|
|
9
|
+
* endpoint role, or edit sequence — an operation asks whether the entity
|
|
10
|
+
* declares the kind it needs, never what its "main type" is. An entity kind
|
|
11
|
+
* that shares nothing declares nothing.
|
|
12
|
+
*
|
|
13
|
+
* `Caption` declares `Sequence` on purpose: it owns the display timing of the
|
|
14
|
+
* one AudioScript segment it shows, which is what admits it into a Clip.
|
|
15
|
+
* `Voice` declares no `Sequence` — it is a timbre identity, not playable
|
|
16
|
+
* content; a rendered voiceover is an `audio` entity.
|
|
17
|
+
*
|
|
18
|
+
* No declaration says anything about resources: whether an entity has one, and
|
|
19
|
+
* which, is the `from-asset` Relation's answer alone.
|
|
20
|
+
*/
|
|
21
|
+
const ENTITY_KINDS = Object.freeze({
|
|
22
|
+
axvideo: Object.freeze([
|
|
23
|
+
"Container",
|
|
24
|
+
"Sequence",
|
|
25
|
+
"Visual",
|
|
26
|
+
"Audible"
|
|
27
|
+
]),
|
|
28
|
+
timeline: Object.freeze(["Container"]),
|
|
29
|
+
track: Object.freeze(["Container"]),
|
|
30
|
+
clip: Object.freeze(["Container"]),
|
|
31
|
+
asset: Object.freeze([]),
|
|
32
|
+
video: Object.freeze([
|
|
33
|
+
"Sequence",
|
|
34
|
+
"Visual",
|
|
35
|
+
"Audible",
|
|
36
|
+
"FromAsset"
|
|
37
|
+
]),
|
|
38
|
+
audio: Object.freeze([
|
|
39
|
+
"Sequence",
|
|
40
|
+
"Audible",
|
|
41
|
+
"FromAsset"
|
|
42
|
+
]),
|
|
43
|
+
image: Object.freeze([
|
|
44
|
+
"Sequence",
|
|
45
|
+
"Visual",
|
|
46
|
+
"FromAsset"
|
|
47
|
+
]),
|
|
48
|
+
voice: Object.freeze(["Container", "IPBound"]),
|
|
49
|
+
"sequence-marker": Object.freeze([]),
|
|
50
|
+
viewport: Object.freeze([]),
|
|
51
|
+
"audio-script": Object.freeze(["SegmentText", "FromAsset"]),
|
|
52
|
+
"phonetic-script": Object.freeze(["SegmentText"]),
|
|
53
|
+
caption: Object.freeze([
|
|
54
|
+
"SegmentText",
|
|
55
|
+
"Sequence",
|
|
56
|
+
"FromAsset"
|
|
57
|
+
])
|
|
58
|
+
});
|
|
59
|
+
/** What a known kind declares; extension kinds declare nothing through this table. */
|
|
60
|
+
function kindsOf(entityKind) {
|
|
61
|
+
return isKnownEntityKind(entityKind) ? ENTITY_KINDS[entityKind] : [];
|
|
62
|
+
}
|
|
63
|
+
function declaresKind(entityKind, kind) {
|
|
64
|
+
return kindsOf(entityKind).includes(kind);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sequence admission is a declaration, not a shape: a known kind is admitted
|
|
68
|
+
* because its `kinds` say so, and a malformed payload is a separate, louder
|
|
69
|
+
* failure than a silently non-Sequence entity. Extension kinds declare nothing
|
|
70
|
+
* through the table and stay structurally detected.
|
|
71
|
+
*/
|
|
3
72
|
function hasSequence(entity) {
|
|
4
|
-
if (
|
|
73
|
+
if (isReservedEntityKind(entity.entityKind)) return false;
|
|
74
|
+
if (isKnownEntityKind(entity.entityKind)) return declaresKind(entity.entityKind, "Sequence");
|
|
5
75
|
return isSequenceFields(entity);
|
|
6
76
|
}
|
|
7
77
|
function isSequenceFields(value) {
|
|
8
78
|
if (!isRecord$1(value)) return false;
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
if (extent.kind === "bounded" && (!("end" in extent) || extent.end === void 0)) return false;
|
|
12
|
-
if (extent.kind === "unbounded" && "end" in extent) return false;
|
|
13
|
-
if (extent.kind !== "bounded" && extent.kind !== "unbounded") return false;
|
|
14
|
-
if (value.sampling !== "native" && value.sampling !== "constant" && value.sampling !== "derived") return false;
|
|
15
|
-
return "coordinateSpace" in value && value.coordinateSpace !== void 0;
|
|
16
|
-
}
|
|
17
|
-
function isKnownSequenceKind(kind) {
|
|
18
|
-
return isMediaAssetVariantKind(kind) || kind === "caption" || kind === "axvideo";
|
|
79
|
+
const duration = value.durationMs;
|
|
80
|
+
return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
|
|
19
81
|
}
|
|
20
82
|
function isMediaAssetVariantKind(kind) {
|
|
21
|
-
return kind === "video" || kind === "image" || kind === "audio"
|
|
83
|
+
return kind === "video" || kind === "image" || kind === "audio";
|
|
22
84
|
}
|
|
23
85
|
function isKnownEntityKind(kind) {
|
|
24
|
-
return
|
|
86
|
+
return Object.hasOwn(ENTITY_KINDS, kind);
|
|
25
87
|
}
|
|
26
88
|
function isReservedEntityKind(kind) {
|
|
27
89
|
const normalized = kind.toLowerCase().replaceAll("-", "").replaceAll("_", "");
|
|
28
90
|
return normalized === "speech" || normalized === "videodocument";
|
|
29
91
|
}
|
|
30
|
-
function isKnownNonSequenceKind(kind) {
|
|
31
|
-
return kind === "timeline" || kind === "track" || kind === "clip" || kind === "asset" || kind === "sequence-marker" || kind === "viewport" || kind === "audio-script" || kind === "phonetic-script";
|
|
32
|
-
}
|
|
33
92
|
function isRecord$1(value) {
|
|
34
93
|
return typeof value === "object" && value != null && !Array.isArray(value);
|
|
35
94
|
}
|
|
@@ -209,15 +268,18 @@ function assembleCaptionContent(rows, captionEntityId) {
|
|
|
209
268
|
requireEntityKind(rows, captionEntityId, "caption");
|
|
210
269
|
const caption = assembleEntityContent(rows, captionEntityId);
|
|
211
270
|
const script = findComposedAudioScript(rows, captionEntityId, "caption");
|
|
212
|
-
const
|
|
271
|
+
const selection = captionSelection(caption);
|
|
272
|
+
const composed = {
|
|
213
273
|
...script,
|
|
214
274
|
payload: caption.payload
|
|
215
|
-
}
|
|
275
|
+
};
|
|
276
|
+
const segment = selectAudioScriptSegment(composed, selection);
|
|
216
277
|
return {
|
|
217
278
|
caption,
|
|
218
279
|
audioScript: script,
|
|
219
|
-
segments,
|
|
220
|
-
|
|
280
|
+
segments: [segment],
|
|
281
|
+
segmentIndex: scriptSegments(composed).findIndex((entry) => entry.segmentId === selection.segmentId),
|
|
282
|
+
text: segment.text
|
|
221
283
|
};
|
|
222
284
|
}
|
|
223
285
|
/**
|
|
@@ -239,20 +301,18 @@ function assemblePhoneticScriptContent(rows, phoneticScriptEntityId) {
|
|
|
239
301
|
text: segments.map((segment) => segment.text).join("")
|
|
240
302
|
};
|
|
241
303
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (!selections.every(isSegmentSelection)) throw new ScriptCompositionError("invalid_selection", caption.entityId, "Caption selections require a segmentId and optional textRange");
|
|
246
|
-
return selections;
|
|
304
|
+
function captionSelection(caption) {
|
|
305
|
+
if (Object.hasOwn(caption.payload, "selections") || !isSegmentSelection(caption.payload.selection)) throw new ScriptCompositionError("invalid_selection", caption.entityId, "Caption requires one selection object with segmentId and optional textRange; selections arrays are forbidden");
|
|
306
|
+
return caption.payload.selection;
|
|
247
307
|
}
|
|
248
308
|
function isSegmentSelection(value) {
|
|
249
309
|
return typeof value === "object" && value != null && !Array.isArray(value) && typeof value.segmentId === "string" && value.segmentId.trim() !== "" && Object.keys(value).every((key) => key === "segmentId" || key === "textRange");
|
|
250
310
|
}
|
|
251
311
|
/** Select source text without creating another authoritative text field. */
|
|
252
|
-
function
|
|
253
|
-
if (!
|
|
312
|
+
function selectAudioScriptSegment(script, selection) {
|
|
313
|
+
if (!isSegmentSelection(selection)) throw new ScriptCompositionError("invalid_selection", script.entityId, "Caption requires one selection object with segmentId and optional textRange");
|
|
254
314
|
const bySegmentId = new Map(scriptSegments(script).map((segment) => [segment.segmentId, segment]));
|
|
255
|
-
const selected =
|
|
315
|
+
const selected = (() => {
|
|
256
316
|
const segment = bySegmentId.get(selection.segmentId);
|
|
257
317
|
if (segment === void 0) throw new ScriptCompositionError("unknown_segment", script.entityId, `Caption selection "${selection.segmentId}" does not name a segment of AudioScript "${script.entityId}"`);
|
|
258
318
|
if (selection.textRange === void 0) return segment;
|
|
@@ -263,8 +323,8 @@ function selectAudioScriptSegments(script, selections) {
|
|
|
263
323
|
...segment,
|
|
264
324
|
text: points.slice(range.start, range.end).join("")
|
|
265
325
|
};
|
|
266
|
-
});
|
|
267
|
-
if (!selected.
|
|
326
|
+
})();
|
|
327
|
+
if (!selected.text.trim()) throw new ScriptCompositionError("empty_selection", script.entityId, "Caption selection must contain visible source text");
|
|
268
328
|
return selected;
|
|
269
329
|
}
|
|
270
330
|
function requireEntityKind(rows, entityIdValue, entityKind) {
|
|
@@ -286,17 +346,15 @@ function entityToRow(entity) {
|
|
|
286
346
|
}
|
|
287
347
|
//#endregion
|
|
288
348
|
//#region ../medeo-dsl/src/invariants.ts
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
|
|
296
|
-
code: "media_physical_asset_forbidden",
|
|
349
|
+
/** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
|
|
350
|
+
function validateAssetSource(entity, index) {
|
|
351
|
+
if (entity.current().entityKind === "asset") return [];
|
|
352
|
+
if ([...index.relationsOf(entity)].filter((relation) => relation.kind === "from-asset").length <= 1) return [];
|
|
353
|
+
return [{
|
|
354
|
+
code: "asset_source_cardinality",
|
|
297
355
|
entityId: entity.entityId,
|
|
298
|
-
message: `
|
|
299
|
-
}
|
|
356
|
+
message: `Entity "${entity.entityId}" is made from more than one Asset`
|
|
357
|
+
}];
|
|
300
358
|
}
|
|
301
359
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
302
360
|
function validateEntityRelationSet(entityRefs, index, options) {
|
|
@@ -319,7 +377,7 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
319
377
|
const current = entity.current();
|
|
320
378
|
const entityIssues = validateEntity(entity, entityIds);
|
|
321
379
|
issues.push(...entityIssues);
|
|
322
|
-
|
|
380
|
+
issues.push(...validateAssetSource(entity, index));
|
|
323
381
|
if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
|
|
324
382
|
if (current.entityKind === "sequence-marker") {
|
|
325
383
|
const marker = entity;
|
|
@@ -544,24 +602,25 @@ function validateMarkerSourceBounds(marker, index, compare) {
|
|
|
544
602
|
const content = contentEdges[0]?.other(marker)?.deref()?.current();
|
|
545
603
|
if (content == null || !hasSequence(content)) return [];
|
|
546
604
|
const sourceRange = marker.current().sourceRange;
|
|
547
|
-
const
|
|
548
|
-
const
|
|
549
|
-
if (!Number.isNaN(
|
|
605
|
+
const startVsOrigin = compare(sourceRange.start, 0);
|
|
606
|
+
const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
|
|
607
|
+
if (!Number.isNaN(startVsOrigin) && startVsOrigin >= 0 && (endVsDuration == null || !Number.isNaN(endVsDuration) && endVsDuration <= 0)) return [];
|
|
550
608
|
return [{
|
|
551
609
|
code: "marker_source_out_of_bounds",
|
|
552
610
|
entityId: marker.entityId,
|
|
553
|
-
message: `Sequence Marker sourceRange must stay within content "${content.entityId}"
|
|
611
|
+
message: `Sequence Marker sourceRange must stay within content "${content.entityId}" duration`
|
|
554
612
|
}];
|
|
555
613
|
}
|
|
556
614
|
function validateSequenceComposition(entity) {
|
|
557
615
|
const current = entity.current();
|
|
558
|
-
const expected =
|
|
559
|
-
if (expected
|
|
560
|
-
|
|
616
|
+
const expected = expectedDurationShape(current.entityKind);
|
|
617
|
+
if (expected === void 0) return [];
|
|
618
|
+
const duration = current.durationMs;
|
|
619
|
+
if ((duration === null ? "none" : isPositiveInteger(duration) ? "own" : "invalid") === expected) return [];
|
|
561
620
|
return [{
|
|
562
621
|
code: "invalid_sequence_composition",
|
|
563
622
|
entityId: current.entityId,
|
|
564
|
-
message: `${current.entityKind} must
|
|
623
|
+
message: expected === "own" ? `${current.entityKind} must state its own durationMs in whole milliseconds` : `${current.entityKind} has no intrinsic length; durationMs must be null`
|
|
565
624
|
}];
|
|
566
625
|
}
|
|
567
626
|
function validateEntity(entity, entityIds = /* @__PURE__ */ new Set()) {
|
|
@@ -592,6 +651,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
592
651
|
if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
|
|
593
652
|
if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
594
653
|
for (const key of [
|
|
654
|
+
"durationMs",
|
|
595
655
|
"extent",
|
|
596
656
|
"sampling",
|
|
597
657
|
"coordinateSpace",
|
|
@@ -600,7 +660,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
600
660
|
"duration",
|
|
601
661
|
"startMs",
|
|
602
662
|
"endMs"
|
|
603
|
-
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an
|
|
663
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
|
|
604
664
|
}
|
|
605
665
|
switch (entity.entityKind) {
|
|
606
666
|
case "track":
|
|
@@ -610,19 +670,25 @@ function validateKnownEntityPayload(entity) {
|
|
|
610
670
|
break;
|
|
611
671
|
case "video":
|
|
612
672
|
case "audio":
|
|
613
|
-
|
|
673
|
+
validateDurationPayload(value, "own", problems);
|
|
674
|
+
break;
|
|
614
675
|
case "caption":
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
676
|
+
validateDurationPayload(value, "own", problems);
|
|
677
|
+
validateCaptionPayload(value, problems);
|
|
678
|
+
break;
|
|
679
|
+
case "voice":
|
|
680
|
+
validateVoicePayload(value, problems);
|
|
619
681
|
break;
|
|
620
682
|
case "image":
|
|
621
|
-
|
|
622
|
-
validateMediaAssetFields(value, problems);
|
|
683
|
+
validateDurationPayload(value, "none", problems);
|
|
623
684
|
break;
|
|
624
685
|
case "axvideo":
|
|
625
|
-
|
|
686
|
+
for (const retired of [
|
|
687
|
+
"extent",
|
|
688
|
+
"sampling",
|
|
689
|
+
"coordinateSpace",
|
|
690
|
+
"durationMs"
|
|
691
|
+
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
|
|
626
692
|
break;
|
|
627
693
|
case "sequence-marker":
|
|
628
694
|
validateMarkerPayload(value, problems);
|
|
@@ -647,17 +713,27 @@ function validateKnownEntityPayload(entity) {
|
|
|
647
713
|
}
|
|
648
714
|
return problems;
|
|
649
715
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Content states its own length and nothing else. The retired `extent` carried
|
|
718
|
+
* a start as well, which had no meaning of its own and drifted into holding a
|
|
719
|
+
* position; rejecting it keeps that mistake unwritable.
|
|
720
|
+
*/
|
|
721
|
+
function validateDurationPayload(value, expected, problems) {
|
|
722
|
+
for (const retired of [
|
|
723
|
+
"extent",
|
|
724
|
+
"sampling",
|
|
725
|
+
"coordinateSpace"
|
|
726
|
+
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is retired; state durationMs instead`);
|
|
727
|
+
if (!Object.hasOwn(value, "durationMs")) {
|
|
728
|
+
problems.push("durationMs is required");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
const duration = value.durationMs;
|
|
732
|
+
if (expected === "none") {
|
|
733
|
+
if (duration !== null) problems.push("durationMs must be null; this content has no intrinsic length");
|
|
734
|
+
return;
|
|
658
735
|
}
|
|
659
|
-
if (
|
|
660
|
-
if (!Object.hasOwn(value, "coordinateSpace") || value.coordinateSpace === void 0) problems.push("coordinateSpace is required");
|
|
736
|
+
if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
|
|
661
737
|
}
|
|
662
738
|
function validateMarkerPayload(value, problems) {
|
|
663
739
|
validateRange(value.sourceRange, "sourceRange", true, problems);
|
|
@@ -693,40 +769,38 @@ function validateSegmentRanges(value, problems) {
|
|
|
693
769
|
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`);
|
|
694
770
|
}
|
|
695
771
|
}
|
|
772
|
+
/** The Asset entity is the locator; it states where the bytes live and nothing else. */
|
|
696
773
|
function validateAssetPayload(value, problems) {
|
|
697
|
-
|
|
698
|
-
|
|
774
|
+
if (value.system !== "memota" && value.system !== "memota-speech") problems.push("system must be \"memota\" or \"memota-speech\"");
|
|
775
|
+
if (typeof value.key !== "string" || value.key.trim() === "") problems.push("key must be a non-empty string");
|
|
699
776
|
if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
|
|
700
|
-
}
|
|
701
|
-
/**
|
|
702
|
-
* Media variants own their Asset locator directly: `external` is required and
|
|
703
|
-
* textual domain content is owned by AudioScript; the physical-location
|
|
704
|
-
* vocabularies cannot mix.
|
|
705
|
-
*/
|
|
706
|
-
function validateMediaAssetFields(value, problems) {
|
|
707
|
-
if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
|
|
708
|
-
else validateExternalLocator(value, problems);
|
|
709
777
|
validateStorageKey(value, problems);
|
|
710
|
-
if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
|
|
711
|
-
}
|
|
712
|
-
function validateExternalLocator(value, problems) {
|
|
713
|
-
const external = value.external;
|
|
714
|
-
if (external === void 0) return;
|
|
715
|
-
if (!isRecord(external)) {
|
|
716
|
-
problems.push("external must be an object when present");
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
|
|
720
|
-
if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
|
|
721
778
|
}
|
|
722
779
|
function validateStorageKey(value, problems) {
|
|
723
780
|
if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
|
|
724
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* Voice is a timbre identity, not playable content: it owns the voice-library
|
|
784
|
+
* descriptor and nothing that would make it look like media or a Sequence. The
|
|
785
|
+
* rendered voiceover is an `audio` entity bound by a `voice-timbre` Relation.
|
|
786
|
+
*/
|
|
725
787
|
function validateVoicePayload(value, problems) {
|
|
788
|
+
for (const key of [
|
|
789
|
+
"durationMs",
|
|
790
|
+
"extent",
|
|
791
|
+
"sampling",
|
|
792
|
+
"coordinateSpace",
|
|
793
|
+
"system",
|
|
794
|
+
"key",
|
|
795
|
+
"storageKey"
|
|
796
|
+
]) if (Object.hasOwn(value, key)) problems.push(`${key} belongs to the rendered Audio, not to the Voice identity`);
|
|
726
797
|
const voice = value.voice;
|
|
727
|
-
if (voice === void 0)
|
|
798
|
+
if (voice === void 0) {
|
|
799
|
+
problems.push("voice is required; a Voice entity is its voice-library identity");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
728
802
|
if (!isRecord(voice)) {
|
|
729
|
-
problems.push("voice must be an object
|
|
803
|
+
problems.push("voice must be an object");
|
|
730
804
|
return;
|
|
731
805
|
}
|
|
732
806
|
if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
|
|
@@ -734,27 +808,16 @@ function validateVoicePayload(value, problems) {
|
|
|
734
808
|
if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
|
|
735
809
|
}
|
|
736
810
|
function validateCaptionPayload(value, problems) {
|
|
737
|
-
validateExternalLocator(value, problems);
|
|
738
811
|
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
739
|
-
|
|
740
|
-
|
|
812
|
+
if (Object.hasOwn(value, "selections")) problems.push("selections is forbidden; Caption requires one selection object");
|
|
813
|
+
const selection = value.selection;
|
|
814
|
+
if (!isRecord(selection)) problems.push("selection must be one AudioScript segment selection object, not an array");
|
|
741
815
|
else {
|
|
742
|
-
if (
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
continue;
|
|
748
|
-
}
|
|
749
|
-
if (typeof selection.segmentId !== "string" || selection.segmentId.trim() === "") problems.push(`selections[${index}].segmentId must be a string`);
|
|
750
|
-
else if (seen.has(selection.segmentId)) problems.push(`selections[${index}].segmentId must be unique within the Caption`);
|
|
751
|
-
else seen.add(selection.segmentId);
|
|
752
|
-
if (Object.keys(selection).some((key) => key !== "segmentId" && key !== "textRange")) problems.push(`selections[${index}] may only contain segmentId and textRange`);
|
|
753
|
-
if (selection.textRange !== void 0) {
|
|
754
|
-
const range = selection.textRange;
|
|
755
|
-
if (!isRecord(range) || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || Object.keys(range).some((key) => key !== "start" && key !== "end")) problems.push(`selections[${index}].textRange must be a non-empty half-open code-point range`);
|
|
756
|
-
}
|
|
757
|
-
}
|
|
816
|
+
if (typeof selection.segmentId !== "string" || !selection.segmentId.trim()) problems.push("selection.segmentId must be a non-empty string");
|
|
817
|
+
if (Object.keys(selection).some((key) => key !== "segmentId" && key !== "textRange")) problems.push("selection may only contain segmentId and textRange");
|
|
818
|
+
const range = selection.textRange;
|
|
819
|
+
if (range !== void 0 && (!isRecord(range) || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || Object.keys(range).some((key) => key !== "start" && key !== "end"))) problems.push("selection.textRange must be a non-empty half-open code-point range");
|
|
820
|
+
if (Array.isArray(value.segmentRanges) && (value.segmentRanges.length !== 1 || !isRecord(value.segmentRanges[0]) || value.segmentRanges[0].segmentId !== selection.segmentId)) problems.push("Caption segmentRanges must contain only the selected segment timing");
|
|
758
821
|
}
|
|
759
822
|
const style = value.style;
|
|
760
823
|
if (style === void 0) return;
|
|
@@ -868,17 +931,15 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
868
931
|
if ([
|
|
869
932
|
"video",
|
|
870
933
|
"audio",
|
|
871
|
-
"voice",
|
|
872
934
|
"image",
|
|
873
935
|
"caption",
|
|
874
936
|
"axvideo"
|
|
875
|
-
].includes(entityKind) && /^
|
|
937
|
+
].includes(entityKind) && /^durationMs$/.test(path)) return true;
|
|
876
938
|
if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
|
|
877
939
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
878
|
-
if (entityKind === "asset" && /^(?:
|
|
879
|
-
if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
|
|
940
|
+
if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
|
|
880
941
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
881
|
-
if (entityKind === "caption" && (path.startsWith("style.") ||
|
|
942
|
+
if (entityKind === "caption" && (path.startsWith("style.") || path === "selection.segmentId")) return true;
|
|
882
943
|
if ([
|
|
883
944
|
"audio-script",
|
|
884
945
|
"caption",
|
|
@@ -893,7 +954,7 @@ function isOwnedLocalIdPath(entityKind, path) {
|
|
|
893
954
|
"caption",
|
|
894
955
|
"phonetic-script"
|
|
895
956
|
].includes(entityKind) && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
|
|
896
|
-
if (entityKind === "caption" &&
|
|
957
|
+
if (entityKind === "caption" && path === "selection.segmentId") return true;
|
|
897
958
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
898
959
|
if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
|
|
899
960
|
return false;
|
|
@@ -931,19 +992,16 @@ function collectRelatedEntities(entityRefs, index) {
|
|
|
931
992
|
issues
|
|
932
993
|
};
|
|
933
994
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
if (kind === "
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
extent: "bounded",
|
|
945
|
-
sampling: "derived"
|
|
946
|
-
};
|
|
995
|
+
/**
|
|
996
|
+
* What a kind must state about its own length. An AXVideo is absent here on
|
|
997
|
+
* purpose: its Timeline decides its length, so it stores none.
|
|
998
|
+
*/
|
|
999
|
+
function expectedDurationShape(kind) {
|
|
1000
|
+
if (kind === "video" || kind === "audio" || kind === "caption") return "own";
|
|
1001
|
+
if (kind === "image") return "none";
|
|
1002
|
+
}
|
|
1003
|
+
function isPositiveInteger(value) {
|
|
1004
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
947
1005
|
}
|
|
948
1006
|
function ofKind(relations, kind) {
|
|
949
1007
|
return relations.filter((relation) => relation.kind === kind);
|
|
@@ -960,9 +1018,18 @@ const markerContentRelationSpec = Object.freeze({
|
|
|
960
1018
|
validateEndpoints: (endpoints) => hasMarkerAndSequence(endpoints),
|
|
961
1019
|
validateMetadata: isEmptyMetadata
|
|
962
1020
|
});
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1021
|
+
/**
|
|
1022
|
+
* `from-asset(entity, asset)` — the entity was made from that stored resource.
|
|
1023
|
+
*
|
|
1024
|
+
* Eligibility is declared: the non-Asset endpoint must carry the `FromAsset`
|
|
1025
|
+
* kind, which says an entity of that kind can be made from stored bytes and
|
|
1026
|
+
* asks nothing of its payload. The bytes stay named in exactly one place, so
|
|
1027
|
+
* no entity copies the locator into its own row and one Asset can back several
|
|
1028
|
+
* entities.
|
|
1029
|
+
*/
|
|
1030
|
+
const fromAssetRelationSpec = Object.freeze({
|
|
1031
|
+
kind: "from-asset",
|
|
1032
|
+
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
966
1033
|
validateMetadata: isAssetBinding
|
|
967
1034
|
});
|
|
968
1035
|
/** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
|
|
@@ -973,7 +1040,7 @@ const generatedRelationSpec = Object.freeze({
|
|
|
973
1040
|
});
|
|
974
1041
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
975
1042
|
kind: "caption-alignment",
|
|
976
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"
|
|
1043
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"])),
|
|
977
1044
|
validateMetadata: isCaptionAlignmentMetadata
|
|
978
1045
|
});
|
|
979
1046
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
@@ -982,20 +1049,29 @@ const clipAnchorRelationSpec = Object.freeze({
|
|
|
982
1049
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
983
1050
|
validateMetadata: isEmptyMetadata
|
|
984
1051
|
});
|
|
985
|
-
/**
|
|
1052
|
+
/**
|
|
1053
|
+
* The rendered voiceover Audio and the PhoneticScript it was synthesized from;
|
|
1054
|
+
* kinds determine roles regardless of endpoint positions.
|
|
1055
|
+
*/
|
|
986
1056
|
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
987
1057
|
kind: "phonetic-script-render",
|
|
988
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["
|
|
1058
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio"]), new Set(["phonetic-script"])),
|
|
1059
|
+
validateMetadata: isEmptyMetadata
|
|
1060
|
+
});
|
|
1061
|
+
/**
|
|
1062
|
+
* The timbre identity a rendered voiceover Audio was synthesized with. Voice
|
|
1063
|
+
* stays an identity: it never carries the audio itself, so the link between the
|
|
1064
|
+
* two is a Relation rather than one shared entity.
|
|
1065
|
+
*/
|
|
1066
|
+
const voiceTimbreRelationSpec = Object.freeze({
|
|
1067
|
+
kind: "voice-timbre",
|
|
1068
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio"]), new Set(["voice"])),
|
|
989
1069
|
validateMetadata: isEmptyMetadata
|
|
990
1070
|
});
|
|
991
1071
|
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
992
1072
|
const audioScriptSourceRelationSpec = Object.freeze({
|
|
993
1073
|
kind: "audio-script-source",
|
|
994
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
995
|
-
"audio",
|
|
996
|
-
"video",
|
|
997
|
-
"voice"
|
|
998
|
-
])),
|
|
1074
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
|
|
999
1075
|
validateMetadata: isEmptyMetadata
|
|
1000
1076
|
});
|
|
1001
1077
|
/**
|
|
@@ -1017,11 +1093,12 @@ const builtInRelationSpecs = Object.freeze([
|
|
|
1017
1093
|
markerContentRelationSpec,
|
|
1018
1094
|
axVideoMarkerRelationSpec,
|
|
1019
1095
|
markerTimelineRelationSpec,
|
|
1020
|
-
|
|
1096
|
+
fromAssetRelationSpec,
|
|
1021
1097
|
generatedRelationSpec,
|
|
1022
1098
|
captionAlignmentRelationSpec,
|
|
1023
1099
|
clipAnchorRelationSpec,
|
|
1024
1100
|
phoneticScriptRenderRelationSpec,
|
|
1101
|
+
voiceTimbreRelationSpec,
|
|
1025
1102
|
audioScriptSourceRelationSpec,
|
|
1026
1103
|
audioScriptMarkerRelationSpec
|
|
1027
1104
|
]);
|
|
@@ -1045,13 +1122,15 @@ function hasMarkerAndSequence(endpoints) {
|
|
|
1045
1122
|
const second = endpoints[1].current();
|
|
1046
1123
|
return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
|
|
1047
1124
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const
|
|
1051
|
-
|
|
1125
|
+
/** Exactly one end is the Asset, so the other end is unambiguously the maker. */
|
|
1126
|
+
function hasExactlyOneAsset(endpoints) {
|
|
1127
|
+
const kinds = endpoints.map((endpoint) => endpoint.current().entityKind);
|
|
1128
|
+
const assets = kinds.map((kind) => kind === "asset");
|
|
1129
|
+
if (assets[0] === assets[1]) return false;
|
|
1130
|
+
return declaresKind(assets[0] ? kinds[1] : kinds[0], "FromAsset");
|
|
1052
1131
|
}
|
|
1053
1132
|
function isGeneratedMedia(entity) {
|
|
1054
|
-
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio"
|
|
1133
|
+
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio";
|
|
1055
1134
|
}
|
|
1056
1135
|
function isEmptyMetadata(value) {
|
|
1057
1136
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -1387,7 +1466,7 @@ function errorMessage(error) {
|
|
|
1387
1466
|
//#endregion
|
|
1388
1467
|
//#region src/sandbox/business-facades.ts
|
|
1389
1468
|
/** Resource IDs are entity data; physical storage paths remain host-owned. */
|
|
1390
|
-
const infrastructureFields = new Set(["storageKey"]);
|
|
1469
|
+
const infrastructureFields = new Set(["storageKey", "assetContentHash"]);
|
|
1391
1470
|
function businessEntity(entity) {
|
|
1392
1471
|
return {
|
|
1393
1472
|
...entity,
|
|
@@ -1400,11 +1479,11 @@ function businessState(state) {
|
|
|
1400
1479
|
revision: state.revision,
|
|
1401
1480
|
audioScriptEntityId: state.audioScriptEntityId,
|
|
1402
1481
|
entities: state.entities.filter((row) => !assets.has(row.entity_id)).map(businessEntity),
|
|
1403
|
-
relations: state.relations.filter((row) => row.relation_kind !== "
|
|
1482
|
+
relations: state.relations.filter((row) => row.relation_kind !== "from-asset" && !assets.has(row.endpoint_0_entity_id) && !assets.has(row.endpoint_1_entity_id))
|
|
1404
1483
|
};
|
|
1405
1484
|
}
|
|
1406
1485
|
/** Keep infrastructure rows available to assembly without exposing their management to scripts. */
|
|
1407
|
-
function businessFacades(entities, relations) {
|
|
1486
|
+
function businessFacades(entities, relations, onDirectScriptWrite) {
|
|
1408
1487
|
const isAsset = (id) => entities.get(id)?.entity_kind === "asset";
|
|
1409
1488
|
const assertBusiness = (id) => {
|
|
1410
1489
|
if (isAsset(id)) throw new Error("Asset entities are managed by host assembly");
|
|
@@ -1415,7 +1494,7 @@ function businessFacades(entities, relations) {
|
|
|
1415
1494
|
for (const id of payload.baseEntityIds) if (typeof id === "string") assertBusiness(id);
|
|
1416
1495
|
}
|
|
1417
1496
|
};
|
|
1418
|
-
const visibleRelation = (relation) => relation.relation_kind !== "
|
|
1497
|
+
const visibleRelation = (relation) => relation.relation_kind !== "from-asset" && !isAsset(relation.endpoint_0_entity_id) && !isAsset(relation.endpoint_1_entity_id);
|
|
1419
1498
|
return {
|
|
1420
1499
|
entities: {
|
|
1421
1500
|
list: () => entities.list().filter((entity) => entity.entity_kind !== "asset").map(businessEntity),
|
|
@@ -1427,7 +1506,9 @@ function businessFacades(entities, relations) {
|
|
|
1427
1506
|
if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
|
|
1428
1507
|
assertBusinessPayload(input.payload);
|
|
1429
1508
|
if (input.entity_id && entities.get(input.entity_id)) throw new Error(`Entity id ${input.entity_id} already exists`);
|
|
1430
|
-
|
|
1509
|
+
const id = entities.create(input);
|
|
1510
|
+
if (input.entity_kind === "audio-script") onDirectScriptWrite?.(id);
|
|
1511
|
+
return id;
|
|
1431
1512
|
},
|
|
1432
1513
|
update: (input) => {
|
|
1433
1514
|
assertBusiness(input.entity_id);
|
|
@@ -1437,11 +1518,13 @@ function businessFacades(entities, relations) {
|
|
|
1437
1518
|
if (change.path?.[0] === "baseEntityIds" && change.op === "list.insert" && typeof change.value === "string") assertBusiness(change.value);
|
|
1438
1519
|
}
|
|
1439
1520
|
entities.changeFields(input);
|
|
1521
|
+
if (entities.get(input.entity_id)?.entity_kind === "audio-script" && input.changes.some((change) => change.path[0] === "segments")) onDirectScriptWrite?.(input.entity_id);
|
|
1440
1522
|
},
|
|
1441
1523
|
declareFields: (input) => {
|
|
1442
1524
|
assertBusiness(input.entity_id);
|
|
1443
1525
|
assertBusinessPayload(input.payload);
|
|
1444
1526
|
entities.declareFields(input);
|
|
1527
|
+
if (entities.get(input.entity_id)?.entity_kind === "audio-script" && Object.hasOwn(input.payload, "segments")) onDirectScriptWrite?.(input.entity_id);
|
|
1445
1528
|
},
|
|
1446
1529
|
delete: (input) => {
|
|
1447
1530
|
assertBusiness(input.entity_id);
|
|
@@ -1458,7 +1541,7 @@ function businessFacades(entities, relations) {
|
|
|
1458
1541
|
list: () => relations.list().filter(visibleRelation),
|
|
1459
1542
|
of: (id, kind) => relations.of(id, kind).filter(visibleRelation),
|
|
1460
1543
|
link: (input) => {
|
|
1461
|
-
if (input.relation_kind === "
|
|
1544
|
+
if (input.relation_kind === "from-asset") throw new Error("Asset bindings are managed by host assembly");
|
|
1462
1545
|
assertBusiness(input.endpoint_0_entity_id);
|
|
1463
1546
|
assertBusiness(input.endpoint_1_entity_id);
|
|
1464
1547
|
return relations.link(input);
|
|
@@ -1478,8 +1561,6 @@ function businessFacades(entities, relations) {
|
|
|
1478
1561
|
}
|
|
1479
1562
|
//#endregion
|
|
1480
1563
|
//#region src/entity/entity-sandbox.ts
|
|
1481
|
-
const ASSET_SOURCE_KEY = "external";
|
|
1482
|
-
const MEMOTA_SYSTEM = "memota";
|
|
1483
1564
|
/** Mutable entity/relation draft whose only durable product is an explicit command plan. */
|
|
1484
1565
|
var EntitySandbox = class {
|
|
1485
1566
|
original;
|
|
@@ -1520,6 +1601,14 @@ var EntitySandbox = class {
|
|
|
1520
1601
|
captionAudioScriptId(entityId) {
|
|
1521
1602
|
return findComposedAudioScript(toDslRows(this.state), createEntityId(entityId), "caption").entityId;
|
|
1522
1603
|
}
|
|
1604
|
+
/** The Asset an entity was made from; the bytes are named there alone. */
|
|
1605
|
+
assetOf(entityId) {
|
|
1606
|
+
const link = this.state.relations.find((relation) => relation.relation_kind === "from-asset" && (relation.endpoint_0_entity_id === entityId || relation.endpoint_1_entity_id === entityId));
|
|
1607
|
+
if (!link) return null;
|
|
1608
|
+
const otherId = link.endpoint_0_entity_id === entityId ? link.endpoint_1_entity_id : link.endpoint_0_entity_id;
|
|
1609
|
+
const other = this.entities.get(otherId);
|
|
1610
|
+
return other?.entity_kind === "asset" ? other : null;
|
|
1611
|
+
}
|
|
1523
1612
|
get audioScriptEntityId() {
|
|
1524
1613
|
return this.state.audioScriptEntityId;
|
|
1525
1614
|
}
|
|
@@ -1532,6 +1621,10 @@ var EntitySandbox = class {
|
|
|
1532
1621
|
this.commands.push(...prefix);
|
|
1533
1622
|
this.onTruncate?.(index);
|
|
1534
1623
|
}
|
|
1624
|
+
/** The working rows, for callers that must ask the graph rather than one row. */
|
|
1625
|
+
rows() {
|
|
1626
|
+
return toDslRows(this.state);
|
|
1627
|
+
}
|
|
1535
1628
|
buildPlan() {
|
|
1536
1629
|
const rows = toDslRows(this.state);
|
|
1537
1630
|
decodeEntityRelationRows(rows, numericMarkerComparators);
|
|
@@ -1596,12 +1689,15 @@ var EntitySandbox = class {
|
|
|
1596
1689
|
},
|
|
1597
1690
|
findByAssetId: (assetId) => {
|
|
1598
1691
|
assertTrimmed(assetId, "assetId");
|
|
1599
|
-
|
|
1692
|
+
const assetIds = new Set(this.state.entities.filter((entity) => entity.entity_kind === "asset" && isImportedMemotaAsset(entity.payload, assetId)).map((entity) => entity.entity_id));
|
|
1693
|
+
const madeIds = new Set(this.state.relations.filter((relation) => relation.relation_kind === "from-asset").flatMap((relation) => assetIds.has(relation.endpoint_1_entity_id) ? [relation.endpoint_0_entity_id] : assetIds.has(relation.endpoint_0_entity_id) ? [relation.endpoint_1_entity_id] : []));
|
|
1694
|
+
return clone(this.state.entities.filter((entity) => madeIds.has(entity.entity_id) && isMediaAssetVariantKind(entity.entity_kind)).map((entity) => this.assembledEntity(entity)));
|
|
1600
1695
|
},
|
|
1601
1696
|
readCaptionContent: (entityId) => {
|
|
1602
1697
|
const assembled = assembleCaptionContent(toDslRows(this.state), createEntityId(entityId));
|
|
1603
1698
|
return clone({
|
|
1604
1699
|
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1700
|
+
segment_index: assembled.segmentIndex,
|
|
1605
1701
|
text: assembled.text,
|
|
1606
1702
|
segments: assembled.segments.map((segment) => ({ ...segment }))
|
|
1607
1703
|
});
|
|
@@ -1657,7 +1753,9 @@ var EntitySandbox = class {
|
|
|
1657
1753
|
});
|
|
1658
1754
|
},
|
|
1659
1755
|
delete: (input) => this.deleteEntity(input),
|
|
1660
|
-
ensureMedia: (fact) => this.ensureMedia(fact)
|
|
1756
|
+
ensureMedia: (fact) => this.ensureMedia(fact),
|
|
1757
|
+
ensureAsset: (locator) => this.ensureAsset(locator),
|
|
1758
|
+
assetIdOf: (entityId) => this.assetOf(entityId)?.entity_id ?? null
|
|
1661
1759
|
};
|
|
1662
1760
|
}
|
|
1663
1761
|
buildRelationFacade() {
|
|
@@ -1686,15 +1784,10 @@ var EntitySandbox = class {
|
|
|
1686
1784
|
if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
|
|
1687
1785
|
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1688
1786
|
}
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
if (matches
|
|
1693
|
-
const existing = matches[0];
|
|
1694
|
-
if (existing !== void 0) {
|
|
1695
|
-
if (existing.entity_kind !== input.entity_kind) throw new Error(`Asset kind conflicts for ${external.key}; cannot change ${existing.entity_kind} to ${input.entity_kind}`);
|
|
1696
|
-
return this.reuseEntity(existing, input, payload);
|
|
1697
|
-
}
|
|
1787
|
+
if (input.entity_kind === "asset" && typeof payload.key === "string") {
|
|
1788
|
+
const matches = this.state.entities.filter((entity) => entity.entity_kind === "asset" && entity.payload.system === payload.system && entity.payload.key === payload.key);
|
|
1789
|
+
if (matches.length > 1) throw new Error(`Ambiguous Assets for ${payload.key}`);
|
|
1790
|
+
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1698
1791
|
}
|
|
1699
1792
|
const entityId = input.entity_id ?? this.idFactory("entity");
|
|
1700
1793
|
assertTrimmed(entityId, "entity_id");
|
|
@@ -1765,6 +1858,18 @@ var EntitySandbox = class {
|
|
|
1765
1858
|
entity_id: input.entity_id
|
|
1766
1859
|
});
|
|
1767
1860
|
}
|
|
1861
|
+
/** Get or create the one Asset entity naming these bytes. */
|
|
1862
|
+
ensureAsset(locator) {
|
|
1863
|
+
const found = this.state.entities.find((row) => row.entity_kind === "asset" && row.payload.system === locator.system && row.payload.key === locator.key);
|
|
1864
|
+
if (found) return found.entity_id;
|
|
1865
|
+
return this.createEntity({
|
|
1866
|
+
entity_kind: "asset",
|
|
1867
|
+
payload: {
|
|
1868
|
+
...locator,
|
|
1869
|
+
...locator.storageKey === void 0 ? {} : { storageKey: locator.storageKey }
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1768
1873
|
ensureMedia(fact) {
|
|
1769
1874
|
const checkpoint = this.commandCount;
|
|
1770
1875
|
try {
|
|
@@ -1791,7 +1896,7 @@ var EntitySandbox = class {
|
|
|
1791
1896
|
}
|
|
1792
1897
|
for (const row of rows.relations) {
|
|
1793
1898
|
if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
|
|
1794
|
-
if (row.relationKind !== "timeline-track") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
|
|
1899
|
+
if (row.relationKind !== "timeline-track" && row.relationKind !== "voice-timbre" && row.relationKind !== "from-asset") throw new Error(`Unexpected resource Relation ${row.relationKind}`);
|
|
1795
1900
|
this.link({
|
|
1796
1901
|
relation_id: row.relationId,
|
|
1797
1902
|
relation_kind: row.relationKind,
|
|
@@ -2048,8 +2153,7 @@ var EntitySandbox = class {
|
|
|
2048
2153
|
}
|
|
2049
2154
|
};
|
|
2050
2155
|
function isImportedMemotaAsset(payload, assetId) {
|
|
2051
|
-
|
|
2052
|
-
return external != null && !Array.isArray(external) && typeof external === "object" && (external.system === MEMOTA_SYSTEM || external.system === "memota-speech") && external.key === assetId;
|
|
2156
|
+
return (payload.system === "memota" || payload.system === "memota-speech") && payload.key === assetId;
|
|
2053
2157
|
}
|
|
2054
2158
|
function toDslEntity(entity) {
|
|
2055
2159
|
return {
|
|
@@ -2098,4 +2202,4 @@ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left,
|
|
|
2098
2202
|
//#endregion
|
|
2099
2203
|
export { createEntityId as a, businessState as i, toDslRows as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EntitySandbox as t };
|
|
2100
2204
|
|
|
2101
|
-
//# sourceMappingURL=entity-sandbox-
|
|
2205
|
+
//# sourceMappingURL=entity-sandbox-BTR2cRl1.mjs.map
|