@mengine/medeo-tool 1.2.1-alpha.0 → 1.2.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -8
- package/dist/entity-contract-DycLxdQ5.d.mts +220 -0
- package/dist/index.d.mts +224 -21
- package/dist/index.mjs +1326 -885
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +467 -715
- package/dist/script-session-CHyIUBkO.mjs +1841 -0
- package/dist/script-session-CHyIUBkO.mjs.map +1 -0
- package/dist/worker-entry.d.mts +6 -3
- package/dist/worker-entry.mjs +308 -8
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/script-session-DdPA4tTf.mjs +0 -447
- package/dist/script-session-DdPA4tTf.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { a as collectAffectedPartIds, o as renderPreview, s as renderCompactProjection, t as EditSandboxSession } from "./script-session-CHyIUBkO.mjs";
|
|
2
|
+
import { EntityGraphHttpClient, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, migrateLegacyTimelineToEntities, replayJournal, toVideoDocument } from "@mengine/medeo-client";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/sandbox/node-host.ts
|
|
@@ -23,6 +23,7 @@ function runEditScript(options) {
|
|
|
23
23
|
const workerEntryUrl = options.workerEntryUrl ?? sourceSibling("worker-entry");
|
|
24
24
|
const resolveRegisterUrl = sourceSibling("node-esm-resolve-register");
|
|
25
25
|
const ops = [];
|
|
26
|
+
const entityCommands = [];
|
|
26
27
|
const logs = [];
|
|
27
28
|
return new Promise((resolve) => {
|
|
28
29
|
let settled = false;
|
|
@@ -33,7 +34,9 @@ function runEditScript(options) {
|
|
|
33
34
|
document: options.document,
|
|
34
35
|
script: options.script,
|
|
35
36
|
inputs: options.inputs,
|
|
36
|
-
|
|
37
|
+
entityState: options.entityState,
|
|
38
|
+
idLabel: options.idLabel,
|
|
39
|
+
entityOnly: options.entityOnly
|
|
37
40
|
},
|
|
38
41
|
execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
|
|
39
42
|
"--experimental-transform-types",
|
|
@@ -54,6 +57,7 @@ function runEditScript(options) {
|
|
|
54
57
|
error: { message: `edit script exceeded timeout of ${timeoutMs}ms` },
|
|
55
58
|
partial: {
|
|
56
59
|
ops: ops.slice(),
|
|
60
|
+
entityCommands: entityCommands.slice(),
|
|
57
61
|
logs: logs.slice()
|
|
58
62
|
}
|
|
59
63
|
});
|
|
@@ -81,6 +85,10 @@ function runEditScript(options) {
|
|
|
81
85
|
ops.push(message.entry);
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
88
|
+
if (message.t === "entity-entry") {
|
|
89
|
+
entityCommands.push(message.command);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
84
92
|
if (message.t === "log") {
|
|
85
93
|
logs.push(message.line);
|
|
86
94
|
return;
|
|
@@ -89,14 +97,19 @@ function runEditScript(options) {
|
|
|
89
97
|
ops.length = Math.max(0, Math.min(message.index, ops.length));
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
100
|
+
if (message.t === "entity-truncate") {
|
|
101
|
+
entityCommands.length = Math.max(0, Math.min(message.index, entityCommands.length));
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
92
104
|
if (message.t === "done") {
|
|
93
|
-
if (message.opsCount !== ops.length) {
|
|
105
|
+
if (message.opsCount !== ops.length || message.entityCommandsCount !== entityCommands.length) {
|
|
94
106
|
finish({
|
|
95
107
|
ok: false,
|
|
96
108
|
phase: "runtime",
|
|
97
|
-
error: { message: `
|
|
109
|
+
error: { message: `journal count mismatch: worker reported timeline=${message.opsCount}, entities=${message.entityCommandsCount}; host collected timeline=${ops.length}, entities=${entityCommands.length}` },
|
|
98
110
|
partial: {
|
|
99
111
|
ops: ops.slice(),
|
|
112
|
+
entityCommands: entityCommands.slice(),
|
|
100
113
|
logs: logs.slice()
|
|
101
114
|
}
|
|
102
115
|
});
|
|
@@ -105,9 +118,15 @@ function runEditScript(options) {
|
|
|
105
118
|
finish({
|
|
106
119
|
ok: true,
|
|
107
120
|
plan: {
|
|
121
|
+
plan_kind: message.planKind,
|
|
108
122
|
doc_id: options.document.meta.draft_id ?? "",
|
|
109
123
|
base_version: options.baseVersion,
|
|
110
124
|
ops: ops.slice(),
|
|
125
|
+
entity_base_revision: message.entityBaseRevision,
|
|
126
|
+
entity_commands: entityCommands.slice(),
|
|
127
|
+
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
128
|
+
deleted_entity_ids: message.deletedEntityIds,
|
|
129
|
+
deleted_relation_ids: message.deletedRelationIds,
|
|
111
130
|
preview: message.preview,
|
|
112
131
|
logs: logs.slice()
|
|
113
132
|
},
|
|
@@ -121,6 +140,7 @@ function runEditScript(options) {
|
|
|
121
140
|
error: message.error,
|
|
122
141
|
partial: {
|
|
123
142
|
ops: ops.slice(),
|
|
143
|
+
entityCommands: entityCommands.slice(),
|
|
124
144
|
logs: logs.slice()
|
|
125
145
|
}
|
|
126
146
|
});
|
|
@@ -137,6 +157,7 @@ function runEditScript(options) {
|
|
|
137
157
|
},
|
|
138
158
|
partial: {
|
|
139
159
|
ops: ops.slice(),
|
|
160
|
+
entityCommands: entityCommands.slice(),
|
|
140
161
|
logs: logs.slice()
|
|
141
162
|
}
|
|
142
163
|
});
|
|
@@ -150,6 +171,7 @@ function runEditScript(options) {
|
|
|
150
171
|
error: { message: `worker exited with code ${code ?? "null"} before completion` },
|
|
151
172
|
partial: {
|
|
152
173
|
ops: ops.slice(),
|
|
174
|
+
entityCommands: entityCommands.slice(),
|
|
153
175
|
logs: logs.slice()
|
|
154
176
|
}
|
|
155
177
|
});
|
|
@@ -157,791 +179,795 @@ function runEditScript(options) {
|
|
|
157
179
|
});
|
|
158
180
|
}
|
|
159
181
|
//#endregion
|
|
160
|
-
//#region src/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"
|
|
171
|
-
"
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
184
|
-
"
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
182
|
+
//#region src/entity/entity-contract.ts
|
|
183
|
+
const KNOWN_ENTITY_KINDS = [
|
|
184
|
+
"axvideo",
|
|
185
|
+
"timeline",
|
|
186
|
+
"track",
|
|
187
|
+
"clip",
|
|
188
|
+
"asset",
|
|
189
|
+
"video",
|
|
190
|
+
"audio",
|
|
191
|
+
"voice",
|
|
192
|
+
"image",
|
|
193
|
+
"sequence-marker",
|
|
194
|
+
"viewport",
|
|
195
|
+
"audio-script",
|
|
196
|
+
"phonetic-script",
|
|
197
|
+
"caption"
|
|
198
|
+
];
|
|
199
|
+
const KNOWN_RELATION_KINDS = [
|
|
200
|
+
"timeline-track",
|
|
201
|
+
"track-clip",
|
|
202
|
+
"clip-marker",
|
|
203
|
+
"marker-content",
|
|
204
|
+
"axvideo-marker",
|
|
205
|
+
"marker-timeline",
|
|
206
|
+
"physical-asset",
|
|
207
|
+
"generated",
|
|
208
|
+
"phonetic-script-provenance",
|
|
209
|
+
"caption-provenance",
|
|
210
|
+
"caption-alignment",
|
|
211
|
+
"clip-anchor",
|
|
212
|
+
"audio-script-render"
|
|
213
|
+
];
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/entity/entity-http-client.ts
|
|
216
|
+
const API_PREFIX = "/api/mengine/v1";
|
|
217
|
+
const entityKinds = new Set(KNOWN_ENTITY_KINDS);
|
|
218
|
+
const relationKinds = new Set(KNOWN_RELATION_KINDS);
|
|
219
|
+
var MengineEntityHttpRequestError = class extends Error {
|
|
220
|
+
status;
|
|
221
|
+
payload;
|
|
222
|
+
constructor(status, payload) {
|
|
223
|
+
super(`mengine entity-state request failed: ${status}`);
|
|
224
|
+
this.status = status;
|
|
225
|
+
this.payload = payload;
|
|
226
|
+
this.name = "MengineEntityHttpRequestError";
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/** Narrow authenticated client for the entity-store CAS endpoint. */
|
|
230
|
+
var EntityHttpClient = class {
|
|
231
|
+
options;
|
|
232
|
+
fetchImpl;
|
|
233
|
+
constructor(options) {
|
|
234
|
+
this.options = options;
|
|
235
|
+
this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
236
|
+
}
|
|
237
|
+
async fetchState() {
|
|
238
|
+
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
239
|
+
}
|
|
240
|
+
async commit(expectedRevision, state, deletions = {}) {
|
|
241
|
+
return toSnapshot(await this.requestJson({
|
|
242
|
+
method: "POST",
|
|
243
|
+
body: JSON.stringify({
|
|
244
|
+
expected_revision: expectedRevision,
|
|
245
|
+
rows: {
|
|
246
|
+
entities: state.entities,
|
|
247
|
+
relations: state.relations
|
|
248
|
+
},
|
|
249
|
+
deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
|
|
250
|
+
deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
|
|
251
|
+
})
|
|
252
|
+
}), this.options.docId);
|
|
253
|
+
}
|
|
254
|
+
async requestJson(init) {
|
|
255
|
+
const response = await this.fetchImpl(this.endpoint(), {
|
|
256
|
+
...init,
|
|
257
|
+
headers: this.headers()
|
|
258
|
+
});
|
|
259
|
+
const payload = await safeReadJson(response);
|
|
260
|
+
if (!response.ok) throw new MengineEntityHttpRequestError(response.status, payload);
|
|
261
|
+
return payload;
|
|
262
|
+
}
|
|
263
|
+
headers() {
|
|
264
|
+
const headers = new Headers({
|
|
265
|
+
accept: "application/json",
|
|
266
|
+
"content-type": "application/json"
|
|
267
|
+
});
|
|
268
|
+
const authToken = typeof this.options.authToken === "function" ? this.options.authToken() : this.options.authToken;
|
|
269
|
+
if (authToken != null && authToken !== "") headers.set("authorization", `Bearer ${authToken}`);
|
|
270
|
+
const userId = typeof this.options.userId === "function" ? this.options.userId() : this.options.userId;
|
|
271
|
+
if (userId != null && userId !== "") headers.set("medeo-user-id", userId);
|
|
272
|
+
return headers;
|
|
273
|
+
}
|
|
274
|
+
endpoint() {
|
|
275
|
+
return `${this.options.httpOrigin.replace(/\/$/, "")}${API_PREFIX}/docs/${encodeURIComponent(this.options.docId)}/entity-state`;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
function toSnapshot(value, expectedDocId) {
|
|
279
|
+
if (!isRecord$1(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
|
+
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
|
+
if (!isRecord$1(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
282
|
+
const response = value;
|
|
283
|
+
return {
|
|
284
|
+
revision: response.revision,
|
|
285
|
+
entities: response.rows.entities.map(parseEntity),
|
|
286
|
+
relations: response.rows.relations.map(parseRelation)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function parseEntity(value) {
|
|
290
|
+
if (!isRecord$1(value) || !isTrimmed(value.entity_id) || typeof value.entity_kind !== "string" || !entityKinds.has(value.entity_kind) || !isJsonObject(value.payload)) throw new Error("invalid Entity row in entity-state response");
|
|
291
|
+
return structuredClone(value);
|
|
292
|
+
}
|
|
293
|
+
function parseRelation(value) {
|
|
294
|
+
if (!isRecord$1(value) || !isTrimmed(value.relation_id) || typeof value.relation_kind !== "string" || !relationKinds.has(value.relation_kind) || !isTrimmed(value.endpoint_0_entity_id) || !isTrimmed(value.endpoint_1_entity_id) || !isJsonObject(value.metadata) || !isJsonObject(value.trace)) throw new Error("invalid Relation row in entity-state response");
|
|
295
|
+
return structuredClone(value);
|
|
296
|
+
}
|
|
297
|
+
function isJsonObject(value) {
|
|
298
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$1(value);
|
|
299
|
+
}
|
|
300
|
+
function isJsonValue(value, ancestors) {
|
|
301
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
302
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
303
|
+
if (typeof value !== "object" || ancestors.has(value)) return false;
|
|
304
|
+
const prototype = Object.getPrototypeOf(value);
|
|
305
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return false;
|
|
306
|
+
ancestors.add(value);
|
|
307
|
+
const valid = Array.isArray(value) ? value.every((item) => isJsonValue(item, ancestors)) : Object.values(value).every((item) => isJsonValue(item, ancestors));
|
|
308
|
+
ancestors.delete(value);
|
|
309
|
+
return valid;
|
|
310
|
+
}
|
|
311
|
+
function isRecord$1(value) {
|
|
312
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
313
|
+
}
|
|
314
|
+
function isTrimmed(value) {
|
|
315
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
316
|
+
}
|
|
317
|
+
function isNonNegativeInteger(value) {
|
|
318
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
319
|
+
}
|
|
320
|
+
async function safeReadJson(response) {
|
|
321
|
+
const text = await response.text();
|
|
322
|
+
if (text.length === 0) return null;
|
|
323
|
+
try {
|
|
324
|
+
return JSON.parse(text);
|
|
325
|
+
} catch {
|
|
326
|
+
return text;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/migration-input.ts
|
|
331
|
+
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
332
|
+
function parseMigrationAssetFacts(value) {
|
|
333
|
+
if (!Array.isArray(value)) throw new Error("asset_facts is required for migrate-legacy and must be an array");
|
|
334
|
+
return value.map((item) => {
|
|
335
|
+
if (!record(item)) throw new Error("Each asset_facts entry must be an object");
|
|
336
|
+
const { assetId, kind, durationMs, storageKey, voice } = item;
|
|
337
|
+
if (!nonempty(assetId)) throw new Error("asset_facts.assetId must be a non-empty trimmed string");
|
|
338
|
+
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "voice") throw new Error("asset_facts.kind must be image, video, audio, or voice");
|
|
339
|
+
if (Object.keys(item).some((key) => ![
|
|
340
|
+
"assetId",
|
|
341
|
+
"kind",
|
|
342
|
+
"durationMs",
|
|
343
|
+
"storageKey",
|
|
344
|
+
"voice"
|
|
345
|
+
].includes(key))) throw new Error("Unknown asset_facts field");
|
|
346
|
+
if (storageKey !== void 0 && !nonempty(storageKey)) throw new Error("asset_facts.storageKey must be non-empty");
|
|
347
|
+
if (kind === "image") {
|
|
348
|
+
if (durationMs !== void 0 || voice !== void 0) throw new Error("Image facts cannot declare duration or voice");
|
|
349
|
+
return {
|
|
350
|
+
assetId,
|
|
351
|
+
kind,
|
|
352
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (typeof durationMs !== "number" || !Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error("asset_facts.durationMs must be factual positive whole milliseconds");
|
|
356
|
+
if (kind === "video") {
|
|
357
|
+
if (voice !== void 0) throw new Error("Video facts cannot declare voice");
|
|
358
|
+
return {
|
|
359
|
+
assetId,
|
|
360
|
+
kind,
|
|
361
|
+
durationMs,
|
|
362
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (!nonempty(storageKey)) throw new Error("Audio and Voice facts require their physical storageKey");
|
|
366
|
+
if (kind === "audio") {
|
|
367
|
+
if (voice !== void 0) throw new Error("Audio facts cannot declare a Voice descriptor");
|
|
368
|
+
return {
|
|
369
|
+
assetId,
|
|
370
|
+
kind,
|
|
371
|
+
durationMs,
|
|
372
|
+
storageKey
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (!record(voice) || voice.system !== "voice-library" || !nonempty(voice.key) || voice.name !== void 0 && typeof voice.name !== "string" || Object.keys(voice).some((key) => ![
|
|
376
|
+
"system",
|
|
377
|
+
"key",
|
|
378
|
+
"name"
|
|
379
|
+
].includes(key))) throw new Error("Voice facts require an explicit voice-library descriptor");
|
|
380
|
+
return {
|
|
381
|
+
assetId,
|
|
382
|
+
kind,
|
|
383
|
+
durationMs,
|
|
384
|
+
storageKey,
|
|
385
|
+
voice: {
|
|
386
|
+
system: "voice-library",
|
|
387
|
+
key: voice.key,
|
|
388
|
+
...voice.name === void 0 ? {} : { name: voice.name }
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function record(value) {
|
|
394
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
395
|
+
}
|
|
396
|
+
function nonempty(value) {
|
|
397
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
398
|
+
}
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
401
|
+
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
402
|
+
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
403
|
+
"/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
|
|
404
|
+
"export interface AudioMediaAssetFact {",
|
|
405
|
+
" readonly assetId: string;",
|
|
406
|
+
" readonly kind: 'audio';",
|
|
407
|
+
" readonly durationMs: number;",
|
|
408
|
+
" readonly storageKey: string;",
|
|
409
|
+
"}",
|
|
410
|
+
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
411
|
+
" extent: {",
|
|
412
|
+
" kind: 'bounded';",
|
|
413
|
+
" start: number;",
|
|
414
|
+
" end: number;",
|
|
415
|
+
" };",
|
|
416
|
+
" sampling: 'derived';",
|
|
417
|
+
" coordinateSpace: JsonValue;",
|
|
418
|
+
"}",
|
|
419
|
+
"export interface BoundedNativeSequencePayload extends JsonObject {",
|
|
420
|
+
" /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
|
|
421
|
+
" extent: {",
|
|
422
|
+
" kind: 'bounded';",
|
|
423
|
+
" start: number;",
|
|
424
|
+
" end: number;",
|
|
425
|
+
" };",
|
|
426
|
+
" sampling: 'native';",
|
|
427
|
+
" coordinateSpace: JsonValue;",
|
|
428
|
+
"}",
|
|
429
|
+
"export interface CaptionFontDescriptor {",
|
|
430
|
+
" readonly system: 'font-library';",
|
|
431
|
+
" readonly key: string;",
|
|
432
|
+
"}",
|
|
433
|
+
"export interface CaptionStyleFields {",
|
|
434
|
+
" readonly font?: CaptionFontDescriptor;",
|
|
435
|
+
" readonly fontSize?: number;",
|
|
436
|
+
" readonly fontColor?: string;",
|
|
437
|
+
" readonly fontWeight?: number;",
|
|
438
|
+
" readonly entranceAnimation?: string;",
|
|
439
|
+
" readonly entranceAnimationDurationMs?: number;",
|
|
440
|
+
" readonly strokeColor?: string;",
|
|
441
|
+
" readonly strokeWidth?: number;",
|
|
442
|
+
" readonly positionX?: number;",
|
|
443
|
+
" readonly positionY?: number;",
|
|
444
|
+
"}",
|
|
445
|
+
"export type ClipEntityId = EntityId;",
|
|
446
|
+
"export type ClipPlacement =",
|
|
447
|
+
" | {",
|
|
448
|
+
" readonly kind: 'sequential';",
|
|
449
|
+
" readonly order: number;",
|
|
450
|
+
" }",
|
|
451
|
+
" | {",
|
|
452
|
+
" readonly kind: 'absolute';",
|
|
453
|
+
" readonly targetRange: SequenceRange<number>;",
|
|
454
|
+
" }",
|
|
455
|
+
" | {",
|
|
456
|
+
" readonly kind: 'anchored';",
|
|
457
|
+
" readonly hostClipEntityId: string;",
|
|
458
|
+
" readonly anchorOffset: number;",
|
|
459
|
+
" };",
|
|
460
|
+
"export type CreateEntityInput = {",
|
|
461
|
+
" [K in KnownEntityKind]: {",
|
|
462
|
+
" entity_id?: string;",
|
|
463
|
+
" entity_kind: K;",
|
|
464
|
+
" payload: EntityPayloadByKind[K];",
|
|
465
|
+
" };",
|
|
466
|
+
"}[KnownEntityKind];",
|
|
467
|
+
"export interface DeleteBgmInput {",
|
|
468
|
+
" readonly timelineEntityId: string;",
|
|
469
|
+
"}",
|
|
470
|
+
"export interface DeleteClipInput {",
|
|
471
|
+
" readonly clipEntityId: string;",
|
|
472
|
+
"}",
|
|
473
|
+
"export interface DeleteClipTreeInput {",
|
|
474
|
+
" readonly clipEntityIds: readonly string[];",
|
|
475
|
+
" readonly onAnchored: 'cascade' | 'detach';",
|
|
476
|
+
"}",
|
|
477
|
+
"export interface DeleteEntityInput {",
|
|
478
|
+
" entity_id: string;",
|
|
479
|
+
"}",
|
|
480
|
+
"export interface DeleteVoiceoverInput {",
|
|
481
|
+
" readonly voiceoverClipEntityIds: readonly string[];",
|
|
482
|
+
"}",
|
|
483
|
+
"export type EmptyRelationKind =",
|
|
484
|
+
" | 'timeline-track'",
|
|
485
|
+
" | 'track-clip'",
|
|
486
|
+
" | 'clip-marker'",
|
|
487
|
+
" | 'marker-content'",
|
|
488
|
+
" | 'axvideo-marker'",
|
|
489
|
+
" | 'marker-timeline';",
|
|
490
|
+
"export interface EntityFacade {",
|
|
491
|
+
" list(): SandboxEntity[];",
|
|
492
|
+
" get(entityId: string): SandboxEntity | null;",
|
|
493
|
+
" /** Return every explicitly imported Asset entity for a Memota asset id. */",
|
|
494
|
+
" findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
|
|
495
|
+
" create(input: CreateEntityInput): string;",
|
|
496
|
+
" /** Replace one Entity's owned payload without changing its identity or kind. */",
|
|
497
|
+
" update(input: UpdateEntityInput): void;",
|
|
498
|
+
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
499
|
+
" delete(input: DeleteEntityInput): void;",
|
|
500
|
+
" /** Import one physical asset without implying a one-to-one media Entity mapping. */",
|
|
501
|
+
" importAsset(input: ImportAssetInput): string;",
|
|
502
|
+
"}",
|
|
503
|
+
"export type EntityId = string;",
|
|
504
|
+
"export interface EntityPayloadByKind {",
|
|
505
|
+
" axvideo: BoundedDerivedSequencePayload;",
|
|
506
|
+
" timeline: JsonObject;",
|
|
507
|
+
" track: JsonObject & {",
|
|
508
|
+
" hidden?: boolean;",
|
|
509
|
+
" role?: string;",
|
|
510
|
+
" };",
|
|
511
|
+
" clip: JsonObject;",
|
|
512
|
+
" /** Asset-owned metadata. Peer media associations belong in physical-asset Relations. */",
|
|
513
|
+
" asset: JsonObject;",
|
|
514
|
+
" video: BoundedNativeSequencePayload;",
|
|
515
|
+
" audio: BoundedNativeSequencePayload;",
|
|
516
|
+
" voice: BoundedNativeSequencePayload;",
|
|
517
|
+
" image: UnboundedConstantSequencePayload;",
|
|
518
|
+
" 'sequence-marker': JsonObject & {",
|
|
519
|
+
" sourceRange: {",
|
|
520
|
+
" start: number;",
|
|
521
|
+
" end: number;",
|
|
522
|
+
" };",
|
|
523
|
+
" targetRange?: {",
|
|
524
|
+
" start: number;",
|
|
525
|
+
" end: number;",
|
|
526
|
+
" };",
|
|
527
|
+
" duration:",
|
|
528
|
+
" | {",
|
|
529
|
+
" mode: 'from-source';",
|
|
530
|
+
" }",
|
|
531
|
+
" | {",
|
|
532
|
+
" mode: 'fixed';",
|
|
533
|
+
" value: number;",
|
|
231
534
|
" };",
|
|
232
|
-
"
|
|
535
|
+
" timeRemapping?: JsonValue;",
|
|
536
|
+
" anchorOffset?: number;",
|
|
537
|
+
" durationPolicy?: 'timeline';",
|
|
538
|
+
" };",
|
|
539
|
+
" viewport: JsonObject;",
|
|
540
|
+
" 'audio-script': JsonObject & {",
|
|
541
|
+
" segments: ScriptTextSegment[];",
|
|
542
|
+
" };",
|
|
543
|
+
" 'phonetic-script': JsonObject & {",
|
|
544
|
+
" segments: ScriptTextSegment[];",
|
|
545
|
+
" };",
|
|
546
|
+
" caption: BoundedNativeSequencePayload;",
|
|
547
|
+
"}",
|
|
548
|
+
"export interface EntityStoreSnapshot {",
|
|
549
|
+
" revision: number;",
|
|
550
|
+
" entities: SandboxEntity[];",
|
|
551
|
+
" relations: SandboxRelation[];",
|
|
552
|
+
"}",
|
|
553
|
+
"export interface ImageMediaAssetFact {",
|
|
554
|
+
" readonly assetId: string;",
|
|
555
|
+
" readonly kind: 'image';",
|
|
556
|
+
" readonly storageKey?: string;",
|
|
557
|
+
"}",
|
|
558
|
+
"export interface ImportAssetInput {",
|
|
559
|
+
" asset_id: string;",
|
|
560
|
+
" entity_id?: string;",
|
|
561
|
+
" payload?: JsonObject;",
|
|
562
|
+
"}",
|
|
563
|
+
"export interface InsertClipInput {",
|
|
564
|
+
" readonly trackEntityId: string;",
|
|
565
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
566
|
+
" readonly contentEntityId: string;",
|
|
567
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
568
|
+
" readonly duration: SequenceDuration<number>;",
|
|
569
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
570
|
+
" readonly clipPayload?: JsonObject;",
|
|
571
|
+
"}",
|
|
572
|
+
"export interface InsertMediaClipInput {",
|
|
573
|
+
" readonly timelineEntityId: string;",
|
|
574
|
+
" readonly clipEntityId?: string;",
|
|
575
|
+
" readonly media: VisualMediaAssetFact;",
|
|
576
|
+
" /** Source/display window in whole milliseconds. Images use this as their finite display span. */",
|
|
577
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
578
|
+
" readonly placement: ClipPlacement;",
|
|
579
|
+
" readonly volume?: number;",
|
|
580
|
+
"}",
|
|
581
|
+
"export interface InsertMediaClipsInput {",
|
|
582
|
+
" readonly timelineEntityId: string;",
|
|
583
|
+
" readonly clips: readonly ReplacementMediaClipInput[];",
|
|
584
|
+
" /** One placement decision for the whole input-ordered block. */",
|
|
585
|
+
" readonly insertion: MediaClipInsertion;",
|
|
586
|
+
"}",
|
|
587
|
+
"export interface InsertPlacedClipInput {",
|
|
588
|
+
" readonly trackEntityId: string;",
|
|
589
|
+
" readonly contentEntityId: string;",
|
|
590
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
591
|
+
" readonly duration: SequenceDuration<number>;",
|
|
592
|
+
" readonly placement: ClipPlacement;",
|
|
593
|
+
" readonly clipPayload?: JsonObject;",
|
|
594
|
+
" /** Stable caller-owned placement identity, when one already exists outside the graph. */",
|
|
595
|
+
" readonly clipEntityId?: string;",
|
|
596
|
+
"}",
|
|
597
|
+
"export interface JsonObject {",
|
|
598
|
+
" [key: string]: JsonValue;",
|
|
599
|
+
"}",
|
|
600
|
+
"export type JsonPrimitive = string | number | boolean | null;",
|
|
601
|
+
"export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
|
|
602
|
+
"export type KnownEntityKind =",
|
|
603
|
+
" | 'axvideo'",
|
|
604
|
+
" | 'timeline'",
|
|
605
|
+
" | 'track'",
|
|
606
|
+
" | 'clip'",
|
|
607
|
+
" | 'asset'",
|
|
608
|
+
" | 'video'",
|
|
609
|
+
" | 'audio'",
|
|
610
|
+
" | 'voice'",
|
|
611
|
+
" | 'image'",
|
|
612
|
+
" | 'sequence-marker'",
|
|
613
|
+
" | 'viewport'",
|
|
614
|
+
" | 'audio-script'",
|
|
615
|
+
" | 'phonetic-script'",
|
|
616
|
+
" | 'caption';",
|
|
617
|
+
"export type KnownRelationKind =",
|
|
618
|
+
" | 'timeline-track'",
|
|
619
|
+
" | 'track-clip'",
|
|
620
|
+
" | 'clip-marker'",
|
|
621
|
+
" | 'marker-content'",
|
|
622
|
+
" | 'axvideo-marker'",
|
|
623
|
+
" | 'marker-timeline'",
|
|
624
|
+
" | 'physical-asset'",
|
|
625
|
+
" | 'generated'",
|
|
626
|
+
" | 'phonetic-script-provenance'",
|
|
627
|
+
" | 'caption-provenance'",
|
|
628
|
+
" | 'caption-alignment'",
|
|
629
|
+
" | 'clip-anchor'",
|
|
630
|
+
" | 'audio-script-render';",
|
|
631
|
+
"export interface LinearClipSpeed {",
|
|
632
|
+
" readonly kind: 'linear';",
|
|
633
|
+
" readonly rate: number;",
|
|
634
|
+
" readonly mode?: string;",
|
|
635
|
+
"}",
|
|
636
|
+
"export interface LinkAudioScriptRenderRelationInput {",
|
|
637
|
+
" relation_id?: string;",
|
|
638
|
+
" output_entity_id: string;",
|
|
639
|
+
" script_entity_id: string;",
|
|
640
|
+
" trace?: JsonObject;",
|
|
233
641
|
"}",
|
|
234
|
-
"",
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
"
|
|
239
|
-
" /**",
|
|
240
|
-
" * @constraint minLength(1)",
|
|
241
|
-
" */",
|
|
242
|
-
" id: string;",
|
|
243
|
-
" name: string;",
|
|
642
|
+
"export interface LinkClipAnchorRelationInput {",
|
|
643
|
+
" relation_id?: string;",
|
|
644
|
+
" child_clip_entity_id: string;",
|
|
645
|
+
" host_clip_entity_id: string;",
|
|
646
|
+
" trace?: JsonObject;",
|
|
244
647
|
"}",
|
|
245
|
-
"",
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
"
|
|
250
|
-
"
|
|
251
|
-
"
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
"
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
273
|
-
"
|
|
274
|
-
"
|
|
275
|
-
"
|
|
276
|
-
"
|
|
277
|
-
"
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
285
|
-
"
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"
|
|
292
|
-
" /**",
|
|
293
|
-
" * @constraint minLength(1)",
|
|
294
|
-
" */",
|
|
295
|
-
" id: string;",
|
|
296
|
-
" name: string;",
|
|
648
|
+
"export interface LinkGeneratedRelationInput {",
|
|
649
|
+
" relation_id?: string;",
|
|
650
|
+
" output_entity_id: string;",
|
|
651
|
+
" input_entity_id: string;",
|
|
652
|
+
" trace?: JsonObject;",
|
|
653
|
+
"}",
|
|
654
|
+
"interface LinkRelationBase {",
|
|
655
|
+
" relation_id?: string;",
|
|
656
|
+
" endpoint_0_entity_id: string;",
|
|
657
|
+
" endpoint_1_entity_id: string;",
|
|
658
|
+
" trace?: JsonObject;",
|
|
659
|
+
"}",
|
|
660
|
+
"export type LinkRelationInput =",
|
|
661
|
+
" | (LinkRelationBase & {",
|
|
662
|
+
" relation_kind: EmptyRelationKind;",
|
|
663
|
+
" metadata?: {",
|
|
664
|
+
" [key: string]: never;",
|
|
665
|
+
" };",
|
|
666
|
+
" })",
|
|
667
|
+
" | (LinkRelationBase & {",
|
|
668
|
+
" relation_kind: 'physical-asset';",
|
|
669
|
+
" metadata?: JsonObject;",
|
|
670
|
+
" })",
|
|
671
|
+
" | (LinkRelationBase & {",
|
|
672
|
+
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
673
|
+
" metadata: JsonObject & {",
|
|
674
|
+
" segmentAlignment: JsonValue;",
|
|
675
|
+
" };",
|
|
676
|
+
" })",
|
|
677
|
+
" | (LinkRelationBase & {",
|
|
678
|
+
" relation_kind: 'caption-alignment';",
|
|
679
|
+
" metadata: JsonObject & {",
|
|
680
|
+
" alignment: JsonValue;",
|
|
681
|
+
" };",
|
|
682
|
+
" });",
|
|
683
|
+
"export type MediaClipInsertion =",
|
|
684
|
+
" | {",
|
|
685
|
+
" readonly kind: 'before';",
|
|
686
|
+
" readonly clipEntityId: string;",
|
|
687
|
+
" }",
|
|
688
|
+
" | {",
|
|
689
|
+
" readonly kind: 'after';",
|
|
690
|
+
" readonly clipEntityId: string;",
|
|
691
|
+
" }",
|
|
692
|
+
" | {",
|
|
693
|
+
" readonly kind: 'firstStart';",
|
|
694
|
+
" readonly startMs: number;",
|
|
297
695
|
" };",
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
" origin_speech_id: string;",
|
|
302
|
-
" /**",
|
|
303
|
-
" * Caption part IDs owned by this speech",
|
|
304
|
-
" */",
|
|
305
|
-
" caption_ids: string[];",
|
|
306
|
-
" }[];",
|
|
307
|
-
" /**",
|
|
308
|
-
" * Materialized caption parts owned by the speeches",
|
|
309
|
-
" */",
|
|
310
|
-
" captions: {",
|
|
311
|
-
" /**",
|
|
312
|
-
" * The caption part ID (= side-effect created_caption_parts[].id)",
|
|
313
|
-
" * @constraint minLength(1)",
|
|
314
|
-
" */",
|
|
315
|
-
" caption_id: string;",
|
|
316
|
-
" /**",
|
|
317
|
-
" * The owning speech part ID",
|
|
318
|
-
" * @constraint minLength(1)",
|
|
319
|
-
" */",
|
|
320
|
-
" speech_part_id: string;",
|
|
321
|
-
" text: string;",
|
|
322
|
-
" /**",
|
|
323
|
-
" * Offset within the host speech (caption.abs = speech.abs + start_ms)",
|
|
324
|
-
" * @constraint int",
|
|
325
|
-
" * @constraint min(0)",
|
|
326
|
-
" */",
|
|
327
|
-
" start_ms: number;",
|
|
328
|
-
" /**",
|
|
329
|
-
" * Duration in milliseconds (> 0)",
|
|
330
|
-
" * @constraint int",
|
|
331
|
-
" * @constraint positive",
|
|
332
|
-
" */",
|
|
333
|
-
" duration_ms: number;",
|
|
334
|
-
" }[];",
|
|
696
|
+
"export interface MoveClipInput {",
|
|
697
|
+
" readonly clipEntityId: string;",
|
|
698
|
+
" readonly trackEntityId: string;",
|
|
335
699
|
"}",
|
|
336
|
-
"",
|
|
337
|
-
"
|
|
338
|
-
"
|
|
339
|
-
"
|
|
340
|
-
" * @constraint minLength(1)",
|
|
341
|
-
" */",
|
|
342
|
-
" clips: {",
|
|
343
|
-
" /**",
|
|
344
|
-
" * The video clip part ID to move",
|
|
345
|
-
" * @constraint minLength(1)",
|
|
346
|
-
" */",
|
|
347
|
-
" clip_id: string;",
|
|
348
|
-
" /**",
|
|
349
|
-
" * New absolute start time in milliseconds on the timeline",
|
|
350
|
-
" * @constraint int",
|
|
351
|
-
" * @constraint min(0)",
|
|
352
|
-
" */",
|
|
353
|
-
" new_start_ms: number;",
|
|
354
|
-
" /**",
|
|
355
|
-
" * Target track ID to move the clip to (optional)",
|
|
356
|
-
" * @constraint minLength(1)",
|
|
357
|
-
" */",
|
|
358
|
-
" new_track_id?: string;",
|
|
700
|
+
"export interface MoveClipsToStartsInput {",
|
|
701
|
+
" readonly moves: readonly {",
|
|
702
|
+
" readonly clipEntityId: string;",
|
|
703
|
+
" readonly newStartMs: number;",
|
|
359
704
|
" }[];",
|
|
705
|
+
" /** Absolute-time drags preserve every voiceover's current visible landing. */",
|
|
706
|
+
" readonly onAnchored: 'keepAbsolute';",
|
|
360
707
|
"}",
|
|
361
|
-
"",
|
|
362
|
-
"
|
|
363
|
-
"
|
|
364
|
-
"
|
|
365
|
-
" * @constraint minLength(1)",
|
|
366
|
-
" */",
|
|
367
|
-
" clip_ids: string[];",
|
|
368
|
-
" /**",
|
|
369
|
-
" * Where the moved block lands: before/after a reference clip, or at the head of the track",
|
|
370
|
-
" */",
|
|
371
|
-
" anchor:",
|
|
372
|
-
" | {",
|
|
373
|
-
" position: 'before';",
|
|
374
|
-
" /**",
|
|
375
|
-
" * The moved block lands immediately before this clip",
|
|
376
|
-
" * @constraint minLength(1)",
|
|
377
|
-
" */",
|
|
378
|
-
" clip_id: string;",
|
|
379
|
-
" }",
|
|
380
|
-
" | {",
|
|
381
|
-
" position: 'after';",
|
|
382
|
-
" /**",
|
|
383
|
-
" * The moved block lands immediately after this clip",
|
|
384
|
-
" * @constraint minLength(1)",
|
|
385
|
-
" */",
|
|
386
|
-
" clip_id: string;",
|
|
387
|
-
" }",
|
|
388
|
-
" | { position: 'track_start' };",
|
|
389
|
-
" /**",
|
|
390
|
-
" * What happens to speeches anchored to the moved clips (required — see the policy doc)",
|
|
391
|
-
" */",
|
|
392
|
-
" on_anchored: 'follow' | 'keep_absolute';",
|
|
708
|
+
"export interface MoveSequentialClipsInput {",
|
|
709
|
+
" readonly clipEntityIds: readonly string[];",
|
|
710
|
+
" readonly anchor: SequentialClipAnchor;",
|
|
711
|
+
" readonly onAnchored: 'follow' | 'keepAbsolute';",
|
|
393
712
|
"}",
|
|
394
|
-
"",
|
|
395
|
-
"
|
|
396
|
-
" /**",
|
|
397
|
-
"
|
|
398
|
-
" * @constraint minLength(1)",
|
|
399
|
-
" */",
|
|
400
|
-
" clip_ids: string[];",
|
|
401
|
-
" /**",
|
|
402
|
-
" * How to treat anchored children (default cascade)",
|
|
403
|
-
" */",
|
|
404
|
-
" on_anchored?: 'cascade' | 'detach';",
|
|
713
|
+
"export interface MoveVoiceoverInput {",
|
|
714
|
+
" readonly voiceoverClipEntityId: string;",
|
|
715
|
+
" /** Absolute requested timeline start; MEngine resolves and persists the host relation. */",
|
|
716
|
+
" readonly newStartMs: number;",
|
|
405
717
|
"}",
|
|
406
|
-
"",
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
" */",
|
|
410
|
-
"export interface AddVideoClipsInput {",
|
|
411
|
-
" /**",
|
|
412
|
-
" * List of video clips to create",
|
|
413
|
-
" * @constraint minLength(1)",
|
|
414
|
-
" */",
|
|
415
|
-
" clips: {",
|
|
416
|
-
" /**",
|
|
417
|
-
" * The media asset ID for the video clip",
|
|
418
|
-
" * @constraint minLength(1)",
|
|
419
|
-
" */",
|
|
420
|
-
" media_id: string;",
|
|
421
|
-
" /**",
|
|
422
|
-
" * Absolute start time in milliseconds on the timeline",
|
|
423
|
-
" * @constraint int",
|
|
424
|
-
" * @constraint min(0)",
|
|
425
|
-
" */",
|
|
426
|
-
" start_ms?: number;",
|
|
427
|
-
" /**",
|
|
428
|
-
" * The source media's intrinsic full length in ms",
|
|
429
|
-
" * @constraint int",
|
|
430
|
-
" * @constraint positive",
|
|
431
|
-
" */",
|
|
432
|
-
" media_duration_ms: number;",
|
|
433
|
-
" /**",
|
|
434
|
-
" * Trim window start in the media (default 0)",
|
|
435
|
-
" * @constraint int",
|
|
436
|
-
" * @constraint min(0)",
|
|
437
|
-
" */",
|
|
438
|
-
" play_in?: number;",
|
|
439
|
-
" /**",
|
|
440
|
-
" * Trim window end in the media (default media_duration_ms)",
|
|
441
|
-
" * @constraint int",
|
|
442
|
-
" * @constraint positive",
|
|
443
|
-
" */",
|
|
444
|
-
" play_out?: number;",
|
|
445
|
-
" /**",
|
|
446
|
-
" * Target track ID (optional, defaults to main track)",
|
|
447
|
-
" * @constraint minLength(1)",
|
|
448
|
-
" */",
|
|
449
|
-
" track_id?: string;",
|
|
450
|
-
" }[];",
|
|
451
|
-
" /**",
|
|
452
|
-
" * Insert new clips before this clip ID",
|
|
453
|
-
" * @constraint minLength(1)",
|
|
454
|
-
" */",
|
|
455
|
-
" before_clip_id?: string;",
|
|
456
|
-
" /**",
|
|
457
|
-
" * Insert new clips after this clip ID",
|
|
458
|
-
" * @constraint minLength(1)",
|
|
459
|
-
" */",
|
|
460
|
-
" after_clip_id?: string;",
|
|
718
|
+
"export interface PatchCaptionStyleInput {",
|
|
719
|
+
" readonly timelineEntityId: string;",
|
|
720
|
+
" readonly style: CaptionStyleFields;",
|
|
461
721
|
"}",
|
|
462
|
-
"",
|
|
463
|
-
"
|
|
464
|
-
" /**",
|
|
465
|
-
"
|
|
466
|
-
"
|
|
467
|
-
"
|
|
468
|
-
"
|
|
469
|
-
"
|
|
470
|
-
"
|
|
471
|
-
"
|
|
472
|
-
"
|
|
473
|
-
"
|
|
474
|
-
"
|
|
475
|
-
"
|
|
476
|
-
" * @constraint min(-60)",
|
|
477
|
-
" * @constraint max(20)",
|
|
478
|
-
" */",
|
|
479
|
-
" volume: number;",
|
|
480
|
-
" }[];",
|
|
722
|
+
"export interface RelationFacade {",
|
|
723
|
+
" list(): SandboxRelation[];",
|
|
724
|
+
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
725
|
+
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
726
|
+
" /** For physical-asset use sequence media as endpoint 0 and Asset as endpoint 1. */",
|
|
727
|
+
" link(input: LinkRelationInput): string;",
|
|
728
|
+
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
729
|
+
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
730
|
+
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
731
|
+
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
732
|
+
" /** Author ordered audio-script-render(output,script) without positional endpoint ambiguity. */",
|
|
733
|
+
" linkAudioScriptRender(input: LinkAudioScriptRenderRelationInput): string;",
|
|
734
|
+
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
735
|
+
" unlink(input: UnlinkRelationInput): void;",
|
|
481
736
|
"}",
|
|
482
|
-
"",
|
|
483
|
-
"
|
|
484
|
-
"
|
|
485
|
-
"
|
|
486
|
-
"
|
|
487
|
-
"
|
|
488
|
-
"
|
|
489
|
-
"
|
|
490
|
-
" */",
|
|
491
|
-
" clips: {",
|
|
492
|
-
" /**",
|
|
493
|
-
" * The video clip part ID to set speed for",
|
|
494
|
-
" * @constraint minLength(1)",
|
|
495
|
-
" */",
|
|
496
|
-
" clip_id: string;",
|
|
497
|
-
" /**",
|
|
498
|
-
" * The new speed setting, or null to reset to 1×",
|
|
499
|
-
" */",
|
|
500
|
-
" speed_shift: {",
|
|
501
|
-
" category: 'linear' | 'curve';",
|
|
502
|
-
" mode: string;",
|
|
503
|
-
" config:",
|
|
504
|
-
" | {",
|
|
505
|
-
" linear: {",
|
|
506
|
-
" /**",
|
|
507
|
-
" * @constraint positive",
|
|
508
|
-
" */",
|
|
509
|
-
" speed: number;",
|
|
510
|
-
" };",
|
|
511
|
-
" }",
|
|
512
|
-
" | {",
|
|
513
|
-
" curve: {",
|
|
514
|
-
" /**",
|
|
515
|
-
" * @constraint minLength(2)",
|
|
516
|
-
" */",
|
|
517
|
-
" keyframes: {",
|
|
518
|
-
" /**",
|
|
519
|
-
" * @constraint min(0)",
|
|
520
|
-
" * @constraint max(1)",
|
|
521
|
-
" */",
|
|
522
|
-
" position: number;",
|
|
523
|
-
" /**",
|
|
524
|
-
" * @constraint min(0)",
|
|
525
|
-
" */",
|
|
526
|
-
" rate: number;",
|
|
527
|
-
" /**",
|
|
528
|
-
" * Bezier tangent handle (x, y)",
|
|
529
|
-
" */",
|
|
530
|
-
" in_tangent?: { x: number; y: number };",
|
|
531
|
-
" /**",
|
|
532
|
-
" * Bezier tangent handle (x, y)",
|
|
533
|
-
" */",
|
|
534
|
-
" out_tangent?: { x: number; y: number };",
|
|
535
|
-
" }[];",
|
|
536
|
-
" };",
|
|
537
|
-
" };",
|
|
538
|
-
" } | null;",
|
|
539
|
-
" }[];",
|
|
737
|
+
"export interface ReplaceClipContentInput {",
|
|
738
|
+
" readonly clipEntityId: string;",
|
|
739
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
740
|
+
" readonly contentEntityId: string;",
|
|
741
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
742
|
+
" readonly duration: SequenceDuration<number>;",
|
|
743
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
744
|
+
" readonly timeRemapping?: JsonValue;",
|
|
540
745
|
"}",
|
|
541
|
-
"",
|
|
542
|
-
"
|
|
543
|
-
"
|
|
544
|
-
"
|
|
545
|
-
"export interface ReplaceVideoClipContentInput {",
|
|
546
|
-
" /**",
|
|
547
|
-
" * Video clips whose media is being replaced",
|
|
548
|
-
" * @constraint minLength(1)",
|
|
549
|
-
" */",
|
|
550
|
-
" clips: {",
|
|
551
|
-
" /**",
|
|
552
|
-
" * Existing video clip part ID to re-point",
|
|
553
|
-
" * @constraint minLength(1)",
|
|
554
|
-
" */",
|
|
555
|
-
" clip_id: string;",
|
|
556
|
-
" /**",
|
|
557
|
-
" * The new media asset ID",
|
|
558
|
-
" * @constraint minLength(1)",
|
|
559
|
-
" */",
|
|
560
|
-
" origin_media_id: string;",
|
|
561
|
-
" /**",
|
|
562
|
-
" * The new media's intrinsic full length",
|
|
563
|
-
" * @constraint int",
|
|
564
|
-
" * @constraint positive",
|
|
565
|
-
" */",
|
|
566
|
-
" media_duration_ms: number;",
|
|
567
|
-
" /**",
|
|
568
|
-
" * Trim window start in the new media (usually 0)",
|
|
569
|
-
" * @constraint int",
|
|
570
|
-
" * @constraint min(0)",
|
|
571
|
-
" */",
|
|
572
|
-
" play_in: number;",
|
|
573
|
-
" /**",
|
|
574
|
-
" * Trim window end in the new media (usually = media_duration_ms)",
|
|
575
|
-
" * @constraint int",
|
|
576
|
-
" * @constraint positive",
|
|
577
|
-
" */",
|
|
578
|
-
" play_out: number;",
|
|
579
|
-
" /**",
|
|
580
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
581
|
-
" * @constraint min(-60)",
|
|
582
|
-
" * @constraint max(20)",
|
|
583
|
-
" */",
|
|
584
|
-
" volume: number;",
|
|
585
|
-
" }[];",
|
|
746
|
+
"export interface ReplaceMediaClipInput {",
|
|
747
|
+
" readonly clipEntityId: string;",
|
|
748
|
+
" readonly media: VisualMediaAssetFact;",
|
|
749
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
586
750
|
"}",
|
|
587
|
-
"",
|
|
588
|
-
"
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
-
"
|
|
592
|
-
" */",
|
|
593
|
-
" old_clip_ids: string[];",
|
|
594
|
-
" /**",
|
|
595
|
-
" * The replacement clips, in the order they take on the track",
|
|
596
|
-
" * @constraint minLength(1)",
|
|
597
|
-
" */",
|
|
598
|
-
" new_clips: {",
|
|
599
|
-
" /**",
|
|
600
|
-
" * The replacement media asset ID. Omit to create an empty placeholder clip.",
|
|
601
|
-
" * @constraint minLength(1)",
|
|
602
|
-
" */",
|
|
603
|
-
" media_id?: string;",
|
|
604
|
-
" /**",
|
|
605
|
-
" * The source media's intrinsic full length in ms",
|
|
606
|
-
" * @constraint int",
|
|
607
|
-
" * @constraint positive",
|
|
608
|
-
" */",
|
|
609
|
-
" media_duration_ms: number;",
|
|
610
|
-
" /**",
|
|
611
|
-
" * Trim window start in the media (default 0)",
|
|
612
|
-
" * @constraint int",
|
|
613
|
-
" * @constraint min(0)",
|
|
614
|
-
" */",
|
|
615
|
-
" play_in?: number;",
|
|
616
|
-
" /**",
|
|
617
|
-
" * Trim window end in the media (default media_duration_ms)",
|
|
618
|
-
" * @constraint int",
|
|
619
|
-
" * @constraint positive",
|
|
620
|
-
" */",
|
|
621
|
-
" play_out?: number;",
|
|
622
|
-
" }[];",
|
|
623
|
-
" /**",
|
|
624
|
-
" * What happens to speeches anchored to the replaced clips (required — see the policy doc)",
|
|
625
|
-
" */",
|
|
626
|
-
" on_anchored: 'remap' | 'cascade';",
|
|
751
|
+
"export interface ReplaceSequentialClipsInput {",
|
|
752
|
+
" readonly timelineEntityId: string;",
|
|
753
|
+
" readonly oldClipEntityIds: readonly string[];",
|
|
754
|
+
" readonly newClips: readonly ReplacementMediaClipInput[];",
|
|
755
|
+
" readonly onAnchored: 'remap' | 'cascade';",
|
|
627
756
|
"}",
|
|
628
|
-
"",
|
|
629
|
-
"
|
|
630
|
-
"
|
|
631
|
-
"
|
|
632
|
-
"
|
|
633
|
-
" /**",
|
|
634
|
-
" * Video clips with their new trim windows",
|
|
635
|
-
" * @constraint minLength(1)",
|
|
636
|
-
" */",
|
|
637
|
-
" clips: {",
|
|
638
|
-
" /**",
|
|
639
|
-
" * The video clip part ID to re-trim",
|
|
640
|
-
" * @constraint minLength(1)",
|
|
641
|
-
" */",
|
|
642
|
-
" clip_id: string;",
|
|
643
|
-
" /**",
|
|
644
|
-
" * New trim window start in the source media",
|
|
645
|
-
" * @constraint int",
|
|
646
|
-
" * @constraint min(0)",
|
|
647
|
-
" */",
|
|
648
|
-
" play_in: number;",
|
|
649
|
-
" /**",
|
|
650
|
-
" * New trim window end in the source media",
|
|
651
|
-
" * @constraint int",
|
|
652
|
-
" * @constraint positive",
|
|
653
|
-
" */",
|
|
654
|
-
" play_out: number;",
|
|
655
|
-
" }[];",
|
|
757
|
+
"export interface ReplacementMediaClipInput {",
|
|
758
|
+
" readonly clipEntityId?: string;",
|
|
759
|
+
" readonly media: VisualMediaAssetFact;",
|
|
760
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
761
|
+
" readonly volume?: number;",
|
|
656
762
|
"}",
|
|
657
|
-
"",
|
|
658
|
-
"
|
|
659
|
-
"
|
|
660
|
-
"
|
|
661
|
-
"export interface AddSpeechesInput extends SpeechAssets {}",
|
|
662
|
-
"",
|
|
663
|
-
"/**",
|
|
664
|
-
" * Delete speeches with their captions.",
|
|
665
|
-
" */",
|
|
666
|
-
"export interface DeleteSpeechesInput {",
|
|
667
|
-
" /**",
|
|
668
|
-
" * Speech part IDs to delete (their captions cascade-delete)",
|
|
669
|
-
" * @constraint minLength(1)",
|
|
670
|
-
" */",
|
|
671
|
-
" speech_ids: string[];",
|
|
763
|
+
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
764
|
+
" entity_id: string;",
|
|
765
|
+
" entity_kind: K;",
|
|
766
|
+
" payload: EntityPayloadByKind[K];",
|
|
672
767
|
"}",
|
|
673
|
-
"",
|
|
674
|
-
"
|
|
675
|
-
"
|
|
676
|
-
"
|
|
677
|
-
"
|
|
678
|
-
"
|
|
679
|
-
"
|
|
680
|
-
" * @constraint minLength(1)",
|
|
681
|
-
" */",
|
|
682
|
-
" speeches: {",
|
|
683
|
-
" /**",
|
|
684
|
-
" * The speech part ID to move",
|
|
685
|
-
" * @constraint minLength(1)",
|
|
686
|
-
" */",
|
|
687
|
-
" speech_id: string;",
|
|
688
|
-
" /**",
|
|
689
|
-
" * New absolute start time on the timeline",
|
|
690
|
-
" * @constraint int",
|
|
691
|
-
" * @constraint min(0)",
|
|
692
|
-
" */",
|
|
693
|
-
" new_start_ms: number;",
|
|
694
|
-
" }[];",
|
|
768
|
+
"export interface SandboxRelation {",
|
|
769
|
+
" relation_id: string;",
|
|
770
|
+
" relation_kind: KnownRelationKind;",
|
|
771
|
+
" endpoint_0_entity_id: string;",
|
|
772
|
+
" endpoint_1_entity_id: string;",
|
|
773
|
+
" metadata: JsonObject;",
|
|
774
|
+
" trace: JsonObject;",
|
|
695
775
|
"}",
|
|
696
|
-
"",
|
|
697
|
-
"
|
|
698
|
-
"
|
|
699
|
-
"
|
|
700
|
-
"
|
|
701
|
-
"",
|
|
702
|
-
"
|
|
703
|
-
"",
|
|
704
|
-
"
|
|
705
|
-
"
|
|
706
|
-
"
|
|
707
|
-
"
|
|
708
|
-
"
|
|
709
|
-
"
|
|
710
|
-
"
|
|
711
|
-
"
|
|
712
|
-
"
|
|
713
|
-
"
|
|
714
|
-
"
|
|
715
|
-
"
|
|
716
|
-
"
|
|
717
|
-
"
|
|
718
|
-
"
|
|
719
|
-
"
|
|
720
|
-
"
|
|
721
|
-
"
|
|
776
|
+
"export type ScriptTextSegment = JsonObject & {",
|
|
777
|
+
" segmentId: string;",
|
|
778
|
+
" text: string;",
|
|
779
|
+
" language?: string;",
|
|
780
|
+
"};",
|
|
781
|
+
"export type SequenceDuration<Span = unknown> =",
|
|
782
|
+
" | {",
|
|
783
|
+
" readonly mode: 'from-source';",
|
|
784
|
+
" }",
|
|
785
|
+
" | {",
|
|
786
|
+
" readonly mode: 'fixed';",
|
|
787
|
+
" readonly value: Span;",
|
|
788
|
+
" };",
|
|
789
|
+
"export interface SequenceRange<Point = unknown> {",
|
|
790
|
+
" readonly start: Point;",
|
|
791
|
+
" readonly end: Point;",
|
|
792
|
+
"}",
|
|
793
|
+
"export type SequentialClipAnchor =",
|
|
794
|
+
" | {",
|
|
795
|
+
" readonly position: 'before' | 'after';",
|
|
796
|
+
" readonly clipEntityId: string;",
|
|
797
|
+
" }",
|
|
798
|
+
" | {",
|
|
799
|
+
" readonly position: 'trackStart';",
|
|
800
|
+
" };",
|
|
801
|
+
"export interface SetBgmInput {",
|
|
802
|
+
" readonly timelineEntityId: string;",
|
|
803
|
+
" readonly bgmClipEntityId: string;",
|
|
804
|
+
" readonly media: AudioMediaAssetFact;",
|
|
805
|
+
" readonly volume: number;",
|
|
722
806
|
"}",
|
|
723
|
-
"",
|
|
724
|
-
"/**",
|
|
725
|
-
" * Toggle caption visibility (the caption track's `is_hidden` flag).",
|
|
726
|
-
" */",
|
|
727
807
|
"export interface SetCaptionVisibilityInput {",
|
|
728
|
-
"
|
|
729
|
-
"
|
|
730
|
-
" */",
|
|
731
|
-
" is_hidden: boolean;",
|
|
808
|
+
" readonly timelineEntityId: string;",
|
|
809
|
+
" readonly hidden: boolean;",
|
|
732
810
|
"}",
|
|
733
|
-
"",
|
|
734
|
-
"
|
|
735
|
-
"
|
|
736
|
-
" */",
|
|
737
|
-
"export interface SetCaptionStyleInput {",
|
|
738
|
-
" /**",
|
|
739
|
-
" * Font ID referencing a font from the font library",
|
|
740
|
-
" * @constraint minLength(1)",
|
|
741
|
-
" */",
|
|
742
|
-
" font_id?: string;",
|
|
743
|
-
" /**",
|
|
744
|
-
" * Font size in points",
|
|
745
|
-
" * @constraint positive",
|
|
746
|
-
" */",
|
|
747
|
-
" font_size?: number;",
|
|
748
|
-
" /**",
|
|
749
|
-
" * Font color as hex string, e.g. \"#FFFFFF\"",
|
|
750
|
-
" * @constraint minLength(1)",
|
|
751
|
-
" */",
|
|
752
|
-
" font_color?: string;",
|
|
753
|
-
" /**",
|
|
754
|
-
" * Numeric font weight, e.g. 400 or 700",
|
|
755
|
-
" * @constraint int",
|
|
756
|
-
" */",
|
|
757
|
-
" font_weight?: number;",
|
|
758
|
-
" /**",
|
|
759
|
-
" * Entrance animation preset ID, e.g. \"fade\" or \"none\"",
|
|
760
|
-
" */",
|
|
761
|
-
" entrance_animation?: string;",
|
|
762
|
-
" /**",
|
|
763
|
-
" * Entrance animation duration in ms",
|
|
764
|
-
" * @constraint min(0)",
|
|
765
|
-
" */",
|
|
766
|
-
" entrance_animation_duration_ms?: number;",
|
|
767
|
-
" /**",
|
|
768
|
-
" * Outline/stroke color as hex string, e.g. \"#000000\"",
|
|
769
|
-
" * @constraint minLength(1)",
|
|
770
|
-
" */",
|
|
771
|
-
" stroke_color?: string;",
|
|
772
|
-
" /**",
|
|
773
|
-
" * Outline/stroke width in pixels",
|
|
774
|
-
" * @constraint min(0)",
|
|
775
|
-
" */",
|
|
776
|
-
" stroke_width?: number;",
|
|
777
|
-
" /**",
|
|
778
|
-
" * Caption center X as a fraction (0.0 to 1.0)",
|
|
779
|
-
" */",
|
|
780
|
-
" position_x?: number;",
|
|
781
|
-
" /**",
|
|
782
|
-
" * Caption center Y as a fraction (0.0 to 1.0)",
|
|
783
|
-
" */",
|
|
784
|
-
" position_y?: number;",
|
|
811
|
+
"export interface SetClipPlacementInput {",
|
|
812
|
+
" readonly clipEntityId: string;",
|
|
813
|
+
" readonly placement: ClipPlacement;",
|
|
785
814
|
"}",
|
|
786
|
-
"",
|
|
787
|
-
"
|
|
788
|
-
"
|
|
789
|
-
" */",
|
|
790
|
-
"export interface SetBgmInput {",
|
|
791
|
-
" /**",
|
|
792
|
-
" * The bgm part ID to write",
|
|
793
|
-
" * @constraint minLength(1)",
|
|
794
|
-
" */",
|
|
795
|
-
" bgm_id: string;",
|
|
796
|
-
" /**",
|
|
797
|
-
" * @constraint minLength(1)",
|
|
798
|
-
" */",
|
|
799
|
-
" audio_storage_key: string;",
|
|
800
|
-
" /**",
|
|
801
|
-
" * The media asset ID",
|
|
802
|
-
" * @constraint minLength(1)",
|
|
803
|
-
" */",
|
|
804
|
-
" origin_media_id: string;",
|
|
805
|
-
" /**",
|
|
806
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
807
|
-
" * @constraint min(-60)",
|
|
808
|
-
" * @constraint max(20)",
|
|
809
|
-
" */",
|
|
810
|
-
" volume: number;",
|
|
815
|
+
"export interface SetClipSpeedInput {",
|
|
816
|
+
" readonly clipEntityId: string;",
|
|
817
|
+
" readonly timeRemapping: LinearClipSpeed | null;",
|
|
811
818
|
"}",
|
|
812
|
-
"",
|
|
813
|
-
"
|
|
814
|
-
"
|
|
815
|
-
"
|
|
816
|
-
"export interface DeleteBgmInput {",
|
|
817
|
-
" [key: string]: never;",
|
|
819
|
+
"export interface SetClipVolumeInput {",
|
|
820
|
+
" readonly clipEntityId: string;",
|
|
821
|
+
" /** Playback gain in decibels. */",
|
|
822
|
+
" readonly volume: number;",
|
|
818
823
|
"}",
|
|
819
|
-
"",
|
|
820
|
-
"
|
|
821
|
-
"
|
|
822
|
-
" * List of bgm parts with their new volume settings",
|
|
823
|
-
" * @constraint minLength(1)",
|
|
824
|
-
" */",
|
|
825
|
-
" bgm: {",
|
|
826
|
-
" /**",
|
|
827
|
-
" * The bgm part ID to adjust volume for",
|
|
828
|
-
" * @constraint minLength(1)",
|
|
829
|
-
" */",
|
|
830
|
-
" bgm_id: string;",
|
|
831
|
-
" /**",
|
|
832
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
833
|
-
" * @constraint min(-60)",
|
|
834
|
-
" * @constraint max(20)",
|
|
835
|
-
" */",
|
|
836
|
-
" volume: number;",
|
|
837
|
-
" }[];",
|
|
824
|
+
"export interface TrimClipInput {",
|
|
825
|
+
" readonly clipEntityId: string;",
|
|
826
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
838
827
|
"}",
|
|
839
|
-
"",
|
|
840
|
-
"
|
|
841
|
-
"
|
|
842
|
-
"
|
|
843
|
-
"
|
|
844
|
-
"
|
|
845
|
-
"
|
|
846
|
-
" addVideoClips(input: AddVideoClipsInput): Promise<void>;",
|
|
847
|
-
" adjustVideoClipVolume(input: AdjustVideoClipVolumeInput): Promise<void>;",
|
|
848
|
-
" /** Set the playback speed of existing video clips. */",
|
|
849
|
-
" setVideoClipSpeedShift(input: SetVideoClipSpeedShiftInput): Promise<void>;",
|
|
850
|
-
" /** Replace the media backing existing video clips. */",
|
|
851
|
-
" replaceVideoClipContent(input: ReplaceVideoClipContentInput): Promise<void>;",
|
|
852
|
-
" replaceVideoClipSequence(input: ReplaceVideoClipSequenceInput): Promise<void>;",
|
|
853
|
-
" /** Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window). */",
|
|
854
|
-
" adjustVideoClipDuration(input: AdjustVideoClipDurationInput): Promise<void>;",
|
|
855
|
-
" /** Add speeches (and their captions). */",
|
|
856
|
-
" addSpeeches(input: AddSpeechesInput): Promise<void>;",
|
|
857
|
-
" /** Delete speeches with their captions. */",
|
|
858
|
-
" deleteSpeeches(input: DeleteSpeechesInput): Promise<void>;",
|
|
859
|
-
" /** Move speeches in time. */",
|
|
860
|
-
" moveSpeeches(input: MoveSpeechesInput): Promise<void>;",
|
|
861
|
-
" /** Change a speech's script or voice. */",
|
|
862
|
-
" changeSpeechScript(input: ChangeSpeechScriptInput): Promise<void>;",
|
|
863
|
-
" changeSpeechVoice(input: ChangeSpeechVoiceInput): Promise<void>;",
|
|
864
|
-
" adjustSpeechVolume(input: AdjustSpeechVolumeInput): Promise<void>;",
|
|
865
|
-
" /** Toggle caption visibility (the caption track's `is_hidden` flag). */",
|
|
866
|
-
" setCaptionVisibility(input: SetCaptionVisibilityInput): Promise<void>;",
|
|
867
|
-
" /** Set the caption visual style. */",
|
|
868
|
-
" setCaptionStyle(input: SetCaptionStyleInput): Promise<void>;",
|
|
869
|
-
" /** Set the document BGM. */",
|
|
870
|
-
" setBgm(input: SetBgmInput): Promise<void>;",
|
|
871
|
-
" /** Remove the document BGM. */",
|
|
872
|
-
" deleteBgm(input: DeleteBgmInput): Promise<void>;",
|
|
873
|
-
" adjustBgmVolume(input: AdjustBgmVolumeInput): Promise<void>;",
|
|
828
|
+
"export interface UnboundedConstantSequencePayload extends JsonObject {",
|
|
829
|
+
" extent: {",
|
|
830
|
+
" kind: 'unbounded';",
|
|
831
|
+
" start: number;",
|
|
832
|
+
" };",
|
|
833
|
+
" sampling: 'constant';",
|
|
834
|
+
" coordinateSpace: JsonValue;",
|
|
874
835
|
"}",
|
|
875
|
-
"",
|
|
876
|
-
"
|
|
877
|
-
"export interface TimelineClipDescriptor {",
|
|
878
|
-
" id: string;",
|
|
879
|
-
" start_ms: number;",
|
|
880
|
-
" end_ms: number;",
|
|
881
|
-
" duration_ms: number;",
|
|
882
|
-
" speed_shift: unknown;",
|
|
883
|
-
" volume: number | undefined;",
|
|
884
|
-
" media_id: string | undefined;",
|
|
836
|
+
"export interface UnlinkRelationInput {",
|
|
837
|
+
" relation_id: string;",
|
|
885
838
|
"}",
|
|
886
|
-
"",
|
|
887
|
-
"
|
|
888
|
-
"
|
|
889
|
-
"
|
|
890
|
-
"
|
|
891
|
-
"
|
|
892
|
-
"
|
|
893
|
-
"
|
|
894
|
-
"
|
|
895
|
-
"
|
|
839
|
+
"export interface UpdateClipInput {",
|
|
840
|
+
" readonly clipEntityId: string;",
|
|
841
|
+
" /** Complete replacement for the Clip-owned payload. */",
|
|
842
|
+
" readonly payload: JsonObject;",
|
|
843
|
+
"}",
|
|
844
|
+
"export interface UpdateClipMarkerInput {",
|
|
845
|
+
" readonly clipEntityId: string;",
|
|
846
|
+
" readonly sourceRange?: SequenceRange<number>;",
|
|
847
|
+
" /** Passing `undefined` explicitly removes the optional target range. */",
|
|
848
|
+
" readonly targetRange?: SequenceRange<number> | undefined;",
|
|
849
|
+
" readonly duration?: SequenceDuration<number>;",
|
|
850
|
+
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
851
|
+
" readonly timeRemapping?: JsonValue | undefined;",
|
|
852
|
+
"}",
|
|
853
|
+
"export interface UpdateEntityInput {",
|
|
854
|
+
" entity_id: string;",
|
|
855
|
+
" payload: JsonObject;",
|
|
856
|
+
"}",
|
|
857
|
+
"export interface VideoMediaAssetFact {",
|
|
858
|
+
" readonly assetId: string;",
|
|
859
|
+
" readonly kind: 'video';",
|
|
860
|
+
" readonly durationMs: number;",
|
|
861
|
+
" readonly storageKey?: string;",
|
|
862
|
+
"}",
|
|
863
|
+
"export type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;",
|
|
864
|
+
"export interface VoiceDescriptor {",
|
|
865
|
+
" readonly system: 'voice-library';",
|
|
866
|
+
" readonly key: string;",
|
|
867
|
+
" readonly name?: string;",
|
|
868
|
+
"}",
|
|
869
|
+
"export interface VoiceMediaAssetFact {",
|
|
870
|
+
" /** Stable external speech result id, independent of the placed Clip id. */",
|
|
871
|
+
" readonly assetId: string;",
|
|
872
|
+
" readonly kind: 'voice';",
|
|
873
|
+
" readonly durationMs: number;",
|
|
874
|
+
" readonly storageKey: string;",
|
|
875
|
+
" readonly voice: VoiceDescriptor;",
|
|
876
|
+
"}",
|
|
877
|
+
"export interface VoiceoverCaptionFact {",
|
|
878
|
+
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
879
|
+
" readonly captionClipEntityId: string;",
|
|
880
|
+
" readonly text: string;",
|
|
881
|
+
" readonly startMs: number;",
|
|
882
|
+
" readonly durationMs: number;",
|
|
883
|
+
" readonly style?: CaptionStyleFields;",
|
|
884
|
+
"}",
|
|
885
|
+
"export interface VoiceoverTakeInput {",
|
|
886
|
+
" readonly timelineEntityId: string;",
|
|
887
|
+
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
888
|
+
" readonly voiceoverClipEntityId: string;",
|
|
889
|
+
" readonly hostClipEntityId: string;",
|
|
890
|
+
" readonly anchorOffset: number;",
|
|
891
|
+
" readonly media: VoiceMediaAssetFact;",
|
|
892
|
+
" /** Complete spoken text; the editor owns the deterministic local script segment identity. */",
|
|
893
|
+
" readonly scriptText: string;",
|
|
894
|
+
" readonly volume: number;",
|
|
895
|
+
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
896
|
+
"}",
|
|
897
|
+
"export interface VoiceoverTakeResult {",
|
|
898
|
+
" readonly voiceoverClipEntityId: string;",
|
|
899
|
+
" readonly voiceEntityId: string;",
|
|
900
|
+
" readonly audioScriptEntityId: string;",
|
|
901
|
+
" readonly captionClipEntityIds: readonly string[];",
|
|
902
|
+
"}",
|
|
903
|
+
"/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
|
|
904
|
+
"export interface EditApi {",
|
|
905
|
+
" insertClip(input: InsertClipInput): ClipEntityId;",
|
|
906
|
+
" insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
|
|
907
|
+
" updateClipMarker(input: UpdateClipMarkerInput): void;",
|
|
908
|
+
" setClipPlacement(input: SetClipPlacementInput): void;",
|
|
909
|
+
" moveSequentialClips(input: MoveSequentialClipsInput): void;",
|
|
910
|
+
" moveClip(input: MoveClipInput): void;",
|
|
911
|
+
" replaceClipContent(input: ReplaceClipContentInput): void;",
|
|
912
|
+
" insertMediaClip(input: InsertMediaClipInput): ClipEntityId;",
|
|
913
|
+
" insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];",
|
|
914
|
+
" replaceMediaClip(input: ReplaceMediaClipInput): void;",
|
|
915
|
+
" setClipVolume(input: SetClipVolumeInput): void;",
|
|
916
|
+
" setClipSpeed(input: SetClipSpeedInput): void;",
|
|
917
|
+
" trimClip(input: TrimClipInput): void;",
|
|
918
|
+
" replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];",
|
|
919
|
+
" deleteClip(input: DeleteClipInput): void;",
|
|
920
|
+
" deleteClipTree(input: DeleteClipTreeInput): void;",
|
|
921
|
+
" updateClip(input: UpdateClipInput): void;",
|
|
922
|
+
" upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;",
|
|
923
|
+
" moveVoiceover(input: MoveVoiceoverInput): void;",
|
|
924
|
+
" moveClipsToStarts(input: MoveClipsToStartsInput): void;",
|
|
925
|
+
" deleteVoiceover(input: DeleteVoiceoverInput): void;",
|
|
926
|
+
" setBgm(input: SetBgmInput): ClipEntityId;",
|
|
927
|
+
" deleteBgm(input: DeleteBgmInput): void;",
|
|
928
|
+
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
929
|
+
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
896
930
|
"}",
|
|
897
|
-
"",
|
|
898
|
-
"/** Opaque VideoDraft projection (full IDL lives in host document types). */",
|
|
899
|
-
"export type VideoDraftProjection = {",
|
|
900
|
-
" readonly timeline?: { readonly duration_ms?: number };",
|
|
901
|
-
" readonly [key: string]: unknown;",
|
|
902
|
-
"};",
|
|
903
|
-
"",
|
|
904
|
-
"/** Agent read surface over the forked document. */",
|
|
905
931
|
"export interface TimelineApi {",
|
|
906
|
-
"
|
|
907
|
-
" snapshot(): VideoDraftProjection;",
|
|
908
|
-
" /** Clips whose midpoint falls in `[startMs, endMs)`. */",
|
|
909
|
-
" clipsInRange(startMs: number, endMs: number): TimelineClipDescriptor[];",
|
|
910
|
-
" /** Look up a part by id, or null if missing. */",
|
|
911
|
-
" part(id: string): TimelinePartDescriptor | null;",
|
|
932
|
+
" snapshot(): EntityStoreSnapshot;",
|
|
912
933
|
"}",
|
|
913
|
-
"",
|
|
914
|
-
"/** Opaque checkpoint handle for rollback. */",
|
|
915
934
|
"export interface SandboxCheckpoint {",
|
|
916
935
|
" readonly index: number;",
|
|
917
936
|
"}",
|
|
918
|
-
"",
|
|
919
937
|
"export declare const edit: EditApi;",
|
|
920
938
|
"export declare const timeline: TimelineApi;",
|
|
921
|
-
"",
|
|
922
|
-
"
|
|
939
|
+
"export declare const entities: EntityFacade;",
|
|
940
|
+
"export declare const relations: RelationFacade;",
|
|
923
941
|
"export declare function checkpoint(): SandboxCheckpoint;",
|
|
924
|
-
"/** Roll the sandbox document back to a prior checkpoint. */",
|
|
925
942
|
"export declare function rollbackTo(cp: SandboxCheckpoint): void;",
|
|
926
|
-
"
|
|
927
|
-
"export declare const inputs: unknown;",
|
|
943
|
+
"export declare const inputs: Readonly<Record<string, unknown>>;",
|
|
928
944
|
""
|
|
929
945
|
].join("\n");
|
|
930
946
|
//#endregion
|
|
931
947
|
//#region src/prompt.ts
|
|
932
948
|
const MEDEO_TOOL_DESCRIPTION = `
|
|
933
|
-
Edit
|
|
949
|
+
Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
|
|
934
950
|
|
|
935
951
|
Operations:
|
|
936
|
-
- snapshot: return the
|
|
937
|
-
-
|
|
938
|
-
-
|
|
952
|
+
- snapshot: return the Entity/Relation state summary and opaque base version.
|
|
953
|
+
- migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
|
|
954
|
+
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation, generated Relations and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled generation/asset facts through inputs. A successful run returns preview, logs, base revision and plan_id.
|
|
955
|
+
- commit-plan: commit the complete Entity/Relation plan through revision CAS. The server derives the read-only timeline projection in the same transaction. There is no separate writable timeline plan and no preflight replay into a legacy editor. A failed transport is unconfirmed, never committed; retry the same plan_id.
|
|
939
956
|
|
|
940
957
|
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
|
|
958
|
+
|
|
959
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, reuse or create the appropriate media Entity and physical-asset Relation, then pass the media Entity id to edit.insertClip. A raw asset id or URL is not valid contentEntityId. Asset and media Entity identity are not one-to-one. Recall generation history and author known generated(output,input) relations in the same plan; do not invent an input for text-only generation. relations.of(entityId) is endpoint-agnostic.
|
|
941
960
|
`.trim();
|
|
942
961
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
943
962
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
944
|
-
|
|
963
|
+
timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
|
|
964
|
+
Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
965
|
+
Before importing an Asset, call entities.findByAssetId(assetId), inspect every match, and decide whether an existing Entity represents the intended logical asset. Multiple matches are valid; do not assume Asset↔media is one-to-one.
|
|
966
|
+
For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
|
|
967
|
+
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. For generated lineage, use linkGenerated so endpoint 0 is output and endpoint 1 is input. relations.of remains endpoint-agnostic for lookup.
|
|
968
|
+
The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
969
|
+
Voice links to AudioScript through audio-script-render(output,script). Caption owns text/style and is placed through a Clip anchored to the Voice Clip; caption alignment/provenance agree with that Voice/AudioScript. BGM Audio keeps factual source duration and its Marker declares durationPolicy:'timeline'. External Asset identity and storageKey are distinct from the placed Clip identity. Never introduce a speech entity kind.
|
|
970
|
+
Create only the known entity kinds. On an empty document, explicitly create Timeline and Track(role='video_clip') and connect timeline-track before inserting a Clip. If snapshot reports legacy migration is required, recall the listed asset facts and call migrate-legacy first. Missing facts, unsupported layouts, and version conflicts fail closed; never fall back to an old timeline method or raw update endpoint.
|
|
945
971
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
946
972
|
`.trim();
|
|
947
973
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -960,15 +986,51 @@ When updated_since_previous_model_call is true, the document changed after the p
|
|
|
960
986
|
|
|
961
987
|
Sandbox TypeScript interface:
|
|
962
988
|
\`\`\`ts
|
|
963
|
-
${
|
|
989
|
+
${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
964
990
|
\`\`\`
|
|
965
991
|
`.trim();
|
|
966
992
|
}
|
|
967
993
|
//#endregion
|
|
968
994
|
//#region src/schema.ts
|
|
969
995
|
const MEDEO_TOOL_NAME = "medeo";
|
|
996
|
+
const assetFactProperties = {
|
|
997
|
+
assetId: {
|
|
998
|
+
type: "string",
|
|
999
|
+
minLength: 1
|
|
1000
|
+
},
|
|
1001
|
+
kind: {
|
|
1002
|
+
type: "string",
|
|
1003
|
+
enum: [
|
|
1004
|
+
"image",
|
|
1005
|
+
"video",
|
|
1006
|
+
"audio",
|
|
1007
|
+
"voice"
|
|
1008
|
+
]
|
|
1009
|
+
},
|
|
1010
|
+
durationMs: {
|
|
1011
|
+
type: "integer",
|
|
1012
|
+
minimum: 1
|
|
1013
|
+
},
|
|
1014
|
+
storageKey: {
|
|
1015
|
+
type: "string",
|
|
1016
|
+
minLength: 1
|
|
1017
|
+
},
|
|
1018
|
+
voice: {
|
|
1019
|
+
type: "object",
|
|
1020
|
+
additionalProperties: false,
|
|
1021
|
+
required: ["system", "key"],
|
|
1022
|
+
properties: {
|
|
1023
|
+
system: { const: "voice-library" },
|
|
1024
|
+
key: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
minLength: 1
|
|
1027
|
+
},
|
|
1028
|
+
name: { type: "string" }
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
970
1032
|
/**
|
|
971
|
-
* JSON Schema for the host-facing
|
|
1033
|
+
* JSON Schema for the host-facing `medeo` tool surface.
|
|
972
1034
|
*
|
|
973
1035
|
* The schema intentionally does not return or accept the full op journal:
|
|
974
1036
|
* journals stay in the tool process and are referenced by `plan_id`. This keeps
|
|
@@ -984,6 +1046,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
984
1046
|
type: "string",
|
|
985
1047
|
enum: [
|
|
986
1048
|
"snapshot",
|
|
1049
|
+
"migrate-legacy",
|
|
987
1050
|
"run-edit-script",
|
|
988
1051
|
"commit-plan"
|
|
989
1052
|
],
|
|
@@ -997,11 +1060,73 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
997
1060
|
script: {
|
|
998
1061
|
type: "string",
|
|
999
1062
|
minLength: 1,
|
|
1000
|
-
description: "JavaScript body for run-edit-script.
|
|
1063
|
+
description: "JavaScript body for run-edit-script. Use edit, timeline, entities, relations, checkpoint, rollbackTo, inputs, and console. Asset import, media relations, and timeline entity edits share one entity plan."
|
|
1001
1064
|
},
|
|
1002
1065
|
inputs: {
|
|
1003
1066
|
type: "object",
|
|
1004
|
-
description: "Pre-materialized, side-effect-free values passed into the script. Generation and network IO must happen in the host before this call."
|
|
1067
|
+
description: "Pre-materialized, side-effect-free values passed into the script, including recalled generation lineage and asset facts. Generation and network IO must happen in the host before this call."
|
|
1068
|
+
},
|
|
1069
|
+
asset_facts: {
|
|
1070
|
+
type: "array",
|
|
1071
|
+
description: "Factual asset metadata recalled by the host for migrate-legacy only. The package reads the canonical legacy snapshot and version itself; never supply a clip trim window as media duration.",
|
|
1072
|
+
items: { oneOf: [
|
|
1073
|
+
{
|
|
1074
|
+
type: "object",
|
|
1075
|
+
additionalProperties: false,
|
|
1076
|
+
required: ["assetId", "kind"],
|
|
1077
|
+
properties: {
|
|
1078
|
+
assetId: assetFactProperties.assetId,
|
|
1079
|
+
kind: { const: "image" },
|
|
1080
|
+
storageKey: assetFactProperties.storageKey
|
|
1081
|
+
}
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
type: "object",
|
|
1085
|
+
additionalProperties: false,
|
|
1086
|
+
required: [
|
|
1087
|
+
"assetId",
|
|
1088
|
+
"kind",
|
|
1089
|
+
"durationMs"
|
|
1090
|
+
],
|
|
1091
|
+
properties: {
|
|
1092
|
+
assetId: assetFactProperties.assetId,
|
|
1093
|
+
kind: { const: "video" },
|
|
1094
|
+
durationMs: assetFactProperties.durationMs,
|
|
1095
|
+
storageKey: assetFactProperties.storageKey
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
type: "object",
|
|
1100
|
+
additionalProperties: false,
|
|
1101
|
+
required: [
|
|
1102
|
+
"assetId",
|
|
1103
|
+
"kind",
|
|
1104
|
+
"durationMs",
|
|
1105
|
+
"storageKey"
|
|
1106
|
+
],
|
|
1107
|
+
properties: {
|
|
1108
|
+
assetId: assetFactProperties.assetId,
|
|
1109
|
+
kind: { const: "audio" },
|
|
1110
|
+
durationMs: assetFactProperties.durationMs,
|
|
1111
|
+
storageKey: assetFactProperties.storageKey
|
|
1112
|
+
}
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
type: "object",
|
|
1116
|
+
additionalProperties: false,
|
|
1117
|
+
required: [
|
|
1118
|
+
"assetId",
|
|
1119
|
+
"kind",
|
|
1120
|
+
"durationMs",
|
|
1121
|
+
"storageKey",
|
|
1122
|
+
"voice"
|
|
1123
|
+
],
|
|
1124
|
+
properties: {
|
|
1125
|
+
...assetFactProperties,
|
|
1126
|
+
kind: { const: "voice" }
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
] }
|
|
1005
1130
|
},
|
|
1006
1131
|
timeout_ms: {
|
|
1007
1132
|
type: "integer",
|
|
@@ -1024,11 +1149,24 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1024
1149
|
},
|
|
1025
1150
|
validation: {
|
|
1026
1151
|
type: "string",
|
|
1027
|
-
enum: ["version"
|
|
1028
|
-
description: "
|
|
1152
|
+
enum: ["version"],
|
|
1153
|
+
description: "Commit with Entity revision CAS; reject concurrent changes."
|
|
1029
1154
|
}
|
|
1030
1155
|
},
|
|
1031
1156
|
oneOf: [
|
|
1157
|
+
{
|
|
1158
|
+
required: [
|
|
1159
|
+
"op",
|
|
1160
|
+
"doc_id",
|
|
1161
|
+
"asset_facts"
|
|
1162
|
+
],
|
|
1163
|
+
properties: {
|
|
1164
|
+
op: { const: "migrate-legacy" },
|
|
1165
|
+
doc_id: { $ref: "#/properties/doc_id" },
|
|
1166
|
+
asset_facts: { $ref: "#/properties/asset_facts" }
|
|
1167
|
+
},
|
|
1168
|
+
additionalProperties: false
|
|
1169
|
+
},
|
|
1032
1170
|
{
|
|
1033
1171
|
required: ["op", "doc_id"],
|
|
1034
1172
|
properties: {
|
|
@@ -1073,39 +1211,37 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1073
1211
|
//#endregion
|
|
1074
1212
|
//#region src/session/commit-plan.ts
|
|
1075
1213
|
/**
|
|
1076
|
-
* Replay a sandbox journal into a
|
|
1077
|
-
*
|
|
1214
|
+
* Replay a sandbox journal into a manually-synchronized document and push the
|
|
1215
|
+
* whole plan as one causally complete update.
|
|
1078
1216
|
*
|
|
1079
|
-
* - Default / `{ validation: 'version' }`: if
|
|
1080
|
-
* `plan.base_version`, reject with zero writes.
|
|
1217
|
+
* - Default / `{ validation: 'version' }`: if the current document mark differs
|
|
1218
|
+
* from `plan.base_version`, reject with zero writes.
|
|
1081
1219
|
* - `{ validation: 'preflight' }`: skip the version gate; revalidate each op
|
|
1082
1220
|
* against a PlainMemoryAdapter seeded from the current live snapshot, then
|
|
1083
1221
|
* replay for real. A SchemaValidator failure becomes `op_conflict` with the
|
|
1084
1222
|
* failing entry's index. Journal integrity errors (unrecorded/unconsumed
|
|
1085
1223
|
* ids) still propagate as throws in both modes.
|
|
1086
1224
|
*/
|
|
1087
|
-
async function commitPlan(
|
|
1088
|
-
if (options?.validation === "preflight") return commitPlanPreflight(
|
|
1089
|
-
const actual =
|
|
1090
|
-
|
|
1225
|
+
async function commitPlan(doc, plan, options) {
|
|
1226
|
+
if (options?.validation === "preflight") return commitPlanPreflight(doc, plan);
|
|
1227
|
+
const actual = encodeDocVersionMark(doc.versionMark());
|
|
1228
|
+
const expected = decodeDocVersionMark(plan.base_version);
|
|
1229
|
+
if (expected == null || doc.hasChangedSince(expected)) return {
|
|
1091
1230
|
kind: "rejected",
|
|
1092
1231
|
reason: "version_mismatch",
|
|
1093
1232
|
expected: plan.base_version,
|
|
1094
1233
|
actual
|
|
1095
1234
|
};
|
|
1096
|
-
await replayJournal(
|
|
1097
|
-
return
|
|
1098
|
-
kind: "committed",
|
|
1099
|
-
ops_applied: plan.ops.length
|
|
1100
|
-
};
|
|
1235
|
+
await doc.replayJournal(plan.ops);
|
|
1236
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
1101
1237
|
}
|
|
1102
1238
|
/**
|
|
1103
1239
|
* Phase-2 path: scratch revalidation then real replay. Each entry is driven
|
|
1104
1240
|
* through `replayJournal` alone so a ValidationError maps to a stable index;
|
|
1105
1241
|
* integrity throws are not wrapped.
|
|
1106
1242
|
*/
|
|
1107
|
-
async function commitPlanPreflight(
|
|
1108
|
-
const scratch = createPlainMemoryAdapter(
|
|
1243
|
+
async function commitPlanPreflight(doc, plan) {
|
|
1244
|
+
const scratch = createPlainMemoryAdapter(doc.snapshot());
|
|
1109
1245
|
for (let index = 0; index < plan.ops.length; index++) {
|
|
1110
1246
|
const entry = plan.ops[index];
|
|
1111
1247
|
if (entry == null) continue;
|
|
@@ -1120,15 +1256,41 @@ async function commitPlanPreflight(session, plan) {
|
|
|
1120
1256
|
const entry = plan.ops[index];
|
|
1121
1257
|
if (entry == null) continue;
|
|
1122
1258
|
try {
|
|
1123
|
-
await replayJournal(
|
|
1259
|
+
await doc.replayJournal([entry]);
|
|
1124
1260
|
} catch (error) {
|
|
1125
1261
|
if (error instanceof ValidationError) return opConflict(index, entry.kind, `real replay: ${error.message}`);
|
|
1126
1262
|
throw error;
|
|
1127
1263
|
}
|
|
1128
1264
|
}
|
|
1265
|
+
return retryPlanPush(doc, plan.ops.length);
|
|
1266
|
+
}
|
|
1267
|
+
/** Push an already-replayed plan again without replaying or re-running its version gate. */
|
|
1268
|
+
async function retryPlanPush(doc, opsApplied) {
|
|
1269
|
+
const result = await doc.push();
|
|
1270
|
+
if (result.kind === "ack" || result.kind === "duplicate" || result.kind === "nothing_to_push") {
|
|
1271
|
+
const reconciled = result.collaborated ? await doc.pull() : void 0;
|
|
1272
|
+
const warnings = reconciled != null && !reconciled.ok ? [{
|
|
1273
|
+
kind: "pull_failed",
|
|
1274
|
+
message: reconciled.error.message
|
|
1275
|
+
}] : void 0;
|
|
1276
|
+
return {
|
|
1277
|
+
kind: "committed",
|
|
1278
|
+
ops_applied: opsApplied,
|
|
1279
|
+
collaborated: result.collaborated,
|
|
1280
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (result.kind === "rejected") return {
|
|
1284
|
+
kind: "rejected",
|
|
1285
|
+
reason: "push_rejected",
|
|
1286
|
+
...result.code !== void 0 ? { code: result.code } : {},
|
|
1287
|
+
message: result.error?.message ?? "mengine rejected the sandbox plan"
|
|
1288
|
+
};
|
|
1129
1289
|
return {
|
|
1130
|
-
kind: "
|
|
1131
|
-
|
|
1290
|
+
kind: "unconfirmed",
|
|
1291
|
+
reason: "push_failed",
|
|
1292
|
+
ops_applied: opsApplied,
|
|
1293
|
+
message: result.error?.message ?? "mengine push failed"
|
|
1132
1294
|
};
|
|
1133
1295
|
}
|
|
1134
1296
|
function opConflict(index, op_kind, message) {
|
|
@@ -1156,6 +1318,112 @@ function requiredContext(value, docId, field) {
|
|
|
1156
1318
|
if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
|
|
1157
1319
|
return resolved;
|
|
1158
1320
|
}
|
|
1321
|
+
function renderEntitySnapshot(state) {
|
|
1322
|
+
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1323
|
+
const shown = rows.slice(0, 200);
|
|
1324
|
+
return [
|
|
1325
|
+
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1326
|
+
...shown,
|
|
1327
|
+
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1328
|
+
].join("\n");
|
|
1329
|
+
}
|
|
1330
|
+
function migrationNotice(document, state) {
|
|
1331
|
+
if (state.entities.some((row) => row.entity_kind === "timeline") || Object.keys(document.part_library ?? {}).length === 0) return "";
|
|
1332
|
+
const assetIds = new Set(Object.values(document.part_library ?? {}).flatMap((part) => {
|
|
1333
|
+
const id = part.video_clip?.origin_media_id ?? part.bgm?.origin_media_id;
|
|
1334
|
+
return typeof id === "string" && id !== "" ? [id] : [];
|
|
1335
|
+
}));
|
|
1336
|
+
return `\nLegacy timeline migration required. Recall factual media metadata for ${JSON.stringify([...assetIds])}, then call migrate-legacy with asset_facts. Speech facts are read from the canonical legacy document. Take a fresh snapshot after migration before editing.`;
|
|
1337
|
+
}
|
|
1338
|
+
async function commitEntityPlan(client, plan) {
|
|
1339
|
+
const rows = plan.entity_rows;
|
|
1340
|
+
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1341
|
+
try {
|
|
1342
|
+
const committed = await client.commit(plan.entity_base_revision, rows, {
|
|
1343
|
+
deleted_entity_ids: plan.deleted_entity_ids ?? [],
|
|
1344
|
+
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1345
|
+
});
|
|
1346
|
+
return {
|
|
1347
|
+
kind: "committed",
|
|
1348
|
+
ops_applied: plan.entity_commands.length,
|
|
1349
|
+
collaborated: false,
|
|
1350
|
+
entity_revision: committed.revision
|
|
1351
|
+
};
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (error instanceof MengineEntityHttpRequestError) {
|
|
1354
|
+
if (error.status === 409 && isRevisionConflictPayload(error.payload)) {
|
|
1355
|
+
const actualFromPayload = revisionConflictActual(error.payload);
|
|
1356
|
+
try {
|
|
1357
|
+
const current = await client.fetchState();
|
|
1358
|
+
if (current.revision === plan.entity_base_revision + 1 && entityRowsEquivalent(current, rows)) return {
|
|
1359
|
+
kind: "committed",
|
|
1360
|
+
ops_applied: plan.entity_commands.length,
|
|
1361
|
+
collaborated: false,
|
|
1362
|
+
entity_revision: current.revision
|
|
1363
|
+
};
|
|
1364
|
+
return {
|
|
1365
|
+
kind: "rejected",
|
|
1366
|
+
reason: "entity_revision_mismatch",
|
|
1367
|
+
expected: plan.entity_base_revision,
|
|
1368
|
+
actual: current.revision
|
|
1369
|
+
};
|
|
1370
|
+
} catch {
|
|
1371
|
+
if (actualFromPayload !== void 0) return {
|
|
1372
|
+
kind: "rejected",
|
|
1373
|
+
reason: "entity_revision_mismatch",
|
|
1374
|
+
expected: plan.entity_base_revision,
|
|
1375
|
+
actual: actualFromPayload
|
|
1376
|
+
};
|
|
1377
|
+
return {
|
|
1378
|
+
kind: "unconfirmed",
|
|
1379
|
+
reason: "push_failed",
|
|
1380
|
+
ops_applied: plan.entity_commands.length,
|
|
1381
|
+
message: "entity-state conflict could not be reconciled"
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
kind: "rejected",
|
|
1387
|
+
reason: "entity_state_rejected",
|
|
1388
|
+
status: error.status,
|
|
1389
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
return {
|
|
1393
|
+
kind: "unconfirmed",
|
|
1394
|
+
reason: "push_failed",
|
|
1395
|
+
ops_applied: plan.entity_commands.length,
|
|
1396
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
function revisionConflictActual(payload) {
|
|
1401
|
+
if (!isRecord(payload)) return void 0;
|
|
1402
|
+
const actual = payload.actual_revision;
|
|
1403
|
+
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1404
|
+
}
|
|
1405
|
+
function isRevisionConflictPayload(payload) {
|
|
1406
|
+
return isRecord(payload) && payload.code === "revision_conflict";
|
|
1407
|
+
}
|
|
1408
|
+
function entityHttpErrorMessage(payload) {
|
|
1409
|
+
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1410
|
+
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1411
|
+
}
|
|
1412
|
+
function commitWarnings(result) {
|
|
1413
|
+
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1414
|
+
}
|
|
1415
|
+
function entityRowsEquivalent(left, right) {
|
|
1416
|
+
const normalize = (state) => ({
|
|
1417
|
+
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1418
|
+
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1419
|
+
});
|
|
1420
|
+
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
1421
|
+
}
|
|
1422
|
+
function canonicalJson(value) {
|
|
1423
|
+
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
1424
|
+
if (!isRecord(value)) return value;
|
|
1425
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
1426
|
+
}
|
|
1159
1427
|
function parseInput(value) {
|
|
1160
1428
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
1161
1429
|
const op = value.op;
|
|
@@ -1166,6 +1434,18 @@ function parseInput(value) {
|
|
|
1166
1434
|
op,
|
|
1167
1435
|
doc_id: docId
|
|
1168
1436
|
};
|
|
1437
|
+
if (op === "migrate-legacy") {
|
|
1438
|
+
if (Object.keys(value).some((key) => ![
|
|
1439
|
+
"op",
|
|
1440
|
+
"doc_id",
|
|
1441
|
+
"asset_facts"
|
|
1442
|
+
].includes(key))) throw new Error("migrate-legacy accepts asset_facts only; the package reads the canonical document and version");
|
|
1443
|
+
return {
|
|
1444
|
+
op,
|
|
1445
|
+
doc_id: docId,
|
|
1446
|
+
asset_facts: parseMigrationAssetFacts(value.asset_facts)
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1169
1449
|
if (op === "run-edit-script") {
|
|
1170
1450
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
1171
1451
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -1199,58 +1479,78 @@ function parseInput(value) {
|
|
|
1199
1479
|
/**
|
|
1200
1480
|
* Create the self-contained Medeo LLM tool.
|
|
1201
1481
|
*
|
|
1202
|
-
* The package owns
|
|
1482
|
+
* The package owns document construction, compact projection, sandbox execution,
|
|
1203
1483
|
* plan caching, commit, document get-or-create, and shutdown. The host supplies
|
|
1204
1484
|
* environment facts plus the authoritative legacy draft loader used only when
|
|
1205
1485
|
* Mengine has no document yet.
|
|
1206
1486
|
*/
|
|
1207
1487
|
function createMedeoTool(options) {
|
|
1208
|
-
const
|
|
1488
|
+
const documents = /* @__PURE__ */ new Map();
|
|
1489
|
+
const entityClients = /* @__PURE__ */ new Map();
|
|
1490
|
+
const documentTails = /* @__PURE__ */ new Map();
|
|
1491
|
+
const pendingPushes = /* @__PURE__ */ new Map();
|
|
1209
1492
|
const plans = /* @__PURE__ */ new Map();
|
|
1210
1493
|
const modelContextVersions = /* @__PURE__ */ new Map();
|
|
1211
1494
|
const maxPlans = options.maxPlans ?? DEFAULT_MAX_PLANS;
|
|
1212
1495
|
const maxModelContexts = options.maxModelContexts ?? DEFAULT_MAX_MODEL_CONTEXTS;
|
|
1213
1496
|
let closed = false;
|
|
1214
|
-
async function
|
|
1497
|
+
async function getDocument(docId) {
|
|
1215
1498
|
if (closed) throw new Error("medeo tool is closed");
|
|
1216
|
-
const existing =
|
|
1499
|
+
const existing = documents.get(docId);
|
|
1217
1500
|
if (existing != null) return await existing;
|
|
1218
1501
|
const created = (async () => {
|
|
1219
|
-
|
|
1502
|
+
return await getOrCreateDocument(new MengineHttpClient({
|
|
1220
1503
|
docId,
|
|
1221
1504
|
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1222
1505
|
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
1223
1506
|
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
1224
1507
|
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1225
|
-
});
|
|
1226
|
-
const peerId = optionalContext(options.peerId, docId);
|
|
1227
|
-
await getOrCreateDocument(client, docId, peerId);
|
|
1228
|
-
const session = new MengineDocSession({
|
|
1229
|
-
docId,
|
|
1230
|
-
client,
|
|
1231
|
-
...peerId !== void 0 ? { peerId } : {},
|
|
1232
|
-
...options.sseReconnectDelayMs !== void 0 ? { sseReconnectDelayMs: options.sseReconnectDelayMs } : {}
|
|
1233
|
-
});
|
|
1234
|
-
try {
|
|
1235
|
-
await session.start();
|
|
1236
|
-
return session;
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
session.destroy();
|
|
1239
|
-
throw error;
|
|
1240
|
-
}
|
|
1508
|
+
}), docId, optionalContext(options.peerId, docId));
|
|
1241
1509
|
})();
|
|
1242
|
-
|
|
1510
|
+
documents.set(docId, created);
|
|
1243
1511
|
try {
|
|
1244
1512
|
return await created;
|
|
1245
1513
|
} catch (error) {
|
|
1246
|
-
if (
|
|
1514
|
+
if (documents.get(docId) === created) documents.delete(docId);
|
|
1247
1515
|
throw error;
|
|
1248
1516
|
}
|
|
1249
1517
|
}
|
|
1518
|
+
function getEntityClient(docId) {
|
|
1519
|
+
if (closed) throw new Error("medeo tool is closed");
|
|
1520
|
+
const existing = entityClients.get(docId);
|
|
1521
|
+
if (existing != null) return existing;
|
|
1522
|
+
const client = new EntityHttpClient({
|
|
1523
|
+
docId,
|
|
1524
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1525
|
+
...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
|
|
1526
|
+
...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
|
|
1527
|
+
...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
|
|
1528
|
+
});
|
|
1529
|
+
entityClients.set(docId, client);
|
|
1530
|
+
return client;
|
|
1531
|
+
}
|
|
1532
|
+
async function runExclusive(docId, use) {
|
|
1533
|
+
const previous = documentTails.get(docId) ?? Promise.resolve();
|
|
1534
|
+
let release;
|
|
1535
|
+
const gate = new Promise((resolve) => {
|
|
1536
|
+
release = resolve;
|
|
1537
|
+
});
|
|
1538
|
+
const tail = previous.catch(() => {}).then(() => gate);
|
|
1539
|
+
documentTails.set(docId, tail);
|
|
1540
|
+
await previous.catch(() => {});
|
|
1541
|
+
try {
|
|
1542
|
+
return await use(await getDocument(docId));
|
|
1543
|
+
} finally {
|
|
1544
|
+
release();
|
|
1545
|
+
if (documentTails.get(docId) === tail) documentTails.delete(docId);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1250
1548
|
async function getOrCreateDocument(client, docId, peerId) {
|
|
1251
1549
|
try {
|
|
1252
|
-
await
|
|
1253
|
-
|
|
1550
|
+
return await ManualSyncDoc.open({
|
|
1551
|
+
client,
|
|
1552
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1553
|
+
});
|
|
1254
1554
|
} catch (error) {
|
|
1255
1555
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
1256
1556
|
if (options.loadInitialDraft === void 0) throw error;
|
|
@@ -1263,8 +1563,11 @@ function createMedeoTool(options) {
|
|
|
1263
1563
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
1264
1564
|
} catch (error) {
|
|
1265
1565
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
|
|
1266
|
-
await client.fetchSnapshot();
|
|
1267
1566
|
}
|
|
1567
|
+
return await ManualSyncDoc.open({
|
|
1568
|
+
client,
|
|
1569
|
+
...peerId !== void 0 ? { peerId } : {}
|
|
1570
|
+
});
|
|
1268
1571
|
}
|
|
1269
1572
|
function rememberPlan(docId, plan) {
|
|
1270
1573
|
const planId = randomUUID();
|
|
@@ -1273,109 +1576,242 @@ function createMedeoTool(options) {
|
|
|
1273
1576
|
plan
|
|
1274
1577
|
});
|
|
1275
1578
|
while (plans.size > maxPlans) {
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
plans.
|
|
1579
|
+
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
1580
|
+
protectedPlanIds.add(planId);
|
|
1581
|
+
const oldestEvictable = [...plans.keys()].find((candidate) => !protectedPlanIds.has(candidate));
|
|
1582
|
+
if (oldestEvictable === void 0) break;
|
|
1583
|
+
plans.delete(oldestEvictable);
|
|
1279
1584
|
}
|
|
1280
1585
|
return planId;
|
|
1281
1586
|
}
|
|
1587
|
+
function assertNoPendingPush(docId) {
|
|
1588
|
+
const pending = pendingPushes.get(docId);
|
|
1589
|
+
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1590
|
+
}
|
|
1591
|
+
function recordPushResult(docId, planId, plan, result) {
|
|
1592
|
+
if (result.kind === "unconfirmed") {
|
|
1593
|
+
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1594
|
+
kind: "timeline",
|
|
1595
|
+
planId,
|
|
1596
|
+
plan,
|
|
1597
|
+
opsApplied: result.ops_applied
|
|
1598
|
+
} : {
|
|
1599
|
+
kind: "entities",
|
|
1600
|
+
planId,
|
|
1601
|
+
plan
|
|
1602
|
+
});
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
pendingPushes.delete(docId);
|
|
1606
|
+
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1607
|
+
}
|
|
1608
|
+
async function fetchEntityStateForSandbox(docId) {
|
|
1609
|
+
return await getEntityClient(docId).fetchState();
|
|
1610
|
+
}
|
|
1611
|
+
async function commitCachedPlan(docId, _doc, plan, validation) {
|
|
1612
|
+
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1613
|
+
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1614
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1615
|
+
return await commitEntityPlan(getEntityClient(docId), plan);
|
|
1616
|
+
}
|
|
1617
|
+
async function observePull(doc) {
|
|
1618
|
+
const result = await doc.pull();
|
|
1619
|
+
if (result.ok) return { collaborated: result.changed };
|
|
1620
|
+
return {
|
|
1621
|
+
collaborated: false,
|
|
1622
|
+
warnings: [{
|
|
1623
|
+
kind: "pull_failed",
|
|
1624
|
+
message: result.error.message
|
|
1625
|
+
}]
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
function mergeWarnings(...groups) {
|
|
1629
|
+
const warnings = groups.flatMap((group) => group ?? []);
|
|
1630
|
+
return warnings.length > 0 ? warnings : void 0;
|
|
1631
|
+
}
|
|
1282
1632
|
async function getModelContext(input) {
|
|
1283
1633
|
const docId = input.doc_id.trim();
|
|
1284
1634
|
const contextId = input.context_id.trim();
|
|
1285
1635
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1286
1636
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
return result;
|
|
1637
|
+
return await runExclusive(docId, async (doc) => {
|
|
1638
|
+
const [, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(docId)]);
|
|
1639
|
+
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1640
|
+
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1641
|
+
const previousVersion = modelContextVersions.get(baselineKey);
|
|
1642
|
+
const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
|
|
1643
|
+
modelContextVersions.delete(baselineKey);
|
|
1644
|
+
modelContextVersions.set(baselineKey, documentVersion);
|
|
1645
|
+
while (modelContextVersions.size > maxModelContexts) {
|
|
1646
|
+
const oldest = modelContextVersions.keys().next().value;
|
|
1647
|
+
if (oldest === void 0) break;
|
|
1648
|
+
modelContextVersions.delete(oldest);
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
prompt: renderMedeoModelContext({
|
|
1652
|
+
documentVersion,
|
|
1653
|
+
updatedSincePreviousModelCall
|
|
1654
|
+
}),
|
|
1655
|
+
document_version: documentVersion,
|
|
1656
|
+
updated_since_previous_model_call: updatedSincePreviousModelCall
|
|
1657
|
+
};
|
|
1658
|
+
});
|
|
1310
1659
|
}
|
|
1311
1660
|
async function snapshot(input) {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1661
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1662
|
+
assertNoPendingPush(input.doc_id);
|
|
1663
|
+
const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
|
|
1664
|
+
return {
|
|
1665
|
+
ok: true,
|
|
1666
|
+
op: "snapshot",
|
|
1667
|
+
doc_id: input.doc_id,
|
|
1668
|
+
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
1669
|
+
preview: renderEntitySnapshot(entityState) + migrationNotice(doc.snapshot(), entityState),
|
|
1670
|
+
collaborated: pull.collaborated,
|
|
1671
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1672
|
+
};
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
async function migrate(input) {
|
|
1676
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1677
|
+
assertNoPendingPush(input.doc_id);
|
|
1678
|
+
const client = new EntityGraphHttpClient({
|
|
1679
|
+
docId: input.doc_id,
|
|
1680
|
+
httpOrigin: requiredContext(options.httpOrigin, input.doc_id, "httpOrigin"),
|
|
1681
|
+
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, input.doc_id) },
|
|
1682
|
+
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, input.doc_id) },
|
|
1683
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1684
|
+
});
|
|
1685
|
+
const base = await client.fetchState();
|
|
1686
|
+
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
1687
|
+
ok: true,
|
|
1688
|
+
op: "migrate-legacy",
|
|
1689
|
+
doc_id: input.doc_id,
|
|
1690
|
+
migration_status: "already_entity",
|
|
1691
|
+
entity_revision: base.revision,
|
|
1692
|
+
next_action: "snapshot"
|
|
1693
|
+
};
|
|
1694
|
+
const pull = await doc.pull();
|
|
1695
|
+
if (!pull.ok) throw new Error(`Migration requires a fresh canonical snapshot: ${pull.error.message}`);
|
|
1696
|
+
const migrationBaseVv = encodeDocVersionMark(doc.versionMark());
|
|
1697
|
+
const nextRows = migrateLegacyTimelineToEntities(doc.snapshot(), input.asset_facts, base.rows);
|
|
1698
|
+
let revision;
|
|
1699
|
+
try {
|
|
1700
|
+
revision = (await client.commit(base, nextRows, { migrationBaseVv })).revision;
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
if (error instanceof MengineHttpRequestError) throw new Error(`Migration rejected (HTTP ${error.status}): ${entityHttpErrorMessage(error.payload)}; take a fresh snapshot before retrying`);
|
|
1703
|
+
throw new Error("Migration submission is unconfirmed; take a fresh snapshot and retry migrate-legacy to inspect whether the Entity timeline already exists");
|
|
1704
|
+
}
|
|
1705
|
+
documents.delete(input.doc_id);
|
|
1706
|
+
for (const [id, cached] of plans) if (cached.docId === input.doc_id) plans.delete(id);
|
|
1707
|
+
return {
|
|
1708
|
+
ok: true,
|
|
1709
|
+
op: "migrate-legacy",
|
|
1710
|
+
doc_id: input.doc_id,
|
|
1711
|
+
migration_status: "committed",
|
|
1712
|
+
entity_revision: revision,
|
|
1713
|
+
next_action: "snapshot"
|
|
1714
|
+
};
|
|
1715
|
+
});
|
|
1321
1716
|
}
|
|
1322
1717
|
async function run(input) {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1718
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1719
|
+
assertNoPendingPush(input.doc_id);
|
|
1720
|
+
const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
|
|
1721
|
+
const document = doc.snapshot();
|
|
1722
|
+
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1723
|
+
const result = await runEditScript({
|
|
1724
|
+
document,
|
|
1725
|
+
baseVersion,
|
|
1726
|
+
entityState,
|
|
1727
|
+
entityOnly: true,
|
|
1728
|
+
script: input.script,
|
|
1729
|
+
...input.inputs !== void 0 ? { inputs: input.inputs } : {},
|
|
1730
|
+
timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
|
|
1731
|
+
memoryLimitMb: input.memory_limit_mb ?? options.sandbox?.memoryLimitMb
|
|
1732
|
+
});
|
|
1733
|
+
if (!result.ok) return {
|
|
1734
|
+
ok: false,
|
|
1735
|
+
op: "run-edit-script",
|
|
1736
|
+
doc_id: input.doc_id,
|
|
1737
|
+
phase: result.phase,
|
|
1738
|
+
error: result.error,
|
|
1739
|
+
partial: {
|
|
1740
|
+
ops_count: result.partial.ops.length + result.partial.entityCommands.length,
|
|
1741
|
+
logs: result.partial.logs
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
const plan = {
|
|
1745
|
+
...result.plan,
|
|
1746
|
+
doc_id: input.doc_id
|
|
1747
|
+
};
|
|
1748
|
+
const planId = rememberPlan(input.doc_id, plan);
|
|
1749
|
+
const base = {
|
|
1750
|
+
ok: true,
|
|
1751
|
+
op: "run-edit-script",
|
|
1752
|
+
doc_id: input.doc_id,
|
|
1753
|
+
plan_id: planId,
|
|
1754
|
+
plan_kind: plan.plan_kind,
|
|
1755
|
+
base_version: baseVersion,
|
|
1756
|
+
entity_base_revision: plan.entity_base_revision,
|
|
1757
|
+
ops_count: plan.ops.length + plan.entity_commands.length,
|
|
1758
|
+
preview: plan.preview,
|
|
1759
|
+
logs: plan.logs,
|
|
1760
|
+
duration_ms: result.durationMs,
|
|
1761
|
+
collaborated: pull.collaborated,
|
|
1762
|
+
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1763
|
+
};
|
|
1764
|
+
if (input.auto_commit !== true) return base;
|
|
1765
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan);
|
|
1766
|
+
recordPushResult(input.doc_id, planId, plan, commit);
|
|
1767
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1768
|
+
return {
|
|
1769
|
+
...base,
|
|
1770
|
+
committed: commit.kind === "committed",
|
|
1771
|
+
commit_result: commit,
|
|
1772
|
+
collaborated: pull.collaborated || commit.kind === "committed" && commit.collaborated,
|
|
1773
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1774
|
+
};
|
|
1333
1775
|
});
|
|
1334
|
-
if (!result.ok) return {
|
|
1335
|
-
ok: false,
|
|
1336
|
-
op: "run-edit-script",
|
|
1337
|
-
doc_id: input.doc_id,
|
|
1338
|
-
phase: result.phase,
|
|
1339
|
-
error: result.error,
|
|
1340
|
-
partial: {
|
|
1341
|
-
ops_count: result.partial.ops.length,
|
|
1342
|
-
logs: result.partial.logs
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
const planId = rememberPlan(input.doc_id, result.plan);
|
|
1346
|
-
const base = {
|
|
1347
|
-
ok: true,
|
|
1348
|
-
op: "run-edit-script",
|
|
1349
|
-
doc_id: input.doc_id,
|
|
1350
|
-
plan_id: planId,
|
|
1351
|
-
base_version: baseVersion,
|
|
1352
|
-
ops_count: result.plan.ops.length,
|
|
1353
|
-
preview: result.plan.preview,
|
|
1354
|
-
logs: result.plan.logs,
|
|
1355
|
-
duration_ms: result.durationMs
|
|
1356
|
-
};
|
|
1357
|
-
if (input.auto_commit !== true) return base;
|
|
1358
|
-
const commit = await confirmCommitted(session, await commitPlan(session, result.plan));
|
|
1359
|
-
return {
|
|
1360
|
-
...base,
|
|
1361
|
-
committed: commit.kind === "committed",
|
|
1362
|
-
commit_result: commit
|
|
1363
|
-
};
|
|
1364
1776
|
}
|
|
1365
1777
|
async function commit(input) {
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1778
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1779
|
+
const pending = pendingPushes.get(input.doc_id);
|
|
1780
|
+
if (pending != null) {
|
|
1781
|
+
if (pending.planId !== input.plan_id) throw new Error(`doc ${input.doc_id} has an unconfirmed push for plan_id ${pending.planId}; retry it before ${input.plan_id}`);
|
|
1782
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
|
|
1783
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
|
|
1784
|
+
const warnings = commitWarnings(result);
|
|
1785
|
+
return {
|
|
1786
|
+
ok: true,
|
|
1787
|
+
op: "commit-plan",
|
|
1788
|
+
doc_id: input.doc_id,
|
|
1789
|
+
plan_id: input.plan_id,
|
|
1790
|
+
plan_kind: pending.plan.plan_kind,
|
|
1791
|
+
committed: result.kind === "committed",
|
|
1792
|
+
result,
|
|
1793
|
+
collaborated: result.kind === "committed" && result.collaborated,
|
|
1794
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
const cached = plans.get(input.plan_id);
|
|
1798
|
+
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
1799
|
+
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
1800
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
|
|
1801
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
|
|
1802
|
+
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1803
|
+
return {
|
|
1804
|
+
ok: true,
|
|
1805
|
+
op: "commit-plan",
|
|
1806
|
+
doc_id: input.doc_id,
|
|
1807
|
+
plan_id: input.plan_id,
|
|
1808
|
+
plan_kind: cached.plan.plan_kind,
|
|
1809
|
+
committed: result.kind === "committed",
|
|
1810
|
+
result,
|
|
1811
|
+
collaborated: pull.collaborated || result.kind === "committed" && result.collaborated,
|
|
1812
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1813
|
+
};
|
|
1814
|
+
});
|
|
1379
1815
|
}
|
|
1380
1816
|
return {
|
|
1381
1817
|
name: MEDEO_TOOL_NAME,
|
|
@@ -1386,6 +1822,7 @@ function createMedeoTool(options) {
|
|
|
1386
1822
|
try {
|
|
1387
1823
|
const parsed = parseInput(input);
|
|
1388
1824
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
1825
|
+
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
1389
1826
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
1390
1827
|
return await commit(parsed);
|
|
1391
1828
|
} catch (error) {
|
|
@@ -1398,18 +1835,22 @@ function createMedeoTool(options) {
|
|
|
1398
1835
|
},
|
|
1399
1836
|
async close() {
|
|
1400
1837
|
closed = true;
|
|
1401
|
-
|
|
1402
|
-
|
|
1838
|
+
await Promise.allSettled(documentTails.values());
|
|
1839
|
+
const opening = [...documents.values()];
|
|
1840
|
+
documents.clear();
|
|
1841
|
+
entityClients.clear();
|
|
1842
|
+
documentTails.clear();
|
|
1843
|
+
pendingPushes.clear();
|
|
1403
1844
|
plans.clear();
|
|
1404
1845
|
modelContextVersions.clear();
|
|
1405
1846
|
const errors = [];
|
|
1406
|
-
for (const
|
|
1407
|
-
|
|
1847
|
+
for (const documentPromise of opening) try {
|
|
1848
|
+
await documentPromise;
|
|
1408
1849
|
} catch (error) {
|
|
1409
1850
|
errors.push(error);
|
|
1410
1851
|
}
|
|
1411
1852
|
if (errors.length === 1) throw errors[0];
|
|
1412
|
-
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool
|
|
1853
|
+
if (errors.length > 1) throw new AggregateError(errors, "failed to close medeo tool documents");
|
|
1413
1854
|
}
|
|
1414
1855
|
};
|
|
1415
1856
|
}
|