removed home

This commit is contained in:
2026-05-06 23:11:00 -06:00
parent 2212d7b265
commit 850b373d42
9 changed files with 97 additions and 252 deletions

View File

@ -9,6 +9,8 @@ from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pathlib import Path from pathlib import Path
from routers import audio
app = FastAPI(title="TalkEdit Dev Backend", version="0.0.1") app = FastAPI(title="TalkEdit Dev Backend", version="0.0.1")
app.add_middleware( app.add_middleware(
@ -34,6 +36,8 @@ MIME_MAP = {
} }
app.include_router(audio.router)
@app.get("/health") @app.get("/health")
async def health(): async def health():
return {"status": "ok"} return {"status": "ok"}

View File

@ -17,7 +17,6 @@ import HelpContent from './components/HelpContent';
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'; import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts';
import { useLicenseStore } from './store/licenseStore'; import { useLicenseStore } from './store/licenseStore';
import { import {
Film,
FolderOpen, FolderOpen,
Settings, Settings,
Sparkles, Sparkles,
@ -136,9 +135,6 @@ export default function App() {
const [speedMode, setSpeedMode] = useState(false); const [speedMode, setSpeedMode] = useState(false);
const [speedModeValue, setSpeedModeValue] = useState(1.25); const [speedModeValue, setSpeedModeValue] = useState(1.25);
const [showReprocessConfirm, setShowReprocessConfirm] = useState(false); const [showReprocessConfirm, setShowReprocessConfirm] = useState(false);
const [showWelcomeOverlay, setShowWelcomeOverlay] = useState(() => {
return localStorage.getItem('talkedit:welcomeDismissed') !== 'true';
});
const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false); const [showUnsavedPrompt, setShowUnsavedPrompt] = useState(false);
const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null); const [pendingProceedAction, setPendingProceedAction] = useState<(() => Promise<void>) | null>(null);
const [lastSavedSignature, setLastSavedSignature] = useState<string | null>(null); const [lastSavedSignature, setLastSavedSignature] = useState<string | null>(null);
@ -582,164 +578,6 @@ export default function App() {
} }
}; };
if (!videoPath) {
return (
<div className="h-screen flex flex-col bg-editor-bg relative overflow-hidden">
{/* Background pattern */}
<div className="absolute inset-0 opacity-[0.03] pointer-events-none" style={{
backgroundImage: `radial-gradient(circle at 25% 25%, #6366f1 1px, transparent 1px),
radial-gradient(circle at 75% 75%, #818cf8 1px, transparent 1px)`,
backgroundSize: '60px 60px, 80px 80px',
}} />
{/* Animated audio waveform background — left and right sides filling to center */}
<div className="absolute inset-y-0 left-0 w-[30%] flex items-center justify-end gap-[3px] pr-16 pointer-events-none opacity-[0.09]">
{[
[60, 1.3, 0.0], [100, 1.0, 0.12], [40, 1.6, 0.24], [120, 0.9, 0.08],
[80, 1.2, 0.2], [30, 1.8, 0.04], [110, 1.1, 0.28], [50, 1.5, 0.16],
[70, 1.4, 0.08], [140, 0.85, 0.24], [60, 1.3, 0.12], [90, 1.2, 0.0],
[130, 0.95, 0.2], [45, 1.7, 0.28], [80, 1.4, 0.04], [55, 1.1, 0.16],
[100, 1.3, 0.24], [35, 1.6, 0.12], [120, 0.9, 0.0], [65, 1.5, 0.2],
[85, 1.2, 0.08], [150, 0.8, 0.28], [40, 1.9, 0.16], [95, 1.1, 0.04],
[75, 1.4, 0.24], [25, 2.0, 0.12], [105, 1.05, 0.2], [155, 0.82, 0.08],
[50, 1.55, 0.28], [115, 0.92, 0.16], [70, 1.35, 0.04], [135, 0.88, 0.24],
[90, 1.15, 0.12], [42, 1.75, 0.0], [125, 0.98, 0.2], [58, 1.45, 0.08],
].map(([peak, dur, delay], i) => (
<div
key={`left-${i}`}
className="welcome-audio-bar"
style={{
'--bar-peak': `${peak}px`,
'--bar-duration': `${dur}s`,
'--bar-delay': `${delay}s`,
} as React.CSSProperties}
/>
))}
</div>
<div className="absolute inset-y-0 right-0 w-[30%] flex items-center justify-start gap-[3px] pl-16 pointer-events-none opacity-[0.09]">
{[
[100, 1.0, 0.0], [60, 1.3, 0.08], [130, 0.9, 0.16], [40, 1.6, 0.04],
[80, 1.2, 0.12], [150, 0.85, 0.24], [50, 1.5, 0.2], [110, 1.1, 0.28],
[70, 1.4, 0.08], [30, 1.8, 0.16], [140, 0.9, 0.0], [90, 1.2, 0.24],
[60, 1.3, 0.12], [120, 0.95, 0.04], [85, 1.4, 0.2], [45, 1.7, 0.28],
[160, 0.8, 0.08], [55, 1.5, 0.24], [100, 1.1, 0.0], [75, 1.3, 0.16],
[35, 1.9, 0.2], [115, 1.0, 0.12], [65, 1.6, 0.28], [140, 0.88, 0.04],
[95, 1.25, 0.24], [25, 1.85, 0.08], [125, 0.93, 0.16], [155, 0.78, 0.28],
[48, 1.65, 0.12], [82, 1.32, 0.2], [108, 1.08, 0.04], [72, 1.42, 0.24],
[135, 0.9, 0.16], [38, 1.78, 0.0], [62, 1.48, 0.08], [118, 1.02, 0.28],
].map(([peak, dur, delay], i) => (
<div
key={`right-${i}`}
className="welcome-audio-bar"
style={{
'--bar-peak': `${peak}px`,
'--bar-duration': `${dur}s`,
'--bar-delay': `${delay}s`,
} as React.CSSProperties}
/>
))}
</div>
<div className="absolute top-1/4 -left-32 w-96 h-96 bg-editor-accent/5 rounded-full blur-3xl pointer-events-none" />
<div className="absolute bottom-1/4 -right-32 w-96 h-96 bg-blue-500/5 rounded-full blur-3xl pointer-events-none" />
<div className="flex-1 flex flex-col items-center justify-center gap-6 px-6 relative z-10">
{/* App icon */}
<div className="w-36 h-36 rounded-[20px] bg-editor-accent/10 border border-editor-accent/20 flex items-center justify-center shadow-lg shadow-editor-accent/10">
<svg width="96" height="96" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 10h12a6 6 0 0 1 0 12H8l-2 4V10Z" stroke="#818cf8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity="0.7"/>
<path d="M6 10h12a6 6 0 0 1 0 12H8l-2 4V10Z" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 14v4M13 13v6M16 14v4" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M22 16h6M22 19h4M22 22h5" stroke="#818cf8" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.6"/>
</svg>
</div>
<div className="flex flex-col items-center gap-2">
<h1 className="text-3xl font-bold tracking-tight text-editor-text">
Talk<span className="text-editor-accent">Edit</span>
</h1>
<p className="text-editor-text-muted text-sm max-w-sm text-center leading-relaxed">
The offline video editor that doesn't slow down on long files.
</p>
</div>
{/* Action row — button + model selector side by side */}
<div className="flex items-center gap-3 mt-2">
<button
onClick={handleOpenFile}
className="flex items-center gap-2 px-8 py-3 bg-editor-accent hover:bg-editor-accent-hover rounded-xl text-white font-medium transition-all hover:scale-[1.02] active:scale-[0.98]"
>
<FolderOpen className="w-5 h-5" />
Process File
</button>
<div className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-editor-surface/50 border border-editor-border/50">
<span className="text-xs text-editor-text-muted whitespace-nowrap">Transcription model:</span>
<select
value={whisperModel}
onChange={(e) => setWhisperModel(e.target.value)}
className="bg-transparent text-xs text-editor-text focus:outline-none [color-scheme:dark]"
title="Choose Whisper model for transcription accuracy"
>
<optgroup label="Multilingual">
<option value="tiny">tiny (75 MB · fastest)</option>
<option value="base">base (140 MB · fast)</option>
<option value="small">small (460 MB · balanced)</option>
<option value="medium">medium (1.5 GB · better)</option>
<option value="large-v3-turbo">large-v3-turbo (1.6 GB ★)</option>
<option value="large-v3">large-v3 (2.9 GB · best)</option>
</optgroup>
<optgroup label="English-only (faster)">
<option value="tiny.en">tiny.en (75 MB)</option>
<option value="base.en">base.en (140 MB)</option>
<option value="small.en">small.en (460 MB)</option>
<option value="medium.en">medium.en (1.5 GB)</option>
<option value="distil-small.en">distil-small.en (190 MB ★)</option>
<option value="distil-medium.en">distil-medium.en (750 MB ★)</option>
</optgroup>
</select>
</div>
</div>
<button
onClick={handleLoadProject}
className="flex items-center gap-2 px-4 py-2 text-sm text-editor-text-muted hover:text-editor-text hover:bg-editor-surface rounded-lg transition-colors"
>
<FileInput className="w-4 h-4" />
Load Project (.aive)
</button>
</div>
{licenseStatus?.tag === 'Trial' && (
<div className="h-9 flex items-center justify-center gap-2 px-4 bg-editor-accent/10 border-t border-editor-accent/20 shrink-0 relative z-10">
<Clock className="w-3.5 h-3.5 text-editor-accent shrink-0" />
<span className="text-xs text-editor-accent">
Free trial: {licenseStatus.days_remaining} day{licenseStatus.days_remaining !== 1 ? 's' : ''} remaining
</span>
<button
onClick={() => setShowLicenseDialog(true)}
className="text-xs text-editor-accent underline font-medium hover:text-editor-accent-hover ml-2"
>
Activate license
</button>
</div>
)}
{licenseStatus?.tag === 'Expired' && (
<div className="h-9 flex items-center justify-center gap-2 px-4 bg-red-500/15 border-t border-red-500/30 shrink-0 relative z-10">
<AlertTriangle className="w-3.5 h-3.5 text-red-400 shrink-0" />
<span className="text-xs text-red-300">Trial expired</span>
<button
onClick={() => setShowLicenseDialog(true)}
className="text-xs text-red-300 underline font-medium hover:text-red-200 ml-2"
>
Activate license
</button>
</div>
)}
</div>
);
}
// Health check timer // Health check timer
useEffect(() => { useEffect(() => {
const checkHealth = async () => { const checkHealth = async () => {
@ -919,7 +757,36 @@ export default function App() {
<div ref={splitRef} className="flex-1 flex min-h-0" style={{ position: 'relative' }}> <div ref={splitRef} className="flex-1 flex min-h-0" style={{ position: 'relative' }}>
{/* Video player */} {/* Video player */}
<div className="p-3 flex items-center justify-center bg-black/20 overflow-hidden" style={{ width: `${splitRatio * 100}%`, minWidth: 0 }}> <div className="p-3 flex items-center justify-center bg-black/20 overflow-hidden" style={{ width: `${splitRatio * 100}%`, minWidth: 0 }}>
<VideoPlayer /> {videoPath ? (
<VideoPlayer />
) : (
<div className="flex flex-col items-center gap-4">
<div className="w-20 h-20 rounded-[18px] bg-editor-accent/10 border border-editor-accent/20 flex items-center justify-center shadow-lg shadow-editor-accent/5">
<svg width="48" height="48" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 10h12a6 6 0 0 1 0 12H8l-2 4V10Z" stroke="#818cf8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" opacity="0.7"/>
<path d="M6 10h12a6 6 0 0 1 0 12H8l-2 4V10Z" stroke="#6366f1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10 14v4M13 13v6M16 14v4" stroke="#6366f1" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M22 16h6M22 19h4M22 22h5" stroke="#818cf8" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" opacity="0.6"/>
</svg>
</div>
<div className="flex flex-col items-center gap-3">
<button
onClick={handleOpenFile}
className="flex items-center gap-2 px-6 py-2.5 bg-editor-accent hover:bg-editor-accent-hover rounded-lg text-white text-sm font-medium transition-all"
>
<FolderOpen className="w-4 h-4" />
Open File
</button>
<button
onClick={handleLoadProject}
className="flex items-center gap-2 px-4 py-1.5 text-xs text-editor-text-muted hover:text-editor-text hover:bg-editor-surface rounded-md transition-colors"
>
<FileInput className="w-3.5 h-3.5" />
Load Project
</button>
</div>
</div>
)}
</div> </div>
{/* Draggable divider */} {/* Draggable divider */}
@ -1044,53 +911,6 @@ export default function App() {
<LicenseDialog /> <LicenseDialog />
{showWelcomeOverlay && (
<div className="fixed inset-0 z-[85] flex items-center justify-center bg-black/70 px-4">
<div className="w-full max-w-sm rounded-xl border border-editor-border bg-editor-bg p-6 space-y-5 shadow-2xl">
<div className="flex flex-col items-center gap-3 text-center">
<Film className="w-10 h-10 text-editor-accent opacity-80" />
<h2 className="text-lg font-semibold">Welcome to TalkEdit</h2>
<p className="text-xs text-editor-text-muted leading-relaxed">
The offline video editor for long-form content. No account. No subscription. Your data never leaves your machine.
</p>
</div>
<div className="space-y-3">
<div className="flex items-start gap-3 p-2 rounded bg-editor-surface">
<span className="w-5 h-5 rounded-full bg-editor-accent/20 text-editor-accent text-[10px] font-semibold flex items-center justify-center shrink-0 mt-0.5">1</span>
<span className="text-xs text-editor-text-muted leading-relaxed">
<strong className="text-editor-text">Open a video</strong> — TalkEdit transcribes it into a word-level transcript.
</span>
</div>
<div className="flex items-start gap-3 p-2 rounded bg-editor-surface">
<span className="w-5 h-5 rounded-full bg-editor-accent/20 text-editor-accent text-[10px] font-semibold flex items-center justify-center shrink-0 mt-0.5">2</span>
<span className="text-xs text-editor-text-muted leading-relaxed">
<strong className="text-editor-text">Edit by selecting words</strong> — deleting words cuts the matching video. Like editing a doc.
</span>
</div>
<div className="flex items-start gap-3 p-2 rounded bg-editor-surface">
<span className="w-5 h-5 rounded-full bg-editor-accent/20 text-editor-accent text-[10px] font-semibold flex items-center justify-center shrink-0 mt-0.5">3</span>
<span className="text-xs text-editor-text-muted leading-relaxed">
<strong className="text-editor-text">Export your final cut</strong> — with captions, background music, AI cleanup, and more.
</span>
</div>
</div>
<div className="flex flex-col items-center gap-2">
<button
onClick={() => { localStorage.setItem('talkedit:welcomeDismissed', 'true'); setShowWelcomeOverlay(false); }}
className="w-full px-6 py-2.5 bg-editor-accent hover:bg-editor-accent-hover rounded-lg text-sm font-medium transition-colors"
>
Got it
</button>
<p className="text-[10px] text-editor-text-muted">
Press <kbd className="px-1 py-0.5 text-[10px] font-mono bg-editor-surface border border-editor-border rounded">?</kbd> anytime to see shortcuts, or click <strong className="text-editor-text">Help</strong> in the toolbar.
</p>
</div>
</div>
</div>
)}
{showReprocessConfirm && ( {showReprocessConfirm && (
<div <div
className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 px-4" className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 px-4"

View File

@ -237,6 +237,7 @@ export default function WaveformTimeline({
const headCanvasRef = useRef<HTMLCanvasElement>(null); const headCanvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const [audioError, setAudioError] = useState<string | null>(null); const [audioError, setAudioError] = useState<string | null>(null);
const [waveformReady, setWaveformReady] = useState(false);
const videoUrl = useEditorStore((s) => s.videoUrl); const videoUrl = useEditorStore((s) => s.videoUrl);
const videoPath = useEditorStore((s) => s.videoPath); const videoPath = useEditorStore((s) => s.videoPath);
@ -349,6 +350,7 @@ export default function WaveformTimeline({
if (cancelled) return; if (cancelled) return;
waveformDataRef.current = waveformData; waveformDataRef.current = waveformData;
drawStaticWaveformRef.current(); drawStaticWaveformRef.current();
setWaveformReady(true);
} catch (err) { } catch (err) {
if (cancelled || (err instanceof DOMException && err.name === 'AbortError')) { if (cancelled || (err instanceof DOMException && err.name === 'AbortError')) {
console.log('[WaveformTimeline] req=', requestId, 'aborted/cancelled'); console.log('[WaveformTimeline] req=', requestId, 'aborted/cancelled');
@ -1328,7 +1330,7 @@ export default function WaveformTimeline({
Retry Retry
</button> </button>
</div> </div>
) : !waveformDataRef.current ? ( ) : !waveformReady ? (
<div className="flex-1 flex items-center justify-center"> <div className="flex-1 flex items-center justify-center">
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-editor-border border-t-indigo-400" /> <div className="animate-spin rounded-full h-8 w-8 border-2 border-editor-border border-t-indigo-400" />

View File

@ -19,15 +19,6 @@
transform-origin: bottom; transform-origin: bottom;
} }
.welcome-audio-bar {
width: 4px;
border-radius: 2px;
background: #6366f1;
transform-origin: bottom;
animation: audioBounce var(--bar-duration) ease-in-out infinite;
animation-delay: var(--bar-delay);
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;

View File

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/AppendClipPanel.tsx","./src/components/BackgroundMusicPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/LicenseDialog.tsx","./src/components/MarkersPanel.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/VolumePanel.tsx","./src/components/WaveformTimeline.tsx","./src/components/ZoneEditor.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/keybindings.ts","./src/lib/tauri-bridge.ts","./src/lib/thumbnails.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/store/licenseStore.ts","./src/types/project.ts"],"version":"5.9.3"} {"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/AppendClipPanel.tsx","./src/components/BackgroundMusicPanel.tsx","./src/components/DevPanel.tsx","./src/components/ErrorBoundary.tsx","./src/components/ExportDialog.tsx","./src/components/HelpContent.tsx","./src/components/LicenseDialog.tsx","./src/components/MarkersPanel.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/WaveformTimeline.tsx","./src/components/ZoneEditor.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/assert.test.ts","./src/lib/assert.ts","./src/lib/dev-logger.ts","./src/lib/keybindings.ts","./src/lib/tauri-bridge.ts","./src/lib/thumbnails.ts","./src/store/aiStore.test.ts","./src/store/aiStore.ts","./src/store/editorStore.test.ts","./src/store/editorStore.ts","./src/store/licenseStore.test.ts","./src/store/licenseStore.ts","./src/types/project.ts"],"version":"5.9.3"}

73
plan.md
View File

@ -65,50 +65,69 @@ TalkEdit's defensible position: **works on hour+ files without degrading**, full
--- ---
## Phase 2: Standout features (post-beta) ## Phase 2: Beta Launch (🚧 next — 24 weeks)
### Long-form content **Goal:** Get working builds into real podcasters' hands. Validate the core promise (long-form, offline) before investing in edge-case features.
- [x] Chapter-based navigation — markers auto-sorted, click to jump (partially done)
- [x] Per-segment re-transcription (done)
- [x] Append multiple clips into one timeline (done)
- [ ] Project stitching — load multiple `.aive` projects, combine into one export
- [ ] Smart chunking for transcription — for files >2hr
### Export ### Must-have for beta
- [x] YouTube chapters from markers (done)
- [x] Export transcript formats: SRT, VTT, TXT (done)
- [ ] Batch export — multiple projects/cuts in sequence
### AI features - [ ] **Smart chunking for transcription** — files >2hr. Without this the niche promise is unproven. Breaks transcription into overlapping chunks, reassembles with correct timestamps.
- [x] AI Smart Clean — filler removal + silence trim + normalize (done) - [ ] **Hardware detection & model selection** — detect CUDA/ROCm/MPS at startup; expose model backend choice in Settings so beta users can configure their system.
- [x] AI sentence rephrase (done) - [ ] **GitHub v1.0.0 release** — tag, binary builds (AppImage + .deb), release notes.
- [x] AI clip suggestions for social media (done)
- [ ] Smart Shorts finder — scan transcript for 1090s segments ### Sales & distribution
- [ ] AI auto-chapters — topic detection from transcript - [ ] **Stripe integration** — payment processing for one-time purchases (Pro $39, Business $79). License key generation + email delivery on payment success.
- [ ] AI show notes — title, description, key moments - [ ] **Landing page + download site** — simple site with: feature overview, pricing tiers, download links (AppImage/.deb), license activation flow. No auth system needed — Stripe handles payments, license keys unlock the app.
- [ ] AI dead-air finder — content-based silence detection
### Beta program
- [ ] **Free licenses to 20 podcasters** — in exchange for feedback + permission to quote. Target: r/podcasting regulars, small-to-medium shows (30min2hr episodes).
- [ ] **Bug/feedback pipeline** — GitHub Issues template for beta testers. Weekly triage.
- [ ] **Messaging for beta landing page:**
1. "The offline video editor that doesn't slow down on long files"
2. "No subscription. One price, owned forever."
3. "AI-powered editing — bring your own API key (Ollama, OpenAI, Claude)"
---
## Phase 3: Post-Beta Enhancements (user-driven priority)
**Goal:** Build what beta testers actually ask for. Deferred items below are ordered by likely demand, not engineering convenience.
### Bundled local LLM ### Bundled local LLM
- [ ] Integrate llama.cpp Rust bindings - [ ] Integrate llama.cpp Rust bindings
- [ ] Auto-download Qwen3 on first AI use (4B: 2.5GB / 1.7B: 1GB) - [ ] Auto-download Qwen3 on first AI use (4B: 2.5GB / 1.7B: 1GB)
- [ ] Hardware detection at runtime, model selection in Settings - [ ] Hardware detection at runtime, model selection in Settings
### Long-form content
- [ ] Project stitching — load multiple `.aive` projects, combine into one export
### Export
- [ ] Batch export — multiple projects/cuts in sequence
### AI features
- [ ] Smart Shorts finder — scan transcript for 1090s segments
- [ ] AI auto-chapters — topic detection from transcript
- [ ] AI show notes — title, description, key moments
- [ ] AI dead-air finder — content-based silence detection
--- ---
## Phase 3: Marketing & launch (post-beta) ## Phase 4: Public Launch
### Messaging pillars **Goal:** Convert beta momentum + testimonials into a public release.
### Messaging pillars (updated)
1. "The offline video editor that doesn't slow down on long files" 1. "The offline video editor that doesn't slow down on long files"
2. "No subscription. One price, owned forever." 2. "No subscription. One price, owned forever."
3. "Zero-setup AI" — bundled Qwen3, no API keys 3. "Zero-setup AI" — bundled Qwen3, no API keys *(activate when Phase 3 ships)*
4. "Your podcast → 10 TikToks in one click" — Smart Shorts finder 4. "Your podcast → 10 TikToks in one click" — Smart Shorts finder *(activate when Phase 3 ships)*
### Channels ### Channels
- [ ] r/podcasting, r/VideoEditing, r/selfhosted - [ ] r/podcasting, r/VideoEditing, r/selfhosted — anchored by beta tester testimonials
- [ ] Product Hunt, Hacker News "Show HN" - [ ] Product Hunt, Hacker News "Show HN"
- [ ] YouTube demo (3-5 min walkthrough) - [ ] YouTube demo (3-5 min walkthrough) — feature the beta tester stories
- [ ] Free licenses to 20 podcasters for testimonials - [ ] Pricing goes live publicly
- [ ] GitHub v1.0.0 release with binaries
### Pricing ### Pricing
- 7-day free trial (no CC, no account) - 7-day free trial (no CC, no account)

View File

@ -11,8 +11,8 @@
"dialog:allow-open", "dialog:allow-open",
"dialog:allow-save", "dialog:allow-save",
"fs:default", "fs:default",
{ "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$HOME/**" }] }, { "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$HOME/**" }, { "path": "**" }] },
{ "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$HOME/**" }] }, { "identifier": "fs:allow-write-text-file", "allow": [{ "path": "$HOME/**" }, { "path": "**" }] },
"fs:allow-app-read-recursive", "fs:allow-app-read-recursive",
"fs:allow-app-write-recursive" "fs:allow-app-write-recursive"
] ]

View File

@ -6,7 +6,7 @@
"build": { "build": {
"frontendDist": "../frontend/dist", "frontendDist": "../frontend/dist",
"devUrl": "http://localhost:5173", "devUrl": "http://localhost:5173",
"beforeDevCommand": "cd frontend && npm run dev", "beforeDevCommand": "lsof -ti:5173 | xargs kill -9 2>/dev/null; cd frontend && npm run dev",
"beforeBuildCommand": "cd frontend && npm run build" "beforeBuildCommand": "cd frontend && npm run build"
}, },
"app": { "app": {

View File

@ -45,7 +45,16 @@ def main():
device = "cpu" device = "cpu"
compute_type = "int8" compute_type = "int8"
model = WhisperModel(model_name, device=device, compute_type=compute_type) try:
model = WhisperModel(model_name, device=device, compute_type=compute_type)
except RuntimeError as e:
if "out of memory" in str(e).lower() and device == "cuda":
print(f"CUDA OOM, falling back to CPU (int8)", file=sys.stderr)
device = "cpu"
compute_type = "int8"
model = WhisperModel(model_name, device=device, compute_type=compute_type)
else:
raise
# Transcribe with progress reporting # Transcribe with progress reporting
print(f"Starting transcription of {wav_path} with model {model_name}", file=sys.stderr) print(f"Starting transcription of {wav_path} with model {model_name}", file=sys.stderr)