Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions src/server/desktop_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import base64
import binascii
import logging
import re
from dataclasses import dataclass

import httpx
Expand All @@ -26,7 +25,6 @@
# 404 the route, so we don't guess.
_STT_PROVIDERS = ("openai", "groq")
_STT_MODEL = {"openai": "whisper-1", "groq": "whisper-large-v3"}
_DATA_URL_RE = re.compile(r"^data:([^;,]*)(;base64)?,(.*)$", re.DOTALL)


@dataclass
Expand All @@ -38,14 +36,33 @@ class TranscriptionResult:


def _decode_data_url(data_url: str) -> tuple[bytes, str] | None:
"""(bytes, mime) from a data: URL, or None if it isn't one."""
match = _DATA_URL_RE.match(data_url or "")
if not match:
"""(bytes, mime) from a data: URL, or None if it isn't one.

MediaRecorder produces MIME types with parameters, e.g.
``data:audio/webm;codecs=opus;base64,<data>`` — the mediatype segment can
carry its own ``;param=value`` pairs before the ``;base64`` marker. Split
on the FIRST comma (the mediatype can't contain one) rather than a regex
that assumes the mediatype is parameter-free.
"""
if not isinstance(data_url, str) or not data_url.startswith("data:"):
return None
mime = match.group(1) or "audio/webm"
payload = match.group(3)
comma = data_url.find(",")
if comma == -1:
return None
header = data_url[len("data:"):comma] # e.g. "audio/webm;codecs=opus;base64"
payload = data_url[comma + 1:]
is_base64 = header.rstrip().endswith(";base64")
if is_base64:
header = header.rstrip()[: -len(";base64")]
# Base mediatype is everything before the first parameter (";codecs=…").
mime = header.split(";", 1)[0].strip() or "audio/webm"
try:
raw = base64.b64decode(payload) if match.group(2) else payload.encode("utf-8")
if is_base64:
raw = base64.b64decode(payload)
else:
from urllib.parse import unquote_to_bytes

raw = unquote_to_bytes(payload)
except (binascii.Error, ValueError):
return None
return raw, mime
Expand Down
15 changes: 15 additions & 0 deletions tests/server/test_desktop_audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ def test_decode_rejects_non_data_url() -> None:
assert _decode_data_url("not a data url") is None


def test_decode_mediarecorder_mime_with_codec_param() -> None:
"""Regression: MediaRecorder emits ``audio/webm;codecs=opus`` — the mediatype
carries a ``;codecs=`` param before ``;base64``. The old regex failed this
and the mic showed 'invalid audio payload'."""
raw = b"\x1aE\xdf\xa3webm-bytes"
for header in ("audio/webm;codecs=opus", "audio/ogg; codecs=opus", "audio/mp4"):
url = f"data:{header};base64," + base64.b64encode(raw).decode()
decoded = _decode_data_url(url)
assert decoded is not None, header
audio, mime = decoded
assert audio == raw
# Base mediatype only — params stripped.
assert mime == header.split(";", 1)[0].strip()


@pytest.mark.asyncio
async def test_transcribe_no_provider_is_actionable(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(desktop_audio, "_configured_stt_provider", lambda: None)
Expand Down
Loading