Gestione errori trascrizione esplicita: timeout 120s, retry su 429/500, report errori reali

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