@mengine/medeo-tool 1.4.1-alpha.1 → 1.4.1-alpha.3

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,138 +1,4 @@
1
- import { SchemaValidator, SemanticEditor, assertCanonicalEditorResources, createEditSandbox, effectiveVideoClipDurationMs, ensureEditorFoundation, fromVideoDocument, importMediaAsset, readDocumentAudioScript, solveVideoDocument, speedOf } from "@mengine/medeo-client";
2
- //#region src/document/compact-projection.ts
3
- const DEFAULT_TEXT_PREVIEW_LENGTH = 24;
4
- /** Kind tag shown in the first column (`video_clip` → `clip`). */
5
- function kindTag(kind) {
6
- return kind === "video_clip" ? "clip" : kind;
7
- }
8
- /** Lane label: `video_clip` tracks display as `main`, otherwise `parts_kind`. */
9
- function laneLabel(partsKind) {
10
- return partsKind === "video_clip" ? "main" : partsKind;
11
- }
12
- /** Speed token: absent → `1`; linear → numeric multiplier; anything else → `custom`. */
13
- function speedToken(speedShift) {
14
- if (speedShift == null) return "1";
15
- if (speedShift.category === "linear") return String(speedOf(speedShift));
16
- return "custom";
17
- }
18
- function effectiveDurationMs(part, timelineDurationMs) {
19
- if (part.video_clip != null) return effectiveVideoClipDurationMs(part.video_clip);
20
- if (part.speech != null) return part.speech.media_duration_ms ?? 0;
21
- if (part.caption != null) return part.caption.initial_duration_ms ?? 0;
22
- if (part.bgm != null) return timelineDurationMs;
23
- return 0;
24
- }
25
- function truncateText(text, budget) {
26
- if (text.length <= budget) return text;
27
- return `${text.slice(0, budget)}…`;
28
- }
29
- function anchorToken(timePosition) {
30
- if (timePosition.mode === "anchored") return `anchor=${timePosition.anchorPartId}+${timePosition.offsetMs}`;
31
- if (timePosition.mode === "absolute") return "anchor=abs";
32
- return "anchor=abs";
33
- }
34
- function clipAttrs(clip) {
35
- const playIn = clip.play_in ?? 0;
36
- const playOut = clip.play_out ?? 0;
37
- return `media=${clip.origin_media_id ?? ""} trim=${playIn}-${playOut} speed=${speedToken(clip.speed_shift)} vol=${clip.volume ?? 0}`;
38
- }
39
- function partAttrs(part, item, textPreviewLength) {
40
- if (part.video_clip != null) return clipAttrs(part.video_clip);
41
- if (part.speech != null) return `${anchorToken(item.time_position)} dur=${part.speech.media_duration_ms ?? 0}`;
42
- if (part.caption != null) {
43
- const preview = truncateText(part.caption.text ?? "", textPreviewLength);
44
- return `${anchorToken(item.time_position)} text="${preview}"`;
45
- }
46
- if (part.bgm != null) return `vol=${part.bgm.volume ?? 0}`;
47
- return "";
48
- }
49
- /**
50
- * Render a `VideoDocument` as compact text: one header line plus one row per
51
- * timeline part (optionally filtered by `onlyPartIds`). Deterministic and
52
- * side-effect free — same document always yields the same string.
53
- */
54
- function renderCompactProjection(document, options) {
55
- const onlyPartIds = options?.onlyPartIds;
56
- const textPreviewLength = options?.textPreviewLength ?? DEFAULT_TEXT_PREVIEW_LENGTH;
57
- const solved = solveVideoDocument(document);
58
- const library = document.part_library ?? {};
59
- const tracks = document.tracks ?? [];
60
- let totalParts = 0;
61
- const rows = [];
62
- for (const track of tracks) {
63
- const partsKind = track.parts_kind;
64
- if (partsKind == null) continue;
65
- const lane = laneLabel(partsKind);
66
- const tag = kindTag(partsKind);
67
- for (const item of track.items ?? []) {
68
- totalParts += 1;
69
- const partId = item.part_id;
70
- if (onlyPartIds != null && !onlyPartIds.has(partId)) continue;
71
- const part = library[partId];
72
- if (part == null) continue;
73
- const abs = solved.absByPartId.get(partId) ?? 0;
74
- const dur = effectiveDurationMs(part, solved.durationMs);
75
- const attrs = partAttrs(part, item, textPreviewLength);
76
- rows.push(`${tag} ${partId} ${lane} [${abs},${abs + dur}) ${attrs}`);
77
- }
78
- }
79
- return [`# draft=${document.meta.draft_id ?? ""} v=${document.meta.version ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
80
- }
81
- //#endregion
82
- //#region src/sandbox/preview.ts
83
- /**
84
- * Collect part ids referenced by a journal for compact preview filtering.
85
- * Walks known id-shaped payload keys (宁多勿少) and unions `generated_ids`.
86
- */
87
- const PART_ID_KEYS = new Set([
88
- "clip_id",
89
- "clip_ids",
90
- "before_clip_id",
91
- "after_clip_id",
92
- "speech_id",
93
- "speech_ids",
94
- "speech_part_id",
95
- "caption_id",
96
- "caption_ids",
97
- "bgm_id",
98
- "anchor_part_id",
99
- "part_id",
100
- "body_part_id"
101
- ]);
102
- /** Extract every part id a journal entry touches (payload refs + minted ids). */
103
- function collectAffectedPartIds(journal) {
104
- const ids = /* @__PURE__ */ new Set();
105
- for (const entry of journal) {
106
- for (const generated of entry.generated_ids ?? []) if (generated.length > 0) ids.add(generated);
107
- collectFromValue(entry.payload, ids);
108
- }
109
- return ids;
110
- }
111
- function collectFromValue(value, ids) {
112
- if (value == null) return;
113
- if (Array.isArray(value)) {
114
- for (const item of value) collectFromValue(item, ids);
115
- return;
116
- }
117
- if (typeof value !== "object") return;
118
- for (const [key, child] of Object.entries(value)) {
119
- if (PART_ID_KEYS.has(key)) {
120
- if (typeof child === "string" && child.length > 0) ids.add(child);
121
- else if (Array.isArray(child)) {
122
- for (const item of child) if (typeof item === "string" && item.length > 0) ids.add(item);
123
- }
124
- }
125
- collectFromValue(child, ids);
126
- }
127
- }
128
- /**
129
- * Render a ChangePlan preview: header + rows for journal-affected parts only.
130
- * Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
131
- */
132
- function renderPreview(document, journal) {
133
- return renderCompactProjection(document, { onlyPartIds: journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal) });
134
- }
135
- //#endregion
1
+ import { applyFieldChanges, assertCanonicalEditorResources, assertFieldChanges, ensureEditorFoundation, importMediaAsset, readDocumentAudioScript } from "@mengine/medeo-client";
136
2
  //#region ../medeo-dsl/src/entities.ts
137
3
  function hasSequence(entity) {
138
4
  if (isKnownNonSequenceKind(entity.entityKind) || isReservedEntityKind(entity.entityKind)) return false;
@@ -868,6 +734,8 @@ function validateVoicePayload(value, problems) {
868
734
  if (voice.name !== void 0 && typeof voice.name !== "string") problems.push("voice.name must be a string when present");
869
735
  }
870
736
  function validateCaptionPayload(value, problems) {
737
+ validateExternalLocator(value, problems);
738
+ if (value.segmentRanges !== void 0) validateSegmentRanges(value.segmentRanges, problems);
871
739
  const selections = value.selections;
872
740
  if (!Array.isArray(selections)) problems.push("selections must be a non-empty array of AudioScript segment selections");
873
741
  else {
@@ -1006,7 +874,7 @@ function isOwnedLocalEntityValuePath(entityKind, path) {
1006
874
  "axvideo"
1007
875
  ].includes(entityKind) && /^(?:sampling|coordinateSpace|coordinateSpace\.unit|extent\.kind)$/.test(path)) return true;
1008
876
  if (entityKind === "sequence-marker" && /^(?:durationPolicy|duration\.mode|timeRemapping\.(?:kind|mode))$/.test(path)) return true;
1009
- if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
877
+ if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
1010
878
  if (entityKind === "asset" && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
1011
879
  if (isMediaAssetVariantKind(entityKind) && /^(?:external\.(?:system|key)|storageKey)$/.test(path)) return true;
1012
880
  if (entityKind === "voice" && /^voice\.(?:system|key|name)$/.test(path)) return true;
@@ -1026,7 +894,7 @@ function isOwnedLocalIdPath(entityKind, path) {
1026
894
  "phonetic-script"
1027
895
  ].includes(entityKind) && /^segments\[\d+\]\.segmentId$/.test(path)) return true;
1028
896
  if (entityKind === "caption" && /^selections\[\d+\]\.segmentId$/.test(path)) return true;
1029
- if (entityKind === "sequence-marker" && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
897
+ if ((entityKind === "sequence-marker" || entityKind === "caption") && /^segmentRanges\[\d+\]\.segmentId$/.test(path)) return true;
1030
898
  if (entityKind === "asset" && /^(?:tracks\[\d+\]\.trackId|renditions\[\d+\]\.renditionId)$/.test(path)) return true;
1031
899
  return false;
1032
900
  }
@@ -1558,12 +1426,17 @@ function businessFacades(entities, relations) {
1558
1426
  create: (input) => {
1559
1427
  if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
1560
1428
  assertBusinessPayload(input.payload);
1429
+ if (input.entity_id && entities.get(input.entity_id)) throw new Error(`Entity id ${input.entity_id} already exists`);
1561
1430
  return entities.create(input);
1562
1431
  },
1563
1432
  update: (input) => {
1564
1433
  assertBusiness(input.entity_id);
1565
- assertBusinessPayload(input.payload);
1566
- entities.update(input);
1434
+ if (!Array.isArray(input.changes)) throw new Error("entities.update requires changes");
1435
+ for (const change of input.changes) {
1436
+ if (infrastructureFields.has(change.path?.[0])) throw new Error("Field is managed by host assembly");
1437
+ if (change.path?.[0] === "baseEntityIds" && change.op === "list.insert" && typeof change.value === "string") assertBusiness(change.value);
1438
+ }
1439
+ entities.changeFields(input);
1567
1440
  },
1568
1441
  declareFields: (input) => {
1569
1442
  assertBusiness(input.entity_id);
@@ -1579,9 +1452,7 @@ function businessFacades(entities, relations) {
1579
1452
  }
1580
1453
  for (const relation of incident) relations.unlink({ relation_id: relation.relation_id });
1581
1454
  entities.delete(input);
1582
- },
1583
- readCaptionContent: (input) => entities.readCaptionContent(input),
1584
- readPhoneticScriptContent: (input) => entities.readPhoneticScriptContent(input)
1455
+ }
1585
1456
  },
1586
1457
  relations: {
1587
1458
  list: () => relations.list().filter(visibleRelation),
@@ -1597,14 +1468,11 @@ function businessFacades(entities, relations) {
1597
1468
  if (relation && !visibleRelation(relation)) throw new Error("Asset bindings are managed by host assembly");
1598
1469
  relations.unlink(input);
1599
1470
  },
1600
- linkGenerated: (input) => {
1601
- assertBusiness(input.output_entity_id);
1602
- assertBusiness(input.input_entity_id);
1603
- return relations.linkGenerated(input);
1604
- },
1605
- linkClipAnchor: (input) => relations.linkClipAnchor(input),
1606
- linkPhoneticScriptRender: (input) => relations.linkPhoneticScriptRender(input),
1607
- linkAudioScriptSource: (input) => relations.linkAudioScriptSource(input)
1471
+ update: (input) => {
1472
+ const relation = relations.list().find((row) => row.relation_id === input.relation_id);
1473
+ if (relation && !visibleRelation(relation)) throw new Error("Asset bindings are managed by host assembly");
1474
+ relations.update(input);
1475
+ }
1608
1476
  }
1609
1477
  };
1610
1478
  }
@@ -1686,6 +1554,12 @@ var EntitySandbox = class {
1686
1554
  case "create-entity":
1687
1555
  lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
1688
1556
  break;
1557
+ case "change-relation":
1558
+ lines.push(`~ relation ${command.relation_id} ${command.changes.map((c) => c.op).join(", ")}`);
1559
+ break;
1560
+ case "change-entity":
1561
+ lines.push(`~ entity ${command.entity_id} ${command.changes.map((c) => c.op).join(", ")}`);
1562
+ break;
1689
1563
  case "update-entity":
1690
1564
  lines.push(`~ entity ${command.entity_id} payload`);
1691
1565
  break;
@@ -1740,6 +1614,7 @@ var EntitySandbox = class {
1740
1614
  });
1741
1615
  },
1742
1616
  create: (input) => this.createEntity(input),
1617
+ changeFields: (input) => this.changeEntity(input),
1743
1618
  update: (input) => {
1744
1619
  assertTrimmed(input.entity_id, "entity_id");
1745
1620
  if (!this.state.entities.some((entity) => entity.entity_id === input.entity_id)) throw new Error(`Entity id "${input.entity_id}" does not exist`);
@@ -1753,12 +1628,28 @@ var EntitySandbox = class {
1753
1628
  const current = this.state.entities.find((entity) => entity.entity_id === input.entity_id);
1754
1629
  if (current === void 0) throw new Error(`Unknown entity "${input.entity_id}"`);
1755
1630
  assembleEntityContent(toDslRows(this.state), createEntityId(input.entity_id));
1631
+ const payload = {
1632
+ ...current.payload,
1633
+ ...input.payload
1634
+ };
1635
+ const rows = toDslRows({
1636
+ ...this.state,
1637
+ entities: this.state.entities.map((row) => row.entity_id === input.entity_id ? {
1638
+ ...row,
1639
+ payload
1640
+ } : row)
1641
+ });
1642
+ for (const row of rows.entities) assembleEntityContent(rows, row.entityId);
1643
+ const assembled = assembleEntityContent(rows, createEntityId(input.entity_id));
1644
+ const issues = validateEntity(createEntityRef({
1645
+ ...assembled.payload,
1646
+ entityId: assembled.entityId,
1647
+ entityKind: assembled.entityKind
1648
+ }));
1649
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("; "));
1756
1650
  this.replaceOwnedPayload({
1757
1651
  entity_id: input.entity_id,
1758
- payload: {
1759
- ...current.payload,
1760
- ...input.payload
1761
- }
1652
+ payload
1762
1653
  });
1763
1654
  },
1764
1655
  delete: (input) => this.deleteEntity(input),
@@ -1778,7 +1669,8 @@ var EntitySandbox = class {
1778
1669
  linkClipAnchor: (input) => this.linkClipAnchor(input),
1779
1670
  linkPhoneticScriptRender: (input) => this.linkPhoneticScriptRender(input),
1780
1671
  linkAudioScriptSource: (input) => this.linkAudioScriptSource(input),
1781
- unlink: (input) => this.unlinkRelation(input)
1672
+ unlink: (input) => this.unlinkRelation(input),
1673
+ update: (input) => this.changeRelation(input)
1782
1674
  };
1783
1675
  }
1784
1676
  createEntity(input) {
@@ -1816,6 +1708,10 @@ var EntitySandbox = class {
1816
1708
  entityId: createEntityId(entityId),
1817
1709
  entityKind: entity.entity_kind
1818
1710
  });
1711
+ assembleEntityContent(toDslRows({
1712
+ ...this.state,
1713
+ entities: [...this.state.entities, entity]
1714
+ }), createEntityId(entityId));
1819
1715
  this.record({
1820
1716
  kind: "create-entity",
1821
1717
  entity
@@ -1846,6 +1742,22 @@ var EntitySandbox = class {
1846
1742
  payload
1847
1743
  });
1848
1744
  }
1745
+ changeEntity(input) {
1746
+ assertFieldChanges(input.changes);
1747
+ this.record({
1748
+ kind: "change-entity",
1749
+ entity_id: input.entity_id,
1750
+ changes: clone(input.changes)
1751
+ });
1752
+ }
1753
+ changeRelation(input) {
1754
+ assertFieldChanges(input.changes);
1755
+ this.record({
1756
+ kind: "change-relation",
1757
+ relation_id: input.relation_id,
1758
+ changes: clone(input.changes)
1759
+ });
1760
+ }
1849
1761
  deleteEntity(input) {
1850
1762
  assertTrimmed(input.entity_id, "entity_id");
1851
1763
  this.record({
@@ -1891,12 +1803,11 @@ var EntitySandbox = class {
1891
1803
  }
1892
1804
  }
1893
1805
  link(input) {
1894
- if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
1895
1806
  const spec = builtInRelationSpecs.find((candidate) => candidate.kind === input.relation_kind);
1896
1807
  if (spec == null) throw new Error(`Unknown Relation kind "${String(input.relation_kind)}"`);
1897
1808
  const relation = this.relationFromInput(input, input.relation_kind);
1898
1809
  const [first, second] = this.refsFor(relation);
1899
- new BiRelationIndex().link({
1810
+ new BiRelationIndex().linkRuntime({
1900
1811
  relationId: createRelationId(relation.relation_id),
1901
1812
  spec,
1902
1813
  endpoints: [first, second],
@@ -2037,7 +1948,7 @@ var EntitySandbox = class {
2037
1948
  if (command.entity.entity_kind === "audio-script" && this.state.entities.some((entity) => entity.entity_kind === "audio-script")) throw new Error("The project AudioScript already exists; edit its segments instead");
2038
1949
  if (enforceIdentity && this.state.entities.some((entity) => entity.entity_id === command.entity.entity_id)) throw new Error(`Entity id "${command.entity.entity_id}" already exists`);
2039
1950
  const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
2040
- if (enforceIdentity && original != null && original.entity_kind !== command.entity.entity_kind) throw new Error(`Entity id "${command.entity.entity_id}" was originally kind "${original.entity_kind}" and cannot be recreated as "${command.entity.entity_kind}"`);
1951
+ if (enforceIdentity && original != null) throw new Error(`Entity id "${command.entity.entity_id}" was originally kind "${original.entity_kind}" and cannot be recreated as "${command.entity.entity_kind}"`);
2041
1952
  this.state.entities.push(clone(command.entity));
2042
1953
  if (command.entity.entity_kind === "audio-script" && this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = command.entity.entity_id;
2043
1954
  return;
@@ -2053,8 +1964,66 @@ var EntitySandbox = class {
2053
1964
  };
2054
1965
  return;
2055
1966
  }
1967
+ case "change-entity": {
1968
+ const original = this.state;
1969
+ this.state = cloneSnapshot(original);
1970
+ try {
1971
+ const touched = new Set([command.entity_id]);
1972
+ for (const change of command.changes) {
1973
+ if (change.path[0] === "baseEntityIds" && change.op === "list.move") throw new Error("Composition bases are unordered; list.move is not applicable");
1974
+ const owner = resolveEntityFieldOwner(toDslRows(this.state), createEntityId(command.entity_id), change.path[0]);
1975
+ const row = this.state.entities.find((row) => row.entity_id === owner);
1976
+ row.payload = applyFieldChanges(row.payload, [change]);
1977
+ touched.add(owner);
1978
+ }
1979
+ const rows = toDslRows(this.state);
1980
+ for (const row of rows.entities) {
1981
+ const assembled = assembleEntityContent(rows, row.entityId);
1982
+ if (touched.has(row.entityId)) {
1983
+ const issues = validateEntity(createEntityRef({
1984
+ ...assembled.payload,
1985
+ entityId: row.entityId,
1986
+ entityKind: row.entityKind
1987
+ }));
1988
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("; "));
1989
+ }
1990
+ }
1991
+ } catch (error) {
1992
+ this.state = original;
1993
+ throw error;
1994
+ }
1995
+ return;
1996
+ }
1997
+ case "change-relation": {
1998
+ const index = this.state.relations.findIndex((row) => row.relation_id === command.relation_id);
1999
+ if (index < 0) throw new Error(`Unknown relation ${command.relation_id}`);
2000
+ for (const change of command.changes) if (!["metadata", "trace"].includes(change.path[0]) || change.path.length < 2) throw new Error("Relation changes may only edit metadata or trace fields");
2001
+ const current = this.state.relations[index];
2002
+ const fields = applyFieldChanges({
2003
+ metadata: current.metadata,
2004
+ trace: current.trace
2005
+ }, command.changes);
2006
+ const next = {
2007
+ ...current,
2008
+ metadata: fields.metadata,
2009
+ trace: fields.trace
2010
+ };
2011
+ const spec = builtInRelationSpecs.find((spec) => spec.kind === next.relation_kind);
2012
+ new BiRelationIndex().linkRuntime({
2013
+ relationId: createRelationId(next.relation_id),
2014
+ spec,
2015
+ endpoints: this.refsFor(next),
2016
+ metadata: next.metadata,
2017
+ trace: next.trace
2018
+ });
2019
+ this.state.relations[index] = next;
2020
+ return;
2021
+ }
2056
2022
  case "delete-entity": {
2057
- if (this.state.entities.find((entity) => entity.entity_id === command.entity_id)?.entity_kind === "audio-script") throw new Error("The document AudioScript is a fixed project identity and cannot be deleted");
2023
+ const target = this.state.entities.find((entity) => entity.entity_id === command.entity_id);
2024
+ if (target && ["audio-script", "timeline"].includes(target.entity_kind)) throw new Error("The document AudioScript is a fixed project identity and cannot be deleted");
2025
+ const dependents = this.state.entities.filter((entity) => Array.isArray(entity.payload.baseEntityIds) && entity.payload.baseEntityIds.includes(command.entity_id));
2026
+ if (dependents.length) throw new Error(`Entity ${command.entity_id} is still referenced by variants: ${dependents.map((row) => row.entity_id).join(", ")}`);
2058
2027
  const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
2059
2028
  if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
2060
2029
  const incidentRelationIds = this.state.relations.filter((relation) => relation.endpoint_0_entity_id === command.entity_id || relation.endpoint_1_entity_id === command.entity_id).map((relation) => relation.relation_id).sort();
@@ -2128,350 +2097,6 @@ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left,
2128
2097
  return left - right;
2129
2098
  } };
2130
2099
  //#endregion
2131
- //#region src/sandbox/script-session.ts
2132
- const LOG_LINE_MAX = 2e3;
2133
- const LOG_LINE_CAP = 1e3;
2134
- const LOG_BYTE_CAP = 64 * 1024;
2135
- const TRUNCATE_MARK = "[truncated]";
2136
- const LOG_TRUNCATED = "[log truncated]";
2137
- const ENTITY_CHECKPOINT_INDEX = Symbol("entityCheckpointIndex");
2138
- /** Session core for one forked document; globals stay identity-stable across rollback. */
2139
- var EditSandboxSession = class {
2140
- original;
2141
- idFactory;
2142
- onEntry;
2143
- onLog;
2144
- onTruncate;
2145
- entitySandbox;
2146
- current;
2147
- /** Adapter journal length already accounted for — new slices are real commits. */
2148
- adapterJournalSeen = 0;
2149
- entries = [];
2150
- logs = [];
2151
- logBytes = 0;
2152
- logCapped = false;
2153
- edit;
2154
- timeline;
2155
- entities;
2156
- relations;
2157
- console;
2158
- checkpoint;
2159
- rollbackTo;
2160
- constructor(document, options) {
2161
- this.original = structuredClone(document);
2162
- this.idFactory = options?.idFactory;
2163
- this.onEntry = options?.onEntry;
2164
- this.onLog = options?.onLog;
2165
- this.onTruncate = options?.onTruncate;
2166
- this.entitySandbox = new EntitySandbox({
2167
- state: options?.entityState,
2168
- idFactory: options?.domainIdFactory ?? (() => {
2169
- throw new Error("Entity id factory is unavailable in this sandbox host");
2170
- }),
2171
- onCommand: options?.onEntityCommand,
2172
- onTruncate: options?.onEntityTruncate
2173
- });
2174
- this.current = this.boot(structuredClone(this.original));
2175
- this.adapterJournalSeen = this.current.adapter.journal.length;
2176
- this.edit = this.buildEditFacade();
2177
- this.timeline = this.buildTimelineFacade();
2178
- this.entities = this.entitySandbox.entities;
2179
- this.relations = this.entitySandbox.relations;
2180
- this.console = this.buildConsoleShim();
2181
- this.checkpoint = () => {
2182
- const checkpoint = { index: this.entries.length };
2183
- Object.defineProperty(checkpoint, ENTITY_CHECKPOINT_INDEX, {
2184
- value: this.entitySandbox.commandCount,
2185
- enumerable: false
2186
- });
2187
- return checkpoint;
2188
- };
2189
- this.rollbackTo = (cp) => this.doRollbackTo(cp);
2190
- }
2191
- /** Assemble a ChangePlan from the self-maintained journal + current preview. */
2192
- buildPlan(baseVersion) {
2193
- const entityCommands = this.entitySandbox.getCommands();
2194
- if (this.entries.length > 0 && entityCommands.length > 0) throw new Error("A sandbox plan cannot mix timeline and entity mutations; run and commit them as separate plans");
2195
- const entityPlan = this.entitySandbox.buildPlan();
2196
- const planKind = entityCommands.length > 0 ? "entities" : "timeline";
2197
- return {
2198
- plan_kind: planKind,
2199
- doc_id: this.original.meta.draft_id ?? "",
2200
- base_version: baseVersion,
2201
- ops: this.entries.slice(),
2202
- entity_base_revision: entityPlan.base_revision,
2203
- entity_commands: entityPlan.commands,
2204
- ...planKind === "entities" ? { entity_rows: entityPlan.rows } : {},
2205
- ...planKind === "entities" ? {
2206
- deleted_entity_ids: entityPlan.deleted_entity_ids,
2207
- deleted_relation_ids: entityPlan.deleted_relation_ids
2208
- } : {},
2209
- preview: planKind === "entities" ? this.entitySandbox.renderPreview() : renderPreview(this.current.adapter.snapshot(), this.entries),
2210
- logs: this.logs.slice()
2211
- };
2212
- }
2213
- getEntries() {
2214
- return this.entries;
2215
- }
2216
- getLogs() {
2217
- return this.logs;
2218
- }
2219
- boot(document) {
2220
- const sandbox = createEditSandbox(document, this.idFactory != null ? { idFactory: this.idFactory } : void 0);
2221
- return {
2222
- adapter: sandbox.adapter,
2223
- editor: sandbox.editor
2224
- };
2225
- }
2226
- doRollbackTo(cp) {
2227
- if (cp.index > this.entries.length) throw new Error(`rollbackTo: checkpoint index ${cp.index} is past journal length ${this.entries.length}`);
2228
- const prefix = this.entries.slice(0, cp.index);
2229
- const next = this.boot(structuredClone(this.original));
2230
- replayJournalSync(next.adapter, prefix);
2231
- this.entries.length = 0;
2232
- this.entries.push(...prefix);
2233
- this.current = next;
2234
- this.adapterJournalSeen = next.adapter.journal.length;
2235
- this.onTruncate?.(prefix.length);
2236
- const entityIndex = cp[ENTITY_CHECKPOINT_INDEX];
2237
- if (entityIndex !== void 0) this.entitySandbox.rollbackTo(entityIndex);
2238
- }
2239
- captureNewEntries() {
2240
- const journal = this.current.adapter.journal;
2241
- if (journal.length <= this.adapterJournalSeen) return;
2242
- const fresh = journal.slice(this.adapterJournalSeen);
2243
- this.adapterJournalSeen = journal.length;
2244
- for (const entry of fresh) {
2245
- this.entries.push(entry);
2246
- this.onEntry?.(entry);
2247
- }
2248
- }
2249
- appendLog(line) {
2250
- if (this.logCapped) return;
2251
- if (this.logs.length >= LOG_LINE_CAP || this.logBytes >= LOG_BYTE_CAP) {
2252
- this.logs.push(LOG_TRUNCATED);
2253
- this.logCapped = true;
2254
- this.onLog?.(LOG_TRUNCATED);
2255
- return;
2256
- }
2257
- let out = line;
2258
- if (out.length > LOG_LINE_MAX) out = `${out.slice(0, LOG_LINE_MAX - 11)}${TRUNCATE_MARK}`;
2259
- this.logs.push(out);
2260
- this.logBytes += out.length;
2261
- this.onLog?.(out);
2262
- }
2263
- buildConsoleShim() {
2264
- const write = (...args) => {
2265
- this.appendLog(args.map(formatLogArg).join(" "));
2266
- };
2267
- return {
2268
- log: write,
2269
- info: write,
2270
- warn: write,
2271
- error: write
2272
- };
2273
- }
2274
- buildEditFacade() {
2275
- const wrap = (method) => async (input) => {
2276
- await method(this.current.editor, input);
2277
- this.captureNewEntries();
2278
- };
2279
- return {
2280
- addSpeeches: wrap((e, i) => e.addSpeeches(i)),
2281
- addVideoClips: async (input) => {
2282
- const needsAppend = input.before_clip_id == null && input.after_clip_id == null && input.clips.some((clip) => clip.start_ms == null);
2283
- const appendAt = this.timeline.snapshot().timeline?.duration_ms ?? 0;
2284
- const normalized = needsAppend ? {
2285
- ...input,
2286
- clips: input.clips.map((clip) => clip.start_ms == null ? {
2287
- ...clip,
2288
- start_ms: appendAt
2289
- } : clip)
2290
- } : input;
2291
- await this.current.editor.addVideoClips(normalized);
2292
- this.captureNewEntries();
2293
- },
2294
- adjustBgmVolume: wrap((e, i) => e.adjustBgmVolume(i)),
2295
- adjustSpeechVolume: wrap((e, i) => e.adjustSpeechVolume(i)),
2296
- adjustVideoClipDuration: wrap((e, i) => e.adjustVideoClipDuration(i)),
2297
- adjustVideoClipVolume: wrap((e, i) => e.adjustVideoClipVolume(i)),
2298
- changeSpeechScript: wrap((e, i) => e.changeSpeechScript(i)),
2299
- changeSpeechVoice: wrap((e, i) => e.changeSpeechVoice(i)),
2300
- deleteBgm: wrap((e, i) => e.deleteBgm(i)),
2301
- deleteSpeeches: wrap((e, i) => e.deleteSpeeches(i)),
2302
- deleteVideoClips: wrap((e, i) => e.deleteVideoClips(i)),
2303
- moveSpeeches: wrap((e, i) => e.moveSpeeches(i)),
2304
- moveVideoClips: wrap((e, i) => e.moveVideoClips(i)),
2305
- moveVideoClipsByAnchor: wrap((e, i) => e.moveVideoClipsByAnchor(i)),
2306
- replaceVideoClipContent: wrap((e, i) => e.replaceVideoClipContent(i)),
2307
- replaceVideoClipSequence: wrap((e, i) => e.replaceVideoClipSequence(i)),
2308
- setBgm: wrap((e, i) => e.setBgm(i)),
2309
- setCaptionStyle: wrap((e, i) => e.setCaptionStyle(i)),
2310
- setCaptionVisibility: wrap((e, i) => e.setCaptionVisibility(i)),
2311
- setVideoClipSpeedShift: wrap((e, i) => e.setVideoClipSpeedShift(i))
2312
- };
2313
- }
2314
- buildTimelineFacade() {
2315
- return {
2316
- snapshot: () => fromVideoDocument(this.current.adapter.snapshot()),
2317
- clipsInRange: (startMs, endMs) => this.clipsInRange(startMs, endMs),
2318
- part: (id) => this.part(id)
2319
- };
2320
- }
2321
- clipsInRange(startMs, endMs) {
2322
- const document = this.current.adapter.snapshot();
2323
- const solved = solveVideoDocument(document);
2324
- const library = document.part_library ?? {};
2325
- const main = document.tracks?.find((track) => track.parts_kind === "video_clip");
2326
- const out = [];
2327
- for (const item of main?.items ?? []) {
2328
- const id = item.part_id;
2329
- if (id == null) continue;
2330
- const clip = library[id]?.video_clip;
2331
- if (clip == null) continue;
2332
- const start = solved.absByPartId.get(id) ?? 0;
2333
- const duration = effectiveVideoClipDurationMs(clip);
2334
- const end = start + duration;
2335
- const mid = start + duration / 2;
2336
- if (!(mid >= startMs && mid < endMs)) continue;
2337
- out.push({
2338
- id,
2339
- start_ms: start,
2340
- end_ms: end,
2341
- duration_ms: duration,
2342
- speed_shift: clip.speed_shift,
2343
- volume: clip.volume,
2344
- media_id: clip.origin_media_id
2345
- });
2346
- }
2347
- return out;
2348
- }
2349
- part(id) {
2350
- const document = this.current.adapter.snapshot();
2351
- const part = (document.part_library ?? {})[id];
2352
- if (part == null) return null;
2353
- let lane = "main";
2354
- let kind = "video_clip";
2355
- for (const track of document.tracks ?? []) {
2356
- if (!(track.items ?? []).some((item) => item.part_id === id)) continue;
2357
- const partsKind = track.parts_kind ?? "video_clip";
2358
- kind = partsKind;
2359
- lane = partsKind === "video_clip" ? "main" : partsKind;
2360
- break;
2361
- }
2362
- const solved = solveVideoDocument(document);
2363
- const start = solved.absByPartId.get(id) ?? 0;
2364
- let duration = 0;
2365
- if (part.video_clip != null) duration = effectiveVideoClipDurationMs(part.video_clip);
2366
- else if (part.speech != null) duration = part.speech.media_duration_ms ?? 0;
2367
- else if (part.caption != null) duration = part.caption.initial_duration_ms ?? 0;
2368
- else if (part.bgm != null) duration = solved.durationMs;
2369
- return {
2370
- id,
2371
- kind,
2372
- lane,
2373
- start_ms: start,
2374
- end_ms: start + duration,
2375
- duration_ms: duration,
2376
- part
2377
- };
2378
- }
2379
- };
2380
- function formatLogArg(value) {
2381
- if (typeof value === "string") return value;
2382
- if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return String(value);
2383
- try {
2384
- return JSON.stringify(value);
2385
- } catch {
2386
- return "[unstringifiable]";
2387
- }
2388
- }
2389
- /**
2390
- * Synchronous journal replay for rollback. Editor methods are `async` only for
2391
- * interface uniformity — their bodies complete before the Promise is returned,
2392
- * so voiding the call applies mutations in-order without yielding.
2393
- */
2394
- function replayJournalSync(adapter, journal) {
2395
- const queue = [];
2396
- const idFactory = (_prefix) => {
2397
- const id = queue.shift();
2398
- if (id == null) throw new Error("unrecorded id");
2399
- return id;
2400
- };
2401
- const editor = new SemanticEditor(adapter, new SchemaValidator(), idFactory);
2402
- for (const entry of journal) {
2403
- queue.push(...entry.generated_ids ?? []);
2404
- const payload = entry.payload;
2405
- switch (entry.kind) {
2406
- case "MoveVideoClips":
2407
- editor.moveVideoClips(payload);
2408
- break;
2409
- case "MoveVideoClipsByAnchor":
2410
- editor.moveVideoClipsByAnchor(payload);
2411
- break;
2412
- case "DeleteVideoClips":
2413
- editor.deleteVideoClips(payload);
2414
- break;
2415
- case "AddVideoClips":
2416
- editor.addVideoClips(payload);
2417
- break;
2418
- case "AdjustVideoClipVolume":
2419
- editor.adjustVideoClipVolume(payload);
2420
- break;
2421
- case "SetVideoClipSpeedShift":
2422
- editor.setVideoClipSpeedShift(payload);
2423
- break;
2424
- case "ReplaceVideoClipContent":
2425
- editor.replaceVideoClipContent(payload);
2426
- break;
2427
- case "ReplaceVideoClipSequence":
2428
- editor.replaceVideoClipSequence(payload);
2429
- break;
2430
- case "AdjustVideoClipDuration":
2431
- editor.adjustVideoClipDuration(payload);
2432
- break;
2433
- case "AddSpeeches":
2434
- editor.addSpeeches(payload);
2435
- break;
2436
- case "DeleteSpeeches":
2437
- editor.deleteSpeeches(payload);
2438
- break;
2439
- case "MoveSpeeches":
2440
- editor.moveSpeeches(payload);
2441
- break;
2442
- case "ChangeSpeechScript":
2443
- editor.changeSpeechScript(payload);
2444
- break;
2445
- case "ChangeSpeechVoice":
2446
- editor.changeSpeechVoice(payload);
2447
- break;
2448
- case "AdjustSpeechVolume":
2449
- editor.adjustSpeechVolume(payload);
2450
- break;
2451
- case "SetCaptionVisibility":
2452
- editor.setCaptionVisibility(payload);
2453
- break;
2454
- case "SetCaptionStyle":
2455
- editor.setCaptionStyle(payload);
2456
- break;
2457
- case "SetBgm":
2458
- editor.setBgm(payload);
2459
- break;
2460
- case "DeleteBgm":
2461
- editor.deleteBgm(payload);
2462
- break;
2463
- case "AdjustBgmVolume":
2464
- editor.adjustBgmVolume(payload);
2465
- break;
2466
- default: {
2467
- const _exhaustive = entry.kind;
2468
- throw new Error(`replayJournalSync: unsupported kind ${String(_exhaustive)}`);
2469
- }
2470
- }
2471
- if (queue.length > 0) throw new Error("unconsumed ids");
2472
- }
2473
- }
2474
- //#endregion
2475
- export { createEntityId as a, collectAffectedPartIds as c, businessState as i, renderPreview as l, EntitySandbox as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EditSandboxSession as t, renderCompactProjection as u };
2100
+ export { createEntityId as a, businessState as i, toDslRows as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EntitySandbox as t };
2476
2101
 
2477
- //# sourceMappingURL=script-session-AukLN7x7.mjs.map
2102
+ //# sourceMappingURL=entity-sandbox-BH-7F5C8.mjs.map