Ricky01anjay commited on
Commit
32d7542
·
verified ·
1 Parent(s): 3915846

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +98 -65
app.py CHANGED
@@ -26,37 +26,37 @@ HTML_TEMPLATE = """
26
  <head>
27
  <meta charset="UTF-8">
28
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
- <title>AI Multi-Clip Editor</title>
30
  <script src="https://cdn.tailwindcss.com"></script>
31
  </head>
32
- <body class="bg-black text-gray-200 min-h-screen flex flex-col items-center p-6">
33
  <div class="w-full max-w-2xl bg-gray-900 p-8 rounded-3xl border border-gray-800 shadow-2xl">
34
- <h1 class="text-3xl font-black text-center mb-2 bg-gradient-to-r from-blue-500 to-cyan-400 bg-clip-text text-transparent">AI VIDEO COMPOSER</h1>
35
- <p class="text-center text-gray-500 text-sm mb-8">Gabungkan momen terbaik secara otomatis</p>
36
 
37
  <form id="clipForm" class="space-y-6">
38
- <input type="url" id="videoUrl" class="w-full p-4 bg-gray-800 rounded-xl border border-gray-700 outline-none focus:ring-2 focus:ring-blue-500" placeholder="Masukkan Link Video MP4...">
39
  <div class="text-center font-bold text-gray-700 text-xs tracking-widest">ATAU UPLOAD FILE</div>
40
- <input type="file" id="videoFile" accept="video/mp4" class="w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-blue-600 file:text-white hover:file:bg-blue-700">
41
 
42
- <button type="submit" class="w-full bg-blue-600 hover:bg-blue-500 text-white font-black py-4 rounded-xl transition-all transform active:scale-95 shadow-xl">MULAI PROSES AI ✨</button>
43
  </form>
44
 
45
  <div id="statusArea" class="mt-10 hidden">
46
  <div class="flex justify-between mb-2">
47
- <span id="statusText" class="text-sm font-bold text-blue-400 italic">Menganalisis...</span>
48
  <span id="percentText" class="text-sm font-black">0%</span>
49
  </div>
50
  <div class="w-full bg-gray-800 rounded-full h-4 p-1">
51
- <div id="progressBar" class="bg-gradient-to-r from-blue-600 to-cyan-400 h-2 rounded-full transition-all duration-700" style="width: 0%"></div>
52
  </div>
53
  </div>
54
 
55
  <div id="resultArea" class="mt-10 hidden text-center">
56
- <video id="player" controls class="w-full rounded-2xl border border-gray-700 shadow-2xl bg-black mb-6" style="max-height: 400px;"></video>
57
  <div class="flex gap-3">
58
- <a id="downloadBtn" href="#" class="flex-1 bg-white text-black font-black py-3 rounded-xl">DOWNLOAD</a>
59
- <button onclick="location.reload()" class="px-6 bg-gray-800 rounded-xl border border-gray-700">ULANG</button>
60
  </div>
61
  </div>
62
  </div>
@@ -112,16 +112,19 @@ HTML_TEMPLATE = """
112
  def time_to_seconds(time_str):
113
  try:
114
  parts = list(map(int, time_str.split(':')))
115
- return parts[0] * 60 + parts[1] if len(parts) == 2 else parts[0]
 
 
116
  except: return 0
117
 
118
  def background_processor(job_id, video_path):
119
  try:
120
  full_clip = VideoFileClip(video_path)
121
  original_width, original_height = full_clip.size
 
122
 
123
- # 1. Transkripsi
124
- JOBS[job_id].update({"message": "AI mentranskrip video...", "progress": 15})
125
  segments_whisper, _ = whisper_model.transcribe(video_path, word_timestamps=True)
126
 
127
  words_data = []
@@ -129,45 +132,62 @@ def background_processor(job_id, video_path):
129
  for segment in segments_whisper:
130
  for word in segment.words:
131
  words_data.append(word)
