Feedback sonori (bip): avvio, stop, annullamento, timeout, completamento
This commit is contained in:
+63
-2
@@ -363,11 +363,16 @@ function startRecording(ctx?: any): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
recording = { proc, file, startTime, timer };
|
recording = { proc, file, startTime, timer };
|
||||||
// se ffmpeg termina da solo (timeout 2 min), azzera lo stato
|
// se ffmpeg termina da solo (timeout max durata), azzera lo stato e avvisa
|
||||||
proc.on("exit", () => {
|
proc.on("exit", (code) => {
|
||||||
if (recording && recording.proc === proc) {
|
if (recording && recording.proc === proc) {
|
||||||
if (recording.timer) clearInterval(recording.timer);
|
if (recording.timer) clearInterval(recording.timer);
|
||||||
recording = null;
|
recording = null;
|
||||||
|
playSound("timeout");
|
||||||
|
if (ctx) {
|
||||||
|
ctx.ui.setStatus("agy-rec", "");
|
||||||
|
ctx.ui.notify("⏰ Tempo massimo di registrazione raggiunto", "warning");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return file;
|
return file;
|
||||||
@@ -416,6 +421,55 @@ function cancelRecording(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Feedback sonori (bip) per le interazioni con la registrazione
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
type SoundType = "start" | "stop" | "cancel" | "timeout" | "done";
|
||||||
|
|
||||||
|
const SOUNDS: Record<SoundType, { freq: number; dur: number; repeat?: number }> = {
|
||||||
|
start: { freq: 880, dur: 0.15 }, // bip acuto breve (inizio)
|
||||||
|
stop: { freq: 660, dur: 0.25 }, // conferma
|
||||||
|
cancel: { freq: 220, dur: 0.3 }, // tono basso (annullamento)
|
||||||
|
timeout: { freq: 440, dur: 0.4, repeat: 2 }, // doppio bip di avviso
|
||||||
|
done: { freq: 990, dur: 0.2 }, // completamento (successo)
|
||||||
|
};
|
||||||
|
|
||||||
|
function playSound(type: SoundType) {
|
||||||
|
const cfg = SOUNDS[type];
|
||||||
|
if (!cfg) return;
|
||||||
|
const wav = path.join(os.tmpdir(), `agy-sound-${type}-${Date.now()}.wav`);
|
||||||
|
const { freq, dur, repeat = 1 } = cfg;
|
||||||
|
// genera il tono con ffmpeg (silenzioso) e riproduce con paplay/aplay/ffplay
|
||||||
|
try {
|
||||||
|
execFileAsync(
|
||||||
|
"ffmpeg",
|
||||||
|
["-hide_banner", "-loglevel", "error", "-y", "-f", "lavfi", "-i", `sine=frequency=${freq}:duration=${dur}`, wav],
|
||||||
|
{ timeout: 10_000 },
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
for (let i = 0; i < repeat; i++) {
|
||||||
|
execFileAsync("paplay", [wav], { timeout: 5000 }).catch(() => {
|
||||||
|
// fallback su aplay/ffplay se paplay non disponibile
|
||||||
|
execFileAsync("aplay", ["-q", wav], { timeout: 5000 }).catch(() => {
|
||||||
|
execFileAsync("ffplay", ["-nodisp", "-autoexit", wav], { timeout: 5000 }).catch(() => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// pulizia
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(wav, { force: true });
|
||||||
|
} catch {
|
||||||
|
/* ignora */
|
||||||
|
}
|
||||||
|
}, repeat * 1500);
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
} catch {
|
||||||
|
/* ignora */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Ottimizza l'audio: taglia il silenzio iniziale/finale e converte in WAV 16kHz mono
|
// Ottimizza l'audio: taglia il silenzio iniziale/finale e converte in WAV 16kHz mono
|
||||||
async function optimizeAudio(input: string): Promise<string> {
|
async function optimizeAudio(input: string): Promise<string> {
|
||||||
const output = input.replace(/\.wav$/, "-opt.wav");
|
const output = input.replace(/\.wav$/, "-opt.wav");
|
||||||
@@ -1336,10 +1390,12 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
async function handleRecordToggle(ctx: any) {
|
async function handleRecordToggle(ctx: any) {
|
||||||
if (!recording) {
|
if (!recording) {
|
||||||
startRecording(ctx);
|
startRecording(ctx);
|
||||||
|
playSound("start");
|
||||||
ctx.ui.notify("🎙️ Registrazione avviata (F12 per fermare, Esc per annullare)", "info");
|
ctx.ui.notify("🎙️ Registrazione avviata (F12 per fermare, Esc per annullare)", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
playSound("stop");
|
||||||
ctx.ui.setStatus("agy-rec", "⏹️ Finalizzazione...");
|
ctx.ui.setStatus("agy-rec", "⏹️ Finalizzazione...");
|
||||||
const file = await stopRecording();
|
const file = await stopRecording();
|
||||||
if (!file) {
|
if (!file) {
|
||||||
@@ -1356,6 +1412,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
if (!tr.text) {
|
if (!tr.text) {
|
||||||
ctx.ui.setStatus("agy-rec", "");
|
ctx.ui.setStatus("agy-rec", "");
|
||||||
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
|
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
|
||||||
|
playSound("cancel");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const transcript = tr.text;
|
const transcript = tr.text;
|
||||||
@@ -1383,6 +1440,7 @@ 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");
|
||||||
|
playSound("done");
|
||||||
|
|
||||||
// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
|
// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
|
||||||
const ttsNotify = getConfig("ttsNotify") ?? "true";
|
const ttsNotify = getConfig("ttsNotify") ?? "true";
|
||||||
@@ -1405,6 +1463,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
await cancelRecording();
|
await cancelRecording();
|
||||||
ctx.ui.setStatus("agy-rec", "");
|
ctx.ui.setStatus("agy-rec", "");
|
||||||
ctx.ui.notify("❌ Registrazione annullata", "info");
|
ctx.ui.notify("❌ Registrazione annullata", "info");
|
||||||
|
playSound("cancel");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1416,6 +1475,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
await cancelRecording();
|
await cancelRecording();
|
||||||
ctx.ui.setStatus("agy-rec", "");
|
ctx.ui.setStatus("agy-rec", "");
|
||||||
ctx.ui.notify("❌ Registrazione annullata", "info");
|
ctx.ui.notify("❌ Registrazione annullata", "info");
|
||||||
|
playSound("cancel");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1448,6 +1508,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
await cancelRecording();
|
await cancelRecording();
|
||||||
ctx.ui.setStatus("agy-rec", "");
|
ctx.ui.setStatus("agy-rec", "");
|
||||||
ctx.ui.notify("❌ Registrazione annullata", "info");
|
ctx.ui.notify("❌ Registrazione annullata", "info");
|
||||||
|
playSound("cancel");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user