Aggiunto TTS via Gemini API: tool agy_tts, comando /agy:speak, notifiche vocali nel flusso F12

This commit is contained in:
dev
2026-08-09 22:43:19 +02:00
parent a575fb6ca6
commit 8eece4506a
+122
View File
@@ -386,6 +386,74 @@ function getConversationContext(ctx: any, maxEntries = 8): string {
} }
} }
// ---------------------------------------------------------------------------
// TTS via Gemini API (nessun engine esterno)
// ---------------------------------------------------------------------------
async function ttsSpeak(text: string, outputDir?: string): Promise<string | null> {
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 null;
const model = process.env.AGY_TTS_MODEL ?? "gemini-2.5-flash-preview-tts";
try {
const body = {
contents: [{ parts: [{ text }] }],
generationConfig: { responseModalities: ["AUDIO"] },
};
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 null;
const data: any = await res.json();
const parts = data?.candidates?.[0]?.content?.parts ?? [];
const audioPart = parts.find((p: any) => p.inlineData);
if (!audioPart) return null;
// salva PCM raw (16-bit, 24kHz) e converti in WAV
const pcmFile = path.join(os.tmpdir(), `tts-${Date.now()}.pcm`);
fs.writeFileSync(pcmFile, Buffer.from(audioPart.inlineData.data, "base64"));
const wavFile = pcmFile.replace(".pcm", ".wav");
await execFileAsync(
"ffmpeg",
["-y", "-f", "s16le", "-ar", "24000", "-ac", "1", "-i", pcmFile, wavFile],
{ timeout: 30_000 },
);
fs.rmSync(pcmFile, { force: true });
// copia in outputDir se richiesto
let finalFile = wavFile;
if (outputDir) {
try {
fs.mkdirSync(outputDir, { recursive: true });
const dest = path.join(outputDir, `tts-${Date.now()}.wav`);
fs.copyFileSync(wavFile, dest);
finalFile = dest;
} catch {
/* ignora */
}
}
// riproduci (fire-and-forget)
execFileAsync("paplay", [finalFile], { timeout: 60_000 }).catch(() => {});
return finalFile;
} catch {
return null;
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Estensione // Estensione
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -827,6 +895,40 @@ export default function agyExtension(pi: ExtensionAPI) {
}, },
}); });
// =========================================================================
// TOOL: agy_tts — text-to-speech via Gemini API
// =========================================================================
pi.registerTool({
name: "agy_tts",
label: "agy TTS (text to speech)",
description:
"Converte un testo in audio (TTS) usando la Gemini API (gemini-2.5-flash-preview-tts) e lo riproduce. " +
"Utile per notifiche vocali, sintesi parlate e aggiornamenti di stato. " +
"Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
parameters: Type.Object({
text: Type.String({ description: "Il testo da pronunciare." }),
play: Type.Optional(Type.Boolean({ description: "true per riprodurre l'audio (default: true)." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove salvare il file audio." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
onUpdate?.("agy_tts: sintesi vocale...");
return ttsSpeak(p.text, p.outputDir).then((file) => {
if (!file) {
return {
content: [{ type: "text", text: "TTS fallito: key Gemini mancante o errore API." }],
details: {},
isError: true,
};
}
return {
content: [{ type: "text", text: `Audio TTS generato: ${file}` }],
details: { audioFile: file },
};
});
},
});
// ========================================================================= // =========================================================================
// TOOL: agy_models — elenca i modelli disponibili // TOOL: agy_models — elenca i modelli disponibili
// ========================================================================= // =========================================================================
@@ -986,6 +1088,11 @@ export default function agyExtension(pi: ExtensionAPI) {
pi.sendUserMessage(finalText, { deliverAs: "followUp" }); pi.sendUserMessage(finalText, { deliverAs: "followUp" });
} }
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info"); ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
// notifica vocale (disattivabile con AGY_TTS_NOTIFY=0)
if (process.env.AGY_TTS_NOTIFY !== "0") {
ttsSpeak("Trascrizione completata e inviata.").catch(() => {});
}
} }
pi.registerShortcut("f12", { pi.registerShortcut("f12", {
@@ -1012,4 +1119,19 @@ export default function agyExtension(pi: ExtensionAPI) {
await handleRecordToggle(ctx); await handleRecordToggle(ctx);
}, },
}); });
pi.registerCommand("agy:speak", {
description: "Pronuncia un testo con TTS Gemini. Uso: /agy:speak <testo>",
handler: async (args, ctx) => {
if (!args?.trim()) {
ctx.ui.notify("Uso: /agy:speak <testo>", "error");
return;
}
ctx.ui.setStatus("agy-tts", "🔊 sintesi vocale...");
const file = await ttsSpeak(args.trim());
ctx.ui.setStatus("agy-tts", "");
if (file) ctx.ui.notify(`🔊 Audio: ${file}`, "info");
else ctx.ui.notify("TTS fallito (key Gemini mancante?)", "error");
},
});
} }