@mengine/medeo-tool 1.0.1-alpha.2 → 1.2.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/README.md CHANGED
@@ -1,15 +1,16 @@
1
1
  # `@mengine/medeo-tool`
2
2
 
3
- Node tool surface for editing a Medeo video document through the deterministic
4
- edit sandbox.
3
+ Node tool surface for editing the authoritative Medeo Entity/Relation graph
4
+ through the deterministic edit sandbox. Timeline editing is entity editing,
5
+ not a legacy document mutation followed by optional entity registration.
5
6
 
6
7
  The package owns the complete tool path:
7
8
 
8
9
  - compact snapshot projection;
9
10
  - trusted worker execution of an agent-authored JavaScript edit script;
10
- - op-journal `ChangePlan` caching;
11
- - versioned or per-op-preflight commit through `MengineDocSession`;
12
- - session cache and shutdown.
11
+ - entity-command `ChangePlan` caching;
12
+ - complete Entity/Relation snapshot reads and revision-CAS commits;
13
+ - per-document serialization, document cache, and shutdown.
13
14
 
14
15
  A host supplies only environment facts: Mengine HTTP origin, optional request
15
16
  identity headers, `fetch`, and a stable agent peer id.
@@ -27,7 +28,8 @@ const snapshot = await medeo.handle({ op: 'snapshot', doc_id: docId });
27
28
  const run = await medeo.handle({
28
29
  op: 'run-edit-script',
29
30
  doc_id: docId,
30
- script: 'await edit.deleteBgm({})',
31
+ script: 'edit.updateClip({ clipEntityId: inputs.clip_id, payload: { volume: -6 } })',
32
+ inputs: { clip_id: existingClipEntityId },
31
33
  });
32
34
  if (!run.ok || run.op !== 'run-edit-script') throw new Error('edit script failed');
