suprimedev commited on
Commit
145d58c
·
verified ·
1 Parent(s): c9db7d2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +140 -288
app.py CHANGED
@@ -12,449 +12,301 @@ import time
12
 
13
  # تنظیمات اولیه
14
  recognizer = sr.Recognizer()
15
- recognizer.energy_threshold = 300
16
  recognizer.dynamic_energy_threshold = True
17
- recognizer.pause_threshold = 0.8 # اضافه: pause detection بهتر برای real-time
18
 
19
- # صف برای accumulate کردن chunk‌های real-time (هر chunk کوچک ~0.5-1s)
20
- audio_queue = queue.Queue(maxsize=20) # محدود برای جلوگیری از overflow
21
- CHUNK_PROCESS_INTERVAL = 3.0 # ثانیه: هر چند ثانیه chunk‌ها را process کنیم
22
- MIN_AUDIO_DURATION = 2.0 # ثانیه: حداقل طول صوت برای process
23
 
24
- # متغیر global برای ذخیره متن real-time
25
  current_transcript = ""
26
  transcript_lock = threading.Lock()
27
 
28
- # Thread background برای پردازش مداوم queue
29
  def background_processor():
30
- """Thread جداگانه برای polling queue و process chunk‌های accumulate‌شده"""
31
- accumulated_audio = [] # لیست برای جمع chunk‌ها
32
- last_process_time = time.time()
33
 
34
  while True:
35
  try:
36
- # اگر queue خالی نیست، chunk جدید اضافه کن
37
  if not audio_queue.empty():
38
- audio_tuple = audio_queue.get(timeout=0.1)
39
- if audio_tuple:
40
- sample_rate, audio_data = audio_tuple
41
- # Normalize و append به accumulated
42
- if audio_data is not None and len(audio_data) > 0:
43
- # Clip به [-1, 1] برای جلوگیری از overflow
44
- audio_data = np.clip(audio_data, -1.0, 1.0)
45
- accumulated_audio.append((sample_rate, audio_data))
46
- print(f"Chunk accumulated: {len(audio_data)/sample_rate:.2f}s") # log
47
 
48
- # هر INTERVAL، اگر طول accumulated کافی باشد، process کن
49
- current_time = time.time()
50
- total_duration = sum(len(data)/rate for rate, data in accumulated_audio) if accumulated_audio else 0
51
- if (current_time - last_process_time >= CHUNK_PROCESS_INTERVAL) and total_duration >= MIN_AUDIO_DURATION:
52
- # Merge chunk‌ها به یک audio واحد
53
- if len(accumulated_audio) > 0:
54
- merged_rate = accumulated_audio[0][0] # فرض sample_rate ثابت
55
- merged_data = np.concatenate([data for _, data in accumulated_audio])
56
 
57
  text = process_audio_chunk((merged_rate, merged_data))
58
- if text and text != "[خطا در اتصال]":
59
  with transcript_lock:
60
  if current_transcript:
61
  current_transcript += " " + text
62
  else:
63
  current_transcript = text
64
- print(f"Processed: {text}") # log
65
 
66
- # Reset accumulated
67
- accumulated_audio = []
68
- last_process_time = current_time
69
 
70
- time.sleep(0.1) # poll هر 100ms
71
 
72
- except queue.Empty:
73
- continue
74
  except Exception as e:
75
- print(f"Error in background processor: {e}")
76
- time.sleep(0.5)
77
 
78
- # شروع thread background (یک بار)
79
  processor_thread = threading.Thread(target=background_processor, daemon=True)
80
  processor_thread.start()
81
 
82
  def numpy_to_audio_segment(audio_data, sample_rate):
83
- """تبدیل numpy array به AudioSegment (بهبود: clip و handling بهتر)"""
84
  if audio_data is None or len(audio_data) == 0:
85
  return None
86
-
87
- # نرمال‌سازی و clip به int16
88
- if audio_data.dtype == np.float32 or audio_data.dtype == np.float64:
89
- audio_data = np.clip(audio_data, -1.0, 1.0) # clip برای جلوگیری از distortion
90
  audio_data = (audio_data * 32767).astype(np.int16)
