feat: workspaces, agentic action tools, local Whisper STT, vec recall
- Projects/workspaces: documents grouped into projects; chat RAG scopes to the active project. Switcher in the Documents page. - Agentic action tools: web_search, fetch_url, and remember (first write tool), allowlist-gated per playbook. - Local Whisper STT (faster-whisper, no torch): on-device dictation replacing the browser Web Speech API. POST /stt + GET /stt/status; browser fallback. - Vector index extended to conversation recall (message_vectors), with the brute-force cosine scan kept as the fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
"""Local speech-to-text via faster-whisper.
|
||||
|
||||
Deliberately torch-free: faster-whisper runs on CTranslate2 (CPU, int8), so it
|
||||
fits the "no ML stack in the venv" rule that keeps Promethean small. Replaces
|
||||
the browser's Web Speech API, which in Chrome ships audio to Google — the whole
|
||||
point is to keep dictation on-device.
|
||||
|
||||
Model size via NEXUS_STT_MODEL (default "base"); downloaded and cached on first
|
||||
use. If faster-whisper isn't installed, `available()` is False and the backend
|
||||
reports it so the UI can fall back.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
_log = logging.getLogger("nexus.stt")
|
||||
_MODEL = None
|
||||
_MODEL_SIZE = os.getenv("NEXUS_STT_MODEL", "base")
|
||||
|
||||
|
||||
def available() -> bool:
|
||||
try:
|
||||
import faster_whisper # noqa: F401
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _get_model():
|
||||
global _MODEL
|
||||
if _MODEL is None:
|
||||
from faster_whisper import WhisperModel
|
||||
_log.info("loading whisper model %s (cpu/int8)", _MODEL_SIZE)
|
||||
_MODEL = WhisperModel(_MODEL_SIZE, device="cpu", compute_type="int8")
|
||||
return _MODEL
|
||||
|
||||
|
||||
def transcribe_b64(audio_b64: str) -> str:
|
||||
"""Transcribe a base64 audio blob (any container PyAV can decode — the
|
||||
browser's MediaRecorder produces webm/opus)."""
|
||||
raw = base64.b64decode(audio_b64)
|
||||
with tempfile.NamedTemporaryFile(suffix=".webm", delete=False) as f:
|
||||
f.write(raw)
|
||||
path = f.name
|
||||
try:
|
||||
segments, _info = _get_model().transcribe(path, vad_filter=True)
|
||||
return " ".join(s.text for s in segments).strip()
|
||||
finally:
|
||||
try:
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
Reference in New Issue
Block a user