""" Audio Recording Studio — collaborative audio collection tool. Flask + SQLite backend. Deploy on HF Spaces (Docker) or any server. """ import os import sqlite3 import uuid import zipfile from datetime import datetime from flask import ( Flask, render_template, request, jsonify, send_from_directory, send_file ) app = Flask(__name__) # Persistent storage mounted at /data (HF Spaces Storage Bucket). # Falls back to /tmp if /data is not writable. def pick_data_dir(): for d in [os.environ.get("DATA_DIR", "/data"), "/tmp/audio_studio"]: try: os.makedirs(os.path.join(d, "audio"), exist_ok=True) test = os.path.join(d, ".write_test") with open(test, "w") as f: f.write("ok") os.remove(test) return d except OSError: continue return "/tmp/audio_studio" DATA_DIR = pick_data_dir() DB_PATH = os.path.join(DATA_DIR, "recordings.db") AUDIO_DIR = os.path.join(DATA_DIR, "audio") # ── Database ──────────────────────────────────────────────────── def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): # Create dirs at runtime (the /data mount isn't available at build time) os.makedirs(AUDIO_DIR, exist_ok=True) with get_db() as db: db.execute(""" CREATE TABLE IF NOT EXISTS recordings ( id TEXT PRIMARY KEY, user TEXT NOT NULL, name TEXT NOT NULL, filename TEXT NOT NULL, duration REAL DEFAULT 0, created_at TEXT NOT NULL, notes TEXT DEFAULT '' ) """) init_db() # ── Routes ────────────────────────────────────────────────────── @app.route("/") def index(): return render_template("index.html") @app.route("/api/recordings", methods=["GET"]) def list_recordings(): with get_db() as db: rows = db.execute( "SELECT * FROM recordings ORDER BY created_at DESC" ).fetchall() return jsonify([dict(r) for r in rows]) @app.route("/api/recordings", methods=["POST"]) def create_recording(): audio = request.files.get("audio") if not audio: return jsonify({"error": "No audio file"}), 400 user = request.form.get("user", "Anonymous").strip() or "Anonymous" name = request.form.get("name", "").strip() duration = float(request.form.get("duration", 0)) notes = request.form.get("notes", "").strip() rec_id = uuid.uuid4().hex[:12] ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") if not name: name = f"Recording {ts}" filename = f"{rec_id}.webm" audio.save(os.path.join(AUDIO_DIR, filename)) with get_db() as db: db.execute( "INSERT INTO recordings (id, user, name, filename, duration, created_at, notes) " "VALUES (?, ?, ?, ?, ?, ?, ?)", (rec_id, user, name, filename, duration, ts, notes), ) return jsonify({ "id": rec_id, "user": user, "name": name, "filename": filename, "duration": duration, "created_at": ts, "notes": notes, }), 201 @app.route("/api/recordings/", methods=["PATCH"]) def update_recording(rec_id): data = request.get_json(force=True) fields, values = [], [] for col in ("name", "notes", "user"): if col in data: fields.append(f"{col} = ?") values.append(data[col]) if not fields: return jsonify({"error": "Nothing to update"}), 400 values.append(rec_id) with get_db() as db: db.execute(f"UPDATE recordings SET {', '.join(fields)} WHERE id = ?", values) return jsonify({"ok": True}) @app.route("/api/recordings/", methods=["DELETE"]) def delete_recording(rec_id): with get_db() as db: row = db.execute("SELECT filename FROM recordings WHERE id = ?", (rec_id,)).fetchone() if row: path = os.path.join(AUDIO_DIR, row["filename"]) if os.path.exists(path): os.remove(path) db.execute("DELETE FROM recordings WHERE id = ?", (rec_id,)) return jsonify({"ok": True}) @app.route("/audio/") def serve_audio(filename): return send_from_directory(AUDIO_DIR, filename) @app.route("/api/export") def export_excel(): """Export Excel only (with playback URLs).""" import pandas as pd base_url = request.host_url.rstrip("/") with get_db() as db: rows = db.execute("SELECT * FROM recordings ORDER BY created_at DESC").fetchall() data = [] for r in rows: data.append({ "Name": r["name"], "User": r["user"], "Timestamp": r["created_at"], "Duration (s)": round(r["duration"], 1), "Notes": r["notes"], "Audio File": r["filename"], "Audio URL": f"{base_url}/audio/{r['filename']}", }) df = pd.DataFrame(data) if data else pd.DataFrame( columns=["Name", "User", "Timestamp", "Duration (s)", "Notes", "Audio File", "Audio URL"] ) path = os.path.join(DATA_DIR, "export.xlsx") df.to_excel(path, index=False) return send_file(path, as_attachment=True, download_name="recordings.xlsx") @app.route("/api/export-zip") def export_zip(): """Export a ZIP containing the Excel + all audio files.""" import pandas as pd base_url = request.host_url.rstrip("/") with get_db() as db: rows = db.execute("SELECT * FROM recordings ORDER BY created_at DESC").fetchall() zip_path = os.path.join(DATA_DIR, "recordings_bundle.zip") excel_path = os.path.join(DATA_DIR, "recordings.xlsx") # Build Excel data = [] for r in rows: data.append({ "Name": r["name"], "User": r["user"], "Timestamp": r["created_at"], "Duration (s)": round(r["duration"], 1), "Notes": r["notes"], "Audio File": r["filename"], "Audio URL": f"{base_url}/audio/{r['filename']}", }) df = pd.DataFrame(data) if data else pd.DataFrame( columns=["Name", "User", "Timestamp", "Duration (s)", "Notes", "Audio File", "Audio URL"] ) df.to_excel(excel_path, index=False) # Build ZIP: excel + audio files with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf: zf.write(excel_path, "recordings.xlsx") for r in rows: audio_path = os.path.join(AUDIO_DIR, r["filename"]) if os.path.exists(audio_path): # Store as audio/.webm inside the zip safe_name = r["name"].replace("/", "_").replace("\\", "_") zf.write(audio_path, f"audio/{safe_name}.webm") return send_file(zip_path, as_attachment=True, download_name="recordings_bundle.zip") if __name__ == "__main__": port = int(os.environ.get("PORT", 7860)) app.run(host="0.0.0.0", port=port, debug=False)