feat(transcription): add shared voice input support (#4232)
* feat(webui): add voice transcription input * feat(webui): render ANSI output in code blocks * refactor(webui): isolate voice recorder logic * refactor(transcription): keep websocket ingress thin * refactor(transcription): resolve channel audio settings on demand * style(webui): neutralize voice waveform color * feat(webui): add voice input tooltip * feat(webui): add voice input keyboard shortcut * fix(webui): distinguish voice shortcut platforms * fix(webui): place voice button after model selector * refactor(webui): share voice hold recording helpers * fix(desktop): allow microphone voice input * fix(webui): stabilize token usage month labels * feat(webui): show voice input on settings overview * fix(webui): label voice capability as recognition * fix(webui): align capability overview status * refactor(webui): isolate transcription socket handling * fix(webui): soften silent voice waveform * refactor(audio): clarify transcription service location * docs(transcription): clarify audio and provider boundaries * fix(exec): reduce session output polling flake
This commit is contained in:
@@ -18,13 +18,30 @@ from nanobot.utils.helpers import safe_filename
|
||||
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
||||
MAX_FILE_SIZE = DEFAULT_MAX_BYTES
|
||||
|
||||
_DATA_URL_RE = re.compile(r"^data:([^;]+);base64,(.+)$", re.DOTALL)
|
||||
_DATA_URL_RE = re.compile(r"^data:([^;,]+)(?:;[^,]*)*;base64,(.+)$", re.DOTALL)
|
||||
_MIME_EXTENSION_OVERRIDES = {
|
||||
# Python's ``mimetypes`` maps browser-recorded audio/webm to ``.weba`` and
|
||||
# audio/ogg to ``.oga`` on macOS. Some transcription APIs validate by the
|
||||
# file extension and accept the canonical container extensions instead.
|
||||
"application/ogg": ".ogg",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/mpga": ".mpga",
|
||||
"audio/wav": ".wav",
|
||||
"audio/webm": ".webm",
|
||||
"audio/x-m4a": ".m4a",
|
||||
"audio/x-wav": ".wav",
|
||||
"audio/vnd.wave": ".wav",
|
||||
"video/webm": ".webm",
|
||||
}
|
||||
|
||||
|
||||
class FileSizeExceeded(Exception):
|
||||
class FileSizeExceededError(Exception):
|
||||
"""Raised when a decoded payload exceeds the caller's size limit."""
|
||||
|
||||
|
||||
FileSizeExceeded = FileSizeExceededError
|
||||
|
||||
|
||||
def save_base64_data_url(
|
||||
data_url: str,
|
||||
media_dir: Path,
|
||||
@@ -40,7 +57,7 @@ def save_base64_data_url(
|
||||
m = _DATA_URL_RE.match(data_url)
|
||||
if not m:
|
||||
return None
|
||||
mime_type, b64_payload = m.group(1), m.group(2)
|
||||
mime_type, b64_payload = m.group(1).strip().lower(), m.group(2)
|
||||
try:
|
||||
raw = base64.b64decode(b64_payload)
|
||||
except Exception:
|
||||
@@ -48,7 +65,7 @@ def save_base64_data_url(
|
||||
limit = DEFAULT_MAX_BYTES if max_bytes is None else max_bytes
|
||||
if len(raw) > limit:
|
||||
raise FileSizeExceeded(f"File exceeds {limit // (1024 * 1024)}MB limit")
|
||||
ext = mimetypes.guess_extension(mime_type) or ".bin"
|
||||
ext = _MIME_EXTENSION_OVERRIDES.get(mime_type) or mimetypes.guess_extension(mime_type) or ".bin"
|
||||
filename = f"{uuid.uuid4().hex[:12]}{ext}"
|
||||
dest = media_dir / safe_filename(filename)
|
||||
dest.write_bytes(raw)
|
||||
|
||||
Reference in New Issue
Block a user