From 9a301fe2a2b298c8fa20b6f8047c0fb8536e5c3d Mon Sep 17 00:00:00 2001 From: dillonj Date: Wed, 6 May 2026 13:18:53 -0600 Subject: [PATCH] robustness plan --- frontend/src/App.tsx | 14 + frontend/src/components/HelpContent.tsx | 6 + frontend/src/hooks/useKeyboardShortcuts.ts | 5 +- polish_plan.md | 608 +++++++++++---------- 4 files changed, 335 insertions(+), 298 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aed41ad..bde83d7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -775,9 +775,16 @@ export default function App() { {/* Draggable divider */}
{ + if (e.key === 'ArrowLeft') setSplitRatio(Math.max(0.15, splitRatio - 0.02)); + if (e.key === 'ArrowRight') setSplitRatio(Math.min(0.85, splitRatio + 0.02)); + }} title="Drag to resize" /> @@ -857,8 +864,15 @@ export default function App() {
{/* Draggable sidebar divider */}
{ + if (e.key === 'ArrowUp') setSidebarWidth(Math.max(180, sidebarWidth - sidebarWidth * 0.02)); + if (e.key === 'ArrowDown') setSidebarWidth(Math.min(600, sidebarWidth + sidebarWidth * 0.02)); + }} title="Drag to resize" />
diff --git a/frontend/src/components/HelpContent.tsx b/frontend/src/components/HelpContent.tsx index 8201920..6486939 100644 --- a/frontend/src/components/HelpContent.tsx +++ b/frontend/src/components/HelpContent.tsx @@ -93,6 +93,12 @@ export default function HelpContent() {
+
diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts index 0305a4d..33f92a5 100644 --- a/frontend/src/hooks/useKeyboardShortcuts.ts +++ b/frontend/src/hooks/useKeyboardShortcuts.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; import { useEditorStore } from '../store/editorStore'; -import { loadBindings } from '../lib/keybindings'; +import { loadBindings, DEFAULT_PRESETS } from '../lib/keybindings'; import type { KeyBinding } from '../types/project'; export function useKeyboardShortcuts() { @@ -180,6 +180,8 @@ function toggleCheatsheet(bindings: KeyBinding[]) { overlay.remove(); }; + const presetName = JSON.stringify(bindings) === JSON.stringify(DEFAULT_PRESETS['left-hand']) ? 'Left-Hand Preset' : 'Standard Preset'; + const rows = bindings .map( (b) => @@ -188,6 +190,7 @@ function toggleCheatsheet(bindings: KeyBinding[]) { .join(''); overlay.innerHTML = `
+
Active preset: ${presetName}

Keyboard Shortcuts

${rows}

Customize in Settings • Press ? to close

diff --git a/polish_plan.md b/polish_plan.md index a6559ad..5d6fa71 100644 --- a/polish_plan.md +++ b/polish_plan.md @@ -1,328 +1,342 @@ -# TalkEdit — UI Polish Plan +# TalkEdit — Testing & Robustness Plan -## 1. Tooltips: show what it does + keyboard shortcut - -Every toolbar button and action button should have a `title` that explains the action and shows the keyboard shortcut if one exists. - -### Toolbar buttons (App.tsx) - -Current: `title={label}` → shows just the name. -New format: `title="Cut the selected or marked range [Ctrl+X]"` - -| Button | Current tooltip | New tooltip | -|--------|----------------|-------------| -| Cut | "Cut" | "Cut selected word range or mark in/out area [Ctrl+X]" | -| Mute | "Mute" | "Mute selected word range or mark in/out area [Ctrl+M]" | -| Gain Zone | "Gain Zone" | "Add gain zone from selection or mark in/out [Ctrl+G]" | -| Speed Zone | "Speed Zone" | "Add speed zone from selection or mark in/out [Ctrl+Shift+S]" | -| Zones | "Zones" | "Open zone editor panel [Ctrl+Shift+Z]" | -| Pause Trim | "Pause Trim" | "Detect and remove silent pauses [Ctrl+T]" | -| Markers | "Markers" | "Add and manage timeline markers [Ctrl+Shift+M]" | -| Music | "Music" | "Add background music track [Ctrl+Shift+B]" | -| Append | "Append" | "Append additional video clips [Ctrl+Shift+A]" | -| Reprocess | "Reprocess transcript with selected model" | "Re-transcribe entire video with selected model | -| AI | "AI" | "AI filler detection and clip suggestions [Ctrl+I]" | -| Export | "Export" | "Export video with current edits [Ctrl+E]" | -| Settings | "Settings" | "Configure AI providers, shortcuts, models [Ctrl+,]" | - -### File menu dropdown items - -| Item | Current | New | -|------|---------|-----| -| New Project | none | "Start a new empty project" | -| Open File | none | "Open a video or audio file for transcription" | -| Load Project | none | "Open a saved .aive project file" | -| Save | none | "Save current project [Ctrl+S]" | -| Save As | none | "Save a copy of the current project" | - -### Waveform timeline controls - -| Element | New tooltip | -|---------|-------------| -| Show adjusted timeline checkbox | "Compress cut regions to see the output timeline without gaps" | -| Cut zones toggle | "Show/hide cut ranges on the timeline" | -| Mute zones toggle | "Show/hide mute ranges on the timeline" | -| Gain zones toggle | "Show/hide gain ranges on the timeline" | -| Speed zones toggle | "Show/hide speed ranges on the timeline" | -| Zoom instruction text | "Scroll to pan · Ctrl+Scroll to zoom [Ctrl+= to reset zoom]" | -| Thumbnail toggle | "Show waveform thumbnail previews from the video" | - -### Transcript selection toolbar - -| Button | New tooltip | -|--------|-------------| -| Cut | "Remove this word range from the output" | -| Mute | "Silence audio for this word range" | -| Gain | "Adjust volume for this word range — positive boosts, negative reduces" | -| Speed | "Change playback speed for this word range — lower is slower, higher is faster" | -| Re-transcribe | "Re-run Whisper transcription on just this segment to improve accuracy" | - -### AIPanel buttons - -| Button | New tooltip | -|--------|-------------| -| Detect Filler Words | "Scan the entire transcript for filler words (um, uh, like, you know…) and mark for removal" | -| Apply All | "Create cut ranges for all detected filler words at once" | -| Dismiss | "Clear detected filler word results without applying" | -| Find Best Clips | "Analyze transcript to find the most engaging 20-60 second segments for social media" | -| Preview clip | "Seek to this clip's position and play a preview" | -| Export clip | "Export just this segment as a standalone video file" | - -### ExportDialog controls - -Every control needs a tooltip — this is the most complex panel with zero tooltips. - -| Control | Tooltip | -|---------|---------| -| Fast export card | "Stream copy — no re-encoding, fast but no effects or cuts applied" | -| Re-encode card | "Full re-encode — applies cuts, gain, speed, zoom, captions, and effects" | -| Resolution select | "Output video resolution — higher = larger file" | -| Format select | "Output container format — MP4 is most compatible" | -| Enable zoom checkbox | "Crop and reposition the video frame — useful for removing black bars or reframing" | -| Zoom slider | "Magnification level — 1.0x is original, higher values zoom in" | -| Pan X slider | "Horizontal position of the crop window — negative moves left, positive moves right" | -| Pan Y slider | "Vertical position of the crop window — negative moves up, positive moves down" | -| Background removal checkbox | "Remove or replace the background behind the speaker" | -| Background blur slider | "Amount of Gaussian blur applied to the background" | -| Loudness normalization checkbox | "Normalize audio to a consistent loudness target — recommended for YouTube" | -| LUFS target select | "Loudness target: YouTube (-14), Spotify (-16), Broadcast (-23)" | -| Audio enhancement checkbox | "Apply noise reduction and speech enhancement (DeepFilterNet)" | -| Captions select | "Burn captions into video, export as separate file (SRT/VTT), or none" | -| Export Transcript section | "Export just the transcript text or subtitles without the video" | - -### SettingsPanel controls - -| Control | Tooltip | -|---------|---------| -| Zone preview padding | "Extra context time shown before and after each zone when previewing" | -| Confidence threshold | "Words below this confidence get an orange underline — lower = show fewer warnings" | -| AI provider selector | "Choose which AI backend powers filler detection, chapters, and suggestions" | -| Ollama base URL | "URL of your Ollama instance — default is localhost:11434" | -| Ollama model | "Model name to use for AI features — requires Ollama running with this model pulled" | -| OpenAI API key | "Your OpenAI API key — stored encrypted on your machine" | -| Claude API key | "Your Anthropic Claude API key — stored encrypted on your machine" | -| Keyboard shortcut inputs | "Click then press the key combination you want to assign" | - -### Zone detail tooltips - -| Element | Tooltip | -|---------|---------| -| Zone preview button | "Preview this zone with {N}s of context before and after" | -| Gain dB input | "Volume adjustment in decibels — +6 dB doubles volume, -6 dB halves it" | -| Speed multiplier | "Playback speed multiplier — 1.0x is normal, 2.0x is twice as fast" | -| Delete zone button | "Remove this zone permanently" | +Tests are critical before launch to prevent regressions and ensure the app is stable. Below is every function in the codebase that needs test coverage, organized by module. --- -## 2. Help menu / feature documentation +## 1. Rust backend (`src-tauri/src/`) -### 2.1 Help button in toolbar +### licensing.rs — already has partial coverage (4 tests) +- [ ] `verify_license_key` — valid key, invalid format, invalid signature, expired key ✅ (exists) +- [ ] `load_cached_license` — file exists, file missing, malformed file +- [ ] `cache_license` — write and read back +- [ ] `remove_license` — removes file, no-ops if missing +- [ ] `get_or_start_trial` — creates new trial file, loads existing, handles corrupt file +- [ ] `get_trial_info` — active trial (29 days), expired trial (0 days), exactly at boundary +- [ ] `get_trial_days_remaining` — active returns Some, expired returns None, +- [ ] `clear_trial` — removes file, no-ops if missing +- [ ] `get_app_status` — licensed (cached license), trial active, expired (no license, no trial) +- [ ] `has_feature` — feature exists, feature missing, empty features list -Add a `?` help button to the right side of the toolbar (next to Settings): +### models.rs — no existing tests +- [ ] `list_models` — whisper models found, llm models found, mixed, empty dirs +- [ ] `delete_model` — deletes file, deletes directory, path doesn't exist +- [ ] `huggingface_cache_dir` — HF_HOME set, XDG_CACHE_HOME set, defaults to ~/.cache -``` -[? Help] -``` +### transcription.rs — no existing tests +- [ ] `ensure_model_downloaded` — returns success (stub function) -Clicking it opens a **Help panel** (not a dialog — uses the existing sidebar panel system, or slides in as an overlay). - -### 2.2 Help panel sections - -#### Getting Started (for first-time users) - -``` -Welcome to TalkEdit - -1. Open a video file → click "Open Video File" or press Ctrl+O -2. Wait for transcription — Whisper processes your audio and creates a word-level transcript -3. Edit by selecting words → choose Cut, Mute, Gain, or Speed from the toolbar -4. Use AI tools → detect filler words, find clips, auto-chapter -5. Export → apply all edits and save your final video - -Pro tip: press ? anytime to see all keyboard shortcuts -``` - -#### Feature reference - -**Transcription** -- Select a Whisper model from the toolbar dropdown (larger = more accurate but slower) -- Click a word to select it, Shift+click to extend the selection -- Ctrl+click any word to seek the video to that timestamp -- Double-click any word to edit its text -- Right-click or use the selection toolbar to apply Cut/Mute/Gain/Speed -- Select a word range and click Re-transcribe to improve accuracy on that segment - -**Zones (Cut / Mute / Gain / Speed)** -- Zones are time-range edits applied during export -- Create zones by: selecting words in the transcript, using mark-in/mark-out on the timeline, or dragging on the waveform while in zone mode -- Cut = removes the segment from output entirely -- Mute = silences audio but keeps the video -- Gain = adjust volume (positive = louder, negative = quieter) -- Speed = change playback speed -- All zones can be resized and moved on the waveform timeline -- View and manage all zones in the Zone Editor panel - -**Waveform Timeline** -- The waveform shows your audio with all zone overlays -- Click to seek, drag to scrub -- Enter Cut/Mute/Gain/Speed mode from the toolbar, then drag on the waveform to create a zone -- Click an existing zone to select it — drag edges to resize, drag body to move -- Press Delete or Backspace to remove the selected zone -- Ctrl+Scroll to zoom in/out, Scroll to pan horizontally -- Toggle individual zone types on/off with the colored buttons -- "Show adjusted timeline" compresses cut regions to preview the output - -**AI Features** -- Filler word detection: finds "um", "uh", "like", "you know" and similar words. Add custom fillers in the AI panel. Apply All to create cut ranges for all detected fillers at once. -- Clip suggestions: analyzes your transcript to find the best 20-60 second segments for TikTok, YouTube Shorts, or Instagram Reels. -- AI features work locally with the bundled Qwen3 model (no internet needed) or via Ollama/OpenAI/Claude — configure in Settings. - -**Markers** -- Markers are named timestamps pinned to the waveform -- Add markers at the current playhead position with a label and color -- Markers auto-sort as chapters — copy as YouTube timestamps format -- Useful for chapter breaks, key moments, or section headings - -**Music & Append** -- Background Music: add a music track with auto-ducking (music lowers when someone speaks) -- Append Clips: load additional video files to concatenate during export -- Both are applied during re-encode export only - -**Export** -- Fast mode (stream copy): no quality loss, but doesn't apply cuts, effects, or music — only works if you haven't made any edits -- Re-encode mode: applies all edits, cuts, effects, captions, and music -- Captions: burn directly into video or export as separate SRT/VTT file -- Loudness normalization: match YouTube (-14 LUFS), Spotify (-16), or Broadcast (-23) standards -- Audio enhancement: noise reduction and speech clarity via DeepFilterNet -- Video zoom: crop and reposition the frame (useful for removing letterboxing or reframing) - -**Keyboard Shortcuts** -[Full table of all shortcuts — same as the ? cheatsheet but always visible in this section] - -**Settings** -- AI Providers: configure Ollama (local), OpenAI (cloud), or Claude (cloud). The bundled Qwen3 model works with zero setup. -- Model Management: view and delete downloaded Whisper and LLM models to free disk space -- Keyboard Shortcuts: remap any shortcut — click a binding then press your desired combination -- Confidence threshold: adjust the low-confidence word highlighting sensitivity -- Zone preview padding: how much context to show before/after zones during preview - -### 2.3 First-run onboarding - -When a user opens the app for the first time (no license activated, no project loaded): - -Show a **welcome overlay** with: -1. "Welcome to TalkEdit" heading -2. Brief description: "The offline video editor for long-form content" -3. Three quick-start steps with icons: - - Open a video → starts transcription - - Edit by deleting words → cuts out the matching video - - Export your final cut -4. "Got it" button that dismisses permanently (store in localStorage) -5. A "Show this again" checkbox in the Help panel +### paths.rs — no existing tests +- [ ] `project_root` — dev layout, packaged (TAURI_RESOURCE_DIR), fallback +- [ ] `python_exe` — bundled path, venv paths, fallback to .venv312 +- [ ] `backend_script` — joins project_root/backend +- [ ] `root_script` — joins project_root --- -## 3. Keyboard shortcut cheatsheet improvements +## 2. Frontend store (`frontend/src/store/`) -Current: `?` key appends a `
` to `document.body` with a table of shortcuts. +### editorStore.ts (Zustand) — partially covered (2 tests) +- [ ] `reset` ✅ (in beforeEach) +- [ ] `setGlobalGainDb` — clamps to -24/+24 ✅ (exists) +- [ ] `addGainRange` — adds with correct start/end/gain ✅ (exists) +- [ ] `addCutRange` — adds with correct start/end, handles overlapping ranges +- [ ] `addMuteRange` — adds with correct start/end +- [ ] `addSpeedRange` — adds with correct start/end/speed +- [ ] `removeCutRange` — removes existing, no-ops on missing +- [ ] `removeMuteRange` — removes existing, no-ops on missing +- [ ] `removeGainRange` — removes existing, no-ops on missing +- [ ] `removeSpeedRange` — removes existing, no-ops on missing +- [ ] `updateCutRange` — updates bounds, prevents negative duration +- [ ] `updateMuteRange` — updates bounds +- [ ] `updateGainRangeBounds` — updates bounds +- [ ] `updateSpeedRangeBounds` — updates bounds +- [ ] `updateGainRange` — updates gain value +- [ ] `updateSpeedRange` — updates speed value +- [ ] `setSelectedWordIndices` — single, multiple, empty, out of range handled +- [ ] `replaceWordRange` — replaces words in middle, replaces at start, handles invalid indices +- [ ] `updateWordText` — updates word, preserves timing, no-ops on bad index +- [ ] `getWordAtTime` — exact match, between words, before first word, after last word, no words +- [ ] `loadVideo` — sets videoUrl, resets state, handles missing file +- [ ] `setCurrentTime` — sets time, clamps to 0-duration +- [ ] `setTranscribing` — toggles flag, sets status +- [ ] `setTranscription` — sets words and segments, handles empty arrays +- [ ] `setMarkInTime` / `setMarkOutTime` — sets and clears +- [ ] `clearMarkRange` — clears both marks +- [ ] `addTimelineMarker` — adds with label/color/time +- [ ] `removeTimelineMarker` — removes by id +- [ ] `updateTimelineMarker` — updates label/color +- [ ] `setZonePreviewPaddingSeconds` — sets and clamps +- [ ] `setBackgroundMusic` / `updateBackgroundMusic` — sets and updates +- [ ] `setAdditionalClips` — adds, removes, reorders +- [ ] `setSilenceTrimGroups` — sets groups -### Fixes: -- [ ] Render the cheatsheet as a React portal (inside a modal overlay) instead of manual DOM -- [ ] Add a close button (×) in the top-right corner -- [ ] Group shortcuts by category with visual headers (Transport, Editing, File, View) -- [ ] Show the current active preset name at the top -- [ ] Add the `?` tooltip "Show/hide keyboard shortcuts" to itself -- [ ] Show the cheatsheet from the Help panel too (not just `?` key) +### licenseStore.ts — no existing tests +- [ ] `canEdit` — true for Licensed, true for Trial, false for Expired, false for null +- [ ] `checkStatus` — calls getAppStatus, sets correct state, handles error (falls to Expired) +- [ ] `activateLicense` — valid key sets Licensed, invalid key returns false +- [ ] `deactivateLicense` — reverts to trial if valid, falls to Expired otherwise +- [ ] `hasFeature` — returns true for matching, false for missing +- [ ] `setShowDialog` — toggles dialog visibility +- [ ] Persist middleware — Licensed status persists, Trial/Expired does not -### Categories and grouping: - -| Transport | Edit | File | View | -|-----------|------|------|------| -| Space — Play/Pause | Delete — Cut selection | Ctrl+S — Save | ? — Toggle cheatsheet | -| ← → — Skip 5s | I — Mark in | Ctrl+O — Open | Ctrl+F — Find | -| J — Slow down | O — Mark out | Ctrl+E — Export | | -| K — Pause | Ctrl+Z — Undo | | | -| L — Speed up | Ctrl+Shift+Z — Redo | | | +### aiStore.ts — no existing tests +- [ ] `setProviderConfig` — updates provider, encrypts API keys +- [ ] `setDefaultProvider` — changes default +- [ ] `setCustomFillerWords` — sets and clears +- [ ] `setFillerResult` — sets and clears +- [ ] `setProcessing` — toggles with message --- -## 4. Missing states (empty/loading/error) +## 3. Frontend hooks (`frontend/src/hooks/`) -### Empty states +### useKeyboardShortcuts.ts — no existing tests +- [ ] Keyboard event dispatch — space plays/pauses, J/K/L speed controls, I/O marks, Delete cuts, Ctrl+Z undo +- [ ] `toggleCheatsheet` — creates overlay with correct content, toggles off +- [ ] Skip logic — skips correctly forward and back from current playhead -| Component | Current | Fix | -|-----------|---------|-----| -| MarkersPanel | Shows nothing when no markers | Add: "No markers yet. Press M or click Add Marker to create one." | -| AIPanel (clips) | Shows nothing before first detection | Add: "Click 'Find Best Clips' to discover the most shareable moments in your video." | -| AppendClipPanel | "No additional clips loaded" | Keep but add hint: "Add video files to concatenate during export." | -| WaveformTimeline (zones) | Canvas is empty | No change needed — zones are overlays, not content | - -### Error states - -| Component | Current | Fix | -|-----------|---------|-----| -| AIPanel | Errors logged to console only | Show error message in the panel with a retry button | -| ExportDialog | Shows export error in a red box | Keep, but add a "Copy error" button | -| VideoPlayer | No error for broken video | Add an error state with "Could not load video" + re-select button | -| WaveformTimeline | Shows error text in a `
` tag | Keep, but add a "Retry" button |
-| Silence detection | Errors use `alert()` | Show error inline in the panel |
-
-### Loading states
-
-| Component | Current | Fix |
-|-----------|---------|-----|
-| WaveformTimeline | Blank canvas while audio loads | Add a centered "Loading waveform…" spinner |
-| Export | Percentage text only | Add a determinate progress bar |
-| Transcription | Spinning waveform bars + text | Add a determinate progress bar for model download phase |
-| AI features | Spinner + "Processing…" | Add descriptive step text ("Analyzing transcript…") |
+### useVideoSync.ts — no existing tests
+- [ ] Synchronization of store `isPlaying` with video element, audio element
+- [ ] `togglePlay` — starts playing, pauses
+- [ ] `seekTo` — seeks video to correct time, seeks audio to correct time
+- [ ] Handles video element ref being null (doesn't crash)
 
 ---
 
-## 5. Consistency fixes
+## 4. Frontend lib (`frontend/src/lib/`)
 
-### 5.1 Fix mute zone color in ZoneEditor
-`ZoneEditor.tsx` uses `border-orange-500/40` for mute zones — should be `border-blue-500/40` to match the waveform timeline's blue mute color.
-
-### 5.2 Unify disabled opacity
-- All disabled buttons: `opacity-40` (currently some use 50%)
-
-### 5.3 Unify border radius
-- All toolbar buttons: `rounded-md` (keep)
-- All sidebar panel inputs: `rounded-lg` (keep)
-- All zone/detection list items: `rounded-lg` (currently `rounded`)
-
-### 5.4 Remove orphaned VolumePanel
-`VolumePanel.tsx` is not imported anywhere. Either wire it into the sidebar or remove it.
+### keybindings.ts — no existing tests
+- [ ] `loadBindings` — loads from localStorage, falls back to standard preset when missing
+- [ ] `saveBindings` — persists and reloads correctly
+- [ ] `applyPreset` — 'standard' and 'left-hand' both apply all required bindings
+- [ ] `detectConflicts` — detects duplicate keys, returns empty when no conflicts
 
 ---
 
-## 6. Quick wins (implement first)
+## 5. Backend services (`backend/services/`)
 
-- [ ] Add `title` tooltips to ALL toolbar buttons with shortcut hints
-- [ ] Add `title` tooltips to ALL ExportDialog controls
-- [ ] Fix mute zone color in ZoneEditor (orange → blue)
-- [ ] Add empty state to MarkersPanel
-- [ ] Add error display to AIPanel
-- [ ] Add close button to keyboard cheatsheet
-- [ ] Unify disabled opacity to 40% everywhere
-- [ ] Remove orphaned VolumePanel.tsx
-- [ ] Add loading spinner to WaveformTimeline
+### video_editor.py — no existing tests
+- [ ] `apply_cut_segments` — keeps correct segments from transcript word list, FFmpeg concat cmd
+- [ ] `apply_mute_ranges` — cuts audio for muted ranges
+- [ ] `apply_gain_ranges` — adjusts volume (positive and negative) for FFmpeg filter chains
+- [ ] `apply_speed_ranges` — time-stretches or compresses segments
+- [ ] `mix_background_music` — mixes with ducking enabled, mixes without ducking, handles no music
+- [ ] `build_export_filters` — stitches together all zone types into correct filter order
 
-## 7. Help system (implement second)
+### audio_cleaner.py — no existing tests
+- [ ] `detect_silence` — detects pauses above threshold, returns correct time ranges
+- [ ] `remove_silence` — splits by silence, re-concatenates keep segments
 
-- [ ] Create `HelpContent.tsx` with all feature documentation content
-- [ ] Add Help button to toolbar (`?` icon, opens sidebar)
-- [ ] Wire Help as a sidebar panel (like AI, Export, Settings)
-- [ ] Build first-run welcome overlay component
-- [ ] Add "Show help on startup" checkbox to Settings
-- [ ] Render keyboard cheatsheet as React portal with close button
+### ai_provider.py — no existing tests
+- [ ] `complete` — Ollama completion succeeds, OpenAI completion succeeds, Claude completion succeeds
+- [ ] `complete` — handles missing provider, timeout, bad JSON response
+- [ ] `list_ollama_models` — returns models list, handles connection error
 
-## 8. Polish (implement third)
+### transcription.py — no existing tests
+- [ ] `transcribe_file` — returns words with correct format (word, start, end, confidence)
+- [ ] `transcribe_segment` — re-transcribes a range with offset-adjusted timestamps
+- [ ] `_load_model` — caches model, returns existing from cache, handles GPU/CPU
+
+### caption_generator.py — no existing tests
+- [ ] `generate_srt` — correct SRT format, sequential numbering, proper timestamps
+- [ ] `generate_vtt` — correct VTT format with header, chroma key tags
+- [ ] `generate_ass` — correct ASS subtitle format
+
+### gpu_utils.py — no existing tests
+- [ ] `get_optimal_device` — returns CUDA when available, returns CPU otherwise
+
+### audio_processing.py — no existing tests
+- [ ] `extract_audio` — extracts wav from video, handles audio-only input, temp file cleanup
+
+---
+
+## 6. Frontend components — integration tests
+
+### TranscriptEditor.tsx
+- [ ] Word selection: click, shift+click range, drag select
+- [ ] Ctrl+click seeks video to correct time
+- [ ] Double-click enters edit mode, Enter commits, Escape cancels
+- [ ] Zone mode drag creates correct zone type
+- [ ] Search finds matches, navigates with Enter/Shift+Enter
+- [ ] "Restore" button appears on hover over words in a zone, removes the zone
+- [ ] Re-transcribe calls backend and updates words
+- [ ] Selection toolbar buttons create correct zone types
+- [ ] When `canEdit` is false, buttons are disabled and zone creation is blocked
+
+### WaveformTimeline.tsx
+- [ ] Canvas renders waveform when audio data loads
+- [ ] Click seeks to correct time
+- [ ] Zone drag creates zone on mouse up
+- [ ] Zone selection and resize with handles
+- [ ] Delete key removes selected zone
+- [ ] Zoom and scroll work correctly
+- [ ] Zone toggle buttons show/hide overlay layers
+- [ ] Loading spinner shows when no waveform data
+- [ ] Error message with retry button on load failure
+
+### ExportDialog.tsx
+- [ ] Fast mode card and re-encode card toggle correctly
+- [ ] Resolution selector only visible in re-encode mode
+- [ ] Format selector disables WAV for video files
+- [ ] Export button triggers export with correct parameters
+- [ ] Progress bar updates during export
+- [ ] Loudness normalization checkbox shows LUFS target selector
+
+### AIPanel.tsx
+- [ ] Filler words tab: detect button sends request, displays results
+- [ ] Apply All creates cut ranges for all fillers
+- [ ] Clips tab: find clips shows suggestions
+- [ ] Reprocess tab: model selector + reprocess button
+- [ ] Error state with retry on API failure
+
+### App.tsx — layout and toolbar
+- [ ] Welcome screen shows when no video loaded
+- [ ] Trial bar shows on welcome screen for Trial/Expired states
+- [ ] Toolbar buttons toggle modes correctly (Cut, Mute, Gain, Speed)
+- [ ] Toolbar buttons open correct panels (Zones, Silence, Markers, Music, Append, AI, Export, Settings, Help)
+- [ ] File menu opens/closes, items work
+- [ ] Split pane dividers are draggable and keyboard-accessible
+- [ ] First-run welcome overlay shows once
+- [ ] Hotkeys work: Escape clears modes, ? opens cheatsheet
+
+---
+
+## 7. Error handling regression tests
+
+- [ ] Backend crash: show reconnect banner, not broken UI
+- [ ] Transcription failure: show error, allow retry with different model
+- [ ] Export failure: show FFmpeg stderr, allow copy
+- [ ] Model download timeout: show error, allow retry
+- [ ] File not found: handled gracefully on open/load
+- [ ] Permission denied: handled gracefully on save/export
+- [ ] Concurrent operations: block export during transcription, block transcription during export
+
+---
+
+## 8. Licensing & trial flow tests
+
+- [ ] Fresh install: shows 30-day trial
+- [ ] Day 29: still allows editing
+- [ ] Day 31: shows expired, editing locked, export still works
+- [ ] Activate valid license: switches to Licensed, clears trial
+- [ ] Activate invalid license: shows error, stays on trial
+- [ ] Deactivate license: returns to trial if valid, expires if trial over
+- [ ] Expired banner shows correct message and activate link
+- [ ] `canEdit` prop correctly gates all editing controls across all components
+
+---
+
+## Test infrastructure
+
+| Layer | Framework | Run command |
+|-------|-----------|-------------|
+| Rust | `cargo test` (built-in) | `cd src-tauri && cargo test` |
+| Frontend (Vitest) | Vitest + jsdom | `cd frontend && npx vitest run` |
+| Frontend (components) | Playwright or Vitest + testing-library | `cd frontend && npx vitest run` |
+| Python backend | pytest | `cd backend && python -m pytest` |
+
+### Setup needed
+- [ ] Frontend: `npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom` (vitest likely already installed)
+- [ ] Frontend: add `"test": "vitest run"` to `package.json` if not present
+- [ ] Python: ensure pytest is installed (`pip install pytest pytest-asyncio`)
+- [ ] CI: add GitHub Actions workflow for `cargo test && vitest run && pytest`
+
+---
+
+## Priority order
+
+1. **store tests** (editorStore, licenseStore, aiStore) — core data integrity
+2. **Rust licensing tests** — payment/trial logic must never break
+3. **Rust models tests** — filesystem operations must be safe
+4. **Backend service unit tests** — export pipeline, transcription, AI
+5. **Component integration tests** — user-facing behavior
+6. **Error handling regression tests** — robustness
+
+---
+
+## Robustness beyond tests
+
+### React Error Boundary
+The app has no error boundary — a single JS error in any component crashes the entire UI to a white screen. Wrap the app in a `` that catches render errors and shows a fallback with "Something went wrong" + a reload button.
+
+- [ ] Create `ErrorBoundary.tsx` component (`componentDidCatch` pattern)
+- [ ] Wrap the entire `` in `main.tsx`
+- [ ] Fallback shows: error message, stack trace (collapsed), "Reload" button, "Reset & Clear State" button
+
+### Global JS error logging
+Uncaught errors in async code and event handlers silently break the app. Add a `window.onerror` and `window.onunhandledrejection` handler that logs to the Tauri backend and shows a toast notification.
+
+- [ ] Add global error handler in `main.tsx` that intercepts all uncaught errors
+- [ ] Log to Rust backend via `invoke('log_error', { message, stack })` 
+- [ ] Show a non-blocking toast notification (bottom-right) for non-fatal errors
+- [ ] Fatal errors still go to the ErrorBoundary
+
+### Input validation layer
+The app trusts user input too much. Invalid values in number inputs, empty file paths, or negative durations can cause crashes or silent failures. Add validation at the store level.
+
+- [ ] `editorStore.ts` — validate all setters: clamp numbers, reject empty strings for paths, enforce min/max on dB and speed values
+- [ ] `licenseStore.ts` — validate license key format before sending to Rust (prefix check, base64 pattern)
+- [ ] `aiStore.ts` — validate API key formatting, model name not empty
+- [ ] Export options — validate resolution, format, loudness target against allowed values before sending to backend
+
+### Frontend runtime assertions
+The app makes assumptions about data shapes (e.g. `words[sorted[0]].start` assumes the index exists). Add assertion checks in critical paths that log a clear error instead of silently producing NaN or undefined.
+
+- [ ] Add an `assert` utility function: `assert(condition, message)` that throws a descriptive error in dev, warns in prod
+- [ ] Guard all array index access in TranscriptEditor, WaveformTimeline, ExportDialog
+- [ ] Guard null/undefined checks on store actions that expect existing data
+
+### Auto-save crash recovery
+If the app crashes or the system loses power during editing, current work is lost. Add periodic auto-save to a temp file that gets restored on next launch.
+
+- [ ] Every 60 seconds, save the full editor state to `app_data_dir/autosave.json`
+- [ ] On launch, check if autosave.json exists and is newer than the last manual save
+- [ ] Show a "Recover unsaved work?" prompt with date/time of autosave
+- [ ] Clean up autosave after a manual save or after recovery is accepted/dismissed
+
+### Backend health check & self-diagnostics
+When the Python backend dies mid-session, the app doesn't know until a request fails. Add periodic health checks and a diagnostics panel.
+
+- [ ] Poll `GET /health` every 15 seconds from the frontend
+- [ ] If backend is unreachable: show a non-blocking banner "Backend disconnected — retrying..."
+- [ ] When backend comes back online, dismiss the banner automatically
+- [ ] Add a `/diagnostics` endpoint that reports: Python version, available FFmpeg, GPU detection, model cache sizes, disk space
+- [ ] Wire to a "System Info" section in Settings or DevPanel
+
+### CI pipeline with automated checks
+Currently no CI exists. A GitHub Actions workflow would catch regressions on every push.
+
+- [ ] Add `.github/workflows/ci.yml`:
+  - `cargo test` — all Rust tests
+  - `cargo clippy -- -D warnings` — enforce Rust lint rules
+  - `npx vitest run` — all frontend tests
+  - `npx tsc --noEmit` — TypeScript type check
+  - `python -m pytest backend/tests/` — Python backend tests
+  - `cargo build --release` — verify release build succeeds
+- [ ] Run on push to `main` and on PRs
+- [ ] Add Rust `#[deny(clippy::all)]` to catch common mistakes at build time
+
+### Fuzz testing for Rust deserialization
+The app deserializes user-provided JSON (project files, API responses) — any malformed input could crash the Rust backend. Add fuzz tests for the critical deserialization paths.
+
+- [ ] Fuzz `TranscriptionResult` deserialization — malformed word/segment JSON
+- [ ] Fuzz `.aive` project file loading — corrupted JSON, missing fields, wrong types
+- [ ] Fuzz `LicensePayload` deserialization — tampered license payloads
+- [ ] Use `cargo-fuzz` or `proptest` crate for property-based testing
+
+### Performance telemetry (opt-in)
+Without real-world data, you can't know where the app is slow. Add lightweight timing around operations that users complain about.
+
+- [ ] Log timing for: transcription time, export time, AI completion time, model download time
+- [ ] Store in localStorage (not sent anywhere — privacy-first)
+- [ ] Show in DevPanel: last N operation timings
+- [ ] Use this data to identify slow paths before users report them
+
+### Recovery from bad project state
+A corrupted `.aive` file or a partially-loaded project can leave the app in an unusable state. Add recovery paths.
+
+- [ ] On project load failure: show error, offer "Load anyway with partial data" or "Cancel"
+- [ ] Add "Reset Editor State" button in DevPanel (clears all state back to empty)
+- [ ] Store action: validate all zone ranges are within video duration, auto-remove invalid ones on save
 
-- [ ] Progress bar for export (determinate bar, not just text)
-- [ ] Progress bar for model downloads
-- [ ] Retry button on waveform load error
-- [ ] Confirmation dialog for zone/marker deletion
-- [ ] Keyboard-accessible split pane resizing
-- [ ] Larger hit targets for canvas zone handles (r=4 → r=6)
-- [ ] Search bar match indicator contrast improvement