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
+56 -26
View File
@@ -395,7 +395,15 @@ async function optimizeAudio(input: string): Promise<string> {
// 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<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
let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
if (!key) {
@@ -405,12 +413,11 @@ async function transcribeWithGeminiAPI(file: string): Promise<string> {
/* 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";
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<string> {
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<string> {
},
],
};
// 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(60_000),
signal: AbortSignal.timeout(120_000),
},
);
if (!res.ok) return "";
if (res.ok) {
const data: any = await res.json();
return data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
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 {
return "";
/* 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<string> {
async function transcribeWithEnne2(file: string): Promise<TranscriptResult> {
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<string> {
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<string> {
async function transcribeAudio(file: string): Promise<TranscriptResult> {
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");