@mengine/medeo-client 2.0.1-alpha.23 → 2.0.1-alpha.25
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-Bx9xf40e.js} +380 -402
- package/dist/{index-mGObzOh4.d.ts → index-BYv9FPSS.d.ts} +14 -37
- package/dist/index.d.ts +79 -51
- package/dist/index.js +412 -479
- 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 lineage, speech inputs, transcription sources, and immutable script revisions. */
|
|
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)) || hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio-script"])),
|
|
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,
|
|
@@ -3011,7 +2970,7 @@ var EntityProjectionGraph = class {
|
|
|
3011
2970
|
timeline;
|
|
3012
2971
|
constructor(rows) {
|
|
3013
2972
|
this.rows = rows;
|
|
3014
|
-
const decoded = decodeEntityRelationRows(rows, {
|
|
2973
|
+
const decoded = decodeEntityRelationRows(rows, {});
|
|
3015
2974
|
this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
|
|
3016
2975
|
const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
|
|
3017
2976
|
if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
|
|
@@ -3025,6 +2984,13 @@ var EntityProjectionGraph = class {
|
|
|
3025
2984
|
adjacent(id, kind) {
|
|
3026
2985
|
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
2986
|
}
|
|
2987
|
+
/** What this entity contains, as opposed to what contains it. */
|
|
2988
|
+
holds(id) {
|
|
2989
|
+
return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint0EntityId === id).map((row) => this.entity(row.endpoint1EntityId));
|
|
2990
|
+
}
|
|
2991
|
+
heldBy(id) {
|
|
2992
|
+
return this.rows.relations.filter((row) => row.relationKind === "contain" && row.endpoint1EntityId === id).map((row) => this.entity(row.endpoint0EntityId));
|
|
2993
|
+
}
|
|
3028
2994
|
one(id, relation, kind) {
|
|
3029
2995
|
const values = this.adjacent(id, relation);
|
|
3030
2996
|
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 +3014,44 @@ function resolveEntityTimelineLayout(rows) {
|
|
|
3048
3014
|
function resolveGraphLayout(graph) {
|
|
3049
3015
|
const facts = /* @__PURE__ */ new Map();
|
|
3050
3016
|
const seenRoles = /* @__PURE__ */ new Set();
|
|
3051
|
-
const tracks = graph.
|
|
3017
|
+
const tracks = graph.holds(graph.timeline.entityId).map((track) => {
|
|
3052
3018
|
const role = track.payload.role;
|
|
3053
3019
|
if (track.entityKind !== "track" || typeof role !== "string" || !Object.hasOwn(roleOrder, role)) fail(`unsupported compatibility Track role on ${track.entityId}`);
|
|
3054
3020
|
if (seenRoles.has(role)) fail(`The compatibility editor supports only one ${role} Track`);
|
|
3055
3021
|
seenRoles.add(role);
|
|
3056
3022
|
const typedRole = role;
|
|
3057
3023
|
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
|
|
3024
|
+
const order = roleOrder[typedRole];
|
|
3025
|
+
const trackFacts = graph.holds(track.entityId).map((clip, clipIndex) => {
|
|
3026
|
+
if (clip.entityKind !== "clip" || graph.heldBy(clip.entityId).length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
|
|
3027
|
+
const marker = graph.one(clip.entityId, "marker", "sequence-marker");
|
|
3028
|
+
const held = graph.holds(clip.entityId);
|
|
3029
|
+
if (held.length !== 1) fail(`Clip ${clip.entityId} must contain exactly one content entity`);
|
|
3030
|
+
const content = held[0];
|
|
3031
|
+
const expected = typedRole === "video_clip" ? ["image", "video"] : typedRole === "speech" ? ["voiceover-recording", "audio"] : typedRole === "caption" ? ["caption"] : ["audio"];
|
|
3032
|
+
if (!expected.includes(content.entityKind)) fail(`${typedRole} Track accepts ${expected.join(" or ")}, got ${content.entityKind}`);
|
|
3033
|
+
const inMs = milliseconds(marker.payload.inMs, "inMs");
|
|
3034
|
+
const outMs = milliseconds(marker.payload.outMs, "outMs");
|
|
3035
|
+
const rate = marker.payload.rate === void 0 ? 1 : positive(marker.payload.rate, "rate");
|
|
3036
|
+
const duration = positive(Math.round((outMs - inMs) / rate), "duration");
|
|
3068
3037
|
if (!Number.isSafeInteger(duration)) fail("Compatibility duration must be whole milliseconds");
|
|
3069
|
-
const fillTimeline = marker.payload.
|
|
3070
|
-
if (marker.payload.
|
|
3038
|
+
const fillTimeline = marker.payload.fillTimeline === true;
|
|
3039
|
+
if (marker.payload.fillTimeline !== void 0 && (!fillTimeline || typedRole !== "bgm")) fail("fillTimeline is only supported for timeline-filling background Audio");
|
|
3071
3040
|
const anchors = graph.rows.relations.filter((row) => row.relationKind === "clip-anchor" && row.endpoint0EntityId === clip.entityId);
|
|
3072
3041
|
if (anchors.length > 1) fail(`Clip ${clip.entityId} has multiple anchors`);
|
|
3073
|
-
const
|
|
3042
|
+
const startMs = marker.payload.startMs === void 0 ? void 0 : milliseconds(marker.payload.startMs, "startMs");
|
|
3074
3043
|
const anchorId = anchors[0]?.endpoint1EntityId;
|
|
3075
3044
|
let mode;
|
|
3076
3045
|
let absoluteStart;
|
|
3077
3046
|
let anchorOffset;
|
|
3078
|
-
let clipOrder;
|
|
3079
3047
|
if (anchorId !== void 0) {
|
|
3080
|
-
if (
|
|
3048
|
+
if (startMs === void 0) fail(`Anchored Marker ${marker.entityId} requires startMs inside its host`);
|
|
3081
3049
|
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
|
-
}
|
|
3050
|
+
anchorOffset = startMs;
|
|
3051
|
+
} else if (startMs !== void 0) {
|
|
3052
|
+
mode = "absolute";
|
|
3053
|
+
absoluteStart = startMs;
|
|
3054
|
+
} else mode = "sequential";
|
|
3096
3055
|
const fact = {
|
|
3097
3056
|
clip,
|
|
3098
3057
|
marker,
|
|
@@ -3101,7 +3060,7 @@ function resolveGraphLayout(graph) {
|
|
|
3101
3060
|
role: typedRole,
|
|
3102
3061
|
duration,
|
|
3103
3062
|
mode,
|
|
3104
|
-
order:
|
|
3063
|
+
order: clipIndex,
|
|
3105
3064
|
absoluteStart,
|
|
3106
3065
|
anchorId,
|
|
3107
3066
|
anchorOffset,
|
|
@@ -3111,8 +3070,7 @@ function resolveGraphLayout(graph) {
|
|
|
3111
3070
|
return fact;
|
|
3112
3071
|
});
|
|
3113
3072
|
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");
|
|
3073
|
+
const sequential = trackFacts.filter((fact) => fact.mode === "sequential");
|
|
3116
3074
|
sequential.forEach((fact, index) => {
|
|
3117
3075
|
fact.predecessor = sequential[index - 1]?.clip.entityId;
|
|
3118
3076
|
});
|
|
@@ -3179,33 +3137,6 @@ function resolveGraphLayout(graph) {
|
|
|
3179
3137
|
}))
|
|
3180
3138
|
};
|
|
3181
3139
|
}
|
|
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
3140
|
//#endregion
|
|
3210
3141
|
//#region src/document/entity-projection.ts
|
|
3211
3142
|
/**
|
|
@@ -3232,7 +3163,7 @@ function projectEntityTimeline(rows) {
|
|
|
3232
3163
|
if (!volume.success) fail("Clip volume must be decibels between -60 and 20");
|
|
3233
3164
|
if (track.role === "video_clip") {
|
|
3234
3165
|
const asset = mediaAssetFacts(graph, content);
|
|
3235
|
-
const source =
|
|
3166
|
+
const source = markerWindow(marker);
|
|
3236
3167
|
const external = externalLocator(asset, "memota");
|
|
3237
3168
|
const speed = videoSpeed(content, marker, placement);
|
|
3238
3169
|
parts[clip.entityId] = { video_clip: {
|
|
@@ -3247,35 +3178,33 @@ function projectEntityTimeline(rows) {
|
|
|
3247
3178
|
} else if (track.role === "speech") {
|
|
3248
3179
|
const asset = mediaAssetFacts(graph, content);
|
|
3249
3180
|
requireUntrimmedAudio(content, marker, placement);
|
|
3181
|
+
const synthesized = content.entityKind === "voiceover-recording";
|
|
3182
|
+
const external = externalLocator(asset, synthesized ? "memota-speech" : "memota");
|
|
3250
3183
|
const timbreEdges = graph.adjacent(content.entityId, "voice-timbre");
|
|
3251
|
-
const
|
|
3252
|
-
const
|
|
3253
|
-
const assembled = isSynthesized ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
|
|
3184
|
+
const assembled = synthesized ? assemblePhoneticScriptContent(rows, synthesizedPhoneticScript(graph, content).entityId) : void 0;
|
|
3185
|
+
const transcript = synthesized ? void 0 : transcribedAudioScript(graph, content);
|
|
3254
3186
|
if (timbreEdges.length > 1) fail("A voiceover resolves at most one Voice identity");
|
|
3255
3187
|
const voice = timbreEdges.length === 0 ? void 0 : graph.one(content.entityId, "voice-timbre", "voice").payload.voice;
|
|
3256
3188
|
if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
|
|
3257
3189
|
const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
|
|
3258
|
-
|
|
3190
|
+
parts[clip.entityId] = { speech: {
|
|
3259
3191
|
id: clip.entityId,
|
|
3260
3192
|
kind: "speech",
|
|
3261
3193
|
media_duration_ms: placement.duration,
|
|
3262
3194
|
audio_storage_key: storageKey(asset),
|
|
3263
3195
|
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")) : ""),
|
|
3196
|
+
audio_script: assembled?.text ?? (transcript === void 0 ? "" : assembleScriptText(transcript)),
|
|
3266
3197
|
volume: volume.data,
|
|
3267
3198
|
voice: object(voice) ? {
|
|
3268
3199
|
id: typeof voice.key === "string" ? voice.key : void 0,
|
|
3269
3200
|
name: typeof voice.name === "string" ? voice.name : void 0
|
|
3270
3201
|
} : void 0,
|
|
3271
3202
|
caption_ids: captionIds
|
|
3272
|
-
};
|
|
3273
|
-
parts[clip.entityId] = { speech };
|
|
3203
|
+
} };
|
|
3274
3204
|
} else if (track.role === "caption") {
|
|
3275
3205
|
const assembled = assembleCaptionContent(rows, content.entityId);
|
|
3276
3206
|
const text = assembled.text;
|
|
3277
3207
|
const cues = captionDisplayCues(assembled, marker, placement.duration);
|
|
3278
|
-
if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
|
|
3279
3208
|
const style = captionStyle(content.payload.style);
|
|
3280
3209
|
const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
|
|
3281
3210
|
const anchoredToSpeech = anchor !== void 0 && trackRoleOf(layout, anchor.trackEntityId) === "speech";
|
|
@@ -3292,9 +3221,9 @@ function projectEntityTimeline(rows) {
|
|
|
3292
3221
|
} else {
|
|
3293
3222
|
const asset = mediaAssetFacts(graph, content);
|
|
3294
3223
|
const external = externalLocator(asset, "memota");
|
|
3295
|
-
if (marker.payload.
|
|
3296
|
-
if (marker.payload.
|
|
3297
|
-
const source =
|
|
3224
|
+
if (marker.payload.fillTimeline !== true) fail("Background Audio requires an explicit fillTimeline");
|
|
3225
|
+
if (marker.payload.rate !== void 0) fail("BGM rate is not supported by this reader");
|
|
3226
|
+
const source = markerWindow(marker);
|
|
3298
3227
|
if (source.start !== 0 || content.payload.durationMs !== source.end) fail("Background Audio compatibility requires the complete source");
|
|
3299
3228
|
parts[clip.entityId] = { bgm: {
|
|
3300
3229
|
id: clip.entityId,
|
|
@@ -3350,10 +3279,7 @@ function storageKey(asset) {
|
|
|
3350
3279
|
function externalLocator(asset, system) {
|
|
3351
3280
|
const { system: found, key } = asset.payload;
|
|
3352
3281
|
if (found !== system || typeof key !== "string" || key.trim() === "") fail("Asset " + asset.entityId + " requires an explicit " + system + " key");
|
|
3353
|
-
return {
|
|
3354
|
-
key,
|
|
3355
|
-
system
|
|
3356
|
-
};
|
|
3282
|
+
return { key };
|
|
3357
3283
|
}
|
|
3358
3284
|
function captionStyle(value) {
|
|
3359
3285
|
if (value === void 0) return void 0;
|
|
@@ -3385,22 +3311,36 @@ function captionStyle(value) {
|
|
|
3385
3311
|
return style;
|
|
3386
3312
|
}
|
|
3387
3313
|
function requireUntrimmedAudio(content, marker, placement) {
|
|
3388
|
-
const source =
|
|
3389
|
-
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.
|
|
3314
|
+
const source = markerWindow(marker);
|
|
3315
|
+
if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.rate !== void 0) fail("Speech compatibility requires the complete, unremapped take");
|
|
3316
|
+
}
|
|
3317
|
+
function synthesizedPhoneticScript(graph, content) {
|
|
3318
|
+
const scripts = graph.rows.relations.filter((row) => row.relationKind === "derived-from" && row.endpoint0EntityId === content.entityId).map((row) => graph.entity(row.endpoint1EntityId)).filter((row) => row.entityKind === "phonetic-script");
|
|
3319
|
+
if (scripts.length !== 1) fail("Synthesized Speech requires exactly one ordered PhoneticScript input");
|
|
3320
|
+
return scripts[0];
|
|
3321
|
+
}
|
|
3322
|
+
function transcribedAudioScript(graph, content) {
|
|
3323
|
+
const scripts = graph.rows.relations.filter((row) => row.relationKind === "derived-from" && row.endpoint1EntityId === content.entityId).map((row) => graph.entity(row.endpoint0EntityId)).filter((row) => row.entityKind === "audio-script");
|
|
3324
|
+
if (scripts.length > 1) fail("Recorded Speech has ambiguous AudioScript transcripts");
|
|
3325
|
+
return scripts[0];
|
|
3326
|
+
}
|
|
3327
|
+
/** The Marker's window, in the {start,end} shape this compatibility reader speaks. */
|
|
3328
|
+
function markerWindow(marker) {
|
|
3329
|
+
return range({
|
|
3330
|
+
start: marker.payload.inMs,
|
|
3331
|
+
end: marker.payload.outMs
|
|
3332
|
+
}, "Sequence Marker window");
|
|
3390
3333
|
}
|
|
3391
3334
|
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");
|
|
3335
|
+
const stated = marker.payload.rate;
|
|
3336
|
+
if (content.entityKind === "image" && stated === void 0) return void 0;
|
|
3337
|
+
const source = markerWindow(marker);
|
|
3338
|
+
if (stated !== void 0) {
|
|
3339
|
+
const rate = positive(stated, "linear speed rate");
|
|
3340
|
+
if (Math.round((source.end - source.start) / rate) !== placement.duration) fail("rate on " + marker.entityId + " must agree with Marker duration");
|
|
3401
3341
|
return {
|
|
3402
3342
|
category: "linear",
|
|
3403
|
-
mode:
|
|
3343
|
+
mode: "constant",
|
|
3404
3344
|
config: { linear: { speed: rate } }
|
|
3405
3345
|
};
|
|
3406
3346
|
}
|
|
@@ -3415,7 +3355,7 @@ function videoSpeed(content, marker, placement) {
|
|
|
3415
3355
|
function captionDisplayCues(assembled, marker, duration) {
|
|
3416
3356
|
const ranges = assembled.caption.payload.segmentRanges;
|
|
3417
3357
|
if (!Array.isArray(ranges)) return void 0;
|
|
3418
|
-
const source =
|
|
3358
|
+
const source = markerWindow(marker);
|
|
3419
3359
|
const scale = duration / (source.end - source.start);
|
|
3420
3360
|
return assembled.segments.flatMap((segment) => {
|
|
3421
3361
|
const timing = ranges.find((entry) => object(entry) && entry.segmentId === segment.segmentId);
|
|
@@ -3439,23 +3379,11 @@ function captionDisplayCues(assembled, marker, duration) {
|
|
|
3439
3379
|
}
|
|
3440
3380
|
//#endregion
|
|
3441
3381
|
//#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
|
-
]);
|
|
3382
|
+
/** Merge behaviour is declared with the field shapes in @mengine/medeo-dsl. */
|
|
3383
|
+
const merge = (key) => FIELD_MERGE[key];
|
|
3384
|
+
const atomicFields = { has: (key) => merge(key) === "atomic" };
|
|
3385
|
+
const textFields = { has: (key) => merge(key) === "text" };
|
|
3386
|
+
const orderedFields = { has: (key) => merge(key) === "list" };
|
|
3459
3387
|
const unsafeKeys = new Set([
|
|
3460
3388
|
"__proto__",
|
|
3461
3389
|
"prototype",
|
|
@@ -3743,27 +3671,24 @@ function phoneticScriptAssetOf(rows, entityId) {
|
|
|
3743
3671
|
}
|
|
3744
3672
|
//#endregion
|
|
3745
3673
|
//#region src/entity-crdt/ordering.ts
|
|
3746
|
-
/**
|
|
3674
|
+
/**
|
|
3675
|
+
* Domain order is scoped by membership: the members of one container, in the
|
|
3676
|
+
* order their `contain` Relations sit in the rows.
|
|
3677
|
+
*
|
|
3678
|
+
* Nothing carries a copy of its own position, so this reads the order rather
|
|
3679
|
+
* than deriving it — the rows are already in it.
|
|
3680
|
+
*/
|
|
3747
3681
|
function entityOrderGroups(rows) {
|
|
3748
3682
|
const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
|
|
3749
3683
|
const groups = /* @__PURE__ */ new Map();
|
|
3750
3684
|
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);
|
|
3685
|
+
if (!isSequencedRelationKind(relation.relationKind)) continue;
|
|
3686
|
+
const childKind = entities.get(String(relation.endpoint1EntityId))?.entityKind;
|
|
3687
|
+
if (childKind !== "track" && childKind !== "clip") continue;
|
|
3688
|
+
const parent = String(relation.endpoint0EntityId);
|
|
3689
|
+
groups.set(parent, [...groups.get(parent) ?? [], String(relation.endpoint1EntityId)]);
|
|
3765
3690
|
}
|
|
3766
|
-
return
|
|
3691
|
+
return groups;
|
|
3767
3692
|
}
|
|
3768
3693
|
function ordering(doc) {
|
|
3769
3694
|
const map = doc.getMap("medeo").get("ordering");
|
|
@@ -3845,12 +3770,7 @@ function content(map) {
|
|
|
3845
3770
|
return requireMap(map, "content").toJSON();
|
|
3846
3771
|
}
|
|
3847
3772
|
function entityContent(doc, key) {
|
|
3848
|
-
|
|
3849
|
-
const order = entityOrdinal(doc, key);
|
|
3850
|
-
return order === void 0 ? payload : {
|
|
3851
|
-
...payload,
|
|
3852
|
-
order
|
|
3853
|
-
};
|
|
3773
|
+
return content(workspace(doc, key));
|
|
3854
3774
|
}
|
|
3855
3775
|
function bases(map) {
|
|
3856
3776
|
const links = requireMap(map, "bases");
|
|
@@ -3921,17 +3841,20 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3921
3841
|
trace: requireMap(map, "trace").toJSON()
|
|
3922
3842
|
};
|
|
3923
3843
|
});
|
|
3844
|
+
sortSequencedRelations(doc, relations);
|
|
3924
3845
|
const project = {};
|
|
3925
3846
|
for (const [key, value] of Object.entries(section(doc, "attachments").toJSON())) {
|
|
3926
3847
|
if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
|
|
3927
3848
|
project[key] = currentId(doc, value, ids);
|
|
3928
3849
|
}
|
|
3850
|
+
const phoneticScriptSelections = readPhoneticScriptSelections(doc, ids);
|
|
3929
3851
|
const state = {
|
|
3930
3852
|
rows: {
|
|
3931
3853
|
entities,
|
|
3932
3854
|
relations
|
|
3933
3855
|
},
|
|
3934
|
-
project
|
|
3856
|
+
project,
|
|
3857
|
+
...phoneticScriptSelections === void 0 ? {} : { phoneticScriptSelections }
|
|
3935
3858
|
};
|
|
3936
3859
|
if (!allowSelectionConflicts) for (const [key, id] of ids) {
|
|
3937
3860
|
const fingerprint = selectionFingerprint(state.rows, id);
|
|
@@ -3939,6 +3862,23 @@ function readEntityDocument(doc, allowSelectionConflicts = false) {
|
|
|
3939
3862
|
}
|
|
3940
3863
|
return state;
|
|
3941
3864
|
}
|
|
3865
|
+
function readPhoneticScriptSelections(doc, ids) {
|
|
3866
|
+
const native = root(doc).get("phoneticScriptSelections");
|
|
3867
|
+
if (native === void 0) return void 0;
|
|
3868
|
+
if (!(native instanceof LoroMap)) throw new Error("Invalid phonetic-script selector");
|
|
3869
|
+
const selections = {};
|
|
3870
|
+
for (const clipRoot of native.keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
3871
|
+
const phoneticScriptRoot = native.get(clipRoot);
|
|
3872
|
+
if (typeof phoneticScriptRoot !== "string") throw new Error("Invalid phonetic-script selection");
|
|
3873
|
+
const clipId = ids.get(clipRoot);
|
|
3874
|
+
const phoneticScriptId = ids.get(phoneticScriptRoot);
|
|
3875
|
+
if (clipId === void 0 || phoneticScriptId === void 0) continue;
|
|
3876
|
+
if (workspace(doc, clipRoot).get("kind") !== "clip") throw new Error("Phonetic-script selection target must be a Clip");
|
|
3877
|
+
if (workspace(doc, phoneticScriptRoot).get("kind") !== "phonetic-script") throw new Error("Phonetic-script selection value must be a PhoneticScript");
|
|
3878
|
+
selections[clipRoot] = phoneticScriptId;
|
|
3879
|
+
}
|
|
3880
|
+
return Object.keys(selections).length === 0 ? void 0 : selections;
|
|
3881
|
+
}
|
|
3942
3882
|
function relationRecords(doc) {
|
|
3943
3883
|
const records = [];
|
|
3944
3884
|
for (const rootId of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
@@ -3946,13 +3886,12 @@ function relationRecords(doc) {
|
|
|
3946
3886
|
if (!(map instanceof LoroMap)) throw new Error(`Invalid relation record ${rootId}`);
|
|
3947
3887
|
const value = map.get("identity");
|
|
3948
3888
|
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
3889
|
records.push({
|
|
3954
3890
|
rootId,
|
|
3955
|
-
identity
|
|
3891
|
+
identity: {
|
|
3892
|
+
kind: value.kind,
|
|
3893
|
+
endpoints: [value.endpoints[0], value.endpoints[1]]
|
|
3894
|
+
},
|
|
3956
3895
|
map,
|
|
3957
3896
|
deleted: map.get("deleted") === true
|
|
3958
3897
|
});
|
|
@@ -3960,9 +3899,15 @@ function relationRecords(doc) {
|
|
|
3960
3899
|
return records;
|
|
3961
3900
|
}
|
|
3962
3901
|
function activeRelationRecords(doc, ids) {
|
|
3963
|
-
|
|
3902
|
+
const selector = root(doc).get("clipContentSelections");
|
|
3903
|
+
if (selector !== void 0 && !(selector instanceof LoroMap)) throw new Error("Invalid clip-content selector");
|
|
3904
|
+
return relationRecords(doc).filter(({ rootId, identity, deleted }) => {
|
|
3964
3905
|
if (deleted) return false;
|
|
3965
|
-
|
|
3906
|
+
if (!identity.endpoints.every((id) => ids.has(family(doc, id)))) return false;
|
|
3907
|
+
if (identity.kind !== "contain" || selector === void 0 || workspace(doc, family(doc, identity.endpoints[0])).get("kind") !== "clip") return true;
|
|
3908
|
+
const selected = selector.get(family(doc, identity.endpoints[0]));
|
|
3909
|
+
if (selected !== void 0 && typeof selected !== "string") throw new Error("Invalid clip-content selection");
|
|
3910
|
+
return selected === void 0 || selected === rootId;
|
|
3966
3911
|
});
|
|
3967
3912
|
}
|
|
3968
3913
|
/** Native editing containers survive domain version changes and merge independently. */
|
|
@@ -3996,7 +3941,9 @@ var LoroEntityDocument = class LoroEntityDocument {
|
|
|
3996
3941
|
"relations",
|
|
3997
3942
|
"project",
|
|
3998
3943
|
"attachments",
|
|
3999
|
-
"ordering"
|
|
3944
|
+
"ordering",
|
|
3945
|
+
"clipContentSelections",
|
|
3946
|
+
"phoneticScriptSelections"
|
|
4000
3947
|
]) root(doc).ensureMergeableMap(name);
|
|
4001
3948
|
for (const row of rows.entities) apply(doc, {
|
|
4002
3949
|
type: "create",
|
|
@@ -4195,39 +4142,11 @@ var LoroEntityDraft = class {
|
|
|
4195
4142
|
});
|
|
4196
4143
|
}
|
|
4197
4144
|
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
4145
|
this.record({
|
|
4201
4146
|
type: "relation",
|
|
4202
4147
|
row: clone(row)
|
|
4203
4148
|
});
|
|
4204
4149
|
}
|
|
4205
|
-
linkGenerated(input) {
|
|
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
4150
|
patchRelationTrace(relationId, trace) {
|
|
4232
4151
|
const relation = this.snapshot().rows.relations.find((row) => row.relationId === relationId);
|
|
4233
4152
|
if (relation == null) throw new Error(`Unknown relation ${relationId}`);
|
|
@@ -4245,6 +4164,35 @@ var LoroEntityDraft = class {
|
|
|
4245
4164
|
root: this.relationRoot(relationId)
|
|
4246
4165
|
});
|
|
4247
4166
|
}
|
|
4167
|
+
/** Select the one content membership projected for this stable Clip identity. */
|
|
4168
|
+
selectClipContent(relationId) {
|
|
4169
|
+
const relationRoot = this.relationRoot(relationId);
|
|
4170
|
+
const map = section(this.doc, "relations").get(relationRoot);
|
|
4171
|
+
if (!(map instanceof LoroMap)) throw new Error(`Unknown relation ${relationId}`);
|
|
4172
|
+
const identity = map.get("identity");
|
|
4173
|
+
if (!isJsonObject(identity) || identity.kind !== "contain" || !Array.isArray(identity.endpoints) || typeof identity.endpoints[0] !== "string") throw new Error("Clip-content selection requires a contain relation");
|
|
4174
|
+
const clipRoot = family(this.doc, identity.endpoints[0]);
|
|
4175
|
+
if (workspace(this.doc, clipRoot).get("kind") !== "clip") throw new Error("Clip-content selection requires a Clip container");
|
|
4176
|
+
this.record({
|
|
4177
|
+
type: "select-clip-content",
|
|
4178
|
+
clipRoot,
|
|
4179
|
+
relationRoot
|
|
4180
|
+
});
|
|
4181
|
+
}
|
|
4182
|
+
/** Select the pending immutable pronunciation variant for one stable Speech Clip. */
|
|
4183
|
+
selectPhoneticScript(clipEntityId, phoneticScriptEntityId) {
|
|
4184
|
+
const clipRoot = family(this.doc, clipEntityId);
|
|
4185
|
+
const phoneticScriptRoot = family(this.doc, phoneticScriptEntityId);
|
|
4186
|
+
const clip = workspace(this.doc, clipRoot);
|
|
4187
|
+
const phoneticScript = workspace(this.doc, phoneticScriptRoot);
|
|
4188
|
+
if (clip.get("deleted") === true || clip.get("kind") !== "clip") throw new Error("Phonetic-script selection target must be an active Clip");
|
|
4189
|
+
if (phoneticScript.get("deleted") === true || phoneticScript.get("kind") !== "phonetic-script") throw new Error("Phonetic-script selection value must be an active PhoneticScript");
|
|
4190
|
+
this.record({
|
|
4191
|
+
type: "select-phonetic-script",
|
|
4192
|
+
clipRoot,
|
|
4193
|
+
phoneticScriptRoot
|
|
4194
|
+
});
|
|
4195
|
+
}
|
|
4248
4196
|
relationRoot(relationId) {
|
|
4249
4197
|
const alias = this.relationAliases.get(relationId);
|
|
4250
4198
|
if (alias !== void 0) return alias;
|
|
@@ -4452,6 +4400,12 @@ function apply(doc, command) {
|
|
|
4452
4400
|
map.set("deleted", true);
|
|
4453
4401
|
break;
|
|
4454
4402
|
}
|
|
4403
|
+
case "select-clip-content":
|
|
4404
|
+
root(doc).ensureMergeableMap("clipContentSelections").set(command.clipRoot, command.relationRoot);
|
|
4405
|
+
break;
|
|
4406
|
+
case "select-phonetic-script":
|
|
4407
|
+
root(doc).ensureMergeableMap("phoneticScriptSelections").set(command.clipRoot, command.phoneticScriptRoot);
|
|
4408
|
+
break;
|
|
4455
4409
|
case "relation-fields": {
|
|
4456
4410
|
const map = section(doc, "relations").get(command.root);
|
|
4457
4411
|
if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
|
|
@@ -4514,6 +4468,30 @@ function materialize(doc, allowSelectionConflicts = false) {
|
|
|
4514
4468
|
const project = section(doc, "project");
|
|
4515
4469
|
for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
|
|
4516
4470
|
}
|
|
4471
|
+
/**
|
|
4472
|
+
* Put each container's sequenced Relations in the order its native list states.
|
|
4473
|
+
*
|
|
4474
|
+
* Per container, never across: a Timeline's Tracks and a Track's Clips are two
|
|
4475
|
+
* separate orders, and sorting them together would interleave them.
|
|
4476
|
+
*/
|
|
4477
|
+
function sortSequencedRelations(doc, relations) {
|
|
4478
|
+
const containers = /* @__PURE__ */ new Map();
|
|
4479
|
+
for (const [slot, relation] of relations.entries()) {
|
|
4480
|
+
if (!isSequencedRelationKind(relation.relationKind)) continue;
|
|
4481
|
+
const container = String(relation.endpoint0EntityId);
|
|
4482
|
+
const ordinal = entityOrdinal(doc, family(doc, String(relation.endpoint1EntityId))) ?? slot;
|
|
4483
|
+
containers.set(container, [...containers.get(container) ?? [], {
|
|
4484
|
+
slot,
|
|
4485
|
+
relation,
|
|
4486
|
+
ordinal
|
|
4487
|
+
}]);
|
|
4488
|
+
}
|
|
4489
|
+
for (const members of containers.values()) {
|
|
4490
|
+
const slots = members.map((member) => member.slot);
|
|
4491
|
+
const ordered = [...members].sort((left, right) => left.ordinal - right.ordinal);
|
|
4492
|
+
slots.forEach((slot, at) => relations[slot] = ordered[at].relation);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4517
4495
|
//#endregion
|
|
4518
4496
|
//#region src/document/document-mutation-guard.ts
|
|
4519
4497
|
/**
|
|
@@ -4849,4 +4827,4 @@ function createPlainMemoryAdapter(document, options) {
|
|
|
4849
4827
|
return new PlainMemoryAdapter(document, options);
|
|
4850
4828
|
}
|
|
4851
4829
|
//#endregion
|
|
4852
|
-
export {
|
|
4830
|
+
export { fromVideoDocument as $, resolveEntityTimelineLayout as A, base64ToBytes as At, assembleScriptText as B, canonicalJson as C, safeDurationMs as Ct, resolveFieldPath as D, partUnionToDraft as Dt, assertFieldChanges as E, speedOf as Et, relationsTo as F, isJsonObject as G, selectAudioScriptSegment as H, ScriptCompositionError as I, hasSequence as J, createEntityId as K, assembleCaptionContent as L, validateEntityRelationTransition as M, decodeEntityRelationRows as N, projectEntityTimeline as O, recordEntries as Ot, relationsFrom as P, derivePositionFromAbs as Q, assembleEntityContent as R, scriptSourceAssetOf as S, partDurationMs as St, applyFieldChanges as T, effectiveVideoClipDurationMs as Tt, updateEntityFields as U, findComposedAudioScript as V, variantBaseEntityIds as W, buildInitialVideoDocument as X, isMediaAssetVariantKind as Y, buildSpeechHostMap as Z, audioScriptAssetHash as _, resolveAllSpeechOverlaps as _t, createMirrorVideoDocument as a, videoDocumentSchema as at, phoneticScriptAssetHash as b, TIMELINE_SKELETON_DURATION_MS as bt, DocumentMutationGuard as c, findLaneTrack as ct, LoroEntityDraft as d, solveVideoDocument as dt, toVideoDocument as et, readEntityDocument as f, cascadeAfterVideoClipChanges as ft, audioScriptAssetFields as g, recalculateTimelineDuration as gt, audioScriptAssetContent as h, reassignSpeechesToVideoClipsByTime as ht, MirrorVideoDocumentAdapter as i, partUnionSchema as it, EntityTimelineProjectionError as j, bytesToBase64 as jt, EntityProjectionGraph as k, videoDocumentMirrorSchema as kt, LORO_ENTITY_SCHEMA as l, hasResolvedPlacement as lt, entityOrderGroups as m, fillMainTrackTimeGaps as mt, createPlainMemoryAdapter as n, assertValidVideoDocument as nt, createMirrorVideoDocumentAdapter as o, LANE_KINDS_IN_STACK_ORDER as ot, readEntityRelationRecords as p, arrangeMainTrackSeamlessly as pt, createRelationId as q, generatePartId as r, validateVideoDocument as rt, readVideoDocumentFromDraft as s, ensureLaneTrack as st, PlainMemoryAdapter as t, VideoDocumentValidationError as tt, LoroEntityDocument as u, laneTrackId as ut, audioScriptAssetOf as v, resolveSpeechOverlapByShiftingVideos as vt, equalJson as w, DEFAULT_UNIT_TIME_MS as wt, phoneticScriptAssetOf as x, isEmptyVideoClip as xt, phoneticScriptAssetContent as y, syncAggregatedClipsTimePosition as yt, assemblePhoneticScriptContent as z };
|