Opera8 commited on
Commit
74f3ef8
·
verified ·
1 Parent(s): b46aadf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -70
app.py CHANGED
@@ -6,6 +6,7 @@ import tempfile
6
  import shutil
7
  import asyncio
8
  import requests
 
9
  from pydub import AudioSegment
10
  import yt_dlp
11
  from google import genai
@@ -61,47 +62,53 @@ def download_youtube_video(url, output_path):
61
  return output_path
62
 
63
  def extract_audio_from_video(video_path, audio_path):
64
- subprocess.run(['ffmpeg', '-i', video_path, '-vn', '-acodec', 'mp3', '-ar', '24000', '-ac', '1', '-b:a', '128k', '-y', audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
 
65
  return audio_path
66
 
67
  def get_video_duration(video_path):
68
  result = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path], capture_output=True, text=True, check=True)
69
  return float(result.stdout.strip())
70
 
71
- def remove_silence_from_edges(audio_segment, silence_thresh=-45.0, chunk_size=10):
72
- if len(audio_segment) < 100: return audio_segment
73
- trim_ms = 0
74
- while trim_ms < len(audio_segment) and audio_segment[trim_ms:trim_ms+chunk_size].dBFS < silence_thresh: trim_ms += chunk_size
75
- start_trim = trim_ms
76
- trim_ms = 0
77
- while trim_ms < len(audio_segment) and audio_segment[len(audio_segment)-trim_ms-chunk_size:len(audio_segment)-trim_ms].dBFS < silence_thresh: trim_ms += chunk_size
78
- end_trim = len(audio_segment) - trim_ms
79
- return audio_segment[start_trim:end_trim]
80
-
81
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
82
  try:
83
  audio = AudioSegment.from_file(input_wav)
84
- trimmed_audio = remove_silence_from_edges(audio)
85
- if len(trimmed_audio) < 50: trimmed_audio = audio
86
- temp_trimmed = input_wav.replace(".wav", "_trimmed.wav")
87
- trimmed_audio.export(temp_trimmed, format="wav")
 
 
 
 
 
 
 
 
 
 
 
88
 
89
- orig_dur = len(trimmed_audio) / 1000.0
 
 
 
90
  if orig_dur <= 0.1 or target_duration <= 0.1:
91
  shutil.copy(temp_trimmed, output_wav); return
92
-
93
  speed_factor = orig_dur / target_duration
 
94
  if speed_factor > 2.0: speed_factor = 2.0
95
  if speed_factor < 0.6: speed_factor = 0.6
96
-
97
  atempo_filters = []
98
  current = speed_factor
99
  while current > 2.0: atempo_filters.append("atempo=2.0"); current /= 2.0
100
  while current < 0.5: atempo_filters.append("atempo=0.5"); current /= 0.5
101
  if current != 1.0: atempo_filters.append(f"atempo={current}")
102
-
103
  if not atempo_filters: shutil.copy(temp_trimmed, output_wav); return
