@mengine/medeo-tool 1.4.1-alpha.2 → 1.4.1-alpha.4

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;
@@ -1560,12 +1426,17 @@ function businessFacades(entities, relations) {
1560
1426
  create: (input) => {
1561
1427
  if (input.entity_kind === "asset") throw new Error("Asset entities are managed by host assembly");
1562
1428
  assertBusinessPayload(input.payload);
1429
+ if (input.entity_id && entities.get(input.entity_id)) throw new Error(`Entity id ${input.entity_id} already exists`);
1563
1430
  return entities.create(input);
1564
1431
  },
1565
1432
  update: (input) => {
1566
1433
  assertBusiness(input.entity_id);
1567
- assertBusinessPayload(input.payload);
1568
- 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);
1569
1440
  },
1570
1441
  declareFields: (input) => {
1571
1442
  assertBusiness(input.entity_id);
@@ -1581,9 +1452,7 @@ function businessFacades(entities, relations) {
1581
1452
  }
1582
1453
  for (const relation of incident) relations.unlink({ relation_id: relation.relation_id });
1583
1454
  entities.delete(input);
1584
- },
1585
- readCaptionContent: (input) => entities.readCaptionContent(input),
1586
- readPhoneticScriptContent: (input) => entities.readPhoneticScriptContent(input)
1455
+ }
1587
1456
  },
1588
1457
  relations: {
1589
1458
  list: () => relations.list().filter(visibleRelation),
@@ -1599,14 +1468,11 @@ function businessFacades(entities, relations) {
1599
1468
  if (relation && !visibleRelation(relation)) throw new Error("Asset bindings are managed by host assembly");
1600
1469
  relations.unlink(input);
1601
1470
  },
1602
- linkGenerated: (input) => {
1603
- assertBusiness(input.output_entity_id);
1604
- assertBusiness(input.input_entity_id);
1605
- return relations.linkGenerated(input);
1606
- },
1607
- linkClipAnchor: (input) => relations.linkClipAnchor(input),
1608
- linkPhoneticScriptRender: (input) => relations.linkPhoneticScriptRender(input),
1609
- 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
+ }
1610
1476
  }
1611
1477
  };
1612
1478
  }
@@ -1646,10 +1512,14 @@ var EntitySandbox = class {
1646
1512
  }
1647
1513
  /** Host-owned fixed structure is journaled through the same CAS graph as model edits. */
1648
1514
  ensureFoundation(timelinePayload = {}) {
1649
- const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload);
1515
+ const foundation = ensureEditorFoundation(toDslRows(this.state), this.idFactory, timelinePayload, this.state.audioScriptEntityId);
1650
1516
  this.appendResourceRows(foundation.rows);
1651
1517
  if (this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = foundation.audioScriptEntityId;
1652
1518
  }
1519
+ /** Host assembly resolves the text owner before a resource Caption is complete. */
1520
+ captionAudioScriptId(entityId) {
1521
+ return findComposedAudioScript(toDslRows(this.state), createEntityId(entityId), "caption").entityId;
1522
+ }
1653
1523
  get audioScriptEntityId() {
1654
1524
  return this.state.audioScriptEntityId;
1655
1525
  }
@@ -1688,6 +1558,12 @@ var EntitySandbox = class {
1688
1558
  case "create-entity":
1689
1559
  lines.push(`+ entity ${command.entity.entity_id} kind=${command.entity.entity_kind}`);
1690
1560
  break;
1561
+ case "change-relation":
1562
+ lines.push(`~ relation ${command.relation_id} ${command.changes.map((c) => c.op).join(", ")}`);
1563
+ break;
1564
+ case "change-entity":
1565
+ lines.push(`~ entity ${command.entity_id} ${command.changes.map((c) => c.op).join(", ")}`);
1566
+ break;
1691
1567
  case "update-entity":
1692
1568
  lines.push(`~ entity ${command.entity_id} payload`);
1693
1569
  break;
@@ -1742,6 +1618,7 @@ var EntitySandbox = class {
1742
1618
  });
1743
1619
  },
1744
1620
  create: (input) => this.createEntity(input),
