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 = """