Files
microdao-daarion/services/node-worker/providers/stt_memory_service.py
Apple 129e4ea1fc feat(platform): add new services, tools, tests and crews modules
New router intelligence modules (26 files): alert_ingest/store, audit_store,
architecture_pressure, backlog_generator/store, cost_analyzer, data_governance,
dependency_scanner, drift_analyzer, incident_* (5 files), llm_enrichment,
platform_priority_digest, provider_budget, release_check_runner, risk_* (6 files),
signature_state_store, sofiia_auto_router, tool_governance

New services:
- sofiia-console: Dockerfile, adapters/, monitor/nodes/ops/voice modules, launchd, react static
- memory-service: integration_endpoints, integrations, voice_endpoints, static UI
- aurora-service: full app suite (analysis, job_store, orchestrator, reporting, schemas, subagents)
- sofiia-supervisor: new supervisor service
- aistalk-bridge-lite: Telegram bridge lite
- calendar-service: CalDAV calendar service with reminders
- mlx-stt-service / mlx-tts-service: Apple Silicon speech services
- binance-bot-monitor: market monitor service
- node-worker: STT/TTS memory providers

New tools (9): agent_email, browser_tool, contract_tool, observability_tool,
oncall_tool, pr_reviewer_tool, repo_tool, safe_code_executor, secure_vault

New crews: agromatrix_crew (10 modules: depth_classifier, doc_facts, doc_focus,
farm_state, light_reply, llm_factory, memory_manager, proactivity, reflection_engine,
session_context, style_adapter, telemetry)

Tests: 85+ test files for all new modules
Made-with: Cursor
2026-03-03 07:14:14 -08:00

115 lines
4.0 KiB
Python

"""STT provider: delegates to existing Memory Service /voice/stt.
Memory Service accepts: multipart/form-data audio file upload.
Returns: {text, model, language}
Fabric contract output: {text, segments[], language, meta}
"""
import base64
import logging
import os
from typing import Any, Dict
import httpx
logger = logging.getLogger("provider.stt_memory_service")
MEMORY_SERVICE_URL = os.getenv("MEMORY_SERVICE_URL", "http://memory-service:8000")
MAX_AUDIO_BYTES = int(os.getenv("STT_MAX_AUDIO_BYTES", str(25 * 1024 * 1024)))
async def _resolve_audio_bytes(payload: Dict[str, Any]) -> tuple[bytes, str, str, str]:
"""Return (raw_bytes, filename, source, content_type) from audio_b64 or audio_url."""
audio_b64 = payload.get("audio_b64", "")
audio_url = payload.get("audio_url", "")
filename = payload.get("filename", "audio.wav")
if audio_b64:
raw = base64.b64decode(audio_b64)
if len(raw) > MAX_AUDIO_BYTES:
raise ValueError(f"Audio exceeds {MAX_AUDIO_BYTES} bytes")
return raw, filename, "b64", "audio/wav"
if audio_url:
if audio_url.startswith(("file://", "/")):
path = audio_url.replace("file://", "")
with open(path, "rb") as f:
raw = f.read()
if len(raw) > MAX_AUDIO_BYTES:
raise ValueError(f"Audio exceeds {MAX_AUDIO_BYTES} bytes")
ext = path.rsplit(".", 1)[-1] if "." in path else "wav"
return raw, f"audio.{ext}", "file", f"audio/{ext}"
# HTTP URL — check Content-Length header first if available
async with httpx.AsyncClient(timeout=30) as c:
try:
head_resp = await c.head(audio_url)
content_length = int(head_resp.headers.get("content-length", 0))
if content_length > MAX_AUDIO_BYTES:
raise ValueError(f"Audio URL Content-Length {content_length} exceeds {MAX_AUDIO_BYTES} bytes")
content_type = head_resp.headers.get("content-type", "audio/wav")
except httpx.HTTPError:
content_type = "audio/wav"
resp = await c.get(audio_url)
resp.raise_for_status()
raw = resp.content
content_type = resp.headers.get("content-type", content_type)
if len(raw) > MAX_AUDIO_BYTES:
raise ValueError(f"Audio exceeds {MAX_AUDIO_BYTES} bytes")
ext = content_type.split("/")[-1].split(";")[0] or "wav"
return raw, f"audio.{ext}", "url", content_type
raise ValueError("audio_b64 or audio_url is required")
async def transcribe(payload: Dict[str, Any]) -> Dict[str, Any]:
"""Fabric STT entry point — delegates to Memory Service.
Payload:
audio_url: str (http/file) — OR —
audio_b64: str (base64)
language: str (optional, e.g. "uk", "en")
filename: str (optional, helps whisper detect format)
Returns Fabric contract: {text, segments[], language, meta, provider, model}
"""
language = payload.get("language")
raw_bytes, filename, source, content_type = await _resolve_audio_bytes(payload)
params = {}
if language:
params["language"] = language
async with httpx.AsyncClient(timeout=90) as c:
resp = await c.post(
f"{MEMORY_SERVICE_URL}/voice/stt",
files={"audio": (filename, raw_bytes, "audio/wav")},
params=params,
)
resp.raise_for_status()
data = resp.json()
text = data.get("text", "")
model_used = data.get("model", "faster-whisper")
lang_detected = data.get("language", language or "")
return {
"text": text,
"segments": [],
"language": lang_detected,
"meta": {
"model": model_used,
"provider": "memory_service",
"engine": model_used,
"service_url": MEMORY_SERVICE_URL,
"source": source,
"bytes": len(raw_bytes),
"filename": filename,
"content_type": content_type,
},
"provider": "memory_service",
"model": model_used,
}