From c2924fee634f399260ac25c8be4fc9c2ce75fbbc Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 10 Aug 2026 00:57:35 +0200 Subject: [PATCH] Gestione errori trascrizione esplicita: timeout 120s, retry su 429/500, report errori reali --- extensions/index.ts | 102 ++++++++++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/extensions/index.ts b/extensions/index.ts index 161e0ea..3148b4a 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -395,7 +395,15 @@ async function optimizeAudio(input: string): Promise { // Trascrizione affidabile e veloce via Gemini API diretta // (agy CLI non supporta audio; la API supporta audio/wav nativamente) -async function transcribeWithGeminiAPI(file: string): Promise { +// Risultato trascrizione con dettaglio errore +interface TranscriptResult { + text: string; + error?: string; +} + +// Trascrizione affidabile e veloce via Gemini API diretta +// (agy CLI non supporta audio; la API supporta audio/mpeg nativamente) +async function transcribeWithGeminiAPI(file: string): Promise { // key da config file o env var let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? ""; if (!key) { @@ -405,12 +413,11 @@ async function transcribeWithGeminiAPI(file: string): Promise { /* ignora */ } } - if (!key) return ""; + if (!key) return { text: "", error: "Key Gemini mancante (imposta con /agy:config set geminiApiKey )" }; const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash"; try { - // Comprimi in MP3 per evitare HTTP 413 (payload troppo grande) su registrazioni lunghe - // (WAV non compresso supera facilmente i 20MB; MP3 è ~19x più piccolo) + // Comprimi in MP3 per evitare HTTP 413 su registrazioni lunghe let audioFile = file; let mimeType = "audio/wav"; if (file.toLowerCase().endsWith(".wav")) { @@ -418,13 +425,13 @@ async function transcribeWithGeminiAPI(file: string): Promise { try { await execFileAsync( "ffmpeg", - ["-y", "-i", file, "-c:a", "libmp3lame", "-q:a", "4", mp3], - { timeout: 30_000 }, + ["-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libmp3lame", "-q:a", "5", mp3], + { timeout: 60_000 }, ); audioFile = mp3; mimeType = "audio/mpeg"; - } catch { - /* fallback al WAV originale */ + } catch (e: any) { + return { text: "", error: `Conversione MP3 fallita: ${e.message}` }; } } const b64 = fs.readFileSync(audioFile).toString("base64"); @@ -438,20 +445,38 @@ async function transcribeWithGeminiAPI(file: string): Promise { }, ], }; - const res = await fetch( - `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - signal: AbortSignal.timeout(60_000), - }, - ); - if (!res.ok) return ""; - const data: any = await res.json(); - return data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? ""; - } catch { - return ""; + + // retry su errori transitori (429, 500, 503) + let lastErr = ""; + for (let attempt = 0; attempt < 3; attempt++) { + const res = await fetch( + `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${key}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: AbortSignal.timeout(120_000), + }, + ); + if (res.ok) { + const data: any = await res.json(); + const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? ""; + return { text }; + } + lastErr = `HTTP ${res.status}`; + const errBody = await res.text().catch(() => ""); + try { + const e = JSON.parse(errBody); + lastErr = `${lastErr}: ${e?.error?.message ?? ""}`.trim(); + } catch { + /* ignora */ + } + if (res.status !== 429 && res.status !== 500 && res.status !== 503) break; + await new Promise((r) => setTimeout(r, 1500 * (attempt + 1))); + } + return { text: "", error: lastErr || "Errore API Gemini sconosciuto" }; + } catch (e: any) { + return { text: "", error: `Errore API Gemini: ${e?.message ?? String(e)}` }; } } @@ -490,7 +515,7 @@ function getConversationContext(ctx: any, maxEntries = 8): string { } // Trascrizione via server locale ai.enne2.net (gemma4:E4B supporta audio) -async function transcribeWithEnne2(file: string): Promise { +async function transcribeWithEnne2(file: string): Promise { const baseUrl = getConfig("sttUrl") ?? "https://ai.enne2.net"; const model = getConfig("sttModel") ?? "gemma4:E4B"; const apiKey = getConfig("enne2ApiKey") ?? process.env.ENNE2_API_KEY ?? ""; @@ -515,18 +540,21 @@ async function transcribeWithEnne2(file: string): Promise { method: "POST", headers, body: JSON.stringify(body), - signal: AbortSignal.timeout(90_000), + signal: AbortSignal.timeout(120_000), }); - if (!res.ok) return ""; + if (!res.ok) { + return { text: "", error: `Errore server enne2: HTTP ${res.status}` }; + } const data: any = await res.json(); - return data?.choices?.[0]?.message?.content?.trim() ?? ""; - } catch { - return ""; + const text = data?.choices?.[0]?.message?.content?.trim() ?? ""; + return { text }; + } catch (e: any) { + return { text: "", error: `Errore server enne2: ${e?.message ?? String(e)}` }; } } // Dispatcher: sceglie il backend di trascrizione (gemini | enne2) -async function transcribeAudio(file: string): Promise { +async function transcribeAudio(file: string): Promise { const backend = getConfig("sttBackend") ?? "gemini"; if (backend === "enne2" || backend === "local") { return transcribeWithEnne2(file); @@ -998,14 +1026,15 @@ export default function agyExtension(pi: ExtensionAPI) { async execute(toolCallId, params, signal, onUpdate, ctx) { const p = params as any; toolUpdate(onUpdate, "agy_transcribe: trascrizione audio..."); - const transcript = await transcribeAudio(p.filePath); - if (!transcript) { + const tr = await transcribeAudio(p.filePath); + if (!tr.text) { return { - content: [{ type: "text", text: "Trascrizione fallita: key Gemini mancante o errore API." }], - details: { filePath: p.filePath }, + content: [{ type: "text", text: `Trascrizione fallita: ${tr.error ?? "vuota"}` }], + details: { filePath: p.filePath, error: tr.error }, isError: true, }; } + const transcript = tr.text; return { content: [{ type: "text", text: transcript }], details: { filePath: p.filePath, model: p.model ?? "gemini-3.5-flash" }, @@ -1213,12 +1242,13 @@ export default function agyExtension(pi: ExtensionAPI) { const optimized = await optimizeAudio(file); ctx.ui.notify("Trascrizione in corso...", "info"); - const transcript = await transcribeAudio(optimized); - if (!transcript) { + const tr = await transcribeAudio(optimized); + if (!tr.text) { ctx.ui.setStatus("agy-rec", ""); - ctx.ui.notify("Trascrizione vuota o errore (key Gemini mancante?)", "error"); + ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error"); return; } + const transcript = tr.text; // Interpreta con Gemini (testo) usando il contesto della conversazione ctx.ui.notify("Interpretazione con Gemini...", "info");