AutoClip / app.py
Ricky01anjay's picture
Update app.py
d279272 verified
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)