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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +68 -82
app.py CHANGED
@@ -7,6 +7,7 @@ import time
7
  import json
8
  from flask import Flask, request, jsonify, render_template_string, send_file
9
  from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip, concatenate_videoclips
 
10
  from faster_whisper import WhisperModel
11
 
12
  app = Flask(__name__)
@@ -18,7 +19,7 @@ JOBS = {}
18
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
19
 
20
  # ==========================================
21
- # UI HTML
22
  # ==========================================
23
  HTML_TEMPLATE = """
24
  <!DOCTYPE html>
@@ -26,13 +27,13 @@ HTML_TEMPLATE = """
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...">
@@ -120,11 +121,11 @@ def time_to_seconds(time_str):
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,111 +133,103 @@ def background_processor(job_id, video_path):
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 = []
194
- current_timeline = 0
195
 
196
- font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
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",
@@ -246,23 +239,17 @@ Transkrip Video:
246
  logger=None
247
  )
248
 
249
- # Bersihkan Memory RAM
250
  full_clip.close()
251
  merged_video.close()
252
- final_result.close()
253
 
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('/')
@@ -289,7 +276,6 @@ def generate():
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
 
 
7
  import json
8
  from flask import Flask, request, jsonify, render_template_string, send_file
9
  from moviepy.editor import VideoFileClip, TextClip, CompositeVideoClip, concatenate_videoclips
10
+ from moviepy.audio.fx.all import audio_fadein, audio_fadeout
11
  from faster_whisper import WhisperModel
12
 
13
  app = Flask(__name__)
 
19
  whisper_model = WhisperModel("base", device="cpu", compute_type="int8")
20
 
21
  # ==========================================
22
+ # UI HTML (Tetap sama, elegan & modern)
23
  # ==========================================
24
  HTML_TEMPLATE = """
25
  <!DOCTYPE html>
 
27
  <head>
28
  <meta charset="UTF-8">
29
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
30
+ <title>AI Pro Clip Editor - Smooth Edit</title>
31
  <script src="https://cdn.tailwindcss.com"></script>
32
  </head>
33
  <body class="bg-gray-950 text-gray-200 min-h-screen flex flex-col items-center p-6">
34
  <div class="w-full max-w-2xl bg-gray-900 p-8 rounded-3xl border border-gray-800 shadow-2xl">
35
+ <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 SMOOTH CLIPPER</h1>
36
+ <p class="text-center text-gray-500 text-sm mb-8">Pemotongan akurat pada jeda napas & kata</p>
37
 
38
  <form id="clipForm" class="space-y-6">
39
  <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...">
 
121
  def background_processor(job_id, video_path):
122
  try:
123
  full_clip = VideoFileClip(video_path)
124
+ original_height = full_clip.h
125
  total_duration = full_clip.duration
126
 
127
+ # 1. Transkripsi dengan akurasi per kata
128
+ JOBS[job_id].update({"message": "AI mentranskrip & memetakan jeda...", "progress": 15})
129
  segments_whisper, _ = whisper_model.transcribe(video_path, word_timestamps=True)
130
 
131
  words_data = []
 
133
  for segment in segments_whisper:
134
  for word in segment.words:
135
  words_data.append(word)
 
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 Editor Memilih Klip
139
+ JOBS[job_id].update({"message": "Menyusun cerita yang nyambung...", "progress": 35})
140
 
141
+ prompt = f"""Kamu adalah Video Editor. Tugasmu memilih 2-3 segmen berdurasi agak panjang (minimal 10-15 detik per klip) dari video ini agar membentuk satu cerita utuh.
142
+ JANGAN memilih klip yang terlalu pendek agar video tidak terlihat patah-patah.
 
143
 
144
+ Syarat:
145
+ 1. Setiap segmen HARUS kalimat utuh.
146
+ 2. Ceritanya harus nyambung dari awal sampai akhir.
147
+ 3. Balas HANYA dengan JSON array:
 
 
148
  [
149
+ {{"start": "MM:SS", "end": "MM:SS", "narasi": "Alasan"}},
150
+ {{"start": "MM:SS", "end": "MM:SS", "narasi": "Alasan"}}
151
  ]
152
 
153
+ Transkrip:
154
  {transcript_text[:3500]}
