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 openai import AsyncOpenAI
|
||||||
|
|
||||||
from . import coloring, printer, render, textgen, wiki
|
from . import audio, coloring, printer, render, textgen, wiki
|
||||||
from .audio import Mic, Speaker
|
from .audio import Mic, Speaker
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
@@ -175,20 +175,29 @@ class Assistant:
|
|||||||
|
|
||||||
# ---------- audio petlje ----------
|
# ---------- audio petlje ----------
|
||||||
async def mic_loop(self) -> None:
|
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:
|
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()
|
chunk = await self.mic.read()
|
||||||
if not chunk:
|
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 self.mic.stop()
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(1)
|
||||||
await self.mic.start()
|
|
||||||
continue
|
continue
|
||||||
if mic_missing:
|
|
||||||
log.info("mikrofon ponovo radi")
|
|
||||||
mic_missing = False
|
|
||||||
if self.speaker.is_playing():
|
if self.speaker.is_playing():
|
||||||
continue # half-duplex: ne šalji dok Idriz govori
|
continue # half-duplex: ne šalji dok Idriz govori
|
||||||
await self.conn.send({"type": "input_audio_buffer.append", "audio": base64.b64encode(chunk).decode()})
|
await self.conn.send({"type": "input_audio_buffer.append", "audio": base64.b64encode(chunk).decode()})
|
||||||
@@ -261,7 +270,6 @@ class Assistant:
|
|||||||
self.conn = None
|
self.conn = None
|
||||||
|
|
||||||
async def run(self) -> None:
|
async def run(self) -> None:
|
||||||
await self.mic.start()
|
|
||||||
await self.speaker.start()
|
await self.speaker.start()
|
||||||
if not await printer.printer_ok(self.cfg.printer):
|
if not await printer.printer_ok(self.cfg.printer):
|
||||||
log.warning("printer %s nije spreman", self.cfg.printer)
|
log.warning("printer %s nije spreman", self.cfg.printer)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import array
|
import array
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = logging.getLogger("idriz.audio")
|
log = logging.getLogger("idriz.audio")
|
||||||
@@ -28,12 +29,33 @@ def apply_gain(pcm: bytes, gain: float) -> bytes:
|
|||||||
return samples.tobytes()
|
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:
|
class Mic:
|
||||||
def __init__(self, device: str = "", gain: float = 1.0) -> None:
|
def __init__(self, device: str = "", gain: float = 1.0) -> None:
|
||||||
self.device = device
|
self.device = device
|
||||||
self.gain = gain
|
self.gain = gain
|
||||||
self.proc: asyncio.subprocess.Process | None = None
|
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:
|
async def start(self) -> None:
|
||||||
self.proc = await asyncio.create_subprocess_exec(
|
self.proc = await asyncio.create_subprocess_exec(
|
||||||
*_pacat_args("--record", self.device),
|
*_pacat_args("--record", self.device),
|
||||||
@@ -50,6 +72,7 @@ class Mic:
|
|||||||
if self.proc and self.proc.returncode is None:
|
if self.proc and self.proc.returncode is None:
|
||||||
self.proc.kill()
|
self.proc.kill()
|
||||||
await self.proc.wait()
|
await self.proc.wait()
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
|
||||||
class Speaker:
|
class Speaker:
|
||||||
|
|||||||
Reference in New Issue
Block a user