Ricky01anjay commited on
Commit
59ecd42
ยท
verified ยท
1 Parent(s): 3d3f60b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -61
app.py CHANGED
@@ -11,12 +11,11 @@ from flask import Flask, request, jsonify, render_template_string, send_from_dir
11
  import whisper
12
  import edge_tts
13
 
14
- # --- KONFIGURASI SILENT ---
15
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
16
  logging.getLogger('werkzeug').setLevel(logging.ERROR)
17
 
18
  app = Flask(__name__)
19
- # Gunakan path absolut untuk menghindari 404
20
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
21
  UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
22
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
@@ -30,7 +29,7 @@ VOICE_MAP = {
30
  'ja-JP': 'ja-JP-KeitaNeural'
31
  }
32
 
33
- # Load Whisper Model (Base)
34
  whisper_model = whisper.load_model("base")
35
 
36
  def get_audio_duration(file_path):
@@ -74,21 +73,17 @@ async def generate_tts(text, voice, path):
74
 
75
  def process_dubbing(task_id, video_path, target_voice, custom_prompt):
76
  try:
77
- # 1. Ekstrak Audio Original
78
  tasks[task_id]['status'] = 'Mengekstrak Audio...'
79
  orig_audio = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_orig.wav")
80
  subprocess.run(['ffmpeg', '-loglevel', 'quiet', '-y', '-i', video_path, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', orig_audio], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
81
 
82
- # 2. Transkripsi Whisper (FIX FP16 Error)
83
  tasks[task_id]['status'] = 'Transkripsi...'
84
  result = whisper_model.transcribe(orig_audio, verbose=False, fp16=False)
85
  segments = result['segments']
86
 
87
- # 3. Translasi
88
  tasks[task_id]['status'] = 'Translasi AI...'
89
  translated_segments = translate_segments_llm(segments, custom_prompt)
90
 
91
- # 4. Generate & Sync TTS per Segmen
92
  processed_audio_files = []
93
  for i, seg in enumerate(translated_segments):
94
  start_t = seg['start']
@@ -108,13 +103,14 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
108
  subprocess.run(['ffmpeg', '-loglevel', 'quiet', '-y', '-i', raw_tts, '-filter:a', f'atempo={speed}', '-ar', '44100', sync_tts], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
109
  processed_audio_files.append({'path': sync_tts, 'start': start_t})
110
 
111
- # 5. Hapus Vokal & Rendering Akhir
112
- tasks[task_id]['status'] = 'Hapus Vokal & Mix...'
113
  output_filename = f"{task_id}_output.mp4"
114
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
115
 
116
- # Filter Phase Cancellation (Hapus vokal center) + Volume BG 100%
117
- filter_complex = "[0:a]pan=stereo|c0=c0-c1|c1=c1-c0,volume=1.0[bg];"
 
 
118
  inputs_cmd = ['ffmpeg', '-loglevel', 'quiet', '-y', '-i', video_path]
119
  amix_inputs = "[bg]"
120
 
@@ -122,10 +118,12 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
122
  idx = i + 1
123
  inputs_cmd.extend(['-i', item['path']])
124
  start_ms = int(item['start'] * 1000)
125
- filter_complex += f"[{idx}:a]adelay={start_ms}|{start_ms}[dub{idx}];"
 
126
  amix_inputs += f"[dub{idx}]"
127
 
128
- filter_complex += f"{amix_inputs}amix=inputs={len(processed_audio_files)+1}:duration=first:dropout_transition=0[outa]"
 
129
 
130
  final_cmd = inputs_cmd + [
131
  '-filter_complex', filter_complex, '-map', '0:v', '-map', '[outa]',
@@ -134,7 +132,7 @@ def process_dubbing(task_id, video_path, target_voice, custom_prompt):
134
 
135
  subprocess.run(final_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
136
 
137
- # 6. Pembersihan File Sampah (Hati-hati jangan hapus _output.mp4)
138
  for file in os.listdir(app.config['UPLOAD_FOLDER']):
139
  if task_id in file and not file.endswith("_output.mp4"):
140
  try: os.remove(os.path.join(app.config['UPLOAD_FOLDER'], file))
@@ -172,64 +170,109 @@ def status():
172
  def download(f):
173
  return send_from_directory(app.config['UPLOAD_FOLDER'], f)
174
 
 
175
  HTML_TEMPLATE = """
176
  <!DOCTYPE html>
177
- <html>
178
  <head>
179
- <title>AI Dubbing Silent</title>
180
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
181
- <style>body{background:#0a0a0a;color:#efefef;display:flex;align-items:center;justify-content:center;height:100vh;}.card{background:#161616;border:1px solid #333;width:400px;padding:25px;border-radius:20px;}</style>
 
182
  </head>
183
- <body>
184
- <div class="card shadow-lg text-center">
185
- <h4 class="mb-3">๐ŸŽ™๏ธ Dubbing Sync</h4>
186
- <p class="text-secondary small mb-4">Vocal Removed | BG 100% | Silent Logs</p>
187
- <form id="f">
188
- <input type="file" id="v" class="form-control mb-2 bg-dark text-white border-secondary" required>
189
- <select id="s" class="form-select mb-2 bg-dark text-white border-secondary">
190
- <option value="id-ID">Indonesia ๐Ÿ‡ฎ๐Ÿ‡ฉ</option>
191
- <option value="en-US">English ๐Ÿ‡บ๐Ÿ‡ธ</option>
192
- <option value="ja-JP">Japanese ๐Ÿ‡ฏ๐Ÿ‡ต</option>
193
- </select>
194
- <textarea id="p" class="form-control mb-3 bg-dark text-white border-secondary" placeholder="Prompt (opsional)..."></textarea>
195
- <button type="submit" id="b" class="btn btn-primary w-100 fw-bold">PROSES VIDEO</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  </form>
197
- <div id="loading" class="mt-4 d-none text-center">
198
- <div class="spinner-border text-primary spinner-border-sm mb-2"></div>
199
- <div id="st" class="small text-primary">Menunggu...</div>
 
 
 
 
 
200
  </div>
201
- <div id="res" class="mt-4 d-none">
202
- <video id="vd" controls class="w-100 rounded border border-secondary mb-3"></video>
203
- <a id="dl" href="#" class="btn btn-success w-100 fw-bold" download>DOWNLOAD VIDEO</a>
 
 
 
 
 
204
  </div>
205
  </div>
 
206
  <script>
207
- const form=document.getElementById('f');
208
- form.onsubmit=async(e)=>{
209
  e.preventDefault();
210
- const fd=new FormData();
211
- fd.append('video',document.getElementById('v').files[0]);
212
- fd.append('voice',document.getElementById('s').value);
213
- fd.append('prompt',document.getElementById('p').value);
214
- document.getElementById('b').disabled=true;
215
- document.getElementById('loading').classList.remove('d-none');
216
- const res=await fetch('/generate',{method:'POST',body:fd});
217
- const data=await res.json();
218
- const pol=setInterval(async()=>{
219
- const r_res=await fetch('/status?task_id='+data.task_id);
220
- const r_data=await r_res.json();
221
- document.getElementById('st').innerText=r_data.status;
222
- if(r_data.status==='Selesai'){
223
- clearInterval(pol);
224
- document.getElementById('loading').classList.add('d-none');
225
- document.getElementById('res').classList.remove('d-none');
226
- document.getElementById('vd').src=r_data.result_video;
227
- document.getElementById('dl').href=r_data.result_video;
228
- document.getElementById('b').disabled=false;
229
- } else if(r_data.status==='Error'){
230
- alert("Error: " + r_data.error_message); location.reload();
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  }
232
- },2000);
233
  };
234
  </script>
235
  </body>
 
11
  import whisper
12
  import edge_tts
13
 
14
+ # --- KONFIGURASI SILENT LOGS ---
15
  os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
16
  logging.getLogger('werkzeug').setLevel(logging.ERROR)
17
 
18
  app = Flask(__name__)
 
19
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
20
  UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
21
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
 
29
  'ja-JP': 'ja-JP-KeitaNeural'
30
  }
31
 
32
+ # Load Whisper (CPU Friendly, FP16 Fixed)
33
  whisper_model = whisper.load_model("base")
34
 
35
  def get_audio_duration(file_path):
 
73
 
74
  def process_dubbing(task_id, video_path, target_voice, custom_prompt):
75
  try:
 
76
  tasks[task_id]['status'] = 'Mengekstrak Audio...'
77
  orig_audio = os.path.join(app.config['UPLOAD_FOLDER'], f"{task_id}_orig.wav")
78
  subprocess.run(['ffmpeg', '-loglevel', 'quiet', '-y', '-i', video_path, '-vn', '-acodec', 'pcm_s16le', '-ar', '44100', orig_audio], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
79
 
 
80
  tasks[task_id]['status'] = 'Transkripsi...'
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']
 
103
  subprocess.run(['ffmpeg', '-loglevel', 'quiet', '-y', '-i', raw_tts, '-filter:a', f'atempo={speed}', '-ar', '44100', sync_tts], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
104
  processed_audio_files.append({'path': sync_tts, 'start': start_t})
105
 
106
+ tasks[task_id]['status'] = 'Mixing Audio & Rendering...'
 
107
  output_filename = f"{task_id}_output.mp4"
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]"
116
 
 
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 + [
129
  '-filter_complex', filter_complex, '-map', '0:v', '-map', '[outa]',
 
132
 
133
  subprocess.run(final_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
134
 
135
+ # Cleanup file temporary
136
  for file in os.listdir(app.config['UPLOAD_FOLDER']):
137
  if task_id in file and not file.endswith("_output.mp4"):
138
  try: os.remove(os.path.join(app.config['UPLOAD_FOLDER'], file))
 
170
  def download(f):
171
  return send_from_directory(app.config['UPLOAD_FOLDER'], f)
172
 
173
+ # --- HTML DENGAN TAILWIND CSS ---
174
  HTML_TEMPLATE = """
175
  <!DOCTYPE html>
176
+ <html lang="id">
177
  <head>
178
+ <meta charset="UTF-8">
179
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
180
+ <title>AI Dubbing Pro</title>
181
+ <script src="https://cdn.tailwindcss.com"></script>
182
  </head>
183
+ <body class="bg-gray-900 text-gray-100 min-h-screen flex items-center justify-center p-4 font-sans">
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>
191
+ <label class="block text-sm font-medium text-gray-300 mb-1">Upload Video (MP4)</label>
192
+ <input type="file" id="videoFile" accept="video/*" required
193
+ class="block w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700 focus:outline-none bg-gray-700 rounded-lg p-2 border border-gray-600">
194
+ </div>
195
+
196
+ <div>
197
+ <label class="block text-sm font-medium text-gray-300 mb-1">Bahasa Target</label>
198
+ <select id="targetVoice" class="w-full bg-gray-700 border border-gray-600 rounded-lg p-2.5 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none">
199
+ <option value="id-ID">Indonesia ๐Ÿ‡ฎ๐Ÿ‡ฉ</option>
200
+ <option value="en-US">English ๐Ÿ‡บ๐Ÿ‡ธ</option>
201
+ <option value="ja-JP">Japanese ๐Ÿ‡ฏ๐Ÿ‡ต</option>
202
+ </select>
203
+ </div>
204
+
205
+ <div>
206
+ <label class="block text-sm font-medium text-gray-300 mb-1">Custom Prompt AI (Opsional)</label>
207
+ <textarea id="customPrompt" rows="2" placeholder="Gaya bahasa santai, dll..."
208
+ class="w-full bg-gray-700 border border-gray-600 rounded-lg p-2.5 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none resize-none"></textarea>
209
+ </div>
210
+
211
+ <button type="submit" id="btnSubmit"
212
+ class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg shadow-blue-500/30">
213
+ Mulai Dubbing
214
+ </button>
215
  </form>
216
+
217
+ <!-- Loading State -->
218
+ <div id="loadingSection" class="hidden mt-6 flex flex-col items-center justify-center space-y-3">
219
+ <svg class="animate-spin h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
220
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
221
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
222
+ </svg>
223
+ <span id="statusText" class="text-blue-400 font-medium tracking-wide">Menyiapkan...</span>
224
  </div>
225
+
226
+ <!-- Result State -->
227
+ <div id="resultSection" class="hidden mt-6 space-y-4">
228
+ <video id="resVideo" controls class="w-full rounded-lg border border-gray-600 bg-black"></video>
229
+ <a id="dlBtn" href="#" download
230
+ class="block text-center w-full bg-green-600 hover:bg-green-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg shadow-green-500/30">
231
+ โฌ‡๏ธ Download Video
232
+ </a>
233
  </div>
234
  </div>
235
+
236
  <script>
237
+ const form = document.getElementById('uploadForm');
238
+ form.onsubmit = async (e) => {
239
  e.preventDefault();
240
+ const fd = new FormData();
241
+ fd.append('video', document.getElementById('videoFile').files[0]);
242
+ fd.append('voice', document.getElementById('targetVoice').value);
243
+ fd.append('prompt', document.getElementById('customPrompt').value);
244
+
245
+ // UI Changes
246
+ document.getElementById('btnSubmit').disabled = true;
247
+ document.getElementById('btnSubmit').classList.add('opacity-50', 'cursor-not-allowed');
248
+ document.getElementById('loadingSection').classList.remove('hidden');
249
+ document.getElementById('resultSection').classList.add('hidden');
250
+
251
+ const res = await fetch('/generate', { method: 'POST', body: fd });
252
+ const data = await res.json();
253
+
254
+ const timer = setInterval(async () => {
255
+ const sRes = await fetch('/status?task_id=' + data.task_id);
256
+ const sData = await sRes.json();
257
+
258
+ document.getElementById('statusText').innerText = sData.status;
259
+
260
+ if (sData.status === 'Selesai') {
261
+ clearInterval(timer);
262
+ document.getElementById('loadingSection').classList.add('hidden');
263
+ document.getElementById('resultSection').classList.remove('hidden');
264
+ document.getElementById('resVideo').src = sData.result_video;
265
+ document.getElementById('dlBtn').href = sData.result_video;
266
+
267
+ // Reset button
268
+ document.getElementById('btnSubmit').disabled = false;
269
+ document.getElementById('btnSubmit').classList.remove('opacity-50', 'cursor-not-allowed');
270
+ } else if (sData.status === 'Error') {
271
+ clearInterval(timer);
272
+ alert("Terjadi Kesalahan: " + sData.error_message);
273
+ location.reload();
274
  }
275
+ }, 2000);
276
  };
277
  </script>
278
  </body>