From c7388dc91d4802b5f3eaa9b0d00d54a1d6980fa2 Mon Sep 17 00:00:00 2001 From: Matteo Benedetto Date: Mon, 10 Aug 2026 13:25:18 +0200 Subject: [PATCH] feat: timer registrazione, tasto Esc per annullare e nascondi output ffmpeg --- extensions/index.ts | 117 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 15 deletions(-) diff --git a/extensions/index.ts b/extensions/index.ts index 80eb927..17aa2b5 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, execFileSync, spawn } from "node:child_process"; +import { execFile, execFileSync, spawn, spawnSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; @@ -329,22 +329,46 @@ const MAX_RECORD_MS = 120_000; // 2 min interface Recording { proc: ReturnType; file: string; + startTime: number; + timer?: NodeJS.Timeout; } let recording: Recording | null = null; -function startRecording(): string { +function startRecording(ctx?: any): string { const maxDur = Number(getConfig("sttMaxDuration") ?? 120); 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", String(maxDur), file], + ["-hide_banner", "-loglevel", "error", "-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", String(maxDur), file], { stdio: "ignore" }, ); - recording = { proc, file }; + const startTime = Date.now(); + let timer: NodeJS.Timeout | undefined; + + if (ctx) { + const updateStatus = () => { + const elapsedSec = Math.floor((Date.now() - startTime) / 1000); + const elapsedMinStr = String(Math.floor(elapsedSec / 60)).padStart(2, "0"); + const elapsedSecStr = String(elapsedSec % 60).padStart(2, "0"); + const maxMinStr = String(Math.floor(maxDur / 60)).padStart(2, "0"); + const maxSecStr = String(maxDur % 60).padStart(2, "0"); + ctx.ui.setStatus( + "agy-rec", + `๐Ÿ”ด REGISTRAZIONE... ${elapsedMinStr}:${elapsedSecStr} / ${maxMinStr}:${maxSecStr} (Esc per annullare)`, + ); + }; + updateStatus(); + timer = setInterval(updateStatus, 500); + } + + recording = { proc, file, startTime, timer }; // se ffmpeg termina da solo (timeout 2 min), azzera lo stato proc.on("exit", () => { - if (recording && recording.proc === proc) recording = null; + if (recording && recording.proc === proc) { + if (recording.timer) clearInterval(recording.timer); + recording = null; + } }); return file; } @@ -352,6 +376,7 @@ function startRecording(): string { function stopRecording(): Promise { return new Promise((resolve) => { if (!recording) return resolve(null); + if (recording.timer) clearInterval(recording.timer); const { proc, file } = recording; recording = null; let done = false; @@ -367,6 +392,30 @@ function stopRecording(): Promise { }); } +function cancelRecording(): Promise { + return new Promise((resolve) => { + if (!recording) return resolve(); + if (recording.timer) clearInterval(recording.timer); + const { proc, file } = recording; + recording = null; + let done = false; + const cleanup = () => { + if (!done) { + done = true; + try { + if (fs.existsSync(file)) fs.unlinkSync(file); + } catch { + /* ignora */ + } + resolve(); + } + }; + proc.on("exit", cleanup); + proc.kill("SIGKILL"); + setTimeout(cleanup, 1000); + }); +} + // Ottimizza l'audio: taglia il silenzio iniziale/finale e converte in WAV 16kHz mono async function optimizeAudio(input: string): Promise { const output = input.replace(/\.wav$/, "-opt.wav"); @@ -374,6 +423,9 @@ async function optimizeAudio(input: string): Promise { await execFileAsync( "ffmpeg", [ + "-hide_banner", + "-loglevel", + "error", "-y", "-i", input, @@ -399,13 +451,14 @@ async function optimizeAudio(input: string): Promise { // Ritorna true se c'รจ segnale, false se รจ silenzio function audioHasSpeech(file: string): boolean { try { - const { stdout } = execFileSync( + const res = spawnSync( "ffmpeg", - ["-i", file, "-af", "volumedetect", "-f", "null", "-"], + ["-hide_banner", "-i", file, "-af", "volumedetect", "-f", "null", "-"], { timeout: 30_000, encoding: "utf8" }, ); - const maxMatch = stdout.match(/max_volume: ([\-0-9.]+) dB/); - const meanMatch = stdout.match(/mean_volume: ([\-0-9.]+) dB/); + const stderr = res.stderr || ""; + const maxMatch = stderr.match(/max_volume: ([\-0-9.]+) dB/); + const meanMatch = stderr.match(/mean_volume: ([\-0-9.]+) dB/); if (maxMatch) { const max = parseFloat(maxMatch[1]); // max < -35dB โ‰ˆ silenzio quasi totale @@ -457,7 +510,7 @@ async function transcribeWithGeminiAPI(file: string): Promise try { await execFileAsync( "ffmpeg", - ["-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libopus", "-b:a", "16k", ogg], + ["-hide_banner", "-loglevel", "error", "-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libopus", "-b:a", "16k", ogg], { timeout: 60_000 }, ); audioFile = ogg; @@ -569,7 +622,7 @@ async function transcribeWithEnne2(file: string): Promise { try { await execFileAsync( "ffmpeg", - ["-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libmp3lame", "-q:a", "5", mp3], + ["-hide_banner", "-loglevel", "error", "-y", "-i", file, "-ac", "1", "-ar", "16000", "-c:a", "libmp3lame", "-q:a", "5", mp3], { timeout: 60_000 }, ); audioFile = mp3; @@ -661,7 +714,7 @@ async function ttsSpeak(text: string, outputDir?: string): Promise { + if (recording) { + await cancelRecording(); + ctx.ui.setStatus("agy-rec", ""); + ctx.ui.notify("โŒ Registrazione annullata", "info"); + } + }, + }); + + pi.registerShortcut("esc", { + description: "Annulla la registrazione microfono in corso", + handler: async (ctx) => { + if (recording) { + await cancelRecording(); + ctx.ui.setStatus("agy-rec", ""); + ctx.ui.notify("โŒ Registrazione annullata", "info"); + } + }, + }); + pi.registerCommand("agy:record", { description: "Avvia/ferma registrazione microfono (come F12)", handler: async (_args, ctx) => { @@ -1364,6 +1438,19 @@ export default function agyExtension(pi: ExtensionAPI) { }, }); + pi.registerCommand("agy:record:cancel", { + description: "Annulla la registrazione microfono in corso", + handler: async (_args, ctx) => { + if (!recording) { + ctx.ui.notify("Nessuna registrazione in corso da annullare", "warning"); + return; + } + await cancelRecording(); + ctx.ui.setStatus("agy-rec", ""); + ctx.ui.notify("โŒ Registrazione annullata", "info"); + }, + }); + pi.registerCommand("agy:speak", { description: "Pronuncia un testo con TTS Gemini. Uso: /agy:speak ", handler: async (args, ctx) => {