@mengine/medeo-tool 1.2.1-alpha.9 → 1.3.1-alpha.11
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 +39 -14
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-DHasvrhq.d.mts} +89 -33
- package/dist/index.d.mts +51 -178
- package/dist/index.mjs +300 -497
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +149 -44
- package/dist/{script-session-CHyIUBkO.mjs → script-session-DLBhoA1R.mjs} +682 -132
- package/dist/script-session-DLBhoA1R.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +39 -11
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/script-session-CHyIUBkO.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { a as isMediaAssetVariantKind, c as renderCompactProjection, i as createRelationId, o as collectAffectedPartIds, r as createEntityId, s as renderPreview, t as EditSandboxSession } from "./script-session-DLBhoA1R.mjs";
|
|
2
|
+
import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, ensureEditorFoundation, replayJournal, toVideoDocument } from "@mengine/medeo-client";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/sandbox/node-host.ts
|
|
@@ -122,6 +122,7 @@ function runEditScript(options) {
|
|
|
122
122
|
doc_id: options.document.meta.draft_id ?? "",
|
|
123
123
|
base_version: options.baseVersion,
|
|
124
124
|
ops: ops.slice(),
|
|
125
|
+
...message.loroUpdate ? { loro_update: message.loroUpdate } : {},
|
|
125
126
|
entity_base_revision: message.entityBaseRevision,
|
|
126
127
|
entity_commands: entityCommands.slice(),
|
|
127
128
|
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
@@ -205,11 +206,11 @@ const KNOWN_RELATION_KINDS = [
|
|
|
205
206
|
"marker-timeline",
|
|
206
207
|
"physical-asset",
|
|
207
208
|
"generated",
|
|
208
|
-
"phonetic-script-provenance",
|
|
209
|
-
"caption-provenance",
|
|
210
209
|
"caption-alignment",
|
|
211
210
|
"clip-anchor",
|
|
212
|
-
"
|
|
211
|
+
"phonetic-script-render",
|
|
212
|
+
"audio-script-source",
|
|
213
|
+
"audio-script-marker"
|
|
213
214
|
];
|
|
214
215
|
//#endregion
|
|
215
216
|
//#region src/entity/entity-http-client.ts
|
|
@@ -226,7 +227,7 @@ var MengineEntityHttpRequestError = class extends Error {
|
|
|
226
227
|
this.name = "MengineEntityHttpRequestError";
|
|
227
228
|
}
|
|
228
229
|
};
|
|
229
|
-
/** Narrow authenticated client for the entity
|
|
230
|
+
/** Narrow authenticated client for the Loro entity endpoint. */
|
|
230
231
|
var EntityHttpClient = class {
|
|
231
232
|
options;
|
|
232
233
|
fetchImpl;
|
|
@@ -237,18 +238,30 @@ var EntityHttpClient = class {
|
|
|
237
238
|
async fetchState() {
|
|
238
239
|
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
239
240
|
}
|
|
240
|
-
async commit(
|
|
241
|
+
async commit(_transportSequence, state, _deletions = {}) {
|
|
242
|
+
if (!state.loroSnapshot) throw new Error("Entity edit is missing its causal Loro baseline");
|
|
243
|
+
const rows = {
|
|
244
|
+
entities: state.entities.map((row) => ({
|
|
245
|
+
entityId: createEntityId(row.entity_id),
|
|
246
|
+
entityKind: row.entity_kind,
|
|
247
|
+
payload: row.payload
|
|
248
|
+
})),
|
|
249
|
+
relations: state.relations.map((row) => ({
|
|
250
|
+
relationId: createRelationId(row.relation_id),
|
|
251
|
+
relationKind: row.relation_kind,
|
|
252
|
+
endpoint0EntityId: createEntityId(row.endpoint_0_entity_id),
|
|
253
|
+
endpoint1EntityId: createEntityId(row.endpoint_1_entity_id),
|
|
254
|
+
metadata: row.metadata,
|
|
255
|
+
trace: row.trace
|
|
256
|
+
}))
|
|
257
|
+
};
|
|
258
|
+
const compiled = compileEntityRows(base64ToBytes(state.loroSnapshot), rows);
|
|
259
|
+
return this.commitUpdate(bytesToBase64(compiled.update));
|
|
260
|
+
}
|
|
261
|
+
async commitUpdate(update) {
|
|
241
262
|
return toSnapshot(await this.requestJson({
|
|
242
263
|
method: "POST",
|
|
243
|
-
body: JSON.stringify({
|
|
244
|
-
expected_revision: expectedRevision,
|
|
245
|
-
rows: {
|
|
246
|
-
entities: state.entities,
|
|
247
|
-
relations: state.relations
|
|
248
|
-
},
|
|
249
|
-
deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
|
|
250
|
-
deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
|
|
251
|
-
})
|
|
264
|
+
body: JSON.stringify({ update })
|
|
252
265
|
}), this.options.docId);
|
|
253
266
|
}
|
|
254
267
|
async requestJson(init) {
|
|
@@ -279,9 +292,15 @@ function toSnapshot(value, expectedDocId) {
|
|
|
279
292
|
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
293
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
294
|
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
295
|
+
if (value.audio_script_entity_id === null) throw new Error("Document AudioScript is not initialized");
|
|
296
|
+
if (!isTrimmed(value.audio_script_entity_id)) throw new Error("invalid document AudioScript identity");
|
|
297
|
+
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
282
298
|
const response = value;
|
|
299
|
+
if (response.audio_script_entity_id !== null && !response.rows.entities.some((entity) => entity.entity_id === response.audio_script_entity_id && entity.entity_kind === "audio-script")) throw new Error("Document AudioScript must name the project AudioScript");
|
|
283
300
|
return {
|
|
301
|
+
loroSnapshot: response.loro_snapshot,
|
|
284
302
|
revision: response.revision,
|
|
303
|
+
audioScriptEntityId: response.audio_script_entity_id,
|
|
285
304
|
entities: response.rows.entities.map(parseEntity),
|
|
286
305
|
relations: response.rows.relations.map(parseRelation)
|
|
287
306
|
};
|
|
@@ -333,15 +352,6 @@ async function safeReadJson(response) {
|
|
|
333
352
|
* Voice results use the speech system; every other medium uses `memota`.
|
|
334
353
|
*/
|
|
335
354
|
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
355
|
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
346
356
|
function parseGenerationFacts(value) {
|
|
347
357
|
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
@@ -359,42 +369,18 @@ function parseGenerationFacts(value) {
|
|
|
359
369
|
});
|
|
360
370
|
}
|
|
361
371
|
function planGenerationScope(base, commands, state) {
|
|
362
|
-
const
|
|
363
|
-
const
|
|
364
|
-
const
|
|
365
|
-
|
|
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
|
-
}
|
|
372
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
373
|
+
for (const command of commands) if (command.kind === "create-entity" && isMediaAssetVariantKind(command.entity.entity_kind)) touchedIds.add(command.entity.entity_id);
|
|
374
|
+
const beforeByKey = resolveMediaByAssetKey(base);
|
|
375
|
+
const scoped = /* @__PURE__ */ new Set();
|
|
391
376
|
const queryKeys = /* @__PURE__ */ new Set();
|
|
392
|
-
for (const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
377
|
+
for (const [key, mediaIds] of resolveMediaByAssetKey(state)) {
|
|
378
|
+
const previousIds = new Set(beforeByKey.get(key) ?? []);
|
|
379
|
+
for (const id of mediaIds) {
|
|
380
|
+
if (!touchedIds.has(id) || previousIds.has(id)) continue;
|
|
381
|
+
scoped.add(id);
|
|
382
|
+
queryKeys.add(key);
|
|
383
|
+
}
|
|
398
384
|
}
|
|
399
385
|
return {
|
|
400
386
|
scopedMediaIds: scoped,
|
|
@@ -403,8 +389,8 @@ function planGenerationScope(base, commands, state) {
|
|
|
403
389
|
}
|
|
404
390
|
/**
|
|
405
391
|
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
406
|
-
* factual records. Both endpoints must already exist and
|
|
407
|
-
*
|
|
392
|
+
* factual records. Both endpoints must already exist and match their own Asset
|
|
393
|
+
* identities, and the pair must involve a media Entity the plan
|
|
408
394
|
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
409
395
|
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
410
396
|
* the facts already resolved against the plan's base state is likewise skipped.
|
|
@@ -414,8 +400,9 @@ function planGenerationScope(base, commands, state) {
|
|
|
414
400
|
function planGeneratedRelations(input) {
|
|
415
401
|
const { state, facts } = input;
|
|
416
402
|
const scoped = input.scopedMediaIds;
|
|
417
|
-
const
|
|
418
|
-
const
|
|
403
|
+
const factKeys = new Set(facts.flatMap((fact) => [fact.outputAssetId, ...fact.inputAssetIds]));
|
|
404
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state, factKeys);
|
|
405
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState, factKeys), facts));
|
|
419
406
|
const linkedPairs = new Set(state.relations.filter((relation) => relation.relation_kind === "generated").map((relation) => pairKey(relation.endpoint_0_entity_id, relation.endpoint_1_entity_id)));
|
|
420
407
|
const relations = [];
|
|
421
408
|
for (const fact of facts) for (const outputId of mediaByAssetKey.get(fact.outputAssetId) ?? []) for (const inputAssetId of fact.inputAssetIds) for (const inputId of mediaByAssetKey.get(inputAssetId) ?? []) {
|
|
@@ -439,54 +426,33 @@ function planGeneratedRelations(input) {
|
|
|
439
426
|
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
440
427
|
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
441
428
|
* commit result is never masked; a successful query that finds nothing is
|
|
442
|
-
* `current`.
|
|
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`.
|
|
429
|
+
* `current`. Asset identities are immutable, so facts are queried once. The native update retains its causal baseline and merges without whole-state retries; deleted endpoints are never recreated.
|
|
446
430
|
*/
|
|
447
431
|
async function syncGeneratedRelations(input) {
|
|
448
432
|
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
449
|
-
let state;
|
|
450
|
-
let facts = [];
|
|
451
|
-
const queriedKeys = /* @__PURE__ */ new Set();
|
|
452
433
|
try {
|
|
453
|
-
state = await client.fetchState();
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
};
|
|
434
|
+
const state = await client.fetchState();
|
|
435
|
+
const scope = planGenerationScope(baseState, entityCommands, state);
|
|
436
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
437
|
+
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
438
|
+
const relations = planGeneratedRelations({
|
|
439
|
+
baseState,
|
|
440
|
+
state,
|
|
441
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
442
|
+
facts,
|
|
443
|
+
newRelationId: mintRelationId
|
|
444
|
+
});
|
|
445
|
+
if (relations.length === 0) return { status: "current" };
|
|
446
|
+
const committed = await client.commit(state.revision, {
|
|
447
|
+
...state,
|
|
448
|
+
relations: [...state.relations, ...relations]
|
|
449
|
+
});
|
|
450
|
+
const active = new Set(committed.relations.map((relation) => relation.relation_id));
|
|
451
|
+
const created = relations.map((relation) => relation.relation_id).filter((id) => active.has(id));
|
|
452
|
+
return created.length ? {
|
|
453
|
+
status: "applied",
|
|
454
|
+
created_relation_ids: created
|
|
455
|
+
} : { status: "current" };
|
|
490
456
|
} catch (error) {
|
|
491
457
|
return {
|
|
492
458
|
status: "failed",
|
|
@@ -495,7 +461,7 @@ async function syncGeneratedRelations(input) {
|
|
|
495
461
|
}
|
|
496
462
|
}
|
|
497
463
|
function assetKeyOf(entity) {
|
|
498
|
-
if (entity.entity_kind
|
|
464
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
|
|
499
465
|
const external = entity.payload?.external;
|
|
500
466
|
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
501
467
|
const { system, key } = external;
|
|
@@ -503,25 +469,24 @@ function assetKeyOf(entity) {
|
|
|
503
469
|
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
504
470
|
return key;
|
|
505
471
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
472
|
+
/** Media variants own their Asset locator; generation lookup never follows Relations. */
|
|
473
|
+
function resolveMediaByAssetKey(state, factKeys) {
|
|
474
|
+
const systemByKey = /* @__PURE__ */ new Map();
|
|
475
|
+
for (const entity of state.entities) {
|
|
476
|
+
const key = assetKeyOf(entity);
|
|
477
|
+
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
478
|
+
const system = entity.payload.external.system;
|
|
479
|
+
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
480
|
+
systemByKey.set(key, system);
|
|
481
|
+
}
|
|
515
482
|
const resolved = /* @__PURE__ */ new Map();
|
|
516
|
-
for (const
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
const asset = assetsById.get(relation.endpoint_1_entity_id);
|
|
520
|
-
const key = asset === void 0 ? void 0 : assetKeyOf(asset);
|
|
483
|
+
for (const entity of state.entities) {
|
|
484
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
485
|
+
const key = assetKeyOf(entity);
|
|
521
486
|
if (key === void 0) continue;
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
487
|
+
const matches = resolved.get(key) ?? [];
|
|
488
|
+
matches.push(entity.entity_id);
|
|
489
|
+
resolved.set(key, matches);
|
|
525
490
|
}
|
|
526
491
|
return resolved;
|
|
527
492
|
}
|
|
@@ -544,76 +509,6 @@ function isRecord$1(value) {
|
|
|
544
509
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
545
510
|
}
|
|
546
511
|
//#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
512
|
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
618
513
|
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
619
514
|
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
@@ -647,6 +542,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
647
542
|
" readonly system: 'font-library';",
|
|
648
543
|
" readonly key: string;",
|
|
649
544
|
"}",
|
|
545
|
+
"/**",
|
|
546
|
+
" * One ordered entry of the Caption's segment selection. `segmentId` quotes the",
|
|
547
|
+
" * composed AudioScript's own stable segment identity — a local id quoted by the",
|
|
548
|
+
" * variant, never a peer Entity reference. Text itself is never copied here;",
|
|
549
|
+
" * complete Caption content is assembled through its direct baseEntityIds.",
|
|
550
|
+
" * The optional `textRange` narrows one Segment to an intra-Segment sub-span",
|
|
551
|
+
" * (intra-segment re-segmentation); without it the whole Segment text is selected.",
|
|
552
|
+
" */",
|
|
553
|
+
"export type CaptionSegmentSelection = JsonObject & {",
|
|
554
|
+
" readonly segmentId: string;",
|
|
555
|
+
" readonly textRange?: CaptionTextRange;",
|
|
556
|
+
"};",
|
|
650
557
|
"export interface CaptionStyleFields {",
|
|
651
558
|
" readonly font?: CaptionFontDescriptor;",
|
|
652
559
|
" readonly fontSize?: number;",
|
|
@@ -659,6 +566,24 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
659
566
|
" readonly positionX?: number;",
|
|
660
567
|
" readonly positionY?: number;",
|
|
661
568
|
"}",
|
|
569
|
+
"/**",
|
|
570
|
+
" * Half-open `[start, end)` position window inside one Segment's text, counted",
|
|
571
|
+
" * in Unicode code points (not UTF-16 code units), so a boundary never splits a",
|
|
572
|
+
" * surrogate pair. Positions are non-negative safe integers with `start < end`;",
|
|
573
|
+
" * `end` must not exceed the Segment's code-point length.",
|
|
574
|
+
" */",
|
|
575
|
+
"export interface CaptionTextRange extends JsonObject {",
|
|
576
|
+
" readonly start: number;",
|
|
577
|
+
" readonly end: number;",
|
|
578
|
+
"}",
|
|
579
|
+
"export type CaptionTextSelection = JsonObject & {",
|
|
580
|
+
" segmentId: string;",
|
|
581
|
+
" /** Half-open Unicode code-point range within the selected source segment. */",
|
|
582
|
+
" textRange?: {",
|
|
583
|
+
" start: number;",
|
|
584
|
+
" end: number;",
|
|
585
|
+
" };",
|
|
586
|
+
"};",
|
|
662
587
|
"export type ClipEntityId = EntityId;",
|
|
663
588
|
"export type ClipPlacement =",
|
|
664
589
|
" | {",
|
|
@@ -674,11 +599,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
674
599
|
" readonly hostClipEntityId: string;",
|
|
675
600
|
" readonly anchorOffset: number;",
|
|
676
601
|
" };",
|
|
602
|
+
"export interface ComposedPhoneticContent extends ComposedScriptContent {",
|
|
603
|
+
" phonemeScript?: string;",
|
|
604
|
+
" prosody?: JsonObject;",
|
|
605
|
+
"}",
|
|
606
|
+
"/** Read result only: base text is assembled from the real AudioScript row. */",
|
|
607
|
+
"export interface ComposedScriptContent {",
|
|
608
|
+
" audio_script_entity_id: string;",
|
|
609
|
+
" text: string;",
|
|
610
|
+
" segments: ScriptTextSegment[];",
|
|
611
|
+
"}",
|
|
677
612
|
"export type CreateEntityInput = {",
|
|
678
613
|
" [K in KnownEntityKind]: {",
|
|
679
614
|
" entity_id?: string;",
|
|
680
615
|
" entity_kind: K;",
|
|
681
|
-
" payload:
|
|
616
|
+
" payload: StoredEntityPayload<K>;",
|
|
682
617
|
" };",
|
|
683
618
|
"}[KnownEntityKind];",
|
|
684
619
|
"export interface DeleteBgmInput {",
|
|
@@ -703,19 +638,29 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
703
638
|
" | 'clip-marker'",
|
|
704
639
|
" | 'marker-content'",
|
|
705
640
|
" | 'axvideo-marker'",
|
|
706
|
-
" | 'marker-timeline'
|
|
641
|
+
" | 'marker-timeline'",
|
|
642
|
+
" | 'audio-script-marker';",
|
|
707
643
|
"export interface EntityFacade {",
|
|
644
|
+
" /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */",
|
|
708
645
|
" list(): SandboxEntity[];",
|
|
709
646
|
" get(entityId: string): SandboxEntity | null;",
|
|
710
|
-
" /**
|
|
711
|
-
" findByAssetId(assetId: string): SandboxEntity<
|
|
647
|
+
" /** Find document resources by external Memota asset id, including directly composed media variants. */",
|
|
648
|
+
" findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];",
|
|
649
|
+
" /** Assemble selected Caption text; missing composition is an error. */",
|
|
650
|
+
" readCaptionContent(entityId: string): ComposedScriptContent;",
|
|
651
|
+
" /** Assemble base text and pronunciation fields before generating Voice. */",
|
|
652
|
+
" readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
|
|
712
653
|
" create(input: CreateEntityInput): string;",
|
|
713
|
-
" /**
|
|
654
|
+
" /** Patch assembled fields, routing inherited fields to their declaring entity. */",
|
|
714
655
|
" update(input: UpdateEntityInput): void;",
|
|
656
|
+
" /** Explicitly declare own fields, overriding unambiguous bases without modifying them. Ordinary edits use update. */",
|
|
657
|
+
" declareFields(input: UpdateEntityInput): void;",
|
|
715
658
|
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
716
659
|
" delete(input: DeleteEntityInput): void;",
|
|
717
|
-
" /**
|
|
718
|
-
"
|
|
660
|
+
" /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */",
|
|
661
|
+
" ensureMedia(fact: MediaAssetFact): {",
|
|
662
|
+
" contentEntityId: string;",
|
|
663
|
+
" };",
|
|
719
664
|
"}",
|
|
720
665
|
"export type EntityId = string;",
|
|
721
666
|
"export interface EntityPayloadByKind {",
|
|
@@ -726,12 +671,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
726
671
|
" role?: string;",
|
|
727
672
|
" };",
|
|
728
673
|
" clip: JsonObject;",
|
|
729
|
-
" /**
|
|
674
|
+
" /** Physical resource fields; never a copy of Caption content. */",
|
|
730
675
|
" asset: JsonObject;",
|
|
731
|
-
" video: BoundedNativeSequencePayload;",
|
|
732
|
-
" audio: BoundedNativeSequencePayload;",
|
|
733
|
-
" voice: BoundedNativeSequencePayload;",
|
|
734
|
-
" image: UnboundedConstantSequencePayload;",
|
|
676
|
+
" video: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
677
|
+
" audio: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
678
|
+
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
679
|
+
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
735
680
|
" 'sequence-marker': JsonObject & {",
|
|
736
681
|
" sourceRange: {",
|
|
737
682
|
" start: number;",
|
|
@@ -752,18 +697,34 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
752
697
|
" timeRemapping?: JsonValue;",
|
|
753
698
|
" anchorOffset?: number;",
|
|
754
699
|
" durationPolicy?: 'timeline';",
|
|
700
|
+
" /** Directly assigned AudioScript annotation times; annotation Markers only. */",
|
|
701
|
+
" segmentRanges?: {",
|
|
702
|
+
" segmentId: string;",
|
|
703
|
+
" startMs: number;",
|
|
704
|
+
" endMs: number;",
|
|
705
|
+
" }[];",
|
|
755
706
|
" };",
|
|
756
707
|
" viewport: JsonObject;",
|
|
757
708
|
" 'audio-script': JsonObject & {",
|
|
758
709
|
" segments: ScriptTextSegment[];",
|
|
759
710
|
" };",
|
|
760
711
|
" 'phonetic-script': JsonObject & {",
|
|
761
|
-
"
|
|
712
|
+
" baseEntityIds: string[];",
|
|
713
|
+
" phonemeScript?: string;",
|
|
714
|
+
" prosody?: JsonObject;",
|
|
715
|
+
" };",
|
|
716
|
+
" caption: BoundedNativeSequencePayload & {",
|
|
717
|
+
" baseEntityIds: string[];",
|
|
718
|
+
" selections: CaptionTextSelection[];",
|
|
719
|
+
" style?: JsonObject;",
|
|
762
720
|
" };",
|
|
763
|
-
" caption: BoundedNativeSequencePayload;",
|
|
764
721
|
"}",
|
|
765
722
|
"export interface EntityStoreSnapshot {",
|
|
723
|
+
" /** Causal compiler baseline; required for publishing edits. */",
|
|
724
|
+
" loroSnapshot?: string;",
|
|
766
725
|
" revision: number;",
|
|
726
|
+
" /** Current AudioScript version attached to the project; initialized projects always attach a script, possibly empty. */",
|
|
727
|
+
" audioScriptEntityId: string | null;",
|
|
767
728
|
" entities: SandboxEntity[];",
|
|
768
729
|
" relations: SandboxRelation[];",
|
|
769
730
|
"}",
|
|
@@ -772,10 +733,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
772
733
|
" readonly kind: 'image';",
|
|
773
734
|
" readonly storageKey?: string;",
|
|
774
735
|
"}",
|
|
775
|
-
"export interface
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"
|
|
736
|
+
"export interface InsertCaptionClipInput {",
|
|
737
|
+
" readonly timelineEntityId: string;",
|
|
738
|
+
" /** Stable placed caption identity, distinct from the Caption content identity. */",
|
|
739
|
+
" readonly captionClipEntityId?: string;",
|
|
740
|
+
" /** Existing bases composed by this variant; includes an AudioScript text owner. */",
|
|
741
|
+
" readonly baseEntityIds: readonly string[];",
|
|
742
|
+
" /** Ordered selection of the AudioScript segments this Caption displays. */",
|
|
743
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
744
|
+
" /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */",
|
|
745
|
+
" readonly durationMs: number;",
|
|
746
|
+
" readonly style?: CaptionStyleFields;",
|
|
747
|
+
" readonly placement: ClipPlacement;",
|
|
779
748
|
"}",
|
|
780
749
|
"export interface InsertClipInput {",
|
|
781
750
|
" readonly trackEntityId: string;",
|
|
@@ -840,20 +809,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
840
809
|
" | 'marker-timeline'",
|
|
841
810
|
" | 'physical-asset'",
|
|
842
811
|
" | 'generated'",
|
|
843
|
-
" | 'phonetic-script-provenance'",
|
|
844
|
-
" | 'caption-provenance'",
|
|
845
812
|
" | 'caption-alignment'",
|
|
846
813
|
" | 'clip-anchor'",
|
|
847
|
-
" | '
|
|
814
|
+
" | 'phonetic-script-render'",
|
|
815
|
+
" | 'audio-script-source'",
|
|
816
|
+
" | 'audio-script-marker';",
|
|
848
817
|
"export interface LinearClipSpeed {",
|
|
849
818
|
" readonly kind: 'linear';",
|
|
850
819
|
" readonly rate: number;",
|
|
851
820
|
" readonly mode?: string;",
|
|
852
821
|
"}",
|
|
853
|
-
"
|
|
822
|
+
"/** `audio-script-source(script, source)`; the script was transcribed from the source media. */",
|
|
823
|
+
"export interface LinkAudioScriptSourceRelationInput {",
|
|
854
824
|
" relation_id?: string;",
|
|
855
|
-
" output_entity_id: string;",
|
|
856
825
|
" script_entity_id: string;",
|
|
826
|
+
" source_entity_id: string;",
|
|
857
827
|
" trace?: JsonObject;",
|
|
858
828
|
"}",
|
|
859
829
|
"export interface LinkClipAnchorRelationInput {",
|
|
@@ -868,6 +838,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
868
838
|
" input_entity_id: string;",
|
|
869
839
|
" trace?: JsonObject;",
|
|
870
840
|
"}",
|
|
841
|
+
"export interface LinkPhoneticScriptRenderRelationInput {",
|
|
842
|
+
" relation_id?: string;",
|
|
843
|
+
" output_entity_id: string;",
|
|
844
|
+
" phonetic_script_entity_id: string;",
|
|
845
|
+
" trace?: JsonObject;",
|
|
846
|
+
"}",
|
|
871
847
|
"interface LinkRelationBase {",
|
|
872
848
|
" relation_id?: string;",
|
|
873
849
|
" endpoint_0_entity_id: string;",
|
|
@@ -886,17 +862,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
886
862
|
" metadata?: JsonObject;",
|
|
887
863
|
" })",
|
|
888
864
|
" | (LinkRelationBase & {",
|
|
889
|
-
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
890
|
-
" metadata: JsonObject & {",
|
|
891
|
-
" segmentAlignment: JsonValue;",
|
|
892
|
-
" };",
|
|
893
|
-
" })",
|
|
894
|
-
" | (LinkRelationBase & {",
|
|
895
865
|
" relation_kind: 'caption-alignment';",
|
|
896
866
|
" metadata: JsonObject & {",
|
|
897
867
|
" alignment: JsonValue;",
|
|
898
868
|
" };",
|
|
899
869
|
" });",
|
|
870
|
+
"/** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */",
|
|
871
|
+
"export type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;",
|
|
872
|
+
"export type MediaAssetPayload = JsonObject & {",
|
|
873
|
+
" external: {",
|
|
874
|
+
" system: 'memota' | 'memota-speech';",
|
|
875
|
+
" key: string;",
|
|
876
|
+
" };",
|
|
877
|
+
" storageKey?: string;",
|
|
878
|
+
"};",
|
|
900
879
|
"export type MediaClipInsertion =",
|
|
901
880
|
" | {",
|
|
902
881
|
" readonly kind: 'before';",
|
|
@@ -940,14 +919,16 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
940
919
|
" list(): SandboxRelation[];",
|
|
941
920
|
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
942
921
|
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
943
|
-
" /**
|
|
922
|
+
" /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */",
|
|
944
923
|
" link(input: LinkRelationInput): string;",
|
|
945
924
|
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
946
925
|
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
947
926
|
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
948
927
|
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
949
|
-
" /** Author ordered
|
|
950
|
-
"
|
|
928
|
+
" /** Author ordered phonetic-script-render(output,script) without positional endpoint ambiguity. */",
|
|
929
|
+
" linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
|
|
930
|
+
" /** Author ordered audio-script-source(script,source) without positional endpoint ambiguity. */",
|
|
931
|
+
" linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
|
|
951
932
|
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
952
933
|
" unlink(input: UnlinkRelationInput): void;",
|
|
953
934
|
"}",
|
|
@@ -977,10 +958,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
977
958
|
" readonly sourceRange: SequenceRange<number>;",
|
|
978
959
|
" readonly volume?: number;",
|
|
979
960
|
"}",
|
|
961
|
+
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
962
|
+
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
980
963
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
981
964
|
" entity_id: string;",
|
|
982
965
|
" entity_kind: K;",
|
|
983
|
-
" payload:
|
|
966
|
+
" payload: StoredEntityPayload<K>;",
|
|
984
967
|
"}",
|
|
985
968
|
"export interface SandboxRelation {",
|
|
986
969
|
" relation_id: string;",
|
|
@@ -1038,6 +1021,13 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1038
1021
|
" /** Playback gain in decibels. */",
|
|
1039
1022
|
" readonly volume: number;",
|
|
1040
1023
|
"}",
|
|
1024
|
+
"/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
|
|
1025
|
+
"export type StoredEntityPayload<K extends KnownEntityKind> =",
|
|
1026
|
+
" | EntityPayloadByKind[K]",
|
|
1027
|
+
" | (JsonObject &",
|
|
1028
|
+
" Partial<EntityPayloadByKind[K]> & {",
|
|
1029
|
+
" baseEntityIds: string[];",
|
|
1030
|
+
" });",
|
|
1041
1031
|
"export interface TrimClipInput {",
|
|
1042
1032
|
" readonly clipEntityId: string;",
|
|
1043
1033
|
" readonly sourceRange: SequenceRange<number>;",
|
|
@@ -1067,6 +1057,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1067
1057
|
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
1068
1058
|
" readonly timeRemapping?: JsonValue | undefined;",
|
|
1069
1059
|
"}",
|
|
1060
|
+
"/** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */",
|
|
1070
1061
|
"export interface UpdateEntityInput {",
|
|
1071
1062
|
" entity_id: string;",
|
|
1072
1063
|
" payload: JsonObject;",
|
|
@@ -1089,31 +1080,47 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1089
1080
|
" readonly kind: 'voice';",
|
|
1090
1081
|
" readonly durationMs: number;",
|
|
1091
1082
|
" readonly storageKey: string;",
|
|
1092
|
-
"
|
|
1083
|
+
" /** Present for synthesized voice, absent for original recorded audio. */",
|
|
1084
|
+
" readonly voice?: VoiceDescriptor;",
|
|
1093
1085
|
"}",
|
|
1094
1086
|
"export interface VoiceoverCaptionFact {",
|
|
1095
1087
|
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
1096
1088
|
" readonly captionClipEntityId: string;",
|
|
1097
|
-
"
|
|
1089
|
+
" /** Directly held bases; includes the AudioScript used by the Voice. */",
|
|
1090
|
+
" readonly baseEntityIds: readonly string[];",
|
|
1091
|
+
" /** Ordered selection of AudioScript segments; caption text is never passed inline. */",
|
|
1092
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
1098
1093
|
" readonly startMs: number;",
|
|
1099
1094
|
" readonly durationMs: number;",
|
|
1100
1095
|
" readonly style?: CaptionStyleFields;",
|
|
1101
1096
|
"}",
|
|
1102
|
-
"export
|
|
1097
|
+
"export type VoiceoverTakeInput = {",
|
|
1103
1098
|
" readonly timelineEntityId: string;",
|
|
1104
1099
|
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
1105
1100
|
" readonly voiceoverClipEntityId: string;",
|
|
1106
|
-
" readonly hostClipEntityId: string;",
|
|
1107
|
-
" readonly anchorOffset: number;",
|
|
1108
1101
|
" readonly media: VoiceMediaAssetFact;",
|
|
1109
|
-
" /**
|
|
1110
|
-
" readonly
|
|
1102
|
+
" /** Existing pronunciation variant; its composed AudioScript stays the text owner. */",
|
|
1103
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1111
1104
|
" readonly volume: number;",
|
|
1112
1105
|
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
1113
|
-
"}",
|
|
1106
|
+
"} & (",
|
|
1107
|
+
" | {",
|
|
1108
|
+
" readonly placement: ClipPlacement;",
|
|
1109
|
+
" readonly hostClipEntityId?: never;",
|
|
1110
|
+
" readonly anchorOffset?: never;",
|
|
1111
|
+
" }",
|
|
1112
|
+
" | {",
|
|
1113
|
+
" readonly placement?: never;",
|
|
1114
|
+
" readonly hostClipEntityId: string;",
|
|
1115
|
+
" readonly anchorOffset: number;",
|
|
1116
|
+
" }",
|
|
1117
|
+
");",
|
|
1114
1118
|
"export interface VoiceoverTakeResult {",
|
|
1115
1119
|
" readonly voiceoverClipEntityId: string;",
|
|
1116
1120
|
" readonly voiceEntityId: string;",
|
|
1121
|
+
" /** The pronunciation variant the Voice was rendered from. */",
|
|
1122
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1123
|
+
" /** The base-text owner resolved from the PhoneticScript baseEntityIds. */",
|
|
1117
1124
|
" readonly audioScriptEntityId: string;",
|
|
1118
1125
|
" readonly captionClipEntityIds: readonly string[];",
|
|
1119
1126
|
"}",
|
|
@@ -1144,9 +1151,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1144
1151
|
" deleteBgm(input: DeleteBgmInput): void;",
|
|
1145
1152
|
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
1146
1153
|
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1154
|
+
" insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
|
|
1147
1155
|
"}",
|
|
1148
1156
|
"export interface TimelineApi {",
|
|
1149
|
-
" snapshot(): EntityStoreSnapshot
|
|
1157
|
+
" snapshot(): EntityStoreSnapshot & {",
|
|
1158
|
+
" audioScriptEntityId: string;",
|
|
1159
|
+
" };",
|
|
1150
1160
|
"}",
|
|
1151
1161
|
"export interface SandboxCheckpoint {",
|
|
1152
1162
|
" readonly index: number;",
|
|
@@ -1166,25 +1176,27 @@ const MEDEO_TOOL_DESCRIPTION = `
|
|
|
1166
1176
|
Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
|
|
1167
1177
|
|
|
1168
1178
|
Operations:
|
|
1169
|
-
- snapshot:
|
|
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.
|
|
1179
|
+
- snapshot: read the current Entity/Relation view, project attachments and causal Loro baseline. Reading does not initialize or mutate domain data.
|
|
1171
1180
|
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation and timeline edits belong in ONE plan. The sandbox has no network, storage or generation access. Pass recalled asset facts through inputs; generation history is not a script input — the host program queries it itself after each commit. A successful run returns preview, logs, base revision and plan_id.
|
|
1172
|
-
- commit-plan:
|
|
1181
|
+
- commit-plan: publish the native Loro update compiled against the plan’s causal baseline. Concurrent independent edits merge through Loro. The timeline and AudioScript panel read the merged entity state. A failed transport is unconfirmed; retry the same plan_id so operation identities are preserved.
|
|
1173
1182
|
|
|
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.
|
|
1183
|
+
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. Concurrent edits do not require replaying the script against a newer snapshot. If a domain conflict is reported, inspect the merged state and resolve it explicitly; never replace the complete document to force the edit through.
|
|
1175
1184
|
|
|
1176
|
-
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts,
|
|
1185
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, call entities.ensureMedia(fact), then pass its contentEntityId to edit.insertClip. A raw external asset id or URL is not valid contentEntityId. Image/Video/Audio/Voice are logical variants of Asset: a resource has ONE identity and ONE typed row owning both media fields and external {system,key}/storageKey, with no separate Asset row or physical-asset relation. ensureMedia returns contentEntityId and reuses that single identity by external asset id. Each placement still gets its own Clip and SequenceMarker. Generation lineage is program-synced: after each successful commit the tool connects existing typed Assets from host-recalled generation facts (endpoint 0 output, endpoint 1 input) that the host queries itself — do not pass generation history through inputs. Do not author generated Relations yourself, and never create an Entity merely to backfill or represent lineage; media variants the edit itself legitimately needs are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
1177
1186
|
`.trim();
|
|
1178
1187
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1179
1188
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1180
1189
|
timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
|
|
1181
1190
|
Generation lineage is not a model input: the host program queries it via loadGenerationFacts and syncs generated Relations after each successful commit. Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
1182
|
-
|
|
1191
|
+
Use entities.ensureMedia(fact) to get or create the canonical typed Asset. Native media placement helpers use the same resolver. findByAssetId includes directly composed media variants, including Voice. Every Image/Video/Audio/Voice must own its external identity; separate Asset+media graphs are invalid. A typed Asset's external identity cannot be removed or rewritten: to replace its source, ensureMedia for the new Asset and replace the Clip's content. Conflicting facts fail closed. Asset generation itself still creates no editor Entities.
|
|
1183
1192
|
For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
|
|
1184
|
-
|
|
1185
|
-
The
|
|
1186
|
-
|
|
1187
|
-
|
|
1193
|
+
Caption content is assembled from AudioScript; never create an inline text Asset for it. Generated media lineage is host-owned; do not author generated Relations yourself. relations.of remains endpoint-agnostic for lookup.
|
|
1194
|
+
The editor projection supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
1195
|
+
Entities own fields; ordinary Relations express associations; variants directly hold baseEntityIds and assemble the referenced entities. These foundations are fixed: implementation must follow them, never redefine them. Any entity may compose multiple bases. Equal field names from multiple bases (even equal values) are errors, even when the variant declares that field itself. After validating all base fields are unambiguous, explicitly declared own fields may override base fields without mutating the bases. Base ordering never resolves conflicts. AudioScript owns segmented text. Caption and PhoneticScript persist baseEntityIds including their AudioScript, plus their own fields; no composition Relation exists. Create the real bases before reading or committing a variant. Inside the DSL sandbox, entities.get/list expose complete assembled fields. Consumers read fields without inspecting base IDs or merging bases. entities.update patches supplied fields and routes inherited fields to their declaring entity; omitted fields remain unchanged. entities.declareFields explicitly declares own overrides and is distinct from an ordinary field edit. Persistence keeps owned fields only. entities.readCaptionContent(id) and entities.readPhoneticScriptContent(id) return assembled text. Missing/cyclic bases and field conflicts fail before persistence.
|
|
1196
|
+
Use edit.insertCaptionClip with baseEntityIds and selections; each voiceover caption also supplies baseEntityIds. A selection names segmentId and may use a half-open Unicode code-point textRange to split a segment for the screen without rewriting AudioScript. Generate Voice from an existing PhoneticScript, then use phoneticScriptEntityId in the voiceover helper or relations.linkPhoneticScriptRender({output_entity_id,phonetic_script_entity_id}) for its render relation. Caption and Voice have their own Clips; display anchoring is explicit and independent of composition/alignment.
|
|
1197
|
+
Move or stretch only the Clip's display Marker; preserve Caption intrinsic Sequence, AudioScript text and its annotation Markers. AudioScript cannot enter a Clip and has no intrinsic time. audio-script-source links its ASR source Audio/Video/Voice; audio-script-marker attaches annotation Markers with directly assigned segmentRanges:{segmentId,startMs,endMs} in whole milliseconds. Annotation Markers have no Clip/AXVideo/content/Timeline relations and never refer to other Markers for time. BGM keeps factual source duration with durationPolicy:'timeline'. Never introduce a speech entity kind.
|
|
1198
|
+
Create only the known entity kinds. Project creation initializes one current Timeline, four Tracks and an attached AudioScript with segments:[]. Read the current AudioScript ID from timeline.snapshot(); the panel displays this attachment and preserves its segments. Normal edits operate this script, not an unrelated newly created script.
|
|
1199
|
+
Immutable updates apply to every entity: editing an owned field creates a new content version ID; changing only a variant's base ID preserves the variant ID. Editing a base through a variant updates the owner, and the compiler advances the affected base links and project attachment. A variant-owned edit creates a new variant version and retains its unchanged bases. Do not manually clone entities or duplicate inherited fields to implement versioning. Versions preserve native text and list editing identities so independent concurrent edits survive. Missing facts, unsupported layouts and composition conflicts are explicit errors; there is no legacy migration or whole-state overwrite path.
|
|
1188
1200
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1189
1201
|
`.trim();
|
|
1190
1202
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -1210,42 +1222,6 @@ ${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
|
1210
1222
|
//#endregion
|
|
1211
1223
|
//#region src/schema.ts
|
|
1212
1224
|
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
|
-
};
|
|
1249
1225
|
/**
|
|
1250
1226
|
* JSON Schema for the host-facing `medeo` tool surface.
|
|
1251
1227
|
*
|
|
@@ -1263,7 +1239,6 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1263
1239
|
type: "string",
|
|
1264
1240
|
enum: [
|
|
1265
1241
|
"snapshot",
|
|
1266
|
-
"migrate-legacy",
|
|
1267
1242
|
"run-edit-script",
|
|
1268
1243
|
"commit-plan"
|
|
1269
1244
|
],
|
|
@@ -1283,68 +1258,6 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1283
1258
|
type: "object",
|
|
1284
1259
|
description: "Pre-materialized, side-effect-free values passed into the script, including recalled asset facts. Generation history is never an input: the host queries lineage itself and syncs generated Relations after each commit. Generation and network IO must happen in the host before this call."
|
|
1285
1260
|
},
|
|
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
|
-
] }
|
|
1347
|
-
},
|
|
1348
1261
|
timeout_ms: {
|
|
1349
1262
|
type: "integer",
|
|
1350
1263
|
minimum: 1,
|
|
@@ -1366,24 +1279,11 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1366
1279
|
},
|
|
1367
1280
|
validation: {
|
|
1368
1281
|
type: "string",
|
|
1369
|
-
enum: ["
|
|
1370
|
-
description: "
|
|
1282
|
+
enum: ["preflight"],
|
|
1283
|
+
description: "Publish the native update already validated on its causal Loro fork."
|
|
1371
1284
|
}
|
|
1372
1285
|
},
|
|
1373
1286
|
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
|
-
},
|
|
1387
1287
|
{
|
|
1388
1288
|
required: ["op", "doc_id"],
|
|
1389
1289
|
properties: {
|
|
@@ -1539,66 +1439,30 @@ function renderEntitySnapshot(state) {
|
|
|
1539
1439
|
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1540
1440
|
const shown = rows.slice(0, 200);
|
|
1541
1441
|
return [
|
|
1542
|
-
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1442
|
+
`Entity revision=${state.revision} audioScriptEntityId=${JSON.stringify(state.audioScriptEntityId)} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1543
1443
|
...shown,
|
|
1544
1444
|
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1545
1445
|
].join("\n");
|
|
1546
1446
|
}
|
|
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
|
-
}
|
|
1555
1447
|
async function commitEntityPlan(client, plan) {
|
|
1556
|
-
|
|
1557
|
-
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1448
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1558
1449
|
try {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1562
|
-
});
|
|
1450
|
+
if (!plan.loro_update) throw new Error("Entity plan has no compiled Loro update");
|
|
1451
|
+
const committed = await client.commitUpdate(plan.loro_update);
|
|
1563
1452
|
return {
|
|
1564
1453
|
kind: "committed",
|
|
1565
1454
|
ops_applied: plan.entity_commands.length,
|
|
1566
|
-
collaborated:
|
|
1455
|
+
collaborated: committed.revision > plan.entity_base_revision + 1,
|
|
1567
1456
|
entity_revision: committed.revision
|
|
1568
1457
|
};
|
|
1569
1458
|
} catch (error) {
|
|
1570
1459
|
if (error instanceof MengineEntityHttpRequestError) {
|
|
1571
|
-
if (error.
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
ops_applied: plan.entity_commands.length,
|
|
1578
|
-
collaborated: false,
|
|
1579
|
-
entity_revision: current.revision
|
|
1580
|
-
};
|
|
1581
|
-
return {
|
|
1582
|
-
kind: "rejected",
|
|
1583
|
-
reason: "entity_revision_mismatch",
|
|
1584
|
-
expected: plan.entity_base_revision,
|
|
1585
|
-
actual: current.revision
|
|
1586
|
-
};
|
|
1587
|
-
} catch {
|
|
1588
|
-
if (actualFromPayload !== void 0) return {
|
|
1589
|
-
kind: "rejected",
|
|
1590
|
-
reason: "entity_revision_mismatch",
|
|
1591
|
-
expected: plan.entity_base_revision,
|
|
1592
|
-
actual: actualFromPayload
|
|
1593
|
-
};
|
|
1594
|
-
return {
|
|
1595
|
-
kind: "unconfirmed",
|
|
1596
|
-
reason: "push_failed",
|
|
1597
|
-
ops_applied: plan.entity_commands.length,
|
|
1598
|
-
message: "entity-state conflict could not be reconciled"
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
1460
|
+
if (isRecord(error.payload) && error.payload.accepted === true && typeof error.payload.revision === "number") return {
|
|
1461
|
+
kind: "conflicted",
|
|
1462
|
+
accepted: true,
|
|
1463
|
+
entity_revision: error.payload.revision,
|
|
1464
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1465
|
+
};
|
|
1602
1466
|
return {
|
|
1603
1467
|
kind: "rejected",
|
|
1604
1468
|
reason: "entity_state_rejected",
|
|
@@ -1614,33 +1478,14 @@ async function commitEntityPlan(client, plan) {
|
|
|
1614
1478
|
};
|
|
1615
1479
|
}
|
|
1616
1480
|
}
|
|
1617
|
-
function revisionConflictActual(payload) {
|
|
1618
|
-
if (!isRecord(payload)) return void 0;
|
|
1619
|
-
const actual = payload.actual_revision;
|
|
1620
|
-
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1621
|
-
}
|
|
1622
|
-
function isRevisionConflictPayload(payload) {
|
|
1623
|
-
return isRecord(payload) && payload.code === "revision_conflict";
|
|
1624
|
-
}
|
|
1625
1481
|
function entityHttpErrorMessage(payload) {
|
|
1482
|
+
if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === "string") return payload.error.message;
|
|
1626
1483
|
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1627
1484
|
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1628
1485
|
}
|
|
1629
1486
|
function commitWarnings(result) {
|
|
1630
1487
|
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1631
1488
|
}
|
|
1632
|
-
function entityRowsEquivalent(left, right) {
|
|
1633
|
-
const normalize = (state) => ({
|
|
1634
|
-
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1635
|
-
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1636
|
-
});
|
|
1637
|
-
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
1638
|
-
}
|
|
1639
|
-
function canonicalJson(value) {
|
|
1640
|
-
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
1641
|
-
if (!isRecord(value)) return value;
|
|
1642
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
1643
|
-
}
|
|
1644
1489
|
function parseInput(value) {
|
|
1645
1490
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
1646
1491
|
const op = value.op;
|
|
@@ -1651,18 +1496,6 @@ function parseInput(value) {
|
|
|
1651
1496
|
op,
|
|
1652
1497
|
doc_id: docId
|
|
1653
1498
|
};
|
|
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
|
-
}
|
|
1666
1499
|
if (op === "run-edit-script") {
|
|
1667
1500
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
1668
1501
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -1683,7 +1516,7 @@ function parseInput(value) {
|
|
|
1683
1516
|
}
|
|
1684
1517
|
if (op === "commit-plan") {
|
|
1685
1518
|
if (typeof value.plan_id !== "string" || value.plan_id.length === 0) throw new Error("plan_id must be a non-empty string");
|
|
1686
|
-
if (value.validation !== void 0 && value.validation !== "
|
|
1519
|
+
if (value.validation !== void 0 && value.validation !== "preflight") throw new Error("validation must be \"preflight\"");
|
|
1687
1520
|
return {
|
|
1688
1521
|
op,
|
|
1689
1522
|
doc_id: docId,
|
|
@@ -1772,10 +1605,21 @@ function createMedeoTool(options) {
|
|
|
1772
1605
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
1773
1606
|
if (options.loadInitialDraft === void 0) throw error;
|
|
1774
1607
|
}
|
|
1775
|
-
const
|
|
1608
|
+
const document = toVideoDocument(await options.loadInitialDraft(docId));
|
|
1609
|
+
if (Object.keys(document.part_library ?? {}).length > 0) throw new Error("Legacy content cannot bootstrap a Loro entity project");
|
|
1610
|
+
const seed = createMirrorVideoDocument(document, {
|
|
1776
1611
|
...peerId !== void 0 ? { peerId } : {},
|
|
1777
1612
|
origin: "mengine.medeo_tool.bootstrap"
|
|
1778
1613
|
});
|
|
1614
|
+
const foundation = ensureEditorFoundation({
|
|
1615
|
+
entities: [],
|
|
1616
|
+
relations: []
|
|
1617
|
+
});
|
|
1618
|
+
const entities = LoroEntityDocument.create(foundation.rows, {
|
|
1619
|
+
timelineEntityId: foundation.timelineEntityId,
|
|
1620
|
+
audioScriptEntityId: foundation.audioScriptEntityId
|
|
1621
|
+
});
|
|
1622
|
+
seed.import(entities.doc.export({ mode: "snapshot" }));
|
|
1779
1623
|
try {
|
|
1780
1624
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
1781
1625
|
} catch (error) {
|
|
@@ -1824,24 +1668,24 @@ function createMedeoTool(options) {
|
|
|
1824
1668
|
pendingPushes.delete(docId);
|
|
1825
1669
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1826
1670
|
}
|
|
1827
|
-
async function fetchEntityStateForSandbox(docId) {
|
|
1828
|
-
|
|
1671
|
+
async function fetchEntityStateForSandbox(docId, _doc, _pull) {
|
|
1672
|
+
const state = await getEntityClient(docId).fetchState();
|
|
1673
|
+
if (!state.loroSnapshot) throw new Error("Project requires the Loro entity contract");
|
|
1674
|
+
return state;
|
|
1829
1675
|
}
|
|
1830
1676
|
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1831
1677
|
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
1678
|
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1834
1679
|
const client = getEntityClient(docId);
|
|
1835
|
-
|
|
1836
|
-
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan),
|
|
1680
|
+
if (options.loadGenerationFacts !== void 0 && baseState === void 0) throw new Error("Generation synchronization is missing the cached causal plan baseline");
|
|
1681
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), baseState);
|
|
1837
1682
|
}
|
|
1838
1683
|
/**
|
|
1839
1684
|
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1840
1685
|
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1841
1686
|
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1842
1687
|
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1843
|
-
* created media
|
|
1844
|
-
* binding or Asset external key, and creations — not untouched pairs.
|
|
1688
|
+
* newly created media Asset identities — not untouched pairs or placement-only edits.
|
|
1845
1689
|
* One-sided facts are skipped silently inside the sync.
|
|
1846
1690
|
*/
|
|
1847
1691
|
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
@@ -1892,7 +1736,7 @@ function createMedeoTool(options) {
|
|
|
1892
1736
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1893
1737
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1894
1738
|
return await runExclusive(docId, async (doc) => {
|
|
1895
|
-
const
|
|
1739
|
+
const entityState = await fetchEntityStateForSandbox(docId, doc, await observePull(doc));
|
|
1896
1740
|
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1897
1741
|
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1898
1742
|
const previousVersion = modelContextVersions.get(baselineKey);
|
|
@@ -1917,64 +1761,24 @@ function createMedeoTool(options) {
|
|
|
1917
1761
|
async function snapshot(input) {
|
|
1918
1762
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1919
1763
|
assertNoPendingPush(input.doc_id);
|
|
1920
|
-
const
|
|
1764
|
+
const pull = await observePull(doc);
|
|
1765
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1921
1766
|
return {
|
|
1922
1767
|
ok: true,
|
|
1923
1768
|
op: "snapshot",
|
|
1924
1769
|
doc_id: input.doc_id,
|
|
1925
1770
|
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
1926
|
-
preview: renderEntitySnapshot(entityState)
|
|
1771
|
+
preview: renderEntitySnapshot(entityState),
|
|
1927
1772
|
collaborated: pull.collaborated,
|
|
1928
1773
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1929
1774
|
};
|
|
1930
1775
|
});
|
|
1931
1776
|
}
|
|
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
|
-
}
|
|
1974
1777
|
async function run(input) {
|
|
1975
1778
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1976
1779
|
assertNoPendingPush(input.doc_id);
|
|
1977
|
-
const
|
|
1780
|
+
const pull = await observePull(doc);
|
|
1781
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1978
1782
|
const document = doc.snapshot();
|
|
1979
1783
|
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1980
1784
|
const result = await runEditScript({
|
|
@@ -2079,7 +1883,6 @@ function createMedeoTool(options) {
|
|
|
2079
1883
|
try {
|
|
2080
1884
|
const parsed = parseInput(input);
|
|
2081
1885
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
2082
|
-
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
2083
1886
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
2084
1887
|
return await commit(parsed);
|
|
2085
1888
|
} catch (error) {
|