Switching the mic off interrupts speech (distinct tone); background jobs keep running

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 20:07:01 +02:00
parent 69b6c838b0
commit ab638aa690
2 changed files with 38 additions and 3 deletions

View File

@@ -91,6 +91,9 @@ class Assistant:
self.conn = None
self.jobs: set[asyncio.Task] = set()
self.greeted = False
self.current_response = ""
self.response_active = False
self.cancelled_responses: set[str] = set()
# ---------- alati ----------
async def tool_wikipedia(self, args: dict) -> str:
@@ -155,6 +158,20 @@ class Assistant:
ok = False
await self.notify_model(("GOTOVO. " if ok else "GREŠKA. ") + result + " Reci to djetetu kratko, na bosanskom.")
async def interrupt_speech(self) -> None:
"""Prekini govor koji je u toku (pozadinski poslovi nastavljaju)."""
speaking = self.speaker.is_playing(0.0) or self.response_active
if self.response_active and self.conn:
self.cancelled_responses.add(self.current_response)
try:
await self.conn.send({"type": "response.cancel"})
except Exception: # noqa: BLE001
log.exception("response.cancel nije uspio")
if speaking:
await self.speaker.clear()
log.info("govor prekinut (mikrofon ugašen)")
await self.speaker.write(audio.tone_off())
async def notify_model(self, text: str) -> None:
if not self.conn:
return
@@ -193,19 +210,20 @@ class Assistant:
present = False
while True:
now = asyncio.get_event_loop().time()
if now - last_check > 1.5:
if now - last_check > 0.7:
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)
await self.interrupt_speech()
await asyncio.sleep(0.3)
continue
if not self.mic.running:
await self.mic.start()
log.info("mikrofon uključen, slušam")
await self.speaker.write(audio.tone())
await self.speaker.write(audio.tone_on())
chunk = await self.mic.read()
if not chunk:
await self.mic.stop()
@@ -220,7 +238,14 @@ class Assistant:
async for event in self.conn:
t = event.type
if t == "response.output_audio.delta":
if getattr(event, "response_id", "") in self.cancelled_responses:
continue
await self.speaker.write(base64.b64decode(event.delta))
elif t == "response.created":
self.current_response = event.response.id or ""
self.response_active = True
elif t == "response.done":
self.response_active = False
elif t == "response.output_audio_transcript.delta":
transcript += event.delta
elif t == "response.output_audio_transcript.done":

View File

@@ -36,6 +36,16 @@ def tone(freq: float = 880.0, ms: int = 150, volume: float = 0.3) -> bytes:
return samples.tobytes()
def tone_on() -> bytes:
"""Mikrofon uključen: dva tona naviše."""
return tone(660, 120) + tone(990, 160)
def tone_off() -> bytes:
"""Mikrofon isključen / prekid govora: dva tona naniže."""
return tone(990, 120) + tone(440, 220)
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: