Rilevamento silenzio (evita allucinazioni su audio vuoto) + gestione risposte vuote/malformate
This commit is contained in:
+45
-5
@@ -10,7 +10,7 @@
|
|||||||
* L'agente (pi) decide quale strumento usare in base al task.
|
* 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 fs from "node:fs";
|
||||||
import * as os from "node:os";
|
import * as os from "node:os";
|
||||||
import * as path from "node:path";
|
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
|
// 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)
|
||||||
|
// 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
|
// Risultato trascrizione con dettaglio errore
|
||||||
interface TranscriptResult {
|
interface TranscriptResult {
|
||||||
text: string;
|
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>)" };
|
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";
|
const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
|
||||||
try {
|
try {
|
||||||
// Comprimi in MP3 per evitare HTTP 413 su registrazioni lunghe
|
// Comprimi in MP3 per evitare HTTP 413 su registrazioni lunghe
|
||||||
@@ -458,11 +489,14 @@ async function transcribeWithGeminiAPI(file: string): Promise<TranscriptResult>
|
|||||||
signal: AbortSignal.timeout(120_000),
|
signal: AbortSignal.timeout(120_000),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data: any = await res.json();
|
const data: any = await res.json();
|
||||||
const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
const text = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
||||||
return { text };
|
if (!text) {
|
||||||
|
return { text: "", error: "La Gemini API ha restituito una risposta vuota" };
|
||||||
}
|
}
|
||||||
|
return { text };
|
||||||
|
}
|
||||||
lastErr = `HTTP ${res.status}`;
|
lastErr = `HTTP ${res.status}`;
|
||||||
const errBody = await res.text().catch(() => "");
|
const errBody = await res.text().catch(() => "");
|
||||||
try {
|
try {
|
||||||
@@ -519,6 +553,12 @@ 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 ?? "";
|
||||||
|
|
||||||
|
// rileva silenzio
|
||||||
|
if (!audioHasSpeech(file)) {
|
||||||
|
return { text: "", error: "Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)" };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const b64 = fs.readFileSync(file).toString("base64");
|
const b64 = fs.readFileSync(file).toString("base64");
|
||||||
const body = {
|
const body = {
|
||||||
|
|||||||
Reference in New Issue
Block a user