Opera8 commited on
Commit
5d44f84
·
verified ·
1 Parent(s): ada416e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -35
app.py CHANGED
@@ -15,7 +15,6 @@ from google.genai import types
15
  # ==========================================
16
 
17
  def download_youtube_video(url, output_path):
18
- """دانلود ویدیو از یوتیوب"""
19
  ydl_opts = {
20
  'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
21
  'outtmpl': output_path,
@@ -27,7 +26,6 @@ def download_youtube_video(url, output_path):
27
  return output_path
28
 
29
  def extract_audio_from_video(video_path, audio_path):
30
- """استخراج صدای اصلی از ویدیو برای ارسال به جیمینای"""
31
  command = [
32
  'ffmpeg', '-i', video_path,
33
  '-vn', '-acodec', 'mp3', '-ar', '16000', '-ac', '1',
@@ -37,7 +35,6 @@ def extract_audio_from_video(video_path, audio_path):
37
  return audio_path
38
 
39
  def get_video_duration(video_path):
40
- """دریافت زمان کل ویدیو"""
41
  result = subprocess.run([
42
  'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
43
  '-of', 'default=noprint_wrappers=1:nokey=1', video_path
@@ -45,7 +42,6 @@ def get_video_duration(video_path):
45
  return float(result.stdout.strip())
46
 
47
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
48
- """تنظیم دقیق سرعت فایل صوتی با استفاده از فیلتر atempo در ffmpeg برای سینک بودن با لب و دهان"""
49
  try:
50
  audio = AudioSegment.from_file(input_wav)
51
  original_duration = len(audio) / 1000.0
@@ -56,8 +52,6 @@ def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
56
 
57
  speed_factor = original_duration / target_duration
58
 
59
- # محدود کردن سرعت بین 0.5 تا 2.0 (محدودیت پیش‌فرض atempo)
60
- # اگر نیاز به سرعت بیشتر/کمتر باشد، فیلترها را زنجیره‌ای می‌کنیم
61
  atempo_filters = []
62
  current_factor = speed_factor
63
 
@@ -85,7 +79,6 @@ def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
85
  subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
86
  except Exception as e:
87
  print(f"Error in speed adjustment: {e}")
88
- # در صورت خطا، فایل اصلی را کپی کن تا پروسه متوقف نشود
89
  shutil.copy(input_wav, output_wav)
90
 
91
  # ==========================================
@@ -102,7 +95,6 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
102
  temp_dir = tempfile.mkdtemp()
103
 
104
  try:
105
- # مقداردهی کلاینت جدید جیمینای
106
  client = genai.Client(api_key=api_key)
107
 
108
  video_path = os.path.join(temp_dir, "input_video.mp4")
@@ -114,7 +106,6 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
114
  if youtube_url:
115
  download_youtube_video(youtube_url, video_path)
116
  else:
117
- # رفع خطای Gradio: دریافت مسیر فایل از String
118
  source_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
119
  shutil.copy(source_path, video_path)
120
 
@@ -124,11 +115,10 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
124
  progress(0.1, desc="در حال استخراج صدای ویدیو...")
125
  extract_audio_from_video(video_path, audio_path)
126
 
127
- # 3. ارسال به Gemini 2.5 Flash برای استخراج و ترجمه (تولید JSON)
128
- progress(0.2, desc=f"در حال پردازش هوش مصنوعی (تشخیص و ترجمه به {target_lang})...")
129
 
130
  gemini_audio_file = client.files.upload(file=audio_path)
131
-
132
  prompt = f"""
133
  Listen to the speech in this audio file.
134
  1. Transcribe the speech.
@@ -137,7 +127,6 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
137
  4. Each object must have exactly three keys: 'start' (float, start time in seconds), 'end' (float, end time in seconds), and 'text' (string, the translated text).
138
  """
139
 
140
- # استفاده از JSON Schema برای دریافت خروجی 100% تمیز و بدون خطا
141
  transcription_response = client.models.generate_content(
142
  model='gemini-2.5-flash',
143
  contents=[gemini_audio_file, prompt],
@@ -146,27 +135,27 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
146
  )
147
  )
148
 
149
- # پاک کردن فایل از سرور گوگل برای امنیت و فضای خالی
150
  try:
151
  client.files.delete(name=gemini_audio_file.name)
152
  except:
153
  pass
154
 
155
- # استخراج JSON از خروجی مدل
156
  response_text = transcription_response.text.strip()
157
-
158
  try:
159
  subtitles = json.loads(response_text)
160
- except json.JSONDecodeError:
161
- raise gr.Error(f"خطا در تجزیه خروجی مدل. لطفاً دوباره تلاش کنید.\nخروجی خام: {response_text[:100]}")
 
 
162
 
163
  # 4. تولید صدا با Gemini Native Audio برای هر سگمنت
164
- # ایجاد یک فایل صوتی سکوت (Canvas) به اندازه کل ویدیو
165
  final_audio_track = AudioSegment.silent(duration=int(video_duration * 1000))
166
-
167
  total_subs = len(subtitles)
 
 
 
168
  for i, sub in enumerate(subtitles):
169
- progress(0.3 + (0.5 * (i / total_subs)), desc=f"تولید صدا و هماهنگ‌سازی (سینک) بخش {i+1} از {total_subs}...")
170
 
171
  text = sub.get('text', '')
172
  start_time = float(sub.get('start', 0))
@@ -176,7 +165,6 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
176
  if not text or target_duration <= 0:
177
  continue
178
 
179
- # درخواست تولید صدا از Native Audio
180
  tts_prompt = f"Speak the following text naturally, fluently, and with human-like emotion in {target_lang}. Text: {text}"
181
 
182
  try:
@@ -188,38 +176,64 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
188
  )
189
  )
190
 
191
- # استخراج بایت‌های صدا از پاسخ
192
  audio_bytes = None
 
 
193
  for part in audio_response.candidates[0].content.parts:
194
  if part.inline_data:
195
  audio_bytes = part.inline_data.data
 
196
  break
197
 
198
  if audio_bytes:
199
- raw_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.wav")
 
200
  adjusted_audio_path = os.path.join(temp_dir, f"adj_tts_{i}.wav")
201
 
 
 
 
 
 
 
202
  with open(raw_audio_path, "wb") as f:
203
  f.write(audio_bytes)
204
 
205
- # تنظیم سرعت صدا برای هماهنگ شدن با لب و دهان (Lip-sync) بر اساس زمان start و end
206
- adjust_audio_speed_ffmpeg(raw_audio_path, adjusted_audio_path, target_duration)
 
 
 
 
 
207
 
208
- # قرار دادن صدای تنظیم شده در زمان مناسب روی Audio Canvas
209
- segment_audio = AudioSegment.from_file(adjusted_audio_path)
210
- position_ms = int(start_time * 1000)
211
- final_audio_track = final_audio_track.overlay(segment_audio, position=position_ms)
 
 
 
 
 
 
 
212
 
213
  except Exception as e:
214
- print(f"Error generating audio for segment {i}: {e}")
215
  continue
216
 
 
 
 
 
 
 
217
  # 5. ترکیب صدای نهایی با ویدیو
218
  progress(0.9, desc="در حال ترکیب صدا و تصویر (میکس نهایی)...")
219
  final_audio_path = os.path.join(temp_dir, "final_audio.wav")
220
  final_audio_track.export(final_audio_path, format="wav")
221
 
222
- # جایگذاری صدای جدید به جای صدای اصلی ویدیو
223
  merge_cmd = [
224
  'ffmpeg', '-y',
225
  '-i', video_path,
@@ -241,7 +255,6 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
241
  # رابط کاربری (Gradio UI)
242
  # ==========================================
243
 
244
- # رفع هشدار Gradio 6: حذف theme از اینجا
245
  with gr.Blocks(title="AI Native Dubbing Studio (Gemini 2.5)") as app:
246
  gr.Markdown("""
247
  # 🎙️ استودیو دوبله خودکار با موتور Gemini 2.5 Native
@@ -270,7 +283,7 @@ with gr.Blocks(title="AI Native Dubbing Studio (Gemini 2.5)") as app:
270
 
271
  with gr.Column(scale=1):
272
  output_video = gr.Video(label="🎬 ویدیوی نهایی دوبله شده (دارای Lip-sync)")
273
- output_logs = gr.Code(label="📜 زیرنویس و زمان‌بندی‌های اعمال شده (JSON)", language="json")
274
 
275
  run_btn.click(
276
  fn=process_dubbing,
@@ -279,5 +292,4 @@ with gr.Blocks(title="AI Native Dubbing Studio (Gemini 2.5)") as app:
279
  )
280
 
281
  if __name__ == "__main__":
282
- # رفع هشدار Gradio 6: انتقال theme به متد launch
283
  app.launch(theme=gr.themes.Soft(), ssr_mode=False)
 
15
  # ==========================================
16
 
17
  def download_youtube_video(url, output_path):
 
18
  ydl_opts = {
19
  'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
20
  'outtmpl': output_path,
 
26
  return output_path
27
 
28
  def extract_audio_from_video(video_path, audio_path):
 
29
  command = [
30
  'ffmpeg', '-i', video_path,
31
  '-vn', '-acodec', 'mp3', '-ar', '16000', '-ac', '1',
 
35
  return audio_path
36
 
37
  def get_video_duration(video_path):
 
38
  result = subprocess.run([
39
  'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
40
  '-of', 'default=noprint_wrappers=1:nokey=1', video_path
 
42
  return float(result.stdout.strip())
43
 
44
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
 
45
  try:
46
  audio = AudioSegment.from_file(input_wav)
47
  original_duration = len(audio) / 1000.0
 
52
 
53
  speed_factor = original_duration / target_duration
54
 
 
 
55
  atempo_filters = []
56
  current_factor = speed_factor
57
 
 
79
  subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
80
  except Exception as e:
81
  print(f"Error in speed adjustment: {e}")
 
82
  shutil.copy(input_wav, output_wav)
83
 
84
  # ==========================================
 
95
  temp_dir = tempfile.mkdtemp()
96
 
97
  try:
 
98
  client = genai.Client(api_key=api_key)
99
 
100
  video_path = os.path.join(temp_dir, "input_video.mp4")
 
106
  if youtube_url:
107
  download_youtube_video(youtube_url, video_path)
108
  else:
 
109
  source_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
110
  shutil.copy(source_path, video_path)
111
 
 
115
  progress(0.1, desc="در حال استخراج صدای ویدیو...")
116
  extract_audio_from_video(video_path, audio_path)
117
 
118
+ # 3. ارسال به Gemini برای استخراج و ترجمه
119
+ progress(0.2, desc=f"در حال پردازش متن و ترجمه به {target_lang}...")
120
 
121
  gemini_audio_file = client.files.upload(file=audio_path)
 
122
  prompt = f"""
123
  Listen to the speech in this audio file.
124
  1. Transcribe the speech.
 
127
  4. Each object must have exactly three keys: 'start' (float, start time in seconds), 'end' (float, end time in seconds), and 'text' (string, the translated text).
128
  """
129
 
 
130
  transcription_response = client.models.generate_content(
131
  model='gemini-2.5-flash',
132
  contents=[gemini_audio_file, prompt],
 
135
  )
136
  )
137
 
 
138
  try:
139
  client.files.delete(name=gemini_audio_file.name)
140
  except:
141
  pass
142
 
 
143
  response_text = transcription_response.text.strip()
 
144
  try:
145
  subtitles = json.loads(response_text)
146
+ if not subtitles:
147
+ raise ValueError("لیست زیرنویس خالی است.")
148
+ except Exception as e:
149
+ raise gr.Error(f"خطا در تجزیه خروجی مدل (JSON نامعتبر). \nجزئیات: {str(e)}\nخروجی: {response_text[:100]}")
150
 
151
  # 4. تولید صدا با Gemini Native Audio برای هر سگمنت
 
152
  final_audio_track = AudioSegment.silent(duration=int(video_duration * 1000))
 
153
  total_subs = len(subtitles)
154
+ successful_segments = 0
155
+ errors_log = []
156
+
157
  for i, sub in enumerate(subtitles):
158
+ progress(0.3 + (0.5 * (i / total_subs)), desc=f"تولید صدا و سینک بخش {i+1} از {total_subs}...")
159
 
160
  text = sub.get('text', '')
161
  start_time = float(sub.get('start', 0))
 
165
  if not text or target_duration <= 0:
166
  continue
167
 
 
168
  tts_prompt = f"Speak the following text naturally, fluently, and with human-like emotion in {target_lang}. Text: {text}"
169
 
170
  try:
 
176
  )
177
  )
178
 
 
179
  audio_bytes = None
180
+ mime_type = None
181
+
182
  for part in audio_response.candidates[0].content.parts:
183
  if part.inline_data:
184
  audio_bytes = part.inline_data.data
185
+ mime_type = part.inline_data.mime_type
186
  break
187
 
188
  if audio_bytes:
189
+ raw_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.bin")
190
+ wav_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.wav")
191
  adjusted_audio_path = os.path.join(temp_dir, f"adj_tts_{i}.wav")
192
 
193
+ # اگر داده‌ها رشته (Base64) بودند دیکد کن
194
+ if isinstance(audio_bytes, str):
195
+ import base64
196
+ audio_bytes = base64.b64decode(audio_bytes)
197
+
198
+ # ذخیره بایت‌های خام
199
  with open(raw_audio_path, "wb") as f:
200
  f.write(audio_bytes)
201
 
202
+ # 🔴 بخش کلیدی برای حل مشکل بی‌صدا بودن 🔴
203
+ # جمینای معمولا PCM برمی‌گرداند. باید به WAV استاندارد تبدیل شود تا قابل خواندن باشد
204
+ if mime_type and ("pcm" in mime_type.lower() or "raw" in mime_type.lower()):
205
+ subprocess.run(['ffmpeg', '-y', '-f', 's16le', '-ar', '24000', '-ac', '1', '-i', raw_audio_path, wav_audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
206
+ else:
207
+ # تلاش برای تبدیل مستقیم در صورت فرمت‌های دیگر
208
+ subprocess.run(['ffmpeg', '-y', '-i', raw_audio_path, wav_audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
209
 
210
+ # حالا فایل WAV استاندارد را به تابع تنظیم سرعت می‌دهیم
211
+ if os.path.exists(wav_audio_path):
212
+ adjust_audio_speed_ffmpeg(wav_audio_path, adjusted_audio_path, target_duration)
213
+ segment_audio = AudioSegment.from_file(adjusted_audio_path)
214
+ position_ms = int(start_time * 1000)
215
+ final_audio_track = final_audio_track.overlay(segment_audio, position=position_ms)
216
+ successful_segments += 1
217
+ else:
218
+ errors_log.append(f"Segment {i+1}: Failed to create WAV file from raw bytes.")
219
+ else:
220
+ errors_log.append(f"Segment {i+1}: Model returned empty audio bytes.")
221
 
222
  except Exception as e:
223
+ errors_log.append(f"Segment {i+1} Exception: {str(e)}")
224
  continue
225
 
226
+ # بررسی اینکه آیا اصلاً صدایی تولید شد یا خیر
227
+ if successful_segments == 0:
228
+ raise gr.Error(f"شکست کامل در تولید صدا! هوش مصنوعی هیچ صدایی برنگرداند.\nلاگ خطاها:\n" + "\n".join(errors_log[:5]))
229
+ elif errors_log:
230
+ gr.Warning(f"فقط {successful_segments} بخش از {total_subs} بخش با موفقیت صداگذاری شد. برخی خطا داشتند.")
231
+
232
  # 5. ترکیب صدای نهایی با ویدیو
233
  progress(0.9, desc="در حال ترکیب صدا و تصویر (میکس نهایی)...")
234
  final_audio_path = os.path.join(temp_dir, "final_audio.wav")
235
  final_audio_track.export(final_audio_path, format="wav")
236
 
 
237
  merge_cmd = [
238
  'ffmpeg', '-y',
239
  '-i', video_path,
 
255
  # رابط کاربری (Gradio UI)
256
  # ==========================================
257
 
 
258
  with gr.Blocks(title="AI Native Dubbing Studio (Gemini 2.5)") as app:
259
  gr.Markdown("""
260
  # 🎙️ استودیو دوبله خودکار با موتور Gemini 2.5 Native
 
283
 
284
  with gr.Column(scale=1):
285
  output_video = gr.Video(label="🎬 ویدیوی نهایی دوبله شده (دارای Lip-sync)")
286
+ output_logs = gr.Code(label="📜 لاگ عملیات و زمان‌بندی (JSON)", language="json")
287
 
288
  run_btn.click(
289
  fn=process_dubbing,
 
292
  )
293
 
294
  if __name__ == "__main__":
 
295
  app.launch(theme=gr.themes.Soft(), ssr_mode=False)