@mengine/medeo-client 1.3.1-alpha.1 → 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.
package/README.md CHANGED
@@ -74,18 +74,17 @@ plan. A speech-overlap shift is explicit placement on the real visual Clips;
74
74
  an uncovered time interval does not require an invented empty-media entity.
75
75
 
76
76
  Generated Voice links to a pre-existing PhoneticScript through `phonetic-script-render`.
77
- Caption and PhoneticScript each compose a real AudioScript through exactly one
78
- `caption-composition` or `phonetic-script-composition` relation. AudioScript owns
79
- `segments[].text`; the variants never persist a text copy. Read complete content
77
+ Caption and PhoneticScript each compose a real AudioScript by directly holding `baseEntityIds`, an array that may compose multiple entities. AudioScript owns
78
+ `segments[].text`; assembly never automatically persists a copy on the variants. Read complete content
80
79
  with `assembleCaptionContent` or `assemblePhoneticScriptContent`. Broken composition
81
- fails explicitly. Recorded Audio/Video/Voice can be the ASR source via
80
+ fails explicitly. Same-name fields from different bases are rejected even for equal values and even if the variant declares that field. After base fields are unambiguous, explicit own fields may override them without mutating the bases. Recorded Audio/Video/Voice can be the ASR source via
82
81
  `audio-script-source`, without creating a synthesis variant.
83
82
 
84
- `insertCaptionClip` requires `audioScriptEntityId` and `selections`. Each selection
83
+ `insertCaptionClip` requires `baseEntityIds` and `selections`. Each selection
85
84
  names a source `segmentId` and optionally a half-open Unicode code-point
86
85
  `textRange: {start, end}`. This permits display re-segmentation without rewriting
87
86
  the original script. `upsertVoiceoverTake` requires the real
88
- `phoneticScriptEntityId` used for synthesis; its captions select the same script.
87
+ `phoneticScriptEntityId` used for synthesis; its captions supply their own `baseEntityIds` and select the same script.
89
88
  External speech identity and storage key stay on Voice.
90
89
 
91
90
  AudioScript has no intrinsic Sequence and cannot enter a Clip. `audio-script-marker`
@@ -107,3 +106,9 @@ It rejects `video-document/entity-projection-v1` documents. Production entity
107
106
  tools and editor adapters must not fall back to it or raw `/updates` on failure.
108
107
  For an entity projection, `meta.version` is the committed entity revision; the
109
108
  Loro version vector remains the transport-sync cursor.
109
+
110
+ ### Transparent variant fields in the DSL sandbox
111
+
112
+ `entities.get/list` return complete assembled fields. Consumers do not query base IDs or merge source payloads. `entities.update` patches supplied fields, preserves omitted fields, and routes inherited fields to their declaring entity. Explicit own-field declarations use `entities.declareFields`; these override unambiguous base fields without changing the base. Persisted entity rows and command payloads contain owned fields only. Base collisions are validated before own overrides.
113
+
114
+ `prepareCaptionContent` accepts caption authoring intent and handles source composition, visible text selection, and preservation of existing intrinsic timing inside the SDK.
@@ -1524,13 +1524,49 @@ function isRecord$1(value) {
1524
1524
  return typeof value === "object" && value != null && !Array.isArray(value);
1525
1525
  }
1526
1526
  //#endregion
1527
+ //#region ../medeo-dsl/src/ids.ts
1528
+ function createEntityId(value) {
1529
+ return createId(value, "EntityId");
1530
+ }
1531
+ function createRelationId(value) {
1532
+ return createId(value, "RelationId");
1533
+ }
1534
+ function createId(value, label) {
1535
+ if (value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
1536
+ return value;
1537
+ }
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
1527
1568
  //#region ../medeo-dsl/src/composition.ts
1528
- /**
1529
- * Raised when a variant's complete content cannot be assembled from its
1530
- * required composition Relation. Assembly never falls back to variant-local
1531
- * text: a missing, ambiguous, or dangling source is an error, not an empty
1532
- * read.
1533
- */
1569
+ /** Assembly fails explicitly when declared bases cannot provide valid, unambiguous content. */
1534
1570
  var ScriptCompositionError = class extends Error {
1535
1571
  code;
1536
1572
  entityId;
@@ -1541,23 +1577,111 @@ var ScriptCompositionError = class extends Error {
1541
1577
  this.name = "ScriptCompositionError";
1542
1578
  }
1543
1579
  };
1544
- function compositionRelationKind(variantKind) {
1545
- return variantKind === "caption" ? "caption-composition" : "phonetic-script-composition";
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)));
1546
1666
  }
1547
- /**
1548
- * Resolve the one AudioScript a variant composes. Relations are queried from
1549
- * both endpoints, so persisted endpoint positions do not matter.
1550
- */
1667
+ /** Find the actual AudioScript text owner through direct variant bases. */
1551
1668
  function findComposedAudioScript(rows, variantEntityId, variantKind) {
1552
- const kind = compositionRelationKind(variantKind);
1553
- const edges = rows.relations.filter((relation) => relation.relationKind === kind && (relation.endpoint0EntityId === variantEntityId || relation.endpoint1EntityId === variantEntityId));
1554
- if (edges.length === 0) throw new ScriptCompositionError("composition_missing", variantEntityId, `${variantKind} "${variantEntityId}" has no ${kind} Relation; it has no content without its AudioScript`);
1555
- if (edges.length > 1) throw new ScriptCompositionError("composition_ambiguous", variantEntityId, `${variantKind} "${variantEntityId}" has ${edges.length} ${kind} Relations; exactly one AudioScript is required`);
1556
- const edge = edges[0];
1557
- const otherId = edge.endpoint0EntityId === variantEntityId ? edge.endpoint1EntityId : edge.endpoint0EntityId;
1558
- const script = rows.entities.find((entity) => entity.entityId === otherId);
1559
- if (script === void 0 || script.entityKind !== "audio-script") throw new ScriptCompositionError("composition_dangling", variantEntityId, `${variantKind} "${variantEntityId}" ${kind} Relation must resolve a live AudioScript, got "${script?.entityKind ?? "missing entity"}"`);
1560
- return script;
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];
1561
1685
  }
1562
1686
  /** Join the whole AudioScript text in segment order. */
