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 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) _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 return jsonify(status) @app.route("/api/recordings") def api_list_recordings(): return jsonify(list_recordings(RECORDINGS_DIR)) @app.route("/api/recordings/") 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/", 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) return {"type": "settings_applied", "settings": 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(f"Unsupported message type: {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 emit("status", audio_engine.get_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)