diff --git a/assets/user-avatar-cosmonauta.jpg b/assets/user-avatar-cosmonauta.jpg new file mode 100644 index 0000000..5b0aa0a Binary files /dev/null and b/assets/user-avatar-cosmonauta.jpg differ diff --git a/assets/user-avatar-soviet-busto.jpg b/assets/user-avatar-soviet-busto.jpg new file mode 100644 index 0000000..afd423f Binary files /dev/null and b/assets/user-avatar-soviet-busto.jpg differ diff --git a/assets/user-avatar-soviet-final.jpg b/assets/user-avatar-soviet-final.jpg new file mode 100644 index 0000000..a5aeeed Binary files /dev/null and b/assets/user-avatar-soviet-final.jpg differ diff --git a/assets/user-avatar-soviet.jpg b/assets/user-avatar-soviet.jpg new file mode 100644 index 0000000..809fe91 Binary files /dev/null and b/assets/user-avatar-soviet.jpg differ diff --git a/extensions/index.ts b/extensions/index.ts index 8464856..468f26e 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -10,7 +10,7 @@ * 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 os from "node:os"; import * as path from "node:path"; @@ -224,6 +224,85 @@ async function executeAgy(opts: AgyExecOptions) { const IMG_SUFFIX = "\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: "; +// --------------------------------------------------------------------------- +// Registrazione microfono (F12) e trascrizione +// --------------------------------------------------------------------------- +const MAX_RECORD_MS = 120_000; // 2 min + +interface Recording { + proc: ReturnType; + 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 { + 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 // --------------------------------------------------------------------------- @@ -772,4 +851,77 @@ export default function agyExtension(pi: ExtensionAPI) { 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); + }, + }); }