XUUUUSID commited on
Commit
bf9f830
Β·
verified Β·
1 Parent(s): b102958

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +48 -1
  2. index.html +8 -4
app.py CHANGED
@@ -6,6 +6,7 @@ Flask + SQLite backend. Deploy on HF Spaces (Docker) or any server.
6
  import os
7
  import sqlite3
8
  import uuid
 
9
  from datetime import datetime
10
 
11
  from flask import (
@@ -148,8 +149,10 @@ def serve_audio(filename):
148
 
149
  @app.route("/api/export")
150
  def export_excel():
 
151
  import pandas as pd
152
 
 
153
  with get_db() as db:
154
  rows = db.execute("SELECT * FROM recordings ORDER BY created_at DESC").fetchall()
155
 
@@ -162,16 +165,60 @@ def export_excel():
162
  "Duration (s)": round(r["duration"], 1),
163
  "Notes": r["notes"],
164
  "Audio File": r["filename"],
 
165
  })
166
 
167
  df = pd.DataFrame(data) if data else pd.DataFrame(
168
- columns=["Name", "User", "Timestamp", "Duration (s)", "Notes", "Audio File"]
169
  )
170
  path = os.path.join(DATA_DIR, "export.xlsx")
171
  df.to_excel(path, index=False)
172
  return send_file(path, as_attachment=True, download_name="recordings.xlsx")
173
 
174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  if __name__ == "__main__":
176
  port = int(os.environ.get("PORT", 7860))
177
  app.run(host="0.0.0.0", port=port, debug=False)
 
6
  import os
7
  import sqlite3
8
  import uuid
9
+ import zipfile
10
  from datetime import datetime
11
 
12
  from flask import (
 
149
 
150
  @app.route("/api/export")
151
  def export_excel():
152
+ """Export Excel only (with playback URLs)."""
153
  import pandas as pd
154
 
155
+ base_url = request.host_url.rstrip("/")
156
  with get_db() as db:
157
  rows = db.execute("SELECT * FROM recordings ORDER BY created_at DESC").fetchall()
158
 
 
165
  "Duration (s)": round(r["duration"], 1),
166
  "Notes": r["notes"],
167
  "Audio File": r["filename"],
168
+ "Audio URL": f"{base_url}/audio/{r['filename']}",
169
  })
170
 
171
  df = pd.DataFrame(data) if data else pd.DataFrame(
172
+ columns=["Name", "User", "Timestamp", "Duration (s)", "Notes", "Audio File", "Audio URL"]
173
  )
174
  path = os.path.join(DATA_DIR, "export.xlsx")
175
  df.to_excel(path, index=False)
176
  return send_file(path, as_attachment=True, download_name="recordings.xlsx")
177
 
178
 
179
+ @app.route("/api/export-zip")
180
+ def export_zip():
181
+ """Export a ZIP containing the Excel + all audio files."""
182
+ import pandas as pd
183
+
184
+ base_url = request.host_url.rstrip("/")
185
+ with get_db() as db:
186
+ rows = db.execute("SELECT * FROM recordings ORDER BY created_at DESC").fetchall()
187
+
188
+ zip_path = os.path.join(DATA_DIR, "recordings_bundle.zip")
189
+ excel_path = os.path.join(DATA_DIR, "recordings.xlsx")
190
+
191
+ # Build Excel
192
+ data = []
193
+ for r in rows:
194
+ data.append({
195
+ "Name": r["name"],
196
+ "User": r["user"],
197
+ "Timestamp": r["created_at"],
198
+ "Duration (s)": round(r["duration"], 1),
199
+ "Notes": r["notes"],
200
+ "Audio File": r["filename"],
201
+ "Audio URL": f"{base_url}/audio/{r['filename']}",
202
+ })
203
+
204
+ df = pd.DataFrame(data) if data else pd.DataFrame(
205
+ columns=["Name", "User", "Timestamp", "Duration (s)", "Notes", "Audio File", "Audio URL"]
206
+ )
207
+ df.to_excel(excel_path, index=False)
208
+
209
+ # Build ZIP: excel + audio files
210
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
211
+ zf.write(excel_path, "recordings.xlsx")
212
+ for r in rows:
213
+ audio_path = os.path.join(AUDIO_DIR, r["filename"])
214
+ if os.path.exists(audio_path):
215
+ # Store as audio/<original_name>.webm inside the zip
216
+ safe_name = r["name"].replace("/", "_").replace("\\", "_")
217
+ zf.write(audio_path, f"audio/{safe_name}.webm")
218
+
219
+ return send_file(zip_path, as_attachment=True, download_name="recordings_bundle.zip")
220
+
221
+
222
  if __name__ == "__main__":
223
  port = int(os.environ.get("PORT", 7860))
224
  app.run(host="0.0.0.0", port=port, debug=False)
