USB mic switch acts as on/off: listen only while the USB source exists, chime when it appears
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user