@mengine/medeo-tool 1.2.1-alpha.9 → 1.3.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -14
- package/dist/{entity-contract-DycLxdQ5.d.mts → entity-contract-Dm3AMEC9.d.mts} +89 -33
- package/dist/index.d.mts +76 -76
- package/dist/index.mjs +268 -141
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +145 -43
- package/dist/{script-session-CHyIUBkO.mjs → script-session-C2uQHYt4.mjs} +689 -132
- package/dist/script-session-C2uQHYt4.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +35 -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,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-C2uQHYt4.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
|
|
@@ -242,6 +242,7 @@ var EntityHttpClient = class {
|
|
|
242
242
|
method: "POST",
|
|
243
243
|
body: JSON.stringify({
|
|
244
244
|
expected_revision: expectedRevision,
|
|
245
|
+
audio_script_entity_id: state.audioScriptEntityId,
|
|
245
246
|
rows: {
|
|
246
247
|
entities: state.entities,
|
|
247
248
|
relations: state.relations
|
|
@@ -279,9 +280,12 @@ function toSnapshot(value, expectedDocId) {
|
|
|
279
280
|
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
280
281
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
281
282
|
if (!isRecord$2(value.rows) || !Array.isArray(value.rows.entities) || !Array.isArray(value.rows.relations)) throw new Error("invalid entity-state response rows");
|
|
283
|
+
if (value.audio_script_entity_id !== null && !isTrimmed(value.audio_script_entity_id)) throw new Error("invalid document AudioScript association");
|
|
282
284
|
const response = value;
|
|
285
|
+
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 an AudioScript in this document");
|
|
283
286
|
return {
|
|
284
287
|
revision: response.revision,
|
|
288
|
+
audioScriptEntityId: response.audio_script_entity_id,
|
|
285
289
|
entities: response.rows.entities.map(parseEntity),
|
|
286
290
|
relations: response.rows.relations.map(parseRelation)
|
|
287
291
|
};
|
|
@@ -333,13 +337,6 @@ async function safeReadJson(response) {
|
|
|
333
337
|
* Voice results use the speech system; every other medium uses `memota`.
|
|
334
338
|
*/
|
|
335
339
|
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
340
|
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
344
341
|
const MAX_COMMIT_ATTEMPTS = 3;
|
|
345
342
|
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
@@ -359,42 +356,18 @@ function parseGenerationFacts(value) {
|
|
|
359
356
|
});
|
|
360
357
|
}
|
|
361
358
|
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
|
-
}
|
|
359
|
+
const touchedIds = /* @__PURE__ */ new Set();
|
|
360
|
+
for (const command of commands) if (command.kind === "create-entity" && isMediaAssetVariantKind(command.entity.entity_kind)) touchedIds.add(command.entity.entity_id);
|
|
361
|
+
const beforeByKey = resolveMediaByAssetKey(base);
|
|
362
|
+
const scoped = /* @__PURE__ */ new Set();
|
|
391
363
|
const queryKeys = /* @__PURE__ */ new Set();
|
|
392
|
-
for (const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
364
|
+
for (const [key, mediaIds] of resolveMediaByAssetKey(state)) {
|
|
365
|
+
const previousIds = new Set(beforeByKey.get(key) ?? []);
|
|
366
|
+
for (const id of mediaIds) {
|
|
367
|
+
if (!touchedIds.has(id) || previousIds.has(id)) continue;
|
|
368
|
+
scoped.add(id);
|
|
369
|
+
queryKeys.add(key);
|
|
370
|
+
}
|
|
398
371
|
}
|
|
399
372
|
return {
|
|
400
373
|
scopedMediaIds: scoped,
|
|
@@ -403,8 +376,8 @@ function planGenerationScope(base, commands, state) {
|
|
|
403
376
|
}
|
|
404
377
|
/**
|
|
405
378
|
* Ordered generated(output,input) Relations missing from `state` for the given
|
|
406
|
-
* factual records. Both endpoints must already exist and
|
|
407
|
-
*
|
|
379
|
+
* factual records. Both endpoints must already exist and match their own Asset
|
|
380
|
+
* identities, and the pair must involve a media Entity the plan
|
|
408
381
|
* newly fact-exposed (`scopedMediaIds`): lineage scopes to the commit's diff,
|
|
409
382
|
* so a pair the user deleted between untouched entities stays deleted. A pair
|
|
410
383
|
* the facts already resolved against the plan's base state is likewise skipped.
|
|
@@ -414,8 +387,9 @@ function planGenerationScope(base, commands, state) {
|
|
|
414
387
|
function planGeneratedRelations(input) {
|
|
415
388
|
const { state, facts } = input;
|
|
416
389
|
const scoped = input.scopedMediaIds;
|
|
417
|
-
const
|
|
418
|
-
const
|
|
390
|
+
const factKeys = new Set(facts.flatMap((fact) => [fact.outputAssetId, ...fact.inputAssetIds]));
|
|
391
|
+
const mediaByAssetKey = resolveMediaByAssetKey(state, factKeys);
|
|
392
|
+
const baseResolvable = new Set(resolvablePairs(resolveMediaByAssetKey(input.baseState, factKeys), facts));
|
|
419
393
|
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
394
|
const relations = [];
|
|
421
395
|
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,23 +413,18 @@ function planGeneratedRelations(input) {
|
|
|
439
413
|
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
440
414
|
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
441
415
|
* commit result is never masked; a successful query that finds nothing is
|
|
442
|
-
* `current`.
|
|
443
|
-
*
|
|
444
|
-
* within `MAX_COMMIT_ATTEMPTS
|
|
445
|
-
* must never let a backfillable edge be reported as `current`.
|
|
416
|
+
* `current`. Asset identities are immutable, so facts are queried once. A
|
|
417
|
+
* revision conflict re-reads current entities and relations, re-plans, and
|
|
418
|
+
* retries within `MAX_COMMIT_ATTEMPTS`; deleted endpoints are never recreated.
|
|
446
419
|
*/
|
|
447
420
|
async function syncGeneratedRelations(input) {
|
|
448
421
|
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
449
|
-
let state;
|
|
450
|
-
let facts = [];
|
|
451
|
-
const queriedKeys = /* @__PURE__ */ new Set();
|
|
452
422
|
try {
|
|
453
|
-
state = await client.fetchState();
|
|
423
|
+
let state = await client.fetchState();
|
|
454
424
|
let scope = planGenerationScope(baseState, entityCommands, state);
|
|
425
|
+
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
426
|
+
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
455
427
|
for (let attempt = 1; attempt <= MAX_COMMIT_ATTEMPTS; attempt++) {
|
|
456
|
-
const unseenKeys = scope.queryAssetKeys.filter((key) => !queriedKeys.has(key));
|
|
457
|
-
for (const key of unseenKeys) queriedKeys.add(key);
|
|
458
|
-
if (unseenKeys.length > 0) facts = [...facts, ...parseGenerationFacts(await loadFacts(docId, unseenKeys))];
|
|
459
428
|
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
460
429
|
const relations = planGeneratedRelations({
|
|
461
430
|
baseState,
|
|
@@ -495,7 +464,7 @@ async function syncGeneratedRelations(input) {
|
|
|
495
464
|
}
|
|
496
465
|
}
|
|
497
466
|
function assetKeyOf(entity) {
|
|
498
|
-
if (entity.entity_kind
|
|
467
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
|
|
499
468
|
const external = entity.payload?.external;
|
|
500
469
|
if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
|
|
501
470
|
const { system, key } = external;
|
|
@@ -503,25 +472,24 @@ function assetKeyOf(entity) {
|
|
|
503
472
|
if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
|
|
504
473
|
return key;
|
|
505
474
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
475
|
+
/** Media variants own their Asset locator; generation lookup never follows Relations. */
|
|
476
|
+
function resolveMediaByAssetKey(state, factKeys) {
|
|
477
|
+
const systemByKey = /* @__PURE__ */ new Map();
|
|
478
|
+
for (const entity of state.entities) {
|
|
479
|
+
const key = assetKeyOf(entity);
|
|
480
|
+
if (key === void 0 || !factKeys?.has(key)) continue;
|
|
481
|
+
const system = entity.payload.external.system;
|
|
482
|
+
if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
|
|
483
|
+
systemByKey.set(key, system);
|
|
484
|
+
}
|
|
515
485
|
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);
|
|
486
|
+
for (const entity of state.entities) {
|
|
487
|
+
if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
|
|
488
|
+
const key = assetKeyOf(entity);
|
|
521
489
|
if (key === void 0) continue;
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
490
|
+
const matches = resolved.get(key) ?? [];
|
|
491
|
+
matches.push(entity.entity_id);
|
|
492
|
+
resolved.set(key, matches);
|
|
525
493
|
}
|
|
526
494
|
return resolved;
|
|
527
495
|
}
|
|
@@ -647,6 +615,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
647
615
|
" readonly system: 'font-library';",
|
|
648
616
|
" readonly key: string;",
|
|
649
617
|
"}",
|
|
618
|
+
"/**",
|
|
619
|
+
" * One ordered entry of the Caption's segment selection. `segmentId` quotes the",
|
|
620
|
+
" * composed AudioScript's own stable segment identity — a local id quoted by the",
|
|
621
|
+
" * variant, never a peer Entity reference. Text itself is never copied here;",
|
|
622
|
+
" * complete Caption content is assembled through its direct baseEntityIds.",
|
|
623
|
+
" * The optional `textRange` narrows one Segment to an intra-Segment sub-span",
|
|
624
|
+
" * (intra-segment re-segmentation); without it the whole Segment text is selected.",
|
|
625
|
+
" */",
|
|
626
|
+
"export type CaptionSegmentSelection = JsonObject & {",
|
|
627
|
+
" readonly segmentId: string;",
|
|
628
|
+
" readonly textRange?: CaptionTextRange;",
|
|
629
|
+
"};",
|
|
650
630
|
"export interface CaptionStyleFields {",
|
|
651
631
|
" readonly font?: CaptionFontDescriptor;",
|
|
652
632
|
" readonly fontSize?: number;",
|
|
@@ -659,6 +639,24 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
659
639
|
" readonly positionX?: number;",
|
|
660
640
|
" readonly positionY?: number;",
|
|
661
641
|
"}",
|
|
642
|
+
"/**",
|
|
643
|
+
" * Half-open `[start, end)` position window inside one Segment's text, counted",
|
|
644
|
+
" * in Unicode code points (not UTF-16 code units), so a boundary never splits a",
|
|
645
|
+
" * surrogate pair. Positions are non-negative safe integers with `start < end`;",
|
|
646
|
+
" * `end` must not exceed the Segment's code-point length.",
|
|
647
|
+
" */",
|
|
648
|
+
"export interface CaptionTextRange extends JsonObject {",
|
|
649
|
+
" readonly start: number;",
|
|
650
|
+
" readonly end: number;",
|
|
651
|
+
"}",
|
|
652
|
+
"export type CaptionTextSelection = JsonObject & {",
|
|
653
|
+
" segmentId: string;",
|
|
654
|
+
" /** Half-open Unicode code-point range within the selected source segment. */",
|
|
655
|
+
" textRange?: {",
|
|
656
|
+
" start: number;",
|
|
657
|
+
" end: number;",
|
|
658
|
+
" };",
|
|
659
|
+
"};",
|
|
662
660
|
"export type ClipEntityId = EntityId;",
|
|
663
661
|
"export type ClipPlacement =",
|
|
664
662
|
" | {",
|
|
@@ -674,11 +672,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
674
672
|
" readonly hostClipEntityId: string;",
|
|
675
673
|
" readonly anchorOffset: number;",
|
|
676
674
|
" };",
|
|
675
|
+
"export interface ComposedPhoneticContent extends ComposedScriptContent {",
|
|
676
|
+
" phonemeScript?: string;",
|
|
677
|
+
" prosody?: JsonObject;",
|
|
678
|
+
"}",
|
|
679
|
+
"/** Read result only: base text is assembled from the real AudioScript row. */",
|
|
680
|
+
"export interface ComposedScriptContent {",
|
|
681
|
+
" audio_script_entity_id: string;",
|
|
682
|
+
" text: string;",
|
|
683
|
+
" segments: ScriptTextSegment[];",
|
|
684
|
+
"}",
|
|
677
685
|
"export type CreateEntityInput = {",
|
|
678
686
|
" [K in KnownEntityKind]: {",
|
|
679
687
|
" entity_id?: string;",
|
|
680
688
|
" entity_kind: K;",
|
|
681
|
-
" payload:
|
|
689
|
+
" payload: StoredEntityPayload<K>;",
|
|
682
690
|
" };",
|
|
683
691
|
"}[KnownEntityKind];",
|
|
684
692
|
"export interface DeleteBgmInput {",
|
|
@@ -703,19 +711,29 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
703
711
|
" | 'clip-marker'",
|
|
704
712
|
" | 'marker-content'",
|
|
705
713
|
" | 'axvideo-marker'",
|
|
706
|
-
" | 'marker-timeline'
|
|
714
|
+
" | 'marker-timeline'",
|
|
715
|
+
" | 'audio-script-marker';",
|
|
707
716
|
"export interface EntityFacade {",
|
|
717
|
+
" /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */",
|
|
708
718
|
" list(): SandboxEntity[];",
|
|
709
719
|
" get(entityId: string): SandboxEntity | null;",
|
|
710
|
-
" /**
|
|
711
|
-
" findByAssetId(assetId: string): SandboxEntity<
|
|
720
|
+
" /** Find document resources by external Memota asset id, including directly composed media variants. */",
|
|
721
|
+
" findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];",
|
|
722
|
+
" /** Assemble selected Caption text; missing composition is an error. */",
|
|
723
|
+
" readCaptionContent(entityId: string): ComposedScriptContent;",
|
|
724
|
+
" /** Assemble base text and pronunciation fields before generating Voice. */",
|
|
725
|
+
" readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
|
|
712
726
|
" create(input: CreateEntityInput): string;",
|
|
713
|
-
" /**
|
|
727
|
+
" /** Patch assembled fields, routing inherited fields to their declaring entity. */",
|
|
714
728
|
" update(input: UpdateEntityInput): void;",
|
|
729
|
+
" /** Explicitly declare own fields, overriding unambiguous bases without modifying them. Ordinary edits use update. */",
|
|
730
|
+
" declareFields(input: UpdateEntityInput): void;",
|
|
715
731
|
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
716
732
|
" delete(input: DeleteEntityInput): void;",
|
|
717
|
-
" /**
|
|
718
|
-
"
|
|
733
|
+
" /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */",
|
|
734
|
+
" ensureMedia(fact: MediaAssetFact): {",
|
|
735
|
+
" contentEntityId: string;",
|
|
736
|
+
" };",
|
|
719
737
|
"}",
|
|
720
738
|
"export type EntityId = string;",
|
|
721
739
|
"export interface EntityPayloadByKind {",
|
|
@@ -726,12 +744,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
726
744
|
" role?: string;",
|
|
727
745
|
" };",
|
|
728
746
|
" clip: JsonObject;",
|
|
729
|
-
" /**
|
|
747
|
+
" /** Physical resource fields; never a copy of Caption content. */",
|
|
730
748
|
" asset: JsonObject;",
|
|
731
|
-
" video: BoundedNativeSequencePayload;",
|
|
732
|
-
" audio: BoundedNativeSequencePayload;",
|
|
733
|
-
" voice: BoundedNativeSequencePayload;",
|
|
734
|
-
" image: UnboundedConstantSequencePayload;",
|
|
749
|
+
" video: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
750
|
+
" audio: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
751
|
+
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
752
|
+
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
735
753
|
" 'sequence-marker': JsonObject & {",
|
|
736
754
|
" sourceRange: {",
|
|
737
755
|
" start: number;",
|
|
@@ -752,18 +770,31 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
752
770
|
" timeRemapping?: JsonValue;",
|
|
753
771
|
" anchorOffset?: number;",
|
|
754
772
|
" durationPolicy?: 'timeline';",
|
|
773
|
+
" /** Directly assigned AudioScript annotation times; annotation Markers only. */",
|
|
774
|
+
" segmentRanges?: {",
|
|
775
|
+
" segmentId: string;",
|
|
776
|
+
" startMs: number;",
|
|
777
|
+
" endMs: number;",
|
|
778
|
+
" }[];",
|
|
755
779
|
" };",
|
|
756
780
|
" viewport: JsonObject;",
|
|
757
781
|
" 'audio-script': JsonObject & {",
|
|
758
782
|
" segments: ScriptTextSegment[];",
|
|
759
783
|
" };",
|
|
760
784
|
" 'phonetic-script': JsonObject & {",
|
|
761
|
-
"
|
|
785
|
+
" baseEntityIds: string[];",
|
|
786
|
+
" phonemeScript?: string;",
|
|
787
|
+
" prosody?: JsonObject;",
|
|
788
|
+
" };",
|
|
789
|
+
" caption: BoundedNativeSequencePayload & {",
|
|
790
|
+
" baseEntityIds: string[];",
|
|
791
|
+
" selections: CaptionTextSelection[];",
|
|
792
|
+
" style?: JsonObject;",
|
|
762
793
|
" };",
|
|
763
|
-
" caption: BoundedNativeSequencePayload;",
|
|
764
794
|
"}",
|
|
765
795
|
"export interface EntityStoreSnapshot {",
|
|
766
796
|
" revision: number;",
|
|
797
|
+
" audioScriptEntityId: string | null;",
|
|
767
798
|
" entities: SandboxEntity[];",
|
|
768
799
|
" relations: SandboxRelation[];",
|
|
769
800
|
"}",
|
|
@@ -772,10 +803,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
772
803
|
" readonly kind: 'image';",
|
|
773
804
|
" readonly storageKey?: string;",
|
|
774
805
|
"}",
|
|
775
|
-
"export interface
|
|
776
|
-
"
|
|
777
|
-
"
|
|
778
|
-
"
|
|
806
|
+
"export interface InsertCaptionClipInput {",
|
|
807
|
+
" readonly timelineEntityId: string;",
|
|
808
|
+
" /** Stable placed caption identity, distinct from the Caption content identity. */",
|
|
809
|
+
" readonly captionClipEntityId?: string;",
|
|
810
|
+
" /** Existing bases composed by this variant; includes an AudioScript text owner. */",
|
|
811
|
+
" readonly baseEntityIds: readonly string[];",
|
|
812
|
+
" /** Ordered selection of the AudioScript segments this Caption displays. */",
|
|
813
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
814
|
+
" /** Intrinsic cue length of the Caption entity itself; display comes from the placement. */",
|
|
815
|
+
" readonly durationMs: number;",
|
|
816
|
+
" readonly style?: CaptionStyleFields;",
|
|
817
|
+
" readonly placement: ClipPlacement;",
|
|
779
818
|
"}",
|
|
780
819
|
"export interface InsertClipInput {",
|
|
781
820
|
" readonly trackEntityId: string;",
|
|
@@ -840,20 +879,21 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
840
879
|
" | 'marker-timeline'",
|
|
841
880
|
" | 'physical-asset'",
|
|
842
881
|
" | 'generated'",
|
|
843
|
-
" | 'phonetic-script-provenance'",
|
|
844
|
-
" | 'caption-provenance'",
|
|
845
882
|
" | 'caption-alignment'",
|
|
846
883
|
" | 'clip-anchor'",
|
|
847
|
-
" | '
|
|
884
|
+
" | 'phonetic-script-render'",
|
|
885
|
+
" | 'audio-script-source'",
|
|
886
|
+
" | 'audio-script-marker';",
|
|
848
887
|
"export interface LinearClipSpeed {",
|
|
849
888
|
" readonly kind: 'linear';",
|
|
850
889
|
" readonly rate: number;",
|
|
851
890
|
" readonly mode?: string;",
|
|
852
891
|
"}",
|
|
853
|
-
"
|
|
892
|
+
"/** `audio-script-source(script, source)`; the script was transcribed from the source media. */",
|
|
893
|
+
"export interface LinkAudioScriptSourceRelationInput {",
|
|
854
894
|
" relation_id?: string;",
|
|
855
|
-
" output_entity_id: string;",
|
|
856
895
|
" script_entity_id: string;",
|
|
896
|
+
" source_entity_id: string;",
|
|
857
897
|
" trace?: JsonObject;",
|
|
858
898
|
"}",
|
|
859
899
|
"export interface LinkClipAnchorRelationInput {",
|
|
@@ -868,6 +908,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
868
908
|
" input_entity_id: string;",
|
|
869
909
|
" trace?: JsonObject;",
|
|
870
910
|
"}",
|
|
911
|
+
"export interface LinkPhoneticScriptRenderRelationInput {",
|
|
912
|
+
" relation_id?: string;",
|
|
913
|
+
" output_entity_id: string;",
|
|
914
|
+
" phonetic_script_entity_id: string;",
|
|
915
|
+
" trace?: JsonObject;",
|
|
916
|
+
"}",
|
|
871
917
|
"interface LinkRelationBase {",
|
|
872
918
|
" relation_id?: string;",
|
|
873
919
|
" endpoint_0_entity_id: string;",
|
|
@@ -886,17 +932,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
886
932
|
" metadata?: JsonObject;",
|
|
887
933
|
" })",
|
|
888
934
|
" | (LinkRelationBase & {",
|
|
889
|
-
" relation_kind: 'phonetic-script-provenance' | 'caption-provenance';",
|
|
890
|
-
" metadata: JsonObject & {",
|
|
891
|
-
" segmentAlignment: JsonValue;",
|
|
892
|
-
" };",
|
|
893
|
-
" })",
|
|
894
|
-
" | (LinkRelationBase & {",
|
|
895
935
|
" relation_kind: 'caption-alignment';",
|
|
896
936
|
" metadata: JsonObject & {",
|
|
897
937
|
" alignment: JsonValue;",
|
|
898
938
|
" };",
|
|
899
939
|
" });",
|
|
940
|
+
"/** Facts resolved from media storage. A trim window never substitutes for intrinsic duration. */",
|
|
941
|
+
"export type MediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact | AudioMediaAssetFact | VoiceMediaAssetFact;",
|
|
942
|
+
"export type MediaAssetPayload = JsonObject & {",
|
|
943
|
+
" external: {",
|
|
944
|
+
" system: 'memota' | 'memota-speech';",
|
|
945
|
+
" key: string;",
|
|
946
|
+
" };",
|
|
947
|
+
" storageKey?: string;",
|
|
948
|
+
"};",
|
|
900
949
|
"export type MediaClipInsertion =",
|
|
901
950
|
" | {",
|
|
902
951
|
" readonly kind: 'before';",
|
|
@@ -940,14 +989,16 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
940
989
|
" list(): SandboxRelation[];",
|
|
941
990
|
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
942
991
|
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
943
|
-
" /**
|
|
992
|
+
" /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */",
|
|
944
993
|
" link(input: LinkRelationInput): string;",
|
|
945
994
|
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
946
995
|
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
947
996
|
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
948
997
|
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
949
|
-
" /** Author ordered
|
|
950
|
-
"
|
|
998
|
+
" /** Author ordered phonetic-script-render(output,script) without positional endpoint ambiguity. */",
|
|
999
|
+
" linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
|
|
1000
|
+
" /** Author ordered audio-script-source(script,source) without positional endpoint ambiguity. */",
|
|
1001
|
+
" linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
|
|
951
1002
|
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
952
1003
|
" unlink(input: UnlinkRelationInput): void;",
|
|
953
1004
|
"}",
|
|
@@ -977,10 +1028,12 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
977
1028
|
" readonly sourceRange: SequenceRange<number>;",
|
|
978
1029
|
" readonly volume?: number;",
|
|
979
1030
|
"}",
|
|
1031
|
+
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
1032
|
+
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
980
1033
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
981
1034
|
" entity_id: string;",
|
|
982
1035
|
" entity_kind: K;",
|
|
983
|
-
" payload:
|
|
1036
|
+
" payload: StoredEntityPayload<K>;",
|
|
984
1037
|
"}",
|
|
985
1038
|
"export interface SandboxRelation {",
|
|
986
1039
|
" relation_id: string;",
|
|
@@ -1038,6 +1091,13 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1038
1091
|
" /** Playback gain in decibels. */",
|
|
1039
1092
|
" readonly volume: number;",
|
|
1040
1093
|
"}",
|
|
1094
|
+
"/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
|
|
1095
|
+
"export type StoredEntityPayload<K extends KnownEntityKind> =",
|
|
1096
|
+
" | EntityPayloadByKind[K]",
|
|
1097
|
+
" | (JsonObject &",
|
|
1098
|
+
" Partial<EntityPayloadByKind[K]> & {",
|
|
1099
|
+
" baseEntityIds: string[];",
|
|
1100
|
+
" });",
|
|
1041
1101
|
"export interface TrimClipInput {",
|
|
1042
1102
|
" readonly clipEntityId: string;",
|
|
1043
1103
|
" readonly sourceRange: SequenceRange<number>;",
|
|
@@ -1067,6 +1127,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1067
1127
|
" /** Passing `undefined` explicitly removes the optional remapping value. */",
|
|
1068
1128
|
" readonly timeRemapping?: JsonValue | undefined;",
|
|
1069
1129
|
"}",
|
|
1130
|
+
"/** Patch supplied fields on the assembled entity; omitted fields remain unchanged. */",
|
|
1070
1131
|
"export interface UpdateEntityInput {",
|
|
1071
1132
|
" entity_id: string;",
|
|
1072
1133
|
" payload: JsonObject;",
|
|
@@ -1089,36 +1150,54 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1089
1150
|
" readonly kind: 'voice';",
|
|
1090
1151
|
" readonly durationMs: number;",
|
|
1091
1152
|
" readonly storageKey: string;",
|
|
1092
|
-
"
|
|
1153
|
+
" /** Present for synthesized voice, absent for original recorded audio. */",
|
|
1154
|
+
" readonly voice?: VoiceDescriptor;",
|
|
1093
1155
|
"}",
|
|
1094
1156
|
"export interface VoiceoverCaptionFact {",
|
|
1095
1157
|
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
1096
1158
|
" readonly captionClipEntityId: string;",
|
|
1097
|
-
"
|
|
1159
|
+
" /** Directly held bases; includes the AudioScript used by the Voice. */",
|
|
1160
|
+
" readonly baseEntityIds: readonly string[];",
|
|
1161
|
+
" /** Ordered selection of AudioScript segments; caption text is never passed inline. */",
|
|
1162
|
+
" readonly selections: readonly CaptionSegmentSelection[];",
|
|
1098
1163
|
" readonly startMs: number;",
|
|
1099
1164
|
" readonly durationMs: number;",
|
|
1100
1165
|
" readonly style?: CaptionStyleFields;",
|
|
1101
1166
|
"}",
|
|
1102
|
-
"export
|
|
1167
|
+
"export type VoiceoverTakeInput = {",
|
|
1103
1168
|
" readonly timelineEntityId: string;",
|
|
1104
1169
|
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
1105
1170
|
" readonly voiceoverClipEntityId: string;",
|
|
1106
|
-
" readonly hostClipEntityId: string;",
|
|
1107
|
-
" readonly anchorOffset: number;",
|
|
1108
1171
|
" readonly media: VoiceMediaAssetFact;",
|
|
1109
|
-
" /**
|
|
1110
|
-
" readonly
|
|
1172
|
+
" /** Existing pronunciation variant; its composed AudioScript stays the text owner. */",
|
|
1173
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1111
1174
|
" readonly volume: number;",
|
|
1112
1175
|
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
1113
|
-
"}",
|
|
1176
|
+
"} & (",
|
|
1177
|
+
" | {",
|
|
1178
|
+
" readonly placement: ClipPlacement;",
|
|
1179
|
+
" readonly hostClipEntityId?: never;",
|
|
1180
|
+
" readonly anchorOffset?: never;",
|
|
1181
|
+
" }",
|
|
1182
|
+
" | {",
|
|
1183
|
+
" readonly placement?: never;",
|
|
1184
|
+
" readonly hostClipEntityId: string;",
|
|
1185
|
+
" readonly anchorOffset: number;",
|
|
1186
|
+
" }",
|
|
1187
|
+
");",
|
|
1114
1188
|
"export interface VoiceoverTakeResult {",
|
|
1115
1189
|
" readonly voiceoverClipEntityId: string;",
|
|
1116
1190
|
" readonly voiceEntityId: string;",
|
|
1191
|
+
" /** The pronunciation variant the Voice was rendered from. */",
|
|
1192
|
+
" readonly phoneticScriptEntityId: string;",
|
|
1193
|
+
" /** The base-text owner resolved from the PhoneticScript baseEntityIds. */",
|
|
1117
1194
|
" readonly audioScriptEntityId: string;",
|
|
1118
1195
|
" readonly captionClipEntityIds: readonly string[];",
|
|
1119
1196
|
"}",
|
|
1120
1197
|
"/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
|
|
1121
1198
|
"export interface EditApi {",
|
|
1199
|
+
" /** Select the document AudioScript, or clear the optional association with null. */",
|
|
1200
|
+
" setDocumentAudioScript(entityId: string | null): void;",
|
|
1122
1201
|
" insertClip(input: InsertClipInput): ClipEntityId;",
|
|
1123
1202
|
" insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
|
|
1124
1203
|
" updateClipMarker(input: UpdateClipMarkerInput): void;",
|
|
@@ -1144,6 +1223,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1144
1223
|
" deleteBgm(input: DeleteBgmInput): void;",
|
|
1145
1224
|
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
1146
1225
|
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1226
|
+
" insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
|
|
1147
1227
|
"}",
|
|
1148
1228
|
"export interface TimelineApi {",
|
|
1149
1229
|
" snapshot(): EntityStoreSnapshot;",
|
|
@@ -1166,25 +1246,27 @@ const MEDEO_TOOL_DESCRIPTION = `
|
|
|
1166
1246
|
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
1247
|
|
|
1168
1248
|
Operations:
|
|
1169
|
-
- snapshot: return the Entity/Relation state summary and opaque base version.
|
|
1249
|
+
- 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.
|
|
1170
1250
|
- migrate-legacy: explicitly migrate an existing legacy timeline using recalled asset_facts. MEngine reads the canonical document and version, verifies that editing facts are preserved, and commits migration alone. Then take a fresh snapshot before any edit; never pass a caller-created legacy snapshot or version.
|
|
1171
1251
|
- 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
1252
|
- 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.
|
|
1173
1253
|
|
|
1174
1254
|
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.
|
|
1175
1255
|
|
|
1176
|
-
Generating an Asset alone does not require an Entity. Using that resource in the editor DOES: recall the Asset facts,
|
|
1256
|
+
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
1257
|
`.trim();
|
|
1178
1258
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1179
1259
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1180
1260
|
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
1261
|
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
|
-
|
|
1262
|
+
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
1263
|
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
|
-
|
|
1264
|
+
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.
|
|
1185
1265
|
The compatibility reader supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
1186
|
-
|
|
1187
|
-
|
|
1266
|
+
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.
|
|
1267
|
+
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.
|
|
1268
|
+
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.
|
|
1269
|
+
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.
|
|
1188
1270
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1189
1271
|
`.trim();
|
|
1190
1272
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -1539,7 +1621,7 @@ function renderEntitySnapshot(state) {
|
|
|
1539
1621
|
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1540
1622
|
const shown = rows.slice(0, 200);
|
|
1541
1623
|
return [
|
|
1542
|
-
`Entity revision=${state.revision} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1624
|
+
`Entity revision=${state.revision} audioScriptEntityId=${JSON.stringify(state.audioScriptEntityId)} entities=${state.entities.length} relations=${state.relations.length}`,
|
|
1543
1625
|
...shown,
|
|
1544
1626
|
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1545
1627
|
].join("\n");
|
|
@@ -1631,6 +1713,7 @@ function commitWarnings(result) {
|
|
|
1631
1713
|
}
|
|
1632
1714
|
function entityRowsEquivalent(left, right) {
|
|
1633
1715
|
const normalize = (state) => ({
|
|
1716
|
+
audioScriptEntityId: state.audioScriptEntityId,
|
|
1634
1717
|
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1635
1718
|
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1636
1719
|
});
|
|
@@ -1824,8 +1907,57 @@ function createMedeoTool(options) {
|
|
|
1824
1907
|
pendingPushes.delete(docId);
|
|
1825
1908
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1826
1909
|
}
|
|
1827
|
-
async function fetchEntityStateForSandbox(docId) {
|
|
1828
|
-
|
|
1910
|
+
async function fetchEntityStateForSandbox(docId, doc, pull) {
|
|
1911
|
+
const client = getEntityClient(docId);
|
|
1912
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1913
|
+
const state = await client.fetchState();
|
|
1914
|
+
if (pendingPushes.has(docId)) return state;
|
|
1915
|
+
const document = doc.snapshot();
|
|
1916
|
+
const hasTimeline = state.entities.some((row) => row.entity_kind === "timeline");
|
|
1917
|
+
const hasLegacyContent = Object.keys(document.part_library ?? {}).length > 0 || (document.tracks ?? []).some((track) => (track.items ?? []).length > 0);
|
|
1918
|
+
if (!hasTimeline && hasLegacyContent) return state;
|
|
1919
|
+
if (!hasTimeline) {
|
|
1920
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1921
|
+
const baseRows = toDslRows(state);
|
|
1922
|
+
const migrated = migrateLegacyTimelineToEntities(document, [], baseRows);
|
|
1923
|
+
try {
|
|
1924
|
+
await getGraphClient(docId).commit({
|
|
1925
|
+
revision: state.revision,
|
|
1926
|
+
audioScriptEntityId: state.audioScriptEntityId,
|
|
1927
|
+
rows: baseRows
|
|
1928
|
+
}, migrated, { migrationBaseVv: encodeDocVersionMark(doc.versionMark()) });
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
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)}`);
|
|
1931
|
+
}
|
|
1932
|
+
pull = await observePull(doc);
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
const sandbox = new EntitySandbox({
|
|
1936
|
+
state,
|
|
1937
|
+
idFactory: (prefix) => `${prefix}_${randomUUID()}`
|
|
1938
|
+
});
|
|
1939
|
+
sandbox.ensureFoundation();
|
|
1940
|
+
if (sandbox.commandCount === 0) return state;
|
|
1941
|
+
if (pull.warnings !== void 0) throw new Error("Editor initialization requires a fresh canonical snapshot; retry snapshot");
|
|
1942
|
+
try {
|
|
1943
|
+
const committed = await client.commit(state.revision, sandbox.buildPlan().rows);
|
|
1944
|
+
await doc.pull();
|
|
1945
|
+
return committed;
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
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)}`);
|
|
1948
|
+
pull = await observePull(doc);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
throw new Error("Editor initialization conflicted repeatedly; take a fresh snapshot");
|
|
1952
|
+
}
|
|
1953
|
+
function getGraphClient(docId) {
|
|
1954
|
+
return new EntityGraphHttpClient({
|
|
1955
|
+
docId,
|
|
1956
|
+
httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
|
|
1957
|
+
...options.authToken === void 0 ? {} : { authToken: () => optionalContext(options.authToken, docId) },
|
|
1958
|
+
...options.userId === void 0 ? {} : { userId: () => optionalContext(options.userId, docId) },
|
|
1959
|
+
...options.fetchImpl === void 0 ? {} : { fetchImpl: options.fetchImpl }
|
|
1960
|
+
});
|
|
1829
1961
|
}
|
|
1830
1962
|
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1831
1963
|
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
@@ -1840,8 +1972,7 @@ function createMedeoTool(options) {
|
|
|
1840
1972
|
* from host-recalled lineage. The commit is already durable, so a sync
|
|
1841
1973
|
* failure never fails the op; it is attached to the result and surfaced as a
|
|
1842
1974
|
* 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.
|
|
1975
|
+
* newly created media Asset identities — not untouched pairs or placement-only edits.
|
|
1845
1976
|
* One-sided facts are skipped silently inside the sync.
|
|
1846
1977
|
*/
|
|
1847
1978
|
async function attachGenerationSync(docId, plan, result, baseState) {
|
|
@@ -1892,7 +2023,7 @@ function createMedeoTool(options) {
|
|
|
1892
2023
|
if (docId.length === 0) throw new Error("doc_id must be a non-empty string");
|
|
1893
2024
|
if (contextId.length === 0) throw new Error("context_id must be a non-empty string");
|
|
1894
2025
|
return await runExclusive(docId, async (doc) => {
|
|
1895
|
-
const
|
|
2026
|
+
const entityState = await fetchEntityStateForSandbox(docId, doc, await observePull(doc));
|
|
1896
2027
|
const documentVersion = `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`;
|
|
1897
2028
|
const baselineKey = `${contextId}\u0000${docId}`;
|
|
1898
2029
|
const previousVersion = modelContextVersions.get(baselineKey);
|
|
@@ -1917,7 +2048,8 @@ function createMedeoTool(options) {
|
|
|
1917
2048
|
async function snapshot(input) {
|
|
1918
2049
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1919
2050
|
assertNoPendingPush(input.doc_id);
|
|
1920
|
-
const
|
|
2051
|
+
const pull = await observePull(doc);
|
|
2052
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1921
2053
|
return {
|
|
1922
2054
|
ok: true,
|
|
1923
2055
|
op: "snapshot",
|
|
@@ -1932,13 +2064,7 @@ function createMedeoTool(options) {
|
|
|
1932
2064
|
async function migrate(input) {
|
|
1933
2065
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1934
2066
|
assertNoPendingPush(input.doc_id);
|
|
1935
|
-
const client =
|
|
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
|
-
});
|
|
2067
|
+
const client = getGraphClient(input.doc_id);
|
|
1942
2068
|
const base = await client.fetchState();
|
|
1943
2069
|
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
1944
2070
|
ok: true,
|
|
@@ -1974,7 +2100,8 @@ function createMedeoTool(options) {
|
|
|
1974
2100
|
async function run(input) {
|
|
1975
2101
|
return runExclusive(input.doc_id, async (doc) => {
|
|
1976
2102
|
assertNoPendingPush(input.doc_id);
|
|
1977
|
-
const
|
|
2103
|
+
const pull = await observePull(doc);
|
|
2104
|
+
const entityState = await fetchEntityStateForSandbox(input.doc_id, doc, pull);
|
|
1978
2105
|
const document = doc.snapshot();
|
|
1979
2106
|
const baseVersion = encodeDocVersionMark(doc.versionMark());
|
|
1980
2107
|
const result = await runEditScript({
|