From c37041ae76972430e6e65e4cb957d41124651cea Mon Sep 17 00:00:00 2001 From: idriz Date: Wed, 2 Sep 2026 19:00:51 +0200 Subject: [PATCH] Mic software gain, VAD threshold 0.5, speech event logging, greeting on connect Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW --- idriz/assistant.py | 14 ++++++++++++-- idriz/audio.py | 16 ++++++++++++++-- idriz/config.py | 3 ++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/idriz/assistant.py b/idriz/assistant.py index 4489202..c2c6b83 100644 --- a/idriz/assistant.py +++ b/idriz/assistant.py @@ -73,10 +73,11 @@ class Assistant: def __init__(self, cfg: Config) -> None: self.cfg = cfg self.client = AsyncOpenAI(api_key=cfg.openai_api_key) - self.mic = Mic(cfg.mic_device) + self.mic = Mic(cfg.mic_device, cfg.mic_gain) self.speaker = Speaker(cfg.speaker_device) self.conn = None self.jobs: set[asyncio.Task] = set() + self.greeted = False # ---------- alati ---------- async def tool_wikipedia(self, args: dict) -> str: @@ -203,12 +204,21 @@ class Assistant: if item.get("type") == "function_call": await self.handle_function_call(item) elif t == "input_audio_buffer.speech_started": + log.info("govor počeo") if self.speaker.is_playing(0.0): await self.speaker.clear() + elif t == "input_audio_buffer.speech_stopped": + log.info("govor stao") elif t == "error": log.error("realtime greška: %s", event.error) - elif t in ("session.created", "session.updated"): + elif t == "session.created": log.info("sesija: %s", t) + elif t == "session.updated": + log.info("sesija: %s", t) + if not self.greeted: + self.greeted = True + await self.conn.send({"type": "response.create", "response": { + "instructions": "Kratko se predstavi na bosanskom u jednoj ili dvije rečenice: ti si Idriz, i reci da možeš odštampati sažetak o nekoj temi ili bojanku."}}) async def session(self) -> None: async with self.client.realtime.connect(model=self.cfg.realtime_model) as conn: diff --git a/idriz/audio.py b/idriz/audio.py index 9659738..7361b31 100644 --- a/idriz/audio.py +++ b/idriz/audio.py @@ -1,4 +1,5 @@ """Mikrofon i zvučnik preko PipeWire-a (pacat), PCM16 mono 24 kHz kako traži Realtime API.""" +import array import asyncio import logging import time @@ -17,9 +18,20 @@ def _pacat_args(mode: str, device: str) -> list[str]: return args +def apply_gain(pcm: bytes, gain: float) -> bytes: + if gain == 1.0: + return pcm + samples = array.array("h", pcm) + for i, v in enumerate(samples): + x = int(v * gain) + samples[i] = 32767 if x > 32767 else (-32768 if x < -32768 else x) + return samples.tobytes() + + class Mic: - def __init__(self, device: str = "") -> None: + def __init__(self, device: str = "", gain: float = 1.0) -> None: self.device = device + self.gain = gain self.proc: asyncio.subprocess.Process | None = None async def start(self) -> None: @@ -32,7 +44,7 @@ class Mic: async def read(self) -> bytes: assert self.proc and self.proc.stdout - return await self.proc.stdout.read(CHUNK) + return apply_gain(await self.proc.stdout.read(CHUNK), self.gain) async def stop(self) -> None: if self.proc and self.proc.returncode is None: diff --git a/idriz/config.py b/idriz/config.py index 24794db..31899a2 100644 --- a/idriz/config.py +++ b/idriz/config.py @@ -23,7 +23,8 @@ class Config: max_pages: int = field(default_factory=lambda: int(_env("IDRIZ_MAX_PAGES", "2"))) dry_run: bool = field(default_factory=lambda: _env("IDRIZ_DRY_RUN", "0") == "1") vad_silence_ms: int = field(default_factory=lambda: int(_env("IDRIZ_VAD_SILENCE_MS", "900"))) - vad_threshold: float = field(default_factory=lambda: float(_env("IDRIZ_VAD_THRESHOLD", "0.6"))) + mic_gain: float = field(default_factory=lambda: float(_env("IDRIZ_MIC_GAIN", "3.0"))) + vad_threshold: float = field(default_factory=lambda: float(_env("IDRIZ_VAD_THRESHOLD", "0.5"))) def __post_init__(self) -> None: self.out_dir.mkdir(parents=True, exist_ok=True)