huang_de_jun commited on
Commit
e8ee7ad
Β·
1 Parent(s): d951f86

feat: add background threads for transcription and show statuses concurrently

Browse files
Files changed (1) hide show
  1. app.py +109 -99
app.py CHANGED
@@ -40,10 +40,72 @@ def get_initial_state():
40
  "questions_html": format_questions_html([]),
41
  "log_html": format_activity_log([]),
42
  "speaker_name": "",
43
- "generation_in_progress": False # Track if background generation is running
 
44
  }
45
 
46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  def generate_questions_background(state: dict):
48
  """
49
  Generate questions in background thread.
@@ -103,7 +165,6 @@ def process_streaming_audio(
103
  if "context" not in state:
104
  state = get_initial_state()
105
 
106
- ctx = state["context"]
107
  activity_log = state.get("activity_log", [])
108
 
109
  # Convert to float32 if needed
@@ -122,11 +183,36 @@ def process_streaming_audio(
122
  # Accumulate audio
123
  state["audio_buffer"] = np.concatenate([state.get("audio_buffer", np.array([], dtype=np.float32)), audio_data])
124
 
125
- # Check if we have enough audio to process (CHUNK_DURATION seconds)
126
  samples_needed = sample_rate * CHUNK_DURATION
127
- if len(state["audio_buffer"]) < samples_needed:
128
- word_count = len(ctx.get_full_transcript().split())
129
- status = f'<div class="status-bar">🎀 Listening... buffering audio ({len(state["audio_buffer"])}/{samples_needed} samples)</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  return (
131
  state.get("transcript", ""),
132
  state.get("log_html", format_activity_log(activity_log)),
@@ -136,107 +222,31 @@ def process_streaming_audio(
136
  )
137
 
138
  # Process the buffered audio with overlap
139
- audio_to_process = state["audio_buffer"][:samples_needed]
140
  # Keep last OVERLAP_DURATION seconds for next chunk to prevent word cutoffs
141
  overlap_samples = sample_rate * OVERLAP_DURATION
142
  state["audio_buffer"] = state["audio_buffer"][samples_needed - overlap_samples:]
143
 
144
- try:
145
- # Save to temp file for Whisper
146
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
147
- import wave
148
- with wave.open(f.name, 'wb') as wav:
149
- wav.setnchannels(1)
150
- wav.setsampwidth(2)
151
- wav.setframerate(sample_rate)
152
- wav.writeframes((audio_to_process * 32767).astype(np.int16).tobytes())
153
- temp_path = f.name
154
-
155
- # Transcribe
156
- new_text = transcribe_audio(temp_path)
157
- os.unlink(temp_path)
158
-
159
- if new_text.strip():
160
- ctx.add_transcript(new_text)
161
- state["transcript"] = ctx.get_full_transcript()
162
-
163
- current_word_count = len(state["transcript"].split())
164
- words_since_last = current_word_count - state.get("last_word_count", 0)
165
-
166
- # Add transcription to activity log
167
- activity_log.append({"type": "transcribe", "words": current_word_count})
168
- state["activity_log"] = activity_log
169
-
170
- # Check if word cap reached
171
- if current_word_count >= MAX_WORDS_CAP:
172
- status = f'<div class="status-bar" style="border-color:#f59e0b;">⚠️ Word limit reached ({MAX_WORDS_CAP} words). Clear session to continue.</div>'
173
- state["log_html"] = format_activity_log(activity_log)
174
- return (
175
- state["transcript"],
176
- state["log_html"],
177
- state.get("questions_html", format_questions_html([])),
178
- status,
179
- state
180
- )
181
-
182
- status = f'<div class="status-bar">🎀 Listening... {current_word_count} words captured</div>'
183
-
184
- # Check if we should generate questions
185
- should_generate = (
186
- current_word_count >= MIN_WORDS_FOR_QUESTIONS and
187
- words_since_last >= WORDS_BETWEEN_GENERATIONS and
188
- not state.get("generation_in_progress", False) # Don't start if already generating
189
- )
190
-
191
- if should_generate:
192
- state["last_word_count"] = current_word_count
193
- state["generation_in_progress"] = True
194
-
195
- # Start background thread for question generation (non-blocking)
196
- thread = threading.Thread(
197
- target=generate_questions_background,
198
- args=(state,),
199
- daemon=True
200
- )
201
- thread.start()
202
-
203
- status = f'<div class="status-bar">🎀 Listening... {current_word_count} words | βš™οΈ Generating questions...</div>'
204
- else:
205
- # Update log HTML
206
- state["log_html"] = format_activity_log(activity_log)
207
-
208
- # Show appropriate status
209
- if state.get("generation_in_progress", False):
210
- status = f'<div class="status-bar">🎀 Listening... {current_word_count} words | βš™οΈ Generating questions...</div>'
211
- elif current_word_count < MIN_WORDS_FOR_QUESTIONS:
212
- need_more = MIN_WORDS_FOR_QUESTIONS - current_word_count
213
- status = f'<div class="status-bar">🎀 Listening... {current_word_count} words (need {need_more} more to start generating)</div>'
214
-
215
- return (
216
- state["transcript"],
217
- state["log_html"],
218
- state.get("questions_html", format_questions_html([])),
219
- status,
220
- state
221
- )
222
 
223
- except Exception as e:
224
- activity_log.append({"type": "error", "msg": str(e)})
225
- state["activity_log"] = activity_log
226
- state["log_html"] = format_activity_log(activity_log)
227
- return (
228
- state.get("transcript", ""),
229
- state["log_html"],
230
- state.get("questions_html", format_questions_html([])),
231
- f'<div class="status-bar" style="border-color:#ef4444;">❌ Error: {str(e)}</div>',
232
- state
233
- )
234
 
235
  return (
236
  state.get("transcript", ""),
237
- state.get("log_html", format_activity_log([])),
238
- state.get("questions_html", format_questions_html([])),
239
- '<div class="status-bar">Processing...</div>',
240
  state
241
  )
242
 
 
40
  "questions_html": format_questions_html([]),
41
  "log_html": format_activity_log([]),
42
  "speaker_name": "",
43
+ "generation_in_progress": False, # Track if background generation is running
44
+ "transcription_in_progress": False # Track if background transcription is running
45
  }
46
 
47
 
48
+ def transcribe_audio_background(audio_data: np.ndarray, sample_rate: int, state: dict):
49
+ """
50
+ Transcribe audio in background thread.
51
+ Updates state dict directly (shared reference).
52
+ After transcription, may trigger question generation.
53
+ """
54
+ try:
55
+ import wave
56
+
57
+ # Save to temp file for Whisper
58
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
59
+ with wave.open(f.name, 'wb') as wav:
60
+ wav.setnchannels(1)
61
+ wav.setsampwidth(2)
62
+ wav.setframerate(sample_rate)
63
+ wav.writeframes((audio_data * 32767).astype(np.int16).tobytes())
64
+ temp_path = f.name
65
+
66
+ # Transcribe
67
+ new_text = transcribe_audio(temp_path)
68
+ os.unlink(temp_path)
69
+
70
+ if new_text.strip():
71
+ ctx = state["context"]
72
+ ctx.add_transcript(new_text)
73
+ state["transcript"] = ctx.get_full_transcript()
74
+
75
+ current_word_count = len(state["transcript"].split())
76
+ words_since_last = current_word_count - state.get("last_word_count", 0)
77
+
78
+ # Add transcription to activity log
79
+ state["activity_log"].append({"type": "transcribe", "words": current_word_count})
80
+ state["log_html"] = format_activity_log(state["activity_log"])
81
+
82
+ # Check if we should generate questions (and not already generating)
83
+ should_generate = (
84
+ current_word_count >= MIN_WORDS_FOR_QUESTIONS and
85
+ current_word_count < MAX_WORDS_CAP and
86
+ words_since_last >= WORDS_BETWEEN_GENERATIONS and
87
+ not state.get("generation_in_progress", False)
88
+ )
89
+
90
+ if should_generate:
91
+ state["last_word_count"] = current_word_count
92
+ state["generation_in_progress"] = True
93
+
94
+ # Start question generation in another thread
95
+ thread = threading.Thread(
96
+ target=generate_questions_background,
97
+ args=(state,),
98
+ daemon=True
99
+ )
100
+ thread.start()
101
+
102
+ except Exception as e:
103
+ state["activity_log"].append({"type": "error", "msg": f"Transcription error: {str(e)}"})
104
+ state["log_html"] = format_activity_log(state["activity_log"])
105
+ finally:
106
+ state["transcription_in_progress"] = False
107
+
108
+
109
  def generate_questions_background(state: dict):
110
  """
