From 68fc2c242124f81dc7bda28551a589f78cc1a3c8 Mon Sep 17 00:00:00 2001 From: ziqiang <1694392889@qq.com> Date: Thu, 2 Jul 2026 12:32:50 +0800 Subject: [PATCH 01/12] feat(core): AI-facing edit API surface + playground demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a second, deterministic-by-default surface on `EditorApi` for callers without UI state (AI tool-loops, scripts, tests). Existing positional methods are untouched — legacy code compiles. Core (@aicut/core): - New `EditResult` discriminated union — every new mutator returns `{ ok: true, data }` or `{ ok: false, reason, hint? }`. Reasons are a typed union (`clip-not-found` | `time-outside-clip` | `overlap` | `invalid-time` | `invalid-range` | `source-not-found` | `source-load-failed` | `internal-error`), so AI callers can branch without guessing. - `Editor.batch(label, fn)` — wraps `beginInteraction / endInteraction` with try/finally + async support. N mutations become one undo entry. `label` reserved for future labeled-history UI. - `Editor.splitClip({ clipId, timeMs })` — split a specific clip, no more first-match-wins across other tracks. Fails cleanly outside the clip's [start, end). - `Editor.moveClipTo({ clipId, toTrackId?, startMs?, onOverlap })` — explicit destination. `onOverlap: "error"` (default) refuses when taken; `"auto"` opts into today's smart-routing. - `Editor.trimClip({ clipId, edge, timeMs })` — move one edge without the ripple-close side effects the UI-facing `trimLeft` applies. - `Editor.deleteClip({ clipId })` — typed removal. - `Editor.addClip({ sourceUrl | sourceId, trackId, startMs, inMs?, outMs?, onOverlap?, meta? })` — one-shot: probe the URL for duration + dimensions (via new `probeMediaSource` helper), register the source, insert the clip. Async. `meta` accepted but silently dropped in MVP (Clip.meta field lands in a future release; passing it now is forward-compatible). - `Editor.captureFrame({ timeMs, source: 'composite' | 'raw', clipId?, format?, quality?, maxWidth? })` — grab a still frame as JPEG/PNG blob. Composite mode reads the compositor canvas after seek + rAF (requires CanvasCompositorEngine or WebCodecsEngine). Raw mode spawns a hidden `