@mengine/medeo-tool 1.4.1-alpha.5 → 2.0.1-alpha.10

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/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as UpdateEntityInput, S as UnlinkRelationInput, _ as LinkRelationInput, a as DeleteEntityInput, b as SandboxEntity, c as EntitySandboxCheckpoint, d as JsonObject, f as JsonPrimitive, g as LinkGeneratedRelationInput, h as KnownRelationKind, i as CreateEntityInput, l as EntityStoreSnapshot, m as KnownEntityKind, n as BusinessEntityFacade, o as EntityCommand, p as JsonValue, r as BusinessRelationFacade, s as EntityPlanState, t as AuthorableRelationKind, u as EntityUpdateInput, v as RelationUpdateInput, x as SandboxRelation, y as ResourceEntityKind } from "./entity-contract-DlmUSouB.mjs";
1
+ import { C as UpdateEntityInput, S as UnlinkRelationInput, _ as LinkRelationInput, a as DeleteEntityInput, b as SandboxEntity, c as EntitySandboxCheckpoint, d as JsonObject, f as JsonPrimitive, g as LinkGeneratedRelationInput, h as KnownRelationKind, i as CreateEntityInput, l as EntityStoreSnapshot, m as KnownEntityKind, n as BusinessEntityFacade, o as EntityCommand, p as JsonValue, r as BusinessRelationFacade, s as EntityPlanState, t as AuthorableRelationKind, u as EntityUpdateInput, v as RelationUpdateInput, x as SandboxRelation, y as ResourceEntityKind } from "./entity-contract-Cf3AiSe7.mjs";
2
2
  import { JournalEntry, ManualSyncDoc, MediaAssetFact, PartIdFactory, SemanticOpName, VideoDocument, VideoDraft } from "@mengine/medeo-client";
3
3
 
4
4
  //#region src/document/compact-projection.d.ts