155
  """
 
156
  try:
157
  r = requests.post("https://www.puruboy.kozow.com/api/ai/letmegpt", json={"message": prompt}, timeout=25).json()
158
  ai_ans = r.get("result", {}).get("answer", "[]")
 
 
159
  json_match = re.search(r'\[\s*\{.*?\}\s*\]', ai_ans, re.DOTALL)
160
  points = json.loads(json_match.group(0)) if json_match else []
161
  except:
162
+ points = [{"start": "00:00", "end": "00:15", "narasi": "Fallback"}]
 
163
 
164
+ if not points: raise Exception("AI gagal merumuskan JSON.")
 
165
 
166
+ # 3. LOGIKA SMART BOUNDARY (Pemotongan Akurat di Jeda Kata)
167
+ JOBS[job_id].update({"message": "Menghaluskan titik potong audio...", "progress": 55})
 
168
 
169
+ final_clips_to_concat = []
170
+ font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
171
+ if not os.path.exists(font_path): font_path = "Arial"
172
+
173
  for p in points:
174
  raw_start = time_to_seconds(p.get('start', '00:00'))
175
+ raw_end = time_to_seconds(p.get('end', '00:15'))
176
 
177
+ # Cari kata aktual yang paling dekat dengan pilihan AI
178
+ start_word = next((w for w in words_data if w.start >= max(0, raw_start - 2)), None)
179
+ end_word = next((w for w in reversed(words_data) if w.end <= min(total_duration, raw_end + 2)), None)
180
+
181
+ if start_word and end_word and start_word.start < end_word.end:
182
+ # Beri ruang napas 0.3 detik sebelum dan sesudah kata diucapkan
183
+ actual_start = max(0, start_word.start - 0.3)
184
+ actual_end = min(total_duration, end_word.end + 0.3)
185
+ else:
186
+ actual_start = max(0, raw_start - 0.3)
187
+ actual_end = min(total_duration, raw_end + 0.3)
188
+
189
+ # Potong Video Berdasarkan Kata (Bukan Angka Mentah AI)
190
+ subclip = full_clip.subclip(actual_start, actual_end)
191
 
192
+ # Terapkan AUDIO FADE IN & OUT agar perpindahan tidak "Klik/Cekrek"
193
+ subclip = subclip.set_audio(
194
+ subclip.audio.fx(audio_fadein, 0.1).fx(audio_fadeout, 0.1)
195
+ )
 
 
 
 
 
196
 
197
+ # Tambahkan Subtitle HANYA untuk klip ini (Sinkronisasi 100% sempurna)
198
+ txt_clips = []
 
 
 
 
 
 
 
199
  for wd in words_data:
200
+ if wd.start >= actual_start and wd.end <= actual_end:
201
+ rel_start = wd.start - actual_start
202
+ rel_end = wd.end - actual_start
 
 
203
 
204
  t = TextClip(
205
  wd.word.strip().upper(),
206
  font=font_path,
207
+ fontsize=original_height * 0.06,
208
  color='yellow',
209
  stroke_color='black',
210
  stroke_width=2,
211
  method='label'
212
+ ).set_start(rel_start).set_end(rel_end).set_position(('center', int(original_height * 0.85)))
213
 
214
+ txt_clips.append(t)
215
+
216
+ # Gabungkan Video + Subtitle untuk segmen ini
217
+ composite_subclip = CompositeVideoClip([subclip] + txt_clips)
218
+ final_clips_to_concat.append(composite_subclip)
219
 
220
+ # 4. GABUNGKAN SEMUA KLIP YANG SUDAH JADI
221
+ JOBS[job_id].update({"message": "Merender video akhir...", "progress": 70})
222
 
223
+ if not final_clips_to_concat:
224
+ raise Exception("Gagal memotong video. Durasi tidak valid.")
225
+
226
+ merged_video = concatenate_videoclips(final_clips_to_concat, method="compose")
227
 
228
  output_filename = f"final_{job_id}.mp4"
229
  output_path = os.path.join(app.config['UPLOAD_FOLDER'], output_filename)
 
230
  JOBS[job_id]['files_to_delete'].append(output_path)
231
 
232
+ merged_video.write_videofile(
 
233
  output_path,
234
  fps=24,
235
  codec="libx264",
 
239
  logger=None
240
  )
241
 
 
242
  full_clip.close()
243
  merged_video.close()
244
+ for c in final_clips_to_concat: c.close()
245
 
246
+ JOBS[job_id].update({"status": "completed", "progress": 100, "message": "Video Selesai!", "result": output_filename})
 
 
 
 
 
247
 
248
  except Exception as e:
249
  JOBS[job_id].update({"status": "error", "message": str(e)})
250
 
251
  # ==========================================
252
+ # ROUTES
253
  # ==========================================
254
 
255
  @app.route('/')
 
276
  "status": "processing", "progress": 5, "message": "Membaca Video...",
277
  "result": None, "created_at": time.time(), "files_to_delete": [video_path]
278
  }
 
279
  threading.Thread(target=background_processor, args=(job_id, video_path)).start()
280
  return jsonify({"job_id": job_id})
281