Shubham2004 commited on
Commit
2a84d57
Β·
verified Β·
1 Parent(s): 669409e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +486 -0
app.py ADDED
@@ -0,0 +1,486 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Gradio interface for Speech Disfluency Detection
3
+ -------------------------------------------------
4
+ Wraps ChildFluencyNet (stage2_fyp_best.pt).
5
+ Part of a multi-modal neurodevelopmental assessment system.
6
+
7
+ Same inference logic as FastAPI version, but deployed on HF Spaces via Gradio.
8
+ """
9
+
10
+ import io
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ import time
16
+ import yaml
17
+ import tempfile
18
+ import traceback
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional, Tuple
21
+
22
+ import torch
23
+ import numpy as np
24
+ import librosa
25
+ import gradio as gr
26
+ from huggingface_hub import hf_hub_download
27
+ from dotenv import load_dotenv
28
+
29
+ load_dotenv()
30
+
31
+ # ── Path setup ────────────────────────────────────────────────────────────
32
+ ROOT = Path(__file__).resolve().parent
33
+ sys.path.insert(0, str(ROOT))
34
+
35
+ # Import your model
36
+ try:
37
+ from childfluency import ChildFluencyNet
38
+ except ImportError:
39
+ print("[startup] Warning: Could not import ChildFluencyNet from local childfluency.py")
40
+ print("[startup] Make sure childfluency.py is in the same directory as app.py")
41
+
42
+ # ── Load params ───────────────────────────────────────────────────────────
43
+ with open(ROOT / 'params.yaml') as f:
44
+ params = yaml.safe_load(f)
45
+
46
+ TARGET_SR = params['data']['target_sr'] # 16000
47
+ WINDOW_SEC = params['data']['window_sec'] # 4.0
48
+ STRIDE_SEC = params['data']['stride_sec'] # 2.0
49
+ MIN_RMS = params['data']['min_rms'] # 0.001
50
+
51
+ LPE_OUTPUT_DIM = params['model']['lpe_dim'] # 32
52
+ LPE_INPUT_FEATURES = 6 # 6
53
+
54
+ DEFAULT_CKPT = os.environ.get(
55
+ 'DISFLUENCY_CHECKPOINT',
56
+ str(ROOT / 'stage2_fyp_best.pt')
57
+ )
58
+
59
+ # ── Gemini & Groq ─────────────────────────────────────────────────────────
60
+ GEMINI_API_KEY = os.environ.get('GEMINI_API_KEY')
61
+ GROK_API_KEY = os.environ.get('GROK_API_KEY')
62
+ gemini_model = None
63
+
64
+ if GEMINI_API_KEY:
65
+ try:
66
+ import google.generativeai as genai
67
+ genai.configure(api_key=GEMINI_API_KEY)
68
+ gemini_model = genai.GenerativeModel('gemini-2.0-flash')
69
+ print("[startup] Gemini API initialised.")
70
+ except Exception as e:
71
+ print(f"[startup] Warning: Gemini init failed: {e}")
72
+ else:
73
+ print("[startup] GEMINI_API_KEY not set β€” will rely on Groq fallback.")
74
+
75
+ if GROK_API_KEY:
76
+ print("[startup] Groq API key found β€” available as fallback.")
77
+ else:
78
+ print("[startup] GROK_API_KEY not set β€” Groq fallback disabled.")
79
+
80
+ # ── Load Model at Startup ──────────────────────────────────────────────────
81
+ print("[startup] Loading model...")
82
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
83
+ print(f"[startup] Device: {device}")
84
+
85
+ # Download model from HF Hub
86
+ print("[startup] Downloading model from HuggingFace Hub...")
87
+ try:
88
+ model_path = hf_hub_download(
89
+ repo_id="yourusername/developmental-stuttering-detection",
90
+ filename="stage2_fyp_best.pt",
91
+ cache_dir="./models"
92
+ )
93
+ except Exception as e:
94
+ print(f"[startup] Warning: HF Hub download failed: {e}")
95
+ model_path = Path(DEFAULT_CKPT)
96
+
97
+ print(f"[startup] Model path: {model_path}")
98
+
99
+ # Initialize and load model
100
+ model = ChildFluencyNet(
101
+ wavlm_name = params['model'].get('wavlm_name', 'microsoft/wavlm-large'),
102
+ acoustic_dim = params['model']['acoustic_dim'],
103
+ lpe_dim = LPE_OUTPUT_DIM,
104
+ hidden_dim = params['model']['hidden_dim'],
105
+ dropout = params['model']['dropout']
106
+ ).to(device)
107
+
108
+ if Path(model_path).exists():
109
+ ckpt = torch.load(str(model_path), map_location=device)
110
+ model.load_state_dict(ckpt['model'])
111
+ epoch = ckpt.get('epoch', '?')
112
+ val_f1 = round(float(ckpt.get('val_f1', 0)), 4)
113
+ print(f"[startup] Loaded β†’ epoch {epoch}, val_f1={val_f1}")
114
+ else:
115
+ print(f"[startup] WARNING: checkpoint not found at {model_path}")
116
+
117
+ model.eval()
118
+ print("[startup] Model ready for inference.")
119
+
120
+
121
+ # ── Inference Logic (kept from FastAPI version) ────────────────────────────
122
+
123
+ def segment_audio(audio: np.ndarray):
124
+ window_samples = int(WINDOW_SEC * TARGET_SR)
125
+ stride_samples = int(STRIDE_SEC * TARGET_SR)
126
+ windows, start = [], 0
127
+ while start + window_samples <= len(audio):
128
+ seg = audio[start: start + window_samples]
129
+ if float(np.sqrt(np.mean(seg ** 2))) >= MIN_RMS:
130
+ windows.append((
131
+ start,
132
+ start + window_samples,
133
+ round(start / TARGET_SR, 3),
134
+ round((start + window_samples) / TARGET_SR, 3),
135
+ ))
136
+ start += stride_samples
137
+ return windows
138
+
139
+
140
+ def predict_windows(audio: np.ndarray, threshold: float = 0.5, batch_size: int = 16) -> List[dict]:
141
+ """Segment audio and run SLD inference."""
142
+ windows = segment_audio(audio)
143
+ if not windows:
144
+ return []
145
+
146
+ results = []
147
+ for b in range(0, len(windows), batch_size):
148
+ batch = windows[b: b + batch_size]
149
+ audio_t = torch.from_numpy(
150
+ np.stack([audio[s:e].astype(np.float32) for s, e, _, _ in batch])
151
+ ).to(device)
152
+
153
+ lpe_t = torch.full(
154
+ (len(batch), LPE_INPUT_FEATURES), 0.5, dtype=torch.float32
155
+ ).to(device)
156
+
157
+ with torch.no_grad():
158
+ if device == 'cuda':
159
+ with torch.cuda.amp.autocast():
160
+ out = model(audio_t, lpe_t)
161
+ else:
162
+ out = model(audio_t, lpe_t)
163
+
164
+ probs = torch.sigmoid(out['sld_logit']).cpu().numpy().flatten()
165
+
166
+ for i, (_, _, s_sec, e_sec) in enumerate(batch):
167
+ p = float(probs[i])
168
+ results.append({
169
+ 'window_idx': b + i,
170
+ 'start_sec' : s_sec,
171
+ 'end_sec' : e_sec,
172
+ 'sld_prob' : round(p, 4),
173
+ 'prediction': 'SLD' if p >= threshold else 'fluent',
174
+ })
175
+
176
+ return results
177
+
178
+
179
+ def compute_confidence(positive_rate: float, n_windows: int) -> str:
180
+ if n_windows < 5:
181
+ return 'low'
182
+ consensus = max(positive_rate, 1 - positive_rate)
183
+ if consensus >= 0.80:
184
+ return 'high'
185
+ elif consensus >= 0.60:
186
+ return 'medium'
187
+ return 'low'
188
+
189
+
190
+ # ── Gradio Main Function ───────────────────────────────────────────────────
191
+
192
+ def analyze_speech(audio_file, threshold: float = 0.5) -> Tuple[str, str]:
193
+ """
194
+ Main Gradio function: analyze audio and return results + optional analysis.
195
+
196
+ Args:
197
+ audio_file: Path to uploaded audio file (Gradio provides this)
198
+ threshold: SLD probability threshold (default 0.5)
199
+
200
+ Returns:
201
+ (results_json, analysis_text)
202
+ """
203
+ try:
204
+ t_start = time.time()
205
+
206
+ # Load audio
207
+ audio, _ = librosa.load(audio_file, sr=TARGET_SR, mono=True)
208
+ duration_sec = round(len(audio) / TARGET_SR, 2)
209
+
210
+ if duration_sec < WINDOW_SEC:
211
+ error_msg = f"Audio too short ({duration_sec}s). Need β‰₯{WINDOW_SEC}s."
212
+ return (
213
+ json.dumps({"error": error_msg, "status": "failed"}, indent=2),
214
+ f"❌ {error_msg}"
215
+ )
216
+
217
+ # Run inference
218
+ window_scores = predict_windows(audio, threshold=threshold)
219
+
220
+ if not window_scores:
221
+ error_msg = "No valid speech windows found."
222
+ return (
223
+ json.dumps({"error": error_msg, "status": "failed"}, indent=2),
224
+ f"❌ {error_msg}"
225
+ )
226
+
227
+ # Compute metrics
228
+ n_total = len(window_scores)
229
+ n_positive = sum(1 for w in window_scores if w['prediction'] == 'SLD')
230
+ pos_rate = round(n_positive / n_total, 4)
231
+
232
+ pos_probs = [w['sld_prob'] for w in window_scores if w['prediction'] == 'SLD']
233
+ sld_prob = round(
234
+ float(np.mean(pos_probs)) if pos_probs
235
+ else float(np.mean([w['sld_prob'] for w in window_scores])),
236
+ 4
237
+ )
238
+
239
+ elapsed = round(time.time() - t_start, 2)
240
+
241
+ # Build results dict
242
+ results = {
243
+ 'filename': Path(audio_file).name,
244
+ 'duration_sec': duration_sec,
245
+ 'sld_probability': sld_prob,
246
+ 'prediction': 'stuttering_detected' if n_positive > 0 else 'fluent',
247
+ 'confidence': compute_confidence(pos_rate, n_total),
248
+ 'n_windows_total': n_total,
249
+ 'n_windows_positive': n_positive,
250
+ 'positive_rate': pos_rate,
251
+ 'processing_time_sec': elapsed,
252
+ 'note': "LPE features set to neutral (0.5). Ablation study: Ξ”F1=+0.002 (negligible)."
253
+ }
254
+
255
+ results_json = json.dumps(results, indent=2)
256
+
257
+ # Generate text summary
258
+ severity = (
259
+ 'Severe' if pos_rate >= 0.50 else
260
+ 'Moderate' if pos_rate >= 0.25 else
261
+ 'Mild' if pos_rate >= 0.10 else
262
+ 'Minimal'
263
+ )
264
+
265
+ summary = f"""
266
+ βœ… ANALYSIS COMPLETE
267
+
268
+ πŸ“Š Results:
269
+ - File: {Path(audio_file).name}
270
+ - Duration: {duration_sec}s
271
+ - SLD Rate: {pos_rate * 100:.1f}%
272
+ - Severity: {severity}
273
+ - Confidence: {results['confidence'].upper()}
274
+ - Windows Analyzed: {n_total}
275
+ - Windows with SLD: {n_positive}
276
+
277
+ πŸ” Prediction: {'🚨 STUTTERING DETECTED' if results['prediction'] == 'stuttering_detected' else 'βœ… FLUENT (No significant stuttering)'}
278
+
279
+ ⏱️ Processing time: {elapsed}s
280
+
281
+ πŸ“ Note: This is a screening aid only. Consult a Speech-Language Pathologist for formal evaluation.
282
+ """
283
+
284
+ return (results_json, summary)
285
+
286
+ except Exception as e:
287
+ error_msg = f"Error: {str(e)}"
288
+ return (
289
+ json.dumps({"error": error_msg, "status": "failed"}, indent=2),
290
+ f"❌ {error_msg}"
291
+ )
292
+
293
+
294
+ # ── Optional: Generate Gemini Analysis ─────────────────────────────────────
295
+
296
+ def generate_analysis(
297
+ audio_file,
298
+ child_name: str = "Child",
299
+ child_age: str = "?",
300
+ threshold: float = 0.5
301
+ ) -> str:
302
+ """Generate a detailed Gemini-powered analysis of the results."""
303
+
304
+ try:
305
+ # First, run the prediction
306
+ audio, _ = librosa.load(audio_file, sr=TARGET_SR, mono=True)
307
+ duration_sec = round(len(audio) / TARGET_SR, 2)
308
+
309
+ window_scores = predict_windows(audio, threshold=threshold)
310
+ if not window_scores:
311
+ return "❌ No valid speech windows found."
312
+
313
+ n_total = len(window_scores)
314
+ n_positive = sum(1 for w in window_scores if w['prediction'] == 'SLD')
315
+ pos_rate = round(n_positive / n_total, 4)
316
+ sld_pct = round(pos_rate * 100, 1)
317
+
318
+ detected = n_positive > 0
319
+ severity = (
320
+ 'Severe' if pos_rate >= 0.50 else
321
+ 'Moderate' if pos_rate >= 0.25 else
322
+ 'Mild' if pos_rate >= 0.10 else
323
+ 'Minimal'
324
+ )
325
+
326
+ confidence = compute_confidence(pos_rate, n_total)
327
+
328
+ # Fallback text
329
+ fallback = (
330
+ f"OVERALL ASSESSMENT\n"
331
+ f"{child_name} ({child_age} years) showed {severity.lower()} stutter-like disfluencies "
332
+ f"in {sld_pct}% of the recorded speech "
333
+ f"({'stuttering detected' if detected else 'no significant stuttering detected'}).\n\n"
334
+ f"ACOUSTIC DETAILS\n"
335
+ f"Total windows analysed: {n_total}\n"
336
+ f"Windows with SLD: {n_positive}\n"
337
+ f"SLD rate: {sld_pct}%\n"
338
+ f"Confidence: {confidence}\n"
339
+ f"Recording duration: {duration_sec:.1f}s\n\n"
340
+ f"RECOMMENDATIONS\n"
341
+ f"1. {'Consult a Speech-Language Pathologist for a formal evaluation.' if detected else 'Continue monitoring speech fluency over time.'}\n"
342
+ f"2. Use slow, relaxed speech when speaking with the child.\n"
343
+ f"3. Create a low-pressure environment to reduce communication anxiety.\n\n"
344
+ f"IMPORTANT DISCLAIMER\n"
345
+ f"This is an automated screening aid and does not constitute a clinical diagnosis. "
346
+ f"A qualified Speech-Language Pathologist must evaluate the child formally."
347
+ )
348
+
349
+ prompt = f"""You are a Speech-Language Pathologist writing a screening summary for a parent.
350
+
351
+ YOUR INSTRUCTIONS:
352
+ 1. DO NOT recalculate any scores.
353
+ 2. DO NOT change the classification.
354
+ 3. Your ONLY job is to format the provided data into a warm, empathetic, easy-to-read report.
355
+
356
+ CHILD INFORMATION:
357
+ - Name: {child_name}
358
+ - Age: {child_age} years
359
+
360
+ ACOUSTIC SCREENING RESULTS (Do not alter):
361
+ - SLD (Stutter-Like Disfluency) Rate: {sld_pct}%
362
+ - Severity Category: {severity}
363
+ - System Classification: {'STUTTERING DETECTED' if detected else 'FLUENT (No significant stuttering)'}
364
+ - Confidence: {confidence}
365
+ - Recording Duration: {duration_sec:.1f} seconds
366
+ - Windows Analysed: {n_total}, Windows with SLD: {n_positive}
367
+
368
+ Please write a comprehensive, plain-text summary (no markdown, no **, no ##, no special characters).
369
+
370
+ Structure your response with these exact section headings on their own lines:
371
+
372
+ OVERALL ASSESSMENT
373
+ 2-3 sentences summarising the result in plain, reassuring language.
374
+
375
+ WHAT THIS MEANS
376
+ Explain what stutter-like disfluencies are and how this result relates to typical speech development for this age.
377
+
378
+ RECOMMENDATIONS
379
+ Simple numbered points (1., 2., 3.) β€” at least three actionable suggestions for parents.
380
+
381
+ IMPORTANT DISCLAIMER
382
+ State clearly this is a screening tool, not a clinical diagnosis, and that a Speech-Language Pathologist evaluation is required.
383
+
384
+ Keep the tone warm, empathetic, and parent-friendly. Plain text only."""
385
+
386
+ # Tier 1: Gemini
387
+ if gemini_model is not None:
388
+ try:
389
+ response = gemini_model.generate_content(prompt)
390
+ return response.text
391
+ except Exception as e:
392
+ print(f"[generate-analysis] Gemini error: {e}. Falling back to Groq.")
393
+
394
+ # Tier 2: Groq
395
+ if GROK_API_KEY:
396
+ try:
397
+ from groq import Groq
398
+ groq_client = Groq(api_key=GROK_API_KEY)
399
+ groq_response = groq_client.chat.completions.create(
400
+ model="llama-3.3-70b-versatile",
401
+ messages=[{"role": "user", "content": prompt}],
402
+ temperature=0.65,
403
+ max_tokens=1024,
404
+ )
405
+ return groq_response.choices[0].message.content
406
+ except Exception as e:
407
+ print(f"[generate-analysis] Groq error: {e}. Using fallback.")
408
+
409
+ # Tier 3: Fallback
410
+ return fallback
411
+
412
+ except Exception as e:
413
+ return f"❌ Error generating analysis: {str(e)}"
414
+
415
+
416
+ # ── Gradio Interface ───────────────────────────────────────────────────────
417
+
418
+ with gr.Blocks(title="Developmental Stuttering Detection") as demo:
419
+ gr.Markdown("# 🎀 Developmental Stuttering Detection")
420
+ gr.Markdown("Upload a recording of a child speaking to screen for signs of stutter-like disfluencies.")
421
+
422
+ with gr.Tabs():
423
+ # TAB 1: Prediction
424
+ with gr.Tab("Analysis"):
425
+ with gr.Row():
426
+ audio_input = gr.Audio(
427
+ type="filepath",
428
+ label="Upload Audio (WAV recommended)",
429
+ info="Minimum 4 seconds"
430
+ )
431
+ threshold_slider = gr.Slider(
432
+ minimum=0.1,
433
+ maximum=0.9,
434
+ value=0.5,
435
+ step=0.05,
436
+ label="SLD Threshold",
437
+ info="Lower = more sensitive, Higher = more conservative"
438
+ )
439
+
440
+ analyze_btn = gr.Button("πŸ” Analyze", variant="primary")
441
+
442
+ with gr.Row():
443
+ results_output = gr.JSON(label="Raw Results")
444
+ summary_output = gr.Textbox(label="Summary", lines=10)
445
+
446
+ analyze_btn.click(
447
+ fn=analyze_speech,
448
+ inputs=[audio_input, threshold_slider],
449
+ outputs=[results_output, summary_output]
450
+ )
451
+
452
+ # TAB 2: Detailed Analysis (Gemini-powered)
453
+ with gr.Tab("Detailed Report"):
454
+ gr.Markdown("Generate a detailed clinical-style report powered by AI.")
455
+
456
+ audio_input_2 = gr.Audio(type="filepath", label="Upload Audio")
457
+ child_name_input = gr.Textbox(value="Child", label="Child's Name")
458
+ child_age_input = gr.Textbox(value="?", label="Child's Age")
459
+ threshold_slider_2 = gr.Slider(
460
+ minimum=0.1, maximum=0.9, value=0.5, step=0.05, label="SLD Threshold"
461
+ )
462
+
463
+ generate_btn = gr.Button("πŸ“„ Generate Report", variant="primary")
464
+ report_output = gr.Textbox(label="Generated Report", lines=15)
465
+
466
+ generate_btn.click(
467
+ fn=generate_analysis,
468
+ inputs=[audio_input_2, child_name_input, child_age_input, threshold_slider_2],
469
+ outputs=report_output
470
+ )
471
+
472
+ gr.Markdown("""
473
+ ---
474
+ ⚠️ **IMPORTANT DISCLAIMER**
475
+
476
+ This tool is a **screening aid only** and does not constitute a clinical diagnosis.
477
+ All results must be reviewed by a qualified Speech-Language Pathologist.
478
+
479
+ For more information, consult a healthcare professional.
480
+ """)
481
+
482
+
483
+ # ── Launch ─────────────────────────────────────────────────────────────────
484
+
485
+ if __name__ == "__main__":
486
+ demo.launch(share=False)