132
- transcript_text += f"[{int(word.start//60):02d}:{int(word.start%60):02d}] {word.word.strip()} "
133
-
134
- # 2. AI Menganalisa Poin Penting (Format JSON)
135
- JOBS[job_id].update({"message": "AI menganalisis konten (JSON)...", "progress": 40})
136
- prompt = (
137
- "Analyze this transcript and find multiple interesting segments. "
138
- "Respond ONLY with a JSON array like this: "
139
- '[{"narasi": "context", "timestamp": "MM:SS"}, {"narasi": "next context", "timestamp": "MM:SS"}]'
140
- "\n\nTranscript: " + transcript_text[:3000]
141
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
  try:
144
- r = requests.post("https://www.puruboy.kozow.com/api/ai/letmegpt", json={"message": prompt}, timeout=20).json()
145
  ai_ans = r.get("result", {}).get("answer", "[]")
146
- # Ekstrak JSON jika AI memberi teks tambahan
147
- json_match = re.search(r'\[.*\]', ai_ans, re.DOTALL)
 
148
  points = json.loads(json_match.group(0)) if json_match else []
149
  except:
150
- points = [{"narasi": "Auto", "timestamp": "00:05"}]
 
 
 
 
151
 
152
- # 3. Hitung Range Pemotongan (Logika Request User)
153
- JOBS[job_id].update({"message": "Menghitung sinkronisasi pemotongan...", "progress": 55})
154
  cut_ranges = []
 
155
  for p in points:
156
- start_point = time_to_seconds(p['timestamp'])
 
157
 
158
- # Cari subtitle berikutnya setelah start_point
159
- next_subtitle_time = start_point + 10 # Default jika tidak ketemu
160
- for wd in words_data:
161
- if wd.start > start_point:
162
- next_subtitle_time = wd.start
163
- break
164
 
165
- # Berhenti di next_subtitle_time + 10 detik (Sesuai logika 2:45 -> 2:55)
166
- end_point = min(full_clip.duration, next_subtitle_time + 10)
167
- cut_ranges.append({'start': max(0, start_point - 1), 'end': end_point})
168
 
169
  # 4. Pemotongan & Penggabungan
170
- JOBS[job_id].update({"message": "Rendering klip gabungan...", "progress": 70})
171
 
172
  subclips = []
173
  final_words_clips = []
@@ -177,46 +197,56 @@ def background_processor(job_id, video_path):
177
  if not os.path.exists(font_path): font_path = "Arial"
178
 
179
  for r in cut_ranges:
180
- # Potong video
181
  v_sub = full_clip.subclip(r['start'], r['end'])
182
  subclips.append(v_sub)
183
 
184
- # Buat Subtitle untuk klip ini dengan timeline yang digeser (shifted)
185
  for wd in words_data:
186
- if r['start'] <= wd.start <= r['end']:
187
- rel_start = (wd.start - r['start']) + current_timeline
188
- rel_end = (wd.end - r['start']) + current_timeline
 
 
189
 
190
  t = TextClip(
191
  wd.word.strip().upper(),
192
- font=font_path, fontsize=original_height*0.06,
193
- color='yellow', stroke_color='black', stroke_width=2,
 
 
 
194
  method='label'
195
  ).set_start(rel_start).set_end(rel_end).set_position(('center', int(original_height*0.85)))
196
 
197
  final_words_clips.append(t)
198
 
 
199
  current_timeline += (r['end'] - r['start'])
200
 
201
- # Gabungkan video
202
  merged_video = concatenate_videoclips(subclips, method="compose")
203
 
204
- # Tambahkan Overlay Subtitle
205
  final_result = CompositeVideoClip([merged_video] + final_words_clips)
206
 
207
  output_filename = f"final_{job_id}.mp4"
208
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
209
 
 
 
 
210
  final_result.write_videofile(
211
  output_path,
212
  fps=24,
213
  codec="libx264",
214
  audio_codec="aac",
215
  preset="ultrafast",
 
216
  logger=None
217
  )
218
 
219
- # Cleanup
220
  full_clip.close()
221
  merged_video.close()
222
  final_result.close()
@@ -224,16 +254,15 @@ def background_processor(job_id, video_path):
224
  JOBS[job_id].update({
225
  "status": "completed",
226
  "progress": 100,
227
- "message": "Video Berhasil Digabung!",
228
- "result": output_filename,
229
- "files_to_delete": JOBS[job_id]['files_to_delete'] + [output_path]
230
  })
231
 
232
  except Exception as e:
233
  JOBS[job_id].update({"status": "error", "message": str(e)})
234
 
235
  # ==========================================
236
- # ROUTES
237
  # ==========================================
238
 
239
  @app.route('/')
@@ -245,24 +274,28 @@ def generate():
245
  job_id = str(uuid.uuid4())
246
  video_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_in.mp4")
247
 
248
- if 'video_file' in request.files:
249
- request.files['video_file'].save(video_path)
250
- elif 'video_url' in request.form:
251
- r = requests.get(request.form['video_url'], stream=True)
252
- with open(video_path, 'wb') as f:
253
- for chunk in r.iter_content(1024*1024): f.write(chunk)
254
- else: return jsonify({"error": "No source"}), 400
 
 
 
255
 
256
  JOBS[job_id] = {
257
- "status": "processing", "progress": 5, "message": "Mengunggah...",
258
  "result": None, "created_at": time.time(), "files_to_delete": [video_path]
259
  }
 
260
  threading.Thread(target=background_processor, args=(job_id, video_path)).start()
261
  return jsonify({"job_id": job_id})
262
 
263
  @app.route('/status/<job_id>')
264
  def status(job_id):
265
- return jsonify(JOBS.get(job_id, {"status": "error", "message": "Expired"}))
266
 
267
  @app.route('/view/<filename>')
268
  def view_video(filename):
 
26
  <head>
27
  <meta charset="UTF-8">
28
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>AI Pro Clip Editor</title>
30
  <script src="https://cdn.tailwindcss.com"></script>
31
  </head>
32
+ <body class="bg-gray-950 text-gray-200 min-h-screen flex flex-col items-center p-6">
33
  <div class="w-full max-w-2xl bg-gray-900 p-8 rounded-3xl border border-gray-800 shadow-2xl">
34
+ <h1 class="text-3xl font-black text-center mb-2 bg-gradient-to-r from-green-400 to-blue-500 bg-clip-text text-transparent">AI SMART CLIPPER</h1>
35
+ <p class="text-center text-gray-500 text-sm mb-8">Memotong video dengan logika penceritaan utuh</p>
36
 
37
  <form id="clipForm" class="space-y-6">
38
+ <input type="url" id="videoUrl" class="w-full p-4 bg-gray-800 rounded-xl border border-gray-700 outline-none focus:ring-2 focus:ring-green-500" placeholder="Masukkan Link Video MP4...">
39
  <div class="text-center font-bold text-gray-700 text-xs tracking-widest">ATAU UPLOAD FILE</div>
40
+ <input type="file" id="videoFile" accept="video/mp4" class="w-full text-sm text-gray-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:bg-green-600 file:text-white hover:file:bg-green-700">
41
 
42
+ <button type="submit" class="w-full bg-green-600 hover:bg-green-500 text-white font-black py-4 rounded-xl transition-all transform active:scale-95 shadow-xl">BUAT VIDEO VIRAL ✨</button>
43
  </form>
44
 
45
  <div id="statusArea" class="mt-10 hidden">
46
  <div class="flex justify-between mb-2">
47
+ <span id="statusText" class="text-sm font-bold text-green-400 italic">Menganalisis...</span>
48
  <span id="percentText" class="text-sm font-black">0%</span>
49
  </div>
50
  <div class="w-full bg-gray-800 rounded-full h-4 p-1">
51
+ <div id="progressBar" class="bg-gradient-to-r from-green-500 to-blue-500 h-2 rounded-full transition-all duration-700" style="width: 0%"></div>
52
  </div>
53
  </div>
54
 
55
  <div id="resultArea" class="mt-10 hidden text-center">
56
+ <video id="player" controls class="w-full rounded-2xl border border-gray-700 shadow-2xl bg-black mb-6" style="max-height: 450px;"></video>
57
  <div class="flex gap-3">
58
+ <a id="downloadBtn" href="#" class="flex-1 bg-white text-black font-black py-3 rounded-xl hover:bg-gray-200 transition">DOWNLOAD HD</a>
59
+ <button onclick="location.reload()" class="px-6 bg-gray-800 rounded-xl border border-gray-700 hover:bg-gray-700">BUAT LAGI</button>
60
  </div>
61
  </div>
62
  </div>
 
112
  def time_to_seconds(time_str):
113
  try:
114
  parts = list(map(int, time_str.split(':')))
115
+ if len(parts) == 3: return parts[0] * 3600 + parts[1] * 60 + parts[2]
116
+ if len(parts) == 2: return parts[0] * 60 + parts[1]
117
+ return parts[0]
118
  except: return 0
119
 
120
  def background_processor(job_id, video_path):
121
  try:
122
  full_clip = VideoFileClip(video_path)
123
  original_width, original_height = full_clip.size
124
+ total_duration = full_clip.duration
125
 
126
+ # 1. Transkripsi Detail
127
+ JOBS[job_id].update({"message": "AI mentranskrip & membaca waktu...", "progress": 15})
128
  segments_whisper, _ = whisper_model.transcribe(video_path, word_timestamps=True)
129
 
130
  words_data = []
 
132
  for segment in segments_whisper:
133
  for word in segment.words:
134
  words_data.append(word)
135
+ # Buat transkrip per kalimat untuk AI agar konteksnya jelas
136
+ transcript_text += f"[{int(segment.start//60):02d}:{int(segment.start%60):02d} - {int(segment.end//60):02d}:{int(segment.end%60):02d}] {segment.text.strip()}\n"
137
+
138
+ # 2. AI Prompt Khusus Editor Video
139
+ JOBS[job_id].update({"message": "Menyusun skenario cerita (AI Editor)...", "progress": 35})
140
+
141
+ # PROMPT SUPER: Memaksa AI memberikan start dan end yang utuh
142
+ prompt = f"""Kamu adalah Editor Video Viral Profesional.
143
+ Tugasmu adalah memotong transkrip video ini menjadi 1 hingga 3 klip pendek yang jika digabungkan akan menjadi satu cerita yang sangat menarik, nyambung, dan tidak terpotong di tengah kalimat.
144
+
145
+ Syarat Wajib:
146
+ 1. Pastikan setiap potongan berisi kalimat yang UTUH (Selesai titik/koma).
147
+ 2. Gabungan potongan ini harus nyambung ceritanya (contoh: Hook -> Isi Masalah -> Punchline/Kesimpulan).
148
+ 3. Balas HANYA dengan format JSON murni, tanpa teks lain di luar JSON.
149
+
150
+ Format JSON yang diwajibkan:
151
+ [
152
+ {{"start": "MM:SS", "end": "MM:SS", "narasi": "Alasan memilih klip ini"}},
153
+ {{"start": "MM:SS", "end": "MM:SS", "narasi": "Alasan klip selanjutnya"}}
154
+ ]
155
+
156
+ Transkrip Video:
157
+ {transcript_text[:3500]}
158
+ """
159
 
160
  try:
161
+ r = requests.post("https://www.puruboy.kozow.com/api/ai/letmegpt", json={"message": prompt}, timeout=25).json()
162
  ai_ans = r.get("result", {}).get("answer", "[]")
163
+
164
+ # Ekstraksi ketat JSON
165
+ json_match = re.search(r'\[\s*\{.*?\}\s*\]', ai_ans, re.DOTALL)
166
  points = json.loads(json_match.group(0)) if json_match else []
167
  except:
168
+ # Fallback jika AI API error
169
+ points = [{"start": "00:00", "end": "00:15", "narasi": "Fallback awal"}]
170
+
171
+ if not points:
172
+ raise Exception("AI gagal merumuskan JSON. Silakan coba lagi.")
173
 
174
+ # 3. Sinkronisasi Pemotongan Presisi
175
+ JOBS[job_id].update({"message": "Merapikan potongan video...", "progress": 55})
176
  cut_ranges = []
177
+
178
  for p in points:
179
+ raw_start = time_to_seconds(p.get('start', '00:00'))
180
+ raw_end = time_to_seconds(p.get('end', '00:10'))
181
 
182
+ # Berikan Buffer/Padding Halus (0.5 Detik) agar suara napas/ujung kata tidak terpotong kasar
183
+ start_sec = max(0, raw_start - 0.5)
184
+ end_sec = min(total_duration, raw_end + 0.5)
 
 
 
185
 
186
+ if start_sec < end_sec:
187
+ cut_ranges.append({'start': start_sec, 'end': end_sec})
 
188
 
189
  # 4. Pemotongan & Penggabungan
190
+ JOBS[job_id].update({"message": "Merender gabungan klip...", "progress": 70})
191
 
192
  subclips = []
193
  final_words_clips = []
 
197
  if not os.path.exists(font_path): font_path = "Arial"
198
 
199
  for r in cut_ranges:
200
+ # Subclip Video Asli
201
  v_sub = full_clip.subclip(r['start'], r['end'])
202
  subclips.append(v_sub)
203
 
204
+ # Buat Subtitle per kata dengan penyesuaian timeline
205
  for wd in words_data:
206
+ # Cek kata yang masuk dalam rentang klip ini
207
+ if wd.start >= (r['start'] - 0.5) and wd.end <= (r['end'] + 0.5):
208
+ # Geser waktu subtitle agar cocok dengan waktu video yang sudah digabung
209
+ rel_start = max(0, wd.start - r['start']) + current_timeline
210
+ rel_end = max(0, wd.end - r['start']) + current_timeline
211
 
212
  t = TextClip(
213
  wd.word.strip().upper(),
214
+ font=font_path,
215
+ fontsize=original_height*0.06, # Dinamis menyesuaikan resolusi
216
+ color='yellow',
217
+ stroke_color='black',
218
+ stroke_width=2,
219
  method='label'
220
  ).set_start(rel_start).set_end(rel_end).set_position(('center', int(original_height*0.85)))
221
 
222
  final_words_clips.append(t)
223
 
224
+ # Tambahkan durasi klip ke timeline untuk klip selanjutnya
225
  current_timeline += (r['end'] - r['start'])
226
 
227
+ # Gabungkan klip-klip yang sudah dipilih AI
228
  merged_video = concatenate_videoclips(subclips, method="compose")
229
 
230
+ # Tempelkan semua subtitle yang sudah disinkronkan ke video gabungan
231
  final_result = CompositeVideoClip([merged_video] + final_words_clips)
232
 
233
  output_filename = f"final_{job_id}.mp4"
234
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
235
 
236
+ JOBS[job_id]['files_to_delete'].append(output_path)
237
+
238
+ # Proses Rendering Final
239
  final_result.write_videofile(
240
  output_path,
241
  fps=24,
242
  codec="libx264",
243
  audio_codec="aac",
244
  preset="ultrafast",
245
+ threads=4,
246
  logger=None
247
  )
248
 
249
+ # Bersihkan Memory RAM
250
  full_clip.close()
251
  merged_video.close()
252
  final_result.close()
 
254
  JOBS[job_id].update({
255
  "status": "completed",
256
  "progress": 100,
257
+ "message": "Video Siap Ditonton!",
258
+ "result": output_filename
 
259
  })
260
 
261
  except Exception as e:
262
  JOBS[job_id].update({"status": "error", "message": str(e)})
263
 
264
  # ==========================================
265
+ # ROUTES & SERVER START
266
  # ==========================================
267
 
268
  @app.route('/')
 
274
  job_id = str(uuid.uuid4())
275
  video_path = os.path.join(app.config['UPLOAD_FOLDER'], f"{job_id}_in.mp4")
276
 
277
+ try:
278
+ if 'video_file' in request.files:
279
+ request.files['video_file'].save(video_path)
280
+ elif 'video_url' in request.form:
281
+ r = requests.get(request.form['video_url'], stream=True)
282
+ with open(video_path, 'wb') as f:
283
+ for chunk in r.iter_content(1024*1024): f.write(chunk)
284
+ else: return jsonify({"error": "Video tidak ditemukan"}), 400
285
+ except Exception as e:
286
+ return jsonify({"error": str(e)}), 500
287
 
288
  JOBS[job_id] = {
289
+ "status": "processing", "progress": 5, "message": "Membaca Video...",
290
  "result": None, "created_at": time.time(), "files_to_delete": [video_path]
291
  }
292
+
293
  threading.Thread(target=background_processor, args=(job_id, video_path)).start()
294
  return jsonify({"job_id": job_id})
295
 
296
  @app.route('/status/<job_id>')
297
  def status(job_id):
298
+ return jsonify(JOBS.get(job_id, {"status": "error", "message": "Job Expired/Tidak valid"}))
299
 
300
  @app.route('/view/<filename>')
301
  def view_video(filename):