Ricky01anjay commited on
Commit
d279272
·
verified ·
1 Parent(s): d161b1b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +77 -35
app.py CHANGED
@@ -7,31 +7,69 @@ import time
7
  import json
8
  import queue
9
  import subprocess
 
10
  from flask import Flask, request, jsonify, render_template_string, send_file
11
  from faster_whisper import WhisperModel
12
 
13
- # --- CONFIG ---
14
  AI_API_URL = "https://puruboy-api.vercel.app/api/ai/notegpt"
15
  app = Flask(__name__)
16
  app.config['UPLOAD_FOLDER'] = 'downloads'
17
  app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # Max 200 MB
18
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
19
 
20
- # --- ANTRIAN & AUTO DELETE CONFIG ---
21
- MAX_QUEUE_SIZE = 5 # Maksimal video dalam antrian
22
- AUTO_DELETE_MINUTES = 5 # Waktu sebelum file dihapus otomatis
23
 
24
  JOBS = {}
25
  job_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
26
 
27
- # Load Model Whisper
28
- print("Loading Whisper Model...")
29
- # Menggunakan VAD untuk mengabaikan musik/noise dan hanya fokus ke suara manusia
30
- whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
 
 
 
 
 
 
 
 
 
 
 
 
31
  print("Model Loaded!")
32
 
33
  # ==========================================
34
- # FUNGSI PARSING AI SSE (Server-Sent Events)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # ==========================================
36
  def get_ai_viral_clip(transcript_str):
37
  payload = {
@@ -52,7 +90,6 @@ TRANSKRIP:
52
  }
53
 
54
  headers = {"Content-Type": "application/json"}
55
-
56
  response = requests.post(AI_API_URL, json=payload, headers=headers, stream=True)
57
 
58
  full_text = ""
@@ -70,7 +107,6 @@ TRANSKRIP:
70
  except json.JSONDecodeError:
71
  continue
72
 
73
- # Ekstrak JSON menggunakan regex (mengatasi jika AI masih memberikan teks basa-basi)
74
  json_match = re.search(r'\{.*\}', full_text, re.DOTALL)
75
  if not json_match:
76
  raise Exception("Gagal mengekstrak format JSON dari AI.")
@@ -81,7 +117,6 @@ TRANSKRIP:
81
  # FUNGSI HELPER VIDEO & SUBTITLE
82
  # ==========================================
83
  def format_time_srt(seconds):
