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

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.
@@ -1,7 +1,9 @@
1
1
  import { u as volumeSchema } from "./shared-iWnU2osE.js";
2
2
  import { Mirror, schema } from "loro-mirror";
3
3
  import { z } from "zod";
4
- import { LoroDoc } from "loro-crdt";
4
+ import { LoroDoc, LoroMap, LoroMovableList, LoroText, UndoManager } from "loro-crdt";
5
+ import { sha256 } from "@noble/hashes/sha2.js";
6
+ import { bytesToHex } from "@noble/hashes/utils.js";
5
7
  import { produce } from "immer";
6
8
  //#region src/client/base64.ts
7
9
  function bytesToBase64(bytes) {
@@ -1181,7 +1183,7 @@ function toVideoDocument(draft) {
1181
1183
  owner_id: draft.owner_id,
1182
1184
  thumbnail_storage_key: draft.thumbnail_storage_key,
1183
1185
  chat_session_id: draft.chat_session_id,
1184
- video_creation_settings: clone(draft.video_creation_settings),
1186
+ video_creation_settings: clone$1(draft.video_creation_settings),
1185
1187
  version: draft.version
1186
1188
  },
1187
1189
  timeline: draft.timeline == null ? void 0 : { unit_time_ms: draft.timeline.unit_time_ms },
@@ -1327,7 +1329,7 @@ function fromVideoDocument(document) {
1327
1329
  duration_ms: view.durationMs,
1328
1330
  unit_time_ms: document.timeline?.unit_time_ms ?? 33.333
1329
1331
  },
1330
- video_creation_settings: clone(document.meta.video_creation_settings),
1332
+ video_creation_settings: clone$1(document.meta.video_creation_settings),
1331
1333
  chat_session_id: document.meta.chat_session_id,
1332
1334
  main_track: draftTrack(mainTrack, view.absByPartId),
1333
1335
  above_main_tracks: aboveTracks.map((t) => draftTrack(t, view.absByPartId)),
@@ -1359,25 +1361,25 @@ function toReadViewPartLibrary(partLibrary, durationMs, derivedFillerPartIds) {
1359
1361
  for (const [partId, part] of Object.entries(partLibrary)) {
1360
1362
  if (derivedFillerPartIds.has(partId)) continue;
1361
1363
  if (part.video_clip != null) {
1362
- const clip = clone(part.video_clip);
1364
+ const clip = clone$1(part.video_clip);
1363
1365
  out[partId] = { video_clip: {
1364
1366
  ...clip,
1365
1367
  duration_ms: effectiveVideoClipDurationMs(clip)
1366
1368
  } };
1367
1369
  } else if (part.speech != null) {
1368
- const speech = clone(part.speech);
1370
+ const speech = clone$1(part.speech);
1369
1371
  out[partId] = { speech: {
1370
1372
  ...speech,
1371
1373
  duration_ms: speech.media_duration_ms
1372
1374
  } };
1373
1375
  } else if (part.caption != null) {
1374
- const caption = clone(part.caption);
1376
+ const caption = clone$1(part.caption);
1375
1377
  out[partId] = { caption: {
1376
1378
  ...caption,
1377
1379
  duration_ms: caption.initial_duration_ms
1378
1380
  } };
1379
1381
  } else if (part.bgm != null) out[partId] = { bgm: {
1380
- ...clone(part.bgm),
1382
+ ...clone$1(part.bgm),
1381
1383
  duration_ms: durationMs
1382
1384
  } };
1383
1385
  }
@@ -1385,7 +1387,7 @@ function toReadViewPartLibrary(partLibrary, durationMs, derivedFillerPartIds) {
1385
1387
  }
1386
1388
  /** Deep-clone a part payload, dropping the derived read-view `duration_ms`. */
1387
1389
  function omitDurationMs(part) {
1388
- const { duration_ms: _drop, ...rest } = clone(part);
1390
+ const { duration_ms: _drop, ...rest } = clone$1(part);
1389
1391
  return rest;
1390
1392
  }
1391
1393
  /**
@@ -1410,7 +1412,7 @@ function withTrimWindowFromDuration(clip) {
1410
1412
  }
1411
1413
  /** Deep-clone a part payload, returning its `duration_ms` separately from the rest. */
1412
1414
  function splitDurationMs(part) {
1413
- const { duration_ms, ...rest } = clone(part);
1415
+ const { duration_ms, ...rest } = clone$1(part);
1414
1416
  return {
1415
1417
  duration_ms,
1416
1418
  rest
@@ -1428,11 +1430,11 @@ function draftTrack(track, absByPartId) {
1428
1430
  }))
1429
1431
  };
1430
1432
  }
