fix: Improve Whisper server startup with async polling and reduce logs
- Make server startup async to avoid Bun's 10s timeout - Add frontend polling to detect when server is ready - Use PowerShell Get-NetTCPConnection for reliable port detection - Add starting state to prevent multiple simultaneous starts - Reduce verbose logging, keep only essential info - Add dev-dist and nul to gitignore
This commit is contained in:
@@ -12,6 +12,7 @@ const WHISPER_SCRIPT = join(import.meta.dir, '..', 'whisper_server.py')
|
||||
interface WhisperState {
|
||||
enabled: boolean
|
||||
running: boolean
|
||||
starting: boolean // Prevents multiple simultaneous start attempts
|
||||
process: Subprocess | null
|
||||
model: string
|
||||
device: string
|
||||
@@ -20,8 +21,9 @@ interface WhisperState {
|
||||
const state: WhisperState = {
|
||||
enabled: false,
|
||||
running: false,
|
||||
starting: false,
|
||||
process: null,
|
||||
model: 'medium',
|
||||
model: 'large-v3',
|
||||
device: 'cuda'
|
||||
}
|
||||
|
||||
@@ -46,89 +48,104 @@ async function killProcessOnPort(port: number): Promise<void> {
|
||||
* Start the Whisper Python server
|
||||
*/
|
||||
export async function startWhisperServer(): Promise<boolean> {
|
||||
// Prevent multiple simultaneous start attempts
|
||||
if (state.starting) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (state.running && state.process) {
|
||||
console.log('[Whisper] Server already running')
|
||||
return true
|
||||
}
|
||||
|
||||
console.log('[Whisper] ====== STARTING (v3) ======')
|
||||
console.log('[Whisper] Script:', WHISPER_SCRIPT)
|
||||
state.starting = true
|
||||
console.log(`[Whisper] Starting (${state.model})...`)
|
||||
|
||||
// Kill any existing process on the port
|
||||
console.log('[Whisper] Cleaning up port', WHISPER_PORT)
|
||||
await killProcessOnPort(WHISPER_PORT)
|
||||
|
||||
try {
|
||||
// Use Bun.spawn with inherit to show logs directly in console
|
||||
const proc = Bun.spawn(['python', WHISPER_SCRIPT], {
|
||||
// -u flag disables Python output buffering for real-time logs
|
||||
const proc = Bun.spawn(['python', '-u', WHISPER_SCRIPT], {
|
||||
cwd: join(import.meta.dir, '..'),
|
||||
stdout: 'inherit',
|
||||
stderr: 'inherit',
|
||||
env: { ...process.env }
|
||||
env: { ...process.env, PYTHONUNBUFFERED: '1' }
|
||||
})
|
||||
|
||||
state.process = proc
|
||||
|
||||
// Wait a bit for the server to start, then check if port is listening
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
// Wait a bit for the server to start
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// Check if process is still running
|
||||
if (proc.exitCode !== null) {
|
||||
console.error('[Whisper] Process exited with code:', proc.exitCode)
|
||||
state.process = null
|
||||
state.starting = false
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if port is listening (simple TCP check)
|
||||
// Check if WebSocket is ready
|
||||
const isListening = await checkPort(WHISPER_PORT)
|
||||
|
||||
if (isListening) {
|
||||
console.log('[Whisper] Server started successfully on port', WHISPER_PORT)
|
||||
console.log('[Whisper] Ready')
|
||||
state.running = true
|
||||
state.enabled = true
|
||||
state.starting = false
|
||||
return true
|
||||
}
|
||||
|
||||
// Wait more if model is still loading (up to 90 seconds total)
|
||||
console.log('[Whisper] Waiting for model to load...')
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// Wait more if model is still loading (up to 120 seconds total for large models)
|
||||
for (let i = 0; i < 40; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 3000))
|
||||
|
||||
if (proc.exitCode !== null) {
|
||||
console.error('[Whisper] Process died while loading')
|
||||
console.error('[Whisper] Process died')
|
||||
state.process = null
|
||||
state.starting = false
|
||||
return false
|
||||
}
|
||||
|
||||
if (await checkPort(WHISPER_PORT)) {
|
||||
console.log('[Whisper] Server ready!')
|
||||
const ready = await checkPort(WHISPER_PORT)
|
||||
if (ready) {
|
||||
console.log('[Whisper] Ready')
|
||||
state.running = true
|
||||
state.enabled = true
|
||||
state.starting = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Whisper] Timeout waiting for server')
|
||||
console.error('[Whisper] Timeout (120s)')
|
||||
state.starting = false
|
||||
return false
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('[Whisper] Failed to start:', err.message)
|
||||
console.error('[Whisper] Error:', err.message)
|
||||
state.process = null
|
||||
state.starting = false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a port is listening using PowerShell
|
||||
* Check if Whisper WebSocket is ready using PowerShell
|
||||
*/
|
||||
async function checkPort(port: number): Promise<boolean> {
|
||||
try {
|
||||
const proc = Bun.spawn(['powershell', '-Command',
|
||||
`if (Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }`
|
||||
], { stdout: 'ignore', stderr: 'ignore' })
|
||||
const proc = Bun.spawn(['powershell', '-NoProfile', '-Command',
|
||||
`$c = Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue; if ($c) { Write-Output 'LISTENING' }`
|
||||
], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore'
|
||||
})
|
||||
|
||||
const exitCode = await proc.exited
|
||||
return exitCode === 0
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
|
||||
return output.trim() === 'LISTENING'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -139,35 +156,43 @@ async function checkPort(port: number): Promise<boolean> {
|
||||
*/
|
||||
export function stopWhisperServer(): boolean {
|
||||
if (!state.process) {
|
||||
console.log('[Whisper] No server running')
|
||||
return true
|
||||
}
|
||||
|
||||
console.log('[Whisper] Stopping server...')
|
||||
|
||||
try {
|
||||
state.process.kill()
|
||||
state.process = null
|
||||
state.running = false
|
||||
state.enabled = false
|
||||
console.log('[Whisper] Server stopped')
|
||||
console.log('[Whisper] Stopped')
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error('[Whisper] Error stopping server:', err)
|
||||
console.error('[Whisper] Stop error:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle Whisper server on/off
|
||||
* Toggle Whisper server on/off (async - returns immediately when starting)
|
||||
*/
|
||||
export async function toggleWhisperServer(): Promise<{ enabled: boolean; success: boolean }> {
|
||||
export async function toggleWhisperServer(): Promise<{ enabled: boolean; success: boolean; starting: boolean }> {
|
||||
// Prevent toggle while starting
|
||||
if (state.starting) {
|
||||
return { enabled: false, success: false, starting: true }
|
||||
}
|
||||
|
||||
if (state.enabled && state.running) {
|
||||
const success = stopWhisperServer()
|
||||
return { enabled: false, success }
|
||||
return { enabled: false, success, starting: false }
|
||||
} else {
|
||||
const success = await startWhisperServer()
|
||||
return { enabled: success, success }
|
||||
// Start server in background - don't await
|
||||
startWhisperServer().catch(err => {
|
||||
console.error('[Whisper] Start error:', err)
|
||||
state.starting = false
|
||||
})
|
||||
|
||||
// Return immediately - frontend will poll for status
|
||||
return { enabled: false, success: true, starting: true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,26 +202,30 @@ export async function toggleWhisperServer(): Promise<{ enabled: boolean; success
|
||||
export async function getWhisperState(): Promise<{
|
||||
enabled: boolean
|
||||
running: boolean
|
||||
starting: boolean
|
||||
port: number
|
||||
model: string
|
||||
device: string
|
||||
}> {
|
||||
// Check if port is actually listening
|
||||
const isListening = await checkPort(WHISPER_PORT)
|
||||
// Check if port is actually listening (skip if starting to avoid interference)
|
||||
if (!state.starting) {
|
||||
const isListening = await checkPort(WHISPER_PORT)
|
||||
|
||||
// Sync state with reality
|
||||
if (isListening && !state.running) {
|
||||
state.running = true
|
||||
state.enabled = true
|
||||
} else if (!isListening && state.running) {
|
||||
state.running = false
|
||||
state.enabled = false
|
||||
state.process = null
|
||||
// Sync state with reality
|
||||
if (isListening && !state.running) {
|
||||
state.running = true
|
||||
state.enabled = true
|
||||
} else if (!isListening && state.running) {
|
||||
state.running = false
|
||||
state.enabled = false
|
||||
state.process = null
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
running: state.running,
|
||||
starting: state.starting,
|
||||
port: WHISPER_PORT,
|
||||
model: state.model,
|
||||
device: state.device
|
||||
|
||||
Reference in New Issue
Block a user