Spaces:
Paused
Paused
File size: 16,469 Bytes
d26d714 3915846 d161b1b d279272 3915846 d26d714 d279272 d161b1b d26d714 d161b1b d26d714 d279272 84b0872 d26d714 84b0872 7aed1b3 d279272 84b0872 d26d714 d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d161b1b d279272 d26d714 d279272 d26d714 ffe2671 d161b1b ffe2671 d161b1b ffe2671 d161b1b ffe2671 d26d714 ffe2671 84b0872 ffe2671 181a914 ffe2671 d26d714 84b0872 ffe2671 84b0872 d279272 d26d714 ffe2671 d26d714 ffe2671 7aed1b3 d161b1b d26d714 7aed1b3 d26d714 ffe2671 84b0872 d26d714 7aed1b3 84b0872 ffe2671 d26d714 84b0872 d26d714 84b0872 d26d714 181a914 d26d714 3915846 d26d714 181a914 d26d714 ffe2671 84b0872 d26d714 ea89406 d26d714 84b0872 d279272 84b0872 ffe2671 7aed1b3 d26d714 ffe2671 d26d714 84b0872 d279272 84b0872 b208f0f 84b0872 d26d714 84b0872 d161b1b 84b0872 d26d714 181a914 84b0872 d161b1b 84b0872 181a914 d26d714 84b0872 d161b1b 84b0872 d26d714 d279272 ffe2671 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | import os
import uuid
import threading
import requests
import re
import time
import json
import queue
import subprocess
import multiprocessing
from flask import Flask, request, jsonify, render_template_string, send_file
from faster_whisper import WhisperModel
# --- CONFIG SERVER & ANTRIAN ---
AI_API_URL = "https://puruboy-api.vercel.app/api/ai/notegpt"
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'downloads'
app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # Max 200 MB
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
MAX_QUEUE_SIZE = 20 # Maksimal antrian
MAX_WORKERS = 5 # Maksimal proses berjalan bersamaan
AUTO_DELETE_MINUTES = 1440 # Waktu file dihapus (1440 menit = 1 hari)
JOBS = {}
job_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
# --- GLOBAL RATE LIMIT CONFIG ---
request_timestamps = []
global_lockout_until = 0
# --- OPTIMASI CPU WHISPER ---
# Mengatur thread CPU per model agar 5 worker tidak membuat CPU bertabrakan (bottleneck)
total_cores = multiprocessing.cpu_count()
threads_per_worker = max(1, total_cores // MAX_WORKERS)
print(f"Loading Whisper Model (CPU Cores/Worker: {threads_per_worker})...")
whisper_model = WhisperModel(
"base",
device="cpu",
compute_type="int8",
cpu_threads=threads_per_worker
)
print("Model Loaded!")
# ==========================================
# GLOBAL RATE LIMITER (Mencegah Spam)
# ==========================================
@app.before_request
def rate_limiter():
global request_timestamps, global_lockout_until
# Hanya batasi endpoint pembuatan video
if request.endpoint == 'generate' and request.method == 'POST':
current_time = time.time()
# Cek apakah sedang dalam masa hukuman 2 menit
if current_time < global_lockout_until:
return jsonify({"error": "demi keamanan kamu sengaja mematikan api ini selama 2 menit karena ada yang spam"}), 429
# Bersihkan timestamp yang sudah lebih dari 60 detik (1 menit)
request_timestamps = [t for t in request_timestamps if current_time - t < 60]
# Jika request lebih dari 30 dalam 1 menit
if len(request_timestamps) >= 30:
global_lockout_until = current_time + 120 # Kunci selama 120 detik (2 menit)
return jsonify({"error": "demi keamanan kamu sengaja mematikan api ini selama 2 menit karena ada yang spam"}), 429
request_timestamps.append(current_time)
# ==========================================
# FUNGSI PARSING AI (Server-Sent Events)
# ==========================================
def get_ai_viral_clip(transcript_str):
payload = {
"prompt": f"""
Kamu adalah video editor TikTok/Reels profesional.
Tugas: Cari 1 bagian (segmen berkelanjutan) PALING MENARIK/LUCU/VIRAL dari transkrip ini.
Aturan:
1. Durasi segmen harus antara 15 detik sampai maksimal 60 detik.
2. Cari kalimat yang punya hook kuat di awal dan konklusi di akhir.
3. OUTPUT HANYA JSON. JANGAN ADA TEKS LAIN.
Format: {{"start_s": 10.5, "end_s": 45.2, "reason": "Alasan kenapa ini viral"}}
TRANSKRIP:
{transcript_str}
""",
"model": "gemini-3-flash-preview",
"chat_mode": "standard"
}
headers = {"Content-Type": "application/json"}
response = requests.post(AI_API_URL, json=payload, headers=headers, stream=True)
full_text = ""
for line in response.iter_lines():
if line:
decoded_line = line.decode('utf-8')
if decoded_line.startswith("data: "):
data_str = decoded_line[len("data: "):]
try:
data_json = json.loads(data_str)
if "text" in data_json:
full_text += data_json["text"]
if data_json.get("done") or data_json.get("type") == "finish":
break
except json.JSONDecodeError:
continue
json_match = re.search(r'\{.*\}', full_text, re.DOTALL)
if not json_match:
raise Exception("Gagal mengekstrak format JSON dari AI.")
return json.loads(json_match.group(0))
# ==========================================
# FUNGSI HELPER VIDEO & SUBTITLE
# ==========================================
def format_time_srt(seconds):
hrs = int(seconds // 3600)
mins = int((seconds % 3600) // 60)
secs = int(seconds % 60)
msec = int((seconds - int(seconds)) * 1000)
return f"{hrs:02d}:{mins:02d}:{secs:02d},{msec:03d}"
def generate_srt(words, start_offset, end_offset, srt_path):
with open(srt_path, 'w', encoding='utf-8') as f:
idx = 1
for w in words:
if w.start >= start_offset and w.end <= end_offset:
s_time = format_time_srt(w.start - start_offset)
e_time = format_time_srt(w.end - start_offset)
text = w.word.strip().upper()
f.write(f"{idx}\n{s_time} --> {e_time}\n{text}\n\n")
idx += 1
def cleanup_files(*file_paths, job_id=None):
for path in file_paths:
if path and os.path.exists(path):
try: os.remove(path)
except: pass
if job_id and job_id in JOBS:
del JOBS[job_id] # Hapus dari memory agar RAM tidak bengkak
print(f"[{job_id}] Auto-cleanup selesai.")
# ==========================================
# PROSES UTAMA (WORKER)
# ==========================================
def process_video(job_id):
path_in = JOBS[job_id]['input_path']
audio_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}.wav")
srt_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}.srt")
out_filename = f"out_{job_id}.mp4"
out_path = os.path.join(app.config['UPLOAD_FOLDER'], out_filename)
try:
# 1. EKSTRAK AUDIO CEPAT
JOBS[job_id].update({"message": "Mengekstrak audio video...", "progress": 10})
subprocess.run(['ffmpeg', '-y', '-i', path_in, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', audio_path],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
# 2. TRANSCRIBE DENGAN OPTIMASI SUPER CEPAT (Untuk Video >20 Menit)
JOBS[job_id].update({"message": "Menganalisa suara manusia (AI Whisper)...", "progress": 25})
# beam_size=1 dan condition_on_previous_text=False membuat proses 3x - 5x lebih cepat
segments, _ = whisper_model.transcribe(
audio_path,
word_timestamps=True,
vad_filter=True,
vad_parameters=dict(min_silence_duration_ms=500),
beam_size=1,
condition_on_previous_text=False
)
transcript_for_ai = []
all_words = []
for s in segments:
transcript_for_ai.append(f"[{s.start:.1f} - {s.end:.1f}]: {s.text}")
for w in s.words:
all_words.append(w)
transcript_str = "\n".join(transcript_for_ai)
if not transcript_str.strip():
raise Exception("Tidak ada suara manusia yang terdeteksi.")
# 3. AI MENCARI BAGIAN VIRAL
JOBS[job_id].update({"message": "AI sedang meracik bagian viral...", "progress": 50})
clip_data = get_ai_viral_clip(transcript_str)
s_time = max(0, float(clip_data['start_s']) - 0.2)
e_time = float(clip_data['end_s']) + 0.3
# 4. BUAT SUBTITLE SRT
JOBS[job_id].update({"message": "Membuat efek subtitle viral...", "progress": 70})
generate_srt(all_words, s_time, e_time, srt_path)
# 5. POTONG & BURN SUBTITLE
JOBS[job_id].update({"message": "Rendering Video Final (Super Cepat)...", "progress": 85})
safe_srt_path = srt_path.replace('\\', '/')
style = "Fontname=Arial,Fontsize=24,PrimaryColour=&H0000FFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=2,Shadow=0,Alignment=2,MarginV=25,Bold=1"
ffmpeg_cmd = [
'ffmpeg', '-y',
'-ss', str(s_time),
'-to', str(e_time),
'-i', path_in,
'-vf', f"subtitles={safe_srt_path}:force_style='{style}'",
'-c:v', 'libx264',
'-preset', 'superfast',
'-crf', '23',
'-c:a', 'aac',
'-b:a', '128k',
out_path
]
subprocess.run(ffmpeg_cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
JOBS[job_id].update({"status": "completed", "progress": 100, "message": "Selesai! Video siap.", "result": out_filename})
except Exception as e:
print(f"ERROR: {str(e)}")
if job_id in JOBS:
JOBS[job_id].update({"status": "error", "message": str(e)})
finally:
# File sementara dihapus langsung
cleanup_files(audio_path, srt_path)
# File final dihapus setelah 1 Hari (1440 menit)
timer = threading.Timer(AUTO_DELETE_MINUTES * 60, cleanup_files, args=(path_in, out_path), kwargs={'job_id': job_id})
timer.start()
# --- MULTI-WORKER SYSTEM (Menjalankan 5 Proses Sekaligus) ---
def queue_worker():
while True:
job_id = job_queue.get()
if job_id in JOBS:
JOBS[job_id]["status"] = "processing"
process_video(job_id)
job_queue.task_done()
# Menjalankan 5 Thread Workers (Sehingga bisa render 5 video serentak)
for _ in range(MAX_WORKERS):
threading.Thread(target=queue_worker, daemon=True).start()
# ==========================================
# UI HTML
# ==========================================
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Viral Clipper Ultra (Pro)</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-black text-slate-200 min-h-screen flex flex-col items-center p-6">
<div class="w-full max-w-xl bg-slate-900 p-8 rounded-3xl border border-slate-800 shadow-2xl">
<h1 class="text-2xl font-black text-center mb-6 bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">AI VIDEO CLIPPER ULTRA</h1>
<form id="clipForm" class="space-y-4">
<div class="border-2 border-dashed border-slate-700 rounded-2xl p-6 text-center hover:border-blue-500 transition-colors">
<input type="file" id="videoFile" accept="video/mp4,video/quicktime" class="hidden">
<label for="videoFile" class="cursor-pointer block">
<span class="text-sm text-slate-400" id="fileName">Klik untuk upload Video (Maks 200MB)</span>
</label>
</div>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-bold py-4 rounded-xl transition-all shadow-lg">BUAT VIDEO VIRAL ✨</button>
</form>
<div id="statusArea" class="mt-8 hidden">
<div class="flex justify-between mb-2 italic">
<span id="statusText" class="text-xs text-blue-400">Menunggu giliran...</span>
<span id="percentText" class="text-xs text-white">0%</span>
</div>
<div class="w-full bg-slate-800 rounded-full h-2">
<div id="progressBar" class="bg-blue-500 h-2 rounded-full transition-all duration-500" style="width: 0%"></div>
</div>
</div>
<div id="resultArea" class="mt-8 hidden text-center">
<video id="player" controls class="w-full rounded-xl border border-slate-700 mb-4"></video>
<a id="downloadBtn" href="#" class="block w-full text-center bg-white text-black font-bold py-3 rounded-xl hover:bg-gray-200 transition">DOWNLOAD CLIP</a>
<p class="text-xs text-red-400 mt-3 font-semibold">⚠️ Video akan dihapus otomatis dari server dalam 24 Jam.</p>
</div>
</div>
<script>
const fileInput = document.getElementById('videoFile');
fileInput.onchange = () => { document.getElementById('fileName').innerText = fileInput.files[0].name; };
document.getElementById('clipForm').onsubmit = async (e) => {
e.preventDefault();
const file = fileInput.files[0];
if (!file) return alert("Pilih video!");
if (file.size > 200 * 1024 * 1024) return alert("File terlalu besar! Maksimal 200MB.");
const formData = new FormData();
formData.append('video_file', file);
document.getElementById('statusArea').classList.remove('hidden');
document.getElementById('clipForm').classList.add('hidden');
document.getElementById('statusText').innerText = "Mengupload video...";
const res = await fetch('/generate', { method: 'POST', body: formData });
const data = await res.json();
if (res.status === 429) {
alert(data.error);
location.reload();
} else if (data.job_id) {
pollStatus(data.job_id);
} else {
alert("Error: " + data.error);
location.reload();
}
};
async function pollStatus(jobId) {
const interval = setInterval(async () => {
const res = await fetch(`/status/${jobId}`);
if(res.status === 404) {
clearInterval(interval);
alert("Sesi telah berakhir atau file sudah dihapus oleh sistem.");
location.reload();
return;
}
const data = await res.json();
document.getElementById('statusText').innerText = data.message;
document.getElementById('progressBar').style.width = data.progress + "%";
document.getElementById('percentText').innerText = data.progress + "%";
if (data.status === 'completed') {
clearInterval(interval);
document.getElementById('statusArea').classList.add('hidden');
document.getElementById('player').src = `/view/${data.result}`;
document.getElementById('downloadBtn').href = `/download/${data.result}`;
document.getElementById('resultArea').classList.remove('hidden');
} else if (data.status === 'error') {
clearInterval(interval);
alert("Gagal: " + data.message);
location.reload();
}
}, 3000);
}
</script>
</body>
</html>
"""
# ==========================================
# ROUTES
# ==========================================
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@app.route('/generate', methods=['POST'])
def generate():
if job_queue.full():
return jsonify({"error": "Antrian sedang penuh (Maksimal 20). Coba beberapa saat lagi."}), 429
file = request.files.get('video_file')
if not file: return jsonify({"error": "File kosong"}), 400
job_id = str(uuid.uuid4())
save_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_in.mp4")
file.save(save_path)
JOBS[job_id] = {
"status": "queued",
"progress": 5,
"message": "Menunggu giliran dalam antrian...",
"input_path": save_path
}
job_queue.put(job_id)
return jsonify({"job_id": job_id})
@app.route('/status/<job_id>')
def status(job_id):
if job_id not in JOBS:
return jsonify({"error": "Sesi tidak ditemukan"}), 404
return jsonify(JOBS[job_id])
@app.route('/view/<filename>')
def view_video(filename):
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if not os.path.exists(file_path):
return "File kadaluarsa", 404
return send_file(file_path)
@app.route('/download/<filename>')
def download(filename):
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if not os.path.exists(file_path):
return "File kadaluarsa", 404
return send_file(file_path, as_attachment=True)
if __name__ == '__main__':
# Untuk level produksi sebenarnya, lebih baik disajikan via Waitress atau Gunicorn
# pip install waitress
# from waitress import serve
# serve(app, host="0.0.0.0", port=7860, threads=10)
app.run(host='0.0.0.0', port=7860, debug=False) |