Opera8 commited on
Commit
f302f9a
·
verified ·
1 Parent(s): d3df3a6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -86
app.py CHANGED
@@ -5,7 +5,9 @@ import json
5
  import tempfile
6
  import re
7
  import shutil
8
- import base64
 
 
9
  from pydub import AudioSegment
10
  import yt_dlp
11
  from google import genai
@@ -16,6 +18,7 @@ from google.genai import types
16
  # ==========================================
17
 
18
  def download_youtube_video(url, output_path):
 
19
  ydl_opts = {
20
  'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
21
  'outtmpl': output_path,
@@ -27,6 +30,7 @@ def download_youtube_video(url, output_path):
27
  return output_path
28
 
29
  def extract_audio_from_video(video_path, audio_path):
 
30
  command = [
31
  'ffmpeg', '-i', video_path,
32
  '-vn', '-acodec', 'mp3', '-ar', '16000', '-ac', '1',
@@ -36,6 +40,7 @@ def extract_audio_from_video(video_path, audio_path):
36
  return audio_path
37
 
38
  def get_video_duration(video_path):
 
39
  result = subprocess.run([
40
  'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
41
  '-of', 'default=noprint_wrappers=1:nokey=1', video_path
@@ -43,6 +48,7 @@ def get_video_duration(video_path):
43
  return float(result.stdout.strip())
44
 
45
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
 
46
  try:
47
  audio = AudioSegment.from_file(input_wav)
48
  original_duration = len(audio) / 1000.0
@@ -83,7 +89,50 @@ def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
83
  shutil.copy(input_wav, output_wav)
84
 
85
  # ==========================================
86
- # هسته اصلی پردازش جیمینای
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # ==========================================
88
 
89
  def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.Progress()):
@@ -96,11 +145,7 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
96
  temp_dir = tempfile.mkdtemp()
97
 
98
  try:
99
- # 🔴 رفع خطای 404: استفاده اجباری از نسخه v1alpha برای پشتیبانی از مدل‌های Preview
100
- client = genai.Client(
101
- api_key=api_key,
102
- http_options={'api_version': 'v1alpha'}
103
- )
104
 
105
  video_path = os.path.join(temp_dir, "input_video.mp4")
106
  audio_path = os.path.join(temp_dir, "extracted_audio.mp3")
@@ -120,7 +165,7 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
120
  progress(0.1, desc="در حال استخراج صدای ویدیو...")
121
  extract_audio_from_video(video_path, audio_path)
122
 
123
- # 3. ارسال به Gemini برای استخراج و ترجمه
124
  progress(0.2, desc=f"در حال پردازش متن و ترجمه به {target_lang}...")
125
 
126
  gemini_audio_file = client.files.upload(file=audio_path)
@@ -151,16 +196,16 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
151
  if not subtitles:
152
  raise ValueError("لیست زیرنویس خالی است.")
153
  except Exception as e:
154
- raise gr.Error(f"خطا در تجزیه خروجی مدل (JSON نامعتبر). \nجزئیات: {str(e)}\nخروجی: {response_text[:100]}")
155
 
156
- # 4. تولید صدا با Gemini Native Audio برای هر سگمنت
157
  final_audio_track = AudioSegment.silent(duration=int(video_duration * 1000))
158
  total_subs = len(subtitles)
159
  successful_segments = 0
160
  errors_log = []
161
 
162
  for i, sub in enumerate(subtitles):
163
- progress(0.3 + (0.5 * (i / total_subs)), desc=f"تولید صدا و سینک بخش {i+1} از {total_subs}...")
164
 
165
  text = sub.get('text', '')
166
  start_time = float(sub.get('start', 0))
@@ -170,86 +215,30 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
170
  if not text or target_duration <= 0:
171
  continue
172
 
173
- tts_prompt = f"Speak the following text naturally, fluently, and with human-like emotion in {target_lang}. Text: {text}"
174
-
175
- audio_response = None
176
  try:
177
- # 🔴 ابتدا تلاش برای استفاده از مدل ویژه مدنظر شما
178
- audio_response = client.models.generate_content(
179
- model='gemini-2.5-flash-native-audio-preview-12-2025',
180
- contents=tts_prompt,
181
- config=types.GenerateContentConfig(
182
- response_modalities=["AUDIO"]
183
- )
184
- )
185
- except Exception as model_error:
186
- # 🔴 سیستم پشتیبان (Fallback): اگر مدل ویژه در دسترس نبود از مدل استاندارد فلش استفاده کن
187
- if "404" in str(model_error) or "NOT_FOUND" in str(model_error):
188
- try:
189
- audio_response = client.models.generate_content(
190
- model='gemini-2.5-flash',
191
- contents=tts_prompt,
192
- config=types.GenerateContentConfig(
193
- response_modalities=["AUDIO"]
194
- )
195
- )
196
- except Exception as fallback_error:
197
- errors_log.append(f"Segment {i+1} Fallback Exception: {str(fallback_error)}")
198
- continue
199
- else:
200
- errors_log.append(f"Segment {i+1} Exception: {str(model_error)}")
201
- continue
202
-
203
- if not audio_response:
204
- continue
205
-
206
- try:
207
- audio_bytes = None
208
- mime_type = None
209
 
210
- # خواندن بایت‌های خام صدا از پاسخ جیمینای
211
- if hasattr(audio_response, 'candidates') and audio_response.candidates:
212
- for part in audio_response.candidates[0].content.parts:
213
- if hasattr(part, 'inline_data') and part.inline_data:
214
- audio_bytes = part.inline_data.data
215
- mime_type = part.inline_data.mime_type
216
- break
217
-
218
- if audio_bytes:
219
- raw_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.bin")
220
- wav_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.wav")
221
- adjusted_audio_path = os.path.join(temp_dir, f"adj_tts_{i}.wav")
222
-
223
- if isinstance(audio_bytes, str):
224
- audio_bytes = base64.b64decode(audio_bytes)
225
-
226
- with open(raw_audio_path, "wb") as f:
227
- f.write(audio_bytes)
228
-
229
- # تبدیل دیتای خام به فایل WAV استاندارد
230
- if mime_type and ("pcm" in mime_type.lower() or "raw" in mime_type.lower()):
231
- subprocess.run(['ffmpeg', '-y', '-f', 's16le', '-ar', '24000', '-ac', '1', '-i', raw_audio_path, wav_audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
232
- else:
233
- subprocess.run(['ffmpeg', '-y', '-i', raw_audio_path, wav_audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
234
 
235
- # سینک کردن صدا (Lip-Sync)
236
- if os.path.exists(wav_audio_path):
237
- adjust_audio_speed_ffmpeg(wav_audio_path, adjusted_audio_path, target_duration)
238
- segment_audio = AudioSegment.from_file(adjusted_audio_path)
239
- position_ms = int(start_time * 1000)
240
- final_audio_track = final_audio_track.overlay(segment_audio, position=position_ms)
241
- successful_segments += 1
242
- else:
243
- errors_log.append(f"Segment {i+1}: Failed to create WAV file.")
244
  else:
245
- errors_log.append(f"Segment {i+1}: No audio data in response.")
246
 
247
  except Exception as e:
248
- errors_log.append(f"Segment {i+1} Processing Error: {str(e)}")
249
  continue
250
 
251
  if successful_segments == 0:
252
- raise gr.Error(f"شکست کامل در تولید صدا!\nلاگ خطاها:\n" + "\n".join(errors_log[:5]))
253
  elif errors_log:
254
  gr.Warning(f"فقط {successful_segments} بخش از {total_subs} بخش با موفقیت صداگذاری شد.")
255
 
@@ -279,16 +268,16 @@ def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.P
279
  # رابط کاربری (Gradio UI)
280
  # ==========================================
281
 
282
- with gr.Blocks(title="AI Native Dubbing Studio (Gemini 2.5)") as app:
283
  gr.Markdown("""
284
- # 🎙️ استودیو دوبله خودکار با موتور Gemini 2.5 Native
285
- این برنامه از مدل **Gemini 2.5 Flash** در محیط `v1alpha` گوگل برای تولید صدای طبیعی و همگام‌سازی استفاده می‌کند.
286
  """)
287
 
288
  with gr.Row():
289
  with gr.Column(scale=1):
290
  api_key_input = gr.Textbox(
291
- label="🔑 کلید API هوش مصنوعی (Google AI Studio Key)",
292
  placeholder="AIzaSy...",
293
  type="password"
294
  )
 
5
  import tempfile
6
  import re
7
  import shutil
8
+ import asyncio
9
+ import websockets
10
+ import requests
11
  from pydub import AudioSegment
12
  import yt_dlp
13
  from google import genai
 
18
  # ==========================================
19
 
20
  def download_youtube_video(url, output_path):
21
+ """دانلود ویدیو از یوتیوب"""
22
  ydl_opts = {
23
  'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
24
  'outtmpl': output_path,
 
30
  return output_path
31
 
32
  def extract_audio_from_video(video_path, audio_path):
33
+ """استخراج صدای اصلی از ویدیو"""
34
  command = [
35
  'ffmpeg', '-i', video_path,
36
  '-vn', '-acodec', 'mp3', '-ar', '16000', '-ac', '1',
 
40
  return audio_path
41
 
42
  def get_video_duration(video_path):
43
+ """دریافت زمان کل ویدیو"""
44
  result = subprocess.run([
45
  'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
46
  '-of', 'default=noprint_wrappers=1:nokey=1', video_path
 
48
  return float(result.stdout.strip())
49
 
50
  def adjust_audio_speed_ffmpeg(input_wav, output_wav, target_duration):
51
+ """تنظیم دقیق سرعت فایل صوتی (Lip-sync)"""
52
  try:
53
  audio = AudioSegment.from_file(input_wav)
54
  original_duration = len(audio) / 1000.0
 
89
  shutil.copy(input_wav, output_wav)
90
 
91
  # ==========================================
92
+ # ارتباط با اسپیس خارجی TTS (opera8-ttslive)
93
+ # ==========================================
94
+ def get_tts_from_external_space(text, output_path):
95
+ """
96
+ این تابع به وب‌سوکت اسپیس دوم شما متصل می‌شود،
97
+ متن را می‌فرستد و پس از آماده شدن، فایل wav را دانلود می‌کند.
98
+ """
99
+ async def _fetch():
100
+ uri = "wss://opera8-ttslive.hf.space/ws"
101
+ async with websockets.connect(uri, ping_interval=None) as websocket:
102
+ # ارسال متن به اسپیس TTS
103
+ await websocket.send(text)
104
+
105
+ while True:
106
+ response = await websocket.recv()
107
+ # ما فقط منتظر پیام متنی (JSON) هستیم. پیام‌های باینری (صدا) را نادیده می‌گیریم.
108
+ if isinstance(response, str):
109
+ try:
110
+ data = json.loads(response)
111
+ if data.get("event") == "STREAM_ENDED":
112
+ # فایل کامل شده است، حالا آن را با HTTP GET دانلود می‌کنیم
113
+ audio_url = "https://opera8-ttslive.hf.space" + data.get("url")
114
+ download_response = requests.get(audio_url)
115
+ download_response.raise_for_status()
116
+
117
+ with open(output_path, "wb") as f:
118
+ f.write(download_response.content)
119
+ return True
120
+
121
+ elif data.get("event") == "ERROR":
122
+ raise Exception(data.get("message"))
123
+ except json.JSONDecodeError:
124
+ pass # اگر JSON نبود رد شو
125
+
126
+ # اجرای تابع async در محیط sync برنامه Gradio
127
+ loop = asyncio.new_event_loop()
128
+ asyncio.set_event_loop(loop)
129
+ try:
130
+ return loop.run_until_complete(_fetch())
131
+ finally:
132
+ loop.close()
133
+
134
+ # ==========================================
135
+ # هسته اصلی پردازش دوبله
136
  # ==========================================
137
 
138
  def process_dubbing(api_key, video_file, youtube_url, target_lang, progress=gr.Progress()):
 
145
  temp_dir = tempfile.mkdtemp()
146
 
147
  try:
148
+ client = genai.Client(api_key=api_key)
 
 
 
 
149
 
150
  video_path = os.path.join(temp_dir, "input_video.mp4")
151
  audio_path = os.path.join(temp_dir, "extracted_audio.mp3")
 
165
  progress(0.1, desc="در حال استخراج صدای ویدیو...")
166
  extract_audio_from_video(video_path, audio_path)
167
 
168
+ # 3. استخراج متن و ترجمه با جمینای
169
  progress(0.2, desc=f"در حال پردازش متن و ترجمه به {target_lang}...")
170
 
171
  gemini_audio_file = client.files.upload(file=audio_path)
 
196
  if not subtitles:
197
  raise ValueError("لیست زیرنویس خالی است.")
198
  except Exception as e:
199
+ raise gr.Error(f"خطا در تجزیه خروجی مدل. \nجزئیات: {str(e)}\nخروجی خام:\n{response_text[:200]}")
200
 
201
+ # 4. تولید صدا با اتصال به اسپیس opera8-ttslive
202
  final_audio_track = AudioSegment.silent(duration=int(video_duration * 1000))
203
  total_subs = len(subtitles)
204
  successful_segments = 0
205
  errors_log = []
206
 
207
  for i, sub in enumerate(subtitles):
208
+ progress(0.3 + (0.5 * (i / total_subs)), desc=f"صداگذاری بخش {i+1}/{total_subs} (ارتباط با سرور TTS)...")
209
 
210
  text = sub.get('text', '')
211
  start_time = float(sub.get('start', 0))
 
215
  if not text or target_duration <= 0:
216
  continue
217
 
 
 
 
218
  try:
219
+ raw_audio_path = os.path.join(temp_dir, f"raw_tts_{i}.wav")
220
+ adjusted_audio_path = os.path.join(temp_dir, f"adj_tts_{i}.wav")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ # درخواست ساخت فایل صوتی از اسپیس دوم
223
+ success = get_tts_from_external_space(text, raw_audio_path)
224
+
225
+ if success and os.path.exists(raw_audio_path):
226
+ # فایل دانلود شده استاندارد است، مستقیما تنظیم سرعت می‌کنیم
227
+ adjust_audio_speed_ffmpeg(raw_audio_path, adjusted_audio_path, target_duration)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
+ segment_audio = AudioSegment.from_file(adjusted_audio_path)
230
+ position_ms = int(start_time * 1000)
231
+ final_audio_track = final_audio_track.overlay(segment_audio, position=position_ms)
232
+ successful_segments += 1
 
 
 
 
 
233
  else:
234
+ errors_log.append(f"Segment {i+1}: Failed to download from TTS Space.")
235
 
236
  except Exception as e:
237
+ errors_log.append(f"Segment {i+1} Exception: {str(e)}")
238
  continue
239
 
240
  if successful_segments == 0:
241
+ raise gr.Error(f"شکست در تولید صدا! ارتباط با سرور TTS ناموفق بود.\nلاگ خطاها:\n" + "\n".join(errors_log[:5]))
242
  elif errors_log:
243
  gr.Warning(f"فقط {successful_segments} بخش از {total_subs} بخش با موفقیت صداگذاری شد.")
244
 
 
268
  # رابط کاربری (Gradio UI)
269
  # ==========================================
270
 
271
+ with gr.Blocks(title="AI Native Dubbing Studio") as app:
272
  gr.Markdown("""
273
+ # 🎙️ استودیو دوبله خودکار با موتور Gemini 2.5
274
+ این برنامه ترجمه و زمان‌بندی را انجام داده و برای تولید صدای طبیعی، به صورت خودکار به سرور اختصاصی TTS شما متصل می‌شود.
275
  """)
276
 
277
  with gr.Row():
278
  with gr.Column(scale=1):
279
  api_key_input = gr.Textbox(
280
+ label="🔑 کلید API جمینای (فقط برای مرحله ترجمه و تشخیص متن)",
281
  placeholder="AIzaSy...",
282
  type="password"
283
  )