33
35
  const commit = await medeo.handle({
@@ -36,9 +38,117 @@ const commit = await medeo.handle({
36
38
  plan_id: run.plan_id,
37
39
  });
38
40
 
41
+ // Media facts are recalled by the host and passed into the sandbox.
42
+ // This example is text-to-image: there is no invented input-media entity.
43
+ const entityRun = await medeo.handle({
44
+ op: 'run-edit-script',
45
+ doc_id: docId,
46
+ inputs: { output_asset_id: 'asset-from-host', track_entity_id: existingTrackEntityId },
47
+ script: `
48
+ if (typeof inputs.output_asset_id !== 'string' || typeof inputs.track_entity_id !== 'string') {
49
+ throw new Error('missing recalled generation facts');
50
+ }
51
+ const { contentEntityId: output } = entities.ensureMedia({
52
+ assetId: inputs.output_asset_id,
53
+ kind: 'image',
54
+ });
55
+ edit.insertClip({
56
+ trackEntityId: inputs.track_entity_id,
57
+ contentEntityId: output,
58
+ sourceRange: { start: 0, end: 1 },
59
+ duration: { mode: 'fixed', value: 5000 },
60
+ targetRange: { start: 0, end: 5000 },
61
+ });
62
+ `,
63
+ });
64
+
39
65
  await medeo.close();
40
66
  ```
41
67
 
42
68
  The sandbox has no network, storage, clock, or generation access. Materialize
43
- speech/media side effects in the host first and pass stable facts through
44
- `inputs`.
69
+ or recall speech/media side effects in the host first and pass stable facts
70
+ through `inputs`. There is no automatic Asset→Entity projection: the model
71
+ selects when to bring a recalled asset into the editor through `entities.ensureMedia`.
72
+ The resolver gets or creates one typed Asset by external `(system, key)` within
73
+ the document. Image/Video/Audio/Voice are logical Asset variants: the typed row
74
+ directly owns `external`, `storageKey` and its media fields, and its Asset ID is
75
+ also its content Entity ID. Imports create no separate physical-only Asset
76
+ row and no `physical-asset` relation. Raw typed resource creation also reuses
77
+ an existing identity; prefer `ensureMedia` with the recalled media kind.
78
+ `findByAssetId` includes Voice (`memota-speech`) variants. There is no separate
79
+ `importAsset` API.
80
+ Generation facts currently carry bare asset IDs. If a returned fact matches
81
+ both media and speech namespaces, synchronization fails explicitly without
82
+ writing ambiguous lineage edges.
83
+ Conflicting facts or duplicate candidates fail explicitly; no automatic
84
+ merge or deletion occurs. This is the MEngine editor profile, not a global DSL
85
+ cardinality rule. Separate Asset+media graphs are invalid: every media variant
86
+ must directly own its external identity, with no read fallback or migration.
87
+ Inline caption Assets without external identity are not deduplicated.
88
+
89
+ Before model context/snapshot/editing, the host ensures one Timeline and the four
90
+ fixed Tracks (`video_clip`, `speech`, `caption`, `bgm`). Existing IDs, settings and
91
+ relations are retained; repeated calls add no rows or revisions. First initialization
92
+ uses revision CAS and may write. An empty legacy draft is initialized through the
93
+ version-guarded migration path to preserve configured track identities/visibility;
94
+ non-empty legacy content still requires explicit `migrate-legacy`. Missing Tracks
95
+ are filled in a subsequent CAS when necessary. A lost initialization response is
96
+ reconciled by a fresh snapshot, never by assuming success.
97
+
98
+ Each placement still creates an independent Clip and SequenceMarker. Reusing a
99
+ media variant does not share per-placement trim, speed, volume or placement state.
100
+
101
+ The production tool exposes native Clip/Marker editing plus visual placement,
102
+ voiceover, caption, and BGM helpers through its generated sandbox interface.
103
+ They preserve structural and anchor relations in the same plan. Timeline targets
104
+ are Entity IDs, never raw asset IDs or URLs. Asset import, media creation, and
105
+ Clip insertion belong in the **same entity plan**.
106
+
107
+ Generation lineage is program-synced, not model-authored. After a confirmed
108
+ entity commit the tool resolves the plan's diff against host-supplied generation
109
+ facts and commits missing `generated` Relations between fact-matched media
110
+ Entities already present in the document (endpoint 0 output, endpoint 1 input;
111
+ lookup can use either endpoint). The diff covers newly exposed media Asset
112
+ identities; placement-only edits and
113
+ pairs already fact-resolvable before the plan stay untouched. Models
114
+ do not pass generation history through inputs — the host queries it with
115
+ `loadGenerationFacts(docId, assetIds)`, returning every known generation record
116
+ involving the given asset ids in either role. Each record must carry an explicit
117
+ `inputAssetIds` array: an explicit empty array declares text-only generation
118
+ with no lineage edge, while a missing or non-array field is a malformed record
119
+ that fails the whole query instead of being silently read as text-only.
120
+ One-sided facts are skipped without creating entities or blocking the commit —
121
+ lineage sync never backfills a missing source or output Entity; entity creation
122
+ stays a model decision inside the edit plan. Repeated commits are idempotent,
123
+ and deletions and revision conflicts are respected: a CAS-conflict retry reads
124
+ the fresh graph and recomputes missing edges without recreating deleted endpoints.
125
+ Asset identities are immutable, so each synchronization queries its facts once.
126
+ An empty result array means no known lineage; a rejection means the lineage
127
+ query failed and is reported as `generation_sync: {status:'failed'}` plus a
128
+ `generation_sync_failed` warning — never as synced state.
129
+
130
+ Entities own their facts: SequenceMarker owns source/target ranges, duration,
131
+ and time remapping; Clip owns volume; Track owns role/visibility. Cross-entity
132
+ references are Relations. There is no separate orientation or timing profile.
133
+
134
+ The server validates and commits the graph and its read-only Loro projection in
135
+ one database transaction. `deleted_entity_ids` and `deleted_relation_ids` make
136
+ deletion explicit; dropped rows without deletion intent are rejected. After
137
+ cutover, raw Loro `/updates` cannot mutate the document. Legacy standalone
138
+ sandbox exports remain library compatibility APIs, not a production-tool mode.
139
+
140
+ Existing nonempty legacy documents require a separate version-CAS migration
141
+ whose projection preserves existing editing facts. `snapshot` identifies that
142
+ requirement and the media IDs needing factual metadata. Call
143
+ `{op:'migrate-legacy',doc_id,asset_facts}` explicitly, then take a fresh snapshot
144
+ before editing. The package reads the canonical document and current Loro
145
+ version itself; it does not accept a caller-supplied snapshot or version, use a
146
+ stale cached document after a failed pull, or combine migration with a new edit.
147
+ Repeating migration on an entity timeline is read-only. An uncertain submission
148
+ must be inspected and retried, never reported as committed.
149
+
150
+ The compatibility reader covers the existing four lanes: Image/Video, Voice,
151
+ Caption, and background Audio. Coordinates are whole milliseconds for this
152
+ reader. It preserves linear visual remapping (`{kind:'linear',rate:2}`), including
153
+ image display speed, and explicit anchor chains. Nonlinear remapping and multiple
154
+ visual overlay tracks are not existing editor features and remain unsupported.
@@ -0,0 +1,228 @@
1
+ import { MediaAssetFact } from "@mengine/medeo-client";
2
+
3
+ //#region src/entity/entity-contract.d.ts
4
+ type JsonPrimitive = string | number | boolean | null;
5
+ type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
6
+ interface JsonObject {
7
+ [key: string]: JsonValue;
8
+ }
9
+ type KnownEntityKind = 'axvideo' | 'timeline' | 'track' | 'clip' | 'asset' | 'video' | 'audio' | 'voice' | 'image' | 'sequence-marker' | 'viewport' | 'audio-script' | 'phonetic-script' | 'caption';
10
+ /** Asset identity, either an old physical-only row or a directly composed media variant. */
11
+ type ResourceEntityKind = 'image' | 'video' | 'audio' | 'voice';
12
+ type KnownRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline' | 'physical-asset' | 'generated' | 'phonetic-script-provenance' | 'caption-provenance' | 'caption-alignment' | 'clip-anchor' | 'audio-script-render';
13
+ type AuthorableRelationKind = Exclude<KnownRelationKind, 'generated'>;
14
+ interface BoundedNativeSequencePayload extends JsonObject {
15
+ /** Factual coordinates from recalled media metadata; never invent an end/duration. */
16
+ extent: {
17
+ kind: 'bounded';
18
+ start: number;
19
+ end: number;
20
+ };
21
+ sampling: 'native';
22
+ coordinateSpace: JsonValue;
23
+ }
24
+ interface UnboundedConstantSequencePayload extends JsonObject {
25
+ extent: {
26
+ kind: 'unbounded';
27
+ start: number;
28
+ };
29
+ sampling: 'constant';
30
+ coordinateSpace: JsonValue;
31
+ }
32
+ interface BoundedDerivedSequencePayload extends JsonObject {
33
+ extent: {
34
+ kind: 'bounded';
35
+ start: number;
36
+ end: number;
37
+ };
38
+ sampling: 'derived';
39
+ coordinateSpace: JsonValue;
40
+ }
41
+ type ScriptTextSegment = JsonObject & {
42
+ segmentId: string;
43
+ text: string;
44
+ language?: string;
45
+ };
46
+ type MediaAssetPayload = JsonObject & {
47
+ external: {
48
+ system: 'memota' | 'memota-speech';
49
+ key: string;
50
+ };
51
+ storageKey?: string;
52
+ };
53
+ interface EntityPayloadByKind {
54
+ axvideo: BoundedDerivedSequencePayload;
55
+ timeline: JsonObject;
56
+ track: JsonObject & {
57
+ hidden?: boolean;
58
+ role?: string;
59
+ };
60
+ clip: JsonObject;
61
+ /** Inline caption text; external media uses a typed Asset variant. */
62
+ asset: JsonObject;
63
+ video: BoundedNativeSequencePayload & MediaAssetPayload;
64
+ audio: BoundedNativeSequencePayload & MediaAssetPayload;
65
+ voice: BoundedNativeSequencePayload & MediaAssetPayload;
66
+ image: UnboundedConstantSequencePayload & MediaAssetPayload;
67
+ 'sequence-marker': JsonObject & {
68
+ sourceRange: {
69
+ start: number;
70
+ end: number;
71
+ };
72
+ targetRange?: {
73
+ start: number;
74
+ end: number;
75
+ };
76
+ duration: {
77
+ mode: 'from-source';
78
+ } | {
79
+ mode: 'fixed';
80
+ value: number;
81
+ };
82
+ timeRemapping?: JsonValue;
83
+ anchorOffset?: number;
84
+ durationPolicy?: 'timeline';
85
+ };
86
+ viewport: JsonObject;
87
+ 'audio-script': JsonObject & {
88
+ segments: ScriptTextSegment[];
89
+ };
90
+ 'phonetic-script': JsonObject & {
91
+ segments: ScriptTextSegment[];
92
+ };
93
+ caption: BoundedNativeSequencePayload;
94
+ }
95
+ interface SandboxEntity<K extends KnownEntityKind = KnownEntityKind> {
96
+ entity_id: string;
97
+ entity_kind: K;
98
+ payload: EntityPayloadByKind[K];
99
+ }
100
+ interface SandboxRelation {
101
+ relation_id: string;
102
+ relation_kind: KnownRelationKind;
103
+ endpoint_0_entity_id: string;
104
+ endpoint_1_entity_id: string;
105
+ metadata: JsonObject;
106
+ trace: JsonObject;
107
+ }
108
+ interface EntityStoreSnapshot {
109
+ revision: number;
110
+ entities: SandboxEntity[];
111
+ relations: SandboxRelation[];
112
+ }
113
+ type CreateEntityInput = { [K in KnownEntityKind]: {
114
+ entity_id?: string;
115
+ entity_kind: K;
116
+ payload: EntityPayloadByKind[K];
117
+ } }[KnownEntityKind];
118
+ interface UpdateEntityInput {
119
+ entity_id: string;
120
+ payload: JsonObject;
121
+ }
122
+ interface DeleteEntityInput {
123
+ entity_id: string;
124
+ }
125
+ type EmptyRelationKind = 'timeline-track' | 'track-clip' | 'clip-marker' | 'marker-content' | 'axvideo-marker' | 'marker-timeline';
126
+ interface LinkRelationBase {
127
+ relation_id?: string;
128
+ endpoint_0_entity_id: string;
129
+ endpoint_1_entity_id: string;
130
+ trace?: JsonObject;
131
+ }
132
+ type LinkRelationInput = (LinkRelationBase & {
133
+ relation_kind: EmptyRelationKind;
134
+ metadata?: {
135
+ [key: string]: never;
136
+ };
137
+ }) | (LinkRelationBase & {
138
+ relation_kind: 'physical-asset';
139
+ metadata?: JsonObject;
140
+ }) | (LinkRelationBase & {
141
+ relation_kind: 'phonetic-script-provenance' | 'caption-provenance';
142
+ metadata: JsonObject & {
143
+ segmentAlignment: JsonValue;
144
+ };
145
+ }) | (LinkRelationBase & {
146
+ relation_kind: 'caption-alignment';
147
+ metadata: JsonObject & {
148
+ alignment: JsonValue;
149
+ };
150
+ });
151
+ interface LinkGeneratedRelationInput {
152
+ relation_id?: string;
153
+ output_entity_id: string;
154
+ input_entity_id: string;
155
+ trace?: JsonObject;
156
+ }
157
+ interface LinkClipAnchorRelationInput {
158
+ relation_id?: string;
159
+ child_clip_entity_id: string;
160
+ host_clip_entity_id: string;
161
+ trace?: JsonObject;
162
+ }
163
+ interface LinkAudioScriptRenderRelationInput {
164
+ relation_id?: string;
165
+ output_entity_id: string;
166
+ script_entity_id: string;
167
+ trace?: JsonObject;
168
+ }
169
+ interface UnlinkRelationInput {
170
+ relation_id: string;
171
+ }
172
+ type EntityCommand = {
173
+ kind: 'create-entity';
174
+ entity: SandboxEntity;
175
+ } | {
176
+ kind: 'update-entity';
177
+ entity_id: string;
178
+ payload: JsonObject;
179
+ } | {
180
+ kind: 'delete-entity';
181
+ entity_id: string;
182
+ } | {
183
+ kind: 'link-relation';
184
+ relation: SandboxRelation;
185
+ } | {
186
+ kind: 'unlink-relation';
187
+ relation_id: string;
188
+ };
189
+ interface EntityPlanState {
190
+ base_revision: number;
191
+ commands: readonly EntityCommand[];
192
+ rows: EntityStoreSnapshot;
193
+ deleted_entity_ids: readonly string[];
194
+ deleted_relation_ids: readonly string[];
195
+ }
196
+ interface EntityFacade {
197
+ list(): SandboxEntity[];
198
+ get(entityId: string): SandboxEntity | null;
199
+ /** Find document resources by external Memota asset id, including directly composed media variants. */
200
+ findByAssetId(assetId: string): SandboxEntity<ResourceEntityKind>[];
201
+ create(input: CreateEntityInput): string;
202
+ /** Replace one Entity's owned payload without changing its identity or kind. */
203
+ update(input: UpdateEntityInput): void;
204
+ /** Delete an Entity only after all of its incident Relations have been explicitly unlinked. */
205
+ delete(input: DeleteEntityInput): void;
206
+ /** Get or create one typed Asset by factual external id and return its single content identity. Never creates a Clip. */
207
+ ensureMedia(fact: MediaAssetFact): {
208
+ contentEntityId: string;
209
+ };
210
+ }
211
+ interface RelationFacade {
212
+ list(): SandboxRelation[];
213
+ /** Incident lookup is endpoint-agnostic; persisted endpoint positions stay unchanged. */
214
+ of(entityId: string, relationKind?: KnownRelationKind): SandboxRelation[];
215
+ /** physical-asset binds inline Caption text, never external media. */
216
+ link(input: LinkRelationInput): string;
217
+ /** Author ordered generated(output,input); generic link() deliberately rejects this kind. */
218
+ linkGenerated(input: LinkGeneratedRelationInput): string;
219
+ /** Author ordered clip-anchor(child,host) without positional endpoint ambiguity. */
220
+ linkClipAnchor(input: LinkClipAnchorRelationInput): string;
221
+ /** Author ordered audio-script-render(output,script) without positional endpoint ambiguity. */
222
+ linkAudioScriptRender(input: LinkAudioScriptRenderRelationInput): string;
223
+ /** Remove a Relation by identity; endpoint replacement is an explicit unlink plus link. */
224
+ unlink(input: UnlinkRelationInput): void;
225
+ }
226
+ //#endregion
227
+ export { SandboxEntity as _, EntityFacade as a, UpdateEntityInput as b, JsonObject as c, KnownEntityKind as d, KnownRelationKind as f, ResourceEntityKind as g, RelationFacade as h, EntityCommand as i, JsonPrimitive as l, LinkRelationInput as m, CreateEntityInput as n, EntityPlanState as o, LinkGeneratedRelationInput as p, DeleteEntityInput as r, EntityStoreSnapshot as s, AuthorableRelationKind as t, JsonValue as u, SandboxRelation as v, UnlinkRelationInput as y };
228
+ //# sourceMappingURL=entity-contract-C5HHaYKk.d.mts.map