Mes canvis
This commit is contained in:
@@ -87,7 +87,7 @@ router.post('/upload', upload.single('file'), async (req, res) => {
|
||||
tmpTxt = txtPath;
|
||||
await writeFileAsync(txtPath, transcription);
|
||||
|
||||
const llmResponse = await llamacppService.chatWithPreamble(transcription).catch(
|
||||
const llmResponse = await llamacppService.chatWithMcpTools(transcription).catch(
|
||||
(err: unknown) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(`[audio] llama.cpp failed: ${msg}`);
|
||||
|
||||
@@ -4,7 +4,7 @@ import router from './routes/router.js';
|
||||
import { getAppPort, getConfig } from './config.js';
|
||||
import { whisperService } from './services/whisper.service.js';
|
||||
import { piperService as piperWorker } from './services/piper.service.js';
|
||||
import { mcpClient } from './services/mcpClient.service.js';
|
||||
import { mcpClient } from './services/mcp.service.js';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -30,12 +30,9 @@ const server = app.listen(getAppPort(), async () => {
|
||||
console.log(`QuiBot backend listening on port ${getAppPort()}`);
|
||||
whisperService.spawn();
|
||||
piperWorker.initWav().catch(() => { /* model may not exist yet → lazy init on first TTS call */ });
|
||||
try {
|
||||
await mcpClient.start();
|
||||
console.log('[server] MCP client started');
|
||||
} catch (err) {
|
||||
console.error(`[server] MCP client failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
mcpClient.connect().catch((err) => {
|
||||
console.error(`[mcp] Failed to start MCP client: ${err instanceof Error ? err.message : String(err)}`);
|
||||
});
|
||||
});
|
||||
|
||||
async function shutdown(signal: string) {
|
||||
|
||||
@@ -1,54 +1,171 @@
|
||||
import { getLlamacppUrl, getLlamacppApiKey, getLlamacppPreamble } from '../config.js';
|
||||
import { mcpClient, McpToolDef } from './mcp.service.js';
|
||||
|
||||
interface LlamaRequest {
|
||||
messages: Array<{ role: string; content: string }>;
|
||||
interface LlamaMessage {
|
||||
role: string;
|
||||
content?: string | null;
|
||||
tool_call_id?: string;
|
||||
tool_calls?: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
}
|
||||
|
||||
interface LlamaChatChoice {
|
||||
message: {
|
||||
content: string;
|
||||
interface LlamaToolCallResult {
|
||||
content: Array<{
|
||||
type: string;
|
||||
text?: string;
|
||||
}>;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
interface LlamaToolDefinition {
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: object;
|
||||
};
|
||||
}
|
||||
|
||||
interface LlamaRequest {
|
||||
messages: LlamaMessage[];
|
||||
tools?: LlamaToolDefinition[];
|
||||
tool_choice?: 'auto' | 'none';
|
||||
}
|
||||
|
||||
interface LlamaResponseChoice {
|
||||
message?: {
|
||||
content?: string;
|
||||
tool_calls?: Array<{
|
||||
id: string;
|
||||
type: string;
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface LlamaResponse {
|
||||
choices?: LlamaChatChoice[];
|
||||
choices?: LlamaResponseChoice[];
|
||||
}
|
||||
|
||||
const MAX_TOOL_ITERATIONS = 10;
|
||||
|
||||
export const llamacppService = {
|
||||
async chat(messages: Array<{ role: string; content: string }>): Promise<string> {
|
||||
let history: LlamaMessage[] = messages.map(m => ({ role: m.role, content: m.content }));
|
||||
|
||||
const apiUrl = getLlamacppUrl();
|
||||
if (!apiUrl) {
|
||||
return '';
|
||||
}
|
||||
if (!apiUrl) return '';
|
||||
|
||||
const apiKey = getLlamacppApiKey();
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) {
|
||||
headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
|
||||
const request: LlamaRequest = { messages: history };
|
||||
const res = await fetch(apiUrl, { method: 'POST', headers, body: JSON.stringify(request) });
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`llama.cpp request failed (${res.status}): ${text.slice(0, 300)}`);
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ messages } satisfies LlamaRequest),
|
||||
});
|
||||
const data = (await res.json()) as LlamaResponse;
|
||||
const choice = data.choices?.[0];
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
throw new Error(`llama.cpp request failed (${response.status}): ${text.slice(0, 300)}`);
|
||||
if (!choice?.message || !choice.message.content) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const data = (await response.json()) as LlamaResponse;
|
||||
const content = data.choices?.[0]?.message?.content?.trim() ?? '';
|
||||
return content;
|
||||
return choice.message.content.trim();
|
||||
},
|
||||
|
||||
async chatWithPreamble(userText: string): Promise<string> {
|
||||
const preamble = getLlamacppPreamble();
|
||||
const messages = preamble ? [
|
||||
{ role: 'system', content: preamble },
|
||||
{ role: 'user', content: userText },
|
||||
] : [{ role: 'user', content: userText }];
|
||||
return this.chat(messages);
|
||||
const msgs = preamble
|
||||
? [{ role: 'system' as const, content: preamble }, { role: 'user' as const, content: userText }]
|
||||
: [{ role: 'user' as const, content: userText }];
|
||||
return this.chat(msgs);
|
||||
},
|
||||
|
||||
async chatWithMcpTools(userText: string): Promise<string> {
|
||||
const preamble = getLlamacppPreamble();
|
||||
const initialMessages: LlamaMessage[] = preamble
|
||||
? [{ role: 'system', content: preamble }, { role: 'user', content: userText }]
|
||||
: [{ role: 'user', content: userText }];
|
||||
|
||||
if (!mcpClient.getReady()) {
|
||||
console.log('[llama] MCP not ready, falling back to preamble-only chat');
|
||||
return this.chatWithPreamble(userText);
|
||||
}
|
||||
|
||||
const tools = mcpClient.getTools();
|
||||
const llamaTools: LlamaToolDefinition[] = tools.map((t) => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: t.name,
|
||||
description: t.description,
|
||||
parameters: t.inputSchema,
|
||||
},
|
||||
}));
|
||||
|
||||
return this._chatWithTools(initialMessages, llamaTools);
|
||||
},
|
||||
|
||||
async _chatWithTools(messages: LlamaMessage[], tools: LlamaToolDefinition[]): Promise<string> {
|
||||
const apiUrl = getLlamacppUrl();
|
||||
if (!apiUrl) return '';
|
||||
|
||||
let iter = 0;
|
||||
const history: LlamaMessage[] = messages.map((m) => ({ ...m }));
|
||||
|
||||
while (iter < MAX_TOOL_ITERATIONS) {
|
||||
const apiKey = getLlamacppApiKey();
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
||||
|
||||
const body: LlamaRequest = { messages: history, tools, tool_choice: 'auto' };
|
||||
const res = await fetch(apiUrl, { method: 'POST', headers, body: JSON.stringify(body) });
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
throw new Error(`llama.cpp request failed (${res.status}): ${text.slice(0, 300)}`);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as LlamaResponse;
|
||||
const choice = data.choices?.[0];
|
||||
|
||||
if (!choice?.message) {
|
||||
throw new Error('llama.cpp response has no message');
|
||||
}
|
||||
|
||||
if (!choice.message.tool_calls || choice.message.tool_calls.length === 0) {
|
||||
return choice.message.content?.trim() ?? '';
|
||||
}
|
||||
|
||||
history.push({ role: 'assistant', ...choice.message });
|
||||
|
||||
for (const toolCall of choice.message.tool_calls) {
|
||||
let resultText = '';
|
||||
try {
|
||||
const args = JSON.parse(toolCall.function.arguments);
|
||||
resultText = await mcpClient.callTool(toolCall.function.name, args);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
resultText = `Error calling tool "${toolCall.function.name}": ${msg}`;
|
||||
}
|
||||
|
||||
history.push({
|
||||
role: 'tool',
|
||||
content: resultText,
|
||||
tool_call_id: toolCall.id,
|
||||
});
|
||||
}
|
||||
|
||||
iter++;
|
||||
}
|
||||
|
||||
throw new Error(`Exceeded max tool-call iterations (${MAX_TOOL_ITERATIONS})`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { getMcpUrl } from '../config';
|
||||
|
||||
class McpHttpService {
|
||||
private sessionId: string | null = null;
|
||||
|
||||
async callTool(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||
const baseUrl = getMcpUrl();
|
||||
if (!baseUrl) {
|
||||
throw new Error('MCP HTTP service not configured (set MCP_URL env var)');
|
||||
}
|
||||
|
||||
const url = `${baseUrl}/mcp`;
|
||||
|
||||
if (!this.sessionId) {
|
||||
const initRes = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 1,
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2025-03-26',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'quibot-backend', version: '1.0.0' },
|
||||
},
|
||||
}),
|
||||
});
|
||||
const initData = await initRes.json();
|
||||
this.sessionId = String(initData.sessionId || initData.result?.sessionId);
|
||||
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
method: 'notifications/initialized',
|
||||
}),
|
||||
...(this.sessionId && { headers: { 'Mcp-SessionId': this.sessionId } }),
|
||||
});
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.sessionId && { 'Mcp-SessionId': this.sessionId }),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: Date.now(),
|
||||
method: 'tools/call',
|
||||
params: { name, arguments: args },
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
return { text: JSON.stringify(data.error), isError: true };
|
||||
}
|
||||
const content = data.result?.content?.[0];
|
||||
if (!content?.text) {
|
||||
throw new Error('MCP tool returned no content');
|
||||
}
|
||||
return { text: content.text };
|
||||
}
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
this.sessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpHttpService = new McpHttpService();
|
||||
105
backend/src/services/mcp.service.ts
Normal file
105
backend/src/services/mcp.service.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
||||
import { getMcpUrl } from '../config.js';
|
||||
|
||||
export interface McpToolDef {
|
||||
name: string;
|
||||
description: string;
|
||||
inputSchema: object;
|
||||
}
|
||||
|
||||
let mc: Client | null = null;
|
||||
let cachedTools: McpToolDef[] = [];
|
||||
let connected = false;
|
||||
let connecting = false;
|
||||
|
||||
async function connectInternal(): Promise<void> {
|
||||
if (connected) return;
|
||||
if (connecting) throw new Error('MCP client connection already in progress');
|
||||
connecting = true;
|
||||
|
||||
const rawUrl = getMcpUrl();
|
||||
if (!rawUrl) {
|
||||
console.warn('[mcp] MCP_URL not configured, tools disabled');
|
||||
connecting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the URL points at the /sse endpoint (FastMCP default)
|
||||
let connectUrl = rawUrl;
|
||||
try {
|
||||
const u = new URL(rawUrl);
|
||||
if (u.pathname === '/' || u.pathname === '') {
|
||||
u.pathname = '/sse';
|
||||
connectUrl = u.toString();
|
||||
}
|
||||
} catch {
|
||||
// not a valid URL, use as-is
|
||||
}
|
||||
|
||||
console.log(`[mcp] Connecting to ${connectUrl}...`);
|
||||
|
||||
try {
|
||||
mc = new Client(
|
||||
{ name: 'quibot-backend', version: '1.0.0' },
|
||||
{ capabilities: {} },
|
||||
);
|
||||
|
||||
const transport = new SSEClientTransport(new URL(connectUrl));
|
||||
await mc.connect(transport);
|
||||
|
||||
console.log('[mcp] Connected, listing tools...');
|
||||
const toolsResult = await mc.listTools();
|
||||
cachedTools = (toolsResult.tools ?? []).map((t) => ({
|
||||
name: t.name,
|
||||
description: t.description ?? '',
|
||||
inputSchema: t.inputSchema as object,
|
||||
}));
|
||||
|
||||
connected = true;
|
||||
connecting = false;
|
||||
console.log(`[mcp] Connected to MCP server with ${cachedTools.length} tool(s): ${cachedTools.map((t) => t.name).join(', ') || '(none)'}`);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
connecting = false;
|
||||
console.error(`[mcp] Connection failed: ${msg}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpClient = {
|
||||
async connect(): Promise<void> {
|
||||
await connectInternal();
|
||||
},
|
||||
|
||||
getReady(): boolean {
|
||||
return connected;
|
||||
},
|
||||
|
||||
getTools(): readonly McpToolDef[] {
|
||||
return cachedTools;
|
||||
},
|
||||
|
||||
async callTool(name: string, args: Record<string, unknown>): Promise<string> {
|
||||
if (!mc) throw new Error('MCP client not connected');
|
||||
const result = await mc.callTool({ name, arguments: args });
|
||||
const content = result.content as Array<{ type: string; text?: string }>;
|
||||
const texts = content
|
||||
.filter((c) => c.type === 'text')
|
||||
.map((c) => c.text ?? '');
|
||||
return texts.join('\n') || '[MCP tool returned no text content]';
|
||||
},
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (mc) {
|
||||
try {
|
||||
await mc.close();
|
||||
} catch {
|
||||
// ignore close errors on shutdown
|
||||
}
|
||||
mc = null;
|
||||
}
|
||||
connected = false;
|
||||
cachedTools = [];
|
||||
},
|
||||
};
|
||||
@@ -1,202 +0,0 @@
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { getMcpUrl } from '../config';
|
||||
import { mcpHttpService } from './mcp.http.service';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = join(__filename, '..');
|
||||
|
||||
// Path to the compiled MCP server (two levels up from backend/src/)
|
||||
const MCP_BIN = join(__dirname, '..', '..', 'mcp', 'dist', 'index.js');
|
||||
|
||||
let _proc: ChildProcess | null = null;
|
||||
let nextId = 1;
|
||||
let pending = new Map<number | string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
||||
|
||||
function send(msg: Record<string, unknown>): number {
|
||||
const id = nextId++;
|
||||
_proc!.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, ...msg }) + '\n');
|
||||
return id;
|
||||
}
|
||||
|
||||
export const mcpClient = {
|
||||
async start(): Promise<void> {
|
||||
const hasMcpBin = (() => {
|
||||
try {
|
||||
require('fs').accessSync(MCP_BIN);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!hasMcpBin) {
|
||||
const url = getMcpUrl();
|
||||
if (url) {
|
||||
console.log('[mcp] Local MCP binary not found, using HTTP service at', url);
|
||||
return;
|
||||
}
|
||||
throw new Error('MCP local binary and HTTP URL both unavailable');
|
||||
}
|
||||
|
||||
if (_proc) return;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
_proc = spawn('node', [MCP_BIN], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } });
|
||||
|
||||
_proc.stdout!.on('data', (chunk: Buffer) => {
|
||||
const text = chunk.toString();
|
||||
for (const line of text.split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
let parsed: { jsonrpc?: string; id?: number | string; method?: string; result?: unknown; error?: unknown };
|
||||
try { parsed = JSON.parse(line); } catch { continue; }
|
||||
if (parsed.jsonrpc !== '2.0') continue;
|
||||
if (parsed.method) {
|
||||
// notifications or responses without matching id — ignore for now
|
||||
continue;
|
||||
}
|
||||
if (!parsed.id) continue;
|
||||
const p = pending.get(parsed.id);
|
||||
if (!p) continue;
|
||||
pending.delete(parsed.id);
|
||||
if (parsed.error) {
|
||||
p.reject(new Error(`MCP error: ${JSON.stringify(parsed.error)}`));
|
||||
} else {
|
||||
p.resolve(parsed.result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_proc.stderr!.on('data', (chunk: Buffer) => {
|
||||
console.log(`[mcp-client] stderr: ${chunk.toString().trim()}`);
|
||||
});
|
||||
|
||||
_proc.on('exit', (code, signal) => {
|
||||
console.error(`[mcp-client] Exited code=${code} signal=${signal}`);
|
||||
_proc = null;
|
||||
for (const [, p] of pending) {
|
||||
p.reject(new Error('MCP client process exited'));
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
_proc.on('error', (err: Error) => {
|
||||
console.error(`[mcp-client] Error: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// Send initialize request
|
||||
const initId = send({
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2025-03-26',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'quibot-backend', version: '1.0.0' },
|
||||
},
|
||||
});
|
||||
|
||||
pending.set(initId, {
|
||||
resolve: () => {
|
||||
// Send initialized notification
|
||||
_proc!.stdin!.write(
|
||||
JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n',
|
||||
);
|
||||
resolve();
|
||||
},
|
||||
reject,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const p = pending.get(initId);
|
||||
if (p) {
|
||||
pending.delete(initId);
|
||||
p.reject(new Error('MCP initialize timed out'));
|
||||
}
|
||||
}, 15_000);
|
||||
});
|
||||
},
|
||||
|
||||
async callTool(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||
if (!_proc) {
|
||||
await this.start();
|
||||
}
|
||||
|
||||
try {
|
||||
return await this._callToolLocal(name, args);
|
||||
} catch (localErr) {
|
||||
const url = getMcpUrl();
|
||||
if (url) {
|
||||
console.log(`[mcp] Local MCP failed: ${localErr instanceof Error ? localErr.message : localErr}. Falling back to HTTP service.`);
|
||||
return await mcpHttpService.callTool(name, args);
|
||||
}
|
||||
throw localErr;
|
||||
}
|
||||
},
|
||||
|
||||
async _callToolLocal(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||
if (!_proc?.stdin) {
|
||||
throw new Error('MCP client not ready');
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = send({
|
||||
method: 'tools/call',
|
||||
params: { name, arguments: args },
|
||||
});
|
||||
|
||||
let cleared = false;
|
||||
const timer = setTimeout(() => {
|
||||
if (cleared) return;
|
||||
cleared = true;
|
||||
pending.delete(id);
|
||||
reject(new Error(`MCP tool "${name}" timed out`));
|
||||
}, 180_000);
|
||||
|
||||
pending.set(id, {
|
||||
resolve: (result: unknown) => {
|
||||
if (cleared) return;
|
||||
cleared = true;
|
||||
clearTimeout(timer);
|
||||
pending.delete(id);
|
||||
const res = result as { content?: Array<{ type: string; text?: string }> };
|
||||
if (res?.content?.[0]?.text !== undefined) {
|
||||
resolve({ text: res.content[0].text });
|
||||
} else {
|
||||
reject(new Error('MCP tool returned no content'));
|
||||
}
|
||||
},
|
||||
reject: (err: Error) => {
|
||||
if (cleared) return;
|
||||
cleared = true;
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async shutdown(): Promise<void> {
|
||||
if (!_proc) return;
|
||||
_proc.kill('SIGTERM');
|
||||
const current = _proc;
|
||||
await new Promise<void>((resolve) => {
|
||||
let done = false;
|
||||
const cleanup = () => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
_proc = null;
|
||||
for (const [, p] of pending) {
|
||||
p.reject(new Error('MCP client shut down'));
|
||||
}
|
||||
pending.clear();
|
||||
resolve();
|
||||
};
|
||||
current.once('exit', () => cleanup());
|
||||
setTimeout(() => {
|
||||
const proc = _proc;
|
||||
if (proc && !proc.killed) proc.kill('SIGKILL');
|
||||
cleanup();
|
||||
}, 3000);
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -161,7 +161,10 @@ private resolveInitError(err: Error): void {
|
||||
// reject all pending (new format: {resolve, reject})
|
||||
for (const [, entry] of this.respMap) entry.reject(new Error('piper process exited'));
|
||||
this.respMap.clear();
|
||||
if (this.pendingInit) { this.initReject(new Error('piper process exited')); this.pendingInit = null; }
|
||||
if (this.pendingInit && this.initReject) {
|
||||
this.initReject(new Error('piper process exited'));
|
||||
this.pendingInit = null;
|
||||
}
|
||||
});
|
||||
|
||||
// ── cleanup old WAV files every 5 min ──
|
||||
|
||||
@@ -10,7 +10,7 @@ const SCRIPT_DIR = join(__dirname, '..');
|
||||
|
||||
const PYTHON = join(SCRIPT_DIR, '..', '.venv', 'bin', 'python3');
|
||||
|
||||
const whisperModel = process.env.WHISPER_MODEL ?? 'base';
|
||||
const whisperModel = process.env.WHISPER_MODEL ?? 'small';
|
||||
const whisperLanguage = process.env.WHISPER_LANGUAGE ?? 'ca';
|
||||
|
||||
interface TranscriptResult {
|
||||
|
||||
Reference in New Issue
Block a user