@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.
@@ -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 { origin_asset_system: _originAssetSystem, ...readSpeech } = part.speech;
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, options) {
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
- source: (left, right) => options.compareMarkerPoints(marker, "source", left, right),
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 relations = [...index.relationsOf(marker)];
1853
- const clipEdges = ofKind(relations, "clip-marker");
1854
- const axVideoEdges = ofKind(relations, "axvideo-marker");
1855
- const contentEdges = ofKind(relations, "marker-content");
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 + axVideoEdges.length + contentEdges.length + timelineEdges.length > 0) issues.push({
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, AXVideo, content, or Timeline relations"
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 + axVideoEdges.length !== 1) issues.push({
1890
+ if (clipEdges.length !== 1) issues.push({
1891
1891
  code: "marker_container_xor",
1892
1892
  entityId: marker.entityId,
1893
- message: "Sequence Marker must have exactly one Clip XOR AXVideo container relation"
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 = ofKind([...index.relationsOf(clip)], "clip-marker");
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
- const marker = markerEdges[0]?.other(clip)?.deref();
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 and one content Relation"
1907
+ message: "Clip must resolve one live Sequence Marker"
1919
1908
  }];
1920
- const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
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 Marker must resolve exactly one content Relation"
1913
+ message: "Clip must contain exactly one content entity"
1925
1914
  }];
1926
- const content = contentEdges[0]?.other(marker)?.deref()?.current();
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 Marker content Relation must resolve one live entity"
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 Marker resolves to non-Sequence entity "${content.entityId}"`
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 = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
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 hasOrder = clip.current().order !== void 0;
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
- const hasAnchor = anchors.length === 1;
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 placement must be exactly one of Clip.order, Marker.targetRange, or clip-anchor with Marker.anchorOffset"
1968
+ message: "A Clip following a host states startMs, the offset inside that host"
1992
1969
  });
1993
- if (markerValue.durationPolicy === "timeline") {
1994
- const content = ofKind([...index.relationsOf(marker)], "marker-content")[0]?.other(marker)?.deref()?.current();
1995
- const track = ofKind([...index.relationsOf(clip)], "track-clip")[0]?.other(clip)?.deref()?.current();
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" || !hasOrder && !hasTarget) issues.push({
1974
+ if (content?.entityKind !== "audio" || trackRole !== "bgm") issues.push({
1998
1975
  code: "timeline_duration_policy_invalid",
1999
1976
  entityId: marker.entityId,
2000
- message: "Marker durationPolicy \"timeline\" is only valid for ordered or absolutely placed Audio Clips on the bgm Track"
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, compare) {
2033
- const current = marker.current();
2034
- const issues = [];
2035
- if (!(compare.source(current.sourceRange.start, current.sourceRange.end) < 0)) issues.push({
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 sourceRange must be a non-empty half-open interval"
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, compare) {
2048
- const contentEdges = ofKind([...index.relationsOf(marker)], "marker-content");
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(marker)?.deref()?.current();
2023
+ const content = contentEdges[0]?.other(clip)?.deref()?.current();
2051
2024
  if (content == null || !hasSequence(content)) return [];
2052
- const sourceRange = marker.current().sourceRange;
2053
- const startVsOrigin = compare(sourceRange.start, 0);
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: `Sequence Marker sourceRange must stay within content "${content.entityId}" duration`
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
- "sourceRange",
2107
- "targetRange",
2108
- "duration",
2077
+ "inMs",
2078
+ "outMs",
2109
2079
  "startMs",
2110
- "endMs"
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
- for (const retired of [
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
- validateRange(value.sourceRange, "sourceRange", true, problems);
2188
- if (Object.hasOwn(value, "targetRange") && value.targetRange !== void 0) validateRange(value.targetRange, "targetRange", true, problems);
2189
- const duration = value.duration;
2190
- if (!isRecord(duration)) problems.push("duration must be an object");
2191
- else if (duration.mode === "fixed") {
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
- "sourceRange",
2326
- "targetRange",
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" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
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
- const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2460
- const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
2461
- const clipMarkerRelationSpec = emptySpec("clip-marker", "clip", "sequence-marker");
2462
- const axVideoMarkerRelationSpec = emptySpec("axvideo-marker", "axvideo", "sequence-marker");
2463
- const markerTimelineRelationSpec = emptySpec("marker-timeline", "sequence-marker", "timeline");
2464
- const markerContentRelationSpec = Object.freeze({
2465
- kind: "marker-content",
2466
- validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "sequence-marker" && hasSequence(endpoints[1].current()),
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
- validateEndpoints: (endpoints) => declaresKind(endpoints[0].current().entityKind, "MediaFile") && endpoints[1].current().entityKind === "asset",
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
- validateEndpoints: (endpoints) => declaresKind(endpoints[0].current().entityKind, "MediaFile") && endpoints[1].current().entityKind === "asset",
2493
+ ordered: true,
2494
+ sequenced: false,
2495
+ validateEndpoints: (endpoints) => hasExactlyOneAsset(endpoints),
2501
2496
  validateMetadata: isAssetBinding
2502
2497
  });
2503
- /** `generated(output, input)` records media generation or an independent AudioScript revision. */
2504
- const generatedRelationSpec = Object.freeze({
2505
- kind: "generated",
2506
- validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())) || endpoints.every((endpoint) => endpoint.current().entityKind === "audio-script"),
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
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["voiceover"])),
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
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voiceover"]), new Set(["voice"])),
2541
- validateMetadata: isEmptyMetadata
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
- timelineTrackRelationSpec,
2567
- trackClipRelationSpec,
2568
- clipMarkerRelationSpec,
2569
- markerContentRelationSpec,
2570
- axVideoMarkerRelationSpec,
2571
- markerTimelineRelationSpec,
2552
+ containRelationSpec,
2553
+ markerRelationSpec,
2572
2554
  fromAssetRelationSpec,
2573
2555
  toAssetRelationSpec,
2574
- generatedRelationSpec,
2556
+ derivedFromRelationSpec,
2575
2557
  captionAlignmentRelationSpec,
2576
2558
  clipAnchorRelationSpec,
2577
- phoneticScriptRenderRelationSpec,
2578
- voiceTimbreRelationSpec,
2579
- audioScriptSourceRelationSpec,
2580
- audioScriptMarkerRelationSpec
2559
+ voiceTimbreRelationSpec
2581
2560
  ]);
2582
- function emptySpec(kind, a, b) {
2583
- return metadataSpec(kind, a, b, isEmptyMetadata);
2584
- }
2585
- function metadataSpec(kind, a, b, validateMetadata) {
2586
- return Object.freeze({
2587
- kind,
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
- const first = endpoints[0].current().entityKind;
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
- function isGeneratedMedia(entity) {
2598
- return entity.entityKind === "video" || entity.entityKind === "image" || entity.entityKind === "audio" || entity.entityKind === "voiceover";
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 === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
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 `generated(output, input)` without exposing positional arguments. */
2668
- linkGenerated(input) {
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: generatedRelationSpec,
2672
- endpoints: [input.output, input.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, options)) issues.push(`${issue.code}: ${issue.message}`);
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, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
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.adjacent(graph.timeline.entityId, "timeline-track").map((track) => {
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 = track.payload.order === void 0 ? roleOrder[typedRole] : finite(track.payload.order, "Track order");
3059
- const trackFacts = graph.adjacent(track.entityId, "track-clip").map((clip) => {
3060
- if (clip.entityKind !== "clip" || graph.adjacent(clip.entityId, "track-clip").length !== 1) fail(`Clip ${clip.entityId} must belong to exactly one Track`);
3061
- const marker = graph.one(clip.entityId, "clip-marker", "sequence-marker");
3062
- const content = graph.one(marker.entityId, "marker-content");
3063
- assertTrackContent(graph, typedRole, content);
3064
- const source = range(marker.payload.sourceRange, "sourceRange");
3065
- const durationFact = marker.payload.duration;
3066
- if (!object(durationFact)) fail(`Marker ${marker.entityId} requires duration`);
3067
- const duration = positive(durationFact.mode === "from-source" ? source.end - source.start : durationFact.value, "duration");
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.durationPolicy === "timeline";
3070
- if (marker.payload.durationPolicy !== void 0 && (!fillTimeline || typedRole !== "bgm")) fail("durationPolicy is only supported for timeline-filling background Audio");
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 target = marker.payload.targetRange === void 0 ? void 0 : range(marker.payload.targetRange, "targetRange");
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 (target !== void 0) fail(`Anchored Marker ${marker.entityId} cannot also have targetRange`);
3048
+ if (startMs === void 0) fail(`Anchored Marker ${marker.entityId} requires startMs inside its host`);
3081
3049
  mode = "anchored";
3082
- anchorOffset = milliseconds(marker.payload.anchorOffset, "anchorOffset");
3083
- } else {
3084
- if (marker.payload.anchorOffset !== void 0) fail(`Marker ${marker.entityId} has anchorOffset without a clip-anchor Relation`);
3085
- if (target !== void 0) {
3086
- mode = "absolute";
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: clipOrder,
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").sort((left, right) => left.order - right.order);
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 = range(marker.payload.sourceRange, "sourceRange");
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 isSynthesized = content.entityKind === "voiceover";
3252
- const external = externalLocator(asset, isSynthesized ? "memota-speech" : "memota");
3253
- const assembled = isSynthesized ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
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
- const speech = {
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
- origin_asset_system: external.system,
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.durationPolicy !== "timeline") fail("Background Audio requires an explicit timeline-fill durationPolicy");
3296
- if (marker.payload.timeRemapping !== void 0) fail("BGM timeRemapping is not supported by this reader");
3297
- const source = range(marker.payload.sourceRange, "sourceRange");
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 = range(marker.payload.sourceRange, "sourceRange");
3389
- if (source.start !== 0 || content.payload.durationMs !== source.end || placement.duration !== source.end || marker.payload.timeRemapping !== void 0) fail("Voiceover compatibility requires the complete, unremapped take");
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 remapping = marker.payload.timeRemapping;
3393
- if (content.entityKind === "image" && remapping === void 0) return void 0;
3394
- const source = range(marker.payload.sourceRange, "sourceRange");
3395
- if (remapping !== void 0) {
3396
- if (!object(remapping) || remapping.kind !== "linear") fail("unsupported timeRemapping on " + marker.entityId);
3397
- if (Object.keys(remapping).some((key) => key !== "kind" && key !== "rate" && key !== "mode")) fail("Unsupported linear timeRemapping fields");
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: typeof remapping.mode === "string" ? remapping.mode : "constant",
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 = range(marker.payload.sourceRange, "Caption sourceRange");
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
- const atomicFields = new Set([
3443
- "sequence",
3444
- "sourceRange",
3445
- "targetRange",
3446
- "duration",
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
- /** Domain order is scoped by membership; relation storage has no global order. */
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
- const childKind = relation.relationKind === "timeline-track" ? "track" : relation.relationKind === "track-clip" ? "clip" : null;
3752
- if (!childKind) continue;
3753
- const endpoints = [relation.endpoint0EntityId, relation.endpoint1EntityId];
3754
- const childId = endpoints.find((id) => entities.get(id)?.entityKind === childKind);
3755
- if (!childId) continue;
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 new Map([...groups].map(([parent, values]) => [parent, values.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((item) => item.id)]));
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
- const payload = content(workspace(doc, key));
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
- return relationRecords(doc).filter(({ identity, deleted }) => {
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
- return identity.endpoints.every((id) => ids.has(family(doc, id)));
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 { VideoDocumentValidationError as $, resolveEntityTimelineLayout as A, selectAudioScriptSegment as B, canonicalJson as C, effectiveVideoClipDurationMs as Ct, resolveFieldPath as D, videoDocumentMirrorSchema as Dt, assertFieldChanges as E, recordEntries as Et, assembleCaptionContent as F, createRelationId as G, variantBaseEntityIds as H, assembleEntityContent as I, buildInitialVideoDocument as J, hasSequence as K, assemblePhoneticScriptContent as L, validateEntityRelationTransition as M, decodeEntityRelationRows as N, projectEntityTimeline as O, base64ToBytes as Ot, ScriptCompositionError as P, toVideoDocument as Q, assembleScriptText as R, scriptSourceAssetOf as S, DEFAULT_UNIT_TIME_MS as St, applyFieldChanges as T, partUnionToDraft as Tt, isJsonObject as U, updateEntityFields as V, createEntityId as W, derivePositionFromAbs as X, buildSpeechHostMap as Y, fromVideoDocument as Z, audioScriptAssetHash as _, syncAggregatedClipsTimePosition as _t, createMirrorVideoDocument as a, ensureLaneTrack as at, phoneticScriptAssetHash as b, partDurationMs as bt, DocumentMutationGuard as c, laneTrackId as ct, LoroEntityDraft as d, arrangeMainTrackSeamlessly as dt, assertValidVideoDocument as et, readEntityDocument as f, fillMainTrackTimeGaps as ft, audioScriptAssetFields as g, resolveSpeechOverlapByShiftingVideos as gt, audioScriptAssetContent as h, resolveAllSpeechOverlaps as ht, MirrorVideoDocumentAdapter as i, LANE_KINDS_IN_STACK_ORDER as it, EntityTimelineProjectionError as j, EntityProjectionGraph as k, bytesToBase64 as kt, LORO_ENTITY_SCHEMA as l, solveVideoDocument as lt, entityOrderGroups as m, recalculateTimelineDuration as mt, createPlainMemoryAdapter as n, partUnionSchema as nt, createMirrorVideoDocumentAdapter as o, findLaneTrack as ot, readEntityRelationRecords as p, reassignSpeechesToVideoClipsByTime as pt, isMediaAssetVariantKind as q, generatePartId as r, videoDocumentSchema as rt, readVideoDocumentFromDraft as s, hasResolvedPlacement as st, PlainMemoryAdapter as t, validateVideoDocument as tt, LoroEntityDocument as u, cascadeAfterVideoClipChanges as ut, audioScriptAssetOf as v, TIMELINE_SKELETON_DURATION_MS as vt, equalJson as w, speedOf as wt, phoneticScriptAssetOf as x, safeDurationMs as xt, phoneticScriptAssetContent as y, isEmptyVideoClip as yt, findComposedAudioScript as z };
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 };