@mengine/medeo-client 1.3.1-alpha.0 → 1.3.1-alpha.2

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.
@@ -1536,11 +1536,255 @@ function createId(value, label) {
1536
1536
  return value;
1537
1537
  }
1538
1538
  //#endregion
1539
+ //#region ../medeo-dsl/src/json-values.ts
1540
+ const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
1541
+ function isJsonObject(value) {
1542
+ return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
1543
+ }
1544
+ function isJsonValue(value, ancestors) {
1545
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1546
+ if (typeof value === "number") return Number.isFinite(value);
1547
+ if (typeof value !== "object") return false;
1548
+ if (!Array.isArray(value) && !isPlainObject(value)) return false;
1549
+ if (ancestors.has(value)) return false;
1550
+ ancestors.add(value);
1551
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
1552
+ ancestors.delete(value);
1553
+ return valid;
1554
+ }
1555
+ /** Recognize an ordinary object from any VM realm without admitting class instances. */
1556
+ function isPlainObject(value) {
1557
+ try {
1558
+ const prototype = Object.getPrototypeOf(value);
1559
+ if (prototype === null) return true;
1560
+ if (Object.getPrototypeOf(prototype) !== null) return false;
1561
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1562
+ return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
1563
+ } catch {
1564
+ return false;
1565
+ }
1566
+ }
1567
+ //#endregion
1568
+ //#region ../medeo-dsl/src/composition.ts
1569
+ /** Assembly fails explicitly when declared bases cannot provide valid, unambiguous content. */
1570
+ var ScriptCompositionError = class extends Error {
1571
+ code;
1572
+ entityId;
1573
+ constructor(code, entityId, message) {
1574
+ super(message);
1575
+ this.code = code;
1576
+ this.entityId = entityId;
1577
+ this.name = "ScriptCompositionError";
1578
+ }
1579
+ };
1580
+ /** Direct base IDs are structural fields, never ordinary Relation endpoints. */
1581
+ function variantBaseEntityIds(entity) {
1582
+ if (!Object.hasOwn(entity.payload, "baseEntityIds")) return [];
1583
+ const ids = entity.payload.baseEntityIds;
1584
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((id) => typeof id === "string" && id.trim() === id && id.length > 0) || new Set(ids).size !== ids.length) throw new ScriptCompositionError("invalid_bases", entity.entityId, `Entity "${entity.entityId}" requires non-empty, unique baseEntityIds`);
1585
+ return ids.map(createEntityId);
1586
+ }
1587
+ /** Validate all base providers before applying explicit own fields; ordering cannot resolve ambiguity. */
1588
+ function assembleEntityContent(rows, entityId) {
1589
+ const byId = /* @__PURE__ */ new Map();
1590
+ for (const row of rows.entities) {
1591
+ if (byId.has(row.entityId)) throw new ScriptCompositionError("composition_ambiguous", row.entityId, `duplicate entity id "${row.entityId}"`);
1592
+ byId.set(row.entityId, row);
1593
+ }
1594
+ const active = /* @__PURE__ */ new Set();
1595
+ const cache = /* @__PURE__ */ new Map();
1596
+ const visit = (id) => {
1597
+ const cached = cache.get(id);
1598
+ if (cached !== void 0) return cached;
1599
+ if (active.has(id)) throw new ScriptCompositionError("composition_cycle", id, `Cyclic baseEntityIds at "${id}"`);
1600
+ const row = byId.get(id);
1601
+ if (row === void 0) throw new ScriptCompositionError("composition_dangling", id, `Missing base entity "${id}" in this document`);
1602
+ if (!isJsonObject(row.payload) || Object.hasOwn(row.payload, "entityId") || Object.hasOwn(row.payload, "entityKind")) throw new ScriptCompositionError("invalid_bases", id, `Entity "${id}" requires JSON own fields without reserved identity fields`);
1603
+ active.add(id);
1604
+ const inherited = {};
1605
+ const providers = /* @__PURE__ */ new Map();
1606
+ for (const baseId of variantBaseEntityIds(row)) {
1607
+ const base = visit(baseId);
1608
+ for (const [field, value] of Object.entries(base.payload)) {
1609
+ if (field === "baseEntityIds") continue;
1610
+ const previous = providers.get(field);
1611
+ if (previous !== void 0) throw new ScriptCompositionError("field_conflict", id, `Entity "${id}" field "${field}" conflicts between bases "${previous}" and "${baseId}"; own fields cannot resolve base ambiguity`);
1612
+ providers.set(field, baseId);
1613
+ Object.defineProperty(inherited, field, {
1614
+ value,
1615
+ enumerable: true,
1616
+ configurable: true,
1617
+ writable: true
1618
+ });
1619
+ }
1620
+ }
1621
+ const assembled = {
1622
+ ...row,
1623
+ payload: JSON.parse(JSON.stringify({
1624
+ ...inherited,
1625
+ ...row.payload
1626
+ }))
1627
+ };
1628
+ active.delete(id);
1629
+ cache.set(id, assembled);
1630
+ return assembled;
1631
+ };
1632
+ return visit(entityId);
1633
+ }
1634
+ /** Resolve a field's declaring entity after validating the entire composition. */
1635
+ function resolveEntityFieldOwner(rows, entityId, field) {
1636
+ assembleEntityContent(rows, entityId);
1637
+ const byId = new Map(rows.entities.map((row) => [row.entityId, row]));
1638
+ const find = (id) => {
1639
+ const row = byId.get(id);
1640
+ if (Object.hasOwn(row.payload, field)) return id;
1641
+ if (field === "baseEntityIds") return void 0;
1642
+ for (const baseId of variantBaseEntityIds(row)) {
1643
+ const owner = find(baseId);
1644
+ if (owner !== void 0) return owner;
1645
+ }
1646
+ };
1647
+ return find(entityId) ?? entityId;
1648
+ }
1649
+ /** Patch assembled fields without copying inherited fields into the variant's stored payload. */
1650
+ function updateEntityFields(rows, entityId, fields) {
1651
+ assembleEntityContent(rows, entityId);
1652
+ if (!isJsonObject(fields) || Object.hasOwn(fields, "entityId") || Object.hasOwn(fields, "entityKind")) throw new ScriptCompositionError("invalid_bases", entityId, "Field updates require JSON without reserved identity fields");
1653
+ const updates = /* @__PURE__ */ new Map();
1654
+ for (const [field, value] of Object.entries(fields)) {
1655
+ const ownerId = resolveEntityFieldOwner(rows, entityId, field);
1656
+ const owner = updates.get(ownerId) ?? rows.entities.find((row) => row.entityId === ownerId);
1657
+ updates.set(ownerId, {
1658
+ ...owner,
1659
+ payload: {
1660
+ ...owner.payload,
1661
+ [field]: value
1662
+ }
1663
+ });
1664
+ }
1665
+ return [...updates.values()].map((row) => JSON.parse(JSON.stringify(row)));
1666
+ }
1667
+ /** Find the actual AudioScript text owner through direct variant bases. */
1668
+ function findComposedAudioScript(rows, variantEntityId, variantKind) {
1669
+ const variant = requireEntityKind(rows, variantEntityId, variantKind);
1670
+ assembleEntityContent(rows, variantEntityId);
1671
+ const sources = /* @__PURE__ */ new Map();
1672
+ const seen = /* @__PURE__ */ new Set();
1673
+ const visit = (row) => {
1674
+ for (const id of variantBaseEntityIds(row)) {
1675
+ if (seen.has(id)) continue;
1676
+ seen.add(id);
1677
+ const base = rows.entities.find((candidate) => candidate.entityId === id);
1678
+ if (base.entityKind === "audio-script") sources.set(id, assembleEntityContent(rows, id));
1679
+ else visit(base);
1680
+ }
1681
+ };
1682
+ visit(variant);
1683
+ if (sources.size !== 1) throw new ScriptCompositionError(sources.size === 0 ? "composition_missing" : "composition_ambiguous", variantEntityId, `${variantKind} "${variantEntityId}" baseEntityIds must resolve one AudioScript text owner, got ${sources.size}`);
1684
+ return [...sources.values()][0];
1685
+ }
1686
+ /** Join the whole AudioScript text in segment order. */
1687
+ function assembleScriptText(script) {
1688
+ return scriptSegments(script).map((segment) => segment.text).join("");
1689
+ }
1690
+ function scriptSegments(script) {
1691
+ const segments = script.payload.segments;
1692
+ if (!Array.isArray(segments)) throw new ScriptCompositionError("composition_dangling", script.entityId, `AudioScript "${script.entityId}" requires segments`);
1693
+ if (!segments.every(isScriptSegment) || new Set(segments.map((s) => s.segmentId)).size !== segments.length) throw new ScriptCompositionError("invalid_script", script.entityId, "AudioScript requires valid segments with unique local segmentId and text");
1694
+ return segments;
1695
+ }
1696
+ function isScriptSegment(value) {
1697
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
1698
+ const segment = value;
1699
+ return typeof segment.segmentId === "string" && segment.segmentId.trim() !== "" && typeof segment.text === "string" && (segment.language === void 0 || typeof segment.language === "string");
1700
+ }
1701
+ /** Assemble selected text after base-field validation and explicit variant overrides. */
1702
+ function assembleCaptionContent(rows, captionEntityId) {
1703
+ requireEntityKind(rows, captionEntityId, "caption");
1704
+ const caption = assembleEntityContent(rows, captionEntityId);
1705
+ const script = findComposedAudioScript(rows, captionEntityId, "caption");
1706
+ const segments = selectAudioScriptSegments({
1707
+ ...script,
1708
+ payload: caption.payload
1709
+ }, captionSelections(caption));
1710
+ return {
1711
+ caption,
1712
+ audioScript: script,
1713
+ segments,
1714
+ text: segments.map((segment) => segment.text).join("")
1715
+ };
1716
+ }
1717
+ /**
1718
+ * Assemble the complete Phonetic Script content for TTS: the composed
1719
+ * AudioScript's base text plus the variant's own phoneme and prosody fields.
1720
+ */
1721
+ function assemblePhoneticScriptContent(rows, phoneticScriptEntityId) {
1722
+ requireEntityKind(rows, phoneticScriptEntityId, "phonetic-script");
1723
+ const phoneticScript = assembleEntityContent(rows, phoneticScriptEntityId);
1724
+ const audioScript = findComposedAudioScript(rows, phoneticScriptEntityId, "phonetic-script");
1725
+ const segments = scriptSegments({
1726
+ ...audioScript,
1727
+ payload: phoneticScript.payload
1728
+ });
1729
+ return {
1730
+ phoneticScript,
1731
+ audioScript,
1732
+ segments,
1733
+ text: segments.map((segment) => segment.text).join("")
1734
+ };
1735
+ }
1736
+ function captionSelections(caption) {
1737
+ const selections = caption.payload.selections;
1738
+ if (!Array.isArray(selections) || selections.length === 0) throw new ScriptCompositionError("empty_selection", caption.entityId, `Caption "${caption.entityId}" requires a non-empty segment selection into its AudioScript`);
1739
+ if (!selections.every(isSegmentSelection)) throw new ScriptCompositionError("invalid_selection", caption.entityId, "Caption selections require a segmentId and optional textRange");
1740
+ return selections;
1741
+ }
1742
+ function isSegmentSelection(value) {
1743
+ return typeof value === "object" && value != null && !Array.isArray(value) && typeof value.segmentId === "string" && value.segmentId.trim() !== "" && Object.keys(value).every((key) => key === "segmentId" || key === "textRange");
1744
+ }
1745
+ /** Select source text without creating another authoritative text field. */
1746
+ function selectAudioScriptSegments(script, selections) {
1747
+ if (!Array.isArray(selections) || selections.length === 0 || !selections.every(isSegmentSelection) || new Set(selections.map((s) => s.segmentId)).size !== selections.length) throw new ScriptCompositionError("invalid_selection", script.entityId, "Caption selections must be non-empty and name unique source segments");
1748
+ const bySegmentId = new Map(scriptSegments(script).map((segment) => [segment.segmentId, segment]));
1749
+ const selected = selections.map((selection) => {
1750
+ const segment = bySegmentId.get(selection.segmentId);
1751
+ if (segment === void 0) throw new ScriptCompositionError("unknown_segment", script.entityId, `Caption selection "${selection.segmentId}" does not name a segment of AudioScript "${script.entityId}"`);
1752
+ if (selection.textRange === void 0) return segment;
1753
+ const range = selection.textRange;
1754
+ const points = Array.from(segment.text);
1755
+ if (typeof range !== "object" || range === null || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || range.end > points.length || Object.keys(range).some((key) => key !== "start" && key !== "end")) throw new ScriptCompositionError("invalid_selection", script.entityId, "Caption textRange must be a non-empty half-open Unicode code-point range within its source segment");
1756
+ return {
1757
+ ...segment,
1758
+ text: points.slice(range.start, range.end).join("")
1759
+ };
1760
+ });
1761
+ if (!selected.map((segment) => segment.text).join("").trim()) throw new ScriptCompositionError("empty_selection", script.entityId, "Caption selection must contain visible source text");
1762
+ return selected;
1763
+ }
1764
+ function requireEntityKind(rows, entityIdValue, entityKind) {
1765
+ const entity = rows.entities.find((candidate) => candidate.entityId === entityIdValue);
1766
+ if (entity === void 0) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" does not exist`);
1767
+ if (entity.entityKind !== entityKind) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" must have kind "${entityKind}", got "${entity.entityKind}"`);
1768
+ return entity;
1769
+ }
1770
+ //#endregion
1771
+ //#region ../medeo-dsl/src/rows.ts
1772
+ function entityToRow(entity) {
1773
+ const { entityId, entityKind, ...payload } = entity;
1774
+ if (!isJsonObject(payload)) throw new Error(`Entity "${entityId}" payload must contain only JSON values`);
1775
+ return {
1776
+ entityId,
1777
+ entityKind,
1778
+ payload
1779
+ };
1780
+ }
1781
+ //#endregion
1539
1782
  //#region ../medeo-dsl/src/invariants.ts
