From e25f8a9b6300d3e5aecb5abbb32084a5364e0d37 Mon Sep 17 00:00:00 2001
From: dillonj
Date: Tue, 7 Apr 2026 23:08:27 -0600
Subject: [PATCH] removed electron
---
.gitignore | 3 -
README.md | 40 +++++--
backend/routers/export.py | 2 +-
close | 19 ++-
electron/main.js | 131 ---------------------
electron/preload.js | 12 --
electron/python-bridge.js | 105 -----------------
frontend/src/App.tsx | 36 +++---
frontend/src/components/ExportDialog.tsx | 2 +-
frontend/src/hooks/useKeyboardShortcuts.ts | 6 +-
frontend/src/lib/tauri-bridge.ts | 13 +-
frontend/src/main.tsx | 2 +-
frontend/src/store/aiStore.ts | 8 +-
frontend/src/vite-env.d.ts | 8 +-
frontend/tsconfig.tsbuildinfo | 2 +-
idea summary.md | 8 +-
open | 3 +-
package.json | 45 +------
plan.md | 12 +-
src-tauri/tauri.conf.json | 2 +-
20 files changed, 96 insertions(+), 363 deletions(-)
delete mode 100644 electron/main.js
delete mode 100644 electron/preload.js
delete mode 100644 electron/python-bridge.js
diff --git a/.gitignore b/.gitignore
index 8c1c296..1f89a79 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,9 +21,6 @@ htmlcov/
.idea/
.cursor/
-# Submodules (can be cloned separately if needed)
-CutScript/
-
# OS files
.env
.env.local
diff --git a/README.md b/README.md
index 5873147..aecdd5c 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/backend/routers/export.py b/backend/routers/export.py
index 91228ca..8ee3f2b 100644
--- a/backend/routers/export.py
+++ b/backend/routers/export.py
@@ -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)
diff --git a/close b/close
index c30d6f1..14fd9ec 100755
--- a/close
+++ b/close
@@ -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
diff --git a/electron/main.js b/electron/main.js
deleted file mode 100644
index ac11da6..0000000
--- a/electron/main.js
+++ /dev/null
@@ -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;
-});
diff --git a/electron/preload.js b/electron/preload.js
deleted file mode 100644
index 32d5ac0..0000000
--- a/electron/preload.js
+++ /dev/null
@@ -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),
-});
diff --git a/electron/python-bridge.js b/electron/python-bridge.js
deleted file mode 100644
index 7db1b41..0000000
--- a/electron/python-bridge.js
+++ /dev/null
@@ -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 };
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 76feaf8..6db2115 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -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.
- {IS_ELECTRON ? (
+ {IS_DESKTOP ? (
}
label="Open"
- onClick={IS_ELECTRON ? handleOpenFile : () => useEditorStore.getState().reset()}
+ onClick={IS_DESKTOP ? handleOpenFile : () => useEditorStore.getState().reset()}
/>
- {IS_ELECTRON && (
+ {IS_DESKTOP && (
}
label="Save"
@@ -323,7 +323,7 @@ export default function App() {
disabled={words.length === 0}
/>
)}
- {IS_ELECTRON && (
+ {IS_DESKTOP && (
}
label="Load"
diff --git a/frontend/src/components/ExportDialog.tsx b/frontend/src/components/ExportDialog.tsx
index b994a78..53b0bec 100644
--- a/frontend/src/components/ExportDialog.tsx
+++ b/frontend/src/components/ExportDialog.tsx
@@ -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'] },
diff --git a/frontend/src/hooks/useKeyboardShortcuts.ts b/frontend/src/hooks/useKeyboardShortcuts.ts
index 1de1cf5..87b27d2 100644
--- a/frontend/src/hooks/useKeyboardShortcuts.ts
+++ b/frontend/src/hooks/useKeyboardShortcuts.ts
@@ -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);
diff --git a/frontend/src/lib/tauri-bridge.ts b/frontend/src/lib/tauri-bridge.ts
index 423cffc..5622406 100644
--- a/frontend/src/lib/tauri-bridge.ts
+++ b/frontend/src/lib/tauri-bridge.ts
@@ -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): Promise => {
const result = await open({
multiple: false,
@@ -53,8 +56,8 @@ window.electronAPI = {
},
getBackendUrl: (): Promise => {
- // 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 => {
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index f3aa778..a9ad0dc 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -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';
diff --git a/frontend/src/store/aiStore.ts b/frontend/src/store/aiStore.ts
index 78bc277..8ef70d4 100644
--- a/frontend/src/store/aiStore.ts
+++ b/frontend/src/store/aiStore.ts
@@ -30,8 +30,8 @@ async function encryptAndStore(key: string, value: string): Promise {
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 {
async function loadAndDecrypt(key: string): Promise {
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 '';
}
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index 4625e75..fe4eb97 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -1,6 +1,10 @@
///
-interface ElectronAPI {
+interface ImportMetaEnv {
+ readonly VITE_BACKEND_PORT?: string;
+}
+
+interface DesktopAPI {
openFile: (options?: Record) => Promise;
saveFile: (options?: Record) => Promise;
openProject: () => Promise;
@@ -15,5 +19,5 @@ interface ElectronAPI {
}
interface Window {
- electronAPI?: ElectronAPI;
+ desktopAPI?: DesktopAPI;
}
diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo
index e72183c..8a957e1 100644
--- a/frontend/tsconfig.tsbuildinfo
+++ b/frontend/tsconfig.tsbuildinfo
@@ -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"}
\ No newline at end of file
+{"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"}
\ No newline at end of file
diff --git a/idea summary.md b/idea summary.md
index 3679d70..590a5dd 100644
--- a/idea summary.md
+++ b/idea summary.md
@@ -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 4–8 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.
diff --git a/open b/open
index b6b9cab..7fbdb53 100755
--- a/open
+++ b/open
@@ -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
diff --git a/package.json b/package.json
index 84b421e..84486b3 100644
--- a/package.json
+++ b/package.json
@@ -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": {}
}
diff --git a/plan.md b/plan.md
index 4f0d3b1..5052dd1 100644
--- a/plan.md
+++ b/plan.md
@@ -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 (~10–20MB), 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** (1–2 weeks):
+1. **Set Up Tauri in TalkEdit** (1–2 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** (2–3 weeks):
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index a34ddd1..f92a49d 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -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": {