@mengine/medeo-tool 1.3.1-alpha.8 → 1.4.1-alpha.0
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 +26 -40
- package/dist/{entity-contract-Dm3AMEC9.d.mts → entity-contract-DHasvrhq.d.mts} +4 -4
- package/dist/index.d.mts +66 -142
- package/dist/index.mjs +369 -621
- package/dist/index.mjs.map +1 -1
- package/dist/sandbox-api.d.mts +43 -186
- package/dist/{script-session-C2uQHYt4.mjs → script-session-DTq_VPEA.mjs} +112 -33
- package/dist/script-session-DTq_VPEA.mjs.map +1 -0
- package/dist/worker-entry.d.mts +1 -1
- package/dist/worker-entry.mjs +13 -14
- package/dist/worker-entry.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/script-session-C2uQHYt4.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
1
|
+
import { a as createEntityId, c as collectAffectedPartIds, i as businessState, l as renderPreview, n as EntitySandbox, o as createRelationId, s as isMediaAssetVariantKind, t as EditSandboxSession, u as renderCompactProjection } from "./script-session-DTq_VPEA.mjs";
|
|
2
|
+
import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, encodeDocVersionMark, ensureEditorFoundation, replayJournal, toVideoDocument } from "@mengine/medeo-client";
|
|
3
3
|
import { Worker } from "node:worker_threads";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
5
|
//#region src/sandbox/node-host.ts
|
|
6
6
|
const DEFAULT_TIMEOUT_MS = 2e3;
|
|
7
7
|
const DEFAULT_MEMORY_MB = 256;
|
|
@@ -122,6 +122,7 @@ function runEditScript(options) {
|
|
|
122
122
|
doc_id: options.document.meta.draft_id ?? "",
|
|
123
123
|
base_version: options.baseVersion,
|
|
124
124
|
ops: ops.slice(),
|
|
125
|
+
...message.loroUpdate ? { loro_update: message.loroUpdate } : {},
|
|
125
126
|
entity_base_revision: message.entityBaseRevision,
|
|
126
127
|
entity_commands: entityCommands.slice(),
|
|
127
128
|
...message.entityRows !== void 0 ? { entity_rows: message.entityRows } : {},
|
|
@@ -179,6 +180,78 @@ function runEditScript(options) {
|
|
|
179
180
|
});
|
|
180
181
|
}
|
|
181
182
|
//#endregion
|
|
183
|
+
//#region src/entity/caption-asset-assembly.ts
|
|
184
|
+
/** Assemble optional infrastructure after business execution, using exact entity identity. */
|
|
185
|
+
async function assembleCaptionAssets(input) {
|
|
186
|
+
try {
|
|
187
|
+
const state = await input.client.fetchState();
|
|
188
|
+
const candidates = state.entities.filter((entity) => entity.entity_kind === "caption" && !state.relations.some((relation) => relation.relation_kind === "physical-asset" && (relation.endpoint_0_entity_id === entity.entity_id || relation.endpoint_1_entity_id === entity.entity_id)));
|
|
189
|
+
if (!candidates.length) return { status: "current" };
|
|
190
|
+
const ids = new Set(candidates.map((entity) => entity.entity_id));
|
|
191
|
+
const facts = await input.loadAssets(input.docId, [...ids]);
|
|
192
|
+
const entities = [...state.entities];
|
|
193
|
+
const relations = [...state.relations];
|
|
194
|
+
const bound = /* @__PURE__ */ new Map();
|
|
195
|
+
for (const fact of facts) {
|
|
196
|
+
if (!ids.has(fact.captionEntityId) || !trimmed(fact.assetId) || !trimmed(fact.storageKey)) throw new Error("Caption Asset fact must identify a requested Caption and a real Asset locator");
|
|
197
|
+
const previous = bound.get(fact.captionEntityId);
|
|
198
|
+
if (previous !== void 0) {
|
|
199
|
+
if (previous !== fact.assetId) throw new Error(`Conflicting Assets for Caption ${fact.captionEntityId}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
bound.set(fact.captionEntityId, fact.assetId);
|
|
203
|
+
const matches = entities.filter((entity) => {
|
|
204
|
+
const external = entity.payload.external;
|
|
205
|
+
return external !== null && typeof external === "object" && !Array.isArray(external) && external.system === "memota" && external.key === fact.assetId;
|
|
206
|
+
});
|
|
207
|
+
if (matches.length > 1 || matches[0] && matches[0].entity_kind !== "asset") throw new Error(`Conflicting resource identity for Caption Asset ${fact.assetId}`);
|
|
208
|
+
let asset = matches[0];
|
|
209
|
+
if (asset && asset.payload.storageKey !== fact.storageKey) throw new Error(`Conflicting storage key for Caption Asset ${fact.assetId}`);
|
|
210
|
+
if (!asset) {
|
|
211
|
+
asset = {
|
|
212
|
+
entity_id: stableId$1("asset", fact.assetId),
|
|
213
|
+
entity_kind: "asset",
|
|
214
|
+
payload: {
|
|
215
|
+
external: {
|
|
216
|
+
system: "memota",
|
|
217
|
+
key: fact.assetId
|
|
218
|
+
},
|
|
219
|
+
storageKey: fact.storageKey
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
if (entities.some((entity) => entity.entity_id === asset.entity_id)) throw new Error("Caption Asset identity collision");
|
|
223
|
+
entities.push(asset);
|
|
224
|
+
}
|
|
225
|
+
relations.push({
|
|
226
|
+
relation_id: stableId$1("relation", fact.captionEntityId, asset.entity_id),
|
|
227
|
+
relation_kind: "physical-asset",
|
|
228
|
+
endpoint_0_entity_id: fact.captionEntityId,
|
|
229
|
+
endpoint_1_entity_id: asset.entity_id,
|
|
230
|
+
metadata: {},
|
|
231
|
+
trace: {}
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (!bound.size) return { status: "current" };
|
|
235
|
+
await input.client.commit(state.revision, {
|
|
236
|
+
...state,
|
|
237
|
+
entities,
|
|
238
|
+
relations
|
|
239
|
+
});
|
|
240
|
+
return { status: "applied" };
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
status: "failed",
|
|
244
|
+
message: error instanceof Error ? error.message : String(error)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function stableId$1(prefix, ...parts) {
|
|
249
|
+
return `${prefix}_${createHash("sha256").update(JSON.stringify(parts)).digest("hex")}`;
|
|
250
|
+
}
|
|
251
|
+
function trimmed(value) {
|
|
252
|
+
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
253
|
+
}
|
|
254
|
+
//#endregion
|
|
182
255
|
//#region src/entity/entity-contract.ts
|
|
183
256
|
const KNOWN_ENTITY_KINDS = [
|
|
184
257
|
"axvideo",
|
|
@@ -226,7 +299,7 @@ var MengineEntityHttpRequestError = class extends Error {
|
|
|
226
299
|
this.name = "MengineEntityHttpRequestError";
|
|
227
300
|
}
|
|
228
301
|
};
|
|
229
|
-
/** Narrow authenticated client for the entity
|
|
302
|
+
/** Narrow authenticated client for the Loro entity endpoint. */
|
|
230
303
|
var EntityHttpClient = class {
|
|
231
304
|
options;
|
|
232
305
|
fetchImpl;
|
|
@@ -237,19 +310,30 @@ var EntityHttpClient = class {
|
|
|
237
310
|
async fetchState() {
|
|
238
311
|
return toSnapshot(await this.requestJson({ method: "GET" }), this.options.docId);
|
|
239
312
|
}
|
|
240
|
-
async commit(
|
|
313
|
+
async commit(_transportSequence, state, _deletions = {}) {
|
|
314
|
+
if (!state.loroSnapshot) throw new Error("Entity edit is missing its causal Loro baseline");
|
|
315
|
+
const rows = {
|
|
316
|
+
entities: state.entities.map((row) => ({
|
|
317
|
+
entityId: createEntityId(row.entity_id),
|
|
318
|
+
entityKind: row.entity_kind,
|
|
319
|
+
payload: row.payload
|
|
320
|
+
})),
|
|
321
|
+
relations: state.relations.map((row) => ({
|
|
322
|
+
relationId: createRelationId(row.relation_id),
|
|
323
|
+
relationKind: row.relation_kind,
|
|
324
|
+
endpoint0EntityId: createEntityId(row.endpoint_0_entity_id),
|
|
325
|
+
endpoint1EntityId: createEntityId(row.endpoint_1_entity_id),
|
|
326
|
+
metadata: row.metadata,
|
|
327
|
+
trace: row.trace
|
|
328
|
+
}))
|
|
329
|
+
};
|
|
330
|
+
const compiled = compileEntityRows(base64ToBytes(state.loroSnapshot), rows);
|
|
331
|
+
return this.commitUpdate(bytesToBase64(compiled.update));
|
|
332
|
+
}
|
|
333
|
+
async commitUpdate(update) {
|
|
241
334
|
return toSnapshot(await this.requestJson({
|
|
242
335
|
method: "POST",
|
|
243
|
-
body: JSON.stringify({
|
|
244
|
-
expected_revision: expectedRevision,
|
|
245
|
-
audio_script_entity_id: state.audioScriptEntityId,
|
|
246
|
-
rows: {
|
|
247
|
-
entities: state.entities,
|
|
248
|
-
relations: state.relations
|
|
249
|
-
},
|
|
250
|
-
deleted_entity_ids: [...deletions.deleted_entity_ids ?? []],
|
|
251
|
-
deleted_relation_ids: [...deletions.deleted_relation_ids ?? []]
|
|
252
|
-
})
|
|
336
|
+
body: JSON.stringify({ update })
|
|
253
337
|
}), this.options.docId);
|
|
254
338
|
}
|
|
255
339
|
async requestJson(init) {
|
|
@@ -280,10 +364,13 @@ function toSnapshot(value, expectedDocId) {
|
|
|
280
364
|
if (!isRecord$2(value) || typeof value.doc_id !== "string" || !isNonNegativeInteger(value.revision)) throw new Error("invalid entity-state response envelope");
|
|
281
365
|
if (value.doc_id !== expectedDocId) throw new Error(`entity-state response doc_id mismatch: expected "${expectedDocId}"`);
|
|
282
366
|
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
|
|
367
|
+
if (value.audio_script_entity_id === null) throw new Error("Document AudioScript is not initialized");
|
|
368
|
+
if (!isTrimmed(value.audio_script_entity_id)) throw new Error("invalid document AudioScript identity");
|
|
369
|
+
if (typeof value.loro_snapshot !== "string") throw new Error("Missing causal Loro snapshot");
|
|
284
370
|
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
|
|
371
|
+
if (response.audio_script_entity_id !== null && !response.rows.entities.some((entity) => entity.entity_id === response.audio_script_entity_id && entity.entity_kind === "audio-script")) throw new Error("Document AudioScript must name the project AudioScript");
|
|
286
372
|
return {
|
|
373
|
+
loroSnapshot: response.loro_snapshot,
|
|
287
374
|
revision: response.revision,
|
|
288
375
|
audioScriptEntityId: response.audio_script_entity_id,
|
|
289
376
|
entities: response.rows.entities.map(parseEntity),
|
|
@@ -337,8 +424,6 @@ async function safeReadJson(response) {
|
|
|
337
424
|
* Voice results use the speech system; every other medium uses `memota`.
|
|
338
425
|
*/
|
|
339
426
|
const ASSET_SYSTEMS = new Set(["memota", "memota-speech"]);
|
|
340
|
-
/** Bounded CAS retry budget for the sync commit after a concurrent winner. */
|
|
341
|
-
const MAX_COMMIT_ATTEMPTS = 3;
|
|
342
427
|
/** Validate host-supplied facts; a malformed record fails the whole query. */
|
|
343
428
|
function parseGenerationFacts(value) {
|
|
344
429
|
if (!Array.isArray(value)) throw new Error("generation facts must be an array");
|
|
@@ -413,49 +498,33 @@ function planGeneratedRelations(input) {
|
|
|
413
498
|
* Sync generation lineage after a confirmed entity commit. Any failure is
|
|
414
499
|
* returned as a `failed` outcome instead of thrown, so the already-durable
|
|
415
500
|
* commit result is never masked; a successful query that finds nothing is
|
|
416
|
-
* `current`. Asset identities are immutable, so facts are queried once.
|
|
417
|
-
* revision conflict re-reads current entities and relations, re-plans, and
|
|
418
|
-
* retries within `MAX_COMMIT_ATTEMPTS`; deleted endpoints are never recreated.
|
|
501
|
+
* `current`. Asset identities are immutable, so facts are queried once. The native update retains its causal baseline and merges without whole-state retries; deleted endpoints are never recreated.
|
|
419
502
|
*/
|
|
420
503
|
async function syncGeneratedRelations(input) {
|
|
421
504
|
const { client, docId, baseState, entityCommands, loadFacts } = input;
|
|
422
505
|
try {
|
|
423
|
-
|
|
424
|
-
|
|
506
|
+
const state = await client.fetchState();
|
|
507
|
+
const scope = planGenerationScope(baseState, entityCommands, state);
|
|
425
508
|
if (scope.queryAssetKeys.length === 0) return { status: "current" };
|
|
426
509
|
const facts = parseGenerationFacts(await loadFacts(docId, scope.queryAssetKeys));
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
};
|
|
446
|
-
} catch (error) {
|
|
447
|
-
if (!(error instanceof MengineEntityHttpRequestError && error.status === 409) || attempt === MAX_COMMIT_ATTEMPTS) return {
|
|
448
|
-
status: "failed",
|
|
449
|
-
message: `generation lineage sync commit failed: ${errorMessage(error)}`
|
|
450
|
-
};
|
|
451
|
-
state = await client.fetchState();
|
|
452
|
-
scope = planGenerationScope(baseState, entityCommands, state);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return {
|
|
456
|
-
status: "failed",
|
|
457
|
-
message: "generation lineage sync exhausted its retry budget"
|
|
458
|
-
};
|
|
510
|
+
const relations = planGeneratedRelations({
|
|
511
|
+
baseState,
|
|
512
|
+
state,
|
|
513
|
+
scopedMediaIds: scope.scopedMediaIds,
|
|
514
|
+
facts,
|
|
515
|
+
newRelationId: mintRelationId
|
|
516
|
+
});
|
|
517
|
+
if (relations.length === 0) return { status: "current" };
|
|
518
|
+
const committed = await client.commit(state.revision, {
|
|
519
|
+
...state,
|
|
520
|
+
relations: [...state.relations, ...relations]
|
|
521
|
+
});
|
|
522
|
+
const active = new Set(committed.relations.map((relation) => relation.relation_id));
|
|
523
|
+
const created = relations.map((relation) => relation.relation_id).filter((id) => active.has(id));
|
|
524
|
+
return created.length ? {
|
|
525
|
+
status: "applied",
|
|
526
|
+
created_relation_ids: created
|
|
527
|
+
} : { status: "current" };
|
|
459
528
|
} catch (error) {
|
|
460
529
|
return {
|
|
461
530
|
status: "failed",
|
|
@@ -512,86 +581,10 @@ function isRecord$1(value) {
|
|
|
512
581
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
513
582
|
}
|
|
514
583
|
//#endregion
|
|
515
|
-
//#region src/migration-input.ts
|
|
516
|
-
/** Validate host-recalled facts without accepting a caller-controlled snapshot or VV. */
|
|
517
|
-
function parseMigrationAssetFacts(value) {
|
|
518
|
-
if (!Array.isArray(value)) throw new Error("asset_facts is required for migrate-legacy and must be an array");
|
|
519
|
-
return value.map((item) => {
|
|
520
|
-
if (!record(item)) throw new Error("Each asset_facts entry must be an object");
|
|
521
|
-
const { assetId, kind, durationMs, storageKey, voice } = item;
|
|
522
|
-
if (!nonempty(assetId)) throw new Error("asset_facts.assetId must be a non-empty trimmed string");
|
|
523
|
-
if (kind !== "image" && kind !== "video" && kind !== "audio" && kind !== "voice") throw new Error("asset_facts.kind must be image, video, audio, or voice");
|
|
524
|
-
if (Object.keys(item).some((key) => ![
|
|
525
|
-
"assetId",
|
|
526
|
-
"kind",
|
|
527
|
-
"durationMs",
|
|
528
|
-
"storageKey",
|
|
529
|
-
"voice"
|
|
530
|
-
].includes(key))) throw new Error("Unknown asset_facts field");
|
|
531
|
-
if (storageKey !== void 0 && !nonempty(storageKey)) throw new Error("asset_facts.storageKey must be non-empty");
|
|
532
|
-
if (kind === "image") {
|
|
533
|
-
if (durationMs !== void 0 || voice !== void 0) throw new Error("Image facts cannot declare duration or voice");
|
|
534
|
-
return {
|
|
535
|
-
assetId,
|
|
536
|
-
kind,
|
|
537
|
-
...storageKey === void 0 ? {} : { storageKey }
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
if (typeof durationMs !== "number" || !Number.isSafeInteger(durationMs) || durationMs <= 0) throw new Error("asset_facts.durationMs must be factual positive whole milliseconds");
|
|
541
|
-
if (kind === "video") {
|
|
542
|
-
if (voice !== void 0) throw new Error("Video facts cannot declare voice");
|
|
543
|
-
return {
|
|
544
|
-
assetId,
|
|
545
|
-
kind,
|
|
546
|
-
durationMs,
|
|
547
|
-
...storageKey === void 0 ? {} : { storageKey }
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
if (!nonempty(storageKey)) throw new Error("Audio and Voice facts require their physical storageKey");
|
|
551
|
-
if (kind === "audio") {
|
|
552
|
-
if (voice !== void 0) throw new Error("Audio facts cannot declare a Voice descriptor");
|
|
553
|
-
return {
|
|
554
|
-
assetId,
|
|
555
|
-
kind,
|
|
556
|
-
durationMs,
|
|
557
|
-
storageKey
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
if (!record(voice) || voice.system !== "voice-library" || !nonempty(voice.key) || voice.name !== void 0 && typeof voice.name !== "string" || Object.keys(voice).some((key) => ![
|
|
561
|
-
"system",
|
|
562
|
-
"key",
|
|
563
|
-
"name"
|
|
564
|
-
].includes(key))) throw new Error("Voice facts require an explicit voice-library descriptor");
|
|
565
|
-
return {
|
|
566
|
-
assetId,
|
|
567
|
-
kind,
|
|
568
|
-
durationMs,
|
|
569
|
-
storageKey,
|
|
570
|
-
voice: {
|
|
571
|
-
system: "voice-library",
|
|
572
|
-
key: voice.key,
|
|
573
|
-
...voice.name === void 0 ? {} : { name: voice.name }
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
function record(value) {
|
|
579
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
580
|
-
}
|
|
581
|
-
function nonempty(value) {
|
|
582
|
-
return typeof value === "string" && value.length > 0 && value.trim() === value;
|
|
583
|
-
}
|
|
584
|
-
//#endregion
|
|
585
584
|
//#region src/sandbox/generated/entity-edit-sandbox-model-context.ts
|
|
586
585
|
/** @generated by gen:sandbox-dts. DO NOT EDIT. */
|
|
587
586
|
const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
588
587
|
"/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
|
|
589
|
-
"export interface AudioMediaAssetFact {",
|
|
590
|
-
" readonly assetId: string;",
|
|
591
|
-
" readonly kind: 'audio';",
|
|
592
|
-
" readonly durationMs: number;",
|
|
593
|
-
" readonly storageKey: string;",
|
|
594
|
-
"}",
|
|
595
588
|
"export interface BoundedDerivedSequencePayload extends JsonObject {",
|
|
596
589
|
" extent: {",
|
|
597
590
|
" kind: 'bounded';",
|
|
@@ -611,6 +604,42 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
611
604
|
" sampling: 'native';",
|
|
612
605
|
" coordinateSpace: JsonValue;",
|
|
613
606
|
"}",
|
|
607
|
+
"/** Business editing surface. Infrastructure Assets are assembled by the host. */",
|
|
608
|
+
"export interface BusinessEntityFacade {",
|
|
609
|
+
" list(): SandboxEntity[];",
|
|
610
|
+
" get(entityId: string): SandboxEntity | null;",
|
|
611
|
+
" readCaptionContent(entityId: string): ComposedScriptContent;",
|
|
612
|
+
" readPhoneticScriptContent(entityId: string): ComposedPhoneticContent;",
|
|
613
|
+
" create(",
|
|
614
|
+
" input: Exclude<",
|
|
615
|
+
" CreateEntityInput,",
|
|
616
|
+
" {",
|
|
617
|
+
" entity_kind: 'asset';",
|
|
618
|
+
" }",
|
|
619
|
+
" >,",
|
|
620
|
+
" ): string;",
|
|
621
|
+
" update(input: UpdateEntityInput): void;",
|
|
622
|
+
" declareFields(input: UpdateEntityInput): void;",
|
|
623
|
+
" delete(input: DeleteEntityInput): void;",
|
|
624
|
+
"}",
|
|
625
|
+
"/** Physical Asset bindings are maintained outside the sandbox. */",
|
|
626
|
+
"export interface BusinessRelationFacade {",
|
|
627
|
+
" list(): SandboxRelation[];",
|
|
628
|
+
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
629
|
+
" link(",
|
|
630
|
+
" input: Exclude<",
|
|
631
|
+
" LinkRelationInput,",
|
|
632
|
+
" {",
|
|
633
|
+
" relation_kind: 'physical-asset';",
|
|
634
|
+
" }",
|
|
635
|
+
" >,",
|
|
636
|
+
" ): string;",
|
|
637
|
+
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
638
|
+
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
639
|
+
" linkPhoneticScriptRender(input: LinkPhoneticScriptRenderRelationInput): string;",
|
|
640
|
+
" linkAudioScriptSource(input: LinkAudioScriptSourceRelationInput): string;",
|
|
641
|
+
" unlink(input: UnlinkRelationInput): void;",
|
|
642
|
+
"}",
|
|
614
643
|
"export interface CaptionFontDescriptor {",
|
|
615
644
|
" readonly system: 'font-library';",
|
|
616
645
|
" readonly key: string;",
|
|
@@ -713,28 +742,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
713
742
|
" | 'axvideo-marker'",
|
|
714
743
|
" | 'marker-timeline'",
|
|
715
744
|
" | 'audio-script-marker';",
|
|
716
|
-
"export interface EntityFacade {",
|
|
717
|
-
" /** Read complete assembled fields; returned objects are snapshots. Use update to persist edits. */",
|
|
718
|
-
" list(): SandboxEntity[];",
|
|
719
|
-
" get(entityId: string): SandboxEntity | null;",
|
|
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;",
|
|
726
|
-
" create(input: CreateEntityInput): string;",
|
|
727
|
-
" /** Patch assembled fields, routing inherited fields to their declaring entity. */",
|
|
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;",
|
|
731
|
-
" /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */",
|
|
732
|
-
" delete(input: DeleteEntityInput): void;",
|
|
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
|
-
" };",
|
|
737
|
-
"}",
|
|
738
745
|
"export type EntityId = string;",
|
|
739
746
|
"export interface EntityPayloadByKind {",
|
|
740
747
|
" axvideo: BoundedDerivedSequencePayload;",
|
|
@@ -744,12 +751,10 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
744
751
|
" role?: string;",
|
|
745
752
|
" };",
|
|
746
753
|
" clip: JsonObject;",
|
|
747
|
-
"
|
|
748
|
-
"
|
|
749
|
-
"
|
|
750
|
-
"
|
|
751
|
-
" voice: BoundedNativeSequencePayload & MediaAssetPayload;",
|
|
752
|
-
" image: UnboundedConstantSequencePayload & MediaAssetPayload;",
|
|
754
|
+
" video: BoundedNativeSequencePayload;",
|
|
755
|
+
" audio: BoundedNativeSequencePayload;",
|
|
756
|
+
" voice: BoundedNativeSequencePayload;",
|
|
757
|
+
" image: UnboundedConstantSequencePayload;",
|
|
753
758
|
" 'sequence-marker': JsonObject & {",
|
|
754
759
|
" sourceRange: {",
|
|
755
760
|
" start: number;",
|
|
@@ -793,18 +798,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
793
798
|
" };",
|
|
794
799
|
"}",
|
|
795
800
|
"export interface EntityStoreSnapshot {",
|
|
801
|
+
" /** Causal compiler baseline; required for publishing edits. */",
|
|
802
|
+
" loroSnapshot?: string;",
|
|
796
803
|
" revision: number;",
|
|
804
|
+
" /** Current AudioScript version attached to the project; initialized projects always attach a script, possibly empty. */",
|
|
797
805
|
" audioScriptEntityId: string | null;",
|
|
798
806
|
" entities: SandboxEntity[];",
|
|
799
807
|
" relations: SandboxRelation[];",
|
|
800
808
|
"}",
|
|
801
|
-
"export interface ImageMediaAssetFact {",
|
|
802
|
-
" readonly assetId: string;",
|
|
803
|
-
" readonly kind: 'image';",
|
|
804
|
-
" readonly storageKey?: string;",
|
|
805
|
-
"}",
|
|
806
809
|
"export interface InsertCaptionClipInput {",
|
|
807
810
|
" readonly timelineEntityId: string;",
|
|
811
|
+
" /** Existing generation identity for newly materialized Caption content, distinct from its Clip. */",
|
|
812
|
+
" readonly captionEntityId?: string;",
|
|
808
813
|
" /** Stable placed caption identity, distinct from the Caption content identity. */",
|
|
809
814
|
" readonly captionClipEntityId?: string;",
|
|
810
815
|
" /** Existing bases composed by this variant; includes an AudioScript text owner. */",
|
|
@@ -825,21 +830,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
825
830
|
" readonly targetRange?: SequenceRange<number>;",
|
|
826
831
|
" readonly clipPayload?: JsonObject;",
|
|
827
832
|
"}",
|
|
828
|
-
"export interface InsertMediaClipInput {",
|
|
829
|
-
" readonly timelineEntityId: string;",
|
|
830
|
-
" readonly clipEntityId?: string;",
|
|
831
|
-
" readonly media: VisualMediaAssetFact;",
|
|
832
|
-
" /** Source/display window in whole milliseconds. Images use this as their finite display span. */",
|
|
833
|
-
" readonly sourceRange: SequenceRange<number>;",
|
|
834
|
-
" readonly placement: ClipPlacement;",
|
|
835
|
-
" readonly volume?: number;",
|
|
836
|
-
"}",
|
|
837
|
-
"export interface InsertMediaClipsInput {",
|
|
838
|
-
" readonly timelineEntityId: string;",
|
|
839
|
-
" readonly clips: readonly ReplacementMediaClipInput[];",
|
|
840
|
-
" /** One placement decision for the whole input-ordered block. */",
|
|
841
|
-
" readonly insertion: MediaClipInsertion;",
|
|
842
|
-
"}",
|
|
843
833
|
"export interface InsertPlacedClipInput {",
|
|
844
834
|
" readonly trackEntityId: string;",
|
|
845
835
|
" readonly contentEntityId: string;",
|
|
@@ -860,7 +850,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
860
850
|
" | 'timeline'",
|
|
861
851
|
" | 'track'",
|
|
862
852
|
" | 'clip'",
|
|
863
|
-
" | 'asset'",
|
|
864
853
|
" | 'video'",
|
|
865
854
|
" | 'audio'",
|
|
866
855
|
" | 'voice'",
|
|
@@ -877,7 +866,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
877
866
|
" | 'marker-content'",
|
|
878
867
|
" | 'axvideo-marker'",
|
|
879
868
|
" | 'marker-timeline'",
|
|
880
|
-
" | 'physical-asset'",
|
|
881
869
|
" | 'generated'",
|
|
882
870
|
" | 'caption-alignment'",
|
|
883
871
|
" | 'clip-anchor'",
|
|
@@ -937,28 +925,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
937
925
|
" alignment: JsonValue;",
|
|
938
926
|
" };",
|
|
939
927
|
" });",
|
|
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
|
-
"};",
|
|
949
|
-
"export type MediaClipInsertion =",
|
|
950
|
-
" | {",
|
|
951
|
-
" readonly kind: 'before';",
|
|
952
|
-
" readonly clipEntityId: string;",
|
|
953
|
-
" }",
|
|
954
|
-
" | {",
|
|
955
|
-
" readonly kind: 'after';",
|
|
956
|
-
" readonly clipEntityId: string;",
|
|
957
|
-
" }",
|
|
958
|
-
" | {",
|
|
959
|
-
" readonly kind: 'firstStart';",
|
|
960
|
-
" readonly startMs: number;",
|
|
961
|
-
" };",
|
|
962
928
|
"export interface MoveClipInput {",
|
|
963
929
|
" readonly clipEntityId: string;",
|
|
964
930
|
" readonly trackEntityId: string;",
|
|
@@ -985,23 +951,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
985
951
|
" readonly timelineEntityId: string;",
|
|
986
952
|
" readonly style: CaptionStyleFields;",
|
|
987
953
|
"}",
|
|
988
|
-
"export interface RelationFacade {",
|
|
989
|
-
" list(): SandboxRelation[];",
|
|
990
|
-
" /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */",
|
|
991
|
-
" of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];",
|
|
992
|
-
" /** Link existing entities through ordinary associations; variant bases are stored directly on the variant. */",
|
|
993
|
-
" link(input: LinkRelationInput): string;",
|
|
994
|
-
" /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */",
|
|
995
|
-
" linkGenerated(input: LinkGeneratedRelationInput): string;",
|
|
996
|
-
" /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */",
|
|
997
|
-
" linkClipAnchor(input: LinkClipAnchorRelationInput): string;",
|
|
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;",
|
|
1002
|
-
" /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */",
|
|
1003
|
-
" unlink(input: UnlinkRelationInput): void;",
|
|
1004
|
-
"}",
|
|
1005
954
|
"export interface ReplaceClipContentInput {",
|
|
1006
955
|
" readonly clipEntityId: string;",
|
|
1007
956
|
" /** Existing Sequence media Entity id. Asset ids and URLs are not content ids. */",
|
|
@@ -1011,25 +960,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1011
960
|
" readonly targetRange?: SequenceRange<number>;",
|
|
1012
961
|
" readonly timeRemapping?: JsonValue;",
|
|
1013
962
|
"}",
|
|
1014
|
-
"export interface ReplaceMediaClipInput {",
|
|
1015
|
-
" readonly clipEntityId: string;",
|
|
1016
|
-
" readonly media: VisualMediaAssetFact;",
|
|
1017
|
-
" readonly sourceRange: SequenceRange<number>;",
|
|
1018
|
-
"}",
|
|
1019
|
-
"export interface ReplaceSequentialClipsInput {",
|
|
1020
|
-
" readonly timelineEntityId: string;",
|
|
1021
|
-
" readonly oldClipEntityIds: readonly string[];",
|
|
1022
|
-
" readonly newClips: readonly ReplacementMediaClipInput[];",
|
|
1023
|
-
" readonly onAnchored: 'remap' | 'cascade';",
|
|
1024
|
-
"}",
|
|
1025
|
-
"export interface ReplacementMediaClipInput {",
|
|
1026
|
-
" readonly clipEntityId?: string;",
|
|
1027
|
-
" readonly media: VisualMediaAssetFact;",
|
|
1028
|
-
" readonly sourceRange: SequenceRange<number>;",
|
|
1029
|
-
" readonly volume?: number;",
|
|
1030
|
-
"}",
|
|
1031
|
-
"/** Asset identity, either an old physical-only row or a directly composed media variant. */",
|
|
1032
|
-
"export type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';",
|
|
1033
963
|
"export interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {",
|
|
1034
964
|
" entity_id: string;",
|
|
1035
965
|
" entity_kind: K;",
|
|
@@ -1068,12 +998,6 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1068
998
|
" | {",
|
|
1069
999
|
" readonly position: 'trackStart';",
|
|
1070
1000
|
" };",
|
|
1071
|
-
"export interface SetBgmInput {",
|
|
1072
|
-
" readonly timelineEntityId: string;",
|
|
1073
|
-
" readonly bgmClipEntityId: string;",
|
|
1074
|
-
" readonly media: AudioMediaAssetFact;",
|
|
1075
|
-
" readonly volume: number;",
|
|
1076
|
-
"}",
|
|
1077
1001
|
"export interface SetCaptionVisibilityInput {",
|
|
1078
1002
|
" readonly timelineEntityId: string;",
|
|
1079
1003
|
" readonly hidden: boolean;",
|
|
@@ -1132,72 +1056,8 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1132
1056
|
" entity_id: string;",
|
|
1133
1057
|
" payload: JsonObject;",
|
|
1134
1058
|
"}",
|
|
1135
|
-
"export interface VideoMediaAssetFact {",
|
|
1136
|
-
" readonly assetId: string;",
|
|
1137
|
-
" readonly kind: 'video';",
|
|
1138
|
-
" readonly durationMs: number;",
|
|
1139
|
-
" readonly storageKey?: string;",
|
|
1140
|
-
"}",
|
|
1141
|
-
"export type VisualMediaAssetFact = ImageMediaAssetFact | VideoMediaAssetFact;",
|
|
1142
|
-
"export interface VoiceDescriptor {",
|
|
1143
|
-
" readonly system: 'voice-library';",
|
|
1144
|
-
" readonly key: string;",
|
|
1145
|
-
" readonly name?: string;",
|
|
1146
|
-
"}",
|
|
1147
|
-
"export interface VoiceMediaAssetFact {",
|
|
1148
|
-
" /** Stable external speech result id, independent of the placed Clip id. */",
|
|
1149
|
-
" readonly assetId: string;",
|
|
1150
|
-
" readonly kind: 'voice';",
|
|
1151
|
-
" readonly durationMs: number;",
|
|
1152
|
-
" readonly storageKey: string;",
|
|
1153
|
-
" /** Present for synthesized voice, absent for original recorded audio. */",
|
|
1154
|
-
" readonly voice?: VoiceDescriptor;",
|
|
1155
|
-
"}",
|
|
1156
|
-
"export interface VoiceoverCaptionFact {",
|
|
1157
|
-
" /** Stable placed caption identity supplied by the materialized side effect. */",
|
|
1158
|
-
" readonly captionClipEntityId: string;",
|
|
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[];",
|
|
1163
|
-
" readonly startMs: number;",
|
|
1164
|
-
" readonly durationMs: number;",
|
|
1165
|
-
" readonly style?: CaptionStyleFields;",
|
|
1166
|
-
"}",
|
|
1167
|
-
"export type VoiceoverTakeInput = {",
|
|
1168
|
-
" readonly timelineEntityId: string;",
|
|
1169
|
-
" /** Stable placed speech identity, distinct from media.assetId. */",
|
|
1170
|
-
" readonly voiceoverClipEntityId: string;",
|
|
1171
|
-
" readonly media: VoiceMediaAssetFact;",
|
|
1172
|
-
" /** Existing pronunciation variant; its composed AudioScript stays the text owner. */",
|
|
1173
|
-
" readonly phoneticScriptEntityId: string;",
|
|
1174
|
-
" readonly volume: number;",
|
|
1175
|
-
" readonly captions: readonly VoiceoverCaptionFact[];",
|
|
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
|
-
");",
|
|
1188
|
-
"export interface VoiceoverTakeResult {",
|
|
1189
|
-
" readonly voiceoverClipEntityId: string;",
|
|
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. */",
|
|
1194
|
-
" readonly audioScriptEntityId: string;",
|
|
1195
|
-
" readonly captionClipEntityIds: readonly string[];",
|
|
1196
|
-
"}",
|
|
1197
1059
|
"/** Timeline writes accept existing media Entity ids, never Memota asset ids or URLs. */",
|
|
1198
1060
|
"export interface EditApi {",
|
|
1199
|
-
" /** Select the document AudioScript, or clear the optional association with null. */",
|
|
1200
|
-
" setDocumentAudioScript(entityId: string | null): void;",
|
|
1201
1061
|
" insertClip(input: InsertClipInput): ClipEntityId;",
|
|
1202
1062
|
" insertPlacedClip(input: InsertPlacedClipInput): ClipEntityId;",
|
|
1203
1063
|
" updateClipMarker(input: UpdateClipMarkerInput): void;",
|
|
@@ -1205,36 +1065,32 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
|
|
|
1205
1065
|
" moveSequentialClips(input: MoveSequentialClipsInput): void;",
|
|
1206
1066
|
" moveClip(input: MoveClipInput): void;",
|
|
1207
1067
|
" replaceClipContent(input: ReplaceClipContentInput): void;",
|
|
1208
|
-
" insertMediaClip(input: InsertMediaClipInput): ClipEntityId;",
|
|
1209
|
-
" insertMediaClips(input: InsertMediaClipsInput): readonly ClipEntityId[];",
|
|
1210
|
-
" replaceMediaClip(input: ReplaceMediaClipInput): void;",
|
|
1211
1068
|
" setClipVolume(input: SetClipVolumeInput): void;",
|
|
1212
1069
|
" setClipSpeed(input: SetClipSpeedInput): void;",
|
|
1213
1070
|
" trimClip(input: TrimClipInput): void;",
|
|
1214
|
-
" replaceSequentialClips(input: ReplaceSequentialClipsInput): readonly ClipEntityId[];",
|
|
1215
1071
|
" deleteClip(input: DeleteClipInput): void;",
|
|
1216
1072
|
" deleteClipTree(input: DeleteClipTreeInput): void;",
|
|
1217
1073
|
" updateClip(input: UpdateClipInput): void;",
|
|
1218
|
-
" upsertVoiceoverTake(input: VoiceoverTakeInput): VoiceoverTakeResult;",
|
|
1219
1074
|
" moveVoiceover(input: MoveVoiceoverInput): void;",
|
|
1220
1075
|
" moveClipsToStarts(input: MoveClipsToStartsInput): void;",
|
|
1221
1076
|
" deleteVoiceover(input: DeleteVoiceoverInput): void;",
|
|
1222
|
-
" setBgm(input: SetBgmInput): ClipEntityId;",
|
|
1223
1077
|
" deleteBgm(input: DeleteBgmInput): void;",
|
|
1224
1078
|
" setCaptionVisibility(input: SetCaptionVisibilityInput): void;",
|
|
1225
1079
|
" patchCaptionStyle(input: PatchCaptionStyleInput): void;",
|
|
1226
1080
|
" insertCaptionClip(input: InsertCaptionClipInput): ClipEntityId;",
|
|
1227
1081
|
"}",
|
|
1228
1082
|
"export interface TimelineApi {",
|
|
1229
|
-
" snapshot(): EntityStoreSnapshot
|
|
1083
|
+
" snapshot(): EntityStoreSnapshot & {",
|
|
1084
|
+
" audioScriptEntityId: string;",
|
|
1085
|
+
" };",
|
|
1230
1086
|
"}",
|
|
1231
1087
|
"export interface SandboxCheckpoint {",
|
|
1232
1088
|
" readonly index: number;",
|
|
1233
1089
|
"}",
|
|
1234
1090
|
"export declare const edit: EditApi;",
|
|
1235
1091
|
"export declare const timeline: TimelineApi;",
|
|
1236
|
-
"export declare const entities:
|
|
1237
|
-
"export declare const relations:
|
|
1092
|
+
"export declare const entities: BusinessEntityFacade;",
|
|
1093
|
+
"export declare const relations: BusinessRelationFacade;",
|
|
1238
1094
|
"export declare function checkpoint(): SandboxCheckpoint;",
|
|
1239
1095
|
"export declare function rollbackTo(cp: SandboxCheckpoint): void;",
|
|
1240
1096
|
"export declare const inputs: Readonly<Record<string, unknown>>;",
|
|
@@ -1246,27 +1102,25 @@ const MEDEO_TOOL_DESCRIPTION = `
|
|
|
1246
1102
|
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.
|
|
1247
1103
|
|
|
1248
1104
|
Operations:
|
|
1249
|
-
- snapshot:
|
|
1250
|
-
-
|
|
1251
|
-
-
|
|
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.
|
|
1105
|
+
- snapshot: read the current Entity/Relation view, project attachments and causal Loro baseline. Reading does not initialize or mutate domain data.
|
|
1106
|
+
- run-edit-script: inspect timeline.snapshot(), entities.*, and relations.*; edit.* operates existing Entity ids and creates the required Clip/SequenceMarker structural graph. The sandbox has no network, storage or generation access. Use assembled entity fields; the host manages resource storage and generation provenance. A successful run returns preview, logs, base revision and plan_id.
|
|
1107
|
+
- commit-plan: publish the native Loro update compiled against the plan’s causal baseline. Concurrent independent edits merge through Loro. The timeline and AudioScript panel read the merged entity state. A failed transport is unconfirmed; retry the same plan_id so operation identities are preserved.
|
|
1253
1108
|
|
|
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.
|
|
1109
|
+
Default flow: snapshot → run-edit-script with auto_commit=false → inspect preview → commit-plan. Use auto_commit=true only for low-risk edits when the host does not need user confirmation. Concurrent edits do not require replaying the script against a newer snapshot. If a domain conflict is reported, inspect the merged state and resolve it explicitly; never replace the complete document to force the edit through.
|
|
1255
1110
|
|
|
1256
|
-
|
|
1111
|
+
Generated resources are materialized into domain Entities by the host. Use the returned Entity ids and assembled fields to edit or place content with edit.insertClip. Asset creation, lookup, reading, binding, resource locators and storage are host infrastructure, unavailable to the model through any tool or sandbox API. Each placement has its own Clip and SequenceMarker. Generation lineage is host-synced; relations.of(entityId) is endpoint-agnostic.
|
|
1257
1112
|
`.trim();
|
|
1258
1113
|
const MEDEO_TOOL_EXECUTION_RULES = `
|
|
1259
1114
|
The host supplies the current document. Do not ask for, invent, or pass a document id.
|
|
1260
1115
|
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.
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
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.
|
|
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.
|
|
1116
|
+
Inspect existing Image/Video/Audio/Voice Entities and their factual extents before placing them. The host materializes generated content and resolves its physical resource. Replace a Clip's content using another content Entity id. Never fabricate a duration.
|
|
1117
|
+
Caption composes AudioScript text. Its optional physical resource binding is host-owned. A Caption created from AudioScript may have no physical resource. Read and edit assembled fields through entities; do not author infrastructure bindings or generated Relations.
|
|
1118
|
+
The editor projection supports the existing four Track roles: video_clip (Image/Video), speech (Voice), caption (Caption), and bgm (Audio), one of each. Clip.volume is decibels (-60 to 20, 0 = original). Marker.sourceRange is the selected source interval; Marker.duration is effective display/playback duration. Coordinates and duration are whole milliseconds for this reader, not a global DSL restriction. Placement is exactly one of Clip.order, Marker.targetRange, or clip-anchor(child,host) plus Marker.anchorOffset. Use the native move/delete/voiceover helpers so placement and cascade decisions are in the same entity plan. Reading the graph never rebinds anchors or invents empty clips. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'} and agrees with rounded source span divided by rate. Image remains unbounded/constant; an explicit linear rate scales its display window, not an invented media extent. Nonlinear speed and multiple visual overlay tracks are unsupported.
|
|
1266
1119
|
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.
|
|
1120
|
+
Use edit.insertCaptionClip with baseEntityIds and selections, plus captionEntityId when generation returned a Caption identity; 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
1121
|
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.
|
|
1122
|
+
Create only the known entity kinds. Project creation initializes one current Timeline, four Tracks and an attached AudioScript with segments:[]. Read the current AudioScript ID from timeline.snapshot(); the panel displays this attachment and preserves its segments. Normal edits operate this script, not an unrelated newly created script.
|
|
1123
|
+
Immutable updates apply to every entity: editing an owned field creates a new content version ID; changing only a variant's base ID preserves the variant ID. Editing a base through a variant updates the owner, and the compiler advances the affected base links and project attachment. A variant-owned edit creates a new variant version and retains its unchanged bases. Do not manually clone entities or duplicate inherited fields to implement versioning. Versions preserve native text and list editing identities so independent concurrent edits survive. Missing facts, unsupported layouts and composition conflicts are explicit errors; there is no legacy migration or whole-state overwrite path.
|
|
1270
1124
|
Use only the globals and methods declared by the following TypeScript interface. Values not declared here are unavailable.
|
|
1271
1125
|
`.trim();
|
|
1272
1126
|
/** Render the complete MEngine-owned context injected before one model call. */
|
|
@@ -1292,42 +1146,6 @@ ${ENTITY_EDIT_SANDBOX_API_DTS}
|
|
|
1292
1146
|
//#endregion
|
|
1293
1147
|
//#region src/schema.ts
|
|
1294
1148
|
const MEDEO_TOOL_NAME = "medeo";
|
|
1295
|
-
const assetFactProperties = {
|
|
1296
|
-
assetId: {
|
|
1297
|
-
type: "string",
|
|
1298
|
-
minLength: 1
|
|
1299
|
-
},
|
|
1300
|
-
kind: {
|
|
1301
|
-
type: "string",
|
|
1302
|
-
enum: [
|
|
1303
|
-
"image",
|
|
1304
|
-
"video",
|
|
1305
|
-
"audio",
|
|
1306
|
-
"voice"
|
|
1307
|
-
]
|
|
1308
|
-
},
|
|
1309
|
-
durationMs: {
|
|
1310
|
-
type: "integer",
|
|
1311
|
-
minimum: 1
|
|
1312
|
-
},
|
|
1313
|
-
storageKey: {
|
|
1314
|
-
type: "string",
|
|
1315
|
-
minLength: 1
|
|
1316
|
-
},
|
|
1317
|
-
voice: {
|
|
1318
|
-
type: "object",
|
|
1319
|
-
additionalProperties: false,
|
|
1320
|
-
required: ["system", "key"],
|
|
1321
|
-
properties: {
|
|
1322
|
-
system: { const: "voice-library" },
|
|
1323
|
-
key: {
|
|
1324
|
-
type: "string",
|
|
1325
|
-
minLength: 1
|
|
1326
|
-
},
|
|
1327
|
-
name: { type: "string" }
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
1149
|
/**
|
|
1332
1150
|
* JSON Schema for the host-facing `medeo` tool surface.
|
|
1333
1151
|
*
|
|
@@ -1345,7 +1163,6 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1345
1163
|
type: "string",
|
|
1346
1164
|
enum: [
|
|
1347
1165
|
"snapshot",
|
|
1348
|
-
"migrate-legacy",
|
|
1349
1166
|
"run-edit-script",
|
|
1350
1167
|
"commit-plan"
|
|
1351
1168
|
],
|
|
@@ -1365,68 +1182,6 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1365
1182
|
type: "object",
|
|
1366
1183
|
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."
|
|
1367
1184
|
},
|
|
1368
|
-
asset_facts: {
|
|
1369
|
-
type: "array",
|
|
1370
|
-
description: "Factual asset metadata recalled by the host for migrate-legacy only. The package reads the canonical legacy snapshot and version itself; never supply a clip trim window as media duration.",
|
|
1371
|
-
items: { oneOf: [
|
|
1372
|
-
{
|
|
1373
|
-
type: "object",
|
|
1374
|
-
additionalProperties: false,
|
|
1375
|
-
required: ["assetId", "kind"],
|
|
1376
|
-
properties: {
|
|
1377
|
-
assetId: assetFactProperties.assetId,
|
|
1378
|
-
kind: { const: "image" },
|
|
1379
|
-
storageKey: assetFactProperties.storageKey
|
|
1380
|
-
}
|
|
1381
|
-
},
|
|
1382
|
-
{
|
|
1383
|
-
type: "object",
|
|
1384
|
-
additionalProperties: false,
|
|
1385
|
-
required: [
|
|
1386
|
-
"assetId",
|
|
1387
|
-
"kind",
|
|
1388
|
-
"durationMs"
|
|
1389
|
-
],
|
|
1390
|
-
properties: {
|
|
1391
|
-
assetId: assetFactProperties.assetId,
|
|
1392
|
-
kind: { const: "video" },
|
|
1393
|
-
durationMs: assetFactProperties.durationMs,
|
|
1394
|
-
storageKey: assetFactProperties.storageKey
|
|
1395
|
-
}
|
|
1396
|
-
},
|
|
1397
|
-
{
|
|
1398
|
-
type: "object",
|
|
1399
|
-
additionalProperties: false,
|
|
1400
|
-
required: [
|
|
1401
|
-
"assetId",
|
|
1402
|
-
"kind",
|
|
1403
|
-
"durationMs",
|
|
1404
|
-
"storageKey"
|
|
1405
|
-
],
|
|
1406
|
-
properties: {
|
|
1407
|
-
assetId: assetFactProperties.assetId,
|
|
1408
|
-
kind: { const: "audio" },
|
|
1409
|
-
durationMs: assetFactProperties.durationMs,
|
|
1410
|
-
storageKey: assetFactProperties.storageKey
|
|
1411
|
-
}
|
|
1412
|
-
},
|
|
1413
|
-
{
|
|
1414
|
-
type: "object",
|
|
1415
|
-
additionalProperties: false,
|
|
1416
|
-
required: [
|
|
1417
|
-
"assetId",
|
|
1418
|
-
"kind",
|
|
1419
|
-
"durationMs",
|
|
1420
|
-
"storageKey",
|
|
1421
|
-
"voice"
|
|
1422
|
-
],
|
|
1423
|
-
properties: {
|
|
1424
|
-
...assetFactProperties,
|
|
1425
|
-
kind: { const: "voice" }
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
] }
|
|
1429
|
-
},
|
|
1430
1185
|
timeout_ms: {
|
|
1431
1186
|
type: "integer",
|
|
1432
1187
|
minimum: 1,
|
|
@@ -1448,24 +1203,11 @@ const MEDEO_TOOL_PARAMETERS = {
|
|
|
1448
1203
|
},
|
|
1449
1204
|
validation: {
|
|
1450
1205
|
type: "string",
|
|
1451
|
-
enum: ["
|
|
1452
|
-
description: "
|
|
1206
|
+
enum: ["preflight"],
|
|
1207
|
+
description: "Publish the native update already validated on its causal Loro fork."
|
|
1453
1208
|
}
|
|
1454
1209
|
},
|
|
1455
1210
|
oneOf: [
|
|
1456
|
-
{
|
|
1457
|
-
required: [
|
|
1458
|
-
"op",
|
|
1459
|
-
"doc_id",
|
|
1460
|
-
"asset_facts"
|
|
1461
|
-
],
|
|
1462
|
-
properties: {
|
|
1463
|
-
op: { const: "migrate-legacy" },
|
|
1464
|
-
doc_id: { $ref: "#/properties/doc_id" },
|
|
1465
|
-
asset_facts: { $ref: "#/properties/asset_facts" }
|
|
1466
|
-
},
|
|
1467
|
-
additionalProperties: false
|
|
1468
|
-
},
|
|
1469
1211
|
{
|
|
1470
1212
|
required: ["op", "doc_id"],
|
|
1471
1213
|
properties: {
|
|
@@ -1617,7 +1359,8 @@ function requiredContext(value, docId, field) {
|
|
|
1617
1359
|
if (resolved == null || resolved.length === 0) throw new Error(`${field} must resolve to a non-empty string for doc ${docId}`);
|
|
1618
1360
|
return resolved;
|
|
1619
1361
|
}
|
|
1620
|
-
function renderEntitySnapshot(
|
|
1362
|
+
function renderEntitySnapshot(raw) {
|
|
1363
|
+
const state = businessState(raw);
|
|
1621
1364
|
const rows = [...state.entities.map((entity) => JSON.stringify(entity)), ...state.relations.map((relation) => JSON.stringify(relation))];
|
|
1622
1365
|
const shown = rows.slice(0, 200);
|
|
1623
1366
|
return [
|
|
@@ -1626,61 +1369,25 @@ function renderEntitySnapshot(state) {
|
|
|
1626
1369
|
...shown.length < rows.length ? ["[truncated; inspect entities/relations in the sandbox]"] : []
|
|
1627
1370
|
].join("\n");
|
|
1628
1371
|
}
|
|
1629
|
-
function migrationNotice(document, state) {
|
|
1630
|
-
if (state.entities.some((row) => row.entity_kind === "timeline") || Object.keys(document.part_library ?? {}).length === 0) return "";
|
|
1631
|
-
const assetIds = new Set(Object.values(document.part_library ?? {}).flatMap((part) => {
|
|
1632
|
-
const id = part.video_clip?.origin_media_id ?? part.bgm?.origin_media_id;
|
|
1633
|
-
return typeof id === "string" && id !== "" ? [id] : [];
|
|
1634
|
-
}));
|
|
1635
|
-
return `\nLegacy timeline migration required. Recall factual media metadata for ${JSON.stringify([...assetIds])}, then call migrate-legacy with asset_facts. Speech facts are read from the canonical legacy document. Take a fresh snapshot after migration before editing.`;
|
|
1636
|
-
}
|
|
1637
1372
|
async function commitEntityPlan(client, plan) {
|
|
1638
|
-
|
|
1639
|
-
if (rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1373
|
+
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1640
1374
|
try {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
deleted_relation_ids: plan.deleted_relation_ids ?? []
|
|
1644
|
-
});
|
|
1375
|
+
if (!plan.loro_update) throw new Error("Entity plan has no compiled Loro update");
|
|
1376
|
+
const committed = await client.commitUpdate(plan.loro_update);
|
|
1645
1377
|
return {
|
|
1646
1378
|
kind: "committed",
|
|
1647
1379
|
ops_applied: plan.entity_commands.length,
|
|
1648
|
-
collaborated:
|
|
1380
|
+
collaborated: committed.revision > plan.entity_base_revision + 1,
|
|
1649
1381
|
entity_revision: committed.revision
|
|
1650
1382
|
};
|
|
1651
1383
|
} catch (error) {
|
|
1652
1384
|
if (error instanceof MengineEntityHttpRequestError) {
|
|
1653
|
-
if (error.
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
ops_applied: plan.entity_commands.length,
|
|
1660
|
-
collaborated: false,
|
|
1661
|
-
entity_revision: current.revision
|
|
1662
|
-
};
|
|
1663
|
-
return {
|
|
1664
|
-
kind: "rejected",
|
|
1665
|
-
reason: "entity_revision_mismatch",
|
|
1666
|
-
expected: plan.entity_base_revision,
|
|
1667
|
-
actual: current.revision
|
|
1668
|
-
};
|
|
1669
|
-
} catch {
|
|
1670
|
-
if (actualFromPayload !== void 0) return {
|
|
1671
|
-
kind: "rejected",
|
|
1672
|
-
reason: "entity_revision_mismatch",
|
|
1673
|
-
expected: plan.entity_base_revision,
|
|
1674
|
-
actual: actualFromPayload
|
|
1675
|
-
};
|
|
1676
|
-
return {
|
|
1677
|
-
kind: "unconfirmed",
|
|
1678
|
-
reason: "push_failed",
|
|
1679
|
-
ops_applied: plan.entity_commands.length,
|
|
1680
|
-
message: "entity-state conflict could not be reconciled"
|
|
1681
|
-
};
|
|
1682
|
-
}
|
|
1683
|
-
}
|
|
1385
|
+
if (isRecord(error.payload) && error.payload.accepted === true && typeof error.payload.revision === "number") return {
|
|
1386
|
+
kind: "conflicted",
|
|
1387
|
+
accepted: true,
|
|
1388
|
+
entity_revision: error.payload.revision,
|
|
1389
|
+
message: entityHttpErrorMessage(error.payload)
|
|
1390
|
+
};
|
|
1684
1391
|
return {
|
|
1685
1392
|
kind: "rejected",
|
|
1686
1393
|
reason: "entity_state_rejected",
|
|
@@ -1696,34 +1403,14 @@ async function commitEntityPlan(client, plan) {
|
|
|
1696
1403
|
};
|
|
1697
1404
|
}
|
|
1698
1405
|
}
|
|
1699
|
-
function revisionConflictActual(payload) {
|
|
1700
|
-
if (!isRecord(payload)) return void 0;
|
|
1701
|
-
const actual = payload.actual_revision;
|
|
1702
|
-
return typeof actual === "number" && Number.isSafeInteger(actual) && actual >= 0 ? actual : void 0;
|
|
1703
|
-
}
|
|
1704
|
-
function isRevisionConflictPayload(payload) {
|
|
1705
|
-
return isRecord(payload) && payload.code === "revision_conflict";
|
|
1706
|
-
}
|
|
1707
1406
|
function entityHttpErrorMessage(payload) {
|
|
1407
|
+
if (isRecord(payload) && isRecord(payload.error) && typeof payload.error.message === "string") return payload.error.message;
|
|
1708
1408
|
if (isRecord(payload) && typeof payload.message === "string" && payload.message.length > 0) return payload.message;
|
|
1709
1409
|
return typeof payload === "string" && payload.length > 0 ? payload : "mengine rejected the entity-state plan";
|
|
1710
1410
|
}
|
|
1711
1411
|
function commitWarnings(result) {
|
|
1712
1412
|
return result.kind === "committed" && "warnings" in result && result.warnings !== void 0 ? [...result.warnings] : void 0;
|
|
1713
1413
|
}
|
|
1714
|
-
function entityRowsEquivalent(left, right) {
|
|
1715
|
-
const normalize = (state) => ({
|
|
1716
|
-
audioScriptEntityId: state.audioScriptEntityId,
|
|
1717
|
-
entities: [...state.entities].sort((a, b) => a.entity_id.localeCompare(b.entity_id)).map((entity) => canonicalJson(entity)),
|
|
1718
|
-
relations: [...state.relations].sort((a, b) => a.relation_id.localeCompare(b.relation_id)).map((relation) => canonicalJson(relation))
|
|
1719
|
-
});
|
|
1720
|
-
return JSON.stringify(normalize(left)) === JSON.stringify(normalize(right));
|
|
1721
|
-
}
|
|
1722
|
-
function canonicalJson(value) {
|
|
1723
|
-
if (Array.isArray(value)) return value.map(canonicalJson);
|
|
1724
|
-
if (!isRecord(value)) return value;
|
|
1725
|
-
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonicalJson(value[key])]));
|
|
1726
|
-
}
|
|
1727
1414
|
function parseInput(value) {
|
|
1728
1415
|
if (!isRecord(value)) throw new Error("input must be an object");
|
|
1729
1416
|
const op = value.op;
|
|
@@ -1734,18 +1421,6 @@ function parseInput(value) {
|
|
|
1734
1421
|
op,
|
|
1735
1422
|
doc_id: docId
|
|
1736
1423
|
};
|
|
1737
|
-
if (op === "migrate-legacy") {
|
|
1738
|
-
if (Object.keys(value).some((key) => ![
|
|
1739
|
-
"op",
|
|
1740
|
-
"doc_id",
|
|
1741
|
-
"asset_facts"
|
|
1742
|
-
].includes(key))) throw new Error("migrate-legacy accepts asset_facts only; the package reads the canonical document and version");
|
|
1743
|
-
return {
|
|
1744
|
-
op,
|
|
1745
|
-
doc_id: docId,
|
|
1746
|
-
asset_facts: parseMigrationAssetFacts(value.asset_facts)
|
|
1747
|
-
};
|
|
1748
|
-
}
|
|
1749
1424
|
if (op === "run-edit-script") {
|
|
1750
1425
|
if (typeof value.script !== "string" || value.script.length === 0) throw new Error("script must be a non-empty string");
|
|
1751
1426
|
if (value.inputs !== void 0 && !isRecord(value.inputs)) throw new Error("inputs must be an object");
|
|
@@ -1766,7 +1441,7 @@ function parseInput(value) {
|
|
|
1766
1441
|
}
|
|
1767
1442
|
if (op === "commit-plan") {
|
|
1768
1443
|
if (typeof value.plan_id !== "string" || value.plan_id.length === 0) throw new Error("plan_id must be a non-empty string");
|
|
1769
|
-
if (value.validation !== void 0 && value.validation !== "
|
|
1444
|
+
if (value.validation !== void 0 && value.validation !== "preflight") throw new Error("validation must be \"preflight\"");
|
|
1770
1445
|
return {
|
|
1771
1446
|
op,
|
|
1772
1447
|
doc_id: docId,
|
|
@@ -1855,10 +1530,21 @@ function createMedeoTool(options) {
|
|
|
1855
1530
|
if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
|
|
1856
1531
|
if (options.loadInitialDraft === void 0) throw error;
|
|
1857
1532
|
}
|
|
1858
|
-
const
|
|
1533
|
+
const document = toVideoDocument(await options.loadInitialDraft(docId));
|
|
1534
|
+
if (Object.keys(document.part_library ?? {}).length > 0) throw new Error("Legacy content cannot bootstrap a Loro entity project");
|
|
1535
|
+
const seed = createMirrorVideoDocument(document, {
|
|
1859
1536
|
...peerId !== void 0 ? { peerId } : {},
|
|
1860
1537
|
origin: "mengine.medeo_tool.bootstrap"
|
|
1861
1538
|
});
|
|
1539
|
+
const foundation = ensureEditorFoundation({
|
|
1540
|
+
entities: [],
|
|
1541
|
+
relations: []
|
|
1542
|
+
});
|
|
1543
|
+
const entities = LoroEntityDocument.create(foundation.rows, {
|
|
1544
|
+
timelineEntityId: foundation.timelineEntityId,
|
|
1545
|
+
audioScriptEntityId: foundation.audioScriptEntityId
|
|
1546
|
+
});
|
|
1547
|
+
seed.import(entities.doc.export({ mode: "snapshot" }));
|
|
1862
1548
|
try {
|
|
1863
1549
|
await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
|
|
1864
1550
|
} catch (error) {
|
|
@@ -1907,65 +1593,31 @@ function createMedeoTool(options) {
|
|
|
1907
1593
|
pendingPushes.delete(docId);
|
|
1908
1594
|
if (plan.plan_kind === "timeline" && result.kind === "rejected" && result.reason === "push_rejected") documents.delete(docId);
|
|
1909
1595
|
}
|
|
1910
|
-
async function fetchEntityStateForSandbox(docId,
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
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
|
-
});
|
|
1596
|
+
async function fetchEntityStateForSandbox(docId, _doc, _pull) {
|
|
1597
|
+
const state = await getEntityClient(docId).fetchState();
|
|
1598
|
+
if (!state.loroSnapshot) throw new Error("Project requires the Loro entity contract");
|
|
1599
|
+
return state;
|
|
1961
1600
|
}
|
|
1962
1601
|
async function commitCachedPlan(docId, _doc, plan, validation, baseState) {
|
|
1963
1602
|
if (plan.plan_kind === "timeline") throw new Error("Legacy timeline plans are not editable; use an Entity/Relation plan");
|
|
1964
|
-
if (validation === "preflight") throw new Error("Entity plans use revision CAS; validation=preflight is not supported");
|
|
1965
1603
|
if (plan.entity_rows === void 0) throw new Error("entity plan is missing its authoritative rows");
|
|
1966
1604
|
const client = getEntityClient(docId);
|
|
1967
|
-
|
|
1968
|
-
|
|
1605
|
+
if (options.loadGenerationFacts !== void 0 && baseState === void 0) throw new Error("Generation synchronization is missing the cached causal plan baseline");
|
|
1606
|
+
const synced = await attachGenerationSync(docId, plan, await commitEntityPlan(client, plan), baseState);
|
|
1607
|
+
if (synced.kind !== "committed" || options.loadCaptionAssets === void 0) return synced;
|
|
1608
|
+
const assembly = await assembleCaptionAssets({
|
|
1609
|
+
client,
|
|
1610
|
+
docId,
|
|
1611
|
+
loadAssets: options.loadCaptionAssets
|
|
1612
|
+
});
|
|
1613
|
+
return {
|
|
1614
|
+
...synced,
|
|
1615
|
+
asset_assembly: assembly,
|
|
1616
|
+
...assembly.status === "failed" ? { warnings: [...synced.warnings ?? [], {
|
|
1617
|
+
kind: "asset_assembly_failed",
|
|
1618
|
+
message: assembly.message ?? "Caption Asset assembly failed"
|
|
1619
|
+
}] } : {}
|
|
1620
|
+
};
|
|
1969
1621
|
}
|
|
1970
1622
|
/**
|
|
1971
1623
|
* After a confirmed entity commit, connect fact-matched generated Relations
|
|
@@ -2055,48 +1707,12 @@ function createMedeoTool(options) {
|
|
|
2055
1707
|
op: "snapshot",
|
|
2056
1708
|
doc_id: input.doc_id,
|
|
2057
1709
|
version: `${encodeDocVersionMark(doc.versionMark())}:entities:${entityState.revision}`,
|
|
2058
|
-
preview: renderEntitySnapshot(entityState)
|
|
1710
|
+
preview: renderEntitySnapshot(entityState),
|
|
2059
1711
|
collaborated: pull.collaborated,
|
|
2060
1712
|
...pull.warnings !== void 0 ? { warnings: pull.warnings } : {}
|
|
2061
1713
|
};
|
|
2062
1714
|
});
|
|
2063
1715
|
}
|
|
2064
|
-
async function migrate(input) {
|
|
2065
|
-
return runExclusive(input.doc_id, async (doc) => {
|
|
2066
|
-
assertNoPendingPush(input.doc_id);
|
|
2067
|
-
const client = getGraphClient(input.doc_id);
|
|
2068
|
-
const base = await client.fetchState();
|
|
2069
|
-
if (base.rows.entities.some((row) => row.entityKind === "timeline")) return {
|
|
2070
|
-
ok: true,
|
|
2071
|
-
op: "migrate-legacy",
|
|
2072
|
-
doc_id: input.doc_id,
|
|
2073
|
-
migration_status: "already_entity",
|
|
2074
|
-
entity_revision: base.revision,
|
|
2075
|
-
next_action: "snapshot"
|
|
2076
|
-
};
|
|
2077
|
-
const pull = await doc.pull();
|
|
2078
|
-
if (!pull.ok) throw new Error(`Migration requires a fresh canonical snapshot: ${pull.error.message}`);
|
|
2079
|
-
const migrationBaseVv = encodeDocVersionMark(doc.versionMark());
|
|
2080
|
-
const nextRows = migrateLegacyTimelineToEntities(doc.snapshot(), input.asset_facts, base.rows);
|
|
2081
|
-
let revision;
|
|
2082
|
-
try {
|
|
2083
|
-
revision = (await client.commit(base, nextRows, { migrationBaseVv })).revision;
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
if (error instanceof MengineHttpRequestError) throw new Error(`Migration rejected (HTTP ${error.status}): ${entityHttpErrorMessage(error.payload)}; take a fresh snapshot before retrying`);
|
|
2086
|
-
throw new Error("Migration submission is unconfirmed; take a fresh snapshot and retry migrate-legacy to inspect whether the Entity timeline already exists");
|
|
2087
|
-
}
|
|
2088
|
-
documents.delete(input.doc_id);
|
|
2089
|
-
for (const [id, cached] of plans) if (cached.docId === input.doc_id) plans.delete(id);
|
|
2090
|
-
return {
|
|
2091
|
-
ok: true,
|
|
2092
|
-
op: "migrate-legacy",
|
|
2093
|
-
doc_id: input.doc_id,
|
|
2094
|
-
migration_status: "committed",
|
|
2095
|
-
entity_revision: revision,
|
|
2096
|
-
next_action: "snapshot"
|
|
2097
|
-
};
|
|
2098
|
-
});
|
|
2099
|
-
}
|
|
2100
1716
|
async function run(input) {
|
|
2101
1717
|
return runExclusive(input.doc_id, async (doc) => {
|
|
2102
1718
|
assertNoPendingPush(input.doc_id);
|
|
@@ -2206,7 +1822,6 @@ function createMedeoTool(options) {
|
|
|
2206
1822
|
try {
|
|
2207
1823
|
const parsed = parseInput(input);
|
|
2208
1824
|
if (parsed.op === "snapshot") return await snapshot(parsed);
|
|
2209
|
-
if (parsed.op === "migrate-legacy") return await migrate(parsed);
|
|
2210
1825
|
if (parsed.op === "run-edit-script") return await run(parsed);
|
|
2211
1826
|
return await commit(parsed);
|
|
2212
1827
|
} catch (error) {
|
|
@@ -2239,6 +1854,139 @@ function createMedeoTool(options) {
|
|
|
2239
1854
|
};
|
|
2240
1855
|
}
|
|
2241
1856
|
//#endregion
|
|
2242
|
-
|
|
1857
|
+
//#region src/entity/materialize-resources.ts
|
|
1858
|
+
/** Persist resources before model consumption. The host serializes repeated imports of the same generation task. */
|
|
1859
|
+
async function materializeResources(options, resources) {
|
|
1860
|
+
const client = new EntityHttpClient(options);
|
|
1861
|
+
const state = await client.fetchState();
|
|
1862
|
+
let resourceKey = "";
|
|
1863
|
+
const sandbox = new EntitySandbox({
|
|
1864
|
+
state,
|
|
1865
|
+
idFactory: (prefix) => stableId(prefix, options.docId, resourceKey)
|
|
1866
|
+
});
|
|
1867
|
+
const ids = [];
|
|
1868
|
+
for (const resource of resources) {
|
|
1869
|
+
resourceKey = `${resource.kind}:${resource.assetId}`;
|
|
1870
|
+
if (resource.kind !== "caption") {
|
|
1871
|
+
ids.push(sandbox.entities.ensureMedia(resource).contentEntityId);
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
const assetId = stableId("asset", resource.assetId);
|
|
1875
|
+
const asset = sandbox.entities.get(assetId);
|
|
1876
|
+
if (asset) {
|
|
1877
|
+
if (asset.entity_kind !== "asset" || asset.payload.storageKey !== resource.storageKey) throw new Error("Conflicting Caption resource identity");
|
|
1878
|
+
const captions = sandbox.relations.of(assetId, "physical-asset").map((edge) => sandbox.entities.get(edge.endpoint_0_entity_id)).filter((entity) => entity?.entity_kind === "caption");
|
|
1879
|
+
if (captions.length !== 1) throw new Error("Caption resource must resolve to one materialized Caption");
|
|
1880
|
+
ids.push(captions[0].entity_id);
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1883
|
+
if (!resource.assetId.trim() || !resource.storageKey.trim() || !resource.segments.length) throw new Error("Caption resource requires a physical locator and timed segments");
|
|
1884
|
+
for (const segment of resource.segments) {
|
|
1885
|
+
if (typeof segment.text !== "string" || !Number.isFinite(segment.startMs) || !Number.isFinite(segment.endMs) || segment.startMs < 0 || segment.endMs <= segment.startMs) throw new Error("Caption resource has invalid ASR text or timing");
|
|
1886
|
+
for (const word of segment.words ?? []) if (typeof word.text !== "string" || !Number.isFinite(word.startMs) || !Number.isFinite(word.endMs) || word.startMs < segment.startMs || word.endMs > segment.endMs || word.endMs < word.startMs) throw new Error("Caption resource has invalid ASR word timing");
|
|
1887
|
+
}
|
|
1888
|
+
const scriptId = sandbox.audioScriptEntityId;
|
|
1889
|
+
if (!scriptId) throw new Error("Document AudioScript is not initialized");
|
|
1890
|
+
const script = sandbox.entities.get(scriptId);
|
|
1891
|
+
const segments = resource.segments.map((segment, index) => ({
|
|
1892
|
+
segmentId: stableId("segment", resource.assetId, String(index)),
|
|
1893
|
+
text: segment.text
|
|
1894
|
+
}));
|
|
1895
|
+
const start = Math.min(...resource.segments.map((segment) => segment.startMs));
|
|
1896
|
+
const end = Math.max(...resource.segments.map((segment) => segment.endMs));
|
|
1897
|
+
sandbox.entities.update({
|
|
1898
|
+
entity_id: scriptId,
|
|
1899
|
+
payload: { segments: [...script.payload.segments, ...segments] }
|
|
1900
|
+
});
|
|
1901
|
+
const captionId = stableId("entity", "caption", resource.assetId);
|
|
1902
|
+
sandbox.entities.create({
|
|
1903
|
+
entity_id: captionId,
|
|
1904
|
+
entity_kind: "caption",
|
|
1905
|
+
payload: {
|
|
1906
|
+
baseEntityIds: [scriptId],
|
|
1907
|
+
selections: segments.map(({ segmentId }) => ({ segmentId })),
|
|
1908
|
+
extent: {
|
|
1909
|
+
kind: "bounded",
|
|
1910
|
+
start,
|
|
1911
|
+
end
|
|
1912
|
+
},
|
|
1913
|
+
sampling: "native",
|
|
1914
|
+
coordinateSpace: "milliseconds"
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
const markerId = sandbox.entities.create({
|
|
1918
|
+
entity_id: stableId("entity", "asr-marker", resource.assetId),
|
|
1919
|
+
entity_kind: "sequence-marker",
|
|
1920
|
+
payload: {
|
|
1921
|
+
sourceRange: {
|
|
1922
|
+
start,
|
|
1923
|
+
end
|
|
1924
|
+
},
|
|
1925
|
+
duration: { mode: "from-source" },
|
|
1926
|
+
segmentRanges: resource.segments.map((segment, index) => ({
|
|
1927
|
+
segmentId: segments[index].segmentId,
|
|
1928
|
+
startMs: segment.startMs,
|
|
1929
|
+
endMs: segment.endMs
|
|
1930
|
+
})),
|
|
1931
|
+
wordRanges: resource.segments.flatMap((segment, index) => (segment.words ?? []).map((word) => ({
|
|
1932
|
+
...word,
|
|
1933
|
+
segmentIndex: index
|
|
1934
|
+
})))
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
sandbox.relations.link({
|
|
1938
|
+
relation_id: stableId("relation", "asr-marker", resource.assetId),
|
|
1939
|
+
relation_kind: "audio-script-marker",
|
|
1940
|
+
endpoint_0_entity_id: scriptId,
|
|
1941
|
+
endpoint_1_entity_id: markerId
|
|
1942
|
+
});
|
|
1943
|
+
sandbox.entities.create({
|
|
1944
|
+
entity_id: assetId,
|
|
1945
|
+
entity_kind: "asset",
|
|
1946
|
+
payload: {
|
|
1947
|
+
external: {
|
|
1948
|
+
system: "memota",
|
|
1949
|
+
key: resource.assetId
|
|
1950
|
+
},
|
|
1951
|
+
storageKey: resource.storageKey
|
|
1952
|
+
}
|
|
1953
|
+
});
|
|
1954
|
+
sandbox.relations.link({
|
|
1955
|
+
relation_id: stableId("relation", captionId, assetId),
|
|
1956
|
+
relation_kind: "physical-asset",
|
|
1957
|
+
endpoint_0_entity_id: captionId,
|
|
1958
|
+
endpoint_1_entity_id: assetId
|
|
1959
|
+
});
|
|
1960
|
+
ids.push(captionId);
|
|
1961
|
+
}
|
|
1962
|
+
const candidate = sandbox.buildPlan();
|
|
1963
|
+
if (candidate.commands.length && options.loadGenerationFacts) {
|
|
1964
|
+
const newIds = new Set(ids.filter((id) => !state.entities.some((row) => row.entity_id === id)));
|
|
1965
|
+
const facts = parseGenerationFacts(await options.loadGenerationFacts(options.docId, resources.map((item) => item.assetId)));
|
|
1966
|
+
const relations = planGeneratedRelations({
|
|
1967
|
+
baseState: state,
|
|
1968
|
+
state: candidate.rows,
|
|
1969
|
+
scopedMediaIds: newIds,
|
|
1970
|
+
facts,
|
|
1971
|
+
newRelationId: () => `relation_${globalThis.crypto.randomUUID()}`
|
|
1972
|
+
});
|
|
1973
|
+
for (const relation of relations) sandbox.relations.linkGenerated({
|
|
1974
|
+
relation_id: relation.relation_id,
|
|
1975
|
+
output_entity_id: relation.endpoint_0_entity_id,
|
|
1976
|
+
input_entity_id: relation.endpoint_1_entity_id,
|
|
1977
|
+
trace: relation.trace
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
const plan = sandbox.buildPlan();
|
|
1981
|
+
if (!plan.commands.length) return ids;
|
|
1982
|
+
const committed = await client.commit(state.revision, plan.rows);
|
|
1983
|
+
for (const id of ids) if (!committed.entities.some((entity) => entity.entity_id === id)) throw new Error(`Resource entity ${id} was not confirmed by the merged document`);
|
|
1984
|
+
return ids;
|
|
1985
|
+
}
|
|
1986
|
+
function stableId(prefix, ...parts) {
|
|
1987
|
+
return `${prefix}_${createHash("sha256").update(JSON.stringify(parts)).digest("hex")}`;
|
|
1988
|
+
}
|
|
1989
|
+
//#endregion
|
|
1990
|
+
export { EditSandboxSession, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
|
|
2243
1991
|
|
|
2244
1992
|
//# sourceMappingURL=index.mjs.map
|