removed home
This commit is contained in:
@ -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"}
|
||||||
|
|||||||
@ -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 }}>
|
||||||
|
{videoPath ? (
|
||||||
<VideoPlayer />
|
<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"
|
||||||
|
|||||||
@ -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" />
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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
73
plan.md
@ -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 — 2–4 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 10–90s 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 (30min–2hr 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 10–90s 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)
|
||||||
|
|||||||
@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -45,7 +45,16 @@ def main():
|
|||||||
device = "cpu"
|
device = "cpu"
|
||||||
compute_type = "int8"
|
compute_type = "int8"
|
||||||
|
|
||||||
|
try:
|
||||||
model = WhisperModel(model_name, device=device, compute_type=compute_type)
|
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user