removed electron

This commit is contained in:
2026-04-07 23:08:27 -06:00
parent d80ff847d8
commit e25f8a9b63
20 changed files with 96 additions and 363 deletions

3
.gitignore vendored
View File

@ -21,9 +21,6 @@ htmlcov/
.idea/
.cursor/
# Submodules (can be cloned separately if needed)
CutScript/
# OS files
.env
.env.local

View File

@ -1,4 +1,4 @@
# CutScript
# TalkEdit
An open-source, local-first, Descript-like text-based audio and video editor powered by AI. Edit audio/video by editing text — delete a word from the transcript and it's cut from the audio/video.
@ -7,7 +7,7 @@ An open-source, local-first, Descript-like text-based audio and video editor pow
## Architecture
- **Electron + React** desktop app with Tailwind CSS
- **Tauri + React** desktop app with Tailwind CSS
- **FastAPI** Python backend (spawned as child process)
- **WhisperX** for word-level transcription with alignment
- **FFmpeg** for video processing (stream-copy and re-encode)
@ -25,7 +25,7 @@ An open-source, local-first, Descript-like text-based audio and video editor pow
### Install
```bash
# Root dependencies (Electron, concurrently)
# Root scripts
npm install
# Frontend dependencies (React, Tailwind, Zustand)
@ -37,32 +37,46 @@ cd backend && pip install -r requirements.txt && cd ..
### Run (Development)
Set a custom backend port once (optional):
```bash
# Start all three (backend + frontend + electron)
export BACKEND_PORT=8000
```
If you run frontend separately, you can also set:
```bash
export VITE_BACKEND_PORT=$BACKEND_PORT
```
```bash
# Start frontend in browser
npm run dev
# Or start the full desktop app (backend + tauri)
npm run dev:tauri
```
Or run them separately:
```bash
# Terminal 1: Backend
cd backend && python -m uvicorn main:app --reload --port 8642
cd backend && python -m uvicorn main:app --reload --port 8000
# Terminal 2: Frontend
cd frontend && npm run dev
# Terminal 3: Electron
npx electron .
# Terminal 3: Tauri app shell
cd frontend && cargo tauri dev
```
## Project Structure
```
cutscript/
├── electron/ # Electron main process
│ ├── main.js # App entry, spawns Python backend
── preload.js # Secure IPC bridge
│ └── python-bridge.js
talkedit/
├── src-tauri/ # Tauri Rust host
│ ├── src/main.rs # App entry and backend orchestration
── tauri.conf.json
├── frontend/ # React + Vite + Tailwind
│ └── src/
│ ├── components/ # VideoPlayer, TranscriptEditor, etc.
@ -97,7 +111,7 @@ cutscript/
| Speaker diarization | Done |
| Virtualized transcript (react-virtuoso) | Done |
| Encrypted API key storage | Done |
| Project save/load (.cutscript) | Done |
| Project save/load (.aive) | Done |
| AI background removal | Planned |
## Keyboard Shortcuts

View File

@ -118,7 +118,7 @@ async def export_video(req: ExportRequest):
# Audio enhancement: clean, then mux back into the exported video
if req.enhanceAudio:
try:
tmp_dir = tempfile.mkdtemp(prefix="cutscript_audio_")
tmp_dir = tempfile.mkdtemp(prefix="talkedit_audio_")
cleaned_audio = os.path.join(tmp_dir, "cleaned.wav")
clean_audio(output, cleaned_audio)

19
close
View File

