huang_de_jun commited on
Commit ·
d951f86
1
Parent(s): 0a9d06f
feat: move question generation to background thread and increase audio chunk to 20s
Browse files
app.py
CHANGED
|
@@ -7,6 +7,7 @@ generate as the talk progresses.
|
|
| 7 |
|
| 8 |
import os
|
| 9 |
import tempfile
|
|
|
|
| 10 |
import numpy as np
|
| 11 |
import gradio as gr
|
| 12 |
from dotenv import load_dotenv
|
|
@@ -20,7 +21,8 @@ load_dotenv()
|
|
| 20 |
|
| 21 |
# Constants
|
| 22 |
SAMPLE_RATE = 16000
|
| 23 |
-
CHUNK_DURATION =
|
|
|
|
| 24 |
MIN_WORDS_FOR_QUESTIONS = 50 # Start generating questions after this many words
|
| 25 |
WORDS_BETWEEN_GENERATIONS = 50 # Generate new questions every N new words
|
| 26 |
MAX_WORDS_CAP = 500 # Stop processing new content after this many words (rate limiting)
|
|
@@ -37,10 +39,41 @@ def get_initial_state():
|
|
| 37 |
"transcript": "",
|
| 38 |
"questions_html": format_questions_html([]),
|
| 39 |
"log_html": format_activity_log([]),
|
| 40 |
-
"speaker_name": ""
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
def process_streaming_audio(
|
| 45 |
audio_chunk,
|
| 46 |
state: dict
|
|
@@ -102,9 +135,11 @@ def process_streaming_audio(
|
|
| 102 |
state
|
| 103 |
)
|
| 104 |
|
| 105 |
-
# Process the buffered audio
|
| 106 |
audio_to_process = state["audio_buffer"][:samples_needed]
|
| 107 |
-
|
|
|
|
|
|
|
| 108 |
|
| 109 |
try:
|
| 110 |
# Save to temp file for Whisper
|
|
@@ -149,33 +184,31 @@ def process_streaming_audio(
|
|
| 149 |
# Check if we should generate questions
|
| 150 |
should_generate = (
|
| 151 |
current_word_count >= MIN_WORDS_FOR_QUESTIONS and
|
| 152 |
-
words_since_last >= WORDS_BETWEEN_GENERATIONS
|
|
|
|
| 153 |
)
|
| 154 |
|
| 155 |
if should_generate:
|
| 156 |
state["last_word_count"] = current_word_count
|
|
|
|
| 157 |
|
| 158 |
-
#
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
activity_log=activity_log,
|
| 164 |
-
speaker_name=speaker
|
| 165 |
)
|
|
|
|
| 166 |
|
| 167 |
-
|
| 168 |
-
state["questions_html"] = questions_html
|
| 169 |
-
state["activity_log"] = activity_log
|
| 170 |
-
state["log_html"] = log_html
|
| 171 |
-
|
| 172 |
-
status = f'<div class="status-bar">✨ Generated {len(questions)} questions | {current_word_count} words</div>'
|
| 173 |
else:
|
| 174 |
# Update log HTML
|
| 175 |
state["log_html"] = format_activity_log(activity_log)
|
| 176 |
|
| 177 |
-
# Show
|
| 178 |
-
if
|
|
|
|
|
|
|
| 179 |
need_more = MIN_WORDS_FOR_QUESTIONS - current_word_count
|
| 180 |
status = f'<div class="status-bar">🎤 Listening... {current_word_count} words (need {need_more} more to start generating)</div>'
|
| 181 |
|
|
@@ -256,23 +289,23 @@ def add_manual_context(text: str, state: dict) -> tuple[str, str, str, str, dict
|
|
| 256 |
|
| 257 |
status = f'<div class="status-bar">Added text. Total: {current_word_count} words</div>'
|
| 258 |
|
| 259 |
-
# Generate questions if enough content
|
| 260 |
-
if current_word_count >= MIN_WORDS_FOR_QUESTIONS:
|
| 261 |
-
speaker = state.get("speaker_name", "")
|
| 262 |
-
questions, activity_log, questions_html, log_html = generate_questions_sync(
|
| 263 |
-
context=ctx,
|
| 264 |
-
existing_questions=state.get("questions", []),
|
| 265 |
-
activity_log=activity_log,
|
| 266 |
-
speaker_name=speaker
|
| 267 |
-
)
|
| 268 |
-
|
| 269 |
-
state["questions"] = questions
|
| 270 |
-
state["questions_html"] = questions_html
|
| 271 |
-
state["activity_log"] = activity_log
|
| 272 |
-
state["log_html"] = log_html
|
| 273 |
state["last_word_count"] = current_word_count
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
-
status = f'<div class="status-bar">
|
|
|
|
|
|
|
|
|
|
| 276 |
else:
|
| 277 |
state["log_html"] = format_activity_log(activity_log)
|
| 278 |
need_more = MIN_WORDS_FOR_QUESTIONS - current_word_count
|
|
@@ -449,7 +482,7 @@ with gr.Blocks(css=custom_css, title="Ask The Right Question") as demo:
|
|
| 449 |
streaming=True,
|
| 450 |
label="Click to start listening"
|
| 451 |
)
|
| 452 |
-
gr.Markdown("*Audio is processed every
|
| 453 |
|
| 454 |
with gr.Tab("Paste Text"):
|
| 455 |
manual_text = gr.Textbox(
|
|
|
|
| 7 |
|
| 8 |
import os
|
| 9 |
import tempfile
|
| 10 |
+
import threading
|
| 11 |
import numpy as np
|
| 12 |
import gradio as gr
|
| 13 |
from dotenv import load_dotenv
|
|
|
|
| 21 |
|
| 22 |
# Constants
|
| 23 |
SAMPLE_RATE = 16000
|
| 24 |
+
CHUNK_DURATION = 20 # Process audio every 20 seconds (longer = better context for Whisper)
|
| 25 |
+
OVERLAP_DURATION = 2 # Keep 2 seconds overlap between chunks to prevent word cutoffs
|
| 26 |
MIN_WORDS_FOR_QUESTIONS = 50 # Start generating questions after this many words
|
| 27 |
WORDS_BETWEEN_GENERATIONS = 50 # Generate new questions every N new words
|
| 28 |
MAX_WORDS_CAP = 500 # Stop processing new content after this many words (rate limiting)
|
|
|
|
| 39 |
"transcript": "",
|
| 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.
|
| 50 |
+
Updates state dict directly (shared reference).
|
| 51 |
+
"""
|
| 52 |
+
try:
|
| 53 |
+
ctx = state["context"]
|
| 54 |
+
speaker = state.get("speaker_name", "")
|
| 55 |
+
activity_log = state.get("activity_log", [])
|
| 56 |
+
|
| 57 |
+
questions, activity_log, questions_html, log_html = generate_questions_sync(
|
| 58 |
+
context=ctx,
|
| 59 |
+
existing_questions=state.get("questions", []),
|
| 60 |
+
activity_log=activity_log,
|
| 61 |
+
speaker_name=speaker
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Update state with results
|
| 65 |
+
state["questions"] = questions
|
| 66 |
+
state["questions_html"] = questions_html
|
| 67 |
+
state["activity_log"] = activity_log
|
| 68 |
+
state["log_html"] = log_html
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
state["activity_log"].append({"type": "error", "msg": str(e)})
|
| 72 |
+
state["log_html"] = format_activity_log(state["activity_log"])
|
| 73 |
+
finally:
|
| 74 |
+
state["generation_in_progress"] = False
|
| 75 |
+
|
| 76 |
+
|
| 77 |
def process_streaming_audio(
|
| 78 |
audio_chunk,
|
| 79 |
state: dict
|
|
|
|
| 135 |
state
|
| 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
|
|
|
|
| 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 |
|
|
|
|
| 289 |
|
| 290 |
status = f'<div class="status-bar">Added text. Total: {current_word_count} words</div>'
|
| 291 |
|
| 292 |
+
# Generate questions if enough content and not already generating
|
| 293 |
+
if current_word_count >= MIN_WORDS_FOR_QUESTIONS and not state.get("generation_in_progress", False):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
state["last_word_count"] = current_word_count
|
| 295 |
+
state["generation_in_progress"] = True
|
| 296 |
+
|
| 297 |
+
# Start background thread for question generation (non-blocking)
|
| 298 |
+
thread = threading.Thread(
|
| 299 |
+
target=generate_questions_background,
|
| 300 |
+
args=(state,),
|
| 301 |
+
daemon=True
|
| 302 |
+
)
|
| 303 |
+
thread.start()
|
| 304 |
|
| 305 |
+
status = f'<div class="status-bar">Added text. Total: {current_word_count} words | ⚙️ Generating questions...</div>'
|
| 306 |
+
elif state.get("generation_in_progress", False):
|
| 307 |
+
state["log_html"] = format_activity_log(activity_log)
|
| 308 |
+
status = f'<div class="status-bar">Added text. Total: {current_word_count} words | ⚙️ Already generating...</div>'
|
| 309 |
else:
|
| 310 |
state["log_html"] = format_activity_log(activity_log)
|
| 311 |
need_more = MIN_WORDS_FOR_QUESTIONS - current_word_count
|
|
|
|
| 482 |
streaming=True,
|
| 483 |
label="Click to start listening"
|
| 484 |
)
|
| 485 |
+
gr.Markdown("*Audio is processed every 20 seconds*")
|
| 486 |
|
| 487 |
with gr.Tab("Paste Text"):
|
| 488 |
manual_text = gr.Textbox(
|