CI / test (push) Failing after 39s
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
69 lines
2.9 KiB
Python
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))}
|