Javedalam commited on
Commit
ecd22c7
Β·
verified Β·
1 Parent(s): b027404

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +250 -0
app.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import uuid
4
+ import threading
5
+
6
+ import gradio as gr
7
+ import numpy as np
8
+ import torch
9
+ import soundfile as sf
10
+
11
+ from transformers import (
12
+ pipeline,
13
+ AutoTokenizer,
14
+ AutoModelForCausalLM,
15
+ AutoProcessor,
16
+ VitsModel,
17
+ )
18
+
19
+ # ----------------------------
20
+ # Config (CPU-friendly defaults)
21
+ # ----------------------------
22
+ ASR_ID = os.environ.get("ASR_ID", "openai/whisper-tiny") # fastest on CPU
23
+ LLM_ID = os.environ.get("LLM_ID", "HuggingFaceTB/SmolLM2-135M-Instruct")
24
+ TTS_ID = os.environ.get("TTS_ID", "facebook/mms-tts-eng")
25
+
26
+ MAX_NEW_TOKENS = int(os.environ.get("MAX_NEW_TOKENS", "120")) # keep short for latency
27
+ MIN_NEW_TOKENS = int(os.environ.get("MIN_NEW_TOKENS", "20"))
28
+
29
+ OUT_DIR = "outputs"
30
+ os.makedirs(OUT_DIR, exist_ok=True)
31
+
32
+ # ----------------------------
33
+ # Global singletons (loaded once)
34
+ # ----------------------------
35
+ _load_lock = threading.Lock()
36
+ _asr = None
37
+ _llm_tok = None
38
+ _llm = None
39
+ _tts_tok = None
40
+ _tts = None
41
+ _tts_sr = None
42
+
43
+
44
+ def _now_ms() -> float:
45
+ return time.perf_counter() * 1000.0
46
+
47
+
48
+ def load_models():
49
+ """Load all models once per Space container."""
50
+ global _asr, _llm_tok, _llm, _tts_tok, _tts, _tts_sr
51
+
52
+ if _asr is not None and _llm is not None and _tts is not None:
53
+ return
54
+
55
+ with _load_lock:
56
+ if _asr is None:
57
+ # CPU-only (Spaces free tier)
58
+ _asr = pipeline(
59
+ "automatic-speech-recognition",
60
+ model=ASR_ID,
61
+ device=-1,
62
+ )
63
+
64
+ if _llm is None or _llm_tok is None:
65
+ _llm_tok = AutoTokenizer.from_pretrained(LLM_ID)
66
+ _llm = AutoModelForCausalLM.from_pretrained(
67
+ LLM_ID,
68
+ torch_dtype=torch.float32,
69
+ low_cpu_mem_usage=True,
70
+ )
71
+ _llm.eval()
72
+
73
+ if _tts is None or _tts_tok is None:
74
+ _tts_tok = AutoTokenizer.from_pretrained(TTS_ID)
75
+ _tts = VitsModel.from_pretrained(
76
+ TTS_ID,
77
+ torch_dtype=torch.float32,
78
+ low_cpu_mem_usage=True,
79
+ )
80
+ _tts.eval()
81
+ _tts_sr = int(_tts.config.sampling_rate)
82
+
83
+
84
+ def _clean_asr_text(s: str) -> str:
85
+ s = (s or "").strip()
86
+ if s.lower().startswith("question,"):
87
+ s = s[len("question,"):].strip()
88
+ return s
89
+
90
+
91
+ def _llm_answer_from_text(user_text: str) -> str:
92
+ """Very small, reliable prompt wrapper for tiny instruct models."""
93
+ user_text = _clean_asr_text(user_text)
94
+ if not user_text:
95
+ return "I didn't catch that. Please repeat your question."
96
+
97
+ # Use chat template if available (best), else minimal wrapper
98
+ if hasattr(_llm_tok, "apply_chat_template"):
99
+ messages = [{"role": "user", "content": user_text}]
100
+ prompt = _llm_tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
101
+ else:
102
+ prompt = f"User: {user_text}\nAssistant:"
103
+
104
+ inputs = _llm_tok(prompt, return_tensors="pt")
105
+
106
+ with torch.no_grad():
107
+ gen = _llm.generate(
108
+ **inputs,
109
+ max_new_tokens=MAX_NEW_TOKENS,
110
+ min_new_tokens=MIN_NEW_TOKENS,
111
+ do_sample=False,
112
+ eos_token_id=_llm_tok.eos_token_id,
113
+ pad_token_id=_llm_tok.eos_token_id,
114
+ )
115
+
116
+ full = _llm_tok.decode(gen[0], skip_special_tokens=True)
117
+
118
+ # Try to extract assistant portion
119
+ if "Assistant:" in full:
120
+ ans = full.split("Assistant:", 1)[-1].strip()
121
+ else:
122
+ ans = full.strip()
123
+ # If it echoed the prompt, strip the prompt prefix crudely
124
+ if ans.startswith(prompt):
125
+ ans = ans[len(prompt):].strip()
126
+
127
+ return ans if ans else "I produced no answer. Please try again."
128
+
129
+
130
+ def _tts_speak(text: str, out_wav_path: str) -> str:
131
+ text = (text or "").strip()
132
+ if not text:
133
+ text = "I have no text to speak."
134
+
135
+ inputs = _tts_tok(text, return_tensors="pt")
136
+
137
+ with torch.no_grad():
138
+ wav = _tts(**inputs).waveform
139
+
140
+ wav = wav.squeeze().detach().cpu().numpy().astype(np.float32)
141
+ sf.write(out_wav_path, wav, _tts_sr)
142
+ return out_wav_path
143
+
144
+
145
+ def voice_qa(audio_path: str):
146
+ """
147
+ Gradio passes a filepath for Audio(type="filepath").
148
+ Return:
149
+ transcript, answer, tts_audio_path, debug_text, transcript_file, answer_file
150
+ """
151
+ load_models()
152
+
153
+ run_id = time.strftime("%Y%m%d-%H%M%S") + "_" + str(uuid.uuid4())[:8]
154
+ run_dir = os.path.join(OUT_DIR, run_id)
155
+ os.makedirs(run_dir, exist_ok=True)
156
+
157
+ transcript_file = os.path.join(run_dir, "transcript.txt")
158
+ answer_file = os.path.join(run_dir, "answer.txt")
159
+ tts_file = os.path.join(run_dir, "tts_answer.wav")
160
+
161
+ dbg_lines = []
162
+ t0 = _now_ms()
163
+
164
+ # --- ASR ---
165
+ t_asr0 = _now_ms()
166
+ # return_timestamps=True avoids Whisper long-form errors for >30s files
167
+ asr_out = _asr(audio_path, return_timestamps=True)
168
+ transcript = _clean_asr_text(asr_out.get("text", ""))
169
+ t_asr1 = _now_ms()
170
+
171
+ with open(transcript_file, "w", encoding="utf-8") as f:
172
+ f.write(transcript)
173
+
174
+ dbg_lines.append(f"[ASR] model={ASR_ID}")
175
+ dbg_lines.append(f"[ASR] ms={(t_asr1 - t_asr0):.1f}")
176
+ dbg_lines.append(f"[ASR] chars={len(transcript)}")
177
+
178
+ # --- LLM ---
179
+ t_llm0 = _now_ms()
180
+ answer = _llm_answer_from_text(transcript)
181
+ t_llm1 = _now_ms()
182
+
183
+ with open(answer_file, "w", encoding="utf-8") as f:
184
+ f.write(answer)
185
+
186
+ dbg_lines.append(f"[LLM] model={LLM_ID}")
187
+ dbg_lines.append(f"[LLM] ms={(t_llm1 - t_llm0):.1f}")
188
+ dbg_lines.append(f"[LLM] chars={len(answer)}")
189
+
190
+ # --- TTS ---
191
+ t_tts0 = _now_ms()
192
+ _tts_speak(answer, tts_file)
193
+ t_tts1 = _now_ms()
194
+
195
+ dbg_lines.append(f"[TTS] model={TTS_ID}")
196
+ dbg_lines.append(f"[TTS] ms={(t_tts1 - t_tts0):.1f}")
197
+ dbg_lines.append(f"[TTS] out={tts_file}")
198
+
199
+ t1 = _now_ms()
200
+ dbg_lines.append(f"[TOTAL] ms={(t1 - t0):.1f}")
201
+ debug_text = "\n".join(dbg_lines)
202
+
203
+ return transcript, answer, tts_file, debug_text, transcript_file, answer_file
204
+
205
+
206
+ # ----------------------------
207
+ # Gradio UI
208
+ # ----------------------------
209
+ with gr.Blocks(title="Voice Q&A (ASR β†’ LLM β†’ TTS)") as demo:
210
+ gr.Markdown(
211
+ "# Voice Q&A (ASR β†’ LLM β†’ TTS)\n"
212
+ "Speak a question β†’ it transcribes β†’ answers β†’ speaks back.\n\n"
213
+ "**CPU-friendly defaults**: Whisper *tiny* + SmolLM2-135M + MMS TTS.\n"
214
+ )
215
+
216
+ with gr.Row():
217
+ audio_in = gr.Audio(
218
+ sources=["microphone"],
219
+ type="filepath",
220
+ label="Microphone input",
221
+ )
222
+
223
+ run_btn = gr.Button("Run (ASR β†’ LLM β†’ TTS)", variant="primary")
224
+
225
+ with gr.Row():
226
+ transcript_out = gr.Textbox(label="Transcript (ASR)", lines=4)
227
+ answer_out = gr.Textbox(label="Answer (LLM)", lines=6)
228
+
229
+ tts_out = gr.Audio(label="Spoken answer (TTS)", type="filepath")
230
+
231
+ debug_out = gr.Textbox(label="Debug / timings", lines=10)
232
+
233
+ with gr.Row():
234
+ transcript_dl = gr.File(label="Download transcript.txt")
235
+ answer_dl = gr.File(label="Download answer.txt")
236
+
237
+ run_btn.click(
238
+ fn=voice_qa,
239
+ inputs=[audio_in],
240
+ outputs=[transcript_out, answer_out, tts_out, debug_out, transcript_dl, answer_dl],
241
+ )
242
+
243
+ gr.Markdown(
244
+ "### Notes\n"
245
+ "- If latency is still high on free CPU, try even shorter questions (2–5 seconds).\n"
246
+ "- You can switch ASR model by setting Space variables: `ASR_ID=openai/whisper-base` (better) or keep `whisper-tiny` (faster).\n"
247
+ )
248
+
249
+ if __name__ == "__main__":
250
+ demo.launch()