1540
1783
  /**
1541
1784
  * Media variants own their Asset identity directly: a `physical-asset`
1542
- * Relation may not bind them. Caption inline text (and other non-media
1543
- * Sequence entities) keep their Asset binding.
1785
+ * Relation may not bind them. Caption text is composed domain content, so
1786
+ * Captions do not bind Physical Assets either; the Relation vocabulary stays
1787
+ * open for future physical pipeline semantics.
1544
1788
  */
1545
1789
  function validateMediaAssetIdentity(entity, index) {
1546
1790
  return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
@@ -1553,6 +1797,19 @@ function validateMediaAssetIdentity(entity, index) {
1553
1797
  function validateEntityRelationSet(entityRefs, index, options) {
1554
1798
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
1555
1799
  const entityIds = new Set(entities.map((entity) => entity.entityId));
1800
+ const rows = {
1801
+ entities: entities.map((entity) => entityToRow(entity.current())),
1802
+ relations: []
1803
+ };
1804
+ for (const entity of entities) try {
1805
+ assembleEntityContent(rows, entity.entityId);
1806
+ } catch (error) {
1807
+ issues.push({
1808
+ code: "invalid_variant_composition",
1809
+ entityId: entity.entityId,
1810
+ message: error instanceof Error ? error.message : "Invalid variant composition"
1811
+ });
1812
+ }
1556
1813
  for (const entity of entities) {
1557
1814
  const current = entity.current();
1558
1815
  const entityIssues = validateEntity(entity, entityIds);
@@ -1573,7 +1830,8 @@ function validateEntityRelationSet(entityRefs, index, options) {
1573
1830
  issues.push(...validateClipPlacement(entity, index));
1574
1831
  }
1575
1832
  if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
1576
- if (current.entityKind === "caption") issues.push(...validateCaptionAsset(entity, index));
1833
+ if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
1834
+ if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
1577
1835
  issues.push(...validateSequenceComposition(entity));
1578
1836
  }
1579
1837
  issues.push(...validateClipAnchorCycles(entities, index));
@@ -1676,25 +1934,35 @@ function validateAXVideoAdmission(axVideo, index) {
1676
1934
  message: "AXVideo must have exactly one live AXVideo-Marker Relation"
1677
1935
  }];
1678
1936
  }
1679
- function validateCaptionAsset(caption, index) {
1680
- const assets = ofKind([...index.relationsOf(caption)], "physical-asset").map((relation) => relation.other(caption)?.deref()?.current()).filter((entity) => entity?.entityKind === "asset");
1681
- if (assets.length > 0) {
1682
- const captionText = caption.current().text;
1683
- if (!assets.some((asset) => {
1684
- const inline = asset.inline;
1685
- return isRecord(inline) && inline.mediaType === "text/plain" && inline.text !== captionText;
1686
- })) return [];
1687
- return [{
1688
- code: "caption_inline_asset_mismatch",
1689
- entityId: caption.entityId,
1690
- message: "Caption text must match the text/plain inline Physical Asset"
1691
- }];
1937
+ /** Validate a Caption against the complete entity set, including its directly held bases. */
1938
+ function validateCaptionComposition(caption, index, entities = []) {
1939
+ return validateVariantComposition(caption, index, entities, "caption");
1940
+ }
1941
+ function validatePhoneticComposition(phonetic, index, entities = []) {
1942
+ return validateVariantComposition(phonetic, index, entities, "phonetic-script");
1943
+ }
1944
+ function validateVariantComposition(variant, index, entities, kind) {
1945
+ const issues = [];
1946
+ if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
1947
+ code: "caption_physical_asset_forbidden",
1948
+ entityId: variant.entityId,
1949
+ message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
1950
+ });
1951
+ const rows = {
1952
+ entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
1953
+ relations: []
1954
+ };
1955
+ try {
1956
+ if (kind === "caption") assembleCaptionContent(rows, variant.entityId);
1957
+ else assemblePhoneticScriptContent(rows, variant.entityId);
1958
+ } catch (error) {
1959
+ issues.push({
1960
+ code: kind === "caption" ? "caption_composition_required" : "phonetic_script_composition_required",
1961
+ entityId: variant.entityId,
1962
+ message: error instanceof Error ? error.message : "Invalid variant composition"
1963
+ });
1692
1964
  }
1693
- return [{
1694
- code: "caption_asset_required",
1695
- entityId: caption.entityId,
1696
- message: "Caption must have a Physical Asset Relation"
1697
- }];
1965
+ return issues;
1698
1966
  }
1699
1967
  function validateClipPlacement(clip, index) {
1700
1968
  const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
@@ -1822,6 +2090,18 @@ function validateKnownEntityPayload(entity) {
1822
2090
  const value = entity;
1823
2091
  const problems = [];
1824
2092
  if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
2093
+ if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
2094
+ for (const key of [
2095
+ "extent",
2096
+ "sampling",
2097
+ "coordinateSpace",
2098
+ "sourceRange",
2099
+ "targetRange",
2100
+ "duration",
2101
+ "startMs",
2102
+ "endMs"
2103
+ ]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an external annotation Marker`);
2104
+ }
1825
2105
  switch (entity.entityKind) {
1826
2106
  case "track":
1827
2107
  validateOptionalField(value, "hidden", "boolean", problems);
@@ -1848,9 +2128,11 @@ function validateKnownEntityPayload(entity) {
1848
2128
  validateMarkerPayload(value, problems);
1849
2129
  break;
1850
2130
  case "audio-script":
1851
- case "phonetic-script":
1852
2131
  validateScriptPayload(value, problems);
1853
2132
  break;
2133
+ case "phonetic-script":
2134
+ validatePhoneticScriptPayload(value, problems);
2135
+ break;
1854
2136
  case "timeline":
1855
2137
  case "viewport": break;
1856
2138
  case "clip":
@@ -1914,24 +2196,18 @@ function validateSegmentRanges(value, problems) {
1914
2196
  function validateAssetPayload(value, problems) {
1915
2197
  validateExternalLocator(value, problems);
1916
2198
  validateStorageKey(value, problems);
1917
- const inline = value.inline;
1918
- if (inline !== void 0) if (!isRecord(inline)) problems.push("inline must be an object when present");
1919
- else {
1920
- if (inline.mediaType !== "text/plain") problems.push("inline.mediaType must be \"text/plain\"");
1921
- if (typeof inline.text !== "string") problems.push("inline.text must be a string");
1922
- }
1923
- if (value.external !== void 0 && value.inline !== void 0) problems.push("Asset must not combine external and inline physical locations");
2199
+ if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
1924
2200
  }
1925
2201
  /**
1926
2202
  * Media variants own their Asset locator directly: `external` is required and
1927
- * inline text stays an Asset-kind-only value so the two physical-location
2203
+ * textual domain content is owned by AudioScript; the physical-location
1928
2204
  * vocabularies cannot mix.
1929
2205
  */
1930
2206
  function validateMediaAssetFields(value, problems) {
1931
2207
  if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
1932
2208
  else validateExternalLocator(value, problems);
1933
2209
  validateStorageKey(value, problems);
1934
- if (value.inline !== void 0) problems.push("inline is an Asset-kind field; media variants use external");
2210
+ if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
1935
2211
  }
1936
2212
  function validateExternalLocator(value, problems) {
1937
2213
  const external = value.external;
@@ -1958,7 +2234,26 @@ function validateVoicePayload(value, problems) {
1958
2234
  if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
1959
2235
  }
1960
2236
  function validateCaptionPayload(value, problems) {
1961
- if (value.text !== void 0 && typeof value.text !== "string") problems.push("text must be a string when present");
2237
+ const selections = value.selections;
2238
+ if (!Array.isArray(selections)) problems.push("selections must be a non-empty array of AudioScript segment selections");
2239
+ else {
2240
+ if (selections.length === 0) problems.push("selections must name at least one AudioScript segment");
2241
+ const seen = /* @__PURE__ */ new Set();
2242
+ for (const [index, selection] of selections.entries()) {
2243
+ if (!isRecord(selection)) {
2244
+ problems.push(`selections[${index}] must be an object`);
2245
+ continue;
2246
+ }
2247
+ if (typeof selection.segmentId !== "string" || selection.segmentId.trim() === "") problems.push(`selections[${index}].segmentId must be a string`);
2248
+ else if (seen.has(selection.segmentId)) problems.push(`selections[${index}].segmentId must be unique within the Caption`);
2249
+ else seen.add(selection.segmentId);
2250
+ if (Object.keys(selection).some((key) => key !== "segmentId" && key !== "textRange")) problems.push(`selections[${index}] may only contain segmentId and textRange`);
2251
+ if (selection.textRange !== void 0) {
2252
+ const range = selection.textRange;
2253
+ if (!isRecord(range) || !Number.isSafeInteger(range.start) || !Number.isSafeInteger(range.end) || range.start < 0 || range.end <= range.start || Object.keys(range).some((key) => key !== "start" && key !== "end")) problems.push(`selections[${index}].textRange must be a non-empty half-open code-point range`);
2254
+ }
2255
+ }
2256
+ }
1962
2257
  const style = value.style;
1963
2258
  if (style === void 0) return;
1964
2259
  if (!isRecord(style)) {
@@ -1993,6 +2288,12 @@ function validateRange(value, path, required, problems) {
1993
2288
  if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
1994
2289
  if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
1995
2290
  }
2291
+ /** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
2292
+ function validatePhoneticScriptPayload(value, problems) {
2293
+ if (value.segments !== void 0) validateScriptPayload(value, problems);
2294
+ if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
2295
+ if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
2296
+ }
1996
2297
  function validateScriptPayload(value, problems) {
1997
2298
  if (!Array.isArray(value.segments)) {
1998
2299
  problems.push("segments must be an array");
@@ -2035,6 +2336,7 @@ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths)
2035
2336
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityIdPaths(item, entityKind, `${parentPath}[${index}]`, ancestors, paths);
2036
2337
  else for (const [key, child] of Object.entries(value)) {
2037
2338
  const path = parentPath.length === 0 ? key : `${parentPath}.${key}`;
2339
+ if (parentPath.length === 0 && key === "baseEntityIds") continue;
2038
2340
  if (!(parentPath.length === 0 && key === "entityId") && !isOwnedLocalIdPath(entityKind, path) && isEntityIdFieldName(key)) paths.push(path);
2039
2341
  visitPeerEntityIdPaths(child, entityKind, path, ancestors, paths);
2040
2342
  }
@@ -2054,7 +2356,7 @@ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path,
2054
2356
  ancestors.add(value);
2055
2357
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
2056
2358
  else for (const [key, child] of Object.entries(value)) {
2057
- if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
2359
+ if (path.length === 0 && (key === "entityId" || key === "entityKind" || key === "baseEntityIds")) continue;
2058
2360
  visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
2059
2361
  }
2060
2362
  ancestors.delete(value);
@@ -2071,16 +2373,25 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2071
2373
  ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
2072
2374
  if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
2073
2375
  if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2074
- if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey|inline\.(?:mediaType|text))$/.test(path)) return true;
2376
+ if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2075
2377
  if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2076
2378
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
2077
- if (entityKind === "caption" && (path === "text" || path.startsWith("style."))) return true;
2078
- if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
2379
+ if (entityKind === "caption" && (path.startsWith("style.") || /^selections\[\d+\]\.segmentId$/.test(path))) return true;
2380
+ if ([
2381
+ "audio-script",
2382
+ "caption",
2383
+ "phonetic-script"
2384
+ ].includes(entityKind) && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
2079
2385
  return false;
2080
2386
  }
2081
2387
  function isOwnedLocalIdPath(entityKind, path) {
2082
2388
  if (path === "lifecycle.actorId") return true;
2083
- if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
2389
+ if ([
2390
+ "audio-script",
2391
+ "caption",
2392
+ "phonetic-script"
2393
+ ].includes(entityKind) && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
2394
+ if (entityKind === "caption" && /^selections\[\d+\]\.segmentId$/.test(path)) return true;
2084
2395
  if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2085
2396
  if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
2086
2397
  return false;
@@ -2136,35 +2447,6 @@ function ofKind(relations, kind) {
2136
2447
  return relations.filter((relation) => relation.kind === kind);
2137
2448
  }
2138
2449
  //#endregion
2139
- //#region ../medeo-dsl/src/json-values.ts
2140
- const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
2141
- function isJsonObject(value) {
2142
- return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
2143
- }
2144
- function isJsonValue(value, ancestors) {
2145
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
2146
- if (typeof value === "number") return Number.isFinite(value);
2147
- if (typeof value !== "object") return false;
2148
- if (!Array.isArray(value) && !isPlainObject(value)) return false;
2149
- if (ancestors.has(value)) return false;
2150
- ancestors.add(value);
2151
- const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
2152
- ancestors.delete(value);
2153
- return valid;
2154
- }
2155
- /** Recognize an ordinary object from any VM realm without admitting class instances. */
2156
- function isPlainObject(value) {
2157
- try {
2158
- const prototype = Object.getPrototypeOf(value);
2159
- if (prototype === null) return true;
2160
- if (Object.getPrototypeOf(prototype) !== null) return false;
2161
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
2162
- return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
2163
- } catch {
2164
- return false;
2165
- }
2166
- }
2167
- //#endregion
2168
2450
  //#region ../medeo-dsl/src/relation-specs.ts
2169
2451
  const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2170
2452
  const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
@@ -2187,8 +2469,6 @@ const generatedRelationSpec = Object.freeze({
2187
2469
  validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
2188
2470
  validateMetadata: isEmptyMetadata
2189
2471
  });
2190
- const phoneticScriptProvenanceRelationSpec = metadataSpec("phonetic-script-provenance", "phonetic-script", "audio-script", isSegmentAlignmentMetadata);
2191
- const captionProvenanceRelationSpec = metadataSpec("caption-provenance", "caption", "audio-script", isSegmentAlignmentMetadata);
2192
2472
  const captionAlignmentRelationSpec = Object.freeze({
2193
2473
  kind: "caption-alignment",
2194
2474
  validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
@@ -2200,19 +2480,20 @@ const clipAnchorRelationSpec = Object.freeze({
2200
2480
  validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
2201
2481
  validateMetadata: isEmptyMetadata
2202
2482
  });
2203
- /** `audio-script-render(output, script)` means endpoint 0 was rendered from endpoint 1. */
2204
- const audioScriptRenderRelationSpec = Object.freeze({
2205
- kind: "audio-script-render",
2206
- validateEndpoints: (endpoints) => {
2207
- const outputKind = endpoints[0].current().entityKind;
2208
- return (outputKind === "audio" || outputKind === "voice") && endpoints[1].current().entityKind === "audio-script";
2209
- },
2483
+ /** Voice is generated from PhoneticScript; kinds determine roles regardless of endpoint positions. */
2484
+ const phoneticScriptRenderRelationSpec = Object.freeze({
2485
+ kind: "phonetic-script-render",
2486
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voice"]), new Set(["phonetic-script"])),
2210
2487
  validateMetadata: isEmptyMetadata
2211
2488
  });
2212
- /** `audio-script-source(script, source)` means endpoint 0 was transcribed from endpoint 1. */
2489
+ /** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
2213
2490
  const audioScriptSourceRelationSpec = Object.freeze({
2214
2491
  kind: "audio-script-source",
2215
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
2492
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
2493
+ "audio",
2494
+ "video",
2495
+ "voice"
2496
+ ])),
2216
2497
  validateMetadata: isEmptyMetadata
2217
2498
  });
2218
2499
  /**
@@ -2236,11 +2517,9 @@ const builtInRelationSpecs = Object.freeze([
2236
2517
  markerTimelineRelationSpec,
2237
2518
  physicalAssetRelationSpec,
2238
2519
  generatedRelationSpec,
2239
- phoneticScriptProvenanceRelationSpec,
2240
- captionProvenanceRelationSpec,
2241
2520
  captionAlignmentRelationSpec,
2242
2521
  clipAnchorRelationSpec,
2243
- audioScriptRenderRelationSpec,
2522
+ phoneticScriptRenderRelationSpec,
2244
2523
  audioScriptSourceRelationSpec,
2245
2524
  audioScriptMarkerRelationSpec
2246
2525
  ]);
@@ -2280,9 +2559,6 @@ function isAssetBinding(value) {
2280
2559
  const orderingKey = /(order|ordinal|position|rank|index|z[_-]?index)/i;
2281
2560
  return Object.keys(value).every((key) => !orderingKey.test(key));
2282
2561
  }
2283
- function isSegmentAlignmentMetadata(value) {
2284
- return isJsonObject(value) && Object.hasOwn(value, "segmentAlignment");
2285
- }
2286
2562
  function isCaptionAlignmentMetadata(value) {
2287
2563
  return isJsonObject(value) && Object.hasOwn(value, "alignment");
2288
2564
  }
@@ -2339,7 +2615,7 @@ var BiRelationIndex = class {
2339
2615
  byRelationId = /* @__PURE__ */ new Map();
2340
2616
  link(input) {
2341
2617
  if (input.spec.kind === "generated") throw new Error("Author generated Relations with linkGenerated({ output, input })");
2342
- if (input.spec.kind === "clip-anchor" || input.spec.kind === "audio-script-render" || input.spec.kind === "audio-script-source") throw new Error(`Author ordered ${input.spec.kind} Relations with the dedicated role-named method`);
2618
+ 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`);
2343
2619
  return this.linkValidated(input);
2344
2620
  }
2345
2621
  /** Author `generated(output, input)` without exposing positional arguments. */
@@ -2362,12 +2638,12 @@ var BiRelationIndex = class {
2362
2638
  trace: input.trace
2363
2639
  });
2364
2640
  }
