diff --git a/FEATURES.md b/FEATURES.md index 561a3ae..c229d9f 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -138,6 +138,21 @@ These aren't features to build — they're things to make more visible in the UI --- +## 🚫 Explicitly deferred — multi-track timeline + +TalkEdit deliberately avoids becoming a general-purpose NLE. Multi-track compositing (z-order, per-track opacity, keyframe animation, nested sequences) would take months and you'd still lose to DaVinci Resolve (free), CapCut (free), and OpenShot (free). + +**TalkEdit's advantage is that it isn't a timeline editor** — the text-is-the-timeline model makes spoken-word editing drastically faster than dragging razor cuts around. + +What exists and what's planned: +- One audio track (main) + optional background music track (done) +- Clip concatenation (append clips end-to-end) (done) +- Potential future: **B-roll overlay** — a single up/down overlay track for inserting images or cutaway clips at specific timestamps. Useful for talking-head content (screenshots, charts, reaction clips) without a full multi-track system. The zone model already supports this — you add an overlay zone type with a file reference. + +Everything beyond that (picture-in-picture, multi-layer compositing, per-layer keyframing) should stay deferred. If the market demands it later, it's easier to add than to remove complexity. + +--- + ## ✅ Already Implemented - [#025] Word-level transcript editing (select, drag, shift-click, delete) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1a32a76..aed41ad 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ import ZoneEditor from './components/ZoneEditor'; import BackgroundMusicPanel from './components/BackgroundMusicPanel'; import AppendClipPanel from './components/AppendClipPanel'; import LicenseDialog from './components/LicenseDialog'; +import HelpContent from './components/HelpContent'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useLicenseStore } from './store/licenseStore'; import { @@ -35,11 +36,12 @@ import { ListVideo, Clock, AlertTriangle, + HelpCircle, } from 'lucide-react'; const LAST_MEDIA_PATH_KEY = 'talkedit:lastMediaPath'; -type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'zones' | 'markers' | 'music' | 'append' | null; +type Panel = 'ai' | 'settings' | 'export' | 'silence' | 'zones' | 'markers' | 'music' | 'append' | 'help' | null; export default function App() { const { @@ -133,6 +135,9 @@ export default function App() { const [speedMode, setSpeedMode] = useState(false); const [speedModeValue, setSpeedModeValue] = useState(1.25); const [showReprocessConfirm, setShowReprocessConfirm] = useState(false); + const [showWelcomeOverlay, setShowWelcomeOverlay] = useState(() => { + return localStorage.getItem('talkedit:welcomeDismissed') !== 'true'; + }); const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false); const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise) | null>(null); const [lastSavedSignature, setLastSavedSignature] = useState(null); @@ -727,34 +732,6 @@ export default function App() { title="Append additional video clips — concatenate multiple files during export" />
-
- -
-
} label="AI Tools" @@ -776,6 +753,13 @@ export default function App() { active={activePanel === 'settings'} onClick={() => togglePanel('settings')} /> + } + label="Help" + active={activePanel === 'help'} + onClick={() => togglePanel('help')} + title="View help and feature documentation" + />
@@ -833,6 +817,9 @@ export default function App() {

Processing audio

{transcriptionStatus || 'Please wait...'}

+
+
+
) : words.length > 0 ? ( @@ -885,6 +872,7 @@ export default function App() { {activePanel === 'ai' && } {activePanel === 'export' && } {activePanel === 'settings' && } + {activePanel === 'help' && }
)} @@ -893,6 +881,53 @@ export default function App() { + {showWelcomeOverlay && ( +
+
+
+ +

Welcome to TalkEdit

+

+ The offline video editor for long-form content. No account. No subscription. Your data never leaves your machine. +

+
+ +
+
+ 1 + + Open a video — TalkEdit transcribes it into a word-level transcript. + +
+
+ 2 + + Edit by selecting words — deleting words cuts the matching video. Like editing a doc. + +
+
+ 3 + + Export your final cut — with captions, background music, AI cleanup, and more. + +
+
+ +
+ +

+ Press ? anytime to see shortcuts, or click Help in the toolbar. +

+
+
+
+ )} + {showReprocessConfirm && (
- {isExporting ? ( - <> - - Exporting... {Math.round(exportProgress)}% - - ) : ( - <> - - Export Video - - )} + + Export Video + {/* Export progress */} + {isExporting && ( +
+
+ + Exporting... + {Math.round(exportProgress)}% +
+
+
+
+

Export in progress...

+
+ )} + {exportError && (
{exportError} diff --git a/frontend/src/components/HelpContent.tsx b/frontend/src/components/HelpContent.tsx new file mode 100644 index 0000000..8201920 --- /dev/null +++ b/frontend/src/components/HelpContent.tsx @@ -0,0 +1,150 @@ +import { HelpCircle, Scissors, VolumeX, SlidersHorizontal, Gauge, Film, Search, FileText, Download, Music, MapPin, ListVideo, Sparkles, Keyboard } from 'lucide-react'; + +export default function HelpContent() { + return ( +
+

+ + Help & Reference +

+ +
}> + Open a video file — click File > Open File or press Ctrl+O + Wait for transcription — Whisper processes your audio and creates a word-level transcript + Edit by selecting words — choose Cut, Mute, Sound Gain, or Speed Adjust from the toolbar + Use AI tools — detect filler words, find clips, re-transcribe with a different model + Export — apply all edits and save your final video + Press ? anytime to see all keyboard shortcuts +
+ +
}> +

These are time-range edits applied during export. You create them in three ways:

+ Select words in the transcript — the toolbar buttons create a zone from the selected word range + Use Mark In (I) and Mark Out (O) on the timeline, then clicking the toolbar button + Click a toolbar button to enter zone mode, then drag on the waveform timeline to draw a zone +

+ Cut — removes the segment from the output entirely
+ Mute — silences the audio but keeps the video
+ Sound Gain — adjusts volume (positive = louder, negative = quieter)
+ Speed Adjust — changes playback speed (1.0x = normal, 2.0x = double) +

+

View and manage all zones in the Edit Zones panel. Click a zone on the waveform to select it — drag edges to resize, drag the body to move.

+
+ +
}> + Click to seek, drag to scrub through the video + Enter zone 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 + Delete or Backspace removes the selected zone (with confirmation) + Ctrl+Scroll to zoom in/out, scroll to pan horizontally + Toggle individual zone types on/off with the colored buttons above the waveform + "Show adjusted timeline" compresses cut regions to preview the output +
+ +
}> + Click a word to select it, Shift+Click to extend the selection + Ctrl+Click any word to seek the video to that exact timestamp + Double-click any word to edit its text directly + Words with low confidence get an orange dotted underline — adjust the threshold in Settings + Ctrl+F to search the transcript — navigate matches with Enter / Shift+Enter + Select a word range and click Re-transcribe to re-run Whisper on just that segment +
+ +
}> + Add markers at the current playhead position with a label and color + Use I / O keys to set mark in/out points on the timeline + Markers auto-sort as chapters — click Copy as YouTube timestamps to get chapter text +
+ +
}> +

Filler Words — detects "um", "uh", "like", "you know" and similar words. Add custom fillers (e.g. "okay", "alright"). Apply All creates cut ranges for every detection at once.

+

Create Clips — analyzes your transcript to find the best 20-60 second segments for TikTok, YouTube Shorts, or Instagram Reels.

+

Reprocess — re-run transcription with a different Whisper model. Larger models are more accurate but slower. English-only models are faster for English content.

+

AI features work with the bundled local model (no setup needed), or via Ollama/OpenAI/Claude — configure in Settings.

+
+ +
}> + Fast mode (stream copy): instant, no quality loss — but doesn't apply cuts or effects + Re-encode mode: applies all edits — cuts, gain, speed, zoom, captions, background music + Captions can be burned into the video or exported as separate SRT/VTT files + Loudness normalization targets: YouTube (-14 LUFS), Spotify (-16), Broadcast (-23) + Audio enhancement: noise reduction and speech clarity + Export Transcript Only — get SRT or plain text without the video +
+ +
}> + Bkg. Music — add a music track with auto-ducking: the music automatically lowers when someone speaks. Adjust volume, duck amount, attack, and release times. + Add Clips — load additional video files to concatenate during export. Drag to reorder. + Both are applied during re-encode export only +
+ +
}> +

