ParulPandey commited on
Commit
254209b
Β·
verified Β·
1 Parent(s): 8be64e3

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +385 -0
app.py ADDED
@@ -0,0 +1,385 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import difflib
4
+ from gradio_client import Client, file as gradio_file # Renamed to avoid conflict
5
+ import time
6
+ import google.generativeai as genai
7
+
8
+ # --- Configuration & Clients ---
9
+
10
+ def configure_gemini_api():
11
+ """Configures the Google Gemini API with API key from Secrets or environment."""
12
+ api_key = None
13
+ try:
14
+ api_key = gr.Secrets.get("GOOGLE_API_KEY") # For Hugging Face Spaces
15
+ except AttributeError: # Running locally, gr.Secrets not available
16
+ api_key = os.environ.get("GOOGLE_API_KEY")
17
+ except FileNotFoundError: # gr.Secrets.get can raise this if no secrets file found
18
+ api_key = os.environ.get("GOOGLE_API_KEY")
19
+
20
+ if api_key:
21
+ try:
22
+ genai.configure(api_key=api_key)
23
+ print("Google Gemini API configured successfully.")
24
+ return True
25
+ except Exception as e:
26
+ print(f"Error configuring Gemini API: {e}")
27
+ return False
28
+ else:
29
+ print("WARN: GOOGLE_API_KEY not found in Gradio Secrets or environment. Story generation with Gemini will be disabled.")
30
+ return False
31
+
32
+ GEMINI_API_CONFIGURED = configure_gemini_api()
33
+
34
+ # Initialize TTS Client (Using ESPnet VITS as an alternative to Bark)
35
+ try:
36
+ tts_client = Client("espnet/kan-bayashi_ljspeech_vits")
37
+ print("ESPnet VITS TTS client initialized successfully.")
38
+ # --- IMPORTANT: For Debugging VITS API if issues persist ---
39
+ # print("--- ESPnet VITS TTS API Details (Uncomment to view) ---")
40
+ # print(tts_client.view_api(all_endpoints=True))
41
+ # print("----------------------------------------------------")
42
+ # For a more structured dictionary output:
43
+ # api_info_tts = tts_client.view_api(return_format="dict")
44
+ # import json
45
+ # print(json.dumps(api_info_tts, indent=2))
46
+ # --- End Debugging Section ---
47
+ except Exception as e:
48
+ print(f"Fatal: Could not initialize ESPnet VITS TTS client: {e}. TTS will not work.")
49
+ tts_client = None
50
+
51
+ # Initialize STT Client for Whisper (abidlabs/whisper-large-v2)
52
+ try:
53
+ whisper_stt_client = Client("abidlabs/whisper-large-v2")
54
+ print("Whisper STT client initialized successfully.")
55
+ # --- For Debugging Whisper API ---
56
+ # print("--- Whisper STT API Details (Uncomment to view) ---")
57
+ # print(whisper_stt_client.view_api(all_endpoints=True))
58
+ # print("-------------------------------------------------")
59
+ except Exception as e:
60
+ print(f"Fatal: Could not initialize Whisper STT client: {e}. STT will not work.")
61
+ whisper_stt_client = None
62
+
63
+ # --- Helper Functions ---
64
+
65
+ def generate_story_with_gemini(name, grade, topic):
66
+ if not GEMINI_API_CONFIGURED:
67
+ return "Google Gemini API key not configured. Story generation is disabled. πŸ”‘"
68
+ try:
69
+ model = genai.GenerativeModel(model_name="gemini-1.5-flash-latest") # Fast and capable
70
+ prompt = (
71
+ f"You are a super friendly and imaginative storyteller for kids. "
72
+ f"Please write an exciting and fun short story (around 100-120 words) for a student named {name} who is in Grade {grade}. "
73
+ f"The story must be about '{topic}'. "
74
+ f"Use simple words and sentences that a Grade {grade} student can easily read aloud and understand. "
75
+ f"Make the story engaging and positive. Jump right into the story without any introduction like 'Here is a story for you'."
76
+ )
77
+ safety_settings = [
78
+ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
79
+ {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
80
+ {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
81
+ {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"},
82
+ ]
83
+ generation_config = genai.types.GenerationConfig(
84
+ candidate_count=1, max_output_tokens=300, temperature=0.75
85
+ )
86
+ response = model.generate_content(
87
+ prompt, generation_config=generation_config, safety_settings=safety_settings
88
+ )
89
+ if response.candidates and response.candidates[0].content.parts:
90
+ story = response.text
91
+ if response.prompt_feedback and response.prompt_feedback.block_reason:
92
+ return f"Oh dear! My story idea for '{topic}' was a bit too wild and got blocked (Reason: {response.prompt_feedback.block_reason}). Let's try a different topic! 😊"
93
+ if not story.strip():
94
+ return f"Hmm, Gemini gave me a blank page for '{topic}'. Let's try a different topic or try again! ✨"
95
+ return story.strip()
96
+ else:
97
+ if response.prompt_feedback and response.prompt_feedback.block_reason:
98
+ return f"Oh dear! My story idea for '{topic}' was a bit too wild and got blocked (Reason: {response.prompt_feedback.block_reason}). Let's try a different topic! 😊"
99
+ print(f"Gemini API response issue: {response}")
100
+ return f"Hmm, Gemini's story magic seems to be on a little break for '{topic}'. Maybe try another topic? πŸ€”"
101
+ except Exception as e:
102
+ print(f"Error generating story with Gemini: {e}")
103
+ if "API_KEY_INVALID" in str(e).lower() or "api key not valid" in str(e).lower():
104
+ return "Oops! The Google Gemini API key seems to be having a problem. Please tell the grown-ups to check it! πŸ”‘"
105
+ return f"Oh no! 😟 I had a little trouble dreaming up a story with Gemini. Error: {e}"
106
+
107
+ def text_to_speech_vits(text_to_speak):
108
+ if not tts_client:
109
+ return "The VITS sound machine isn't working right now. πŸ› οΈ Please tell the grown-ups!"
110
+ try:
111
+ # Parameters for espnet/kan-bayashi_ljspeech_vits.
112
+ # YOU MUST VERIFY these with tts_client.view_api() if TTS fails.
113
+ # The fn_index (or api_name) and the order/names of parameters are critical.
114
+ job = tts_client.submit(
115
+ text_to_speak, # text (str)
116
+ "EN", # lang (str) - e.g., "EN" for English in this model
117
+ 0, # speaker_id (int | float) - usually 0 for LJSpeech default
118
+ 0.667, # noise_scale (float) - variance of Z
119
+ 0.8, # noise_scale_w (float) - variance of Z in duration
120
+ 1.0, # length_scale (float) - controls speed
121
+ fn_index=0 # ASSUMPTION: TTS is the first function (index 0).
122
+ # If view_api() shows a different fn_index or an api_name like "/predict", use that.
123
+ )
124
+ # VITS is generally faster than Bark, but network can add delays
125
+ audio_filepath = job.result(timeout=90)
126
+
127
+ # This space typically returns just the audio filepath directly.
128
+ if isinstance(audio_filepath, str) and audio_filepath.endswith(('.wav', '.mp3', '.flac')):
129
+ return audio_filepath
130
+ else:
131
+ # Sometimes the result might be a tuple, e.g., (filepath, samplerate)
132
+ # Check the actual output structure from view_api() or by printing audio_filepath
133
+ print(f"Unexpected VITS TTS result format: {audio_filepath}")
134
+ if isinstance(audio_filepath, tuple) and len(audio_filepath) > 0 and isinstance(audio_filepath[0], str):
135
+ return audio_filepath[0] # Assume audio path is the first element if it's a tuple
136
+ return "Hmm, the sound from VITS came out a bit funny. πŸ€”"
137
+ except Exception as e:
138
+ print(f"Error with VITS TTS (espnet/kan-bayashi_ljspeech_vits): {e}")
139
+ if "Queue full" in str(e).lower() or "too much pending traffic" in str(e).lower():
140
+ return "The VITS sound machine is busy! Please try again in a moment. πŸ•’"
141
+ # Provide more specific error if submit call itself failed due to wrong params
142
+ if "expected" in str(e).lower() and ("argument" in str(e).lower() or "parameter" in str(e).lower()):
143
+ return f"VITS TTS had a hiccup with parameters. (Details: {e}). Please check view_api() output."
144
+ return f"Oh dear, VITS couldn't make the sound. πŸ”‡ Error: {e}"
145
+
146
+
147
+ def speech_to_text_whisper_space(audio_filepath):
148
+ if not whisper_stt_client:
149
+ return "The Whisper listening ears aren't working right now. πŸ› οΈ Please tell the grown-ups!"
150
+ if not audio_filepath:
151
+ return "Oops! I didn't get any recording to listen to. 🎀"
152
+ try:
153
+ # API for abidlabs/whisper-large-v2 usually takes audio, task, language.
154
+ job = whisper_stt_client.submit(
155
+ gradio_file(audio_filepath), # Use gradio_client.file to handle the upload
156
+ "transcribe", # task
157
+ "English", # language (can be None for auto-detect)
158
+ api_name="/predict" # This is common for abidlabs/whisper spaces
159
+ )
160
+ result_dict = job.result(timeout=120) # Wait up to 2 minutes
161
+
162
+ if isinstance(result_dict, dict) and 'text' in result_dict:
163
+ return result_dict['text']
164
+ elif isinstance(result_dict, str): # Fallback if it's simpler and returns text directly
165
+ return result_dict
166
+ else:
167
+ print(f"Unexpected Whisper STT result format: {result_dict}")
168
+ return "Hmm, I couldn't quite understand the words from Whisper. πŸ€”"
169
+ except Exception as e:
170
+ print(f"Error transcribing audio with Whisper Space: {e}")
171
+ if "Queue full" in str(e).lower() or "too much pending traffic" in str(e).lower():
172
+ return "The Whisper listening ears are super busy! 인기폭발! ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ! Please try again in a bit. πŸ•’"
173
+ return f"Oh no! Whisper had trouble hearing that. πŸ™‰ Error: {e}"
174
+
175
+ def clean_text_for_comparison(text):
176
+ if not isinstance(text, str): return []
177
+ text = text.lower()
178
+ punctuation_to_remove = "!\"#$%&()*+,-./:;<=>?@[\\]^_`{|}~" # Keeps apostrophes for contractions
179
+ text = text.translate(str.maketrans('', '', punctuation_to_remove))
180
+ return text.split()
181
+
182
+ def compare_texts_for_feedback(original_text, student_text):
183
+ original_words = clean_text_for_comparison(original_text)
184
+ student_words = clean_text_for_comparison(student_text)
185
+
186
+ if not student_words:
187
+ return "It sounds like you didn't record anything, or maybe it was super quiet! 🀫 Try recording again nice and clear!", ""
188
+
189
+ matcher = difflib.SequenceMatcher(None, original_words, student_words, autojunk=False)
190
+ feedback_lines = []
191
+ highlighted_passage_parts = []
192
+
193
+ for tag, i1, i2, j1, j2 in matcher.get_opcodes():
194
+ original_segment = original_words[i1:i2]
195
+ student_segment = student_words[j1:j2]
196
+
197
+ if tag == 'equal':
198
+ highlighted_passage_parts.append(" ".join(original_segment))
199
+ elif tag == 'replace':
200
+ # Try to highlight word by word if segments are same length for better visual
201
+ if len(original_segment) == len(student_segment):
202
+ for i in range(len(original_segment)):
203
+ o_word = original_segment[i]
204
+ s_word = student_segment[i]
205
+ feedback_lines.append(f"- You said: \"*{s_word}*\" instead of: \"**{o_word}**\"")
206
+ highlighted_passage_parts.append(f"~~{o_word}~~ **{s_word}**")
207
+ else: # General replacement if segment lengths differ
208
+ feedback_lines.append(f"- Instead of: \"**{' '.join(original_segment)}**\", you said: \"*{' '.join(student_segment)}*\"")
209
+ highlighted_passage_parts.append(f"~~{' '.join(original_segment)}~~ **{' '.join(student_segment)}**")
210
+ elif tag == 'delete': # Student skipped words from original
211
+ feedback_lines.append(f"- You missed: \"**{' '.join(original_segment)}**\"")
212
+ highlighted_passage_parts.append(f"~~{' '.join(original_segment)}~~ (*skipped*)")
213
+ elif tag == 'insert': # Student added words not in original
214
+ feedback_lines.append(f"- You added: \"*{' '.join(student_segment)}*\" (which wasn't in the story)")
215
+ highlighted_passage_parts.append(f"(*added:* **{' '.join(student_segment)}**)")
216
+
217
+ final_highlighted_text = " ".join(highlighted_passage_parts)
218
+
219
+ if not feedback_lines:
220
+ return "πŸŽ‰πŸ₯³ WOOHOO! Amazing reading! You got all the words spot on! πŸ₯³πŸŽ‰", final_highlighted_text
221
+ else:
222
+ feedback_summary = "Great try! Here are a few words to practice to make it even better:\n" + "\n".join(feedback_lines)
223
+ return feedback_summary, final_highlighted_text
224
+
225
+ # --- Gradio UI Functions ---
226
+ def generate_story_and_audio_for_ui(name, grade, topic, progress=gr.Progress(track_tqdm=True)):
227
+ if not name or not grade or not topic:
228
+ return "Oops! Please tell me your name, grade, and a fun topic first! 😊", None, gr.update(visible=False), ""
229
+
230
+ progress(0.1, desc="πŸ“– Asking Gemini to dream up a cool story for you...")
231
+ story_text = generate_story_with_gemini(name, grade, topic)
232
+ gemini_error_keywords = ["Gemini API key not configured", "Oh no!", "Oops!", "Hmm,"]
233
+ if any(keyword in story_text for keyword in gemini_error_keywords) or not story_text.strip() :
234
+ return story_text, None, gr.update(visible=False), story_text # Keep recording area hidden
235
+
236
+ progress(0.5, desc="🎧 Warming up the VITS sound machine... (this should be quicker!)")
237
+ tts_audio_path = text_to_speech_vits(story_text) # Use VITS TTS
238
+ error_conditions_tts = [
239
+ "couldn't make the sound", "sound came out a bit funny", "sound machine isn't working",
240
+ "sound machine is busy", "VITS had a hiccup" # Check for VITS specific errors
241
+ ]
242
+ if any(err in (tts_audio_path or "") for err in error_conditions_tts):
243
+ return story_text, tts_audio_path, gr.update(visible=False), story_text # Keep recording hidden
244
+
245
+ progress(1.0, desc="βœ… Story and sound are ready! Let's go!")
246
+ return (
247
+ story_text,
248
+ tts_audio_path,
249
+ gr.update(visible=True), # Show recording_assessment_area
250
+ story_text # Pass story_text to gr.State
251
+ )
252
+
253
+ def assess_student_reading_ui(original_passage_state, student_audio_path, progress=gr.Progress(track_tqdm=True)):
254
+ if not student_audio_path:
255
+ return "🎀 Whoops! Did you forget to record your awesome reading? Try again!", ""
256
+ if not original_passage_state: # Should not happen if UI flow is correct
257
+ return "Hmm, I lost the story! 😟 Please generate a new story first.", ""
258
+
259
+ progress(0.2, desc="πŸ‘‚ Whisper is listening carefully to your recording...")
260
+ transcribed_text = speech_to_text_whisper_space(student_audio_path)
261
+ error_conditions_stt = [
262
+ "couldn't understand the words", "had trouble hearing that", "listening ears aren't working",
263
+ "listening ears are super busy", "didn't get any recording"
264
+ ]
265
+ if any(err in (transcribed_text or "") for err in error_conditions_stt):
266
+ return transcribed_text, "" # Show STT error
267
+
268
+ progress(0.7, desc="🧠 Thinking about the words...")
269
+ feedback, highlighted_passage = compare_texts_for_feedback(original_passage_state, transcribed_text)
270
+ progress(1.0, desc="⭐ Feedback is ready!")
271
+ return feedback, highlighted_passage
272
+
273
+ # --- Gradio Interface ---
274
+ css = """
275
+ body { font-family: 'Comic Sans MS', 'Chalkboard SE', 'Comic Neue', cursive; background-color: #F0F8FF; } /* AliceBlue background */
276
+ .gr-button {
277
+ background-color: #FF69B4 !important; /* HotPink */
278
+ color: white !important;
279
+ border-radius: 20px !important;
280
+ font-weight: bold !important;
281
+ border: 2px solid #FF1493 !important; /* DeepPink border */
282
+ box-shadow: 0px 3px 5px rgba(0,0,0,0.2) !important;
283
+ }
284
+ .gr-button:hover { background-color: #FF1493 !important; } /* DeepPink on hover */
285
+ .gr-panel {
286
+ border-radius: 15px !important;
287
+ box-shadow: 5px 5px 15px rgba(0,0,0,0.1) !important;
288
+ background-color: #FFFACD !important; /* LemonChiffon panel background */
289
+ border: 2px dashed #FFD700 !important; /* Gold dashed border */
290
+ }
291
+ label, .gr-checkbox-label { color: #4B0082 !important; font-weight: bold !important; } /* Indigo */
292
+ .gr-textbox, .gr-dropdown { border-radius: 10px !important; border: 1px solid #DDA0DD !important; } /* Plum border for inputs */
293
+ #student_audio_input audio { background-color: #E6E6FA; border-radius: 10px; } /* Lavender for audio player */
294
+ #feedback_output, #highlighted_passage_output {
295
+ background-color: #FFFFE0; /* LightYellow */
296
+ padding: 15px;
297
+ border-radius: 10px;
298
+ border: 1px solid #FAFAD2; /* LightGoldenrodYellow */
299
+ }
300
+ """
301
+
302
+ # Using a theme that allows CSS to take more precedence
303
+ with gr.Blocks(theme=gr.themes.Base(), css=css) as app: # theme=gr.themes.Soft() or gr.themes.Base()
304
+ gr.Markdown(
305
+ """
306
+ <div style="text-align: center; padding: 20px 0;">
307
+ <h1 style="color: #FF6347; font-size: 3em; text-shadow: 2px 2px #D3D3D3;">πŸŒˆπŸ¦„βœ¨ AI Reading Buddy βœ¨πŸ¦„πŸŒˆ</h1>
308
+ <p style="font-size: 1.3em; color: #483D8B;">Let's read a super fun story from Gemini and practice our words!</p>
309
+ </div>
310
+ """
311
+ )
312
+
313
+ original_passage_state = gr.State("") # To store the generated story
314
+
315
+ with gr.Row():
316
+ with gr.Column(scale=1):
317
+ gr.Markdown("### <span style='color:#DB7093;'>✏️ Tell Me About You!</span>")
318
+ student_name_input = gr.Textbox(label="πŸ‘‘ Your Awesome Name:", placeholder="E.g., Princess Lily")
319
+ student_grade_input = gr.Dropdown(
320
+ label="πŸ§‘β€πŸŽ“ Your Grade:",
321
+ choices=[f"{i}" for i in range(1, 11)], # Grades 1 to 10
322
+ value="3" # Default value
323
+ )
324
+ topic_input = gr.Textbox(label="πŸš€ Story Topic Idea:", placeholder="E.g., brave little astronaut")
325
+ generate_button = gr.Button(value="🎈 Get My Gemini Story!")
326
+
327
+ with gr.Column(scale=2):
328
+ gr.Markdown("### <span style='color:#DB7093;'>πŸ“– Your Special Story (from Gemini AI):</span>")
329
+ passage_output = gr.Textbox(label="Read this aloud:", lines=10, interactive=False)
330
+ gr.Markdown("### <span style='color:#DB7093;'>πŸ”Š Listen to the Story:</span>")
331
+ audio_output = gr.Audio(label="Hear how it sounds (with VITS TTS)", type="filepath") # Label updated for VITS
332
+
333
+ gr.Markdown("<hr style='border:1px dashed #FFB6C1;'>") # LightPink dashed separator
334
+
335
+ with gr.Row(visible=False) as recording_assessment_area: # Initially hidden
336
+ with gr.Column(scale=1):
337
+ gr.Markdown("### <span style='color:#32CD32;'>🀩 Your Turn to Shine! 🀩</span>")
338
+ student_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="🎀 Record yourself reading the story! Press the mic, then stop.", elem_id="student_audio_input")
339
+ assess_button = gr.Button(value="🧐 Check My Reading!", elem_id="assess_button")
340
+
341
+ with gr.Column(scale=2):
342
+ gr.Markdown("### <span style='color:#32CD32;'>πŸ’‘ Word Detective Feedback:</span>")
343
+ feedback_output = gr.Markdown(value="Your amazing feedback will pop up here! ✨", elem_id="feedback_output")
344
+ highlighted_passage_output = gr.Markdown(value="See your reading journey here! πŸ—ΊοΈ", elem_id="highlighted_passage_output")
345
+
346
+
347
+ generate_button.click(
348
+ fn=generate_story_and_audio_for_ui,
349
+ inputs=[student_name_input, student_grade_input, topic_input],
350
+ outputs=[
351
+ passage_output,
352
+ audio_output,
353
+ recording_assessment_area, # Directly control visibility of the row
354
+ original_passage_state
355
+ ]
356
+ )
357
+
358
+ assess_button.click(
359
+ fn=assess_student_reading_ui,
360
+ inputs=[original_passage_state, student_audio_input],
361
+ outputs=[feedback_output, highlighted_passage_output]
362
+ )
363
+
364
+ gr.Markdown(
365
+ """
366
+ ---
367
+ <div style="text-align: center; font-size: 0.9em; color: #555;">
368
+ Built with ❀️ for the Agentic Demo Track Hackathon! Tag: <code>agent-demo-track</code>
369
+ <br>Stories by Google Gemini, voices by ESPnet VITS @ HF, and listening by Whisper @ HF.
370
+ </div>
371
+ """
372
+ )
373
+
374
+ # --- Launching the App ---
375
+ if __name__ == "__main__":
376
+ if not GEMINI_API_CONFIGURED:
377
+ print("🚨 GOOGLE_API_KEY not configured for local testing or failed to initialize!")
378
+ print("Please set it: export GOOGLE_API_KEY='your_key_here'")
379
+
380
+ if not tts_client:
381
+ print("🚨 ESPnet VITS TTS client (espnet/kan-bayashi_ljspeech_vits) could not be initialized. TTS will not work.")
382
+ if not whisper_stt_client:
383
+ print("🚨 Whisper STT client (abidlabs/whisper-large-v2) could not be initialized. STT will not work.")
384
+
385
+ app.launch(debug=True) # Set share=True for a temporary public link if running locally