robustness plan
This commit is contained in:
@ -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 }}>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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 • Press ? to close</p>
|
<p style="margin:16px 0 0;font-size:11px;color:#94a3b8;text-align:center">Customize in Settings • Press ? to close</p>
|
||||||
|
|||||||
608
polish_plan.md
608
polish_plan.md
@ -1,328 +1,342 @@
|
|||||||
# TalkEdit — UI Polish Plan
|
# TalkEdit — Testing & 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
|
|
||||||
|
|||||||
Reference in New Issue
Block a user