removed electron
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,9 +21,6 @@ htmlcov/
|
|||||||
.idea/
|
.idea/
|
||||||
.cursor/
|
.cursor/
|
||||||
|
|
||||||
# Submodules (can be cloned separately if needed)
|
|
||||||
CutScript/
|
|
||||||
|
|
||||||
# OS files
|
# OS files
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
40
README.md
40
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.
|
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
|
## Architecture
|
||||||
|
|
||||||
- **Electron + React** desktop app with Tailwind CSS
|
- **Tauri + React** desktop app with Tailwind CSS
|
||||||
- **FastAPI** Python backend (spawned as child process)
|
- **FastAPI** Python backend (spawned as child process)
|
||||||
- **WhisperX** for word-level transcription with alignment
|
- **WhisperX** for word-level transcription with alignment
|
||||||
- **FFmpeg** for video processing (stream-copy and re-encode)
|
- **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
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Root dependencies (Electron, concurrently)
|
# Root scripts
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
# Frontend dependencies (React, Tailwind, Zustand)
|
# Frontend dependencies (React, Tailwind, Zustand)
|
||||||
@ -37,32 +37,46 @@ cd backend && pip install -r requirements.txt && cd ..
|
|||||||
|
|
||||||
### Run (Development)
|
### Run (Development)
|
||||||
|
|
||||||
|
Set a custom backend port once (optional):
|
||||||
|
|
||||||
```bash
|
```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
|
npm run dev
|
||||||
|
|
||||||
|
# Or start the full desktop app (backend + tauri)
|
||||||
|
npm run dev:tauri
|
||||||
```
|
```
|
||||||
|
|
||||||
Or run them separately:
|
Or run them separately:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Terminal 1: Backend
|
# 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
|
# Terminal 2: Frontend
|
||||||
cd frontend && npm run dev
|
cd frontend && npm run dev
|
||||||
|
|
||||||
# Terminal 3: Electron
|
# Terminal 3: Tauri app shell
|
||||||
npx electron .
|
cd frontend && cargo tauri dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
cutscript/
|
talkedit/
|
||||||
├── electron/ # Electron main process
|
├── src-tauri/ # Tauri Rust host
|
||||||
│ ├── main.js # App entry, spawns Python backend
|
│ ├── src/main.rs # App entry and backend orchestration
|
||||||
│ ├── preload.js # Secure IPC bridge
|
│ └── tauri.conf.json
|
||||||
│ └── python-bridge.js
|
|
||||||
├── frontend/ # React + Vite + Tailwind
|
├── frontend/ # React + Vite + Tailwind
|
||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── components/ # VideoPlayer, TranscriptEditor, etc.
|
│ ├── components/ # VideoPlayer, TranscriptEditor, etc.
|
||||||
@ -97,7 +111,7 @@ cutscript/
|
|||||||
| Speaker diarization | Done |
|
| Speaker diarization | Done |
|
||||||
| Virtualized transcript (react-virtuoso) | Done |
|
| Virtualized transcript (react-virtuoso) | Done |
|
||||||
| Encrypted API key storage | Done |
|
| Encrypted API key storage | Done |
|
||||||
| Project save/load (.cutscript) | Done |
|
| Project save/load (.aive) | Done |
|
||||||
| AI background removal | Planned |
|
| AI background removal | Planned |
|
||||||
|
|
||||||
## Keyboard Shortcuts
|
## Keyboard Shortcuts
|
||||||
|
|||||||
@ -118,7 +118,7 @@ async def export_video(req: ExportRequest):
|
|||||||
# Audio enhancement: clean, then mux back into the exported video
|
# Audio enhancement: clean, then mux back into the exported video
|
||||||
if req.enhanceAudio:
|
if req.enhanceAudio:
|
||||||
try:
|
try:
|
||||||
tmp_dir = tempfile.mkdtemp(prefix="cutscript_audio_")
|
tmp_dir = tempfile.mkdtemp(prefix="talkedit_audio_")
|
||||||
cleaned_audio = os.path.join(tmp_dir, "cleaned.wav")
|
cleaned_audio = os.path.join(tmp_dir, "cleaned.wav")
|
||||||
clean_audio(output, cleaned_audio)
|
clean_audio(output, cleaned_audio)
|
||||||
|
|
||||||
|
|||||||
19
close
19
close
@ -1,7 +1,8 @@
|
|||||||
#!/bin/bash
|
#!/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
|
KILLED_ANY=0
|
||||||
|
BACKEND_PORT="${BACKEND_PORT:-8000}"
|
||||||
|
|
||||||
kill_port() {
|
kill_port() {
|
||||||
local port=$1
|
local port=$1
|
||||||
@ -27,23 +28,17 @@ kill_pattern() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- TalkEdit (Tauri, port 8000) ---
|
# --- TalkEdit (Tauri) ---
|
||||||
kill_port 8000 "TalkEdit"
|
kill_port "$BACKEND_PORT" "TalkEdit"
|
||||||
kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)"
|
kill_pattern "tauri.*TalkEdit\|TalkEdit.*tauri\|cargo.*tauri dev\|/TalkEdit/target/debug" "TalkEdit (Tauri dev)"
|
||||||
# Vite dev server for TalkEdit (port 5173)
|
# Vite dev server for TalkEdit (port 5173)
|
||||||
kill_pattern "vite.*5173\|rsbuild.*5173" "TalkEdit frontend dev server"
|
kill_pattern "vite.*5173\|rsbuild.*5173" "TalkEdit frontend dev server"
|
||||||
|
|
||||||
# --- CutScript (Electron, port 8642) ---
|
# --- Orphaned uvicorn workers for TalkEdit ---
|
||||||
kill_port 8642 "CutScript"
|
kill_pattern "uvicorn.*main:app.*--port ${BACKEND_PORT}" "leftover uvicorn workers (TalkEdit)"
|
||||||
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)"
|
|
||||||
|
|
||||||
if [[ $KILLED_ANY -eq 0 ]]; then
|
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
|
else
|
||||||
echo "Done."
|
echo "Done."
|
||||||
fi
|
fi
|
||||||
|
|||||||
131
electron/main.js
131
electron/main.js
@ -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;
|
|
||||||
});
|
|
||||||
@ -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),
|
|
||||||
});
|
|
||||||
@ -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 };
|
|
||||||
@ -22,7 +22,7 @@ import {
|
|||||||
VolumeX,
|
VolumeX,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
const IS_ELECTRON = !!window.electronAPI;
|
const IS_DESKTOP = !!window.desktopAPI;
|
||||||
|
|
||||||
type Panel = 'ai' | 'settings' | 'export' | 'silence' | null;
|
type Panel = 'ai' | 'settings' | 'export' | 'silence' | null;
|
||||||
|
|
||||||
@ -66,8 +66,8 @@ export default function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (IS_ELECTRON) {
|
if (IS_DESKTOP) {
|
||||||
window.electronAPI!.getBackendUrl().then(setBackendUrl);
|
window.desktopAPI!.getBackendUrl().then(setBackendUrl);
|
||||||
}
|
}
|
||||||
// In Tauri on Linux/WebKit2GTK the ipc:// custom protocol is blocked by
|
// In Tauri on Linux/WebKit2GTK the ipc:// custom protocol is blocked by
|
||||||
// WebKit internals; postMessage fallback works but logs noisy warnings.
|
// WebKit internals; postMessage fallback works but logs noisy warnings.
|
||||||
@ -75,11 +75,11 @@ export default function App() {
|
|||||||
}, [setBackendUrl]);
|
}, [setBackendUrl]);
|
||||||
|
|
||||||
const handleLoadProject = async () => {
|
const handleLoadProject = async () => {
|
||||||
if (!IS_ELECTRON) return;
|
if (!IS_DESKTOP) return;
|
||||||
try {
|
try {
|
||||||
const projectPath = await window.electronAPI!.openProject();
|
const projectPath = await window.desktopAPI!.openProject();
|
||||||
if (!projectPath) return;
|
if (!projectPath) return;
|
||||||
const content = await window.electronAPI!.readFile(projectPath);
|
const content = await window.desktopAPI!.readFile(projectPath);
|
||||||
const data = JSON.parse(content);
|
const data = JSON.parse(content);
|
||||||
useEditorStore.getState().loadProject(data);
|
useEditorStore.getState().loadProject(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -89,13 +89,13 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveProject = async () => {
|
const handleSaveProject = async () => {
|
||||||
if (!IS_ELECTRON) return;
|
if (!IS_DESKTOP) return;
|
||||||
try {
|
try {
|
||||||
const savePath = await window.electronAPI!.saveProject();
|
const savePath = await window.desktopAPI!.saveProject();
|
||||||
if (!savePath) return;
|
if (!savePath) return;
|
||||||
const data = useEditorStore.getState().saveProject();
|
const data = useEditorStore.getState().saveProject();
|
||||||
const path = savePath.endsWith('.aive') ? savePath : `${savePath}.aive`;
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to save project:', err);
|
console.error('Failed to save project:', err);
|
||||||
alert(`Failed to save project: ${err}`);
|
alert(`Failed to save project: ${err}`);
|
||||||
@ -103,8 +103,8 @@ export default function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenFile = async () => {
|
const handleOpenFile = async () => {
|
||||||
if (IS_ELECTRON) {
|
if (IS_DESKTOP) {
|
||||||
const path = await window.electronAPI!.openFile();
|
const path = await window.desktopAPI!.openFile();
|
||||||
if (path) {
|
if (path) {
|
||||||
loadVideo(path);
|
loadVideo(path);
|
||||||
await transcribeVideo(path);
|
await transcribeVideo(path);
|
||||||
@ -130,7 +130,7 @@ export default function App() {
|
|||||||
const transcribeVideo = async (path: string) => {
|
const transcribeVideo = async (path: string) => {
|
||||||
setTranscribing(true, 0, 'Checking model...');
|
setTranscribing(true, 0, 'Checking model...');
|
||||||
try {
|
try {
|
||||||
if (!window.electronAPI?.transcribe) {
|
if (!window.desktopAPI?.transcribe) {
|
||||||
throw new Error('Transcription not available');
|
throw new Error('Transcription not available');
|
||||||
}
|
}
|
||||||
// Step 1: ensure model is downloaded (may take a while on first run)
|
// 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';
|
const modelLabel = MODEL_SIZES[whisperModel] ?? 'unknown size';
|
||||||
setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`);
|
setTranscribing(true, 5, `Downloading ${whisperModel} model (${modelLabel})...`);
|
||||||
await window.electronAPI.ensureModel(whisperModel);
|
await window.desktopAPI.ensureModel(whisperModel);
|
||||||
|
|
||||||
// Step 2: run transcription
|
// Step 2: run transcription
|
||||||
setTranscribing(true, 20, 'Transcribing audio...');
|
setTranscribing(true, 20, 'Transcribing audio...');
|
||||||
const data = await window.electronAPI.transcribe(path, whisperModel);
|
const data = await window.desktopAPI.transcribe(path, whisperModel);
|
||||||
setTranscription(data);
|
setTranscription(data);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Transcription error:', 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.
|
English-only models are ~10% faster and more accurate for English content.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{IS_ELECTRON ? (
|
{IS_DESKTOP ? (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenFile}
|
onClick={handleOpenFile}
|
||||||
@ -313,9 +313,9 @@ export default function App() {
|
|||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<FolderOpen className="w-4 h-4" />}
|
icon={<FolderOpen className="w-4 h-4" />}
|
||||||
label="Open"
|
label="Open"
|
||||||
onClick={IS_ELECTRON ? handleOpenFile : () => useEditorStore.getState().reset()}
|
onClick={IS_DESKTOP ? handleOpenFile : () => useEditorStore.getState().reset()}
|
||||||
/>
|
/>
|
||||||
{IS_ELECTRON && (
|
{IS_DESKTOP && (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<Save className="w-4 h-4" />}
|
icon={<Save className="w-4 h-4" />}
|
||||||
label="Save"
|
label="Save"
|
||||||
@ -323,7 +323,7 @@ export default function App() {
|
|||||||
disabled={words.length === 0}
|
disabled={words.length === 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{IS_ELECTRON && (
|
{IS_DESKTOP && (
|
||||||
<ToolbarButton
|
<ToolbarButton
|
||||||
icon={<FileInput className="w-4 h-4" />}
|
icon={<FileInput className="w-4 h-4" />}
|
||||||
label="Load"
|
label="Load"
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export default function ExportDialog() {
|
|||||||
const handleExport = useCallback(async () => {
|
const handleExport = useCallback(async () => {
|
||||||
if (!videoPath) return;
|
if (!videoPath) return;
|
||||||
|
|
||||||
const outputPath = await window.electronAPI?.saveFile({
|
const outputPath = await window.desktopAPI?.saveFile({
|
||||||
defaultPath: videoPath.replace(/\.[^.]+$/, '_edited.mp4'),
|
defaultPath: videoPath.replace(/\.[^.]+$/, '_edited.mp4'),
|
||||||
filters: [
|
filters: [
|
||||||
{ name: 'MP4', extensions: ['mp4'] },
|
{ name: 'MP4', extensions: ['mp4'] },
|
||||||
|
|||||||
@ -167,14 +167,14 @@ async function saveProject() {
|
|||||||
modifiedAt: new Date().toISOString(),
|
modifiedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const outputPath = await window.electronAPI?.saveFile({
|
const outputPath = await window.desktopAPI?.saveFile({
|
||||||
defaultPath: state.videoPath.replace(/\.[^.]+$/, '.aive'),
|
defaultPath: state.videoPath.replace(/\.[^.]+$/, '.aive'),
|
||||||
filters: [{ name: 'TalkEdit Project', extensions: ['aive'] }],
|
filters: [{ name: 'TalkEdit Project', extensions: ['aive'] }],
|
||||||
});
|
});
|
||||||
|
|
||||||
if (outputPath) {
|
if (outputPath) {
|
||||||
if (window.electronAPI?.writeFile) {
|
if (window.desktopAPI?.writeFile) {
|
||||||
await window.electronAPI.writeFile(outputPath, JSON.stringify(projectData, null, 2));
|
await window.desktopAPI.writeFile(outputPath, JSON.stringify(projectData, null, 2));
|
||||||
} else {
|
} else {
|
||||||
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
|
const blob = new Blob([JSON.stringify(projectData, null, 2)], { type: 'application/json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* tauri-bridge.ts
|
* tauri-bridge.ts
|
||||||
*
|
*
|
||||||
* Polyfills window.electronAPI with Tauri equivalents so all existing
|
* Exposes window.desktopAPI using Tauri equivalents so UI code can stay
|
||||||
* call-sites in App.tsx, hooks, and stores continue to work unchanged.
|
* desktop-runtime agnostic.
|
||||||
*
|
*
|
||||||
* Imported once at the top of main.tsx.
|
* Imported once at the top of main.tsx.
|
||||||
*/
|
*/
|
||||||
@ -25,7 +25,10 @@ const EXPORT_FILTERS = [
|
|||||||
{ name: 'Project Files', extensions: ['aive'] },
|
{ 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> => {
|
openFile: async (_options?: Record<string, unknown>): Promise<string | null> => {
|
||||||
const result = await open({
|
const result = await open({
|
||||||
multiple: false,
|
multiple: false,
|
||||||
@ -53,8 +56,8 @@ window.electronAPI = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getBackendUrl: (): Promise<string> => {
|
getBackendUrl: (): Promise<string> => {
|
||||||
// Backend URL is fixed; avoid invoke() which triggers ipc:// CSP errors on Linux/WebKit2GTK
|
// Use env-driven backend URL and avoid invoke() to bypass ipc:// noise on Linux/WebKit2GTK.
|
||||||
return Promise.resolve('http://127.0.0.1:8000');
|
return Promise.resolve(BACKEND_URL);
|
||||||
},
|
},
|
||||||
|
|
||||||
encryptString: (data: string): Promise<string> => {
|
encryptString: (data: string): Promise<string> => {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
// Forward console.error/warn/log to backend in dev mode so we can tail webview.log
|
// Forward console.error/warn/log to backend in dev mode so we can tail webview.log
|
||||||
import './lib/dev-logger';
|
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 './lib/tauri-bridge';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|||||||
@ -30,8 +30,8 @@ async function encryptAndStore(key: string, value: string): Promise<void> {
|
|||||||
localStorage.removeItem(ENCRYPTED_KEY_PREFIX + key);
|
localStorage.removeItem(ENCRYPTED_KEY_PREFIX + key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (window.electronAPI) {
|
if (window.desktopAPI) {
|
||||||
const encrypted = await window.electronAPI.encryptString(value);
|
const encrypted = await window.desktopAPI.encryptString(value);
|
||||||
localStorage.setItem(ENCRYPTED_KEY_PREFIX + key, encrypted);
|
localStorage.setItem(ENCRYPTED_KEY_PREFIX + key, encrypted);
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(ENCRYPTED_KEY_PREFIX + key, btoa(value));
|
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> {
|
async function loadAndDecrypt(key: string): Promise<string> {
|
||||||
const stored = localStorage.getItem(ENCRYPTED_KEY_PREFIX + key);
|
const stored = localStorage.getItem(ENCRYPTED_KEY_PREFIX + key);
|
||||||
if (!stored) return '';
|
if (!stored) return '';
|
||||||
if (window.electronAPI) {
|
if (window.desktopAPI) {
|
||||||
try {
|
try {
|
||||||
return await window.electronAPI.decryptString(stored);
|
return await window.desktopAPI.decryptString(stored);
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
8
frontend/src/vite-env.d.ts
vendored
8
frontend/src/vite-env.d.ts
vendored
@ -1,6 +1,10 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
interface ElectronAPI {
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_BACKEND_PORT?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DesktopAPI {
|
||||||
openFile: (options?: Record<string, unknown>) => Promise<string | null>;
|
openFile: (options?: Record<string, unknown>) => Promise<string | null>;
|
||||||
saveFile: (options?: Record<string, unknown>) => Promise<string | null>;
|
saveFile: (options?: Record<string, unknown>) => Promise<string | null>;
|
||||||
openProject: () => Promise<string | null>;
|
openProject: () => Promise<string | null>;
|
||||||
@ -15,5 +19,5 @@ interface ElectronAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI?: ElectronAPI;
|
desktopAPI?: DesktopAPI;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"}
|
||||||
@ -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.
|
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)
|
### What You Should Do (Step-by-Step Plan)
|
||||||
1. **Fork an existing open-source base** (don't start from scratch)
|
1. **Build from the existing TalkEdit 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).
|
- 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.
|
- 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)
|
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
|
### 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.
|
- **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.
|
- **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.
|
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**:
|
**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.
|
- 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.
|
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
3
open
@ -3,7 +3,8 @@
|
|||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
PROJECT_DIR="$PWD"
|
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"
|
BACKEND_URL="http://127.0.0.1:${BACKEND_PORT}/health"
|
||||||
|
|
||||||
# Check if backend is already running
|
# Check if backend is already running
|
||||||
|
|||||||
45
package.json
45
package.json
@ -3,48 +3,15 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "TalkEdit — Open-source AI-powered text-based video editor",
|
"description": "TalkEdit — Open-source AI-powered text-based video editor",
|
||||||
"main": "electron/main.js",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tauri": "tauri",
|
"tauri": "tauri",
|
||||||
"dev": "cd frontend && npm run dev -- --host",
|
"dev": "VITE_BACKEND_PORT=${VITE_BACKEND_PORT:-${BACKEND_PORT:-8000}}; cd frontend && VITE_BACKEND_PORT=$VITE_BACKEND_PORT npm run dev -- --host",
|
||||||
"dev:tauri": "cd backend && python -m uvicorn main:app --reload --port 8642 & cd frontend && cargo tauri dev",
|
"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",
|
"build:tauri": "cd frontend && cargo tauri build",
|
||||||
"dev:frontend": "cd frontend && npm run dev",
|
"dev:frontend": "VITE_BACKEND_PORT=${VITE_BACKEND_PORT:-${BACKEND_PORT:-8000}}; cd frontend && VITE_BACKEND_PORT=$VITE_BACKEND_PORT npm run dev",
|
||||||
"dev:backend": "cd backend && python -m uvicorn main:app --reload --port 8642",
|
"dev:backend": "BACKEND_PORT=${BACKEND_PORT:-8000}; cd backend && python -m uvicorn main:app --reload --port $BACKEND_PORT",
|
||||||
"lint": "cd frontend && npm run lint"
|
"lint": "cd frontend && npm run lint"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {},
|
||||||
"concurrently": "^9.1.0",
|
"dependencies": {}
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
12
plan.md
12
plan.md
@ -1,6 +1,6 @@
|
|||||||
# Plan for Building TalkEdit (Whisper.cpp + Tauri)
|
# 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
|
## 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.
|
- **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.
|
- **Key Differentiators**: Fully local, text-based editing like Google Docs, smart cuts with fades.
|
||||||
|
|
||||||
## 2. Tech Stack
|
## 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.
|
- **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`).
|
- **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`).
|
- **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.
|
- **Packaging**: Tauri's `tauri build` for cross-platform installers.
|
||||||
- **AI Features**: Local models only (no APIs); optional Ollama for fillers.
|
- **AI Features**: Local models only (no APIs); optional Ollama for fillers.
|
||||||
|
|
||||||
## 3. Step-by-Step Development Plan
|
## 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.
|
- Install `tauri-cli` globally.
|
||||||
- In CutScript root: `npx tauri init` (choose Rust backend, link to existing React frontend).
|
- In TalkEdit 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).
|
- Implement Tauri `src/main.rs` host flow (window lifecycle, file dialogs, backend coordination).
|
||||||
- Update `tauri.conf.json` for app metadata, bundle settings.
|
- Update `tauri.conf.json` for app metadata, bundle settings.
|
||||||
|
|
||||||
2. **Integrate Whisper.cpp in Rust** (2–3 weeks):
|
2. **Integrate Whisper.cpp in Rust** (2–3 weeks):
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"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": {
|
"bundle": {
|
||||||
|
|||||||
Reference in New Issue
Block a user