@ -1,7 +1,8 @@
#!/bin/bash
# Close TalkEdit and/or CutScript processes (Tauri dev, Electron, and Python backends)
# Close TalkEdit processes (Tauri dev and Python backend)
KILLED_ANY=0
BACKEND_PORT="${BACKEND_PORT:-8000}"
kill_port() {
local port=$1
@ -27,23 +28,17 @@ kill_pattern() {
fi
}
# --- TalkEdit (Tauri, port 8000) ---
kill_port 8000 "TalkEdit"
# --- TalkEdit (Tauri) ---
kill_port "$BACKEND_PORT" "TalkEdit"
kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)"
# Vite dev server for TalkEdit (port 5173)
kill_pattern "vite.*5173\|rsbuild.*5173" "TalkEdit frontend dev server"
# --- CutScript (Electron, port 8642) ---
kill_port 8642 "CutScript"
kill_pattern "electron.*CutScript\|CutScript.*electron" "CutScript (Electron)"
kill_pattern "vite.*CutScript\|CutScript.*vite" "CutScript frontend dev server"
# --- Orphaned uvicorn workers for either app ---
kill_pattern "uvicorn.*main:app.*--port 800[012]" "leftover uvicorn workers (TalkEdit)"
kill_pattern "uvicorn.*main:app.*--port 864" "leftover uvicorn workers (CutScript)"
# --- Orphaned uvicorn workers for TalkEdit ---
kill_pattern "uvicorn.*main:app.*--port ${BACKEND_PORT}" "leftover uvicorn workers (TalkEdit)"
if [[ $KILLED_ANY -eq 0 ]]; then
echo "Nothing to close — no TalkEdit or CutScript processes found."
echo "Nothing to close — no TalkEdit processes found."
else
echo "Done."
fi

View File

@ -1,131 +0,0 @@
const { app, BrowserWindow, ipcMain, dialog, safeStorage } = require('electron');
const path = require('path');
const { PythonBackend } = require('./python-bridge');
let mainWindow = null;
let pythonBackend = null;
const isDev = !app.isPackaged;
const BACKEND_PORT = 8642;
function createWindow() {
mainWindow = new BrowserWindow({
width: 1400,
height: 900,
minWidth: 1024,
minHeight: 700,
title: 'CutScript',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
webSecurity: isDev ? false : true,
},
show: false,
});
if (isDev) {
mainWindow.loadURL('http://localhost:5173');
mainWindow.webContents.openDevTools();
} else {
mainWindow.loadFile(path.join(__dirname, '..', 'frontend', 'dist', 'index.html'));
}
mainWindow.once('ready-to-show', () => {
mainWindow.show();
});
mainWindow.on('closed', () => {
mainWindow = null;
});
}
app.whenReady().then(async () => {
pythonBackend = new PythonBackend(BACKEND_PORT, isDev);
await pythonBackend.start();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
if (pythonBackend) {
pythonBackend.stop();
}
});
// IPC Handlers
ipcMain.handle('dialog:openFile', async (_event, options) => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'Video Files', extensions: ['mp4', 'avi', 'mov', 'mkv', 'webm'] },
{ name: 'Audio Files', extensions: ['m4a', 'wav', 'mp3', 'flac'] },
{ name: 'All Files', extensions: ['*'] },
],
...options,
});
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('dialog:saveFile', async (_event, options) => {
const result = await dialog.showSaveDialog(mainWindow, {
filters: [
{ name: 'Video Files', extensions: ['mp4', 'mov', 'webm'] },
{ name: 'Project Files', extensions: ['aive'] },
],
...options,
});
return result.canceled ? null : result.filePath;
});
ipcMain.handle('dialog:openProject', async () => {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openFile'],
filters: [
{ name: 'AI Video Editor Project', extensions: ['aive'] },
],
});
return result.canceled ? null : result.filePaths[0];
});
ipcMain.handle('safe-storage:encrypt', (_event, data) => {
if (safeStorage.isEncryptionAvailable()) {
return safeStorage.encryptString(data).toString('base64');
}
return data;
});
ipcMain.handle('safe-storage:decrypt', (_event, encrypted) => {
if (safeStorage.isEncryptionAvailable()) {
return safeStorage.decryptString(Buffer.from(encrypted, 'base64'));
}
return encrypted;
});
ipcMain.handle('get-backend-url', () => {
return `http://localhost:${BACKEND_PORT}`;
});
ipcMain.handle('fs:readFile', async (_event, filePath) => {
const fs = require('fs');
return fs.readFileSync(filePath, 'utf-8');
});
ipcMain.handle('fs:writeFile', async (_event, filePath, content) => {
const fs = require('fs');
fs.writeFileSync(filePath, content, 'utf-8');
return true;
});

View File

