Spaces:
Paused
Paused
| 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) | |
| # ========================================== | |
| 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 | |
| # ========================================== | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| 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}) | |
| def status(job_id): | |
| if job_id not in JOBS: | |
| return jsonify({"error": "Sesi tidak ditemukan"}), 404 | |
| return jsonify(JOBS[job_id]) | |
| 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) | |
| 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) |