Ricky01anjay commited on
Commit
355e25c
·
verified ·
1 Parent(s): 79b8c9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +83 -17
app.py CHANGED
@@ -7,6 +7,7 @@ import json
7
  import time
8
  import subprocess
9
  import logging
 
10
  from flask import Flask, request, jsonify, render_template_string, send_from_directory
11
  import whisper
12
  import edge_tts
@@ -23,10 +24,18 @@ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
23
 
24
  tasks = {}
25
 
 
26
  VOICE_MAP = {
27
- 'id-ID': 'id-ID-ArdiNeural',
28
- 'en-US': 'en-US-ChristopherNeural',
29
- 'ja-JP': 'ja-JP-KeitaNeural'
 
 
 
 
 
 
 
30
  }
31
 
32
  # Load Whisper (CPU Friendly, FP16 Fixed)
@@ -38,10 +47,54 @@ def get_audio_duration(file_path):
38
  '-of', 'default=noprint_wrappers=1:nokey=1', file_path
39
  ]
40
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
41
- return float(result.stdout)
 
 
 
42
 
43
- def translate_segments_llm(segments, custom_prompt):
44
- instruction = custom_prompt if custom_prompt else "Terjemahkan teks dalam JSON ini. Balas HANYA JSON array."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  input_data = [{"id": i, "text": s['text']} for i, s in enumerate(segments)]
46
  full_prompt = f"{instruction}\n\nFormat: [{{'id': 0, 'text': '...'}}]\n\nData:\n{json.dumps(input_data)}"
47
 
@@ -63,12 +116,14 @@ def translate_segments_llm(segments, custom_prompt):
63
  translated_list = json.loads(full_text[start_idx:end_idx])
64
  for item in translated_list:
65
  segments[item['id']]['translated_text'] = item['text']
66
- except:
 
67
  for s in segments: s['translated_text'] = s['text']
68
  return segments
69
 
70
- async def generate_tts(text, voice, path):
71
- communicate = edge_tts.Communicate(text, voice)
 
72
  await communicate.save(path)
73
 
74
  def process_dubbing(task_id, video_path, target_voice, custom_prompt):
@@ -81,10 +136,13 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
81
  result = whisper_model.transcribe(orig_audio, verbose=False, fp16=False)
82
  segments = result['segments']
83
 
84
- tasks[task_id]['status'] = 'Translasi AI...'
85
- translated_segments = translate_segments_llm(segments, custom_prompt)
 
86
 
 
87
  processed_audio_files = []
 
88
  for i, seg in enumerate(translated_segments):
89
  start_t = seg['start']
90
  end_t = seg['end']
@@ -92,10 +150,21 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
92
  text = seg.get('translated_text', seg['text'])
93
  if not text.strip(): continue
94
 
 
 
 
 
 
 
 
 
 
 
95
  raw_tts = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_raw_{i}.mp3")
96
  sync_tts = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_sync_{i}.wav")
97
 
98
- asyncio.run(generate_tts(text, VOICE_MAP.get(target_voice, 'id-ID-ArdiNeural'), raw_tts))
 
99
 
100
  tts_dur = get_audio_duration(raw_tts)
101
  speed = min(max(tts_dur / duration_orig, 0.7), 1.8) if duration_orig > 0 else 1.0
@@ -108,8 +177,6 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
108
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
109
 
110
  # LOGIKA AUDIO BARU:
111
- # 1. Background (Video asli): Turunkan frekuensi vokal (-15dB di 1000Hz) & Set volume ke 40% (0.4) agar backsound tetap ada.
112
- # 2. TTS Dubbing AI: Besarkan volumenya ke 300% (3.0) agar sangat jelas.
113
  filter_complex = "[0:a]equalizer=f=1000:width_type=o:w=2:g=-15,volume=0.4[bg];"
114
  inputs_cmd = ['ffmpeg', '-loglevel', 'quiet', '-y', '-i', video_path]
115
  amix_inputs = "[bg]"
@@ -118,11 +185,9 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
118
  idx = i + 1
119
  inputs_cmd.extend(['-i', item['path']])
120
  start_ms = int(item['start'] * 1000)
121
- # Beri delay, dan besarkan volume TTS 3x lipat (300%)
122
  filter_complex += f"[{idx}:a]adelay={start_ms}|{start_ms},volume=3.0[dub{idx}];"
123
  amix_inputs += f"[dub{idx}]"
124
 
125
- # Gabungkan semua, tambah volume akhir sedikit untuk kompensasi penurunan dari filter amix
126
  filter_complex += f"{amix_inputs}amix=inputs={len(processed_audio_files)+1}:duration=first:dropout_transition=0,volume=1.5[outa]"