2365
- /** Author `audio-script-render(output, script)` without exposing positional arguments. */
2366
- linkAudioScriptRender(input) {
2641
+ /** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
2642
+ linkPhoneticScriptRender(input) {
2367
2643
  return this.linkValidated({
2368
2644
  relationId: input.relationId,
2369
- spec: audioScriptRenderRelationSpec,
2370
- endpoints: [input.output, input.script],
2645
+ spec: phoneticScriptRenderRelationSpec,
2646
+ endpoints: [input.output, input.phoneticScript],
2371
2647
  metadata: {},
2372
2648
  trace: input.trace
2373
2649
  });
@@ -2468,7 +2744,13 @@ function decodeEntityRelationRows(rows, options) {
2468
2744
  const entitiesById = /* @__PURE__ */ new Map();
2469
2745
  const extensionEntityKinds = new Set(options.entityKinds ?? []);
2470
2746
  for (const row of rows.entities) {
2471
- const entity = decodeEntityRow(row, extensionEntityKinds, issues);
2747
+ let entity = decodeEntityRow(row, extensionEntityKinds, issues);
2748
+ if (entity != null) try {
2749
+ entity = decodeEntityRow(assembleEntityContent(rows, row.entityId), extensionEntityKinds, issues);
2750
+ } catch (error) {
2751
+ issues.push(errorMessage(error));
2752
+ continue;
2753
+ }
2472
2754
  if (entity == null) continue;
2473
2755
  const ref = createEntityRef(entity);
2474
2756
  if (entitiesById.has(ref.entityId)) {
@@ -2648,9 +2930,9 @@ var EntityProjectionGraph = class {
2648
2930
  timeline;
2649
2931
  constructor(rows) {
2650
2932
  this.rows = rows;
2651
- decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2652
- this.entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
2653
- const roots = rows.entities.filter((row) => row.entityKind === "timeline");
2933
+ const decoded = decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2934
+ this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
2935
+ const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
2654
2936
  if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
2655
2937
  this.timeline = roots[0];
2656
2938
  }
@@ -2860,7 +3142,7 @@ function projectEntityTimeline(rows, meta) {
2860
3142
  const asset = mediaAssetFacts(graph, content);
2861
3143
  requireUntrimmedAudio(content, marker, placement);
2862
3144
  const external = externalLocator(asset, "memota-speech");
2863
- const script = graph.one(content.entityId, "audio-script-render", "audio-script");
3145
+ const assembled = graph.adjacent(content.entityId, "phonetic-script-render").length > 0 || content.payload.voice !== void 0 ? assemblePhoneticScriptContent(rows, graph.one(content.entityId, "phonetic-script-render", "phonetic-script").entityId) : void 0;
2864
3146
  const voice = content.payload.voice;
2865
3147
  if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
2866
3148
  const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
@@ -2870,7 +3152,7 @@ function projectEntityTimeline(rows, meta) {
2870
3152
  media_duration_ms: placement.duration,
2871
3153
  audio_storage_key: storageKey(asset),
2872
3154
  origin_speech_id: external.key,
2873
- audio_script: scriptText(script),
3155
+ audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "audio-script-source").length > 0 ? assembleScriptText(graph.one(content.entityId, "audio-script-source", "audio-script")) : ""),
2874
3156
  volume: volume.data,
2875
3157
  voice: object(voice) ? {
2876
3158
  id: typeof voice.key === "string" ? voice.key : void 0,
@@ -2879,12 +3161,7 @@ function projectEntityTimeline(rows, meta) {
2879
3161
  caption_ids: captionIds
2880
3162
  } };
2881
3163
  } else if (track.role === "caption") {
2882
- const text = content.payload.text;
2883
- if (typeof text !== "string") fail("Caption " + content.entityId + " requires its text");
2884
- const asset = graph.one(content.entityId, "physical-asset", "asset");
2885
- const inline = asset.payload.inline;
2886
- if (inline !== void 0 && (!object(inline) || inline.mediaType !== "text/plain" || inline.text !== text)) fail("Inline Caption Asset and Caption text must agree");
2887
- if (inline === void 0 && !object(asset.payload.external)) fail("Caption Asset must identify physical text or contain actual inline text");
3164
+ const text = assembleCaptionContent(rows, content.entityId).text;
2888
3165
  if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
2889
3166
  const style = captionStyle(content.payload.style);
2890
3167
  const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
@@ -2972,14 +3249,6 @@ function storageKey(asset) {
2972
3249
  if (typeof asset.payload.storageKey !== "string" || asset.payload.storageKey.trim() === "") fail("Asset " + asset.entityId + " requires its factual storageKey");
2973
3250
  return asset.payload.storageKey;
2974
3251
  }
2975
- function scriptText(script) {
2976
- const segments = script.payload.segments;
2977
- if (!Array.isArray(segments)) fail("AudioScript requires segments");
2978
- return segments.map((segment) => {
2979
- if (!object(segment) || typeof segment.text !== "string") fail("AudioScript segment requires text");
2980
- return segment.text;
2981
- }).join("");
2982
- }
2983
3252
  function captionStyle(value) {
2984
3253
  if (value === void 0) return void 0;
2985
3254
  if (!object(value)) fail("Caption style must be an object");
@@ -3351,4 +3620,4 @@ function createPlainMemoryAdapter(document, options) {
3351
3620
  return new PlainMemoryAdapter(document, options);
3352
3621
  }
3353
3622
  //#endregion
3354
- export { videoDocumentMirrorSchema as $, ensureLaneTrack as A, resolveSpeechOverlapByShiftingVideos as B, toVideoDocument as C, partUnionSchema as D, validateVideoDocument as E, arrangeMainTrackSeamlessly as F, safeDurationMs as G, TIMELINE_SKELETON_DURATION_MS as H, fillMainTrackTimeGaps as I, VIDEO_DOCUMENT_SCHEMA_VERSION as J, DEFAULT_UNIT_TIME_MS as K, reassignSpeechesToVideoClipsByTime as L, laneTrackId as M, solveVideoDocument as N, videoDocumentSchema as O, cascadeAfterVideoClipChanges as P, recordEntries as Q, recalculateTimelineDuration as R, fromVideoDocument as S, assertValidVideoDocument as T, isEmptyVideoClip as U, syncAggregatedClipsTimePosition as V, partDurationMs as W, speedOf as X, effectiveVideoClipDurationMs as Y, partUnionToDraft as Z, hasSequence as _, applyEntityTimelineProjection as a, buildSpeechHostMap as b, writeVideoDocumentToDraft as c, resolveEntityTimelineLayout as d, base64ToBytes as et, EntityTimelineProjectionError as f, createRelationId as g, createEntityId as h, MirrorVideoDocumentAdapter as i, findLaneTrack as j, LANE_KINDS_IN_STACK_ORDER as k, readVideoDocumentFromDraft as l, isJsonObject as m, createPlainMemoryAdapter as n, createMirrorVideoDocument as o, decodeEntityRelationRows as p, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as q, generatePartId as r, createMirrorVideoDocumentAdapter as s, PlainMemoryAdapter as t, bytesToBase64 as tt, projectEntityTimeline as u, isMediaAssetVariantKind as v, VideoDocumentValidationError as w, derivePositionFromAbs as x, buildInitialVideoDocument as y, resolveAllSpeechOverlaps as z };
3623
+ export { isEmptyVideoClip as $, buildSpeechHostMap as A, ensureLaneTrack as B, variantBaseEntityIds as C, hasSequence as D, createRelationId as E, assertValidVideoDocument as F, arrangeMainTrackSeamlessly as G, laneTrackId as H, validateVideoDocument as I, recalculateTimelineDuration as J, fillMainTrackTimeGaps as K, partUnionSchema as L, fromVideoDocument as M, toVideoDocument as N, isMediaAssetVariantKind as O, VideoDocumentValidationError as P, TIMELINE_SKELETON_DURATION_MS as Q, videoDocumentSchema as R, updateEntityFields as S, createEntityId as T, solveVideoDocument as U, findLaneTrack as V, cascadeAfterVideoClipChanges as W, resolveSpeechOverlapByShiftingVideos as X, resolveAllSpeechOverlaps as Y, syncAggregatedClipsTimePosition as Z, assembleEntityContent as _, applyEntityTimelineProjection as a, effectiveVideoClipDurationMs as at, findComposedAudioScript as b, writeVideoDocumentToDraft as c, recordEntries as ct, EntityProjectionGraph as d, bytesToBase64 as dt, partDurationMs as et, resolveEntityTimelineLayout as f, assembleCaptionContent as g, ScriptCompositionError as h, MirrorVideoDocumentAdapter as i, VIDEO_DOCUMENT_SCHEMA_VERSION as it, derivePositionFromAbs as j, buildInitialVideoDocument as k, readVideoDocumentFromDraft as l, videoDocumentMirrorSchema as lt, decodeEntityRelationRows as m, createPlainMemoryAdapter as n, DEFAULT_UNIT_TIME_MS as nt, createMirrorVideoDocument as o, speedOf as ot, EntityTimelineProjectionError as p, reassignSpeechesToVideoClipsByTime as q, generatePartId as r, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as rt, createMirrorVideoDocumentAdapter as s, partUnionToDraft as st, PlainMemoryAdapter as t, safeDurationMs as tt, projectEntityTimeline as u, base64ToBytes as ut, assemblePhoneticScriptContent as v, isJsonObject as w, selectAudioScriptSegments as x, assembleScriptText as y, LANE_KINDS_IN_STACK_ORDER as z };