Rilevamento silenzio (evita allucinazioni su audio vuoto) + gestione risposte vuote/malformate
This commit is contained in:
+41
-1
@@ -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<string> {
|
||||
|
||||
// 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<TranscriptResult>
|
||||
}
|
||||
if (!key) return { text: "", error: "Key Gemini mancante (imposta con /agy:config set geminiApiKey <chiave>)" };
|
||||
|
||||
// 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
|
||||
@@ -461,6 +492,9 @@ async function transcribeWithGeminiAPI(file: string): Promise<TranscriptResult>
|
||||
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}`;
|
||||
@@ -519,6 +553,12 @@ 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 ?? "";
|
||||
|
||||
// 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 = {
|
||||
|
||||
Reference in New Issue
Block a user