91
 
92
- # ذخیره در buffer موقت
93
  buffer = io.BytesIO()
94
  with wave.open(buffer, 'wb') as wav_file:
95
  wav_file.setnchannels(1)
96
  wav_file.setsampwidth(2)
97
  wav_file.setframerate(sample_rate)
98
  wav_file.writeframes(audio_data.tobytes())
99
-
100
  buffer.seek(0)
101
  return AudioSegment.from_wav(buffer)
102
 
103
  def process_audio_chunk(audio_tuple):
104
- """پردازش یک قطعه صوتی با Google Speech Recognition (بهبود: handling chunk کوتاه)"""
105
  try:
106
  if audio_tuple is None:
107
  return ""
108
-
109
  sample_rate, audio_data = audio_tuple
110
- duration = len(audio_data) / sample_rate if sample_rate > 0 else 0
111
-
112
- if duration < MIN_AUDIO_DURATION:
113
- return "" # skip chunk‌های خیلی کوتاه
114
 
115
- # تبدیل به AudioSegment
116
  audio_segment = numpy_to_audio_segment(audio_data, sample_rate)
117
- if audio_segment is None or len(audio_segment) < MIN_AUDIO_DURATION * 1000:
118
  return ""
119
 
120
- # ذخیره موقت برای speech recognition
121
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
122
- audio_segment.export(tmp_file.name, format="wav")
123
- tmp_path = tmp_file.name
124
 
125
- # استفاده از speech recognition با adjust برای chunk
126
  with sr.AudioFile(tmp_path) as source:
127
- recognizer.adjust_for_ambient_noise(source, duration=0.5) # adjust noise برای real-time
128
  audio = recognizer.record(source)
129
-
130
- # تشخیص با اولویت فارسی
131
  text = ""
132
  try:
133
- text = recognizer.recognize_google(audio, language='fa-IR')
 
134
  except sr.UnknownValueError:
135
- # تلاش با انگلیسی
136
  try:
137
  text = recognizer.recognize_google(audio, language='en-US')
138
  except sr.UnknownValueError:
139
- text = "" # بهبود: لاگ
140
- print("No speech detected in chunk")
141
  except sr.RequestError as e:
142
- text = "[خطا در اتصال]"
143
  print(f"Google API error: {e}")
 
144
 
145
- # پاک کردن فایل موقت
146
  if os.path.exists(tmp_path):
147
  os.unlink(tmp_path)
148
-
149
- return text.strip()
150
 
 
151
  except Exception as e:
152
- print(f"خطا در پردازش: {e}")
153
  return ""
154
 
155
- def transcribe_realtime(audio):
156
- """تبدیل real-time صوت به متن (بهبود: queue chunk و return current)"""
157
- global current_transcript
158
-
159
  if audio is None:
160
- return current_transcript
161
-
162
- # Add to queue (non-blocking)
163
  try:
164
  audio_queue.put(audio, block=False)
165
  except queue.Full:
166
- print("Queue full, skipping chunk") # log
167
-
168
- # Return current transcript برای آپدیت UI
 
 
169
  with transcript_lock:
170
  return current_transcript
171
 
172
- def clear_realtime():
173
- """پاک کردن متن real-time"""
174
  global current_transcript
175
  with transcript_lock:
176
  current_transcript = ""
177
- # Clear queue هم
178
  while not audio_queue.empty():
179
- try:
180
- audio_queue.get_nowait()
181
- except queue.Empty:
182
- break
183
  return ""
184
 
185
- # توابع file و UI بدون تغییر (برای اختصار، اما کامل هستند)
186
  def transcribe_file(audio_file, chunk_duration=30):
187
- """تبدیل فایل صوتی به متن با تقسیم به بخش‌ها"""
188
  if audio_file is None:
189
- yield "لطفاً یک فایل صوتی آپلود کنید", ""
190
  return
191
-
192
  try:
193
- # خواندن فایل صوتی
194
  audio = AudioSegment.from_file(audio_file)
