Spaces:
Paused
Paused
Update app.py
Browse files
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
|
| 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 |
-
|
| 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 |
-
#
|
| 117 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 126 |
amix_inputs += f"[dub{idx}]"
|
| 127 |
|
| 128 |
-
|
|
|
|
| 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 |
-
#
|
| 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 |
-
<
|
| 180 |
-
<
|
| 181 |
-
<
|
|
|
|
| 182 |
</head>
|
| 183 |
-
<body>
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
<
|
| 187 |
-
<
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
<
|
| 192 |
-
<
|
| 193 |
-
|
| 194 |
-
<
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
</form>
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</div>
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
</div>
|
| 205 |
</div>
|
|
|
|
| 206 |
<script>
|
| 207 |
-
const form=document.getElementById('
|
| 208 |
-
form.onsubmit=async(e)=>{
|
| 209 |
e.preventDefault();
|
| 210 |
-
const fd=new FormData();
|
| 211 |
-
fd.append('video',document.getElementById('
|
| 212 |
-
fd.append('voice',document.getElementById('
|
| 213 |
-
fd.append('prompt',document.getElementById('
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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>
|