From 910263d7d78826f0615217b9b361e7e27064bf71 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 9 Aug 2026 22:27:19 +0200 Subject: [PATCH] Trascrizione audio via Gemini API diretta (gemini-3.5-flash): veloce e affidabile --- extensions/index.ts | 95 ++++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/extensions/index.ts b/extensions/index.ts index f9e09ab..50cf54c 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -308,23 +308,45 @@ async function optimizeAudio(input: string): Promise { } } -// Trascrizione locale affidabile con faster-whisper (agy CLI non supporta audio) -async function transcribeWithWhisper(file: string): Promise { - const model = process.env.AGY_WHISPER_MODEL ?? "small"; - const script = ` -from faster_whisper import WhisperModel -import sys -model = WhisperModel('${model}', device='cpu', compute_type='int8') -segments, info = model.transcribe(sys.argv[1], language='it') -for seg in segments: - print(seg.text, end='') -`; +// 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 { + // key da env var o file config + let key = process.env.GEMINI_API_KEY ?? ""; + if (!key) { + try { + key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim(); + } catch { + /* ignora */ + } + } + if (!key) return ""; + + const model = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash"; try { - const { stdout } = await execFileAsync("python3", ["-c", script, file], { - timeout: 180_000, - maxBuffer: 10 * 1024 * 1024, - }); - return stdout.trim(); + const b64 = fs.readFileSync(file).toString("base64"); + const body = { + contents: [ + { + parts: [ + { text: "Trascrivi fedelmente il contenuto di questo audio in italiano. Restituisci SOLO la trascrizione testuale." }, + { inline_data: { mime_type: "audio/wav", data: b64 } }, + ], + }, + ], + }; + 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 ""; } @@ -737,40 +759,33 @@ export default function agyExtension(pi: ExtensionAPI) { }); // ========================================================================= - // TOOL: agy_transcribe — trascrizione audio + // TOOL: agy_transcribe — trascrizione audio (via Gemini API diretta) // ========================================================================= pi.registerTool({ name: "agy_transcribe", label: "agy transcribe audio", description: - "Trascrive il contenuto di un file audio (voce, discorso) in testo. " + - "Richiede --yolo (auto-approva gli strumenti).", + "Trascrive il contenuto di un file audio (voce, discorso) in testo usando la Gemini API diretta " + + "(veloce e affidabile; agy CLI non supporta audio). Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.", parameters: Type.Object({ filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }), language: Type.Optional(Type.String({ description: "Lingua del contenuto (es. 'italiano', 'english')." })), - model: Type.Optional(Type.String({ description: "Modello agy." })), + model: Type.Optional(Type.String({ description: "Modello Gemini (default: gemini-3.5-flash)." })), }), async execute(toolCallId, params, signal, onUpdate, ctx) { const p = params as any; - const lang = p.language ? ` La lingua del contenuto è ${p.language}.` : ""; - const prompt = - `Trascrivi il contenuto del file audio al percorso ${p.filePath}.` + - lang + - ` Restituisci la trascrizione testuale completa e fedele.`; - onUpdate?.("agy_transcribe: trascrizione audio..."); - const res = await executeAgy({ - prompt, - mode: "analyze", - model: p.model, - stateless: true, - filePaths: [p.filePath], - yolo: true, - signal, - }); + const transcript = await transcribeWithGeminiAPI(p.filePath); + if (!transcript) { + return { + content: [{ type: "text", text: "Trascrizione fallita: key Gemini mancante o errore API." }], + details: { filePath: p.filePath }, + isError: true, + }; + } return { - content: [{ type: "text", text: res.text }], - details: { filePath: p.filePath, exitCode: res.exitCode }, + content: [{ type: "text", text: transcript }], + details: { filePath: p.filePath, model: p.model ?? "gemini-3.5-flash" }, }; }, }); @@ -940,11 +955,11 @@ export default function agyExtension(pi: ExtensionAPI) { ctx.ui.notify("Registrazione fermata, ottimizzazione audio...", "info"); const optimized = await optimizeAudio(file); - ctx.ui.notify("Trascrizione in corso (faster-whisper)...", "info"); - const transcript = await transcribeWithWhisper(optimized); + ctx.ui.notify("Trascrizione in corso (Gemini API)...", "info"); + const transcript = await transcribeWithGeminiAPI(optimized); if (!transcript) { ctx.ui.setStatus("agy-rec", ""); - ctx.ui.notify("Trascrizione vuota o errore", "error"); + ctx.ui.notify("Trascrizione vuota o errore (key Gemini mancante?)", "error"); return; }