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
This commit is contained in:
114
services/node-worker/providers/stt_memory_service.py
Normal file
114
services/node-worker/providers/stt_memory_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""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,
|
||||
}
|
||||
77
services/node-worker/providers/tts_memory_service.py
Normal file
77
services/node-worker/providers/tts_memory_service.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""TTS provider: delegates to existing Memory Service /voice/tts.
|
||||
|
||||
Memory Service accepts: JSON {text, voice, speed}
|
||||
Returns: StreamingResponse — audio/mpeg (MP3 bytes)
|
||||
|
||||
Fabric contract output: {audio_b64, format, meta}
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger("provider.tts_memory_service")
|
||||
|
||||
MEMORY_SERVICE_URL = os.getenv("MEMORY_SERVICE_URL", "http://memory-service:8000")
|
||||
MAX_TEXT_CHARS = int(os.getenv("TTS_MAX_TEXT_CHARS", "500")) # Memory Service limits to 500
|
||||
|
||||
|
||||
async def synthesize(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Fabric TTS entry point — delegates to Memory Service.
|
||||
|
||||
Payload:
|
||||
text: str (required)
|
||||
voice: str (optional; Polina/Ostap/default/uk-UA-PolinaNeural/etc.)
|
||||
speed: float (optional, default 1.0)
|
||||
|
||||
Returns Fabric contract: {audio_b64, format, meta, provider, model}
|
||||
|
||||
Note: Memory Service uses edge-tts and returns MP3.
|
||||
No format conversion — caller receives base64-encoded MP3.
|
||||
"""
|
||||
text = payload.get("text", "").strip()
|
||||
if not text:
|
||||
raise ValueError("text is required")
|
||||
orig_len = len(text)
|
||||
truncated = orig_len > MAX_TEXT_CHARS
|
||||
if truncated:
|
||||
text = text[:MAX_TEXT_CHARS]
|
||||
logger.warning(f"TTS text truncated {orig_len} → {MAX_TEXT_CHARS} chars")
|
||||
|
||||
voice = payload.get("voice", "default")
|
||||
speed = float(payload.get("speed", 1.0))
|
||||
|
||||
async with httpx.AsyncClient(timeout=30) as c:
|
||||
resp = await c.post(
|
||||
f"{MEMORY_SERVICE_URL}/voice/tts",
|
||||
json={"text": text, "voice": voice, "speed": speed},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
audio_bytes = resp.content
|
||||
|
||||
engine = resp.headers.get("X-TTS-Engine", "edge-tts")
|
||||
tts_voice = resp.headers.get("X-TTS-Voice", voice)
|
||||
content_type = resp.headers.get("content-type", "audio/mpeg")
|
||||
fmt = "mp3" if "mpeg" in content_type else "wav"
|
||||
|
||||
audio_b64 = base64.b64encode(audio_bytes).decode()
|
||||
|
||||
return {
|
||||
"audio_b64": audio_b64,
|
||||
"format": fmt,
|
||||
"meta": {
|
||||
"model": engine,
|
||||
"voice": tts_voice,
|
||||
"provider": "memory_service",
|
||||
"engine": engine,
|
||||
"audio_bytes": len(audio_bytes),
|
||||
"service_url": MEMORY_SERVICE_URL,
|
||||
"truncated": truncated,
|
||||
"orig_len": orig_len,
|
||||
"used_len": len(text),
|
||||
},
|
||||
"provider": "memory_service",
|
||||
"model": engine,
|
||||
}
|
||||
Reference in New Issue
Block a user