111
  Generate questions in background thread.
 
165
  if "context" not in state:
166
  state = get_initial_state()
167
 
 
168
  activity_log = state.get("activity_log", [])
169
 
170
  # Convert to float32 if needed
 
183
  # Accumulate audio
184
  state["audio_buffer"] = np.concatenate([state.get("audio_buffer", np.array([], dtype=np.float32)), audio_data])
185
 
186
+ # Calculate status info
187
  samples_needed = sample_rate * CHUNK_DURATION
188
+ buffer_samples = len(state["audio_buffer"])
189
+ buffer_pct = int(buffer_samples / samples_needed * 100)
190
+ word_count = len(state.get("transcript", "").split())
191
+ is_transcribing = state.get("transcription_in_progress", False)
192
+ is_generating = state.get("generation_in_progress", False)
193
+
194
+ # Build status parts
195
+ status_parts = [f"🎀 Buffer: {buffer_pct}%", f"πŸ“ {word_count} words"]
196
+ if is_transcribing:
197
+ status_parts.append("πŸ”„ Transcribing...")
198
+ if is_generating:
199
+ status_parts.append("βš™οΈ Generating...")
200
+
201
+ status = f'<div class="status-bar">{" | ".join(status_parts)}</div>'
202
+
203
+ # Check if we have enough audio to process (CHUNK_DURATION seconds)
204
+ if buffer_samples < samples_needed:
205
+ return (
206
+ state.get("transcript", ""),
207
+ state.get("log_html", format_activity_log(activity_log)),
208
+ state.get("questions_html", format_questions_html(state.get("questions", []))),
209
+ status,
210
+ state
211
+ )
212
+
213
+ # Check if transcription is already running (sequential queue)
214
+ if is_transcribing:
215
+ # Keep buffering, don't start new transcription yet
216
  return (
217
  state.get("transcript", ""),
218
  state.get("log_html", format_activity_log(activity_log)),
 
222
  )