195
  duration_ms = len(audio)
196
- chunk_duration_ms = chunk_duration * 1000
197
-
198
  all_text = []
199
- progress_text = ""
200
 
201
- # تقسیم و پردازش
202
- num_chunks = (duration_ms + chunk_duration_ms - 1) // chunk_duration_ms
203
-
204
- for i in range(0, duration_ms, chunk_duration_ms):
205
- chunk = audio[i:i + chunk_duration_ms]
206
-
207
- # ذخیره chunk موقت
208
- with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp_file:
209
- chunk.export(tmp_file.name, format="wav")
210
- tmp_path = tmp_file.name
211
 
212
- # تشخیص گفتار
213
  try:
214
  with sr.AudioFile(tmp_path) as source:
215
  audio_data = recognizer.record(source)
216
-
217
- # تلاش با فارسی
218
  try:
219
  text = recognizer.recognize_google(audio_data, language='fa-IR')
220
  except sr.UnknownValueError:
221
- # تلاش با انگلیسی
222
  try:
223
  text = recognizer.recognize_google(audio_data, language='en-US')
224
  except sr.UnknownValueError:
225
  text = ""
226
  except sr.RequestError:
227
- text = "[خطا در اتصال به سرویس]"
228
-
229
- if text and text != "[خطا در اتصال به سرویس]":
230
  all_text.append(text)
231
-
232
  except Exception as e:
233
- print(f"خطا در پردازش chunk: {e}")
234
 
235
- # پاک کردن فایل موقت
236
  if os.path.exists(tmp_path):
237
  os.unlink(tmp_path)
238
 
