@mengine/medeo-tool 1.2.1-alpha.7 → 1.2.1-alpha.9
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 -16
- package/dist/{entity-contract-B3txrzTt.d.mts → entity-contract-DycLxdQ5.d.mts} +49 -3
- package/dist/index.d.mts +192 -13
- package/dist/index.mjs +1030 -923
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +397 -818
- package/dist/{script-session-BF44uKv_.mjs → script-session-CHyIUBkO.mjs} +366 -26
- package/dist/script-session-CHyIUBkO.mjs.map +1 -0
- package/dist/worker-entry.d.mts +5 -4
- package/dist/worker-entry.mjs +286 -5
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/script-session-BF44uKv_.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, replayJournal, toVideoDocument } from "@mengine/medeo-client";
|
|
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
|
|
@@ -35,7 +35,8 @@ function runEditScript(options) {
|
|
|
35
35
|
script: options.script,
|
|
36
36
|
inputs: options.inputs,
|
|
37
37
|
entityState: options.entityState,
|
|
38
|
-
idLabel: options.idLabel
|
|
38
|
+
idLabel: options.idLabel,
|
|
39
|
+
entityOnly: options.entityOnly
|
|
39
40
|
},
|
|
40
41
|
execArgv: resolveRegisterUrl.pathname.endsWith(".ts") ? [
|
|
41
42
|
"--experimental-transform-types",
|
|
@@ -124,6 +125,8 @@ function runEditScript(options) {
|
|
|
124
125
|
entity_base_revision: message.entityBaseRevision,
|
|
125
126
|
entity_commands: entityCommands.slice(),
|
|
126
127
|
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
128
|
+
deleted_entity_ids: message.deletedEntityIds,
|
|
129
|
+
deleted_relation_ids: message.deletedRelationIds,
|
|
127
130
|
preview: message.preview,
|
|
128
131
|
logs: logs.slice()
|
|
129
132
|
},
|
|
@@ -204,7 +207,9 @@ const KNOWN_RELATION_KINDS = [
|
|
|
204
207
|
"generated",
|
|
205
208
|
"phonetic-script-provenance",
|
|
206
209
|
"caption-provenance",
|
|
207
|
-
"caption-alignment"
|
|
210
|
+
"caption-alignment",
|
|
211
|
+
"clip-anchor",
|
|
212
|
+
"audio-script-render"
|
|
208
213
|
];
|
|
209
214
|
//#endregion
|
|
210
215
|
//#region src/entity/entity-http-client.ts
|
|
@@ -232,7 +237,7 @@ var EntityHttpClient = class {
|
|
|
232
237
|
async fetchState() {
|
|
233
238
|
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
234
239
|
}
|
|
235
|
-
async commit(expectedRevision, state) {
|
|
240
|
+
async commit(expectedRevision, state, deletions = {}) {
|
|
236
241
|
return toSnapshot(await this.requestJson({
|
|
237
242
|
method: "POST",
|
|
238
243
|
body: JSON.stringify({
|
|
@@ -240,7 +245,9 @@ var EntityHttpClient = class {
|
|
|
240
245
|
rows: {
|
|
241
246
|
entities: state.entities,
|
|
242
247
|
relations: state.relations
|
|
243
|
-
}
|
|
248
|
+
},
|
|
249
|
+
deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
|
|
250
|
+
deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
|
|
244
251
|
})
|
|
245
252
|
}), this.options.docId);
|
|
246
253
|
}
|
|
@@ -269,9 +276,9 @@ var EntityHttpClient = class {
|
|
|
269
276
|
}
|
|
270
277
|
};
|
|
271
278
|
function toSnapshot(value, expectedDocId) {
|
|
272
|
-
if (!isRecord$
|
|
279
|
+
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
273
280
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
274
|
-
if (!isRecord$
|
|
281
|
+
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
275
282
|
const response = value;
|
|
276
283
|
return {
|
|
277
284
|
revision: response.revision,
|
|
@@ -280,15 +287,15 @@ function toSnapshot(value, expectedDocId) {
|
|
|
280
287
|
};
|
|
281
288
|
}
|
|
282
289
|
function parseEntity(value) {
|
|
283
|
-
if (!isRecord$
|
|
290
|
+
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");
|
|
284
291
|
return structuredClone(value);
|
|
285
292
|
}
|
|
286
293
|
function parseRelation(value) {
|
|
287
|
-
if (!isRecord$
|
|
294
|
+
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");
|
|
288
295
|
return structuredClone(value);
|
|
289
296
|
}
|
|
290
297
|
function isJsonObject(value) {
|
|
291
|
-
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$
|
|
298
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$2(value);
|
|
292
299
|
}
|
|
293
300
|
function isJsonValue(value, ancestors) {
|
|
294
301
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
@@ -301,7 +308,7 @@ function isJsonValue(value, ancestors) {
|
|
|
301
308
|
ancestors.delete(value);
|
|
302
309
|
return valid;
|
|
303
310
|
}
|
|
304
|
-
function isRecord$
|
|
311
|
+
function isRecord$2(value) {
|
|
305
312
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
306
313
|
}
|
|
307
314
|
function isTrimmed(value) {
|
|
@@ -320,736 +327,495 @@ async function safeReadJson(response) {
|
|
|
320
327
|
}
|
|
321
328
|
}
|
|
322
329
|
//#endregion
|
|
323
|
-
//#region src/
|
|
330
|
+
//#region src/entity/generation-sync.ts
|
|
324
331
|
/**
|
|
325
|
-
*
|
|
326
|
-
*
|
|
327
|
-
* Runtime copy of the sandbox TypeScript disclosure. The model prompt imports
|
|
328
|
-
* this value so its interface and the checked-in declaration cannot drift.
|
|
332
|
+
* External systems whose asset entities carry a factual Memota identity.
|
|
333
|
+
* Voice results use the speech system; every other medium uses `memota`.
|
|
329
334
|
*/
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
"
|
|
334
|
-
"
|
|
335
|
-
"
|
|
336
|
-
"
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
"
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
"
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
"
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
"
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
335
|
+
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
336
|
+
/** Entity kinds that may carry a generated Relation endpoint (DSL `GeneratedMedia`). */
|
|
337
|
+
const GENERATED_MEDIA_KINDS = new Set([
|
|
338
|
+
"video",
|
|
339
|
+
"image",
|
|
340
|
+
"audio",
|
|
341
|
+
"voice"
|
|
342
|
+
]);
|
|
343
|
+
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
344
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
345
|
+
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
346
|
+
function parseGenerationFacts(value) {
|
|
347
|
+
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
348
|
+
return value.map((item) => {
|
|
349
|
+
if (!isRecord$1(item)) throw new Error("each generation fact must be an object");
|
|
350
|
+
const { outputAssetId, inputAssetIds } = item;
|
|
351
|
+
if (typeof outputAssetId !== "string" || outputAssetId.length === 0 || outputAssetId.trim() !== outputAssetId) throw new Error("generation fact outputAssetId must be a non-empty trimmed string");
|
|
352
|
+
if (!Array.isArray(inputAssetIds)) throw new Error("generation fact inputAssetIds must be an array (explicit [] means text-only)");
|
|
353
|
+
const inputs = inputAssetIds;
|
|
354
|
+
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");
|
|
355
|
+
return {
|
|
356
|
+
outputAssetId,
|
|
357
|
+
inputAssetIds: [...inputs]
|
|
358
|
+
};
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
function planGenerationScope(base, commands, state) {
|
|
362
|
+
const createdMediaIds = /* @__PURE__ */ new Set();
|
|
363
|
+
const keyChangedAssetIds = /* @__PURE__ */ new Set();
|
|
364
|
+
const bindingTouchedMediaIds = /* @__PURE__ */ new Set();
|
|
365
|
+
for (const command of commands) if (command.kind === "create-entity") {
|
|
366
|
+
if (command.entity.entity_kind === "asset") keyChangedAssetIds.add(command.entity.entity_id);
|
|
367
|
+
else if (GENERATED_MEDIA_KINDS.has(command.entity.entity_kind)) createdMediaIds.add(command.entity.entity_id);
|
|
368
|
+
} else if (command.kind === "update-entity") {
|
|
369
|
+
const baseEntity = base.entities.find((entity) => entity.entity_id === command.entity_id);
|
|
370
|
+
if (baseEntity?.entity_kind !== "asset") continue;
|
|
371
|
+
const before = assetKeyOf(baseEntity);
|
|
372
|
+
const after = assetKeyOf({
|
|
373
|
+
entity_id: command.entity_id,
|
|
374
|
+
entity_kind: "asset",
|
|
375
|
+
payload: command.payload
|
|
376
|
+
});
|
|
377
|
+
if (after !== void 0 && before !== after) keyChangedAssetIds.add(command.entity_id);
|
|
378
|
+
} else if (command.kind === "link-relation") {
|
|
379
|
+
if (command.relation.relation_kind === "physical-asset") bindingTouchedMediaIds.add(command.relation.endpoint_0_entity_id);
|
|
380
|
+
} else if (command.kind === "unlink-relation") {
|
|
381
|
+
const removed = base.relations.find((relation) => relation.relation_id === command.relation_id);
|
|
382
|
+
if (removed?.relation_kind === "physical-asset") bindingTouchedMediaIds.add(removed.endpoint_0_entity_id);
|
|
383
|
+
}
|
|
384
|
+
const scoped = new Set([...createdMediaIds, ...bindingTouchedMediaIds]);
|
|
385
|
+
const assetsById = new Map(state.entities.filter((entity) => entity.entity_kind === "asset").map((entity) => [entity.entity_id, entity]));
|
|
386
|
+
for (const relation of state.relations) {
|
|
387
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
388
|
+
if (!keyChangedAssetIds.has(relation.endpoint_1_entity_id)) continue;
|
|
389
|
+
scoped.add(relation.endpoint_0_entity_id);
|
|
390
|
+
}
|
|
391
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
392
|
+
for (const relation of state.relations) {
|
|
393
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
394
|
+
if (!scoped.has(relation.endpoint_0_entity_id)) continue;
|
|
395
|
+
const asset = assetsById.get(relation.endpoint_1_entity_id);
|
|
396
|
+
const key = asset === void 0 ? void 0 : assetKeyOf(asset);
|
|
397
|
+
if (key !== void 0) queryKeys.add(key);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
scopedMediaIds: scoped,
|
|
401
|
+
queryAssetKeys: [...queryKeys].sort()
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
406
|
+
* factual records. Both endpoints must already exist and fact-match through
|
|
407
|
+
* physical-asset bindings, and the pair must involve a media Entity the plan
|
|
408
|
+
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
409
|
+
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
410
|
+
* the facts already resolved against the plan's base state is likewise skipped.
|
|
411
|
+
* One-sided facts, text-only records, self pairs, and already-linked pairs are
|
|
412
|
+
* skipped. Duplicate records collapse to one Relation.
|
|
413
|
+
*/
|
|
414
|
+
function planGeneratedRelations(input) {
|
|
415
|
+
const { state, facts } = input;
|
|
416
|
+
const scoped = input.scopedMediaIds;
|
|
417
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state);
|
|
418
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState), facts));
|
|
419
|
+
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)));
|
|
420
|
+
const relations = [];
|
|
421
|
+
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) ?? []) {
|
|
422
|
+
if (outputId === inputId) continue;
|
|
423
|
+
if (!scoped.has(outputId) && !scoped.has(inputId)) continue;
|
|
424
|
+
const pair = pairKey(outputId, inputId);
|
|
425
|
+
if (linkedPairs.has(pair) || baseResolvable.has(pair)) continue;
|
|
426
|
+
linkedPairs.add(pair);
|
|
427
|
+
relations.push({
|
|
428
|
+
relation_id: input.newRelationId(),
|
|
429
|
+
relation_kind: "generated",
|
|
430
|
+
endpoint_0_entity_id: outputId,
|
|
431
|
+
endpoint_1_entity_id: inputId,
|
|
432
|
+
metadata: {},
|
|
433
|
+
trace: { synced_by: "generation-sync" }
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return relations;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
440
|
+
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
441
|
+
* commit result is never masked; a successful query that finds nothing is
|
|
442
|
+
* `current`. A revision conflict re-reads, re-derives the trigger keys from
|
|
443
|
+
* the fresh bindings, queries any newly scoped facts, re-plans, and retries
|
|
444
|
+
* within `MAX_COMMIT_ATTEMPTS` before reporting failure — a stale asset query
|
|
445
|
+
* must never let a backfillable edge be reported as `current`.
|
|
446
|
+
*/
|
|
447
|
+
async function syncGeneratedRelations(input) {
|
|
448
|
+
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
449
|
+
let state;
|
|
450
|
+
let facts = [];
|
|
451
|
+
const queriedKeys = /* @__PURE__ */ new Set();
|
|
452
|
+
try {
|
|
453
|
+
state = await client.fetchState();
|
|
454
|
+
let scope = planGenerationScope(baseState, entityCommands, state);
|
|
455
|
+
for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
|
|
456
|
+
const unseenKeys = scope.queryAssetKeys.filter((key) => !queriedKeys.has(key));
|
|
457
|
+
for (const key of unseenKeys) queriedKeys.add(key);
|
|
458
|
+
if (unseenKeys.length > 0) facts = [...facts, ...parseGenerationFacts(await loadFacts(docId, unseenKeys))];
|
|
459
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
460
|
+
const relations = planGeneratedRelations({
|
|
461
|
+
baseState,
|
|
462
|
+
state,
|
|
463
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
464
|
+
facts,
|
|
465
|
+
newRelationId: mintRelationId
|
|
466
|
+
});
|
|
467
|
+
if (relations.length === 0) return { status: "current" };
|
|
468
|
+
try {
|
|
469
|
+
await client.commit(state.revision, {
|
|
470
|
+
...state,
|
|
471
|
+
relations: [...state.relations, ...relations]
|
|
472
|
+
});
|
|
473
|
+
return {
|
|
474
|
+
status: "applied",
|
|
475
|
+
created_relation_ids: relations.map((relation) => relation.relation_id)
|
|
476
|
+
};
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (!(error instanceof MengineEntityHttpRequestError && error.status === 409) || attempt === MAX_COMMIT_ATTEMPTS) return {
|
|
479
|
+
status: "failed",
|
|
480
|
+
message: `generation lineage sync commit failed: ${errorMessage(error)}`
|
|
481
|
+
};
|
|
482
|
+
state = await client.fetchState();
|
|
483
|
+
scope = planGenerationScope(baseState, entityCommands, state);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
status: "failed",
|
|
488
|
+
message: "generation lineage sync exhausted its retry budget"
|
|
489
|
+
};
|
|
490
|
+
} catch (error) {
|
|
491
|
+
return {
|
|
492
|
+
status: "failed",
|
|
493
|
+
message: `generation lineage query failed: ${errorMessage(error)}`
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function assetKeyOf(entity) {
|
|
498
|
+
if (entity.entity_kind !== "asset") return void 0;
|
|
499
|
+
const external = entity.payload?.external;
|
|
500
|
+
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
501
|
+
const { system, key } = external;
|
|
502
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
|
|
503
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
504
|
+
return key;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Media Entity ids fact-matched to each external asset key. Bindings follow
|
|
508
|
+
* the canonical physical-asset direction (endpoint 0 = sequence media,
|
|
509
|
+
* endpoint 1 = Asset) that the DSL spec enforces.
|
|
510
|
+
*/
|
|
511
|
+
function resolveMediaByAssetKey(state) {
|
|
512
|
+
const assetsById = new Map(state.entities.filter((entity) => entity.entity_kind === "asset").map((entity) => [entity.entity_id, entity]));
|
|
513
|
+
const mediaKinds = new Set([...GENERATED_MEDIA_KINDS]);
|
|
514
|
+
const mediaIds = new Set(state.entities.filter((entity) => mediaKinds.has(entity.entity_kind)).map((entity) => entity.entity_id));
|
|
515
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
516
|
+
for (const relation of state.relations) {
|
|
517
|
+
if (relation.relation_kind !== "physical-asset") continue;
|
|
518
|
+
if (!mediaIds.has(relation.endpoint_0_entity_id)) continue;
|
|
519
|
+
const asset = assetsById.get(relation.endpoint_1_entity_id);
|
|
520
|
+
const key = asset === void 0 ? void 0 : assetKeyOf(asset);
|
|
521
|
+
if (key === void 0) continue;
|
|
522
|
+
const media = resolved.get(key);
|
|
523
|
+
if (media === void 0) resolved.set(key, [relation.endpoint_0_entity_id]);
|
|
524
|
+
else if (!media.includes(relation.endpoint_0_entity_id)) media.push(relation.endpoint_0_entity_id);
|
|
525
|
+
}
|
|
526
|
+
return resolved;
|
|
527
|
+
}
|
|
528
|
+
function pairKey(endpoint0, endpoint1) {
|
|
529
|
+
return `${endpoint0}\u0000${endpoint1}`;
|
|
530
|
+
}
|
|
531
|
+
/** Pair keys the facts already resolve to under the given base bindings. */
|
|
532
|
+
function resolvablePairs(mediaByAssetKey, facts) {
|
|
533
|
+
const pairs = [];
|
|
534
|
+
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));
|
|
535
|
+
return pairs;
|
|
536
|
+
}
|
|
537
|
+
function mintRelationId() {
|
|
538
|
+
return `relation_${randomUUID()}`;
|
|
539
|
+
}
|
|
540
|
+
function errorMessage(error) {
|
|
541
|
+
return error instanceof Error ? error.message : String(error);
|
|
542
|
+
}
|
|
543
|
+
function isRecord$1(value) {
|
|
544
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
545
|
+
}
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/migration-input.ts
|
|
548
|
+
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
549
|
+
function parseMigrationAssetFacts(value) {
|
|
550
|
+
if (!Array.isArray(value)) throw new Error("asset_facts is required for migrate-legacy and must be an array");
|
|
551
|
+
return value.map((item) => {
|
|
552
|
+
if (!record(item)) throw new Error("Each asset_facts entry must be an object");
|
|
553
|
+
const { assetId, kind, durationMs, storageKey, voice } = item;
|
|
554
|
+
if (!nonempty(assetId)) throw new Error("asset_facts.assetId must be a non-empty trimmed string");
|
|
555
|
+
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "voice") throw new Error("asset_facts.kind must be image, video, audio, or voice");
|
|
556
|
+
if (Object.keys(item).some((key) => ![
|
|
557
|
+
"assetId",
|
|
558
|
+
"kind",
|
|
559
|
+
"durationMs",
|
|
560
|
+
"storageKey",
|
|
561
|
+
"voice"
|
|
562
|
+
].includes(key))) throw new Error("Unknown asset_facts field");
|
|
563
|
+
if (storageKey !== void 0 && !nonempty(storageKey)) throw new Error("asset_facts.storageKey must be non-empty");
|
|
564
|
+
if (kind === "image") {
|
|
565
|
+
if (durationMs !== void 0 || voice !== void 0) throw new Error("Image facts cannot declare duration or voice");
|
|
566
|
+
return {
|
|
567
|
+
assetId,
|
|
568
|
+
kind,
|
|
569
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
if (typeof durationMs !== "number" || !Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error("asset_facts.durationMs must be factual positive whole milliseconds");
|
|
573
|
+
if (kind === "video") {
|
|
574
|
+
if (voice !== void 0) throw new Error("Video facts cannot declare voice");
|
|
575
|
+
return {
|
|
576
|
+
assetId,
|
|
577
|
+
kind,
|
|
578
|
+
durationMs,
|
|
579
|
+
...storageKey === void 0 ? {} : { storageKey }
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
if (!nonempty(storageKey)) throw new Error("Audio and Voice facts require their physical storageKey");
|
|
583
|
+
if (kind === "audio") {
|
|
584
|
+
if (voice !== void 0) throw new Error("Audio facts cannot declare a Voice descriptor");
|
|
585
|
+
return {
|
|
586
|
+
assetId,
|
|
587
|
+
kind,
|
|
588
|
+
durationMs,
|
|
589
|
+
storageKey
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
if (!record(voice) || voice.system !== "voice-library" || !nonempty(voice.key) || voice.name !== void 0 && typeof voice.name !== "string" || Object.keys(voice).some((key) => ![
|
|
593
|
+
"system",
|
|
594
|
+
"key",
|
|
595
|
+
"name"
|
|
596
|
+
].includes(key))) throw new Error("Voice facts require an explicit voice-library descriptor");
|
|
597
|
+
return {
|
|
598
|
+
assetId,
|
|
599
|
+
kind,
|
|
600
|
+
durationMs,
|
|
601
|
+
storageKey,
|
|
602
|
+
voice: {
|
|
603
|
+
system: "voice-library",
|
|
604
|
+
key: voice.key,
|
|
605
|
+
...voice.name === void 0 ? {} : { name: voice.name }
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
function record(value) {
|
|
611
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
612
|
+
}
|
|
613
|
+
function nonempty(value) {
|
|
614
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
618
|
+
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
619
|
+
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
620
|
+
"/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
|
|
621
|
+
"export interface AudioMediaAssetFact {",
|
|
622
|
+
" readonly assetId: string;",
|
|
623
|
+
" readonly kind: 'audio';",
|
|
624
|
+
" readonly durationMs: number;",
|
|
625
|
+
" readonly storageKey: string;",
|
|
407
626
|
"}",
|
|
408
|
-
"",
|
|
409
|
-
"
|
|
410
|
-
"
|
|
411
|
-
"
|
|
412
|
-
"
|
|
413
|
-
"
|
|
414
|
-
"
|
|
415
|
-
"
|
|
416
|
-
" */",
|
|
417
|
-
" speeches: {",
|
|
418
|
-
" /**",
|
|
419
|
-
" * The speech part ID (= side-effect speech_parts[].id)",
|
|
420
|
-
" * @constraint minLength(1)",
|
|
421
|
-
" */",
|
|
422
|
-
" speech_id: string;",
|
|
423
|
-
" /**",
|
|
424
|
-
" * Host video clip part ID the speech anchors to (RFC 02 §4)",
|
|
425
|
-
" * @constraint minLength(1)",
|
|
426
|
-
" */",
|
|
427
|
-
" anchor_part_id: string;",
|
|
428
|
-
" /**",
|
|
429
|
-
" * Offset within the host clip (speech.abs = host.abs + offset_ms)",
|
|
430
|
-
" * @constraint int",
|
|
431
|
-
" * @constraint min(0)",
|
|
432
|
-
" */",
|
|
433
|
-
" offset_ms: number;",
|
|
434
|
-
" /**",
|
|
435
|
-
" * @constraint minLength(1)",
|
|
436
|
-
" */",
|
|
437
|
-
" audio_storage_key: string;",
|
|
438
|
-
" /**",
|
|
439
|
-
" * Duration in milliseconds (> 0)",
|
|
440
|
-
" * @constraint int",
|
|
441
|
-
" * @constraint positive",
|
|
442
|
-
" */",
|
|
443
|
-
" duration_ms: number;",
|
|
444
|
-
" audio_script: string;",
|
|
445
|
-
" /**",
|
|
446
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
447
|
-
" * @constraint min(-60)",
|
|
448
|
-
" * @constraint max(20)",
|
|
449
|
-
" */",
|
|
450
|
-
" volume: number;",
|
|
451
|
-
" /**",
|
|
452
|
-
" * TTS voice summary attached to a speech",
|
|
453
|
-
" */",
|
|
454
|
-
" voice: {",
|
|
455
|
-
" /**",
|
|
456
|
-
" * @constraint minLength(1)",
|
|
457
|
-
" */",
|
|
458
|
-
" id: string;",
|
|
459
|
-
" name: string;",
|
|
460
|
-
" };",
|
|
461
|
-
" /**",
|
|
462
|
-
" * @constraint minLength(1)",
|
|
463
|
-
" */",
|
|
464
|
-
" origin_speech_id: string;",
|
|
465
|
-
" /**",
|
|
466
|
-
" * Caption part IDs owned by this speech",
|
|
467
|
-
" */",
|
|
468
|
-
" caption_ids: string[];",
|
|
469
|
-
" }[];",
|
|
470
|
-
" /**",
|
|
471
|
-
" * Materialized caption parts owned by the speeches",
|
|
472
|
-
" */",
|
|
473
|
-
" captions: {",
|
|
474
|
-
" /**",
|
|
475
|
-
" * The caption part ID (= side-effect created_caption_parts[].id)",
|
|
476
|
-
" * @constraint minLength(1)",
|
|
477
|
-
" */",
|
|
478
|
-
" caption_id: string;",
|
|
479
|
-
" /**",
|
|
480
|
-
" * The owning speech part ID",
|
|
481
|
-
" * @constraint minLength(1)",
|
|
482
|
-
" */",
|
|
483
|
-
" speech_part_id: string;",
|
|
484
|
-
" text: string;",
|
|
485
|
-
" /**",
|
|
486
|
-
" * Offset within the host speech (caption.abs = speech.abs + start_ms)",
|
|
487
|
-
" * @constraint int",
|
|
488
|
-
" * @constraint min(0)",
|
|
489
|
-
" */",
|
|
490
|
-
" start_ms: number;",
|
|
491
|
-
" /**",
|
|
492
|
-
" * Duration in milliseconds (> 0)",
|
|
493
|
-
" * @constraint int",
|
|
494
|
-
" * @constraint positive",
|
|
495
|
-
" */",
|
|
496
|
-
" duration_ms: number;",
|
|
497
|
-
" }[];",
|
|
627
|
+
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
628
|
+
" extent: {",
|
|
629
|
+
" kind: 'bounded';",
|
|
630
|
+
" start: number;",
|
|
631
|
+
" end: number;",
|
|
632
|
+
" };",
|
|
633
|
+
" sampling: 'derived';",
|
|
634
|
+
" coordinateSpace: JsonValue;",
|
|
498
635
|
"}",
|
|
499
|
-
"",
|
|
500
|
-
"
|
|
501
|
-
"
|
|
502
|
-
"
|
|
503
|
-
"
|
|
504
|
-
"
|
|
505
|
-
"
|
|
506
|
-
"
|
|
507
|
-
"
|
|
508
|
-
" * @constraint minLength(1)",
|
|
509
|
-
" */",
|
|
510
|
-
" clip_id: string;",
|
|
511
|
-
" /**",
|
|
512
|
-
" * New absolute start time in milliseconds on the timeline",
|
|
513
|
-
" * @constraint int",
|
|
514
|
-
" * @constraint min(0)",
|
|
515
|
-
" */",
|
|
516
|
-
" new_start_ms: number;",
|
|
517
|
-
" /**",
|
|
518
|
-
" * Target track ID to move the clip to (optional)",
|
|
519
|
-
" * @constraint minLength(1)",
|
|
520
|
-
" */",
|
|
521
|
-
" new_track_id?: string;",
|
|
522
|
-
" }[];",
|
|
636
|
+
"export interface BoundedNativeSequencePayload extends JsonObject {",
|
|
637
|
+
" /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
|
|
638
|
+
" extent: {",
|
|
639
|
+
" kind: 'bounded';",
|
|
640
|
+
" start: number;",
|
|
641
|
+
" end: number;",
|
|
642
|
+
" };",
|
|
643
|
+
" sampling: 'native';",
|
|
644
|
+
" coordinateSpace: JsonValue;",
|
|
523
645
|
"}",
|
|
524
|
-
"",
|
|
525
|
-
"
|
|
526
|
-
"
|
|
527
|
-
" */",
|
|
528
|
-
"export interface MoveVideoClipsByAnchorInput {",
|
|
529
|
-
" /**",
|
|
530
|
-
" * Clips to move as one block, keeping their relative order. Need not be contiguous on the track.",
|
|
531
|
-
" * @constraint minLength(1)",
|
|
532
|
-
" */",
|
|
533
|
-
" clip_ids: string[];",
|
|
534
|
-
" /**",
|
|
535
|
-
" * Where the moved block lands: before/after a reference clip, or at the head of the track",
|
|
536
|
-
" */",
|
|
537
|
-
" anchor:",
|
|
538
|
-
" | {",
|
|
539
|
-
" position: 'before';",
|
|
540
|
-
" /**",
|
|
541
|
-
" * The moved block lands immediately before this clip",
|
|
542
|
-
" * @constraint minLength(1)",
|
|
543
|
-
" */",
|
|
544
|
-
" clip_id: string;",
|
|
545
|
-
" }",
|
|
546
|
-
" | {",
|
|
547
|
-
" position: 'after';",
|
|
548
|
-
" /**",
|
|
549
|
-
" * The moved block lands immediately after this clip",
|
|
550
|
-
" * @constraint minLength(1)",
|
|
551
|
-
" */",
|
|
552
|
-
" clip_id: string;",
|
|
553
|
-
" }",
|
|
554
|
-
" | { position: 'track_start' };",
|
|
555
|
-
" /**",
|
|
556
|
-
" * What happens to speeches anchored to the moved clips (required — see the policy doc)",
|
|
557
|
-
" */",
|
|
558
|
-
" on_anchored: 'follow' | 'keep_absolute';",
|
|
646
|
+
"export interface CaptionFontDescriptor {",
|
|
647
|
+
" readonly system: 'font-library';",
|
|
648
|
+
" readonly key: string;",
|
|
559
649
|
"}",
|
|
560
|
-
"",
|
|
561
|
-
"
|
|
562
|
-
"
|
|
563
|
-
"
|
|
564
|
-
"
|
|
565
|
-
"
|
|
566
|
-
"
|
|
567
|
-
"
|
|
568
|
-
"
|
|
569
|
-
"
|
|
570
|
-
"
|
|
650
|
+
"export interface CaptionStyleFields {",
|
|
651
|
+
" readonly font?: CaptionFontDescriptor;",
|
|
652
|
+
" readonly fontSize?: number;",
|
|
653
|
+
" readonly fontColor?: string;",
|
|
654
|
+
" readonly fontWeight?: number;",
|
|
655
|
+
" readonly entranceAnimation?: string;",
|
|
656
|
+
" readonly entranceAnimationDurationMs?: number;",
|
|
657
|
+
" readonly strokeColor?: string;",
|
|
658
|
+
" readonly strokeWidth?: number;",
|
|
659
|
+
" readonly positionX?: number;",
|
|
660
|
+
" readonly positionY?: number;",
|
|
571
661
|
"}",
|
|
572
|
-
"",
|
|
573
|
-
"
|
|
574
|
-
"
|
|
575
|
-
"
|
|
576
|
-
"
|
|
577
|
-
"
|
|
578
|
-
"
|
|
579
|
-
"
|
|
580
|
-
"
|
|
581
|
-
"
|
|
582
|
-
"
|
|
583
|
-
"
|
|
584
|
-
"
|
|
585
|
-
"
|
|
586
|
-
"
|
|
587
|
-
"
|
|
588
|
-
"
|
|
589
|
-
"
|
|
590
|
-
"
|
|
591
|
-
"
|
|
592
|
-
"
|
|
593
|
-
"
|
|
594
|
-
"
|
|
595
|
-
"
|
|
596
|
-
" * @constraint positive",
|
|
597
|
-
" */",
|
|
598
|
-
" media_duration_ms: number;",
|
|
599
|
-
" /**",
|
|
600
|
-
" * Trim window start in the media (default 0)",
|
|
601
|
-
" * @constraint int",
|
|
602
|
-
" * @constraint min(0)",
|
|
603
|
-
" */",
|
|
604
|
-
" play_in?: number;",
|
|
605
|
-
" /**",
|
|
606
|
-
" * Trim window end in the media (default media_duration_ms)",
|
|
607
|
-
" * @constraint int",
|
|
608
|
-
" * @constraint positive",
|
|
609
|
-
" */",
|
|
610
|
-
" play_out?: number;",
|
|
611
|
-
" /**",
|
|
612
|
-
" * Target track ID (optional, defaults to main track)",
|
|
613
|
-
" * @constraint minLength(1)",
|
|
614
|
-
" */",
|
|
615
|
-
" track_id?: string;",
|
|
616
|
-
" }[];",
|
|
617
|
-
" /**",
|
|
618
|
-
" * Insert new clips before this clip ID",
|
|
619
|
-
" * @constraint minLength(1)",
|
|
620
|
-
" */",
|
|
621
|
-
" before_clip_id?: string;",
|
|
622
|
-
" /**",
|
|
623
|
-
" * Insert new clips after this clip ID",
|
|
624
|
-
" * @constraint minLength(1)",
|
|
625
|
-
" */",
|
|
626
|
-
" after_clip_id?: string;",
|
|
627
|
-
"}",
|
|
628
|
-
"",
|
|
629
|
-
"export interface AdjustVideoClipVolumeInput {",
|
|
630
|
-
" /**",
|
|
631
|
-
" * List of video clips with their new volume settings",
|
|
632
|
-
" * @constraint minLength(1)",
|
|
633
|
-
" */",
|
|
634
|
-
" clips: {",
|
|
635
|
-
" /**",
|
|
636
|
-
" * The video clip part ID to adjust volume for",
|
|
637
|
-
" * @constraint minLength(1)",
|
|
638
|
-
" */",
|
|
639
|
-
" clip_id: string;",
|
|
640
|
-
" /**",
|
|
641
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
642
|
-
" * @constraint min(-60)",
|
|
643
|
-
" * @constraint max(20)",
|
|
644
|
-
" */",
|
|
645
|
-
" volume: number;",
|
|
646
|
-
" }[];",
|
|
662
|
+
"export type ClipEntityId = EntityId;",
|
|
663
|
+
"export type ClipPlacement =",
|
|
664
|
+
" | {",
|
|
665
|
+
" readonly kind: 'sequential';",
|
|
666
|
+
" readonly order: number;",
|
|
667
|
+
" }",
|
|
668
|
+
" | {",
|
|
669
|
+
" readonly kind: 'absolute';",
|
|
670
|
+
" readonly targetRange: SequenceRange<number>;",
|
|
671
|
+
" }",
|
|
672
|
+
" | {",
|
|
673
|
+
" readonly kind: 'anchored';",
|
|
674
|
+
" readonly hostClipEntityId: string;",
|
|
675
|
+
" readonly anchorOffset: number;",
|
|
676
|
+
" };",
|
|
677
|
+
"export type CreateEntityInput = {",
|
|
678
|
+
" [K in KnownEntityKind]: {",
|
|
679
|
+
" entity_id?: string;",
|
|
680
|
+
" entity_kind: K;",
|
|
681
|
+
" payload: EntityPayloadByKind[K];",
|
|
682
|
+
" };",
|
|
683
|
+
"}[KnownEntityKind];",
|
|
684
|
+
"export interface DeleteBgmInput {",
|
|
685
|
+
" readonly timelineEntityId: string;",
|
|
647
686
|
"}",
|
|
648
|
-
"",
|
|
649
|
-
"
|
|
650
|
-
" * Set the playback speed of existing video clips.",
|
|
651
|
-
" */",
|
|
652
|
-
"export interface SetVideoClipSpeedShiftInput {",
|
|
653
|
-
" /**",
|
|
654
|
-
" * Video clips with their new speed settings",
|
|
655
|
-
" * @constraint minLength(1)",
|
|
656
|
-
" */",
|
|
657
|
-
" clips: {",
|
|
658
|
-
" /**",
|
|
659
|
-
" * The video clip part ID to set speed for",
|
|
660
|
-
" * @constraint minLength(1)",
|
|
661
|
-
" */",
|
|
662
|
-
" clip_id: string;",
|
|
663
|
-
" /**",
|
|
664
|
-
" * The new speed setting, or null to reset to 1×",
|
|
665
|
-
" */",
|
|
666
|
-
" speed_shift: {",
|
|
667
|
-
" category: 'linear' | 'curve';",
|
|
668
|
-
" mode: string;",
|
|
669
|
-
" config:",
|
|
670
|
-
" | {",
|
|
671
|
-
" linear: {",
|
|
672
|
-
" /**",
|
|
673
|
-
" * @constraint positive",
|
|
674
|
-
" */",
|
|
675
|
-
" speed: number;",
|
|
676
|
-
" };",
|
|
677
|
-
" }",
|
|
678
|
-
" | {",
|
|
679
|
-
" curve: {",
|
|
680
|
-
" /**",
|
|
681
|
-
" * @constraint minLength(2)",
|
|
682
|
-
" */",
|
|
683
|
-
" keyframes: {",
|
|
684
|
-
" /**",
|
|
685
|
-
" * @constraint min(0)",
|
|
686
|
-
" * @constraint max(1)",
|
|
687
|
-
" */",
|
|
688
|
-
" position: number;",
|
|
689
|
-
" /**",
|
|
690
|
-
" * @constraint min(0)",
|
|
691
|
-
" */",
|
|
692
|
-
" rate: number;",
|
|
693
|
-
" /**",
|
|
694
|
-
" * Bezier tangent handle (x, y)",
|
|
695
|
-
" */",
|
|
696
|
-
" in_tangent?: { x: number; y: number };",
|
|
697
|
-
" /**",
|
|
698
|
-
" * Bezier tangent handle (x, y)",
|
|
699
|
-
" */",
|
|
700
|
-
" out_tangent?: { x: number; y: number };",
|
|
701
|
-
" }[];",
|
|
702
|
-
" };",
|
|
703
|
-
" };",
|
|
704
|
-
" } | null;",
|
|
705
|
-
" }[];",
|
|
687
|
+
"export interface DeleteClipInput {",
|
|
688
|
+
" readonly clipEntityId: string;",
|
|
706
689
|
"}",
|
|
707
|
-
"",
|
|
708
|
-
"
|
|
709
|
-
"
|
|
710
|
-
" */",
|
|
711
|
-
"export interface ReplaceVideoClipContentInput {",
|
|
712
|
-
" /**",
|
|
713
|
-
" * Video clips whose media is being replaced",
|
|
714
|
-
" * @constraint minLength(1)",
|
|
715
|
-
" */",
|
|
716
|
-
" clips: {",
|
|
717
|
-
" /**",
|
|
718
|
-
" * Existing video clip part ID to re-point",
|
|
719
|
-
" * @constraint minLength(1)",
|
|
720
|
-
" */",
|
|
721
|
-
" clip_id: string;",
|
|
722
|
-
" /**",
|
|
723
|
-
" * The new media asset ID",
|
|
724
|
-
" * @constraint minLength(1)",
|
|
725
|
-
" */",
|
|
726
|
-
" origin_media_id: string;",
|
|
727
|
-
" /**",
|
|
728
|
-
" * The new media's intrinsic full length",
|
|
729
|
-
" * @constraint int",
|
|
730
|
-
" * @constraint positive",
|
|
731
|
-
" */",
|
|
732
|
-
" media_duration_ms: number;",
|
|
733
|
-
" /**",
|
|
734
|
-
" * Trim window start in the new media (usually 0)",
|
|
735
|
-
" * @constraint int",
|
|
736
|
-
" * @constraint min(0)",
|
|
737
|
-
" */",
|
|
738
|
-
" play_in: number;",
|
|
739
|
-
" /**",
|
|
740
|
-
" * Trim window end in the new media (usually = media_duration_ms)",
|
|
741
|
-
" * @constraint int",
|
|
742
|
-
" * @constraint positive",
|
|
743
|
-
" */",
|
|
744
|
-
" play_out: number;",
|
|
745
|
-
" /**",
|
|
746
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
747
|
-
" * @constraint min(-60)",
|
|
748
|
-
" * @constraint max(20)",
|
|
749
|
-
" */",
|
|
750
|
-
" volume: number;",
|
|
751
|
-
" }[];",
|
|
690
|
+
"export interface DeleteClipTreeInput {",
|
|
691
|
+
" readonly clipEntityIds: readonly string[];",
|
|
692
|
+
" readonly onAnchored: 'cascade' | 'detach';",
|
|
752
693
|
"}",
|
|
753
|
-
"",
|
|
754
|
-
"
|
|
755
|
-
" * Replace a contiguous run of main-track clips with a new run.",
|
|
756
|
-
" */",
|
|
757
|
-
"export interface ReplaceVideoClipSequenceInput {",
|
|
758
|
-
" /**",
|
|
759
|
-
" * The clips being replaced: a contiguous main-track run, listed in timeline order",
|
|
760
|
-
" * @constraint minLength(1)",
|
|
761
|
-
" */",
|
|
762
|
-
" old_clip_ids: string[];",
|
|
763
|
-
" /**",
|
|
764
|
-
" * The replacement clips, in the order they take on the track",
|
|
765
|
-
" * @constraint minLength(1)",
|
|
766
|
-
" */",
|
|
767
|
-
" new_clips: {",
|
|
768
|
-
" /**",
|
|
769
|
-
" * The replacement media asset ID. Omit to create an empty placeholder clip.",
|
|
770
|
-
" * @constraint minLength(1)",
|
|
771
|
-
" */",
|
|
772
|
-
" media_id?: string;",
|
|
773
|
-
" /**",
|
|
774
|
-
" * The source media's intrinsic full length in ms",
|
|
775
|
-
" * @constraint int",
|
|
776
|
-
" * @constraint positive",
|
|
777
|
-
" */",
|
|
778
|
-
" media_duration_ms: number;",
|
|
779
|
-
" /**",
|
|
780
|
-
" * Trim window start in the media (default 0)",
|
|
781
|
-
" * @constraint int",
|
|
782
|
-
" * @constraint min(0)",
|
|
783
|
-
" */",
|
|
784
|
-
" play_in?: number;",
|
|
785
|
-
" /**",
|
|
786
|
-
" * Trim window end in the media (default media_duration_ms)",
|
|
787
|
-
" * @constraint int",
|
|
788
|
-
" * @constraint positive",
|
|
789
|
-
" */",
|
|
790
|
-
" play_out?: number;",
|
|
791
|
-
" }[];",
|
|
792
|
-
" /**",
|
|
793
|
-
" * What happens to speeches anchored to the replaced clips (required — see the policy doc)",
|
|
794
|
-
" */",
|
|
795
|
-
" on_anchored: 'remap' | 'cascade';",
|
|
694
|
+
"export interface DeleteEntityInput {",
|
|
695
|
+
" entity_id: string;",
|
|
796
696
|
"}",
|
|
797
|
-
"",
|
|
798
|
-
"
|
|
799
|
-
" * Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window).",
|
|
800
|
-
" */",
|
|
801
|
-
"export interface AdjustVideoClipDurationInput {",
|
|
802
|
-
" /**",
|
|
803
|
-
" * Video clips with their new trim windows",
|
|
804
|
-
" * @constraint minLength(1)",
|
|
805
|
-
" */",
|
|
806
|
-
" clips: {",
|
|
807
|
-
" /**",
|
|
808
|
-
" * The video clip part ID to re-trim",
|
|
809
|
-
" * @constraint minLength(1)",
|
|
810
|
-
" */",
|
|
811
|
-
" clip_id: string;",
|
|
812
|
-
" /**",
|
|
813
|
-
" * New trim window start in the source media",
|
|
814
|
-
" * @constraint int",
|
|
815
|
-
" * @constraint min(0)",
|
|
816
|
-
" */",
|
|
817
|
-
" play_in: number;",
|
|
818
|
-
" /**",
|
|
819
|
-
" * New trim window end in the source media",
|
|
820
|
-
" * @constraint int",
|
|
821
|
-
" * @constraint positive",
|
|
822
|
-
" */",
|
|
823
|
-
" play_out: number;",
|
|
824
|
-
" }[];",
|
|
697
|
+
"export interface DeleteVoiceoverInput {",
|
|
698
|
+
" readonly voiceoverClipEntityIds: readonly string[];",
|
|
825
699
|
"}",
|
|
826
|
-
"",
|
|
827
|
-
"
|
|
828
|
-
"
|
|
829
|
-
"
|
|
830
|
-
"
|
|
831
|
-
"",
|
|
832
|
-
"
|
|
833
|
-
"
|
|
834
|
-
"
|
|
835
|
-
"
|
|
836
|
-
" /**",
|
|
837
|
-
"
|
|
838
|
-
"
|
|
839
|
-
"
|
|
840
|
-
"
|
|
700
|
+
"export type EmptyRelationKind =",
|
|
701
|
+
" | 'timeline-track'",
|
|
702
|
+
" | 'track-clip'",
|
|
703
|
+
" | 'clip-marker'",
|
|
704
|
+
" | 'marker-content'",
|
|
705
|
+
" | 'axvideo-marker'",
|
|
706
|
+
" | 'marker-timeline';",
|
|
707
|
+
"export interface EntityFacade {",
|
|
708
|
+
" list(): SandboxEntity[];",
|
|
709
|
+
" get(entityId: string): SandboxEntity | null;",
|
|
710
|
+
" /** Return every explicitly imported Asset entity for a Memota asset id. */",
|
|
711
|
+
" findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
|
|
712
|
+
" create(input: CreateEntityInput): string;",
|
|
713
|
+
" /** Replace one Entity's owned payload without changing its identity or kind. */",
|
|
714
|
+
" update(input: UpdateEntityInput): void;",
|
|
715
|
+
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
716
|
+
" delete(input: DeleteEntityInput): void;",
|
|
717
|
+
" /** Import one physical asset without implying a one-to-one media Entity mapping. */",
|
|
718
|
+
" importAsset(input: ImportAssetInput): string;",
|
|
841
719
|
"}",
|
|
842
|
-
"",
|
|
843
|
-
"
|
|
844
|
-
"
|
|
845
|
-
"
|
|
846
|
-
"
|
|
847
|
-
"
|
|
848
|
-
"
|
|
849
|
-
"
|
|
850
|
-
"
|
|
851
|
-
"
|
|
852
|
-
"
|
|
853
|
-
"
|
|
854
|
-
"
|
|
855
|
-
"
|
|
856
|
-
"
|
|
857
|
-
"
|
|
858
|
-
"
|
|
859
|
-
"
|
|
860
|
-
"
|
|
861
|
-
"
|
|
862
|
-
"
|
|
863
|
-
"
|
|
720
|
+
"export type EntityId = string;",
|
|
721
|
+
"export interface EntityPayloadByKind {",
|
|
722
|
+
" axvideo: BoundedDerivedSequencePayload;",
|
|
723
|
+
" timeline: JsonObject;",
|
|
724
|
+
" track: JsonObject & {",
|
|
725
|
+
" hidden?: boolean;",
|
|
726
|
+
" role?: string;",
|
|
727
|
+
" };",
|
|
728
|
+
" clip: JsonObject;",
|
|
729
|
+
" /** Asset-owned metadata. Peer media associations belong in physical-asset Relations. */",
|
|
730
|
+
" asset: JsonObject;",
|
|
731
|
+
" video: BoundedNativeSequencePayload;",
|
|
732
|
+
" audio: BoundedNativeSequencePayload;",
|
|
733
|
+
" voice: BoundedNativeSequencePayload;",
|
|
734
|
+
" image: UnboundedConstantSequencePayload;",
|
|
735
|
+
" 'sequence-marker': JsonObject & {",
|
|
736
|
+
" sourceRange: {",
|
|
737
|
+
" start: number;",
|
|
738
|
+
" end: number;",
|
|
739
|
+
" };",
|
|
740
|
+
" targetRange?: {",
|
|
741
|
+
" start: number;",
|
|
742
|
+
" end: number;",
|
|
743
|
+
" };",
|
|
744
|
+
" duration:",
|
|
745
|
+
" | {",
|
|
746
|
+
" mode: 'from-source';",
|
|
747
|
+
" }",
|
|
748
|
+
" | {",
|
|
749
|
+
" mode: 'fixed';",
|
|
750
|
+
" value: number;",
|
|
751
|
+
" };",
|
|
752
|
+
" timeRemapping?: JsonValue;",
|
|
753
|
+
" anchorOffset?: number;",
|
|
754
|
+
" durationPolicy?: 'timeline';",
|
|
755
|
+
" };",
|
|
756
|
+
" viewport: JsonObject;",
|
|
757
|
+
" 'audio-script': JsonObject & {",
|
|
758
|
+
" segments: ScriptTextSegment[];",
|
|
759
|
+
" };",
|
|
760
|
+
" 'phonetic-script': JsonObject & {",
|
|
761
|
+
" segments: ScriptTextSegment[];",
|
|
762
|
+
" };",
|
|
763
|
+
" caption: BoundedNativeSequencePayload;",
|
|
864
764
|
"}",
|
|
865
|
-
"",
|
|
866
|
-
"
|
|
867
|
-
"
|
|
868
|
-
"
|
|
869
|
-
"export interface ChangeSpeechScriptInput extends SpeechAssets {}",
|
|
870
|
-
"",
|
|
871
|
-
"export interface ChangeSpeechVoiceInput extends SpeechAssets {}",
|
|
872
|
-
"",
|
|
873
|
-
"export interface AdjustSpeechVolumeInput {",
|
|
874
|
-
" /**",
|
|
875
|
-
" * List of speeches with their new volume settings",
|
|
876
|
-
" * @constraint minLength(1)",
|
|
877
|
-
" */",
|
|
878
|
-
" speeches: {",
|
|
879
|
-
" /**",
|
|
880
|
-
" * The speech part ID to adjust volume for",
|
|
881
|
-
" * @constraint minLength(1)",
|
|
882
|
-
" */",
|
|
883
|
-
" speech_id: string;",
|
|
884
|
-
" /**",
|
|
885
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
886
|
-
" * @constraint min(-60)",
|
|
887
|
-
" * @constraint max(20)",
|
|
888
|
-
" */",
|
|
889
|
-
" volume: number;",
|
|
890
|
-
" }[];",
|
|
765
|
+
"export interface EntityStoreSnapshot {",
|
|
766
|
+
" revision: number;",
|
|
767
|
+
" entities: SandboxEntity[];",
|
|
768
|
+
" relations: SandboxRelation[];",
|
|
891
769
|
"}",
|
|
892
|
-
"",
|
|
893
|
-
"
|
|
894
|
-
"
|
|
895
|
-
"
|
|
896
|
-
"export interface SetCaptionVisibilityInput {",
|
|
897
|
-
" /**",
|
|
898
|
-
" * Whether the caption track is hidden",
|
|
899
|
-
" */",
|
|
900
|
-
" is_hidden: boolean;",
|
|
770
|
+
"export interface ImageMediaAssetFact {",
|
|
771
|
+
" readonly assetId: string;",
|
|
772
|
+
" readonly kind: 'image';",
|
|
773
|
+
" readonly storageKey?: string;",
|
|
901
774
|
"}",
|
|
902
|
-
"",
|
|
903
|
-
"
|
|
904
|
-
"
|
|
905
|
-
"
|
|
906
|
-
"export interface SetCaptionStyleInput {",
|
|
907
|
-
" /**",
|
|
908
|
-
" * Font ID referencing a font from the font library",
|
|
909
|
-
" * @constraint minLength(1)",
|
|
910
|
-
" */",
|
|
911
|
-
" font_id?: string;",
|
|
912
|
-
" /**",
|
|
913
|
-
" * Font size in points",
|
|
914
|
-
" * @constraint positive",
|
|
915
|
-
" */",
|
|
916
|
-
" font_size?: number;",
|
|
917
|
-
" /**",
|
|
918
|
-
" * Font color as hex string, e.g. \"#FFFFFF\"",
|
|
919
|
-
" * @constraint minLength(1)",
|
|
920
|
-
" */",
|
|
921
|
-
" font_color?: string;",
|
|
922
|
-
" /**",
|
|
923
|
-
" * Numeric font weight, e.g. 400 or 700",
|
|
924
|
-
" * @constraint int",
|
|
925
|
-
" */",
|
|
926
|
-
" font_weight?: number;",
|
|
927
|
-
" /**",
|
|
928
|
-
" * Entrance animation preset ID, e.g. \"fade\" or \"none\"",
|
|
929
|
-
" */",
|
|
930
|
-
" entrance_animation?: string;",
|
|
931
|
-
" /**",
|
|
932
|
-
" * Entrance animation duration in ms",
|
|
933
|
-
" * @constraint min(0)",
|
|
934
|
-
" */",
|
|
935
|
-
" entrance_animation_duration_ms?: number;",
|
|
936
|
-
" /**",
|
|
937
|
-
" * Outline/stroke color as hex string, e.g. \"#000000\"",
|
|
938
|
-
" * @constraint minLength(1)",
|
|
939
|
-
" */",
|
|
940
|
-
" stroke_color?: string;",
|
|
941
|
-
" /**",
|
|
942
|
-
" * Outline/stroke width in pixels",
|
|
943
|
-
" * @constraint min(0)",
|
|
944
|
-
" */",
|
|
945
|
-
" stroke_width?: number;",
|
|
946
|
-
" /**",
|
|
947
|
-
" * Caption center X as a fraction (0.0 to 1.0)",
|
|
948
|
-
" */",
|
|
949
|
-
" position_x?: number;",
|
|
950
|
-
" /**",
|
|
951
|
-
" * Caption center Y as a fraction (0.0 to 1.0)",
|
|
952
|
-
" */",
|
|
953
|
-
" position_y?: number;",
|
|
775
|
+
"export interface ImportAssetInput {",
|
|
776
|
+
" asset_id: string;",
|
|
777
|
+
" entity_id?: string;",
|
|
778
|
+
" payload?: JsonObject;",
|
|
954
779
|
"}",
|
|
955
|
-
"",
|
|
956
|
-
"
|
|
957
|
-
"
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
"
|
|
961
|
-
"
|
|
962
|
-
"
|
|
963
|
-
" */",
|
|
964
|
-
" bgm_id: string;",
|
|
965
|
-
" /**",
|
|
966
|
-
" * @constraint minLength(1)",
|
|
967
|
-
" */",
|
|
968
|
-
" audio_storage_key: string;",
|
|
969
|
-
" /**",
|
|
970
|
-
" * The media asset ID",
|
|
971
|
-
" * @constraint minLength(1)",
|
|
972
|
-
" */",
|
|
973
|
-
" origin_media_id: string;",
|
|
974
|
-
" /**",
|
|
975
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original, -60 = mute, +20 = max)",
|
|
976
|
-
" * @constraint min(-60)",
|
|
977
|
-
" * @constraint max(20)",
|
|
978
|
-
" */",
|
|
979
|
-
" volume: number;",
|
|
780
|
+
"export interface InsertClipInput {",
|
|
781
|
+
" readonly trackEntityId: string;",
|
|
782
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
783
|
+
" readonly contentEntityId: string;",
|
|
784
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
785
|
+
" readonly duration: SequenceDuration<number>;",
|
|
786
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
787
|
+
" readonly clipPayload?: JsonObject;",
|
|
980
788
|
"}",
|
|
981
|
-
"",
|
|
982
|
-
"
|
|
983
|
-
"
|
|
984
|
-
"
|
|
985
|
-
"
|
|
986
|
-
"
|
|
789
|
+
"export interface InsertMediaClipInput {",
|
|
790
|
+
" readonly timelineEntityId: string;",
|
|
791
|
+
" readonly clipEntityId?: string;",
|
|
792
|
+
" readonly media: VisualMediaAssetFact;",
|
|
793
|
+
" /** Source/display window in whole milliseconds. Images use this as their finite display span. */",
|
|
794
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
795
|
+
" readonly placement: ClipPlacement;",
|
|
796
|
+
" readonly volume?: number;",
|
|
987
797
|
"}",
|
|
988
|
-
"",
|
|
989
|
-
"
|
|
990
|
-
"
|
|
991
|
-
"
|
|
992
|
-
"
|
|
993
|
-
" */",
|
|
994
|
-
" bgm: {",
|
|
995
|
-
" /**",
|
|
996
|
-
" * The bgm part ID to adjust volume for",
|
|
997
|
-
" * @constraint minLength(1)",
|
|
998
|
-
" */",
|
|
999
|
-
" bgm_id: string;",
|
|
1000
|
-
" /**",
|
|
1001
|
-
" * Volume in decibels (-60.0 to 20.0; 0.0 = original)",
|
|
1002
|
-
" * @constraint min(-60)",
|
|
1003
|
-
" * @constraint max(20)",
|
|
1004
|
-
" */",
|
|
1005
|
-
" volume: number;",
|
|
1006
|
-
" }[];",
|
|
798
|
+
"export interface InsertMediaClipsInput {",
|
|
799
|
+
" readonly timelineEntityId: string;",
|
|
800
|
+
" readonly clips: readonly ReplacementMediaClipInput[];",
|
|
801
|
+
" /** One placement decision for the whole input-ordered block. */",
|
|
802
|
+
" readonly insertion: MediaClipInsertion;",
|
|
1007
803
|
"}",
|
|
1008
|
-
"",
|
|
1009
|
-
"
|
|
1010
|
-
"
|
|
1011
|
-
"
|
|
1012
|
-
"
|
|
1013
|
-
"
|
|
1014
|
-
"
|
|
1015
|
-
" /**
|
|
1016
|
-
"
|
|
1017
|
-
" adjustVideoClipVolume(input: AdjustVideoClipVolumeInput): Promise<void>;",
|
|
1018
|
-
" /** Set the playback speed of existing video clips. */",
|
|
1019
|
-
" setVideoClipSpeedShift(input: SetVideoClipSpeedShiftInput): Promise<void>;",
|
|
1020
|
-
" /** Replace the media backing existing video clips. */",
|
|
1021
|
-
" replaceVideoClipContent(input: ReplaceVideoClipContentInput): Promise<void>;",
|
|
1022
|
-
" /** Replace a contiguous run of main-track clips with a new run. */",
|
|
1023
|
-
" replaceVideoClipSequence(input: ReplaceVideoClipSequenceInput): Promise<void>;",
|
|
1024
|
-
" /** Re-trim existing video clips (the user-facing \"adjust duration\" gesture is a trim of the source window). */",
|
|
1025
|
-
" adjustVideoClipDuration(input: AdjustVideoClipDurationInput): Promise<void>;",
|
|
1026
|
-
" /** Add speeches (and their captions). */",
|
|
1027
|
-
" addSpeeches(input: AddSpeechesInput): Promise<void>;",
|
|
1028
|
-
" /** Delete speeches with their captions. */",
|
|
1029
|
-
" deleteSpeeches(input: DeleteSpeechesInput): Promise<void>;",
|
|
1030
|
-
" /** Move speeches in time. */",
|
|
1031
|
-
" moveSpeeches(input: MoveSpeechesInput): Promise<void>;",
|
|
1032
|
-
" /** Change a speech's script or voice. */",
|
|
1033
|
-
" changeSpeechScript(input: ChangeSpeechScriptInput): Promise<void>;",
|
|
1034
|
-
" changeSpeechVoice(input: ChangeSpeechVoiceInput): Promise<void>;",
|
|
1035
|
-
" adjustSpeechVolume(input: AdjustSpeechVolumeInput): Promise<void>;",
|
|
1036
|
-
" /** Toggle caption visibility (the caption track's `is_hidden` flag). */",
|
|
1037
|
-
" setCaptionVisibility(input: SetCaptionVisibilityInput): Promise<void>;",
|
|
1038
|
-
" /** Set the caption visual style. */",
|
|
1039
|
-
" setCaptionStyle(input: SetCaptionStyleInput): Promise<void>;",
|
|
1040
|
-
" /** Set the document BGM. */",
|
|
1041
|
-
" setBgm(input: SetBgmInput): Promise<void>;",
|
|
1042
|
-
" /** Remove the document BGM. */",
|
|
1043
|
-
" deleteBgm(input: DeleteBgmInput): Promise<void>;",
|
|
1044
|
-
" adjustBgmVolume(input: AdjustBgmVolumeInput): Promise<void>;",
|
|
804
|
+
"export interface InsertPlacedClipInput {",
|
|
805
|
+
" readonly trackEntityId: string;",
|
|
806
|
+
" readonly contentEntityId: string;",
|
|
807
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
808
|
+
" readonly duration: SequenceDuration<number>;",
|
|
809
|
+
" readonly placement: ClipPlacement;",
|
|
810
|
+
" readonly clipPayload?: JsonObject;",
|
|
811
|
+
" /** Stable caller-owned placement identity, when one already exists outside the graph. */",
|
|
812
|
+
" readonly clipEntityId?: string;",
|
|
1045
813
|
"}",
|
|
1046
|
-
"",
|
|
1047
|
-
"export type JsonPrimitive = string | number | boolean | null;",
|
|
1048
|
-
"export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
|
|
1049
814
|
"export interface JsonObject {",
|
|
1050
815
|
" [key: string]: JsonValue;",
|
|
1051
816
|
"}",
|
|
1052
|
-
"",
|
|
817
|
+
"export type JsonPrimitive = string | number | boolean | null;",
|
|
818
|
+
"export type JsonValue = JsonPrimitive | JsonObject | JsonValue[];",
|
|
1053
819
|
"export type KnownEntityKind =",
|
|
1054
820
|
" | 'axvideo'",
|
|
1055
821
|
" | 'timeline'",
|
|
@@ -1065,7 +831,6 @@ const EDIT_SANDBOX_API_DTS = [
|
|
|
1065
831
|
" | 'audio-script'",
|
|
1066
832
|
" | 'phonetic-script'",
|
|
1067
833
|
" | 'caption';",
|
|
1068
|
-
"",
|
|
1069
834
|
"export type KnownRelationKind =",
|
|
1070
835
|
" | 'timeline-track'",
|
|
1071
836
|
" | 'track-clip'",
|
|
@@ -1077,72 +842,146 @@ const EDIT_SANDBOX_API_DTS = [
|
|
|
1077
842
|
" | 'generated'",
|
|
1078
843
|
" | 'phonetic-script-provenance'",
|
|
1079
844
|
" | 'caption-provenance'",
|
|
1080
|
-
" | 'caption-alignment'
|
|
1081
|
-
"",
|
|
1082
|
-
"
|
|
1083
|
-
"
|
|
1084
|
-
"
|
|
1085
|
-
"
|
|
1086
|
-
"
|
|
845
|
+
" | 'caption-alignment'",
|
|
846
|
+
" | 'clip-anchor'",
|
|
847
|
+
" | 'audio-script-render';",
|
|
848
|
+
"export interface LinearClipSpeed {",
|
|
849
|
+
" readonly kind: 'linear';",
|
|
850
|
+
" readonly rate: number;",
|
|
851
|
+
" readonly mode?: string;",
|
|
1087
852
|
"}",
|
|
1088
|
-
"export interface
|
|
1089
|
-
"
|
|
1090
|
-
"
|
|
1091
|
-
"
|
|
853
|
+
"export interface LinkAudioScriptRenderRelationInput {",
|
|
854
|
+
" relation_id?: string;",
|
|
855
|
+
" output_entity_id: string;",
|
|
856
|
+
" script_entity_id: string;",
|
|
857
|
+
" trace?: JsonObject;",
|
|
1092
858
|
"}",
|
|
1093
|
-
"export interface
|
|
1094
|
-
"
|
|
1095
|
-
"
|
|
1096
|
-
"
|
|
859
|
+
"export interface LinkClipAnchorRelationInput {",
|
|
860
|
+
" relation_id?: string;",
|
|
861
|
+
" child_clip_entity_id: string;",
|
|
862
|
+
" host_clip_entity_id: string;",
|
|
863
|
+
" trace?: JsonObject;",
|
|
1097
864
|
"}",
|
|
1098
|
-
"export
|
|
1099
|
-
"
|
|
1100
|
-
"
|
|
1101
|
-
"
|
|
1102
|
-
"
|
|
1103
|
-
"",
|
|
1104
|
-
"export interface EntityPayloadByKind {",
|
|
1105
|
-
" axvideo: BoundedDerivedSequencePayload;",
|
|
1106
|
-
" timeline: JsonObject;",
|
|
1107
|
-
" track: JsonObject & { hidden?: boolean; role?: string };",
|
|
1108
|
-
" clip: JsonObject;",
|
|
1109
|
-
" asset: JsonObject;",
|
|
1110
|
-
" video: BoundedNativeSequencePayload;",
|
|
1111
|
-
" audio: BoundedNativeSequencePayload;",
|
|
1112
|
-
" voice: BoundedNativeSequencePayload;",
|
|
1113
|
-
" image: UnboundedConstantSequencePayload;",
|
|
1114
|
-
" 'sequence-marker': JsonObject & {",
|
|
1115
|
-
" sourceRange: { start: number; end: number };",
|
|
1116
|
-
" targetRange?: { start: number; end: number };",
|
|
1117
|
-
" duration: { mode: 'from-source' } | { mode: 'fixed'; value: number };",
|
|
1118
|
-
" timeRemapping?: JsonValue;",
|
|
1119
|
-
" };",
|
|
1120
|
-
" viewport: JsonObject;",
|
|
1121
|
-
" 'audio-script': JsonObject & { segments: ScriptTextSegment[] };",
|
|
1122
|
-
" 'phonetic-script': JsonObject & { segments: ScriptTextSegment[] };",
|
|
1123
|
-
" caption: BoundedNativeSequencePayload;",
|
|
865
|
+
"export interface LinkGeneratedRelationInput {",
|
|
866
|
+
" relation_id?: string;",
|
|
867
|
+
" output_entity_id: string;",
|
|
868
|
+
" input_entity_id: string;",
|
|
869
|
+
" trace?: JsonObject;",
|
|
1124
870
|
"}",
|
|
1125
|
-
"",
|
|
1126
|
-
"
|
|
1127
|
-
"
|
|
1128
|
-
"
|
|
1129
|
-
"
|
|
1130
|
-
"
|
|
1131
|
-
"
|
|
1132
|
-
"
|
|
1133
|
-
"",
|
|
1134
|
-
"
|
|
1135
|
-
"
|
|
1136
|
-
"
|
|
1137
|
-
"
|
|
871
|
+
"interface LinkRelationBase {",
|
|
872
|
+
" relation_id?: string;",
|
|
873
|
+
" endpoint_0_entity_id: string;",
|
|
874
|
+
" endpoint_1_entity_id: string;",
|
|
875
|
+
" trace?: JsonObject;",
|
|
876
|
+
"}",
|
|
877
|
+
"export type LinkRelationInput =",
|
|
878
|
+
" | (LinkRelationBase & {",
|
|
879
|
+
" relation_kind: EmptyRelationKind;",
|
|
880
|
+
" metadata?: {",
|
|
881
|
+
" [key: string]: never;",
|
|
882
|
+
" };",
|
|
883
|
+
" })",
|
|
884
|
+
" | (LinkRelationBase & {",
|
|
885
|
+
" relation_kind: 'physical-asset';",
|
|
886
|
+
" metadata?: JsonObject;",
|
|
887
|
+
" })",
|
|
888
|
+
" | (LinkRelationBase & {",
|
|
889
|
+
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
890
|
+
" metadata: JsonObject & {",
|
|
891
|
+
" segmentAlignment: JsonValue;",
|
|
892
|
+
" };",
|
|
893
|
+
" })",
|
|
894
|
+
" | (LinkRelationBase & {",
|
|
895
|
+
" relation_kind: 'caption-alignment';",
|
|
896
|
+
" metadata: JsonObject & {",
|
|
897
|
+
" alignment: JsonValue;",
|
|
898
|
+
" };",
|
|
899
|
+
" });",
|
|
900
|
+
"export type MediaClipInsertion =",
|
|
901
|
+
" | {",
|
|
902
|
+
" readonly kind: 'before';",
|
|
903
|
+
" readonly clipEntityId: string;",
|
|
904
|
+
" }",
|
|
905
|
+
" | {",
|
|
906
|
+
" readonly kind: 'after';",
|
|
907
|
+
" readonly clipEntityId: string;",
|
|
908
|
+
" }",
|
|
909
|
+
" | {",
|
|
910
|
+
" readonly kind: 'firstStart';",
|
|
911
|
+
" readonly startMs: number;",
|
|
912
|
+
" };",
|
|
913
|
+
"export interface MoveClipInput {",
|
|
914
|
+
" readonly clipEntityId: string;",
|
|
915
|
+
" readonly trackEntityId: string;",
|
|
916
|
+
"}",
|
|
917
|
+
"export interface MoveClipsToStartsInput {",
|
|
918
|
+
" readonly moves: readonly {",
|
|
919
|
+
" readonly clipEntityId: string;",
|
|
920
|
+
" readonly newStartMs: number;",
|
|
921
|
+
" }[];",
|
|
922
|
+
" /** Absolute-time drags preserve every voiceover's current visible landing. */",
|
|
923
|
+
" readonly onAnchored: 'keepAbsolute';",
|
|
924
|
+
"}",
|
|
925
|
+
"export interface MoveSequentialClipsInput {",
|
|
926
|
+
" readonly clipEntityIds: readonly string[];",
|
|
927
|
+
" readonly anchor: SequentialClipAnchor;",
|
|
928
|
+
" readonly onAnchored: 'follow' | 'keepAbsolute';",
|
|
929
|
+
"}",
|
|
930
|
+
"export interface MoveVoiceoverInput {",
|
|
931
|
+
" readonly voiceoverClipEntityId: string;",
|
|
932
|
+
" /** Absolute requested timeline start; MEngine resolves and persists the host relation. */",
|
|
933
|
+
" readonly newStartMs: number;",
|
|
934
|
+
"}",
|
|
935
|
+
"export interface PatchCaptionStyleInput {",
|
|
936
|
+
" readonly timelineEntityId: string;",
|
|
937
|
+
" readonly style: CaptionStyleFields;",
|
|
938
|
+
"}",
|
|
939
|
+
"export interface RelationFacade {",
|
|
940
|
+
" list(): SandboxRelation[];",
|
|
941
|
+
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
942
|
+
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
943
|
+
" /** For physical-asset use sequence media as endpoint 0 and Asset as endpoint 1. */",
|
|
944
|
+
" link(input: LinkRelationInput): string;",
|
|
945
|
+
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
946
|
+
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
947
|
+
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
948
|
+
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
949
|
+
" /** Author ordered audio-script-render(output,script) without positional endpoint ambiguity. */",
|
|
950
|
+
" linkAudioScriptRender(input: LinkAudioScriptRenderRelationInput): string;",
|
|
951
|
+
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
952
|
+
" unlink(input: UnlinkRelationInput): void;",
|
|
953
|
+
"}",
|
|
954
|
+
"export interface ReplaceClipContentInput {",
|
|
955
|
+
" readonly clipEntityId: string;",
|
|
956
|
+
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
957
|
+
" readonly contentEntityId: string;",
|
|
958
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
959
|
+
" readonly duration: SequenceDuration<number>;",
|
|
960
|
+
" readonly targetRange?: SequenceRange<number>;",
|
|
961
|
+
" readonly timeRemapping?: JsonValue;",
|
|
962
|
+
"}",
|
|
963
|
+
"export interface ReplaceMediaClipInput {",
|
|
964
|
+
" readonly clipEntityId: string;",
|
|
965
|
+
" readonly media: VisualMediaAssetFact;",
|
|
966
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
967
|
+
"}",
|
|
968
|
+
"export interface ReplaceSequentialClipsInput {",
|
|
969
|
+
" readonly timelineEntityId: string;",
|
|
970
|
+
" readonly oldClipEntityIds: readonly string[];",
|
|
971
|
+
" readonly newClips: readonly ReplacementMediaClipInput[];",
|
|
972
|
+
" readonly onAnchored: 'remap' | 'cascade';",
|
|
973
|
+
"}",
|
|
974
|
+
"export interface ReplacementMediaClipInput {",
|
|
975
|
+
" readonly clipEntityId?: string;",
|
|
976
|
+
" readonly media: VisualMediaAssetFact;",
|
|
977
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
978
|
+
" readonly volume?: number;",
|
|
1138
979
|
"}",
|
|
1139
|
-
"",
|
|
1140
980
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
1141
981
|
" entity_id: string;",
|
|
1142
982
|
" entity_kind: K;",
|
|
1143
983
|
" payload: EntityPayloadByKind[K];",
|
|
1144
984
|
"}",
|
|
1145
|
-
"",
|
|
1146
985
|
"export interface SandboxRelation {",
|
|
1147
986
|
" relation_id: string;",
|
|
1148
987
|
" relation_kind: KnownRelationKind;",
|
|
@@ -1151,155 +990,201 @@ const EDIT_SANDBOX_API_DTS = [
|
|
|
1151
990
|
" metadata: JsonObject;",
|
|
1152
991
|
" trace: JsonObject;",
|
|
1153
992
|
"}",
|
|
1154
|
-
"",
|
|
1155
|
-
"
|
|
1156
|
-
"
|
|
1157
|
-
"
|
|
1158
|
-
"
|
|
1159
|
-
"
|
|
1160
|
-
" | 'axvideo-marker'",
|
|
1161
|
-
" | 'marker-timeline';",
|
|
1162
|
-
"export type LinkRelationInput =",
|
|
993
|
+
"export type ScriptTextSegment = JsonObject & {",
|
|
994
|
+
" segmentId: string;",
|
|
995
|
+
" text: string;",
|
|
996
|
+
" language?: string;",
|
|
997
|
+
"};",
|
|
998
|
+
"export type SequenceDuration<Span = unknown> =",
|
|
1163
999
|
" | {",
|
|
1164
|
-
"
|
|
1165
|
-
" relation_kind: EmptyRelationKind;",
|
|
1166
|
-
" endpoint_0_entity_id: string;",
|
|
1167
|
-
" endpoint_1_entity_id: string;",
|
|
1168
|
-
" metadata?: { [key: string]: never };",
|
|
1169
|
-
" trace?: JsonObject;",
|
|
1000
|
+
" readonly mode: 'from-source';",
|
|
1170
1001
|
" }",
|
|
1171
1002
|
" | {",
|
|
1172
|
-
"
|
|
1173
|
-
"
|
|
1174
|
-
"
|
|
1175
|
-
"
|
|
1176
|
-
"
|
|
1177
|
-
"
|
|
1178
|
-
"
|
|
1179
|
-
"
|
|
1003
|
+
" readonly mode: 'fixed';",
|
|
1004
|
+
" readonly value: Span;",
|
|
1005
|
+
" };",
|
|
1006
|
+
"export interface SequenceRange<Point = unknown> {",
|
|
1007
|
+
" readonly start: Point;",
|
|
1008
|
+
" readonly end: Point;",
|
|
1009
|
+
"}",
|
|
1010
|
+
"export type SequentialClipAnchor =",
|
|
1180
1011
|
" | {",
|
|
1181
|
-
"
|
|
1182
|
-
"
|
|
1183
|
-
" endpoint_0_entity_id: string;",
|
|
1184
|
-
" endpoint_1_entity_id: string;",
|
|
1185
|
-
" metadata: JsonObject & { segmentAlignment: JsonValue };",
|
|
1186
|
-
" trace?: JsonObject;",
|
|
1012
|
+
" readonly position: 'before' | 'after';",
|
|
1013
|
+
" readonly clipEntityId: string;",
|
|
1187
1014
|
" }",
|
|
1188
1015
|
" | {",
|
|
1189
|
-
"
|
|
1190
|
-
" relation_kind: 'caption-alignment';",
|
|
1191
|
-
" endpoint_0_entity_id: string;",
|
|
1192
|
-
" endpoint_1_entity_id: string;",
|
|
1193
|
-
" metadata: JsonObject & { alignment: JsonValue };",
|
|
1194
|
-
" trace?: JsonObject;",
|
|
1016
|
+
" readonly position: 'trackStart';",
|
|
1195
1017
|
" };",
|
|
1196
|
-
"",
|
|
1197
|
-
"
|
|
1198
|
-
"
|
|
1199
|
-
"
|
|
1200
|
-
"
|
|
1201
|
-
" /** Input media Entity used to generate the output; persisted as endpoint 1. */",
|
|
1202
|
-
" input_entity_id: string;",
|
|
1203
|
-
" trace?: JsonObject;",
|
|
1018
|
+
"export interface SetBgmInput {",
|
|
1019
|
+
" readonly timelineEntityId: string;",
|
|
1020
|
+
" readonly bgmClipEntityId: string;",
|
|
1021
|
+
" readonly media: AudioMediaAssetFact;",
|
|
1022
|
+
" readonly volume: number;",
|
|
1204
1023
|
"}",
|
|
1205
|
-
"",
|
|
1206
|
-
"
|
|
1207
|
-
"
|
|
1208
|
-
" list(): SandboxEntity[];",
|
|
1209
|
-
" get(entityId: string): SandboxEntity | null;",
|
|
1210
|
-
" /** Call before importAsset; inspect every match and decide whether to reuse one. */",
|
|
1211
|
-
" findByAssetId(assetId: string): SandboxEntity<'asset'>[];",
|
|
1212
|
-
" create(input: CreateEntityInput): string;",
|
|
1213
|
-
" /** Create only an Asset Entity when no existing match should be reused; this does not infer media. */",
|
|
1214
|
-
" importAsset(input: ImportAssetInput): string;",
|
|
1024
|
+
"export interface SetCaptionVisibilityInput {",
|
|
1025
|
+
" readonly timelineEntityId: string;",
|
|
1026
|
+
" readonly hidden: boolean;",
|
|
1215
1027
|
"}",
|
|
1216
|
-
"",
|
|
1217
|
-
"
|
|
1218
|
-
"
|
|
1219
|
-
" list(): SandboxRelation[];",
|
|
1220
|
-
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
1221
|
-
" link(input: LinkRelationInput): string;",
|
|
1222
|
-
" /** Author ordered generated(output,input). */",
|
|
1223
|
-
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
1028
|
+
"export interface SetClipPlacementInput {",
|
|
1029
|
+
" readonly clipEntityId: string;",
|
|
1030
|
+
" readonly placement: ClipPlacement;",
|
|
1224
1031
|
"}",
|
|
1225
|
-
"",
|
|
1226
|
-
"
|
|
1227
|
-
"
|
|
1228
|
-
" id: string;",
|
|
1229
|
-
" start_ms: number;",
|
|
1230
|
-
" end_ms: number;",
|
|
1231
|
-
" duration_ms: number;",
|
|
1232
|
-
" speed_shift: unknown;",
|
|
1233
|
-
" volume: number | undefined;",
|
|
1234
|
-
" media_id: string | undefined;",
|
|
1032
|
+
"export interface SetClipSpeedInput {",
|
|
1033
|
+
" readonly clipEntityId: string;",
|
|
1034
|
+
" readonly timeRemapping: LinearClipSpeed | null;",
|
|
1235
1035
|
"}",
|
|
1236
|
-
"",
|
|
1237
|
-
"
|
|
1238
|
-
"
|
|
1239
|
-
"
|
|
1240
|
-
"
|
|
1241
|
-
"
|
|
1242
|
-
"
|
|
1243
|
-
"
|
|
1244
|
-
"
|
|
1245
|
-
"
|
|
1036
|
+
"export interface SetClipVolumeInput {",
|
|
1037
|
+
" readonly clipEntityId: string;",
|
|
1038
|
+
" /** Playback gain in decibels. */",
|
|
1039
|
+
" readonly volume: number;",
|
|
1040
|
+
"}",
|
|
1041
|
+
"export interface TrimClipInput {",
|
|
1042
|
+
" readonly clipEntityId: string;",
|
|
1043
|
+
" readonly sourceRange: SequenceRange<number>;",
|
|
1044
|
+
"}",
|
|
1045
|
+
"export interface UnboundedConstantSequencePayload extends JsonObject {",
|
|
1046
|
+
" extent: {",
|
|
1047
|
+
" kind: 'unbounded';",
|
|
1048
|
+
" start: number;",
|
|
1049
|
+
" };",
|
|
1050
|
+
" sampling: 'constant';",
|
|
1051
|
+
" coordinateSpace: JsonValue;",
|
|
1052
|
+
"}",
|
|
1053
|
+
"export interface UnlinkRelationInput {",
|
|
1054
|
+
" relation_id: string;",
|
|
1055
|
+
"}",
|
|
1056
|
+
"export interface UpdateClipInput {",
|
|
1057
|
+
" readonly clipEntityId: string;",
|
|
1058
|
+
" /** Complete replacement for the Clip-owned payload. */",
|
|
1059
|
+
" readonly payload: JsonObject;",
|
|
1060
|
+
"}",
|
|
1061
|
+
"export interface UpdateClipMarkerInput {",
|
|
1062
|
+
" readonly clipEntityId: string;",
|
|
1063
|
+
" readonly sourceRange?: SequenceRange<number>;",
|
|
1064
|
+
" /** Passing `undefined` explicitly removes the optional target range. */",
|
|
1065
|
+
" readonly targetRange?: SequenceRange<number> | undefined;",
|
|
1066
|
+
" readonly duration?: SequenceDuration<number>;",
|
|
1067
|
+
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
1068
|
+
" readonly timeRemapping?: JsonValue | undefined;",
|
|
1069
|
+
"}",
|
|
1070
|
+
"export interface UpdateEntityInput {",
|
|
1071
|
+
" entity_id: string;",
|
|
1072
|
+
" payload: JsonObject;",
|
|
1073
|
+
"}",
|
|
1074
|
+
"export interface VideoMediaAssetFact {",
|
|
1075
|
+
" readonly assetId: string;",
|
|
1076
|
+
" readonly kind: 'video';",
|
|
1077
|
+
" readonly durationMs: number;",
|
|
1078
|
+
" readonly storageKey?: string;",
|
|
1079
|
+
"}",
|
|
1080
|
+
"export type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;",
|
|
1081
|
+
"export interface VoiceDescriptor {",
|
|
1082
|
+
" readonly system: 'voice-library';",
|
|
1083
|
+
" readonly key: string;",
|
|
1084
|
+
" readonly name?: string;",
|
|
1085
|
+
"}",
|
|
1086
|
+
"export interface VoiceMediaAssetFact {",
|
|
1087
|
+
" /** Stable external speech result id, independent of the placed Clip id. */",
|
|
1088
|
+
" readonly assetId: string;",
|
|
1089
|
+
" readonly kind: 'voice';",
|
|
1090
|
+
" readonly durationMs: number;",
|
|
1091
|
+
" readonly storageKey: string;",
|
|
1092
|
+
" readonly voice: VoiceDescriptor;",
|
|
1093
|
+
"}",
|
|
1094
|
+
"export interface VoiceoverCaptionFact {",
|
|
1095
|
+
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
1096
|
+
" readonly captionClipEntityId: string;",
|
|
1097
|
+
" readonly text: string;",
|
|
1098
|
+
" readonly startMs: number;",
|
|
1099
|
+
" readonly durationMs: number;",
|
|
1100
|
+
" readonly style?: CaptionStyleFields;",
|
|
1101
|
+
"}",
|
|
1102
|
+
"export interface VoiceoverTakeInput {",
|
|
1103
|
+
" readonly timelineEntityId: string;",
|
|
1104
|
+
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
1105
|
+
" readonly voiceoverClipEntityId: string;",
|
|
1106
|
+
" readonly hostClipEntityId: string;",
|
|
1107
|
+
" readonly anchorOffset: number;",
|
|
1108
|
+
" readonly media: VoiceMediaAssetFact;",
|
|
1109
|
+
" /** Complete spoken text; the editor owns the deterministic local script segment identity. */",
|
|
1110
|
+
" readonly scriptText: string;",
|
|
1111
|
+
" readonly volume: number;",
|
|
1112
|
+
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
1113
|
+
"}",
|
|
1114
|
+
"export interface VoiceoverTakeResult {",
|
|
1115
|
+
" readonly voiceoverClipEntityId: string;",
|
|
1116
|
+
" readonly voiceEntityId: string;",
|
|
1117
|
+
" readonly audioScriptEntityId: string;",
|
|
1118
|
+
" readonly captionClipEntityIds: readonly string[];",
|
|
1119
|
+
"}",
|
|
1120
|
+
"/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
|
|
1121
|
+
"export interface EditApi {",
|
|
1122
|
+
" insertClip(input: InsertClipInput): ClipEntityId;",
|
|
1123
|
+
" insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
|
|
1124
|
+
" updateClipMarker(input: UpdateClipMarkerInput): void;",
|
|
1125
|
+
" setClipPlacement(input: SetClipPlacementInput): void;",
|
|
1126
|
+
" moveSequentialClips(input: MoveSequentialClipsInput): void;",
|
|
1127
|
+
" moveClip(input: MoveClipInput): void;",
|
|
1128
|
+
" replaceClipContent(input: ReplaceClipContentInput): void;",
|
|
1129
|
+
" insertMediaClip(input: InsertMediaClipInput): ClipEntityId;",
|
|
1130
|
+
" insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];",
|
|
1131
|
+
" replaceMediaClip(input: ReplaceMediaClipInput): void;",
|
|
1132
|
+
" setClipVolume(input: SetClipVolumeInput): void;",
|
|
1133
|
+
" setClipSpeed(input: SetClipSpeedInput): void;",
|
|
1134
|
+
" trimClip(input: TrimClipInput): void;",
|
|
1135
|
+
" replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];",
|
|
1136
|
+
" deleteClip(input: DeleteClipInput): void;",
|
|
1137
|
+
" deleteClipTree(input: DeleteClipTreeInput): void;",
|
|
1138
|
+
" updateClip(input: UpdateClipInput): void;",
|
|
1139
|
+
" upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;",
|
|
1140
|
+
" moveVoiceover(input: MoveVoiceoverInput): void;",
|
|
1141
|
+
" moveClipsToStarts(input: MoveClipsToStartsInput): void;",
|
|
1142
|
+
" deleteVoiceover(input: DeleteVoiceoverInput): void;",
|
|
1143
|
+
" setBgm(input: SetBgmInput): ClipEntityId;",
|
|
1144
|
+
" deleteBgm(input: DeleteBgmInput): void;",
|
|
1145
|
+
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
1146
|
+
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1246
1147
|
"}",
|
|
1247
|
-
"",
|
|
1248
|
-
"/** Opaque VideoDraft projection (full IDL lives in host document types). */",
|
|
1249
|
-
"export type VideoDraftProjection = {",
|
|
1250
|
-
" readonly timeline?: { readonly duration_ms?: number };",
|
|
1251
|
-
" readonly [key: string]: unknown;",
|
|
1252
|
-
"};",
|
|
1253
|
-
"",
|
|
1254
|
-
"/** Agent read surface over the forked document. */",
|
|
1255
1148
|
"export interface TimelineApi {",
|
|
1256
|
-
"
|
|
1257
|
-
" snapshot(): VideoDraftProjection;",
|
|
1258
|
-
" /** Clips whose midpoint falls in `[startMs, endMs)`. */",
|
|
1259
|
-
" clipsInRange(startMs: number, endMs: number): TimelineClipDescriptor[];",
|
|
1260
|
-
" /** Look up a part by id, or null if missing. */",
|
|
1261
|
-
" part(id: string): TimelinePartDescriptor | null;",
|
|
1149
|
+
" snapshot(): EntityStoreSnapshot;",
|
|
1262
1150
|
"}",
|
|
1263
|
-
"",
|
|
1264
|
-
"/** Opaque checkpoint handle for rollback. */",
|
|
1265
1151
|
"export interface SandboxCheckpoint {",
|
|
1266
1152
|
" readonly index: number;",
|
|
1267
1153
|
"}",
|
|
1268
|
-
"",
|
|
1269
1154
|
"export declare const edit: EditApi;",
|
|
1270
1155
|
"export declare const timeline: TimelineApi;",
|
|
1271
|
-
"export declare const entities:
|
|
1272
|
-
"export declare const relations:
|
|
1273
|
-
"",
|
|
1274
|
-
"/** Capture a rollback point. */",
|
|
1156
|
+
"export declare const entities: EntityFacade;",
|
|
1157
|
+
"export declare const relations: RelationFacade;",
|
|
1275
1158
|
"export declare function checkpoint(): SandboxCheckpoint;",
|
|
1276
|
-
"/** Roll the sandbox document back to a prior checkpoint. */",
|
|
1277
1159
|
"export declare function rollbackTo(cp: SandboxCheckpoint): void;",
|
|
1278
|
-
"/** Host-injected, pre-materialized facts. Validate each field before use. */",
|
|
1279
1160
|
"export declare const inputs: Readonly<Record<string, unknown>>;",
|
|
1280
1161
|
""
|
|
1281
1162
|
].join("\n");
|
|
1282
1163
|
//#endregion
|
|
1283
1164
|
//#region src/prompt.ts
|
|
1284
1165
|
const MEDEO_TOOL_DESCRIPTION = `
|
|
1285
|
-
Edit
|
|
1166
|
+
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.
|
|
1286
1167
|
|
|
1287
1168
|
Operations:
|
|
1288
|
-
- snapshot: return the
|
|
1289
|
-
-
|
|
1290
|
-
-
|
|
1169
|
+
- snapshot: return the Entity/Relation state summary and opaque base version.
|
|
1170
|
+
- 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.
|
|
1171
|
+
- 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.
|
|
1172
|
+
- 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.
|
|
1291
1173
|
|
|
1292
1174
|
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.
|
|
1293
1175
|
|
|
1294
|
-
|
|
1176
|
+
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. Generation lineage is program-synced: after each successful commit the tool connects existing media Entities 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 Entities the edit itself legitimately needs (for example placing a recalled input asset) are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
1295
1177
|
`.trim();
|
|
1296
1178
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1297
1179
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1298
|
-
|
|
1299
|
-
Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1180
|
+
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.
|
|
1181
|
+
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.
|
|
1300
1182
|
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.
|
|
1301
1183
|
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.
|
|
1302
|
-
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id.
|
|
1184
|
+
For physical-asset authoring, use sequence media as endpoint_0_entity_id and Asset as endpoint_1_entity_id. Generated lineage Relations are not model-authored: the tool derives them from host generation facts after a successful commit, so do not call relations.linkGenerated to record provenance. relations.of remains endpoint-agnostic for lookup.
|
|
1185
|
+
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.
|
|
1186
|
+
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.
|
|
1187
|
+
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.
|
|
1303
1188
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1304
1189
|
`.trim();
|
|
1305
1190
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -1318,15 +1203,51 @@ When updated_since_previous_model_call is true, the document changed after the p
|
|
|
1318
1203
|
|
|
1319
1204
|
Sandbox TypeScript interface:
|
|
1320
1205
|
\`\`\`ts
|
|
1321
|
-
${
|
|
1206
|
+
${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
1322
1207
|
\`\`\`
|
|
1323
1208
|
`.trim();
|
|
1324
1209
|
}
|
|
1325
1210
|
//#endregion
|
|
1326
1211
|
//#region src/schema.ts
|
|
1327
1212
|
const MEDEO_TOOL_NAME = "medeo";
|
|
1213
|
+
const assetFactProperties = {
|
|
1214
|
+
assetId: {
|
|
1215
|
+
type: "string",
|
|
1216
|
+
minLength: 1
|
|
1217
|
+
},
|
|
1218
|
+
kind: {
|
|
1219
|
+
type: "string",
|
|
1220
|
+
enum: [
|
|
1221
|
+
"image",
|
|
1222
|
+
"video",
|
|
1223
|
+
"audio",
|
|
1224
|
+
"voice"
|
|
1225
|
+
]
|
|
1226
|
+
},
|
|
1227
|
+
durationMs: {
|
|
1228
|
+
type: "integer",
|
|
1229
|
+
minimum: 1
|
|
1230
|
+
},
|
|
1231
|
+
storageKey: {
|
|
1232
|
+
type: "string",
|
|
1233
|
+
minLength: 1
|
|
1234
|
+
},
|
|
1235
|
+
voice: {
|
|
1236
|
+
type: "object",
|
|
1237
|
+
additionalProperties: false,
|
|
1238
|
+
required: ["system", "key"],
|
|
1239
|
+
properties: {
|
|
1240
|
+
system: { const: "voice-library" },
|
|
1241
|
+
key: {
|
|
1242
|
+
type: "string",
|
|
1243
|
+
minLength: 1
|
|
1244
|
+
},
|
|
1245
|
+
name: { type: "string" }
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1328
1249
|
/**
|
|
1329
|
-
* JSON Schema for the host-facing
|
|
1250
|
+
* JSON Schema for the host-facing `medeo` tool surface.
|
|
1330
1251
|
*
|
|
1331
1252
|
* The schema intentionally does not return or accept the full op journal:
|
|
1332
1253
|
* journals stay in the tool process and are referenced by `plan_id`. This keeps
|
|
@@ -1342,6 +1263,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1342
1263
|
type: "string",
|
|
1343
1264
|
enum: [
|
|
1344
1265
|
"snapshot",
|
|
1266
|
+
"migrate-legacy",
|
|
1345
1267
|
"run-edit-script",
|
|
1346
1268
|
"commit-plan"
|
|
1347
1269
|
],
|
|
@@ -1355,11 +1277,73 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1355
1277
|
script: {
|
|
1356
1278
|
type: "string",
|
|
1357
1279
|
minLength: 1,
|
|
1358
|
-
description: "JavaScript body for run-edit-script.
|
|
1280
|
+
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."
|
|
1359
1281
|
},
|
|
1360
1282
|
inputs: {
|
|
1361
1283
|
type: "object",
|
|
1362
|
-
description: "Pre-materialized, side-effect-free values passed into the script, including recalled
|
|
1284
|
+
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."
|
|
1285
|
+
},
|
|
1286
|
+
asset_facts: {
|
|
1287
|
+
type: "array",
|
|
1288
|
+
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.",
|
|
1289
|
+
items: { oneOf: [
|
|
1290
|
+
{
|
|
1291
|
+
type: "object",
|
|
1292
|
+
additionalProperties: false,
|
|
1293
|
+
required: ["assetId", "kind"],
|
|
1294
|
+
properties: {
|
|
1295
|
+
assetId: assetFactProperties.assetId,
|
|
1296
|
+
kind: { const: "image" },
|
|
1297
|
+
storageKey: assetFactProperties.storageKey
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
type: "object",
|
|
1302
|
+
additionalProperties: false,
|
|
1303
|
+
required: [
|
|
1304
|
+
"assetId",
|
|
1305
|
+
"kind",
|
|
1306
|
+
"durationMs"
|
|
1307
|
+
],
|
|
1308
|
+
properties: {
|
|
1309
|
+
assetId: assetFactProperties.assetId,
|
|
1310
|
+
kind: { const: "video" },
|
|
1311
|
+
durationMs: assetFactProperties.durationMs,
|
|
1312
|
+
storageKey: assetFactProperties.storageKey
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
type: "object",
|
|
1317
|
+
additionalProperties: false,
|
|
1318
|
+
required: [
|
|
1319
|
+
"assetId",
|
|
1320
|
+
"kind",
|
|
1321
|
+
"durationMs",
|
|
1322
|
+
"storageKey"
|
|
1323
|
+
],
|
|
1324
|
+
properties: {
|
|
1325
|
+
assetId: assetFactProperties.assetId,
|
|
1326
|
+
kind: { const: "audio" },
|
|
1327
|
+
durationMs: assetFactProperties.durationMs,
|
|
1328
|
+
storageKey: assetFactProperties.storageKey
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
type: "object",
|
|
1333
|
+
additionalProperties: false,
|
|
1334
|
+
required: [
|
|
1335
|
+
"assetId",
|
|
1336
|
+
"kind",
|
|
1337
|
+
"durationMs",
|
|
1338
|
+
"storageKey",
|
|
1339
|
+
"voice"
|
|
1340
|
+
],
|
|
1341
|
+
properties: {
|
|
1342
|
+
...assetFactProperties,
|
|
1343
|
+
kind: { const: "voice" }
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
] }
|
|
1363
1347
|
},
|
|
1364
1348
|
timeout_ms: {
|
|
1365
1349
|
type: "integer",
|
|
@@ -1382,11 +1366,24 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1382
1366
|
},
|
|
1383
1367
|
validation: {
|
|
1384
1368
|
type: "string",
|
|
1385
|
-
enum: ["version"
|
|
1386
|
-
description: "
|
|
1369
|
+
enum: ["version"],
|
|
1370
|
+
description: "Commit with Entity revision CAS; reject concurrent changes."
|
|
1387
1371
|
}
|
|
1388
1372
|
},
|
|
1389
1373
|
oneOf: [
|
|
1374
|
+
{
|
|
1375
|
+
required: [
|
|
1376
|
+
"op",
|
|
1377
|
+
"doc_id",
|
|
1378
|
+
"asset_facts"
|
|
1379
|
+
],
|
|
1380
|
+
properties: {
|
|
1381
|
+
op: { const: "migrate-legacy" },
|
|
1382
|
+
doc_id: { $ref: "#/properties/doc_id" },
|
|
1383
|
+
asset_facts: { $ref: "#/properties/asset_facts" }
|
|
1384
|
+
},
|
|
1385
|
+
additionalProperties: false
|
|
1386
|
+
},
|
|
1390
1387
|
{
|
|
1391
1388
|
required: ["op", "doc_id"],
|
|
1392
1389
|
properties: {
|
|
@@ -1538,11 +1535,31 @@ function requiredContext(value, docId, field) {
|
|
|
1538
1535
|
if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
|
|
1539
1536
|
return resolved;
|
|
1540
1537
|
}
|
|
1538
|
+
function renderEntitySnapshot(state) {
|
|
1539
|
+
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1540
|
+
const shown = rows.slice(0, 200);
|
|
1541
|
+
return [
|
|
1542
|
+
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1543
|
+
...shown,
|
|
1544
|
+
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1545
|
+
].join("\n");
|
|
1546
|
+
}
|
|
1547
|
+
function migrationNotice(document, state) {
|
|
1548
|
+
if (state.entities.some((row) => row.entity_kind === "timeline") || Object.keys(document.part_library ?? {}).length === 0) return "";
|
|
1549
|
+
const assetIds = new Set(Object.values(document.part_library ?? {}).flatMap((part) => {
|
|
1550
|
+
const id = part.video_clip?.origin_media_id ?? part.bgm?.origin_media_id;
|
|
1551
|
+
return typeof id === "string" && id !== "" ? [id] : [];
|
|
1552
|
+
}));
|
|
1553
|
+
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.`;
|
|
1554
|
+
}
|
|
1541
1555
|
async function commitEntityPlan(client, plan) {
|
|
1542
1556
|
const rows = plan.entity_rows;
|
|
1543
1557
|
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1544
1558
|
try {
|
|
1545
|
-
const committed = await client.commit(plan.entity_base_revision, rows
|
|
1559
|
+
const committed = await client.commit(plan.entity_base_revision, rows, {
|
|
1560
|
+
deleted_entity_ids: plan.deleted_entity_ids ?? [],
|
|
1561
|
+
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1562
|
+
});
|
|
1546
1563
|
return {
|
|
1547
1564
|
kind: "committed",
|
|
1548
1565
|
ops_applied: plan.entity_commands.length,
|
|
@@ -1551,7 +1568,7 @@ async function commitEntityPlan(client, plan) {
|
|
|
1551
1568
|
};
|
|
1552
1569
|
} catch (error) {
|
|
1553
1570
|
if (error instanceof MengineEntityHttpRequestError) {
|
|
1554
|
-
if (error.status === 409) {
|
|
1571
|
+
if (error.status === 409 && isRevisionConflictPayload(error.payload)) {
|
|
1555
1572
|
const actualFromPayload = revisionConflictActual(error.payload);
|
|
1556
1573
|
try {
|
|
1557
1574
|
const current = await client.fetchState();
|
|
@@ -1602,6 +1619,9 @@ function revisionConflictActual(payload) {
|
|
|
1602
1619
|
const actual = payload.actual_revision;
|
|
1603
1620
|
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1604
1621
|
}
|
|
1622
|
+
function isRevisionConflictPayload(payload) {
|
|
1623
|
+
return isRecord(payload) && payload.code === "revision_conflict";
|
|
1624
|
+
}
|
|
1605
1625
|
function entityHttpErrorMessage(payload) {
|
|
1606
1626
|
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1607
1627
|
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
@@ -1631,6 +1651,18 @@ function parseInput(value) {
|
|
|
1631
1651
|
op,
|
|
1632
1652
|
doc_id: docId
|
|
1633
1653
|
};
|
|
1654
|
+
if (op === "migrate-legacy") {
|
|
1655
|
+
if (Object.keys(value).some((key) => ![
|
|
1656
|
+
"op",
|
|
1657
|
+
"doc_id",
|
|
1658
|
+
"asset_facts"
|
|
1659
|
+
].includes(key))) throw new Error("migrate-legacy accepts asset_facts only; the package reads the canonical document and version");
|
|
1660
|
+
return {
|
|
1661
|
+
op,
|
|
1662
|
+
doc_id: docId,
|
|
1663
|
+
asset_facts: parseMigrationAssetFacts(value.asset_facts)
|
|
1664
|
+
};
|
|
1665
|
+
}
|
|
1634
1666
|
if (op === "run-edit-script") {
|
|
1635
1667
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
1636
1668
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -1754,11 +1786,12 @@ function createMedeoTool(options) {
|
|
|
1754
1786
|
...peerId !== void 0 ? { peerId } : {}
|
|
1755
1787
|
});
|
|
1756
1788
|
}
|
|
1757
|
-
function rememberPlan(docId, plan) {
|
|
1789
|
+
function rememberPlan(docId, plan, baseState) {
|
|
1758
1790
|
const planId = randomUUID();
|
|
1759
1791
|
plans.set(planId, {
|
|
1760
1792
|
docId,
|
|
1761
|
-
plan
|
|
1793
|
+
plan,
|
|
1794
|
+
...plan.plan_kind === "entities" ? { baseState: baseState && structuredClone(baseState) } : {}
|
|
1762
1795
|
});
|
|
1763
1796
|
while (plans.size > maxPlans) {
|
|
1764
1797
|
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
@@ -1773,7 +1806,7 @@ function createMedeoTool(options) {
|
|
|
1773
1806
|
const pending = pendingPushes.get(docId);
|
|
1774
1807
|
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1775
1808
|
}
|
|
1776
|
-
function recordPushResult(docId, planId, plan, result) {
|
|
1809
|
+
function recordPushResult(docId, planId, plan, result, baseState) {
|
|
1777
1810
|
if (result.kind === "unconfirmed") {
|
|
1778
1811
|
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1779
1812
|
kind: "timeline",
|
|
@@ -1783,7 +1816,8 @@ function createMedeoTool(options) {
|
|
|
1783
1816
|
} : {
|
|
1784
1817
|
kind: "entities",
|
|
1785
1818
|
planId,
|
|
1786
|
-
plan
|
|
1819
|
+
plan,
|
|
1820
|
+
...baseState !== void 0 ? { baseState } : {}
|
|
1787
1821
|
});
|
|
1788
1822
|
return;
|
|
1789
1823
|
}
|
|
@@ -1791,22 +1825,51 @@ function createMedeoTool(options) {
|
|
|
1791
1825
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1792
1826
|
}
|
|
1793
1827
|
async function fetchEntityStateForSandbox(docId) {
|
|
1828
|
+
return await getEntityClient(docId).fetchState();
|
|
1829
|
+
}
|
|
1830
|
+
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1831
|
+
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1832
|
+
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1833
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1834
|
+
const client = getEntityClient(docId);
|
|
1835
|
+
const preCommitState = baseState ?? (options.loadGenerationFacts !== void 0 ? await client.fetchState() : void 0);
|
|
1836
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), preCommitState);
|
|
1837
|
+
}
|
|
1838
|
+
/**
|
|
1839
|
+
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1840
|
+
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1841
|
+
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1842
|
+
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1843
|
+
* created media Entities, edits that re-point an existing physical-asset
|
|
1844
|
+
* binding or Asset external key, and creations — not untouched pairs.
|
|
1845
|
+
* One-sided facts are skipped silently inside the sync.
|
|
1846
|
+
*/
|
|
1847
|
+
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
1848
|
+
if (result.kind !== "committed" || options.loadGenerationFacts === void 0 || baseState === void 0) return result;
|
|
1849
|
+
let outcome;
|
|
1794
1850
|
try {
|
|
1795
|
-
|
|
1851
|
+
outcome = await syncGeneratedRelations({
|
|
1852
|
+
client: getEntityClient(docId),
|
|
1853
|
+
docId,
|
|
1854
|
+
baseState,
|
|
1855
|
+
entityCommands: plan.entity_commands,
|
|
1856
|
+
loadFacts: options.loadGenerationFacts
|
|
1857
|
+
});
|
|
1796
1858
|
} catch (error) {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
relations: []
|
|
1859
|
+
outcome = {
|
|
1860
|
+
status: "failed",
|
|
1861
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1801
1862
|
};
|
|
1802
|
-
throw error;
|
|
1803
1863
|
}
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1864
|
+
const warnings = outcome.status === "failed" ? [{
|
|
1865
|
+
kind: "generation_sync_failed",
|
|
1866
|
+
message: outcome.message ?? "generation lineage sync failed"
|
|
1867
|
+
}] : void 0;
|
|
1868
|
+
return {
|
|
1869
|
+
...result,
|
|
1870
|
+
generation_sync: outcome,
|
|
1871
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1872
|
+
};
|
|
1810
1873
|
}
|
|
1811
1874
|
async function observePull(doc) {
|
|
1812
1875
|
const result = await doc.pull();
|
|
@@ -1829,8 +1892,8 @@ function createMedeoTool(options) {
|
|
|
1829
1892
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1830
1893
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1831
1894
|
return await runExclusive(docId, async (doc) => {
|
|
1832
|
-
await observePull(doc);
|
|
1833
|
-
const documentVersion = encodeDocVersionMark(doc.versionMark())
|
|
1895
|
+
const [, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(docId)]);
|
|
1896
|
+
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1834
1897
|
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1835
1898
|
const previousVersion = modelContextVersions.get(baselineKey);
|
|
1836
1899
|
const updatedSincePreviousModelCall = previousVersion == null ? null : previousVersion !== documentVersion;
|
|
@@ -1854,18 +1917,60 @@ function createMedeoTool(options) {
|
|
|
1854
1917
|
async function snapshot(input) {
|
|
1855
1918
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1856
1919
|
assertNoPendingPush(input.doc_id);
|
|
1857
|
-
const pull = await observePull(doc);
|
|
1920
|
+
const [pull, entityState] = await Promise.all([observePull(doc), fetchEntityStateForSandbox(input.doc_id)]);
|
|
1858
1921
|
return {
|
|
1859
1922
|
ok: true,
|
|
1860
1923
|
op: "snapshot",
|
|
1861
1924
|
doc_id: input.doc_id,
|
|
1862
|
-
version: encodeDocVersionMark(doc.versionMark())
|
|
1863
|
-
preview:
|
|
1925
|
+
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
1926
|
+
preview: renderEntitySnapshot(entityState) + migrationNotice(doc.snapshot(), entityState),
|
|
1864
1927
|
collaborated: pull.collaborated,
|
|
1865
1928
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1866
1929
|
};
|
|
1867
1930
|
});
|
|
1868
1931
|
}
|
|
1932
|
+
async function migrate(input) {
|
|
1933
|
+
return runExclusive(input.doc_id, async (doc) => {
|
|
1934
|
+
assertNoPendingPush(input.doc_id);
|
|
1935
|
+
const client = new EntityGraphHttpClient({
|
|
1936
|
+
docId: input.doc_id,
|
|
1937
|
+
httpOrigin: requiredContext(options.httpOrigin, input.doc_id, "httpOrigin"),
|
|
1938
|
+
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, input.doc_id) },
|
|
1939
|
+
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, input.doc_id) },
|
|
1940
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1941
|
+
});
|
|
1942
|
+
const base = await client.fetchState();
|
|
1943
|
+
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
1944
|
+
ok: true,
|
|
1945
|
+
op: "migrate-legacy",
|
|
1946
|
+
doc_id: input.doc_id,
|
|
1947
|
+
migration_status: "already_entity",
|
|
1948
|
+
entity_revision: base.revision,
|
|
1949
|
+
next_action: "snapshot"
|
|
1950
|
+
};
|
|
1951
|
+
const pull = await doc.pull();
|
|
1952
|
+
if (!pull.ok) throw new Error(`Migration requires a fresh canonical snapshot: ${pull.error.message}`);
|
|
1953
|
+
const migrationBaseVv = encodeDocVersionMark(doc.versionMark());
|
|
1954
|
+
const nextRows = migrateLegacyTimelineToEntities(doc.snapshot(), input.asset_facts, base.rows);
|
|
1955
|
+
let revision;
|
|
1956
|
+
try {
|
|
1957
|
+
revision = (await client.commit(base, nextRows, { migrationBaseVv })).revision;
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
if (error instanceof MengineHttpRequestError) throw new Error(`Migration rejected (HTTP ${error.status}): ${entityHttpErrorMessage(error.payload)}; take a fresh snapshot before retrying`);
|
|
1960
|
+
throw new Error("Migration submission is unconfirmed; take a fresh snapshot and retry migrate-legacy to inspect whether the Entity timeline already exists");
|
|
1961
|
+
}
|
|
1962
|
+
documents.delete(input.doc_id);
|
|
1963
|
+
for (const [id, cached] of plans) if (cached.docId === input.doc_id) plans.delete(id);
|
|
1964
|
+
return {
|
|
1965
|
+
ok: true,
|
|
1966
|
+
op: "migrate-legacy",
|
|
1967
|
+
doc_id: input.doc_id,
|
|
1968
|
+
migration_status: "committed",
|
|
1969
|
+
entity_revision: revision,
|
|
1970
|
+
next_action: "snapshot"
|
|
1971
|
+
};
|
|
1972
|
+
});
|
|
1973
|
+
}
|
|
1869
1974
|
async function run(input) {
|
|
1870
1975
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1871
1976
|
assertNoPendingPush(input.doc_id);
|
|
@@ -1876,6 +1981,7 @@ function createMedeoTool(options) {
|
|
|
1876
1981
|
document,
|
|
1877
1982
|
baseVersion,
|
|
1878
1983
|
entityState,
|
|
1984
|
+
entityOnly: true,
|
|
1879
1985
|
script: input.script,
|
|
1880
1986
|
...input.inputs !== void 0 ? { inputs: input.inputs } : {},
|
|
1881
1987
|
timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs,
|
|
@@ -1896,7 +2002,7 @@ function createMedeoTool(options) {
|
|
|
1896
2002
|
...result.plan,
|
|
1897
2003
|
doc_id: input.doc_id
|
|
1898
2004
|
};
|
|
1899
|
-
const planId = rememberPlan(input.doc_id, plan);
|
|
2005
|
+
const planId = rememberPlan(input.doc_id, plan, entityState);
|
|
1900
2006
|
const base = {
|
|
1901
2007
|
ok: true,
|
|
1902
2008
|
op: "run-edit-script",
|
|
@@ -1913,8 +2019,8 @@ function createMedeoTool(options) {
|
|
|
1913
2019
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1914
2020
|
};
|
|
1915
2021
|
if (input.auto_commit !== true) return base;
|
|
1916
|
-
const commit = await commitCachedPlan(input.doc_id, doc, plan);
|
|
1917
|
-
recordPushResult(input.doc_id, planId, plan, commit);
|
|
2022
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan, void 0, entityState);
|
|
2023
|
+
recordPushResult(input.doc_id, planId, plan, commit, entityState);
|
|
1918
2024
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1919
2025
|
return {
|
|
1920
2026
|
...base,
|
|
@@ -1930,8 +2036,8 @@ function createMedeoTool(options) {
|
|
|
1930
2036
|
const pending = pendingPushes.get(input.doc_id);
|
|
1931
2037
|
if (pending != null) {
|
|
1932
2038
|
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}`);
|
|
1933
|
-
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
|
|
1934
|
-
recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
|
|
2039
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation, pending.baseState);
|
|
2040
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result, pending.kind === "entities" ? pending.baseState : void 0);
|
|
1935
2041
|
const warnings = commitWarnings(result);
|
|
1936
2042
|
return {
|
|
1937
2043
|
ok: true,
|
|
@@ -1948,8 +2054,8 @@ function createMedeoTool(options) {
|
|
|
1948
2054
|
const cached = plans.get(input.plan_id);
|
|
1949
2055
|
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
1950
2056
|
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
1951
|
-
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
|
|
1952
|
-
recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
|
|
2057
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation, cached.baseState);
|
|
2058
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result, cached.baseState);
|
|
1953
2059
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1954
2060
|
return {
|
|
1955
2061
|
ok: true,
|
|
@@ -1973,6 +2079,7 @@ function createMedeoTool(options) {
|
|
|
1973
2079
|
try {
|
|
1974
2080
|
const parsed = parseInput(input);
|
|
1975
2081
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
2082
|
+
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
1976
2083
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
1977
2084
|
return await commit(parsed);
|
|
1978
2085
|
} catch (error) {
|