1621
+ changeFields: (input) => this.changeEntity(input),
1745
1622
  update: (input) => {
1746
1623
  assertTrimmed(input.entity_id, "entity_id");
1747
1624
  if (!this.state.entities.some((entity) => entity.entity_id === input.entity_id)) throw new Error(`Entity id "${input.entity_id}" does not exist`);
@@ -1755,12 +1632,28 @@ var EntitySandbox = class {
1755
1632
  const current = this.state.entities.find((entity) => entity.entity_id === input.entity_id);
1756
1633
  if (current === void 0) throw new Error(`Unknown entity "${input.entity_id}"`);
1757
1634
  assembleEntityContent(toDslRows(this.state), createEntityId(input.entity_id));
1635
+ const payload = {
1636
+ ...current.payload,
1637
+ ...input.payload
1638
+ };
1639
+ const rows = toDslRows({
1640
+ ...this.state,
1641
+ entities: this.state.entities.map((row) => row.entity_id === input.entity_id ? {
1642
+ ...row,
1643
+ payload
1644
+ } : row)
1645
+ });
1646
+ for (const row of rows.entities) assembleEntityContent(rows, row.entityId);
1647
+ const assembled = assembleEntityContent(rows, createEntityId(input.entity_id));
1648
+ const issues = validateEntity(createEntityRef({
1649
+ ...assembled.payload,
1650
+ entityId: assembled.entityId,
1651
+ entityKind: assembled.entityKind
1652
+ }));
1653
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("; "));
1758
1654
  this.replaceOwnedPayload({
1759
1655
  entity_id: input.entity_id,
1760
- payload: {
1761
- ...current.payload,
1762
- ...input.payload
1763
- }
1656
+ payload
1764
1657
  });
1765
1658
  },
1766
1659
  delete: (input) => this.deleteEntity(input),
@@ -1780,7 +1673,8 @@ var EntitySandbox = class {
1780
1673
  linkClipAnchor: (input) => this.linkClipAnchor(input),
1781
1674
  linkPhoneticScriptRender: (input) => this.linkPhoneticScriptRender(input),
1782
1675
  linkAudioScriptSource: (input) => this.linkAudioScriptSource(input),
1783
- unlink: (input) => this.unlinkRelation(input)
1676
+ unlink: (input) => this.unlinkRelation(input),
1677
+ update: (input) => this.changeRelation(input)
1784
1678
  };
1785
1679
  }
