@mengine/medeo-client 2.0.1-alpha.23 → 2.0.1-alpha.24
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/dist/{document-7Vi1AeQc.js → document-Ct11SMBS.js} +315 -482
- package/dist/{index-mGObzOh4.d.ts → index-CcIoMxs1.d.ts} +88 -376
- package/dist/index.d.ts +39 -77
- package/dist/index.js +241 -557
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
|
@@ -1177,8 +1177,7 @@ function toAuthoritativePartLibrary(partLibrary) {
|
|
|
1177
1177
|
if (typeof part !== "object" || part == null) continue;
|
|
1178
1178
|
if (part.video_clip != null) out[partId] = { video_clip: withTrimWindowFromDuration(part.video_clip) };
|
|
1179
1179
|
else if (part.speech != null) {
|
|
1180
|
-
const {
|
|
1181
|
-
const { duration_ms, rest } = splitDurationMs(readSpeech);
|
|
1180
|
+
const { duration_ms, rest } = splitDurationMs(part.speech);
|
|
1182
1181
|
out[partId] = { speech: {
|
|
1183
1182
|
...rest,
|
|
1184
1183
|
media_duration_ms: rest.media_duration_ms ?? duration_ms
|
|
@@ -1447,6 +1446,20 @@ function emptyLaneTracks() {
|
|
|
1447
1446
|
}));
|
|
1448
1447
|
}
|
|
1449
1448
|
//#endregion
|
|
1449
|
+
//#region ../medeo-dsl/src/field-merge.ts
|
|
1450
|
+
const FIELD_MERGE = Object.freeze({
|
|
1451
|
+
/** One segment reference, narrowed by an optional code-point range. */
|
|
1452
|
+
selection: "atomic",
|
|
1453
|
+
textRange: "atomic",
|
|
1454
|
+
/** Collaborative text, merged by code point. */
|
|
1455
|
+
text: "text",
|
|
1456
|
+
phonemeScript: "text",
|
|
1457
|
+
/** Element identity is the segment id, so inserts and moves survive each other. */
|
|
1458
|
+
segments: "list",
|
|
1459
|
+
segmentRanges: "list",
|
|
1460
|
+
baseEntityIds: "list"
|
|
1461
|
+
});
|
|
1462
|
+
//#endregion
|
|
1450
1463
|
//#region ../medeo-dsl/src/entities.ts
|
|
1451
1464
|
/**
|
|
1452
1465
|
* What every known entity kind declares.
|
|
@@ -1468,12 +1481,6 @@ function emptyLaneTracks() {
|
|
|
1468
1481
|
* flowed, is the `from-asset` / `to-asset` Relation's answer alone.
|
|
1469
1482
|
*/
|
|
1470
1483
|
const ENTITY_KINDS = Object.freeze({
|
|
1471
|
-
axvideo: Object.freeze([
|
|
1472
|
-
"Container",
|
|
1473
|
-
"Sequence",
|
|
1474
|
-
"Visual",
|
|
1475
|
-
"Audible"
|
|
1476
|
-
]),
|
|
1477
1484
|
timeline: Object.freeze(["Container"]),
|
|
1478
1485
|
track: Object.freeze(["Container"]),
|
|
1479
1486
|
clip: Object.freeze(["Container"]),
|
|
@@ -1489,7 +1496,7 @@ const ENTITY_KINDS = Object.freeze({
|
|
|
1489
1496
|
"Audible",
|
|
1490
1497
|
"MediaFile"
|
|
1491
1498
|
]),
|
|
1492
|
-
voiceover: Object.freeze([
|
|
1499
|
+
"voiceover-recording": Object.freeze([
|
|
1493
1500
|
"Sequence",
|
|
1494
1501
|
"Audible",
|
|
1495
1502
|
"MediaFile"
|
|
@@ -1501,7 +1508,6 @@ const ENTITY_KINDS = Object.freeze({
|
|
|
1501
1508
|
]),
|
|
1502
1509
|
voice: Object.freeze(["Container", "IPBound"]),
|
|
1503
1510
|
"sequence-marker": Object.freeze([]),
|
|
1504
|
-
viewport: Object.freeze([]),
|
|
1505
1511
|
"audio-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1506
1512
|
"phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1507
1513
|
caption: Object.freeze([
|
|
@@ -1534,7 +1540,7 @@ function isSequenceFields(value) {
|
|
|
1534
1540
|
return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
|
|
1535
1541
|
}
|
|
1536
1542
|
function isMediaAssetVariantKind(kind) {
|
|
1537
|
-
return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover";
|
|
1543
|
+
return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover-recording";
|
|
1538
1544
|
}
|
|
1539
1545
|
function isKnownEntityKind(kind) {
|
|
1540
1546
|
return Object.hasOwn(ENTITY_KINDS, kind);
|
|
@@ -1806,7 +1812,7 @@ function entityToRow(entity) {
|
|
|
1806
1812
|
//#region ../medeo-dsl/src/invariants.ts
|
|
1807
1813
|
/** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
|
|
1808
1814
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
1809
|
-
function validateEntityRelationSet(entityRefs, index
|
|
1815
|
+
function validateEntityRelationSet(entityRefs, index) {
|
|
1810
1816
|
const { entities, issues } = collectRelatedEntities(entityRefs, index);
|
|
1811
1817
|
const entityIds = new Set(entities.map((entity) => entity.entityId));
|
|
1812
1818
|
const rows = {
|
|
@@ -1830,17 +1836,13 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
1830
1836
|
if (current.entityKind === "sequence-marker") {
|
|
1831
1837
|
const marker = entity;
|
|
1832
1838
|
issues.push(...validateMarkerUse(entity, index));
|
|
1833
|
-
issues.push(...validateMarkerRanges(marker
|
|
1834
|
-
|
|
1835
|
-
target: (left, right) => options.compareMarkerPoints(marker, "target", left, right)
|
|
1836
|
-
}));
|
|
1837
|
-
issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
|
|
1839
|
+
issues.push(...validateMarkerRanges(marker));
|
|
1840
|
+
issues.push(...validateMarkerSourceBounds(marker, index));
|
|
1838
1841
|
}
|
|
1839
1842
|
if (current.entityKind === "clip") {
|
|
1840
1843
|
issues.push(...validateClipAdmission(entity, index));
|
|
1841
1844
|
issues.push(...validateClipPlacement(entity, index));
|
|
1842
1845
|
}
|
|
1843
|
-
if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
|
|
1844
1846
|
if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
|
|
1845
1847
|
if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
|
|
1846
1848
|
issues.push(...validateSequenceComposition(entity));
|
|
@@ -1849,18 +1851,16 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
1849
1851
|
return issues;
|
|
1850
1852
|
}
|
|
1851
1853
|
function validateMarkerUse(marker, index) {
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
1854
|
-
const
|
|
1855
|
-
const
|
|
1856
|
-
const timelineEdges = ofKind(relations, "marker-timeline");
|
|
1857
|
-
const scriptEdges = ofKind(relations, "audio-script-marker");
|
|
1854
|
+
const markerEdges = ofKind([...index.relationsOf(marker)], "marker");
|
|
1855
|
+
const markedKind = (relation) => relation.other(marker)?.deref()?.current().entityKind;
|
|
1856
|
+
const clipEdges = markerEdges.filter((relation) => markedKind(relation) === "clip");
|
|
1857
|
+
const scriptEdges = markerEdges.filter((relation) => markedKind(relation) === "audio-script");
|
|
1858
1858
|
const issues = [];
|
|
1859
1859
|
if (scriptEdges.length > 0) {
|
|
1860
|
-
if (clipEdges.length
|
|
1860
|
+
if (clipEdges.length > 0) issues.push({
|
|
1861
1861
|
code: "marker_container_xor",
|
|
1862
1862
|
entityId: marker.entityId,
|
|
1863
|
-
message: "An AudioScript annotation Marker must not carry Clip
|
|
1863
|
+
message: "An AudioScript annotation Marker must not carry Clip or content relations"
|
|
1864
1864
|
});
|
|
1865
1865
|
const ranges = marker.current().segmentRanges;
|
|
1866
1866
|
if (!ranges?.length) issues.push({
|
|
@@ -1887,64 +1887,44 @@ function validateMarkerUse(marker, index) {
|
|
|
1887
1887
|
entityId: marker.entityId,
|
|
1888
1888
|
message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
|
|
1889
1889
|
});
|
|
1890
|
-
if (clipEdges.length
|
|
1890
|
+
if (clipEdges.length !== 1) issues.push({
|
|
1891
1891
|
code: "marker_container_xor",
|
|
1892
1892
|
entityId: marker.entityId,
|
|
1893
|
-
message: "Sequence Marker
|
|
1894
|
-
});
|
|
1895
|
-
if (contentEdges.length + timelineEdges.length !== 1) issues.push({
|
|
1896
|
-
code: "marker_content_xor",
|
|
1897
|
-
entityId: marker.entityId,
|
|
1898
|
-
message: "Sequence Marker must have exactly one Sequence content XOR Timeline relation"
|
|
1899
|
-
});
|
|
1900
|
-
if (clipEdges.length === 1 && timelineEdges.length === 1 || axVideoEdges.length === 1 && contentEdges.length === 1) issues.push({
|
|
1901
|
-
code: "marker_pair_mismatch",
|
|
1902
|
-
entityId: marker.entityId,
|
|
1903
|
-
message: "Only Clip+Content or AXVideo+Timeline Marker relation pairs are valid"
|
|
1893
|
+
message: "A display Sequence Marker belongs to exactly one Clip"
|
|
1904
1894
|
});
|
|
1905
1895
|
return issues;
|
|
1906
1896
|
}
|
|
1907
1897
|
function validateClipAdmission(clip, index) {
|
|
1908
|
-
const markerEdges =
|
|
1898
|
+
const markerEdges = markersOn(clip, index);
|
|
1909
1899
|
if (markerEdges.length !== 1) return [{
|
|
1910
1900
|
code: "clip_marker_cardinality",
|
|
1911
1901
|
entityId: clip.entityId,
|
|
1912
1902
|
message: "Clip must have exactly one authoritative Clip-Marker Relation"
|
|
1913
1903
|
}];
|
|
1914
|
-
|
|
1915
|
-
if (marker == null) return [{
|
|
1904
|
+
if (markerEdges[0]?.other(clip)?.deref() == null) return [{
|
|
1916
1905
|
code: "clip_content_cardinality",
|
|
1917
1906
|
entityId: clip.entityId,
|
|
1918
|
-
message: "Clip must resolve one live Sequence Marker
|
|
1907
|
+
message: "Clip must resolve one live Sequence Marker"
|
|
1919
1908
|
}];
|
|
1920
|
-
const contentEdges =
|
|
1909
|
+
const contentEdges = holds(clip, index);
|
|
1921
1910
|
if (contentEdges.length !== 1) return [{
|
|
1922
1911
|
code: "clip_content_cardinality",
|
|
1923
1912
|
entityId: clip.entityId,
|
|
1924
|
-
message: "Clip
|
|
1913
|
+
message: "Clip must contain exactly one content entity"
|
|
1925
1914
|
}];
|
|
1926
|
-
const content = contentEdges[0]?.other(
|
|
1915
|
+
const content = contentEdges[0]?.other(clip)?.deref()?.current();
|
|
1927
1916
|
if (content == null) return [{
|
|
1928
1917
|
code: "clip_content_cardinality",
|
|
1929
1918
|
entityId: clip.entityId,
|
|
1930
|
-
message: "Clip
|
|
1919
|
+
message: "Clip content Relation must resolve one live entity"
|
|
1931
1920
|
}];
|
|
1932
1921
|
if (!hasSequence(content)) return [{
|
|
1933
1922
|
code: "clip_content_not_sequence",
|
|
1934
1923
|
entityId: clip.entityId,
|
|
1935
|
-
message: `Clip
|
|
1924
|
+
message: `Clip contains non-Sequence entity "${content.entityId}"`
|
|
1936
1925
|
}];
|
|
1937
1926
|
return [];
|
|
1938
1927
|
}
|
|
1939
|
-
function validateAXVideoAdmission(axVideo, index) {
|
|
1940
|
-
const markerEdges = ofKind([...index.relationsOf(axVideo)], "axvideo-marker");
|
|
1941
|
-
if (markerEdges.length === 1 && markerEdges[0]?.other(axVideo)?.deref() != null) return [];
|
|
1942
|
-
return [{
|
|
1943
|
-
code: "axvideo_marker_cardinality",
|
|
1944
|
-
entityId: axVideo.entityId,
|
|
1945
|
-
message: "AXVideo must have exactly one live AXVideo-Marker Relation"
|
|
1946
|
-
}];
|
|
1947
|
-
}
|
|
1948
1928
|
/** Validate a Caption against the complete entity set, including its directly held bases. */
|
|
1949
1929
|
function validateCaptionComposition(caption, index, entities = []) {
|
|
1950
1930
|
return validateVariantComposition(caption, index, entities, "caption");
|
|
@@ -1971,33 +1951,30 @@ function validateVariantComposition(variant, index, entities, kind) {
|
|
|
1971
1951
|
return issues;
|
|
1972
1952
|
}
|
|
1973
1953
|
function validateClipPlacement(clip, index) {
|
|
1974
|
-
const marker =
|
|
1954
|
+
const marker = markersOn(clip, index)[0]?.other(clip)?.deref();
|
|
1975
1955
|
if (marker?.current().entityKind !== "sequence-marker") return [];
|
|
1976
1956
|
const markerValue = marker.current();
|
|
1977
1957
|
const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
|
|
1978
|
-
const
|
|
1979
|
-
const hasTarget = markerValue.targetRange !== void 0;
|
|
1980
|
-
const hasAnchorOffset = markerValue.anchorOffset !== void 0;
|
|
1958
|
+
const hasStart = markerValue.startMs !== void 0;
|
|
1981
1959
|
const issues = [];
|
|
1982
1960
|
if (anchors.length > 1) issues.push({
|
|
1983
1961
|
code: "clip_anchor_cardinality",
|
|
1984
1962
|
entityId: clip.entityId,
|
|
1985
1963
|
message: "A Clip may follow at most one host Clip"
|
|
1986
1964
|
});
|
|
1987
|
-
|
|
1988
|
-
if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
|
|
1965
|
+
if (anchors.length > 0 && !hasStart) issues.push({
|
|
1989
1966
|
code: "clip_placement_invalid",
|
|
1990
1967
|
entityId: clip.entityId,
|
|
1991
|
-
message: "Clip
|
|
1968
|
+
message: "A Clip following a host states startMs, the offset inside that host"
|
|
1992
1969
|
});
|
|
1993
|
-
if (markerValue.
|
|
1994
|
-
const content =
|
|
1995
|
-
const track =
|
|
1970
|
+
if (markerValue.fillTimeline === true) {
|
|
1971
|
+
const content = holds(clip, index)[0]?.other(clip)?.deref()?.current();
|
|
1972
|
+
const track = heldBy(clip, index)[0]?.other(clip)?.deref()?.current();
|
|
1996
1973
|
const trackRole = track?.entityKind === "track" ? track.role : void 0;
|
|
1997
|
-
if (content?.entityKind !== "audio" || trackRole !== "bgm"
|
|
1974
|
+
if (content?.entityKind !== "audio" || trackRole !== "bgm") issues.push({
|
|
1998
1975
|
code: "timeline_duration_policy_invalid",
|
|
1999
1976
|
entityId: marker.entityId,
|
|
2000
|
-
message: "Marker
|
|
1977
|
+
message: "Marker fillTimeline is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
|
|
2001
1978
|
});
|
|
2002
1979
|
}
|
|
2003
1980
|
return issues;
|
|
@@ -2029,34 +2006,28 @@ function validateClipAnchorCycles(entities, index) {
|
|
|
2029
2006
|
}
|
|
2030
2007
|
return issues;
|
|
2031
2008
|
}
|
|
2032
|
-
function validateMarkerRanges(marker
|
|
2033
|
-
const
|
|
2034
|
-
|
|
2035
|
-
|
|
2009
|
+
function validateMarkerRanges(marker) {
|
|
2010
|
+
const { inMs, outMs } = marker.current();
|
|
2011
|
+
if (Number.isFinite(inMs) && Number.isFinite(outMs) && outMs > inMs) return [];
|
|
2012
|
+
return [{
|
|
2036
2013
|
code: "marker_source_range_empty",
|
|
2037
2014
|
entityId: marker.entityId,
|
|
2038
|
-
message: "Sequence Marker
|
|
2039
|
-
}
|
|
2040
|
-
if (current.targetRange != null && !(compare.target(current.targetRange.start, current.targetRange.end) < 0)) issues.push({
|
|
2041
|
-
code: "marker_target_range_empty",
|
|
2042
|
-
entityId: marker.entityId,
|
|
2043
|
-
message: "Sequence Marker targetRange must be a non-empty half-open interval"
|
|
2044
|
-
});
|
|
2045
|
-
return issues;
|
|
2015
|
+
message: "Sequence Marker outMs must be a finite millisecond value greater than inMs"
|
|
2016
|
+
}];
|
|
2046
2017
|
}
|
|
2047
|
-
function validateMarkerSourceBounds(marker, index
|
|
2048
|
-
const
|
|
2018
|
+
function validateMarkerSourceBounds(marker, index) {
|
|
2019
|
+
const clip = markedBy(marker, index)[0]?.other(marker)?.deref();
|
|
2020
|
+
if (clip == null) return [];
|
|
2021
|
+
const contentEdges = holds(clip, index);
|
|
2049
2022
|
if (contentEdges.length !== 1) return [];
|
|
2050
|
-
const content = contentEdges[0]?.other(
|
|
2023
|
+
const content = contentEdges[0]?.other(clip)?.deref()?.current();
|
|
2051
2024
|
if (content == null || !hasSequence(content)) return [];
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
|
|
2055
|
-
if (!Number.isNaN(startVsOrigin) && startVsOrigin >= 0 && (endVsDuration == null || !Number.isNaN(endVsDuration) && endVsDuration <= 0)) return [];
|
|
2025
|
+
const { inMs, outMs } = marker.current();
|
|
2026
|
+
if (inMs >= 0 && (content.durationMs == null || outMs <= content.durationMs)) return [];
|
|
2056
2027
|
return [{
|
|
2057
2028
|
code: "marker_source_out_of_bounds",
|
|
2058
2029
|
entityId: marker.entityId,
|
|
2059
|
-
message:
|
|
2030
|
+
message: "Sequence Marker in and out points must stay inside the length its content states"
|
|
2060
2031
|
}];
|
|
2061
2032
|
}
|
|
2062
2033
|
function validateSequenceComposition(entity) {
|
|
@@ -2103,21 +2074,20 @@ function validateKnownEntityPayload(entity) {
|
|
|
2103
2074
|
"extent",
|
|
2104
2075
|
"sampling",
|
|
2105
2076
|
"coordinateSpace",
|
|
2106
|
-
"
|
|
2107
|
-
"
|
|
2108
|
-
"duration",
|
|
2077
|
+
"inMs",
|
|
2078
|
+
"outMs",
|
|
2109
2079
|
"startMs",
|
|
2110
|
-
"
|
|
2080
|
+
"rate"
|
|
2111
2081
|
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
|
|
2112
2082
|
}
|
|
2113
2083
|
switch (entity.entityKind) {
|
|
2114
2084
|
case "track":
|
|
2115
2085
|
validateOptionalField(value, "hidden", "boolean", problems);
|
|
2116
2086
|
validateOptionalField(value, "role", "string", problems);
|
|
2117
|
-
validateOptionalFiniteNumber(value, "order", problems);
|
|
2118
2087
|
break;
|
|
2119
2088
|
case "video":
|
|
2120
2089
|
case "audio":
|
|
2090
|
+
case "voiceover-recording":
|
|
2121
2091
|
validateDurationPayload(value, "own", problems);
|
|
2122
2092
|
break;
|
|
2123
2093
|
case "caption":
|
|
@@ -2131,12 +2101,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
2131
2101
|
validateDurationPayload(value, "none", problems);
|
|
2132
2102
|
break;
|
|
2133
2103
|
case "axvideo":
|
|
2134
|
-
|
|
2135
|
-
"extent",
|
|
2136
|
-
"sampling",
|
|
2137
|
-
"coordinateSpace",
|
|
2138
|
-
"durationMs"
|
|
2139
|
-
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
|
|
2104
|
+
validateDurationPayload(value, "none", problems);
|
|
2140
2105
|
break;
|
|
2141
2106
|
case "sequence-marker":
|
|
2142
2107
|
validateMarkerPayload(value, problems);
|
|
@@ -2150,7 +2115,6 @@ function validateKnownEntityPayload(entity) {
|
|
|
2150
2115
|
case "timeline":
|
|
2151
2116
|
case "viewport": break;
|
|
2152
2117
|
case "clip":
|
|
2153
|
-
validateOptionalFiniteNumber(value, "order", problems);
|
|
2154
2118
|
validateOptionalFiniteNumber(value, "volume", problems);
|
|
2155
2119
|
if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
|
|
2156
2120
|
break;
|
|
@@ -2184,17 +2148,16 @@ function validateDurationPayload(value, expected, problems) {
|
|
|
2184
2148
|
if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
|
|
2185
2149
|
}
|
|
2186
2150
|
function validateMarkerPayload(value, problems) {
|
|
2187
|
-
|
|
2188
|
-
if (
|
|
2189
|
-
|
|
2190
|
-
if (!
|
|
2191
|
-
|
|
2192
|
-
if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
|
|
2193
|
-
} else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
|
|
2194
|
-
if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
|
|
2195
|
-
if (value.durationPolicy !== void 0 && value.durationPolicy !== "timeline") problems.push("durationPolicy must be \"timeline\" when present");
|
|
2151
|
+
if (!isWholeMilliseconds(value.inMs) || value.inMs < 0) problems.push("inMs must be whole milliseconds at or after zero");
|
|
2152
|
+
if (!isWholeMilliseconds(value.outMs)) problems.push("outMs must be whole milliseconds");
|
|
2153
|
+
if (Object.hasOwn(value, "startMs") && (!isWholeMilliseconds(value.startMs) || value.startMs < 0)) problems.push("startMs must be whole milliseconds at or after zero when present");
|
|
2154
|
+
if (Object.hasOwn(value, "rate") && (typeof value.rate !== "number" || !(value.rate > 0))) problems.push("rate must be a positive number when present");
|
|
2155
|
+
if (Object.hasOwn(value, "fillTimeline") && value.fillTimeline !== true) problems.push("fillTimeline is stated only as true");
|
|
2196
2156
|
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
2197
2157
|
}
|
|
2158
|
+
function isWholeMilliseconds(value) {
|
|
2159
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
2160
|
+
}
|
|
2198
2161
|
/** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
|
|
2199
2162
|
function validateSegmentRanges(value, problems) {
|
|
2200
2163
|
if (!Array.isArray(value)) {
|
|
@@ -2293,19 +2256,10 @@ function validateCaptionPayload(value, problems) {
|
|
|
2293
2256
|
"strokeColor"
|
|
2294
2257
|
]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
|
|
2295
2258
|
}
|
|
2296
|
-
function validateRange(value, path, required, problems) {
|
|
2297
|
-
if (!isRecord(value)) {
|
|
2298
|
-
if (required) problems.push(`${path} must be an object`);
|
|
2299
|
-
return;
|
|
2300
|
-
}
|
|
2301
|
-
if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
|
|
2302
|
-
if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
|
|
2303
|
-
}
|
|
2304
2259
|
/** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
|
|
2305
2260
|
function validatePhoneticScriptPayload(value, problems) {
|
|
2306
2261
|
if (value.segments !== void 0) validateScriptPayload(value, problems);
|
|
2307
2262
|
if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
|
|
2308
|
-
if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
|
|
2309
2263
|
}
|
|
2310
2264
|
function validateScriptPayload(value, problems) {
|
|
2311
2265
|
if (!Array.isArray(value.segments)) {
|
|
@@ -2322,9 +2276,8 @@ function validateScriptPayload(value, problems) {
|
|
|
2322
2276
|
"endMs",
|
|
2323
2277
|
"start_ms",
|
|
2324
2278
|
"end_ms",
|
|
2325
|
-
"
|
|
2326
|
-
"
|
|
2327
|
-
"duration"
|
|
2279
|
+
"inMs",
|
|
2280
|
+
"outMs"
|
|
2328
2281
|
]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
|
|
2329
2282
|
if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
|
|
2330
2283
|
if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
|
|
@@ -2379,11 +2332,11 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2379
2332
|
if ([
|
|
2380
2333
|
"video",
|
|
2381
2334
|
"audio",
|
|
2335
|
+
"voiceover-recording",
|
|
2382
2336
|
"image",
|
|
2383
|
-
"caption"
|
|
2384
|
-
"axvideo"
|
|
2337
|
+
"caption"
|
|
2385
2338
|
].includes(entityKind) && /^durationMs$/.test(path)) return true;
|
|
2386
|
-
if (entityKind === "sequence-marker" && /^(?:
|
|
2339
|
+
if (entityKind === "sequence-marker" && /^(?:fillTimeline|rate)$/.test(path)) return true;
|
|
2387
2340
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
2388
2341
|
if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
|
|
2389
2342
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
@@ -2451,19 +2404,57 @@ function expectedDurationShape(kind) {
|
|
|
2451
2404
|
function isPositiveInteger(value) {
|
|
2452
2405
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2453
2406
|
}
|
|
2407
|
+
/** Marker edges where this entity is what the Marker is marked on. */
|
|
2408
|
+
function markersOn(target, index) {
|
|
2409
|
+
return ofKind([...index.relationsOf(target)], "marker").filter((relation) => relation.endpoints[0].deref()?.entityId === target.entityId);
|
|
2410
|
+
}
|
|
2411
|
+
/** Marker edges seen from the Marker itself. */
|
|
2412
|
+
function markedBy(marker, index) {
|
|
2413
|
+
return ofKind([...index.relationsOf(marker)], "marker").filter((relation) => relation.endpoints[1].deref()?.entityId === marker.entityId);
|
|
2414
|
+
}
|
|
2415
|
+
/** Contain edges where this entity is the Container. */
|
|
2416
|
+
function holds(container, index) {
|
|
2417
|
+
return ofKind([...index.relationsOf(container)], "contain").filter((relation) => relation.endpoints[0].deref()?.entityId === container.entityId);
|
|
2418
|
+
}
|
|
2419
|
+
/** Contain edges where this entity is what a Container holds. */
|
|
2420
|
+
function heldBy(contained, index) {
|
|
2421
|
+
return ofKind([...index.relationsOf(contained)], "contain").filter((relation) => relation.endpoints[1].deref()?.entityId === contained.entityId);
|
|
2422
|
+
}
|
|
2454
2423
|
function ofKind(relations, kind) {
|
|
2455
2424
|
return relations.filter((relation) => relation.kind === kind);
|
|
2456
2425
|
}
|
|
2457
2426
|
//#endregion
|
|
2458
2427
|
//#region ../medeo-dsl/src/relation-specs.ts
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
const
|
|
2465
|
-
kind: "
|
|
2466
|
-
|
|
2428
|
+
/**
|
|
2429
|
+
* What a Container holds. Which pairs are legal is the Container's own rule:
|
|
2430
|
+
* a Timeline holds Tracks, a Track holds Clips, and a Clip — the placement
|
|
2431
|
+
* itself — holds content that declares Sequence.
|
|
2432
|
+
*/
|
|
2433
|
+
const containRelationSpec = Object.freeze({
|
|
2434
|
+
kind: "contain",
|
|
2435
|
+
ordered: true,
|
|
2436
|
+
sequenced: true,
|
|
2437
|
+
validateEndpoints: (endpoints) => containerAdmits(endpoints[0].current(), endpoints[1].current()),
|
|
2438
|
+
validateMetadata: isEmptyMetadata
|
|
2439
|
+
});
|
|
2440
|
+
function containerAdmits(container, contained) {
|
|
2441
|
+
if (!declaresKind(container.entityKind, "Container")) return false;
|
|
2442
|
+
if (container.entityKind === "timeline") return contained.entityKind === "track";
|
|
2443
|
+
if (container.entityKind === "track") return contained.entityKind === "clip";
|
|
2444
|
+
if (container.entityKind === "clip") return hasSequence(contained);
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
/**
|
|
2448
|
+
* A point marked on endpoint 0's timeline. A Clip takes a display Marker — the
|
|
2449
|
+
* window and placement it plays its content through; an AudioScript takes an
|
|
2450
|
+
* annotation Marker — assigned per-Segment times. What the Marker means is the
|
|
2451
|
+
* marked entity's own business.
|
|
2452
|
+
*/
|
|
2453
|
+
const markerRelationSpec = Object.freeze({
|
|
2454
|
+
kind: "marker",
|
|
2455
|
+
ordered: true,
|
|
2456
|
+
sequenced: false,
|
|
2457
|
+
validateEndpoints: (endpoints) => (endpoints[0].current().entityKind === "clip" || endpoints[0].current().entityKind === "audio-script") && endpoints[1].current().entityKind === "sequence-marker",
|
|
2467
2458
|
validateMetadata: isEmptyMetadata
|
|
2468
2459
|
});
|
|
2469
2460
|
/**
|
|
@@ -2477,7 +2468,9 @@ const markerContentRelationSpec = Object.freeze({
|
|
|
2477
2468
|
*/
|
|
2478
2469
|
const fromAssetRelationSpec = Object.freeze({
|
|
2479
2470
|
kind: "from-asset",
|
|
2480
|
-
|
|
2471
|
+
ordered: true,
|
|
2472
|
+
sequenced: false,
|
|
2473
|
+
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2481
2474
|
validateMetadata: isAssetBinding
|
|
2482
2475
|
});
|
|
2483
2476
|
/**
|
|
@@ -2497,105 +2490,87 @@ const fromAssetRelationSpec = Object.freeze({
|
|
|
2497
2490
|
*/
|
|
2498
2491
|
const toAssetRelationSpec = Object.freeze({
|
|
2499
2492
|
kind: "to-asset",
|
|
2500
|
-
|
|
2493
|
+
ordered: true,
|
|
2494
|
+
sequenced: false,
|
|
2495
|
+
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2501
2496
|
validateMetadata: isAssetBinding
|
|
2502
2497
|
});
|
|
2503
|
-
/**
|
|
2504
|
-
const
|
|
2505
|
-
|
|
2506
|
-
|
|
2498
|
+
/** Media a generated medium can be made from, and a transcript can be read off. */
|
|
2499
|
+
const MEDIA_VARIANT_KINDS = [
|
|
2500
|
+
"video",
|
|
2501
|
+
"image",
|
|
2502
|
+
"audio",
|
|
2503
|
+
"voiceover-recording"
|
|
2504
|
+
];
|
|
2505
|
+
const MEDIA_AUDIBLE_KINDS = [
|
|
2506
|
+
"video",
|
|
2507
|
+
"audio",
|
|
2508
|
+
"voiceover-recording"
|
|
2509
|
+
];
|
|
2510
|
+
/**
|
|
2511
|
+
* What a thing was made from: a Voiceover Recording from the Phonetic Script it
|
|
2512
|
+
* was spoken from, a transcribed Audio Script from the media it was read off, a
|
|
2513
|
+
* generated medium from the media it was made with. Endpoint 0 is the product,
|
|
2514
|
+
* endpoint 1 what it came from.
|
|
2515
|
+
*/
|
|
2516
|
+
const derivedFromRelationSpec = Object.freeze({
|
|
2517
|
+
kind: "derived-from",
|
|
2518
|
+
ordered: true,
|
|
2519
|
+
sequenced: false,
|
|
2520
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(MEDIA_VARIANT_KINDS), new Set(MEDIA_VARIANT_KINDS)) || hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["phonetic-script"])) || hasKinds(endpoints, new Set(["audio-script"]), new Set(MEDIA_AUDIBLE_KINDS)),
|
|
2507
2521
|
validateMetadata: isEmptyMetadata
|
|
2508
2522
|
});
|
|
2509
2523
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
2510
2524
|
kind: "caption-alignment",
|
|
2511
|
-
|
|
2525
|
+
ordered: true,
|
|
2526
|
+
sequenced: false,
|
|
2527
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover-recording"])),
|
|
2512
2528
|
validateMetadata: isCaptionAlignmentMetadata
|
|
2513
2529
|
});
|
|
2514
2530
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
2515
2531
|
const clipAnchorRelationSpec = Object.freeze({
|
|
2516
2532
|
kind: "clip-anchor",
|
|
2533
|
+
ordered: true,
|
|
2534
|
+
sequenced: false,
|
|
2517
2535
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
2518
2536
|
validateMetadata: isEmptyMetadata
|
|
2519
2537
|
});
|
|
2520
2538
|
/**
|
|
2521
|
-
* The Voiceover and the PhoneticScript it was spoken from. Endpoint 0 is the
|
|
2522
|
-
* output and endpoint 1 is the input script.
|
|
2523
|
-
*
|
|
2524
|
-
* Every Voiceover must carry one: which words were said, and how, is the only
|
|
2525
|
-
* thing that makes speech reproducible, and a Voiceover that cannot answer it
|
|
2526
|
-
* is refused when its plan is built.
|
|
2527
|
-
*/
|
|
2528
|
-
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
2529
|
-
kind: "phonetic-script-render",
|
|
2530
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["phonetic-script"])),
|
|
2531
|
-
validateMetadata: isEmptyMetadata
|
|
2532
|
-
});
|
|
2533
|
-
/**
|
|
2534
2539
|
* The timbre identity a Voiceover was synthesized with. Voice stays an
|
|
2535
2540
|
* identity: it never carries the audio itself, so the link between the two is a
|
|
2536
2541
|
* Relation rather than one shared entity, and several takes can name one voice.
|
|
2537
2542
|
*/
|
|
2538
2543
|
const voiceTimbreRelationSpec = Object.freeze({
|
|
2539
2544
|
kind: "voice-timbre",
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
/** AudioScript was transcribed from Audio or Video: `[script, source]`. */
|
|
2544
|
-
const audioScriptSourceRelationSpec = Object.freeze({
|
|
2545
|
-
kind: "audio-script-source",
|
|
2546
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
2547
|
-
"audio",
|
|
2548
|
-
"voiceover",
|
|
2549
|
-
"video"
|
|
2550
|
-
])),
|
|
2551
|
-
validateMetadata: isEmptyMetadata
|
|
2552
|
-
});
|
|
2553
|
-
/**
|
|
2554
|
-
* `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
|
|
2555
|
-
* whose `segmentRanges` hold directly assigned per-Segment time values. The
|
|
2556
|
-
* script keeps no Sequence and no Clip admission; annotation Markers cannot
|
|
2557
|
-
* enter Clip/AXVideo use chains (enforced by the marker-use invariant).
|
|
2558
|
-
*/
|
|
2559
|
-
const audioScriptMarkerRelationSpec = Object.freeze({
|
|
2560
|
-
kind: "audio-script-marker",
|
|
2561
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["sequence-marker"])),
|
|
2545
|
+
ordered: true,
|
|
2546
|
+
sequenced: false,
|
|
2547
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover-recording"]), new Set(["voice"])),
|
|
2562
2548
|
validateMetadata: isEmptyMetadata
|
|
2563
2549
|
});
|
|
2564
2550
|
/** Built-in kinds are reserved; callers may add specs only under new names. */
|
|
2565
2551
|
const builtInRelationSpecs = Object.freeze([
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
clipMarkerRelationSpec,
|
|
2569
|
-
markerContentRelationSpec,
|
|
2570
|
-
axVideoMarkerRelationSpec,
|
|
2571
|
-
markerTimelineRelationSpec,
|
|
2552
|
+
containRelationSpec,
|
|
2553
|
+
markerRelationSpec,
|
|
2572
2554
|
fromAssetRelationSpec,
|
|
2573
2555
|
toAssetRelationSpec,
|
|
2574
|
-
|
|
2556
|
+
derivedFromRelationSpec,
|
|
2575
2557
|
captionAlignmentRelationSpec,
|
|
2576
2558
|
clipAnchorRelationSpec,
|
|
2577
|
-
|
|
2578
|
-
voiceTimbreRelationSpec,
|
|
2579
|
-
audioScriptSourceRelationSpec,
|
|
2580
|
-
audioScriptMarkerRelationSpec
|
|
2559
|
+
voiceTimbreRelationSpec
|
|
2581
2560
|
]);
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set([a]), new Set([b])),
|
|
2589
|
-
validateMetadata
|
|
2590
|
-
});
|
|
2591
|
-
}
|
|
2561
|
+
/**
|
|
2562
|
+
* Endpoint 0 is one of `aKinds` and endpoint 1 one of `bKinds`.
|
|
2563
|
+
*
|
|
2564
|
+
* Positional, because every built-in kind is ordered: a reversed row states a
|
|
2565
|
+
* different fact, not the same one written backwards.
|
|
2566
|
+
*/
|
|
2592
2567
|
function hasKinds(endpoints, aKinds, bKinds) {
|
|
2593
|
-
|
|
2594
|
-
const second = endpoints[1].current().entityKind;
|
|
2595
|
-
return aKinds.has(first) && bKinds.has(second);
|
|
2568
|
+
return aKinds.has(endpoints[0].current().entityKind) && bKinds.has(endpoints[1].current().entityKind);
|
|
2596
2569
|
}
|
|
2597
|
-
|
|
2598
|
-
|
|
2570
|
+
/** `(entity, asset)`: the bound entity first, the Asset naming its bytes second. */
|
|
2571
|
+
function hasExactlyOneAsset(endpoints) {
|
|
2572
|
+
const bound = endpoints[0].current().entityKind;
|
|
2573
|
+
return endpoints[1].current().entityKind === "asset" && bound !== "asset" && declaresKind(bound, "MediaFile");
|
|
2599
2574
|
}
|
|
2600
2575
|
function isEmptyMetadata(value) {
|
|
2601
2576
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -2608,6 +2583,18 @@ function isAssetBinding(value) {
|
|
|
2608
2583
|
function isCaptionAlignmentMetadata(value) {
|
|
2609
2584
|
return isJsonObject(value) && Object.hasOwn(value, "alignment");
|
|
2610
2585
|
}
|
|
2586
|
+
/** Whether this kind's Relations are ordered among themselves inside one container. */
|
|
2587
|
+
function isSequencedRelationKind(kind) {
|
|
2588
|
+
return builtInRelationSpecs.find((spec) => spec.kind === kind)?.sequenced ?? false;
|
|
2589
|
+
}
|
|
2590
|
+
/** Relations of `kind` where `entityId` is endpoint 0 — the container, the product, the child. */
|
|
2591
|
+
function relationsFrom(relations, kind, entityId) {
|
|
2592
|
+
return relations.filter((row) => row.relationKind === kind && row.endpoint0EntityId === entityId);
|
|
2593
|
+
}
|
|
2594
|
+
/** Relations of `kind` where `entityId` is endpoint 1 — what is contained, the source, the host. */
|
|
2595
|
+
function relationsTo(relations, kind, entityId) {
|
|
2596
|
+
return relations.filter((row) => row.relationKind === kind && row.endpoint1EntityId === entityId);
|
|
2597
|
+
}
|
|
2611
2598
|
//#endregion
|
|
2612
2599
|
//#region ../medeo-dsl/src/relations.ts
|
|
2613
2600
|
function createEntityRef(entity) {
|
|
@@ -2660,16 +2647,15 @@ var BiRelationIndex = class {
|
|
|
2660
2647
|
canonicalRefs = /* @__PURE__ */ new Map();
|
|
2661
2648
|
byRelationId = /* @__PURE__ */ new Map();
|
|
2662
2649
|
link(input) {
|
|
2663
|
-
if (input.spec.kind === "
|
|
2664
|
-
if (input.spec.kind === "clip-anchor" || input.spec.kind === "phonetic-script-render" || input.spec.kind === "audio-script-source") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
|
|
2650
|
+
if (input.spec.kind === "derived-from" || input.spec.kind === "clip-anchor") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
|
|
2665
2651
|
return this.linkValidated(input);
|
|
2666
2652
|
}
|
|
2667
|
-
/** Author `
|
|
2668
|
-
|
|
2653
|
+
/** Author `derived-from(product, source)` without exposing positional arguments. */
|
|
2654
|
+
linkDerivedFrom(input) {
|
|
2669
2655
|
return this.linkValidated({
|
|
2670
2656
|
relationId: input.relationId,
|
|
2671
|
-
spec:
|
|
2672
|
-
endpoints: [input.
|
|
2657
|
+
spec: derivedFromRelationSpec,
|
|
2658
|
+
endpoints: [input.product, input.source],
|
|
2673
2659
|
metadata: {},
|
|
2674
2660
|
trace: input.trace
|
|
2675
2661
|
});
|
|
@@ -2684,26 +2670,6 @@ var BiRelationIndex = class {
|
|
|
2684
2670
|
trace: input.trace
|
|
2685
2671
|
});
|
|
2686
2672
|
}
|
|
2687
|
-
/** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
|
|
2688
|
-
linkPhoneticScriptRender(input) {
|
|
2689
|
-
return this.linkValidated({
|
|
2690
|
-
relationId: input.relationId,
|
|
2691
|
-
spec: phoneticScriptRenderRelationSpec,
|
|
2692
|
-
endpoints: [input.output, input.phoneticScript],
|
|
2693
|
-
metadata: {},
|
|
2694
|
-
trace: input.trace
|
|
2695
|
-
});
|
|
2696
|
-
}
|
|
2697
|
-
/** Author `audio-script-source(script, source)` without exposing positional arguments. */
|
|
2698
|
-
linkAudioScriptSource(input) {
|
|
2699
|
-
return this.linkValidated({
|
|
2700
|
-
relationId: input.relationId,
|
|
2701
|
-
spec: audioScriptSourceRelationSpec,
|
|
2702
|
-
endpoints: [input.script, input.source],
|
|
2703
|
-
metadata: {},
|
|
2704
|
-
trace: input.trace
|
|
2705
|
-
});
|
|
2706
|
-
}
|
|
2707
2673
|
/**
|
|
2708
2674
|
* Rehydrate a persisted row after resolving its spec and endpoint refs.
|
|
2709
2675
|
*
|
|
@@ -2719,7 +2685,6 @@ var BiRelationIndex = class {
|
|
|
2719
2685
|
this.assertRelationIdAvailable(input.relationId);
|
|
2720
2686
|
const builtInSpec = builtInSpecByKind.get(input.spec.kind);
|
|
2721
2687
|
if (builtInSpec != null && builtInSpec !== input.spec) throw new Error(`Relation kind "${input.spec.kind}" must use its built-in specification`);
|
|
2722
|
-
if (isForbiddenAuthoritativeRelation(input.spec.kind, input.endpoints)) throw new Error("A direct Clip-Content Relation is derived-only and cannot be authoritative");
|
|
2723
2688
|
if (!input.spec.validateEndpoints(input.endpoints)) throw new Error(`Relation "${input.spec.kind}" received invalid endpoints`);
|
|
2724
2689
|
if (!input.spec.validateMetadata(input.metadata)) throw new Error(`Relation "${input.spec.kind}" received invalid metadata`);
|
|
2725
2690
|
this.assertCanonicalRefAvailable(first);
|
|
@@ -2768,12 +2733,6 @@ var BiRelationIndex = class {
|
|
|
2768
2733
|
}
|
|
2769
2734
|
};
|
|
2770
2735
|
const builtInSpecByKind = new Map(builtInRelationSpecs.map((spec) => [spec.kind, spec]));
|
|
2771
|
-
function isForbiddenAuthoritativeRelation(kind, endpoints) {
|
|
2772
|
-
if (kind === "clip-content") return true;
|
|
2773
|
-
const first = endpoints[0].current();
|
|
2774
|
-
const second = endpoints[1].current();
|
|
2775
|
-
return first.entityKind === "clip" && hasSequence(second) || second.entityKind === "clip" && hasSequence(first);
|
|
2776
|
-
}
|
|
2777
2736
|
//#endregion
|
|
2778
2737
|
//#region ../medeo-dsl/src/entity-relation-rows.ts
|
|
2779
2738
|
var InvalidEntityRelationRowsError = class extends Error {
|
|
@@ -2862,7 +2821,7 @@ function decodeEntityRelationRows(rows, options) {
|
|
|
2862
2821
|
issues.push(errorMessage(error));
|
|
2863
2822
|
}
|
|
2864
2823
|
}
|
|
2865
|
-
if (issues.length === 0) for (const issue of validateEntityRelationSet([...entitiesById.values()], relationIndex
|
|
2824
|
+
if (issues.length === 0) for (const issue of validateEntityRelationSet([...entitiesById.values()], relationIndex)) issues.push(`${issue.code}: ${issue.message}`);
|
|
2866
2825
|
if (issues.length > 0) throw new InvalidEntityRelationRowsError(issues);
|
|
2867
2826
|
return {
|
|
2868
2827
|
entitiesById,
|
|
@@ -2928,42 +2887,6 @@ function errorMessage(error) {
|
|
|
2928
2887
|
return error instanceof Error ? error.message : String(error);
|
|
2929
2888
|
}
|
|
2930
2889
|
//#endregion
|
|
2931
|
-
//#region ../medeo-dsl/src/relation-transition.ts
|
|
2932
|
-
var InvalidEntityRelationTransitionError = class extends Error {
|
|
2933
|
-
issues;
|
|
2934
|
-
constructor(issues) {
|
|
2935
|
-
super(`Invalid Medeo relation transition:\n- ${issues.join("\n- ")}`);
|
|
2936
|
-
this.issues = issues;
|
|
2937
|
-
this.name = "InvalidEntityRelationTransitionError";
|
|
2938
|
-
}
|
|
2939
|
-
};
|
|
2940
|
-
/** Enforce relation fact identity while admitting a complete candidate view. */
|
|
2941
|
-
function validateEntityRelationTransition(before, candidate) {
|
|
2942
|
-
const issues = [];
|
|
2943
|
-
const previous = new Map(before.relations.map((row) => [row.relationId, row]));
|
|
2944
|
-
for (const relation of candidate.relations) {
|
|
2945
|
-
const original = previous.get(relation.relationId);
|
|
2946
|
-
if (original == null) {
|
|
2947
|
-
if (relation.relationKind === "caption-alignment") issues.push(`new caption-alignment relation "${relation.relationId}" is forbidden`);
|
|
2948
|
-
continue;
|
|
2949
|
-
}
|
|
2950
|
-
if (original.relationKind !== relation.relationKind || original.endpoint0EntityId !== relation.endpoint0EntityId || original.endpoint1EntityId !== relation.endpoint1EntityId) {
|
|
2951
|
-
issues.push(`relation "${relation.relationId}" changed its kind or ordered endpoints`);
|
|
2952
|
-
continue;
|
|
2953
|
-
}
|
|
2954
|
-
if (!sameJson(original.metadata, relation.metadata)) issues.push(`relation "${relation.relationId}" changed semantic metadata without a new identity`);
|
|
2955
|
-
if (relation.relationKind === "caption-alignment" && !sameJson(original.trace, relation.trace)) issues.push(`legacy caption-alignment relation "${relation.relationId}" may only be carried unchanged or deleted`);
|
|
2956
|
-
}
|
|
2957
|
-
if (issues.length > 0) throw new InvalidEntityRelationTransitionError(issues);
|
|
2958
|
-
}
|
|
2959
|
-
function sameJson(left, right) {
|
|
2960
|
-
if (left === right) return true;
|
|
2961
|
-
if (Array.isArray(left) && Array.isArray(right)) return left.length === right.length && left.every((value, index) => sameJson(value, right[index]));
|
|
2962
|
-
if (!isJsonObject(left) || !isJsonObject(right)) return false;
|
|
2963
|
-
const leftKeys = Object.keys(left);
|
|
2964
|
-
return leftKeys.length === Object.keys(right).length && leftKeys.every((key) => Object.hasOwn(right, key) && sameJson(left[key], right[key]));
|
|
2965
|
-
}
|
|
2966
|
-
//#endregion
|
|
2967
2890
|
//#region src/document/entity-projection-values.ts
|
|
2968
2891
|
/** A graph cannot be represented faithfully by the compatibility reader. */
|
|
2969
2892
|
var EntityTimelineProjectionError = class extends Error {
|
|
@@ -3011,7 +2934,7 @@ var EntityProjectionGraph = class {
|
|
|
3011
2934
|
timeline;
|
|
3012
2935
|
constructor(rows) {
|
|
3013
2936
|
this.rows = rows;
|
|
3014
|
-
const decoded = decodeEntityRelationRows(rows, {
|
|
2937
|
+
const decoded = decodeEntityRelationRows(rows, {});
|
|
3015
2938
|
this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
|
|
3016
2939
|
const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
|
|
3017
2940
|
if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
|
|
@@ -3025,6 +2948,13 @@ var EntityProjectionGraph = class {
|
|
|
3025
2948
|
adjacent(id, kind) {
|
|
3026
2949
|
return [...new Set(this.rows.relations.filter((row) => row.relationKind === kind && (row.endpoint0EntityId === id || row.endpoint1EntityId === id)).map((row) => row.endpoint0EntityId === id ? row.endpoint1EntityId : row.endpoint0EntityId))].map((entityId) => this.entity(entityId));
|
|
3027
2950
|
}
|
|
2951
|
+
/** What this entity contains, as opposed to what contains it. */
|
|
2952
|
+
holds(id) {
|
|
2953
|
+
return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint0EntityId === id).map((row) => this.entity(row.endpoint1EntityId));
|
|
2954
|
+
}
|
|
2955
|
+
heldBy(id) {
|
|
2956
|
+
return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint1EntityId === id).map((row) => this.entity(row.endpoint0EntityId));
|
|
2957
|
+
}
|
|
3028
2958
|
one(id, relation, kind) {
|
|
3029
2959
|
const values = this.adjacent(id, relation);
|
|
3030
2960
|
if (values.length !== 1 || kind !== void 0 && values[0]?.entityKind !== kind) fail(`entity ${id} requires exactly one ${relation}${kind === void 0 ? "" : ` to ${kind}`}`);
|
|
@@ -3048,51 +2978,44 @@ function resolveEntityTimelineLayout(rows) {
|
|
|
3048
2978
|
function resolveGraphLayout(graph) {
|
|
3049
2979
|
const facts = /* @__PURE__ */ new Map();
|
|
3050
2980
|
const seenRoles = /* @__PURE__ */ new Set();
|
|
3051
|
-
const tracks = graph.
|
|
2981
|
+
const tracks = graph.holds(graph.timeline.entityId).map((track) => {
|
|
3052
2982
|
const role = track.payload.role;
|
|
3053
2983
|
if (track.entityKind !== "track" || typeof role !== "string" || !Object.hasOwn(roleOrder, role)) fail(`unsupported compatibility Track role on ${track.entityId}`);
|
|
3054
2984
|
if (seenRoles.has(role)) fail(`The compatibility editor supports only one ${role} Track`);
|
|
3055
2985
|
seenRoles.add(role);
|
|
3056
2986
|
const typedRole = role;
|
|
3057
2987
|
if (track.payload.hidden !== void 0 && typeof track.payload.hidden !== "boolean") fail("Track hidden must be boolean");
|
|
3058
|
-
const order =
|
|
3059
|
-
const trackFacts = graph.
|
|
3060
|
-
if (clip.entityKind !== "clip" || graph.
|
|
3061
|
-
const marker = graph.one(clip.entityId, "
|
|
3062
|
-
const
|
|
3063
|
-
|
|
3064
|
-
const
|
|
3065
|
-
const
|
|
3066
|
-
if (!
|
|
3067
|
-
const
|
|
2988
|
+
const order = roleOrder[typedRole];
|
|
2989
|
+
const trackFacts = graph.holds(track.entityId).map((clip, clipIndex) => {
|
|
2990
|
+
if (clip.entityKind !== "clip" || graph.heldBy(clip.entityId).length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
|
|
2991
|
+
const marker = graph.one(clip.entityId, "marker", "sequence-marker");
|
|
2992
|
+
const held = graph.holds(clip.entityId);
|
|
2993
|
+
if (held.length !== 1) fail(`Clip ${clip.entityId} must contain exactly one content entity`);
|
|
2994
|
+
const content = held[0];
|
|
2995
|
+
const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover-recording"] : typedRole === "caption" ? ["caption"] : ["audio"];
|
|
2996
|
+
if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
|
|
2997
|
+
const inMs = milliseconds(marker.payload.inMs, "inMs");
|
|
2998
|
+
const outMs = milliseconds(marker.payload.outMs, "outMs");
|
|
2999
|
+
const rate = marker.payload.rate === void 0 ? 1 : positive(marker.payload.rate, "rate");
|
|
3000
|
+
const duration = positive(Math.round((outMs - inMs) / rate), "duration");
|
|
3068
3001
|
if (!Number.isSafeInteger(duration)) fail("Compatibility duration must be whole milliseconds");
|
|
3069
|
-
const fillTimeline = marker.payload.
|
|
3070
|
-
if (marker.payload.
|
|
3002
|
+
const fillTimeline = marker.payload.fillTimeline === true;
|
|
3003
|
+
if (marker.payload.fillTimeline !== void 0 && (!fillTimeline || typedRole !== "bgm")) fail("fillTimeline is only supported for timeline-filling background Audio");
|
|
3071
3004
|
const anchors = graph.rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
3072
3005
|
if (anchors.length > 1) fail(`Clip ${clip.entityId} has multiple anchors`);
|
|
3073
|
-
const
|
|
3006
|
+
const startMs = marker.payload.startMs === void 0 ? void 0 : milliseconds(marker.payload.startMs, "startMs");
|
|
3074
3007
|
const anchorId = anchors[0]?.endpoint1EntityId;
|
|
3075
3008
|
let mode;
|
|
3076
3009
|
let absoluteStart;
|
|
3077
3010
|
let anchorOffset;
|
|
3078
|
-
let clipOrder;
|
|
3079
3011
|
if (anchorId !== void 0) {
|
|
3080
|
-
if (
|
|
3012
|
+
if (startMs === void 0) fail(`Anchored Marker ${marker.entityId} requires startMs inside its host`);
|
|
3081
3013
|
mode = "anchored";
|
|
3082
|
-
anchorOffset =
|
|
3083
|
-
} else {
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
absoluteStart = milliseconds(target.start, "target start");
|
|
3088
|
-
milliseconds(target.end, "target end");
|
|
3089
|
-
if (Math.abs(target.end - target.start - duration) > 1e-6) fail(`Marker ${marker.entityId} targetRange must match its effective duration`);
|
|
3090
|
-
} else {
|
|
3091
|
-
mode = "sequential";
|
|
3092
|
-
if (clip.payload.order === void 0) fail(`Clip ${clip.entityId} has no placement: targetRange or explicit order is required`);
|
|
3093
|
-
clipOrder = finite(clip.payload.order, "Clip order");
|
|
3094
|
-
}
|
|
3095
|
-
}
|
|
3014
|
+
anchorOffset = startMs;
|
|
3015
|
+
} else if (startMs !== void 0) {
|
|
3016
|
+
mode = "absolute";
|
|
3017
|
+
absoluteStart = startMs;
|
|
3018
|
+
} else mode = "sequential";
|
|
3096
3019
|
const fact = {
|
|
3097
3020
|
clip,
|
|
3098
3021
|
marker,
|
|
@@ -3101,7 +3024,7 @@ function resolveGraphLayout(graph) {
|
|
|
3101
3024
|
role: typedRole,
|
|
3102
3025
|
duration,
|
|
3103
3026
|
mode,
|
|
3104
|
-
order:
|
|
3027
|
+
order: clipIndex,
|
|
3105
3028
|
absoluteStart,
|
|
3106
3029
|
anchorId,
|
|
3107
3030
|
anchorOffset,
|
|
@@ -3111,8 +3034,7 @@ function resolveGraphLayout(graph) {
|
|
|
3111
3034
|
return fact;
|
|
3112
3035
|
});
|
|
3113
3036
|
if (typedRole === "bgm" && trackFacts.length > 1) fail("The compatibility editor supports only one background Audio Clip");
|
|
3114
|
-
const sequential = trackFacts.filter((fact) => fact.mode === "sequential")
|
|
3115
|
-
if (new Set(sequential.map((fact) => fact.order)).size !== sequential.length) fail("sequential Clips must have distinct order values");
|
|
3037
|
+
const sequential = trackFacts.filter((fact) => fact.mode === "sequential");
|
|
3116
3038
|
sequential.forEach((fact, index) => {
|
|
3117
3039
|
fact.predecessor = sequential[index - 1]?.clip.entityId;
|
|
3118
3040
|
});
|
|
@@ -3179,33 +3101,6 @@ function resolveGraphLayout(graph) {
|
|
|
3179
3101
|
}))
|
|
3180
3102
|
};
|
|
3181
3103
|
}
|
|
3182
|
-
function assertTrackContent(graph, role, content) {
|
|
3183
|
-
if (role === "speech") {
|
|
3184
|
-
assertSpeechContent(graph, content);
|
|
3185
|
-
return;
|
|
3186
|
-
}
|
|
3187
|
-
const expected = role === "video_clip" ? ["image", "video"] : role === "caption" ? ["caption"] : ["audio"];
|
|
3188
|
-
if (!expected.includes(content.entityKind)) fail(`${role} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
|
|
3189
|
-
}
|
|
3190
|
-
function assertSpeechContent(graph, content) {
|
|
3191
|
-
const assetLinks = graph.rows.relations.filter((relation) => relation.relationKind === "from-asset" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3192
|
-
if (assetLinks.length !== 1 || assetLinks[0].endpoint0EntityId !== content.entityId) fail("Speech content requires one ordered from-asset binding");
|
|
3193
|
-
const asset = graph.entity(assetLinks[0].endpoint1EntityId, "asset");
|
|
3194
|
-
if (content.entityKind === "voiceover") {
|
|
3195
|
-
const renders = graph.rows.relations.filter((relation) => relation.relationKind === "phonetic-script-render" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3196
|
-
if (renders.length !== 1 || renders[0].endpoint0EntityId !== content.entityId || graph.entity(renders[0].endpoint1EntityId).entityKind !== "phonetic-script") fail("Synthesized Speech Voiceover requires one ordered PhoneticScript render");
|
|
3197
|
-
if (asset.payload.system !== "memota-speech") fail("Synthesized Speech Voiceover requires an explicit memota-speech key");
|
|
3198
|
-
return;
|
|
3199
|
-
}
|
|
3200
|
-
if (content.entityKind === "audio") {
|
|
3201
|
-
const sources = graph.rows.relations.filter((relation) => relation.relationKind === "audio-script-source" && (relation.endpoint0EntityId === content.entityId || relation.endpoint1EntityId === content.entityId));
|
|
3202
|
-
if (sources.length > 1) fail("Recorded speech has an ambiguous AudioScript source receipt");
|
|
3203
|
-
if (sources.length === 1 && (sources[0].endpoint1EntityId !== content.entityId || graph.entity(sources[0].endpoint0EntityId).entityKind !== "audio-script")) fail("Recorded Speech AudioScript source must use ordered endpoints");
|
|
3204
|
-
if (asset.payload.system !== "memota") fail("Recorded Speech Audio requires an explicit memota key");
|
|
3205
|
-
return;
|
|
3206
|
-
}
|
|
3207
|
-
fail(`speech Track accepts voiceover or audio, got ${content.entityKind}`);
|
|
3208
|
-
}
|
|
3209
3104
|
//#endregion
|
|
3210
3105
|
//#region src/document/entity-projection.ts
|
|
3211
3106
|
/**
|
|
@@ -3232,7 +3127,7 @@ function projectEntityTimeline(rows) {
|
|
|
3232
3127
|
if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
|
|
3233
3128
|
if (track.role === "video_clip") {
|
|
3234
3129
|
const asset = mediaAssetFacts(graph, content);
|
|
3235
|
-
const source =
|
|
3130
|
+
const source = markerWindow(marker);
|
|
3236
3131
|
const external = externalLocator(asset, "memota");
|
|
3237
3132
|
const speed = videoSpeed(content, marker, placement);
|
|
3238
3133
|
parts[clip.entityId] = { video_clip: {
|
|
@@ -3247,35 +3142,32 @@ function projectEntityTimeline(rows) {
|
|
|
3247
3142
|
} else if (track.role === "speech") {
|
|
3248
3143
|
const asset = mediaAssetFacts(graph, content);
|
|
3249
3144
|
requireUntrimmedAudio(content, marker, placement);
|
|
3145
|
+
const external = externalLocator(asset, "memota-speech");
|
|
3146
|
+
const renderEdges = graph.adjacent(content.entityId, "derived-from");
|
|
3250
3147
|
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3251
|
-
const
|
|
3252
|
-
const external = externalLocator(asset, isSynthesized ? "memota-speech" : "memota");
|
|
3253
|
-
const assembled = isSynthesized ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
|
|
3148
|
+
const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "derived-from", "phonetic-script").entityId) : void 0;
|
|
3254
3149
|
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3255
3150
|
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3256
3151
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
3257
3152
|
const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
|
|
3258
|
-
|
|
3153
|
+
parts[clip.entityId] = { speech: {
|
|
3259
3154
|
id: clip.entityId,
|
|
3260
3155
|
kind: "speech",
|
|
3261
3156
|
media_duration_ms: placement.duration,
|
|
3262
3157
|
audio_storage_key: storageKey(asset),
|
|
3263
3158
|
origin_speech_id: external.key,
|
|
3264
|
-
|
|
3265
|
-
audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "audio-script-source").length > 0 ? assembleScriptText(graph.one(content.entityId, "audio-script-source", "audio-script")) : ""),
|
|
3159
|
+
audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "derived-from").length > 0 ? assembleScriptText(graph.one(content.entityId, "derived-from", "audio-script")) : ""),
|
|
3266
3160
|
volume: volume.data,
|
|
3267
3161
|
voice: object(voice) ? {
|
|
3268
3162
|
id: typeof voice.key === "string" ? voice.key : void 0,
|
|
3269
3163
|
name: typeof voice.name === "string" ? voice.name : void 0
|
|
3270
3164
|
} : void 0,
|
|
3271
3165
|
caption_ids: captionIds
|
|
3272
|
-
};
|
|
3273
|
-
parts[clip.entityId] = { speech };
|
|
3166
|
+
} };
|
|
3274
3167
|
} else if (track.role === "caption") {
|
|
3275
3168
|
const assembled = assembleCaptionContent(rows, content.entityId);
|
|
3276
3169
|
const text = assembled.text;
|
|
3277
3170
|
const cues = captionDisplayCues(assembled, marker, placement.duration);
|
|
3278
|
-
if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
|
|
3279
3171
|
const style = captionStyle(content.payload.style);
|
|
3280
3172
|
const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
|
|
3281
3173
|
const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
|
|
@@ -3292,9 +3184,9 @@ function projectEntityTimeline(rows) {
|
|
|
3292
3184
|
} else {
|
|
3293
3185
|
const asset = mediaAssetFacts(graph, content);
|
|
3294
3186
|
const external = externalLocator(asset, "memota");
|
|
3295
|
-
if (marker.payload.
|
|
3296
|
-
if (marker.payload.
|
|
3297
|
-
const source =
|
|
3187
|
+
if (marker.payload.fillTimeline !== true) fail("Background Audio requires an explicit fillTimeline");
|
|
3188
|
+
if (marker.payload.rate !== void 0) fail("BGM rate is not supported by this reader");
|
|
3189
|
+
const source = markerWindow(marker);
|
|
3298
3190
|
if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
|
|
3299
3191
|
parts[clip.entityId] = { bgm: {
|
|
3300
3192
|
id: clip.entityId,
|
|
@@ -3350,10 +3242,7 @@ function storageKey(asset) {
|
|
|
3350
3242
|
function externalLocator(asset, system) {
|
|
3351
3243
|
const { system: found, key } = asset.payload;
|
|
3352
3244
|
if (found !== system || typeof key !== "string" || key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " key");
|
|
3353
|
-
return {
|
|
3354
|
-
key,
|
|
3355
|
-
system
|
|
3356
|
-
};
|
|
3245
|
+
return { key };
|
|
3357
3246
|
}
|
|
3358
3247
|
function captionStyle(value) {
|
|
3359
3248
|
if (value === void 0) return void 0;
|
|
@@ -3385,22 +3274,26 @@ function captionStyle(value) {
|
|
|
3385
3274
|
return style;
|
|
3386
3275
|
}
|
|
3387
3276
|
function requireUntrimmedAudio(content, marker, placement) {
|
|
3388
|
-
const source =
|
|
3389
|
-
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.
|
|
3277
|
+
const source = markerWindow(marker);
|
|
3278
|
+
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.rate !== void 0) fail("Voiceover compatibility requires the complete, unremapped take");
|
|
3279
|
+
}
|
|
3280
|
+
/** The Marker's window, in the {start,end} shape this compatibility reader speaks. */
|
|
3281
|
+
function markerWindow(marker) {
|
|
3282
|
+
return range({
|
|
3283
|
+
start: marker.payload.inMs,
|
|
3284
|
+
end: marker.payload.outMs
|
|
3285
|
+
}, "Sequence Marker window");
|
|
3390
3286
|
}
|
|
3391
3287
|
function videoSpeed(content, marker, placement) {
|
|
3392
|
-
const
|
|
3393
|
-
if (content.entityKind === "image" &&
|
|
3394
|
-
const source =
|
|
3395
|
-
if (
|
|
3396
|
-
|
|
3397
|
-
if (
|
|
3398
|
-
const rate = positive(remapping.rate, "linear remapping rate");
|
|
3399
|
-
if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("timeRemapping on " + marker.entityId + " must agree with Marker duration");
|
|
3400
|
-
if (remapping.mode !== void 0 && typeof remapping.mode !== "string") fail("Linear remapping mode must be a string");
|
|
3288
|
+
const stated = marker.payload.rate;
|
|
3289
|
+
if (content.entityKind === "image" && stated === void 0) return void 0;
|
|
3290
|
+
const source = markerWindow(marker);
|
|
3291
|
+
if (stated !== void 0) {
|
|
3292
|
+
const rate = positive(stated, "linear speed rate");
|
|
3293
|
+
if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("rate on " + marker.entityId + " must agree with Marker duration");
|
|
3401
3294
|
return {
|
|
3402
3295
|
category: "linear",
|
|
3403
|
-
mode:
|
|
3296
|
+
mode: "constant",
|
|
3404
3297
|
config: { linear: { speed: rate } }
|
|
3405
3298
|
};
|
|
3406
3299
|
}
|
|
@@ -3415,7 +3308,7 @@ function videoSpeed(content, marker, placement) {
|
|
|
3415
3308
|
function captionDisplayCues(assembled, marker, duration) {
|
|
3416
3309
|
const ranges = assembled.caption.payload.segmentRanges;
|
|
3417
3310
|
if (!Array.isArray(ranges)) return void 0;
|
|
3418
|
-
const source =
|
|
3311
|
+
const source = markerWindow(marker);
|
|
3419
3312
|
const scale = duration / (source.end - source.start);
|
|
3420
3313
|
return assembled.segments.flatMap((segment) => {
|
|
3421
3314
|
const timing = ranges.find((entry) => object(entry) && entry.segmentId === segment.segmentId);
|
|
@@ -3439,23 +3332,11 @@ function captionDisplayCues(assembled, marker, duration) {
|
|
|
3439
3332
|
}
|
|
3440
3333
|
//#endregion
|
|
3441
3334
|
//#region src/entity-crdt/field-changes.ts
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
"durationMs",
|
|
3448
|
-
"timeRemapping",
|
|
3449
|
-
"textRange",
|
|
3450
|
-
"selection",
|
|
3451
|
-
"external"
|
|
3452
|
-
]);
|
|
3453
|
-
const textFields = new Set(["text", "phonemeScript"]);
|
|
3454
|
-
const orderedFields = new Set([
|
|
3455
|
-
"segments",
|
|
3456
|
-
"segmentRanges",
|
|
3457
|
-
"baseEntityIds"
|
|
3458
|
-
]);
|
|
3335
|
+
/** Merge behaviour is declared with the field shapes in @mengine/medeo-dsl. */
|
|
3336
|
+
const merge = (key) => FIELD_MERGE[key];
|
|
3337
|
+
const atomicFields = { has: (key) => merge(key) === "atomic" };
|
|
3338
|
+
const textFields = { has: (key) => merge(key) === "text" };
|
|
3339
|
+
const orderedFields = { has: (key) => merge(key) === "list" };
|
|
3459
3340
|
const unsafeKeys = new Set([
|
|
3460
3341
|
"__proto__",
|
|
3461
3342
|
"prototype",
|
|
@@ -3743,27 +3624,24 @@ function phoneticScriptAssetOf(rows, entityId) {
|
|
|
3743
3624
|
}
|
|
3744
3625
|
//#endregion
|
|
3745
3626
|
//#region src/entity-crdt/ordering.ts
|
|
3746
|
-
/**
|
|
3627
|
+
/**
|
|
3628
|
+
* Domain order is scoped by membership: the members of one container, in the
|
|
3629
|
+
* order their `contain` Relations sit in the rows.
|
|
3630
|
+
*
|
|
3631
|
+
* Nothing carries a copy of its own position, so this reads the order rather
|
|
3632
|
+
* than deriving it — the rows are already in it.
|
|
3633
|
+
*/
|
|
3747
3634
|
function entityOrderGroups(rows) {
|
|
3748
3635
|
const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
|
|
3749
3636
|
const groups = /* @__PURE__ */ new Map();
|
|
3750
3637
|
for (const relation of rows.relations) {
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
const
|
|
3755
|
-
|
|
3756
|
-
const child = entities.get(childId);
|
|
3757
|
-
if (typeof child.payload.order !== "number") continue;
|
|
3758
|
-
const parent = endpoints.find((id) => id !== childId);
|
|
3759
|
-
const values = groups.get(parent) ?? [];
|
|
3760
|
-
values.push({
|
|
3761
|
-
id: childId,
|
|
3762
|
-
order: child.payload.order
|
|
3763
|
-
});
|
|
3764
|
-
groups.set(parent, values);
|
|
3638
|
+
if (!isSequencedRelationKind(relation.relationKind)) continue;
|
|
3639
|
+
const childKind = entities.get(String(relation.endpoint1EntityId))?.entityKind;
|
|
3640
|
+
if (childKind !== "track" && childKind !== "clip") continue;
|
|
3641
|
+
const parent = String(relation.endpoint0EntityId);
|
|
3642
|
+
groups.set(parent, [...groups.get(parent) ?? [], String(relation.endpoint1EntityId)]);
|
|
3765
3643
|
}
|
|
3766
|
-
return
|
|
3644
|
+
return groups;
|
|
3767
3645
|
}
|
|
3768
3646
|
function ordering(doc) {
|
|
3769
3647
|
const map = doc.getMap("medeo").get("ordering");
|
|
@@ -3806,19 +3684,6 @@ function writeEntityOrder(doc, group, roots, token) {
|
|
|
3806
3684
|
//#region src/entity-crdt/entity-document.ts
|
|
3807
3685
|
const LORO_ENTITY_SCHEMA = "medeo.entities.loro.v1";
|
|
3808
3686
|
const MATERIALIZE_ORIGIN = "medeo.materialize";
|
|
3809
|
-
/** Read persisted relation roots before current entity versions are projected. */
|
|
3810
|
-
function readEntityRelationRecords(doc) {
|
|
3811
|
-
if (root(doc).get("schema") !== "medeo.entities.loro.v1") throw new Error("Document has no Loro entity state");
|
|
3812
|
-
return relationRecords(doc).map(({ rootId, identity, map, deleted }) => ({
|
|
3813
|
-
relationId: createRelationId(rootId),
|
|
3814
|
-
relationKind: identity.kind,
|
|
3815
|
-
endpoint0EntityId: createEntityId(identity.endpoints[0]),
|
|
3816
|
-
endpoint1EntityId: createEntityId(identity.endpoints[1]),
|
|
3817
|
-
metadata: requireMap(map, "metadata").toJSON(),
|
|
3818
|
-
trace: requireMap(map, "trace").toJSON(),
|
|
3819
|
-
deleted
|
|
3820
|
-
}));
|
|
3821
|
-
}
|
|
3822
3687
|
function hash(value) {
|
|
3823
3688
|
return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
|
|
3824
3689
|
}
|
|
@@ -3845,12 +3710,7 @@ function content(map) {
|
|
|
3845
3710
|
return requireMap(map, "content").toJSON();
|
|
3846
3711
|
}
|
|
3847
3712
|
function entityContent(doc, key) {
|
|
3848
|
-
|
|
3849
|
-
const order = entityOrdinal(doc, key);
|
|
3850
|
-
return order === void 0 ? payload : {
|
|
3851
|
-
...payload,
|
|
3852
|
-
order
|
|
3853
|
-
};
|
|
3713
|
+
return content(workspace(doc, key));
|
|
3854
3714
|
}
|
|
3855
3715
|
function bases(map) {
|
|
3856
3716
|
const links = requireMap(map, "bases");
|
|
@@ -3910,17 +3770,24 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3910
3770
|
}
|
|
3911
3771
|
};
|
|
3912
3772
|
});
|
|
3913
|
-
const relations =
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3773
|
+
const relations = [];
|
|
3774
|
+
for (const key of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
3775
|
+
const map = section(doc, "relations").get(key);
|
|
3776
|
+
if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
|
|
3777
|
+
const original = map.get("identity");
|
|
3778
|
+
if (original.endpoints.some((id) => !ids.has(family(doc, id)))) continue;
|
|
3779
|
+
const endpoints = original.endpoints.map((id) => currentId(doc, id, ids));
|
|
3780
|
+
const relationId = equalJson(endpoints, original.endpoints) ? key : `relation_${hash([key, endpoints])}`;
|
|
3781
|
+
relations.push({
|
|
3782
|
+
relationId: createRelationId(relationId),
|
|
3783
|
+
relationKind: original.kind,
|
|
3918
3784
|
endpoint0EntityId: createEntityId(endpoints[0]),
|
|
3919
3785
|
endpoint1EntityId: createEntityId(endpoints[1]),
|
|
3920
3786
|
metadata: requireMap(map, "metadata").toJSON(),
|
|
3921
3787
|
trace: requireMap(map, "trace").toJSON()
|
|
3922
|
-
};
|
|
3923
|
-
}
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
sortSequencedRelations(doc, relations);
|
|
3924
3791
|
const project = {};
|
|
3925
3792
|
for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
|
|
3926
3793
|
if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
|
|
@@ -3939,32 +3806,6 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3939
3806
|
}
|
|
3940
3807
|
return state;
|
|
3941
3808
|
}
|
|
3942
|
-
function relationRecords(doc) {
|
|
3943
|
-
const records = [];
|
|
3944
|
-
for (const rootId of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
3945
|
-
const map = section(doc, "relations").get(rootId);
|
|
3946
|
-
if (!(map instanceof LoroMap)) throw new Error(`Invalid relation record ${rootId}`);
|
|
3947
|
-
const value = map.get("identity");
|
|
3948
|
-
if (!isJsonObject(value) || typeof value.kind !== "string" || !Array.isArray(value.endpoints) || value.endpoints.length !== 2 || !value.endpoints.every((endpoint) => typeof endpoint === "string")) throw new Error(`Invalid relation identity ${rootId}`);
|
|
3949
|
-
const identity = {
|
|
3950
|
-
kind: value.kind,
|
|
3951
|
-
endpoints: [value.endpoints[0], value.endpoints[1]]
|
|
3952
|
-
};
|
|
3953
|
-
records.push({
|
|
3954
|
-
rootId,
|
|
3955
|
-
identity,
|
|
3956
|
-
map,
|
|
3957
|
-
deleted: map.get("deleted") === true
|
|
3958
|
-
});
|
|
3959
|
-
}
|
|
3960
|
-
return records;
|
|
3961
|
-
}
|
|
3962
|
-
function activeRelationRecords(doc, ids) {
|
|
3963
|
-
return relationRecords(doc).filter(({ identity, deleted }) => {
|
|
3964
|
-
if (deleted) return false;
|
|
3965
|
-
return identity.endpoints.every((id) => ids.has(family(doc, id)));
|
|
3966
|
-
});
|
|
3967
|
-
}
|
|
3968
3809
|
/** Native editing containers survive domain version changes and merge independently. */
|
|
3969
3810
|
var LoroEntityDocument = class LoroEntityDocument {
|
|
3970
3811
|
doc;
|
|
@@ -4195,47 +4036,16 @@ var LoroEntityDraft = class {
|
|
|
4195
4036
|
});
|
|
4196
4037
|
}
|
|
4197
4038
|
link(row) {
|
|
4198
|
-
if (row.relationKind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
|
|
4199
|
-
if (row.relationKind === "generated" || row.relationKind === "clip-anchor") throw new Error(`Author ordered ${row.relationKind} Relations with the dedicated role-named method`);
|
|
4200
4039
|
this.record({
|
|
4201
4040
|
type: "relation",
|
|
4202
4041
|
row: clone(row)
|
|
4203
4042
|
});
|
|
4204
4043
|
}
|
|
4205
|
-
|
|
4206
|
-
this.record({
|
|
4207
|
-
type: "relation",
|
|
4208
|
-
row: {
|
|
4209
|
-
relationId: input.relationId,
|
|
4210
|
-
relationKind: "generated",
|
|
4211
|
-
endpoint0EntityId: input.outputEntityId,
|
|
4212
|
-
endpoint1EntityId: input.inputEntityId,
|
|
4213
|
-
metadata: {},
|
|
4214
|
-
trace: clone(input.trace ?? {})
|
|
4215
|
-
}
|
|
4216
|
-
});
|
|
4217
|
-
}
|
|
4218
|
-
linkClipAnchor(input) {
|
|
4219
|
-
this.record({
|
|
4220
|
-
type: "relation",
|
|
4221
|
-
row: {
|
|
4222
|
-
relationId: input.relationId,
|
|
4223
|
-
relationKind: "clip-anchor",
|
|
4224
|
-
endpoint0EntityId: input.childClipEntityId,
|
|
4225
|
-
endpoint1EntityId: input.hostClipEntityId,
|
|
4226
|
-
metadata: {},
|
|
4227
|
-
trace: clone(input.trace ?? {})
|
|
4228
|
-
}
|
|
4229
|
-
});
|
|
4230
|
-
}
|
|
4231
|
-
patchRelationTrace(relationId, trace) {
|
|
4232
|
-
const relation = this.snapshot().rows.relations.find((row) => row.relationId === relationId);
|
|
4233
|
-
if (relation == null) throw new Error(`Unknown relation ${relationId}`);
|
|
4234
|
-
if (relation.relationKind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
|
|
4044
|
+
patchRelation(relationId, metadata, trace) {
|
|
4235
4045
|
this.record({
|
|
4236
4046
|
type: "relation-fields",
|
|
4237
4047
|
root: this.relationRoot(relationId),
|
|
4238
|
-
metadata: clone(
|
|
4048
|
+
metadata: clone(metadata),
|
|
4239
4049
|
trace: clone(trace)
|
|
4240
4050
|
});
|
|
4241
4051
|
}
|
|
@@ -4331,9 +4141,8 @@ var LoroEntityDraft = class {
|
|
|
4331
4141
|
const key = this.relationRoot(relationId);
|
|
4332
4142
|
const map = section(this.doc, "relations").get(key);
|
|
4333
4143
|
if (!(map instanceof LoroMap) || map.get("deleted") === true) throw new Error("Unknown relation");
|
|
4334
|
-
if (map.get("identity").kind === "caption-alignment") throw new Error("caption-alignment Relations are read-only legacy facts");
|
|
4335
4144
|
for (const change of changes) {
|
|
4336
|
-
if (change.path[0]
|
|
4145
|
+
if (!["metadata", "trace"].includes(change.path[0]) || change.path.length < 2) throw new Error("Relation changes may only edit metadata or trace fields");
|
|
4337
4146
|
const payload = {
|
|
4338
4147
|
metadata: requireMap(map, "metadata").toJSON(),
|
|
4339
4148
|
trace: requireMap(map, "trace").toJSON()
|
|
@@ -4514,6 +4323,30 @@ function materialize(doc, allowSelectionConflicts = false) {
|
|
|
4514
4323
|
const project = section(doc, "project");
|
|
4515
4324
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4516
4325
|
}
|
|
4326
|
+
/**
|
|
4327
|
+
* Put each container's sequenced Relations in the order its native list states.
|
|
4328
|
+
*
|
|
4329
|
+
* Per container, never across: a Timeline's Tracks and a Track's Clips are two
|
|
4330
|
+
* separate orders, and sorting them together would interleave them.
|
|
4331
|
+
*/
|
|
4332
|
+
function sortSequencedRelations(doc, relations) {
|
|
4333
|
+
const containers = /* @__PURE__ */ new Map();
|
|
4334
|
+
for (const [slot, relation] of relations.entries()) {
|
|
4335
|
+
if (!isSequencedRelationKind(relation.relationKind)) continue;
|
|
4336
|
+
const container = String(relation.endpoint0EntityId);
|
|
4337
|
+
const ordinal = entityOrdinal(doc, family(doc, String(relation.endpoint1EntityId))) ?? slot;
|
|
4338
|
+
containers.set(container, [...containers.get(container) ?? [], {
|
|
4339
|
+
slot,
|
|
4340
|
+
relation,
|
|
4341
|
+
ordinal
|
|
4342
|
+
}]);
|
|
4343
|
+
}
|
|
4344
|
+
for (const members of containers.values()) {
|
|
4345
|
+
const slots = members.map((member) => member.slot);
|
|
4346
|
+
const ordered = [...members].sort((left, right) => left.ordinal - right.ordinal);
|
|
4347
|
+
slots.forEach((slot, at) => relations[slot] = ordered[at].relation);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4517
4350
|
//#endregion
|
|
4518
4351
|
//#region src/document/document-mutation-guard.ts
|
|
4519
4352
|
/**
|
|
@@ -4849,4 +4682,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4849
4682
|
return new PlainMemoryAdapter(document, options);
|
|
4850
4683
|
}
|
|
4851
4684
|
//#endregion
|
|
4852
|
-
export {
|
|
4685
|
+
export { assertValidVideoDocument as $, decodeEntityRelationRows as A, updateEntityFields as B, applyFieldChanges as C, speedOf as Ct, EntityProjectionGraph as D, base64ToBytes as Dt, projectEntityTimeline as E, videoDocumentMirrorSchema as Et, assembleEntityContent as F, hasSequence as G, isJsonObject as H, assemblePhoneticScriptContent as I, buildSpeechHostMap as J, isMediaAssetVariantKind as K, assembleScriptText as L, relationsTo as M, ScriptCompositionError as N, resolveEntityTimelineLayout as O, bytesToBase64 as Ot, assembleCaptionContent as P, VideoDocumentValidationError as Q, findComposedAudioScript as R, equalJson as S, effectiveVideoClipDurationMs as St, resolveFieldPath as T, recordEntries as Tt, createEntityId as U, variantBaseEntityIds as V, createRelationId as W, fromVideoDocument as X, derivePositionFromAbs as Y, toVideoDocument as Z, audioScriptAssetOf as _, TIMELINE_SKELETON_DURATION_MS as _t, createMirrorVideoDocument as a, findLaneTrack as at, phoneticScriptAssetOf as b, safeDurationMs as bt, DocumentMutationGuard as c, solveVideoDocument as ct, LoroEntityDraft as d, fillMainTrackTimeGaps as dt, validateVideoDocument as et, readEntityDocument as f, reassignSpeechesToVideoClipsByTime as ft, audioScriptAssetHash as g, syncAggregatedClipsTimePosition as gt, audioScriptAssetFields as h, resolveSpeechOverlapByShiftingVideos as ht, MirrorVideoDocumentAdapter as i, ensureLaneTrack as it, relationsFrom as j, EntityTimelineProjectionError as k, LORO_ENTITY_SCHEMA as l, cascadeAfterVideoClipChanges as lt, audioScriptAssetContent as m, resolveAllSpeechOverlaps as mt, createPlainMemoryAdapter as n, videoDocumentSchema as nt, createMirrorVideoDocumentAdapter as o, hasResolvedPlacement as ot, entityOrderGroups as p, recalculateTimelineDuration as pt, buildInitialVideoDocument as q, generatePartId as r, LANE_KINDS_IN_STACK_ORDER as rt, readVideoDocumentFromDraft as s, laneTrackId as st, PlainMemoryAdapter as t, partUnionSchema as tt, LoroEntityDocument as u, arrangeMainTrackSeamlessly as ut, phoneticScriptAssetContent as v, isEmptyVideoClip as vt, assertFieldChanges as w, partUnionToDraft as wt, scriptSourceAssetOf as x, DEFAULT_UNIT_TIME_MS as xt, phoneticScriptAssetHash as y, partDurationMs as yt, selectAudioScriptSegment as z };
|