From cb726a498e3d707ce942d8e42cc6a1d8c47f2fec Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 10 Aug 2026 13:04:27 +0200 Subject: [PATCH] Rilevamento silenzio (evita allucinazioni su audio vuoto) + gestione risposte vuote/malformate --- extensions/index.ts | 50 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/extensions/index.ts b/extensions/index.ts index 3148b4a..f0436b0 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -10,7 +10,7 @@ * L'agente (pi) decide quale strumento usare in base al task. */ -import { execFile, spawn } from "node:child_process"; +import { execFile, execFileSync, spawn } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -395,6 +395,32 @@ 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) +// Rileva se l'audio contiene parlato reale (non solo silenzio) +// Ritorna true se c'è segnale, false se è silenzio +function audioHasSpeech(file: string): boolean { + try { + const { stdout } = execFileSync( + "ffmpeg", + ["-i", file, "-af", "volumedetect", "-f", "null", "-"], + { timeout: 30_000, encoding: "utf8" }, + ); + const maxMatch = stdout.match(/max_volume: ([\-0-9.]+) dB/); + const meanMatch = stdout.match(/mean_volume: ([\-0-9.]+) dB/); + if (maxMatch) { + const max = parseFloat(maxMatch[1]); + // max < -35dB ≈ silenzio quasi totale + if (max < -35) return false; + } + if (meanMatch) { + const mean = parseFloat(meanMatch[1]); + if (mean < -45) return false; + } + return true; + } catch { + return true; // se non possiamo verificare, assumiamo che ci sia parlato + } +} + // Risultato trascrizione con dettaglio errore interface TranscriptResult { text: string; @@ -415,6 +441,11 @@ async function transcribeWithGeminiAPI(file: string): Promise } if (!key) return { text: "", error: "Key Gemini mancante (imposta con /agy:config set geminiApiKey )" }; + // rileva silenzio prima di chiamare l'API (evita allucinazioni su audio vuoto) + if (!audioHasSpeech(file)) { + return { text: "", error: "Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)" }; + } + const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash"; try { // Comprimi in MP3 per evitare HTTP 413 su registrazioni lunghe @@ -458,11 +489,14 @@ async function transcribeWithGeminiAPI(file: string): Promise 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 }; + if (res.ok) { + const data: any = await res.json(); + const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? ""; + if (!text) { + return { text: "", error: "La Gemini API ha restituito una risposta vuota" }; } + return { text }; + } lastErr = `HTTP ${res.status}`; const errBody = await res.text().catch(() => ""); try { @@ -519,6 +553,12 @@ 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 ?? ""; + + // rileva silenzio + if (!audioHasSpeech(file)) { + return { text: "", error: "Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)" }; + } + try { const b64 = fs.readFileSync(file).toString("base64"); const body = {