104
-
105
  subprocess.run(['ffmpeg', '-y', '-i', temp_trimmed, '-filter:a', ",".join(atempo_filters), output_wav], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
106
  except: shutil.copy(input_wav, output_wav)
107
 
@@ -132,7 +139,7 @@ async def generate_audio_via_podcast_api(text, speaker_name, output_path):
132
 
133
  return await asyncio.to_thread(_sync_request)
134
 
135
- # پردازش تک‌تک قطعات زیرنویس جهت اجرای همزمان (Concurrency)
136
  async def process_single_subtitle(i, sub, temp_dir, sem):
137
  async with sem:
138
  assigned_speaker = sub.get('speaker_id', 'Charon')
@@ -164,7 +171,7 @@ async def process_single_subtitle(i, sub, temp_dir, sem):
164
  # ==========================================
165
  # هسته اصلی: کارگردان هوشمند
166
  # ==========================================
167
- async def process_dubbing(api_key, video_file, youtube_url, target_lang, keep_original_audio, original_audio_volume, progress=gr.Progress()):
168
  if not api_key: raise gr.Error("کلید API جمینای الزامی است.")
169
  if not video_file and not youtube_url: raise gr.Error("ویدیو الزامی است.")
170
 
@@ -172,90 +179,122 @@ async def process_dubbing(api_key, video_file, youtube_url, target_lang, keep_or
172
 
173
  try:
174
  client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key)
175
-
176
  video_path = os.path.join(temp_dir, "input.mp4")
177
  audio_path = os.path.join(temp_dir, "source.mp3")
178
  final_path = os.path.join(temp_dir, "output.mp4")
179
-
180
  progress(0.05, desc="دریافت ویدیو...")
181
- if youtube_url:
182
- download_youtube_video(youtube_url, video_path)
183
  else:
184
  s_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
185
  shutil.copy(s_path, video_path)
186
-
187
  duration = get_video_duration(video_path)
188
-
189
  progress(0.1, desc="استخراج صدا...")
190
  extract_audio_from_video(video_path, audio_path)
191
-
192
- progress(0.2, desc="تحلیل ویدیو، تشخیص گویندگان و انتخاب دوبلور مناسب...")
 
193
  gemini_file = client.files.upload(file=audio_path)
194
-
195
  prompt = f"""
196
  ROLE: You are an expert Dubbing Director using AI Voice Actors.
 
197
  {CAST_PROMPT}
 
198
  TASK:
199
- 1. Listen to the audio and identify different speakers (Male/Female, Tone, Emotion).
200
- 2. For EACH sentence, select the BEST MATCH from the 'AVAILABLE VOICE ACTORS' list above.
201
  3. Translate the text EXACTLY to {target_lang}.
202
  4. Output JSON Array.
 
203
  JSON FORMAT:
204
  [
205
- {{"start": 0.0, "end": 3.5, "speaker_id": "Charon", "original_speaker_desc": "Male, Deep voice", "text": "ترجمه دقیق متن به زبان مقصد"}},
206
- {{"start": 3.6, "end": 6.0, "speaker_id": "Zephyr", "original_speaker_desc": "Female, Soft voice", "text": "پاسخ زن در ویدیو"}}
 
 
 
 
 
207
  ]
208
  """
 
209
  transcription = client.models.generate_content(
210
  model='gemini-2.5-flash',
211
  contents=[gemini_file, prompt],
212
  config=types.GenerateContentConfig(response_mime_type="application/json")
213
  )
 
214
  try: client.files.delete(name=gemini_file.name)
215
  except: pass
216
-
217
  json_clean = transcription.text.strip().replace("```json", "").replace("```", "")
218
  subtitles = json.loads(json_clean)
219
-
220
  if not subtitles: raise ValueError("زیرنویس خالی است")
221
 
222
- # --- بخش جدید: ایجاد صدای پس‌زمینه بر اساس انتخاب کاربر ---
223
- if keep_original_audio:
224
- progress(0.25, desc="آماده‌سازی صدای پس‌زمینه...")
225
- background_audio = AudioSegment.from_file(audio_path)
226
- # تبدیل مقیاس 0-1 به دسی‌بل (dB) برای کاهش صدا
227
- # کاهش 50 دسی‌بلی تقریباً معادل سکوت است.
228
- volume_reduction_db = (1.0 - original_audio_volume) * 50
229
- final_track = background_audio - volume_reduction_db
230
- else:
231
- # ایجاد یک فایل صوتی کاملاً ساکت با طول ویدیو
232
- final_track = AudioSegment.silent(duration=int(duration * 1000))
233
-
234
  total = len(subtitles)
235
  ok_cnt = 0
236
 
237
  sem = asyncio.Semaphore(20)
238
  tasks = [process_single_subtitle(i, sub, temp_dir, sem) for i, sub in enumerate(subtitles)]
 
239
  completed = 0
240
  for coro in asyncio.as_completed(tasks):
241
  res = await coro
242
  completed += 1
243
- progress(0.3 + (0.6 * (completed / total)), desc=f"تولید صداها ({completed} از {total})...")
244
-
245
  if res is not None:
246
  seg = AudioSegment.from_file(res["adj_p"])
247
- final_track = final_track.overlay(seg, position=int(res["start"] * 1000))
 
248
  ok_cnt += 1
249
 
250
  if ok_cnt == 0: raise gr.Error("خطا: صدایی تولید نشد.")
251
 
252
- progress(0.95, desc="میکس نهایی...")
253
- final_audio_p = os.path.join(temp_dir, "final_mix.wav")
254
- final_track.export(final_audio_p, format="wav")
 
 
 
 
 
 
 
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  cmd = ['ffmpeg', '-y', '-i', video_path, '-i', final_audio_p, '-c:v', 'copy', '-c:a', 'aac', '-map', '0:v:0', '-map', '1:a:0', final_path]
257
  subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
258
-
259
  return final_path, json.dumps(subtitles, ensure_ascii=False, indent=2)
260
 
261
  except Exception as e:
@@ -267,31 +306,28 @@ async def process_dubbing(api_key, video_file, youtube_url, target_lang, keep_or
267
  with gr.Blocks(title="AI Smart Director Dubbing", theme=gr.themes.Soft()) as app:
268
  gr.Markdown("""
269
  # 🎬 استودیو دوبله هوشمند (AI Director)
270
- **قابلیت ویژه:** تشخیص خودکار گوینده‌های ویدیو (زن/مرد) و انتخاب بهترین صدا از بین ۳۰ گوینده حرفه‌ای توسط هوش مصنوعی.
271
  """)
272
-
273
  with gr.Row():
274
  with gr.Column():
275
- api_key = gr.Textbox(label="کلید API جمینای", type="password")
276
  vid = gr.Video(label="فایل ویدیو")
277
  url = gr.Textbox(label="لینک یوتیوب")
278
  lang = gr.Dropdown(["Persian (فارسی)", "English", "Arabic"], value="Persian (فارسی)", label="زبان مقصد")
279
 
280
- with gr.Accordion("تنظیمات پیشرفته صدا", open=False):
281
- keep_audio_checkbox = gr.Checkbox(label="حفظ صدای پس‌زمینه (موسیقی و افکت‌ها)", value=True)
282
- audio_volume_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.1, label="میزان بلندی صدای پس‌زمینه", info="اگر گزینه بالا غیرفعال باشد، این اسلایدر تاثیری ندارد.")
283
-
 
284
  btn = gr.Button("🚀 شروع دوبله هوشمند", variant="primary")
285
-
286
  with gr.Column():
287
  out_vid = gr.Video(label="ویدیو خروجی")
288
- out_log = gr.Code(label="گزارش کستینگ (چه کسی چه گفت؟)", language="json")
289
 
290
- btn.click(
291
- process_dubbing,
292
- [api_key, vid, url, lang, keep_audio_checkbox, audio_volume_slider],
293
- [out_vid, out_log]
294
- )
295
 
296
  if __name__ == "__main__":
297
  app.launch(ssr_mode=False)
 
6
  import shutil
7
  import asyncio
8
  import requests
9
+ import math
10
  from pydub import AudioSegment
11
  import yt_dlp
12
  from google import genai
 
62
  return output_path
63
 
64
  def extract_audio_from_video(video_path, audio_path):
65
+ # استخراج صدا با فرمت mp3 برای پردازش
66
+ subprocess.run(['ffmpeg', '-i', video_path, '-vn', '-acodec', 'mp3', '-ar', '44100', '-ac', '2', '-b:a', '192k', '-y', audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
67
  return audio_path
68
 
69
  def get_video_duration(video_path):
70
  result = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', video_path], capture_output=True, text=True, check=True)
71
  return float(result.stdout.strip())
72
 
 
 
 
 
 
 
 
 
 
 
73
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
74
  try:
75
  audio = AudioSegment.from_file(input_wav)
76
+ # حذف سکوت اضافه
77
+ if len(audio) > 100:
78
+ def trim(snd):
79
+ start_trim = 0
80
+ end_trim = 0
81
+ silence_thresh = -45.0
82
+ chunk = 10
83
+ while start_trim < len(snd) and snd[start_trim:start_trim+chunk].dBFS < silence_thresh: start_trim += chunk
84
+ while end_trim < len(snd) and snd[len(snd)-end_trim-chunk:len(snd)-end_trim].dBFS < silence_thresh: end_trim += chunk
85
+ return snd[start_trim:len(snd)-end_trim]
86
+ audio = trim(audio)
87
+
88
+ if len(audio) < 50:
89
+ shutil.copy(input_wav, output_wav)
90
+ return
91
 
92
+ temp_trimmed = input_wav.replace(".wav", "_trimmed.wav")
93
+ audio.export(temp_trimmed, format="wav")
94
+
95
+ orig_dur = len(audio) / 1000.0
96
  if orig_dur <= 0.1 or target_duration <= 0.1:
97
  shutil.copy(temp_trimmed, output_wav); return
98
+
99
  speed_factor = orig_dur / target_duration
100
+ # محدودیت سرعت برای جلوگیری از رباتی شدن بیش از حد
101
  if speed_factor > 2.0: speed_factor = 2.0
102
  if speed_factor < 0.6: speed_factor = 0.6
103
+
104
  atempo_filters = []
105
  current = speed_factor
106
  while current > 2.0: atempo_filters.append("atempo=2.0"); current /= 2.0
107
  while current < 0.5: atempo_filters.append("atempo=0.5"); current /= 0.5
108
  if current != 1.0: atempo_filters.append(f"atempo={current}")
109
+
110
  if not atempo_filters: shutil.copy(temp_trimmed, output_wav); return
111
+
112
  subprocess.run(['ffmpeg', '-y', '-i', temp_trimmed, '-filter:a', ",".join(atempo_filters), output_wav], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
113
  except: shutil.copy(input_wav, output_wav)
114
 
 
139
 
140
  return await asyncio.to_thread(_sync_request)
141
 
142
+ # پردازش تک‌تک قطعات زیرنویس
143
  async def process_single_subtitle(i, sub, temp_dir, sem):
144
  async with sem:
145
  assigned_speaker = sub.get('speaker_id', 'Charon')
 
171
  # ==========================================
172
  # هسته اصلی: کارگردان هوشمند
173
  # ==========================================
174
+ async def process_dubbing(api_key, video_file, youtube_url, target_lang, keep_bg_audio, bg_audio_volume, progress=gr.Progress()):
175
  if not api_key: raise gr.Error("کلید API جمینای الزامی است.")
176
  if not video_file and not youtube_url: raise gr.Error("ویدیو الزامی است.")
177
 
 
179
 
180
  try:
181
  client = genai.Client(http_options={"api_version": "v1beta"}, api_key=api_key)
182
+
183
  video_path = os.path.join(temp_dir, "input.mp4")
184
  audio_path = os.path.join(temp_dir, "source.mp3")
185
  final_path = os.path.join(temp_dir, "output.mp4")
186
+
187
  progress(0.05, desc="دریافت ویدیو...")
188
+ if youtube_url: download_youtube_video(youtube_url, video_path)
 
189
  else:
190
  s_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
191
  shutil.copy(s_path, video_path)
192
+
193
  duration = get_video_duration(video_path)
194
+
195
  progress(0.1, desc="استخراج صدا...")
196
  extract_audio_from_video(video_path, audio_path)
197
+
198
+ # --- تحلیل هوش مصنوعی ---
199
+ progress(0.2, desc="تحلیل ویدیو و کستینگ...")
200
  gemini_file = client.files.upload(file=audio_path)
201
+
202
  prompt = f"""
203
  ROLE: You are an expert Dubbing Director using AI Voice Actors.
204
+
205
  {CAST_PROMPT}
206
+
207
  TASK:
208
+ 1. Listen to the audio and identify different speakers.
209
+ 2. Assign the BEST MATCH from 'AVAILABLE VOICE ACTORS'.
210
  3. Translate the text EXACTLY to {target_lang}.
211
  4. Output JSON Array.
212
+
213
  JSON FORMAT:
214
  [
215
+ {{
216
+ "start": 0.0,
217
+ "end": 3.5,
218
+ "speaker_id": "Charon",
219
+ "original_speaker_desc": "Male, Deep voice",
220
+ "text": "ترجمه متن"
221
+ }}
222
  ]
223
  """
224
+
225
  transcription = client.models.generate_content(
226
  model='gemini-2.5-flash',
227
  contents=[gemini_file, prompt],
228
  config=types.GenerateContentConfig(response_mime_type="application/json")
229
  )
230
+
231
  try: client.files.delete(name=gemini_file.name)
232
  except: pass
233
+
234
  json_clean = transcription.text.strip().replace("```json", "").replace("```", "")
235
  subtitles = json.loads(json_clean)
236
+
237
  if not subtitles: raise ValueError("زیرنویس خالی است")
238
 
239
+ # --- تولید صدا ---
240
+ # ایجاد ترک خالی برای صداهای دوبله (Voices Only)
241
+ voice_track = AudioSegment.silent(duration=int(duration * 1000))
 
 
 
 
 
 
 
 
 
242
  total = len(subtitles)
243
  ok_cnt = 0
244
 
245
  sem = asyncio.Semaphore(20)
246
  tasks = [process_single_subtitle(i, sub, temp_dir, sem) for i, sub in enumerate(subtitles)]
247
+
248
  completed = 0
249
  for coro in asyncio.as_completed(tasks):
250
  res = await coro
251
  completed += 1
252
+ progress(0.3 + (0.5 * (completed / total)), desc=f"تولید صداها ({completed} از {total})...")
253
+
254
  if res is not None:
255
  seg = AudioSegment.from_file(res["adj_p"])
256
+ # قرار دادن صدای دوبله روی ترک خالی
257
+ voice_track = voice_track.overlay(seg, position=int(res["start"] * 1000))
258
  ok_cnt += 1
259
 
260
  if ok_cnt == 0: raise gr.Error("خطا: صدایی تولید نشد.")
261
 
262
+ # --- میکس صدای پس‌زمینه (Duck / Mix) ---
263
+ progress(0.90, desc="میکس صدای پس‌زمینه...")
264
+
265
+ original_audio = AudioSegment.from_file(audio_path)
266
+
267
+ # اطمینان از هم‌اندازه بودن ترک‌ها (گاهی ffmpeg دقیق نیست)
268
+ if len(voice_track) > len(original_audio):
269
+ original_audio = original_audio + AudioSegment.silent(duration=len(voice_track)-len(original_audio))
270
+ else:
271
+ original_audio = original_audio[:len(voice_track)]
272
 
273
+ if keep_bg_audio:
274
+ if bg_audio_volume <= 0.001:
275
+ # اگر ولوم صفر بود، سکوت مطلق
276
+ bg_music = AudioSegment.silent(duration=len(original_audio))
277
+ else:
278
+ # تبدیل درصد ولوم (0 تا 1) به دسی‌بل
279
+ # فرمول: 20 * log10(ratio)
280
+ # مثال: 0.1 (10%) حدودا -20dB می‌شود
281
+ gain_db = 20 * math.log10(max(bg_audio_volume, 0.0001))
282
+ bg_music = original_audio + gain_db
283
+
284
+ # ترکیب صدای دوبله روی صدای پس‌زمینه
285
+ final_mix = bg_music.overlay(voice_track)
286
+ else:
287
+ # فقط صدای دوبله
288
+ final_mix = voice_track
289
+
290
+ final_audio_p = os.path.join(temp_dir, "final_mix.wav")
291
+ final_mix.export(final_audio_p, format="wav")
292
+
293
+ # --- چسباندن صدا به تصویر ---
294
+ progress(0.95, desc="رندر نهایی ویدیو...")
295
  cmd = ['ffmpeg', '-y', '-i', video_path, '-i', final_audio_p, '-c:v', 'copy', '-c:a', 'aac', '-map', '0:v:0', '-map', '1:a:0', final_path]
296
  subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
297
+
298
  return final_path, json.dumps(subtitles, ensure_ascii=False, indent=2)
299
 
300
  except Exception as e:
 
306
  with gr.Blocks(title="AI Smart Director Dubbing", theme=gr.themes.Soft()) as app:
307
  gr.Markdown("""
308
  # 🎬 استودیو دوبله هوشمند (AI Director)
309
+ **قابلیتها:** کستینگ خودکار گوینده‌ها، تنظیم صدای پس‌زمینه و سرعت بالا با پردازش موازی.
310
  """)
311
+
312
  with gr.Row():
313
  with gr.Column():
314
+ api_key = gr.Textbox(label="کلید API جمینای (Gemini API Key)", type="password")
315
  vid = gr.Video(label="فایل ویدیو")
316
  url = gr.Textbox(label="لینک یوتیوب")
317
  lang = gr.Dropdown(["Persian (فارسی)", "English", "Arabic"], value="Persian (فارسی)", label="زبان مقصد")
318
 
319
+ # --- تنظیمات جدید صدا ---
320
+ gr.Markdown("### 🎚️ تنظیمات صدا")
321
+ keep_bg = gr.Checkbox(label="حفظ صدای پس‌زمینه (Original Audio)", value=True)
322
+ bg_vol = gr.Slider(minimum=0.0, maximum=1.0, value=0.15, step=0.01, label="میزان صدای پس‌زمینه (0 = سکوت، 1 = صدای اصلی)")
323
+
324
  btn = gr.Button("🚀 شروع دوبله هوشمند", variant="primary")
325
+
326
  with gr.Column():
327
  out_vid = gr.Video(label="ویدیو خروجی")
328
+ out_log = gr.Code(label="گزارش کستینگ (JSON)", language="json")
329
 
330
+ btn.click(process_dubbing, [api_key, vid, url, lang, keep_bg, bg_vol], [out_vid, out_log])
 
 
 
 
331
 
332
  if __name__ == "__main__":
333
  app.launch(ssr_mode=False)