84
- """Konversi detik (float) ke format jam:menit:detik,milis (untuk SRT)"""
85
  hrs = int(seconds // 3600)
86
  mins = int((seconds % 3600) // 60)
87
  secs = int(seconds % 60)
@@ -89,28 +124,23 @@ def format_time_srt(seconds):
89
  return f"{hrs:02d}:{mins:02d}:{secs:02d},{msec:03d}"
90
 
91
  def generate_srt(words, start_offset, end_offset, srt_path):
92
- """Membuat file SRT hanya untuk bagian video yang dipotong"""
93
  with open(srt_path, 'w', encoding='utf-8') as f:
94
  idx = 1
95
  for w in words:
96
- # Hanya ambil kata dalam rentang waktu yang dipotong
97
  if w.start >= start_offset and w.end <= end_offset:
98
- # Sesuaikan waktu agar dimulai dari 00:00:00 untuk video klip baru
99
  s_time = format_time_srt(w.start - start_offset)
100
  e_time = format_time_srt(w.end - start_offset)
101
  text = w.word.strip().upper()
102
-
103
  f.write(f"{idx}\n{s_time} --> {e_time}\n{text}\n\n")
104
  idx += 1
105
 
106
  def cleanup_files(*file_paths, job_id=None):
107
- """Menghapus semua file sementara dan membersihkan antrian RAM"""
108
  for path in file_paths:
109
  if path and os.path.exists(path):
110
  try: os.remove(path)
111
  except: pass
112
  if job_id and job_id in JOBS:
113
- del JOBS[job_id]
114
  print(f"[{job_id}] Auto-cleanup selesai.")
115
 
116
  # ==========================================
@@ -124,14 +154,23 @@ def process_video(job_id):
124
  out_path = os.path.join(app.config['UPLOAD_FOLDER'], out_filename)
125
 
126
  try:
127
- # 1. EKSTRAK AUDIO CEPAT (FFmpeg)
128
  JOBS[job_id].update({"message": "Mengekstrak audio video...", "progress": 10})
129
  subprocess.run(['ffmpeg', '-y', '-i', path_in, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', audio_path],
130
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
131
 
132
- # 2. TRANSCRIBE DENGAN VAD (Mengabaikan musik)
133
  JOBS[job_id].update({"message": "Menganalisa suara manusia (AI Whisper)...", "progress": 25})
134
- segments, _ = whisper_model.transcribe(audio_path, word_timestamps=True, vad_filter=True, vad_parameters=dict(min_silence_duration_ms=500))
 
 
 
 
 
 
 
 
 
135
 
136
  transcript_for_ai = []
137
  all_words = []
@@ -145,11 +184,10 @@ def process_video(job_id):
145
  if not transcript_str.strip():
146
  raise Exception("Tidak ada suara manusia yang terdeteksi.")
147
 
148
- # 3. AI MENCARI BAGIAN VIRAL (Memanggil fungsi SSE)
149
  JOBS[job_id].update({"message": "AI sedang meracik bagian viral...", "progress": 50})
150
  clip_data = get_ai_viral_clip(transcript_str)
151
 
152
- # Buffer waktu (agar pemotongan tidak terlalu kaku)
153
  s_time = max(0, float(clip_data['start_s']) - 0.2)
154
  e_time = float(clip_data['end_s']) + 0.3
155
 
@@ -157,11 +195,9 @@ def process_video(job_id):
157
  JOBS[job_id].update({"message": "Membuat efek subtitle viral...", "progress": 70})
158
  generate_srt(all_words, s_time, e_time, srt_path)
159
 
160
- # 5. POTONG & BURN SUBTITLE SANGAT CEPAT (FFmpeg murni)
161
  JOBS[job_id].update({"message": "Rendering Video Final (Super Cepat)...", "progress": 85})
162
 
163
- # Style Subtitle: Font Arial/Tebal, Warna Kuning, Outline Hitam Tebal, Tengah Bawah
164
- # Note: Penggunaan format path pada Windows untuk FFmpeg filter kadang tricky, ini dibersihkan
165
  safe_srt_path = srt_path.replace('\\', '/')
166
  style = "Fontname=Arial,Fontsize=24,PrimaryColour=&H0000FFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=2,Shadow=0,Alignment=2,MarginV=25,Bold=1"
167
 
@@ -189,14 +225,14 @@ def process_video(job_id):
189
  JOBS[job_id].update({"status": "error", "message": str(e)})
190
 
191
  finally:
192
- # Hapus file sementara langsung setelah selesai (hemat disk)
193
  cleanup_files(audio_path, srt_path)
194
 
195
- # Hapus video input dan output setelah 5 menit
196
  timer = threading.Timer(AUTO_DELETE_MINUTES * 60, cleanup_files, args=(path_in, out_path), kwargs={'job_id': job_id})
197
  timer.start()
198
 
199
- # --- WORKER LOOP ANTRIAN ---
200
  def queue_worker():
201
  while True:
202
  job_id = job_queue.get()
@@ -205,10 +241,12 @@ def queue_worker():
205
  process_video(job_id)
206
  job_queue.task_done()
207
 
208
- threading.Thread(target=queue_worker, daemon=True).start()
 
 
209
 
210
  # ==========================================
211
- # UI HTML (TETAP SAMA SEPERTI MILIK ANDA)
212
  # ==========================================
213
  HTML_TEMPLATE = """
214
  <!DOCTYPE html>
@@ -216,7 +254,7 @@ HTML_TEMPLATE = """
216
  <head>
217
  <meta charset="UTF-8">
218
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
219
- <title>AI Viral Clipper Ultra</title>
220
  <script src="https://cdn.tailwindcss.com"></script>
221
  </head>
222
  <body class="bg-black text-slate-200 min-h-screen flex flex-col items-center p-6">
@@ -246,7 +284,7 @@ HTML_TEMPLATE = """
246
  <div id="resultArea" class="mt-8 hidden text-center">
247
  <video id="player" controls class="w-full rounded-xl border border-slate-700 mb-4"></video>
248
  <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>
249
- <p class="text-xs text-red-400 mt-3 font-semibold">⚠️ Video akan dihapus otomatis dari server dalam 5 menit.</p>
250
  </div>
251
  </div>
252
 
@@ -325,7 +363,7 @@ def index():
325
  @app.route('/generate', methods=['POST'])
326
  def generate():
327
  if job_queue.full():
328
- return jsonify({"error": "Antrian penuh. Coba beberapa saat lagi."}), 429
329
 
330
  file = request.files.get('video_file')
331
  if not file: return jsonify({"error": "File kosong"}), 400
@@ -337,7 +375,7 @@ def generate():
337
  JOBS[job_id] = {
338
  "status": "queued",
339
  "progress": 5,
340
- "message": "Menunggu giliran...",
341
  "input_path": save_path
342
  }
343
 
@@ -365,4 +403,8 @@ def download(filename):
365
  return send_file(file_path, as_attachment=True)
366
 
367
  if __name__ == '__main__':
 
 
 
 
368
  app.run(host='0.0.0.0', port=7860, debug=False)
 
7
  import json
8
  import queue
9
  import subprocess
10
+ import multiprocessing
11
  from flask import Flask, request, jsonify, render_template_string, send_file
12
  from faster_whisper import WhisperModel
13
 
14
+ # --- CONFIG SERVER & ANTRIAN ---
15
  AI_API_URL = "https://puruboy-api.vercel.app/api/ai/notegpt"
16
  app = Flask(__name__)
17
  app.config['UPLOAD_FOLDER'] = 'downloads'
18
  app.config['MAX_CONTENT_LENGTH'] = 200 * 1024 * 1024 # Max 200 MB
19
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
20
 
21
+ MAX_QUEUE_SIZE = 20 # Maksimal antrian
22
+ MAX_WORKERS = 5 # Maksimal proses berjalan bersamaan
23
+ AUTO_DELETE_MINUTES = 1440 # Waktu file dihapus (1440 menit = 1 hari)
24
 
25
  JOBS = {}
26
  job_queue = queue.Queue(maxsize=MAX_QUEUE_SIZE)
27
 
28
+ # --- GLOBAL RATE LIMIT CONFIG ---
29
+ request_timestamps = []
30
+ global_lockout_until = 0
31
+
32
+ # --- OPTIMASI CPU WHISPER ---
33
+ # Mengatur thread CPU per model agar 5 worker tidak membuat CPU bertabrakan (bottleneck)
34
+ total_cores = multiprocessing.cpu_count()
35
+ threads_per_worker = max(1, total_cores // MAX_WORKERS)
36
+
37
+ print(f"Loading Whisper Model (CPU Cores/Worker: {threads_per_worker})...")
38
+ whisper_model = WhisperModel(
39
+ "base",
40
+ device="cpu",
41
+ compute_type="int8",
42
+ cpu_threads=threads_per_worker
43
+ )
44
  print("Model Loaded!")
45
 
46
  # ==========================================
47
+ # GLOBAL RATE LIMITER (Mencegah Spam)
48
+ # ==========================================
49
+ @app.before_request
50
+ def rate_limiter():
51
+ global request_timestamps, global_lockout_until
52
+
53
+ # Hanya batasi endpoint pembuatan video
54
+ if request.endpoint == 'generate' and request.method == 'POST':
55
+ current_time = time.time()
56
+
57
+ # Cek apakah sedang dalam masa hukuman 2 menit
58
+ if current_time < global_lockout_until:
59
+ return jsonify({"error": "demi keamanan kamu sengaja mematikan api ini selama 2 menit karena ada yang spam"}), 429
60
+
61
+ # Bersihkan timestamp yang sudah lebih dari 60 detik (1 menit)
62
+ request_timestamps = [t for t in request_timestamps if current_time - t < 60]
63
+
64
+ # Jika request lebih dari 30 dalam 1 menit
65
+ if len(request_timestamps) >= 30:
66
+ global_lockout_until = current_time + 120 # Kunci selama 120 detik (2 menit)
67
+ return jsonify({"error": "demi keamanan kamu sengaja mematikan api ini selama 2 menit karena ada yang spam"}), 429
68
+
69
+ request_timestamps.append(current_time)
70
+
71
+ # ==========================================
72
+ # FUNGSI PARSING AI (Server-Sent Events)
73
  # ==========================================
74
  def get_ai_viral_clip(transcript_str):
75
  payload = {
 
90
  }
91
 
92
  headers = {"Content-Type": "application/json"}
 
93
  response = requests.post(AI_API_URL, json=payload, headers=headers, stream=True)
94
 
95
  full_text = ""
 
107
  except json.JSONDecodeError:
108
  continue
109
 
 
110
  json_match = re.search(r'\{.*\}', full_text, re.DOTALL)
111
  if not json_match:
112
  raise Exception("Gagal mengekstrak format JSON dari AI.")
 
117
  # FUNGSI HELPER VIDEO & SUBTITLE
118
  # ==========================================
119
  def format_time_srt(seconds):
 
120
  hrs = int(seconds // 3600)
121
  mins = int((seconds % 3600) // 60)
122
  secs = int(seconds % 60)
 
124
  return f"{hrs:02d}:{mins:02d}:{secs:02d},{msec:03d}"
125
 
126
  def generate_srt(words, start_offset, end_offset, srt_path):
 
127
  with open(srt_path, 'w', encoding='utf-8') as f:
128
  idx = 1
129
  for w in words:
 
130
  if w.start >= start_offset and w.end <= end_offset:
 
131
  s_time = format_time_srt(w.start - start_offset)
132
  e_time = format_time_srt(w.end - start_offset)
133
  text = w.word.strip().upper()
 
134
  f.write(f"{idx}\n{s_time} --> {e_time}\n{text}\n\n")
135
  idx += 1
136
 
137
  def cleanup_files(*file_paths, job_id=None):
 
138
  for path in file_paths:
139
  if path and os.path.exists(path):
140
  try: os.remove(path)
141
  except: pass
142
  if job_id and job_id in JOBS:
143
+ del JOBS[job_id] # Hapus dari memory agar RAM tidak bengkak
144
  print(f"[{job_id}] Auto-cleanup selesai.")
145
 
146
  # ==========================================
 
154
  out_path = os.path.join(app.config['UPLOAD_FOLDER'], out_filename)
155
 
156
  try:
157
+ # 1. EKSTRAK AUDIO CEPAT
158
  JOBS[job_id].update({"message": "Mengekstrak audio video...", "progress": 10})
159
  subprocess.run(['ffmpeg', '-y', '-i', path_in, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000', '-ac', '1', audio_path],
160
  stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
161
 
162
+ # 2. TRANSCRIBE DENGAN OPTIMASI SUPER CEPAT (Untuk Video >20 Menit)
163
  JOBS[job_id].update({"message": "Menganalisa suara manusia (AI Whisper)...", "progress": 25})
164
+
165
+ # beam_size=1 dan condition_on_previous_text=False membuat proses 3x - 5x lebih cepat
166
+ segments, _ = whisper_model.transcribe(
167
+ audio_path,
168
+ word_timestamps=True,
169
+ vad_filter=True,
170
+ vad_parameters=dict(min_silence_duration_ms=500),
171
+ beam_size=1,
172
+ condition_on_previous_text=False
173
+ )
174
 
175
  transcript_for_ai = []
176
  all_words = []
 
184
  if not transcript_str.strip():
185
  raise Exception("Tidak ada suara manusia yang terdeteksi.")
186
 
187
+ # 3. AI MENCARI BAGIAN VIRAL
188
  JOBS[job_id].update({"message": "AI sedang meracik bagian viral...", "progress": 50})
189
  clip_data = get_ai_viral_clip(transcript_str)
190
 
 
191
  s_time = max(0, float(clip_data['start_s']) - 0.2)
192
  e_time = float(clip_data['end_s']) + 0.3
193
 
 
195
  JOBS[job_id].update({"message": "Membuat efek subtitle viral...", "progress": 70})
196
  generate_srt(all_words, s_time, e_time, srt_path)
197
 
198
+ # 5. POTONG & BURN SUBTITLE
199
  JOBS[job_id].update({"message": "Rendering Video Final (Super Cepat)...", "progress": 85})
200
 
 
 
201
  safe_srt_path = srt_path.replace('\\', '/')
202
  style = "Fontname=Arial,Fontsize=24,PrimaryColour=&H0000FFFF,OutlineColour=&H00000000,BorderStyle=1,Outline=2,Shadow=0,Alignment=2,MarginV=25,Bold=1"
203
 
 
225
  JOBS[job_id].update({"status": "error", "message": str(e)})
226
 
227
  finally:
228
+ # File sementara dihapus langsung
229
  cleanup_files(audio_path, srt_path)
230
 
231
+ # File final dihapus setelah 1 Hari (1440 menit)
232
  timer = threading.Timer(AUTO_DELETE_MINUTES * 60, cleanup_files, args=(path_in, out_path), kwargs={'job_id': job_id})
233
  timer.start()
234
 
235
+ # --- MULTI-WORKER SYSTEM (Menjalankan 5 Proses Sekaligus) ---
236
  def queue_worker():
237
  while True:
238
  job_id = job_queue.get()
 
241
  process_video(job_id)
242
  job_queue.task_done()
243
 
244
+ # Menjalankan 5 Thread Workers (Sehingga bisa render 5 video serentak)
245
+ for _ in range(MAX_WORKERS):
246
+ threading.Thread(target=queue_worker, daemon=True).start()
247
 
248
  # ==========================================
249
+ # UI HTML
250
  # ==========================================
251
  HTML_TEMPLATE = """
252
  <!DOCTYPE html>
 
254
  <head>
255
  <meta charset="UTF-8">
256
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
257
+ <title>AI Viral Clipper Ultra (Pro)</title>
258
  <script src="https://cdn.tailwindcss.com"></script>
259
  </head>
260
  <body class="bg-black text-slate-200 min-h-screen flex flex-col items-center p-6">
 
284
  <div id="resultArea" class="mt-8 hidden text-center">
285
  <video id="player" controls class="w-full rounded-xl border border-slate-700 mb-4"></video>
286
  <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>
287
+ <p class="text-xs text-red-400 mt-3 font-semibold">⚠️ Video akan dihapus otomatis dari server dalam 24 Jam.</p>
288
  </div>
289
  </div>
290
 
 
363
  @app.route('/generate', methods=['POST'])
364
  def generate():
365
  if job_queue.full():
366
+ return jsonify({"error": "Antrian sedang penuh (Maksimal 20). Coba beberapa saat lagi."}), 429
367
 
368
  file = request.files.get('video_file')
369
  if not file: return jsonify({"error": "File kosong"}), 400
 
375
  JOBS[job_id] = {
376
  "status": "queued",
377
  "progress": 5,
378
+ "message": "Menunggu giliran dalam antrian...",
379
  "input_path": save_path
380
  }
381
 
 
403
  return send_file(file_path, as_attachment=True)
404
 
405
  if __name__ == '__main__':
406
+ # Untuk level produksi sebenarnya, lebih baik disajikan via Waitress atau Gunicorn
407
+ # pip install waitress
408
+ # from waitress import serve
409
+ # serve(app, host="0.0.0.0", port=7860, threads=10)
410
  app.run(host='0.0.0.0', port=7860, debug=False)