Aggiunta registrazione microfono (F12) con trascrizione Gemini e inserimento come prompt su pi

This commit is contained in:
dev
2026-08-09 20:43:39 +02:00
parent ef499126df
commit ef7071cea5
5 changed files with 153 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

+153 -1
View File
@@ -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 } from "node:child_process"; import { execFile, 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";
@@ -224,6 +224,85 @@ async function executeAgy(opts: AgyExecOptions) {
const IMG_SUFFIX = const IMG_SUFFIX =
"\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>"; "\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>";
// ---------------------------------------------------------------------------
// Registrazione microfono (F12) e trascrizione
// ---------------------------------------------------------------------------
const MAX_RECORD_MS = 120_000; // 2 min
interface Recording {
proc: ReturnType<typeof spawn>;
file: string;
}
let recording: Recording | null = null;
function startRecording(): string {
const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`);
const proc = spawn(
"ffmpeg",
["-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", "120", file],
{ stdio: "ignore" },
);
recording = { proc, file };
// se ffmpeg termina da solo (timeout 2 min), azzera lo stato
proc.on("exit", () => {
if (recording && recording.proc === proc) recording = null;
});
return file;
}
function stopRecording(): Promise<string | null> {
return new Promise((resolve) => {
if (!recording) return resolve(null);
const { proc, file } = recording;
recording = null;
let done = false;
const finish = () => {
if (!done) {
done = true;
resolve(file);
}
};
proc.on("exit", finish);
proc.kill("SIGINT"); // ffmpeg finalizza il file
setTimeout(finish, 3000); // fallback
});
}
function extractText(content: any): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) {
return content
.filter((p: any) => p && p.type === "text" && typeof p.text === "string")
.map((p: any) => p.text)
.join(" ");
}
return "";
}
function getConversationContext(ctx: any, maxEntries = 8): string {
try {
const entries = ctx.sessionManager.getEntries();
const recent = entries.slice(-maxEntries);
const lines: string[] = [];
for (const e of recent) {
if (e.type !== "message" || !e.message) continue;
const text = extractText(e.message.content);
if (!text) continue;
const who =
e.message.role === "user"
? "Utente"
: e.message.role === "assistant"
? "Assistente"
: "Strumento";
lines.push(`${who}: ${text}`);
}
return lines.join("\n");
} catch {
return "";
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Estensione // Estensione
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -772,4 +851,77 @@ export default function agyExtension(pi: ExtensionAPI) {
ctx.ui.notify("Stato conversazione azzerato.", "info"); ctx.ui.notify("Stato conversazione azzerato.", "info");
}, },
}); });
// =========================================================================
// Registrazione microfono (F12) + trascrizione via Gemini + prompt su pi
// =========================================================================
async function handleRecordToggle(ctx: any) {
if (!recording) {
startRecording();
ctx.ui.notify("🎙️ Registrazione avviata (F12 per fermare, max 2 min)", "info");
ctx.ui.setStatus("agy-rec", "🔴 REGISTRAZIONE...");
return;
}
ctx.ui.setStatus("agy-rec", "⏹️ Finalizzazione...");
const file = await stopRecording();
if (!file) {
ctx.ui.setStatus("agy-rec", "");
ctx.ui.notify("Nessuna registrazione attiva", "warning");
return;
}
ctx.ui.notify("Registrazione fermata, trascrizione in corso...", "info");
const context = getConversationContext(ctx);
const res = await executeAgy({
prompt:
`Trascrivi e interpreta il file audio al percorso ${file}.` +
`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
`\n\nRestituisci la trascrizione fedele e una breve interpretazione.`,
mode: "analyze",
stateless: true,
filePaths: [file],
yolo: true,
});
ctx.ui.setStatus("agy-rec", "");
const transcription = res.text.trim();
if (!transcription) {
ctx.ui.notify("Trascrizione vuota o errore", "error");
return;
}
// Inserisci il risultato come prompt su pi
if (ctx.isIdle()) {
pi.sendUserMessage(transcription);
} else {
pi.sendUserMessage(transcription, { deliverAs: "followUp" });
}
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
}
pi.registerShortcut("f12", {
description: "Avvia/ferma registrazione microfono (max 2 min) e trascrive via Gemini",
handler: async (ctx) => {
await handleRecordToggle(ctx);
},
});
pi.registerCommand("agy:record", {
description: "Avvia/ferma registrazione microfono (come F12)",
handler: async (_args, ctx) => {
await handleRecordToggle(ctx);
},
});
pi.registerCommand("agy:record:stop", {
description: "Ferma la registrazione microfono in corso",
handler: async (_args, ctx) => {
if (!recording) {
ctx.ui.notify("Nessuna registrazione in corso", "warning");
return;
}
await handleRecordToggle(ctx);
},
});
} }