239
- # به‌روزرسانی پیشرفت
240
- current_chunk = (i // chunk_duration_ms) + 1
241
- progress = min((i + chunk_duration_ms) / duration_ms * 100, 100)
242
- progress_text = f"پیشرفت: {progress:.1f}% - بخش {current_chunk} از {num_chunks}"
243
-
244
- yield " ".join(all_text), progress_text
245
-
246
- # تاخیر کوتاه برای جلوگیری از rate limiting
247
  time.sleep(0.5)
248
 
249
- final_text = " ".join(all_text)
250
- yield final_text, "تبدیل کامل شد! ✅"
251
-
252
  except Exception as e:
253
- yield f"خطا: {str(e)}", "خطا در پردازش ❌"
254
 
255
  def save_text(text):
256
- """ذخیره متن در فایل موقت و برگرداندن آن"""
257
  if not text.strip():
258
  return None
259
-
260
  temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8')
261
  temp_file.write(text)
262
  temp_file.close()
263
-
264
  return temp_file.name
265
 
266
- # رابط کاربری Gradio (بدون تغییر، اما اتصال real-time بهبود یافته)
267
  with gr.Blocks(
268
- title="تبدیل گفتار به متن",
269
  theme=gr.themes.Soft(),
270
  css="""
271
- .gradio-container {
272
- font-family: 'Vazir', 'Tahoma', sans-serif !important;
273
- }
274
- .rtl {
275
- direction: rtl;
276
- text-align: right;
277
- }
278
  """
279
  ) as demo:
280
  gr.HTML("""
281
  <div style="text-align: center; max-width: 800px; margin: 0 auto;">
282
- <h1 style="font-size: 2.5em; margin-bottom: 0.5em;">
283
- 🎤 تبدیل گفتار به متن
284
- </h1>
285
  <p style="font-size: 1.1em; color: #666; margin-bottom: 2em;">
286
- ابزار قدرتمند تبدیل صوت به متن با پشتیبانی از زبان فارسی و انگلیسی
287
  </p>
288
  </div>
289
  """)
290
 
291
  with gr.Tabs():
292
- # تب Real-time
293
  with gr.TabItem("🎙️ ضبط مستقیم"):
294
- gr.Markdown("### میکروفون خود را فعال کرده و شروع به صحبت کنید", elem_classes="rtl")
295
 
296
  with gr.Row():
297
- with gr.Column(scale=1):
298
- audio_input = gr.Audio(
299
- sources=["microphone"],
300
- type="numpy",
301
- streaming=True,
302
- label="میکروفون",
303
- show_label=True,
304
- elem_classes="rtl"
305
- )
306
-
307
- with gr.Row():
308
- clear_btn = gr.Button(
309
- "🗑️ پاک کردن متن",
310
- variant="secondary",
311
- size="sm"
312
- )
313
 
314
  realtime_output = gr.Textbox(
315
- label="متن تشخیص داده شده",
316
- placeholder="شروع به صحبت کنید و متن اینجا ظاهر می‌شود... (پس از 3-5 ثانیه)",
317
- lines=12,
318
  elem_classes="rtl",
319
  rtl=True,
320
- show_copy_button=True
321
- )
322
-
323
- # اتصال برای real-time transcription (بهبود: stream هر chunk به queue)
324
- audio_input.stream(
325
- transcribe_realtime,
326
- inputs=[audio_input],
327
- outputs=[realtime_output],
328
- show_progress=False
329
  )
330
 
331
- clear_btn.click(
332
- clear_realtime,
333
- outputs=[realtime_output]
334
- )
335
 
336
- # تب فایل صوتی (بدون تغییر)
337
- with gr.TabItem("📁 فایل صوتی"):
338
- gr.Markdown("### فایل صوتی خود را انتخاب کنید", elem_classes="rtl")
339
 
340
- with gr.Row():
341
- with gr.Column(scale=3):
342
- file_input = gr.Audio(
343
- sources=["upload"],
344
- type="filepath",
345
- label="انتخاب فایل صوتی",
346
- elem_classes="rtl"
347
- )
348
-
349
- with gr.Column(scale=1):
350
- chunk_duration = gr.Slider(
351
- minimum=10,
352
- maximum=60,
353
- value=30,
354
- step=5,
355
- label="مدت هر بخش (ثانیه)",
356
- elem_classes="rtl"
357
- )
358
 
359
- process_btn = gr.Button(
360
- "🚀 شروع تبدیل",
361
- variant="primary",
362
- size="lg"
363
- )
364
 
365
- progress_label = gr.Textbox(
366
- label="وضعیت پردازش",
367
- interactive=False,
368
- elem_classes="rtl"
369
- )
370
 
371
- file_output = gr.Textbox(
372
- label="متن تشخیص داده شده",
373
- placeholder="متن پس از پردازش اینجا نمایش داده می‌شود...",
374
- lines=12,
375
- elem_classes="rtl",
376
- rtl=True,
377
- show_copy_button=True
378
  )
 
 
 
 
379
 
380
- # دکمه‌های ذخیره و پاک کردن
381
  with gr.Row():
382
- save_btn = gr.Button(
383
- "💾 ذخیره متن",
384
- variant="secondary"
385
- )
386
- clear_file_btn = gr.Button(
387
- "🗑️ پاک کردن",
388
- variant="secondary"
389
- )
390
- download_file = gr.File(
391
- label="دانلود فایل متن",
392
- visible=False,
393
- elem_classes="rtl"
394
- )
395
 
396
- # اتصالات
397
- process_btn.click(
398
- transcribe_file,
399
- inputs=[file_input, chunk_duration],
400
- outputs=[file_output, progress_label]
401
- )
402
 
403
- save_btn.click(
404
- save_text,
405
- inputs=[file_output],
406
- outputs=[download_file]
407
- ).then(
408
- lambda: gr.update(visible=True),
409
- outputs=[download_file]
410
- )
411
 
412
- clear_file_btn.click(
413
- lambda: ("", ""),
414
- outputs=[file_output, progress_label]
415
- )
416
 
417
- # بخش راهنما (بهبود: نکته real-time)
418
- with gr.Accordion("📖 راهنمای استفاده", open=False, elem_classes="rtl"):
419
  gr.Markdown("""
420
- ### نحوه استفاده:
421
-
422
- **برای ضبط مستقیم (بهبود یافته):**
423
- 1. به تب "ضبط مستقیم" بروید
424
- 2. اجازه دسترسی به میکروفون را بدهید
425
- 3. حداقل 3-5 ثانیه واضح صحبت کنید (chunk‌ها جمع می‌شوند)
426
- 4. متن هر 3 ثانیه آپدیت می‌شود
427
-
428
- **برای فایل صوتی:**
429
- 1. به تب "فایل صوتی" بروید
430
- 2. فایل مورد نظر را انتخاب کنید
431
- 3. مدت زمان تقسیم‌بندی را تنظیم کنید (پیش‌فرض: 30 ثانیه)
432
- 4. روی "شروع تبدیل" کلیک کنید
433
- 5. منتظر بمانید تا پردازش کامل شود
434
-
435
- ### فرمت‌های پشتیبانی شده:
436
- - MP3, WAV, M4A, FLAC, OGG, MP4, AVI, MOV
437
-
438
- ### نکات مهم:
439
- - 🎯 برای دقت بیشتر، از فایل‌های با کیفیت بالا استفاده کنید
440
- - 🔇 نویز پس‌زمینه را به حداقل برسانید
441
- - 🗣️ واضح و شمرده صحبت کنید (حداقل 3 ثانیه)
442
- - 🌐 اتصال اینترنت پایدار داشته باشید
443
- - ⚠️ Real-time ممکن است 1-2 ثانیه تاخیر داشته باشد
444
  """, elem_classes="rtl")
445
 
446
- # فوتر
447
- gr.HTML("""
448
- <div style="text-align: center; margin-top: 2em; padding: 1em; background-color: #f8f9fa;">
449
- <p style="color: #666;">
450
- ساخته شده با ❤️ | نسخه 2.1 (بهبود real-time)
451
- </p>
452
- </div>
453
- """)
454
 
455
- # اجرای برنامه
456
  if __name__ == "__main__":
457
- demo.queue().launch(
458
- share=True,
459
- show_error=True
460
- )
 
12
 
13
  # تنظیمات اولیه
14
  recognizer = sr.Recognizer()
15
+ recognizer.energy_threshold = 400 # کمی بالاتر برای real-time
16
  recognizer.dynamic_energy_threshold = True
17
+ recognizer.pause_threshold = 0.5 # حساس‌تر به pause
18
 
19
+ # صف برای accumulate chunk‌های real-time
20
+ audio_queue = queue.Queue(maxsize=10) # کوچک‌تر برای real-time
21
+ PROCESS_INTERVAL = 3.0 # هر 3 ثانیه process
22
+ MIN_DURATION = 1.5 # حداقل 1.5s صوت برای process
23
 
24
+ # Transcript global
25
  current_transcript = ""
26
  transcript_lock = threading.Lock()
27
 
28
+ # Background thread برای پردازش queue
29
  def background_processor():
30
+ accumulated = []
31
+ last_process = time.time()
 
32
 
33
  while True:
34
  try:
35
+ # Get chunk if available
36
  if not audio_queue.empty():
37
+ audio_tuple = audio_queue.get(timeout=0.2)
38
+ if audio_tuple and audio_tuple[1] is not None:
39
+ rate, data = audio_tuple
40
+ data = np.clip(data, -1.0, 1.0) # Normalize
41
+ accumulated.append((rate, data))
42
+ print(f"Accumulated chunk: {len(data)/rate:.2f}s")
 
 
 
43
 
44
+ # Process if interval passed and enough audio
45
+ now = time.time()
46
+ total_dur = sum(len(d)/r for r, d in accumulated) if accumulated else 0
47
+ if (now - last_process >= PROCESS_INTERVAL) and total_dur >= MIN_DURATION:
48
+ if accumulated:
49
+ merged_rate = accumulated[0][0]
50
+ merged_data = np.concatenate([d for _, d in accumulated])
 
51
 
52
  text = process_audio_chunk((merged_rate, merged_data))
53
+ if text and text not in ["", "[خطا در اتصال]"]:
54
  with transcript_lock:
55
  if current_transcript:
56
  current_transcript += " " + text
57
  else:
58
  current_transcript = text
59
+ print(f"Processed: {text[:50]}...") # Log کوتاه
60
 
61
+ # Reset
62
+ accumulated = []
63
+ last_process = now
64
 
65
+ time.sleep(0.2) # Poll faster for responsiveness
66
 
 
 
67
  except Exception as e:
68
+ print(f"Background error: {e}")
69
+ time.sleep(1)
70
 
71
+ # Start background thread
72
  processor_thread = threading.Thread(target=background_processor, daemon=True)
73
  processor_thread.start()
74
 
75
  def numpy_to_audio_segment(audio_data, sample_rate):
 
76
  if audio_data is None or len(audio_data) == 0:
77
  return None
78
+ if audio_data.dtype in [np.float32, np.float64]:
79
+ audio_data = np.clip(audio_data, -1.0, 1.0)
 
 
80
  audio_data = (audio_data * 32767).astype(np.int16)
81
 
 
82
  buffer = io.BytesIO()
83
  with wave.open(buffer, 'wb') as wav_file:
84
  wav_file.setnchannels(1)
85
  wav_file.setsampwidth(2)
86
  wav_file.setframerate(sample_rate)
87
  wav_file.writeframes(audio_data.tobytes())
 
88
  buffer.seek(0)
89
  return AudioSegment.from_wav(buffer)
90
 
91
  def process_audio_chunk(audio_tuple):
 
92
  try:
93
  if audio_tuple is None:
94
  return ""
 
95
  sample_rate, audio_data = audio_tuple
96
+ duration = len(audio_data) / sample_rate if sample_rate else 0
97
+ if duration < MIN_DURATION:
98
+ return ""
 
99
 
 
100
  audio_segment = numpy_to_audio_segment(audio_data, sample_rate)
101
+ if audio_segment is None or len(audio_segment) < MIN_DURATION * 1000:
102
  return ""
103
 
104
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
105
+ audio_segment.export(tmp.name, format="wav")
106
+ tmp_path = tmp.name
 
107
 
 
108
  with sr.AudioFile(tmp_path) as source:
109
+ recognizer.adjust_for_ambient_noise(source, duration=0.3) # Quick adjust
110
  audio = recognizer.record(source)
111
+
112
+ # Google recognition
113
  text = ""
114
  try:
115
+ text = recognizer.recognize_google(audio, language='fa-IR') # Persian first
116
+ # اگر key داری: recognize_google(audio, language='fa-IR', key="YOUR_GOOGLE_API_KEY")
117
  except sr.UnknownValueError:
 
118
  try:
119
  text = recognizer.recognize_google(audio, language='en-US')
120
  except sr.UnknownValueError:
121
+ print("No speech in chunk")
122
+ text = ""
123
  except sr.RequestError as e:
 
124
  print(f"Google API error: {e}")
125
+ text = "[خطا اتصال]"
126
 
 
127
  if os.path.exists(tmp_path):
128
  os.unlink(tmp_path)
 
 
129
 
130
+ return text.strip()
131
  except Exception as e:
132
+ print(f"Process error: {e}")
133
  return ""
134
 
135
+ def handle_realtime_audio(audio):
136
+ """Handle incoming audio chunks from microphone"""
 
 
137
  if audio is None:
138
+ return gr.update()
 
 
139
  try:
140
  audio_queue.put(audio, block=False)
141
  except queue.Full:
142
+ print("Queue full, skip")
143
+ return gr.update()
144
+
145
+ def get_current_transcript():
146
+ """Poll transcript for UI update"""
147
  with transcript_lock:
148
  return current_transcript
149
 
150
+ def clear_transcript():
 
151
  global current_transcript
152
  with transcript_lock:
153
  current_transcript = ""
154
+ # Clear queue
155
  while not audio_queue.empty():
156
+ audio_queue.get_nowait()
 
 
 
157
  return ""
158
 
159
+ # File transcription (unchanged, works fine)
160
  def transcribe_file(audio_file, chunk_duration=30):
 
161
  if audio_file is None:
162
+ yield "لطفاً فایل آپلود کنید", ""
163
  return
 
164
  try:
 
165
  audio = AudioSegment.from_file(audio_file)
166
  duration_ms = len(audio)
167
+ chunk_ms = chunk_duration * 1000
 
168
  all_text = []
 
169
 
170
+ num_chunks = (duration_ms + chunk_ms - 1) // chunk_ms
171
+ for i in range(0, duration_ms, chunk_ms):
172
+ chunk = audio[i:i + chunk_ms]
173
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
174
+ chunk.export(tmp.name, format="wav")
175
+ tmp_path = tmp.name
 
 
 
 
176
 
 
177
  try:
178
  with sr.AudioFile(tmp_path) as source:
179
  audio_data = recognizer.record(source)
 
 
180
  try:
181
  text = recognizer.recognize_google(audio_data, language='fa-IR')
182
  except sr.UnknownValueError:
 
183
  try:
184
  text = recognizer.recognize_google(audio_data, language='en-US')
185
  except sr.UnknownValueError:
186
  text = ""
187
  except sr.RequestError:
188
+ text = "[خطا اتصال]"
189
+ if text and text != "[خطا اتصال]":
 
190
  all_text.append(text)
 
191
  except Exception as e:
192
+ print(f"File chunk error: {e}")
193
 
 
194
  if os.path.exists(tmp_path):
195
  os.unlink(tmp_path)
196
 
197
+ progress = min((i + chunk_ms) / duration_ms * 100, 100)
198
+ yield " ".join(all_text), f"پیشرفت: {progress:.1f}% - بخش {(i // chunk_ms)+1} از {num_chunks}"
 
 
 
 
 
 
199
  time.sleep(0.5)
200
 
201
+ yield " ".join(all_text), "کامل شد! ✅"
 
 
202
  except Exception as e:
203
+ yield f"خطا: {e}", "خطا ❌"
204
 
205
  def save_text(text):
 
206
  if not text.strip():
207
  return None
 
208
  temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.txt', encoding='utf-8')
209
  temp_file.write(text)
210
  temp_file.close()
 
211
  return temp_file.name
212
 
213
+ # Gradio UI with Timer for live updates
214
  with gr.Blocks(
215
+ title="تبدیل گفتار به متن - Real-time Fixed",
216
  theme=gr.themes.Soft(),
217
  css="""
218
+ .gradio-container { font-family: 'Vazir', 'Tahoma', sans-serif !important; }
219
+ .rtl { direction: rtl; text-align: right; }
 
 
 
 
 
220
  """
221
  ) as demo:
222
  gr.HTML("""
223
  <div style="text-align: center; max-width: 800px; margin: 0 auto;">
224
+ <h1 style="font-size: 2.5em; margin-bottom: 0.5em;">🎤 تبدیل گفتار به متن</h1>
 
 
225
  <p style="font-size: 1.1em; color: #666; margin-bottom: 2em;">
226
+ Real-time با Google API - پردازش background | قابل توزیع روی مرورگرها
227
  </p>
228
  </div>
229
  """)
230
 
231
  with gr.Tabs():
 
232
  with gr.TabItem("🎙️ ضبط مستقیم"):
233
+ gr.Markdown("### فعال کنید و 5+ ثانیه واضح صحبت کنید (متن هر 3s آپدیت می‌شه)", elem_classes="rtl")
234
 
235
  with gr.Row():
236
+ audio_input = gr.Audio(
237
+ sources=["microphone"],
238
+ type="numpy",
239
+ label="میکروفون (ضبط رو شروع کن)",
240
+ elem_classes="rtl"
241
+ )
242
+
243
+ start_btn = gr.Button("▶️ شروع real-time", variant="primary")
244
+ stop_btn = gr.Button("⏹️ توقف", variant="secondary")
 
 
 
 
 
 
 
245
 
246
  realtime_output = gr.Textbox(
247
+ label="متن live",
248
+ placeholder="پس از 3-5s صحبت، متن ظاهر می‌شه...",
249
+ lines=10,
250
  elem_classes="rtl",
251
  rtl=True,
252
+ show_copy_button=True,
253
+ interactive=False
 
 
 
 
 
 
 
254
  )
255
 
256
+ clear_btn = gr.Button("🗑️ پاک کردن", variant="secondary")
 
 
 
257
 
258
+ # Events
259
+ audio_input.change(handle_realtime_audio, inputs=[audio_input], outputs=[audio_input]) # Handle chunks
 
260
 
261
+ # Timer for live update (هر 2s transcript رو pull کن)
262
+ timer = gr.Timer(2.0) # Start after tab open
263
+ timer.tick(get_current_transcript, outputs=[realtime_output])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
+ clear_btn.click(clear_transcript, outputs=[realtime_output])
 
 
 
 
266
 
267
+ # Start/stop recording (toggle microphone)
268
+ def toggle_recording(active):
269
+ return gr.update(value=active) # Simple toggle, but Gradio handles start/stop
 
 
270
 
271
+ start_btn.click(lambda: gr.update(visible=True), outputs=[stop_btn]).click(
272
+ toggle_recording, inputs=[audio_input], outputs=[audio_input]
 
 
 
 
 
273
  )
274
+ stop_btn.click(lambda: gr.update(visible=False), outputs=[start_btn])
275
+
276
+ with gr.TabItem("📁 فایل صوتی"):
277
+ gr.Markdown("### فایل آپلود کن و تبدیل کن", elem_classes="rtl")
278
 
 
279
  with gr.Row():
280
+ file_input = gr.Audio(sources=["upload"], type="filepath", label="فایل صوتی", elem_classes="rtl")
281
+ chunk_slider = gr.Slider(10, 60, 30, 5, label="بخش‌بندی (s)", elem_classes="rtl")
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ process_btn = gr.Button("🚀 تبدیل", variant="primary")
284
+ progress_label = gr.Textbox(label="وضعیت", interactive=False, elem_classes="rtl")
285
+ file_output = gr.Textbox(label="متن", lines=10, elem_classes="rtl", rtl=True, show_copy_button=True)
 
 
 
286
 
287
+ with gr.Row():
288
+ save_btn = gr.Button("💾 ذخیره", variant="secondary")
289
+ clear_file_btn = gr.Button("🗑️ پاک", variant="secondary")
290
+ download_file = gr.File(label="دانلود TXT", visible=False, elem_classes="rtl")
 
 
 
 
291
 
292
+ process_btn.click(transcribe_file, [file_input, chunk_slider], [file_output, progress_label])
293
+ save_btn.click(save_text, file_output, download_file).then(lambda: gr.update(visible=True), download_file)
294
+ clear_file_btn.click(lambda: ("", ""), [file_output, progress_label])
 
295
 
296
+ with gr.Accordion("📖 راهنما", open=False, elem_classes="rtl"):
 
297
  gr.Markdown("""
298
+ ### استفاده:
299
+ - **Real-time**: میکروفون رو فعال کن، 5s+ صحبت کن. هر 3s متن آپدیت می‌شه (background).
300
+ - **فایل**: آپلود و دکمه بزن.
301
+ ### نکات:
302
+ - 🗣️ واضح صحبت کن، نویز کم.
303
+ - 🌐 اینترنت پایدار (Google API).
304
+ - 📱 روی موبایل/دسکتاپ کار می‌کنه (mic access بده).
305
+ - ⚠️ محدودیت Google: ~60 req/min - برای حجم بالا، API key اضافه کن (در کد کامنت).
306
+ - توزیع: share لینک رو share کن، همه browserها ساپورت.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  """, elem_classes="rtl")
308
 
309
+ gr.HTML('<div style="text-align: center; margin-top: 2em; padding: 1em; background: #f8f9fa;"><p style="color: #666;">نسخه 2.2 - Fixed Real-time با Timer | Google Backend</p></div>')
 
 
 
 
 
 
 
310
 
 
311
  if __name__ == "__main__":
312
+ demo.queue().launch(share=True, show_error=True, server_name="0.0.0.0", server_port=7860)