feat(audio): add pluggable audio preprocessing and denoising layer before STT

- Add audio_enhancer module with 60s timeout, zero-transcoding default, and fail-soft fallback
- Persist enhanced audio in daily folders with _enhanced suffix and emit audio.enhance.result event
- Connect audio enhancer to transcribe_multi and transcribe in stt.py
- Forward message_id and trace_id from handlers
- Ensure consensus_text fallback persistence and remove scenario leakage in nlu.py
- Add test_enhancer.py unit suite and update smoke_test.py
This commit is contained in:
2026-10-15 20:40:45 +03:30
parent b44816aa98
commit 76b297f823
9 changed files with 442 additions and 12 deletions
+13 -3
View File
@@ -76,7 +76,8 @@ async def _call_single_stt(client: httpx.AsyncClient, provider: dict, converted_
async def transcribe_multi(settings: Settings, audio: bytes, mime_type: str | None = None,
filename: str | None = None, trace_id: str | None = None) -> list[dict]:
filename: str | None = None, trace_id: str | None = None,
message_id: int | None = None) -> list[dict]:
"""Transcribe audio with all configured STT providers in parallel.
Returns a list of candidate dicts: [{"name": str, "text": str, "model": str}, ...]
@@ -87,6 +88,13 @@ async def transcribe_multi(settings: Settings, audio: bytes, mime_type: str | No
if not providers:
raise RuntimeError("No STT providers are configured (PROCUREMENT_BOT_STT_*)")
# Audio enhancement / denoising step (pluggable, fail-soft, zero-transcode by default)
from app.audio_enhancer import enhance_voice
audio, _ = await enhance_voice(
settings, audio, mime_type=mime_type, filename=filename,
trace_id=trace_id, message_id=message_id,
)
suffix = Path(filename or "").suffix
if not suffix and mime_type:
suffix = {"audio/ogg": ".ogg", "audio/opus": ".ogg", "audio/mpeg": ".mp3",
@@ -117,8 +125,10 @@ async def transcribe_multi(settings: Settings, audio: bytes, mime_type: str | No
async def transcribe(settings: Settings, audio: bytes, mime_type: str | None = None,
filename: str | None = None, trace_id: str | None = None) -> str:
filename: str | None = None, trace_id: str | None = None,
message_id: int | None = None) -> str:
"""Return the Persian transcript for a voice note (empty string on silence)."""
transcripts = await transcribe_multi(settings, audio, mime_type=mime_type,
filename=filename, trace_id=trace_id)
filename=filename, trace_id=trace_id,
message_id=message_id)
return transcripts[0]["text"] if transcripts else ""