index.html CHANGED
@@ -131,6 +131,7 @@ tbody tr.playing{background:#eef1ff}
131
  <input class="search-box" id="searchBox" placeholder="Search…" autocomplete="off">
132
  <button class="btn-outline" id="btnRefresh">Refresh</button>
133
  <button class="btn-outline" id="btnExport">Export Excel</button>
 
134
  </div>
135
 
136
  <table>
@@ -162,7 +163,7 @@ tbody tr.playing{background:#eef1ff}
162
  var searchBox=$("searchBox"),tablePlayer=$("tablePlayer");
163
 
164
  var mediaRecorder,audioChunks,recordedBlob;
165
- var timerInt,startTime,analyser,animFrame,audioCtx,stream;
166
  var allRecordings=[];
167
  var playingId=null;
168
 
@@ -173,7 +174,7 @@ tbody tr.playing{background:#eef1ff}
173
  /* ── Timer ──────────────────────────────────── */
174
  function resetTimer(){timerEl.textContent="00:00";timerEl.classList.remove("active")}
175
  function startTimerFn(){
176
- startTime=Date.now();timerEl.classList.add("active");
177
  timerInt=setInterval(function(){
178
  var s=Math.floor((Date.now()-startTime)/1000);
179
  timerEl.textContent=String(Math.floor(s/60)).padStart(2,"0")+":"+String(s%60).padStart(2,"0");
@@ -221,13 +222,15 @@ tbody tr.playing{background:#eef1ff}
221
  btnSave.disabled=false;
222
  setStatus("Ready to save.");
223
  };
224
- mediaRecorder.start(1000);startTimerFn();
 
225
  btnRecord.disabled=true;btnStop.disabled=false;
226
  setStatus("Recording…");
227
  }).catch(function(e){setStatus("Mic denied: "+e.message,false)});
228
  });
229
 
230
  btnStop.addEventListener("click",function(){
 
231
  if(mediaRecorder&&mediaRecorder.state!=="inactive")mediaRecorder.stop();
232
  stopTimerFn();stopVis();
233
  if(stream){stream.getTracks().forEach(function(t){t.stop()});stream=null}
@@ -245,7 +248,7 @@ tbody tr.playing{background:#eef1ff}
245
  fd.append("user",userName.value.trim()||"Anonymous");
246
  fd.append("name",recName.value.trim());
247
  fd.append("notes",recNotes.value.trim());
248
- fd.append("duration",((Date.now()-startTime)/1000).toFixed(1));
249
 
250
  fetch("/api/recordings",{method:"POST",body:fd})
251
  .then(function(r){if(!r.ok)throw new Error(r.statusText);return r.json()})
@@ -345,6 +348,7 @@ tbody tr.playing{background:#eef1ff}
345
  /* Refresh & export */
346
  $("btnRefresh").addEventListener("click",loadTable);
347
  $("btnExport").addEventListener("click",function(){window.location="/api/export"});
 
348
 
349
  /* Auto-refresh every 10s for collaboration */
350
  setInterval(loadTable,10000);
 
131
  <input class="search-box" id="searchBox" placeholder="Search…" autocomplete="off">
132
  <button class="btn-outline" id="btnRefresh">Refresh</button>
133
  <button class="btn-outline" id="btnExport">Export Excel</button>
134
+ <button class="btn-outline" id="btnExportZip">Download All (ZIP)</button>
135
  </div>
136
 
137
  <table>
 
163
  var searchBox=$("searchBox"),tablePlayer=$("tablePlayer");
164
 
165
  var mediaRecorder,audioChunks,recordedBlob;
166
+ var timerInt,startTime,recordedDuration,analyser,animFrame,audioCtx,stream;
167
  var allRecordings=[];
168
  var playingId=null;
169
 
 
174
  /* ── Timer ──────────────────────────────────── */
175
  function resetTimer(){timerEl.textContent="00:00";timerEl.classList.remove("active")}
176
  function startTimerFn(){
177
+ timerEl.classList.add("active");
178
  timerInt=setInterval(function(){
179
  var s=Math.floor((Date.now()-startTime)/1000);
180
  timerEl.textContent=String(Math.floor(s/60)).padStart(2,"0")+":"+String(s%60).padStart(2,"0");
 
222
  btnSave.disabled=false;
223
  setStatus("Ready to save.");
224
  };
225
+ mediaRecorder.start(1000);
226
+ startTime=Date.now();recordedDuration=0;startTimerFn();
227
  btnRecord.disabled=true;btnStop.disabled=false;
228
  setStatus("Recording…");
229
  }).catch(function(e){setStatus("Mic denied: "+e.message,false)});
230
  });
231
 
232
  btnStop.addEventListener("click",function(){
233
+ recordedDuration=(Date.now()-startTime)/1000;
234
  if(mediaRecorder&&mediaRecorder.state!=="inactive")mediaRecorder.stop();
235
  stopTimerFn();stopVis();
236
  if(stream){stream.getTracks().forEach(function(t){t.stop()});stream=null}
 
248
  fd.append("user",userName.value.trim()||"Anonymous");
249
  fd.append("name",recName.value.trim());
250
  fd.append("notes",recNotes.value.trim());
251
+ fd.append("duration",(recordedDuration||0).toFixed(1));
252
 
253
  fetch("/api/recordings",{method:"POST",body:fd})
254
  .then(function(r){if(!r.ok)throw new Error(r.statusText);return r.json()})
 
348
  /* Refresh & export */
349
  $("btnRefresh").addEventListener("click",loadTable);
350
  $("btnExport").addEventListener("click",function(){window.location="/api/export"});
351
+ $("btnExportZip").addEventListener("click",function(){window.location="/api/export-zip"});
352
 
353
  /* Auto-refresh every 10s for collaboration */
354
  setInterval(loadTable,10000);