bluenevus commited on
Commit
6dc9c9e
·
1 Parent(s): 4568c7d

Update app.py via AI Editor

Browse files
Files changed (1) hide show
  1. app.py +155 -50
app.py CHANGED
@@ -7,18 +7,17 @@ import threading
7
  import logging
8
  import time
9
  import base64
10
- import json
11
  from datetime import datetime
 
12
  from flask import request
13
  import dash
14
  from dash import Dash, dcc, html, Input, Output, State, callback_context, no_update
15
  import dash_bootstrap_components as dbc
16
- import dash_daq as daq
17
  import requests
18
  import docx
19
 
20
  AUDIO_CHUNK_SIZE_MB = 25
21
- AUDIO_EXTS = ['.mp3', '.wav', '.m4a', '.ogg']
22
  TRANSCRIPT_FILENAME = 'transcript.docx'
23
  MINUTES_FILENAME = 'minutes.docx'
24
  RAW_AUDIO_FILENAME = 'meeting_audio.wav'
@@ -171,7 +170,108 @@ external_stylesheets = [dbc.themes.BOOTSTRAP, '/assets/custom.css']
171
  app = Dash(__name__, external_stylesheets=external_stylesheets, suppress_callback_exceptions=True)
172
  server = app.server
173
 
174
- def build_left_nav(sid, user_dir):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
  audio_path = get_audio_path(user_dir)
176
  transcript_path = get_transcript_path(user_dir)
177
  minutes_path = get_minutes_path(user_dir)
@@ -186,22 +286,23 @@ def build_left_nav(sid, user_dir):
186
  links.append(dbc.NavLink("Meeting Minutes", href="#", id="nav-minutes", n_clicks=0, className="mb-2"))
187
  links.append(html.A("Download Minutes (docx)", href=f"/download/minutes/{sid}", target="_blank", id="dl-minutes", className="mb-2 d-block"))
188
  links.append(html.Hr())
189
- links.append(dbc.Button("Delete All Files", id="btn-clear-all", color="secondary", className="mb-2", n_clicks=0))
 
 
 
 
 
 
 
 
 
 
 
 
190
  return dbc.Card(
191
  dbc.CardBody([
192
  html.H4("Navigation/Downloads", className="card-title"),
193
  html.Div(links, id="left-links"),
194
- html.Hr(),
195
- dbc.Button("Record Meeting", id="btn-record", color="primary", className="mb-2", n_clicks=0),
196
- dcc.Upload(
197
- id='upload-audio',
198
- children=html.Div([
199
- 'Or drag and drop/upload meeting audio here'
200
- ]),
201
- multiple=False,
202
- accept="audio/*",
203
- className='mb-2'
204
- ),
205
  ]),
206
  className="mb-2"
207
  )
@@ -210,12 +311,12 @@ def build_right_preview(content_type, content, sid, user_dir):
210
  if content_type == "audio":
211
  audio_path = get_audio_path(user_dir)
212
  if not file_exists(audio_path):
213
- return html.Div("No audio recorded/uploaded yet.")
214
  return html.Audio(src=f"/preview/audio/{sid}", controls=True, style={"width": "100%"})
215
  elif content_type == "transcript":
216
  transcript_path = get_transcript_path(user_dir)
217
  if not file_exists(transcript_path):
218
- return html.Div("No transcript found. Please record/upload and transcribe meeting first.")
219
  with open(transcript_path, "rb") as f:
220
  doc = docx.Document(f)
221
  text = "\n".join([para.text for para in doc.paragraphs])
@@ -226,7 +327,7 @@ def build_right_preview(content_type, content, sid, user_dir):
226
  elif content_type == "minutes":
227
  minutes_path = get_minutes_path(user_dir)
228
  if not file_exists(minutes_path):
229
- return html.Div("No meeting minutes found. Please transcribe meeting first.")
230
  with open(minutes_path, "rb") as f:
231
  doc = docx.Document(f)
232
  text = "\n".join([para.text for para in doc.paragraphs])
