Aggiunto TTS via Gemini API: tool agy_tts, comando /agy:speak, notifiche vocali nel flusso F12
This commit is contained in:
@@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -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
|
||||
// =========================================================================
|
||||
@@ -986,6 +1088,11 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
pi.sendUserMessage(finalText, { deliverAs: "followUp" });
|
||||
}
|
||||
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", {
|
||||
@@ -1012,4 +1119,19 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
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");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user