1563
1687
  function assembleScriptText(script) {
@@ -1574,16 +1698,15 @@ function isScriptSegment(value) {
1574
1698
  const segment = value;
1575
1699
  return typeof segment.segmentId === "string" && segment.segmentId.trim() !== "" && typeof segment.text === "string" && (segment.language === void 0 || typeof segment.language === "string");
1576
1700
  }
1577
- /**
1578
- * Assemble the complete Caption content from its AudioScript: the selected
1579
- * segments in selection order, joined as display text. The Caption payload
1580
- * never carries text, so a broken composition fails here instead of silently
1581
- * rendering an empty caption.
1582
- */
1701
+ /** Assemble selected text after base-field validation and explicit variant overrides. */
1583
1702
  function assembleCaptionContent(rows, captionEntityId) {
1584
- const caption = requireEntityKind(rows, captionEntityId, "caption");
1703
+ requireEntityKind(rows, captionEntityId, "caption");
1704
+ const caption = assembleEntityContent(rows, captionEntityId);
1585
1705
  const script = findComposedAudioScript(rows, captionEntityId, "caption");
1586
- const segments = selectAudioScriptSegments(script, captionSelections(caption));
1706
+ const segments = selectAudioScriptSegments({
1707
+ ...script,
1708
+ payload: caption.payload
1709
+ }, captionSelections(caption));
1587
1710
  return {
1588
1711
  caption,
1589
1712
  audioScript: script,
@@ -1596,9 +1719,13 @@ function assembleCaptionContent(rows, captionEntityId) {
1596
1719
  * AudioScript's base text plus the variant's own phoneme and prosody fields.
1597
1720
  */
1598
1721
  function assemblePhoneticScriptContent(rows, phoneticScriptEntityId) {
1599
- const phoneticScript = requireEntityKind(rows, phoneticScriptEntityId, "phonetic-script");
1722
+ requireEntityKind(rows, phoneticScriptEntityId, "phonetic-script");
1723
+ const phoneticScript = assembleEntityContent(rows, phoneticScriptEntityId);
1600
1724
  const audioScript = findComposedAudioScript(rows, phoneticScriptEntityId, "phonetic-script");
1601
- const segments = scriptSegments(audioScript);
1725
+ const segments = scriptSegments({
1726
+ ...audioScript,
1727
+ payload: phoneticScript.payload
1728
+ });
1602
1729
  return {
1603
1730
  phoneticScript,
1604
1731
  audioScript,
@@ -1641,16 +1768,15 @@ function requireEntityKind(rows, entityIdValue, entityKind) {
1641
1768
  return entity;
1642
1769
  }
1643
1770
  //#endregion
1644
- //#region ../medeo-dsl/src/ids.ts
1645
- function createEntityId(value) {
1646
- return createId(value, "EntityId");
1647
- }
1648
- function createRelationId(value) {
1649
- return createId(value, "RelationId");
1650
- }
1651
- function createId(value, label) {
1652
- if (value.length === 0 || value.trim() !== value) throw new Error(`${label} must be a non-empty trimmed string`);
1653
- return value;
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
+ };
1654
1780
  }
1655
1781
  //#endregion
1656
1782
  //#region ../medeo-dsl/src/invariants.ts
@@ -1671,6 +1797,19 @@ function validateMediaAssetIdentity(entity, index) {
1671
1797
  function validateEntityRelationSet(entityRefs, index, options) {
1672
1798
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
1673
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
+ }
1674
1813
  for (const entity of entities) {
1675
1814
  const current = entity.current();
1676
1815
  const entityIssues = validateEntity(entity, entityIds);
@@ -1691,8 +1830,8 @@ function validateEntityRelationSet(entityRefs, index, options) {
1691
1830
  issues.push(...validateClipPlacement(entity, index));
1692
1831
  }
1693
1832
  if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
1694
- if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index));
1695
- if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(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));
1696
1835
  issues.push(...validateSequenceComposition(entity));
1697
1836
  }
1698
1837
  issues.push(...validateClipAnchorCycles(entities, index));
@@ -1795,60 +1934,32 @@ function validateAXVideoAdmission(axVideo, index) {
1795
1934
  message: "AXVideo must have exactly one live AXVideo-Marker Relation"
1796
1935
  }];
1797
1936
  }
1798
- /**
1799
- * Variant composition is the content contract, not an optional label: a
1800
- * Caption must compose exactly one live AudioScript and its segment selection
1801
- * must name that script's own segments. Text is never stored on the variant,
1802
- * so a missing source means no valid caption content and fails validation.
1803
- */
1804
- function validateCaptionComposition(caption, index) {
1805
- return validateVariantComposition(caption, index, "caption", "caption-composition", "caption_composition_required");
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");
1806
1940
  }
1807
- /** Same required-composition contract for the pronunciation variant. */
1808
- function validatePhoneticComposition(phonetic, index) {
1809
- return validateVariantComposition(phonetic, index, "phonetic-script", "phonetic-script-composition", "phonetic_script_composition_required");
1941
+ function validatePhoneticComposition(phonetic, index, entities = []) {
1942
+ return validateVariantComposition(phonetic, index, entities, "phonetic-script");
1810
1943
  }
1811
- function validateVariantComposition(variant, index, variantKind, compositionKind, issueCode) {
1812
- const relations = [...index.relationsOf(variant)];
1944
+ function validateVariantComposition(variant, index, entities, kind) {
1813
1945
  const issues = [];
1814
- const physicalEdges = ofKind(relations, "physical-asset");
1815
- if (variantKind === "caption" && physicalEdges.length > 0) issues.push({
1946
+ if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
1816
1947
  code: "caption_physical_asset_forbidden",
1817
1948
  entityId: variant.entityId,
1818
1949
  message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
1819
1950
  });
1820
- const compositionEdges = ofKind(relations, compositionKind);
1821
- if (compositionEdges.length !== 1) {
1822
- issues.push({
1823
- code: issueCode,
1824
- entityId: variant.entityId,
1825
- message: `${variantKind} must have exactly one ${compositionKind} Relation to a live AudioScript`
1826
- });
1827
- return issues;
1828
- }
1829
- const script = compositionEdges[0]?.other(variant)?.deref()?.current();
1830
- if (script?.entityKind !== "audio-script") {
1831
- issues.push({
1832
- code: issueCode,
1833
- entityId: variant.entityId,
1834
- message: `${variantKind} ${compositionKind} Relation must resolve a live AudioScript`
1835
- });
1836
- return issues;
1837
- }
1838
- if (variantKind !== "caption") return issues;
1839
- const selections = variant.current().selections;
1840
- if (!Array.isArray(selections)) return issues;
1951
+ const rows = {
1952
+ entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
1953
+ relations: []
1954
+ };
1841
1955
  try {
1842
- selectAudioScriptSegments({
1843
- entityId: script.entityId,
1844
- entityKind: "audio-script",
1845
- payload: { segments: script.segments.map((segment) => ({ ...segment })) }
1846
- }, selections);
1956
+ if (kind === "caption") assembleCaptionContent(rows, variant.entityId);
1957
+ else assemblePhoneticScriptContent(rows, variant.entityId);
1847
1958
  } catch (error) {
1848
1959
  issues.push({
1849
- code: "caption_selection_invalid",
1960
+ code: kind === "caption" ? "caption_composition_required" : "phonetic_script_composition_required",
1850
1961
  entityId: variant.entityId,
1851
- message: error instanceof Error ? error.message : "Invalid Caption selection"
1962
+ message: error instanceof Error ? error.message : "Invalid variant composition"
1852
1963
  });
1853
1964
  }
1854
1965
  return issues;
@@ -2123,7 +2234,6 @@ function validateVoicePayload(value, problems) {
2123
2234
  if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
2124
2235
  }
2125
2236
  function validateCaptionPayload(value, problems) {
2126
- if (value.text !== void 0) problems.push("text is composed from the AudioScript; Caption must not store a text copy");
2127
2237
  const selections = value.selections;
2128
2238
  if (!Array.isArray(selections)) problems.push("selections must be a non-empty array of AudioScript segment selections");
2129
2239
  else {
@@ -2180,8 +2290,7 @@ function validateRange(value, path, required, problems) {
2180
2290
  }
2181
2291
  /** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
2182
2292
  function validatePhoneticScriptPayload(value, problems) {
2183
- if (value.text !== void 0) problems.push("text is composed from AudioScript; PhoneticScript must not store a text copy");
2184
- if (value.segments !== void 0) problems.push("segments carry the base text owned by the AudioScript; PhoneticScript stores pronunciation fields only");
2293
+ if (value.segments !== void 0) validateScriptPayload(value, problems);
2185
2294
  if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
2186
2295
  if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
2187
2296
  }
@@ -2227,6 +2336,7 @@ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths)
2227
2336
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityIdPaths(item, entityKind, `${parentPath}[${index}]`, ancestors, paths);
2228
2337
  else for (const [key, child] of Object.entries(value)) {
2229
2338
  const path = parentPath.length === 0 ? key : `${parentPath}.${key}`;
2339
+ if (parentPath.length === 0 && key === "baseEntityIds") continue;
2230
2340
  if (!(parentPath.length === 0 && key === "entityId") && !isOwnedLocalIdPath(entityKind, path) && isEntityIdFieldName(key)) paths.push(path);
2231
2341
  visitPeerEntityIdPaths(child, entityKind, path, ancestors, paths);
2232
2342
  }
@@ -2246,7 +2356,7 @@ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path,
2246
2356
  ancestors.add(value);
2247
2357
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
2248
2358
  else for (const [key, child] of Object.entries(value)) {
2249
- if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
2359
+ if (path.length === 0 && (key === "entityId" || key === "entityKind" || key === "baseEntityIds")) continue;
2250
2360
  visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
2251
2361
  }
2252
2362
  ancestors.delete(value);
@@ -2267,12 +2377,20 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2267
2377
  if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2268
2378
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
2269
2379
  if (entityKind === "caption" && (path.startsWith("style.") || /^selections\[\d+\]\.segmentId$/.test(path))) return true;
2270
- if (entityKind === "audio-script" && /^segments\[\d+\]\.(?:segmentId|text|language)$/.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;
2271
2385
  return false;
2272
2386
  }
2273
2387
  function isOwnedLocalIdPath(entityKind, path) {
2274
2388
  if (path === "lifecycle.actorId") return true;
2275
- if (entityKind === "audio-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;
2276
2394
  if (entityKind === "caption" && /^selections\[\d+\]\.segmentId$/.test(path)) return true;
2277
2395
  if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2278
2396
  if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
@@ -2329,35 +2447,6 @@ function ofKind(relations, kind) {
2329
2447
  return relations.filter((relation) => relation.kind === kind);
2330
2448
  }
2331
2449
  //#endregion
2332
- //#region ../medeo-dsl/src/json-values.ts
2333
- const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
2334
- function isJsonObject(value) {
2335
- return isJsonValue(value, /* @__PURE__ */ new Set()) && !Array.isArray(value) && value !== null;
2336
- }
2337
- function isJsonValue(value, ancestors) {
2338
- if (value === null || typeof value === "string" || typeof value === "boolean") return true;
2339
- if (typeof value === "number") return Number.isFinite(value);
2340
- if (typeof value !== "object") return false;
2341
- if (!Array.isArray(value) && !isPlainObject(value)) return false;
2342
- if (ancestors.has(value)) return false;
2343
- ancestors.add(value);
2344
- const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
2345
- ancestors.delete(value);
2346
- return valid;
2347
- }
2348
- /** Recognize an ordinary object from any VM realm without admitting class instances. */
2349
- function isPlainObject(value) {
2350
- try {
2351
- const prototype = Object.getPrototypeOf(value);
2352
- if (prototype === null) return true;
2353
- if (Object.getPrototypeOf(prototype) !== null) return false;
2354
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
2355
- return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
2356
- } catch {
2357
- return false;
2358
- }
2359
- }
2360
- //#endregion
2361
2450
  //#region ../medeo-dsl/src/relation-specs.ts
2362
2451
  const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2363
2452
  const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
@@ -2380,15 +2469,6 @@ const generatedRelationSpec = Object.freeze({
2380
2469
  validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
2381
2470
  validateMetadata: isEmptyMetadata
2382
2471
  });
2383
- /**
2384
- * Variant composition: the required content relation between a variant and its
2385
- * AudioScript. This is the assembly source for the variant's base text — never
2386
- * a downgradeable provenance label. A variant without exactly one live
2387
- * composition Relation has no valid content and fails validation.
2388
- */
2389
- const phoneticScriptCompositionRelationSpec = metadataSpec("phonetic-script-composition", "phonetic-script", "audio-script", isSegmentAlignmentMetadata);
2390
- /** See {@link phoneticScriptCompositionRelationSpec}; Caption is the display variant. */
2391
- const captionCompositionRelationSpec = metadataSpec("caption-composition", "caption", "audio-script", isSegmentAlignmentMetadata);
2392
2472
  const captionAlignmentRelationSpec = Object.freeze({
2393
2473
  kind: "caption-alignment",
2394
2474
  validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
@@ -2437,8 +2517,6 @@ const builtInRelationSpecs = Object.freeze([
2437
2517
  markerTimelineRelationSpec,
2438
2518
  physicalAssetRelationSpec,
2439
2519
  generatedRelationSpec,
2440
- phoneticScriptCompositionRelationSpec,
2441
- captionCompositionRelationSpec,
2442
2520
  captionAlignmentRelationSpec,
2443
2521
  clipAnchorRelationSpec,
2444
2522
  phoneticScriptRenderRelationSpec,
@@ -2481,9 +2559,6 @@ function isAssetBinding(value) {
2481
2559
  const orderingKey = /(order|ordinal|position|rank|index|z[_-]?index)/i;
2482
2560
  return Object.keys(value).every((key) => !orderingKey.test(key));
2483
2561
  }
2484
- function isSegmentAlignmentMetadata(value) {
2485
- return isJsonObject(value) && Object.hasOwn(value, "segmentAlignment");
2486
- }
2487
2562
  function isCaptionAlignmentMetadata(value) {
2488
2563
  return isJsonObject(value) && Object.hasOwn(value, "alignment");
2489
2564
  }
@@ -2669,7 +2744,13 @@ function decodeEntityRelationRows(rows, options) {
2669
2744
  const entitiesById = /* @__PURE__ */ new Map();
2670
2745
  const extensionEntityKinds = new Set(options.entityKinds ?? []);
2671
2746
  for (const row of rows.entities) {
2672
- 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
+ }
2673
2754
  if (entity == null) continue;
2674
2755
  const ref = createEntityRef(entity);
2675
2756
  if (entitiesById.has(ref.entityId)) {
@@ -2849,9 +2930,9 @@ var EntityProjectionGraph = class {
2849
2930
  timeline;
2850
2931
  constructor(rows) {
2851
2932
  this.rows = rows;
2852
- decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2853
- this.entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
2854
- 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");
2855
2936
  if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
2856
2937
  this.timeline = roots[0];
2857
2938
  }
@@ -3539,4 +3620,4 @@ function createPlainMemoryAdapter(document, options) {
3539
3620
  return new PlainMemoryAdapter(document, options);
3540
3621
  }
3541
3622
  //#endregion
3542
- export { ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as $, VideoDocumentValidationError as A, cascadeAfterVideoClipChanges as B, hasSequence as C, derivePositionFromAbs as D, buildSpeechHostMap as E, LANE_KINDS_IN_STACK_ORDER as F, resolveAllSpeechOverlaps as G, fillMainTrackTimeGaps as H, ensureLaneTrack as I, TIMELINE_SKELETON_DURATION_MS as J, resolveSpeechOverlapByShiftingVideos as K, findLaneTrack as L, validateVideoDocument as M, partUnionSchema as N, fromVideoDocument as O, videoDocumentSchema as P, DEFAULT_UNIT_TIME_MS as Q, laneTrackId as R, selectAudioScriptSegments as S, buildInitialVideoDocument as T, reassignSpeechesToVideoClipsByTime as U, arrangeMainTrackSeamlessly as V, recalculateTimelineDuration as W, partDurationMs as X, isEmptyVideoClip as Y, safeDurationMs as Z, ScriptCompositionError as _, applyEntityTimelineProjection as a, videoDocumentMirrorSchema as at, assembleScriptText as b, writeVideoDocumentToDraft as c, resolveEntityTimelineLayout as d, VIDEO_DOCUMENT_SCHEMA_VERSION as et, EntityTimelineProjectionError as f, createRelationId as g, createEntityId as h, MirrorVideoDocumentAdapter as i, recordEntries as it, assertValidVideoDocument as j, toVideoDocument as k, readVideoDocumentFromDraft as l, isJsonObject as m, createPlainMemoryAdapter as n, speedOf as nt, createMirrorVideoDocument as o, base64ToBytes as ot, decodeEntityRelationRows as p, syncAggregatedClipsTimePosition as q, generatePartId as r, partUnionToDraft as rt, createMirrorVideoDocumentAdapter as s, bytesToBase64 as st, PlainMemoryAdapter as t, effectiveVideoClipDurationMs as tt, projectEntityTimeline as u, assembleCaptionContent as v, isMediaAssetVariantKind as w, findComposedAudioScript as x, assemblePhoneticScriptContent as y, solveVideoDocument 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 };
@@ -914,7 +914,7 @@ interface CaptionTextRange extends JsonObject {
914
914
  * One ordered entry of the Caption's segment selection. `segmentId` quotes the
915
915
  * composed AudioScript's own stable segment identity — a local id quoted by the
916
916
  * variant, never a peer Entity reference. Text itself is never copied here;
917
- * complete Caption content is assembled along the caption-composition Relation.
917
+ * complete Caption content is assembled through its direct baseEntityIds.
918
918
  * The optional `textRange` narrows one Segment to an intra-Segment sub-span
919
919
  * (intra-segment re-segmentation); without it the whole Segment text is selected.
920
920
  */
@@ -924,7 +924,7 @@ type CaptionSegmentSelection = JsonObject & {
924
924
  };
925
925
  //#endregion
926
926
  //#region ../medeo-dsl/src/relations.d.ts
927
- type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'phonetic-script-composition' | 'caption-composition' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'audio-script-source' | 'audio-script-marker';
927
+ type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'caption-alignment' | 'clip-anchor' | 'phonetic-script-render' | 'audio-script-source' | 'audio-script-marker';
928
928
  type RelationKind = KnownRelationKind | (string & {});
929
929
  type RelationTrace = JsonObject;
930
930
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as index_d_exports } from "./index-CMa6ZCtX.js";
2
- import { $ as VoiceDescriptor, A as generatePartId, At as effectiveVideoClipDurationMs, B as ResolvedEntityClipPlacement, C as PlannedSemanticOpKind, Ct as VideoClipPart, D as SnapshotReadable, Dt as VideoDocumentValidationIssueCode, E as SchemaValidator, Et as VideoDocumentValidationIssue, F as videoDocumentMirrorSchema, Ft as PartAggregation, G as EntityTimelineProjectionError, H as ResolvedEntityTrack, I as VideoDocumentValidationError, It as SpeedShift, J as CaptionSegmentSelection, K as EntityRelationRows, L as assertValidVideoDocument, Lt as Timeline$1, M as TrackItemDraft, Mt as Attachment, N as VideoDocumentDraft, Nt as CaptionPart$1, O as ValidationError, Ot as VideoDraft, P as VideoDocumentMirrorSchema, Pt as CaptionStyle, Q as SequenceRange, R as validateVideoDocument, Rt as Track$1, S as ImplementedSemanticOpKind, St as VIDEO_DOCUMENT_SCHEMA_VERSION, T as isImplementedSemanticOpKind, Tt as VideoDocumentSchemaVersion, U as resolveEntityTimelineLayout, V as ResolvedEntityTimelineLayout, W as projectEntityTimeline, X as ScriptTextSegment, Y as CaptionStyleFields, Z as SequenceDuration, _ as SemanticEditor, _t as SpeechPart, a as PlainMemoryAdapter, at as DerivedItemPosition, b as TransactAudit, bt as TrackItem, c as MirrorVideoDocumentAdapter, ct as derivePositionFromAbs, d as createMirrorVideoDocument, dt as BgmPart, et as JsonObject, f as createMirrorVideoDocumentAdapter, ft as CaptionPart, g as SemanticDocumentAdapter, gt as PartUnion, h as OpActor, ht as PartKind, i as JournalEntry, it as buildInitialVideoDocument, j as TrackDraft, jt as speedOf, k as PartIdFactory, kt as VideoDraftPartUnion, l as MirrorVideoDocumentOptions, lt as fromVideoDocument, m as CommitOptions, mt as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, n as videoDocumentSchema, nt as EntityId, o as PlainMemoryAdapterOptions, ot as SpeechHostMap, p as writeVideoDocumentToDraft, pt as DEFAULT_UNIT_TIME_MS, q as EntityRow, r as readVideoDocumentFromDraft, rt as InitialDocumentFacts, s as createPlainMemoryAdapter, st as buildSpeechHostMap, t as partUnionSchema, tt as JsonValue, u as applyEntityTimelineProjection, ut as toVideoDocument, v as SemanticOpInput, vt as Timeline, w as SemanticOpKind, wt as VideoDocument, x as IMPLEMENTED_SEMANTIC_OP_KINDS, xt as TrackItemTimePosition, y as SemanticOpName, yt as Track, z as EntityTimelineTrackRole, zt as TrackItem$1 } from "./index-PYtGeXNk.js";
2
+ import { $ as VoiceDescriptor, A as generatePartId, At as effectiveVideoClipDurationMs, B as ResolvedEntityClipPlacement, C as PlannedSemanticOpKind, Ct as VideoClipPart, D as SnapshotReadable, Dt as VideoDocumentValidationIssueCode, E as SchemaValidator, Et as VideoDocumentValidationIssue, F as videoDocumentMirrorSchema, Ft as PartAggregation, G as EntityTimelineProjectionError, H as ResolvedEntityTrack, I as VideoDocumentValidationError, It as SpeedShift, J as CaptionSegmentSelection, K as EntityRelationRows, L as assertValidVideoDocument, Lt as Timeline$1, M as TrackItemDraft, Mt as Attachment, N as VideoDocumentDraft, Nt as CaptionPart$1, O as ValidationError, Ot as VideoDraft, P as VideoDocumentMirrorSchema, Pt as CaptionStyle, Q as SequenceRange, R as validateVideoDocument, Rt as Track$1, S as ImplementedSemanticOpKind, St as VIDEO_DOCUMENT_SCHEMA_VERSION, T as isImplementedSemanticOpKind, Tt as VideoDocumentSchemaVersion, U as resolveEntityTimelineLayout, V as ResolvedEntityTimelineLayout, W as projectEntityTimeline, X as ScriptTextSegment, Y as CaptionStyleFields, Z as SequenceDuration, _ as SemanticEditor, _t as SpeechPart, a as PlainMemoryAdapter, at as DerivedItemPosition, b as TransactAudit, bt as TrackItem, c as MirrorVideoDocumentAdapter, ct as derivePositionFromAbs, d as createMirrorVideoDocument, dt as BgmPart, et as JsonObject, f as createMirrorVideoDocumentAdapter, ft as CaptionPart, g as SemanticDocumentAdapter, gt as PartUnion, h as OpActor, ht as PartKind, i as JournalEntry, it as buildInitialVideoDocument, j as TrackDraft, jt as speedOf, k as PartIdFactory, kt as VideoDraftPartUnion, l as MirrorVideoDocumentOptions, lt as fromVideoDocument, m as CommitOptions, mt as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, n as videoDocumentSchema, nt as EntityId, o as PlainMemoryAdapterOptions, ot as SpeechHostMap, p as writeVideoDocumentToDraft, pt as DEFAULT_UNIT_TIME_MS, q as EntityRow, r as readVideoDocumentFromDraft, rt as InitialDocumentFacts, s as createPlainMemoryAdapter, st as buildSpeechHostMap, t as partUnionSchema, tt as JsonValue, u as applyEntityTimelineProjection, ut as toVideoDocument, v as SemanticOpInput, vt as Timeline, w as SemanticOpKind, wt as VideoDocument, x as IMPLEMENTED_SEMANTIC_OP_KINDS, xt as TrackItemTimePosition, y as SemanticOpName, yt as Track, z as EntityTimelineTrackRole, zt as TrackItem$1 } from "./index-Dxxu0Mi1.js";
3
3
  import { PeerID } from "loro-crdt";
4
4
  import { DocState } from "@mengine/sync";
5
5
  import { BaseDocStorage, Connection, DocDiff, DocPushReceipt, DocSnapshotRecord, DocStorage, DocStorageOptions, DocUpdate, DocUpdateRecord } from "@mengine/storage";
@@ -179,13 +179,8 @@ interface MengineEventStreamOptions {
179
179
  declare function readMengineEventStream(options: MengineEventStreamOptions): Promise<void>;
180
180
  //#endregion
181
181
  //#region ../medeo-dsl/src/composition.d.ts
182
- type ScriptCompositionIssueCode = 'composition_missing' | 'composition_ambiguous' | 'composition_dangling' | 'unknown_segment' | 'empty_selection' | 'invalid_script' | 'invalid_selection';
183
- /**
184
- * Raised when a variant's complete content cannot be assembled from its
185
- * required composition Relation. Assembly never falls back to variant-local
186
- * text: a missing, ambiguous, or dangling source is an error, not an empty
187
- * read.
188
- */
182
+ type ScriptCompositionIssueCode = 'field_conflict' | 'composition_cycle' | 'invalid_bases' | 'composition_missing' | 'composition_ambiguous' | 'composition_dangling' | 'unknown_segment' | 'empty_selection' | 'invalid_script' | 'invalid_selection';
183
+ /** Assembly fails explicitly when declared bases cannot provide valid, unambiguous content. */
189
184
  declare class ScriptCompositionError extends Error {
190
185
  readonly code: ScriptCompositionIssueCode;
191
186
  readonly entityId: EntityId;
@@ -207,19 +202,13 @@ interface AssembledPhoneticScriptContent {
207
202
  /** Complete base text owned by the AudioScript. */
208
203
  readonly text: string;
209
204
  }
210
- /**
211
- * Resolve the one AudioScript a variant composes. Relations are queried from
212
- * both endpoints, so persisted endpoint positions do not matter.
213
- */
205
+ /** Validate all base providers before applying explicit own fields; ordering cannot resolve ambiguity. */
206
+ declare function assembleEntityContent(rows: EntityRelationRows, entityId: EntityId): EntityRow;
207
+ /** Find the actual AudioScript text owner through direct variant bases. */
214
208
  declare function findComposedAudioScript(rows: EntityRelationRows, variantEntityId: EntityId, variantKind: 'caption' | 'phonetic-script'): EntityRow<'audio-script'>;
215
209
  /** Join the whole AudioScript text in segment order. */
216
210
  declare function assembleScriptText(script: EntityRow<'audio-script'>): string;
217
- /**
218
- * Assemble the complete Caption content from its AudioScript: the selected
219
- * segments in selection order, joined as display text. The Caption payload
220
- * never carries text, so a broken composition fails here instead of silently
221
- * rendering an empty caption.
222
- */
211
+ /** Assemble selected text after base-field validation and explicit variant overrides. */
223
212
  declare function assembleCaptionContent(rows: EntityRelationRows, captionEntityId: EntityId): AssembledCaptionContent;
224
213
  /**
225
214
  * Assemble the complete Phonetic Script content for TTS: the composed
@@ -339,6 +328,8 @@ interface DeleteClipTreeInput {
339
328
  interface VoiceoverCaptionFact {
340
329
  /** Stable placed caption identity supplied by the materialized side effect. */
341
330
  readonly captionClipEntityId: string;
331
+ /** Directly held bases; includes the AudioScript used by the Voice. */
332
+ readonly baseEntityIds: readonly string[];
342
333
  /** Ordered selection of AudioScript segments; caption text is never passed inline. */
343
334
  readonly selections: readonly CaptionSegmentSelection[];
344
335
  readonly startMs: number;
@@ -427,7 +418,7 @@ interface VoiceoverTakeResult {
427
418
  readonly voiceEntityId: string;
428
419
  /** The pronunciation variant the Voice was rendered from. */
429
420
  readonly phoneticScriptEntityId: string;
430
- /** The base-text owner resolved along the phonetic composition Relation. */
421
+ /** The base-text owner resolved from the PhoneticScript baseEntityIds. */
431
422
  readonly audioScriptEntityId: string;
432
423
  readonly captionClipEntityIds: readonly string[];
433
424
  }
@@ -460,8 +451,8 @@ interface InsertCaptionClipInput {
460
451
  readonly timelineEntityId: string;
461
452
  /** Stable placed caption identity, distinct from the Caption content identity. */
462
453
  readonly captionClipEntityId?: string;
463
- /** Existing base-text owner; the Caption composes it instead of storing text. */
464
- readonly audioScriptEntityId: string;
454
+ /** Existing bases composed by this variant; includes an AudioScript text owner. */
455
+ readonly baseEntityIds: readonly string[];
465
456
  /** Ordered selection of the AudioScript segments this Caption displays. */
466
457
  readonly selections: readonly CaptionSegmentSelection[];
467
458
  /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */
@@ -549,8 +540,8 @@ declare class EntityTimelineEditor {
549
540
  patchCaptionStyle(input: PatchCaptionStyleInput): void;
550
541
  /**
551
542
  * Place a Caption as a composed variant of an existing AudioScript: the
552
- * entity stores only its selection, style, and inherent cue-bounded
553
- * Sequence — never text. The Clip and its display Marker describe this use;
543
+ * entity stores baseEntityIds, selection, optional style, and inherent cue-bounded
544
+ * Sequence. This helper does not copy base text. The Clip and its display Marker describe this use;
554
545
  * moving or stretching the returned Clip later changes the Clip/Marker,
555
546
  * never the Caption entity or the AudioScript.
556
547
  */
@@ -563,7 +554,7 @@ declare class EntityTimelineEditor {
563
554
  private ensurePhoneticRender;
564
555
  private reconcileVoiceoverCaptions;
565
556
  private ensureRoleTrackForClip;
566
- private replaceCaptionSemanticRelations;
557
+ private replaceCaptionAlignment;
567
558
  /**
568
559
  * Remove one placed Caption use. The composed AudioScript outlives the
569
560
  * caption: it may anchor further captions and remains the text owner.
@@ -651,6 +642,17 @@ declare function migrateLegacyTimelineToEntities(legacy: VideoDocument, assetFac
651
642
  /** Server gate: a cutover may change representation, never the existing render facts. */
652
643
  declare function assertEntityProjectionPreservesLegacy(legacy: VideoDocument, projection: VideoDocument, rows: EntityRelationRows): void;
653
644
  //#endregion
645
+ //#region src/entity-editor/caption-content.d.ts
646
+ interface CaptionContentInput {
647
+ readonly contentEntityId: string;
648
+ readonly audioScriptEntityId: string;
649
+ readonly text: string;
650
+ readonly selections?: readonly CaptionSegmentSelection[];
651
+ readonly durationMs: number;
652
+ }
653
+ /** Author Caption content from semantic intent; callers never reconcile variant bases or assembled fields. */
654
+ declare function prepareCaptionContent(rows: EntityRelationRows, input: CaptionContentInput): EntityRow<'caption'>;
655
+ //#endregion
654
656
  //#region src/editor/journal.d.ts
655
657
  /**
656
658
  * Wire a plain-memory adapter + editor that share a recording id factory, so
@@ -1555,4 +1557,4 @@ declare function hostForAbsMs(ranges: MainClipRange[], absMs: number): MainClipR
1555
1557
  */
1556
1558
  declare function relativePositionForAbs(ranges: MainClipRange[], absMs: number): TrackItemTimePosition;
1557
1559
  //#endregion
1558
- export { type Aggregation, type AssembledCaptionContent, type AssembledPhoneticScriptContent, type Attachment, type AudioMediaAssetFact, type BgmPart, type CaptionPart, type CaptionSegmentSelection, type CaptionStyle, type ClipEntityId, type ClipPlacement, type CommitOptions, DEFAULT_UNIT_TIME_MS, type DeleteBgmInput, type DeleteClipInput, type DeleteClipTreeInput, type DeleteVoiceoverInput, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, type EditorFoundation, type EntityGraphCommitOptions, EntityGraphHttpClient, type EntityGraphState, EntityTimelineEditor, type EntityTimelineIdFactory, EntityTimelineProjectionError, type EntityTimelineTrackRole, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImageMediaAssetFact, type ImplementedSemanticOpKind, type ImportedMediaAsset, type InitialDocumentFacts, type InsertCaptionClipInput, type InsertClipInput, type InsertMediaClipInput, type InsertMediaClipsInput, type InsertPlacedClipInput, type JournalEntry, LANE_KINDS_IN_STACK_ORDER, type LaneKind, type LinearClipSpeed, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, type MediaAssetFact, type MediaClipInsertion, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUpdateMeta, MirrorVideoDocumentAdapter, type MirrorVideoDocumentOptions, type MoveClipInput, type MoveClipsToStartsInput, type MoveSequentialClipsInput, type MoveVoiceoverInput, type OpActor, type PartAggregation, type PartIdFactory, type PartKind, type PartUnion, type PatchCaptionStyleInput, PlainMemoryAdapter, type PlainMemoryAdapterOptions, type PlannedSemanticOpKind, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, type ReplaceClipContentInput, type ReplaceMediaClipInput, type ReplaceSequentialClipsInput, type ReplacementMediaClipInput, type ResolvedEntityClipPlacement, type ResolvedEntityTimelineLayout, type ResolvedEntityTrack, SchemaValidator, ScriptCompositionError, type SemanticDocumentAdapter, SemanticEditor, type SemanticOpInput, type SemanticOpKind, type SemanticOpName, type SequentialClipAnchor, type SetBgmInput, type SetCaptionVisibilityInput, type SetClipPlacementInput, type SetClipSpeedInput, type SetClipVolumeInput, type SnapshotReadable, type SolvedVideoDocument, type SpeechHostMap, type SpeechPart, type SpeedShift, TIMELINE_SKELETON_DURATION_MS, type Timeline, type TimelineDoc, type TimelineItem, type Track, type TrackDraft, type TrackItem, type TrackItemDraft, type TrackItemTimePosition, type TransactAudit, type TrimClipInput, type UpdateClipInput, type UpdateClipMarkerInput, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, type VideoDocumentSchemaVersion, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type VideoMediaAssetFact, type VisualMediaAssetFact, type VoiceMediaAssetFact, type VoiceoverCaptionFact, type VoiceoverTakeInput, type VoiceoverTakeResult, type WaitForServerAckOptions, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
1560
+ export { type Aggregation, type AssembledCaptionContent, type AssembledPhoneticScriptContent, type Attachment, type AudioMediaAssetFact, type BgmPart, type CaptionContentInput, type CaptionPart, type CaptionSegmentSelection, type CaptionStyle, type ClipEntityId, type ClipPlacement, type CommitOptions, DEFAULT_UNIT_TIME_MS, type DeleteBgmInput, type DeleteClipInput, type DeleteClipTreeInput, type DeleteVoiceoverInput, type DerivedItemPosition, type DocStorageLike, type DocVersionMark, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, type EditorFoundation, type EntityGraphCommitOptions, EntityGraphHttpClient, type EntityGraphState, EntityTimelineEditor, type EntityTimelineIdFactory, EntityTimelineProjectionError, type EntityTimelineTrackRole, IMPLEMENTED_SEMANTIC_OP_KINDS, type ImageMediaAssetFact, type ImplementedSemanticOpKind, type ImportedMediaAsset, type InitialDocumentFacts, type InsertCaptionClipInput, type InsertClipInput, type InsertMediaClipInput, type InsertMediaClipsInput, type InsertPlacedClipInput, type JournalEntry, LANE_KINDS_IN_STACK_ORDER, type LaneKind, type LinearClipSpeed, type MainClipRange, type MakeEmptyPart, ManualSyncDoc, type ManualSyncDocOptions, MedeoHttpDocStorage, type MedeoHttpDocStorageOptions, type MediaAssetFact, type MediaClipInsertion, MemoryDocStorage, MengineAckFailedError, type MengineAuditEntry, type MengineAuditResponse, MengineDocSession, type MengineDocSessionOptions, type MengineDocSessionUpdateEvent, type MengineDocumentVersion, type MengineEventStreamOptions, MengineHttpClient, type MengineHttpClientOptions, MengineHttpRequestError, MenginePushRejectedError, type MenginePushResponse, type MenginePushUpdateResponse, type MengineRejectedResponse, type MengineSnapshotResponse, type MengineSseUpdateEvent, type MengineSyncResponse, type MengineUpdateMeta, MirrorVideoDocumentAdapter, type MirrorVideoDocumentOptions, type MoveClipInput, type MoveClipsToStartsInput, type MoveSequentialClipsInput, type MoveVoiceoverInput, type OpActor, type PartAggregation, type PartIdFactory, type PartKind, type PartUnion, type PatchCaptionStyleInput, PlainMemoryAdapter, type PlainMemoryAdapterOptions, type PlannedSemanticOpKind, type PullFailureReason, type PullResult, type PushOutcome, type PushOutcomeKind, type PushResult, type PushResultKind, type ReplaceClipContentInput, type ReplaceMediaClipInput, type ReplaceSequentialClipsInput, type ReplacementMediaClipInput, type ResolvedEntityClipPlacement, type ResolvedEntityTimelineLayout, type ResolvedEntityTrack, SchemaValidator, ScriptCompositionError, type SemanticDocumentAdapter, SemanticEditor, type SemanticOpInput, type SemanticOpKind, type SemanticOpName, type SequentialClipAnchor, type SetBgmInput, type SetCaptionVisibilityInput, type SetClipPlacementInput, type SetClipSpeedInput, type SetClipVolumeInput, type SnapshotReadable, type SolvedVideoDocument, type SpeechHostMap, type SpeechPart, type SpeedShift, TIMELINE_SKELETON_DURATION_MS, type Timeline, type TimelineDoc, type TimelineItem, type Track, type TrackDraft, type TrackItem, type TrackItemDraft, type TrackItemTimePosition, type TransactAudit, type TrimClipInput, type UpdateClipInput, type UpdateClipMarkerInput, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, type VideoClipPart, type VideoDocument, type VideoDocumentDraft, type VideoDocumentMirrorSchema, type VideoDocumentSchemaVersion, VideoDocumentValidationError, type VideoDocumentValidationIssue, type VideoDocumentValidationIssueCode, type VideoDraft, type CaptionPart$1 as VideoDraftCaptionPart, type VideoDraftPartUnion, type Timeline$1 as VideoDraftTimeline, type Track$1 as VideoDraftTrack, type TrackItem$1 as VideoDraftTrackItem, type VideoMediaAssetFact, type VisualMediaAssetFact, type VoiceMediaAssetFact, type VoiceoverCaptionFact, type VoiceoverTakeInput, type VoiceoverTakeResult, type WaitForServerAckOptions, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, prepareCaptionContent, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, index_d_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, A as VideoDocumentValidationError, B as cascadeAfterVideoClipChanges, C as hasSequence, D as derivePositionFromAbs, E as buildSpeechHostMap, F as LANE_KINDS_IN_STACK_ORDER, G as resolveAllSpeechOverlaps, H as fillMainTrackTimeGaps, I as ensureLaneTrack, J as TIMELINE_SKELETON_DURATION_MS, K as resolveSpeechOverlapByShiftingVideos, L as findLaneTrack, M as validateVideoDocument, N as partUnionSchema, O as fromVideoDocument, P as videoDocumentSchema, Q as DEFAULT_UNIT_TIME_MS, R as laneTrackId, S as selectAudioScriptSegments, T as buildInitialVideoDocument, U as reassignSpeechesToVideoClipsByTime, V as arrangeMainTrackSeamlessly, W as recalculateTimelineDuration, X as partDurationMs, Y as isEmptyVideoClip, Z as safeDurationMs, _ as ScriptCompositionError, a as applyEntityTimelineProjection, at as videoDocumentMirrorSchema, b as assembleScriptText, c as writeVideoDocumentToDraft, d as resolveEntityTimelineLayout, et as VIDEO_DOCUMENT_SCHEMA_VERSION, f as EntityTimelineProjectionError, g as createRelationId, h as createEntityId, i as MirrorVideoDocumentAdapter, it as recordEntries, j as assertValidVideoDocument, k as toVideoDocument, l as readVideoDocumentFromDraft, m as isJsonObject, n as createPlainMemoryAdapter, nt as speedOf, o as createMirrorVideoDocument, ot as base64ToBytes, p as decodeEntityRelationRows, q as syncAggregatedClipsTimePosition, r as generatePartId, rt as partUnionToDraft, s as createMirrorVideoDocumentAdapter, st as bytesToBase64, t as PlainMemoryAdapter, tt as effectiveVideoClipDurationMs, u as projectEntityTimeline, v as assembleCaptionContent, w as isMediaAssetVariantKind, x as findComposedAudioScript, y as assemblePhoneticScriptContent, z as solveVideoDocument } from "./document-Bf200CmD.js";
1
+ import { $ as isEmptyVideoClip, A as buildSpeechHostMap, B as ensureLaneTrack, C as variantBaseEntityIds, D as hasSequence, E as createRelationId, F as assertValidVideoDocument, G as arrangeMainTrackSeamlessly, H as laneTrackId, I as validateVideoDocument, J as recalculateTimelineDuration, K as fillMainTrackTimeGaps, L as partUnionSchema, M as fromVideoDocument, N as toVideoDocument, O as isMediaAssetVariantKind, P as VideoDocumentValidationError, Q as TIMELINE_SKELETON_DURATION_MS, R as videoDocumentSchema, S as updateEntityFields, T as createEntityId, U as solveVideoDocument, V as findLaneTrack, W as cascadeAfterVideoClipChanges, X as resolveSpeechOverlapByShiftingVideos, Y as resolveAllSpeechOverlaps, Z as syncAggregatedClipsTimePosition, _ as assembleEntityContent, a as applyEntityTimelineProjection, at as effectiveVideoClipDurationMs, b as findComposedAudioScript, c as writeVideoDocumentToDraft, ct as recordEntries, d as EntityProjectionGraph, dt as bytesToBase64, et as partDurationMs, f as resolveEntityTimelineLayout, g as assembleCaptionContent, h as ScriptCompositionError, i as MirrorVideoDocumentAdapter, it as VIDEO_DOCUMENT_SCHEMA_VERSION, j as derivePositionFromAbs, k as buildInitialVideoDocument, l as readVideoDocumentFromDraft, lt as videoDocumentMirrorSchema, m as decodeEntityRelationRows, n as createPlainMemoryAdapter, nt as DEFAULT_UNIT_TIME_MS, o as createMirrorVideoDocument, ot as speedOf, p as EntityTimelineProjectionError, q as reassignSpeechesToVideoClipsByTime, r as generatePartId, rt as ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, s as createMirrorVideoDocumentAdapter, st as partUnionToDraft, t as PlainMemoryAdapter, tt as safeDurationMs, u as projectEntityTimeline, ut as base64ToBytes, v as assemblePhoneticScriptContent, w as isJsonObject, x as selectAudioScriptSegments, y as assembleScriptText, z as LANE_KINDS_IN_STACK_ORDER } from "./document-BLYcbv8E.js";
2
2
  import { addSpeechesInputSchema, addVideoClipsInputSchema, adjustBgmVolumeInputSchema, adjustSpeechVolumeInputSchema, adjustVideoClipDurationInputSchema, adjustVideoClipVolumeInputSchema, changeSpeechScriptInputSchema, changeSpeechVoiceInputSchema, deleteBgmInputSchema, deleteSpeechesInputSchema, deleteVideoClipsInputSchema, moveSpeechesInputSchema, moveVideoClipsByAnchorInputSchema, moveVideoClipsInputSchema, replaceVideoClipContentInputSchema, replaceVideoClipSequenceInputSchema, setBgmInputSchema, setCaptionStyleInputSchema, setCaptionVisibilityInputSchema, setVideoClipSpeedShiftInputSchema, t as schemas_exports } from "./schemas.js";
3
3
  import { LoroDoc, VersionVector } from "loro-crdt";
4
4
  import { ClientServerSynchronizer, DocManager } from "@mengine/sync";
@@ -616,8 +616,12 @@ function requireComposedAudioScript(rows, phoneticScriptEntityId) {
616
616
  function captionSelections(selections) {
617
617
  return structuredClone(selections);
618
618
  }
619
- function assertSelectionNamesScriptSegments(rows, audioScriptEntityId, selections) {
620
- selectAudioScriptSegments(requireEntityKind(rows, audioScriptEntityId, "audio-script"), selections);
619
+ function captionBaseIds(ids) {
620
+ if (!Array.isArray(ids) || ids.length === 0) throw new Error("Caption requires non-empty baseEntityIds");
621
+ return ids.map(createEntityId);
622
+ }
623
+ function assertCaptionScript(rows, captionId, scriptId) {
624
+ if (assembleCaptionContent(rows, captionId).audioScript.entityId !== scriptId) throw new Error("Voiceover Caption must compose the same AudioScript as the PhoneticScript");
621
625
  }
622
626
  const roleOrder = {
623
627
  caption: 0,
@@ -1281,28 +1285,23 @@ var EntityTimelineEditor = class {
1281
1285
  const track = findRoleTrack(draft, input.timelineEntityId, "caption");
1282
1286
  if (track === void 0) return;
1283
1287
  const patch = cloneJsonObject(input.style, "Caption style");
1288
+ const assembled = new EntityProjectionGraph(draft);
1284
1289
  for (const clip of clipsOnTrack(draft, track.entityId)) {
1285
1290
  const topology = requireClipTopology(draft, clip.entityId);
1286
1291
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1287
- const prior = caption.payload.style;
1292
+ const prior = assembled.entity(caption.entityId).payload.style;
1288
1293
  const style = isJsonObject(prior) ? prior : {};
1289
- replaceEntity(draft, caption.entityId, {
1290
- ...caption,
1291
- payload: {
1292
- ...caption.payload,
1293
- style: {
1294
- ...style,
1295
- ...patch
1296
- }
1297
- }
1298
- });
1294
+ for (const updated of updateEntityFields(draft, caption.entityId, { style: {
1295
+ ...style,
1296
+ ...patch
1297
+ } })) replaceEntity(draft, updated.entityId, updated);
1299
1298
  }
1300
1299
  });
1301
1300
  }
1302
1301
  /**
1303
1302
  * Place a Caption as a composed variant of an existing AudioScript: the
1304
- * entity stores only its selection, style, and inherent cue-bounded
1305
- * Sequence — never text. The Clip and its display Marker describe this use;
1303
+ * entity stores baseEntityIds, selection, optional style, and inherent cue-bounded
1304
+ * Sequence. This helper does not copy base text. The Clip and its display Marker describe this use;
1306
1305
  * moving or stretching the returned Clip later changes the Clip/Marker,
1307
1306
  * never the Caption entity or the AudioScript.
1308
1307
  */
@@ -1310,8 +1309,6 @@ var EntityTimelineEditor = class {
1310
1309
  return this.transact((draft) => {
1311
1310
  const durationMs = finitePositive(input.durationMs, "Caption durationMs");
1312
1311
  const selections = captionSelections(input.selections);
1313
- const script = requireEntityKind(draft, input.audioScriptEntityId, "audio-script");
1314
- assertSelectionNamesScriptSegments(draft, script.entityId, selections);
1315
1312
  const track = this.ensureRoleTrack(draft, input.timelineEntityId, "caption");
1316
1313
  const content = {
1317
1314
  entityId: this.mintEntityId(draft),
@@ -1324,12 +1321,13 @@ var EntityTimelineEditor = class {
1324
1321
  },
1325
1322
  sampling: "native",
1326
1323
  coordinateSpace: "milliseconds",
1324
+ baseEntityIds: captionBaseIds(input.baseEntityIds),
1327
1325
  selections,
1328
1326
  ...input.style === void 0 ? {} : { style: cloneJsonObject(input.style, "Caption style") }
1329
1327
  }
1330
1328
  };
1331
1329
  draft.entities.push(content);
1332
- draft.relations.push(this.createRelation(draft, "caption-composition", content.entityId, script.entityId, { segmentAlignment: [] }));
1330
+ assembleCaptionContent(draft, content.entityId);
1333
1331
  return this.insertPlacedClipIntoDraft(draft, {
1334
1332
  trackEntityId: track.entityId,
1335
1333
  contentEntityId: content.entityId,
@@ -1434,7 +1432,7 @@ var EntityTimelineEditor = class {
1434
1432
  const startMs = finiteNonNegative(caption.startMs, "Caption startMs");
1435
1433
  const durationMs = finitePositive(caption.durationMs, "Caption durationMs");
1436
1434
  const selections = captionSelections(caption.selections);
1437
- assertSelectionNamesScriptSegments(draft, audioScriptEntityId, selections);
1435
+ const baseEntityIds = captionBaseIds(caption.baseEntityIds);
1438
1436
  if (draft.entities.find((entity) => entity.entityId === caption.captionClipEntityId) === void 0) {
1439
1437
  const content = {
1440
1438
  entityId: this.mintEntityId(draft),
@@ -1447,12 +1445,14 @@ var EntityTimelineEditor = class {
1447
1445
  },
1448
1446
  sampling: "native",
1449
1447
  coordinateSpace: "milliseconds",
1448
+ baseEntityIds,
1450
1449
  selections,
1451
1450
  ...caption.style === void 0 ? {} : { style: cloneJsonObject(caption.style, "Caption style") }
1452
1451
  }
1453
1452
  };
1454
1453
  draft.entities.push(content);
1455
- draft.relations.push(this.createRelation(draft, "caption-composition", content.entityId, audioScriptEntityId, { segmentAlignment: [] }), this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
1454
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1455
+ draft.relations.push(this.createRelation(draft, "caption-alignment", content.entityId, voiceEntityId, { alignment: {
1456
1456
  start: startMs,
1457
1457
  end: startMs + durationMs
1458
1458
  } }));
@@ -1487,10 +1487,12 @@ var EntityTimelineEditor = class {
1487
1487
  ...content,
1488
1488
  payload: {
1489
1489
  ...content.payload,
1490
+ baseEntityIds,
1490
1491
  selections,
1491
1492
  ...Object.keys(style).length === 0 ? {} : { style }
1492
1493
  }
1493
1494
  });
1495
+ assertCaptionScript(draft, content.entityId, audioScriptEntityId);
1494
1496
  this.applyClipPlacement(draft, topology.clip.entityId, {
1495
1497
  kind: "anchored",
1496
1498
  hostClipEntityId: voiceoverClipEntityId,
@@ -1507,7 +1509,7 @@ var EntityTimelineEditor = class {
1507
1509
  }
1508
1510
  }
1509
1511
  });
1510
- this.replaceCaptionSemanticRelations(draft, content.entityId, voiceEntityId, audioScriptEntityId, startMs, durationMs);
1512
+ this.replaceCaptionAlignment(draft, content.entityId, voiceEntityId, startMs, durationMs);
1511
1513
  }
1512
1514
  }
1513
1515
  ensureRoleTrackForClip(draft, clipEntityId, role) {
@@ -1516,13 +1518,13 @@ var EntityTimelineEditor = class {
1516
1518
  const timeline = requireEntityKind(draft, otherEndpoint(requireOneRelation(draft, track.entityId, "timeline-track"), track.entityId), "timeline");
1517
1519
  return this.ensureRoleTrack(draft, timeline.entityId, role);
1518
1520
  }
1519
- replaceCaptionSemanticRelations(draft, captionEntityId, voiceEntityId, scriptEntityId, startMs, durationMs) {
1520
- const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment" || relation.relationKind === "caption-composition").map((relation) => relation.relationId));
1521
+ replaceCaptionAlignment(draft, captionEntityId, voiceEntityId, startMs, durationMs) {
1522
+ const removed = new Set(incidentRelations(draft, captionEntityId).filter((relation) => relation.relationKind === "caption-alignment").map((relation) => relation.relationId));
1521
1523
  draft.relations = draft.relations.filter((relation) => !removed.has(relation.relationId));
1522
1524
  draft.relations.push(this.createRelation(draft, "caption-alignment", captionEntityId, voiceEntityId, { alignment: {
1523
1525
  start: startMs,
1524
1526
  end: startMs + durationMs
1525
- } }), this.createRelation(draft, "caption-composition", captionEntityId, scriptEntityId, { segmentAlignment: [] }));
1527
+ } }));
1526
1528
  }
1527
1529
  /**
1528
1530
  * Remove one placed Caption use. The composed AudioScript outlives the
@@ -1532,11 +1534,7 @@ var EntityTimelineEditor = class {
1532
1534
  const topology = requireClipOnRole(draft, clipEntityId, "caption");
1533
1535
  const caption = requireEntityKind(draft, otherEndpoint(topology.markerContentRelation, topology.marker.entityId), "caption");
1534
1536
  const contentRelations = incidentRelations(draft, caption.entityId);
1535
- const allowed = new Set([
1536
- "caption-composition",
1537
- "caption-alignment",
1538
- "marker-content"
1539
- ]);
1537
+ const allowed = new Set(["caption-alignment", "marker-content"]);
1540
1538
  const unsupported = contentRelations.filter((relation) => !allowed.has(relation.relationKind));
1541
1539
  if (unsupported.length > 0) throw new Error(`Cannot delete Caption with unsupported Relation ${unsupported[0].relationId}`);
1542
1540
  this.removeClipGraph(draft, clipEntityId);
@@ -2076,7 +2074,7 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2076
2074
  const voice = contentByPartId.get(caption.speech_part_id);
2077
2075
  const render = relations.find((relation) => relation.relationKind === "phonetic-script-render" && relation.endpoint0EntityId === voice);
2078
2076
  const phonetic = entities.find((entity) => entity.entityId === render?.endpoint1EntityId);
2079
- const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities, relations);
2077
+ const script = phonetic === void 0 ? void 0 : composedAudioScript(phonetic, entities);
2080
2078
  if (script === void 0) fail("Legacy Caption has no composed AudioScript");
2081
2079
  const sourceText = scriptText(script);
2082
2080
  const captionText = caption.text;
@@ -2097,10 +2095,10 @@ function migrateLegacyTimelineToEntities(legacy, assetFacts, initial = {
2097
2095
  },
2098
2096
  sampling: "native",
2099
2097
  coordinateSpace: "milliseconds",
2098
+ baseEntityIds: [script.entityId],
2100
2099
  selections,
2101
2100
  ...caption.style === void 0 ? {} : { style: toEntityCaptionStyle(caption.style) }
2102
2101
  });
2103
- link("caption-composition", content, script.entityId, { segmentAlignment: [] });
2104
2102
  link("caption-alignment", content, voice, { alignment: {
2105
2103
  start: caption.start_ms,
2106
2104
  end: caption.start_ms + duration
@@ -2450,25 +2448,25 @@ function ensureSpeechPhoneticChain(voiceEntityId, audioScriptText, phonemeScript
2450
2448
  if (renders.length === 1) {
2451
2449
  const phonetic = entities.find((entity) => entity.entityId === renders[0].endpoint1EntityId && entity.entityKind === "phonetic-script");
2452
2450
  if (phonetic === void 0) fail(`Voice ${voiceEntityId} has a dangling PhoneticScript render`);
2453
- const script = composedAudioScript(phonetic, entities, relations);
2451
+ const script = composedAudioScript(phonetic, entities);
2454
2452
  if (script === void 0 || scriptText(script) !== audioScriptText) fail(`Voice ${voiceEntityId} conflicts with the legacy Speech script`);
2455
2453
  return phonetic.entityId;
2456
2454
  }
2457
- const script = addEntity("audio-script", { segments: [{
2458
- segmentId: "segment-0",
2459
- text: audioScriptText
2460
- }] });
2461
- const phonetic = addEntity("phonetic-script", phonemeScript === void 0 ? {} : { phonemeScript });
2462
- link("phonetic-script-composition", phonetic, script, { segmentAlignment: [] });
2455
+ const phonetic = addEntity("phonetic-script", {
2456
+ baseEntityIds: [addEntity("audio-script", { segments: [{
2457
+ segmentId: "segment-0",
2458
+ text: audioScriptText
2459
+ }] })],
2460
+ ...phonemeScript === void 0 ? {} : { phonemeScript }
2461
+ });
2463
2462
  link("phonetic-script-render", voiceEntityId, phonetic);
2464
2463
  return phonetic;
2465
2464
  }
2466
- function composedAudioScript(phonetic, entities, relations) {
2467
- const edges = relations.filter((relation) => relation.relationKind === "phonetic-script-composition" && (relation.endpoint0EntityId === phonetic.entityId || relation.endpoint1EntityId === phonetic.entityId));
2468
- if (edges.length !== 1) return void 0;
2469
- const edge = edges[0];
2470
- const otherId = edge.endpoint0EntityId === phonetic.entityId ? edge.endpoint1EntityId : edge.endpoint0EntityId;
2471
- return entities.find((entity) => entity.entityId === otherId);
2465
+ function composedAudioScript(phonetic, entities) {
2466
+ return findComposedAudioScript({
2467
+ entities,
2468
+ relations: []
2469
+ }, phonetic.entityId, "phonetic-script");
2472
2470
  }
2473
2471
  function scriptText(script) {
2474
2472
  const segments = script.payload.segments;
@@ -2558,6 +2556,82 @@ function fail(message) {
2558
2556
  throw new EntityTimelineProjectionError(message);
2559
2557
  }
2560
2558
  //#endregion
2559
+ //#region src/entity-editor/caption-content.ts
2560
+ /** Author Caption content from semantic intent; callers never reconcile variant bases or assembled fields. */
2561
+ function prepareCaptionContent(rows, input) {
2562
+ const entityId = createEntityId(input.contentEntityId);
2563
+ const scriptId = createEntityId(input.audioScriptEntityId);
2564
+ if (!Number.isFinite(input.durationMs) || input.durationMs <= 0) throw new Error("Caption duration must be positive");
2565
+ const previous = rows.entities.find((row) => row.entityId === entityId);
2566
+ if (previous !== void 0 && previous.entityKind !== "caption") throw new Error("Caption content identity has a different kind");
2567
+ let baseEntityIds = [scriptId];
2568
+ if (previous !== void 0) {
2569
+ const source = assembleCaptionContent(rows, entityId).audioScript.entityId;
2570
+ const bases = variantBaseEntityIds(previous);
2571
+ if (source !== scriptId && !bases.includes(source)) throw new Error("Replacing an indirectly composed AudioScript requires an explicit composition change");
2572
+ baseEntityIds = bases.map((id) => id === source ? scriptId : id);
2573
+ }
2574
+ const candidate = {
2575
+ entityId,
2576
+ entityKind: "caption",
2577
+ payload: {
2578
+ ...previous?.payload,
2579
+ baseEntityIds,
2580
+ extent: previous?.payload.extent ?? {
2581
+ kind: "bounded",
2582
+ start: 0,
2583
+ end: input.durationMs
2584
+ },
2585
+ sampling: previous?.payload.sampling ?? "native",
2586
+ coordinateSpace: previous?.payload.coordinateSpace ?? "milliseconds"
2587
+ }
2588
+ };
2589
+ const graph = {
2590
+ ...rows,
2591
+ entities: [...rows.entities.filter((row) => row.entityId !== entityId), candidate]
2592
+ };
2593
+ const textSource = {
2594
+ ...assembleEntityContent(graph, entityId),
2595
+ entityKind: "audio-script"
2596
+ };
2597
+ const selected = {
2598
+ ...candidate,
2599
+ payload: {
2600
+ ...candidate.payload,
2601
+ selections: structuredClone(input.selections ?? selectCaptionText(textSource, input.text))
2602
+ }
2603
+ };
2604
+ if (assembleCaptionContent({
2605
+ ...graph,
2606
+ entities: graph.entities.map((row) => row.entityId === entityId ? selected : row)
2607
+ }, entityId).text !== input.text) throw new Error("Caption review text differs from its assembled variant content");
2608
+ return selected;
2609
+ }
2610
+ function selectCaptionText(script, text) {
2611
+ if (!text.trim()) throw new Error("Caption text selection must not be blank");
2612
+ const source = script;
2613
+ const fullText = assembleScriptText(source);
2614
+ const segments = source.payload.segments;
2615
+ if (text === fullText) return segments.map((segment) => ({ segmentId: segment.segmentId }));
2616
+ const matches = [];
2617
+ for (const segment of segments) {
2618
+ let offset = segment.text.indexOf(text);
2619
+ while (offset !== -1) {
2620
+ const start = Array.from(segment.text.slice(0, offset)).length;
2621
+ matches.push({
2622
+ segmentId: segment.segmentId,
2623
+ textRange: {
2624
+ start,
2625
+ end: start + Array.from(text).length
2626
+ }
2627
+ });
2628
+ offset = segment.text.indexOf(text, offset + 1);
2629
+ }
2630
+ }
2631
+ if (matches.length !== 1) throw new Error("Caption text must select a unique AudioScript range; provide explicit selections");
2632
+ return matches;
2633
+ }
2634
+ //#endregion
2561
2635
  //#region src/editor/snapshot-utils.ts
2562
2636
  function isMap(value) {
2563
2637
  return value instanceof Map;
@@ -4696,4 +4770,4 @@ function serverCovers(serverVV, target) {
4696
4770
  return covers(server, target);
4697
4771
  }
4698
4772
  //#endregion
4699
- export { DEFAULT_UNIT_TIME_MS, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
4773
+ export { DEFAULT_UNIT_TIME_MS, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION, EntityGraphHttpClient, EntityTimelineEditor, EntityTimelineProjectionError, IMPLEMENTED_SEMANTIC_OP_KINDS, LANE_KINDS_IN_STACK_ORDER, ManualSyncDoc, MedeoHttpDocStorage, MemoryDocStorage, MengineAckFailedError, MengineDocSession, MengineHttpClient, MengineHttpRequestError, MenginePushRejectedError, MirrorVideoDocumentAdapter, PlainMemoryAdapter, SchemaValidator, ScriptCompositionError, SemanticEditor, TIMELINE_SKELETON_DURATION_MS, VIDEO_DOCUMENT_SCHEMA_VERSION, ValidationError, VideoDocumentValidationError, applyEntityTimelineProjection, arrangeMainTrackSeamlessly, assembleCaptionContent, assembleEntityContent, assemblePhoneticScriptContent, assembleScriptText, assertCanonicalEditorResources, assertEntityProjectionPreservesLegacy, assertMediaAssetWritePolicy, assertValidVideoDocument, base64ToBytes, buildInitialVideoDocument, buildSpeechHostMap, bytesToBase64, cascadeAfterVideoClipChanges, createEditSandbox, createMirrorVideoDocument, createMirrorVideoDocumentAdapter, createPlainMemoryAdapter, decodeDocVersionMark, derivePositionFromAbs, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, ensureLaneTrack, fillMainTrackTimeGaps, findComposedAudioScript, findLaneTrack, fromVideoDocument, generatePartId, getAt, hostForAbsMs, importMediaAsset, isEmptyVideoClip, isImplementedSemanticOpKind, isMap, laneTrackId, mainTrackRanges, migrateLegacyTimelineToEntities, partDurationMs, partUnionSchema, prepareCaptionContent, projectEntityTimeline, readMainTrackItems, readMengineEventStream, readPart, readPartDurationMs, readVideoDocumentFromDraft, reassignSpeechesToVideoClipsByTime, recalculateTimelineDuration, relativePositionForAbs, replayJournal, resolveAllSpeechOverlaps, resolveEntityTimelineLayout, resolveSpeechOverlapByShiftingVideos, safeDurationMs, schemas_exports as schemas, selectAudioScriptSegments, snapshotToPlain, solveVideoDocument, speedOf, syncAggregatedClipsTimePosition, toVideoDocument, validateVideoDocument, videoDocumentMirrorSchema, videoDocumentSchema, writeVideoDocumentToDraft };
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Ot as VideoDraft, c as MirrorVideoDocumentAdapter } from "./index-PYtGeXNk.js";
1
+ import { Ot as VideoDraft, c as MirrorVideoDocumentAdapter } from "./index-Dxxu0Mi1.js";
2
2
 
3
3
  //#region src/testing/in-memory-mengine-server.d.ts
4
4
  /**
package/dist/testing.js CHANGED
@@ -1,4 +1,4 @@
1
- import { et as VIDEO_DOCUMENT_SCHEMA_VERSION, ot as base64ToBytes, s as createMirrorVideoDocumentAdapter, st as bytesToBase64 } from "./document-Bf200CmD.js";
1
+ import { dt as bytesToBase64, it as VIDEO_DOCUMENT_SCHEMA_VERSION, s as createMirrorVideoDocumentAdapter, ut as base64ToBytes } from "./document-BLYcbv8E.js";
2
2
  import { t as classifyUpdate } from "./loro-relay-doc-ssdYpuef.js";
3
3
  import { LoroDoc, VersionVector, encodeFrontiers } from "loro-crdt";
4
4
  //#region src/testing/in-memory-mengine-server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mengine/medeo-client",
3
- "version": "1.3.1-alpha.1",
3
+ "version": "1.3.1-alpha.2",
4
4
  "license": "UNLICENSED",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,9 @@
27
27
  "immer": "^10.2.0",
28
28
  "loro-mirror": "^2.2.0",
29
29
  "zod": "^4.4.3",
30
- "@mengine/utils": "1.3.1-alpha.1",
31
- "@mengine/storage": "1.3.1-alpha.1",
32
- "@mengine/sync": "1.3.1-alpha.1"
30
+ "@mengine/storage": "1.3.1-alpha.2",
31
+ "@mengine/sync": "1.3.1-alpha.2",
32
+ "@mengine/utils": "1.3.1-alpha.2"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^25.9.1",
@@ -39,8 +39,8 @@
39
39
  "typescript": "^6.0.3",
40
40
  "vite-plugin-wasm": "^3.6.0",
41
41
  "vite-plus": "^0.1.23",
42
- "@mengine/medeo-dsl": "0.0.0",
43
- "@mengine/idl-codegen": "0.0.0"
42
+ "@mengine/idl-codegen": "0.0.0",
43
+ "@mengine/medeo-dsl": "0.0.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "loro-crdt": "^1.13.6"