robustness plan

This commit is contained in:
2026-05-06 13:18:53 -06:00
parent 6ac1d68887
commit 9a301fe2a2
4 changed files with 335 additions and 298 deletions

View File

@ -775,9 +775,16 @@ export default function App() {
{/* Draggable divider */} {/* Draggable divider */}
<div <div
tabIndex={0}
role="separator"
aria-label="Resize panel"
className="w-1 shrink-0 bg-editor-border cursor-col-resize hover:bg-editor-accent/50 active:bg-editor-accent transition-colors relative z-10" className="w-1 shrink-0 bg-editor-border cursor-col-resize hover:bg-editor-accent/50 active:bg-editor-accent transition-colors relative z-10"
style={{ cursor: isDraggingSplit.current ? 'col-resize' : 'col-resize' }} style={{ cursor: isDraggingSplit.current ? 'col-resize' : 'col-resize' }}
onMouseDown={startSplitDrag} onMouseDown={startSplitDrag}
onKeyDown={(e) => {
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" title="Drag to resize"
/> />
@ -857,8 +864,15 @@ export default function App() {
<div className="flex shrink-0"> <div className="flex shrink-0">
{/* Draggable sidebar divider */} {/* Draggable sidebar divider */}
<div <div
tabIndex={0}
role="separator"
aria-label="Resize panel"
className="w-1 shrink-0 bg-editor-border cursor-col-resize hover:bg-editor-accent/50 active:bg-editor-accent transition-colors relative z-10" className="w-1 shrink-0 bg-editor-border cursor-col-resize hover:bg-editor-accent/50 active:bg-editor-accent transition-colors relative z-10"
onMouseDown={startSidebarDrag} onMouseDown={startSidebarDrag}
onKeyDown={(e) => {
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" title="Drag to resize"
/> />
<div className="overflow-y-auto" style={{ width: sidebarWidth }}> <div className="overflow-y-auto" style={{ width: sidebarWidth }}>

View File

@ -93,6 +93,12 @@ export default function HelpContent() {
<Shortcut keys="Ctrl+F" desc="Find in transcript" /> <Shortcut keys="Ctrl+F" desc="Find in transcript" />
<Shortcut keys="?" desc="Toggle cheatsheet" /> <Shortcut keys="?" desc="Toggle cheatsheet" />
</div> </div>
<button
onClick={() => window.dispatchEvent(new KeyboardEvent('keydown', { key: '?' }))}
className="text-editor-accent hover:underline text-xs mt-2"
>
View full keyboard shortcut reference
</button>
</Section> </Section>
<div className="text-[10px] text-editor-text-muted leading-relaxed border-t border-editor-border pt-4"> <div className="text-[10px] text-editor-text-muted leading-relaxed border-t border-editor-border pt-4">

View File

@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useEditorStore } from '../store/editorStore'; import { useEditorStore } from '../store/editorStore';
import { loadBindings } from '../lib/keybindings'; import { loadBindings, DEFAULT_PRESETS } from '../lib/keybindings';
import type { KeyBinding } from '../types/project'; import type { KeyBinding } from '../types/project';
export function useKeyboardShortcuts() { export function useKeyboardShortcuts() {
@ -180,6 +180,8 @@ function toggleCheatsheet(bindings: KeyBinding[]) {
overlay.remove(); overlay.remove();
}; };
const presetName = JSON.stringify(bindings) === JSON.stringify(DEFAULT_PRESETS['left-hand']) ? 'Left-Hand Preset' : 'Standard Preset';
const rows = bindings const rows = bindings
.map( .map(
(b) => (b) =>
@ -188,6 +190,7 @@ function toggleCheatsheet(bindings: KeyBinding[]) {
.join(''); .join('');
overlay.innerHTML = `<div style="background:#1a1d27;border:1px solid #2a2d3a;border-radius:12px;padding:24px 32px;max-width:450px;position:relative;" onclick="event.stopPropagation()"> overlay.innerHTML = `<div style="background:#1a1d27;border:1px solid #2a2d3a;border-radius:12px;padding:24px 32px;max-width:450px;position:relative;" onclick="event.stopPropagation()">
<div style="font-size:11px;color:#94a3b8;margin-bottom:12px">Active preset: <span style="color:#818cf8;font-weight:500">${presetName}</span></div>
<h3 style="margin:0 0 16px;font-size:14px;font-weight:600;color:#e2e8f0">Keyboard Shortcuts</h3> <h3 style="margin:0 0 16px;font-size:14px;font-weight:600;color:#e2e8f0">Keyboard Shortcuts</h3>
<table style="font-size:13px">${rows}</table> <table style="font-size:13px">${rows}</table>
<p style="margin:16px 0 0;font-size:11px;color:#94a3b8;text-align:center">Customize in Settings &bull; Press ? to close</p> <p style="margin:16px 0 0;font-size:11px;color:#94a3b8;text-align:center">Customize in Settings &bull; Press ? to close</p>

View File

@ -1,328 +1,342 @@
# TalkEdit — UI Polish Plan # TalkEdit — Testing &amp; Robustness Plan
## 1. Tooltips: show what it does + keyboard shortcut 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.
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" |
--- ---
## 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
``` ### transcription.rs — no existing tests
[? Help] - [ ] `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). ### paths.rs — no existing tests
- [ ] `project_root` — dev layout, packaged (TAURI_RESOURCE_DIR), fallback
### 2.2 Help panel sections - [ ] `python_exe` — bundled path, venv paths, fallback to .venv312
- [ ] `backend_script` — joins project_root/backend
#### Getting Started (for first-time users) - [ ] `root_script` — joins project_root
```
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
--- ---
## 3. Keyboard shortcut cheatsheet improvements ## 2. Frontend store (`frontend/src/store/`)
Current: `?` key appends a `<div>` 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: ### licenseStore.ts — no existing tests
- [ ] Render the cheatsheet as a React portal (inside a modal overlay) instead of manual DOM - [ ] `canEdit` — true for Licensed, true for Trial, false for Expired, false for null
- [ ] Add a close button (×) in the top-right corner - [ ] `checkStatus` — calls getAppStatus, sets correct state, handles error (falls to Expired)
- [ ] Group shortcuts by category with visual headers (Transport, Editing, File, View) - [ ] `activateLicense` — valid key sets Licensed, invalid key returns false
- [ ] Show the current active preset name at the top - [ ] `deactivateLicense` — reverts to trial if valid, falls to Expired otherwise
- [ ] Add the `?` tooltip "Show/hide keyboard shortcuts" to itself - [ ] `hasFeature` — returns true for matching, false for missing
- [ ] Show the cheatsheet from the Help panel too (not just `?` key) - [ ] `setShowDialog` — toggles dialog visibility
- [ ] Persist middleware — Licensed status persists, Trial/Expired does not
### Categories and grouping: ### aiStore.ts — no existing tests
- [ ] `setProviderConfig` — updates provider, encrypts API keys
| Transport | Edit | File | View | - [ ] `setDefaultProvider` — changes default
|-----------|------|------|------| - [ ] `setCustomFillerWords` — sets and clears
| Space — Play/Pause | Delete — Cut selection | Ctrl+S — Save | ? — Toggle cheatsheet | - [ ] `setFillerResult` — sets and clears
| ← → — Skip 5s | I — Mark in | Ctrl+O — Open | Ctrl+F — Find | - [ ] `setProcessing` — toggles with message
| J — Slow down | O — Mark out | Ctrl+E — Export | |
| K — Pause | Ctrl+Z — Undo | | |
| L — Speed up | Ctrl+Shift+Z — Redo | | |
--- ---
## 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 | ### useVideoSync.ts — no existing tests
|-----------|---------|-----| - [ ] Synchronization of store `isPlaying` with video element, audio element
| MarkersPanel | Shows nothing when no markers | Add: "No markers yet. Press M or click Add Marker to create one." | - [ ] `togglePlay` — starts playing, pauses
| AIPanel (clips) | Shows nothing before first detection | Add: "Click 'Find Best Clips' to discover the most shareable moments in your video." | - [ ] `seekTo` — seeks video to correct time, seeks audio to correct time
| AppendClipPanel | "No additional clips loaded" | Keep but add hint: "Add video files to concatenate during export." | - [ ] Handles video element ref being null (doesn't crash)
| 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 `<pre>` 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…") |
--- ---
## 5. Consistency fixes ## 4. Frontend lib (`frontend/src/lib/`)
### 5.1 Fix mute zone color in ZoneEditor ### keybindings.ts — no existing tests
`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. - [ ] `loadBindings` — loads from localStorage, falls back to standard preset when missing
- [ ] `saveBindings` — persists and reloads correctly
### 5.2 Unify disabled opacity - [ ] `applyPreset` — 'standard' and 'left-hand' both apply all required bindings
- All disabled buttons: `opacity-40` (currently some use 50%) - [ ] `detectConflicts` — detects duplicate keys, returns empty when no conflicts
### 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.
--- ---
## 6. Quick wins (implement first) ## 5. Backend services (`backend/services/`)
- [ ] Add `title` tooltips to ALL toolbar buttons with shortcut hints ### video_editor.py — no existing tests
- [ ] Add `title` tooltips to ALL ExportDialog controls - [ ] `apply_cut_segments` — keeps correct segments from transcript word list, FFmpeg concat cmd
- [ ] Fix mute zone color in ZoneEditor (orange → blue) - [ ] `apply_mute_ranges` — cuts audio for muted ranges
- [ ] Add empty state to MarkersPanel - [ ] `apply_gain_ranges` — adjusts volume (positive and negative) for FFmpeg filter chains
- [ ] Add error display to AIPanel - [ ] `apply_speed_ranges` — time-stretches or compresses segments
- [ ] Add close button to keyboard cheatsheet - [ ] `mix_background_music` — mixes with ducking enabled, mixes without ducking, handles no music
- [ ] Unify disabled opacity to 40% everywhere - [ ] `build_export_filters` — stitches together all zone types into correct filter order
- [ ] Remove orphaned VolumePanel.tsx
- [ ] Add loading spinner to WaveformTimeline
## 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 ### ai_provider.py — no existing tests
- [ ] Add Help button to toolbar (`?` icon, opens sidebar) - [ ] `complete` — Ollama completion succeeds, OpenAI completion succeeds, Claude completion succeeds
- [ ] Wire Help as a sidebar panel (like AI, Export, Settings) - [ ] `complete` — handles missing provider, timeout, bad JSON response
- [ ] Build first-run welcome overlay component - [ ] `list_ollama_models` — returns models list, handles connection error
- [ ] Add "Show help on startup" checkbox to Settings
- [ ] Render keyboard cheatsheet as React portal with close button
## 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 `<ErrorBoundary>` that catches render errors and shows a fallback with "Something went wrong" + a reload button.
- [ ] Create `ErrorBoundary.tsx` component (`componentDidCatch` pattern)
- [ ] Wrap the entire `<App />` 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