UNPKG

whisper-node-server

Version:

Local audio transcription on CPU. Node.js bindings for OpenAI's Whisper. Modified from node-whisper

211 lines (177 loc) 6.37 kB
import path from 'path' import shell, { shellExec, initializeWhisperCpp, IShellOptions } from './shell'; import { createCppCommand, IFlagTypes } from './whisper'; import transcriptToArray, { ITranscriptLine } from './tsToArray'; import fetch from 'node-fetch'; import FormData from 'form-data'; import fs from 'fs'; import { ChildProcess } from 'child_process'; interface IOptions { modelName?: string, modelPath?: string, whisperOptions?: IFlagTypes, shellOptions?: IShellOptions, serverUrl?: string } interface IServerState { isRunning: boolean; process: ChildProcess | null; port: number; modelName?: string; modelPath?: string; } let serverState: IServerState = { isRunning: false, process: null, port: 8080 }; // Handle process termination function setupCleanupHandlers() { const cleanup = async () => { await whisperCleanup(); process.exit(0); }; // Handle various termination signals process.on('SIGINT', cleanup); process.on('SIGTERM', cleanup); process.on('SIGHUP', cleanup); // Handle Windows-specific signals if (process.platform === 'win32') { process.on('SIGBREAK', cleanup); } // Handle uncaught exceptions process.on('uncaughtException', async (err) => { console.error('[whisper-node-server] Uncaught exception:', err); await cleanup(); }); } export async function whisperInit(options?: IOptions): Promise<void> { if (serverState.isRunning) { console.log("[whisper-node-server] Server already running on port", serverState.port); return; } try { // Ensure whisper.cpp is built await initializeWhisperCpp(); const command = createCppCommand({ modelName: options?.modelName, modelPath: options?.modelPath, options: options?.whisperOptions, isServer: true }); console.log("[whisper-node-server] Starting server with command:", command); serverState.process = shell(command, options?.shellOptions); serverState.modelName = options?.modelName; serverState.modelPath = options?.modelPath; // Set up process event handlers if (serverState.process) { serverState.process.on('error', (error) => { console.error("[whisper-node-server] Server process error:", error); serverState.isRunning = false; }); serverState.process.on('exit', (code, signal) => { console.log("[whisper-node-server] Server process exited with code:", code, "signal:", signal); serverState.isRunning = false; serverState.process = null; }); } // Wait for server to start await new Promise(resolve => setTimeout(resolve, 2000)); serverState.isRunning = true; // Set up cleanup handlers setupCleanupHandlers(); console.log("[whisper-node-server] Server started successfully on port", serverState.port); } catch (error) { console.error("[whisper-node-server] Failed to start server:", error); throw error; } } export async function whisperCleanup(): Promise<void> { if (!serverState.process) { return; } console.log("[whisper-node-server] Shutting down server..."); try { // Try graceful shutdown first serverState.process.kill('SIGTERM'); // Wait for process to exit gracefully await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => { if (serverState.process) { console.log("[whisper-node-server] Force killing server process..."); try { serverState.process.kill('SIGKILL'); } catch (e) { console.error("[whisper-node-server] Error during force kill:", e); } } resolve(); }, 5000); // Wait 5 seconds for graceful shutdown serverState.process!.once('exit', () => { clearTimeout(timeout); resolve(); }); serverState.process!.once('error', (err) => { clearTimeout(timeout); reject(err); }); }); } catch (error) { console.error("[whisper-node-server] Error during server shutdown:", error); } finally { serverState.process = null; serverState.isRunning = false; console.log("[whisper-node-server] Server shutdown complete"); } } // returns array[]: {start, end, speech} export const whisper = async (filePath: string, options?: IOptions): Promise<ITranscriptLine[]> => { try { console.log("[whisper-node-server] Transcribing:", filePath, "\n"); if (serverState.isRunning) { const serverUrl = options?.serverUrl || `http://localhost:${serverState.port}/inference`; const form = new FormData(); // Add the audio file form.append('file', fs.createReadStream(filePath)); // Add parameters as regular form fields form.append('response_format', 'verbose_json'); // Add optional whisper parameters if provided if (options?.whisperOptions) { Object.entries(options.whisperOptions).forEach(([key, value]) => { // Convert boolean values to lowercase strings as expected by the server const formValue = typeof value === 'boolean' ? String(value).toLowerCase() : String(value); form.append(key, formValue); }); } console.log('Sending request with options:', { response_format: 'verbose_json', ...options?.whisperOptions }); const response = await fetch(serverUrl, { method: 'POST', body: form }); if (!response.ok) { const text = await response.text(); console.error('Server response:', text); throw new Error(`Server error: ${response.statusText}`); } const result = await response.json(); return transcriptToArray(result); } else { // Fall back to one-off command if server isn't running const command = createCppCommand({ filePath: path.normalize(`"${filePath}"`), modelName: options?.modelName, modelPath: options?.modelPath ? `"${options?.modelPath}"` : undefined, options: options?.whisperOptions }); const transcript = await shellExec(command, options?.shellOptions); return transcriptToArray(transcript); } } catch (error) { console.error("[whisper-node-server] Error:", error); throw error; } }; export default whisper;