Reprenez un projet existant ou parcourez vos fichiers
+
+
+
+
+
+
+
+
+
+
+ Récents
+
+
+
+
+
+
+
+
+
demo2
+
~/OpenScreen/demo2
+
+ à l'instant
+
+
+
+
+
+
+
+
launch-demo-q3
+
~/OpenScreen/launch-demo-q3
+
+ il y a 2 h
+
+
+
+
+
+
+
+
tutorial-cut
+
~/Documents/OpenScreen/tutorial-cut
+
+ hier
+
+
+
+
+
+
+
+
podcast-ep-12
+
~/OpenScreen/podcast-ep-12
+
+ il y a 3 j
+
+
+
+
+
+
+
+
onboarding-2026
+
~/Work/onboarding-2026
+
+ semaine dernière
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Nouveau projet
+
Choisissez un point de départ et donnez-lui un nom
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/architecture/ai-edition-collision-analysis.md b/docs/architecture/ai-edition-collision-analysis.md
new file mode 100644
index 000000000..f469b8925
--- /dev/null
+++ b/docs/architecture/ai-edition-collision-analysis.md
@@ -0,0 +1,355 @@
+# AI Edition Merge — Collision Analysis & Edge Cases
+
+> Cross-references `ai-edition-merge-plan.md`, `openscreen-inventory.md`, `axcut-inventory.md`. Every collision listed here must be resolved before its owning phase starts.
+>
+> **Severity legend:** 🔴 breaks the merge · 🟠 will cause user-visible regressions · 🟡 polish / nice-to-have.
+
+---
+
+## 1. Schema collisions (Phase 0)
+
+### 1.1 Both projects have `version: 2` but mean different things 🔴
+- OpenScreen: `projectPersistence.ts:65` — `PROJECT_VERSION = 2`, denotes the `EditorProjectData` envelope.
+- axcut: `axcut-schema/src/index.ts:1` — `axcutSchemaVersion = 2`, denotes the `AxcutDocument` shape.
+- **Resolution:** bump everything to v3. Single `SCHEMA_VERSION = 3` constant lives in `src/lib/ai-edition/schema/`. Migration is bidirectional: v2 OpenScreen ↔ v3 ↔ v2 axcut. Document the migration paths in `migrateDocument(value, fromVersion)`:
+ - `fromVersion = 2, source = 'openscreen'` → wrap `EditorProjectData` into `AxcutDocument` (single asset, single clip spanning full duration, trimRegions inverted to skipRanges).
+ - `fromVersion = 2, source = 'axcut'` → pass-through (already valid).
+ - `fromVersion = 3` → no-op.
+- Auto-detect source by sniffing for `media.screenVideoPath` (OpenScreen) vs `assets[]` (axcut). No source-of-origin field needed in v3.
+
+### 1.2 Axcut's merged primary `transcript` vs per-asset `transcripts[]` 🟠
+- `axcut-schema/src/index.ts:96-100` — `documentSchema` carries **both** a single merged `transcript` and a `transcripts[]` array.
+- `document-service.ts:73-83` — every `updateTranscript` call merges all per-asset transcripts into the single primary via `mergeTranscripts`.
+- The merge exists because axcut's `VirtualPreview` only consumes one transcript.
+- **Collision:** with multi-asset projects, the merge is lossy — you can't tell which words came from which asset.
+- **Resolution:** Phase 1 stores only `transcripts[]`; `VirtualPreview` (Phase 2) reads the per-clip `clip.wordRefs[]` instead of a single primary. Drop the `transcript` (singular) field in v3.
+
+### 1.3 OpenScreen's `trimRegions` are inverses of axcut's `skipRanges` 🟠
+- OpenScreen: `types.ts:211` — `TrimRegion { id, startMs, endMs }` — regions are **kept**.
+- axcut: `axcut-schema/src/index.ts:79` — `skipRangeSchema` — regions are **removed**.
+- **Resolution:** migration from v2 OpenScreen inverts: each `trimRegion` becomes a `skipRange` pair (everything outside trim = kept, inside trim = skipped). For the simple case of a single trim region `[a, b]`, you get `skipRanges = [{startSec: 0, endSec: a}, {startSec: b, endSec: duration}]`.
+- **Edge case:** when an OpenScreen project has **zero trim regions** (entire recording kept), the migrated project has `skipRanges: []` and **one clip spanning the full duration**. This is the "single-take" baseline — should be the most common migration result.
+
+### 1.4 OpenScreen has fields axcut lacks 🔴
+| OpenScreen field | Map to AxcutDocument v3 |
+|---|---|
+| `editor.trimRegions[]` | invert into `skipRanges[]` |
+| `editor.zoomRegions[]` | new `zoomRanges[]` field in `documentSchema` |
+| `editor.annotationRegions[]` | new `annotations[]` field in `documentSchema` |
+| `editor.speedRegions[]` | already in axcut as `timeline.speedRanges[]` (rangeSchema shape) |
+| `editor.{wallpaper, shadowIntensity, showBlur, motionBlurAmount, borderRadius, padding, cropRegion, aspectRatio, webcamLayoutPreset, webcamMaskShape, webcamMirrored, webcamReactiveZoom, webcamSizePreset, webcamPosition, cursorTheme}` | new `legacyEditor` envelope; applied at export time |
+| Cursor telemetry (`useScreenRecorder.ts` writes `.cursor.json` next to WebM) | **NOT** in document; stays as sidecar asset metadata. Add `asset.cursorTelemetryPath?` to `assetSchema`. |
+
+### 1.5 Schema fields need asset-scoped addressing 🟠
+- In a single-asset OpenScreen project, `zoomRegion.startMs/endMs` are timeline-absolute.
+- In a multi-asset project, a zoom region applies to **one source asset** within one **clip**. The simplest model is: zoom regions are clip-scoped, not document-scoped.
+- **Resolution:** in v3, `zoomRanges[]`, `annotations[]`, and `speedRanges[]` all carry `clipId?` (optional — null means "apply globally if there's exactly one clip, otherwise error"). OpenScreen's migrated projects (one clip) keep their regions with `clipId: null`. Multi-clip projects require `clipId`.
+- This needs a `resolveRegionScope(document, region)` helper.
+
+### 1.6 axcut's `clip_` id generation is fragile 🟡
+- `timeline.ts:50-66` — auto-generated ids `clip_1, clip_2, ...`. New clips scan for the next free index. Could collide with user-provided ids.
+- **Resolution:** use `crypto.randomUUID()` for all auto-generated clip ids (matching `TimelineEditor.tsx:17` pattern OpenScreen already uses). Update `retimeClips` to skip the numeric generation.
+
+---
+
+## 2. Process model collisions (Phases 0, 6)
+
+### 2.1 Python worker + Faster-Whisper → drop entirely 🔴
+- axcut spawns Python as a child process via `PythonWorker` (`apps/server/src/services/python-worker.ts:14`). On Windows, finding `python` on PATH is unreliable; on macOS, codesigning a Python interpreter is a nightmare.
+- OpenScreen's `transformers.js` Whisper (`src/lib/captioning/transcribe.ts:44`) runs in a Web Worker, already bundled via `caption-assets/`.
+- **Resolution:** axcut's `enqueueTranscription` becomes a renderer-side call to `transcribeMono16kToSegments`. `enqueueAssetIngest` (which calls probe + proxy + transcribe) becomes: renderer reads metadata via `mediabunny` probe; transcribes via Whisper; updates the document.
+- **Edge case:** axcut's Whisper defaults to `medium` (1.5 GB); OpenScreen bundles `tiny` (~75 MB). Need a user setting `transcriptionModel` (default `tiny`, opt-in to `medium`). The model download flow must happen in the renderer (transformers.js) — no Python download server.
+
+### 2.2 Fastify + SSE → Electron IPC + `webContents.send` 🔴
+- axcut: Fastify listens on `127.0.0.1:4010`, SSE stream at `GET /api/projects/:id/events`.
+- OpenScreen: Electron main process + IPC handlers.
+- **Resolution:** every axcut route becomes a `native-bridge:invoke` domain action (or a new `ai-edition:invoke` channel). The EventBus stays in the main process; fan-out happens via `webContents.send('ai-edition:event', { projectId, event })`. Renderer subscribes via a new `useAiEditionEvents(projectId)` hook.
+- **Edge case:** SSE supports auto-reconnect; raw `webContents.send` does not. If the renderer reloads (e.g. user opens DevTools and hits Cmd+R), it must re-subscribe on mount. Plan accordingly in `VideoEditor.tsx`'s `useEffect`.
+- **Edge case:** `webContents.send` only reaches the window it targets. Multiple editor windows? OpenScreen currently has one editor window; verify the renderer doesn't open multiple before assuming fan-out works.
+
+### 2.3 SQLite location `.axcut-data/metadata.sqlite` → `app.getPath('userData')` 🔴
+- axcut hardcodes the data dir under the repo.
+- OpenScreen uses `app.getPath('userData')` (the OS-standard user data location: `~/Library/Application Support/OpenScreen` on macOS, `%APPDATA%/openscreen` on Windows).
+- **Resolution:** move SQLite to `app.getPath('userData')/ai-edition/metadata.sqlite`. WAL mode is fine for Electron.
+- **Edge case:** `better-sqlite3` is a native module. OpenScreen already has `@electron/rebuild` in devDeps — verify it's wired in `electron-builder.json5` rebuild step. If not, add `npm rebuild better-sqlite3` to the build script.
+
+### 2.4 axcut's `queueMicrotask` job runner → real cancellation 🔴
+- axcut `job-service.ts:18-22` schedules work via `queueMicrotask(() => void runX(...))`. No way to cancel.
+- **Edge case:** user closes the editor mid-ingest. axcut's Fastify stays alive, the job keeps running, the renderer never sees the result. User is confused.
+- **Resolution:** main-process job runner tracks `Set` per window. When the editor window closes, mark all its jobs as `cancelled` and abort the underlying `Worker.terminate()` (caption) or `AbortController.abort()` (HTTP fetch for transcription).
+
+### 2.5 axcut credentials in plain JSON → `safeStorage` 🟠
+- axcut: `llm-credentials.json` is plain JSON, written under `.axcut-data/`.
+- **Security regression:** OpenScreen should not store credentials as plain JSON. Use Electron's `safeStorage.encryptString()` (OS keychain: Keychain on macOS, DPAPI on Windows, libsecret on Linux).
+- **Resolution:** `LlmConfigService` (Phase 7) encrypts keys with `safeStorage` before write; decrypts on read. Keys migrate: on first run, detect plain JSON and re-encrypt.
+- **Edge case:** `safeStorage.isEncryptionAvailable()` may return false on Linux without a desktop session. Fallback: `electron-store` with a user-set passphrase, or warn + opt-out.
+
+---
+
+## 3. UX / UI collisions (Phases 1, 5)
+
+### 3.1 Single recording vs multi-take workflow 🔴
+- Today: `useScreenRecorder.ts:621` `stopRecording` → `switchToEditor()` is unconditional.
+- Plan said: stay-in-recorder with prompt. **Conflict with the existing flow.** Need a decision:
+ - **Stay-in-recorder** (Screen Studio feel): every stop → toast "Saved to project X. Record another or open editor?" → two buttons.
+ - **Auto-open-editor** (today's feel): every stop → editor opens. The user's mental model matches existing.
+- **Recommendation:** default to **stay-in-recorder** when `aiEdition` flag is on and there's an active project; default to **auto-open-editor** when there's no active project. The HUD needs a "Switch project" / "New project" affordance.
+- **Edge case:** if the user starts recording without an active project, axcut's flow says "create a new project first." OpenScreen today has no project concept on the HUD. Need a quick-create button.
+- **Edge case:** what if recording fails (stream error)? Plan path: don't create the asset; don't switch window; show toast.
+
+### 3.2 `Ctrl+C / Ctrl+V` clip duplicate collides with Electron Edit menu 🔴
+- Inventory §9 already flagged this. **Resolution:**
+ - Override the application menu's `Edit > Copy` / `Edit > Paste` roles to call `webContents.send('editor:clipboard', { op: 'copy' | 'paste' })` when the editor window is focused. The editor's React handler intercepts and triggers clip duplicate.
+ - In text inputs (``, `