Ricky01anjay commited on
Commit
181a914
·
verified ·
1 Parent(s): dbfd979

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +165 -177
app.py CHANGED
@@ -4,56 +4,44 @@ import threading
4
  import requests
5
  import re
6
  import time
7
- from flask import Flask, request, jsonify, render_template_string, send_file
8
  from werkzeug.utils import secure_filename
9
  from faster_whisper import WhisperModel
10
  from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip
11
 
12
  app = Flask(__name__)
13
  app.config['UPLOAD_FOLDER'] = 'downloads'
14
- app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # Max upload 500MB
15
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
16
 
17
  # In-memory database
18
  JOBS = {}
19
- JOB_TTL = 3600 # Waktu hidup file: 3600 detik (1 Jam)
20
 
21
- # Model AI Whisper (Dioptimalkan untuk CPU HuggingFace)
22
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
23
 
24
  # ==========================================
25
- # AUTO CLEANUP (Pembersih Sampah agar HuggingFace Tidak Penuh)
26
  # ==========================================
27
  def cleanup_old_jobs():
28
  while True:
29
- time.sleep(600) # Cek setiap 10 Menit
30
  now = time.time()
31
  keys_to_delete = []
32
-
33
  for job_id, job in JOBS.items():
34
  if now - job['created_at'] > JOB_TTL:
35
- print(f"[CLEANUP] Menghapus Job kedaluwarsa: {job_id}")
36
- # Hapus file fisik dari SSD Container
37
  for file_path in job.get('files_to_delete', []):
38
  if file_path and os.path.exists(file_path):
39
- try:
40
- os.remove(file_path)
41
- print(f"[CLEANUP] File dihapus: {file_path}")
42
- except Exception as e:
43
- print(f"[CLEANUP] Gagal hapus file: {e}")
44
-
45
  keys_to_delete.append(job_id)
46
-
47
- # Hapus data dari RAM
48
- for key in keys_to_delete:
49
- del JOBS[key]
50
 
51
- # Mulai thread pembersih otomatis
52
- cleanup_thread = threading.Thread(target=cleanup_old_jobs, daemon=True)
53
- cleanup_thread.start()
54
 
55
  # ==========================================
56
- # UI HTML (Tailwind & AJAX Polling)
57
  # ==========================================
58
  HTML_TEMPLATE = """
59
  <!DOCTYPE html>
@@ -61,71 +49,98 @@ HTML_TEMPLATE = """
61
  <head>
62
  <meta charset="UTF-8">
63
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
64
- <title>AI Auto Clipper Video</title>
65
  <script src="https://cdn.tailwindcss.com"></script>
 
 
 
 
 
 
 
 
 
66
  </head>
67
- <body class="bg-gray-900 text-white min-h-screen p-4 flex flex-col items-center">
68
- <div class="w-full max-w-md bg-gray-800 p-6 rounded-xl shadow-lg mt-10">
69
- <h1 class="text-2xl font-bold mb-4 text-center text-blue-400">AI Auto Clipper</h1>
 
70
 
71
- <form id="clipForm" class="space-y-4">
72
  <div>
73
- <label class="block text-sm font-medium mb-1">Direct URL Video (MP4)</label>
74
- <input type="url" id="videoUrl" class="w-full p-2 bg-gray-700 rounded border border-gray-600 focus:border-blue-500 outline-none" placeholder="https://domain.com/video.mp4">
 
 
 
 
 
75
  </div>
76
- <div class="text-center text-sm font-bold text-gray-400">ATAU</div>
77
  <div>
78
- <label class="block text-sm font-medium mb-1">Upload Video</label>
79
- <input type="file" id="videoFile" accept="video/mp4,video/webm" class="w-full p-2 bg-gray-700 rounded border border-gray-600">
80
  </div>
81
  <div>
82
- <label class="block text-sm font-medium mb-1">Target Durasi</label>
83
- <select id="duration" class="w-full p-2 bg-gray-700 rounded border border-gray-600">
84
- <option value="30">30 Detik (Shorts/Reels)</option>
85
- <option value="60">60 Detik</option>
86
- <option value="120">120 Detik</option>
87
  </select>
