Files
weirsoe-party-protocol/party/stt.py
T
agw d149aa323e
CI / test (push) Failing after 39s
Udbyder-agnostisk spilhjerne, rummets mikrofon og rigtig musik
Hjernen (party/brain) skriver kommentarer til løgnene, dømmer løgne
(sandheden eller upassende), personlige drillerier og en slut-anmeldelse,
alt i baggrunden med faste replikker som fallback. Udbyderen vælges med
BRAIN_PROVIDER: anthropic (Claude), openai (OpenAI-kompatibel, fx Codex)
eller ollama (fx Gemma). Rummets mikrofon er eksperimentel: storskærmen
sender korte lydstumper til talegenkendelse (STT_PROVIDER), og kun et kort
resumé af latter og udsagn huskes til hjernen. Musikken er nu lavet med
en musikmodel.

Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
2026-10-01 21:59:50 +02:00

69 lines
2.9 KiB
Python

"""Talegenkendelse til rummets mikrofon (eksperimentel). Udbyder-agnostisk som hjernen.
STT_PROVIDER=elevenlabs (Scribe; dansk; markerer selv latter som "(griner)"/"(laughter)")
STT_PROVIDER=openai (Whisper-kompatibelt /audio/transcriptions, fx OpenAI eller en lokal server)
Lyden behandles i hukommelsen og gemmes aldrig. Resultatet er kun tekst + om der blev grinet.
"""
from __future__ import annotations
import asyncio
import json
import logging
import re
import urllib.request
import uuid
from django.conf import settings
log = logging.getLogger(__name__)
LAUGH = re.compile(r"\((?:[^)]*(?:grin|griner|latter|laugh|chuckl|giggl|fnis)[^)]*)\)", re.I)
EVENT = re.compile(r"\([^)]*\)")
def enabled() -> bool:
return bool(_key()) and settings.STT_PROVIDER in ("elevenlabs", "openai")
def _key() -> str:
if settings.STT_API_KEY:
return settings.STT_API_KEY
return settings.ELEVENLABS_API_KEY if settings.STT_PROVIDER == "elevenlabs" else ""
def _multipart(fields: dict, file_bytes: bytes, content_type: str) -> tuple[bytes, str]:
boundary = uuid.uuid4().hex
out = b""
for k, v in fields.items():
out += f"--{boundary}\r\nContent-Disposition: form-data; name=\"{k}\"\r\n\r\n{v}\r\n".encode()
out += (f"--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"rum.webm\"\r\n"
f"Content-Type: {content_type}\r\n\r\n").encode() + file_bytes + f"\r\n--{boundary}--\r\n".encode()
return out, f"multipart/form-data; boundary={boundary}"
def _transcribe_sync(audio: bytes, content_type: str) -> str:
if settings.STT_PROVIDER == "elevenlabs":
body, ctype = _multipart({"model_id": settings.STT_MODEL or "scribe_v1", "language_code": "da",
"tag_audio_events": "true"}, audio, content_type)
req = urllib.request.Request(settings.STT_BASE_URL or "https://api.elevenlabs.io/v1/speech-to-text", body,
{"xi-api-key": _key(), "Content-Type": ctype})
else:
body, ctype = _multipart({"model": settings.STT_MODEL or "whisper-1", "language": "da"}, audio, content_type)
req = urllib.request.Request((settings.STT_BASE_URL or "https://api.openai.com/v1") + "/audio/transcriptions",
body, {"Authorization": f"Bearer {_key()}", "Content-Type": ctype})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r).get("text", "")
async def transcribe(audio: bytes, content_type: str = "audio/webm") -> dict | None:
"""{"text": tale uden lyd-tags, "laughter": bool} eller None ved fejl."""
if not enabled() or len(audio) < 2000:
return None
try:
raw = await asyncio.to_thread(_transcribe_sync, audio, content_type)
except Exception as e: # noqa: BLE001
log.warning("STT fejlede: %r", e)
return None
return {"text": re.sub(r"\s+", " ", EVENT.sub(" ", raw)).strip(), "laughter": bool(LAUGH.search(raw))}