@ -1,12 +0,0 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
openFile: (options) => ipcRenderer.invoke('dialog:openFile', options),
saveFile: (options) => ipcRenderer.invoke('dialog:saveFile', options),
openProject: () => ipcRenderer.invoke('dialog:openProject'),
getBackendUrl: () => ipcRenderer.invoke('get-backend-url'),
encryptString: (data) => ipcRenderer.invoke('safe-storage:encrypt', data),
decryptString: (encrypted) => ipcRenderer.invoke('safe-storage:decrypt', encrypted),
readFile: (path) => ipcRenderer.invoke('fs:readFile', path),
writeFile: (path, content) => ipcRenderer.invoke('fs:writeFile', path, content),
});

View File

@ -1,105 +0,0 @@
const { spawn } = require('child_process');
const path = require('path');
const http = require('http');
class PythonBackend {
constructor(port, isDev) {
this.port = port;
this.isDev = isDev;
this.process = null;
}
async start() {
// In dev mode, check if a backend is already running (e.g. from `npm run dev:backend`)
// If so, reuse it instead of spawning a duplicate.
if (this.isDev) {
const alreadyRunning = await this._isPortOpen(2000);
if (alreadyRunning) {
console.log(`[backend] Dev backend already running on port ${this.port} — reusing it.`);
return;
}
}
const backendDir = this.isDev
? path.join(__dirname, '..', 'backend')
: path.join(process.resourcesPath, 'backend');
const pythonCmd = process.platform === 'win32' ? 'python' : '/home/dillon/.pyenv/versions/3.11.15/bin/python';
this.process = spawn(pythonCmd, [
'-m', 'uvicorn', 'main:app',
'--host', '127.0.0.1',
'--port', String(this.port),
], {
cwd: backendDir,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env, PYTHONUNBUFFERED: '1' },
});
this.process.stdout.on('data', (data) => {
console.log(`[backend] ${data.toString().trim()}`);
});
this.process.stderr.on('data', (data) => {
console.error(`[backend] ${data.toString().trim()}`);
});
this.process.on('error', (err) => {
console.error('[backend] Failed to start Python backend:', err.message);
});
this.process.on('exit', (code) => {
console.log(`[backend] Process exited with code ${code}`);
this.process = null;
});
await this._waitForReady(30000);
console.log(`[backend] Ready on port ${this.port}`);
}
_isPortOpen(timeoutMs) {
return new Promise((resolve) => {
const req = http.get(`http://127.0.0.1:${this.port}/health`, (res) => {
resolve(res.statusCode === 200);
});
req.on('error', () => resolve(false));
req.setTimeout(timeoutMs, () => { req.destroy(); resolve(false); });
req.end();
});
}
stop() {
if (this.process) {
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', String(this.process.pid), '/f', '/t']);
} else {
this.process.kill('SIGTERM');
}
this.process = null;
}
}
_waitForReady(timeoutMs) {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const check = () => {
if (Date.now() - startTime > timeoutMs) {
reject(new Error('Backend startup timed out'));
return;
}
const req = http.get(`http://127.0.0.1:${this.port}/health`, (res) => {
if (res.statusCode === 200) {
resolve();
} else {
setTimeout(check, 500);
}
});
req.on('error', () => setTimeout(check, 500));
req.end();
};
setTimeout(check, 1000);
});
}
}
module.exports = { PythonBackend };

View File

