@mengine/medeo-tool 2.0.1-alpha.10 → 2.0.1-alpha.12

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-Cf3AiSe7.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-DNIIwpn1.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
@@ -50,7 +50,15 @@ interface EntityAssetContent {
50
50
  content: JsonValue;
51
51
  }
52
52
  /** Host-only I/O. The sandbox supplies an Entity, never an arbitrary Asset query. */
53
- type EntityAssetLoader = (docId: string, entity: SandboxEntity) => Promise<EntityAssetContent>;
53
+ /**
54
+ * Resolve one entity's stored resource.
55
+ *
56
+ * The Asset travels with the entity because the locator lives on the Asset
57
+ * alone: an entity made from stored bytes names them through a `from-asset`
58
+ * Relation, never on its own row, so a loader given only the entity would have
59
+ * nothing to look up.
60
+ */
61
+ type EntityAssetLoader = (docId: string, entity: SandboxEntity, asset: SandboxEntity) => Promise<EntityAssetContent>;
54
62
  /** Program-only initial resource creation; never exposed as a sandbox API. */
55
63
  type EntityAssetWriter = (docId: string, entity: SandboxEntity, content: JsonValue) => Promise<EntityAssetContent>;
56
64
  //#endregion
@@ -108,7 +116,7 @@ interface ConsoleShim {
108
116
  interface RunEditScriptOptions {
109
117
  /** Document the plan will be committed against; stated by the host. */
110
118
  docId: string;
111
- loadEntityAsset?: (entity: SandboxEntity) => Promise<EntityAssetContent>;
119
+ loadEntityAsset?: (entity: SandboxEntity, asset: SandboxEntity) => Promise<EntityAssetContent>;
112
120
  writeEntityAsset?: (entity: SandboxEntity, content: EntityAssetContent['content']) => Promise<EntityAssetContent>;
113
121
  document: VideoDocument;
114
122
  baseVersion: string;
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { a as createEntityId, i as businessState, o as createRelationId, s as isMediaAssetVariantKind, t as EntitySandbox } from "./entity-sandbox-BTR2cRl1.mjs";
1
+ import { a as createEntityId, i as businessState, o as createRelationId, s as isMediaAssetVariantKind, t as EntitySandbox } from "./entity-sandbox-DpIkxcei.mjs";
2
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";
@@ -226,7 +226,7 @@ function runEditScript(options) {
226
226
  throw new Error("Entity Asset writer is unavailable");
227
227
  }))(message.entity, message.content) : await (options.loadEntityAsset ?? (() => {
228
228
  throw new Error("Entity Asset loader is unavailable");
229
- }))(message.entity);
229
+ }))(message.entity, message.asset);
230
230
  if (!settled) worker.postMessage({
231
231
  t: "entity-asset-result",
232
232
  requestId: message.requestId,
@@ -689,33 +689,39 @@ async function syncGeneratedRelations(input) {
689
689
  };
690
690
  }
691
691
  }
692
- function assetKeyOf(entity) {
693
- if (!isMediaAssetVariantKind(entity.entity_kind)) return void 0;
694
- const external = entity.payload?.external;
695
- if (external == null || typeof external !== "object" || Array.isArray(external)) return void 0;
696
- const { system, key } = external;
697
- if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) return void 0;
698
- if (typeof key !== "string" || key.length === 0 || key.trim() !== key) return void 0;
699
- return key;
692
+ /** The locator an entity reaches through `from-asset`; only Assets state one. */
693
+ function assetLocatorOf(state, entityId) {
694
+ const assetIds = state.relations.filter((relation) => relation.relation_kind === "from-asset").map((relation) => relation.endpoint_0_entity_id === entityId ? relation.endpoint_1_entity_id : relation.endpoint_1_entity_id === entityId ? relation.endpoint_0_entity_id : void 0).filter((id) => id !== void 0);
695
+ for (const assetId of assetIds) {
696
+ const asset = state.entities.find((row) => row.entity_id === assetId && row.entity_kind === "asset");
697
+ const system = asset?.payload.system;
698
+ const key = asset?.payload.key;
699
+ if (typeof system !== "string" || !ASSET_SYSTEMS.has(system)) continue;
700
+ if (typeof key !== "string" || key.length === 0 || key.trim() !== key) continue;
701
+ return {
702
+ system,
703
+ key
704
+ };
705
+ }
700
706
  }
