@mengine/medeo-tool 1.2.1-alpha.8 → 1.3.1-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -11
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-hKZbJSRP.d.mts} +85 -33
- package/dist/index.d.mts +125 -79
- package/dist/index.mjs +460 -85
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +142 -43
- package/dist/{script-session-CHyIUBkO.mjs → script-session-1eqBGegp.mjs} +664 -132
- package/dist/script-session-1eqBGegp.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +28 -8
- 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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { c as renderPreview, l as renderCompactProjection, n as EntitySandbox, o as isMediaAssetVariantKind, r as toDslRows, s as collectAffectedPartIds, t as EditSandboxSession } from "./script-session-1eqBGegp.mjs";
|
|
2
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";
|
|
@@ -205,11 +205,11 @@ const KNOWN_RELATION_KINDS = [
|
|
|
205
205
|
"marker-timeline",
|
|
206
206
|
"physical-asset",
|
|
207
207
|
"generated",
|
|
208
|
-
"phonetic-script-provenance",
|
|
209
|
-
"caption-provenance",
|
|
210
208
|
"caption-alignment",
|
|
211
209
|
"clip-anchor",
|
|
212
|
-
"
|
|
210
|
+
"phonetic-script-render",
|
|
211
|
+
"audio-script-source",
|
|
212
|
+
"audio-script-marker"
|
|
213
213
|
];
|
|
214
214
|
//#endregion
|
|
215
215
|
//#region src/entity/entity-http-client.ts
|
|
@@ -276,9 +276,9 @@ var EntityHttpClient = class {
|
|
|
276
276
|
}
|
|
277
277
|
};
|
|
278
278
|
function toSnapshot(value, expectedDocId) {
|
|
279
|
-
if (!isRecord$
|
|
279
|
+
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
280
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
|
-
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");
|
|
282
282
|
const response = value;
|
|
283
283
|
return {
|
|
284
284
|
revision: response.revision,
|
|
@@ -287,15 +287,15 @@ function toSnapshot(value, expectedDocId) {
|
|
|
287
287
|
};
|
|
288
288
|
}
|
|
289
289
|
function parseEntity(value) {
|
|
290
|
-
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");
|
|
291
291
|
return structuredClone(value);
|
|
292
292
|
}
|
|
293
293
|
function parseRelation(value) {
|
|
294
|
-
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");
|
|
295
295
|
return structuredClone(value);
|
|
296
296
|
}
|
|
297
297
|
function isJsonObject(value) {
|
|
298
|
-
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$
|
|
298
|
+
return isJsonValue(value, /* @__PURE__ */ new Set()) && isRecord$2(value);
|
|
299
299
|
}
|
|
300
300
|
function isJsonValue(value, ancestors) {
|
|
301
301
|
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
@@ -308,7 +308,7 @@ function isJsonValue(value, ancestors) {
|
|
|
308
308
|
ancestors.delete(value);
|
|
309
309
|
return valid;
|
|
310
310
|
}
|
|
311
|
-
function isRecord$
|
|
311
|
+
function isRecord$2(value) {
|
|
312
312
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
313
313
|
}
|
|
314
314
|
function isTrimmed(value) {
|
|
@@ -327,6 +327,187 @@ async function safeReadJson(response) {
|
|
|
327
327
|
}
|
|
328
328
|
}
|
|
329
329
|
//#endregion
|
|
330
|
+
//#region src/entity/generation-sync.ts
|
|
331
|
+
/**
|
|
332
|
+
* External systems whose asset entities carry a factual Memota identity.
|
|
333
|
+
* Voice results use the speech system; every other medium uses `memota`.
|
|
334
|
+
*/
|
|
335
|
+
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
336
|
+
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
337
|
+
const MAX_COMMIT_ATTEMPTS = 3;
|
|
338
|
+
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
339
|
+
function parseGenerationFacts(value) {
|
|
340
|
+
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
341
|
+
return value.map((item) => {
|
|
342
|
+
if (!isRecord$1(item)) throw new Error("each generation fact must be an object");
|
|
343
|
+
const { outputAssetId, inputAssetIds } = item;
|
|
344
|
+
if (typeof outputAssetId !== "string" || outputAssetId.length === 0 || outputAssetId.trim() !== outputAssetId) throw new Error("generation fact outputAssetId must be a non-empty trimmed string");
|
|
345
|
+
if (!Array.isArray(inputAssetIds)) throw new Error("generation fact inputAssetIds must be an array (explicit [] means text-only)");
|
|
346
|
+
const inputs = inputAssetIds;
|
|
347
|
+
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");
|
|
348
|
+
return {
|
|
349
|
+
outputAssetId,
|
|
350
|
+
inputAssetIds: [...inputs]
|
|
351
|
+
};
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
function planGenerationScope(base, commands, state) {
|
|
355
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
356
|
+
for (const command of commands) if (command.kind === "create-entity" && isMediaAssetVariantKind(command.entity.entity_kind)) touchedIds.add(command.entity.entity_id);
|
|
357
|
+
const beforeByKey = resolveMediaByAssetKey(base);
|
|
358
|
+
const scoped = /* @__PURE__ */ new Set();
|
|
359
|
+
const queryKeys = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const [key, mediaIds] of resolveMediaByAssetKey(state)) {
|
|
361
|
+
const previousIds = new Set(beforeByKey.get(key) ?? []);
|
|
362
|
+
for (const id of mediaIds) {
|
|
363
|
+
if (!touchedIds.has(id) || previousIds.has(id)) continue;
|
|
364
|
+
scoped.add(id);
|
|
365
|
+
queryKeys.add(key);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
scopedMediaIds: scoped,
|
|
370
|
+
queryAssetKeys: [...queryKeys].sort()
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
375
|
+
* factual records. Both endpoints must already exist and match their own Asset
|
|
376
|
+
* identities, and the pair must involve a media Entity the plan
|
|
377
|
+
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
378
|
+
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
379
|
+
* the facts already resolved against the plan's base state is likewise skipped.
|
|
380
|
+
* One-sided facts, text-only records, self pairs, and already-linked pairs are
|
|
381
|
+
* skipped. Duplicate records collapse to one Relation.
|
|
382
|
+
*/
|
|
383
|
+
function planGeneratedRelations(input) {
|
|
384
|
+
const { state, facts } = input;
|
|
385
|
+
const scoped = input.scopedMediaIds;
|
|
386
|
+
const factKeys = new Set(facts.flatMap((fact) => [fact.outputAssetId, ...fact.inputAssetIds]));
|
|
387
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state, factKeys);
|
|
388
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState, factKeys), facts));
|
|
389
|
+
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)));
|
|
390
|
+
const relations = [];
|
|
391
|
+
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) ?? []) {
|
|
392
|
+
if (outputId === inputId) continue;
|
|
393
|
+
if (!scoped.has(outputId) && !scoped.has(inputId)) continue;
|
|
394
|
+
const pair = pairKey(outputId, inputId);
|
|
395
|
+
if (linkedPairs.has(pair) || baseResolvable.has(pair)) continue;
|
|
396
|
+
linkedPairs.add(pair);
|
|
397
|
+
relations.push({
|
|
398
|
+
relation_id: input.newRelationId(),
|
|
399
|
+
relation_kind: "generated",
|
|
400
|
+
endpoint_0_entity_id: outputId,
|
|
401
|
+
endpoint_1_entity_id: inputId,
|
|
402
|
+
metadata: {},
|
|
403
|
+
trace: { synced_by: "generation-sync" }
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return relations;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
410
|
+
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
411
|
+
* commit result is never masked; a successful query that finds nothing is
|
|
412
|
+
* `current`. Asset identities are immutable, so facts are queried once. A
|
|
413
|
+
* revision conflict re-reads current entities and relations, re-plans, and
|
|
414
|
+
* retries within `MAX_COMMIT_ATTEMPTS`; deleted endpoints are never recreated.
|
|
415
|
+
*/
|
|
416
|
+
async function syncGeneratedRelations(input) {
|
|
417
|
+
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
418
|
+
try {
|
|
419
|
+
let state = await client.fetchState();
|
|
420
|
+
let scope = planGenerationScope(baseState, entityCommands, state);
|
|
421
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
422
|
+
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
423
|
+
for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
|
|
424
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
425
|
+
const relations = planGeneratedRelations({
|
|
426
|
+
baseState,
|
|
427
|
+
state,
|
|
428
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
429
|
+
facts,
|
|
430
|
+
newRelationId: mintRelationId
|
|
431
|
+
});
|
|
432
|
+
if (relations.length === 0) return { status: "current" };
|
|
433
|
+
try {
|
|
434
|
+
await client.commit(state.revision, {
|
|
435
|
+
...state,
|
|
436
|
+
relations: [...state.relations, ...relations]
|
|
437
|
+
});
|
|
438
|
+
return {
|
|
439
|
+
status: "applied",
|
|
440
|
+
created_relation_ids: relations.map((relation) => relation.relation_id)
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (!(error instanceof MengineEntityHttpRequestError && error.status === 409) || attempt === MAX_COMMIT_ATTEMPTS) return {
|
|
444
|
+
status: "failed",
|
|
445
|
+
message: `generation lineage sync commit failed: ${errorMessage(error)}`
|
|
446
|
+
};
|
|
447
|
+
state = await client.fetchState();
|
|
448
|
+
scope = planGenerationScope(baseState, entityCommands, state);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return {
|
|
452
|
+
status: "failed",
|
|
453
|
+
message: "generation lineage sync exhausted its retry budget"
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
return {
|
|
457
|
+
status: "failed",
|
|
458
|
+
message: `generation lineage query failed: ${errorMessage(error)}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function assetKeyOf(entity) {
|
|
463
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
|
|
464
|
+
const external = entity.payload?.external;
|
|
465
|
+
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
466
|
+
const { system, key } = external;
|
|
467
|
+
if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
|
|
468
|
+
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
469
|
+
return key;
|
|
470
|
+
}
|
|
471
|
+
/** Media variants own their Asset locator; generation lookup never follows Relations. */
|
|
472
|
+
function resolveMediaByAssetKey(state, factKeys) {
|
|
473
|
+
const systemByKey = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const entity of state.entities) {
|
|
475
|
+
const key = assetKeyOf(entity);
|
|
476
|
+
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
477
|
+
const system = entity.payload.external.system;
|
|
478
|
+
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
479
|
+
systemByKey.set(key, system);
|
|
480
|
+
}
|
|
481
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
482
|
+
for (const entity of state.entities) {
|
|
483
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
484
|
+
const key = assetKeyOf(entity);
|
|
485
|
+
if (key === void 0) continue;
|
|
486
|
+
const matches = resolved.get(key) ?? [];
|
|
487
|
+
matches.push(entity.entity_id);
|
|
488
|
+
resolved.set(key, matches);
|
|
489
|
+
}
|
|
490
|
+
return resolved;
|
|
491
|
+
}
|
|
492
|
+
function pairKey(endpoint0, endpoint1) {
|
|
493
|
+
return `${endpoint0}\u0000${endpoint1}`;
|
|
494
|
+
}
|
|
495
|
+
/** Pair keys the facts already resolve to under the given base bindings. */
|
|
496
|
+
function resolvablePairs(mediaByAssetKey, facts) {
|
|
497
|
+
const pairs = [];
|
|
498
|
+
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));
|
|
499
|
+
return pairs;
|
|
500
|
+
}
|
|
501
|
+
function mintRelationId() {
|
|
502
|
+
return `relation_${randomUUID()}`;
|
|
503
|
+
}
|
|
504
|
+
function errorMessage(error) {
|
|
505
|
+
return error instanceof Error ? error.message : String(error);
|
|
506
|
+
}
|
|
507
|
+
function isRecord$1(value) {
|
|
508
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
330
511
|
//#region src/migration-input.ts
|
|
331
512
|
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
332
513
|
function parseMigrationAssetFacts(value) {
|
|
@@ -430,6 +611,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
430
611
|
" readonly system: 'font-library';",
|
|
431
612
|
" readonly key: string;",
|
|
432
613
|
"}",
|
|
614
|
+
"/**",
|
|
615
|
+
" * One ordered entry of the Caption's segment selection. `segmentId` quotes the",
|
|
616
|
+
" * composed AudioScript's own stable segment identity — a local id quoted by the",
|
|
617
|
+
" * variant, never a peer Entity reference. Text itself is never copied here;",
|
|
618
|
+
" * complete Caption content is assembled through its direct baseEntityIds.",
|
|
619
|
+
" * The optional `textRange` narrows one Segment to an intra-Segment sub-span",
|
|
620
|
+
" * (intra-segment re-segmentation); without it the whole Segment text is selected.",
|
|
621
|
+
" */",
|
|
622
|
+
"export type CaptionSegmentSelection = JsonObject & {",
|
|
623
|
+
" readonly segmentId: string;",
|
|
624
|
+
" readonly textRange?: CaptionTextRange;",
|
|
625
|
+
"};",
|
|
433
626
|
"export interface CaptionStyleFields {",
|
|
434
627
|
" readonly font?: CaptionFontDescriptor;",
|
|
435
628
|
" readonly fontSize?: number;",
|
|
@@ -442,6 +635,24 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
442
635
|
" readonly positionX?: number;",
|
|
443
636
|
" readonly positionY?: number;",
|
|
444
637
|
"}",
|
|
638
|
+
"/**",
|
|
639
|
+
" * Half-open `[start, end)` position window inside one Segment's text, counted",
|
|
640
|
+
" * in Unicode code points (not UTF-16 code units), so a boundary never splits a",
|
|
641
|
+
" * surrogate pair. Positions are non-negative safe integers with `start < end`;",
|
|
642
|
+
" * `end` must not exceed the Segment's code-point length.",
|
|
643
|
+
" */",
|
|
644
|
+
"export interface CaptionTextRange extends JsonObject {",
|
|
645
|
+
" readonly start: number;",
|
|
646
|
+
" readonly end: number;",
|
|
647
|
+
"}",
|
|
648
|
+
"export type CaptionTextSelection = JsonObject & {",
|
|
649
|
+
" segmentId: string;",
|
|
650
|
+
" /** Half-open Unicode code-point range within the selected source segment. */",
|
|
651
|
+
" textRange?: {",
|
|
652
|
+
" start: number;",
|
|
653
|
+
" end: number;",
|
|
654
|
+
" };",
|
|
655
|
+
"};",
|
|
445
656
|
"export type ClipEntityId = EntityId;",
|
|
446
657
|
"export type ClipPlacement =",
|
|
447
658
|
" | {",
|
|
@@ -457,11 +668,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
457
668
|
" readonly hostClipEntityId: string;",
|
|
458
669
|
" readonly anchorOffset: number;",
|
|
459
670
|
" };",
|
|
671
|
+
"export interface ComposedPhoneticContent extends ComposedScriptContent {",
|
|
672
|
+
" phonemeScript?: string;",
|
|
673
|
+
" prosody?: JsonObject;",
|
|
674
|
+
"}",
|
|
675
|
+
"/** Read result only: base text is assembled from the real AudioScript row. */",
|
|
676
|
+
"export interface ComposedScriptContent {",
|
|
677
|
+
" audio_script_entity_id: string;",
|
|
678
|
+
" text: string;",
|
|
679
|
+
" segments: ScriptTextSegment[];",
|
|
680
|
+
"}",
|
|
460
681
|
"export type CreateEntityInput = {",
|
|
461
682
|
" [K in KnownEntityKind]: {",
|
|
462
683
|
" entity_id?: string;",
|
|
463
684
|
" entity_kind: K;",
|
|
464
|
-
" payload:
|
|
685
|
+
" payload: StoredEntityPayload<K>;",
|
|
465
686
|
" };",
|
|
466
687
|
"}[KnownEntityKind];",
|
|
467
688
|
"export interface DeleteBgmInput {",
|
|
@@ -486,19 +707,29 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
486
707
|
" | 'clip-marker'",
|
|
487
708
|
" | 'marker-content'",
|
|
488
709
|
" | 'axvideo-marker'",
|
|
489
|
-
" | 'marker-timeline'
|
|
710
|
+
" | 'marker-timeline'",
|
|
711
|
+
" | 'audio-script-marker';",
|
|
490
712
|
"export interface EntityFacade {",
|
|
713
|
+
" /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */",
|
|
491
714
|
" list(): SandboxEntity[];",
|
|
492
715
|
" get(entityId: string): SandboxEntity | null;",
|
|
493
|
-
" /**
|
|
494
|
-
" findByAssetId(assetId: string): SandboxEntity<
|
|
716
|
+
" /** Find document resources by external Memota asset id, including directly composed media variants. */",
|
|
717
|
+
" findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];",
|
|
718
|
+
" /** Assemble selected Caption text; missing composition is an error. */",
|
|
719
|
+
" readCaptionContent(entityId: string): ComposedScriptContent;",
|
|
720
|
+
" /** Assemble base text and pronunciation fields before generating Voice. */",
|
|
721
|
+
" readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
|
|
495
722
|
" create(input: CreateEntityInput): string;",
|
|
496
|
-
" /**
|
|
723
|
+
" /** Patch assembled fields, routing inherited fields to their declaring entity. */",
|
|
497
724
|
" update(input: UpdateEntityInput): void;",
|
|
725
|
+
" /** Explicitly declare own fields, overriding unambiguous bases without modifying them. Ordinary edits use update. */",
|
|
726
|
+
" declareFields(input: UpdateEntityInput): void;",
|
|
498
727
|
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
499
728
|
" delete(input: DeleteEntityInput): void;",
|
|
500
|
-
" /**
|
|
501
|
-
"
|
|
729
|
+
" /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */",
|
|
730
|
+
" ensureMedia(fact: MediaAssetFact): {",
|
|
731
|
+
" contentEntityId: string;",
|
|
732
|
+
" };",
|
|
502
733
|
"}",
|
|
503
734
|
"export type EntityId = string;",
|
|
504
735
|
"export interface EntityPayloadByKind {",
|
|
@@ -509,12 +740,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
509
740
|
" role?: string;",
|
|
510
741
|
" };",
|
|
511
742
|
" clip: JsonObject;",
|
|
512
|
-
" /**
|
|
743
|
+
" /** Physical resource fields; never a copy of Caption content. */",
|
|
513
744
|
" asset: JsonObject;",
|
|
514
|
-
" video: BoundedNativeSequencePayload;",
|
|
515
|
-
" audio: BoundedNativeSequencePayload;",
|
|
516
|
-
" voice: BoundedNativeSequencePayload;",
|
|
517
|
-
" image: UnboundedConstantSequencePayload;",
|
|
745
|
+
" video: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
746
|
+
" audio: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
747
|
+
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
748
|
+
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
518
749
|
" 'sequence-marker': JsonObject & {",
|
|
519
750
|
" sourceRange: {",
|
|
520
751
|
" start: number;",
|
|
@@ -535,15 +766,27 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
535
766
|
" timeRemapping?: JsonValue;",
|
|
536
767
|
" anchorOffset?: number;",
|
|
537
768
|
" durationPolicy?: 'timeline';",
|
|
769
|
+
" /** Directly assigned AudioScript annotation times; annotation Markers only. */",
|
|
770
|
+
" segmentRanges?: {",
|
|
771
|
+
" segmentId: string;",
|
|
772
|
+
" startMs: number;",
|
|
773
|
+
" endMs: number;",
|
|
774
|
+
" }[];",
|
|
538
775
|
" };",
|
|
539
776
|
" viewport: JsonObject;",
|
|
540
777
|
" 'audio-script': JsonObject & {",
|
|
541
778
|
" segments: ScriptTextSegment[];",
|
|
542
779
|
" };",
|
|
543
780
|
" 'phonetic-script': JsonObject & {",
|
|
544
|
-
"
|
|
781
|
+
" baseEntityIds: string[];",
|
|
782
|
+
" phonemeScript?: string;",
|
|
783
|
+
" prosody?: JsonObject;",
|
|
784
|
+
" };",
|
|
785
|
+
" caption: BoundedNativeSequencePayload & {",
|
|
786
|
+
" baseEntityIds: string[];",
|
|
787
|
+
" selections: CaptionTextSelection[];",
|
|
788
|
+
" style?: JsonObject;",
|
|
545
789
|
" };",
|
|
546
|
-
" caption: BoundedNativeSequencePayload;",
|
|
547
790
|
"}",
|
|
548
791
|
"export interface EntityStoreSnapshot {",
|
|
549
792
|
" revision: number;",
|
|
@@ -555,10 +798,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
555
798
|
" readonly kind: 'image';",
|
|
556
799
|
" readonly storageKey?: string;",
|
|
557
800
|
"}",
|
|
558
|
-
"export interface
|
|
559
|
-
"
|
|
560
|
-
"
|
|
561
|
-
"
|
|
801
|
+
"export interface InsertCaptionClipInput {",
|
|
802
|
+
" readonly timelineEntityId: string;",
|
|
803
|
+
" /** Stable placed caption identity, distinct from the Caption content identity. */",
|
|
804
|
+
" readonly captionClipEntityId?: string;",
|
|
805
|
+
" /** Existing bases composed by this variant; includes an AudioScript text owner. */",
|
|
806
|
+
" readonly baseEntityIds: readonly string[];",
|
|
807
|
+
" /** Ordered selection of the AudioScript segments this Caption displays. */",
|
|
808
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
809
|
+
" /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */",
|
|
810
|
+
" readonly durationMs: number;",
|
|
811
|
+
" readonly style?: CaptionStyleFields;",
|
|
812
|
+
" readonly placement: ClipPlacement;",
|
|
562
813
|
"}",
|
|
563
814
|
"export interface InsertClipInput {",
|
|
564
815
|
" readonly trackEntityId: string;",
|
|
@@ -623,20 +874,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
623
874
|
" | 'marker-timeline'",
|
|
624
875
|
" | 'physical-asset'",
|
|
625
876
|
" | 'generated'",
|
|
626
|
-
" | 'phonetic-script-provenance'",
|
|
627
|
-
" | 'caption-provenance'",
|
|
628
877
|
" | 'caption-alignment'",
|
|
629
878
|
" | 'clip-anchor'",
|
|
630
|
-
" | '
|
|
879
|
+
" | 'phonetic-script-render'",
|
|
880
|
+
" | 'audio-script-source'",
|
|
881
|
+
" | 'audio-script-marker';",
|
|
631
882
|
"export interface LinearClipSpeed {",
|
|
632
883
|
" readonly kind: 'linear';",
|
|
633
884
|
" readonly rate: number;",
|
|
634
885
|
" readonly mode?: string;",
|
|
635
886
|
"}",
|
|
636
|
-
"
|
|
887
|
+
"/** `audio-script-source(script, source)`; the script was transcribed from the source media. */",
|
|
888
|
+
"export interface LinkAudioScriptSourceRelationInput {",
|
|
637
889
|
" relation_id?: string;",
|
|
638
|
-
" output_entity_id: string;",
|
|
639
890
|
" script_entity_id: string;",
|
|
891
|
+
" source_entity_id: string;",
|
|
640
892
|
" trace?: JsonObject;",
|
|
641
893
|
"}",
|
|
642
894
|
"export interface LinkClipAnchorRelationInput {",
|
|
@@ -651,6 +903,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
651
903
|
" input_entity_id: string;",
|
|
652
904
|
" trace?: JsonObject;",
|
|
653
905
|
"}",
|
|
906
|
+
"export interface LinkPhoneticScriptRenderRelationInput {",
|
|
907
|
+
" relation_id?: string;",
|
|
908
|
+
" output_entity_id: string;",
|
|
909
|
+
" phonetic_script_entity_id: string;",
|
|
910
|
+
" trace?: JsonObject;",
|
|
911
|
+
"}",
|
|
654
912
|
"interface LinkRelationBase {",
|
|
655
913
|
" relation_id?: string;",
|
|
656
914
|
" endpoint_0_entity_id: string;",
|
|
@@ -669,17 +927,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
669
927
|
" metadata?: JsonObject;",
|
|
670
928
|
" })",
|
|
671
929
|
" | (LinkRelationBase & {",
|
|
672
|
-
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
673
|
-
" metadata: JsonObject & {",
|
|
674
|
-
" segmentAlignment: JsonValue;",
|
|
675
|
-
" };",
|
|
676
|
-
" })",
|
|
677
|
-
" | (LinkRelationBase & {",
|
|
678
930
|
" relation_kind: 'caption-alignment';",
|
|
679
931
|
" metadata: JsonObject & {",
|
|
680
932
|
" alignment: JsonValue;",
|
|
681
933
|
" };",
|
|
682
934
|
" });",
|
|
935
|
+
"/** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */",
|
|
936
|
+
"export type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;",
|
|
937
|
+
"export type MediaAssetPayload = JsonObject & {",
|
|
938
|
+
" external: {",
|
|
939
|
+
" system: 'memota' | 'memota-speech';",
|
|
940
|
+
" key: string;",
|
|
941
|
+
" };",
|
|
942
|
+
" storageKey?: string;",
|
|
943
|
+
"};",
|
|
683
944
|
"export type MediaClipInsertion =",
|
|
684
945
|
" | {",
|
|
685
946
|
" readonly kind: 'before';",
|
|
@@ -723,14 +984,16 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
723
984
|
" list(): SandboxRelation[];",
|
|
724
985
|
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
725
986
|
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
726
|
-
" /**
|
|
987
|
+
" /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */",
|
|
727
988
|
" link(input: LinkRelationInput): string;",
|
|
728
989
|
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
729
990
|
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
730
991
|
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
731
992
|
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
732
|
-
" /** Author ordered
|
|
733
|
-
"
|
|
993
|
+
" /** Author ordered phonetic-script-render(output,script) without positional endpoint ambiguity. */",
|
|
994
|
+
" linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
|
|
995
|
+
" /** Author ordered audio-script-source(script,source) without positional endpoint ambiguity. */",
|
|
996
|
+
" linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
|
|
734
997
|
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
735
998
|
" unlink(input: UnlinkRelationInput): void;",
|
|
736
999
|
"}",
|
|
@@ -760,10 +1023,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
760
1023
|
" readonly sourceRange: SequenceRange<number>;",
|
|
761
1024
|
" readonly volume?: number;",
|
|
762
1025
|
"}",
|
|
1026
|
+
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
1027
|
+
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
763
1028
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
764
1029
|
" entity_id: string;",
|
|
765
1030
|
" entity_kind: K;",
|
|
766
|
-
" payload:
|
|
1031
|
+
" payload: StoredEntityPayload<K>;",
|
|
767
1032
|
"}",
|
|
768
1033
|
"export interface SandboxRelation {",
|
|
769
1034
|
" relation_id: string;",
|
|
@@ -821,6 +1086,13 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
821
1086
|
" /** Playback gain in decibels. */",
|
|
822
1087
|
" readonly volume: number;",
|
|
823
1088
|
"}",
|
|
1089
|
+
"/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
|
|
1090
|
+
"export type StoredEntityPayload<K extends KnownEntityKind> =",
|
|
1091
|
+
" | EntityPayloadByKind[K]",
|
|
1092
|
+
" | (JsonObject &",
|
|
1093
|
+
" Partial<EntityPayloadByKind[K]> & {",
|
|
1094
|
+
" baseEntityIds: string[];",
|
|
1095
|
+
" });",
|
|
824
1096
|
"export interface TrimClipInput {",
|
|
825
1097
|
" readonly clipEntityId: string;",
|
|
826
1098
|
" readonly sourceRange: SequenceRange<number>;",
|
|
@@ -850,6 +1122,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
850
1122
|
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
851
1123
|
" readonly timeRemapping?: JsonValue | undefined;",
|
|
852
1124
|
"}",
|
|
1125
|
+
"/** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */",
|
|
853
1126
|
"export interface UpdateEntityInput {",
|
|
854
1127
|
" entity_id: string;",
|
|
855
1128
|
" payload: JsonObject;",
|
|
@@ -872,31 +1145,47 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
872
1145
|
" readonly kind: 'voice';",
|
|
873
1146
|
" readonly durationMs: number;",
|
|
874
1147
|
" readonly storageKey: string;",
|
|
875
|
-
"
|
|
1148
|
+
" /** Present for synthesized voice, absent for original recorded audio. */",
|
|
1149
|
+
" readonly voice?: VoiceDescriptor;",
|
|
876
1150
|
"}",
|
|
877
1151
|
"export interface VoiceoverCaptionFact {",
|
|
878
1152
|
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
879
1153
|
" readonly captionClipEntityId: string;",
|
|
880
|
-
"
|
|
1154
|
+
" /** Directly held bases; includes the AudioScript used by the Voice. */",
|
|
1155
|
+
" readonly baseEntityIds: readonly string[];",
|
|
1156
|
+
" /** Ordered selection of AudioScript segments; caption text is never passed inline. */",
|
|
1157
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
881
1158
|
" readonly startMs: number;",
|
|
882
1159
|
" readonly durationMs: number;",
|
|
883
1160
|
" readonly style?: CaptionStyleFields;",
|
|
884
1161
|
"}",
|
|
885
|
-
"export
|
|
1162
|
+
"export type VoiceoverTakeInput = {",
|
|
886
1163
|
" readonly timelineEntityId: string;",
|
|
887
1164
|
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
888
1165
|
" readonly voiceoverClipEntityId: string;",
|
|
889
|
-
" readonly hostClipEntityId: string;",
|
|
890
|
-
" readonly anchorOffset: number;",
|
|
891
1166
|
" readonly media: VoiceMediaAssetFact;",
|
|
892
|
-
" /**
|
|
893
|
-
" readonly
|
|
1167
|
+
" /** Existing pronunciation variant; its composed AudioScript stays the text owner. */",
|
|
1168
|
+
" readonly phoneticScriptEntityId: string;",
|
|
894
1169
|
" readonly volume: number;",
|
|
895
1170
|
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
896
|
-
"}",
|
|
1171
|
+
"} & (",
|
|
1172
|
+
" | {",
|
|
1173
|
+
" readonly placement: ClipPlacement;",
|
|
1174
|
+
" readonly hostClipEntityId?: never;",
|
|
1175
|
+
" readonly anchorOffset?: never;",
|
|
1176
|
+
" }",
|
|
1177
|
+
" | {",
|
|
1178
|
+
" readonly placement?: never;",
|
|
1179
|
+
" readonly hostClipEntityId: string;",
|
|
1180
|
+
" readonly anchorOffset: number;",
|
|
1181
|
+
" }",
|
|
1182
|
+
");",
|
|
897
1183
|
"export interface VoiceoverTakeResult {",
|
|
898
1184
|
" readonly voiceoverClipEntityId: string;",
|
|
899
1185
|
" readonly voiceEntityId: string;",
|
|
1186
|
+
" /** The pronunciation variant the Voice was rendered from. */",
|
|
1187
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1188
|
+
" /** The base-text owner resolved from the PhoneticScript baseEntityIds. */",
|
|
900
1189
|
" readonly audioScriptEntityId: string;",
|
|
901
1190
|
" readonly captionClipEntityIds: readonly string[];",
|
|
902
1191
|
"}",
|
|
@@ -927,6 +1216,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
927
1216
|
" deleteBgm(input: DeleteBgmInput): void;",
|
|
928
1217
|
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
929
1218
|
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1219
|
+
" insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
|
|
930
1220
|
"}",
|
|
931
1221
|
"export interface TimelineApi {",
|
|
932
1222
|
" snapshot(): EntityStoreSnapshot;",
|
|
@@ -949,25 +1239,27 @@ const MEDEO_TOOL_DESCRIPTION = `
|
|
|
949
1239
|
Edit the authoritative Medeo Entity/Relation graph through a deterministic, side-effect-free JavaScript sandbox. Timeline objects and edit targets are Entities, not Memota assets or legacy parts.
|
|
950
1240
|
|
|
951
1241
|
Operations:
|
|
952
|
-
- snapshot: return the Entity/Relation state summary and opaque base version.
|
|
1242
|
+
- snapshot: initialize missing fixed editor structure, then return the Entity/Relation state summary and opaque base version. Initialization is idempotent and may advance the entity revision once; unchanged snapshots do not write.
|
|
953
1243
|
- migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
|
|
954
|
-
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. Asset import, media Entity creation
|
|
1244
|
+
- 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.
|
|
955
1245
|
- 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.
|
|
956
1246
|
|
|
957
1247
|
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. On version mismatch, rerun snapshot and the script; never try to patch a rejected journal by hand.
|
|
958
1248
|
|
|
959
|
-
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts,
|
|
1249
|
+
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts, call entities.ensureMedia(fact), then pass its contentEntityId to edit.insertClip. A raw external asset id or URL is not valid contentEntityId. Image/Video/Audio/Voice are logical variants of Asset: a resource has ONE identity and ONE typed row owning both media fields and external {system,key}/storageKey, with no separate Asset row or physical-asset relation. ensureMedia returns contentEntityId and reuses that single identity by external asset id. Each placement still gets its own Clip and SequenceMarker. Generation lineage is program-synced: after each successful commit the tool connects existing typed Assets from host-recalled generation facts (endpoint 0 output, endpoint 1 input) that the host queries itself — do not pass generation history through inputs. Do not author generated Relations yourself, and never create an Entity merely to backfill or represent lineage; media variants the edit itself legitimately needs are still created normally. Text-only generation has no input and no lineage edge. relations.of(entityId) is endpoint-agnostic.
|
|
960
1250
|
`.trim();
|
|
961
1251
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
962
1252
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
963
1253
|
timeline.snapshot() returns the Entity/Relation graph with its revision, not a legacy VideoDraft. Inspect Timeline, Track, Clip, SequenceMarker and their relations to plan edits.
|
|
964
|
-
Generation lineage and Memota asset facts are host-provided through inputs. Never invent an asset id, Entity kind, or peer Entity id.
|
|
965
|
-
|
|
1254
|
+
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.
|
|
1255
|
+
Use entities.ensureMedia(fact) to get or create the canonical typed Asset. Native media placement helpers use the same resolver. findByAssetId includes directly composed media variants, including Voice. Every Image/Video/Audio/Voice must own its external identity; separate Asset+media graphs are invalid. A typed Asset's external identity cannot be removed or rewritten: to replace its source, ensureMedia for the new Asset and replace the Clip's content. Conflicting facts fail closed. Asset generation itself still creates no editor Entities.
|
|
966
1256
|
For recalled video/audio/voice, create a bounded/native payload whose extent end comes from factual media duration/coordinates in inputs; never fabricate a duration. Image uses unbounded/constant semantics and has no invented end. If required facts are absent, do not create the media Entity yet.
|
|
967
|
-
|
|
1257
|
+
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.
|
|
968
1258
|
The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
969
|
-
|
|
970
|
-
|
|
1259
|
+
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.
|
|
1260
|
+
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.
|
|
1261
|
+
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.
|
|
1262
|
+
Create only the known entity kinds. The host initializes one Timeline and four fixed Tracks before editing; inspect and reuse their IDs from timeline.snapshot(), never create another Timeline or Track for each operation. 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.
|
|
971
1263
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
972
1264
|
`.trim();
|
|
973
1265
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -1064,7 +1356,7 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1064
1356
|
},
|
|
1065
1357
|
inputs: {
|
|
1066
1358
|
type: "object",
|
|
1067
|
-
description: "Pre-materialized, side-effect-free values passed into the script, including recalled
|
|
1359
|
+
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."
|
|
1068
1360
|
},
|
|
1069
1361
|
asset_facts: {
|
|
1070
1362
|
type: "array",
|
|
@@ -1569,11 +1861,12 @@ function createMedeoTool(options) {
|
|
|
1569
1861
|
...peerId !== void 0 ? { peerId } : {}
|
|
1570
1862
|
});
|
|
1571
1863
|
}
|
|
1572
|
-
function rememberPlan(docId, plan) {
|
|
1864
|
+
function rememberPlan(docId, plan, baseState) {
|
|
1573
1865
|
const planId = randomUUID();
|
|
1574
1866
|
plans.set(planId, {
|
|
1575
1867
|
docId,
|
|
1576
|
-
plan
|
|
1868
|
+
plan,
|
|
1869
|
+
...plan.plan_kind === "entities" ? { baseState: baseState && structuredClone(baseState) } : {}
|
|
1577
1870
|
});
|
|
1578
1871
|
while (plans.size > maxPlans) {
|
|
1579
1872
|
const protectedPlanIds = new Set([...pendingPushes.values()].map((pending) => pending.planId));
|
|
@@ -1588,7 +1881,7 @@ function createMedeoTool(options) {
|
|
|
1588
1881
|
const pending = pendingPushes.get(docId);
|
|
1589
1882
|
if (pending != null) throw new Error(`doc ${docId} has an unconfirmed push; retry plan_id ${pending.planId} before continuing`);
|
|
1590
1883
|
}
|
|
1591
|
-
function recordPushResult(docId, planId, plan, result) {
|
|
1884
|
+
function recordPushResult(docId, planId, plan, result, baseState) {
|
|
1592
1885
|
if (result.kind === "unconfirmed") {
|
|
1593
1886
|
pendingPushes.set(docId, plan.plan_kind === "timeline" ? {
|
|
1594
1887
|
kind: "timeline",
|
|
@@ -1598,21 +1891,107 @@ function createMedeoTool(options) {
|
|
|
1598
1891
|
} : {
|
|
1599
1892
|
kind: "entities",
|
|
1600
1893
|
planId,
|
|
1601
|
-
plan
|
|
1894
|
+
plan,
|
|
1895
|
+
...baseState !== void 0 ? { baseState } : {}
|
|
1602
1896
|
});
|
|
1603
1897
|
return;
|
|
1604
1898
|
}
|
|
1605
1899
|
pendingPushes.delete(docId);
|
|
1606
1900
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1607
1901
|
}
|
|
1608
|
-
async function fetchEntityStateForSandbox(docId) {
|
|
1609
|
-
|
|
1902
|
+
async function fetchEntityStateForSandbox(docId, doc, pull) {
|
|
1903
|
+
const client = getEntityClient(docId);
|
|
1904
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1905
|
+
const state = await client.fetchState();
|
|
1906
|
+
if (pendingPushes.has(docId)) return state;
|
|
1907
|
+
const document = doc.snapshot();
|
|
1908
|
+
const hasTimeline = state.entities.some((row) => row.entity_kind === "timeline");
|
|
1909
|
+
const hasLegacyContent = Object.keys(document.part_library ?? {}).length > 0 || (document.tracks ?? []).some((track) => (track.items ?? []).length > 0);
|
|
1910
|
+
if (!hasTimeline && hasLegacyContent) return state;
|
|
1911
|
+
if (!hasTimeline) {
|
|
1912
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1913
|
+
const baseRows = toDslRows(state);
|
|
1914
|
+
const migrated = migrateLegacyTimelineToEntities(document, [], baseRows);
|
|
1915
|
+
try {
|
|
1916
|
+
await getGraphClient(docId).commit({
|
|
1917
|
+
revision: state.revision,
|
|
1918
|
+
rows: baseRows
|
|
1919
|
+
}, migrated, { migrationBaseVv: encodeDocVersionMark(doc.versionMark()) });
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
if (!(error instanceof MengineHttpRequestError) || error.status !== 409) throw new Error(`Editor initialization was not confirmed; retry snapshot to reconcile state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1922
|
+
}
|
|
1923
|
+
pull = await observePull(doc);
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
const sandbox = new EntitySandbox({
|
|
1927
|
+
state,
|
|
1928
|
+
idFactory: (prefix) => `${prefix}_${randomUUID()}`
|
|
1929
|
+
});
|
|
1930
|
+
sandbox.ensureFoundation();
|
|
1931
|
+
if (sandbox.commandCount === 0) return state;
|
|
1932
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1933
|
+
try {
|
|
1934
|
+
const committed = await client.commit(state.revision, sandbox.buildPlan().rows);
|
|
1935
|
+
await doc.pull();
|
|
1936
|
+
return committed;
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
if (!(error instanceof MengineEntityHttpRequestError) || error.status !== 409) throw new Error(`Editor initialization was not confirmed; retry snapshot to reconcile state: ${error instanceof Error ? error.message : String(error)}`);
|
|
1939
|
+
pull = await observePull(doc);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
throw new Error("Editor initialization conflicted repeatedly; take a fresh snapshot");
|
|
1943
|
+
}
|
|
1944
|
+
function getGraphClient(docId) {
|
|
1945
|
+
return new EntityGraphHttpClient({
|
|
1946
|
+
docId,
|
|
1947
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1948
|
+
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, docId) },
|
|
1949
|
+
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, docId) },
|
|
1950
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1951
|
+
});
|
|
1610
1952
|
}
|
|
1611
|
-
async function commitCachedPlan(docId, _doc, plan, validation) {
|
|
1953
|
+
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1612
1954
|
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1613
1955
|
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1614
1956
|
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1615
|
-
|
|
1957
|
+
const client = getEntityClient(docId);
|
|
1958
|
+
const preCommitState = baseState ?? (options.loadGenerationFacts !== void 0 ? await client.fetchState() : void 0);
|
|
1959
|
+
return await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), preCommitState);
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
1963
|
+
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1964
|
+
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1965
|
+
* warning instead. The plan's diff against `baseState` scopes the sync:
|
|
1966
|
+
* newly created media Asset identities — not untouched pairs or placement-only edits.
|
|
1967
|
+
* One-sided facts are skipped silently inside the sync.
|
|
1968
|
+
*/
|
|
1969
|
+
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
1970
|
+
if (result.kind !== "committed" || options.loadGenerationFacts === void 0 || baseState === void 0) return result;
|
|
1971
|
+
let outcome;
|
|
1972
|
+
try {
|
|
1973
|
+
outcome = await syncGeneratedRelations({
|
|
1974
|
+
client: getEntityClient(docId),
|
|
1975
|
+
docId,
|
|
1976
|
+
baseState,
|
|
1977
|
+
entityCommands: plan.entity_commands,
|
|
1978
|
+
loadFacts: options.loadGenerationFacts
|
|
1979
|
+
});
|
|
1980
|
+
} catch (error) {
|
|
1981
|
+
outcome = {
|
|
1982
|
+
status: "failed",
|
|
1983
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1984
|
+
};
|
|
1985
|
+
}
|
|
1986
|
+
const warnings = outcome.status === "failed" ? [{
|
|
1987
|
+
kind: "generation_sync_failed",
|
|
1988
|
+
message: outcome.message ?? "generation lineage sync failed"
|
|
1989
|
+
}] : void 0;
|
|
1990
|
+
return {
|
|
1991
|
+
...result,
|
|
1992
|
+
generation_sync: outcome,
|
|
1993
|
+
...warnings !== void 0 ? { warnings } : {}
|
|
1994
|
+
};
|
|
1616
1995
|
}
|
|
1617
1996
|
async function observePull(doc) {
|
|
1618
1997
|
const result = await doc.pull();
|
|
@@ -1635,7 +2014,7 @@ function createMedeoTool(options) {
|
|
|
1635
2014
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1636
2015
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1637
2016
|
return await runExclusive(docId, async (doc) => {
|
|
1638
|
-
const
|
|
2017
|
+
const entityState = await fetchEntityStateForSandbox(docId, doc, await observePull(doc));
|
|
1639
2018
|
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1640
2019
|
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1641
2020
|
const previousVersion = modelContextVersions.get(baselineKey);
|
|
@@ -1660,7 +2039,8 @@ function createMedeoTool(options) {
|
|
|
1660
2039
|
async function snapshot(input) {
|
|
1661
2040
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1662
2041
|
assertNoPendingPush(input.doc_id);
|
|
1663
|
-
const
|
|
2042
|
+
const pull = await observePull(doc);
|
|
2043
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1664
2044
|
return {
|
|
1665
2045
|
ok: true,
|
|
1666
2046
|
op: "snapshot",
|
|
@@ -1675,13 +2055,7 @@ function createMedeoTool(options) {
|
|
|
1675
2055
|
async function migrate(input) {
|
|
1676
2056
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1677
2057
|
assertNoPendingPush(input.doc_id);
|
|
1678
|
-
const client =
|
|
1679
|
-
docId: input.doc_id,
|
|
1680
|
-
httpOrigin: requiredContext(options.httpOrigin, input.doc_id, "httpOrigin"),
|
|
1681
|
-
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, input.doc_id) },
|
|
1682
|
-
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, input.doc_id) },
|
|
1683
|
-
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1684
|
-
});
|
|
2058
|
+
const client = getGraphClient(input.doc_id);
|
|
1685
2059
|
const base = await client.fetchState();
|
|
1686
2060
|
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
1687
2061
|
ok: true,
|
|
@@ -1717,7 +2091,8 @@ function createMedeoTool(options) {
|
|
|
1717
2091
|
async function run(input) {
|
|
1718
2092
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1719
2093
|
assertNoPendingPush(input.doc_id);
|
|
1720
|
-
const
|
|
2094
|
+
const pull = await observePull(doc);
|
|
2095
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1721
2096
|
const document = doc.snapshot();
|
|
1722
2097
|
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1723
2098
|
const result = await runEditScript({
|
|
@@ -1745,7 +2120,7 @@ function createMedeoTool(options) {
|
|
|
1745
2120
|
...result.plan,
|
|
1746
2121
|
doc_id: input.doc_id
|
|
1747
2122
|
};
|
|
1748
|
-
const planId = rememberPlan(input.doc_id, plan);
|
|
2123
|
+
const planId = rememberPlan(input.doc_id, plan, entityState);
|
|
1749
2124
|
const base = {
|
|
1750
2125
|
ok: true,
|
|
1751
2126
|
op: "run-edit-script",
|
|
@@ -1762,8 +2137,8 @@ function createMedeoTool(options) {
|
|
|
1762
2137
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
1763
2138
|
};
|
|
1764
2139
|
if (input.auto_commit !== true) return base;
|
|
1765
|
-
const commit = await commitCachedPlan(input.doc_id, doc, plan);
|
|
1766
|
-
recordPushResult(input.doc_id, planId, plan, commit);
|
|
2140
|
+
const commit = await commitCachedPlan(input.doc_id, doc, plan, void 0, entityState);
|
|
2141
|
+
recordPushResult(input.doc_id, planId, plan, commit, entityState);
|
|
1767
2142
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(commit));
|
|
1768
2143
|
return {
|
|
1769
2144
|
...base,
|
|
@@ -1779,8 +2154,8 @@ function createMedeoTool(options) {
|
|
|
1779
2154
|
const pending = pendingPushes.get(input.doc_id);
|
|
1780
2155
|
if (pending != null) {
|
|
1781
2156
|
if (pending.planId !== input.plan_id) throw new Error(`doc ${input.doc_id} has an unconfirmed push for plan_id ${pending.planId}; retry it before ${input.plan_id}`);
|
|
1782
|
-
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation);
|
|
1783
|
-
recordPushResult(input.doc_id, input.plan_id, pending.plan, result);
|
|
2157
|
+
const result = pending.kind === "timeline" ? await retryPlanPush(doc, pending.opsApplied) : await commitCachedPlan(input.doc_id, doc, pending.plan, input.validation, pending.baseState);
|
|
2158
|
+
recordPushResult(input.doc_id, input.plan_id, pending.plan, result, pending.kind === "entities" ? pending.baseState : void 0);
|
|
1784
2159
|
const warnings = commitWarnings(result);
|
|
1785
2160
|
return {
|
|
1786
2161
|
ok: true,
|
|
@@ -1797,8 +2172,8 @@ function createMedeoTool(options) {
|
|
|
1797
2172
|
const cached = plans.get(input.plan_id);
|
|
1798
2173
|
if (cached == null || cached.docId !== input.doc_id) throw new Error(`plan_id ${input.plan_id} is not available for doc ${input.doc_id}`);
|
|
1799
2174
|
const pull = cached.plan.plan_kind === "timeline" ? await observePull(doc) : { collaborated: false };
|
|
1800
|
-
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation);
|
|
1801
|
-
recordPushResult(input.doc_id, input.plan_id, cached.plan, result);
|
|
2175
|
+
const result = await commitCachedPlan(input.doc_id, doc, cached.plan, input.validation, cached.baseState);
|
|
2176
|
+
recordPushResult(input.doc_id, input.plan_id, cached.plan, result, cached.baseState);
|
|
1802
2177
|
const warnings = mergeWarnings(pull.warnings, commitWarnings(result));
|
|
1803
2178
|
return {
|
|
1804
2179
|
ok: true,
|