223
 
224
  # Process the buffered audio with overlap
225
+ audio_to_process = state["audio_buffer"][:samples_needed].copy() # Copy for thread safety
226
  # Keep last OVERLAP_DURATION seconds for next chunk to prevent word cutoffs
227
  overlap_samples = sample_rate * OVERLAP_DURATION
228
  state["audio_buffer"] = state["audio_buffer"][samples_needed - overlap_samples:]
229
 
230
+ # Start background transcription (non-blocking)
231
+ state["transcription_in_progress"] = True
232
+ thread = threading.Thread(
233
+ target=transcribe_audio_background,
234
+ args=(audio_to_process, sample_rate, state),
235
+ daemon=True
236
+ )
237
+ thread.start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
 
239
+ # Update status to show transcribing started
240
+ status_parts = [f"🎀 Buffer: {int(overlap_samples / samples_needed * 100)}%", f"πŸ“ {word_count} words", "πŸ”„ Transcribing..."]
241
+ if is_generating:
242
+ status_parts.append("βš™οΈ Generating...")
243
+ status = f'<div class="status-bar">{" | ".join(status_parts)}</div>'
 
 
 
 
 
 
244
 
245
  return (
246
  state.get("transcript", ""),
247
+ state.get("log_html", format_activity_log(activity_log)),
248
+ state.get("questions_html", format_questions_html(state.get("questions", []))),
249
+ status,
250
  state
251
  )
252