88
  </div>
89
- <button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition">Buat Clip!</button>
90
  </form>
91
 
92
- <div id="statusArea" class="mt-6 hidden">
93
- <h3 class="font-bold text-yellow-400 mb-2">Status Proses:</h3>
94
- <p id="statusText" class="text-sm bg-gray-700 p-3 rounded">Memulai...</p>
95
- <div class="w-full bg-gray-600 rounded-full h-2.5 mt-2">
96
- <div id="progressBar" class="bg-blue-500 h-2.5 rounded-full" style="width: 0%"></div>
 
 
 
97
  </div>
98
  </div>
99
 
100
- <div id="resultArea" class="mt-6 hidden text-center">
101
- <h3 class="font-bold text-green-400 mb-2">Selesai!</h3>
102
- <p class="text-xs text-gray-400 mb-3">Link berlaku selama 1 Jam</p>
103
- <a id="downloadBtn" href="#" class="inline-block bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded">Download Video</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  </div>
105
  </div>
106
 
107
  <script>
108
- document.getElementById('clipForm').addEventListener('submit', async (e) => {
 
109
  e.preventDefault();
110
  const formData = new FormData();
111
  formData.append('duration', document.getElementById('duration').value);
112
-
113
  const file = document.getElementById('videoFile').files[0];
114
  const url = document.getElementById('videoUrl').value;
115
 
116
  if (file) formData.append('video_file', file);
117
  else if (url) formData.append('video_url', url);
118
- else return alert("Masukkan URL atau Upload Video!");
119
 
120
  document.getElementById('statusArea').classList.remove('hidden');
121
- document.getElementById('clipForm').classList.add('hidden');
122
 
123
  const res = await fetch('/generate', { method: 'POST', body: formData });
124
  const data = await res.json();
125
-
126
- if (data.job_id) {
127
- pollStatus(data.job_id);
128
- }
129
  });
130
 