127
 
128
  final_cmd = inputs_cmd + [
@@ -145,6 +210,7 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
145
  tasks[task_id]['status'] = 'Error'
146
  tasks[task_id]['error_message'] = str(e)
147
 
 
148
  # --- ROUTES ---
149
 
150
  @app.route('/')
@@ -184,7 +250,7 @@ HTML_TEMPLATE = """
184
 
185
  <div class="bg-gray-800 rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700">
186
  <h2 class="text-2xl font-bold text-center mb-2 text-white">🎙️ Dubbing Sync Pro</h2>
187
- <p class="text-sm text-center text-gray-400 mb-6">Suara AI 300% | Backsound Asli 40%</p>
188
 
189
  <form id="uploadForm" class="space-y-4">
190
  <div>
 
7
  import time
8
  import subprocess
9
  import logging
10
+ import numpy as np
11
  from flask import Flask, request, jsonify, render_template_string, send_from_directory
12
  import whisper
13
  import edge_tts
 
24
 
25
  tasks = {}
26
 
27
+ # --- MAP SUARA (MALE & FEMALE) ---
28
  VOICE_MAP = {
29
+ 'id-ID': {'Male': 'id-ID-ArdiNeural', 'Female': 'id-ID-GadisNeural'},
30
+ 'en-US': {'Male': 'en-US-ChristopherNeural', 'Female': 'en-US-AriaNeural'},
31
+ 'ja-JP': {'Male': 'ja-JP-KeitaNeural', 'Female': 'ja-JP-NanamiNeural'}
32
+ }
33
+
34
+ # Mapping Bahasa untuk Prompt AI
35
+ LANG_MAP = {
36
+ 'id-ID': 'Indonesia',
37
+ 'en-US': 'Inggris',
38
+ 'ja-JP': 'Jepang'
39
  }
40
 
41
  # Load Whisper (CPU Friendly, FP16 Fixed)
 
47
  '-of', 'default=noprint_wrappers=1:nokey=1', file_path
48
  ]
49
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
50
+ try:
51
+ return float(result.stdout)
52
+ except:
53
+ return 0.0
54
 
55
+ def analyze_gender_and_pitch(audio_path):
56
+ """Menganalisis potongan audio untuk menentukan gender dan variasi pitch."""
57
+ try:
58
+ import librosa
59
+ # Load audio dengan sample rate standard
60
+ y, sr = librosa.load(audio_path, sr=22050)
61
+
62
+ if len(y) == 0: return "Male", "+0Hz"
63
+
64
+ # Deteksi Fundamental Frequency (F0)
65
+ f0 = librosa.yin(y, fmin=65, fmax=300)
66
+ valid_f0 = f0[~np.isnan(f0)]
67
+
68
+ if len(valid_f0) > 0:
69
+ mean_f0 = np.mean(valid_f0)
70
+
71
+ # Threshold umum: > 165Hz = Perempuan, < 165Hz = Laki-laki
72
+ gender = "Female" if mean_f0 >= 165 else "Male"
73
+
74
+ # Hitung variasi pitch (agar tiap orang suaranya beda)
75
+ # Normal cowok ~120Hz, cewek ~210Hz. Dibagi 2 agar tidak terlalu ekstrem
76
+ base_f0 = 210.0 if gender == "Female" else 120.0
77
+ pitch_shift = int((mean_f0 - base_f0) / 2)
78
+
79
+ # Batasi modifikasi pitch Edge TTS agar tidak rusak (antara -20Hz s/d +20Hz)
80
+ pitch_shift = max(-20, min(20, pitch_shift))
81
+ pitch_str = f"+{pitch_shift}Hz" if pitch_shift >= 0 else f"{pitch_shift}Hz"
82
+
83
+ return gender, pitch_str
84
+ except Exception as e:
85
+ print(f"Pitch analysis warning: {e}")
86
+
87
+ return "Male", "+0Hz" # Default fallback
88
+
89
+ def translate_segments_llm(segments, custom_prompt, target_voice):
90
+ target_lang = LANG_MAP.get(target_voice, 'Indonesia')
91
+
92
+ # PERBAIKAN: Memasukkan bahasa target secara paksa ke dalam prompt
93
+ if custom_prompt:
94
+ instruction = f"{custom_prompt}\n\nPENTING: Terjemahkan SEMUA teks ke dalam bahasa {target_lang}."
95
+ else:
96
+ instruction = f"Terjemahkan teks dalam JSON ini ke bahasa {target_lang} dengan akurat. Balas HANYA dengan JSON array."
97
+
98
  input_data = [{"id": i, "text": s['text']} for i, s in enumerate(segments)]
99
  full_prompt = f"{instruction}\n\nFormat: [{{'id': 0, 'text': '...'}}]\n\nData:\n{json.dumps(input_data)}"
100
 
 
116
  translated_list = json.loads(full_text[start_idx:end_idx])
117
  for item in translated_list:
118
  segments[item['id']]['translated_text'] = item['text']
119
+ except Exception as e:
120
+ print(f"Translation Error: {e}")
121
  for s in segments: s['translated_text'] = s['text']
122
  return segments
123
 
124
+ # PERBAIKAN: Menambahkan parameter pitch
125
+ async def generate_tts(text, voice, path, pitch_str="+0Hz"):
126
+ communicate = edge_tts.Communicate(text, voice, pitch=pitch_str)
127
  await communicate.save(path)
128
 
129
  def process_dubbing(task_id, video_path, target_voice, custom_prompt):
 
136
  result = whisper_model.transcribe(orig_audio, verbose=False, fp16=False)
137
  segments = result['segments']
138
 
139
+ tasks[task_id]['status'] = f'Translasi AI ({LANG_MAP.get(target_voice, target_voice)})...'
140
+ # Pass target_voice ke translator
141
+ translated_segments = translate_segments_llm(segments, custom_prompt, target_voice)
142
 
143
+ tasks[task_id]['status'] = 'Menganalisis Suara & Dubbing...'
144
  processed_audio_files = []
145
+
146
  for i, seg in enumerate(translated_segments):
147
  start_t = seg['start']
148
  end_t = seg['end']
 
150
  text = seg.get('translated_text', seg['text'])
151
  if not text.strip(): continue
152
 
153
+ # Potong audio asli khusus untuk segmen ini guna deteksi suara
154
+ chunk_wav = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_chunk_{i}.wav")
155
+ subprocess.run(['ffmpeg', '-loglevel', 'quiet', '-y', '-i', orig_audio, '-ss', str(start_t), '-t', str(duration_orig), chunk_wav], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
156
+
157
+ # Deteksi Cewek/Cowok dan variasi pitch
158
+ gender, pitch_str = analyze_gender_and_pitch(chunk_wav)
159
+
160
+ # Pilih Voice ID yang sesuai berdasarkan bahasa dan gender
161
+ selected_voice = VOICE_MAP.get(target_voice, VOICE_MAP['id-ID'])[gender]
162
+
163
  raw_tts = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_raw_{i}.mp3")
164
  sync_tts = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_sync_{i}.wav")
165
 
166
+ # Generate TTS dengan pitch modifier
167
+ asyncio.run(generate_tts(text, selected_voice, raw_tts, pitch_str))
168
 
169
  tts_dur = get_audio_duration(raw_tts)
170
  speed = min(max(tts_dur / duration_orig, 0.7), 1.8) if duration_orig > 0 else 1.0
 
177
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
178
 
179
  # LOGIKA AUDIO BARU:
 
 
180
  filter_complex = "[0:a]equalizer=f=1000:width_type=o:w=2:g=-15,volume=0.4[bg];"
181
  inputs_cmd = ['ffmpeg', '-loglevel', 'quiet', '-y', '-i', video_path]
182
  amix_inputs = "[bg]"
 
185
  idx = i + 1
186
  inputs_cmd.extend(['-i', item['path']])
187
  start_ms = int(item['start'] * 1000)
 
188
  filter_complex += f"[{idx}:a]adelay={start_ms}|{start_ms},volume=3.0[dub{idx}];"
189
  amix_inputs += f"[dub{idx}]"
190
 
 
191
  filter_complex += f"{amix_inputs}amix=inputs={len(processed_audio_files)+1}:duration=first:dropout_transition=0,volume=1.5[outa]"
192
 
193
  final_cmd = inputs_cmd + [
 
210
  tasks[task_id]['status'] = 'Error'
211
  tasks[task_id]['error_message'] = str(e)
212
 
213
+
214
  # --- ROUTES ---
215
 
216
  @app.route('/')
 
250
 
251
  <div class="bg-gray-800 rounded-2xl shadow-2xl p-8 w-full max-w-md border border-gray-700">
252
  <h2 class="text-2xl font-bold text-center mb-2 text-white">🎙️ Dubbing Sync Pro</h2>
253
+ <p class="text-sm text-center text-gray-400 mb-6">Deteksi Gender & Multi-Speaker Auto-Pitch</p>
254
 
255
  <form id="uploadForm" class="space-y-4">
256
  <div>