@@ -11,6 +11,14 @@ import { JournalEntry, ManualSyncDoc, MediaAssetFact, PartIdFactory, SemanticOpN
11
11
  * `fromVideoDocument`.
12
12
  */
13
13
  interface CompactProjectionOptions {
14
+ /**
15
+ * Document identity for the header. Stated by the caller: the document
16
+ * content no longer carries its own id or revision (business metadata left
17
+ * the CRDT), and the model needs to know which draft this summary describes.
18
+ * Omitted values keep the header's fixed shape with the historical blanks.
19
+ */
20
+ docId?: string;
21
+ revision?: number;
14
22
  /** Only render these parts (header still reports the full timeline total). Default = all. */
15
23
  onlyPartIds?: ReadonlySet<string>;
16
24
  /** Caption text preview truncation length. Default 24. */
@@ -30,7 +38,10 @@ declare function collectAffectedPartIds(journal: readonly JournalEntry[]): Set<s
30
38
  * Render a ChangePlan preview: header + rows for journal-affected parts only.
31
39
  * Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
32
40
  */
33
- declare function renderPreview(document: VideoDocument, journal: readonly JournalEntry[]): string;
41
+ declare function renderPreview(document: VideoDocument, journal: readonly JournalEntry[], identity?: {
42
+ docId?: string;
43
+ revision?: number;
44
+ }): string;
34
45
  //#endregion
35
46
  //#region src/entity/entity-asset.d.ts
36
47
  /** Immutable resource content resolved by the host for a document Entity. */
@@ -40,6 +51,8 @@ interface EntityAssetContent {
40
51
  }
41
52
  /** Host-only I/O. The sandbox supplies an Entity, never an arbitrary Asset query. */
42
53
  type EntityAssetLoader = (docId: string, entity: SandboxEntity) => Promise<EntityAssetContent>;
54
+ /** Program-only initial resource creation; never exposed as a sandbox API. */
55
+ type EntityAssetWriter = (docId: string, entity: SandboxEntity, content: JsonValue) => Promise<EntityAssetContent>;
43
56
  //#endregion
44
57
  //#region src/entity/entity-sandbox.d.ts
45
58
  type DomainIdFactory = (prefix: 'entity' | 'relation') => string;
@@ -61,6 +74,12 @@ interface ChangePlan {
61
74
  logs: string[];
62
75
  }
63
76
  interface EditSandboxSessionOptions {
77
+ /**
78
+ * Document this session edits. Stated by the host: the document content no
79
+ * longer carries its own identity (business metadata left the CRDT), and a
80
+ * plan must name the document it will be committed against.
81
+ */
82
+ docId?: string;
64
83
  idFactory?: PartIdFactory;
65
84
  onEntry?: (entry: JournalEntry) => void;
66
85
  onLog?: (line: string) => void;
@@ -87,7 +106,10 @@ interface ConsoleShim {
87
106
  * injects loaders that break worker boot.
88
107
  */
89
108
  interface RunEditScriptOptions {
109
+ /** Document the plan will be committed against; stated by the host. */
110
+ docId: string;
90
111
  loadEntityAsset?: (entity: SandboxEntity) => Promise<EntityAssetContent>;
112
+ writeEntityAsset?: (entity: SandboxEntity, content: EntityAssetContent['content']) => Promise<EntityAssetContent>;
91
113
  document: VideoDocument;
92
114
  baseVersion: string;
93
115
  script: string;
@@ -376,13 +398,6 @@ interface CreateMedeoToolOptions {
376
398
  userId?: ContextualValue<string>;
377
399
  /** Stable agent peer id. Supply a host-scoped value so audit provenance is durable. */
378
400
  peerId?: ContextualValue<string>;
379
- /**
380
- * Load the authoritative legacy draft used to create a missing Mengine
381
- * document. The tool owns the get-or-create flow: it first probes Mengine,
382
- * converts this draft into a VideoDocument only on a 404, bootstraps the
383
- * snapshot, and tolerates a concurrent creator winning the race.
384
- */
385
- loadInitialDraft?: (docId: string) => Promise<VideoDraft>;
386
401
  /**
387
402
  * Resolve factual generation lineage by external asset id after a confirmed
388
403
  * entity commit. Return every known generation record involving the given
@@ -395,6 +410,8 @@ interface CreateMedeoToolOptions {
395
410
  /** Assemble optional Caption artifacts by immutable entity ID; never exposed to scripts. */
396
411
  loadCaptionAssets?: CaptionAssetsLoader;
397
412
  loadEntityAsset?: EntityAssetLoader;
413
+ /** Program-side persistence for immutable AudioScript resources. */
414
+ writeEntityAsset?: EntityAssetWriter;
398
415
  fetchImpl?: typeof fetch;
399
416
  /** @deprecated ManualSyncDoc has no SSE or reconnect loop. */
400
417
  sseReconnectDelayMs?: number;
@@ -567,5 +584,5 @@ declare function materializeResources(options: EntityHttpClientOptions & {
567
584
  loadGenerationFacts?: GenerationFactsLoader;
568
585
  }, resources: readonly GeneratedResource[]): Promise<readonly string[]>;
569
586
  //#endregion
570
- export { type AssetGenerationFact, type AuthorableRelationKind, type BusinessEntityFacade, type BusinessRelationFacade, type CaptionAssetAssemblyOutcome, type CaptionAssetFact, type CaptionAssetsLoader, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditSandboxSessionOptions, type EditScriptResult, type EntityAssetContent, type EntityAssetLoader, type EntityCommand, type EntityCommitResult, type EntityPlanState, type EntitySandboxCheckpoint, type EntityStoreSnapshot, type EntityUpdateInput, type GeneratedResource, type GenerationFactsLoader, type GenerationSyncOutcome, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationUpdateInput, type ResourceEntityKind, type RunEditScriptOptions, type SandboxEntity, type SandboxRelation, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
587
+ export { type AssetGenerationFact, type AuthorableRelationKind, type BusinessEntityFacade, type BusinessRelationFacade, type CaptionAssetAssemblyOutcome, type CaptionAssetFact, type CaptionAssetsLoader, type ChangePlan, type CommitPlan, type CommitPlanOptions, type CommitPlanResult, type CompactProjectionOptions, type ConsoleShim, type CreateEntityInput, type CreateMedeoToolOptions, type DeleteEntityInput, type EditSandboxSessionOptions, type EditScriptResult, type EntityAssetContent, type EntityAssetLoader, type EntityAssetWriter, type EntityCommand, type EntityCommitResult, type EntityPlanState, type EntitySandboxCheckpoint, type EntityStoreSnapshot, type EntityUpdateInput, type GeneratedResource, type GenerationFactsLoader, type GenerationSyncOutcome, type JsonObject, type JsonPrimitive, type JsonValue, type KnownEntityKind, type KnownRelationKind, type LinkGeneratedRelationInput, type LinkRelationInput, MEDEO_TOOL_DESCRIPTION, MEDEO_TOOL_NAME, MEDEO_TOOL_PARAMETERS, type MedeoCommitResult, type MedeoInitialDraft, type MedeoModelContext, type MedeoModelContextInput, type MedeoTool, type MedeoToolInput, type MedeoToolOp, type MedeoToolResult, type RelationUpdateInput, type ResourceEntityKind, type RunEditScriptOptions, type SandboxEntity, type SandboxRelation, type UnlinkRelationInput, type UpdateEntityInput, collectAffectedPartIds, commitPlan, createMedeoTool, materializeResources, renderCompactProjection, renderPreview, runEditScript };
571
588
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { c as isMediaAssetVariantKind, i as businessState, o as createEntityId, s as createRelationId, t as EntitySandbox } from "./entity-sandbox-TaUVT3on.mjs";
2
- import { LoroEntityDocument, ManualSyncDoc, MengineHttpClient, MengineHttpRequestError, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createMirrorVideoDocument, createPlainMemoryAdapter, decodeDocVersionMark, effectiveVideoClipDurationMs, encodeDocVersionMark, ensureEditorFoundation, replayJournal, solveVideoDocument, speedOf, toVideoDocument } from "@mengine/medeo-client";
1
+ import { a as createEntityId, i as businessState, o as createRelationId, s as isMediaAssetVariantKind, t as EntitySandbox } from "./entity-sandbox-BTR2cRl1.mjs";
2
+ import { ManualSyncDoc, MengineHttpClient, ValidationError, base64ToBytes, bytesToBase64, compileEntityRows, createPlainMemoryAdapter, decodeDocVersionMark, effectiveVideoClipDurationMs, encodeDocVersionMark, replayJournal, solveVideoDocument, speedOf } from "@mengine/medeo-client";
3
3
  import { Worker } from "node:worker_threads";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  //#region src/document/compact-projection.ts
@@ -79,7 +79,7 @@ function renderCompactProjection(document, options) {
79
79
  rows.push(`${tag} ${partId} ${lane} [${abs},${abs + dur}) ${attrs}`);
80
80
  }
81
81
  }
82
- return [`# draft=${document.meta.draft_id ?? ""} v=${document.meta.version ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
82
+ return [`# draft=${options?.docId ?? ""} v=${options?.revision ?? 0} duration=${solved.durationMs} parts=${totalParts} shown=${rows.length}`, ...rows].join("\n");
83
83
  }
84
84
  //#endregion
85
85
  //#region src/sandbox/preview.ts
@@ -132,8 +132,12 @@ function collectFromValue(value, ids) {
132
132
  * Render a ChangePlan preview: header + rows for journal-affected parts only.
133
133
  * Empty journal → empty `onlyPartIds` (header alone), matching the T2 contract.
134
134
  */
135
- function renderPreview(document, journal) {
136
- return renderCompactProjection(document, { onlyPartIds: journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal) });
135
+ function renderPreview(document, journal, identity) {
136
+ const onlyPartIds = journal.length === 0 ? /* @__PURE__ */ new Set() : collectAffectedPartIds(journal);
137
+ return renderCompactProjection(document, {
138
+ ...identity,
139
+ onlyPartIds
140
+ });
137
141
  }
138
142
  //#endregion
139
143
  //#region src/sandbox/node-host.ts
@@ -165,6 +169,7 @@ function runEditScript(options) {
165
169
  let timer;
166
170
  const worker = new Worker(workerEntryUrl, {
167
171
  workerData: {
172
+ docId: options.docId,
168
173
  document: options.document,
169
174
  script: options.script,
170
175
  inputs: options.inputs,
@@ -214,11 +219,14 @@ function runEditScript(options) {
214
219
  armTimeout();
215
220
  return;
216
221
  }
217
- if (message.t === "entity-asset") {
222
+ if (message.t === "entity-asset" || message.t === "write-entity-asset") {
218
223
  (async () => {
219
224
  try {
220
- if (!options.loadEntityAsset) throw new Error("Entity Asset loader is unavailable");
221
- const result = await options.loadEntityAsset(message.entity);
225
+ const result = message.t === "write-entity-asset" ? await (options.writeEntityAsset ?? (() => {
226
+ throw new Error("Entity Asset writer is unavailable");
227
+ }))(message.entity, message.content) : await (options.loadEntityAsset ?? (() => {
228
+ throw new Error("Entity Asset loader is unavailable");
229
+ }))(message.entity);
222
230
  if (!settled) worker.postMessage({
223
231
  t: "entity-asset-result",
224
232
  requestId: message.requestId,
@@ -272,7 +280,7 @@ function runEditScript(options) {
272
280
  ok: true,
273
281
  plan: {
274
282
  plan_kind: message.planKind,
275
- doc_id: options.document.meta.draft_id ?? "",
283
+ doc_id: options.docId,
276
284
  base_version: options.baseVersion,
277
285
  ops: ops.slice(),
278
286
  ...message.loroUpdate ? { loro_update: message.loroUpdate } : {},
@@ -338,7 +346,7 @@ function runEditScript(options) {
338
346
  async function assembleCaptionAssets(input) {
339
347
  try {
340
348
  const state = await input.client.fetchState();
341
- 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)));
349
+ const candidates = state.entities.filter((entity) => entity.entity_kind === "caption" && !state.relations.some((relation) => relation.relation_kind === "from-asset" && (relation.endpoint_0_entity_id === entity.entity_id || relation.endpoint_1_entity_id === entity.entity_id)));
342
350
  if (!candidates.length) return { status: "current" };
343
351
  const ids = new Set(candidates.map((entity) => entity.entity_id));
344
352
  const facts = await input.loadAssets(input.docId, [...ids]);
@@ -353,11 +361,8 @@ async function assembleCaptionAssets(input) {
353
361
  continue;
354
362
  }
355
363
  bound.set(fact.captionEntityId, fact.assetId);
356
- const matches = entities.filter((entity) => {
357
- const external = entity.payload.external;
358
- return external !== null && typeof external === "object" && !Array.isArray(external) && external.system === "memota" && external.key === fact.assetId;
359
- });
360
- if (matches.length > 1 || matches[0] && matches[0].entity_kind !== "asset") throw new Error(`Conflicting resource identity for Caption Asset ${fact.assetId}`);
364
+ const matches = entities.filter((entity) => entity.entity_kind === "asset" && entity.payload.system === "memota" && entity.payload.key === fact.assetId);
365
+ if (matches.length > 1) throw new Error(`Conflicting resource identity for Caption Asset ${fact.assetId}`);
361
366
  let asset = matches[0];
362
367
  if (asset && asset.payload.storageKey !== fact.storageKey) throw new Error(`Conflicting storage key for Caption Asset ${fact.assetId}`);
363
368
  if (!asset) {
@@ -365,10 +370,8 @@ async function assembleCaptionAssets(input) {
365
370
  entity_id: stableId$1("asset", fact.assetId),
366
371
  entity_kind: "asset",
367
372
  payload: {
368
- external: {
369
- system: "memota",
370
- key: fact.assetId
371
- },
373
+ system: "memota",
374
+ key: fact.assetId,
372
375
  storageKey: fact.storageKey
373
376
  }
374
377
  };
@@ -377,7 +380,7 @@ async function assembleCaptionAssets(input) {
377
380
  }
378
381
  relations.push({
379
382
  relation_id: stableId$1("relation", fact.captionEntityId, asset.entity_id),
380
- relation_kind: "physical-asset",
383
+ relation_kind: "from-asset",
381
384
  endpoint_0_entity_id: fact.captionEntityId,
382
385
  endpoint_1_entity_id: asset.entity_id,
383
386
  metadata: {},
@@ -429,11 +432,12 @@ const KNOWN_RELATION_KINDS = [
429
432
  "marker-content",
430
433
  "axvideo-marker",
431
434
  "marker-timeline",
432
- "physical-asset",
435
+ "from-asset",
433
436
  "generated",
434
437
  "caption-alignment",
435
438
  "clip-anchor",
436
439
  "phonetic-script-render",
440
+ "voice-timbre",
437
441
  "audio-script-source",
438
442
  "audio-script-marker"
439
443
  ];
@@ -738,25 +742,6 @@ function isRecord$1(value) {
738
742
  /** @generated by gen:sandbox-dts. DO NOT EDIT. */
739
743
  const ENTITY_EDIT_SANDBOX_API_DTS = [
740
744
  "/** @generated by gen:sandbox-dts. Entity-native editor contract; DO NOT EDIT. */",
741
- "export interface BoundedDerivedSequencePayload extends JsonObject {",
742
- " extent: {",
743
- " kind: 'bounded';",
744
- " start: number;",
745
- " end: number;",
746
- " };",
747
- " sampling: 'derived';",
748
- " coordinateSpace: JsonValue;",
749
- "}",
750
- "export interface BoundedNativeSequencePayload extends JsonObject {",
751
- " /** Factual coordinates from recalled media metadata; never invent an end/duration. */",
752
- " extent: {",
753
- " kind: 'bounded';",
754
- " start: number;",
755
- " end: number;",
756
- " };",
757
- " sampling: 'native';",
758
- " coordinateSpace: JsonValue;",
759
- "}",
760
745
  "/** Business editing surface. Reads are assembled snapshots; writes preserve native operation intent. */",
761
746
  "export interface BusinessEntityFacade {",
762
747
  " list(): SandboxEntity[];",
@@ -805,17 +790,20 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
805
790
  " content: JsonValue;",
806
791
  "}",
807
792
  "export interface EntityPayloadByKind {",
808
- " axvideo: BoundedDerivedSequencePayload;",
793
+ " /** Its Timeline decides how long it runs, so it stores no length. */",
794
+ " axvideo: JsonObject;",
809
795
  " timeline: JsonObject;",
810
796
  " track: JsonObject & {",
811
797
  " hidden?: boolean;",
812
798
  " role?: string;",
813
799
  " };",
814
800
  " clip: JsonObject;",
815
- " video: BoundedNativeSequencePayload & MediaAssetPayload;",
816
- " audio: BoundedNativeSequencePayload & MediaAssetPayload;",
817
- " voice: BoundedNativeSequencePayload & MediaAssetPayload;",
818
- " image: UnboundedConstantSequencePayload & MediaAssetPayload;",
801
+ " video: OwnDurationPayload;",
802
+ " /** Every playable sound, including video original audio and synthesized voiceovers. */",
803
+ " audio: OwnDurationPayload;",
804
+ " /** A timbre identity, never playable content; the rendered take is an `audio` entity. */",
805
+ " voice: VoiceIdentityPayload;",
806
+ " image: NoDurationPayload;",
819
807
  " 'sequence-marker': JsonObject & {",
820
808
  " sourceRange: {",
821
809
  " start: number;",
@@ -852,7 +840,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
852
840
  " phonemeScript?: string;",
853
841
  " prosody?: JsonObject;",
854
842
  " };",
855
- " caption: BoundedNativeSequencePayload & {",
843
+ " caption: OwnDurationPayload & {",
856
844
  " baseEntityIds: string[];",
857
845
  " selection: CaptionTextSelection;",
858
846
  " style?: JsonObject;",
@@ -942,6 +930,7 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
942
930
  " | 'caption-alignment'",
943
931
  " | 'clip-anchor'",
944
932
  " | 'phonetic-script-render'",
933
+ " | 'voice-timbre'",
945
934
  " | 'audio-script-source'",
946
935
  " | 'audio-script-marker';",
947
936
  "interface LinkRelationBase {",
@@ -954,12 +943,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
954
943
  " relation_kind: KnownRelationKind;",
955
944
  " metadata?: JsonObject;",
956
945
  "}",
957
- "export type MediaAssetPayload = JsonObject & {",
958
- " external: {",
959
- " system: 'memota' | 'memota-speech';",
960
- " key: string;",
961
- " };",
962
- "};",
946
+ "/** A still frame runs for as long as its use asks, so it states no length. */",
947
+ "export interface NoDurationPayload extends JsonObject {",
948
+ " durationMs: null;",
949
+ "}",
950
+ "/**",
951
+ " * Content states how long it runs and nothing else: where it is taken from and",
952
+ " * where it lands both belong to its Sequence Marker.",
953
+ " */",
954
+ "export interface OwnDurationPayload extends JsonObject {",
955
+ " /** Factual whole milliseconds from recalled media metadata; never invented. */",
956
+ " durationMs: number;",
957
+ "}",
963
958
  "export interface RelationUpdateInput {",
964
959
  " relation_id: string;",
965
960
  " changes: FieldChange[];",
@@ -982,22 +977,22 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
982
977
  " text: string;",
983
978
  " language?: string;",
984
979
  "};",
985
- "/** Stored own fields; a variant may obtain required content fields from its declared bases. */",
980
+ "/**",
981
+ " * Stored own fields; a variant may obtain required content fields from its",
982
+ " * declared bases.",
983
+ " *",
984
+ " * Caption and Audio may also be declared resource-only: the ASR transcript and",
985
+ " * the synthesized voiceover are attached by the host, which then fills in the",
986
+ " * remaining factual fields.",
987
+ " */",
986
988
  "export type StoredEntityPayload<K extends KnownEntityKind> =",
987
989
  " | EntityPayloadByKind[K]",
988
- " | (K extends 'caption' ? JsonObject & Pick<MediaAssetPayload, 'external'> : never)",
990
+ " // Resource-only: the host fills the factual fields after reading the Asset.",
991
+ " | (K extends 'caption' | 'audio' ? Record<string, never> : never)",
989
992
  " | (JsonObject &",
990
993
  " Partial<EntityPayloadByKind[K]> & {",
991
994
  " baseEntityIds: string[];",
992
995
  " });",
993
- "export interface UnboundedConstantSequencePayload extends JsonObject {",
994
- " extent: {",
995
- " kind: 'unbounded';",
996
- " start: number;",
997
- " };",
998
- " sampling: 'constant';",
999
- " coordinateSpace: JsonValue;",
1000
- "}",
1001
996
  "export interface UnlinkRelationInput {",
1002
997
  " relation_id: string;",
1003
998
  "}",
@@ -1006,9 +1001,17 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
1006
1001
  " entity_id: string;",
1007
1002
  " payload: JsonObject;",
1008
1003
  "}",
1004
+ "/** The voice-library timbre a synthesized Audio was rendered with. */",
1005
+ "export type VoiceIdentityPayload = JsonObject & {",
1006
+ " voice: {",
1007
+ " system: 'voice-library';",
1008
+ " key: string;",
1009
+ " name?: string;",
1010
+ " };",
1011
+ "};",
1009
1012
  "export declare const entities: BusinessEntityFacade;",
1010
1013
  "export declare const relations: BusinessRelationFacade;",
1011
- "/** Resolve this Entity's attached resource through the host; await before using an uninitialized Caption. */",
1014
+ "/** Resolve this Entity's attached resource through the host; await before using an uninitialized Caption or Voice. */",
1012
1015
  "export declare function rgetAssetFromEntity(entityId: string): Promise<EntityAssetContent>;",
1013
1016
  "export declare function checkpoint(): EntitySandboxCheckpoint;",
1014
1017
  "export declare function rollbackTo(cp: EntitySandboxCheckpoint): void;",
@@ -1038,19 +1041,21 @@ The host supplies the current document. Do not invent or request a document id.
1038
1041
 
1039
1042
  Entity reads return assembled snapshots. Mutating returned objects has no persistence effect. Create declares own fields. Update requires a nonempty changes array and atomically applies its ordered operations to assembled fields, routing each to its declaring owner. DeclareFields explicitly establishes own fields or an override; it is not the normal way to edit inherited content. Deletion requires explicitly handling incident relations, variant references and protected project attachments; it never deletes external resources or cascades business entities.
1040
1043
 
1041
- FieldChange paths start at payload for an entity, and at metadata or trace for a relation. Use strings for fields and {elementId:'...'} for list members, never numeric indices. Segment identity is segmentId; reference identity is the referenced id. set writes scalars or schema-declared atomic values, including complete sourceRange, targetRange, extent, duration, timeRemapping, selection, textRange and external. It cannot replace collaborative text, lists or maps. Edit text with text.splice (Unicode code-point index and deleteCount), and segments/segmentRanges with list.insert, list.move or list.remove. beforeElementId:null means append; moving before itself, duplicate identity or an absent anchor is an error. Unset removes optional fields only. Map fields are edited through leaf paths; initialize a new own map via declareFields. baseEntityIds is unordered composition membership: insert/remove are supported, moving bases cannot resolve field conflicts. Independent fields and list-item text edits retain native Loro identities.
1044
+ FieldChange paths start at payload for an entity, and at metadata or trace for a relation. Use strings for fields and {elementId:'...'} for list members, never numeric indices. Segment identity is segmentId; reference identity is the referenced id. set writes scalars or schema-declared atomic values, including complete sourceRange, targetRange, duration, timeRemapping, selection, textRange and external. It cannot replace collaborative text, lists or maps. Edit text with text.splice (Unicode code-point index and deleteCount), and segments/segmentRanges with list.insert, list.move or list.remove. beforeElementId:null means append; moving before itself, duplicate identity or an absent anchor is an error. Unset removes optional fields only. Map fields are edited through leaf paths; initialize a new own map via declareFields. baseEntityIds is unordered composition membership: insert/remove are supported, moving bases cannot resolve field conflicts. Independent fields and list-item text edits retain native Loro identities.
1042
1045
 
1043
1046
  Entities own data. Relations associate two existing entities; endpoint_0 and endpoint_1 retain their declared positions. Each relation kind defines direction and meaning; neither relation listing order nor endpoint storage order implies a global business order. Relations.update changes metadata/trace fields only; kind/endpoints require explicit unlink plus link with a new identity. Generated relations use generated(output,input). Ordinary associations do not implement composition.
1044
1047
 
1045
1048
  Variants directly hold baseEntityIds. Assembly validates every base before applying explicit own overrides: two bases supplying the same field are an error even if values match or the variant declares that field. Missing/cyclic bases fail. Consumers operate assembled fields without copying inherited content or resolving owners themselves. An owned-field edit creates a new immutable entity version. Base changes automatically advance variant base references and project attachments without versioning an unchanged variant. A variant-owned change versions the variant and retains unchanged bases. Do not manually clone entities for versioning. Use the current plan's ids throughout that execution; refresh after commit.
1046
1049
 
1047
- Generation tools return resource references. An asset id may be passed in an entity's external:{system:'memota',key:assetId}; it is never an entity id, baseEntityId or relation endpoint. Physical resources are host infrastructure. The sandbox has no direct resource lookup/CRUD by asset id. rgetAssetFromEntity takes only an existing business entity id and returns {assetId,content} for its attached resource. For output_caption, create a Caption with payload:{external:{system:'memota',key:assetId}}, then await rgetAssetFromEntity(captionId). The host initializes the complete AudioScript and one Caption per resource segment in the same causal plan when selection is omitted. The requested Caption represents the first segment; additional Caption entities share that same script and external reference. After awaiting, discover ALL resource Captions with entities.list filtered by entity_kind and payload.external.key, inspect each singular selection/extent, and place each in its own Clip/SequenceMarker. An explicit selection initializes only that selected Caption. Never place the first Caption over the whole transcript; Without explicit bases, the host creates an independent AudioScript for that resource. With bases, it resolves their unique AudioScript text owner and preserves other nonconflicting bases. It never routes text through the panel selection. Read the editable fields with entities.get afterwards. Repeated reads preserve edited text and return the original immutable resource content. Unawaited initialization is completed before validation; failure prevents publication. Resource-free Caption composition from AudioScript is also valid.
1050
+ Generation tools return resource references. An asset id names an Asset entity ({system:'memota',key:assetId}); every entity made from it links to that Asset with from-asset(entity,asset) and copies no locator of its own. An asset id is never an entity id, baseEntityId or relation endpoint. Physical resources are host infrastructure. The sandbox has no direct resource lookup/CRUD by asset id. rgetAssetFromEntity takes only an existing business entity id and returns {assetId,content} for its attached resource. For output_caption, create a Caption, link it with from-asset to the Asset for that assetId, then await rgetAssetFromEntity(captionId). The host initializes the complete AudioScript and one Caption per resource segment in the same causal plan when selection is omitted. The requested Caption represents the first segment; additional Caption entities share that same script and external reference. After awaiting, discover ALL resource Captions with entities.list filtered by entity_kind and their from-asset Asset, inspect each singular selection/durationMs, and place each in its own Clip/SequenceMarker. Discover them by following from-asset from the Asset. An explicit selection initializes only that selected Caption. Never place the first Caption over the whole transcript; Without explicit bases, the host creates an independent AudioScript for that resource. With bases, it resolves their unique AudioScript text owner and preserves other nonconflicting bases. It never routes text through the panel selection. Read the editable fields with entities.get afterwards. Repeated reads preserve edited text and return the original immutable resource content. Unawaited initialization is completed before validation; failure prevents publication. Resource-free Caption composition from AudioScript is also valid.
1051
+
1052
+ Project initialization creates one current Timeline, the four standard Tracks and one attached AudioScript with segments:[]. A project may contain multiple independent complete AudioScripts (for example a generated script and an uploaded video transcript). The project AudioScript attachment selects only the panel view; it is not a singleton or membership constraint. Locate scripts with entities.list and choose the appropriate source. AudioScript owns independent multi-segment text; each can have multiple Caption variants. Caption and PhoneticScript compose it through baseEntityIds. Caption locates its text as AudioScript plus index: it owns exactly one selection object ({segmentId,textRange?}), never an array or the legacy selections field. segmentId is that segment's stable identity rather than its ordinal, so a concurrent insert or re-segmentation cannot slide a Caption onto other text. It selects one script segment or a contiguous substring, and owns only that selection's intrinsic timing and style; selection textRange is a half-open code-point range and can split display text without rewriting the script. PhoneticScript owns pronunciation/prosody; the voiceover is an Audio rendered from it and related by phonetic-script-render(audio,phoneticScript). Voice is only a timbre identity ({voice:{system:'voice-library',key}}) and is never playable; link the rendered Audio to it with voice-timbre(audio,voice). Speech and caption occupy separate Clips. Caption carries Sequence: it owns its display timing and can be Clip content directly. Subtitles normally accompany sound; use source media or render a voiceover Audio from PhoneticScript when the request requires narration. Do not fabricate a sound resource.
1048
1053
 
1049
- Project initialization creates one current Timeline, the four standard Tracks and one attached AudioScript with segments:[]. A project may contain multiple independent complete AudioScripts (for example a generated script and an uploaded video transcript). The project AudioScript attachment selects only the panel view; it is not a singleton or membership constraint. Locate scripts with entities.list and choose the appropriate source. AudioScript owns independent multi-segment text; each can have multiple Caption variants. Caption and PhoneticScript compose it through baseEntityIds. Caption owns exactly one selection object ({segmentId,textRange?}), never an array or the legacy selections field. It selects one script segment or a contiguous substring, and owns only that selection's intrinsic timing and style; segmentId is a stable script-local identity, not an entity reference or transient array index; selection textRange is a half-open code-point range and can split display text without rewriting the script. PhoneticScript owns pronunciation/prosody; Voice is generated from it and related by phonetic-script-render(voice,phoneticScript). Speech and caption occupy separate Clips. Subtitles normally accompany sound; use source media or generate Voice from PhoneticScript when the request requires narration. Do not fabricate a sound resource.
1054
+ For Speech resources, create an Audio and link it with from-asset to the Asset {system:'memota-speech',key:speechId}, then await rgetAssetFromEntity(audioId) before linking its placement. The host supplies the factual durationMs and assembles physical storage facts internally; the voice-library identity stays on its own Voice entity. Read the visible fields through entities.get; never invent them or use a Speech ID as a Memota media ID. A TTS voiceover still requires its real PhoneticScript relation; resource loading does not invent script content.
1050
1055
 
1051
- AudioScript has no intrinsic time and cannot be Clip content. ASR input Audio/Video/Voice associates via audio-script-source(script,source). Its external annotation SequenceMarker associates via audio-script-marker(script,marker) and receives assigned segmentRanges, not references to another marker. Caption intrinsic timing and its Clip's display SequenceMarker are distinct. Move/stretch display by editing only the placement marker.
1056
+ Direct AudioScript creation saves its segments as a resource before publication. Its assembled segments remain readable and editable; committed text changes preserve native Loro operations and save a new immutable resource, automatically advancing the external reference. AudioScripts initialized through Caption resources keep inline segments without an additional script resource. Do not manage these resources yourself. AudioScript has no intrinsic time and cannot be Clip content. ASR input Audio/Video associates via audio-script-source(script,source). Its external annotation SequenceMarker associates via audio-script-marker(script,marker) and receives assigned segmentRanges, not references to another marker. Caption intrinsic timing and its Clip's display SequenceMarker are distinct. Move/stretch display by editing only the placement marker.
1052
1057
 
1053
- To place content, create a Clip and a SequenceMarker, then link track-clip(track,clip), clip-marker(clip,marker) and marker-content(marker,content). Existing track membership uses timeline-track(timeline,track). The editor has one track of each role: video_clip for Image/Video, speech for Voice, caption for Caption, bgm for Audio. Inspect factual media extents; never invent duration. Each placement is exactly one of Clip.order (sequential), Marker.targetRange (absolute) or clip-anchor(childClip,hostClip) plus Marker.anchorOffset. Marker.sourceRange selects source, duration describes playback/display. Whole milliseconds are required by this editor projection. Clip.volume is decibels (-60..20, 0 original). Image is unbounded/constant. Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'}, with duration matching the rounded source span/rate. BGM can use durationPolicy:'timeline'. Nonlinear speed and multiple overlay tracks are unsupported. Caption visibility is the caption Track's hidden field. Caption style is its style map. Handle structural links explicitly when moving/deleting; there is no implicit cascade.
1058
+ To place content, create a Clip and a SequenceMarker, then link track-clip(track,clip), clip-marker(clip,marker) and marker-content(marker,content). Existing track membership uses timeline-track(timeline,track). The editor has one track of each role: video_clip for Image/Video, speech for the voiceover Audio, caption for Caption, bgm for Audio. Content states only its own durationMs in whole milliseconds, never a position: where it is taken from is Marker.sourceRange and where it lands is Marker.targetRange. A still Image states durationMs:null and each use decides how long it runs; an AXVideo stores none because its Timeline decides it. Inspect factual media durations; never invent one. Each placement is exactly one of Clip.order (sequential), Marker.targetRange (absolute) or clip-anchor(childClip,hostClip) plus Marker.anchorOffset. Marker.sourceRange selects source, duration describes playback/display. Whole milliseconds are required by this editor projection. Clip.volume is decibels (-60..20, 0 original). Linear timeRemapping is {kind:'linear',rate:2,mode:'constant'}, with duration matching the rounded source span/rate. BGM can use durationPolicy:'timeline'. Nonlinear speed and multiple overlay tracks are unsupported. Caption visibility is the caption Track's hidden field. Caption style is its style map. Handle structural links explicitly when moving/deleting; there is no implicit cascade.
1054
1059
 
1055
1060
  checkpoint returns an opaque token valid only in this execution. rollbackTo restores that point and invalidates later checkpoints. Returned read snapshots and inputs are not writable document authorities. Use only the declarations below.
1056
1061
  `.trim();
@@ -1213,7 +1218,7 @@ async function commitPlan(doc, plan, options) {
1213
1218
  * integrity throws are not wrapped.
1214
1219
  */
1215
1220
  async function commitPlanPreflight(doc, plan) {
1216
- const scratch = createPlainMemoryAdapter(doc.snapshot());
1221
+ const scratch = createPlainMemoryAdapter(doc.snapshot(), { readOnly: doc.isEntityDocument() });
1217
1222
  for (let index = 0; index < plan.ops.length; index++) {
1218
1223
  const entry = plan.ops[index];
1219
1224
  if (entry == null) continue;
@@ -1405,13 +1410,13 @@ function createMedeoTool(options) {
1405
1410
  const existing = documents.get(docId);
1406
1411
  if (existing != null) return await existing;
1407
1412
  const created = (async () => {
1408
- return await getOrCreateDocument(new MengineHttpClient({
1413
+ return await openDocument(new MengineHttpClient({
1409
1414
  docId,
1410
1415
  httpOrigin: requiredContext(options.httpOrigin, docId, "httpOrigin"),
1411
1416
  ...options.authToken !== void 0 ? { authToken: () => optionalContext(options.authToken, docId) } : {},
1412
1417
  ...options.userId !== void 0 ? { userId: () => optionalContext(options.userId, docId) } : {},
1413
1418
  ...options.fetchImpl !== void 0 ? { fetchImpl: options.fetchImpl } : {}
1414
- }), docId, optionalContext(options.peerId, docId));
1419
+ }), optionalContext(options.peerId, docId));
1415
1420
  })();
1416
1421
  documents.set(docId, created);
1417
1422
  try {
@@ -1451,36 +1456,14 @@ function createMedeoTool(options) {
1451
1456
  if (documentTails.get(docId) === tail) documentTails.delete(docId);
1452
1457
  }
1453
1458
  }
1454
- async function getOrCreateDocument(client, docId, peerId) {
1455
- try {
1456
- return await ManualSyncDoc.open({
1457
- client,
1458
- ...peerId !== void 0 ? { peerId } : {}
1459
- });
1460
- } catch (error) {
1461
- if (!(error instanceof MengineHttpRequestError) || error.status !== 404) throw error;
1462
- if (options.loadInitialDraft === void 0) throw error;
1463
- }
1464
- const document = toVideoDocument(await options.loadInitialDraft(docId));
1465
- if (Object.keys(document.part_library ?? {}).length > 0) throw new Error("Legacy content cannot bootstrap a Loro entity project");
1466
- const seed = createMirrorVideoDocument(document, {
1467
- ...peerId !== void 0 ? { peerId } : {},
1468
- origin: "mengine.medeo_tool.bootstrap"
1469
- });
1470
- const foundation = ensureEditorFoundation({
1471
- entities: [],
1472
- relations: []
1473
- });
1474
- const entities = LoroEntityDocument.create(foundation.rows, {
1475
- timelineEntityId: foundation.timelineEntityId,
1476
- audioScriptEntityId: foundation.audioScriptEntityId
1477
- });
1478
- seed.import(entities.doc.export({ mode: "snapshot" }));
1479
- try {
1480
- await client.bootstrapSnapshot(seed.export({ mode: "snapshot" }));
1481
- } catch (error) {
1482
- if (!(error instanceof MengineHttpRequestError) || error.status !== 400) throw error;
1483
- }
1459
+ /**
1460
+ * Open an existing Mengine document. Creation belongs to Director, which
1461
+ * initializes every project's document (`POST .../initialize`) before any
1462
+ * agent edit; Mengine's `bootstrap` is the internal Legacy-migration route
1463
+ * and is deliberately absent from the client SDK. A 404 here is a real
1464
+ * missing document, not something this tool may paper over.
1465
+ */
1466
+ async function openDocument(client, peerId) {
1484
1467
  return await ManualSyncDoc.open({
1485
1468
  client,
1486
1469
  ...peerId !== void 0 ? { peerId } : {}
@@ -1652,10 +1635,12 @@ function createMedeoTool(options) {
1652
1635
  const document = doc.snapshot();
1653
1636
  const baseVersion = encodeDocVersionMark(doc.versionMark());
1654
1637
  const result = await runEditScript({
1638
+ docId: input.doc_id,
1655
1639
  document,
1656
1640
  baseVersion,
1657
1641
  entityState,
1658
1642
  loadEntityAsset: options.loadEntityAsset ? (entity) => options.loadEntityAsset(input.doc_id, entity) : void 0,
1643
+ writeEntityAsset: options.writeEntityAsset ? (entity, content) => options.writeEntityAsset(input.doc_id, entity, content) : void 0,
1659
1644
  script: input.script,
1660
1645
  ...input.inputs !== void 0 ? { inputs: input.inputs } : {},
1661
1646
  timeoutMs: input.timeout_ms ?? options.sandbox?.timeoutMs ?? 3e4,
@@ -1791,20 +1776,24 @@ async function materializeResources(options, resources) {
1791
1776
  const client = new EntityHttpClient(options);
1792
1777
  const state = await client.fetchState();
1793
1778
  let resourceKey = "";
1779
+ let mintOrdinal = 0;
1794
1780
  const sandbox = new EntitySandbox({
1795
1781
  state,
1796
- idFactory: (prefix) => stableId(prefix, options.docId, resourceKey)
1782
+ idFactory: (prefix) => stableId(prefix, options.docId, resourceKey, String(mintOrdinal++))
1797
1783
  });
1798
1784
  const ids = [];
1799
1785
  for (const resource of resources) {
1800
1786
  resourceKey = `${resource.kind}:${resource.assetId}`;
1787
+ mintOrdinal = 0;
1801
1788
  if (resource.kind !== "caption") {
1802
1789
  ids.push(sandbox.entities.ensureMedia(resource).contentEntityId);
1803
1790
  continue;
1804
1791
  }
1805
- const existingCaptions = sandbox.entities.list().filter((row) => row.entity_kind === "caption" && row.payload.external?.key === resource.assetId);
1792
+ const rows = sandbox.entities.list();
1793
+ const assetRow = rows.find((row) => row.entity_kind === "asset" && row.payload.key === resource.assetId);
1794
+ const existingCaptions = assetRow === void 0 ? [] : rows.filter((row) => row.entity_kind === "caption" && sandbox.entities.assetIdOf(row.entity_id) === assetRow.entity_id);
1806
1795
  if (existingCaptions.length) {
1807
- if (existingCaptions.some((caption) => caption.payload.storageKey !== resource.storageKey)) throw new Error("Conflicting Caption resource identity");
1796
+ if (assetRow?.payload.storageKey !== resource.storageKey) throw new Error("Conflicting Caption resource identity");
1808
1797
  ids.push(...existingCaptions.map((caption) => caption.entity_id));
1809
1798
  continue;
1810
1799
  }
@@ -1825,6 +1814,11 @@ async function materializeResources(options, resources) {
1825
1814
  entity_kind: "audio-script",
1826
1815
  payload: { segments }
1827
1816
  });
1817
+ const captionAssetId = sandbox.entities.ensureAsset({
1818
+ system: "memota",
1819
+ key: resource.assetId,
1820
+ storageKey: resource.storageKey
1821
+ });
1828
1822
  const ranges = resource.segments.map((segment, index) => ({
1829
1823
  segmentId: segments[index].segmentId,
1830
1824
  startMs: segment.startMs,
@@ -1837,22 +1831,16 @@ async function materializeResources(options, resources) {
1837
1831
  entity_kind: "caption",
1838
1832
  payload: {
1839
1833
  baseEntityIds: [scriptId],
1840
- external: {
1841
- system: "memota",
1842
- key: resource.assetId
1843
- },
1844
- storageKey: resource.storageKey,
1845
1834
  selection: { segmentId: range.segmentId },
1846
1835
  segmentRanges: [range],
1847
- extent: {
1848
- kind: "bounded",
1849
- start: range.startMs,
1850
- end: range.endMs
1851
- },
1852
- sampling: "native",
1853
- coordinateSpace: "milliseconds"
1836
+ durationMs: range.endMs - range.startMs
1854
1837
  }
1855
1838
  });
1839
+ sandbox.relations.link({
1840
+ relation_kind: "from-asset",
1841
+ endpoint_0_entity_id: captionId,
1842
+ endpoint_1_entity_id: captionAssetId
1843
+ });
1856
1844
  ids.push(captionId);
1857
1845
  }
1858
1846
  const markerId = sandbox.entities.create({