701
- /** Media variants own their Asset locator; generation lookup never follows Relations. */
707
+ /**
708
+ * Media reaches its Asset through `from-asset`, so lineage lookup follows that
709
+ * Relation — the locator is never copied onto the medium's own row.
710
+ */
702
711
  function resolveMediaByAssetKey(state, factKeys) {
703
712
  const systemByKey = /* @__PURE__ */ new Map();
704
- for (const entity of state.entities) {
705
- const key = assetKeyOf(entity);
706
- if (key === void 0 || !factKeys?.has(key)) continue;
707
- const system = entity.payload.external.system;
708
- if (systemByKey.has(key) && systemByKey.get(key) !== system) throw new Error(`Ambiguous generation asset id ${key} across media and speech namespaces`);
709
- systemByKey.set(key, system);
710
- }
711
713
  const resolved = /* @__PURE__ */ new Map();
712
714
  for (const entity of state.entities) {
713
715
  if (!isMediaAssetVariantKind(entity.entity_kind)) continue;
714
- const key = assetKeyOf(entity);
715
- if (key === void 0) continue;
716
- const matches = resolved.get(key) ?? [];
716
+ const locator = assetLocatorOf(state, entity.entity_id);
717
+ if (locator === void 0) continue;
718
+ if (factKeys?.has(locator.key)) {
719
+ if (systemByKey.has(locator.key) && systemByKey.get(locator.key) !== locator.system) throw new Error(`Ambiguous generation asset id ${locator.key} across media and speech namespaces`);
720
+ systemByKey.set(locator.key, locator.system);
721
+ }
722
+ const matches = resolved.get(locator.key) ?? [];
717
723
  matches.push(entity.entity_id);
718
- resolved.set(key, matches);
724
+ resolved.set(locator.key, matches);
719
725
  }
720
726
  return resolved;
721
727
  }
@@ -779,6 +785,18 @@ const ENTITY_EDIT_SANDBOX_API_DTS = [
779
785
  " entity_id?: string;",
780
786
  " entity_kind: K;",
781
787
  " payload: StoredEntityPayload<K>;",
788
+ " /**",
789
+ " * The stored resource this entity is made from.",
790
+ " *",
791
+ " * Naming it here is the only way to say where an entity's bytes come from:",
792
+ " * the Asset row and the `from-asset` Relation that reaches it are assembled",
793
+ " * by the host, never authored. Only a kind that declares `FromAsset` —",
794
+ " * video, audio, image, caption, audio-script — accepts one.",
795
+ " */",
796
+ " asset?: {",
797
+ " system: 'memota' | 'memota-speech';",
798
+ " key: string;",
799
+ " };",
782
800
  " };",
783
801
  "}[KnownEntityKind];",
784
802
  "export interface DeleteEntityInput {",
@@ -1047,13 +1065,13 @@ Entities own data. Relations associate two existing entities; endpoint_0 and end
1047
1065
 
1048
1066
  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.
1049
1067
 
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.
1068
+ Generation tools return resource references. An asset id names stored bytes. Say which bytes an entity is made from by passing asset:{system,key} to entities.create; the host mints the Asset row and the from-asset Relation for you. Asset rows and from-asset are invisible to scripts and cannot be created, linked or unlinked by them, and no entity copies a 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 with asset:{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 the same Asset. After awaiting, discover ALL resource Captions with entities.list filtered by entity_kind, inspect each singular selection/durationMs, 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.
1051
1069
 
1052
1070
  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.
1053
1071
 
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.
1072
+ For Speech resources, create an Audio with 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.
1055
1073
 
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.
1074
+ 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 which Asset the script is made from. 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.
1057
1075
 
1058
1076
  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.
1059
1077
 
@@ -1639,7 +1657,7 @@ function createMedeoTool(options) {
1639
1657
  document,
1640
1658
  baseVersion,
1641
1659
  entityState,
1642
- loadEntityAsset: options.loadEntityAsset ? (entity) => options.loadEntityAsset(input.doc_id, entity) : void 0,
1660
+ loadEntityAsset: options.loadEntityAsset ? (entity, asset) => options.loadEntityAsset(input.doc_id, entity, asset) : void 0,
1643
1661
  writeEntityAsset: options.writeEntityAsset ? (entity, content) => options.writeEntityAsset(input.doc_id, entity, content) : void 0,
1644
1662
  script: input.script,
1645
1663
  ...input.inputs !== void 0 ? { inputs: input.inputs } : {},