@mengine/medeo-client 2.0.1-alpha.22 → 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-BB8U25jt.js → document-Ct11SMBS.js} +290 -336
- package/dist/{index-C0uUWqkz.d.ts → index-CcIoMxs1.d.ts} +12 -17
- package/dist/index.d.ts +35 -27
- package/dist/index.js +218 -377
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/package.json +4 -4
|
@@ -1446,6 +1446,20 @@ function emptyLaneTracks() {
|
|
|
1446
1446
|
}));
|
|
1447
1447
|
}
|
|
1448
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
|
|
1449
1463
|
//#region ../medeo-dsl/src/entities.ts
|
|
1450
1464
|
/**
|
|
1451
1465
|
* What every known entity kind declares.
|
|
@@ -1467,12 +1481,6 @@ function emptyLaneTracks() {
|
|
|
1467
1481
|
* flowed, is the `from-asset` / `to-asset` Relation's answer alone.
|
|
1468
1482
|
*/
|
|
1469
1483
|
const ENTITY_KINDS = Object.freeze({
|
|
1470
|
-
axvideo: Object.freeze([
|
|
1471
|
-
"Container",
|
|
1472
|
-
"Sequence",
|
|
1473
|
-
"Visual",
|
|
1474
|
-
"Audible"
|
|
1475
|
-
]),
|
|
1476
1484
|
timeline: Object.freeze(["Container"]),
|
|
1477
1485
|
track: Object.freeze(["Container"]),
|
|
1478
1486
|
clip: Object.freeze(["Container"]),
|
|
@@ -1488,7 +1496,7 @@ const ENTITY_KINDS = Object.freeze({
|
|
|
1488
1496
|
"Audible",
|
|
1489
1497
|
"MediaFile"
|
|
1490
1498
|
]),
|
|
1491
|
-
voiceover: Object.freeze([
|
|
1499
|
+
"voiceover-recording": Object.freeze([
|
|
1492
1500
|
"Sequence",
|
|
1493
1501
|
"Audible",
|
|
1494
1502
|
"MediaFile"
|
|
@@ -1500,7 +1508,6 @@ const ENTITY_KINDS = Object.freeze({
|
|
|
1500
1508
|
]),
|
|
1501
1509
|
voice: Object.freeze(["Container", "IPBound"]),
|
|
1502
1510
|
"sequence-marker": Object.freeze([]),
|
|
1503
|
-
viewport: Object.freeze([]),
|
|
1504
1511
|
"audio-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1505
1512
|
"phonetic-script": Object.freeze(["SegmentText", "MediaFile"]),
|
|
1506
1513
|
caption: Object.freeze([
|
|
@@ -1533,7 +1540,7 @@ function isSequenceFields(value) {
|
|
|
1533
1540
|
return duration === null || typeof duration === "number" && Number.isSafeInteger(duration) && duration > 0;
|
|
1534
1541
|
}
|
|
1535
1542
|
function isMediaAssetVariantKind(kind) {
|
|
1536
|
-
return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover";
|
|
1543
|
+
return kind === "video" || kind === "image" || kind === "audio" || kind === "voiceover-recording";
|
|
1537
1544
|
}
|
|
1538
1545
|
function isKnownEntityKind(kind) {
|
|
1539
1546
|
return Object.hasOwn(ENTITY_KINDS, kind);
|
|
@@ -1805,7 +1812,7 @@ function entityToRow(entity) {
|
|
|
1805
1812
|
//#region ../medeo-dsl/src/invariants.ts
|
|
1806
1813
|
/** An entity is made from at most one Asset; two would leave its bytes ambiguous. */
|
|
1807
1814
|
/** Validates one complete set of entities and its authoritative Relation rows. */
|
|
1808
|
-
function validateEntityRelationSet(entityRefs, index
|
|
1815
|
+
function validateEntityRelationSet(entityRefs, index) {
|
|
1809
1816
|
const { entities, issues } = collectRelatedEntities(entityRefs, index);
|
|
1810
1817
|
const entityIds = new Set(entities.map((entity) => entity.entityId));
|
|
1811
1818
|
const rows = {
|
|
@@ -1829,17 +1836,13 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
1829
1836
|
if (current.entityKind === "sequence-marker") {
|
|
1830
1837
|
const marker = entity;
|
|
1831
1838
|
issues.push(...validateMarkerUse(entity, index));
|
|
1832
|
-
issues.push(...validateMarkerRanges(marker
|
|
1833
|
-
|
|
1834
|
-
target: (left, right) => options.compareMarkerPoints(marker, "target", left, right)
|
|
1835
|
-
}));
|
|
1836
|
-
issues.push(...validateMarkerSourceBounds(marker, index, (left, right) => options.compareMarkerPoints(marker, "source", left, right)));
|
|
1839
|
+
issues.push(...validateMarkerRanges(marker));
|
|
1840
|
+
issues.push(...validateMarkerSourceBounds(marker, index));
|
|
1837
1841
|
}
|
|
1838
1842
|
if (current.entityKind === "clip") {
|
|
1839
1843
|
issues.push(...validateClipAdmission(entity, index));
|
|
1840
1844
|
issues.push(...validateClipPlacement(entity, index));
|
|
1841
1845
|
}
|
|
1842
|
-
if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
|
|
1843
1846
|
if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
|
|
1844
1847
|
if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
|
|
1845
1848
|
issues.push(...validateSequenceComposition(entity));
|
|
@@ -1848,18 +1851,16 @@ function validateEntityRelationSet(entityRefs, index, options) {
|
|
|
1848
1851
|
return issues;
|
|
1849
1852
|
}
|
|
1850
1853
|
function validateMarkerUse(marker, index) {
|
|
1851
|
-
const
|
|
1852
|
-
const
|
|
1853
|
-
const
|
|
1854
|
-
const
|
|
1855
|
-
const timelineEdges = ofKind(relations, "marker-timeline");
|
|
1856
|
-
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");
|
|
1857
1858
|
const issues = [];
|
|
1858
1859
|
if (scriptEdges.length > 0) {
|
|
1859
|
-
if (clipEdges.length
|
|
1860
|
+
if (clipEdges.length > 0) issues.push({
|
|
1860
1861
|
code: "marker_container_xor",
|
|
1861
1862
|
entityId: marker.entityId,
|
|
1862
|
-
message: "An AudioScript annotation Marker must not carry Clip
|
|
1863
|
+
message: "An AudioScript annotation Marker must not carry Clip or content relations"
|
|
1863
1864
|
});
|
|
1864
1865
|
const ranges = marker.current().segmentRanges;
|
|
1865
1866
|
if (!ranges?.length) issues.push({
|
|
@@ -1886,64 +1887,44 @@ function validateMarkerUse(marker, index) {
|
|
|
1886
1887
|
entityId: marker.entityId,
|
|
1887
1888
|
message: "segmentRanges belongs to an AudioScript annotation Marker, not a display Marker"
|
|
1888
1889
|
});
|
|
1889
|
-
if (clipEdges.length
|
|
1890
|
+
if (clipEdges.length !== 1) issues.push({
|
|
1890
1891
|
code: "marker_container_xor",
|
|
1891
1892
|
entityId: marker.entityId,
|
|
1892
|
-
message: "Sequence Marker
|
|
1893
|
-
});
|
|
1894
|
-
if (contentEdges.length + timelineEdges.length !== 1) issues.push({
|
|
1895
|
-
code: "marker_content_xor",
|
|
1896
|
-
entityId: marker.entityId,
|
|
1897
|
-
message: "Sequence Marker must have exactly one Sequence content XOR Timeline relation"
|
|
1898
|
-
});
|
|
1899
|
-
if (clipEdges.length === 1 && timelineEdges.length === 1 || axVideoEdges.length === 1 && contentEdges.length === 1) issues.push({
|
|
1900
|
-
code: "marker_pair_mismatch",
|
|
1901
|
-
entityId: marker.entityId,
|
|
1902
|
-
message: "Only Clip+Content or AXVideo+Timeline Marker relation pairs are valid"
|
|
1893
|
+
message: "A display Sequence Marker belongs to exactly one Clip"
|
|
1903
1894
|
});
|
|
1904
1895
|
return issues;
|
|
1905
1896
|
}
|
|
1906
1897
|
function validateClipAdmission(clip, index) {
|
|
1907
|
-
const markerEdges =
|
|
1898
|
+
const markerEdges = markersOn(clip, index);
|
|
1908
1899
|
if (markerEdges.length !== 1) return [{
|
|
1909
1900
|
code: "clip_marker_cardinality",
|
|
1910
1901
|
entityId: clip.entityId,
|
|
1911
1902
|
message: "Clip must have exactly one authoritative Clip-Marker Relation"
|
|
1912
1903
|
}];
|
|
1913
|
-
|
|
1914
|
-
if (marker == null) return [{
|
|
1904
|
+
if (markerEdges[0]?.other(clip)?.deref() == null) return [{
|
|
1915
1905
|
code: "clip_content_cardinality",
|
|
1916
1906
|
entityId: clip.entityId,
|
|
1917
|
-
message: "Clip must resolve one live Sequence Marker
|
|
1907
|
+
message: "Clip must resolve one live Sequence Marker"
|
|
1918
1908
|
}];
|
|
1919
|
-
const contentEdges =
|
|
1909
|
+
const contentEdges = holds(clip, index);
|
|
1920
1910
|
if (contentEdges.length !== 1) return [{
|
|
1921
1911
|
code: "clip_content_cardinality",
|
|
1922
1912
|
entityId: clip.entityId,
|
|
1923
|
-
message: "Clip
|
|
1913
|
+
message: "Clip must contain exactly one content entity"
|
|
1924
1914
|
}];
|
|
1925
|
-
const content = contentEdges[0]?.other(
|
|
1915
|
+
const content = contentEdges[0]?.other(clip)?.deref()?.current();
|
|
1926
1916
|
if (content == null) return [{
|
|
1927
1917
|
code: "clip_content_cardinality",
|
|
1928
1918
|
entityId: clip.entityId,
|
|
1929
|
-
message: "Clip
|
|
1919
|
+
message: "Clip content Relation must resolve one live entity"
|
|
1930
1920
|
}];
|
|
1931
1921
|
if (!hasSequence(content)) return [{
|
|
1932
1922
|
code: "clip_content_not_sequence",
|
|
1933
1923
|
entityId: clip.entityId,
|
|
1934
|
-
message: `Clip
|
|
1924
|
+
message: `Clip contains non-Sequence entity "${content.entityId}"`
|
|
1935
1925
|
}];
|
|
1936
1926
|
return [];
|
|
1937
1927
|
}
|
|
1938
|
-
function validateAXVideoAdmission(axVideo, index) {
|
|
1939
|
-
const markerEdges = ofKind([...index.relationsOf(axVideo)], "axvideo-marker");
|
|
1940
|
-
if (markerEdges.length === 1 && markerEdges[0]?.other(axVideo)?.deref() != null) return [];
|
|
1941
|
-
return [{
|
|
1942
|
-
code: "axvideo_marker_cardinality",
|
|
1943
|
-
entityId: axVideo.entityId,
|
|
1944
|
-
message: "AXVideo must have exactly one live AXVideo-Marker Relation"
|
|
1945
|
-
}];
|
|
1946
|
-
}
|
|
1947
1928
|
/** Validate a Caption against the complete entity set, including its directly held bases. */
|
|
1948
1929
|
function validateCaptionComposition(caption, index, entities = []) {
|
|
1949
1930
|
return validateVariantComposition(caption, index, entities, "caption");
|
|
@@ -1970,33 +1951,30 @@ function validateVariantComposition(variant, index, entities, kind) {
|
|
|
1970
1951
|
return issues;
|
|
1971
1952
|
}
|
|
1972
1953
|
function validateClipPlacement(clip, index) {
|
|
1973
|
-
const marker =
|
|
1954
|
+
const marker = markersOn(clip, index)[0]?.other(clip)?.deref();
|
|
1974
1955
|
if (marker?.current().entityKind !== "sequence-marker") return [];
|
|
1975
1956
|
const markerValue = marker.current();
|
|
1976
1957
|
const anchors = ofKind([...index.relationsOf(clip)], "clip-anchor").filter((relation) => relation.endpoints[0].deref() === clip);
|
|
1977
|
-
const
|
|
1978
|
-
const hasTarget = markerValue.targetRange !== void 0;
|
|
1979
|
-
const hasAnchorOffset = markerValue.anchorOffset !== void 0;
|
|
1958
|
+
const hasStart = markerValue.startMs !== void 0;
|
|
1980
1959
|
const issues = [];
|
|
1981
1960
|
if (anchors.length > 1) issues.push({
|
|
1982
1961
|
code: "clip_anchor_cardinality",
|
|
1983
1962
|
entityId: clip.entityId,
|
|
1984
1963
|
message: "A Clip may follow at most one host Clip"
|
|
1985
1964
|
});
|
|
1986
|
-
|
|
1987
|
-
if (hasAnchor && (!hasAnchorOffset || hasOrder || hasTarget) || !hasAnchor && (hasAnchorOffset || Number(hasOrder) + Number(hasTarget) !== 1)) issues.push({
|
|
1965
|
+
if (anchors.length > 0 && !hasStart) issues.push({
|
|
1988
1966
|
code: "clip_placement_invalid",
|
|
1989
1967
|
entityId: clip.entityId,
|
|
1990
|
-
message: "Clip
|
|
1968
|
+
message: "A Clip following a host states startMs, the offset inside that host"
|
|
1991
1969
|
});
|
|
1992
|
-
if (markerValue.
|
|
1993
|
-
const content =
|
|
1994
|
-
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();
|
|
1995
1973
|
const trackRole = track?.entityKind === "track" ? track.role : void 0;
|
|
1996
|
-
if (content?.entityKind !== "audio" || trackRole !== "bgm"
|
|
1974
|
+
if (content?.entityKind !== "audio" || trackRole !== "bgm") issues.push({
|
|
1997
1975
|
code: "timeline_duration_policy_invalid",
|
|
1998
1976
|
entityId: marker.entityId,
|
|
1999
|
-
message: "Marker
|
|
1977
|
+
message: "Marker fillTimeline is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
|
|
2000
1978
|
});
|
|
2001
1979
|
}
|
|
2002
1980
|
return issues;
|
|
@@ -2028,34 +2006,28 @@ function validateClipAnchorCycles(entities, index) {
|
|
|
2028
2006
|
}
|
|
2029
2007
|
return issues;
|
|
2030
2008
|
}
|
|
2031
|
-
function validateMarkerRanges(marker
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2009
|
+
function validateMarkerRanges(marker) {
|
|
2010
|
+
const { inMs, outMs } = marker.current();
|
|
2011
|
+
if (Number.isFinite(inMs) && Number.isFinite(outMs) && outMs > inMs) return [];
|
|
2012
|
+
return [{
|
|
2035
2013
|
code: "marker_source_range_empty",
|
|
2036
2014
|
entityId: marker.entityId,
|
|
2037
|
-
message: "Sequence Marker
|
|
2038
|
-
}
|
|
2039
|
-
if (current.targetRange != null && !(compare.target(current.targetRange.start, current.targetRange.end) < 0)) issues.push({
|
|
2040
|
-
code: "marker_target_range_empty",
|
|
2041
|
-
entityId: marker.entityId,
|
|
2042
|
-
message: "Sequence Marker targetRange must be a non-empty half-open interval"
|
|
2043
|
-
});
|
|
2044
|
-
return issues;
|
|
2015
|
+
message: "Sequence Marker outMs must be a finite millisecond value greater than inMs"
|
|
2016
|
+
}];
|
|
2045
2017
|
}
|
|
2046
|
-
function validateMarkerSourceBounds(marker, index
|
|
2047
|
-
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);
|
|
2048
2022
|
if (contentEdges.length !== 1) return [];
|
|
2049
|
-
const content = contentEdges[0]?.other(
|
|
2023
|
+
const content = contentEdges[0]?.other(clip)?.deref()?.current();
|
|
2050
2024
|
if (content == null || !hasSequence(content)) return [];
|
|
2051
|
-
const
|
|
2052
|
-
|
|
2053
|
-
const endVsDuration = content.durationMs == null ? void 0 : compare(sourceRange.end, content.durationMs);
|
|
2054
|
-
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 [];
|
|
2055
2027
|
return [{
|
|
2056
2028
|
code: "marker_source_out_of_bounds",
|
|
2057
2029
|
entityId: marker.entityId,
|
|
2058
|
-
message:
|
|
2030
|
+
message: "Sequence Marker in and out points must stay inside the length its content states"
|
|
2059
2031
|
}];
|
|
2060
2032
|
}
|
|
2061
2033
|
function validateSequenceComposition(entity) {
|
|
@@ -2102,21 +2074,20 @@ function validateKnownEntityPayload(entity) {
|
|
|
2102
2074
|
"extent",
|
|
2103
2075
|
"sampling",
|
|
2104
2076
|
"coordinateSpace",
|
|
2105
|
-
"
|
|
2106
|
-
"
|
|
2107
|
-
"duration",
|
|
2077
|
+
"inMs",
|
|
2078
|
+
"outMs",
|
|
2108
2079
|
"startMs",
|
|
2109
|
-
"
|
|
2080
|
+
"rate"
|
|
2110
2081
|
]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an annotation Marker instead`);
|
|
2111
2082
|
}
|
|
2112
2083
|
switch (entity.entityKind) {
|
|
2113
2084
|
case "track":
|
|
2114
2085
|
validateOptionalField(value, "hidden", "boolean", problems);
|
|
2115
2086
|
validateOptionalField(value, "role", "string", problems);
|
|
2116
|
-
validateOptionalFiniteNumber(value, "order", problems);
|
|
2117
2087
|
break;
|
|
2118
2088
|
case "video":
|
|
2119
2089
|
case "audio":
|
|
2090
|
+
case "voiceover-recording":
|
|
2120
2091
|
validateDurationPayload(value, "own", problems);
|
|
2121
2092
|
break;
|
|
2122
2093
|
case "caption":
|
|
@@ -2130,12 +2101,7 @@ function validateKnownEntityPayload(entity) {
|
|
|
2130
2101
|
validateDurationPayload(value, "none", problems);
|
|
2131
2102
|
break;
|
|
2132
2103
|
case "axvideo":
|
|
2133
|
-
|
|
2134
|
-
"extent",
|
|
2135
|
-
"sampling",
|
|
2136
|
-
"coordinateSpace",
|
|
2137
|
-
"durationMs"
|
|
2138
|
-
]) if (Object.hasOwn(value, retired)) problems.push(`${retired} is derived from the Timeline, not stored`);
|
|
2104
|
+
validateDurationPayload(value, "none", problems);
|
|
2139
2105
|
break;
|
|
2140
2106
|
case "sequence-marker":
|
|
2141
2107
|
validateMarkerPayload(value, problems);
|
|
@@ -2149,7 +2115,6 @@ function validateKnownEntityPayload(entity) {
|
|
|
2149
2115
|
case "timeline":
|
|
2150
2116
|
case "viewport": break;
|
|
2151
2117
|
case "clip":
|
|
2152
|
-
validateOptionalFiniteNumber(value, "order", problems);
|
|
2153
2118
|
validateOptionalFiniteNumber(value, "volume", problems);
|
|
2154
2119
|
if (typeof value.volume === "number" && (value.volume < -60 || value.volume > 20)) problems.push("volume must be decibels between -60 and 20");
|
|
2155
2120
|
break;
|
|
@@ -2183,17 +2148,16 @@ function validateDurationPayload(value, expected, problems) {
|
|
|
2183
2148
|
if (!isPositiveInteger(duration)) problems.push("durationMs must be positive whole milliseconds");
|
|
2184
2149
|
}
|
|
2185
2150
|
function validateMarkerPayload(value, problems) {
|
|
2186
|
-
|
|
2187
|
-
if (
|
|
2188
|
-
|
|
2189
|
-
if (!
|
|
2190
|
-
|
|
2191
|
-
if (!Object.hasOwn(duration, "value") || duration.value === void 0) problems.push("duration.value is required when duration.mode is \"fixed\"");
|
|
2192
|
-
} else if (duration.mode !== "from-source") problems.push("duration.mode must be \"from-source\" or \"fixed\"");
|
|
2193
|
-
if (value.anchorOffset === void 0 && Object.hasOwn(value, "anchorOffset")) problems.push("anchorOffset cannot be undefined when present");
|
|
2194
|
-
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");
|
|
2195
2156
|
if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
|
|
2196
2157
|
}
|
|
2158
|
+
function isWholeMilliseconds(value) {
|
|
2159
|
+
return typeof value === "number" && Number.isSafeInteger(value);
|
|
2160
|
+
}
|
|
2197
2161
|
/** Annotation Marker time values are directly assigned facts; no cross-references are allowed. */
|
|
2198
2162
|
function validateSegmentRanges(value, problems) {
|
|
2199
2163
|
if (!Array.isArray(value)) {
|
|
@@ -2292,19 +2256,10 @@ function validateCaptionPayload(value, problems) {
|
|
|
2292
2256
|
"strokeColor"
|
|
2293
2257
|
]) if (style[key] !== void 0 && typeof style[key] !== "string") problems.push(`style.${key} must be a string`);
|
|
2294
2258
|
}
|
|
2295
|
-
function validateRange(value, path, required, problems) {
|
|
2296
|
-
if (!isRecord(value)) {
|
|
2297
|
-
if (required) problems.push(`${path} must be an object`);
|
|
2298
|
-
return;
|
|
2299
|
-
}
|
|
2300
|
-
if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
|
|
2301
|
-
if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
|
|
2302
|
-
}
|
|
2303
2259
|
/** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
|
|
2304
2260
|
function validatePhoneticScriptPayload(value, problems) {
|
|
2305
2261
|
if (value.segments !== void 0) validateScriptPayload(value, problems);
|
|
2306
2262
|
if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
|
|
2307
|
-
if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
|
|
2308
2263
|
}
|
|
2309
2264
|
function validateScriptPayload(value, problems) {
|
|
2310
2265
|
if (!Array.isArray(value.segments)) {
|
|
@@ -2321,9 +2276,8 @@ function validateScriptPayload(value, problems) {
|
|
|
2321
2276
|
"endMs",
|
|
2322
2277
|
"start_ms",
|
|
2323
2278
|
"end_ms",
|
|
2324
|
-
"
|
|
2325
|
-
"
|
|
2326
|
-
"duration"
|
|
2279
|
+
"inMs",
|
|
2280
|
+
"outMs"
|
|
2327
2281
|
]) if (Object.hasOwn(segment, key)) problems.push(`segments[${index}].${key} is timing; attach an AudioScript annotation Marker instead`);
|
|
2328
2282
|
if (typeof segment.segmentId !== "string") problems.push(`segments[${index}].segmentId must be a string`);
|
|
2329
2283
|
if (typeof segment.text !== "string") problems.push(`segments[${index}].text must be a string`);
|
|
@@ -2378,11 +2332,11 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
|
|
|
2378
2332
|
if ([
|
|
2379
2333
|
"video",
|
|
2380
2334
|
"audio",
|
|
2335
|
+
"voiceover-recording",
|
|
2381
2336
|
"image",
|
|
2382
|
-
"caption"
|
|
2383
|
-
"axvideo"
|
|
2337
|
+
"caption"
|
|
2384
2338
|
].includes(entityKind) && /^durationMs$/.test(path)) return true;
|
|
2385
|
-
if (entityKind === "sequence-marker" && /^(?:
|
|
2339
|
+
if (entityKind === "sequence-marker" && /^(?:fillTimeline|rate)$/.test(path)) return true;
|
|
2386
2340
|
if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
|
|
2387
2341
|
if (entityKind === "asset" && /^(?:system|key|storageKey)$/.test(path)) return true;
|
|
2388
2342
|
if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
|
|
@@ -2450,19 +2404,57 @@ function expectedDurationShape(kind) {
|
|
|
2450
2404
|
function isPositiveInteger(value) {
|
|
2451
2405
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2452
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
|
+
}
|
|
2453
2423
|
function ofKind(relations, kind) {
|
|
2454
2424
|
return relations.filter((relation) => relation.kind === kind);
|
|
2455
2425
|
}
|
|
2456
2426
|
//#endregion
|
|
2457
2427
|
//#region ../medeo-dsl/src/relation-specs.ts
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
const
|
|
2464
|
-
kind: "
|
|
2465
|
-
|
|
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",
|
|
2466
2458
|
validateMetadata: isEmptyMetadata
|
|
2467
2459
|
});
|
|
2468
2460
|
/**
|
|
@@ -2476,6 +2468,8 @@ const markerContentRelationSpec = Object.freeze({
|
|
|
2476
2468
|
*/
|
|
2477
2469
|
const fromAssetRelationSpec = Object.freeze({
|
|
2478
2470
|
kind: "from-asset",
|
|
2471
|
+
ordered: true,
|
|
2472
|
+
sequenced: false,
|
|
2479
2473
|
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2480
2474
|
validateMetadata: isAssetBinding
|
|
2481
2475
|
});
|
|
@@ -2496,117 +2490,87 @@ const fromAssetRelationSpec = Object.freeze({
|
|
|
2496
2490
|
*/
|
|
2497
2491
|
const toAssetRelationSpec = Object.freeze({
|
|
2498
2492
|
kind: "to-asset",
|
|
2493
|
+
ordered: true,
|
|
2494
|
+
sequenced: false,
|
|
2499
2495
|
validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
|
|
2500
2496
|
validateMetadata: isAssetBinding
|
|
2501
2497
|
});
|
|
2502
|
-
/**
|
|
2503
|
-
const
|
|
2504
|
-
|
|
2505
|
-
|
|
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)),
|
|
2506
2521
|
validateMetadata: isEmptyMetadata
|
|
2507
2522
|
});
|
|
2508
2523
|
const captionAlignmentRelationSpec = Object.freeze({
|
|
2509
2524
|
kind: "caption-alignment",
|
|
2510
|
-
|
|
2525
|
+
ordered: true,
|
|
2526
|
+
sequenced: false,
|
|
2527
|
+
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover-recording"])),
|
|
2511
2528
|
validateMetadata: isCaptionAlignmentMetadata
|
|
2512
2529
|
});
|
|
2513
2530
|
/** `clip-anchor(child, host)` means endpoint 0 follows endpoint 1. */
|
|
2514
2531
|
const clipAnchorRelationSpec = Object.freeze({
|
|
2515
2532
|
kind: "clip-anchor",
|
|
2533
|
+
ordered: true,
|
|
2534
|
+
sequenced: false,
|
|
2516
2535
|
validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
|
|
2517
2536
|
validateMetadata: isEmptyMetadata
|
|
2518
2537
|
});
|
|
2519
2538
|
/**
|
|
2520
|
-
* The Voiceover and the PhoneticScript it was spoken from; kinds determine
|
|
2521
|
-
* roles regardless of endpoint positions.
|
|
2522
|
-
*
|
|
2523
|
-
* Every Voiceover must carry one: which words were said, and how, is the only
|
|
2524
|
-
* thing that makes speech reproducible, and a Voiceover that cannot answer it
|
|
2525
|
-
* is refused when its plan is built.
|
|
2526
|
-
*/
|
|
2527
|
-
const phoneticScriptRenderRelationSpec = Object.freeze({
|
|
2528
|
-
kind: "phonetic-script-render",
|
|
2529
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["phonetic-script"])),
|
|
2530
|
-
validateMetadata: isEmptyMetadata
|
|
2531
|
-
});
|
|
2532
|
-
/**
|
|
2533
2539
|
* The timbre identity a Voiceover was synthesized with. Voice stays an
|
|
2534
2540
|
* identity: it never carries the audio itself, so the link between the two is a
|
|
2535
2541
|
* Relation rather than one shared entity, and several takes can name one voice.
|
|
2536
2542
|
*/
|
|
2537
2543
|
const voiceTimbreRelationSpec = Object.freeze({
|
|
2538
2544
|
kind: "voice-timbre",
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
/** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
|
|
2543
|
-
const audioScriptSourceRelationSpec = Object.freeze({
|
|
2544
|
-
kind: "audio-script-source",
|
|
2545
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
|
|
2546
|
-
"audio",
|
|
2547
|
-
"voiceover",
|
|
2548
|
-
"video"
|
|
2549
|
-
])),
|
|
2550
|
-
validateMetadata: isEmptyMetadata
|
|
2551
|
-
});
|
|
2552
|
-
/**
|
|
2553
|
-
* `audio-script-marker(script, marker)` attaches an annotation Sequence Marker
|
|
2554
|
-
* whose `segmentRanges` hold directly assigned per-Segment time values. The
|
|
2555
|
-
* script keeps no Sequence and no Clip admission; annotation Markers cannot
|
|
2556
|
-
* enter Clip/AXVideo use chains (enforced by the marker-use invariant).
|
|
2557
|
-
*/
|
|
2558
|
-
const audioScriptMarkerRelationSpec = Object.freeze({
|
|
2559
|
-
kind: "audio-script-marker",
|
|
2560
|
-
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"])),
|
|
2561
2548
|
validateMetadata: isEmptyMetadata
|
|
2562
2549
|
});
|
|
2563
2550
|
/** Built-in kinds are reserved; callers may add specs only under new names. */
|
|
2564
2551
|
const builtInRelationSpecs = Object.freeze([
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
clipMarkerRelationSpec,
|
|
2568
|
-
markerContentRelationSpec,
|
|
2569
|
-
axVideoMarkerRelationSpec,
|
|
2570
|
-
markerTimelineRelationSpec,
|
|
2552
|
+
containRelationSpec,
|
|
2553
|
+
markerRelationSpec,
|
|
2571
2554
|
fromAssetRelationSpec,
|
|
2572
2555
|
toAssetRelationSpec,
|
|
2573
|
-
|
|
2556
|
+
derivedFromRelationSpec,
|
|
2574
2557
|
captionAlignmentRelationSpec,
|
|
2575
2558
|
clipAnchorRelationSpec,
|
|
2576
|
-
|
|
2577
|
-
voiceTimbreRelationSpec,
|
|
2578
|
-
audioScriptSourceRelationSpec,
|
|
2579
|
-
audioScriptMarkerRelationSpec
|
|
2559
|
+
voiceTimbreRelationSpec
|
|
2580
2560
|
]);
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
validateEndpoints: (endpoints) => hasKinds(endpoints, new Set([a]), new Set([b])),
|
|
2588
|
-
validateMetadata
|
|
2589
|
-
});
|
|
2590
|
-
}
|
|
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
|
+
*/
|
|
2591
2567
|
function hasKinds(endpoints, aKinds, bKinds) {
|
|
2592
|
-
|
|
2593
|
-
const second = endpoints[1].current().entityKind;
|
|
2594
|
-
return aKinds.has(first) && bKinds.has(second) || aKinds.has(second) && bKinds.has(first);
|
|
2595
|
-
}
|
|
2596
|
-
function hasMarkerAndSequence(endpoints) {
|
|
2597
|
-
const first = endpoints[0].current();
|
|
2598
|
-
const second = endpoints[1].current();
|
|
2599
|
-
return first.entityKind === "sequence-marker" && hasSequence(second) || second.entityKind === "sequence-marker" && hasSequence(first);
|
|
2568
|
+
return aKinds.has(endpoints[0].current().entityKind) && bKinds.has(endpoints[1].current().entityKind);
|
|
2600
2569
|
}
|
|
2601
|
-
/**
|
|
2570
|
+
/** `(entity, asset)`: the bound entity first, the Asset naming its bytes second. */
|
|
2602
2571
|
function hasExactlyOneAsset(endpoints) {
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
if (assets[0] === assets[1]) return false;
|
|
2606
|
-
return declaresKind(assets[0] ? kinds[1] : kinds[0], "MediaFile");
|
|
2607
|
-
}
|
|
2608
|
-
function isGeneratedMedia(entity) {
|
|
2609
|
-
return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voiceover";
|
|
2572
|
+
const bound = endpoints[0].current().entityKind;
|
|
2573
|
+
return endpoints[1].current().entityKind === "asset" && bound !== "asset" && declaresKind(bound, "MediaFile");
|
|
2610
2574
|
}
|
|
2611
2575
|
function isEmptyMetadata(value) {
|
|
2612
2576
|
return isJsonObject(value) && Object.keys(value).length === 0;
|
|
@@ -2619,6 +2583,18 @@ function isAssetBinding(value) {
|
|
|
2619
2583
|
function isCaptionAlignmentMetadata(value) {
|
|
2620
2584
|
return isJsonObject(value) && Object.hasOwn(value, "alignment");
|
|
2621
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
|
+
}
|
|
2622
2598
|
//#endregion
|
|
2623
2599
|
//#region ../medeo-dsl/src/relations.ts
|
|
2624
2600
|
function createEntityRef(entity) {
|
|
@@ -2671,16 +2647,15 @@ var BiRelationIndex = class {
|
|
|
2671
2647
|
canonicalRefs = /* @__PURE__ */ new Map();
|
|
2672
2648
|
byRelationId = /* @__PURE__ */ new Map();
|
|
2673
2649
|
link(input) {
|
|
2674
|
-
if (input.spec.kind === "
|
|
2675
|
-
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`);
|
|
2676
2651
|
return this.linkValidated(input);
|
|
2677
2652
|
}
|
|
2678
|
-
/** Author `
|
|
2679
|
-
|
|
2653
|
+
/** Author `derived-from(product, source)` without exposing positional arguments. */
|
|
2654
|
+
linkDerivedFrom(input) {
|
|
2680
2655
|
return this.linkValidated({
|
|
2681
2656
|
relationId: input.relationId,
|
|
2682
|
-
spec:
|
|
2683
|
-
endpoints: [input.
|
|
2657
|
+
spec: derivedFromRelationSpec,
|
|
2658
|
+
endpoints: [input.product, input.source],
|
|
2684
2659
|
metadata: {},
|
|
2685
2660
|
trace: input.trace
|
|
2686
2661
|
});
|
|
@@ -2695,26 +2670,6 @@ var BiRelationIndex = class {
|
|
|
2695
2670
|
trace: input.trace
|
|
2696
2671
|
});
|
|
2697
2672
|
}
|
|
2698
|
-
/** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
|
|
2699
|
-
linkPhoneticScriptRender(input) {
|
|
2700
|
-
return this.linkValidated({
|
|
2701
|
-
relationId: input.relationId,
|
|
2702
|
-
spec: phoneticScriptRenderRelationSpec,
|
|
2703
|
-
endpoints: [input.output, input.phoneticScript],
|
|
2704
|
-
metadata: {},
|
|
2705
|
-
trace: input.trace
|
|
2706
|
-
});
|
|
2707
|
-
}
|
|
2708
|
-
/** Author `audio-script-source(script, source)` without exposing positional arguments. */
|
|
2709
|
-
linkAudioScriptSource(input) {
|
|
2710
|
-
return this.linkValidated({
|
|
2711
|
-
relationId: input.relationId,
|
|
2712
|
-
spec: audioScriptSourceRelationSpec,
|
|
2713
|
-
endpoints: [input.script, input.source],
|
|
2714
|
-
metadata: {},
|
|
2715
|
-
trace: input.trace
|
|
2716
|
-
});
|
|
2717
|
-
}
|
|
2718
2673
|
/**
|
|
2719
2674
|
* Rehydrate a persisted row after resolving its spec and endpoint refs.
|
|
2720
2675
|
*
|
|
@@ -2730,7 +2685,6 @@ var BiRelationIndex = class {
|
|
|
2730
2685
|
this.assertRelationIdAvailable(input.relationId);
|
|
2731
2686
|
const builtInSpec = builtInSpecByKind.get(input.spec.kind);
|
|
2732
2687
|
if (builtInSpec != null && builtInSpec !== input.spec) throw new Error(`Relation kind "${input.spec.kind}" must use its built-in specification`);
|
|
2733
|
-
if (isForbiddenAuthoritativeRelation(input.spec.kind, input.endpoints)) throw new Error("A direct Clip-Content Relation is derived-only and cannot be authoritative");
|
|
2734
2688
|
if (!input.spec.validateEndpoints(input.endpoints)) throw new Error(`Relation "${input.spec.kind}" received invalid endpoints`);
|
|
2735
2689
|
if (!input.spec.validateMetadata(input.metadata)) throw new Error(`Relation "${input.spec.kind}" received invalid metadata`);
|
|
2736
2690
|
this.assertCanonicalRefAvailable(first);
|
|
@@ -2779,12 +2733,6 @@ var BiRelationIndex = class {
|
|
|
2779
2733
|
}
|
|
2780
2734
|
};
|
|
2781
2735
|
const builtInSpecByKind = new Map(builtInRelationSpecs.map((spec) => [spec.kind, spec]));
|
|
2782
|
-
function isForbiddenAuthoritativeRelation(kind, endpoints) {
|
|
2783
|
-
if (kind === "clip-content") return true;
|
|
2784
|
-
const first = endpoints[0].current();
|
|
2785
|
-
const second = endpoints[1].current();
|
|
2786
|
-
return first.entityKind === "clip" && hasSequence(second) || second.entityKind === "clip" && hasSequence(first);
|
|
2787
|
-
}
|
|
2788
2736
|
//#endregion
|
|
2789
2737
|
//#region ../medeo-dsl/src/entity-relation-rows.ts
|
|
2790
2738
|
var InvalidEntityRelationRowsError = class extends Error {
|
|
@@ -2873,7 +2821,7 @@ function decodeEntityRelationRows(rows, options) {
|
|
|
2873
2821
|
issues.push(errorMessage(error));
|
|
2874
2822
|
}
|
|
2875
2823
|
}
|
|
2876
|
-
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}`);
|
|
2877
2825
|
if (issues.length > 0) throw new InvalidEntityRelationRowsError(issues);
|
|
2878
2826
|
return {
|
|
2879
2827
|
entitiesById,
|
|
@@ -2986,7 +2934,7 @@ var EntityProjectionGraph = class {
|
|
|
2986
2934
|
timeline;
|
|
2987
2935
|
constructor(rows) {
|
|
2988
2936
|
this.rows = rows;
|
|
2989
|
-
const decoded = decodeEntityRelationRows(rows, {
|
|
2937
|
+
const decoded = decodeEntityRelationRows(rows, {});
|
|
2990
2938
|
this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
|
|
2991
2939
|
const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
|
|
2992
2940
|
if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
|
|
@@ -3000,6 +2948,13 @@ var EntityProjectionGraph = class {
|
|
|
3000
2948
|
adjacent(id, kind) {
|
|
3001
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));
|
|
3002
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
|
+
}
|
|
3003
2958
|
one(id, relation, kind) {
|
|
3004
2959
|
const values = this.adjacent(id, relation);
|
|
3005
2960
|
if (values.length !== 1 || kind !== void 0 && values[0]?.entityKind !== kind) fail(`entity ${id} requires exactly one ${relation}${kind === void 0 ? "" : ` to ${kind}`}`);
|
|
@@ -3023,52 +2978,44 @@ function resolveEntityTimelineLayout(rows) {
|
|
|
3023
2978
|
function resolveGraphLayout(graph) {
|
|
3024
2979
|
const facts = /* @__PURE__ */ new Map();
|
|
3025
2980
|
const seenRoles = /* @__PURE__ */ new Set();
|
|
3026
|
-
const tracks = graph.
|
|
2981
|
+
const tracks = graph.holds(graph.timeline.entityId).map((track) => {
|
|
3027
2982
|
const role = track.payload.role;
|
|
3028
2983
|
if (track.entityKind !== "track" || typeof role !== "string" || !Object.hasOwn(roleOrder, role)) fail(`unsupported compatibility Track role on ${track.entityId}`);
|
|
3029
2984
|
if (seenRoles.has(role)) fail(`The compatibility editor supports only one ${role} Track`);
|
|
3030
2985
|
seenRoles.add(role);
|
|
3031
2986
|
const typedRole = role;
|
|
3032
2987
|
if (track.payload.hidden !== void 0 && typeof track.payload.hidden !== "boolean") fail("Track hidden must be boolean");
|
|
3033
|
-
const order =
|
|
3034
|
-
const trackFacts = graph.
|
|
3035
|
-
if (clip.entityKind !== "clip" || graph.
|
|
3036
|
-
const marker = graph.one(clip.entityId, "
|
|
3037
|
-
const
|
|
3038
|
-
|
|
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"];
|
|
3039
2996
|
if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
|
|
3040
|
-
const
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3043
|
-
const duration = positive(
|
|
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");
|
|
3044
3001
|
if (!Number.isSafeInteger(duration)) fail("Compatibility duration must be whole milliseconds");
|
|
3045
|
-
const fillTimeline = marker.payload.
|
|
3046
|
-
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");
|
|
3047
3004
|
const anchors = graph.rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
3048
3005
|
if (anchors.length > 1) fail(`Clip ${clip.entityId} has multiple anchors`);
|
|
3049
|
-
const
|
|
3006
|
+
const startMs = marker.payload.startMs === void 0 ? void 0 : milliseconds(marker.payload.startMs, "startMs");
|
|
3050
3007
|
const anchorId = anchors[0]?.endpoint1EntityId;
|
|
3051
3008
|
let mode;
|
|
3052
3009
|
let absoluteStart;
|
|
3053
3010
|
let anchorOffset;
|
|
3054
|
-
let clipOrder;
|
|
3055
3011
|
if (anchorId !== void 0) {
|
|
3056
|
-
if (
|
|
3012
|
+
if (startMs === void 0) fail(`Anchored Marker ${marker.entityId} requires startMs inside its host`);
|
|
3057
3013
|
mode = "anchored";
|
|
3058
|
-
anchorOffset =
|
|
3059
|
-
} else {
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
absoluteStart = milliseconds(target.start, "target start");
|
|
3064
|
-
milliseconds(target.end, "target end");
|
|
3065
|
-
if (Math.abs(target.end - target.start - duration) > 1e-6) fail(`Marker ${marker.entityId} targetRange must match its effective duration`);
|
|
3066
|
-
} else {
|
|
3067
|
-
mode = "sequential";
|
|
3068
|
-
if (clip.payload.order === void 0) fail(`Clip ${clip.entityId} has no placement: targetRange or explicit order is required`);
|
|
3069
|
-
clipOrder = finite(clip.payload.order, "Clip order");
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3014
|
+
anchorOffset = startMs;
|
|
3015
|
+
} else if (startMs !== void 0) {
|
|
3016
|
+
mode = "absolute";
|
|
3017
|
+
absoluteStart = startMs;
|
|
3018
|
+
} else mode = "sequential";
|
|
3072
3019
|
const fact = {
|
|
3073
3020
|
clip,
|
|
3074
3021
|
marker,
|
|
@@ -3077,7 +3024,7 @@ function resolveGraphLayout(graph) {
|
|
|
3077
3024
|
role: typedRole,
|
|
3078
3025
|
duration,
|
|
3079
3026
|
mode,
|
|
3080
|
-
order:
|
|
3027
|
+
order: clipIndex,
|
|
3081
3028
|
absoluteStart,
|
|
3082
3029
|
anchorId,
|
|
3083
3030
|
anchorOffset,
|
|
@@ -3087,8 +3034,7 @@ function resolveGraphLayout(graph) {
|
|
|
3087
3034
|
return fact;
|
|
3088
3035
|
});
|
|
3089
3036
|
if (typedRole === "bgm" && trackFacts.length > 1) fail("The compatibility editor supports only one background Audio Clip");
|
|
3090
|
-
const sequential = trackFacts.filter((fact) => fact.mode === "sequential")
|
|
3091
|
-
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");
|
|
3092
3038
|
sequential.forEach((fact, index) => {
|
|
3093
3039
|
fact.predecessor = sequential[index - 1]?.clip.entityId;
|
|
3094
3040
|
});
|
|
@@ -3181,7 +3127,7 @@ function projectEntityTimeline(rows) {
|
|
|
3181
3127
|
if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
|
|
3182
3128
|
if (track.role === "video_clip") {
|
|
3183
3129
|
const asset = mediaAssetFacts(graph, content);
|
|
3184
|
-
const source =
|
|
3130
|
+
const source = markerWindow(marker);
|
|
3185
3131
|
const external = externalLocator(asset, "memota");
|
|
3186
3132
|
const speed = videoSpeed(content, marker, placement);
|
|
3187
3133
|
parts[clip.entityId] = { video_clip: {
|
|
@@ -3197,9 +3143,9 @@ function projectEntityTimeline(rows) {
|
|
|
3197
3143
|
const asset = mediaAssetFacts(graph, content);
|
|
3198
3144
|
requireUntrimmedAudio(content, marker, placement);
|
|
3199
3145
|
const external = externalLocator(asset, "memota-speech");
|
|
3200
|
-
const renderEdges = graph.adjacent(content.entityId, "
|
|
3146
|
+
const renderEdges = graph.adjacent(content.entityId, "derived-from");
|
|
3201
3147
|
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3202
|
-
const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "
|
|
3148
|
+
const assembled = renderEdges.length > 0 || timbreEdges.length > 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "derived-from", "phonetic-script").entityId) : void 0;
|
|
3203
3149
|
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3204
3150
|
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3205
3151
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
@@ -3210,7 +3156,7 @@ function projectEntityTimeline(rows) {
|
|
|
3210
3156
|
media_duration_ms: placement.duration,
|
|
3211
3157
|
audio_storage_key: storageKey(asset),
|
|
3212
3158
|
origin_speech_id: external.key,
|
|
3213
|
-
audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "
|
|
3159
|
+
audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "derived-from").length > 0 ? assembleScriptText(graph.one(content.entityId, "derived-from", "audio-script")) : ""),
|
|
3214
3160
|
volume: volume.data,
|
|
3215
3161
|
voice: object(voice) ? {
|
|
3216
3162
|
id: typeof voice.key === "string" ? voice.key : void 0,
|
|
@@ -3222,7 +3168,6 @@ function projectEntityTimeline(rows) {
|
|
|
3222
3168
|
const assembled = assembleCaptionContent(rows, content.entityId);
|
|
3223
3169
|
const text = assembled.text;
|
|
3224
3170
|
const cues = captionDisplayCues(assembled, marker, placement.duration);
|
|
3225
|
-
if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
|
|
3226
3171
|
const style = captionStyle(content.payload.style);
|
|
3227
3172
|
const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
|
|
3228
3173
|
const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
|
|
@@ -3239,9 +3184,9 @@ function projectEntityTimeline(rows) {
|
|
|
3239
3184
|
} else {
|
|
3240
3185
|
const asset = mediaAssetFacts(graph, content);
|
|
3241
3186
|
const external = externalLocator(asset, "memota");
|
|
3242
|
-
if (marker.payload.
|
|
3243
|
-
if (marker.payload.
|
|
3244
|
-
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);
|
|
3245
3190
|
if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
|
|
3246
3191
|
parts[clip.entityId] = { bgm: {
|
|
3247
3192
|
id: clip.entityId,
|
|
@@ -3329,22 +3274,26 @@ function captionStyle(value) {
|
|
|
3329
3274
|
return style;
|
|
3330
3275
|
}
|
|
3331
3276
|
function requireUntrimmedAudio(content, marker, placement) {
|
|
3332
|
-
const source =
|
|
3333
|
-
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");
|
|
3334
3286
|
}
|
|
3335
3287
|
function videoSpeed(content, marker, placement) {
|
|
3336
|
-
const
|
|
3337
|
-
if (content.entityKind === "image" &&
|
|
3338
|
-
const source =
|
|
3339
|
-
if (
|
|
3340
|
-
|
|
3341
|
-
if (
|
|
3342
|
-
const rate = positive(remapping.rate, "linear remapping rate");
|
|
3343
|
-
if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("timeRemapping on " + marker.entityId + " must agree with Marker duration");
|
|
3344
|
-
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");
|
|
3345
3294
|
return {
|
|
3346
3295
|
category: "linear",
|
|
3347
|
-
mode:
|
|
3296
|
+
mode: "constant",
|
|
3348
3297
|
config: { linear: { speed: rate } }
|
|
3349
3298
|
};
|
|
3350
3299
|
}
|
|
@@ -3359,7 +3308,7 @@ function videoSpeed(content, marker, placement) {
|
|
|
3359
3308
|
function captionDisplayCues(assembled, marker, duration) {
|
|
3360
3309
|
const ranges = assembled.caption.payload.segmentRanges;
|
|
3361
3310
|
if (!Array.isArray(ranges)) return void 0;
|
|
3362
|
-
const source =
|
|
3311
|
+
const source = markerWindow(marker);
|
|
3363
3312
|
const scale = duration / (source.end - source.start);
|
|
3364
3313
|
return assembled.segments.flatMap((segment) => {
|
|
3365
3314
|
const timing = ranges.find((entry) => object(entry) && entry.segmentId === segment.segmentId);
|
|
@@ -3383,23 +3332,11 @@ function captionDisplayCues(assembled, marker, duration) {
|
|
|
3383
3332
|
}
|
|
3384
3333
|
//#endregion
|
|
3385
3334
|
//#region src/entity-crdt/field-changes.ts
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
"durationMs",
|
|
3392
|
-
"timeRemapping",
|
|
3393
|
-
"textRange",
|
|
3394
|
-
"selection",
|
|
3395
|
-
"external"
|
|
3396
|
-
]);
|
|
3397
|
-
const textFields = new Set(["text", "phonemeScript"]);
|
|
3398
|
-
const orderedFields = new Set([
|
|
3399
|
-
"segments",
|
|
3400
|
-
"segmentRanges",
|
|
3401
|
-
"baseEntityIds"
|
|
3402
|
-
]);
|
|
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" };
|
|
3403
3340
|
const unsafeKeys = new Set([
|
|
3404
3341
|
"__proto__",
|
|
3405
3342
|
"prototype",
|
|
@@ -3687,27 +3624,24 @@ function phoneticScriptAssetOf(rows, entityId) {
|
|
|
3687
3624
|
}
|
|
3688
3625
|
//#endregion
|
|
3689
3626
|
//#region src/entity-crdt/ordering.ts
|
|
3690
|
-
/**
|
|
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
|
+
*/
|
|
3691
3634
|
function entityOrderGroups(rows) {
|
|
3692
3635
|
const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
|
|
3693
3636
|
const groups = /* @__PURE__ */ new Map();
|
|
3694
3637
|
for (const relation of rows.relations) {
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
const
|
|
3699
|
-
|
|
3700
|
-
const child = entities.get(childId);
|
|
3701
|
-
if (typeof child.payload.order !== "number") continue;
|
|
3702
|
-
const parent = endpoints.find((id) => id !== childId);
|
|
3703
|
-
const values = groups.get(parent) ?? [];
|
|
3704
|
-
values.push({
|
|
3705
|
-
id: childId,
|
|
3706
|
-
order: child.payload.order
|
|
3707
|
-
});
|
|
3708
|
-
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)]);
|
|
3709
3643
|
}
|
|
3710
|
-
return
|
|
3644
|
+
return groups;
|
|
3711
3645
|
}
|
|
3712
3646
|
function ordering(doc) {
|
|
3713
3647
|
const map = doc.getMap("medeo").get("ordering");
|
|
@@ -3776,12 +3710,7 @@ function content(map) {
|
|
|
3776
3710
|
return requireMap(map, "content").toJSON();
|
|
3777
3711
|
}
|
|
3778
3712
|
function entityContent(doc, key) {
|
|
3779
|
-
|
|
3780
|
-
const order = entityOrdinal(doc, key);
|
|
3781
|
-
return order === void 0 ? payload : {
|
|
3782
|
-
...payload,
|
|
3783
|
-
order
|
|
3784
|
-
};
|
|
3713
|
+
return content(workspace(doc, key));
|
|
3785
3714
|
}
|
|
3786
3715
|
function bases(map) {
|
|
3787
3716
|
const links = requireMap(map, "bases");
|
|
@@ -3858,6 +3787,7 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3858
3787
|
trace: requireMap(map, "trace").toJSON()
|
|
3859
3788
|
});
|
|
3860
3789
|
}
|
|
3790
|
+
sortSequencedRelations(doc, relations);
|
|
3861
3791
|
const project = {};
|
|
3862
3792
|
for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
|
|
3863
3793
|
if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
|
|
@@ -4393,6 +4323,30 @@ function materialize(doc, allowSelectionConflicts = false) {
|
|
|
4393
4323
|
const project = section(doc, "project");
|
|
4394
4324
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4395
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
|
+
}
|
|
4396
4350
|
//#endregion
|
|
4397
4351
|
//#region src/document/document-mutation-guard.ts
|
|
4398
4352
|
/**
|
|
@@ -4728,4 +4682,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4728
4682
|
return new PlainMemoryAdapter(document, options);
|
|
4729
4683
|
}
|
|
4730
4684
|
//#endregion
|
|
4731
|
-
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 };
|