Mic software gain, VAD threshold 0.5, speech event logging, greeting on connect

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzKpYaEsuWDevuNnvSjcFW
This commit is contained in:
idriz
2026-09-02 19:00:51 +02:00
parent e594971fbf
commit c37041ae76
3 changed files with 28 additions and 5 deletions

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)