@mengine/medeo-tool 1.4.1-alpha.5 → 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-DlmUSouB.d.mts → entity-contract-Cf3AiSe7.d.mts} +45 -43
- package/dist/{entity-sandbox-TaUVT3on.mjs → entity-sandbox-BTR2cRl1.mjs} +255 -139
- package/dist/entity-sandbox-BTR2cRl1.mjs.map +1 -0
- package/dist/index.d.mts +27 -10
- package/dist/index.mjs +104 -116
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +42 -44
- package/dist/worker-entry.d.mts +2 -1
- package/dist/worker-entry.mjs +101 -108
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +3 -3
- package/dist/entity-sandbox-TaUVT3on.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,14 +268,17 @@ 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
280
|
segments: [segment],
|
|
281
|
+
segmentIndex: scriptSegments(composed).findIndex((entry) => entry.segmentId === selection.segmentId),
|
|
220
282
|
text: segment.text
|
|
221
283
|
};
|
|
222
284
|
}
|
|
@@ -284,17 +346,15 @@ function entityToRow(entity) {
|
|
|
284
346
|
}
|
|
285
347
|
//#endregion
|
|
286
348
|
//#region ../medeo-dsl/src/invariants.ts
|
|
287
|
-
/**
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
|
|
294
|
-
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",
|
|
295
355
|
entityId: entity.entityId,
|
|
296
|
-
message: `
|
|
297
|
-
}
|
|
356
|
+
message: `Entity "${entity.entityId}" is made from more than one Asset`
|
|
357
|
+
}];
|
|
298
358
|
}
|
|
299
359
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
300
360
|
function validateEntityRelationSet(entityRefs, index, options) {
|
|
@@ -317,7 +377,7 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
317
377
|
const current = entity.current();
|
|
318
378
|
const entityIssues = validateEntity(entity, entityIds);
|
|
319
379
|
issues.push(...entityIssues);
|
|
320
|
-
|
|
380
|
+
issues.push(...validateAssetSource(entity, index));
|
|
321
381
|
if (entityIssues.some((issue) => issue.code === "invalid_entity_payload")) continue;
|
|
322
382
|
if (current.entityKind === "sequence-marker") {
|
|
323
383
|
const marker = entity;
|
|
@@ -542,24 +602,25 @@ function validateMarkerSourceBounds(marker, index, compare) {
|
|
|
542
602
|
const content = contentEdges[0]?.other(marker)?.deref()?.current();
|
|
543
603
|
if (content == null || !hasSequence(content)) return [];
|
|
544
604
|
const sourceRange = marker.current().sourceRange;
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
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 [];
|
|
548
608
|
return [{
|
|
549
609
|
code: "marker_source_out_of_bounds",
|
|
550
610
|
entityId: marker.entityId,
|
|
551
|
-
message: `Sequence Marker sourceRange must stay within content "${content.entityId}"
|
|
611
|
+
message: `Sequence Marker sourceRange must stay within content "${content.entityId}" duration`
|
|
552
612
|
}];
|
|
553
613
|
}
|
|
554
614
|
function validateSequenceComposition(entity) {
|
|
555
615
|
const current = entity.current();
|
|
556
|
-
const expected =
|
|
557
|
-
if (expected
|
|
558
|
-
|
|
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 [];
|
|
559
620
|
return [{
|
|
560
621
|
code: "invalid_sequence_composition",
|
|
561
622
|
entityId: current.entityId,
|
|
562
|
-
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`
|
|
563
624
|
}];
|
|
564
625
|
}
|
|
565
626
|
function validateEntity(entity, entityIds = /* @__PURE__ */ new Set()) {
|
|
@@ -590,6 +651,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
590
651
|
if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
|
|
591
652
|
if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
|
|
592
653
|
for (const key of [
|
|
654
|
+
"durationMs",
|
|
593
655
|
"extent",
|
|
594
656
|
"sampling",
|
|
595
657
|
"coordinateSpace",
|
|
@@ -598,7 +660,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
598
660
|
"duration",
|
|
599
661
|
"startMs",
|
|
600
662
|
"endMs"
|
|
601
|
-
]) 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`);
|
|
602
664
|
}
|
|
603
665
|
switch (entity.entityKind) {
|
|
604
666
|
case "track":
|
|
@@ -608,19 +670,25 @@ function validateKnownEntityPayload(entity) {
|
|
|
608
670
|
break;
|
|
609
671
|
case "video":
|
|
610
672
|
case "audio":
|
|
611
|
-
|
|
673
|
+
validateDurationPayload(value, "own", problems);
|
|
674
|
+
break;
|
|
612
675
|
case "caption":
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
676
|
+
validateDurationPayload(value, "own", problems);
|
|
677
|
+
validateCaptionPayload(value, problems);
|
|
678
|
+
break;
|
|
679
|
+
case "voice":
|
|
680
|
+
validateVoicePayload(value, problems);
|
|
617
681
|
break;
|
|
618
682
|
case "image":
|
|
619
|
-
|
|
620
|
-
validateMediaAssetFields(value, problems);
|
|
683
|
+
validateDurationPayload(value, "none", problems);
|
|
621
684
|
break;
|
|
622
685
|
case "axvideo":
|
|
623
|
-
|
|
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`);
|
|
624
692
|
break;
|
|
625
693
|
case "sequence-marker":
|
|
626
694
|
validateMarkerPayload(value, problems);
|
|
@@ -645,17 +713,27 @@ function validateKnownEntityPayload(entity) {
|
|
|
645
713
|
}
|
|
646
714
|
return problems;
|
|
647
715
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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;
|
|
656
735
|
}
|
|
657
|
-
if (
|
|
658
|
-
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");
|
|
659
737
|
}
|
|
660
738
|
function validateMarkerPayload(value, problems) {
|
|
661
739
|
validateRange(value.sourceRange, "sourceRange", true, problems);
|
|
@@ -691,40 +769,38 @@ function validateSegmentRanges(value, problems) {
|
|
|
691
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`);
|
|
692
770
|
}
|
|
693
771
|
}
|
|
772
|
+
/** The Asset entity is the locator; it states where the bytes live and nothing else. */
|
|
694
773
|
function validateAssetPayload(value, problems) {
|
|
695
|
-
|
|
696
|
-
|
|
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");
|
|
697
776
|
if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
|
|
698
|
-
}
|
|
699
|
-
/**
|
|
700
|
-
* Media variants own their Asset locator directly: `external` is required and
|
|
701
|
-
* textual domain content is owned by AudioScript; the physical-location
|
|
702
|
-
* vocabularies cannot mix.
|
|
703
|
-
*/
|
|
704
|
-
function validateMediaAssetFields(value, problems) {
|
|
705
|
-
if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
|
|
706
|
-
else validateExternalLocator(value, problems);
|
|
707
777
|
validateStorageKey(value, problems);
|
|
708
|
-
if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
|
|
709
|
-
}
|
|
710
|
-
function validateExternalLocator(value, problems) {
|
|
711
|
-
const external = value.external;
|
|
712
|
-
if (external === void 0) return;
|
|
713
|
-
if (!isRecord(external)) {
|
|
714
|
-
problems.push("external must be an object when present");
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (external.system !== "memota" && external.system !== "memota-speech") problems.push("external.system must be \"memota\" or \"memota-speech\"");
|
|
718
|
-
if (typeof external.key !== "string" || external.key.trim() === "") problems.push("external.key must be a non-empty string");
|
|
719
778
|
}
|
|
720
779
|
function validateStorageKey(value, problems) {
|
|
721
780
|
if (value.storageKey !== void 0 && (typeof value.storageKey !== "string" || value.storageKey.trim() === "")) problems.push("storageKey must be a non-empty string when present");
|
|
722
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
|
+
*/
|
|
723
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`);
|
|
724
797
|
const voice = value.voice;
|
|
725
|
-
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
|
+
}
|
|
726
802
|
if (!isRecord(voice)) {
|
|
727
|
-
problems.push("voice must be an object
|
|
803
|
+
problems.push("voice must be an object");
|
|
728
804
|
return;
|
|
729
805
|
}
|
|
730
806
|
if (voice.system !== "voice-library") problems.push("voice.system must be \"voice-library\"");
|
|
@@ -732,7 +808,6 @@ function validateVoicePayload(value, problems) {
|
|
|
732
808
|
if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
|
|
733
809
|
}
|
|
734
810
|
function validateCaptionPayload(value, problems) {
|
|
735
|
-
validateExternalLocator(value, problems);
|
|
736
811
|
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
737
812
|
if (Object.hasOwn(value, "selections")) problems.push("selections is forbidden; Caption requires one selection object");
|
|
738
813
|
const selection = value.selection;
|
|
@@ -856,15 +931,13 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
856
931
|
if ([
|
|
857
932
|
"video",
|
|
858
933
|
"audio",
|
|
859
|
-
"voice",
|
|
860
934
|
"image",
|
|
861
935
|
"caption",
|
|
862
936
|
"axvideo"
|
|
863
|
-
].includes(entityKind) && /^
|
|
937
|
+
].includes(entityKind) && /^durationMs$/.test(path)) return true;
|
|
864
938
|
if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
|
|
865
939
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
866
|
-
if (entityKind === "asset" && /^(?:
|
|
867
|
-
if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
|
|
940
|
+
if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
|
|
868
941
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
869
942
|
if (entityKind === "caption" && (path.startsWith("style.") || path === "selection.segmentId")) return true;
|
|
870
943
|
if ([
|
|
@@ -919,19 +992,16 @@ function collectRelatedEntities(entityRefs, index) {
|
|
|
919
992
|
issues
|
|
920
993
|
};
|
|
921
994
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (kind === "
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
extent: "bounded",
|
|
933
|
-
sampling: "derived"
|
|
934
|
-
};
|
|
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;
|
|
935
1005
|
}
|
|
936
1006
|
function ofKind(relations, kind) {
|
|
937
1007
|
return relations.filter((relation) => relation.kind === kind);
|
|
@@ -948,9 +1018,18 @@ const markerContentRelationSpec = Object.freeze({
|
|
|
948
1018
|
validateEndpoints: (endpoints) => hasMarkerAndSequence(endpoints),
|
|
949
1019
|
validateMetadata: isEmptyMetadata
|
|
950
1020
|
});
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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),
|
|
954
1033
|
validateMetadata: isAssetBinding
|
|
955
1034
|
});
|
|
956
1035
|
/** `generated(output, input)` means endpoint 0 was generated from endpoint 1. */
|
|
@@ -961,7 +1040,7 @@ const generatedRelationSpec = Object.freeze({
|
|
|
961
1040
|
});
|
|
962
1041
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
963
1042
|
kind: "caption-alignment",
|
|
964
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"
|
|
1043
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio"])),
|
|
965
1044
|
validateMetadata: isCaptionAlignmentMetadata
|
|
966
1045
|
});
|
|
967
1046
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
@@ -970,20 +1049,29 @@ const clipAnchorRelationSpec = Object.freeze({
|
|
|
970
1049
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
971
1050
|
validateMetadata: isEmptyMetadata
|
|
972
1051
|
});
|
|
973
|
-
/**
|
|
1052
|
+
/**
|
|
1053
|
+
* The rendered voiceover Audio and the PhoneticScript it was synthesized from;
|
|
1054
|
+
* kinds determine roles regardless of endpoint positions.
|
|
1055
|
+
*/
|
|
974
1056
|
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
975
1057
|
kind: "phonetic-script-render",
|
|
976
|
-
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"])),
|
|
977
1069
|
validateMetadata: isEmptyMetadata
|
|
978
1070
|
});
|
|
979
1071
|
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
980
1072
|
const audioScriptSourceRelationSpec = Object.freeze({
|
|
981
1073
|
kind: "audio-script-source",
|
|
982
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
983
|
-
"audio",
|
|
984
|
-
"video",
|
|
985
|
-
"voice"
|
|
986
|
-
])),
|
|
1074
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
|
|
987
1075
|
validateMetadata: isEmptyMetadata
|
|
988
1076
|
});
|
|
989
1077
|
/**
|
|
@@ -1005,11 +1093,12 @@ const builtInRelationSpecs = Object.freeze([
|
|
|
1005
1093
|
markerContentRelationSpec,
|
|
1006
1094
|
axVideoMarkerRelationSpec,
|
|
1007
1095
|
markerTimelineRelationSpec,
|
|
1008
|
-
|
|
1096
|
+
fromAssetRelationSpec,
|
|
1009
1097
|
generatedRelationSpec,
|
|
1010
1098
|
captionAlignmentRelationSpec,
|
|
1011
1099
|
clipAnchorRelationSpec,
|
|
1012
1100
|
phoneticScriptRenderRelationSpec,
|
|
1101
|
+
voiceTimbreRelationSpec,
|
|
1013
1102
|
audioScriptSourceRelationSpec,
|
|
1014
1103
|
audioScriptMarkerRelationSpec
|
|
1015
1104
|
]);
|
|
@@ -1033,13 +1122,15 @@ function hasMarkerAndSequence(endpoints) {
|
|
|
1033
1122
|
const second = endpoints[1].current();
|
|
1034
1123
|
return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
|
|
1035
1124
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const
|
|
1039
|
-
|
|
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");
|
|
1040
1131
|
}
|
|
1041
1132
|
function isGeneratedMedia(entity) {
|
|
1042
|
-
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio"
|
|
1133
|
+
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio";
|
|
1043
1134
|
}
|
|
1044
1135
|
function isEmptyMetadata(value) {
|
|
1045
1136
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -1375,7 +1466,7 @@ function errorMessage(error) {
|
|
|
1375
1466
|
//#endregion
|
|
1376
1467
|
//#region src/sandbox/business-facades.ts
|
|
1377
1468
|
/** Resource IDs are entity data; physical storage paths remain host-owned. */
|
|
1378
|
-
const infrastructureFields = new Set(["storageKey"]);
|
|
1469
|
+
const infrastructureFields = new Set(["storageKey", "assetContentHash"]);
|
|
1379
1470
|
function businessEntity(entity) {
|
|
1380
1471
|
return {
|
|
1381
1472
|
...entity,
|
|
@@ -1388,11 +1479,11 @@ function businessState(state) {
|
|
|
1388
1479
|
revision: state.revision,
|
|
1389
1480
|
audioScriptEntityId: state.audioScriptEntityId,
|
|
1390
1481
|
entities: state.entities.filter((row) => !assets.has(row.entity_id)).map(businessEntity),
|
|
1391
|
-
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))
|
|
1392
1483
|
};
|
|
1393
1484
|
}
|
|
1394
1485
|
/** Keep infrastructure rows available to assembly without exposing their management to scripts. */
|
|
1395
|
-
function businessFacades(entities, relations) {
|
|
1486
|
+
function businessFacades(entities, relations, onDirectScriptWrite) {
|
|
1396
1487
|
const isAsset = (id) => entities.get(id)?.entity_kind === "asset";
|
|
1397
1488
|
const assertBusiness = (id) => {
|
|
1398
1489
|
if (isAsset(id)) throw new Error("Asset entities are managed by host assembly");
|
|
@@ -1403,7 +1494,7 @@ function businessFacades(entities, relations) {
|
|
|
1403
1494
|
for (const id of payload.baseEntityIds) if (typeof id === "string") assertBusiness(id);
|
|
1404
1495
|
}
|
|
1405
1496
|
};
|
|
1406
|
-
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);
|
|
1407
1498
|
return {
|
|
1408
1499
|
entities: {
|
|
1409
1500
|
list: () => entities.list().filter((entity) => entity.entity_kind !== "asset").map(businessEntity),
|
|
@@ -1415,7 +1506,9 @@ function businessFacades(entities, relations) {
|
|
|
1415
1506
|
if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
|
|
1416
1507
|
assertBusinessPayload(input.payload);
|
|
1417
1508
|
if (input.entity_id && entities.get(input.entity_id)) throw new Error(`Entity id ${input.entity_id} already exists`);
|
|
1418
|
-
|
|
1509
|
+
const id = entities.create(input);
|
|
1510
|
+
if (input.entity_kind === "audio-script") onDirectScriptWrite?.(id);
|
|
1511
|
+
return id;
|
|
1419
1512
|
},
|
|
1420
1513
|
update: (input) => {
|
|
1421
1514
|
assertBusiness(input.entity_id);
|
|
@@ -1425,11 +1518,13 @@ function businessFacades(entities, relations) {
|
|
|
1425
1518
|
if (change.path?.[0] === "baseEntityIds" && change.op === "list.insert" && typeof change.value === "string") assertBusiness(change.value);
|
|
1426
1519
|
}
|
|
1427
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);
|
|
1428
1522
|
},
|
|
1429
1523
|
declareFields: (input) => {
|
|
1430
1524
|
assertBusiness(input.entity_id);
|
|
1431
1525
|
assertBusinessPayload(input.payload);
|
|
1432
1526
|
entities.declareFields(input);
|
|
1527
|
+
if (entities.get(input.entity_id)?.entity_kind === "audio-script" && Object.hasOwn(input.payload, "segments")) onDirectScriptWrite?.(input.entity_id);
|
|
1433
1528
|
},
|
|
1434
1529
|
delete: (input) => {
|
|
1435
1530
|
assertBusiness(input.entity_id);
|
|
@@ -1446,7 +1541,7 @@ function businessFacades(entities, relations) {
|
|
|
1446
1541
|
list: () => relations.list().filter(visibleRelation),
|
|
1447
1542
|
of: (id, kind) => relations.of(id, kind).filter(visibleRelation),
|
|
1448
1543
|
link: (input) => {
|
|
1449
|
-
if (input.relation_kind === "
|
|
1544
|
+
if (input.relation_kind === "from-asset") throw new Error("Asset bindings are managed by host assembly");
|
|
1450
1545
|
assertBusiness(input.endpoint_0_entity_id);
|
|
1451
1546
|
assertBusiness(input.endpoint_1_entity_id);
|
|
1452
1547
|
return relations.link(input);
|
|
@@ -1466,8 +1561,6 @@ function businessFacades(entities, relations) {
|
|
|
1466
1561
|
}
|
|
1467
1562
|
//#endregion
|
|
1468
1563
|
//#region src/entity/entity-sandbox.ts
|
|
1469
|
-
const ASSET_SOURCE_KEY = "external";
|
|
1470
|
-
const MEMOTA_SYSTEM = "memota";
|
|
1471
1564
|
/** Mutable entity/relation draft whose only durable product is an explicit command plan. */
|
|
1472
1565
|
var EntitySandbox = class {
|
|
1473
1566
|
original;
|
|
@@ -1508,6 +1601,14 @@ var EntitySandbox = class {
|
|
|
1508
1601
|
captionAudioScriptId(entityId) {
|
|
1509
1602
|
return findComposedAudioScript(toDslRows(this.state), createEntityId(entityId), "caption").entityId;
|
|
1510
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
|
+
}
|
|
1511
1612
|
get audioScriptEntityId() {
|
|
1512
1613
|
return this.state.audioScriptEntityId;
|
|
1513
1614
|
}
|
|
@@ -1520,6 +1621,10 @@ var EntitySandbox = class {
|
|
|
1520
1621
|
this.commands.push(...prefix);
|
|
1521
1622
|
this.onTruncate?.(index);
|
|
1522
1623
|
}
|
|
1624
|
+
/** The working rows, for callers that must ask the graph rather than one row. */
|
|
1625
|
+
rows() {
|
|
1626
|
+
return toDslRows(this.state);
|
|
1627
|
+
}
|
|
1523
1628
|
buildPlan() {
|
|
1524
1629
|
const rows = toDslRows(this.state);
|
|
1525
1630
|
decodeEntityRelationRows(rows, numericMarkerComparators);
|
|
@@ -1584,12 +1689,15 @@ var EntitySandbox = class {
|
|
|
1584
1689
|
},
|
|
1585
1690
|
findByAssetId: (assetId) => {
|
|
1586
1691
|
assertTrimmed(assetId, "assetId");
|
|
1587
|
-
|
|
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)));
|
|
1588
1695
|
},
|
|
1589
1696
|
readCaptionContent: (entityId) => {
|
|
1590
1697
|
const assembled = assembleCaptionContent(toDslRows(this.state), createEntityId(entityId));
|
|
1591
1698
|
return clone({
|
|
1592
1699
|
audio_script_entity_id: assembled.audioScript.entityId,
|
|
1700
|
+
segment_index: assembled.segmentIndex,
|
|
1593
1701
|
text: assembled.text,
|
|
1594
1702
|
segments: assembled.segments.map((segment) => ({ ...segment }))
|
|
1595
1703
|
});
|
|
@@ -1645,7 +1753,9 @@ var EntitySandbox = class {
|
|
|
1645
1753
|
});
|
|
1646
1754
|
},
|
|
1647
1755
|
delete: (input) => this.deleteEntity(input),
|
|
1648
|
-
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
|
|
1649
1759
|
};
|
|
1650
1760
|
}
|
|
1651
1761
|
buildRelationFacade() {
|
|
@@ -1674,15 +1784,10 @@ var EntitySandbox = class {
|
|
|
1674
1784
|
if (matches.length > 1) throw new Error(`Ambiguous editor ${input.entity_kind}; resolve existing identities`);
|
|
1675
1785
|
if (matches[0] !== void 0) return this.reuseEntity(matches[0], input, payload);
|
|
1676
1786
|
}
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
if (matches
|
|
1681
|
-
const existing = matches[0];
|
|
1682
|
-
if (existing !== void 0) {
|
|
1683
|
-
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}`);
|
|
1684
|
-
return this.reuseEntity(existing, input, payload);
|
|
1685
|
-
}
|
|
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);
|
|
1686
1791
|
}
|
|
1687
1792
|
const entityId = input.entity_id ?? this.idFactory("entity");
|
|
1688
1793
|
assertTrimmed(entityId, "entity_id");
|
|
@@ -1753,6 +1858,18 @@ var EntitySandbox = class {
|
|
|
1753
1858
|
entity_id: input.entity_id
|
|
1754
1859
|
});
|
|
1755
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
|
+
}
|
|
1756
1873
|
ensureMedia(fact) {
|
|
1757
1874
|
const checkpoint = this.commandCount;
|
|
1758
1875
|
try {
|
|
@@ -1779,7 +1896,7 @@ var EntitySandbox = class {
|
|
|
1779
1896
|
}
|
|
1780
1897
|
for (const row of rows.relations) {
|
|
1781
1898
|
if (this.state.relations.some((relation) => relation.relation_id === row.relationId)) continue;
|
|
1782
|
-
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}`);
|
|
1783
1900
|
this.link({
|
|
1784
1901
|
relation_id: row.relationId,
|
|
1785
1902
|
relation_kind: row.relationKind,
|
|
@@ -2036,8 +2153,7 @@ var EntitySandbox = class {
|
|
|
2036
2153
|
}
|
|
2037
2154
|
};
|
|
2038
2155
|
function isImportedMemotaAsset(payload, assetId) {
|
|
2039
|
-
|
|
2040
|
-
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;
|
|
2041
2157
|
}
|
|
2042
2158
|
function toDslEntity(entity) {
|
|
2043
2159
|
return {
|
|
@@ -2084,6 +2200,6 @@ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left,
|
|
|
2084
2200
|
return left - right;
|
|
2085
2201
|
} };
|
|
2086
2202
|
//#endregion
|
|
2087
|
-
export {
|
|
2203
|
+
export { createEntityId as a, businessState as i, toDslRows as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EntitySandbox as t };
|
|
2088
2204
|
|
|
2089
|
-
//# sourceMappingURL=entity-sandbox-
|
|
2205
|
+
//# sourceMappingURL=entity-sandbox-BTR2cRl1.mjs.map
|