diff --git a/idriz/assistant.py b/idriz/assistant.py index 2d68ed5..2257c66 100644 --- a/idriz/assistant.py +++ b/idriz/assistant.py @@ -9,7 +9,7 @@ from pathlib import Path from openai import AsyncOpenAI -from . import coloring, printer, render, textgen, wiki +from . import audio, coloring, printer, render, textgen, wiki from .audio import Mic, Speaker from .config import Config @@ -175,20 +175,29 @@ class Assistant: # ---------- audio petlje ---------- async def mic_loop(self) -> None: - mic_missing = False + """Šalje zvuk modelu. Ako je USB mikrofon isključen prekidačem (nestane iz PipeWire-a), ne sluša ništa.""" + last_check = 0.0 + present = False while True: + now = asyncio.get_event_loop().time() + if now - last_check > 1.5: + present = await audio.source_present(self.cfg.mic_device) + last_check = now + if not present: + if self.mic.running: + await self.mic.stop() + log.info("mikrofon isključen (prekidač), ne slušam") + await asyncio.sleep(0.5) + continue + if not self.mic.running: + await self.mic.start() + log.info("mikrofon uključen, slušam") + await self.speaker.write(audio.tone()) chunk = await self.mic.read() if not chunk: - if not mic_missing: - log.error("mikrofon ne daje podatke (isključen?), čekam da se pojavi") - mic_missing = True await self.mic.stop() - await asyncio.sleep(2) - await self.mic.start() + await asyncio.sleep(1) continue - if mic_missing: - log.info("mikrofon ponovo radi") - mic_missing = False if self.speaker.is_playing(): continue # half-duplex: ne šalji dok Idriz govori await self.conn.send({"type": "input_audio_buffer.append", "audio": base64.b64encode(chunk).decode()}) @@ -261,7 +270,6 @@ class Assistant: self.conn = None async def run(self) -> None: - await self.mic.start() await self.speaker.start() if not await printer.printer_ok(self.cfg.printer): log.warning("printer %s nije spreman", self.cfg.printer) diff --git a/idriz/audio.py b/idriz/audio.py index 7361b31..06379d2 100644 --- a/idriz/audio.py +++ b/idriz/audio.py @@ -2,6 +2,7 @@ import array import asyncio import logging +import math import time log = logging.getLogger("idriz.audio") @@ -28,12 +29,33 @@ def apply_gain(pcm: bytes, gain: float) -> bytes: return samples.tobytes() +def tone(freq: float = 880.0, ms: int = 150, volume: float = 0.3) -> bytes: + """Kratak signalni ton (PCM16 mono).""" + n = RATE * ms // 1000 + samples = array.array("h", (int(volume * 32767 * math.sin(2 * math.pi * freq * i / RATE) * min(1.0, (n - i) / (n * 0.3))) for i in range(n))) + return samples.tobytes() + + +async def source_present(name: str) -> bool: + """Da li PipeWire trenutno ima izvor tog imena (USB mikrofon nestane kad se isključi prekidač).""" + if not name: + return True + proc = await asyncio.create_subprocess_exec("pactl", "list", "sources", "short", + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL) + out, _ = await proc.communicate() + return any(name in line for line in out.decode(errors="replace").splitlines()) + + class Mic: def __init__(self, device: str = "", gain: float = 1.0) -> None: self.device = device self.gain = gain self.proc: asyncio.subprocess.Process | None = None + @property + def running(self) -> bool: + return self.proc is not None and self.proc.returncode is None + async def start(self) -> None: self.proc = await asyncio.create_subprocess_exec( *_pacat_args("--record", self.device), @@ -50,6 +72,7 @@ class Mic: if self.proc and self.proc.returncode is None: self.proc.kill() await self.proc.wait() + self.proc = None class Speaker: