Gestione errori trascrizione esplicita: timeout 120s, retry su 429/500, report errori reali
This commit is contained in:
+66
-36
@@ -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> {
|
||||
},
|
||||
],
|
||||
};
|
||||
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<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");
|
||||
|
||||
Reference in New Issue
Block a user