@mengine/medeo-client 2.1.0 → 2.1.1-dsl.1
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 +126 -25
- package/dist/base64-D-jd-dr5.js +16 -0
- package/dist/dsl-EV6kCdr6.js +5852 -0
- package/dist/dsl.d.ts +3 -0
- package/dist/dsl.js +3 -0
- package/dist/index-B7fC7q7P.d.ts +728 -0
- package/dist/index-Cu7SdKN5.d.ts +940 -0
- package/dist/index-yifEtZSP.d.ts +197 -0
- package/dist/index.d.ts +5 -1141
- package/dist/index.js +109 -2606
- package/dist/legacy.d.ts +1044 -0
- package/dist/legacy.js +4349 -0
- package/dist/{loro-relay-doc-cJSY-uau.js → loro-relay-doc-Bi6JKqtQ.js} +11 -7
- package/dist/relay.js +1 -1
- package/dist/schemas.d.ts +606 -0
- package/dist/schemas.js +442 -0
- package/dist/shared-iWnU2osE.js +39 -0
- package/dist/storage-BW3tI_ER.js +801 -0
- package/dist/testing.d.ts +22 -9
- package/dist/testing.js +168 -50
- package/dist/video-draft-types-TXRv3wS6.js +1419 -0
- package/dist/video-draft-types-c_shaRyq.d.ts +809 -0
- package/package.json +16 -6
- package/dist/chunk-D7D4PA-g.js +0 -13
- package/dist/document-B_JQwrC5.js +0 -1630
- package/dist/index-CnZ9l3rb.d.ts +0 -2077
package/README.md
CHANGED
|
@@ -1,49 +1,150 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@mengine/medeo-client`
|
|
2
|
+
|
|
3
|
+
The default client edits one Medeo DSL document. `DslEditor` writes entities, relations, native Text/Cursors and the containment Tree through one audited transaction. VideoDraft is a read view, never a second writable document.
|
|
4
|
+
|
|
5
|
+
## Open, edit and synchronize
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { MengineDocSession, MengineHttpClient } from '@mengine/medeo-client';
|
|
2
9
|
|
|
3
|
-
|
|
10
|
+
const session = new MengineDocSession({ docId, client: new MengineHttpClient({ docId, httpOrigin, authToken }) });
|
|
11
|
+
await session.start(); // The server must have initialized the DSL document.
|
|
12
|
+
const [clipId] = session.editor.addClips({
|
|
13
|
+
trackId: 'main_track',
|
|
14
|
+
clips: [{ media: { kind: 'video', system: 'memota', key: mediaId, durationMs: 5000 } }],
|
|
15
|
+
});
|
|
16
|
+
session.editor.setClipVolume(clipId, -3, { actor: { user_id: userId, role: 'user' } });
|
|
17
|
+
await session.waitForServerAck(); // Local completion is separate from durable confirmation.
|
|
18
|
+
const { content, diagnostics } = session.projectVideoDraft();
|
|
19
|
+
session.destroy();
|
|
20
|
+
```
|
|
4
21
|
|
|
5
|
-
|
|
22
|
+
`start()`, `snapshot()` and subscription events expose `MedeoDslSnapshot`. `session.dsl` exposes controlled `transact`/`applyMutations`; ordinary business callers use `session.editor`. Rich text uses the native text adapter. `readonlyMode` blocks editing and Undo/Redo. Closing a session also invalidates captured editor/document references.
|
|
6
23
|
|
|
7
|
-
|
|
24
|
+
For explicit synchronization, use `ManualSyncDoc.open({client, peerId})`, then `pull()`, `editor`, `push()` and `dispose()`. Both lifecycles use the same binary snapshot/update and version-coverage contracts. Neither silently upgrades old documents or overwrites an incompatible cache.
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
## Editing and conversion
|
|
10
27
|
|
|
11
|
-
|
|
28
|
+
- Track: create, reorder, hide and delete.
|
|
29
|
+
- Clip: batch insert with registered sources or full media facts; move, trim, speed, volume, replace and delete.
|
|
30
|
+
- Placement: sequential, absolute or anchored. Reordering and replacement require an explicit associated-content policy.
|
|
31
|
+
- Voiceover: `applyVoiceover` consumes materialized recording/script/caption results in one commit. Upload, ASR and TTS belong to the host.
|
|
32
|
+
- BGM: `setBgm` requires full resource facts and explicitly fills the Timeline; trim/speed are not enabled.
|
|
33
|
+
- Caption: independent native Text or AudioScript Cursor reference; style and source switching.
|
|
12
34
|
|
|
13
|
-
|
|
35
|
+
`resolveDslLayout`, `toVideoDraft`, `fromVideoDraft`, `MedeoDsl` and the model types are exported from the root and `/dsl`. Projection returns diagnostics; unrepresentable legacy output throws `DslProjectionError`. `MengineHttpClient.fetchDraft()` / `fetchDraftAt(seq)` read the server's content, diagnostics and matching version.
|
|
14
36
|
|
|
15
|
-
|
|
37
|
+
## The DSL document
|
|
16
38
|
|
|
17
|
-
`
|
|
39
|
+
The live document lives here: `src/dsl/document` holds the `MedeoDsl` handle, transactions, draft guards and native reads; `src/dsl/projection` holds layout solving, VideoDraft conversion and the generated IDL types. Definitions, the Loro schema and change constraints come from the private `@mengine/medeo-dsl` and are re-exported by name, so the published API does not grow when the DSL does. Transactions ask the DSL's constraint functions; only what needs a live Loro document — caption ranges in the current body, Tree identities, Mirror's K03 limit — is checked here. See [RFC 04](../../docs/projects/medeo-dsl/rfc/04-entity-relation-to-loro-mapping.md).
|
|
18
40
|
|
|
19
|
-
|
|
41
|
+
### Using the editor
|
|
20
42
|
|
|
21
|
-
|
|
43
|
+
```ts
|
|
44
|
+
import { DslEditor, MedeoDsl } from '@mengine/medeo-client/dsl';
|
|
22
45
|
|
|
23
|
-
|
|
46
|
+
const dsl = MedeoDsl.create();
|
|
47
|
+
const editor = new DslEditor(dsl);
|
|
48
|
+
const trackId = editor.createTrack('video_clip');
|
|
49
|
+
const sourceId = editor.registerMedia({
|
|
50
|
+
kind: 'video',
|
|
51
|
+
system: 'memota',
|
|
52
|
+
key: 'video-1',
|
|
53
|
+
durationMs: 5000,
|
|
54
|
+
});
|
|
55
|
+
const [first, second] = editor.addClips({
|
|
56
|
+
trackId,
|
|
57
|
+
clips: [
|
|
58
|
+
{ sourceId, outMs: 3000 },
|
|
59
|
+
{ sourceId, inMs: 1000, outMs: 3000 },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
editor.moveClip(second, trackId, 0);
|
|
63
|
+
editor.setClipVolume(first, -3, { intent: 'Lower the opening clip', actor: { user_id: 'user-1', role: 'user' } });
|
|
64
|
+
const snapshot = dsl.doc.export({ mode: 'snapshot' });
|
|
65
|
+
dsl.dispose();
|
|
66
|
+
```
|
|
24
67
|
|
|
25
|
-
|
|
68
|
+
`MedeoDsl.create()` owns its fresh LoroDoc and frees it on dispose. `new MedeoDsl(doc)` borrows an existing doc and only releases subscriptions. Mirror registers the schema's empty root handles on opening; this adds no CRDT operations or audit entries. The caller uses native binary snapshots/updates for persistence and sync; Mirror JSON is not a backup of text marks or Cursor identities. Opening a legacy content document is rejected.
|
|
26
69
|
|
|
27
|
-
|
|
70
|
+
### Interface and invariants
|
|
28
71
|
|
|
29
|
-
`
|
|
72
|
+
- `getSnapshot()` returns a frozen, read-only `MedeoDslSnapshot`: entity and relation tables, effective containment, rich-text runs, resolved Caption text and diagnostics. It does not compute layout or VideoDraft. Reads reuse the same snapshot until a DSL content event invalidates it; no-op Actions and repeated imports retain it. Reading never commits changes.
|
|
73
|
+
- `subscribe(listener)` includes text-only and marks-only changes. The returned function unsubscribes; dispose is idempotent. As with other synchronous document observers, listeners should not throw or initiate nested writes.
|
|
74
|
+
- `applyMutations(mutations, audit)` is the preferred interface for typed entity, relation, Tree, Text and Caption-selection commands. It opens one `transact`; `tx.applyMutations` inside an existing callback uses the same draft and commit.
|
|
75
|
+
- `transact(edit, audit)` exposes a guarded `tx.draft` for ordinary fields and Tree, `tx.editText(scriptId, edits, expectedBodyId?)`, `tx.editCaptionText(captionId, edits, expectedBodyId?)`, and `tx.setCaptionContent({captionId, audioScriptId, start, end})`. The synchronous callback and every draft reference expire on return. Async callbacks, nested writes, identity changes, direct overwrites of existing bodies, and same-transaction entity deletion/recreation are rejected. Catching a guard/command error inside the callback does not allow a partial commit.
|
|
76
|
+
- `editText(scriptId, expectedBodyId, edits, audit)` delegates to the same transaction. Text supports insert/delete/mark/unmark/editing Delta; offsets are UTF-16 and cannot split surrogate pairs. `tx.draft` exposes the text preview after preceding edits, while existing body strings are never sent through Mirror's full-text diff. A supplied body identity must match the current Text; Actions editing an existing body retain that stale-view check.
|
|
77
|
+
- Caption selections are interpreted at their position in the command sequence. Their Cursors are created after earlier native edits, and follow later edits. New selections are deferred commands, not synthetic Cursor values in `tx.draft`; do not read that relation table to retrieve a pending selection. Explicit Cursor relation writes and deletes also replay in sequence with Text edits.
|
|
78
|
+
- `textRange(scriptId, start, end)` returns native Cursors for a nonempty continuous selection. Caption content stores their native bytes through the central codec. The permanent first-boundary limitation remains deferred.
|
|
30
79
|
|
|
31
|
-
|
|
80
|
+
Every business write requires a structured `DslAudit`: `{semantic_op, payload, intent?, actor?}`. The transaction exit serializes the existing server envelope exactly once; initialization alone uses an internal unaudited path. No-op transactions add no history. External pending native operations must be committed separately first. Known preparation failures write nothing; unexpected native/runtime failures do not receive database-style rollback.
|
|
32
81
|
|
|
33
|
-
|
|
82
|
+
```ts
|
|
83
|
+
const audit = {
|
|
84
|
+
semantic_op: 'createCaptionFromScript',
|
|
85
|
+
payload: { scriptId: 'script-1', captionId: 'caption-1' },
|
|
86
|
+
actor: { user_id: 'user-1', role: 'user' as const },
|
|
87
|
+
};
|
|
34
88
|
|
|
35
|
-
|
|
89
|
+
dsl.transact((tx) => {
|
|
90
|
+
tx.draft['audio-script-entities']['script-1'] = { body: 'hello' };
|
|
91
|
+
tx.draft['caption-entities']['caption-1'] = {};
|
|
92
|
+
tx.editText('script-1', [{ type: 'insert', at: 5, text: ' world' }]);
|
|
93
|
+
// Reads the prepared preview: "hello world". The native Text does not yet exist.
|
|
94
|
+
tx.setCaptionContent({ captionId: 'caption-1', audioScriptId: 'script-1', start: 0, end: 5 });
|
|
95
|
+
}, audit);
|
|
96
|
+
```
|
|
36
97
|
|
|
37
|
-
|
|
98
|
+
`medeoDslLoroShape` defines the persisted containers; `MedeoDslLoroState` is directly inferred from it. `MedeoDslDraft` derives a writable view with optional absent fields and unallocated Tree IDs, accounting for Mirror's input-type limitations. `$cid`/existing TreeID are protected at runtime. `MedeoDslSnapshot` remains the separate read-only domain view, including diagnostics, rich text and resolved Cursors; it is not a schema alias or a second SSOT.
|
|
38
99
|
|
|
39
|
-
|
|
100
|
+
Default fields stay absent. Explicit `seqMarker.rate: 1` or `volume: 0` remain explicit; resetting them deletes the field. A Clip's `seqMarker` — window, rate and timeline fill — is one atomic value: concurrent edits of the same marker keep one whole marker, while `volume` merges on its own. Editor inputs keep their flat `inMs`/`outMs`/`speed`/`fillTimeline` and are translated to the marker. Existing Caption style Maps stay present when their last field is reset. Resource locator changes require a new resource identity; optional facts can be updated independently.
|
|
40
101
|
|
|
41
|
-
|
|
102
|
+
Ordinary entity IDs are fresh allocations. Deleting and recreating the same ordinary entity in one transaction is rejected so Mirror cannot collapse replacement into an in-place field diff; later independent recreation does not inherit old Caption references. Do not split a single business Action into multiple transactions to bypass this restriction. Enhanced collaborative Undo is not enabled. Duplicate containment references are excluded from the effective tree with diagnostics, while the original Tree remains unchanged; this is not a repair or an Undo resolution policy.
|
|
42
103
|
|
|
43
|
-
|
|
104
|
+
`DslEditor` accepts the existing client `CommitOptions` (`intent`, `actor`). Each effective Action writes `{semantic_op, payload, intent, actor}` in one native commit; generated IDs accompany business inputs, and explicit resets use `null` in audit payloads. No-op Actions leave no audit entry. Transport can combine several Actions into one binary update without combining their audit records.
|
|
44
105
|
|
|
45
|
-
|
|
106
|
+
First-phase Track admission is Video/Image on `video_clip`, Audio/VoiceoverRecording on `speech`, Caption on `caption`, and Audio on `bgm`. `fillTimeline: true` is explicit on a Clip input and survives moving tracks; the role does not manufacture this fact. Filling uses currently reject trim/speed edits. Image and Caption uses require an explicit window.
|
|
46
107
|
|
|
47
|
-
|
|
108
|
+
### Implementation and verification boundary
|
|
48
109
|
|
|
49
|
-
|
|
110
|
+
Each handle owns one long-lived Mirror. Ordinary fields and Tree read from its in-memory state; rich-text runs and Cursor resolution use the same native Text. Content events invalidate a lazily rebuilt snapshot, including marks-only events. There is no full native field scan or JSON fingerprint on every read. Rebuilding a changed snapshot still traverses the document in JavaScript and reads native Text; fine-grained read caches, UI binding/IME and large-document performance remain future work.
|
|
111
|
+
|
|
112
|
+
Mirror 2.3.3 cannot safely move an existing node under a parent created in the same draft. The document transaction rejects this combination before writing. When a business Action actually needs it, implement its scoped native path with one audited commit as specified by [K03](../../docs/projects/medeo-dsl/known-unfixed.md#k03-mirror-同批新建父节点并移入已有节点); that fallback is not implemented yet.
|
|
113
|
+
|
|
114
|
+
Loro 1.16.1 and loro-mirror 2.3.3 are the lockfile baseline. Mirror is pinned with a [workspace dependency patch](../../patches/README.md): its optional `setState(..., {applyNative})` hook runs after the ordinary draft reaches Loro and before one commit. The full native event updates Mirror before DSL subscribers are notified. The hook is internal to the DSL adapter; Actions never receive a native-write callback. It supports non-ephemeral Mirror instances, and must not commit/export/import or await. A registry install of loro-mirror never carries the patch, so the published package bundles the patched Mirror, runtime and declarations; consumers install only the `loro-crdt` peer and configure no patch. Transactions probe the Mirror once and refuse to write when the hook is missing rather than drop native edits. Native writes preserve marks and actual container identities; a plain Mirror string snapshot still does not contain rich-text marks. Tests cover snapshot/update round trips, concurrent moves and replacements, field merging, optional reset, malformed codecs, deletion/recreation, rich text, Cursor ownership, subscriptions and Action failure preflight. The conversion library is implemented; production server/editor/player entry points have not switched.
|
|
115
|
+
|
|
116
|
+
Run the package test/build tasks through `vp run @mengine/medeo-client#test` and `vp run @mengine/medeo-client#build`.
|
|
117
|
+
|
|
118
|
+
### Layout and VideoDraft conversion
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { fromVideoDraft, resolveDslLayout, toVideoDraft } from '@mengine/medeo-client/dsl';
|
|
122
|
+
|
|
123
|
+
const { dsl, diagnostics } = fromVideoDraft(legacyContent, {
|
|
124
|
+
mediaFacts, // physical Image/Video/Audio facts supplied by the host
|
|
125
|
+
audit: { semantic_op: 'importVideoDraft', payload: { source: 'legacy' } },
|
|
126
|
+
});
|
|
127
|
+
try {
|
|
128
|
+
const layout = resolveDslLayout(dsl.getSnapshot());
|
|
129
|
+
const { content, diagnostics: projectionDiagnostics } = toVideoDraft(dsl.getSnapshot());
|
|
130
|
+
// The host owns business metadata and persistence; content has no revision/owner/settings.
|
|
131
|
+
} finally {
|
|
132
|
+
dsl.dispose();
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`resolveDslLayout` supports multiple Tracks, sequential/absolute/anchored placement, fallback, window intersection, bounded overlap constraints and timeline fill. `toVideoDraft` directly emits the legacy content contract. Unsupported representations (multiple visual Tracks, audio trim/speed, etc.) throw `DslProjectionError`; recoverable states return diagnostics. Legacy consumer support is not implied by successful conversion.
|
|
137
|
+
|
|
138
|
+
`fromVideoDraft` creates a new document, never overwrites an existing one. Its required audit is used by one content transaction after Timeline initialization. Loro may split a large commit into multiple Changes carrying the same message; the current server lists these separately ([K04](../../docs/projects/medeo-dsl/known-unfixed.md#k04-大提交的原生-change-分段与审计展示)). It restores legacy positioning (main Track sequential, aggregation/caption anchors, otherwise absolute), so original gaps are not guaranteed to survive. Media facts must state intrinsic source duration. Unsupported curves, timed caption sub-cues, orphan parts and inconsistent identities/ownership reject explicitly.
|
|
139
|
+
|
|
140
|
+
Caption text is matched forward in `speech.caption_ids` order. A failed exact match preserves the text in the Caption's own optional **LoroText**, without advancing the match cursor or inventing another AudioScript. Inline text is also available through `DslEditor.createCaption({text, style?})`, `setCaptionText`, and `editCaptionText`. Switching to a reference clears the inline field in the same transaction; direct `transact` callers must clear the other source explicitly. Concurrent coexistence reads the reference, even when it later becomes invalid (no stale-text fallback).
|
|
141
|
+
|
|
142
|
+
`richText` is keyed by AudioScript or inline Caption entity ID; `captionText` resolves either content source. Optional Caption Texts have ordinary identities and are created in the native hook, preserving mergeable style/font while avoiding duplicate text on mergeable-Text deletion Undo. See [RFC 02](../../docs/projects/medeo-dsl/rfc/02-videodraft-projection.md) for layout, import normalization and compatibility limits.
|
|
143
|
+
|
|
144
|
+
## Legacy consumers
|
|
145
|
+
|
|
146
|
+
`SemanticEditor`, `VideoDocument`, their adapters and old timeline projection have been removed. `@mengine/medeo-client/legacy` retains only the old EntityTimelineEditor, entity transport and its direct VideoDraft projection. Old entity tool/sandbox imports use this entry until their business adaptation is implemented. The obsolete timeline-journal replay rejects nonempty plans before writing. They cannot open new DSL documents. `/schemas` remains the old tool's schema contract, not the new Editor input model.
|
|
147
|
+
|
|
148
|
+
`/relay` and `/testing` remain host/test-only entrypoints. The private DSL package is bundled into the client distribution.
|
|
149
|
+
|
|
150
|
+
See [DSL index](../../docs/projects/medeo-dsl/README.md), [capability mapping and cutover](../../docs/projects/medeo-dsl/rfc/03-editor-and-document-cutover.md) and [known limits](../../docs/projects/medeo-dsl/known-unfixed.md). K04 audit segmentation is unchanged; one native commit can contain multiple Changes.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/client/base64.ts
|
|
2
|
+
function bytesToBase64(bytes) {
|
|
3
|
+
const native = bytes.toBase64;
|
|
4
|
+
if (typeof native === "function") return native.call(bytes);
|
|
5
|
+
const chunks = [];
|
|
6
|
+
for (let offset = 0; offset < bytes.length; offset += 8192) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 8192)));
|
|
7
|
+
return btoa(chunks.join(""));
|
|
8
|
+
}
|
|
9
|
+
function base64ToBytes(base64) {
|
|
10
|
+
const binary = atob(base64);
|
|
11
|
+
const bytes = new Uint8Array(binary.length);
|
|
12
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
13
|
+
return bytes;
|
|
14
|
+
}
|
|
15
|
+
//#endregion
|
|
16
|
+
export { bytesToBase64 as n, base64ToBytes as t };
|