@mengine/medeo-tool 1.2.1-alpha.8 → 1.3.1-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -11
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-DHasvrhq.d.mts} +89 -33
- package/dist/index.d.mts +100 -181
- package/dist/index.mjs +457 -397
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +149 -44
- package/dist/{script-session-CHyIUBkO.mjs → script-session-DLBhoA1R.mjs} +682 -132
- package/dist/script-session-DLBhoA1R.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +39 -11
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/script-session-CHyIUBkO.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { a as isMediaAssetVariantKind, c as renderCompactProjection, i as createRelationId, o as collectAffectedPartIds, r as createEntityId, s as renderPreview, t as EditSandboxSession } from "./script-session-DLBhoA1R.mjs";
|
|
2
|
+
import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, ensureEditorFoundation, 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
|
|
@@ -122,6 +122,7 @@ function runEditScript(options) {
|
|
|
122
122
|
doc_id: options.document.meta.draft_id ?? "",
|
|
123
123
|
base_version: options.baseVersion,
|
|
124
124
|
ops: ops.slice(),
|
|
125
|
+
...message.loroUpdate ? { loro_update: message.loroUpdate } : {},
|
|
125
126
|
entity_base_revision: message.entityBaseRevision,
|
|
126
127
|
entity_commands: entityCommands.slice(),
|
|
127
128
|
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
@@ -205,11 +206,11 @@ const KNOWN_RELATION_KINDS = [
|
|
|
205
206
|
"marker-timeline",
|
|
206
207
|
"physical-asset",
|
|
207
208
|
"generated",
|
|
208
|
-
"phonetic-script-provenance",
|
|
209
|
-
"caption-provenance",
|
|
210
209
|
"caption-alignment",
|
|
211
210
|
"clip-anchor",
|
|
212
|
-
"
|
|
211
|
+
"phonetic-script-render",
|
|
212
|
+
"audio-script-source",
|
|
213
|
+
"audio-script-marker"
|
|
213
214
|
];
|
|
214
215
|
//#endregion
|
|
215
216
|
//#region src/entity/entity-http-client.ts
|
|
@@ -226,7 +227,7 @@ var MengineEntityHttpRequestError = class extends Error {
|
|
|
226
227
|
this.name = "MengineEntityHttpRequestError";
|
|
227
228
|
}
|
|
228
229
|
};
|
|
229
|
-
/** Narrow authenticated client for the entity
|
|
230
|
+
/** Narrow authenticated client for the Loro entity endpoint. */
|
|
230
231
|
var EntityHttpClient = class {
|
|
231
232
|
options;
|
|
232
233
|
fetchImpl;
|
|
@@ -237,18 +238,30 @@ var EntityHttpClient = class {
|
|
|
237
238
|
async fetchState() {
|
|
238
239
|
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
239
240
|
}
|
|
240
|
-
async commit(
|
|
241
|
+
async commit(_transportSequence, state, _deletions = {}) {
|
|
242
|
+
if (!state.loroSnapshot) throw new Error("Entity edit is missing its causal Loro baseline");
|
|
243
|
+
const rows = {
|
|
244
|
+
entities: state.entities.map((row) => ({
|
|
245
|
+
entityId: createEntityId(row.entity_id),
|
|
246
|
+
entityKind: row.entity_kind,
|
|
247
|
+
payload: row.payload
|
|
248
|
+
})),
|
|
249
|
+
relations: state.relations.map((row) => ({
|
|
250
|
+
relationId: createRelationId(row.relation_id),
|
|
251
|
+
relationKind: row.relation_kind,
|
|
252
|
+
endpoint0EntityId: createEntityId(row.endpoint_0_entity_id),
|
|
253
|
+
endpoint1EntityId: createEntityId(row.endpoint_1_entity_id),
|
|
254
|
+
metadata: row.metadata,
|
|
255
|
+
trace: row.trace
|
|
256
|
+
}))
|
|
257
|
+
};
|
|
258
|
+
const compiled = compileEntityRows(base64ToBytes(state.loroSnapshot), rows);
|
|
259
|
+
return this.commitUpdate(bytesToBase64(compiled.update));
|
|
260
|
+
}
|
|
261
|
+
async commitUpdate(update) {
|
|
241
262
|
return toSnapshot(await this.requestJson({
|
|
242
263
|
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
|
-
})
|
|
264
|
+
body: JSON.stringify({ update })
|
|
252
265
|
}), this.options.docId);
|
|
253
266
|
}
|
|
254
267
|
async requestJson(init) {
|
|
@@ -276,26 +289,32 @@ var EntityHttpClient = class {
|
|
|
276
289
|
}
|
|
277
290
|
};
|
|
278
291
|
function toSnapshot(value, expectedDocId) {
|
|
279
|
-
if (!isRecord$
|
|
292
|
+
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
293
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
|
-
if (!isRecord$
|
|
294
|
+
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
295
|
+
if (value.audio_script_entity_id === null) throw new Error("Document AudioScript is not initialized");
|
|
296
|
+
if (!isTrimmed(value.audio_script_entity_id)) throw new Error("invalid document AudioScript identity");
|
|
297
|
+
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
282
298
|
const response = value;
|
|
299
|
+
if (response.audio_script_entity_id !== null && !response.rows.entities.some((entity) => entity.entity_id === response.audio_script_entity_id && entity.entity_kind === "audio-script")) throw new Error("Document AudioScript must name the project AudioScript");
|
|
283
300
|
return {
|
|
301
|
+
loroSnapshot: response.loro_snapshot,
|
|
284
302
|
revision: response.revision,
|
|
303
|
+
audioScriptEntityId: response.audio_script_entity_id,
|
|
285
304
|
entities: response.rows.entities.map(parseEntity),
|
|
286
305
|
relations: response.rows.relations.map(parseRelation)
|
|
287
306
|
};
|
|
288
307
|
}
|
|
289
308
|
function parseEntity(value) {
|
|
290
|
-
if (!isRecord$
|
|
309
|
+
if (!isRecord$2(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
310
|
return structuredClone(value);
|
|
292
311
|
}
|
|
293
312
|
function parseRelation(value) {
|
|
294
|
-
if (!isRecord$
|
|
313
|
+
if (!isRecord$2(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
314
|
return structuredClone(value);
|
|
296
315
|
}
|
|
297
316
|
function isJsonObject(value) {
|
|
298
|
-
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$
|
|
317
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$2(value);
|
|
299
318
|
}
|
|
300
319
|
function isJsonValue(value, ancestors) {
|
|
301
320
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
@@ -308,7 +327,7 @@ function isJsonValue(value, ancestors) {
|
|
|
308
327
|
ancestors.delete(value);
|
|
309
328
|
return valid;
|
|
310
329
|
}
|
|
311
|
-
function isRecord$
|
|
330
|
+
function isRecord$2(value) {
|
|
312
331
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
313
332
|
}
|
|
314
333
|
function isTrimmed(value) {
|
|
@@ -327,74 +346,167 @@ async function safeReadJson(response) {
|
|
|
327
346
|
}
|
|
328
347
|
}
|
|
329
348
|
//#endregion
|
|
330
|
-
//#region src/
|
|
331
|
-
/**
|
|
332
|
-
|
|
333
|
-
|
|
349
|
+
//#region src/entity/generation-sync.ts
|
|
350
|
+
/**
|
|
351
|
+
* External systems whose asset entities carry a factual Memota identity.
|
|
352
|
+
* Voice results use the speech system; every other medium uses `memota`.
|
|
353
|
+
*/
|
|
354
|
+
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
355
|
+
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
356
|
+
function parseGenerationFacts(value) {
|
|
357
|
+
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
334
358
|
return value.map((item) => {
|
|
335
|
-
if (!
|
|
336
|
-
const {
|
|
337
|
-
if (
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
|
|
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");
|
|
359
|
+
if (!isRecord$1(item)) throw new Error("each generation fact must be an object");
|
|
360
|
+
const { outputAssetId, inputAssetIds } = item;
|
|
361
|
+
if (typeof outputAssetId !== "string" || outputAssetId.length === 0 || outputAssetId.trim() !== outputAssetId) throw new Error("generation fact outputAssetId must be a non-empty trimmed string");
|
|
362
|
+
if (!Array.isArray(inputAssetIds)) throw new Error("generation fact inputAssetIds must be an array (explicit [] means text-only)");
|
|
363
|
+
const inputs = inputAssetIds;
|
|
364
|
+
for (const input of inputs) if (typeof input !== "string" || input.length === 0 || input.trim() !== input) throw new Error("generation fact inputAssetIds entries must be non-empty trimmed strings");
|
|
380
365
|
return {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
durationMs,
|
|
384
|
-
storageKey,
|
|
385
|
-
voice: {
|
|
386
|
-
system: "voice-library",
|
|
387
|
-
key: voice.key,
|
|
388
|
-
...voice.name === void 0 ? {} : { name: voice.name }
|
|
389
|
-
}
|
|
366
|
+
outputAssetId,
|
|
367
|
+
inputAssetIds: [...inputs]
|
|
390
368
|
};
|
|
391
369
|
});
|
|
392
370
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
371
|
+
function planGenerationScope(base, commands, state) {
|
|
372
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
373
|
+
for (const command of commands) if (command.kind === "create-entity" && isMediaAssetVariantKind(command.entity.entity_kind)) touchedIds.add(command.entity.entity_id);
|
|
374
|
+
const beforeByKey = resolveMediaByAssetKey(base);
|
|
375
|
+
const scoped = /* @__PURE__ */ new Set();
|
|
376
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
377
|
+
for (const [key, mediaIds] of resolveMediaByAssetKey(state)) {
|
|
378
|
+
const previousIds = new Set(beforeByKey.get(key) ?? []);
|
|
379
|
+
for (const id of mediaIds) {
|
|
380
|
+
if (!touchedIds.has(id) || previousIds.has(id)) continue;
|
|
381
|
+
scoped.add(id);
|
|
382
|
+
queryKeys.add(key);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
scopedMediaIds: scoped,
|
|
387
|
+
queryAssetKeys: [...queryKeys].sort()
|
|
388
|
+
};
|
|
395
389
|
}
|
|
396
|
-
|
|
397
|
-
|
|
390
|
+
/**
|
|
391
|
+
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
392
|
+
* factual records. Both endpoints must already exist and match their own Asset
|
|
393
|
+
* identities, and the pair must involve a media Entity the plan
|
|
394
|
+
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
395
|
+
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
396
|
+
* the facts already resolved against the plan's base state is likewise skipped.
|
|
397
|
+
* One-sided facts, text-only records, self pairs, and already-linked pairs are
|
|
398
|
+
* skipped. Duplicate records collapse to one Relation.
|
|
399
|
+
*/
|
|
400
|
+
function planGeneratedRelations(input) {
|
|
401
|
+
const { state, facts } = input;
|
|
402
|
+
const scoped = input.scopedMediaIds;
|
|
403
|
+
const factKeys = new Set(facts.flatMap((fact) => [fact.outputAssetId, ...fact.inputAssetIds]));
|
|
404
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state, factKeys);
|
|
405
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState, factKeys), facts));
|
|
406
|
+
const linkedPairs = new Set(state.relations.filter((relation) => relation.relation_kind === "generated").map((relation) => pairKey(relation.endpoint_0_entity_id, relation.endpoint_1_entity_id)));
|
|
407
|
+
const relations = [];
|
|
408
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) {
|
|
409
|
+
if (outputId === inputId) continue;
|
|
410
|
+
if (!scoped.has(outputId) && !scoped.has(inputId)) continue;
|
|
411
|
+
const pair = pairKey(outputId, inputId);
|
|
412
|
+
if (linkedPairs.has(pair) || baseResolvable.has(pair)) continue;
|
|
413
|
+
linkedPairs.add(pair);
|
|
414
|
+
relations.push({
|
|
415
|
+
relation_id: input.newRelationId(),
|
|
416
|
+
relation_kind: "generated",
|
|
417
|
+
endpoint_0_entity_id: outputId,
|
|
418
|
+
endpoint_1_entity_id: inputId,
|
|
419
|
+
metadata: {},
|
|
420
|
+
trace: { synced_by: "generation-sync" }
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return relations;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
427
|
+
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
428
|
+
* commit result is never masked; a successful query that finds nothing is
|
|
429
|
+
* `current`. Asset identities are immutable, so facts are queried once. The native update retains its causal baseline and merges without whole-state retries; deleted endpoints are never recreated.
|
|
430
|
+
*/
|
|
431
|
+
async function syncGeneratedRelations(input) {
|
|
432
|
+
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
433
|
+
try {
|
|
434
|
+
const state = await client.fetchState();
|
|
435
|
+
const scope = planGenerationScope(baseState, entityCommands, state);
|
|
436
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
437
|
+
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
438
|
+
const relations = planGeneratedRelations({
|
|
439
|
+
baseState,
|
|
440
|
+
state,
|
|
441
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
442
|
+
facts,
|
|
443
|
+
newRelationId: mintRelationId
|
|
444
|
+
});
|
|
445
|
+
if (relations.length === 0) return { status: "current" };
|
|
446
|
+
const committed = await client.commit(state.revision, {
|
|
447
|
+
...state,
|
|
448
|
+
relations: [...state.relations, ...relations]
|
|
449
|
+
});
|
|
450
|
+
const active = new Set(committed.relations.map((relation) => relation.relation_id));
|
|
451
|
+
const created = relations.map((relation) => relation.relation_id).filter((id) => active.has(id));
|
|
452
|
+
return created.length ? {
|
|
453
|
+
status: "applied",
|
|
454
|
+
created_relation_ids: created
|
|
455
|
+
} : { status: "current" };
|
|
456
|
+
} catch (error) {
|
|
457
|
+
return {
|
|
458
|
+
status: "failed",
|
|
459
|
+
message: `generation lineage query failed: ${errorMessage(error)}`
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function assetKeyOf(entity) {
|
|
464
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
|
|
465
|
+
const external = entity.payload?.external;
|
|
466
|
+
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
467
|
+
const { system, key } = external;
|
|
468
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
|
|
469
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
470
|
+
return key;
|
|
471
|
+
}
|
|
472
|
+
/** Media variants own their Asset locator; generation lookup never follows Relations. */
|
|
473
|
+
function resolveMediaByAssetKey(state, factKeys) {
|
|
474
|
+
const systemByKey = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const entity of state.entities) {
|
|
476
|
+
const key = assetKeyOf(entity);
|
|
477
|
+
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
478
|
+
const system = entity.payload.external.system;
|
|
479
|
+
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
480
|
+
systemByKey.set(key, system);
|
|
481
|
+
}
|
|
482
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
483
|
+
for (const entity of state.entities) {
|
|
484
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
485
|
+
const key = assetKeyOf(entity);
|
|
486
|
+
if (key === void 0) continue;
|
|
487
|
+
const matches = resolved.get(key) ?? [];
|
|
488
|
+
matches.push(entity.entity_id);
|
|
489
|
+
resolved.set(key, matches);
|
|
490
|
+
}
|
|
491
|
+
return resolved;
|
|
492
|
+
}
|
|
493
|
+
function pairKey(endpoint0, endpoint1) {
|
|
494
|
+
return `${endpoint0}\u0000${endpoint1}`;
|
|
495
|
+
}
|
|
496
|
+
/** Pair keys the facts already resolve to under the given base bindings. */
|
|
497
|
+
function resolvablePairs(mediaByAssetKey, facts) {
|
|
498
|
+
const pairs = [];
|
|
499
|
+
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) if (outputId !== inputId) pairs.push(pairKey(outputId, inputId));
|
|
500
|
+
return pairs;
|
|
501
|
+
}
|
|
502
|
+
function mintRelationId() {
|
|
503
|
+
return `relation_${randomUUID()}`;
|
|
504
|
+
}
|
|
505
|
+
function errorMessage(error) {
|
|
506
|
+
return error instanceof Error ? error.message : String(error);
|
|
507
|
+
}
|
|
508
|
+
function isRecord$1(value) {
|
|
509
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
398
510
|
}
|
|
399
511
|
//#endregion
|
|
400
512
|
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
@@ -430,6 +542,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
430
542
|
" readonly system: 'font-library';",
|
|
431
543
|
" readonly key: string;",
|
|
432
544
|
"}",
|
|
545
|
+
"/**",
|
|
546
|
+
" * One ordered entry of the Caption's segment selection. `segmentId` quotes the",
|
|
547
|
+
" * composed AudioScript's own stable segment identity — a local id quoted by the",
|
|
548
|
+
" * variant, never a peer Entity reference. Text itself is never copied here;",
|
|
549
|
+
" * complete Caption content is assembled through its direct baseEntityIds.",
|
|
550
|
+
" * The optional `textRange` narrows one Segment to an intra-Segment sub-span",
|
|
551
|
+
" * (intra-segment re-segmentation); without it the whole Segment text is selected.",
|
|
552
|
+
" */",
|
|
553
|
+
"export type CaptionSegmentSelection = JsonObject & {",
|
|
554
|
+
" readonly segmentId: string;",
|
|
555
|
+
" readonly textRange?: CaptionTextRange;",
|
|
556
|
+
"};",
|
|
433
557
|
"export interface CaptionStyleFields {",
|
|
434
558
|
" readonly font?: CaptionFontDescriptor;",
|
|
435
559
|
" readonly fontSize?: number;",
|
|
@@ -442,6 +566,24 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
442
566
|
" readonly positionX?: number;",
|
|
443
567
|
" readonly positionY?: number;",
|
|
444
568
|
"}",
|
|
569
|
+
"/**",
|
|
570
|
+
" * Half-open `[start, end)` position window inside one Segment's text, counted",
|
|
571
|
+
" * in Unicode code points (not UTF-16 code units), so a boundary never splits a",
|
|
572
|
+
" * surrogate pair. Positions are non-negative safe integers with `start < end`;",
|
|
573
|
+
" * `end` must not exceed the Segment's code-point length.",
|
|
574
|
+
" */",
|
|
575
|
+
"export interface CaptionTextRange extends JsonObject {",
|
|
576
|
+
" readonly start: number;",
|
|
577
|
+
" readonly end: number;",
|
|
578
|
+
"}",
|
|
579
|
+
"export type CaptionTextSelection = JsonObject & {",
|
|
580
|
+
" segmentId: string;",
|
|
581
|
+
" /** Half-open Unicode code-point range within the selected source segment. */",
|
|
582
|
+
" textRange?: {",
|
|
583
|
+
" start: number;",
|
|
584
|
+
" end: number;",
|
|
585
|
+
" };",
|
|
586
|
+
"};",
|
|
445
587
|
"export type ClipEntityId = EntityId;",
|
|
446
588
|
"export type ClipPlacement =",
|
|
447
589
|
" | {",
|
|
@@ -457,11 +599,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
457
599
|
" readonly hostClipEntityId: string;",
|
|
458
600
|
" readonly anchorOffset: number;",
|
|
459
601
|
" };",
|
|
602
|
+
"export interface ComposedPhoneticContent extends ComposedScriptContent {",
|
|
603
|
+
" phonemeScript?: string;",
|
|
604
|
+
" prosody?: JsonObject;",
|
|
605
|
+
"}",
|
|
606
|
+
"/** Read result only: base text is assembled from the real AudioScript row. */",
|
|
607
|
+
"export interface ComposedScriptContent {",
|
|
608
|
+
" audio_script_entity_id: string;",
|
|
609
|
+
" text: string;",
|
|
610
|
+
" segments: ScriptTextSegment[];",
|
|
611
|
+
"}",
|
|
460
612
|
"export type CreateEntityInput = {",
|
|
461
613
|
" [K in KnownEntityKind]: {",
|
|
462
614
|
" entity_id?: string;",
|
|
463
615
|
" entity_kind: K;",
|
|
464
|
-
" payload:
|
|
616
|
+
" payload: StoredEntityPayload<K>;",
|
|
465
617
|
" };",
|
|
466
618
|
"}[KnownEntityKind];",
|
|
467
619
|
"export interface DeleteBgmInput {",
|
|
@@ -486,19 +638,29 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
486
638
|
" | 'clip-marker'",
|
|
487
639
|
" | 'marker-content'",
|
|
488
640
|
" | 'axvideo-marker'",
|
|
489
|
-
" | 'marker-timeline'
|
|
641
|
+
" | 'marker-timeline'",
|
|
642
|
+
" | 'audio-script-marker';",
|
|
490
643
|
"export interface EntityFacade {",
|
|
644
|
+
" /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */",
|
|
491
645
|
" list(): SandboxEntity[];",
|
|
492
646
|
" get(entityId: string): SandboxEntity | null;",
|
|
493
|
-
" /**
|
|
494
|
-
" findByAssetId(assetId: string): SandboxEntity<
|
|
647
|
+
" /** Find document resources by external Memota asset id, including directly composed media variants. */",
|
|
648
|
+
" findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];",
|
|
649
|
+
" /** Assemble selected Caption text; missing composition is an error. */",
|
|
650
|
+
" readCaptionContent(entityId: string): ComposedScriptContent;",
|
|
651
|
+
" /** Assemble base text and pronunciation fields before generating Voice. */",
|
|
652
|
+
" readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
|
|
495
653
|
" create(input: CreateEntityInput): string;",
|
|
496
|
-
" /**
|
|
654
|
+
" /** Patch assembled fields, routing inherited fields to their declaring entity. */",
|
|
497
655
|
" update(input: UpdateEntityInput): void;",
|
|
656
|
+
" /** Explicitly declare own fields, overriding unambiguous bases without modifying them. Ordinary edits use update. */",
|
|
657
|
+
" declareFields(input: UpdateEntityInput): void;",
|
|
498
658
|
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
499
659
|
" delete(input: DeleteEntityInput): void;",
|
|
500
|
-
" /**
|
|
501
|
-
"
|
|
660
|
+
" /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */",
|
|
661
|
+
" ensureMedia(fact: MediaAssetFact): {",
|
|
662
|
+
" contentEntityId: string;",
|
|
663
|
+
" };",
|
|
502
664
|
"}",
|
|
503
665
|
"export type EntityId = string;",
|
|
504
666
|
"export interface EntityPayloadByKind {",
|
|
@@ -509,12 +671,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
509
671
|
" role?: string;",
|
|
510
672
|
" };",
|
|
511
673
|
" clip: JsonObject;",
|
|
512
|
-
" /**
|
|
674
|
+
" /** Physical resource fields; never a copy of Caption content. */",
|
|
513
675
|
" asset: JsonObject;",
|
|
514
|
-
" video: BoundedNativeSequencePayload;",
|
|
515
|
-
" audio: BoundedNativeSequencePayload;",
|
|
516
|
-
" voice: BoundedNativeSequencePayload;",
|
|
517
|
-
" image: UnboundedConstantSequencePayload;",
|
|
676
|
+
" video: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
677
|
+
" audio: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
678
|
+
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
679
|
+
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
518
680
|
" 'sequence-marker': JsonObject & {",
|
|
519
681
|
" sourceRange: {",
|
|
520
682
|
" start: number;",
|
|
@@ -535,18 +697,34 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
535
697
|
" timeRemapping?: JsonValue;",
|
|
536
698
|
" anchorOffset?: number;",
|
|
537
699
|
" durationPolicy?: 'timeline';",
|
|
700
|
+
" /** Directly assigned AudioScript annotation times; annotation Markers only. */",
|
|
701
|
+
" segmentRanges?: {",
|
|
702
|
+
" segmentId: string;",
|
|
703
|
+
" startMs: number;",
|
|
704
|
+
" endMs: number;",
|
|
705
|
+
" }[];",
|
|
538
706
|
" };",
|
|
539
707
|
" viewport: JsonObject;",
|
|
540
708
|
" 'audio-script': JsonObject & {",
|
|
541
709
|
" segments: ScriptTextSegment[];",
|
|
542
710
|
" };",
|
|
543
711
|
" 'phonetic-script': JsonObject & {",
|
|
544
|
-
"
|
|
712
|
+
" baseEntityIds: string[];",
|
|
713
|
+
" phonemeScript?: string;",
|
|
714
|
+
" prosody?: JsonObject;",
|
|
715
|
+
" };",
|
|
716
|
+
" caption: BoundedNativeSequencePayload & {",
|
|
717
|
+
" baseEntityIds: string[];",
|
|
718
|
+
" selections: CaptionTextSelection[];",
|
|
719
|
+
" style?: JsonObject;",
|
|
545
720
|
" };",
|
|
546
|
-
" caption: BoundedNativeSequencePayload;",
|
|
547
721
|
"}",
|
|
548
722
|
"export interface EntityStoreSnapshot {",
|
|
723
|
+
" /** Causal compiler baseline; required for publishing edits. */",
|
|
724
|
+
" loroSnapshot?: string;",
|
|
549
725
|
" revision: number;",
|
|
726
|
+
" /** Current AudioScript version attached to the project; initialized projects always attach a script, possibly empty. */",
|
|
727
|
+
" audioScriptEntityId: string | null;",
|
|
550
728
|
" entities: SandboxEntity[];",
|
|
551
729
|
" relations: SandboxRelation[];",
|
|
552
730
|
"}",
|
|
@@ -555,10 +733,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
555
733
|
" readonly kind: 'image';",
|
|
556
734
|
" readonly storageKey?: string;",
|
|
557
735
|
"}",
|
|
558
|
-
"export interface
|
|
559
|
-
"
|
|
560
|
-
"
|
|
561
|
-
"
|
|
736
|
+
"export interface InsertCaptionClipInput {",
|
|
737
|
+
" readonly timelineEntityId: string;",
|
|
738
|
+
" /** Stable placed caption identity, distinct from the Caption content identity. */",
|
|
739
|
+
" readonly captionClipEntityId?: string;",
|
|
740
|
+
" /** Existing bases composed by this variant; includes an AudioScript text owner. */",
|
|
741
|
+
" readonly baseEntityIds: readonly string[];",
|
|
742
|
+
" /** Ordered selection of the AudioScript segments this Caption displays. */",
|
|
743
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
744
|
+
" /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */",
|
|
745
|
+
" readonly durationMs: number;",
|
|
746
|
+
" readonly style?: CaptionStyleFields;",
|
|
747
|
+
" readonly placement: ClipPlacement;",
|
|
562
748
|
"}",
|
|
563
749
|
"export interface InsertClipInput {",
|
|
564
750
|
" readonly trackEntityId: string;",
|
|
@@ -623,20 +809,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
623
809
|
" | 'marker-timeline'",
|
|
624
810
|
" | 'physical-asset'",
|
|
625
811
|
" | 'generated'",
|
|
626
|
-
" | 'phonetic-script-provenance'",
|
|
627
|
-
" | 'caption-provenance'",
|
|
628
812
|
" | 'caption-alignment'",
|
|
629
813
|
" | 'clip-anchor'",
|
|
630
|
-
" | '
|
|
814
|
+
" | 'phonetic-script-render'",
|
|
815
|
+
" | 'audio-script-source'",
|
|
816
|
+
" | 'audio-script-marker';",
|
|
631
817
|
"export interface LinearClipSpeed {",
|
|
632
818
|
" readonly kind: 'linear';",
|
|
633
819
|
" readonly rate: number;",
|
|
634
820
|
" readonly mode?: string;",
|
|
635
821
|
"}",
|
|
636
|
-
"
|
|
822
|
+
"/** `audio-script-source(script, source)`; the script was transcribed from the source media. */",
|
|
823
|
+
"export interface LinkAudioScriptSourceRelationInput {",
|
|
637
824
|
" relation_id?: string;",
|
|
638
|
-
" output_entity_id: string;",
|
|
639
825
|
" script_entity_id: string;",
|
|
826
|
+
" source_entity_id: string;",
|
|
640
827
|
" trace?: JsonObject;",
|
|
641
828
|
"}",
|
|
642
829
|
"export interface LinkClipAnchorRelationInput {",
|
|
@@ -651,6 +838,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
651
838
|
" input_entity_id: string;",
|
|
652
839
|
" trace?: JsonObject;",
|
|
653
840
|
"}",
|
|
841
|
+
"export interface LinkPhoneticScriptRenderRelationInput {",
|
|
842
|
+
" relation_id?: string;",
|
|
843
|
+
" output_entity_id: string;",
|
|
844
|
+
" phonetic_script_entity_id: string;",
|
|
845
|
+
" trace?: JsonObject;",
|
|
846
|
+
"}",
|
|
654
847
|
"interface LinkRelationBase {",
|
|
655
848
|
" relation_id?: string;",
|
|
656
849
|
" endpoint_0_entity_id: string;",
|
|
@@ -669,17 +862,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
669
862
|
" metadata?: JsonObject;",
|
|
670
863
|
" })",
|
|
671
864
|
" | (LinkRelationBase & {",
|
|
672
|
-
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
673
|
-
" metadata: JsonObject & {",
|
|
674
|
-
" segmentAlignment: JsonValue;",
|
|
675
|
-
" };",
|
|
676
|
-
" })",
|
|
677
|
-
" | (LinkRelationBase & {",
|
|
678
865
|
" relation_kind: 'caption-alignment';",
|
|
679
866
|
" metadata: JsonObject & {",
|
|
680
867
|
" alignment: JsonValue;",
|
|
681
868
|
" };",
|
|
682
869
|
" });",
|
|
870
|
+
"/** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */",
|
|
871
|
+
"export type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;",
|
|
872
|
+
"export type MediaAssetPayload = JsonObject & {",
|
|
873
|
+
" external: {",
|
|
874
|
+
" system: 'memota' | 'memota-speech';",
|
|
875
|
+
" key: string;",
|
|
876
|
+
" };",
|
|
877
|
+
" storageKey?: string;",
|
|
878
|
+
"};",
|
|
683
879
|
"export type MediaClipInsertion =",
|
|
684
880
|
" | {",
|
|
685
881
|
" readonly kind: 'before';",
|
|
@@ -723,14 +919,16 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
723
919
|
" list(): SandboxRelation[];",
|
|
724
920
|
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
725
921
|
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
726
|
-
" /**
|
|
922
|
+
" /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */",
|
|
727
923
|
" link(input: LinkRelationInput): string;",
|
|
728
924
|
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
729
925
|
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
730
926
|
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
731
927
|
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
732
|
-
" /** Author ordered
|
|
733
|
-
"
|
|
928
|
+
" /** Author ordered phonetic-script-render(output,script) without positional endpoint ambiguity. */",
|
|
929
|
+
" linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
|
|
930
|
+
" /** Author ordered audio-script-source(script,source) without positional endpoint ambiguity. */",
|
|
931
|
+
" linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
|
|
734
932
|
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
735
933
|
" unlink(input: UnlinkRelationInput): void;",
|
|
736
934
|
"}",
|
|
@@ -760,10 +958,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
760
958
|
" readonly sourceRange: SequenceRange<number>;",
|
|
761
959
|
" readonly volume?: number;",
|
|
762
960
|
"}",
|
|
961
|
+
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
962
|
+
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
763
963
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
764
964
|
" entity_id: string;",
|
|
765
965
|
" entity_kind: K;",
|
|
766
|
-
" payload:
|
|
966
|
+
" payload: StoredEntityPayload<K>;",
|
|
767
967
|
"}",
|
|
768
968
|
"export interface SandboxRelation {",
|
|
769
969
|
" relation_id: string;",
|
|
@@ -821,6 +1021,13 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
821
1021
|
" /** Playback gain in decibels. */",
|
|
822
1022
|
" readonly volume: number;",
|
|
823
1023
|
"}",
|
|
1024
|
+
"/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
|
|
1025
|
+
"export type StoredEntityPayload<K extends KnownEntityKind> =",
|
|
1026
|
+
" | EntityPayloadByKind[K]",
|
|
1027
|
+
" | (JsonObject &",
|
|
1028
|
+
" Partial<EntityPayloadByKind[K]> & {",
|
|
1029
|
+
" baseEntityIds: string[];",
|
|
1030
|
+
" });",
|
|
824
1031
|
"export interface TrimClipInput {",
|
|
825
1032
|
" readonly clipEntityId: string;",
|
|
826
1033
|
" readonly sourceRange: SequenceRange<number>;",
|
|
@@ -850,6 +1057,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
850
1057
|
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
851
1058
|
" readonly timeRemapping?: JsonValue | undefined;",
|
|
852
1059
|
"}",
|
|
1060
|
+
"/** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */",
|
|
853
1061
|
"export interface UpdateEntityInput {",
|
|
854
1062
|
" entity_id: string;",
|
|
855
1063
|
" payload: JsonObject;",
|
|
@@ -872,31 +1080,47 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
872
1080
|
" readonly kind: 'voice';",
|
|
873
1081
|
" readonly durationMs: number;",
|
|
874
1082
|
" readonly storageKey: string;",
|
|
875
|
-
"
|
|
1083
|
+
" /** Present for synthesized voice, absent for original recorded audio. */",
|
|
1084
|
+
" readonly voice?: VoiceDescriptor;",
|
|
876
1085
|
"}",
|
|
877
1086
|
"export interface VoiceoverCaptionFact {",
|
|
878
1087
|
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
879
1088
|
" readonly captionClipEntityId: string;",
|
|
880
|
-
"
|
|
1089
|
+
" /** Directly held bases; includes the AudioScript used by the Voice. */",
|
|
1090
|
+
" readonly baseEntityIds: readonly string[];",
|
|
1091
|
+
" /** Ordered selection of AudioScript segments; caption text is never passed inline. */",
|
|
1092
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
881
1093
|
" readonly startMs: number;",
|
|
882
1094
|
" readonly durationMs: number;",
|
|
883
1095
|
" readonly style?: CaptionStyleFields;",
|
|
884
1096
|
"}",
|
|
885
|
-
"export
|
|
1097
|
+
"export type VoiceoverTakeInput = {",
|
|
886
1098
|
" readonly timelineEntityId: string;",
|
|
887
1099
|
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
888
1100
|
" readonly voiceoverClipEntityId: string;",
|
|
889
|
-
" readonly hostClipEntityId: string;",
|
|
890
|
-
" readonly anchorOffset: number;",
|
|
891
1101
|
" readonly media: VoiceMediaAssetFact;",
|
|
892
|
-
" /**
|
|
893
|
-
" readonly
|
|
1102
|
+
" /** Existing pronunciation variant; its composed AudioScript stays the text owner. */",
|
|
1103
|
+
" readonly phoneticScriptEntityId: string;",
|
|
894
1104
|
" readonly volume: number;",
|
|
895
1105
|
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
896
|
-
"}",
|
|
1106
|
+
"} & (",
|
|
1107
|
+
" | {",
|
|
1108
|
+
" readonly placement: ClipPlacement;",
|
|
1109
|
+
" readonly hostClipEntityId?: never;",
|
|
1110
|
+
" readonly anchorOffset?: never;",
|
|
1111
|
+
" }",
|
|
1112
|
+
" | {",
|
|
1113
|
+
" readonly placement?: never;",
|
|
1114
|
+
" readonly hostClipEntityId: string;",
|
|
1115
|
+
" readonly anchorOffset: number;",
|
|
1116
|
+
" }",
|
|
1117
|
+
");",
|
|
897
1118
|
"export interface VoiceoverTakeResult {",
|
|
898
1119
|
" readonly voiceoverClipEntityId: string;",
|
|
899
1120
|
" readonly voiceEntityId: string;",
|
|
1121
|
+
" /** The pronunciation variant the Voice was rendered from. */",
|
|
1122
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1123
|
+
" /** The base-text owner resolved from the PhoneticScript baseEntityIds. */",
|
|
900
1124
|
" readonly audioScriptEntityId: string;",
|
|
901
1125
|
" readonly captionClipEntityIds: readonly string[];",
|
|
902
1126
|
"}",
|
|
@@ -927,9 +1151,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
927
1151
|
" deleteBgm(input: DeleteBgmInput): void;",
|
|
928
1152
|
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
929
1153
|
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1154
|
+
" insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
|
|
930
1155
|
"}",
|
|
931
1156
|
"export interface TimelineApi {",
|
|
932
|
-
" snapshot(): EntityStoreSnapshot
|
|
1157
|
+
" snapshot(): EntityStoreSnapshot & {",
|
|
1158
|
+
" audioScriptEntityId: string;",
|
|
1159
|
+
" };",
|
|
933
1160
|
"}",
|
|
934
1161
|
"export interface SandboxCheckpoint {",
|
|
935
1162
|
" readonly index: number;",
|
|
@@ -949,25 +1176,27 @@ const MEDEO_TOOL_DESCRIPTION = `
|
|
|
949
1176
|
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.
|
|
950
1177
|
|
|
951
1178
|
Operations:
|
|
952
|
-
- snapshot:
|
|
953
|
-
-
|
|
954
|
-
-
|
|
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.
|
|
1179
|
+
- snapshot: read the current Entity/Relation view, project attachments and causal Loro baseline. Reading does not initialize or mutate domain data.
|
|
1180
|
+
- 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 and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled asset facts through inputs; generation history is not a script input — the host program queries it itself after each commit. A successful run returns preview, logs, base revision and plan_id.
|
|
1181
|
+
- commit-plan: publish the native Loro update compiled against the plan’s causal baseline. Concurrent independent edits merge through Loro. The timeline and AudioScript panel read the merged entity state. A failed transport is unconfirmed; retry the same plan_id so operation identities are preserved.
|
|
956
1182
|
|
|
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.
|
|
1183
|
+
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. Concurrent edits do not require replaying the script against a newer snapshot. If a domain conflict is reported, inspect the merged state and resolve it explicitly; never replace the complete document to force the edit through.
|
|
958
1184
|
|
|
959
|
-
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts,
|
|
1185
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, call entities.ensureMedia(fact), then pass its contentEntityId to edit.insertClip. A raw external asset id or URL is not valid contentEntityId. Image/Video/Audio/Voice are logical variants of Asset: a resource has ONE identity and ONE typed row owning both media fields and external {system,key}/storageKey, with no separate Asset row or physical-asset relation. ensureMedia returns contentEntityId and reuses that single identity by external asset id. Each placement still gets its own Clip and SequenceMarker. Generation lineage is program-synced: after each successful commit the tool connects existing typed Assets from host-recalled generation facts (endpoint 0 output, endpoint 1 input) that the host queries itself — do not pass generation history through inputs. Do not author generated Relations yourself, and never create an Entity merely to backfill or represent lineage; media variants the edit itself legitimately needs are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
960
1186
|
`.trim();
|
|
961
1187
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
962
1188
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
963
1189
|
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
|
-
|
|
1190
|
+
Generation lineage is not a model input: the host program queries it via loadGenerationFacts and syncs generated Relations after each successful commit. Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1191
|
+
Use entities.ensureMedia(fact) to get or create the canonical typed Asset. Native media placement helpers use the same resolver. findByAssetId includes directly composed media variants, including Voice. Every Image/Video/Audio/Voice must own its external identity; separate Asset+media graphs are invalid. A typed Asset's external identity cannot be removed or rewritten: to replace its source, ensureMedia for the new Asset and replace the Clip's content. Conflicting facts fail closed. Asset generation itself still creates no editor Entities.
|
|
966
1192
|
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
|
-
|
|
968
|
-
The
|
|
969
|
-
|
|
970
|
-
|
|
1193
|
+
Caption content is assembled from AudioScript; never create an inline text Asset for it. Generated media lineage is host-owned; do not author generated Relations yourself. relations.of remains endpoint-agnostic for lookup.
|
|
1194
|
+
The editor projection 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.
|
|
1195
|
+
Entities own fields; ordinary Relations express associations; variants directly hold baseEntityIds and assemble the referenced entities. These foundations are fixed: implementation must follow them, never redefine them. Any entity may compose multiple bases. Equal field names from multiple bases (even equal values) are errors, even when the variant declares that field itself. After validating all base fields are unambiguous, explicitly declared own fields may override base fields without mutating the bases. Base ordering never resolves conflicts. AudioScript owns segmented text. Caption and PhoneticScript persist baseEntityIds including their AudioScript, plus their own fields; no composition Relation exists. Create the real bases before reading or committing a variant. Inside the DSL sandbox, entities.get/list expose complete assembled fields. Consumers read fields without inspecting base IDs or merging bases. entities.update patches supplied fields and routes inherited fields to their declaring entity; omitted fields remain unchanged. entities.declareFields explicitly declares own overrides and is distinct from an ordinary field edit. Persistence keeps owned fields only. entities.readCaptionContent(id) and entities.readPhoneticScriptContent(id) return assembled text. Missing/cyclic bases and field conflicts fail before persistence.
|
|
1196
|
+
Use edit.insertCaptionClip with baseEntityIds and selections; each voiceover caption also supplies baseEntityIds. A selection names segmentId and may use a half-open Unicode code-point textRange to split a segment for the screen without rewriting AudioScript. Generate Voice from an existing PhoneticScript, then use phoneticScriptEntityId in the voiceover helper or relations.linkPhoneticScriptRender({output_entity_id,phonetic_script_entity_id}) for its render relation. Caption and Voice have their own Clips; display anchoring is explicit and independent of composition/alignment.
|
|
1197
|
+
Move or stretch only the Clip's display Marker; preserve Caption intrinsic Sequence, AudioScript text and its annotation Markers. AudioScript cannot enter a Clip and has no intrinsic time. audio-script-source links its ASR source Audio/Video/Voice; audio-script-marker attaches annotation Markers with directly assigned segmentRanges:{segmentId,startMs,endMs} in whole milliseconds. Annotation Markers have no Clip/AXVideo/content/Timeline relations and never refer to other Markers for time. BGM keeps factual source duration with durationPolicy:'timeline'. Never introduce a speech entity kind.
|
|
1198
|
+
Create only the known entity kinds. Project creation initializes one current Timeline, four Tracks and an attached AudioScript with segments:[]. Read the current AudioScript ID from timeline.snapshot(); the panel displays this attachment and preserves its segments. Normal edits operate this script, not an unrelated newly created script.
|
|
1199
|
+
Immutable updates apply to every entity: editing an owned field creates a new content version ID; changing only a variant's base ID preserves the variant ID. Editing a base through a variant updates the owner, and the compiler advances the affected base links and project attachment. A variant-owned edit creates a new variant version and retains its unchanged bases. Do not manually clone entities or duplicate inherited fields to implement versioning. Versions preserve native text and list editing identities so independent concurrent edits survive. Missing facts, unsupported layouts and composition conflicts are explicit errors; there is no legacy migration or whole-state overwrite path.
|
|
971
1200
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
972
1201
|
`.trim();
|
|
973
1202
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -993,42 +1222,6 @@ ${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
|
993
1222
|
//#endregion
|
|
994
1223
|
//#region src/schema.ts
|
|
995
1224
|
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
|
-
};
|
|
1032
1225
|
/**
|
|
1033
1226
|
* JSON Schema for the host-facing `medeo` tool surface.
|
|
1034
1227
|
*
|
|
@@ -1046,7 +1239,6 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1046
1239
|
type: "string",
|
|
1047
1240
|
enum: [
|
|
1048
1241
|
"snapshot",
|
|
1049
|
-
"migrate-legacy",
|
|
1050
1242
|
"run-edit-script",
|
|
1051
1243
|
"commit-plan"
|
|
1052
1244
|
],
|
|
@@ -1064,69 +1256,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1064
1256
|
},
|
|
1065
1257
|
inputs: {
|
|
1066
1258
|
type: "object",
|
|
1067
|
-
description: "Pre-materialized, side-effect-free values passed into the script, including recalled
|
|
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
|
-
] }
|
|
1259
|
+
description: "Pre-materialized, side-effect-free values passed into the script, including recalled asset facts. Generation history is never an input: the host queries lineage itself and syncs generated Relations after each commit. Generation and network IO must happen in the host before this call."
|
|
1130
1260
|
},
|
|
1131
1261
|
timeout_ms: {
|
|
1132
1262
|
type: "integer",
|
|
@@ -1149,24 +1279,11 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1149
1279
|
},
|
|
1150
1280
|
validation: {
|
|
1151
1281
|
type: "string",
|
|
1152
|
-
enum: ["
|
|
1153
|
-
description: "
|
|
1282
|
+
enum: ["preflight"],
|
|
1283
|
+
description: "Publish the native update already validated on its causal Loro fork."
|
|
1154
1284
|
}
|
|
1155
1285
|
},
|
|
1156
1286
|
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
|
-
},
|
|
1170
1287
|
{
|
|
1171
1288
|
required: ["op", "doc_id"],
|
|
1172
1289
|
properties: {
|
|
@@ -1322,66 +1439,30 @@ function renderEntitySnapshot(state) {
|
|
|
1322
1439
|
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1323
1440
|
const shown = rows.slice(0, 200);
|
|
1324
1441
|
return [
|
|
1325
|
-
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1442
|
+
`Entity revision=${state.revision} audioScriptEntityId=${JSON.stringify(state.audioScriptEntityId)} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1326
1443
|
...shown,
|
|
1327
1444
|
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1328
1445
|
].join("\n");
|
|
1329
1446
|
}
|
|
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
1447
|
async function commitEntityPlan(client, plan) {
|
|
1339
|
-
|
|
1340
|
-
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1448
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1341
1449
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1345
|
-
});
|
|
1450
|
+
if (!plan.loro_update) throw new Error("Entity plan has no compiled Loro update");
|
|
1451
|
+
const committed = await client.commitUpdate(plan.loro_update);
|
|
1346
1452
|
return {
|
|
1347
1453
|
kind: "committed",
|
|
1348
1454
|
ops_applied: plan.entity_commands.length,
|
|
1349
|
-
collaborated:
|
|
1455
|
+
collaborated: committed.revision > plan.entity_base_revision + 1,
|
|
1350
1456
|
entity_revision: committed.revision
|
|
1351
1457
|
};
|
|
1352
1458
|
} catch (error) {
|
|
1353
1459
|
if (error instanceof MengineEntityHttpRequestError) {
|
|
1354
|
-
if (error.
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
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
|
-
}
|
|
1460
|
+
if (isRecord(error.payload) && error.payload.accepted === true && typeof error.payload.revision === "number") return {
|
|
1461
|
+
kind: "conflicted",
|
|
1462
|
+
accepted: true,
|
|
1463
|
+
entity_revision: error.payload.revision,
|
|
1464
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1465
|
+
};
|
|
1385
1466
|
return {
|
|
1386
1467
|
kind: "rejected",
|
|
1387
1468
|
reason: "entity_state_rejected",
|
|
@@ -1397,33 +1478,14 @@ async function commitEntityPlan(client, plan) {
|
|
|
1397
1478
|
};
|
|
1398
1479
|
}
|
|
1399
1480
|
}
|
|
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
1481
|
function entityHttpErrorMessage(payload) {
|
|
1482
|
+
if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === "string") return payload.error.message;
|
|
1409
1483
|
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1410
1484
|
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1411
1485
|
}
|
|
1412
1486
|
function commitWarnings(result) {
|
|
1413
1487
|
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1414
1488
|
}
|
|
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
|
-
}
|
|
1427
1489
|
function parseInput(value) {
|
|
1428
1490
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
1429
1491
|
const op = value.op;
|
|
@@ -1434,18 +1496,6 @@ function parseInput(value) {
|
|
|
1434
1496
|
op,
|
|
1435
1497
|
doc_id: docId
|
|
1436
1498
|
};
|
|
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
|
-
}
|
|
1449
1499
|
if (op === "run-edit-script") {
|
|
1450
1500
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
1451
1501
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -1466,7 +1516,7 @@ function parseInput(value) {
|
|
|
1466
1516
|
}
|
|
1467
1517
|
if (op === "commit-plan") {
|
|
1468
1518
|
if (typeof value.plan_id !== "string" || value.plan_id.length === 0) throw new Error("plan_id must be a non-empty string");
|
|
1469
|
-
if (value.validation !== void 0 && value.validation !== "
|
|
1519
|
+
if (value.validation !== void 0 && value.validation !== "preflight") throw new Error("validation must be \"preflight\"");
|
|
1470
1520
|
return {
|
|
1471
1521
|
op,
|
|
1472
1522
|
doc_id: docId,
|
|
@@ -1555,10 +1605,21 @@ function createMedeoTool(options) {
|
|
|
1555
1605
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
1556
1606
|
if (options.loadInitialDraft === void 0) throw error;
|
|
1557
1607
|
}
|
|
1558
|
-
const
|
|
1608
|
+
const document = toVideoDocument(await options.loadInitialDraft(docId));
|
|
1609
|
+
if (Object.keys(document.part_library ?? {}).length > 0) throw new Error("Legacy content cannot bootstrap a Loro entity project");
|
|
1610
|
+
const seed = createMirrorVideoDocument(document, {
|
|
1559
1611
|
...peerId !== void 0 ? { peerId } : {},
|
|
1560
1612
|
origin: "mengine.medeo_tool.bootstrap"
|
|
1561
1613
|
});
|
|
1614
|
+
const foundation = ensureEditorFoundation({
|
|
1615
|
+
entities: [],
|
|
1616
|
+
relations: []
|
|
1617
|
+
});
|
|
1618
|
+
const entities = LoroEntityDocument.create(foundation.rows, {
|
|
1619
|
+
timelineEntityId: foundation.timelineEntityId,
|
|
1620
|
+
audioScriptEntityId: foundation.audioScriptEntityId
|
|
1621
|
+
});
|
|
1622
|
+
seed.import(entities.doc.export({ mode: "snapshot" }));
|
|
1562
1623
|
try {
|
|
1563
1624
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
1564
1625
|
} catch (error) {
|
|
@@ -1569,11 +1630,12 @@ function createMedeoTool(options) {
|
|
|
1569
1630
|
...peerId !== void 0 ? { peerId } : {}
|
|
1570
1631
|
});
|
|
1571
1632
|
}
|
|
1572
|
-
function rememberPlan(docId, plan) {
|
|
1633
|
+
function rememberPlan(docId, plan, baseState) {
|
|
1573
1634
|
const planId = randomUUID();
|
|
1574
1635
|
plans.set(planId, {
|
|
1575
1636
|
docId,
|
|
1576
|
-
plan
|
|
1637
|
+
plan,
|
|
1638
|
+
...plan.plan_kind === "entities" ? { baseState: baseState && structuredClone(baseState) } : {}
|
|
1577
1639
|
});
|
|
1578
1640
|
while (plans.size > maxPlans) {
|
|
1579
1641
|
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
@@ -1588,7 +1650,7 @@ function createMedeoTool(options) {
|
|
|
1588
1650
|
const pending = pendingPushes.get(docId);
|
|
1589
1651
|
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1590
1652
|
}
|
|
1591
|
-
function recordPushResult(docId, planId, plan, result) {
|
|
1653
|
+
function recordPushResult(docId, planId, plan, result, baseState) {
|
|
1592
1654
|
if (result.kind === "unconfirmed") {
|
|
1593
1655
|
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1594
1656
|
kind: "timeline",
|
|
@@ -1598,21 +1660,60 @@ function createMedeoTool(options) {
|
|
|
1598
1660
|
} : {
|
|
1599
1661
|
kind: "entities",
|
|
1600
1662
|
planId,
|
|
1601
|
-
plan
|
|
1663
|
+
plan,
|
|
1664
|
+
...baseState !== void 0 ? { baseState } : {}
|
|
1602
1665
|
});
|
|
1603
1666
|
return;
|
|
1604
1667
|
}
|
|
1605
1668
|
pendingPushes.delete(docId);
|
|
1606
1669
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1607
1670
|
}
|
|
1608
|
-
async function fetchEntityStateForSandbox(docId) {
|
|
1609
|
-
|
|
1671
|
+
async function fetchEntityStateForSandbox(docId, _doc, _pull) {
|
|
1672
|
+
const state = await getEntityClient(docId).fetchState();
|
|
1673
|
+
if (!state.loroSnapshot) throw new Error("Project requires the Loro entity contract");
|
|
1674
|
+
return state;
|
|
1610
1675
|
}
|
|
1611
|
-
async function commitCachedPlan(docId, _doc, plan, validation) {
|
|
1676
|
+
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1612
1677
|
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
1678
|
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1615
|
-
|
|
1679
|
+
const client = getEntityClient(docId);
|
|
1680
|
+
if (options.loadGenerationFacts !== void 0 && baseState === void 0) throw new Error("Generation synchronization is missing the cached causal plan baseline");
|
|
1681
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), baseState);
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1685
|
+
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1686
|
+
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1687
|
+
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1688
|
+
* newly created media Asset identities — not untouched pairs or placement-only edits.
|
|
1689
|
+
* One-sided facts are skipped silently inside the sync.
|
|
1690
|
+
*/
|
|
1691
|
+
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
1692
|
+
if (result.kind !== "committed" || options.loadGenerationFacts === void 0 || baseState === void 0) return result;
|
|
1693
|
+
let outcome;
|
|
1694
|
+
try {
|
|
1695
|
+
outcome = await syncGeneratedRelations({
|
|
1696
|
+
client: getEntityClient(docId),
|
|
1697
|
+
docId,
|
|
1698
|
+
baseState,
|
|
1699
|
+
entityCommands: plan.entity_commands,
|
|
1700
|
+
loadFacts: options.loadGenerationFacts
|
|
1701
|
+
});
|
|
1702
|
+
} catch (error) {
|
|
1703
|
+
outcome = {
|
|
1704
|
+
status: "failed",
|
|
1705
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
const warnings = outcome.status === "failed" ? [{
|
|
1709
|
+
kind: "generation_sync_failed",
|
|
1710
|
+
message: outcome.message ?? "generation lineage sync failed"
|
|
1711
|
+
}] : void 0;
|
|
1712
|
+
return {
|
|
1713
|
+
...result,
|
|
1714
|
+
generation_sync: outcome,
|
|
1715
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1716
|
+
};
|
|
1616
1717
|
}
|
|
1617
1718
|
async function observePull(doc) {
|
|
1618
1719
|
const result = await doc.pull();
|
|
@@ -1635,7 +1736,7 @@ function createMedeoTool(options) {
|
|
|
1635
1736
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1636
1737
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1637
1738
|
return await runExclusive(docId, async (doc) => {
|
|
1638
|
-
const
|
|
1739
|
+
const entityState = await fetchEntityStateForSandbox(docId, doc, await observePull(doc));
|
|
1639
1740
|
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1640
1741
|
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1641
1742
|
const previousVersion = modelContextVersions.get(baselineKey);
|
|
@@ -1660,64 +1761,24 @@ function createMedeoTool(options) {
|
|
|
1660
1761
|
async function snapshot(input) {
|
|
1661
1762
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1662
1763
|
assertNoPendingPush(input.doc_id);
|
|
1663
|
-
const
|
|
1764
|
+
const pull = await observePull(doc);
|
|
1765
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1664
1766
|
return {
|
|
1665
1767
|
ok: true,
|
|
1666
1768
|
op: "snapshot",
|
|
1667
1769
|
doc_id: input.doc_id,
|
|
1668
1770
|
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
1669
|
-
preview: renderEntitySnapshot(entityState)
|
|
1771
|
+
preview: renderEntitySnapshot(entityState),
|
|
1670
1772
|
collaborated: pull.collaborated,
|
|
1671
1773
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1672
1774
|
};
|
|
1673
1775
|
});
|
|
1674
1776
|
}
|
|
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
|
-
});
|
|
1716
|
-
}
|
|
1717
1777
|
async function run(input) {
|
|
1718
1778
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1719
1779
|
assertNoPendingPush(input.doc_id);
|
|
1720
|
-
const
|
|
1780
|
+
const pull = await observePull(doc);
|
|
1781
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1721
1782
|
const document = doc.snapshot();
|
|
1722
1783
|
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1723
1784
|
const result = await runEditScript({
|
|
@@ -1745,7 +1806,7 @@ function createMedeoTool(options) {
|
|
|
1745
1806
|
...result.plan,
|
|
1746
1807
|
doc_id: input.doc_id
|
|
1747
1808
|
};
|
|
1748
|
-
const planId = rememberPlan(input.doc_id, plan);
|
|
1809
|
+
const planId = rememberPlan(input.doc_id, plan, entityState);
|
|
1749
1810
|
const base = {
|
|
1750
1811
|
ok: true,
|
|
1751
1812
|
op: "run-edit-script",
|
|
@@ -1762,8 +1823,8 @@ function createMedeoTool(options) {
|
|
|
1762
1823
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1763
1824
|
};
|
|
1764
1825
|
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);
|
|
1826
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan, void 0, entityState);
|
|
1827
|
+
recordPushResult(input.doc_id, planId, plan, commit, entityState);
|
|
1767
1828
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1768
1829
|
return {
|
|
1769
1830
|
...base,
|
|
@@ -1779,8 +1840,8 @@ function createMedeoTool(options) {
|
|
|
1779
1840
|
const pending = pendingPushes.get(input.doc_id);
|
|
1780
1841
|
if (pending != null) {
|
|
1781
1842
|
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);
|
|
1843
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation, pending.baseState);
|
|
1844
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result, pending.kind === "entities" ? pending.baseState : void 0);
|
|
1784
1845
|
const warnings = commitWarnings(result);
|
|
1785
1846
|
return {
|
|
1786
1847
|
ok: true,
|
|
@@ -1797,8 +1858,8 @@ function createMedeoTool(options) {
|
|
|
1797
1858
|
const cached = plans.get(input.plan_id);
|
|
1798
1859
|
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
1860
|
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);
|
|
1861
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation, cached.baseState);
|
|
1862
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result, cached.baseState);
|
|
1802
1863
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1803
1864
|
return {
|
|
1804
1865
|
ok: true,
|
|
@@ -1822,7 +1883,6 @@ function createMedeoTool(options) {
|
|
|
1822
1883
|
try {
|
|
1823
1884
|
const parsed = parseInput(input);
|
|
1824
1885
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
1825
|
-
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
1826
1886
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
1827
1887
|
return await commit(parsed);
|
|
1828
1888
|
} catch (error) {
|