feat(agent): integrate procurement AI agent with Bale bot, real-time draft sync, and git history bundle
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
"""Speech-to-text via an OpenAI-compatible /audio/transcriptions endpoint.
|
||||
|
||||
Bale voice notes arrive as OGG/Opus which not every provider accepts, so the
|
||||
audio is first converted to mono 16 kHz MP3 with ffmpeg (installed in the bot
|
||||
image). Any OpenAI-compatible provider works (OpenAI, GLM, OpenRouter proxies,
|
||||
self-hosted whisper servers exposing the same contract).
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import Settings
|
||||
|
||||
logger = logging.getLogger("bot.stt")
|
||||
|
||||
_CONVERT_TIMEOUT = 60
|
||||
_STT_TIMEOUT = 120
|
||||
|
||||
|
||||
def _to_mp3(raw: bytes, suffix: str) -> bytes:
|
||||
"""Convert arbitrary audio bytes to mono 16 kHz MP3 using ffmpeg."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = Path(tmp) / f"in{suffix or '.ogg'}"
|
||||
dst = Path(tmp) / "out.mp3"
|
||||
src.write_bytes(raw)
|
||||
proc = subprocess.run(
|
||||
["ffmpeg", "-y", "-i", str(src), "-vn", "-ac", "1", "-ar", "16000", "-b:a", "64k", str(dst)],
|
||||
capture_output=True,
|
||||
timeout=_CONVERT_TIMEOUT,
|
||||
)
|
||||
if proc.returncode != 0 or not dst.exists():
|
||||
raise RuntimeError(f"ffmpeg failed: {proc.stderr[-400:]!r}")
|
||||
return dst.read_bytes()
|
||||
|
||||
|
||||
async def transcribe(settings: Settings, audio: bytes, mime_type: str | None = None,
|
||||
filename: str | None = None) -> str:
|
||||
"""Return the Persian transcript for a voice note (empty string on silence)."""
|
||||
if not settings.stt_base_url or not settings.stt_api_key:
|
||||
raise RuntimeError("STT provider is not configured (PROCUREMENT_BOT_STT_BASE_URL / _AI_BASE_URL)")
|
||||
|
||||
suffix = Path(filename or "").suffix
|
||||
if not suffix and mime_type:
|
||||
suffix = {"audio/ogg": ".ogg", "audio/opus": ".ogg", "audio/mpeg": ".mp3",
|
||||
"audio/mp4": ".m4a", "audio/wav": ".wav"}.get(mime_type.split(";")[0].strip(), ".ogg")
|
||||
converted = await asyncio.to_thread(_to_mp3, audio, suffix)
|
||||
|
||||
url = f"{settings.stt_base_url.rstrip('/')}/audio/transcriptions"
|
||||
async with httpx.AsyncClient(timeout=_STT_TIMEOUT) as client:
|
||||
resp = await client.post(
|
||||
url,
|
||||
headers={"Authorization": f"Bearer {settings.stt_api_key}"},
|
||||
files={"file": ("voice.mp3", converted, "audio/mpeg")},
|
||||
data={"model": settings.STT_MODEL, "language": settings.STT_LANGUAGE},
|
||||
)
|
||||
if resp.status_code != 200:
|
||||
raise RuntimeError(f"STT provider returned HTTP {resp.status_code}: {resp.text[:300]}")
|
||||
return str((resp.json() or {}).get("text") or "").strip()
|
||||
Reference in New Issue
Block a user