1431
- function clone(value) {
1432
- if (Array.isArray(value)) return value.map((item) => clone(item));
1433
+ function clone$1(value) {
1434
+ if (Array.isArray(value)) return value.map((item) => clone$1(item));
1433
1435
  if (value != null && typeof value === "object") {
1434
1436
  const result = {};
1435
- for (const [key, child] of Object.entries(value)) result[key] = clone(child);
1437
+ for (const [key, child] of Object.entries(value)) result[key] = clone$1(child);
1436
1438
  return result;
1437
1439
  }
1438
1440
  return value;
@@ -1536,11 +1538,255 @@ function createId(value, label) {
1536
1538
  return value;
1537
1539
  }
1538
1540
  //#endregion
1541
+ //#region ../medeo-dsl/src/json-values.ts
1542
+ const nativeObjectConstructorSource = Function.prototype.toString.call(Object);
1543
+ function isJsonObject(value) {
1544
+ return typeof value === "object" && value !== null && !Array.isArray(value) && isJsonValue(value, /* @__PURE__ */ new Set());
1545
+ }
1546
+ function isJsonValue(value, ancestors) {
1547
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
1548
+ if (typeof value === "number") return Number.isFinite(value);
1549
+ if (typeof value !== "object") return false;
1550
+ if (!Array.isArray(value) && !isPlainObject(value)) return false;
1551
+ if (ancestors.has(value)) return false;
1552
+ ancestors.add(value);
1553
+ const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
1554
+ ancestors.delete(value);
1555
+ return valid;
1556
+ }
1557
+ /** Recognize an ordinary object from any VM realm without admitting class instances. */
1558
+ function isPlainObject(value) {
1559
+ try {
1560
+ const prototype = Object.getPrototypeOf(value);
1561
+ if (prototype === null) return true;
1562
+ if (Object.getPrototypeOf(prototype) !== null) return false;
1563
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1564
+ return typeof constructor === "function" && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === nativeObjectConstructorSource;
1565
+ } catch {
1566
+ return false;
1567
+ }
1568
+ }
1569
+ //#endregion
1570
+ //#region ../medeo-dsl/src/composition.ts
1571
+ /** Assembly fails explicitly when declared bases cannot provide valid, unambiguous content. */
1572
+ var ScriptCompositionError = class extends Error {
1573
+ code;
1574
+ entityId;
1575
+ constructor(code, entityId, message) {
1576
+ super(message);
1577
+ this.code = code;
1578
+ this.entityId = entityId;
1579
+ this.name = "ScriptCompositionError";
1580
+ }
1581
+ };
1582
+ /** Direct base IDs are structural fields, never ordinary Relation endpoints. */
1583
+ function variantBaseEntityIds(entity) {
1584
+ if (!Object.hasOwn(entity.payload, "baseEntityIds")) return [];
1585
+ const ids = entity.payload.baseEntityIds;
1586
+ 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`);
1587
+ return ids.map(createEntityId);
1588
+ }
1589
+ /** Validate all base providers before applying explicit own fields; ordering cannot resolve ambiguity. */
1590
+ function assembleEntityContent(rows, entityId) {
1591
+ const byId = /* @__PURE__ */ new Map();
1592
+ for (const row of rows.entities) {
1593
+ if (byId.has(row.entityId)) throw new ScriptCompositionError("composition_ambiguous", row.entityId, `duplicate entity id "${row.entityId}"`);
1594
+ byId.set(row.entityId, row);
1595
+ }
1596
+ const active = /* @__PURE__ */ new Set();
1597
+ const cache = /* @__PURE__ */ new Map();
1598
+ const visit = (id) => {
1599
+ const cached = cache.get(id);
1600
+ if (cached !== void 0) return cached;
1601
+ if (active.has(id)) throw new ScriptCompositionError("composition_cycle", id, `Cyclic baseEntityIds at "${id}"`);
1602
+ const row = byId.get(id);
1603
+ if (row === void 0) throw new ScriptCompositionError("composition_dangling", id, `Missing base entity "${id}" in this document`);
1604
+ 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`);
1605
+ active.add(id);
1606
+ const inherited = {};
1607
+ const providers = /* @__PURE__ */ new Map();
1608
+ for (const baseId of variantBaseEntityIds(row)) {
1609
+ const base = visit(baseId);
1610
+ for (const [field, value] of Object.entries(base.payload)) {
1611
+ if (field === "baseEntityIds") continue;
1612
+ const previous = providers.get(field);
1613
+ 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`);
1614
+ providers.set(field, baseId);
1615
+ Object.defineProperty(inherited, field, {
1616
+ value,
1617
+ enumerable: true,
1618
+ configurable: true,
1619
+ writable: true
1620
+ });
1621
+ }
1622
+ }
1623
+ const assembled = {
1624
+ ...row,
1625
+ payload: JSON.parse(JSON.stringify({
1626
+ ...inherited,
1627
+ ...row.payload
1628
+ }))
1629
+ };
1630
+ active.delete(id);
1631
+ cache.set(id, assembled);
1632
+ return assembled;
1633
+ };
1634
+ return visit(entityId);
1635
+ }
1636
+ /** Resolve a field's declaring entity after validating the entire composition. */
1637
+ function resolveEntityFieldOwner(rows, entityId, field) {
1638
+ assembleEntityContent(rows, entityId);
1639
+ const byId = new Map(rows.entities.map((row) => [row.entityId, row]));
1640
+ const find = (id) => {
1641
+ const row = byId.get(id);
1642
+ if (Object.hasOwn(row.payload, field)) return id;
1643
+ if (field === "baseEntityIds") return void 0;
1644
+ for (const baseId of variantBaseEntityIds(row)) {
1645
+ const owner = find(baseId);
1646
+ if (owner !== void 0) return owner;
1647
+ }
1648
+ };
1649
+ return find(entityId) ?? entityId;
1650
+ }
1651
+ /** Patch assembled fields without copying inherited fields into the variant's stored payload. */
1652
+ function updateEntityFields(rows, entityId, fields) {
1653
+ assembleEntityContent(rows, entityId);
1654
+ 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");
1655
+ const updates = /* @__PURE__ */ new Map();
1656
+ for (const [field, value] of Object.entries(fields)) {
1657
+ const ownerId = resolveEntityFieldOwner(rows, entityId, field);
1658
+ const owner = updates.get(ownerId) ?? rows.entities.find((row) => row.entityId === ownerId);
1659
+ updates.set(ownerId, {
1660
+ ...owner,
1661
+ payload: {
1662
+ ...owner.payload,
1663
+ [field]: value
1664
+ }
1665
+ });
1666
+ }
1667
+ return [...updates.values()].map((row) => JSON.parse(JSON.stringify(row)));
1668
+ }
1669
+ /** Find the actual AudioScript text owner through direct variant bases. */
1670
+ function findComposedAudioScript(rows, variantEntityId, variantKind) {
1671
+ const variant = requireEntityKind(rows, variantEntityId, variantKind);
1672
+ assembleEntityContent(rows, variantEntityId);
1673
+ const sources = /* @__PURE__ */ new Map();
1674
+ const seen = /* @__PURE__ */ new Set();
1675
+ const visit = (row) => {
1676
+ for (const id of variantBaseEntityIds(row)) {
1677
+ if (seen.has(id)) continue;
1678
+ seen.add(id);
1679
+ const base = rows.entities.find((candidate) => candidate.entityId === id);
1680
+ if (base.entityKind === "audio-script") sources.set(id, assembleEntityContent(rows, id));
1681
+ else visit(base);
1682
+ }
1683
+ };
1684
+ visit(variant);
1685
+ 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}`);
1686
+ return [...sources.values()][0];
1687
+ }
1688
+ /** Join the whole AudioScript text in segment order. */
1689
+ function assembleScriptText(script) {
1690
+ return scriptSegments(script).map((segment) => segment.text).join("");
1691
+ }
1692
+ function scriptSegments(script) {
1693
+ const segments = script.payload.segments;
1694
+ if (!Array.isArray(segments)) throw new ScriptCompositionError("composition_dangling", script.entityId, `AudioScript "${script.entityId}" requires segments`);
1695
+ 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");
1696
+ return segments;
1697
+ }
1698
+ function isScriptSegment(value) {
1699
+ if (typeof value !== "object" || value == null || Array.isArray(value)) return false;
1700
+ const segment = value;
1701
+ return typeof segment.segmentId === "string" && segment.segmentId.trim() !== "" && typeof segment.text === "string" && (segment.language === void 0 || typeof segment.language === "string");
1702
+ }
1703
+ /** Assemble selected text after base-field validation and explicit variant overrides. */
1704
+ function assembleCaptionContent(rows, captionEntityId) {
1705
+ requireEntityKind(rows, captionEntityId, "caption");
1706
+ const caption = assembleEntityContent(rows, captionEntityId);
1707
+ const script = findComposedAudioScript(rows, captionEntityId, "caption");
1708
+ const segments = selectAudioScriptSegments({
1709
+ ...script,
1710
+ payload: caption.payload
1711
+ }, captionSelections(caption));
1712
+ return {
1713
+ caption,
1714
+ audioScript: script,
1715
+ segments,
1716
+ text: segments.map((segment) => segment.text).join("")
1717
+ };
1718
+ }
1719
+ /**
1720
+ * Assemble the complete Phonetic Script content for TTS: the composed
1721
+ * AudioScript's base text plus the variant's own phoneme and prosody fields.
1722
+ */
1723
+ function assemblePhoneticScriptContent(rows, phoneticScriptEntityId) {
1724
+ requireEntityKind(rows, phoneticScriptEntityId, "phonetic-script");
1725
+ const phoneticScript = assembleEntityContent(rows, phoneticScriptEntityId);
1726
+ const audioScript = findComposedAudioScript(rows, phoneticScriptEntityId, "phonetic-script");
1727
+ const segments = scriptSegments({
1728
+ ...audioScript,
1729
+ payload: phoneticScript.payload
1730
+ });
1731
+ return {
1732
+ phoneticScript,
1733
+ audioScript,
1734
+ segments,
1735
+ text: segments.map((segment) => segment.text).join("")
1736
+ };
1737
+ }
1738
+ function captionSelections(caption) {
1739
+ const selections = caption.payload.selections;
1740
+ 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`);
1741
+ if (!selections.every(isSegmentSelection)) throw new ScriptCompositionError("invalid_selection", caption.entityId, "Caption selections require a segmentId and optional textRange");
1742
+ return selections;
1743
+ }
1744
+ function isSegmentSelection(value) {
1745
+ 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");
1746
+ }
1747
+ /** Select source text without creating another authoritative text field. */
1748
+ function selectAudioScriptSegments(script, selections) {
1749
+ 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");
1750
+ const bySegmentId = new Map(scriptSegments(script).map((segment) => [segment.segmentId, segment]));
1751
+ const selected = selections.map((selection) => {
1752
+ const segment = bySegmentId.get(selection.segmentId);
1753
+ if (segment === void 0) throw new ScriptCompositionError("unknown_segment", script.entityId, `Caption selection "${selection.segmentId}" does not name a segment of AudioScript "${script.entityId}"`);
1754
+ if (selection.textRange === void 0) return segment;
1755
+ const range = selection.textRange;
1756
+ const points = Array.from(segment.text);
1757
+ 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");
1758
+ return {
1759
+ ...segment,
1760
+ text: points.slice(range.start, range.end).join("")
1761
+ };
1762
+ });
1763
+ if (!selected.map((segment) => segment.text).join("").trim()) throw new ScriptCompositionError("empty_selection", script.entityId, "Caption selection must contain visible source text");
1764
+ return selected;
1765
+ }
1766
+ function requireEntityKind(rows, entityIdValue, entityKind) {
1767
+ const entity = rows.entities.find((candidate) => candidate.entityId === entityIdValue);
1768
+ if (entity === void 0) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" does not exist`);
1769
+ if (entity.entityKind !== entityKind) throw new ScriptCompositionError("composition_dangling", entityIdValue, `Entity "${entityIdValue}" must have kind "${entityKind}", got "${entity.entityKind}"`);
1770
+ return entity;
1771
+ }
1772
+ //#endregion
1773
+ //#region ../medeo-dsl/src/rows.ts
1774
+ function entityToRow(entity) {
1775
+ const { entityId, entityKind, ...payload } = entity;
1776
+ if (!isJsonObject(payload)) throw new Error(`Entity "${entityId}" payload must contain only JSON values`);
1777
+ return {
1778
+ entityId,
1779
+ entityKind,
1780
+ payload
1781
+ };
1782
+ }
1783
+ //#endregion
1539
1784
  //#region ../medeo-dsl/src/invariants.ts
1540
1785
  /**
1541
1786
  * 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.
1787
+ * Relation may not bind them. Caption text is composed domain content, so
1788
+ * Captions do not bind Physical Assets either; the Relation vocabulary stays
1789
+ * open for future physical pipeline semantics.
1544
1790
  */
1545
1791
  function validateMediaAssetIdentity(entity, index) {
1546
1792
  return [...index.relationsOf(entity)].filter((relation) => relation.kind === "physical-asset").map(() => ({
@@ -1553,6 +1799,19 @@ function validateMediaAssetIdentity(entity, index) {
1553
1799
  function validateEntityRelationSet(entityRefs, index, options) {
1554
1800
  const { entities, issues } = collectRelatedEntities(entityRefs, index);
1555
1801
  const entityIds = new Set(entities.map((entity) => entity.entityId));
1802
+ const rows = {
1803
+ entities: entities.map((entity) => entityToRow(entity.current())),
1804
+ relations: []
1805
+ };
1806
+ for (const entity of entities) try {
1807
+ assembleEntityContent(rows, entity.entityId);
1808
+ } catch (error) {
1809
+ issues.push({
1810
+ code: "invalid_variant_composition",
1811
+ entityId: entity.entityId,
1812
+ message: error instanceof Error ? error.message : "Invalid variant composition"
1813
+ });
1814
+ }
1556
1815
  for (const entity of entities) {
1557
1816
  const current = entity.current();
1558
1817
  const entityIssues = validateEntity(entity, entityIds);
@@ -1573,7 +1832,8 @@ function validateEntityRelationSet(entityRefs, index, options) {
1573
1832
  issues.push(...validateClipPlacement(entity, index));
1574
1833
  }
1575
1834
  if (current.entityKind === "axvideo") issues.push(...validateAXVideoAdmission(entity, index));
1576
- if (current.entityKind === "caption") issues.push(...validateCaptionAsset(entity, index));
1835
+ if (current.entityKind === "caption") issues.push(...validateCaptionComposition(entity, index, entities));
1836
+ if (current.entityKind === "phonetic-script") issues.push(...validatePhoneticComposition(entity, index, entities));
1577
1837
  issues.push(...validateSequenceComposition(entity));
1578
1838
  }
1579
1839
  issues.push(...validateClipAnchorCycles(entities, index));
@@ -1676,25 +1936,35 @@ function validateAXVideoAdmission(axVideo, index) {
1676
1936
  message: "AXVideo must have exactly one live AXVideo-Marker Relation"
1677
1937
  }];
1678
1938
  }
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
- }];
1939
+ /** Validate a Caption against the complete entity set, including its directly held bases. */
1940
+ function validateCaptionComposition(caption, index, entities = []) {
1941
+ return validateVariantComposition(caption, index, entities, "caption");
1942
+ }
1943
+ function validatePhoneticComposition(phonetic, index, entities = []) {
1944
+ return validateVariantComposition(phonetic, index, entities, "phonetic-script");
1945
+ }
1946
+ function validateVariantComposition(variant, index, entities, kind) {
1947
+ const issues = [];
1948
+ if (kind === "caption" && ofKind([...index.relationsOf(variant)], "physical-asset").length > 0) issues.push({
1949
+ code: "caption_physical_asset_forbidden",
1950
+ entityId: variant.entityId,
1951
+ message: "Caption text is composed from its AudioScript; Caption must not bind a Physical Asset"
1952
+ });
1953
+ const rows = {
1954
+ entities: collectRelatedEntities([...entities, variant], index).entities.map((ref) => entityToRow(ref.current())),
1955
+ relations: []
1956
+ };
1957
+ try {
1958
+ if (kind === "caption") assembleCaptionContent(rows, variant.entityId);
1959
+ else assemblePhoneticScriptContent(rows, variant.entityId);
1960
+ } catch (error) {
1961
+ issues.push({
1962
+ code: kind === "caption" ? "caption_composition_required" : "phonetic_script_composition_required",
1963
+ entityId: variant.entityId,
1964
+ message: error instanceof Error ? error.message : "Invalid variant composition"
1965
+ });
1692
1966
  }
1693
- return [{
1694
- code: "caption_asset_required",
1695
- entityId: caption.entityId,
1696
- message: "Caption must have a Physical Asset Relation"
1697
- }];
1967
+ return issues;
1698
1968
  }
1699
1969
  function validateClipPlacement(clip, index) {
1700
1970
  const marker = ofKind([...index.relationsOf(clip)], "clip-marker")[0]?.other(clip)?.deref();
@@ -1822,6 +2092,18 @@ function validateKnownEntityPayload(entity) {
1822
2092
  const value = entity;
1823
2093
  const problems = [];
1824
2094
  if (value.lifecycle !== void 0 && !isRecord(value.lifecycle)) problems.push("lifecycle must be an object when present");
2095
+ if (entity.entityKind === "audio-script" || entity.entityKind === "phonetic-script") {
2096
+ for (const key of [
2097
+ "extent",
2098
+ "sampling",
2099
+ "coordinateSpace",
2100
+ "sourceRange",
2101
+ "targetRange",
2102
+ "duration",
2103
+ "startMs",
2104
+ "endMs"
2105
+ ]) if (Object.hasOwn(value, key)) problems.push(`${key} is not intrinsic script data; assign an external annotation Marker`);
2106
+ }
1825
2107
  switch (entity.entityKind) {
1826
2108
  case "track":
1827
2109
  validateOptionalField(value, "hidden", "boolean", problems);
@@ -1848,9 +2130,11 @@ function validateKnownEntityPayload(entity) {
1848
2130
  validateMarkerPayload(value, problems);
1849
2131
  break;
1850
2132
  case "audio-script":
1851
- case "phonetic-script":
1852
2133
  validateScriptPayload(value, problems);
1853
2134
  break;
2135
+ case "phonetic-script":
2136
+ validatePhoneticScriptPayload(value, problems);
2137
+ break;
1854
2138
  case "timeline":
1855
2139
  case "viewport": break;
1856
2140
  case "clip":
@@ -1914,24 +2198,18 @@ function validateSegmentRanges(value, problems) {
1914
2198
  function validateAssetPayload(value, problems) {
1915
2199
  validateExternalLocator(value, problems);
1916
2200
  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");
2201
+ if (value.inline !== void 0) problems.push("inline is not a physical location; text is domain content owned by its AudioScript");
1924
2202
  }
1925
2203
  /**
1926
2204
  * 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
2205
+ * textual domain content is owned by AudioScript; the physical-location
1928
2206
  * vocabularies cannot mix.
1929
2207
  */
1930
2208
  function validateMediaAssetFields(value, problems) {
1931
2209
  if (value.external === void 0) problems.push("external is required; media variants own their Asset identity");
1932
2210
  else validateExternalLocator(value, problems);
1933
2211
  validateStorageKey(value, problems);
1934
- if (value.inline !== void 0) problems.push("inline is an Asset-kind field; media variants use external");
2212
+ if (value.inline !== void 0) problems.push("media variants use external locators, not inline domain text");
1935
2213
  }
1936
2214
  function validateExternalLocator(value, problems) {
1937
2215
  const external = value.external;
@@ -1958,7 +2236,26 @@ function validateVoicePayload(value, problems) {
1958
2236
  if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
1959
2237
  }
1960
2238
  function validateCaptionPayload(value, problems) {
1961
- if (value.text !== void 0 && typeof value.text !== "string") problems.push("text must be a string when present");
2239
+ const selections = value.selections;
2240
+ if (!Array.isArray(selections)) problems.push("selections must be a non-empty array of AudioScript segment selections");
2241
+ else {
2242
+ if (selections.length === 0) problems.push("selections must name at least one AudioScript segment");
2243
+ const seen = /* @__PURE__ */ new Set();
2244
+ for (const [index, selection] of selections.entries()) {
2245
+ if (!isRecord(selection)) {
2246
+ problems.push(`selections[${index}] must be an object`);
2247
+ continue;
2248
+ }
2249
+ if (typeof selection.segmentId !== "string" || selection.segmentId.trim() === "") problems.push(`selections[${index}].segmentId must be a string`);
2250
+ else if (seen.has(selection.segmentId)) problems.push(`selections[${index}].segmentId must be unique within the Caption`);
2251
+ else seen.add(selection.segmentId);
2252
+ if (Object.keys(selection).some((key) => key !== "segmentId" && key !== "textRange")) problems.push(`selections[${index}] may only contain segmentId and textRange`);
2253
+ if (selection.textRange !== void 0) {
2254
+ const range = selection.textRange;
2255
+ 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`);
2256
+ }
2257
+ }
2258
+ }
1962
2259
  const style = value.style;
1963
2260
  if (style === void 0) return;
1964
2261
  if (!isRecord(style)) {
@@ -1993,6 +2290,12 @@ function validateRange(value, path, required, problems) {
1993
2290
  if (!Object.hasOwn(value, "start") || value.start === void 0) problems.push(`${path}.start is required`);
1994
2291
  if (!Object.hasOwn(value, "end") || value.end === void 0) problems.push(`${path}.end is required`);
1995
2292
  }
2293
+ /** Phonetic variants store pronunciation fields only; base text stays in the AudioScript. */
2294
+ function validatePhoneticScriptPayload(value, problems) {
2295
+ if (value.segments !== void 0) validateScriptPayload(value, problems);
2296
+ if (value.phonemeScript !== void 0 && (typeof value.phonemeScript !== "string" || value.phonemeScript.trim() === "")) problems.push("phonemeScript must be a non-empty string when present");
2297
+ if (value.prosody !== void 0 && !isRecord(value.prosody)) problems.push("prosody must be an object when present");
2298
+ }
1996
2299
  function validateScriptPayload(value, problems) {
1997
2300
  if (!Array.isArray(value.segments)) {
1998
2301
  problems.push("segments must be an array");
@@ -2035,6 +2338,7 @@ function visitPeerEntityIdPaths(value, entityKind, parentPath, ancestors, paths)
2035
2338
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityIdPaths(item, entityKind, `${parentPath}[${index}]`, ancestors, paths);
2036
2339
  else for (const [key, child] of Object.entries(value)) {
2037
2340
  const path = parentPath.length === 0 ? key : `${parentPath}.${key}`;
2341
+ if (parentPath.length === 0 && key === "baseEntityIds") continue;
2038
2342
  if (!(parentPath.length === 0 && key === "entityId") && !isOwnedLocalIdPath(entityKind, path) && isEntityIdFieldName(key)) paths.push(path);
2039
2343
  visitPeerEntityIdPaths(child, entityKind, path, ancestors, paths);
2040
2344
  }
@@ -2054,7 +2358,7 @@ function visitPeerEntityValues(value, entityKind, ownEntityId, entityIds, path,
2054
2358
  ancestors.add(value);
2055
2359
  if (Array.isArray(value)) for (const [index, item] of value.entries()) visitPeerEntityValues(item, entityKind, ownEntityId, entityIds, `${path}[${index}]`, ancestors, paths);
2056
2360
  else for (const [key, child] of Object.entries(value)) {
2057
- if (path.length === 0 && (key === "entityId" || key === "entityKind")) continue;
2361
+ if (path.length === 0 && (key === "entityId" || key === "entityKind" || key === "baseEntityIds")) continue;
2058
2362
  visitPeerEntityValues(child, entityKind, ownEntityId, entityIds, path.length === 0 ? key : `${path}.${key}`, ancestors, paths);
2059
2363
  }
2060
2364
  ancestors.delete(value);
@@ -2071,16 +2375,25 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
2071
2375
  ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
2072
2376
  if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
2073
2377
  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;
2378
+ if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2075
2379
  if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
2076
2380
  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;
2381
+ if (entityKind === "caption" && (path.startsWith("style.") || /^selections\[\d+\]\.segmentId$/.test(path))) return true;
2382
+ if ([
2383
+ "audio-script",
2384
+ "caption",
2385
+ "phonetic-script"
2386
+ ].includes(entityKind) && /^segments\[\d+\]\.(?:segmentId|text|language)$/.test(path)) return true;
2079
2387
  return false;
2080
2388
  }
2081
2389
  function isOwnedLocalIdPath(entityKind, path) {
2082
2390
  if (path === "lifecycle.actorId") return true;
2083
- if ((entityKind === "audio-script" || entityKind === "phonetic-script") && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
2391
+ if ([
2392
+ "audio-script",
2393
+ "caption",
2394
+ "phonetic-script"
2395
+ ].includes(entityKind) && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
2396
+ if (entityKind === "caption" && /^selections\[\d+\]\.segmentId$/.test(path)) return true;
2084
2397
  if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
2085
2398
  if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
2086
2399
  return false;
@@ -2136,35 +2449,6 @@ function ofKind(relations, kind) {
2136
2449
  return relations.filter((relation) => relation.kind === kind);
2137
2450
  }
2138
2451
  //#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
2452
  //#region ../medeo-dsl/src/relation-specs.ts
2169
2453
  const timelineTrackRelationSpec = emptySpec("timeline-track", "timeline", "track");
2170
2454
  const trackClipRelationSpec = emptySpec("track-clip", "track", "clip");
@@ -2187,8 +2471,6 @@ const generatedRelationSpec = Object.freeze({
2187
2471
  validateEndpoints: (endpoints) => endpoints.every((endpoint) => isGeneratedMedia(endpoint.current())),
2188
2472
  validateMetadata: isEmptyMetadata
2189
2473
  });
2190
- const phoneticScriptProvenanceRelationSpec = metadataSpec("phonetic-script-provenance", "phonetic-script", "audio-script", isSegmentAlignmentMetadata);
2191
- const captionProvenanceRelationSpec = metadataSpec("caption-provenance", "caption", "audio-script", isSegmentAlignmentMetadata);
2192
2474
  const captionAlignmentRelationSpec = Object.freeze({
2193
2475
  kind: "caption-alignment",
2194
2476
  validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["caption"]), new Set(["audio", "voice"])),
@@ -2200,19 +2482,20 @@ const clipAnchorRelationSpec = Object.freeze({
2200
2482
  validateEndpoints: (endpoints) => endpoints[0].current().entityKind === "clip" && endpoints[1].current().entityKind === "clip",
2201
2483
  validateMetadata: isEmptyMetadata
2202
2484
  });
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
- },
2485
+ /** Voice is generated from PhoneticScript; kinds determine roles regardless of endpoint positions. */
2486
+ const phoneticScriptRenderRelationSpec = Object.freeze({
2487
+ kind: "phonetic-script-render",
2488
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["voice"]), new Set(["phonetic-script"])),
2210
2489
  validateMetadata: isEmptyMetadata
2211
2490
  });
2212
- /** `audio-script-source(script, source)` means endpoint 0 was transcribed from endpoint 1. */
2491
+ /** AudioScript was transcribed from Audio, Video, or recorded Voice; kinds determine roles. */
2213
2492
  const audioScriptSourceRelationSpec = Object.freeze({
2214
2493
  kind: "audio-script-source",
2215
- validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set(["audio", "video"])),
2494
+ validateEndpoints: (endpoints) => hasKinds(endpoints, new Set(["audio-script"]), new Set([
2495
+ "audio",
2496
+ "video",
2497
+ "voice"
2498
+ ])),
2216
2499
  validateMetadata: isEmptyMetadata
2217
2500
  });
2218
2501
  /**
@@ -2236,11 +2519,9 @@ const builtInRelationSpecs = Object.freeze([
2236
2519
  markerTimelineRelationSpec,
2237
2520
  physicalAssetRelationSpec,
2238
2521
  generatedRelationSpec,
2239
- phoneticScriptProvenanceRelationSpec,
2240
- captionProvenanceRelationSpec,
2241
2522
  captionAlignmentRelationSpec,
2242
2523
  clipAnchorRelationSpec,
2243
- audioScriptRenderRelationSpec,
2524
+ phoneticScriptRenderRelationSpec,
2244
2525
  audioScriptSourceRelationSpec,
2245
2526
  audioScriptMarkerRelationSpec
2246
2527
  ]);
@@ -2280,9 +2561,6 @@ function isAssetBinding(value) {
2280
2561
  const orderingKey = /(order|ordinal|position|rank|index|z[_-]?index)/i;
2281
2562
  return Object.keys(value).every((key) => !orderingKey.test(key));
2282
2563
  }
2283
- function isSegmentAlignmentMetadata(value) {
2284
- return isJsonObject(value) && Object.hasOwn(value, "segmentAlignment");
2285
- }
2286
2564
  function isCaptionAlignmentMetadata(value) {
2287
2565
  return isJsonObject(value) && Object.hasOwn(value, "alignment");
2288
2566
  }
@@ -2339,7 +2617,7 @@ var BiRelationIndex = class {
2339
2617
  byRelationId = /* @__PURE__ */ new Map();
2340
2618
  link(input) {
2341
2619
  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`);
2620
+ 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
2621
  return this.linkValidated(input);
2344
2622
  }
2345
2623
  /** Author `generated(output, input)` without exposing positional arguments. */
@@ -2362,12 +2640,12 @@ var BiRelationIndex = class {
2362
2640
  trace: input.trace
2363
2641
  });
2364
2642
  }
2365
- /** Author `audio-script-render(output, script)` without exposing positional arguments. */
2366
- linkAudioScriptRender(input) {
2643
+ /** Author `phonetic-script-render(output, phoneticScript)` without exposing positional arguments. */
2644
+ linkPhoneticScriptRender(input) {
2367
2645
  return this.linkValidated({
2368
2646
  relationId: input.relationId,
2369
- spec: audioScriptRenderRelationSpec,
2370
- endpoints: [input.output, input.script],
2647
+ spec: phoneticScriptRenderRelationSpec,
2648
+ endpoints: [input.output, input.phoneticScript],
2371
2649
  metadata: {},
2372
2650
  trace: input.trace
2373
2651
  });
@@ -2468,7 +2746,13 @@ function decodeEntityRelationRows(rows, options) {
2468
2746
  const entitiesById = /* @__PURE__ */ new Map();
2469
2747
  const extensionEntityKinds = new Set(options.entityKinds ?? []);
2470
2748
  for (const row of rows.entities) {
2471
- const entity = decodeEntityRow(row, extensionEntityKinds, issues);
2749
+ let entity = decodeEntityRow(row, extensionEntityKinds, issues);
2750
+ if (entity != null) try {
2751
+ entity = decodeEntityRow(assembleEntityContent(rows, row.entityId), extensionEntityKinds, issues);
2752
+ } catch (error) {
2753
+ issues.push(errorMessage(error));
2754
+ continue;
2755
+ }
2472
2756
  if (entity == null) continue;
2473
2757
  const ref = createEntityRef(entity);
2474
2758
  if (entitiesById.has(ref.entityId)) {
@@ -2648,9 +2932,9 @@ var EntityProjectionGraph = class {
2648
2932
  timeline;
2649
2933
  constructor(rows) {
2650
2934
  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");
2935
+ const decoded = decodeEntityRelationRows(rows, { compareMarkerPoints: (_marker, _range, left, right) => finite(left, "range point") - finite(right, "range point") });
2936
+ this.entities = new Map([...decoded.entitiesById.values()].map((ref) => [String(ref.entityId), entityToRow(ref.current())]));
2937
+ const roots = [...this.entities.values()].filter((row) => row.entityKind === "timeline");
2654
2938
  if (roots.length !== 1) fail("compatibility projection requires exactly one Timeline");
2655
2939
  this.timeline = roots[0];
2656
2940
  }
@@ -2860,7 +3144,7 @@ function projectEntityTimeline(rows, meta) {
2860
3144
  const asset = mediaAssetFacts(graph, content);
2861
3145
  requireUntrimmedAudio(content, marker, placement);
2862
3146
  const external = externalLocator(asset, "memota-speech");
2863
- const script = graph.one(content.entityId, "audio-script-render", "audio-script");
3147
+ 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
3148
  const voice = content.payload.voice;
2865
3149
  if (voice !== void 0 && (!object(voice) || voice.system !== "voice-library" || typeof voice.key !== "string")) fail("Voice must use an explicit voice-library locator");
2866
3150
  const captionIds = layout.tracks.flatMap((candidate) => candidate.role === "caption" ? candidate.clips.filter((caption) => caption.anchorClipEntityId === clip.entityId).map((caption) => caption.clipEntityId) : []);
@@ -2870,7 +3154,7 @@ function projectEntityTimeline(rows, meta) {
2870
3154
  media_duration_ms: placement.duration,
2871
3155
  audio_storage_key: storageKey(asset),
2872
3156
  origin_speech_id: external.key,
2873
- audio_script: scriptText(script),
3157
+ audio_script: assembled?.text ?? (graph.adjacent(content.entityId, "audio-script-source").length > 0 ? assembleScriptText(graph.one(content.entityId, "audio-script-source", "audio-script")) : ""),
2874
3158
  volume: volume.data,
2875
3159
  voice: object(voice) ? {
2876
3160
  id: typeof voice.key === "string" ? voice.key : void 0,
@@ -2879,12 +3163,7 @@ function projectEntityTimeline(rows, meta) {
2879
3163
  caption_ids: captionIds
2880
3164
  } };
2881
3165
  } 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");
3166
+ const text = assembleCaptionContent(rows, content.entityId).text;
2888
3167
  if (marker.payload.timeRemapping !== void 0) fail("Caption timeRemapping is not supported by this reader");
2889
3168
  const style = captionStyle(content.payload.style);
2890
3169
  const anchor = placement.anchorClipEntityId === void 0 ? void 0 : layout.clips.get(placement.anchorClipEntityId);
@@ -2972,14 +3251,6 @@ function storageKey(asset) {
2972
3251
  if (typeof asset.payload.storageKey !== "string" || asset.payload.storageKey.trim() === "") fail("Asset " + asset.entityId + " requires its factual storageKey");
2973
3252
  return asset.payload.storageKey;
2974
3253
  }
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
3254
  function captionStyle(value) {
2984
3255
  if (value === void 0) return void 0;
2985
3256
  if (!object(value)) fail("Caption style must be an object");
@@ -3038,6 +3309,667 @@ function videoSpeed(content, marker, placement) {
3038
3309
  };
3039
3310
  }
3040
3311
  //#endregion
3312
+ //#region src/entity-crdt/json-containers.ts
3313
+ const atomicFields = new Set([
3314
+ "sequence",
3315
+ "sourceRange",
3316
+ "targetRange",
3317
+ "duration",
3318
+ "extent",
3319
+ "timeRemapping",
3320
+ "textRange",
3321
+ "external"
3322
+ ]);
3323
+ const textFields = new Set(["text", "phonemeScript"]);
3324
+ const orderedFields = new Set([
3325
+ "segments",
3326
+ "selections",
3327
+ "baseEntityIds"
3328
+ ]);
3329
+ function equalJson(a, b) {
3330
+ return canonicalJson(a) === canonicalJson(b);
3331
+ }
3332
+ function canonicalJson(value) {
3333
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
3334
+ if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
3335
+ return JSON.stringify(value);
3336
+ }
3337
+ /** Compile only the changed fields; untouched containers retain their operation identity. */
3338
+ function writeObject(map, before, after) {
3339
+ for (const key of Object.keys(before)) if (!Object.hasOwn(after, key)) map.delete(key);
3340
+ for (const [key, value] of Object.entries(after)) {
3341
+ if (equalJson(before[key], value)) continue;
3342
+ if (atomicFields.has(key)) map.set(key, value);
3343
+ else if (typeof value === "string" && textFields.has(key)) map.ensureMergeableText(key).update(value);
3344
+ else if (Array.isArray(value) && orderedFields.has(key)) writeList(map.ensureMergeableMovableList(key), value);
3345
+ else if (isJsonObject(value)) writeObject(map.ensureMergeableMap(key), isJsonObject(before[key]) ? before[key] : {}, value);
3346
+ else map.set(key, value);
3347
+ }
3348
+ }
3349
+ function itemKey(value) {
3350
+ if (typeof value === "string") return value;
3351
+ if (isJsonObject(value) && typeof value.segmentId === "string") return value.segmentId;
3352
+ throw new Error("Collaborative lists require stable segmentId or unique string identities");
3353
+ }
3354
+ function readItem(list, index) {
3355
+ const value = list.get(index);
3356
+ return value instanceof LoroMap ? value.toJSON() : value;
3357
+ }
3358
+ function writeList(list, values) {
3359
+ const wanted = values.map(itemKey);
3360
+ if (new Set(wanted).size !== wanted.length) throw new Error("Duplicate collaborative list identity");
3361
+ for (let index = list.length - 1; index >= 0; index--) if (!wanted.includes(itemKey(readItem(list, index)))) list.delete(index, 1);
3362
+ for (let index = 0; index < values.length; index++) {
3363
+ const value = values[index];
3364
+ const key = wanted[index];
3365
+ let found = -1;
3366
+ for (let cursor = index; cursor < list.length; cursor++) if (itemKey(readItem(list, cursor)) === key) {
3367
+ found = cursor;
3368
+ break;
3369
+ }
3370
+ if (found === -1) if (isJsonObject(value)) writeObject(list.insertContainer(index, new LoroMap()), {}, value);
3371
+ else list.insert(index, value);
3372
+ else {
3373
+ if (found !== index) list.move(found, index);
3374
+ const current = list.get(index);
3375
+ if (isJsonObject(value)) {
3376
+ if (!(current instanceof LoroMap)) throw new Error("Collaborative list item changed type");
3377
+ writeObject(current, current.toJSON(), value);
3378
+ }
3379
+ }
3380
+ }
3381
+ }
3382
+ function requireText(map, path) {
3383
+ let value = map;
3384
+ for (const part of path) if (value instanceof LoroMap && typeof part === "string") value = value.get(part);
3385
+ else if (value instanceof LoroMovableList && typeof part === "number") value = value.get(part);
3386
+ else throw new Error("Invalid collaborative text path");
3387
+ if (!(value instanceof LoroText)) throw new Error("Field is not collaborative text");
3388
+ return value;
3389
+ }
3390
+ //#endregion
3391
+ //#region src/entity-crdt/ordering.ts
3392
+ /** Domain order is scoped by membership; relation storage has no global order. */
3393
+ function entityOrderGroups(rows) {
3394
+ const entities = new Map(rows.entities.map((row) => [String(row.entityId), row]));
3395
+ const groups = /* @__PURE__ */ new Map();
3396
+ for (const relation of rows.relations) {
3397
+ const childKind = relation.relationKind === "timeline-track" ? "track" : relation.relationKind === "track-clip" ? "clip" : null;
3398
+ if (!childKind) continue;
3399
+ const endpoints = [relation.endpoint0EntityId, relation.endpoint1EntityId];
3400
+ const childId = endpoints.find((id) => entities.get(id)?.entityKind === childKind);
3401
+ if (!childId) continue;
3402
+ const child = entities.get(childId);
3403
+ if (typeof child.payload.order !== "number") continue;
3404
+ const parent = endpoints.find((id) => id !== childId);
3405
+ const values = groups.get(parent) ?? [];
3406
+ values.push({
3407
+ id: childId,
3408
+ order: child.payload.order
3409
+ });
3410
+ groups.set(parent, values);
3411
+ }
3412
+ return new Map([...groups].map(([parent, values]) => [parent, values.sort((a, b) => a.order - b.order || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)).map((item) => item.id)]));
3413
+ }
3414
+ function ordering(doc) {
3415
+ const map = doc.getMap("medeo").get("ordering");
3416
+ if (!(map instanceof LoroMap)) throw new Error("Missing native entity ordering");
3417
+ return map;
3418
+ }
3419
+ /** Returns a derived ordinal while the stable element retains its native move identity. */
3420
+ function entityOrdinal(doc, root) {
3421
+ let found;
3422
+ for (const key of ordering(doc).keys()) {
3423
+ const list = ordering(doc).get(key);
3424
+ if (!(list instanceof LoroMovableList)) throw new Error("Invalid entity order container");
3425
+ const index = list.toArray().indexOf(root);
3426
+ if (index < 0) continue;
3427
+ if (found !== void 0) throw new Error("Entity belongs to multiple ordered groups");
3428
+ found = index;
3429
+ }
3430
+ return found;
3431
+ }
3432
+ /** Move existing elements, insert new identities and delete removed members on the causal list. */
3433
+ function writeEntityOrder(doc, group, roots, token) {
3434
+ if (new Set(roots).size !== roots.length) throw new Error("Duplicate ordered entity identity");
3435
+ const list = ordering(doc).ensureMergeableMovableList(group);
3436
+ const before = list.toArray();
3437
+ for (let index = list.length - 1; index >= 0; index--) if (!roots.includes(String(list.get(index)))) list.delete(index, 1);
3438
+ roots.forEach((root, index) => {
3439
+ const current = list.toArray().indexOf(root);
3440
+ if (current < 0) list.insert(index, root);
3441
+ else if (current !== index) list.move(current, index);
3442
+ });
3443
+ if (token) {
3444
+ const editing = doc.getMap("medeo").get("editing");
3445
+ for (const root of new Set([...before, ...roots])) {
3446
+ if (before.indexOf(root) < 0 || before.indexOf(root) === roots.indexOf(root)) continue;
3447
+ editing.get(root).get("changes").set(token, true);
3448
+ }
3449
+ }
3450
+ }
3451
+ //#endregion
3452
+ //#region src/entity-crdt/entity-document.ts
3453
+ const LORO_ENTITY_SCHEMA = "medeo.entities.loro.v1";
3454
+ const MATERIALIZE_ORIGIN = "medeo.materialize";
3455
+ function hash(value) {
3456
+ return bytesToHex(sha256(new TextEncoder().encode(canonicalJson(value))));
3457
+ }
3458
+ function clone(value) {
3459
+ return structuredClone(value);
3460
+ }
3461
+ function root(doc) {
3462
+ return doc.getMap("medeo");
3463
+ }
3464
+ function requireMap(parent, key) {
3465
+ const value = parent.get(key);
3466
+ if (!(value instanceof LoroMap)) throw new Error(`Missing Loro map ${key}`);
3467
+ return value;
3468
+ }
3469
+ function section(doc, key) {
3470
+ return requireMap(root(doc), key);
3471
+ }
3472
+ function workspace(doc, id) {
3473
+ const value = section(doc, "editing").get(id);
3474
+ if (!(value instanceof LoroMap)) throw new Error(`Unknown entity editing identity ${id}`);
3475
+ return value;
3476
+ }
3477
+ function content(map) {
3478
+ return requireMap(map, "content").toJSON();
3479
+ }
3480
+ function entityContent(doc, key) {
3481
+ const payload = content(workspace(doc, key));
3482
+ const order = entityOrdinal(doc, key);
3483
+ return order === void 0 ? payload : {
3484
+ ...payload,
3485
+ order
3486
+ };
3487
+ }
3488
+ function bases(map) {
3489
+ const links = requireMap(map, "bases");
3490
+ return links.keys().sort((a, b) => Number(a) - Number(b)).map((key) => String(links.get(key)));
3491
+ }
3492
+ function owned(payload) {
3493
+ const { baseEntityIds: _bases, ...fields } = payload;
3494
+ return fields;
3495
+ }
3496
+ function baseIds(payload) {
3497
+ const ids = payload.baseEntityIds ?? [];
3498
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string") || new Set(ids).size !== ids.length) throw new Error("baseEntityIds must contain unique entity IDs");
3499
+ return ids;
3500
+ }
3501
+ function family(doc, id) {
3502
+ const value = section(doc, "origins").get(id);
3503
+ if (typeof value === "string") return value;
3504
+ for (const [key, value] of currentIds(doc)) if (value === id) return key;
3505
+ throw new Error(`Unknown entity ${id}`);
3506
+ }
3507
+ function currentIds(doc) {
3508
+ const result = /* @__PURE__ */ new Map();
3509
+ for (const key of section(doc, "editing").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3510
+ const map = workspace(doc, key);
3511
+ if (map.get("deleted") === true) continue;
3512
+ const tokens = requireMap(map, "changes").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3513
+ const initial = section(doc, "entities").get(key);
3514
+ const { id: _id, kind: _kind, ...initialPayload } = isJsonObject(initial) ? initial : {};
3515
+ const payload = entityContent(doc, key);
3516
+ const changed = tokens.length > 0 || initial !== void 0 && !equalJson(owned(initialPayload), payload);
3517
+ result.set(key, changed ? `entity_${hash([
3518
+ key,
3519
+ tokens,
3520
+ payload
3521
+ ])}` : key);
3522
+ }
3523
+ return result;
3524
+ }
3525
+ function currentId(doc, id, ids = currentIds(doc)) {
3526
+ const resolved = ids.get(family(doc, id));
3527
+ if (resolved === void 0) throw new Error(`Referenced entity ${id} was deleted`);
3528
+ return resolved;
3529
+ }
3530
+ /** Plain domain objects assembled from native Loro state, never a second writable authority. */
3531
+ function readEntityDocument(doc, allowSelectionConflicts = false) {
3532
+ if (root(doc).get("schema") !== "medeo.entities.loro.v1") throw new Error("Document has no Loro entity state");
3533
+ const ids = currentIds(doc);
3534
+ const entities = [...ids].map(([key, entityId]) => {
3535
+ const map = workspace(doc, key);
3536
+ const baseEntityIds = bases(map).map((id) => currentId(doc, id, ids));
3537
+ return {
3538
+ entityId: createEntityId(entityId),
3539
+ entityKind: map.get("kind"),
3540
+ payload: {
3541
+ ...entityContent(doc, key),
3542
+ ...baseEntityIds.length ? { baseEntityIds } : {}
3543
+ }
3544
+ };
3545
+ });
3546
+ const relations = [];
3547
+ for (const key of section(doc, "relations").keys().sort((a, b) => a < b ? -1 : a > b ? 1 : 0)) {
3548
+ const map = section(doc, "relations").get(key);
3549
+ if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
3550
+ const original = map.get("identity");
3551
+ if (original.endpoints.some((id) => !ids.has(family(doc, id)))) continue;
3552
+ const endpoints = original.endpoints.map((id) => currentId(doc, id, ids));
3553
+ const relationId = equalJson(endpoints, original.endpoints) ? key : `relation_${hash([key, endpoints])}`;
3554
+ relations.push({
3555
+ relationId: createRelationId(relationId),
3556
+ relationKind: original.kind,
3557
+ endpoint0EntityId: createEntityId(endpoints[0]),
3558
+ endpoint1EntityId: createEntityId(endpoints[1]),
3559
+ metadata: requireMap(map, "metadata").toJSON(),
3560
+ trace: requireMap(map, "trace").toJSON()
3561
+ });
3562
+ }
3563
+ const project = {};
3564
+ for (const [key, value] of Object.entries(section(doc, "project").toJSON())) {
3565
+ if (typeof value !== "string") throw new Error(`Invalid project attachment ${key}`);
3566
+ project[key] = currentId(doc, value, ids);
3567
+ }
3568
+ const state = {
3569
+ rows: {
3570
+ entities,
3571
+ relations
3572
+ },
3573
+ project
3574
+ };
3575
+ if (!allowSelectionConflicts) for (const [key, id] of ids) {
3576
+ const fingerprint = selectionFingerprint(state.rows, id);
3577
+ if (fingerprint !== void 0 && workspace(doc, key).get("selectionBaseline") !== fingerprint) throw new Error(`Caption text selection conflict at ${id}: source text changed; revise its text ranges explicitly`);
3578
+ }
3579
+ return state;
3580
+ }
3581
+ /** Native editing containers survive domain version changes and merge independently. */
3582
+ var LoroEntityDocument = class LoroEntityDocument {
3583
+ doc;
3584
+ validate;
3585
+ undoManager;
3586
+ constructor(doc, validate) {
3587
+ this.doc = doc;
3588
+ this.validate = validate;
3589
+ this.undoManager = new UndoManager(doc, {
3590
+ mergeInterval: 0,
3591
+ excludeOriginPrefixes: [MATERIALIZE_ORIGIN]
3592
+ });
3593
+ }
3594
+ /** Open a causal snapshot using the same Loro runtime as the compiler. */
3595
+ static fromSnapshot(snapshot, validate) {
3596
+ const doc = new LoroDoc();
3597
+ doc.import(snapshot);
3598
+ readEntityDocument(doc);
3599
+ return new LoroEntityDocument(doc, validate);
3600
+ }
3601
+ static create(rows, project = {}, peerId) {
3602
+ const doc = new LoroDoc();
3603
+ if (peerId !== void 0) doc.setPeerId(peerId);
3604
+ root(doc).set("schema", LORO_ENTITY_SCHEMA);
3605
+ for (const name of [
3606
+ "editing",
3607
+ "entities",
3608
+ "origins",
3609
+ "relations",
3610
+ "project",
3611
+ "ordering"
3612
+ ]) root(doc).ensureMergeableMap(name);
3613
+ for (const row of rows.entities) apply(doc, {
3614
+ type: "create",
3615
+ row
3616
+ });
3617
+ for (const row of rows.relations) apply(doc, {
3618
+ type: "relation",
3619
+ row
3620
+ });
3621
+ for (const [slot, entityId] of Object.entries(project)) apply(doc, {
3622
+ type: "attach",
3623
+ slot,
3624
+ entityId
3625
+ });
3626
+ for (const [group, ids] of entityOrderGroups(rows)) writeEntityOrder(doc, group, ids);
3627
+ const initialState = readEntityDocument(doc, true);
3628
+ for (const row of initialState.rows.entities) {
3629
+ const fingerprint = selectionFingerprint(initialState.rows, row.entityId);
3630
+ if (fingerprint !== void 0) workspace(doc, row.entityId).set("selectionBaseline", fingerprint);
3631
+ }
3632
+ assertState(readEntityDocument(doc));
3633
+ materialize(doc);
3634
+ doc.commit({ origin: MATERIALIZE_ORIGIN });
3635
+ return new LoroEntityDocument(doc);
3636
+ }
3637
+ snapshot() {
3638
+ return readEntityDocument(this.doc);
3639
+ }
3640
+ relationsOf(entityId) {
3641
+ const id = currentIds(this.doc).get(family(this.doc, entityId));
3642
+ if (id === void 0) return [];
3643
+ return this.snapshot().rows.relations.filter((row) => row.endpoint0EntityId === id || row.endpoint1EntityId === id);
3644
+ }
3645
+ getVersion(id) {
3646
+ const entity = section(this.doc, "entities").get(id);
3647
+ if (!isJsonObject(entity)) return void 0;
3648
+ const { id: entityId, kind, ...payload } = entity;
3649
+ if (typeof entityId !== "string") throw new Error("Invalid stored entity ID");
3650
+ return {
3651
+ entityId: createEntityId(entityId),
3652
+ entityKind: kind,
3653
+ payload
3654
+ };
3655
+ }
3656
+ /** The callback and every domain validation execute on a causally complete fork first. */
3657
+ transact(edit) {
3658
+ const probe = this.doc.fork();
3659
+ const draft = new LoroEntityDraft(probe);
3660
+ edit(draft);
3661
+ draft.captureSelectionBaselines();
3662
+ const state = readEntityDocument(probe);
3663
+ assertState(state);
3664
+ this.validate?.(state);
3665
+ materialize(probe);
3666
+ const from = this.doc.oplogVersion();
3667
+ for (const command of draft.commands) apply(this.doc, command);
3668
+ this.doc.commit({ origin: "medeo.edit" });
3669
+ materialize(this.doc);
3670
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3671
+ return this.doc.export({
3672
+ mode: "update",
3673
+ from
3674
+ });
3675
+ }
3676
+ /** Import preserves native operation identities; projection errors never discard remote operations. */
3677
+ import(update) {
3678
+ this.doc.import(update);
3679
+ const state = this.snapshot();
3680
+ assertState(state);
3681
+ this.validate?.(state);
3682
+ materialize(this.doc);
3683
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3684
+ }
3685
+ canUndo() {
3686
+ return this.undoManager.canUndo();
3687
+ }
3688
+ canRedo() {
3689
+ return this.undoManager.canRedo();
3690
+ }
3691
+ undo() {
3692
+ return this.historyEdit(() => this.undoManager.undo());
3693
+ }
3694
+ redo() {
3695
+ return this.historyEdit(() => this.undoManager.redo());
3696
+ }
3697
+ historyEdit(run) {
3698
+ const before = this.snapshot();
3699
+ if (!run()) return false;
3700
+ const after = this.snapshot();
3701
+ for (const row of after.rows.entities) {
3702
+ const id = family(this.doc, row.entityId);
3703
+ const previous = before.rows.entities.find((item) => family(this.doc, item.entityId) === id);
3704
+ if (previous && !equalJson(owned(previous.payload), owned(row.payload))) workspace(this.doc, id).ensureMergeableMap("changes").set(globalThis.crypto.randomUUID(), true);
3705
+ }
3706
+ materialize(this.doc);
3707
+ this.doc.commit({ origin: MATERIALIZE_ORIGIN });
3708
+ return true;
3709
+ }
3710
+ };
3711
+ var LoroEntityDraft = class {
3712
+ doc;
3713
+ commands = [];
3714
+ selectedRoots = /* @__PURE__ */ new Set();
3715
+ constructor(doc) {
3716
+ this.doc = doc;
3717
+ }
3718
+ snapshot() {
3719
+ return readEntityDocument(this.doc, true);
3720
+ }
3721
+ order(groupId, entityIds) {
3722
+ this.record({
3723
+ type: "order",
3724
+ group: family(this.doc, groupId),
3725
+ roots: entityIds.map((id) => family(this.doc, id)),
3726
+ token: globalThis.crypto.randomUUID()
3727
+ });
3728
+ }
3729
+ create(row) {
3730
+ this.record({
3731
+ type: "create",
3732
+ row: clone(row)
3733
+ });
3734
+ }
3735
+ delete(entityId) {
3736
+ this.record({
3737
+ type: "delete",
3738
+ root: family(this.doc, entityId)
3739
+ });
3740
+ }
3741
+ attach(slot, entityId) {
3742
+ this.record({
3743
+ type: "attach",
3744
+ slot,
3745
+ entityId
3746
+ });
3747
+ }
3748
+ link(row) {
3749
+ this.record({
3750
+ type: "relation",
3751
+ row: clone(row)
3752
+ });
3753
+ }
3754
+ patchRelation(relationId, metadata, trace) {
3755
+ this.record({
3756
+ type: "relation-fields",
3757
+ root: this.relationRoot(relationId),
3758
+ metadata: clone(metadata),
3759
+ trace: clone(trace)
3760
+ });
3761
+ }
3762
+ unlink(relationId) {
3763
+ this.record({
3764
+ type: "unlink",
3765
+ root: this.relationRoot(relationId)
3766
+ });
3767
+ }
3768
+ relationRoot(relationId) {
3769
+ const ids = currentIds(this.doc);
3770
+ for (const key of section(this.doc, "relations").keys()) {
3771
+ const map = section(this.doc, "relations").get(key);
3772
+ if (!(map instanceof LoroMap) || map.get("deleted") === true) continue;
3773
+ const identity = map.get("identity");
3774
+ if (identity.endpoints.some((id) => !ids.has(family(this.doc, id)))) continue;
3775
+ const endpoints = identity.endpoints.map((id) => currentId(this.doc, id, ids));
3776
+ if ((equalJson(endpoints, identity.endpoints) ? key : `relation_${hash([key, endpoints])}`) === relationId) return key;
3777
+ }
3778
+ throw new Error(`Unknown relation ${relationId}`);
3779
+ }
3780
+ update(entityId, fields) {
3781
+ const state = this.snapshot();
3782
+ const id = createEntityId(currentId(this.doc, entityId));
3783
+ const updates = /* @__PURE__ */ new Map();
3784
+ for (const [field, value] of Object.entries(fields)) {
3785
+ const owner = resolveEntityFieldOwner(state.rows, id, field);
3786
+ const payload = updates.get(owner) ?? clone(state.rows.entities.find((row) => row.entityId === owner).payload);
3787
+ updates.set(owner, {
3788
+ ...payload,
3789
+ [field]: clone(value)
3790
+ });
3791
+ }
3792
+ for (const [owner, payload] of updates) this.replaceOwned(owner, payload);
3793
+ }
3794
+ declare(entityId, fields) {
3795
+ const row = this.snapshot().rows.entities.find((row) => row.entityId === currentId(this.doc, entityId));
3796
+ if (!row) throw new Error(`Unknown entity ${entityId}`);
3797
+ this.replaceOwned(entityId, {
3798
+ ...row.payload,
3799
+ ...fields
3800
+ });
3801
+ }
3802
+ replaceOwned(entityId, payload) {
3803
+ this.record({
3804
+ type: "write",
3805
+ root: family(this.doc, entityId),
3806
+ payload: clone(payload),
3807
+ token: globalThis.crypto.randomUUID()
3808
+ });
3809
+ }
3810
+ editText(entityId, path, start, deleteCount, text) {
3811
+ if (typeof path[0] !== "string") throw new Error("Text path must start with a field");
3812
+ const owner = resolveEntityFieldOwner(this.snapshot().rows, createEntityId(currentId(this.doc, entityId)), path[0]);
3813
+ this.record({
3814
+ type: "text",
3815
+ root: family(this.doc, owner),
3816
+ path: [...path],
3817
+ start,
3818
+ deleteCount,
3819
+ text,
3820
+ token: globalThis.crypto.randomUUID()
3821
+ });
3822
+ }
3823
+ captureSelectionBaselines() {
3824
+ const state = this.snapshot();
3825
+ for (const key of this.selectedRoots) {
3826
+ const id = currentIds(this.doc).get(key);
3827
+ if (id === void 0) continue;
3828
+ const fingerprint = selectionFingerprint(state.rows, id);
3829
+ if (fingerprint !== void 0) this.record({
3830
+ type: "selection-baseline",
3831
+ root: key,
3832
+ fingerprint
3833
+ });
3834
+ }
3835
+ }
3836
+ record(command) {
3837
+ if (command.type === "create") this.selectedRoots.add(command.row.entityId);
3838
+ if (command.type === "write" && !equalJson(content(workspace(this.doc, command.root)).selections, command.payload.selections)) this.selectedRoots.add(command.root);
3839
+ apply(this.doc, command);
3840
+ this.commands.push(command);
3841
+ }
3842
+ };
3843
+ function apply(doc, command) {
3844
+ switch (command.type) {
3845
+ case "selection-baseline":
3846
+ workspace(doc, command.root).set("selectionBaseline", command.fingerprint);
3847
+ break;
3848
+ case "order":
3849
+ writeEntityOrder(doc, command.group, command.roots, command.token);
3850
+ break;
3851
+ case "create": {
3852
+ const row = command.row;
3853
+ if (section(doc, "origins").get(row.entityId) !== void 0) throw new Error(`Duplicate entity ${row.entityId}`);
3854
+ const map = section(doc, "editing").ensureMergeableMap(row.entityId);
3855
+ map.set("kind", row.entityKind);
3856
+ map.ensureMergeableMap("changes");
3857
+ section(doc, "origins").set(row.entityId, row.entityId);
3858
+ writeObject(map.ensureMergeableMap("content"), {}, owned(row.payload));
3859
+ writeBases(map, baseIds(row.payload));
3860
+ break;
3861
+ }
3862
+ case "write": {
3863
+ const map = workspace(doc, command.root);
3864
+ const before = content(map);
3865
+ const after = owned(command.payload);
3866
+ if (!equalJson(before, after)) {
3867
+ writeObject(map.ensureMergeableMap("content"), before, after);
3868
+ map.ensureMergeableMap("changes").set(command.token, true);
3869
+ }
3870
+ writeBases(map, baseIds(command.payload));
3871
+ break;
3872
+ }
3873
+ case "text": {
3874
+ const map = workspace(doc, command.root);
3875
+ const text = requireText(map.ensureMergeableMap("content"), command.path);
3876
+ if (!Number.isSafeInteger(command.start) || !Number.isSafeInteger(command.deleteCount) || command.start < 0 || command.deleteCount < 0 || command.start + command.deleteCount > Array.from(text.toString()).length) throw new Error("Invalid text edit range");
3877
+ const start = text.convertPos(command.start, "unicode", "utf16");
3878
+ const end = text.convertPos(command.start + command.deleteCount, "unicode", "utf16");
3879
+ text.splice(start, end - start, command.text);
3880
+ map.ensureMergeableMap("changes").set(command.token, true);
3881
+ break;
3882
+ }
3883
+ case "delete":
3884
+ workspace(doc, command.root).set("deleted", true);
3885
+ break;
3886
+ case "relation": {
3887
+ const row = command.row;
3888
+ const map = section(doc, "relations").ensureMergeableMap(row.relationId);
3889
+ const identity = {
3890
+ kind: row.relationKind,
3891
+ endpoints: [row.endpoint0EntityId, row.endpoint1EntityId]
3892
+ };
3893
+ const previous = map.get("identity");
3894
+ if (previous !== void 0 && !equalJson(previous, identity)) throw new Error("Relation identity cannot change kind or endpoint positions");
3895
+ if (previous === void 0) map.set("identity", identity);
3896
+ if (map.get("deleted") === true) throw new Error("Deleted relation IDs cannot be reused");
3897
+ const metadata = map.ensureMergeableMap("metadata");
3898
+ const trace = map.ensureMergeableMap("trace");
3899
+ writeObject(metadata, metadata.toJSON(), row.metadata);
3900
+ writeObject(trace, trace.toJSON(), row.trace);
3901
+ break;
3902
+ }
3903
+ case "unlink": {
3904
+ const map = section(doc, "relations").get(command.root);
3905
+ if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
3906
+ map.set("deleted", true);
3907
+ break;
3908
+ }
3909
+ case "relation-fields": {
3910
+ const map = section(doc, "relations").get(command.root);
3911
+ if (!(map instanceof LoroMap)) throw new Error("Unknown relation");
3912
+ const metadata = map.ensureMergeableMap("metadata");
3913
+ const trace = map.ensureMergeableMap("trace");
3914
+ writeObject(metadata, metadata.toJSON(), command.metadata);
3915
+ writeObject(trace, trace.toJSON(), command.trace);
3916
+ break;
3917
+ }
3918
+ case "attach":
3919
+ section(doc, "project").set(command.slot, command.entityId);
3920
+ break;
3921
+ }
3922
+ }
3923
+ function writeBases(map, ids) {
3924
+ const links = map.ensureMergeableMap("bases");
3925
+ for (const key of links.keys()) if (Number(key) >= ids.length) links.delete(key);
3926
+ ids.forEach((id, index) => {
3927
+ if (links.get(String(index)) !== id) links.set(String(index), id);
3928
+ });
3929
+ }
3930
+ /** Numeric sub-spans must not silently quote different text after a collaborative edit. */
3931
+ function selectionFingerprint(rows, id) {
3932
+ const row = rows.entities.find((row) => row.entityId === id);
3933
+ if (row?.entityKind !== "caption" || !Array.isArray(row.payload.selections)) return void 0;
3934
+ const ranged = row.payload.selections.filter((selection) => isJsonObject(selection) && selection.textRange !== void 0);
3935
+ if (ranged.length === 0) return void 0;
3936
+ const segments = assembleEntityContent(rows, row.entityId).payload.segments;
3937
+ if (!Array.isArray(segments)) throw new Error("Caption text selection has no source segments");
3938
+ return hash(ranged.map((selection) => {
3939
+ if (!isJsonObject(selection)) throw new Error("Invalid Caption text selection");
3940
+ const source = segments.find((segment) => isJsonObject(segment) && segment.segmentId === selection.segmentId);
3941
+ if (!isJsonObject(source) || typeof source.text !== "string") throw new Error("Caption text selection source is missing");
3942
+ return [selection, source.text];
3943
+ }));
3944
+ }
3945
+ function assertState(state) {
3946
+ for (const row of state.rows.entities) assembleEntityContent(state.rows, row.entityId);
3947
+ const ids = new Set(state.rows.entities.map((row) => String(row.entityId)));
3948
+ for (const row of state.rows.relations) if (!ids.has(row.endpoint0EntityId) || !ids.has(row.endpoint1EntityId)) throw new Error("Relation has a missing endpoint");
3949
+ }
3950
+ function materialize(doc) {
3951
+ const state = readEntityDocument(doc);
3952
+ const byId = section(doc, "entities");
3953
+ const ids = currentIds(doc);
3954
+ for (const [source, id] of ids) {
3955
+ const origins = section(doc, "origins");
3956
+ if (origins.get(id) !== source) origins.set(id, source);
3957
+ }
3958
+ for (const row of state.rows.entities) {
3959
+ const next = {
3960
+ id: row.entityId,
3961
+ kind: row.entityKind,
3962
+ ...row.payload
3963
+ };
3964
+ const previous = byId.get(row.entityId);
3965
+ if (isJsonObject(previous) && !equalJson(owned(previous), owned(next))) throw new Error("Immutable entity version collision");
3966
+ if (!equalJson(previous, next)) byId.set(row.entityId, next);
3967
+ writeBases(workspace(doc, family(doc, row.entityId)), baseIds(row.payload));
3968
+ }
3969
+ const project = section(doc, "project");
3970
+ for (const [slot, id] of Object.entries(state.project)) if (project.get(slot) !== id) project.set(slot, id);
3971
+ }
3972
+ //#endregion
3041
3973
  //#region src/document/mirror-read.ts
3042
3974
  /**
3043
3975
  * Project the mirror state (`VideoDocumentDraft`) into the authoritative
@@ -3129,7 +4061,9 @@ var MirrorVideoDocumentAdapter = class {
3129
4061
  });
3130
4062
  }
3131
4063
  snapshot() {
3132
- return readVideoDocumentFromDraft(this.mirror.getState());
4064
+ const document = readVideoDocumentFromDraft(this.mirror.getState());
4065
+ if (this.doc.getMap("medeo").get("schema") === "medeo.entities.loro.v1") return projectEntityTimeline(readEntityDocument(this.doc).rows, document.meta);
4066
+ return document;
3133
4067
  }
3134
4068
  /**
3135
4069
  * True once the doc holds real document content. A fresh mirror over an empty
@@ -3351,4 +4285,4 @@ function createPlainMemoryAdapter(document, options) {
3351
4285
  return new PlainMemoryAdapter(document, options);
3352
4286
  }
3353
4287
  //#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 };
4288
+ export { reassignSpeechesToVideoClipsByTime as $, isJsonObject as A, VideoDocumentValidationError as B, assembleEntityContent as C, selectAudioScriptSegments as D, findComposedAudioScript as E, buildInitialVideoDocument as F, LANE_KINDS_IN_STACK_ORDER as G, validateVideoDocument as H, buildSpeechHostMap as I, laneTrackId as J, ensureLaneTrack as K, derivePositionFromAbs as L, createRelationId as M, hasSequence as N, updateEntityFields as O, isMediaAssetVariantKind as P, fillMainTrackTimeGaps as Q, fromVideoDocument as R, assembleCaptionContent as S, assembleScriptText as T, partUnionSchema as U, assertValidVideoDocument as V, videoDocumentSchema as W, cascadeAfterVideoClipChanges as X, solveVideoDocument as Y, arrangeMainTrackSeamlessly as Z, EntityProjectionGraph as _, bytesToBase64 as _t, applyEntityTimelineProjection as a, isEmptyVideoClip as at, decodeEntityRelationRows as b, writeVideoDocumentToDraft as c, DEFAULT_UNIT_TIME_MS as ct, LoroEntityDocument as d, effectiveVideoClipDurationMs as dt, recalculateTimelineDuration as et, LoroEntityDraft as f, speedOf as ft, projectEntityTimeline as g, base64ToBytes as gt, equalJson as h, videoDocumentMirrorSchema as ht, MirrorVideoDocumentAdapter as i, TIMELINE_SKELETON_DURATION_MS as it, createEntityId as j, variantBaseEntityIds as k, readVideoDocumentFromDraft as l, ENTITY_TIMELINE_PROJECTION_SCHEMA_VERSION as lt, entityOrderGroups as m, recordEntries as mt, createPlainMemoryAdapter as n, resolveSpeechOverlapByShiftingVideos as nt, createMirrorVideoDocument as o, partDurationMs as ot, readEntityDocument as p, partUnionToDraft as pt, findLaneTrack as q, generatePartId as r, syncAggregatedClipsTimePosition as rt, createMirrorVideoDocumentAdapter as s, safeDurationMs as st, PlainMemoryAdapter as t, resolveAllSpeechOverlaps as tt, LORO_ENTITY_SCHEMA as u, VIDEO_DOCUMENT_SCHEMA_VERSION as ut, resolveEntityTimelineLayout as v, assemblePhoneticScriptContent as w, ScriptCompositionError as x, EntityTimelineProjectionError as y, toVideoDocument as z };