131
  async function pollStatus(jobId) {
@@ -135,16 +150,22 @@ HTML_TEMPLATE = """
135
 
136
  document.getElementById('statusText').innerText = data.message;
137
  document.getElementById('progressBar').style.width = data.progress + "%";
 
138
 
139
  if (data.status === 'completed') {
140
  clearInterval(interval);
141
  document.getElementById('statusArea').classList.add('hidden');
142
- document.getElementById('resultArea').classList.remove('hidden');
 
 
 
 
143
  document.getElementById('downloadBtn').href = `/download/${data.result}`;
 
 
144
  } else if (data.status === 'error') {
145
  clearInterval(interval);
146
- document.getElementById('statusText').innerText = "Error: " + data.message;
147
- document.getElementById('statusText').classList.add('text-red-400');
148
  }
149
  }, 3000);
150
  }
@@ -154,108 +175,101 @@ HTML_TEMPLATE = """
154
  """
155
 
156
  # ==========================================
157
- # HELPER FUNCTIONS
158
  # ==========================================
159
 
160
  def time_to_seconds(time_str):
161
  try:
162
- parts = time_str.split(':')
163
- return int(parts[0]) * 60 + int(parts[1])
164
- except:
165
- return 0
166
 
167
- def background_processor(job_id, video_path, duration):
168
  try:
169
- JOBS[job_id]['message'] = "Step 2: Melakukan Transkripsi (AI Whisper)..."
170
- JOBS[job_id]['progress'] = 20
 
171
 
172
- # Transkripsi Video
173
- segments, info = whisper_model.transcribe(video_path, word_timestamps=True)
174
- transcript = ""
 
175
  words_data = []
176
-
177
  for segment in segments:
178
  for word in segment.words:
179
  words_data.append(word)
180
- transcript += f"[{int(word.start//60):02d}:{int(word.start%60):02d}] {word.word.strip()}\n"
181
-
182
- JOBS[job_id]['message'] = "Step 3: Meminta AI menganalisis Hook terbaik..."
183
- JOBS[job_id]['progress'] = 50
184
 
185
- # Meminta LLM menganalisa clip
186
- prompt = f"Cari bagian paling menarik (hook bagus) durasi ~{duration} detik dari transkrip ini. Balas HANYA format 'MM:SS - MM:SS'.\n\nTranskrip:\n{transcript[:3000]}"
187
- api_url = "https://www.puruboy.kozow.com/api/ai/letmegpt"
188
- response = requests.post(api_url, json={"message": prompt})
189
 
190
- ai_result = response.json().get("result", {}).get("answer", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
- # Ekstrak waktu
193
- times = re.findall(r'(\d{1,2}:\d{2})', ai_result)
194
- if len(times) >= 2:
195
- start_sec = time_to_seconds(times[0])
196
- end_sec = time_to_seconds(times[1])
197
- else:
198
- start_sec = 0
199
- end_sec = int(duration)
200
-
201
- JOBS[job_id]['message'] = f"Step 4: Memotong & render ({times[0]} - {times[1]})..."
202
- JOBS[job_id]['progress'] = 70
203
-
204
- # Penyiapan output
205
  output_filename = f"clip_{job_id}.mp4"
206
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
207
-
208
- # Tambahkan output hasil ke daftar sampah yang akan dihapus nanti (1 Jam kemudian)
209
  JOBS[job_id]['files_to_delete'].append(output_path)
210
 
211
- # Potong & Crop Rasio 9:16 (Vertikal)
212
- clip = VideoFileClip(video_path).subclip(start_sec, end_sec)
213
  w, h = clip.size
214
  target_w = int(h * 9 / 16)
215
- x_center = w / 2
216
- cropped_clip = clip.crop(x1=x_center - target_w/2, y1=0, x2=x_center + target_w/2, y2=h)
217
 
218
- # Font Absolute Path (Sesuai dengan apt-get fonts-dejavu di Dockerfile)
219
  font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
 
220
 
221
- # Pembuatan Subtitle
222
  txt_clips = []
223
  for word in words_data:
224
  if word.start >= start_sec and word.end <= end_sec:
225
- rel_start = word.start - start_sec
226
  rel_end = word.end - start_sec
227
 
228
- txt = TextClip(
229
- word.word.strip(),
230
- font=font_path,
231
- fontsize=50,
232
- color='yellow',
233
- stroke_color='black',
234
- stroke_width=3
235
- )
236
- txt = txt.set_position(('center', 'center')).set_start(rel_start).set_end(rel_end)
237
- txt_clips.append(txt)
238
-
239
- final_video = CompositeVideoClip([cropped_clip] + txt_clips)
240
-
241
- # CPU Render Optimized
242
- final_video.write_videofile(output_path, fps=30, codec="libx264", audio_codec="aac", threads=4, preset="ultrafast", logger=None)
243
-
244
- # Tutup memori MoviePy
 
 
 
 
 
245
  clip.close()
246
- cropped_clip.close()
247
- final_video.close()
248
 
249
- # Update Status Selesai
250
- JOBS[job_id]['status'] = "completed"
251
- JOBS[job_id]['progress'] = 100
252
- JOBS[job_id]['message'] = "Video selesai dibuat!"
253
- JOBS[job_id]['result'] = output_filename
254
 
255
  except Exception as e:
256
- JOBS[job_id]['status'] = "error"
257
- JOBS[job_id]['message'] = str(e)
258
-
259
 
260
  # ==========================================
261
  # ROUTES
@@ -268,64 +282,38 @@ def index():
268
  @app.route('/generate', methods=['POST'])
269
  def generate():
270
  job_id = str(uuid.uuid4())
271
- duration = request.form.get('duration', '60')
272
- video_path = ""
273
-
274
- # Menangani Upload vs URL
275
- if 'video_file' in request.files and request.files['video_file'].filename != '':
276
- file = request.files['video_file']
277
- filename = secure_filename(file.filename)
278
- video_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_{filename}")
279
- file.save(video_path)
280
- elif 'video_url' in request.form and request.form['video_url'] != '':
281
- video_url = request.form['video_url']
282
- video_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_downloaded.mp4")
283
-
284
- # Download dengan sistem chunk (aman untuk RAM)
285
- r = requests.get(video_url, stream=True)
286
  with open(video_path, 'wb') as f:
287
- for chunk in r.iter_content(chunk_size=1024*1024):
288
- if chunk: f.write(chunk)
289
- else:
290
- return jsonify({"error": "Video tidak ditemukan"}), 400
291
 
292
- # Daftarkan Job (beserta tracker file mentah agar bisa dibersihkan 1 Jam ke depan)
293
  JOBS[job_id] = {
294
- "status": "processing",
295
- "progress": 10,
296
- "message": "Step 1: Video berhasil dimuat. Bersiap memproses...",
297
- "result": None,
298
- "created_at": time.time(),
299
- "files_to_delete": [video_path]
300
  }
301
-
302
- # Jalankan proses secara asynchronous
303
- thread = threading.Thread(target=background_processor, args=(job_id, video_path, duration))
304
- thread.start()
305
-
306
  return jsonify({"job_id": job_id})
307
 
308
  @app.route('/status/<job_id>')
309
  def status(job_id):
310
- job = JOBS.get(job_id)
311
- if not job:
312
- return jsonify({"status": "error", "message": "Job sudah kedaluwarsa atau tidak ditemukan"})
313
-
314
- # Sembunyikan field sensitif (seperti path/files_to_delete) saat kirim ke client UI
315
- return jsonify({
316
- "status": job['status'],
317
- "progress": job['progress'],
318
- "message": job['message'],
319
- "result": job['result']
320
- })
321
 
 
 
 
 
 
 
 
322
  @app.route('/download/<filename>')
323
  def download(filename):
324
- file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
325
- if not os.path.exists(file_path):
326
- return "File tidak ditemukan atau sudah dihapus server.", 404
327
- return send_file(file_path, as_attachment=True)
328
 
329
  if __name__ == '__main__':
330
- # Harus jalan di port 7860 untuk HuggingFace Spaces
331
- app.run(host='0.0.0.0', port=7860, debug=False)
 
4
  import requests
5
  import re
6
  import time
7
+ from flask import Flask, request, jsonify, render_template_string, send_file, url_for
8
  from werkzeug.utils import secure_filename
9
  from faster_whisper import WhisperModel
10
  from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip
11
 
12
  app = Flask(__name__)
13
  app.config['UPLOAD_FOLDER'] = 'downloads'
14
+ app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024
15
  os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
16
 
17
  # In-memory database
18
  JOBS = {}
19
+ JOB_TTL = 3600 # File dihapus setelah 1 jam
20
 
21
+ # Model AI Whisper (Optimasi CPU)
22
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
23
 
24
  # ==========================================
25
+ # AUTO CLEANUP
26
  # ==========================================
27
  def cleanup_old_jobs():
28
  while True:
29
+ time.sleep(600)
30
  now = time.time()
31
  keys_to_delete = []
 
32
  for job_id, job in JOBS.items():
33
  if now - job['created_at'] > JOB_TTL:
 
 
34
  for file_path in job.get('files_to_delete', []):
35
  if file_path and os.path.exists(file_path):
36
+ try: os.remove(file_path)
37
+ except: pass
 
 
 
 
38
  keys_to_delete.append(job_id)
39
+ for key in keys_to_delete: del JOBS[key]
 
 
 
40
 
41
+ threading.Thread(target=cleanup_old_jobs, daemon=True).start()
 
 
42
 
43
  # ==========================================
44
+ # UI HTML (With Video Player)
45
  # ==========================================
46
  HTML_TEMPLATE = """
47
  <!DOCTYPE html>
 
49
  <head>
50
  <meta charset="UTF-8">
51
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
52
+ <title>AI Video Clipper Player</title>
53
  <script src="https://cdn.tailwindcss.com"></script>
54
+ <style>
55
+ .video-container video {
56
+ max-height: 500px;
57
+ width: auto;
58
+ margin: 0 auto;
59
+ border-radius: 12px;
60
+ box-shadow: 0 10px 30px rgba(0,0,0,0.5);
61
+ }
62
+ </style>
63
  </head>
64
+ <body class="bg-gray-950 text-white min-h-screen p-4 flex flex-col items-center">
65
+ <div class="w-full max-w-lg bg-gray-900 p-8 rounded-3xl shadow-2xl mt-6 border border-gray-800">
66
+ <h1 class="text-3xl font-black mb-1 text-center bg-gradient-to-r from-blue-400 to-purple-500 bg-clip-text text-transparent">AI CLIPPER</h1>
67
+ <p class="text-center text-gray-500 text-sm mb-8">Potong Video Otomatis & Nonton Langsung</p>
68
 
69
+ <form id="clipForm" class="space-y-5">
70
  <div>
71
+ <label class="block text-xs font-bold text-gray-400 mb-2 tracking-widest">URL VIDEO (MP4)</label>
72
+ <input type="url" id="videoUrl" class="w-full p-3 bg-gray-800 rounded-xl border border-gray-700 outline-none focus:ring-2 focus:ring-blue-500 transition" placeholder="https://example.com/video.mp4">
73
+ </div>
74
+ <div class="relative flex py-2 items-center">
75
+ <div class="flex-grow border-t border-gray-800"></div>
76
+ <span class="flex-shrink mx-4 text-gray-600 text-xs font-bold">ATAU</span>
77
+ <div class="flex-grow border-t border-gray-800"></div>
78
  </div>
 
79
  <div>
80
+ <label class="block text-xs font-bold text-gray-400 mb-2 tracking-widest">UPLOAD DARI HP/PC</label>
81
+ <input type="file" id="videoFile" accept="video/mp4" class="w-full p-2 bg-gray-800 rounded-xl border border-gray-700 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-blue-600 file:text-white hover:file:bg-blue-700">
82
  </div>
83
  <div>
84
+ <label class="block text-xs font-bold text-gray-400 mb-2 tracking-widest">DURASI CLIP</label>
85
+ <select id="duration" class="w-full p-3 bg-gray-800 rounded-xl border border-gray-700 outline-none">
86
+ <option value="30">30 Detik (Shorts/TikTok)</option>
87
+ <option value="60">60 Detik (Instagram Reels)</option>
 
88
  </select>
89
  </div>
90
+ <button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-4 rounded-xl shadow-lg transition transform active:scale-95">PROSES VIDEO ✨</button>
91
  </form>
92
 
93
+ <!-- Status Area -->
94
+ <div id="statusArea" class="mt-8 hidden">
95
+ <div class="flex justify-between mb-2">
96
+ <span id="statusText" class="text-sm font-medium text-blue-400">Menyiapkan...</span>
97
+ <span id="percentText" class="text-sm font-bold">0%</span>
98
+ </div>
99
+ <div class="w-full bg-gray-800 rounded-full h-3">
100
+ <div id="progressBar" class="bg-blue-500 h-3 rounded-full transition-all duration-500" style="width: 0%"></div>
101
  </div>
102
  </div>
103
 
104
+ <!-- Result Area (Video Player) -->
105
+ <div id="resultArea" class="mt-8 hidden animate-fade-in">
106
+ <div class="video-container flex flex-col items-center">
107
+ <h2 class="text-green-400 font-bold mb-4 flex items-center gap-2">
108
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
109
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
110
+ </svg>
111
+ Video Siap Ditonton!
112
+ </h2>
113
+ <video id="mainPlayer" controls playsinline class="bg-black mb-6">
114
+ <source id="videoSource" src="" type="video/mp4">
115
+ Browser Anda tidak mendukung pemutar video.
116
+ </video>
117
+ <div class="flex gap-4 w-full">
118
+ <a id="downloadBtn" href="#" class="flex-1 text-center bg-gray-100 text-black font-bold py-3 rounded-xl hover:bg-white transition">Download</a>
119
+ <button onclick="location.reload()" class="flex-1 bg-gray-800 text-white font-bold py-3 rounded-xl border border-gray-700">Buat Lagi</button>
120
+ </div>
121
+ </div>
122
  </div>
123
  </div>
124
 
125
  <script>
126
+ const form = document.getElementById('clipForm');
127
+ form.addEventListener('submit', async (e) => {
128
  e.preventDefault();
129
  const formData = new FormData();
130
  formData.append('duration', document.getElementById('duration').value);
 
131
  const file = document.getElementById('videoFile').files[0];
132
  const url = document.getElementById('videoUrl').value;
133
 
134
  if (file) formData.append('video_file', file);
135
  else if (url) formData.append('video_url', url);
136
+ else return alert("Pilih file atau isi URL!");
137
 
138
  document.getElementById('statusArea').classList.remove('hidden');
139
+ form.classList.add('hidden');
140
 
141
  const res = await fetch('/generate', { method: 'POST', body: formData });
142
  const data = await res.json();
143
+ if (data.job_id) pollStatus(data.job_id);
 
 
 
144
  });
145
 
146
  async function pollStatus(jobId) {
 
150
 
151
  document.getElementById('statusText').innerText = data.message;
152
  document.getElementById('progressBar').style.width = data.progress + "%";
153
+ document.getElementById('percentText').innerText = data.progress + "%";
154
 
155
  if (data.status === 'completed') {
156
  clearInterval(interval);
157
  document.getElementById('statusArea').classList.add('hidden');
158
+
159
+ // Update Player
160
+ const videoUrl = `/view/${data.result}`;
161
+ document.getElementById('videoSource').src = videoUrl;
162
+ document.getElementById('mainPlayer').load();
163
  document.getElementById('downloadBtn').href = `/download/${data.result}`;
164
+
165
+ document.getElementById('resultArea').classList.remove('hidden');
166
  } else if (data.status === 'error') {
167
  clearInterval(interval);
168
+ alert("Gagal memproses: " + data.message);
 
169
  }
170
  }, 3000);
