app.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. from __future__ import annotations
  2. from dataclasses import asdict
  3. from pathlib import Path
  4. import os
  5. from flask import Flask, abort, jsonify, render_template, request, send_from_directory
  6. from flask_socketio import SocketIO, emit
  7. from audio_capture import AudioEngine
  8. from recorder import list_recordings, resolve_recording_path
  9. from stt_bridge import SttBridge
  10. BASE_DIR = Path(__file__).resolve().parent
  11. RECORDINGS_DIR = BASE_DIR / "recordings"
  12. app = Flask(__name__, template_folder="templates", static_folder="static")
  13. app.config["SECRET_KEY"] = os.environ.get("MIC_SYSTEM_SECRET", "mic-system-dev")
  14. socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
  15. audio_engine = AudioEngine(RECORDINGS_DIR)
  16. def _stt_message_callback(msg):
  17. """Called from SttBridge background thread when STT sends a message."""
  18. socketio.emit("stt_message", msg)
  19. stt_bridge = SttBridge(on_message=_stt_message_callback)
  20. # Attach STT bridge to audio engine so it receives processed audio
  21. audio_engine.set_stt_bridge(stt_bridge)
  22. _stream_task_started = False
  23. audio_start_error = ""
  24. def ensure_audio_started() -> None:
  25. global audio_start_error
  26. if audio_engine.is_running():
  27. return
  28. try:
  29. audio_engine.start()
  30. audio_start_error = ""
  31. except Exception as exc: # pragma: no cover - runtime environment specific
  32. audio_start_error = str(exc)
  33. def stream_audio_packets() -> None:
  34. while True:
  35. packet = audio_engine.get_latest_packet()
  36. socketio.emit("audio_data", packet)
  37. socketio.sleep(0.08)
  38. @app.route("/")
  39. def index() -> str:
  40. ensure_audio_started()
  41. return render_template("index.html")
  42. @app.route("/api/status")
  43. def api_status():
  44. ensure_audio_started()
  45. status = audio_engine.get_status()
  46. status["audio_error"] = audio_start_error
  47. status.update(stt_bridge.get_settings())
  48. return jsonify(status)
  49. @app.route("/api/recordings")
  50. def api_list_recordings():
  51. return jsonify(list_recordings(RECORDINGS_DIR))
  52. @app.route("/api/recordings/<path:filename>")
  53. def api_get_recording(filename: str):
  54. try:
  55. path = resolve_recording_path(RECORDINGS_DIR, filename)
  56. except ValueError:
  57. abort(400, description="Invalid filename")
  58. if not path.exists():
  59. abort(404)
  60. return send_from_directory(RECORDINGS_DIR, path.name, as_attachment=True)
  61. @app.route("/api/recordings/<path:filename>", methods=["DELETE"])
  62. def api_delete_recording(filename: str):
  63. try:
  64. path = resolve_recording_path(RECORDINGS_DIR, filename)
  65. except ValueError:
  66. abort(400, description="Invalid filename")
  67. if not path.exists():
  68. abort(404)
  69. path.unlink()
  70. socketio.emit("recordings_updated", {"ok": True})
  71. return jsonify({"ok": True})
  72. def _handle_command(payload: dict) -> dict:
  73. msg_type = payload.get("type")
  74. if msg_type == "settings":
  75. settings = audio_engine.update_settings(payload)
  76. # Handle STT settings
  77. stt_keys = {k: v for k, v in payload.items() if k.startswith("stt_")}
  78. if stt_keys:
  79. stt_settings = stt_bridge.update_settings(**stt_keys)
  80. settings.update(stt_settings)
  81. return {"type": "settings_applied", "settings": settings}
  82. if msg_type == "stt_settings":
  83. stt_keys = {k: v for k, v in payload.items() if k.startswith("stt_")}
  84. stt_settings = stt_bridge.update_settings(**stt_keys)
  85. return {"type": "stt_settings_applied", "settings": stt_settings}
  86. if msg_type == "record_start":
  87. source = str(payload.get("source", "mic1"))
  88. duration_sec = payload.get("duration_sec")
  89. record_info = audio_engine.start_recording(source, duration_sec=duration_sec)
  90. socketio.emit("recordings_updated", {"ok": True})
  91. return {"type": "record_started", **record_info}
  92. if msg_type == "record_stop":
  93. status = audio_engine.stop_recording()
  94. socketio.emit("recordings_updated", {"ok": True})
  95. return {"type": "record_stopped", "status": asdict(status)}
  96. if msg_type == "delete_recording":
  97. filename = str(payload.get("filename", ""))
  98. if not filename:
  99. raise ValueError("Missing filename")
  100. path = resolve_recording_path(RECORDINGS_DIR, filename)
  101. if path.exists():
  102. path.unlink()
  103. socketio.emit("recordings_updated", {"ok": True})
  104. return {"type": "recording_deleted", "filename": filename}
  105. raise ValueError("Unsupported message type: " + str(msg_type))
  106. @socketio.on("connect")
  107. def ws_connect():
  108. global _stream_task_started
  109. ensure_audio_started()
  110. if not _stream_task_started:
  111. socketio.start_background_task(stream_audio_packets)
  112. _stream_task_started = True
  113. status = audio_engine.get_status()
  114. status.update(stt_bridge.get_settings())
  115. emit("status", status)
  116. if audio_start_error:
  117. emit("server_error", {"message": audio_start_error})
  118. @socketio.on("client_message")
  119. def ws_client_message(payload):
  120. if not isinstance(payload, dict):
  121. emit("server_error", {"message": "Invalid payload"})
  122. return
  123. try:
  124. response = _handle_command(payload)
  125. except Exception as exc:
  126. emit("server_error", {"message": str(exc)})
  127. return
  128. emit("server_ack", response)
  129. @socketio.on("disconnect")
  130. def ws_disconnect():
  131. return
  132. if __name__ == "__main__":
  133. ensure_audio_started()
  134. host = os.environ.get("MIC_SYSTEM_HOST", "0.0.0.0")
  135. port = int(os.environ.get("MIC_SYSTEM_PORT", "5000"))
  136. socketio.run(app, host=host, port=port)