app.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  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. BASE_DIR = Path(__file__).resolve().parent
  10. RECORDINGS_DIR = BASE_DIR / "recordings"
  11. app = Flask(__name__, template_folder="templates", static_folder="static")
  12. app.config["SECRET_KEY"] = os.environ.get("MIC_SYSTEM_SECRET", "mic-system-dev")
  13. socketio = SocketIO(app, cors_allowed_origins="*", async_mode="eventlet")
  14. audio_engine = AudioEngine(RECORDINGS_DIR)
  15. _stream_task_started = False
  16. audio_start_error = ""
  17. def ensure_audio_started() -> None:
  18. global audio_start_error
  19. if audio_engine.is_running():
  20. return
  21. try:
  22. audio_engine.start()
  23. audio_start_error = ""
  24. except Exception as exc: # pragma: no cover - runtime environment specific
  25. audio_start_error = str(exc)
  26. def stream_audio_packets() -> None:
  27. while True:
  28. packet = audio_engine.get_latest_packet()
  29. socketio.emit("audio_data", packet)
  30. socketio.sleep(0.08)
  31. @app.route("/")
  32. def index() -> str:
  33. ensure_audio_started()
  34. return render_template("index.html")
  35. @app.route("/api/status")
  36. def api_status():
  37. ensure_audio_started()
  38. status = audio_engine.get_status()
  39. status["audio_error"] = audio_start_error
  40. return jsonify(status)
  41. @app.route("/api/recordings")
  42. def api_list_recordings():
  43. return jsonify(list_recordings(RECORDINGS_DIR))
  44. @app.route("/api/recordings/<path:filename>")
  45. def api_get_recording(filename: str):
  46. try:
  47. path = resolve_recording_path(RECORDINGS_DIR, filename)
  48. except ValueError:
  49. abort(400, description="Invalid filename")
  50. if not path.exists():
  51. abort(404)
  52. return send_from_directory(RECORDINGS_DIR, path.name, as_attachment=True)
  53. @app.route("/api/recordings/<path:filename>", methods=["DELETE"])
  54. def api_delete_recording(filename: str):
  55. try:
  56. path = resolve_recording_path(RECORDINGS_DIR, filename)
  57. except ValueError:
  58. abort(400, description="Invalid filename")
  59. if not path.exists():
  60. abort(404)
  61. path.unlink()
  62. socketio.emit("recordings_updated", {"ok": True})
  63. return jsonify({"ok": True})
  64. def _handle_command(payload: dict) -> dict:
  65. msg_type = payload.get("type")
  66. if msg_type == "settings":
  67. settings = audio_engine.update_settings(payload)
  68. return {"type": "settings_applied", "settings": settings}
  69. if msg_type == "record_start":
  70. source = str(payload.get("source", "mic1"))
  71. duration_sec = payload.get("duration_sec")
  72. record_info = audio_engine.start_recording(source, duration_sec=duration_sec)
  73. socketio.emit("recordings_updated", {"ok": True})
  74. return {"type": "record_started", **record_info}
  75. if msg_type == "record_stop":
  76. status = audio_engine.stop_recording()
  77. socketio.emit("recordings_updated", {"ok": True})
  78. return {"type": "record_stopped", "status": asdict(status)}
  79. if msg_type == "delete_recording":
  80. filename = str(payload.get("filename", ""))
  81. if not filename:
  82. raise ValueError("Missing filename")
  83. path = resolve_recording_path(RECORDINGS_DIR, filename)
  84. if path.exists():
  85. path.unlink()
  86. socketio.emit("recordings_updated", {"ok": True})
  87. return {"type": "recording_deleted", "filename": filename}
  88. raise ValueError(f"Unsupported message type: {msg_type}")
  89. @socketio.on("connect")
  90. def ws_connect():
  91. global _stream_task_started
  92. ensure_audio_started()
  93. if not _stream_task_started:
  94. socketio.start_background_task(stream_audio_packets)
  95. _stream_task_started = True
  96. emit("status", audio_engine.get_status())
  97. if audio_start_error:
  98. emit("server_error", {"message": audio_start_error})
  99. @socketio.on("client_message")
  100. def ws_client_message(payload):
  101. if not isinstance(payload, dict):
  102. emit("server_error", {"message": "Invalid payload"})
  103. return
  104. try:
  105. response = _handle_command(payload)
  106. except Exception as exc:
  107. emit("server_error", {"message": str(exc)})
  108. return
  109. emit("server_ack", response)
  110. @socketio.on("disconnect")
  111. def ws_disconnect():
  112. return
  113. if __name__ == "__main__":
  114. ensure_audio_started()
  115. host = os.environ.get("MIC_SYSTEM_HOST", "0.0.0.0")
  116. port = int(os.environ.get("MIC_SYSTEM_PORT", "5000"))
  117. socketio.run(app, host=host, port=port)