| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- from __future__ import annotations
- from dataclasses import asdict
- from pathlib import Path
- import os
- from flask import Flask, abort, jsonify, render_template, request, send_from_directory
- from flask_socketio import SocketIO, emit
- from audio_capture import AudioEngine
- from recorder import list_recordings, resolve_recording_path
- from stt_bridge import SttBridge
- BASE_DIR = Path(__file__).resolve().parent
- RECORDINGS_DIR = BASE_DIR / "recordings"
- app = Flask(__name__, template_folder="templates", static_folder="static")
- app.config["SECRET_KEY"] = os.environ.get("MIC_SYSTEM_SECRET", "mic-system-dev")
- socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
- audio_engine = AudioEngine(RECORDINGS_DIR)
- def _stt_message_callback(msg):
- """Called from SttBridge background thread when STT sends a message."""
- socketio.emit("stt_message", msg)
- stt_bridge = SttBridge(on_message=_stt_message_callback)
- # Attach STT bridge to audio engine so it receives processed audio
- audio_engine.set_stt_bridge(stt_bridge)
- _stream_task_started = False
- audio_start_error = ""
- def ensure_audio_started() -> None:
- global audio_start_error
- if audio_engine.is_running():
- return
- try:
- audio_engine.start()
- audio_start_error = ""
- except Exception as exc: # pragma: no cover - runtime environment specific
- audio_start_error = str(exc)
- def stream_audio_packets() -> None:
- while True:
- packet = audio_engine.get_latest_packet()
- socketio.emit("audio_data", packet)
- socketio.sleep(0.08)
- @app.route("/")
- def index() -> str:
- ensure_audio_started()
- return render_template("index.html")
- @app.route("/api/status")
- def api_status():
- ensure_audio_started()
- status = audio_engine.get_status()
- status["audio_error"] = audio_start_error
- status.update(stt_bridge.get_settings())
- return jsonify(status)
- @app.route("/api/recordings")
- def api_list_recordings():
- return jsonify(list_recordings(RECORDINGS_DIR))
- @app.route("/api/recordings/<path:filename>")
- def api_get_recording(filename: str):
- try:
- path = resolve_recording_path(RECORDINGS_DIR, filename)
- except ValueError:
- abort(400, description="Invalid filename")
- if not path.exists():
- abort(404)
- return send_from_directory(RECORDINGS_DIR, path.name, as_attachment=True)
- @app.route("/api/recordings/<path:filename>", methods=["DELETE"])
- def api_delete_recording(filename: str):
- try:
- path = resolve_recording_path(RECORDINGS_DIR, filename)
- except ValueError:
- abort(400, description="Invalid filename")
- if not path.exists():
- abort(404)
- path.unlink()
- socketio.emit("recordings_updated", {"ok": True})
- return jsonify({"ok": True})
- def _handle_command(payload: dict) -> dict:
- msg_type = payload.get("type")
- if msg_type == "settings":
- settings = audio_engine.update_settings(payload)
- # Handle STT settings
- stt_keys = {k: v for k, v in payload.items() if k.startswith("stt_")}
- if stt_keys:
- stt_settings = stt_bridge.update_settings(**stt_keys)
- settings.update(stt_settings)
- return {"type": "settings_applied", "settings": settings}
- if msg_type == "stt_settings":
- stt_keys = {k: v for k, v in payload.items() if k.startswith("stt_")}
- stt_settings = stt_bridge.update_settings(**stt_keys)
- return {"type": "stt_settings_applied", "settings": stt_settings}
- if msg_type == "record_start":
- source = str(payload.get("source", "mic1"))
- duration_sec = payload.get("duration_sec")
- record_info = audio_engine.start_recording(source, duration_sec=duration_sec)
- socketio.emit("recordings_updated", {"ok": True})
- return {"type": "record_started", **record_info}
- if msg_type == "record_stop":
- status = audio_engine.stop_recording()
- socketio.emit("recordings_updated", {"ok": True})
- return {"type": "record_stopped", "status": asdict(status)}
- if msg_type == "delete_recording":
- filename = str(payload.get("filename", ""))
- if not filename:
- raise ValueError("Missing filename")
- path = resolve_recording_path(RECORDINGS_DIR, filename)
- if path.exists():
- path.unlink()
- socketio.emit("recordings_updated", {"ok": True})
- return {"type": "recording_deleted", "filename": filename}
- raise ValueError("Unsupported message type: " + str(msg_type))
- @socketio.on("connect")
- def ws_connect():
- global _stream_task_started
- ensure_audio_started()
- if not _stream_task_started:
- socketio.start_background_task(stream_audio_packets)
- _stream_task_started = True
- status = audio_engine.get_status()
- status.update(stt_bridge.get_settings())
- emit("status", status)
- if audio_start_error:
- emit("server_error", {"message": audio_start_error})
- @socketio.on("client_message")
- def ws_client_message(payload):
- if not isinstance(payload, dict):
- emit("server_error", {"message": "Invalid payload"})
- return
- try:
- response = _handle_command(payload)
- except Exception as exc:
- emit("server_error", {"message": str(exc)})
- return
- emit("server_ack", response)
- @socketio.on("disconnect")
- def ws_disconnect():
- return
- if __name__ == "__main__":
- ensure_audio_started()
- host = os.environ.get("MIC_SYSTEM_HOST", "0.0.0.0")
- port = int(os.environ.get("MIC_SYSTEM_PORT", "5000"))
- socketio.run(app, host=host, port=port)
|