@@ -241,16 +342,8 @@ app.layout = dbc.Container([
241
  dcc.Store(id="store-session", storage_type="session"),
242
  dcc.Store(id="store-preview", data={"type": "none"}),
243
  dcc.Store(id="store-transcript-status", data={"status": "idle"}),
 
244
  dcc.Interval(id="interval-refresh", interval=60000, n_intervals=0),
245
- # Hidden dcc.Upload to satisfy Dash callback registration
246
- html.Div([
247
- dcc.Upload(
248
- id='upload-audio',
249
- children=None,
250
- style={"display": "none"}
251
- )
252
- ], style={"display": "none"}),
253
- # Hidden dummy components for Dash callback recognition
254
  html.Div([
255
  dbc.Button(id="btn-record", style={"display": "none"}),
256
  dbc.Button(id="btn-clear-all", style={"display": "none"}),
@@ -292,20 +385,19 @@ app.layout = dbc.Container([
292
  Output("right-preview", "children"),
293
  Output("store-preview", "data"),
294
  Input("interval-refresh", "n_intervals"),
295
- Input("upload-audio", "contents"),
296
- Input("btn-record", "n_clicks"),
297
- Input("store-transcript-status", "data"),
298
  Input("nav-audio", "n_clicks"),
299
  Input("nav-transcript", "n_clicks"),
300
  Input("nav-minutes", "n_clicks"),
301
  Input("btn-clear-all", "n_clicks"),
302
  State("store-session", "data"),
303
  State("store-preview", "data"),
 
304
  prevent_initial_call=False
305
  )
306
- def main_controller(interval_refresh, upload_contents, btn_record, transcript_status_data,
307
  nav_audio, nav_transcript, nav_minutes, btn_clear_all,
308
- session_data, store_preview_data):
309
 
310
  ctx = callback_context
311
  triggered = ctx.triggered
@@ -314,7 +406,8 @@ def main_controller(interval_refresh, upload_contents, btn_record, transcript_st
314
  sid = session_manager.get_session_id()
315
  user_dir = session_manager.get_user_dir(sid)
316
  lock = session_manager.get_lock(sid)
317
- left_nav = build_left_nav(sid, user_dir)
 
318
  right_preview = build_right_preview("none", None, sid, user_dir)
319
  store_preview = {"type": "none"}
320
  transcript_status = {"status": "idle"}
@@ -325,19 +418,18 @@ def main_controller(interval_refresh, upload_contents, btn_record, transcript_st
325
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
326
  store_preview = store_preview_data if store_preview_data else {"type": "none"}
327
  right_preview = build_right_preview(store_preview.get('type', 'none'), None, sid, user_dir)
 
328
  return session_store, left_nav, transcript_status, right_preview, store_preview
329
 
330
- elif trigger_id == "upload-audio" and upload_contents:
331
- with lock:
332
- save_uploaded_audio(upload_contents, user_dir)
333
- logging.info(f"Audio uploaded for session: {sid}")
334
- transcript_status = {"status": "audio_uploaded"}
335
- left_nav = build_left_nav(sid, user_dir)
336
- return session_store, left_nav, transcript_status, right_preview, store_preview
337
-
338
- elif trigger_id == "btn-record":
339
- transcript_status = {"status": "idle"}
340
- return session_store, left_nav, transcript_status, right_preview, store_preview
341
 
342
  elif trigger_id == "store-transcript-status":
343
  if transcript_status_data and transcript_status_data.get("status") == "audio_uploaded":
@@ -356,16 +448,16 @@ def main_controller(interval_refresh, upload_contents, btn_record, transcript_st
356
  minutes_path = get_minutes_path(user_dir)
357
  write_docx(minutes_text, minutes_path)
358
  logging.info(f"Transcription and minutes generated for session {sid}")
359
- left_nav = build_left_nav(sid, user_dir)
360
  transcript_status = {"status": "done"}
361
  return session_store, left_nav, transcript_status, right_preview, store_preview
362
  else:
363
- left_nav = build_left_nav(sid, user_dir)
364
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
365
  return session_store, left_nav, transcript_status, right_preview, store_preview
366
 
367
  elif trigger_id in ["nav-audio", "nav-transcript", "nav-minutes"]:
368
- left_nav = build_left_nav(sid, user_dir)
369
  if trigger_id == "nav-audio":
370
  right_preview = build_right_preview("audio", None, sid, user_dir)
371
  store_preview = {"type": "audio"}
@@ -382,18 +474,31 @@ def main_controller(interval_refresh, upload_contents, btn_record, transcript_st
382
  session_manager.clear_session(sid)
383
  logging.info(f"Cleared all files for session {sid}")
384
  user_dir = session_manager.get_user_dir(sid)
385
- left_nav = build_left_nav(sid, user_dir)
386
  right_preview = build_right_preview("none", None, sid, user_dir)
387
  transcript_status = {"status": "idle"}
388
  store_preview = {"type": "none"}
389
  return session_store, left_nav, transcript_status, right_preview, store_preview
390
 
391
- left_nav = build_left_nav(sid, user_dir)
392
  right_preview = build_right_preview("none", None, sid, user_dir)
393
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
394
  store_preview = store_preview_data if store_preview_data else {"type": "none"}
395
  return session_store, left_nav, transcript_status, right_preview, store_preview
396
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  @server.route('/preview/audio/<sid>')
398
  def serve_audio_preview(sid):
399
  user_dir = session_manager.get_user_dir(sid)
 
7
  import logging
8
  import time
9
  import base64
 
10
  from datetime import datetime
11
+ import json
12
  from flask import request
13
  import dash
14
  from dash import Dash, dcc, html, Input, Output, State, callback_context, no_update
15
  import dash_bootstrap_components as dbc
 
16
  import requests
17
  import docx
18
 
19
  AUDIO_CHUNK_SIZE_MB = 25
20
+ AUDIO_EXTS = ['.wav']
21
  TRANSCRIPT_FILENAME = 'transcript.docx'
22
  MINUTES_FILENAME = 'minutes.docx'
23
  RAW_AUDIO_FILENAME = 'meeting_audio.wav'
 
170
  app = Dash(__name__, external_stylesheets=external_stylesheets, suppress_callback_exceptions=True)
171
  server = app.server
172
 
173
+ recording_js = '''
174
+ window.dash_clientside = window.dash_clientside || {};
175
+ window.dash_clientside.meetingrecorder = {
176
+ recorderState: {
177
+ mediaRecorder: null,
178
+ audioChunks: [],
179
+ isRecording: false
180
+ },
181
+ recordOrStop: function(nClicks, currentState) {
182
+ let that = window.dash_clientside.meetingrecorder;
183
+ if (!nClicks) {
184
+ return window.dash_clientside.no_update;
185
+ }
186
+ if (!that.recorderState.isRecording) {
187
+ navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1 }, video: false }).then(function(stream) {
188
+ const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/wav' });
189
+ that.recorderState.mediaRecorder = mediaRecorder;
190
+ that.recorderState.audioChunks = [];
191
+ that.recorderState.isRecording = true;
192
+ mediaRecorder.ondataavailable = function(event) {
193
+ if (event.data.size > 0) {
194
+ that.recorderState.audioChunks.push(event.data);
195
+ }
196
+ };
197
+ mediaRecorder.onstop = function() {
198
+ const audioBlob = new Blob(that.recorderState.audioChunks, {type: 'audio/wav'});
199
+ const reader = new FileReader();
200
+ reader.onloadend = function() {
201
+ const base64data = reader.result;
202
+ const payload = { audio: base64data };
203
+ window.localStorage.setItem("meeting-audio-latest", base64data);
204
+ const evt = new CustomEvent("meeting-audio-recorded");
205
+ window.dispatchEvent(evt);
206
+ };
207
+ reader.readAsDataURL(audioBlob);
208
+ };
209
+ mediaRecorder.start();
210
+ });
211
+ return {isRecording: true};
212
+ } else {
213
+ if (that.recorderState.mediaRecorder) {
214
+ that.recorderState.mediaRecorder.stop();
215
+ that.recorderState.isRecording = false;
216
+ }
217
+ return {isRecording: false};
218
+ }
219
+ }
220
+ };
221
+ '''
222
+
223
+ app.clientside_callback(
224
+ recording_js + '''
225
+ return window.dash_clientside.meetingrecorder.recordOrStop(nClicks, currentState);
226
+ ''',
227
+ Output("store-recording-state", "data"),
228
+ Input("btn-record", "n_clicks"),
229
+ State("store-recording-state", "data"),
230
+ prevent_initial_call=False
231
+ )
232
+
233
+ app.index_string = '''
234
+ <!DOCTYPE html>
235
+ <html>
236
+ <head>
237
+ {%metas%}
238
+ <title>Meeting Recorder & Transcriber</title>
239
+ {%favicon%}
240
+ {%css%}
241
+ </head>
242
+ <body>
243
+ {%app_entry%}
244
+ <script>
245
+ {recording_js}
246
+ document.addEventListener("DOMContentLoaded", function() {
247
+ if (!window.meetingAudioListenerRegistered) {
248
+ window.addEventListener("meeting-audio-recorded", function() {
249
+ var audioData = window.localStorage.getItem("meeting-audio-latest");
250
+ if (audioData) {
251
+ var req = new XMLHttpRequest();
252
+ req.open("POST", "/record_audio", true);
253
+ req.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
254
+ req.onreadystatechange = function() {
255
+ if (req.readyState === 4) {
256
+ window.localStorage.removeItem("meeting-audio-latest");
257
+ window.location.reload();
258
+ }
259
+ };
260
+ req.send(JSON.stringify({ audio: audioData }));
261
+ }
262
+ });
263
+ window.meetingAudioListenerRegistered = true;
264
+ }
265
+ });
266
+ </script>
267
+ {%config%}
268
+ {%scripts%}
269
+ {%renderer%}
270
+ </body>
271
+ </html>
272
+ '''.replace("{recording_js}", recording_js)
273
+
274
+ def build_left_nav(sid, user_dir, is_recording):
275
  audio_path = get_audio_path(user_dir)
276
  transcript_path = get_transcript_path(user_dir)
277
  minutes_path = get_minutes_path(user_dir)
 
286
  links.append(dbc.NavLink("Meeting Minutes", href="#", id="nav-minutes", n_clicks=0, className="mb-2"))
287
  links.append(html.A("Download Minutes (docx)", href=f"/download/minutes/{sid}", target="_blank", id="dl-minutes", className="mb-2 d-block"))
288
  links.append(html.Hr())
289
+ links.append(dbc.Button(
290
+ "Delete All Files", id="btn-clear-all", color="secondary", className="mb-2", n_clicks=0))
291
+ record_label = "Stop Recording" if is_recording else "Record Meeting"
292
+ links.append(
293
+ dbc.Button(
294
+ record_label,
295
+ id="btn-record",
296
+ color="primary" if not is_recording else "danger",
297
+ className="mb-2",
298
+ n_clicks=0,
299
+ style={"width": "100%"}
300
+ )
301
+ )
302
  return dbc.Card(
303
  dbc.CardBody([
304
  html.H4("Navigation/Downloads", className="card-title"),
305
  html.Div(links, id="left-links"),
 
 
 
 
 
 
 
 
 
 
 
306
  ]),
307
  className="mb-2"
308
  )
 
311
  if content_type == "audio":
312
  audio_path = get_audio_path(user_dir)
313
  if not file_exists(audio_path):
314
+ return html.Div("No audio recorded yet.")
315
  return html.Audio(src=f"/preview/audio/{sid}", controls=True, style={"width": "100%"})
316
  elif content_type == "transcript":
317
  transcript_path = get_transcript_path(user_dir)
318
  if not file_exists(transcript_path):
319
+ return html.Div("No transcript found. Please record meeting first.")
320
  with open(transcript_path, "rb") as f:
321
  doc = docx.Document(f)
322
  text = "\n".join([para.text for para in doc.paragraphs])
 
327
  elif content_type == "minutes":
328
  minutes_path = get_minutes_path(user_dir)
329
  if not file_exists(minutes_path):
330
+ return html.Div("No meeting minutes found. Please record and transcribe meeting first.")
331
  with open(minutes_path, "rb") as f:
332
  doc = docx.Document(f)
333
  text = "\n".join([para.text for para in doc.paragraphs])
 
342
  dcc.Store(id="store-session", storage_type="session"),
343
  dcc.Store(id="store-preview", data={"type": "none"}),
344
  dcc.Store(id="store-transcript-status", data={"status": "idle"}),
345
+ dcc.Store(id="store-recording-state", data={"isRecording": False}),
346
  dcc.Interval(id="interval-refresh", interval=60000, n_intervals=0),
 
 
 
 
 
 
 
 
 
347
  html.Div([
348
  dbc.Button(id="btn-record", style={"display": "none"}),
349
  dbc.Button(id="btn-clear-all", style={"display": "none"}),
 
385
  Output("right-preview", "children"),
386
  Output("store-preview", "data"),
387
  Input("interval-refresh", "n_intervals"),
388
+ Input("store-recording-state", "data"),
 
 
389
  Input("nav-audio", "n_clicks"),
390
  Input("nav-transcript", "n_clicks"),
391
  Input("nav-minutes", "n_clicks"),
392
  Input("btn-clear-all", "n_clicks"),
393
  State("store-session", "data"),
394
  State("store-preview", "data"),
395
+ State("store-transcript-status", "data"),
396
  prevent_initial_call=False
397
  )
398
+ def main_controller(interval_refresh, recording_state,
399
  nav_audio, nav_transcript, nav_minutes, btn_clear_all,
400
+ session_data, store_preview_data, transcript_status_data):
401
 
402
  ctx = callback_context
403
  triggered = ctx.triggered
 
406
  sid = session_manager.get_session_id()
407
  user_dir = session_manager.get_user_dir(sid)
408
  lock = session_manager.get_lock(sid)
409
+ is_recording = (recording_state or {}).get("isRecording", False)
410
+ left_nav = build_left_nav(sid, user_dir, is_recording)
411
  right_preview = build_right_preview("none", None, sid, user_dir)
412
  store_preview = {"type": "none"}
413
  transcript_status = {"status": "idle"}
 
418
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
419
  store_preview = store_preview_data if store_preview_data else {"type": "none"}
420
  right_preview = build_right_preview(store_preview.get('type', 'none'), None, sid, user_dir)
421
+ left_nav = build_left_nav(sid, user_dir, is_recording)
422
  return session_store, left_nav, transcript_status, right_preview, store_preview
423
 
424
+ elif trigger_id == "store-recording-state":
425
+ audio_path = get_audio_path(user_dir)
426
+ if file_exists(audio_path) and not is_recording:
427
+ left_nav = build_left_nav(sid, user_dir, False)
428
+ transcript_status = {"status": "audio_uploaded"}
429
+ return session_store, left_nav, transcript_status, right_preview, store_preview
430
+ else:
431
+ left_nav = build_left_nav(sid, user_dir, is_recording)
432
+ return session_store, left_nav, transcript_status_data if transcript_status_data else {"status": "idle"}, right_preview, store_preview
 
 
433
 
434
  elif trigger_id == "store-transcript-status":
435
  if transcript_status_data and transcript_status_data.get("status") == "audio_uploaded":
 
448
  minutes_path = get_minutes_path(user_dir)
449
  write_docx(minutes_text, minutes_path)
450
  logging.info(f"Transcription and minutes generated for session {sid}")
451
+ left_nav = build_left_nav(sid, user_dir, False)
452
  transcript_status = {"status": "done"}
453
  return session_store, left_nav, transcript_status, right_preview, store_preview
454
  else:
455
+ left_nav = build_left_nav(sid, user_dir, is_recording)
456
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
457
  return session_store, left_nav, transcript_status, right_preview, store_preview
458
 
459
  elif trigger_id in ["nav-audio", "nav-transcript", "nav-minutes"]:
460
+ left_nav = build_left_nav(sid, user_dir, is_recording)
461
  if trigger_id == "nav-audio":
462
  right_preview = build_right_preview("audio", None, sid, user_dir)
463
  store_preview = {"type": "audio"}
 
474
  session_manager.clear_session(sid)
475
  logging.info(f"Cleared all files for session {sid}")
476
  user_dir = session_manager.get_user_dir(sid)
477
+ left_nav = build_left_nav(sid, user_dir, False)
478
  right_preview = build_right_preview("none", None, sid, user_dir)
479
  transcript_status = {"status": "idle"}
480
  store_preview = {"type": "none"}
481
  return session_store, left_nav, transcript_status, right_preview, store_preview
482
 
483
+ left_nav = build_left_nav(sid, user_dir, is_recording)
484
  right_preview = build_right_preview("none", None, sid, user_dir)
485
  transcript_status = transcript_status_data if transcript_status_data else {"status": "idle"}
486
  store_preview = store_preview_data if store_preview_data else {"type": "none"}
487
  return session_store, left_nav, transcript_status, right_preview, store_preview
488
 
489
+ @server.route('/record_audio', methods=["POST"])
490
+ def record_audio():
491
+ sid = session_manager.get_session_id()
492
+ user_dir = session_manager.get_user_dir(sid)
493
+ data = request.get_json(force=True)
494
+ audio_b64 = data.get("audio", "")
495
+ if audio_b64:
496
+ save_uploaded_audio(audio_b64, user_dir)
497
+ logging.info(f"Audio recorded and saved for session {sid}")
498
+ return "OK", 200
499
+ else:
500
+ return "No audio data", 400
501
+
502
  @server.route('/preview/audio/<sid>')
503
  def serve_audio_preview(sid):
504
  user_dir = session_manager.get_user_dir(sid)