1786
1680
  createEntity(input) {
@@ -1804,10 +1698,6 @@ var EntitySandbox = class {
1804
1698
  }
1805
1699
  const entityId = input.entity_id ?? this.idFactory("entity");
1806
1700
  assertTrimmed(entityId, "entity_id");
1807
- if (input.entity_kind === "audio-script") {
1808
- const existing = this.state.entities.find((entity) => entity.entity_kind === "audio-script");
1809
- if (existing !== void 0 && existing.entity_id !== entityId) throw new Error(`Editor requires exactly one AudioScript; edit ${existing.entity_id} instead of creating ${entityId}`);
1810
- }
1811
1701
  const entity = {
1812
1702
  entity_id: entityId,
1813
1703
  entity_kind: input.entity_kind,
@@ -1818,6 +1708,10 @@ var EntitySandbox = class {
1818
1708
  entityId: createEntityId(entityId),
1819
1709
  entityKind: entity.entity_kind
1820
1710
  });
1711
+ assembleEntityContent(toDslRows({
1712
+ ...this.state,
1713
+ entities: [...this.state.entities, entity]
1714
+ }), createEntityId(entityId));
1821
1715
  this.record({
1822
1716
  kind: "create-entity",
1823
1717
  entity
@@ -1848,6 +1742,22 @@ var EntitySandbox = class {
1848
1742
  payload
1849
1743
  });
1850
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
+ }
1851
1761
  deleteEntity(input) {
1852
1762
  assertTrimmed(input.entity_id, "entity_id");
1853
1763
  this.record({
@@ -1893,12 +1803,11 @@ var EntitySandbox = class {
1893
1803
  }
1894
1804
  }
1895
1805
  link(input) {
1896
- if (input.relation_kind === "generated") throw new Error("Author generated Relations with relations.linkGenerated({ output_entity_id, input_entity_id })");
1897
1806
  const spec = builtInRelationSpecs.find((candidate) => candidate.kind === input.relation_kind);
1898
1807
  if (spec == null) throw new Error(`Unknown Relation kind "${String(input.relation_kind)}"`);
1899
1808
  const relation = this.relationFromInput(input, input.relation_kind);
1900
1809
  const [first, second] = this.refsFor(relation);
1901
- new BiRelationIndex().link({
1810
+ new BiRelationIndex().linkRuntime({
1902
1811
  relationId: createRelationId(relation.relation_id),
1903
1812
  spec,
1904
1813
  endpoints: [first, second],
@@ -2036,10 +1945,9 @@ var EntitySandbox = class {
2036
1945
  apply(command, enforceIdentity) {
2037
1946
  switch (command.kind) {
2038
1947
  case "create-entity": {
2039
- 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");
2040
1948
  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`);
2041
1949
  const original = this.original.entities.find((entity) => entity.entity_id === command.entity.entity_id);
2042
- 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}"`);
1950
+ 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}"`);
2043
1951
  this.state.entities.push(clone(command.entity));
2044
1952
  if (command.entity.entity_kind === "audio-script" && this.state.audioScriptEntityId === null) this.state.audioScriptEntityId = command.entity.entity_id;
2045
1953
  return;
@@ -2055,8 +1963,66 @@ var EntitySandbox = class {
2055
1963
  };
2056
1964
  return;
2057
1965
  }
1966
+ case "change-entity": {
1967
+ const original = this.state;
1968
+ this.state = cloneSnapshot(original);
1969
+ try {
1970
+ const touched = new Set([command.entity_id]);
1971
+ for (const change of command.changes) {
1972
+ if (change.path[0] === "baseEntityIds" && change.op === "list.move") throw new Error("Composition bases are unordered; list.move is not applicable");
1973
+ const owner = resolveEntityFieldOwner(toDslRows(this.state), createEntityId(command.entity_id), change.path[0]);
1974
+ const row = this.state.entities.find((row) => row.entity_id === owner);
1975
+ row.payload = applyFieldChanges(row.payload, [change]);
1976
+ touched.add(owner);
1977
+ }
1978
+ const rows = toDslRows(this.state);
1979
+ for (const row of rows.entities) {
1980
+ const assembled = assembleEntityContent(rows, row.entityId);
1981
+ if (touched.has(row.entityId)) {
1982
+ const issues = validateEntity(createEntityRef({
1983
+ ...assembled.payload,
1984
+ entityId: row.entityId,
1985
+ entityKind: row.entityKind
1986
+ }));
1987
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("; "));
1988
+ }
1989
+ }
1990
+ } catch (error) {
1991
+ this.state = original;
1992
+ throw error;
1993
+ }
1994
+ return;
1995
+ }
1996
+ case "change-relation": {
1997
+ const index = this.state.relations.findIndex((row) => row.relation_id === command.relation_id);
1998
+ if (index < 0) throw new Error(`Unknown relation ${command.relation_id}`);
1999
+ 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");
2000
+ const current = this.state.relations[index];
2001
+ const fields = applyFieldChanges({
2002
+ metadata: current.metadata,
2003
+ trace: current.trace
2004
+ }, command.changes);
2005
+ const next = {
2006
+ ...current,
2007
+ metadata: fields.metadata,
2008
+ trace: fields.trace
2009
+ };
2010
+ const spec = builtInRelationSpecs.find((spec) => spec.kind === next.relation_kind);
2011
+ new BiRelationIndex().linkRuntime({
2012
+ relationId: createRelationId(next.relation_id),
2013
+ spec,
2014
+ endpoints: this.refsFor(next),
2015
+ metadata: next.metadata,
2016
+ trace: next.trace
2017
+ });
2018
+ this.state.relations[index] = next;
2019
+ return;
2020
+ }
2058
2021
  case "delete-entity": {
2059
- 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");
2022
+ const target = this.state.entities.find((entity) => entity.entity_id === command.entity_id);
2023
+ if (target && (target.entity_kind === "timeline" || target.entity_id === this.state.audioScriptEntityId)) throw new Error("An entity attached to the project editor cannot be deleted");
2024
+ const dependents = this.state.entities.filter((entity) => Array.isArray(entity.payload.baseEntityIds) && entity.payload.baseEntityIds.includes(command.entity_id));
2025
+ if (dependents.length) throw new Error(`Entity ${command.entity_id} is still referenced by variants: ${dependents.map((row) => row.entity_id).join(", ")}`);
2060
2026
  const index = this.state.entities.findIndex((entity) => entity.entity_id === command.entity_id);
2061
2027
  if (index < 0) throw new Error(`Entity id "${command.entity_id}" does not exist`);
2062
2028
  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();
@@ -2130,350 +2096,6 @@ const numericMarkerComparators = { compareMarkerPoints: (_marker, _range, left,
2130
2096
  return left - right;
2131
2097
  } };
2132
2098
  //#endregion
2133
- //#region src/sandbox/script-session.ts
2134
- const LOG_LINE_MAX = 2e3;
2135
- const LOG_LINE_CAP = 1e3;
2136
- const LOG_BYTE_CAP = 64 * 1024;
2137
- const TRUNCATE_MARK = "[truncated]";
2138
- const LOG_TRUNCATED = "[log truncated]";
2139
- const ENTITY_CHECKPOINT_INDEX = Symbol("entityCheckpointIndex");
2140
- /** Session core for one forked document; globals stay identity-stable across rollback. */
2141
- var EditSandboxSession = class {
2142
- original;
2143
- idFactory;
2144
- onEntry;
2145
- onLog;
2146
- onTruncate;
2147
- entitySandbox;
2148
- current;
2149
- /** Adapter journal length already accounted for — new slices are real commits. */
2150
- adapterJournalSeen = 0;
2151
- entries = [];
2152
- logs = [];
2153
- logBytes = 0;
2154
- logCapped = false;
2155
- edit;
2156
- timeline;
2157
- entities;
2158
- relations;
2159
- console;
2160
- checkpoint;
2161
- rollbackTo;
2162
- constructor(document, options) {
2163
- this.original = structuredClone(document);
2164
- this.idFactory = options?.idFactory;
2165
- this.onEntry = options?.onEntry;
2166
- this.onLog = options?.onLog;
2167
- this.onTruncate = options?.onTruncate;
2168
- this.entitySandbox = new EntitySandbox({
2169
- state: options?.entityState,
2170
- idFactory: options?.domainIdFactory ?? (() => {
2171
- throw new Error("Entity id factory is unavailable in this sandbox host");
2172
- }),
2173
- onCommand: options?.onEntityCommand,
2174
- onTruncate: options?.onEntityTruncate
2175
- });
2176
- this.current = this.boot(structuredClone(this.original));
2177
- this.adapterJournalSeen = this.current.adapter.journal.length;
2178
- this.edit = this.buildEditFacade();
2179
- this.timeline = this.buildTimelineFacade();
2180
- this.entities = this.entitySandbox.entities;
2181
- this.relations = this.entitySandbox.relations;
2182
- this.console = this.buildConsoleShim();
2183
- this.checkpoint = () => {
2184
- const checkpoint = { index: this.entries.length };
2185
- Object.defineProperty(checkpoint, ENTITY_CHECKPOINT_INDEX, {
2186
- value: this.entitySandbox.commandCount,
2187
- enumerable: false
2188
- });
2189
- return checkpoint;
2190
- };
2191
- this.rollbackTo = (cp) => this.doRollbackTo(cp);
2192
- }
2193
- /** Assemble a ChangePlan from the self-maintained journal + current preview. */
2194
- buildPlan(baseVersion) {
2195
- const entityCommands = this.entitySandbox.getCommands();
2196
- 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");
2197
- const entityPlan = this.entitySandbox.buildPlan();
2198
- const planKind = entityCommands.length > 0 ? "entities" : "timeline";
2199
- return {
2200
- plan_kind: planKind,
2201
- doc_id: this.original.meta.draft_id ?? "",
2202
- base_version: baseVersion,
2203
- ops: this.entries.slice(),
2204
- entity_base_revision: entityPlan.base_revision,
2205
- entity_commands: entityPlan.commands,
2206
- ...planKind === "entities" ? { entity_rows: entityPlan.rows } : {},
2207
- ...planKind === "entities" ? {
2208
- deleted_entity_ids: entityPlan.deleted_entity_ids,
2209
- deleted_relation_ids: entityPlan.deleted_relation_ids
2210
- } : {},
2211
- preview: planKind === "entities" ? this.entitySandbox.renderPreview() : renderPreview(this.current.adapter.snapshot(), this.entries),
2212
- logs: this.logs.slice()
2213
- };
2214
- }
2215
- getEntries() {
2216
- return this.entries;
2217
- }
2218
- getLogs() {
2219
- return this.logs;
2220
- }
2221
- boot(document) {
2222
- const sandbox = createEditSandbox(document, this.idFactory != null ? { idFactory: this.idFactory } : void 0);
2223
- return {
2224
- adapter: sandbox.adapter,
2225
- editor: sandbox.editor
2226
- };
2227
- }
2228
- doRollbackTo(cp) {
2229
- if (cp.index > this.entries.length) throw new Error(`rollbackTo: checkpoint index ${cp.index} is past journal length ${this.entries.length}`);
2230
- const prefix = this.entries.slice(0, cp.index);
2231
- const next = this.boot(structuredClone(this.original));
2232
- replayJournalSync(next.adapter, prefix);
2233
- this.entries.length = 0;
2234
- this.entries.push(...prefix);
2235
- this.current = next;
2236
- this.adapterJournalSeen = next.adapter.journal.length;
2237
- this.onTruncate?.(prefix.length);
2238
- const entityIndex = cp[ENTITY_CHECKPOINT_INDEX];
2239
- if (entityIndex !== void 0) this.entitySandbox.rollbackTo(entityIndex);
2240
- }
2241
- captureNewEntries() {
2242
- const journal = this.current.adapter.journal;
2243
- if (journal.length <= this.adapterJournalSeen) return;
2244
- const fresh = journal.slice(this.adapterJournalSeen);
2245
- this.adapterJournalSeen = journal.length;
2246
- for (const entry of fresh) {
2247
- this.entries.push(entry);
2248
- this.onEntry?.(entry);
2249
- }
2250
- }
2251
- appendLog(line) {
2252
- if (this.logCapped) return;
2253
- if (this.logs.length >= LOG_LINE_CAP || this.logBytes >= LOG_BYTE_CAP) {
2254
- this.logs.push(LOG_TRUNCATED);
2255
- this.logCapped = true;
2256
- this.onLog?.(LOG_TRUNCATED);
2257
- return;
2258
- }
2259
- let out = line;
2260
- if (out.length > LOG_LINE_MAX) out = `${out.slice(0, LOG_LINE_MAX - 11)}${TRUNCATE_MARK}`;
2261
- this.logs.push(out);
2262
- this.logBytes += out.length;
2263
- this.onLog?.(out);
2264
- }
2265
- buildConsoleShim() {
2266
- const write = (...args) => {
2267
- this.appendLog(args.map(formatLogArg).join(" "));
2268
- };
2269
- return {
2270
- log: write,
2271
- info: write,
2272
- warn: write,
2273
- error: write
2274
- };
2275
- }
2276
- buildEditFacade() {
2277
- const wrap = (method) => async (input) => {
2278
- await method(this.current.editor, input);
2279
- this.captureNewEntries();
2280
- };
2281
- return {
2282
- addSpeeches: wrap((e, i) => e.addSpeeches(i)),
2283
- addVideoClips: async (input) => {
2284
- const needsAppend = input.before_clip_id == null && input.after_clip_id == null && input.clips.some((clip) => clip.start_ms == null);
2285
- const appendAt = this.timeline.snapshot().timeline?.duration_ms ?? 0;
2286
- const normalized = needsAppend ? {
2287
- ...input,
2288
- clips: input.clips.map((clip) => clip.start_ms == null ? {
2289
- ...clip,
2290
- start_ms: appendAt
2291
- } : clip)
2292
- } : input;
2293
- await this.current.editor.addVideoClips(normalized);
2294
- this.captureNewEntries();
2295
- },
2296
- adjustBgmVolume: wrap((e, i) => e.adjustBgmVolume(i)),
2297
- adjustSpeechVolume: wrap((e, i) => e.adjustSpeechVolume(i)),
2298
- adjustVideoClipDuration: wrap((e, i) => e.adjustVideoClipDuration(i)),
2299
- adjustVideoClipVolume: wrap((e, i) => e.adjustVideoClipVolume(i)),
2300
- changeSpeechScript: wrap((e, i) => e.changeSpeechScript(i)),
2301
- changeSpeechVoice: wrap((e, i) => e.changeSpeechVoice(i)),
2302
- deleteBgm: wrap((e, i) => e.deleteBgm(i)),
2303
- deleteSpeeches: wrap((e, i) => e.deleteSpeeches(i)),
2304
- deleteVideoClips: wrap((e, i) => e.deleteVideoClips(i)),
2305
- moveSpeeches: wrap((e, i) => e.moveSpeeches(i)),
2306
- moveVideoClips: wrap((e, i) => e.moveVideoClips(i)),
2307
- moveVideoClipsByAnchor: wrap((e, i) => e.moveVideoClipsByAnchor(i)),
2308
- replaceVideoClipContent: wrap((e, i) => e.replaceVideoClipContent(i)),
2309
- replaceVideoClipSequence: wrap((e, i) => e.replaceVideoClipSequence(i)),
2310
- setBgm: wrap((e, i) => e.setBgm(i)),
2311
- setCaptionStyle: wrap((e, i) => e.setCaptionStyle(i)),
2312
- setCaptionVisibility: wrap((e, i) => e.setCaptionVisibility(i)),
2313
- setVideoClipSpeedShift: wrap((e, i) => e.setVideoClipSpeedShift(i))
2314
- };
2315
- }
2316
- buildTimelineFacade() {
2317
- return {
2318
- snapshot: () => fromVideoDocument(this.current.adapter.snapshot()),
2319
- clipsInRange: (startMs, endMs) => this.clipsInRange(startMs, endMs),
2320
- part: (id) => this.part(id)
2321
- };
2322
- }
2323
- clipsInRange(startMs, endMs) {
2324
- const document = this.current.adapter.snapshot();
2325
- const solved = solveVideoDocument(document);
2326
- const library = document.part_library ?? {};
2327
- const main = document.tracks?.find((track) => track.parts_kind === "video_clip");
2328
- const out = [];
2329
- for (const item of main?.items ?? []) {
2330
- const id = item.part_id;
2331
- if (id == null) continue;
2332
- const clip = library[id]?.video_clip;
2333
- if (clip == null) continue;
2334
- const start = solved.absByPartId.get(id) ?? 0;
2335
- const duration = effectiveVideoClipDurationMs(clip);
2336
- const end = start + duration;
2337
- const mid = start + duration / 2;
2338
- if (!(mid >= startMs && mid < endMs)) continue;
2339
- out.push({
2340
- id,
2341
- start_ms: start,
2342
- end_ms: end,
2343
- duration_ms: duration,
2344
- speed_shift: clip.speed_shift,
2345
- volume: clip.volume,
2346
- media_id: clip.origin_media_id
2347
- });
2348
- }
2349
- return out;
2350
- }
2351
- part(id) {
2352
- const document = this.current.adapter.snapshot();
2353
- const part = (document.part_library ?? {})[id];
2354
- if (part == null) return null;
2355
- let lane = "main";
2356
- let kind = "video_clip";
2357
- for (const track of document.tracks ?? []) {
2358
- if (!(track.items ?? []).some((item) => item.part_id === id)) continue;
2359
- const partsKind = track.parts_kind ?? "video_clip";
2360
- kind = partsKind;
2361
- lane = partsKind === "video_clip" ? "main" : partsKind;
2362
- break;
2363
- }
2364
- const solved = solveVideoDocument(document);
2365
- const start = solved.absByPartId.get(id) ?? 0;
2366
- let duration = 0;
2367
- if (part.video_clip != null) duration = effectiveVideoClipDurationMs(part.video_clip);
2368
- else if (part.speech != null) duration = part.speech.media_duration_ms ?? 0;
2369
- else if (part.caption != null) duration = part.caption.initial_duration_ms ?? 0;
2370
- else if (part.bgm != null) duration = solved.durationMs;
2371
- return {
2372
- id,
2373
- kind,
2374
- lane,
2375
- start_ms: start,
2376
- end_ms: start + duration,
2377
- duration_ms: duration,
2378
- part
2379
- };
2380
- }
2381
- };
2382
- function formatLogArg(value) {
2383
- if (typeof value === "string") return value;
2384
- if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return String(value);
2385
- try {
2386
- return JSON.stringify(value);
2387
- } catch {
2388
- return "[unstringifiable]";
2389
- }
2390
- }
2391
- /**
2392
- * Synchronous journal replay for rollback. Editor methods are `async` only for
2393
- * interface uniformity — their bodies complete before the Promise is returned,
2394
- * so voiding the call applies mutations in-order without yielding.
2395
- */
2396
- function replayJournalSync(adapter, journal) {
2397
- const queue = [];
2398
- const idFactory = (_prefix) => {
2399
- const id = queue.shift();
2400
- if (id == null) throw new Error("unrecorded id");
2401
- return id;
2402
- };
2403
- const editor = new SemanticEditor(adapter, new SchemaValidator(), idFactory);
2404
- for (const entry of journal) {
2405
- queue.push(...entry.generated_ids ?? []);
2406
- const payload = entry.payload;
2407
- switch (entry.kind) {
2408
- case "MoveVideoClips":
2409
- editor.moveVideoClips(payload);
2410
- break;
2411
- case "MoveVideoClipsByAnchor":
2412
- editor.moveVideoClipsByAnchor(payload);
2413
- break;
2414
- case "DeleteVideoClips":
2415
- editor.deleteVideoClips(payload);
2416
- break;
2417
- case "AddVideoClips":
2418
- editor.addVideoClips(payload);
2419
- break;
2420
- case "AdjustVideoClipVolume":
2421
- editor.adjustVideoClipVolume(payload);
2422
- break;
2423
- case "SetVideoClipSpeedShift":
2424
- editor.setVideoClipSpeedShift(payload);
2425
- break;
2426
- case "ReplaceVideoClipContent":
2427
- editor.replaceVideoClipContent(payload);
2428
- break;
2429
- case "ReplaceVideoClipSequence":
2430
- editor.replaceVideoClipSequence(payload);
2431
- break;
2432
- case "AdjustVideoClipDuration":
2433
- editor.adjustVideoClipDuration(payload);
2434
- break;
2435
- case "AddSpeeches":
2436
- editor.addSpeeches(payload);
2437
- break;
2438
- case "DeleteSpeeches":
2439
- editor.deleteSpeeches(payload);
2440
- break;
2441
- case "MoveSpeeches":
2442
- editor.moveSpeeches(payload);
2443
- break;
2444
- case "ChangeSpeechScript":
2445
- editor.changeSpeechScript(payload);
2446
- break;
2447
- case "ChangeSpeechVoice":
2448
- editor.changeSpeechVoice(payload);
2449
- break;
2450
- case "AdjustSpeechVolume":
2451
- editor.adjustSpeechVolume(payload);
2452
- break;
2453
- case "SetCaptionVisibility":
2454
- editor.setCaptionVisibility(payload);
2455
- break;
2456
- case "SetCaptionStyle":
2457
- editor.setCaptionStyle(payload);
2458
- break;
2459
- case "SetBgm":
2460
- editor.setBgm(payload);
2461
- break;
2462
- case "DeleteBgm":
2463
- editor.deleteBgm(payload);
2464
- break;
2465
- case "AdjustBgmVolume":
2466
- editor.adjustBgmVolume(payload);
2467
- break;
2468
- default: {
2469
- const _exhaustive = entry.kind;
2470
- throw new Error(`replayJournalSync: unsupported kind ${String(_exhaustive)}`);
2471
- }
2472
- }
2473
- if (queue.length > 0) throw new Error("unconsumed ids");
2474
- }
2475
- }
2476
- //#endregion
2477
- 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 };
2099
+ export { createEntityId as a, businessState as i, toDslRows as n, createRelationId as o, businessFacades as r, isMediaAssetVariantKind as s, EntitySandbox as t };
2478
2100
 
2479
- //# sourceMappingURL=script-session-lXpqmupK.mjs.map
2101
+ //# sourceMappingURL=entity-sandbox-OArq9NSH.mjs.map