M6 + M0: hardening, deploy docs, PoC scripts

- Pipeline-down fallback: dialplan plays gogo-fallback after a failed
  AudioSocket (§15) + script to synthesize the Bosnian prompt via TTS
- Voice heartbeat file → /health/voice endpoint + live status in admin panel
- Retention job also purges stale call registrations; chat sessions metered;
  100%-usage super-admin alert email (GOGO_ADMIN_ALERT_EMAIL)
- .env.example, docs/DEPLOY.md (compose stack, GPU node, backups pg_dump,
  update procedure, security notes)
- §16 Definition of Done as an automated test: softphone-style call → fake
  partner availability → request pushed → webhook confirm → console SMS
- M0 PoC scripts: STT accuracy harness (CER/WER + spoken-digit check; dry-run
  verified with espeak-ng samples + faster-whisper small on CPU — phone number
  extracted exactly), TTS bake-off (Azure vs ElevenLabs, latency+cost), and
  end-to-end latency smoke test speaking real AudioSocket to the live pipeline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 11:27:05 +02:00
parent de5a2abdeb
commit fd45ff84dc
16 changed files with 733 additions and 5 deletions

View File

@@ -94,14 +94,38 @@ async def handle_connection(reader: asyncio.StreamReader, writer: asyncio.Stream
await session.run()
async def _heartbeat() -> None:
"""Touch a heartbeat file every 15 s; /health/voice + admin panel read its age (M6)."""
import os
path = heartbeat_path()
os.makedirs(os.path.dirname(path), exist_ok=True)
while True:
try:
with open(path, "w") as f:
f.write(str(int(asyncio.get_running_loop().time())))
except OSError:
log.exception("cannot write heartbeat %s", path)
await asyncio.sleep(15)
def heartbeat_path() -> str:
import os
return os.path.join(get_settings().recordings_dir, ".voice-heartbeat")
async def serve() -> None:
s = get_settings()
server = await asyncio.start_server(handle_connection, s.voice_host, s.voice_port)
addrs = ", ".join(str(sock.getsockname()) for sock in server.sockets)
log.info("AudioSocket server listening on %s", addrs)
# health heartbeat file for the admin panel / monitoring (M6)
async with server:
await server.serve_forever()
heartbeat = asyncio.create_task(_heartbeat())
try:
async with server:
await server.serve_forever()
finally:
heartbeat.cancel()
def main() -> None: