Opera8 commited on
Commit
acec6d4
·
verified ·
1 Parent(s): 44edb17

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -454
app.py CHANGED
@@ -1,97 +1,60 @@
1
  import os
2
- import json
3
- import re
4
- from collections.abc import Iterator
5
- from threading import Thread
6
-
7
  import gradio as gr
8
- import spaces
9
  import torch
10
  import librosa
11
- from transformers import (
12
- AutoModelForSpeechSeq2Seq,
13
- AutoProcessor,
14
- pipeline,
15
- AutoModelForMultimodalLM,
16
- BatchFeature,
17
- StoppingCriteria
18
- )
19
- from transformers.generation.streamers import TextIteratorStreamer
20
 
21
- # ==========================================
22
- # 1. تنظیمات و بارگذاری مدل تبدیل صدا به متن (Whisper)
23
- # ==========================================
24
- whisper_model_id = "openai/whisper-large-v3"
25
 
 
26
  if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
27
- whisper_dtype = torch.bfloat16
28
- print("Whisper: Using optimized BFloat16 for A100/H200")
29
  else:
30
- whisper_dtype = torch.float16
31
- print("Whisper: Using Float16")
32
 
 
33
  attn_implementation = "sdpa"
34
  try:
35
  import flash_attn
36
  attn_implementation = "flash_attention_2"
37
- print("Whisper: Using Flash Attention 2")
38
  except ImportError:
39
- print("Whisper: Flash Attention 2 not found. Falling back to SDPA")
40
 
41
- whisper_model = AutoModelForSpeechSeq2Seq.from_pretrained(
42
- whisper_model_id,
43
- torch_dtype=whisper_dtype,
 
44
  low_cpu_mem_usage=True,
45
  use_safetensors=True,
46
  attn_implementation=attn_implementation
47
  )
48
 
49
- whisper_device = "cuda" if torch.cuda.is_available() else "cpu"
50
- whisper_model.to(whisper_device)
51
- whisper_processor = AutoProcessor.from_pretrained(whisper_model_id)
52
 
53
- whisper_pipe = pipeline(
54
- "automatic-speech-recognition",
55
- model=whisper_model,
56
- tokenizer=whisper_processor.tokenizer,
57
- feature_extractor=whisper_processor.feature_extractor,
58
- dtype=whisper_dtype,
59
- device=whisper_device,
60
- )
61
-
62
- # ==========================================
63
- # 2. تنظیمات و بارگذاری مدل زبانی چندرسانه‌ای (Gemma)
64
- # ==========================================
65
- gemma_model_id = "google/gemma-4-e4b-it"
66
-
67
- gemma_processor = AutoProcessor.from_pretrained(gemma_model_id, use_fast=False)
68
- gemma_model = AutoModelForMultimodalLM.from_pretrained(
69
- gemma_model_id,
70
- device_map="auto",
71
- dtype=torch.bfloat16
72
- )
73
-
74
- IMAGE_FILE_TYPES = (".jpg", ".jpeg", ".png", ".webp")
75
- AUDIO_FILE_TYPES = (".wav", ".mp3", ".flac", ".ogg")
76
- VIDEO_FILE_TYPES = (".mp4", ".mov", ".avi", ".webm")
77
- MAX_INPUT_TOKENS = int(os.getenv("MAX_INPUT_TOKENS", "10_000"))
78
 
79
- THINKING_START = "<|channel>"
80
- THINKING_END = "<channel|>"
81
 
82
- _KEEP_TOKENS = {THINKING_START, THINKING_END}
83
- _STRIP_TOKENS = sorted(
84
- (t for t in gemma_processor.tokenizer.all_special_tokens if t not in _KEEP_TOKENS),
85
- key=len,
86
- reverse=True,
 
 
 
87
  )
88
 
89
-
90
- # ==========================================
91
- # توابع کمکی تبدیل صدا به متن (Whisper)
92
- # ==========================================
93
- prompt_text = "کلمات دقیقاً همان‌طور که تلفظ می‌شوند، با رعایت حروف اضافه و ساختار عامیانه، محاوره‌ای و شکسته نوشته شوند."
94
-
95
  LANGUAGE_MAPPING = {
96
  "Auto-Detect (تشخیص خودکار)": None,
97
  "Persian / فارسی": "fa",
@@ -108,6 +71,7 @@ LANGUAGE_MAPPING = {
108
  "Korean / کره‌ای": "ko"
109
  }
110
 
 
111
  def format_timestamp(seconds):
112
  hours = int(seconds // 3600)
113
  minutes = int((seconds % 3600) // 60)
@@ -115,49 +79,7 @@ def format_timestamp(seconds):
115
  millis = int(round((seconds % 1) * 1000))
116
  return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
117
 
118
- def create_srt_from_json_data(word_items, max_words_per_segment=6, max_duration_per_segment=3.0):
119
- """تولید استاندارد فایل زیرنویس SRT بر پایه داده‌های جیسون بازنویسی شده کلمات"""
120
- srt_lines = []
121
- segment_idx = 1
122
- current_segment_words = []
123
- current_start = None
124
-
125
- for item in word_items:
126
- text = item.get("word", "").strip()
127
- start = item.get("start")
128
- end = item.get("end")
129
-
130
- if not text or start is None:
131
- continue
132
- if end is None:
133
- end = start + 0.3
134
-
135
- if current_start is None:
136
- current_start = start
137
-
138
- current_segment_words.append(text)
139
- duration = end - current_start
140
-
141
- if len(current_segment_words) >= max_words_per_segment or duration >= max_duration_per_segment:
142
- line_text = " ".join(current_segment_words)
143
- srt_lines.append(f"{segment_idx}")
144
- srt_lines.append(f"{format_timestamp(current_start)} --> {format_timestamp(end)}")
145
- srt_lines.append(line_text)
146
- srt_lines.append("")
147
- segment_idx += 1
148
- current_segment_words = []
149
- current_start = None
150
-
151
- if current_segment_words:
152
- last_end = word_items[-1].get("end") if word_items and word_items[-1].get("end") else current_start + 1.0
153
- line_text = " ".join(current_segment_words)
154
- srt_lines.append(f"{segment_idx}")
155
- srt_lines.append(f"{format_timestamp(current_start)} --> {format_timestamp(last_end)}")
156
- srt_lines.append(line_text)
157
- srt_lines.append("")
158
-
159
- return "\n".join(srt_lines)
160
-
161
  def create_srt_from_chunks(chunks, max_words_per_segment=6, max_duration_per_segment=3.0):
162
  srt_lines = []
163
  segment_idx = 1
@@ -175,7 +97,7 @@ def create_srt_from_chunks(chunks, max_words_per_segment=6, max_duration_per_seg
175
  if start is None:
176
  continue
177
  if end is None:
178
- end = start + 0.3
179
 
180
  if current_start is None:
181
  current_start = start
@@ -183,6 +105,7 @@ def create_srt_from_chunks(chunks, max_words_per_segment=6, max_duration_per_seg
183
  current_segment_words.append(text)
184
  duration = end - current_start
185
 
 
186
  if len(current_segment_words) >= max_words_per_segment or duration >= max_duration_per_segment:
187
  line_text = " ".join(current_segment_words)
188
  srt_lines.append(f"{segment_idx}")
@@ -203,6 +126,7 @@ def create_srt_from_chunks(chunks, max_words_per_segment=6, max_duration_per_seg
203
 
204
  return "\n".join(srt_lines)
205
 
 
206
  def get_raw_word_timestamps(chunks):
207
  word_timestamps = []
208
  for chunk in chunks:
@@ -216,392 +140,123 @@ def get_raw_word_timestamps(chunks):
216
  })
217
  return json.dumps(word_timestamps, ensure_ascii=False, indent=2)
218
 
219
- @spaces.GPU(duration=120)
220
  def transcribe_audio(audio_path, language_selection, optimize_persian):
221
  if audio_path is None:
222
  return "Please upload or record an audio file first. / لطفاً ابتدا فایل صوتی را آپلود یا ضبط کنید.", "", ""
223
 
224
  try:
225
- # پردازش Whisper
226
  audio_data, sampling_rate = librosa.load(audio_path, sr=16000)
 
 
227
  lang_code = LANGUAGE_MAPPING.get(language_selection, None)
228
 
 
229
  generate_kwargs = {
230
  "task": "transcribe",
231
  "do_sample": False,
232
- "num_beams": 5
233
  }
234
 
 
235
  if lang_code is not None:
236
  generate_kwargs["language"] = lang_code
237
 
 
238
  if optimize_persian:
239
- prompt_ids = whisper_processor.get_prompt_ids(prompt_text, return_tensors="pt")
240
- prompt_ids = prompt_ids.to(whisper_device)
241
  generate_kwargs["prompt_ids"] = prompt_ids
242
- generate_kwargs["language"] = "fa"
243
 
244
- result = whisper_pipe(
 
245
  {"raw": audio_data, "sampling_rate": sampling_rate},
246
  chunk_length_s=30,
247
  stride_length_s=(6, 6),
248
  batch_size=24,
249
- return_timestamps="word",
250
  generate_kwargs=generate_kwargs
251
  )
252
 
253
  full_text = result["text"].strip()
254
  chunks = result.get("chunks", [])
255
 
 
256
  srt_content = create_srt_from_chunks(chunks)
257
  word_timestamps_json = get_raw_word_timestamps(chunks)
258
 
259
- # فرآیند اصلاح هوشمند اجباری با هدایت مستقیم مدل زبانی روی ساختار کد JSON
260
- if word_timestamps_json:
261
- correction_prompt = (
262
- "You are an expert subtitle corrector. Below is a JSON array representing words with their exact start and end timestamps in Persian. "
263
- "Your task is ONLY to fix spelling, grammatical, or listening errors inside the 'word' field of each item. "
264
- "CRITICAL RULES:\n"
265
- "1. Keep the JSON structure exactly the same. Do NOT translate or summarize.\n"
266
- "2. The start and end timestamps must NOT be modified under any circumstance.\n"
267
- "3. Do NOT add or remove items. The list length must remain exactly identical.\n"
268
- "4. Output ONLY the corrected valid JSON array starting with '[' and ending with ']'. "
269
- "Do NOT include any markdown code blocks, conversational text, explanations, or introductory sentences. Output raw JSON only.\n\n"
270
- f"JSON:\n{word_timestamps_json}"
271
- )
272
-
273
- messages = [{"role": "user", "content": [{"type": "text", "text": correction_prompt}]}]
274
-
275
- inputs = gemma_processor.apply_chat_template(
276
- messages,
277
- tokenize=True,
278
- add_generation_prompt=True,
279
- return_dict=True,
280
- return_tensors="pt"
281
- )
282
- inputs = inputs.to(device=gemma_model.device, dtype=torch.bfloat16)
283
-
284
- with torch.inference_mode():
285
- outputs = gemma_model.generate(
286
- **inputs,
287
- max_new_tokens=4000,
288
- disable_compile=True
289
- )
290
-
291
- input_length = inputs["input_ids"].shape[1]
292
- generated_tokens = outputs[0][input_length:]
293
- corrected_text = gemma_processor.decode(generated_tokens, skip_special_tokens=True).strip()
294
-
295
- # پاکسازی و استخراج ایمن بلاک JSON
296
- start_idx = corrected_text.find('[')
297
- end_idx = corrected_text.rfind(']')
298
-
299
- if start_idx != -1 and end_idx != -1:
300
- clean_json_str = corrected_text[start_idx:end_idx+1]
301
- try:
302
- corrected_data = json.loads(clean_json_str)
303
-
304
- # بازسازی تمام خروجی‌ها از روی دیتای تصحیح‌شده
305
- full_text = " ".join([item["word"] for item in corrected_data])
306
- srt_content = create_srt_from_json_data(corrected_data)
307
- word_timestamps_json = json.dumps(corrected_data, ensure_ascii=False, indent=2)
308
- except Exception as je:
309
- print(f"Failed to parse corrected JSON from Gemma: {je}")
310
- # در صورت هرگونه خطا، از نتایج خام Whisper استفاده می‌شود تا برنامه متوقف نشود
311
-
312
  return full_text, srt_content, word_timestamps_json
313
 
314
  except Exception as e:
315
  return f"An error occurred during processing / خطایی در حین پردازش رخ داد: {str(e)}", "", ""
316
 
317
-
318
- # ==========================================
319
- # توابع کمکی چت‌بات (Gemma)
320
- # ==========================================
321
- def _strip_special_tokens(text: str) -> str:
322
- for tok in _STRIP_TOKENS:
323
- text = text.replace(tok, "")
324
- return text
325
-
326
- def _classify_file(path: str) -> str | None:
327
- lower = path.lower()
328
- if lower.endswith(IMAGE_FILE_TYPES):
329
- return "image"
330
- if lower.endswith(AUDIO_FILE_TYPES):
331
- return "audio"
332
- if lower.endswith(VIDEO_FILE_TYPES):
333
- return "video"
334
- return None
335
-
336
- def process_new_user_message(message: dict) -> list[dict]:
337
- content: list[dict] = []
338
- for path in message.get("files", []):
339
- kind = _classify_file(path)
340
- if kind:
341
- content.append({"type": kind, "url": path})
342
- content.append({"type": "text", "text": message.message.get("text", "") if hasattr(message, 'message') else message.get("text", "")})
343
- return content
344
-
345
- def process_history(history: list[dict]) -> list[dict]:
346
- messages: list[dict] = []
347
- for item in history:
348
- if item["role"] == "assistant":
349
- if (item.get("metadata") or {}).get("title") == "Reasoning":
350
- continue
351
- text_parts = [p["text"] for p in item["content"] if p.get("type") == "text"]
352
- messages.append(
353
- {
354
- "role": "assistant",
355
- "content": [{"type": "text", "text": " ".join(text_parts)}],
356
- }
357
- )
358
- else:
359
- user_content: list[dict] = []
360
- for part in item["content"]:
361
- if part.get("type") == "text":
362
- user_content.append({"type": "text", "text": part["text"]})
363
- elif part.get("type") == "file":
364
- filepath = part["file"]["path"]
365
- kind = _classify_file(filepath)
366
- if kind:
367
- user_content.append({"type": kind, "url": filepath})
368
- if user_content:
369
- messages.append({"role": "user", "content": user_content})
370
- return messages
371
-
372
- class StopOnSignal(StoppingCriteria):
373
- def __init__(self) -> None:
374
- self.stopped = False
375
-
376
- def __call__(self, input_ids: torch.Tensor, scores: torch.Tensor, **kwargs: object) -> bool:
377
- return self.stopped
378
-
379
- @spaces.GPU(duration=120)
380
- @torch.inference_mode()
381
- def _generate_on_gpu(inputs: BatchFeature, max_new_tokens: int, thinking: bool) -> Iterator[str]:
382
- inputs = inputs.to(device=gemma_model.device, dtype=torch.bfloat16)
383
-
384
- streamer = TextIteratorStreamer(
385
- gemma_processor,
386
- timeout=30.0,
387
- skip_prompt=True,
388
- skip_special_tokens=not thinking,
389
- )
390
- stop_criteria = StopOnSignal()
391
- generate_kwargs = {
392
- **inputs,
393
- "streamer": streamer,
394
- "stopping_criteria": [stop_criteria],
395
- "max_new_tokens": max_new_tokens,
396
- "disable_compile": True,
397
- }
398
-
399
- exception_holder: list[Exception] = []
400
-
401
- def _generate() -> None:
402
- try:
403
- gemma_model.generate(**generate_kwargs)
404
- except Exception as e:
405
- exception_holder.append(e)
406
-
407
- thread = Thread(target=_generate)
408
- thread.start()
409
-
410
- chunks: list[str] = []
411
- try:
412
- for text in streamer:
413
- chunks.append(text)
414
- accumulated = "".join(chunks)
415
- if thinking:
416
- yield _strip_special_tokens(accumulated)
417
- else:
418
- yield accumulated
419
- except GeneratorExit:
420
- stop_criteria.stopped = True
421
- for _ in streamer:
422
- pass
423
- thread.join()
424
- raise
425
-
426
- thread.join()
427
- if exception_holder:
428
- msg = f"Generation failed: {exception_holder[0]}"
429
- raise gr.Error(msg)
430
-
431
- def validate_input(message: dict) -> dict:
432
- has_text = bool(message.get("text", "").strip())
433
- has_files = bool(message.get("files"))
434
- if not (has_text or has_files):
435
- return gr.validate(False, "Please enter a message or upload a file.")
436
-
437
- files = message.get("files", [])
438
- kinds = [_classify_file(f) for f in files]
439
- kinds = [k for k in kinds if k is not None]
440
- unique_kinds = set(kinds)
441
-
442
- if len(unique_kinds) > 1:
443
- return gr.validate(False, "Please upload only one type of media at a time.")
444
- if kinds.count("audio") > 1:
445
- return gr.validate(False, "Only one audio file can be uploaded at a time.")
446
- if kinds.count("video") > 1:
447
- return gr.validate(False, "Only one video file can be uploaded at a time.")
448
-
449
- return gr.validate(True, "")
450
-
451
- def _has_media_type(messages: list[dict], media_type: str) -> bool:
452
- return any(c.get("type") == media_type for m in messages for c in m["content"])
453
-
454
- def generate(
455
- message: dict,
456
- history: list[dict],
457
- thinking: bool = False,
458
- max_new_tokens: int = 1024,
459
- max_soft_tokens: int = 280,
460
- system_prompt: str = "",
461
- ) -> Iterator[str]:
462
- messages: list[dict] = []
463
- if system_prompt:
464
- messages.append({"role": "system", "content": [{"type": "text", "text": system_prompt}]})
465
-
466
- messages.extend(process_history(history))
467
- messages.append({"role": "user", "content": process_new_user_message(message)})
468
-
469
- template_kwargs: dict = {
470
- "tokenize": True,
471
- "return_dict": True,
472
- "return_tensors": "pt",
473
- "add_generation_prompt": True,
474
- "load_audio_from_video": _has_media_type(messages, "video"),
475
- "processor_kwargs": {"images_kwargs": {"max_soft_tokens": max_soft_tokens}},
476
- }
477
- if thinking:
478
- template_kwargs["enable_thinking"] = True
479
-
480
- inputs = gemma_processor.apply_chat_template(messages, **template_kwargs)
481
-
482
- n_tokens = inputs["input_ids"].shape[1]
483
- if n_tokens > MAX_INPUT_TOKENS:
484
- msg = f"Input too long ({n_tokens} tokens). Maximum is {MAX_INPUT_TOKENS} tokens."
485
- raise gr.Error(msg)
486
-
487
- yield from _generate_on_gpu(inputs=inputs, max_new_tokens=max_new_tokens, thinking=thinking)
488
-
489
-
490
- # ==========================================
491
- # رابط کاربری مشترک (Gradio Blocks)
492
- # ==========================================
493
- examples_chat = [
494
- [{"text": "What is the capital of France?", "files": []}],
495
- [{"text": "Explain quantum entanglement in simple terms.", "files": []}],
496
- [{"text": "Describe this image.", "files": ["https://news.bbc.co.uk/media/images/38107000/jpg/_38107299_ronaldogoal_ap_300.jpg"]}],
497
- [{"text": "Transcribe the audio.", "files": ["https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/bcn_weather.mp3"]}],
498
- [{"text": "What is happening in this video?", "files": ["https://huggingface.co/datasets/merve/vlm_test_images/resolve/main/concert.mp4"]}],
499
- ]
500
-
501
- with gr.Blocks(theme=gr.themes.Default(primary_hue="blue")) as demo:
502
  gr.Markdown(
503
  """
504
- # 🎙️ Multi-Modal Studio & Speech-to-Text
505
- ### استودیو هوشمند تبدیل گفتار به متن (Whisper V3) + دستیار هوش مصنوعی (Gemma 4)
 
 
 
506
  """
507
  )
508
 
509
- with gr.Tabs():
510
- # -------- TAB 1: Speech to Text --------
511
- with gr.Tab("Speech-to-Text / استودیو تبدیل صدا"):
512
- with gr.Row():
513
- with gr.Column(scale=1):
514
- audio_input = gr.Audio(
515
- sources=["upload", "microphone"],
516
- type="filepath",
517
- label="Audio Input / ورودی صدا"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  )
519
-
520
- language_dd = gr.Dropdown(
521
- choices=list(LANGUAGE_MAPPING.keys()),
522
- value="Auto-Detect (تشخیص خودکار)",
523
- label="Select Language / انتخاب زبان صوتی"
 
524
  )
525
-
526
- optimize_fa_chk = gr.Checkbox(
527
- label="Optimize for Spoken Persian / بهینه‌سازی برای محاوره عامیانه فارسی",
528
- value=False
 
 
529
  )
530
-
531
- submit_btn = gr.Button("Transcribe & Correct / شروع تبدیل و اصلاح هوشمند", variant="primary")
532
-
533
- with gr.Column(scale=1):
534
- with gr.Tabs():
535
- with gr.Tab("Full Text / متن کامل"):
536
- text_output = gr.Textbox(
537
- label="Transcription Output (AI Corrected) / متن پیوسته (اصلاح شده)",
538
- lines=14,
539
- buttons=["copy"],
540
- interactive=False
541
- )
542
- with gr.Tab("SRT Subtitles / زیرنویس استاندارد"):
543
- srt_output = gr.Textbox(
544
- label="Subtitles (SRT Format) / فایل زیرنویس",
545
- lines=14,
546
- buttons=["copy"],
547
- interactive=False
548
- )
549
- with gr.Tab("Word Timestamps / زمان‌بندی کلمات (JSON)"):
550
- json_output = gr.Textbox(
551
- label="Word-Level Timestamps (JSON) / زمان خام کلمات",
552
- lines=14,
553
- buttons=["copy"],
554
- interactive=False
555
- )
556
 
557
- submit_btn.click(
558
- fn=transcribe_audio,
559
- inputs=[audio_input, language_dd, optimize_fa_chk],
560
- outputs=[text_output, srt_output, json_output],
561
- api_name="transcribe"
562
- )
563
-
564
- # -------- TAB 2: Chat Interface --------
565
- with gr.Tab("AI Chatbot / چت‌بات چندرسانه‌ای"):
566
- gr.ChatInterface(
567
- fn=generate,
568
- validator=validate_input,
569
- chatbot=gr.Chatbot(
570
- scale=1,
571
- latex_delimiters=[
572
- {"left": "$$", "right": "$$", "display": True},
573
- {"left": "$", "right": "$", "display": False},
574
- {"left": "\\(", "right": "\\)", "display": False},
575
- {"left": "\\[", "right": "\\]", "display": True},
576
- ],
577
- reasoning_tags=[(THINKING_START, THINKING_END)],
578
- ),
579
- textbox=gr.MultimodalTextbox(
580
- sources=["upload", "microphone"],
581
- file_types=[*IMAGE_FILE_TYPES, *AUDIO_FILE_TYPES, *VIDEO_FILE_TYPES],
582
- file_count="multiple",
583
- autofocus=True,
584
- stop_btn=True,
585
- ),
586
- multimodal=True,
587
- additional_inputs=[
588
- gr.Checkbox(label="Thinking", value=False),
589
- gr.Slider(label="Max New Tokens", minimum=100, maximum=4000, step=10, value=2000),
590
- gr.Dropdown(
591
- label="Image Token Budget",
592
- info="Higher values preserve more visual detail. Lower values are faster.",
593
- choices=[70, 140, 280, 560, 1120],
594
- value=280,
595
- ),
596
- gr.Textbox(label="System Prompt", value=""),
597
- ],
598
- additional_inputs_accordion=gr.Accordion("Settings", open=False),
599
- title="",
600
- examples=examples_chat,
601
- run_examples_on_click=False,
602
- cache_examples=False,
603
- delete_cache=(1800, 1800),
604
- )
605
 
606
  if __name__ == "__main__":
607
- demo.launch(max_file_size="20MB")
 
 
1
  import os
 
 
 
 
 
2
  import gradio as gr
 
3
  import torch
4
  import librosa
5
+ import json
6
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor, pipeline
7
+ import spaces
 
 
 
 
 
 
8
 
9
+ # انتخاب نسخه مرجع جهانی چندزبانه OpenAI Whisper V3
10
+ model_id = "openai/whisper-large-v3"
 
 
11
 
12
+ # ۱. انتخاب بهینه‌ترین فرمت عددی برای سخت‌افزار A100/H200
13
  if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
14
+ torch_dtype = torch.bfloat16
15
+ print("Using optimized BFloat16 for A100/H200")
16
  else:
17
+ torch_dtype = torch.float16
18
+ print("Using Float16")
19
 
20
+ # ۲. فعال‌سازی مکانیسم شتاب‌دهنده توجه (Attention)
21
  attn_implementation = "sdpa"
22
  try:
23
  import flash_attn
24
  attn_implementation = "flash_attention_2"
25
+ print("Using Flash Attention 2 for maximum hardware utilization")
26
  except ImportError:
27
+ print("Flash Attention 2 not found. Falling back to high-performance PyTorch SDPA")
28
 
29
+ # ۳. بارگذاری بهینه مدل چندزبانه با تکیه بر Safetensors و Memory optimization
30
+ model = AutoModelForSpeechSeq2Seq.from_pretrained(
31
+ model_id,
32
+ torch_dtype=torch_dtype,
33
  low_cpu_mem_usage=True,
34
  use_safetensors=True,
35
  attn_implementation=attn_implementation
36
  )
37
 
38
+ # انتقال مدل به حافظه گرافیکی در صورت در دسترس بودن
39
+ device = "cuda" if torch.cuda.is_available() else "cpu"
40
+ model.to(device)
41
 
42
+ processor = AutoProcessor.from_pretrained(model_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
+ # تعریف متن راهنمای پرامپت فارسی به صورت سراسری
45
+ prompt_text = "کلمات دقیقاً همان‌طور که تلفظ می‌شوند، با رعایت حروف اضافه و ساختار عامیانه، محاوره‌ای و شکسته نوشته شوند."
46
 
47
+ # ۴. ساخت پایپ‌لاین تشخیص گفتار چندزبانه با استفاده از پارامتر اصلاح شده‌ی dtype
48
+ pipe = pipeline(
49
+ "automatic-speech-recognition",
50
+ model=model,
51
+ tokenizer=processor.tokenizer,
52
+ feature_extractor=processor.feature_extractor,
53
+ dtype=torch_dtype,
54
+ device=device,
55
  )
56
 
57
+ # نقشه کدهای زبانی برای منوی کشویی انتخاب زبان
 
 
 
 
 
58
  LANGUAGE_MAPPING = {
59
  "Auto-Detect (تشخیص خودکار)": None,
60
  "Persian / فارسی": "fa",
 
71
  "Korean / کره‌ای": "ko"
72
  }
73
 
74
+ # تابع کمکی برای فرمت‌بندی زمان در ساختار استاندارد زیرنویس (HH:MM:SS,mmm)
75
  def format_timestamp(seconds):
76
  hours = int(seconds // 3600)
77
  minutes = int((seconds % 3600) // 60)
 
79
  millis = int(round((seconds % 1) * 1000))
80
  return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
81
 
82
+ # مبدل داده‌های کلمه به کلمه ویسپر به ساختار استاندارد خوانای SRT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  def create_srt_from_chunks(chunks, max_words_per_segment=6, max_duration_per_segment=3.0):
84
  srt_lines = []
85
  segment_idx = 1
 
97
  if start is None:
98
  continue
99
  if end is None:
100
+ end = start + 0.3 # مقدار جایگزین در صورت عدم ثبت زمان پایان توکن
101
 
102
  if current_start is None:
103
  current_start = start
 
105
  current_segment_words.append(text)
106
  duration = end - current_start
107
 
108
+ # دسته‌بندی کلمات در قالب خطوط زیرنویس خوانا (مثلا حداکثر ۶ کلمه یا ۳ ثانیه برای هر خط)
109
  if len(current_segment_words) >= max_words_per_segment or duration >= max_duration_per_segment:
110
  line_text = " ".join(current_segment_words)
111
  srt_lines.append(f"{segment_idx}")
 
126
 
127
  return "\n".join(srt_lines)
128
 
129
+ # تابع تولید دیتای خام زمان‌بندی کلمات (JSON) برای دسترسی توسعه‌دهندگان
130
  def get_raw_word_timestamps(chunks):
131
  word_timestamps = []
132
  for chunk in chunks:
 
140
  })
141
  return json.dumps(word_timestamps, ensure_ascii=False, indent=2)
142
 
143
+ @spaces.GPU
144
  def transcribe_audio(audio_path, language_selection, optimize_persian):
145
  if audio_path is None:
146
  return "Please upload or record an audio file first. / لطفاً ابتدا فایل صوتی را آپلود یا ضبط کنید.", "", ""
147
 
148
  try:
149
+ # ۱. بارگذاری ایمن فایل صوتی به فرکانس ۱۶۰۰۰ هرتز (سازگار با همه فرمت‌ها مانند میکروفون وب‌ام یا m4a)
150
  audio_data, sampling_rate = librosa.load(audio_path, sr=16000)
151
+
152
+ # ۲. پیکربندی پویا بر اساس زبان انتخاب شده توسط کاربر
153
  lang_code = LANGUAGE_MAPPING.get(language_selection, None)
154
 
155
+ # تنظیمات پایه تولید متن با استفاده از روش پیشرفته Beam Search برای افزایش حداکثری دقت تفکیک کلمات
156
  generate_kwargs = {
157
  "task": "transcribe",
158
  "do_sample": False,
159
+ "num_beams": 5 # ارزیابی موازی ۵ مسیر واژگانی برای کاهش خطاهای شنیداری صوتی
160
  }
161
 
162
+ # اگر کاربر زبانی به جز تشخیص خودکار انتخاب کرده باشد
163
  if lang_code is not None:
164
  generate_kwargs["language"] = lang_code
165
 
166
+ # ۳. اعمال پرامپت اختصاصی فارسی فقط در صورت تمایل کاربر (به عنوان تانسور ۱ بعدی پایتورچِ روی GPU)
167
  if optimize_persian:
168
+ prompt_ids = processor.get_prompt_ids(prompt_text, return_tensors="pt")
169
+ prompt_ids = prompt_ids.to(device)
170
  generate_kwargs["prompt_ids"] = prompt_ids
171
+ generate_kwargs["language"] = "fa" # اجبار به فارسی در صورت فعال بودن این گزینه
172
 
173
+ # ۴. پردازش نهایی با حداکثر ظرفیت سخت‌افزاری روی A100/H200 و درخواست ثبت تایم‌استمپ کلمات
174
+ result = pipe(
175
  {"raw": audio_data, "sampling_rate": sampling_rate},
176
  chunk_length_s=30,
177
  stride_length_s=(6, 6),
178
  batch_size=24,
179
+ return_timestamps="word", # فعال‌سازی ثبت دقیق زمان‌بندی برای هر کلمه
180
  generate_kwargs=generate_kwargs
181
  )
182
 
183
  full_text = result["text"].strip()
184
  chunks = result.get("chunks", [])
185
 
186
+ # ۵. تولید ساختارهای سه‌گانه دیتای خروجی
187
  srt_content = create_srt_from_chunks(chunks)
188
  word_timestamps_json = get_raw_word_timestamps(chunks)
189
 
190
+ # بازگرداندن هر سه خروجی در قالب تاپل
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  return full_text, srt_content, word_timestamps_json
192
 
193
  except Exception as e:
194
  return f"An error occurred during processing / خطایی در حین پردازش رخ داد: {str(e)}", "", ""
195
 
196
+ # رابط کاربری چندزبانه و مدرن بر پایه Gradio 6.0
197
+ with gr.Blocks() as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  gr.Markdown(
199
  """
200
+ # 🌐 Global Speech-to-Text & Subtitle Studio (Whisper Large V3)
201
+ ### سیستم هوشمند و بین‌المللی تبدیل گفتار به متن و زیرنویس خودکار هینه‌سازی شده روی A100/H200)
202
+
203
+ *Supports over 99 languages with automatic detection and Word-Level Timestamps.*
204
+ *پشتیبانی از بیش از ۹۹ زبان زنده دنیا به همراه تشخیص خودکار زبان و زمان‌بندی دقیق کلمه به کلمه.*
205
  """
206
  )
207
 
208
+ with gr.Row():
209
+ with gr.Column(scale=1):
210
+ audio_input = gr.Audio(
211
+ sources=["upload", "microphone"],
212
+ type="filepath",
213
+ label="Audio Input / ورودی صدا"
214
+ )
215
+
216
+ language_dd = gr.Dropdown(
217
+ choices=list(LANGUAGE_MAPPING.keys()),
218
+ value="Auto-Detect (تشخیص خودکار)",
219
+ label="Select Language / انتخاب زبان صوتی"
220
+ )
221
+
222
+ optimize_fa_chk = gr.Checkbox(
223
+ label="Optimize for Spoken Persian / بهینه‌سازی برای محاوره عامیانه فارسی (فقط برای ویس‌های فارسی)",
224
+ value=False
225
+ )
226
+
227
+ submit_btn = gr.Button("Transcribe / شروع فرآیند تبدیل", variant="primary")
228
+
229
+ with gr.Column(scale=1):
230
+ with gr.Tabs():
231
+ with gr.Tab("Full Text / متن کامل"):
232
+ text_output = gr.Textbox(
233
+ label="Transcription Output / متن پیوسته",
234
+ lines=14,
235
+ buttons=["copy"],
236
+ interactive=False
237
  )
238
+ with gr.Tab("SRT Subtitles / زیرنویس استاندارد"):
239
+ srt_output = gr.Textbox(
240
+ label="Subtitles (SRT Format) / فایل زیرنویس",
241
+ lines=14,
242
+ buttons=["copy"],
243
+ interactive=False
244
  )
245
+ with gr.Tab("Word Timestamps / زمان‌بندی کلمات (JSON)"):
246
+ json_output = gr.Textbox(
247
+ label="Word-Level Timestamps (JSON) / زمان خام کلمات",
248
+ lines=14,
249
+ buttons=["copy"],
250
+ interactive=False
251
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
+ submit_btn.click(
254
+ fn=transcribe_audio,
255
+ inputs=[audio_input, language_dd, optimize_fa_chk],
256
+ outputs=[text_output, srt_output, json_output],
257
+ api_name="transcribe" # نام اختصاصی متد برای فراخوانی API کلاینت
258
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
 
260
  if __name__ == "__main__":
261
+ # فعال‌سازی تم آبی‌رنگ در زمان راه‌اندازی بر اساس استانداردهای Gradio 6.0
262
+ demo.launch(theme=gr.themes.Default(primary_hue="blue"))