171
  }
 
175
  """
176
 
177
  # ==========================================
178
+ # PROCESSING LOGIC
179
  # ==========================================
180
 
181
  def time_to_seconds(time_str):
182
  try:
183
+ parts = list(map(int, time_str.split(':')))
184
+ return parts[0] * 60 + parts[1] if len(parts) == 2 else parts[0]
185
+ except: return 0
 
186
 
187
+ def background_processor(job_id, video_path, duration_limit):
188
  try:
189
+ # Cek durasi asli video
190
+ full_clip = VideoFileClip(video_path)
191
+ total_duration = full_clip.duration
192
 
193
+ # 1. Transkripsi
194
+ JOBS[job_id].update({"message": "Menganalisis audio (Whisper AI)...", "progress": 20})
195
+ segments, _ = whisper_model.transcribe(video_path, word_timestamps=True)
196
+
197
  words_data = []
198
+ transcript_text = ""
199
  for segment in segments:
200
  for word in segment.words:
201
  words_data.append(word)
202
+ transcript_text += f"[{int(word.start//60):02d}:{int(word.start%60):02d}] {word.word.strip()} "
 
 
 
203
 
204
+ # 2. Cari Hook via AI External
205
+ JOBS[job_id].update({"message": "AI sedang mencari momen viral...", "progress": 45})
206
+ prompt = f"Cari bagian paling menarik durasi {duration_limit} detik dari transkrip ini. Balas HANYA format 'MM:SS - MM:SS'.\n\n{transcript_text[:2500]}"
 
207
 
208
+ try:
209
+ r = requests.post("https://www.puruboy.kozow.com/api/ai/letmegpt", json={"message": prompt}, timeout=15).json()
210
+ ai_ans = r.get("result", {}).get("answer", "")
211
+ times = re.findall(r'(\d{1,2}:\d{2})', ai_ans)
212
+ raw_start = time_to_seconds(times[0]) if len(times) >= 2 else 0
213
+ raw_end = time_to_seconds(times[1]) if len(times) >= 2 else int(duration_limit)
214
+ except:
215
+ raw_start, raw_end = 0, int(duration_limit)
216
+
217
+ # --- LOGIKA BONUS DETIK (PADDING) ---
218
+ start_sec = max(0, raw_start - 1.5) # Bonus 1.5 detik di awal
219
+ end_sec = min(total_duration, raw_end + 2.5) # Bonus 2.5 detik di akhir
220
+ # ------------------------------------
221
+
222
+ JOBS[job_id].update({"message": f"Rendering ({int(end_sec - start_sec)} detik)...", "progress": 65})
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  output_filename = f"clip_{job_id}.mp4"
225
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
 
 
226
  JOBS[job_id]['files_to_delete'].append(output_path)
227
 
228
+ # 3. Potong & Crop 9:16
229
+ clip = full_clip.subclip(start_sec, end_sec)
230
  w, h = clip.size
231
  target_w = int(h * 9 / 16)
232
+ cropped = clip.crop(x_center=w/2, width=min(w, target_w), height=h)
 
233
 
234
+ # 4. Subtitle di Bagian Bawah
235
  font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
236
+ if not os.path.exists(font_path): font_path = "Arial"
237
 
 
238
  txt_clips = []
239
  for word in words_data:
240
  if word.start >= start_sec and word.end <= end_sec:
241
+ rel_start = max(0, word.start - start_sec)
242
  rel_end = word.end - start_sec
243
 
244
+ t = TextClip(
245
+ word.word.strip().upper(),
246
+ font=font_path, fontsize=h*0.055,
247
+ color='yellow', stroke_color='black', stroke_width=2,
248
+ method='label'
249
+ ).set_start(rel_start).set_end(rel_end).set_position(('center', int(h*0.8)))
250
+
251
+ txt_clips.append(t)
252
+
253
+ # 5. Gabungkan & Simpan (Format Browser-Friendly)
254
+ final = CompositeVideoClip([cropped] + txt_clips)
255
+ final.write_videofile(
256
+ output_path,
257
+ fps=24,
258
+ codec="libx264", # Codec standar browser
259
+ audio_codec="aac",
260
+ preset="ultrafast",
261
+ threads=4,
262
+ logger=None
263
+ )
264
+
265
+ full_clip.close()
266
  clip.close()
267
+ final.close()
 
268
 
269
+ JOBS[job_id].update({"status": "completed", "progress": 100, "message": "Selesai!", "result": output_filename})
 
 
 
 
270
 
271
  except Exception as e:
272
+ JOBS[job_id].update({"status": "error", "message": str(e)})
 
 
273
 
274
  # ==========================================
275
  # ROUTES
 
282
  @app.route('/generate', methods=['POST'])
283
  def generate():
284
  job_id = str(uuid.uuid4())
285
+ duration = request.form.get('duration', '30')
286
+ video_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_in.mp4")
287
+
288
+ if 'video_file' in request.files:
289
+ request.files['video_file'].save(video_path)
290
+ elif 'video_url' in request.form:
291
+ r = requests.get(request.form['video_url'], stream=True)
 
 
 
 
 
 
 
 
292
  with open(video_path, 'wb') as f:
293
+ for chunk in r.iter_content(1024*1024): f.write(chunk)
294
+ else: return jsonify({"error": "No source"}), 400
 
 
295
 
 
296
  JOBS[job_id] = {
297
+ "status": "processing", "progress": 10, "message": "Video diterima...",
298
+ "result": None, "created_at": time.time(), "files_to_delete": [video_path]
 
 
 
 
299
  }
300
+ threading.Thread(target=background_processor, args=(job_id, video_path, duration)).start()
 
 
 
 
301
  return jsonify({"job_id": job_id})
302
 
303
  @app.route('/status/<job_id>')
304
  def status(job_id):
305
+ return jsonify(JOBS.get(job_id, {"status": "error", "message": "Job expired"}))
 
 
 
 
 
 
 
 
 
 
306
 
307
+ # Route untuk menonton video langsung di player
308
+ @app.route('/view/<filename>')
309
+ def view_video(filename):
310
+ file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
311
+ return send_file(file_path, mimetype='video/mp4')
312
+
313
+ # Route untuk download video
314
  @app.route('/download/<filename>')
315
  def download(filename):
316
+ return send_file(os.path.join(app.config['UPLOAD_FOLDER'], filename), as_attachment=True)
 
 
 
317
 
318
  if __name__ == '__main__':
319
+ app.run(host='0.0.0.0', port=7860)