@ -22,7 +22,7 @@ import {
VolumeX,
} from 'lucide-react';
const IS_ELECTRON = !!window.electronAPI;
const IS_DESKTOP = !!window.desktopAPI;
type Panel = 'ai' | 'settings' | 'export' | 'silence' | null;
@ -66,8 +66,8 @@ export default function App() {
}, []);
useEffect(() => {
if (IS_ELECTRON) {
window.electronAPI!.getBackendUrl().then(setBackendUrl);
if (IS_DESKTOP) {
window.desktopAPI!.getBackendUrl().then(setBackendUrl);
}
// In Tauri on Linux/WebKit2GTK the ipc:// custom protocol is blocked by
// WebKit internals; postMessage fallback works but logs noisy warnings.
@ -75,11 +75,11 @@ export default function App() {
}, [setBackendUrl]);
const handleLoadProject = async () => {
if (!IS_ELECTRON) return;
if (!IS_DESKTOP) return;
try {
const projectPath = await window.electronAPI!.openProject();
const projectPath = await window.desktopAPI!.openProject();
if (!projectPath) return;
const content = await window.electronAPI!.readFile(projectPath);
const content = await window.desktopAPI!.readFile(projectPath);
const data = JSON.parse(content);
useEditorStore.getState().loadProject(data);
} catch (err) {
@ -89,13 +89,13 @@ export default function App() {
};
const handleSaveProject = async () => {
if (!IS_ELECTRON) return;
if (!IS_DESKTOP) return;
try {
const savePath = await window.electronAPI!.saveProject();
const savePath = await window.desktopAPI!.saveProject();
if (!savePath) return;
const data = useEditorStore.getState().saveProject();
const path = savePath.endsWith('.aive') ? savePath : `${savePath}.aive`;
await window.electronAPI!.writeFile(path, JSON.stringify(data, null, 2));
await window.desktopAPI!.writeFile(path, JSON.stringify(data, null, 2));
} catch (err) {
console.error('Failed to save project:', err);
alert(`Failed to save project: ${err}`);
@ -103,8 +103,8 @@ export default function App() {
};
const handleOpenFile = async () => {
if (IS_ELECTRON) {
const path = await window.electronAPI!.openFile();
if (IS_DESKTOP) {
const path = await window.desktopAPI!.openFile();
if (path) {
loadVideo(path);
await transcribeVideo(path);
@ -130,7 +130,7 @@ export default function App() {
const transcribeVideo = async (path: string) => {
setTranscribing(true, 0, 'Checking model...');
try {
if (!window.electronAPI?.transcribe) {
if (!window.desktopAPI?.transcribe) {
throw new Error('Transcription not available');
}
// Step 1: ensure model is downloaded (may take a while on first run)
@ -153,11 +153,11 @@ export default function App() {
};
const modelLabel = MODEL_SIZES[whisperModel] ?? 'unknown size';
setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`);
await window.electronAPI.ensureModel(whisperModel);
await window.desktopAPI.ensureModel(whisperModel);
// Step 2: run transcription
setTranscribing(true, 20, 'Transcribing audio...');
const data = await window.electronAPI.transcribe(path, whisperModel);
const data = await window.desktopAPI.transcribe(path, whisperModel);
setTranscription(data);
} catch (err) {
console.error('Transcription error:', err);
@ -243,7 +243,7 @@ export default function App() {
English-only models are ~10% faster and more accurate for English content.
</p>
{IS_ELECTRON ? (
{IS_DESKTOP ? (
<div className="flex flex-col items-center gap-3">
<button
onClick={handleOpenFile}
@ -313,9 +313,9 @@ export default function App() {
<ToolbarButton
icon={<FolderOpen className="w-4 h-4" />}
label="Open"
onClick={IS_ELECTRON ? handleOpenFile : () => useEditorStore.getState().reset()}
onClick={IS_DESKTOP ? handleOpenFile : () => useEditorStore.getState().reset()}
/>
{IS_ELECTRON && (
{IS_DESKTOP && (
<ToolbarButton
icon={<Save className="w-4 h-4" />}
label="Save"
@ -323,7 +323,7 @@ export default function App() {
disabled={words.length === 0}
/>
)}
{IS_ELECTRON && (
{IS_DESKTOP && (
<ToolbarButton
icon={<FileInput className="w-4 h-4" />}
label="Load"

View File

@ -20,7 +20,7 @@ export default function ExportDialog() {
const handleExport = useCallback(async () => {
if (!videoPath) return;
const outputPath = await window.electronAPI?.saveFile({
const outputPath = await window.desktopAPI?.saveFile({
defaultPath: videoPath.replace(/\.[^.]+$/, '_edited.mp4'),
filters: [
{ name: 'MP4', extensions: ['mp4'] },

View File

@ -167,14 +167,14 @@ async function saveProject() {
modifiedAt: new Date().toISOString(),
};
const outputPath = await window.electronAPI?.saveFile({
const outputPath = await window.desktopAPI?.saveFile({
defaultPath: state.videoPath.replace(/\.[^.]+$/, '.aive'),
filters: [{ name: 'TalkEdit Project', extensions: ['aive'] }],
});
if (outputPath) {
if (window.electronAPI?.writeFile) {
await window.electronAPI.writeFile(outputPath, JSON.stringify(projectData, null, 2));
if (window.desktopAPI?.writeFile) {
await window.desktopAPI.writeFile(outputPath, JSON.stringify(projectData, null, 2));
} else {
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);

View File

@ -1,8 +1,8 @@
/**
* tauri-bridge.ts
*
* Polyfills window.electronAPI with Tauri equivalents so all existing
* call-sites in App.tsx, hooks, and stores continue to work unchanged.
* Exposes window.desktopAPI using Tauri equivalents so UI code can stay
* desktop-runtime agnostic.
*
* Imported once at the top of main.tsx.
*/
@ -25,7 +25,10 @@ const EXPORT_FILTERS = [
{ name: 'Project Files', extensions: ['aive'] },
];
window.electronAPI = {
const BACKEND_PORT = import.meta.env.VITE_BACKEND_PORT || '8000';
const BACKEND_URL = `http://127.0.0.1:${BACKEND_PORT}`;
window.desktopAPI = {
openFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
const result = await open({
multiple: false,
@ -53,8 +56,8 @@ window.electronAPI = {
},
getBackendUrl: (): Promise<string> => {
// Backend URL is fixed; avoid invoke() which triggers ipc:// CSP errors on Linux/WebKit2GTK
return Promise.resolve('http://127.0.0.1:8000');
// Use env-driven backend URL and avoid invoke() to bypass ipc:// noise on Linux/WebKit2GTK.
return Promise.resolve(BACKEND_URL);
},
encryptString: (data: string): Promise<string> => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
// Forward console.error/warn/log to backend in dev mode so we can tail webview.log
import './lib/dev-logger';
// Must be imported before App so window.electronAPI is patched before any component runs.
// Must be imported before App so window.desktopAPI is patched before any component runs.
import './lib/tauri-bridge';
import App from './App';
import './index.css';

View File

@ -30,8 +30,8 @@ async function encryptAndStore(key: string, value: string): Promise<void> {
localStorage.removeItem(ENCRYPTED_KEY_PREFIX + key);
return;
}
if (window.electronAPI) {
const encrypted = await window.electronAPI.encryptString(value);
if (window.desktopAPI) {
const encrypted = await window.desktopAPI.encryptString(value);
localStorage.setItem(ENCRYPTED_KEY_PREFIX + key, encrypted);
} else {
localStorage.setItem(ENCRYPTED_KEY_PREFIX + key, btoa(value));
@ -41,9 +41,9 @@ async function encryptAndStore(key: string, value: string): Promise<void> {
async function loadAndDecrypt(key: string): Promise<string> {
const stored = localStorage.getItem(ENCRYPTED_KEY_PREFIX + key);
if (!stored) return '';
if (window.electronAPI) {
if (window.desktopAPI) {
try {
return await window.electronAPI.decryptString(stored);
return await window.desktopAPI.decryptString(stored);
} catch {
return '';
}

View File

@ -1,6 +1,10 @@
/// <reference types="vite/client" />
interface ElectronAPI {
interface ImportMetaEnv {
readonly VITE_BACKEND_PORT?: string;
}
interface DesktopAPI {
openFile: (options?: Record<string, unknown>) => Promise<string | null>;
saveFile: (options?: Record<string, unknown>) => Promise<string | null>;
openProject: () => Promise<string | null>;
@ -15,5 +19,5 @@ interface ElectronAPI {
}
interface Window {
electronAPI?: ElectronAPI;
desktopAPI?: DesktopAPI;
}

View File

@ -1 +1 @@
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/WaveformTimeline.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.ts","./src/types/project.ts"],"errors":true,"version":"5.9.3"}
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/AIPanel.tsx","./src/components/DevPanel.tsx","./src/components/ExportDialog.tsx","./src/components/SettingsPanel.tsx","./src/components/SilenceTrimmerPanel.tsx","./src/components/TranscriptEditor.tsx","./src/components/VideoPlayer.tsx","./src/components/WaveformTimeline.tsx","./src/hooks/useKeyboardShortcuts.ts","./src/hooks/useVideoSync.ts","./src/lib/dev-logger.ts","./src/lib/tauri-bridge.ts","./src/store/aiStore.ts","./src/store/editorStore.ts","./src/types/project.ts"],"version":"5.9.3"}

View File

@ -1,8 +1,8 @@
Here's a clear, actionable **summary** of what you (as a solo developer using AI tools heavily) should do to build and monetize this product, based on current market demand in 2026.
### What You Should Do (Step-by-Step Plan)
1. **Fork an existing open-source base** (don't start from scratch)
- Best choice: **CutScript** (newest, explicitly built as "offline Descript alternative" with text-based editing) or **Audapolis** (more mature, ~1.8k stars, wordprocessor-like experience for spoken-word video/audio).
1. **Build from the existing TalkEdit base** (don't start from scratch)
- Keep TalkEdit as the primary codebase and borrow ideas from mature open-source editors like **Audapolis** where useful.
- Reason: The hard parts (local Whisper transcription with word-level timestamps, syncing text deletions to video cuts, FFmpeg handling) are already solved. You save 48 weeks and focus on polish.
2. **Migrate/refactor to Tauri 2.0** (Rust backend + React/Vite + Tailwind + shadcn-ui frontend)
@ -48,13 +48,13 @@ That's it. No multi-track timelines, no voice cloning, no collaboration, no fanc
### Why This Will Work
- **Market demand is real**: Creators love text-based editing because it feels revolutionary for dialogue-heavy videos. They want it faster, cheaper, and private/offline. Existing alternatives are either cloud-based with subscriptions or clunky open-source tools.
- **Competition gap**: CutScript and Audapolis prove interest but lack slick UX and the "one magic button" polish. You can own the "delightful local Descript killer" niche.
- **Competition gap**: Existing local editors prove interest but often lack slick UX and the "one magic button" polish. You can own the "delightful local Descript killer" niche.
- **Solo-dev friendly**: Forking + AI code generation makes this realistic without a team.
Once you ship the MVP and get initial users, you can add nice-to-haves (e.g., custom filler lists, better subtitle export, optional cloud boost) based on real feedback.
**Next immediate actions**:
- Clone CutScript or Audapolis today and run it locally to see the current state.
- Continue from TalkEdit and benchmark against Audapolis today to compare current UX quality.
- Set up a new Tauri project and start refactoring the UI/transcript editor.
If you want, I can give you the exact Git commands, first AI prompts for refactoring, folder structure, or even sample code for the "Clean it" button + FFmpeg polish chain.

3
open
View File

@ -3,7 +3,8 @@
cd "$(dirname "$0")"
PROJECT_DIR="$PWD"
BACKEND_PORT=8000
export BACKEND_PORT="${BACKEND_PORT:-8000}"
export VITE_BACKEND_PORT="${VITE_BACKEND_PORT:-$BACKEND_PORT}"
BACKEND_URL="http://127.0.0.1:${BACKEND_PORT}/health"
# Check if backend is already running

View File

@ -3,48 +3,15 @@
"version": "0.1.0",
"private": true,
"description": "TalkEdit — Open-source AI-powered text-based video editor",
"main": "electron/main.js",
"scripts": {
"tauri": "tauri",
"dev": "cd frontend && npm run dev -- --host",
"dev:tauri": "cd backend && python -m uvicorn main:app --reload --port 8642 & cd frontend && cargo tauri dev",
"dev": "VITE_BACKEND_PORT=${VITE_BACKEND_PORT:-${BACKEND_PORT:-8000}}; cd frontend && VITE_BACKEND_PORT=$VITE_BACKEND_PORT npm run dev -- --host",
"dev:tauri": "BACKEND_PORT=${BACKEND_PORT:-8000}; VITE_BACKEND_PORT=${VITE_BACKEND_PORT:-$BACKEND_PORT}; cd backend && python -m uvicorn main:app --reload --port $BACKEND_PORT & cd frontend && VITE_BACKEND_PORT=$VITE_BACKEND_PORT cargo tauri dev",
"build:tauri": "cd frontend && cargo tauri build",
"dev:frontend": "cd frontend && npm run dev",
"dev:backend": "cd backend && python -m uvicorn main:app --reload --port 8642",
"dev:frontend": "VITE_BACKEND_PORT=${VITE_BACKEND_PORT:-${BACKEND_PORT:-8000}}; cd frontend && VITE_BACKEND_PORT=$VITE_BACKEND_PORT npm run dev",
"dev:backend": "BACKEND_PORT=${BACKEND_PORT:-8000}; cd backend && python -m uvicorn main:app --reload --port $BACKEND_PORT",
"lint": "cd frontend && npm run lint"
},
"devDependencies": {
"concurrently": "^9.1.0",
"electron": "^33.2.0",
"electron-builder": "^25.1.0",
"wait-on": "^8.0.0"
},
"dependencies": {
"python-shell": "^5.0.0"
},
"build": {
"appId": "com.talkedit.app",
"productName": "TalkEdit",
"files": [
"electron/**/*",
"frontend/dist/**/*",
"backend/**/*",
"shared/**/*"
],
"extraResources": [
{
"from": "backend",
"to": "backend"
}
],
"win": {
"target": "nsis"
},
"mac": {
"target": "dmg"
},
"linux": {
"target": "AppImage"
}
}
"devDependencies": {},
"dependencies": {}
}

12
plan.md
View File

@ -1,6 +1,6 @@
# Plan for Building TalkEdit (Whisper.cpp + Tauri)
Based on your original idea summary and our discussions, here's a detailed plan to build a standalone, local audio/video editor app. We'll modify CutScript as the base, migrate to **Tauri 2.0** (Rust backend + React frontend) for tiny, dependency-free installers, and use **Whisper.cpp** for fast, accurate transcription. This keeps the scope minimal, focuses on text-based editing for spoken content, and targets podcasters/YouTubers.
Based on your original idea summary and our discussions, here's a detailed plan to build a standalone, local audio/video editor app. We'll continue evolving the existing TalkEdit codebase on **Tauri 2.0** (Rust backend + React frontend) for tiny, dependency-free installers, and use **Whisper.cpp** for fast, accurate transcription. This keeps the scope minimal, focuses on text-based editing for spoken content, and targets podcasters/YouTubers.
## 1. Overview
- **Goal**: Create an offline Descript alternative with word-level editing, transcription, and export. Users download one file (~1020MB), install, and run—no Python, FFmpeg, or external deps.
@ -9,19 +9,19 @@ Based on your original idea summary and our discussions, here's a detailed plan
- **Key Differentiators**: Fully local, text-based editing like Google Docs, smart cuts with fades.
## 2. Tech Stack
- **Frontend**: React + Vite + Tailwind CSS + shadcn/ui (from CutScript; minimal changes).
- **Frontend**: React + Vite + Tailwind CSS + shadcn/ui.
- **Backend**: Tauri 2.0 (Rust) handles file I/O, FFmpeg calls, Whisper.cpp integration.
- **Transcription**: Whisper.cpp (via Rust bindings like `whisper-cpp-sys` or `whisper-rs`).
- **Audio/Video Processing**: FFmpeg (bundled or called via Rust wrappers like `ffmpeg-next`).
- **State Management**: Zustand (from CutScript).
- **State Management**: Zustand.
- **Packaging**: Tauri's `tauri build` for cross-platform installers.
- **AI Features**: Local models only (no APIs); optional Ollama for fillers.
## 3. Step-by-Step Development Plan
1. **Set Up Tauri in CutScript** (12 weeks):
1. **Set Up Tauri in TalkEdit** (12 weeks):
- Install `tauri-cli` globally.
- In CutScript root: `npx tauri init` (choose Rust backend, link to existing React frontend).
- Migrate Electron main.js to Tauri's `src/main.rs` (handle window, file dialogs).
- In TalkEdit root: `npx tauri init` (choose Rust backend, link to existing React frontend).
- Implement Tauri `src/main.rs` host flow (window lifecycle, file dialogs, backend coordination).
- Update `tauri.conf.json` for app metadata, bundle settings.
2. **Integrate Whisper.cpp in Rust** (23 weeks):

View File

@ -23,7 +23,7 @@
}
],
"security": {
"csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:8000; media-src 'self' http://127.0.0.1:8000; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:"
"csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:* http://localhost:*; media-src 'self' http://127.0.0.1:* http://localhost:*; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:"
}
},
"bundle": {