Press ? anytime to see the full cheatsheet overlay. Remap any shortcut in Settings.

+
+ + + + + + + + + + + +
+
+ +
+ TalkEdit is 100% offline. No account required. No data leaves your machine. No subscription — buy once, own forever. +
+
+ ); +} + +function Section({ title, icon, children }: { title: string; icon: React.ReactNode; children: React.ReactNode }) { + return ( +
+

+ {icon} + {title} +

+
+ {children} +
+
+ ); +} + +function P({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return

{children}

; +} + +function Bullet({ children }: { children: React.ReactNode }) { + return ( +
+ + {children} +
+ ); +} + +function Step({ num, children }: { num?: number; children: React.ReactNode }) { + return ( +
+ + {num} + + {children} +
+ ); +} + +function Shortcut({ keys, desc }: { keys: string; desc: string }) { + return ( +
+ {keys} + {desc} +
+ ); +} diff --git a/frontend/src/components/MarkersPanel.tsx b/frontend/src/components/MarkersPanel.tsx index fe85bec..8c9e02b 100644 --- a/frontend/src/components/MarkersPanel.tsx +++ b/frontend/src/components/MarkersPanel.tsx @@ -124,7 +124,7 @@ export default function MarkersPanel() { <> {m.label} - + )}
diff --git a/frontend/src/components/TranscriptEditor.tsx b/frontend/src/components/TranscriptEditor.tsx index 710ecb9..558206d 100644 --- a/frontend/src/components/TranscriptEditor.tsx +++ b/frontend/src/components/TranscriptEditor.tsx @@ -457,8 +457,8 @@ export default function TranscriptEditor({ ${isZoneDragSelected && muteMode ? 'bg-blue-500/30 ring-1 ring-blue-400/60' : ''} ${isZoneDragSelected && gainMode ? 'bg-amber-500/30 ring-1 ring-amber-400/60' : ''} ${isZoneDragSelected && speedMode ? 'bg-emerald-500/30 ring-1 ring-emerald-400/60' : ''} - ${isSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/15 ring-1 ring-editor-accent/35' : ''} - ${isActiveSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/35 ring-1 ring-editor-accent text-white' : ''} + ${isSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/15 ring-2 ring-editor-accent/50' : ''} + ${isActiveSearchMatch && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/35 ring-2 ring-editor-accent text-white font-medium' : ''} ${isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-selected text-white' : ''} ${isActive && !isSelected && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-accent/20 text-editor-accent' : ''} ${isHovered && !isSelected && !isActive && !cutRange && !muteRange && !gainRange && !speedRange ? 'bg-editor-word-hover' : ''} diff --git a/frontend/src/components/WaveformTimeline.tsx b/frontend/src/components/WaveformTimeline.tsx index d74af1a..39494c4 100644 --- a/frontend/src/components/WaveformTimeline.tsx +++ b/frontend/src/components/WaveformTimeline.tsx @@ -473,10 +473,10 @@ export default function WaveformTimeline({ // Draw resize handles ctx.fillStyle = '#ef4444'; ctx.beginPath(); - ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x1, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x2, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); } } @@ -498,10 +498,10 @@ export default function WaveformTimeline({ // Draw resize handles ctx.fillStyle = '#3b82f6'; ctx.beginPath(); - ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x1, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x2, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); } } @@ -522,10 +522,10 @@ export default function WaveformTimeline({ ctx.fillStyle = '#f59e0b'; ctx.beginPath(); - ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x1, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x2, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); } } @@ -546,10 +546,10 @@ export default function WaveformTimeline({ ctx.fillStyle = '#10b981'; ctx.beginPath(); - ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x1, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); ctx.beginPath(); - ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI); + ctx.arc(x2, waveTop + waveH / 2, 6, 0, 2 * Math.PI); ctx.fill(); } @@ -849,7 +849,7 @@ export default function WaveformTimeline({ // Check if click is in waveform area if (y < waveTop || y > waveTop + waveH) return null; - const handleSize = forHover ? 6 : 8; // Smaller hit area for hover, larger for click + const handleSize = forHover ? 8 : 10; // Check cut ranges for (const range of showCutZones ? cutRanges : []) { @@ -1188,6 +1188,7 @@ export default function WaveformTimeline({ e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); + if (!window.confirm("Delete this zone?")) return; if (selectedZone.type === 'cut') { removeCutRange(selectedZone.id); } else if (selectedZone.type === 'mute') { @@ -1313,6 +1314,15 @@ export default function WaveformTimeline({ > {audioError} +
) : !waveformDataRef.current ? (
diff --git a/frontend/src/components/ZoneEditor.tsx b/frontend/src/components/ZoneEditor.tsx index 218ea58..7d7b75a 100644 --- a/frontend/src/components/ZoneEditor.tsx +++ b/frontend/src/components/ZoneEditor.tsx @@ -120,6 +120,7 @@ export default function ZoneEditor() { ); const removeZone = useCallback((type: 'cut' | 'mute' | 'gain' | 'speed', id: string) => { + if (!window.confirm("Delete this zone?")) return; if (type === 'cut') removeCutRange(id); else if (type === 'mute') removeMuteRange(id); else if (type === 'gain') removeGainRange(id);