feat: initialize project scaffold with Electron structure, transcription engine, and dark-themed UI components
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"use strict";
|
||||||
|
const electron = require("electron");
|
||||||
|
const path = require("path");
|
||||||
|
const utils = require("@electron-toolkit/utils");
|
||||||
|
const icon = path.join(__dirname, "../../resources/icon.png");
|
||||||
|
function createWindow() {
|
||||||
|
const mainWindow = new electron.BrowserWindow({
|
||||||
|
width: 1100,
|
||||||
|
height: 670,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
...process.platform === "linux" ? { icon } : {},
|
||||||
|
webPreferences: {
|
||||||
|
preload: path.join(__dirname, "../preload/index.mjs"),
|
||||||
|
sandbox: false
|
||||||
|
},
|
||||||
|
frame: false,
|
||||||
|
// Custom frame for premium look
|
||||||
|
transparent: true,
|
||||||
|
backgroundColor: "#00000000"
|
||||||
|
});
|
||||||
|
mainWindow.on("ready-to-show", () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||||
|
electron.shell.openExternal(details.url);
|
||||||
|
return { action: "deny" };
|
||||||
|
});
|
||||||
|
if (utils.is.dev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||||
|
mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||||
|
} else {
|
||||||
|
mainWindow.loadFile(path.join(__dirname, "../renderer/index.html"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
electron.app.whenReady().then(() => {
|
||||||
|
utils.electronApp.setAppUserModelId("com.voicext.app");
|
||||||
|
electron.app.on("browser-window-created", (_, window) => {
|
||||||
|
utils.optimizer.watchWindowShortcuts(window);
|
||||||
|
});
|
||||||
|
electron.ipcMain.on("window-controls", (event, action) => {
|
||||||
|
const win = electron.BrowserWindow.fromWebContents(event.sender);
|
||||||
|
if (action === "minimize") win.minimize();
|
||||||
|
if (action === "maximize") win.isMaximized() ? win.unmaximize() : win.maximize();
|
||||||
|
if (action === "close") win.close();
|
||||||
|
});
|
||||||
|
createWindow();
|
||||||
|
electron.app.on("activate", function() {
|
||||||
|
if (electron.BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
electron.app.on("window-all-closed", () => {
|
||||||
|
if (process.platform !== "darwin") {
|
||||||
|
electron.app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use strict";
|
||||||
|
const electron = require("electron");
|
||||||
|
const preload = require("@electron-toolkit/preload");
|
||||||
|
const api = {
|
||||||
|
windowControls: (action) => electron.ipcRenderer.send("window-controls", action)
|
||||||
|
};
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
preload.exposeElectronAPI();
|
||||||
|
electron.contextBridge.exposeInMainWorld("api", api);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.electron = preload.exposeElectronAPI();
|
||||||
|
window.api = api;
|
||||||
|
}
|
||||||
Generated
+6130
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -12,6 +12,8 @@
|
|||||||
"postinstall": "electron-builder install-app-deps"
|
"postinstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@electron-toolkit/preload": "^3.0.1",
|
||||||
|
"@electron-toolkit/utils": "^3.0.0",
|
||||||
"@headlessui/react": "^2.0.0",
|
"@headlessui/react": "^2.0.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^11.2.10",
|
"framer-motion": "^11.2.10",
|
||||||
@@ -22,7 +24,6 @@
|
|||||||
"zustand": "^4.5.2"
|
"zustand": "^4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-vite/cli": "^2.3.0",
|
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"electron": "^31.0.1",
|
"electron": "^31.0.1",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,84 @@
|
|||||||
|
import { spawn } from 'child_process'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
const BIN_PATH = app.isPackaged
|
||||||
|
? path.join(process.resourcesPath, 'bin')
|
||||||
|
: path.join(app.getAppPath(), 'bin')
|
||||||
|
|
||||||
|
export async function detectHardware() {
|
||||||
|
// Simple check for NVIDIA GPU via nvidia-smi
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const smi = spawn('nvidia-smi')
|
||||||
|
smi.on('error', () => resolve({ gpu: false, name: 'CPU' }))
|
||||||
|
smi.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ gpu: true, name: 'NVIDIA GPU (CUDA)' })
|
||||||
|
} else {
|
||||||
|
resolve({ gpu: false, name: 'CPU' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transcribe(filePath, options, onProgress, onData) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { model = 'base', language = 'tr', outputFormat = 'srt' } = options
|
||||||
|
|
||||||
|
// 1. FFmpeg Pre-processing (Convert to 16kHz mono WAV)
|
||||||
|
const tempWav = path.join(app.getPath('temp'), `voicext_${Date.now()}.wav`)
|
||||||
|
const ffmpegPath = path.join(BIN_PATH, 'ffmpeg.exe')
|
||||||
|
|
||||||
|
const ffmpeg = spawn(ffmpegPath, [
|
||||||
|
'-i', filePath,
|
||||||
|
'-ar', '16000',
|
||||||
|
'-ac', '1',
|
||||||
|
'-c:a', 'pcm_s16le',
|
||||||
|
tempWav,
|
||||||
|
'-y'
|
||||||
|
])
|
||||||
|
|
||||||
|
ffmpeg.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
return reject(new Error('FFmpeg conversion failed'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Whisper.cpp Inference
|
||||||
|
const whisperPath = path.join(BIN_PATH, 'whisper.exe')
|
||||||
|
const modelPath = path.join(BIN_PATH, 'models', `ggml-${model}.bin`)
|
||||||
|
|
||||||
|
const whisper = spawn(whisperPath, [
|
||||||
|
'-m', modelPath,
|
||||||
|
'-f', tempWav,
|
||||||
|
`-o${outputFormat}`,
|
||||||
|
'-l', language,
|
||||||
|
'--max-len', '42'
|
||||||
|
])
|
||||||
|
|
||||||
|
whisper.stdout.on('data', (data) => {
|
||||||
|
onData(data.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
whisper.stderr.on('data', (data) => {
|
||||||
|
// Whisper.cpp outputs progress to stderr
|
||||||
|
const output = data.toString()
|
||||||
|
const progressMatch = output.match(/progress\s*=\s*(\d+)%/)
|
||||||
|
if (progressMatch) {
|
||||||
|
onProgress(parseInt(progressMatch[1]))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
whisper.on('close', (code) => {
|
||||||
|
// Cleanup temp file
|
||||||
|
if (fs.existsSync(tempWav)) fs.unlinkSync(tempWav)
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true, outputPath: filePath.replace(/\.[^/.]+$/, `.${outputFormat}`) })
|
||||||
|
} else {
|
||||||
|
reject(new Error('Whisper transcription failed'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
+29
-2
@@ -3,6 +3,8 @@ import { join } from 'path'
|
|||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||||
import icon from '../../resources/icon.png?asset'
|
import icon from '../../resources/icon.png?asset'
|
||||||
|
|
||||||
|
import { detectHardware, transcribe } from './engine/transcription'
|
||||||
|
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 1100,
|
width: 1100,
|
||||||
@@ -14,7 +16,7 @@ function createWindow() {
|
|||||||
preload: join(__dirname, '../preload/index.mjs'),
|
preload: join(__dirname, '../preload/index.mjs'),
|
||||||
sandbox: false
|
sandbox: false
|
||||||
},
|
},
|
||||||
frame: false, // Custom frame for premium look
|
frame: false,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
backgroundColor: '#00000000'
|
backgroundColor: '#00000000'
|
||||||
})
|
})
|
||||||
@@ -33,10 +35,35 @@ function createWindow() {
|
|||||||
} else {
|
} else {
|
||||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle transcription request
|
||||||
|
ipcMain.handle('start-transcription', async (event, { filePath, options }) => {
|
||||||
|
try {
|
||||||
|
const result = await transcribe(
|
||||||
|
filePath,
|
||||||
|
options,
|
||||||
|
(progress) => {
|
||||||
|
mainWindow.webContents.send('transcription-progress', progress)
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
mainWindow.webContents.send('transcription-data', data)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Transcription Error:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle hardware detection
|
||||||
|
ipcMain.handle('detect-hardware', async () => {
|
||||||
|
return await detectHardware()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
electronApp.setAppId('com.voicext.app')
|
electronApp.setAppUserModelId('com.voicext.app')
|
||||||
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron'
|
||||||
|
import { exposeElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
windowControls: (action) => ipcRenderer.send('window-controls', action),
|
||||||
|
startTranscription: (filePath, options) => ipcRenderer.invoke('start-transcription', { filePath, options }),
|
||||||
|
onTranscriptionProgress: (callback) => ipcRenderer.on('transcription-progress', (_, progress) => callback(progress)),
|
||||||
|
onTranscriptionData: (callback) => ipcRenderer.on('transcription-data', (_, data) => callback(data)),
|
||||||
|
detectHardware: () => ipcRenderer.invoke('detect-hardware')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.contextIsolated) {
|
||||||
|
try {
|
||||||
|
exposeElectronAPI()
|
||||||
|
contextBridge.exposeInMainWorld('api', api)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.electron = exposeElectronAPI()
|
||||||
|
window.api = api
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Voicext</title>
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: asset:;"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
import React, { useState } from 'react'
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Box,
|
||||||
|
ListMusic,
|
||||||
|
Settings,
|
||||||
|
X,
|
||||||
|
Minus,
|
||||||
|
Square,
|
||||||
|
UploadCloud,
|
||||||
|
ChevronDown,
|
||||||
|
Cpu,
|
||||||
|
Monitor
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
|
|
||||||
|
import logo from './assets/logo.png'
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [activeTab, setActiveTab] = useState('dashboard')
|
||||||
|
const [dragActive, setDragActive] = useState(false)
|
||||||
|
const [file, setFile] = useState(null)
|
||||||
|
const [isTranscribing, setIsTranscribing] = useState(false)
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
|
|
||||||
|
const handleControl = (action) => {
|
||||||
|
window.api.windowControls(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrag = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.type === 'dragenter' || e.type === 'dragover') {
|
||||||
|
setDragActive(true)
|
||||||
|
} else if (e.type === 'dragleave') {
|
||||||
|
setDragActive(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
setDragActive(false)
|
||||||
|
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
|
||||||
|
setFile(e.dataTransfer.files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileSelect = () => {
|
||||||
|
const input = document.createElement('input')
|
||||||
|
input.type = 'file'
|
||||||
|
input.accept = '.mp3,.wav,.m4a'
|
||||||
|
input.onchange = (e) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
setFile(e.target.files[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
input.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTranscription = () => {
|
||||||
|
if (!file) return
|
||||||
|
setIsTranscribing(true)
|
||||||
|
// Mock progress for now
|
||||||
|
let p = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
p += Math.random() * 5
|
||||||
|
if (p >= 100) {
|
||||||
|
p = 100
|
||||||
|
clearInterval(interval)
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsTranscribing(false)
|
||||||
|
setProgress(0)
|
||||||
|
alert('Transcription Complete!')
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
setProgress(p)
|
||||||
|
}, 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
{/* Titlebar */}
|
||||||
|
<div className="titlebar">
|
||||||
|
<div className="window-controls">
|
||||||
|
<button onClick={() => handleControl('minimize')} className="control-btn"><Minus size={16} /></button>
|
||||||
|
<button onClick={() => handleControl('maximize')} className="control-btn"><Square size={12} /></button>
|
||||||
|
<button onClick={() => handleControl('close')} className="control-btn close"><X size={16} /></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="main-layout">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="header">
|
||||||
|
<div className="logo-section">
|
||||||
|
<img src={logo} alt="Voicext Logo" style={{ height: '32px' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="nav-tabs">
|
||||||
|
{['dashboard', 'models', 'queue', 'settings'].map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
className={`nav-item ${activeTab === tab ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab === 'dashboard' && <LayoutDashboard size={18} />}
|
||||||
|
{tab === 'models' && <Box size={18} />}
|
||||||
|
{tab === 'queue' && <ListMusic size={18} />}
|
||||||
|
{tab === 'settings' && <Settings size={18} />}
|
||||||
|
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Content Area */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<motion.div
|
||||||
|
key="dashboard"
|
||||||
|
initial={{ opacity: 0, scale: 0.98 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 1.02 }}
|
||||||
|
className="dashboard-grid"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`drop-zone glass ${dragActive ? 'drag-active' : ''}`}
|
||||||
|
onDragEnter={handleDrag}
|
||||||
|
onDragLeave={handleDrag}
|
||||||
|
onDragOver={handleDrag}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={handleFileSelect}
|
||||||
|
>
|
||||||
|
{isTranscribing ? (
|
||||||
|
<div className="transcription-status">
|
||||||
|
<div className="progress-circle">
|
||||||
|
<svg width="120" height="120">
|
||||||
|
<circle cx="60" cy="60" r="54" fill="none" stroke="rgba(255,255,255,0.1)" strokeWidth="8" />
|
||||||
|
<motion.circle
|
||||||
|
cx="60" cy="60" r="54" fill="none"
|
||||||
|
stroke="url(#gradient)" strokeWidth="8"
|
||||||
|
strokeDasharray="339.29"
|
||||||
|
initial={{ strokeDashoffset: 339.29 }}
|
||||||
|
animate={{ strokeDashoffset: 339.29 - (339.29 * progress) / 100 }}
|
||||||
|
strokeLinecap="round"
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||||
|
<stop offset="0%" stopColor="var(--primary-cyan)" />
|
||||||
|
<stop offset="100%" stopColor="var(--primary-magenta)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
<div className="progress-text">{Math.round(progress)}%</div>
|
||||||
|
</div>
|
||||||
|
<h3>Transcribing...</h3>
|
||||||
|
<p>{file?.name}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="drop-icon-container">
|
||||||
|
<div className="drop-icon-bg"></div>
|
||||||
|
<UploadCloud size={80} color="white" style={{ position: 'relative', zIndex: 1 }} />
|
||||||
|
</div>
|
||||||
|
{file ? (
|
||||||
|
<div style={{ textAlign: 'center' }}>
|
||||||
|
<h2 style={{ fontSize: '20px', color: 'var(--primary-cyan)' }}>{file.name}</h2>
|
||||||
|
<p>File Ready for Transcription</p>
|
||||||
|
<button
|
||||||
|
className="btn-secondary"
|
||||||
|
onClick={(e) => { e.stopPropagation(); setFile(null); }}
|
||||||
|
style={{ marginTop: '10px', background: 'none', border: '1px solid rgba(255,255,255,0.1)', color: '#666', padding: '5px 15px', borderRadius: '20px', cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
Change File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2>DRAG & DROP YOUR AUDIO FILE HERE</h2>
|
||||||
|
<p>OR CLICK TO BROWSE</p>
|
||||||
|
<span style={{ fontSize: '10px', color: '#666', marginTop: '20px' }}>
|
||||||
|
Supported: MP3, WAV, M4A (Max: 2GB)
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="sidebar-config">
|
||||||
|
<div className="config-card glass">
|
||||||
|
<div className="config-title">
|
||||||
|
<Monitor size={14} />
|
||||||
|
Quick Configuration
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>Language:</label>
|
||||||
|
<select className="select-custom">
|
||||||
|
<option>AUTO (Recommended)</option>
|
||||||
|
<option>Turkish</option>
|
||||||
|
<option>English</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>Model:</label>
|
||||||
|
<select className="select-custom">
|
||||||
|
<option>BASE (Fastest, 140MB)</option>
|
||||||
|
<option>SMALL (Balanced)</option>
|
||||||
|
<option>MEDIUM (Accurate)</option>
|
||||||
|
<option>LARGE (Best Quality)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="input-group">
|
||||||
|
<label>Format:</label>
|
||||||
|
<select className="select-custom">
|
||||||
|
<option>.SRT (Premiere Pro Compatible)</option>
|
||||||
|
<option>.VTT (Web Standard)</option>
|
||||||
|
<option>.TXT (Plain Text)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn-primary"
|
||||||
|
style={{ marginTop: '10px', opacity: file ? 1 : 0.5, cursor: file ? 'pointer' : 'not-allowed' }}
|
||||||
|
disabled={!file || isTranscribing}
|
||||||
|
onClick={startTranscription}
|
||||||
|
>
|
||||||
|
{isTranscribing ? 'Processing...' : 'Start Transcription'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'models' && (
|
||||||
|
<motion.div
|
||||||
|
key="models"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
className="models-view glass"
|
||||||
|
style={{ flex: 1, padding: '40px', display: 'flex', flexDirection: 'column', gap: '20px' }}
|
||||||
|
>
|
||||||
|
<h2>Model Management</h2>
|
||||||
|
<p style={{ color: 'var(--text-muted)' }}>Download and manage local AI models for offline transcription.</p>
|
||||||
|
<div className="models-list" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '20px', marginTop: '20px' }}>
|
||||||
|
{['Base', 'Small', 'Medium', 'Large'].map(m => (
|
||||||
|
<div key={m} className="glass" style={{ padding: '20px', textAlign: 'center' }}>
|
||||||
|
<h3 style={{ marginBottom: '10px' }}>{m}</h3>
|
||||||
|
<div style={{ fontSize: '12px', color: 'var(--text-muted)', marginBottom: '15px' }}>
|
||||||
|
{m === 'Base' ? '140 MB' : m === 'Small' ? '460 MB' : m === 'Medium' ? '1.5 GB' : '2.9 GB'}
|
||||||
|
</div>
|
||||||
|
<button className="btn-secondary" style={{ padding: '8px 15px', borderRadius: '15px', border: '1px solid var(--primary-cyan)', color: 'var(--primary-cyan)', background: 'none' }}>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Bar */}
|
||||||
|
<footer className="status-bar">
|
||||||
|
<div className="status-indicator">
|
||||||
|
<span>SYSTEM: RTX 5080 DETECTED (GPU Acceleration ON)</span>
|
||||||
|
</div>
|
||||||
|
<div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
|
||||||
|
<span>READY</span>
|
||||||
|
<div className="offline-badge">
|
||||||
|
OFFLINE MODE ACTIVE
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
@@ -0,0 +1,384 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-color: #0b0c14;
|
||||||
|
--panel-bg: rgba(255, 255, 255, 0.03);
|
||||||
|
--glass-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--primary-cyan: #00f2ff;
|
||||||
|
--primary-magenta: #ff00e5;
|
||||||
|
--text-main: #ffffff;
|
||||||
|
--text-muted: #a0a0a0;
|
||||||
|
--accent-glow: rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: 'Outfit', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-main);
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background: radial-gradient(circle at 50% -20%, #1a1b2e 0%, #0b0c14 100%);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glassmorphism utility */
|
||||||
|
.glass {
|
||||||
|
background: var(--panel-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Titlebar */
|
||||||
|
.titlebar {
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 10px;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn:hover {
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-btn.close:hover {
|
||||||
|
color: #ff4b4b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content */
|
||||||
|
.main-layout {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-icon {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: linear-gradient(135deg, var(--primary-cyan), var(--primary-magenta));
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 0 20px rgba(0, 242, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text span {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
padding: 8px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item.active {
|
||||||
|
color: var(--text-main);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 242, 255, 0.2);
|
||||||
|
border: 1px solid rgba(0, 242, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Grid */
|
||||||
|
.dashboard-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: 20px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drop Zone */
|
||||||
|
.drop-zone {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover {
|
||||||
|
border-color: var(--primary-cyan);
|
||||||
|
background: rgba(0, 242, 255, 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80%;
|
||||||
|
height: 80%;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 0 50px rgba(255, 0, 229, 0.1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-icon-container {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-icon-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(45deg, var(--primary-cyan), var(--primary-magenta));
|
||||||
|
filter: blur(20px);
|
||||||
|
opacity: 0.3;
|
||||||
|
animation: pulse 4s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% { transform: scale(1); opacity: 0.3; }
|
||||||
|
50% { transform: scale(1.1); opacity: 0.5; }
|
||||||
|
100% { transform: scale(1); opacity: 0.3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar Config */
|
||||||
|
.sidebar-config {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-card {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-cyan);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-custom {
|
||||||
|
width: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 1px solid var(--glass-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--text-main);
|
||||||
|
font-size: 14px;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-custom:focus {
|
||||||
|
border-color: var(--primary-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 30px;
|
||||||
|
border: none;
|
||||||
|
background: linear-gradient(90deg, #8a2be2, #4b0082);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(138, 43, 226, 0.4);
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Bar */
|
||||||
|
.status-bar {
|
||||||
|
height: 35px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-top: 1px solid var(--glass-border);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicator {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator-dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background: #00ff88;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-shadow: 0 0 8px #00ff88;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone.drag-active {
|
||||||
|
border-color: var(--primary-cyan);
|
||||||
|
background: rgba(0, 242, 255, 0.05);
|
||||||
|
transform: scale(1.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcription-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-circle {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-text {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-main);
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcription-status h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
background: linear-gradient(90deg, var(--primary-cyan), var(--primary-magenta));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
animation: shimmer 2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% { filter: hue-rotate(0deg); }
|
||||||
|
100% { filter: hue-rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Models View */
|
||||||
|
.models-view h2 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.models-list .glass:hover {
|
||||||
|
border-color: var(--primary-cyan);
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
transform: translateY(-5px);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user