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
|
// 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");
|
||||||
|
|||||||
Reference in New Issue
Block a user