testdeep123 commited on
Commit
c7600a9
Β·
verified Β·
1 Parent(s): e827af3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +644 -258
app.py CHANGED
@@ -1,77 +1,92 @@
1
- # Import necessary libraries
2
- import os
3
- import re
4
- import random
5
- import math
6
- import shutil
7
- import tempfile
8
- import requests
9
- from urllib.parse import quote
10
- from bs4 import BeautifulSoup
11
- import numpy as np
12
- from PIL import Image
13
- import cv2
14
- from pydub import AudioSegment
15
- from gtts import gTTS
16
- import gradio as gr
17
  import soundfile as sf
18
- from moviepy.editor import (
19
- VideoFileClip, concatenate_videoclips, AudioFileClip, ImageClip,
20
- CompositeVideoClip, TextClip, CompositeAudioClip
21
- )
22
  import moviepy.video.fx.all as vfx
23
  import moviepy.config as mpy_config
 
 
 
 
 
 
 
 
 
 
24
 
25
- # Initialize Kokoro TTS pipeline (assuming it's available; replace with dummy if not)
26
- try:
27
- from kokoro import KPipeline
28
- pipeline = KPipeline(lang_code='a')
29
- except ImportError:
30
- pipeline = None # Fallback to gTTS if Kokoro is unavailable
31
-
32
- # Ensure ImageMagick binary is set (adjust path as needed for your system)
33
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
34
 
35
- # Global Configuration
36
  PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
37
  OPENROUTER_API_KEY = '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'
38
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
39
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
40
  USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
41
- MAX_CLIPS = 10 # Maximum number of editable clips
42
-
43
- # Temporary folder setup
44
- TEMP_FOLDER = tempfile.mkdtemp()
45
 
46
- # Helper Functions
 
 
 
47
 
48
- ## Script Generation
49
  def generate_script(user_input):
 
50
  headers = {
51
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
52
  'HTTP-Referer': 'https://your-domain.com',
53
  'X-Title': 'AI Documentary Maker'
54
  }
55
  prompt = f"""Short Documentary Script GeneratorInstructions:
 
56
  If I say "use this," just output the script exactly as I gave it.
57
  If I only give topics, generate a script based on them.
58
  If I provide a full script, rewrite it without any changes. Make everything short simple and humarous funny and act as serious but humarous. And don't say anything off topic. Also alway say a funny statement to subscribe based on the video topic at the end. Use normal conversational text like a normal person talking and avoid AI phase make the statements humanize and normal conversational
59
  And always talk negative views don't really wants any positive statement and the full script will be one video about one topic so keep in mind
60
  Formatting Rules:
 
61
  Title in Square Brackets:
 
62
  Each section starts with a one-word title inside [ ] (max two words if necessary).
63
  This title will be used as a search term for Pexels footage.
 
64
  Casual & Funny Narration:
 
65
  Each section has 5-10 words of narration.
66
  Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
 
67
  No Special Formatting:
 
68
  No bold, italics, or special characters. You are a assistant AI your task is to create script. You aren't a chatbot. So, don't write extra text
 
69
  Generalized Search Terms:
 
70
  If a term is too specific, make it more general for Pexels search.
 
71
  Scene-Specific Writing:
 
72
  Each section describes only what should be shown in the video.
73
- Output Only the Script, and also make it funny and humarous and helirous and also add to subscribe with a funny statement like subscribe now or .....
 
 
74
  No extra text, just the script.
 
75
  Example Output:
76
  [North Korea]
77
  Top 5 unknown facts about North Korea.
@@ -101,333 +116,704 @@ Now here is the Topic/scrip: {user_input}
101
  timeout=30
102
  )
103
  if response.status_code == 200:
104
- return response.json()['choices'][0]['message']['content']
 
 
 
 
 
105
  else:
106
  print(f"API Error {response.status_code}: {response.text}")
107
- return "Failed to generate script due to API error."
108
  except Exception as e:
109
  print(f"Request failed: {str(e)}")
110
- return "Oops, script generation broke. Blame the internet!"
111
 
112
  def parse_script(script_text):
 
 
 
 
113
  sections = {}
114
  current_title = None
115
  current_text = ""
116
- for line in script_text.splitlines():
117
- line = line.strip()
118
- if line.startswith("[") and "]" in line:
119
- bracket_start = line.find("[")
120
- bracket_end = line.find("]", bracket_start)
121
- if bracket_start != -1 and bracket_end != -1:
122
- if current_title:
123
- sections[current_title] = current_text.strip()
124
- current_title = line[bracket_start+1:bracket_end]
125
- current_text = line[bracket_end+1:].strip()
126
- elif current_title:
127
- current_text += line + " "
128
- if current_title:
129
- sections[current_title] = current_text.strip()
130
- clips = [{"title": title, "narration": narration} for title, narration in sections.items()]
131
- return clips
132
-
133
- ## Media Fetching
134
- def search_pexels_videos(query, pexels_api_key):
135
- headers = {'Authorization': pexels_api_key}
136
- url = "https://api.pexels.com/videos/search"
137
- params = {"query": query, "per_page": 15}
138
  try:
139
- response = requests.get(url, headers=headers, params=params, timeout=10)
140
- if response.status_code == 200:
141
- videos = response.json().get("videos", [])
142
- hd_videos = [v["video_files"][0]["link"] for v in videos if v["video_files"] and v["video_files"][0]["quality"] == "hd"]
143
- return random.choice(hd_videos) if hd_videos else None
144
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
  except Exception as e:
146
- print(f"Video search error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  return None
148
 
149
  def search_pexels_images(query, pexels_api_key):
 
150
  headers = {'Authorization': pexels_api_key}
151
  url = "https://api.pexels.com/v1/search"
152
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
153
- try:
154
- response = requests.get(url, headers=headers, params=params, timeout=10)
155
- if response.status_code == 200:
156
- photos = response.json().get("photos", [])
157
- return random.choice(photos)["src"]["original"] if photos else None
158
- return None
159
- except Exception as e:
160
- print(f"Image search error: {e}")
161
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  def search_google_images(query):
164
- search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch"
165
- headers = {"User-Agent": USER_AGENT}
166
  try:
 
 
167
  response = requests.get(search_url, headers=headers, timeout=10)
168
  soup = BeautifulSoup(response.text, "html.parser")
169
- img_urls = [img["src"] for img in soup.find_all("img") if img.get("src", "").startswith("http") and "gstatic" not in img["src"]]
170
- return random.choice(img_urls[:5]) if img_urls else None
 
 
 
 
 
 
 
 
 
171
  except Exception as e:
172
- print(f"Google Images error: {e}")
173
  return None
174
 
175
  def download_image(image_url, filename):
 
176
  try:
177
- response = requests.get(image_url, headers={"User-Agent": USER_AGENT}, stream=True, timeout=15)
 
 
178
  response.raise_for_status()
179
  with open(filename, 'wb') as f:
180
  for chunk in response.iter_content(chunk_size=8192):
181
  f.write(chunk)
182
- img = Image.open(filename)
183
- if img.mode != 'RGB':
184
- img = img.convert('RGB')
185
- img.save(filename)
186
- return filename
 
 
 
 
 
 
 
 
 
 
187
  except Exception as e:
188
  print(f"Image download error: {e}")
 
 
189
  return None
190
 
191
  def download_video(video_url, filename):
 
192
  try:
193
  response = requests.get(video_url, stream=True, timeout=30)
194
  response.raise_for_status()
195
  with open(filename, 'wb') as f:
196
  for chunk in response.iter_content(chunk_size=8192):
197
  f.write(chunk)
 
198
  return filename
199
  except Exception as e:
200
  print(f"Video download error: {e}")
 
 
201
  return None
202
 
203
- ## Media and TTS Generation
204
- def generate_media(prompt, custom_media=None, video_prob=0.25):
205
- if isinstance(custom_media, str) and os.path.exists(custom_media):
206
- asset_type = "video" if custom_media.lower().endswith(('.mp4', '.avi', '.mov')) else "image"
207
- return {"path": custom_media, "asset_type": asset_type}
 
208
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
 
 
 
 
 
209
  if "news" in prompt.lower():
 
210
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_news.jpg")
211
  image_url = search_google_images(prompt)
212
- if image_url and download_image(image_url, image_file):
213
- return {"path": image_file, "asset_type": "image"}
214
- if random.random() < video_prob:
 
 
215
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
216
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
217
- if video_url and download_video(video_url, video_file):
218
- return {"path": video_file, "asset_type": "video"}
 
 
219
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
220
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
221
- if image_url and download_image(image_url, image_file):
222
- return {"path": image_file, "asset_type": "image"}
223
- print(f"No media generated for prompt: {prompt}")
 
 
 
 
 
 
 
 
 
 
224
  return None
225
 
226
- def generate_tts(text, voice='en'):
227
- if not text.strip():
228
- return None
 
 
 
 
 
 
 
229
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
230
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
231
  if os.path.exists(file_path):
232
  return file_path
233
  try:
234
- if pipeline:
235
- audio_segments = [audio for _, _, audio in pipeline(text, voice='af_heart', speed=0.9, split_pattern=r'\n+')]
236
- full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
237
- sf.write(file_path, full_audio, 24000)
238
- else:
239
- tts = gTTS(text=text, lang=voice)
 
 
 
 
 
240
  mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
241
  tts.save(mp3_path)
242
- AudioSegment.from_mp3(mp3_path).export(file_path, format="wav")
 
243
  os.remove(mp3_path)
244
- return file_path
245
- except Exception as e:
246
- print(f"TTS generation failed: {e}")
247
- return None
248
 
249
- ## Video Processing
250
- def apply_kenburns_effect(clip, target_resolution):
251
  target_w, target_h = target_resolution
252
  clip_aspect = clip.w / clip.h
253
  target_aspect = target_w / target_h
254
  if clip_aspect > target_aspect:
255
- new_width = int(target_h * clip_aspect)
256
- clip = clip.resize(width=new_width)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  else:
258
- new_height = int(target_w / clip_aspect)
259
- clip = clip.resize(height=new_height)
260
- clip = clip.resize(zoom=1.15)
261
- return vfx.crop(clip.fx(vfx.resize, width=target_w * 1.1), width=target_w, height=target_h, x_center=clip.w / 2, y_center=clip.h / 2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
  def resize_to_fill(clip, target_resolution):
 
264
  target_w, target_h = target_resolution
265
  clip_aspect = clip.w / clip.h
266
  target_aspect = target_w / target_h
267
  if clip_aspect > target_aspect:
268
  clip = clip.resize(height=target_h)
269
  crop_amount = (clip.w - target_w) / 2
270
- clip = clip.crop(x1=crop_amount, x2=clip.w - crop_amount)
271
  else:
272
  clip = clip.resize(width=target_w)
273
  crop_amount = (clip.h - target_h) / 2
274
- clip = clip.crop(y1=crop_amount, y2=clip.h - crop_amount)
275
  return clip
276
 
277
- def add_background_music(final_video, bgm_path=None, bgm_volume=0.15):
278
- if bgm_path and os.path.exists(bgm_path):
279
- bg_music = AudioFileClip(bgm_path)
280
- if bg_music.duration < final_video.duration:
281
- loops_needed = math.ceil(final_video.duration / bg_music.duration)
282
- bg_music = concatenate_audioclips([bg_music] * loops_needed)
283
- bg_music = bg_music.subclip(0, final_video.duration).volumex(bgm_volume)
284
- mixed_audio = CompositeAudioClip([final_video.audio, bg_music])
285
- return final_video.set_audio(mixed_audio)
286
- return final_video
287
-
288
- def create_clip(media_path, asset_type, tts_path, narration_text, target_resolution, subtitles_enabled, font, font_size, outline_width, font_color, outline_color, position, zoom_pan_effect):
289
- if not media_path or not os.path.exists(media_path) or (tts_path and not os.path.exists(tts_path)):
290
- print("Missing media or TTS file")
291
- return None
292
- audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2) if tts_path else None
293
- target_duration = audio_clip.duration + 0.2 if audio_clip else 5.0
294
- if asset_type == "video":
295
- clip = VideoFileClip(media_path)
296
- clip = resize_to_fill(clip, target_resolution)
297
- clip = clip.loop(duration=target_duration) if clip.duration < target_duration else clip.subclip(0, target_duration)
298
- else:
299
- clip = ImageClip(media_path).set_duration(target_duration).fadein(0.3).fadeout(0.3)
300
- if zoom_pan_effect:
301
- clip = apply_kenburns_effect(clip, target_resolution)
302
- clip = resize_to_fill(clip, target_resolution)
303
- if subtitles_enabled and narration_text and audio_clip:
304
- words = narration_text.split()
305
- chunks = [' '.join(words[i:i+5]) for i in range(0, len(words), 5)]
306
- chunk_duration = audio_clip.duration / max(len(chunks), 1)
307
- subtitle_clips = []
308
- y_position = target_resolution[1] * (0.1 if position == "top" else 0.8 if position == "bottom" else 0.5)
309
- for i, chunk in enumerate(chunks):
310
- txt_clip = TextClip(
311
- chunk,
312
- fontsize=font_size,
313
- font=font,
314
- color=font_color,
315
- stroke_color=outline_color,
316
- stroke_width=outline_width,
317
- method='caption',
318
- align='center',
319
- size=(target_resolution[0] * 0.8, None)
320
- ).set_start(i * chunk_duration).set_end((i + 1) * chunk_duration).set_position(('center', y_position))
321
- subtitle_clips.append(txt_clip)
322
- clip = CompositeVideoClip([clip] + subtitle_clips)
323
- if audio_clip:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  clip = clip.set_audio(audio_clip)
325
- return clip
 
 
 
326
 
327
- ## Main Video Generation Function
328
- def generate_video(resolution, render_speed, video_clip_percent, zoom_pan_effect, bgm_upload, bgm_volume, subtitles_enabled, font, font_size, outline_width, font_color, outline_color, position, *clip_data):
329
- target_resolution = (1080, 1920) if resolution == "Short (1080x1920)" else (1920, 1080)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
  clips = []
331
- for i in range(0, len(clip_data), 3):
332
- prompt, narration, custom_media = clip_data[i], clip_data[i+1], clip_data[i+2]
333
- if prompt.strip() or narration.strip():
334
- media_asset = generate_media(prompt, custom_media, video_clip_percent / 100.0)
335
- if not media_asset:
336
- continue
337
- tts_path = generate_tts(narration) if narration.strip() else None
338
- clip = create_clip(
339
- media_asset['path'], media_asset['asset_type'], tts_path, narration, target_resolution,
340
- subtitles_enabled, font, font_size, outline_width, font_color, outline_color, position, zoom_pan_effect
341
- )
342
- if clip:
343
- clips.append(clip)
 
 
 
 
 
 
344
  if not clips:
345
- print("No clips generated.")
346
  return None
347
  final_video = concatenate_videoclips(clips, method="compose")
348
- final_video = add_background_music(final_video, bgm_upload, bgm_volume)
349
- final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=24, preset=render_speed, logger=None)
350
  shutil.rmtree(TEMP_FOLDER)
351
  return OUTPUT_VIDEO_FILENAME
352
 
353
- ## Load Clips Function
354
- def load_clips(topic, script):
355
- raw_script = script.strip() if script.strip() else generate_script(topic)
356
- clips = parse_script(raw_script)[:MAX_CLIPS]
357
- updates = [gr.update(value=raw_script)]
358
- for i in range(MAX_CLIPS):
359
- if i < len(clips):
360
- updates.extend([
361
- gr.update(value=clips[i]["title"]),
362
- gr.update(value=clips[i]["narration"]),
363
- gr.update(value=None) # Clear custom media
364
- ])
365
- else:
366
- updates.extend([
367
- gr.update(value=""),
368
- gr.update(value=""),
369
- gr.update(value=None)
370
- ])
371
- return updates
372
-
373
- # Gradio Interface
374
- with gr.Blocks(title="πŸš€ Orbit Video Engine") as app:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  with gr.Row():
376
  # Column 1: Content Input & Script Generation
377
- with gr.Column():
378
- gr.Markdown("### 1. Content Input")
379
- topic_input = gr.Textbox(label="Video Topic", placeholder="e.g., Funny Cat Facts")
380
- script_input = gr.Textbox(label="Or Paste Full Script", lines=10, placeholder="[Title]\nNarration...")
381
  generate_script_btn = gr.Button("πŸ“ Generate Script & Load Clips")
382
- generated_script_display = gr.Textbox(label="Generated Script", interactive=False, lines=10)
383
-
384
- # Column 2: Clip Editor
385
- with gr.Column():
386
- gr.Markdown("### 2. Edit Clips")
387
- gr.Markdown("Modify prompts, narration, and upload custom media for each clip.")
388
- prompts, narrations, custom_medias = [], [], []
389
- for i in range(MAX_CLIPS):
390
- with gr.Row(): # Always visible
391
- prompt = gr.Textbox(label=f"Clip {i+1} Visual Prompt")
392
- narration = gr.Textbox(label=f"Clip {i+1} Narration", lines=3)
393
- custom_media = gr.File(label=f"Clip {i+1} Upload Custom Media (Image/Video)", file_types=["image", "video"], value=None)
394
- prompts.append(prompt)
395
- narrations.append(narration)
396
- custom_medias.append(custom_media)
397
-
398
  # Column 3: Settings & Output
399
- with gr.Column():
400
- gr.Markdown("### 3. Video Settings")
401
- resolution = gr.Radio(["Short (1080x1920)", "Full HD (1920x1080)"], label="Resolution", value="Full HD (1920x1080)")
402
- render_speed = gr.Dropdown(["ultrafast", "veryfast", "fast", "medium", "slow", "veryslow"], label="Render Speed", value="fast")
403
- video_clip_percent = gr.Slider(0, 100, value=25, label="Video Clip Percentage")
404
- zoom_pan_effect = gr.Checkbox(label="Add Zoom/Pan Effect (Images)", value=True)
 
 
405
  with gr.Accordion("Background Music", open=False):
406
- bgm_upload = gr.Audio(label="Upload Background Music", type="filepath")
407
- bgm_volume = gr.Slider(0.0, 1.0, value=0.15, label="BGM Volume")
408
  with gr.Accordion("Subtitle Settings", open=True):
409
- subtitles_enabled = gr.Checkbox(label="Enable Subtitles", value=True)
410
- font = gr.Dropdown(["Impact", "Arial", "Times New Roman"], label="Font", value="Arial")
 
411
  font_size = gr.Number(label="Font Size", value=45)
412
  outline_width = gr.Number(label="Outline Width", value=2)
413
  font_color = gr.ColorPicker(label="Font Color", value="#FFFFFF")
414
  outline_color = gr.ColorPicker(label="Outline Color", value="#000000")
415
- position = gr.Radio(["top", "center", "bottom"], label="Position", value="bottom")
 
416
  generate_video_btn = gr.Button("🎬 Generate Video")
417
- gr.Markdown("### 4. Output")
418
- video_output = gr.Video(label="Generated Video")
419
-
420
- # Event Handlers
421
  generate_script_btn.click(
422
- fn=load_clips,
423
- inputs=[topic_input, script_input],
424
- outputs=[generated_script_display] + prompts + narrations + custom_medias
 
 
 
 
 
 
 
 
 
 
 
 
425
  )
426
  generate_video_btn.click(
427
- fn=generate_video,
428
- inputs=[resolution, render_speed, video_clip_percent, zoom_pan_effect, bgm_upload, bgm_volume, subtitles_enabled, font, font_size, outline_width, font_color, outline_color, position] + prompts + narrations + custom_medias,
429
- outputs=video_output
 
 
430
  )
431
-
432
- # Launch the app
433
- app.launch(share=True)
 
1
+ """
2
+ Full Code: Orbit Video Engine with Advanced Gradio UI
3
+
4
+ This script combines the video-generation code (using Kokoro for TTS,
5
+ MoviePy for video operations, Pexels/Google image/video search, etc.) with a
6
+ Gradio Blocks UI that allows:
7
+ 1. Content input and script generation.
8
+ 2. Dynamic clip editing (change prompt, narration, or upload custom media).
9
+ 3. Video settings (resolution, render speed, background music, subtitle settings).
10
+ 4. Final video generation and preview/download.
11
+ """
12
+
13
+ # --------- IMPORTS ---------
14
+ from kokoro import KPipeline
15
+
 
16
  import soundfile as sf
17
+ import torch
18
+ import os, time, random, math, json, tempfile, shutil, re, requests
19
+ from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips, CompositeVideoClip, TextClip, CompositeAudioClip
 
20
  import moviepy.video.fx.all as vfx
21
  import moviepy.config as mpy_config
22
+ from pydub import AudioSegment
23
+ from pydub.generators import Sine
24
+ from PIL import Image, ImageDraw, ImageFont
25
+ import numpy as np
26
+ from bs4 import BeautifulSoup
27
+ from urllib.parse import quote
28
+ import pysrt
29
+ from gtts import gTTS
30
+ import cv2
31
+ import gradio as gr
32
 
33
+ # --------- GLOBAL CONFIGURATION ---------
34
+ pipeline = KPipeline(lang_code='a') # Use American English voice. (Uses 'af_heart' for American English)
 
 
 
 
 
 
35
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
36
 
 
37
  PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
38
  OPENROUTER_API_KEY = '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'
39
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
40
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
41
  USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
 
 
 
 
42
 
43
+ # These globals are later set per run
44
+ TARGET_RESOLUTION = None
45
+ CAPTION_COLOR = None
46
+ TEMP_FOLDER = None
47
 
48
+ # --------- HELPER FUNCTIONS ---------
49
  def generate_script(user_input):
50
+ """Generate documentary script using OpenRouter API."""
51
  headers = {
52
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
53
  'HTTP-Referer': 'https://your-domain.com',
54
  'X-Title': 'AI Documentary Maker'
55
  }
56
  prompt = f"""Short Documentary Script GeneratorInstructions:
57
+
58
  If I say "use this," just output the script exactly as I gave it.
59
  If I only give topics, generate a script based on them.
60
  If I provide a full script, rewrite it without any changes. Make everything short simple and humarous funny and act as serious but humarous. And don't say anything off topic. Also alway say a funny statement to subscribe based on the video topic at the end. Use normal conversational text like a normal person talking and avoid AI phase make the statements humanize and normal conversational
61
  And always talk negative views don't really wants any positive statement and the full script will be one video about one topic so keep in mind
62
  Formatting Rules:
63
+
64
  Title in Square Brackets:
65
+
66
  Each section starts with a one-word title inside [ ] (max two words if necessary).
67
  This title will be used as a search term for Pexels footage.
68
+
69
  Casual & Funny Narration:
70
+
71
  Each section has 5-10 words of narration.
72
  Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
73
+
74
  No Special Formatting:
75
+
76
  No bold, italics, or special characters. You are a assistant AI your task is to create script. You aren't a chatbot. So, don't write extra text
77
+
78
  Generalized Search Terms:
79
+
80
  If a term is too specific, make it more general for Pexels search.
81
+
82
  Scene-Specific Writing:
83
+
84
  Each section describes only what should be shown in the video.
85
+
86
+ Output Only the Script, and also make it funny and humarous and helirous and also add to subscribe with a funny statement like subscribe now or .....
87
+
88
  No extra text, just the script.
89
+
90
  Example Output:
91
  [North Korea]
92
  Top 5 unknown facts about North Korea.
 
116
  timeout=30
117
  )
118
  if response.status_code == 200:
119
+ response_data = response.json()
120
+ if 'choices' in response_data and len(response_data['choices']) > 0:
121
+ return response_data['choices'][0]['message']['content']
122
+ else:
123
+ print("Unexpected response format:", response_data)
124
+ return None
125
  else:
126
  print(f"API Error {response.status_code}: {response.text}")
127
+ return None
128
  except Exception as e:
129
  print(f"Request failed: {str(e)}")
130
+ return None
131
 
132
  def parse_script(script_text):
133
+ """
134
+ Parse the generated script into a list of clip elements.
135
+ Each clip consists of a media element (with a 'prompt') and a TTS element with narration.
136
+ """
137
  sections = {}
138
  current_title = None
139
  current_text = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  try:
141
+ for line in script_text.splitlines():
142
+ line = line.strip()
143
+ if line.startswith("[") and "]" in line:
144
+ bracket_start = line.find("[")
145
+ bracket_end = line.find("]", bracket_start)
146
+ if bracket_start != -1 and bracket_end != -1:
147
+ if current_title is not None:
148
+ sections[current_title] = current_text.strip()
149
+ current_title = line[bracket_start+1:bracket_end]
150
+ current_text = line[bracket_end+1:].strip()
151
+ elif current_title:
152
+ current_text += line + " "
153
+ if current_title:
154
+ sections[current_title] = current_text.strip()
155
+ elements = []
156
+ for title, narration in sections.items():
157
+ if not title or not narration:
158
+ continue
159
+ media_element = {"type": "media", "prompt": title, "effects": "fade-in"}
160
+ words = narration.split()
161
+ duration = max(3, len(words) * 0.5)
162
+ tts_element = {"type": "tts", "text": narration, "voice": "en", "duration": duration}
163
+ elements.append(media_element)
164
+ elements.append(tts_element)
165
+ return elements
166
  except Exception as e:
167
+ print(f"Error parsing script: {e}")
168
+ return []
169
+
170
+ def search_pexels_videos(query, pexels_api_key):
171
+ """Search for a video on Pexels by query and return a random HD video link."""
172
+ headers = {'Authorization': pexels_api_key}
173
+ base_url = "https://api.pexels.com/videos/search"
174
+ num_pages = 3
175
+ videos_per_page = 15
176
+ max_retries = 3
177
+ retry_delay = 1
178
+ search_query = query
179
+ all_videos = []
180
+ for page in range(1, num_pages + 1):
181
+ for attempt in range(max_retries):
182
+ try:
183
+ params = {"query": search_query, "per_page": videos_per_page, "page": page}
184
+ response = requests.get(base_url, headers=headers, params=params, timeout=10)
185
+ if response.status_code == 200:
186
+ data = response.json()
187
+ videos = data.get("videos", [])
188
+ if not videos:
189
+ print(f"No videos found on page {page}.")
190
+ break
191
+ for video in videos:
192
+ video_files = video.get("video_files", [])
193
+ for file in video_files:
194
+ if file.get("quality") == "hd":
195
+ all_videos.append(file.get("link"))
196
+ break
197
+ break
198
+ elif response.status_code == 429:
199
+ print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying...")
200
+ time.sleep(retry_delay)
201
+ retry_delay *= 2
202
+ else:
203
+ print(f"Error fetching videos: {response.status_code} {response.text}")
204
+ if attempt < max_retries - 1:
205
+ time.sleep(retry_delay)
206
+ retry_delay *= 2
207
+ else:
208
+ break
209
+ except requests.exceptions.RequestException as e:
210
+ print(f"Request exception: {e}")
211
+ if attempt < max_retries - 1:
212
+ time.sleep(retry_delay)
213
+ retry_delay *= 2
214
+ else:
215
+ break
216
+ if all_videos:
217
+ random_video = random.choice(all_videos)
218
+ print(f"Selected random video from {len(all_videos)} HD videos")
219
+ return random_video
220
+ else:
221
+ print("No suitable videos found.")
222
  return None
223
 
224
  def search_pexels_images(query, pexels_api_key):
225
+ """Search for an image on Pexels by query."""
226
  headers = {'Authorization': pexels_api_key}
227
  url = "https://api.pexels.com/v1/search"
228
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
229
+ max_retries = 3
230
+ retry_delay = 1
231
+ for attempt in range(max_retries):
232
+ try:
233
+ response = requests.get(url, headers=headers, params=params, timeout=10)
234
+ if response.status_code == 200:
235
+ data = response.json()
236
+ photos = data.get("photos", [])
237
+ if photos:
238
+ photo = random.choice(photos[:min(5, len(photos))])
239
+ img_url = photo.get("src", {}).get("original")
240
+ return img_url
241
+ else:
242
+ print(f"No images found for query: {query}")
243
+ return None
244
+ elif response.status_code == 429:
245
+ print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying...")
246
+ time.sleep(retry_delay)
247
+ retry_delay *= 2
248
+ else:
249
+ print(f"Error fetching images: {response.status_code} {response.text}")
250
+ if attempt < max_retries - 1:
251
+ time.sleep(retry_delay)
252
+ retry_delay *= 2
253
+ except requests.exceptions.RequestException as e:
254
+ print(f"Request exception: {e}")
255
+ if attempt < max_retries - 1:
256
+ time.sleep(retry_delay)
257
+ retry_delay *= 2
258
+ print(f"No Pexels images found for query: {query}")
259
+ return None
260
 
261
  def search_google_images(query):
262
+ """Search for images on Google Images."""
 
263
  try:
264
+ search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch"
265
+ headers = {"User-Agent": USER_AGENT}
266
  response = requests.get(search_url, headers=headers, timeout=10)
267
  soup = BeautifulSoup(response.text, "html.parser")
268
+ img_tags = soup.find_all("img")
269
+ image_urls = []
270
+ for img in img_tags:
271
+ src = img.get("src", "")
272
+ if src.startswith("http") and "gstatic" not in src:
273
+ image_urls.append(src)
274
+ if image_urls:
275
+ return random.choice(image_urls[:5]) if len(image_urls) >= 5 else image_urls[0]
276
+ else:
277
+ print(f"No Google Images found for query: {query}")
278
+ return None
279
  except Exception as e:
280
+ print(f"Error in Google Images search: {e}")
281
  return None
282
 
283
  def download_image(image_url, filename):
284
+ """Download an image from a URL to a local file with verification."""
285
  try:
286
+ headers = {"User-Agent": USER_AGENT}
287
+ print(f"Downloading image from: {image_url} to {filename}")
288
+ response = requests.get(image_url, headers=headers, stream=True, timeout=15)
289
  response.raise_for_status()
290
  with open(filename, 'wb') as f:
291
  for chunk in response.iter_content(chunk_size=8192):
292
  f.write(chunk)
293
+ print(f"Image downloaded to: {filename}")
294
+ try:
295
+ img = Image.open(filename)
296
+ img.verify()
297
+ img = Image.open(filename)
298
+ if img.mode != 'RGB':
299
+ img = img.convert('RGB')
300
+ img.save(filename)
301
+ print(f"Image validated and processed: {filename}")
302
+ return filename
303
+ except Exception as e_validate:
304
+ print(f"Downloaded file is not a valid image: {e_validate}")
305
+ if os.path.exists(filename):
306
+ os.remove(filename)
307
+ return None
308
  except Exception as e:
309
  print(f"Image download error: {e}")
310
+ if os.path.exists(filename):
311
+ os.remove(filename)
312
  return None
313
 
314
  def download_video(video_url, filename):
315
+ """Download a video from a URL to a local file."""
316
  try:
317
  response = requests.get(video_url, stream=True, timeout=30)
318
  response.raise_for_status()
319
  with open(filename, 'wb') as f:
320
  for chunk in response.iter_content(chunk_size=8192):
321
  f.write(chunk)
322
+ print(f"Video downloaded to: {filename}")
323
  return filename
324
  except Exception as e:
325
  print(f"Video download error: {e}")
326
+ if os.path.exists(filename):
327
+ os.remove(filename)
328
  return None
329
 
330
+ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
331
+ """
332
+ Generate a visual asset for the clip.
333
+ If user_image is provided (from custom media upload) use it; otherwise,
334
+ use Pexels (and Google Images for news related queries) with fallbacks.
335
+ """
336
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
337
+ if user_image is not None:
338
+ # Assume user_image is a local path or file-like object.
339
+ print(f"Using custom media provided for prompt: {prompt}")
340
+ return {"path": user_image, "asset_type": "image"}
341
+
342
  if "news" in prompt.lower():
343
+ print(f"News query detected: {prompt}. Using Google Images...")
344
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_news.jpg")
345
  image_url = search_google_images(prompt)
346
+ if image_url:
347
+ downloaded_image = download_image(image_url, image_file)
348
+ if downloaded_image:
349
+ return {"path": downloaded_image, "asset_type": "image"}
350
+ if random.random() < 0.25:
351
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
352
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
353
+ if video_url:
354
+ downloaded_video = download_video(video_url, video_file)
355
+ if downloaded_video:
356
+ return {"path": downloaded_video, "asset_type": "video"}
357
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
358
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
359
+ if image_url:
360
+ downloaded_image = download_image(image_url, image_file)
361
+ if downloaded_image:
362
+ return {"path": downloaded_image, "asset_type": "image"}
363
+ fallback_terms = ["nature", "people", "landscape", "technology", "business"]
364
+ for term in fallback_terms:
365
+ fallback_file = os.path.join(TEMP_FOLDER, f"fallback_{term}.jpg")
366
+ fallback_url = search_pexels_images(term, PEXELS_API_KEY)
367
+ if fallback_url:
368
+ downloaded_fallback = download_image(fallback_url, fallback_file)
369
+ if downloaded_fallback:
370
+ return {"path": downloaded_fallback, "asset_type": "image"}
371
+ print(f"Failed to generate asset for: {prompt}")
372
  return None
373
 
374
+ def generate_silent_audio(duration, sample_rate=24000):
375
+ """Generate a silent WAV file for TTS fallback."""
376
+ num_samples = int(duration * sample_rate)
377
+ silence = np.zeros(num_samples, dtype=np.float32)
378
+ silent_path = os.path.join(TEMP_FOLDER, f"silent_{int(time.time())}.wav")
379
+ sf.write(silent_path, silence, sample_rate)
380
+ return silent_path
381
+
382
+ def generate_tts(text, voice):
383
+ """Generate TTS audio using Kokoro, falling back to gTTS if needed."""
384
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
385
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
386
  if os.path.exists(file_path):
387
  return file_path
388
  try:
389
+ kokoro_voice = 'af_heart' if voice == 'en' else voice
390
+ generator = pipeline(text, voice=kokoro_voice, speed=0.9, split_pattern=r'\n+')
391
+ audio_segments = []
392
+ for i, (gs, ps, audio) in enumerate(generator):
393
+ audio_segments.append(audio)
394
+ full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
395
+ sf.write(file_path, full_audio, 24000)
396
+ return file_path
397
+ except Exception as e:
398
+ try:
399
+ tts = gTTS(text=text, lang='en')
400
  mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
401
  tts.save(mp3_path)
402
+ audio = AudioSegment.from_mp3(mp3_path)
403
+ audio.export(file_path, format="wav")
404
  os.remove(mp3_path)
405
+ return file_path
406
+ except Exception as fallback_error:
407
+ return generate_silent_audio(duration=max(3, len(text.split()) * 0.5))
 
408
 
409
+ def apply_kenburns_effect(clip, target_resolution, effect_type=None):
410
+ """Apply a smooth Ken Burns effect to images."""
411
  target_w, target_h = target_resolution
412
  clip_aspect = clip.w / clip.h
413
  target_aspect = target_w / target_h
414
  if clip_aspect > target_aspect:
415
+ new_height = target_h
416
+ new_width = int(new_height * clip_aspect)
417
+ else:
418
+ new_width = target_w
419
+ new_height = int(new_width / clip_aspect)
420
+ clip = clip.resize(newsize=(new_width, new_height))
421
+ base_scale = 1.15
422
+ new_width = int(new_width * base_scale)
423
+ new_height = int(new_height * base_scale)
424
+ clip = clip.resize(newsize=(new_width, new_height))
425
+ max_offset_x = new_width - target_w
426
+ max_offset_y = new_height - target_h
427
+ available_effects = ["zoom-in", "zoom-out", "pan-left", "pan-right", "up-left"]
428
+ if effect_type is None or effect_type == "random":
429
+ effect_type = random.choice(available_effects)
430
+ if effect_type == "zoom-in":
431
+ start_zoom = 0.9
432
+ end_zoom = 1.1
433
+ start_center = (new_width / 2, new_height / 2)
434
+ end_center = start_center
435
+ elif effect_type == "zoom-out":
436
+ start_zoom = 1.1
437
+ end_zoom = 0.9
438
+ start_center = (new_width / 2, new_height / 2)
439
+ end_center = start_center
440
+ elif effect_type == "pan-left":
441
+ start_zoom = 1.0
442
+ end_zoom = 1.0
443
+ start_center = (max_offset_x + target_w / 2, (max_offset_y // 2) + target_h / 2)
444
+ end_center = (target_w / 2, (max_offset_y // 2) + target_h / 2)
445
+ elif effect_type == "pan-right":
446
+ start_zoom = 1.0
447
+ end_zoom = 1.0
448
+ start_center = (target_w / 2, (max_offset_y // 2) + target_h / 2)
449
+ end_center = (max_offset_x + target_w / 2, (max_offset_y // 2) + target_h / 2)
450
+ elif effect_type == "up-left":
451
+ start_zoom = 1.0
452
+ end_zoom = 1.0
453
+ start_center = (max_offset_x + target_w / 2, max_offset_y + target_h / 2)
454
+ end_center = (target_w / 2, target_h / 2)
455
  else:
456
+ raise ValueError(f"Unsupported effect_type: {effect_type}")
457
+ def transform_frame(get_frame, t):
458
+ frame = get_frame(t)
459
+ ratio = t / clip.duration if clip.duration > 0 else 0
460
+ ratio = 0.5 - 0.5 * math.cos(math.pi * ratio)
461
+ current_zoom = start_zoom + (end_zoom - start_zoom) * ratio
462
+ crop_w = int(target_w / current_zoom)
463
+ crop_h = int(target_h / current_zoom)
464
+ current_center_x = start_center[0] + (end_center[0] - start_center[0]) * ratio
465
+ current_center_y = start_center[1] + (end_center[1] - start_center[1]) * ratio
466
+ min_center_x = crop_w / 2
467
+ max_center_x = new_width - crop_w / 2
468
+ min_center_y = crop_h / 2
469
+ max_center_y = new_height - crop_h / 2
470
+ current_center_x = max(min_center_x, min(current_center_x, max_center_x))
471
+ current_center_y = max(min_center_y, min(current_center_y, max_center_y))
472
+ cropped_frame = cv2.getRectSubPix(frame, (crop_w, crop_h), (current_center_x, current_center_y))
473
+ resized_frame = cv2.resize(cropped_frame, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
474
+ return resized_frame
475
+ return clip.fl(transform_frame)
476
 
477
  def resize_to_fill(clip, target_resolution):
478
+ """Resize and crop a clip to fill the target resolution."""
479
  target_w, target_h = target_resolution
480
  clip_aspect = clip.w / clip.h
481
  target_aspect = target_w / target_h
482
  if clip_aspect > target_aspect:
483
  clip = clip.resize(height=target_h)
484
  crop_amount = (clip.w - target_w) / 2
485
+ clip = clip.crop(x1=crop_amount, x2=clip.w - crop_amount, y1=0, y2=clip.h)
486
  else:
487
  clip = clip.resize(width=target_w)
488
  crop_amount = (clip.h - target_h) / 2
489
+ clip = clip.crop(x1=0, x2=clip.w, y1=crop_amount, y2=clip.h - crop_amount)
490
  return clip
491
 
492
+ def find_mp3_files():
493
+ """Find an MP3 file for background music."""
494
+ mp3_files = []
495
+ for root, dirs, files in os.walk('.'):
496
+ for file in files:
497
+ if file.endswith('.mp3'):
498
+ mp3_path = os.path.join(root, file)
499
+ mp3_files.append(mp3_path)
500
+ return mp3_files[0] if mp3_files else None
501
+
502
+ def add_background_music(final_video, bg_music_volume=0.08):
503
+ """Add background music to the final video."""
504
+ try:
505
+ bg_music_path = find_mp3_files()
506
+ if bg_music_path and os.path.exists(bg_music_path):
507
+ bg_music = AudioFileClip(bg_music_path)
508
+ if bg_music.duration < final_video.duration:
509
+ loops_needed = math.ceil(final_video.duration / bg_music.duration)
510
+ bg_segments = [bg_music] * loops_needed
511
+ bg_music = concatenate_videoclips(bg_segments)
512
+ bg_music = bg_music.subclip(0, final_video.duration)
513
+ bg_music = bg_music.volumex(bg_music_volume)
514
+ video_audio = final_video.audio
515
+ mixed_audio = CompositeAudioClip([video_audio, bg_music])
516
+ final_video = final_video.set_audio(mixed_audio)
517
+ return final_video
518
+ except Exception as e:
519
+ print(f"Error adding background music: {e}")
520
+ return final_video
521
+
522
+ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, narration_text=None, segment_index=0):
523
+ """Create a video clip from a media asset and its corresponding TTS audio, optionally with subtitles."""
524
+ try:
525
+ if not os.path.exists(media_path) or not os.path.exists(tts_path):
526
+ return None
527
+ audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2)
528
+ audio_duration = audio_clip.duration
529
+ target_duration = audio_duration + 0.2
530
+ if asset_type == "video":
531
+ clip = VideoFileClip(media_path)
532
+ clip = resize_to_fill(clip, TARGET_RESOLUTION)
533
+ if clip.duration < target_duration:
534
+ clip = clip.loop(duration=target_duration)
535
+ else:
536
+ clip = clip.subclip(0, target_duration)
537
+ elif asset_type == "image":
538
+ img = Image.open(media_path)
539
+ if img.mode != 'RGB':
540
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp:
541
+ img.convert('RGB').save(temp.name)
542
+ media_path = temp.name
543
+ img.close()
544
+ clip = ImageClip(media_path).set_duration(target_duration)
545
+ clip = apply_kenburns_effect(clip, TARGET_RESOLUTION)
546
+ clip = clip.fadein(0.3).fadeout(0.3)
547
+ else:
548
+ return None
549
+ if narration_text and CAPTION_COLOR != "transparent":
550
+ try:
551
+ words = narration_text.split()
552
+ chunks = []
553
+ current_chunk = []
554
+ for word in words:
555
+ current_chunk.append(word)
556
+ if len(current_chunk) >= 5:
557
+ chunks.append(' '.join(current_chunk))
558
+ current_chunk = []
559
+ if current_chunk:
560
+ chunks.append(' '.join(current_chunk))
561
+ chunk_duration = audio_duration / len(chunks)
562
+ subtitle_clips = []
563
+ subtitle_y_position = int(TARGET_RESOLUTION[1] * 0.70)
564
+ for i, chunk_text in enumerate(chunks):
565
+ start_time = i * chunk_duration
566
+ end_time = (i + 1) * chunk_duration
567
+ txt_clip = TextClip(
568
+ chunk_text,
569
+ fontsize=45,
570
+ font='Arial-Bold',
571
+ color=CAPTION_COLOR,
572
+ bg_color='rgba(0, 0, 0, 0.25)',
573
+ method='caption',
574
+ align='center',
575
+ stroke_width=2,
576
+ stroke_color=CAPTION_COLOR,
577
+ size=(TARGET_RESOLUTION[0] * 0.8, None)
578
+ ).set_start(start_time).set_end(end_time)
579
+ txt_clip = txt_clip.set_position(('center', subtitle_y_position))
580
+ subtitle_clips.append(txt_clip)
581
+ clip = CompositeVideoClip([clip] + subtitle_clips)
582
+ except Exception as sub_error:
583
+ txt_clip = TextClip(
584
+ narration_text,
585
+ fontsize=28,
586
+ color=CAPTION_COLOR,
587
+ align='center',
588
+ size=(TARGET_RESOLUTION[0] * 0.7, None)
589
+ ).set_position(('center', int(TARGET_RESOLUTION[1] / 3))).set_duration(clip.duration)
590
+ clip = CompositeVideoClip([clip, txt_clip])
591
  clip = clip.set_audio(audio_clip)
592
+ return clip
593
+ except Exception as e:
594
+ print(f"Error in create_clip: {str(e)}")
595
+ return None
596
 
597
+ def fix_imagemagick_policy():
598
+ """Attempt to fix ImageMagick security policies."""
599
+ try:
600
+ policy_paths = [
601
+ "/etc/ImageMagick-6/policy.xml",
602
+ "/etc/ImageMagick-7/policy.xml",
603
+ "/etc/ImageMagick/policy.xml",
604
+ "/usr/local/etc/ImageMagick-7/policy.xml"
605
+ ]
606
+ found_policy = next((path for path in policy_paths if os.path.exists(path)), None)
607
+ if not found_policy:
608
+ return False
609
+ os.system(f"sudo cp {found_policy} {found_policy}.bak")
610
+ os.system(f"sudo sed -i 's/rights=\"none\"/rights=\"read|write\"/g' {found_policy}")
611
+ os.system(f"sudo sed -i 's/<policy domain=\"path\" pattern=\"@\\*\"[^>]*>/<policy domain=\"path\" pattern=\"@*\" rights=\"read|write\"/g' {found_policy}")
612
+ os.system(f"sudo sed -i 's/<policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"[^>]*>/<!-- <policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"> -->/g' {found_policy}")
613
+ return True
614
+ except Exception as e:
615
+ return False
616
+
617
+ def generate_video(user_input, resolution, caption_option):
618
+ """
619
+ Original video generation function.
620
+ This version takes only the basic inputs and uses the global clip data generated by the script.
621
+ You may need to extend this to use updated clip data from the dynamic clip editor.
622
+ """
623
+ global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
624
+ if resolution == "Full HD (1920x1080)":
625
+ TARGET_RESOLUTION = (1920, 1080)
626
+ elif resolution == "Short (1080x1920)":
627
+ TARGET_RESOLUTION = (1080, 1920)
628
+ else:
629
+ TARGET_RESOLUTION = (1920, 1080)
630
+ CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
631
+ TEMP_FOLDER = tempfile.mkdtemp()
632
+ fix_success = fix_imagemagick_policy()
633
+ if not fix_success:
634
+ print("Unable to fix ImageMagick policies; proceeding with alternative methods.")
635
+ print("Generating script from API...")
636
+ script = generate_script(user_input)
637
+ if not script:
638
+ shutil.rmtree(TEMP_FOLDER)
639
+ return None
640
+ print("Generated Script:\n", script)
641
+ elements = parse_script(script)
642
+ if not elements:
643
+ shutil.rmtree(TEMP_FOLDER)
644
+ return None
645
+ paired_elements = []
646
+ for i in range(0, len(elements), 2):
647
+ if i + 1 < len(elements):
648
+ paired_elements.append((elements[i], elements[i + 1]))
649
+ if not paired_elements:
650
+ shutil.rmtree(TEMP_FOLDER)
651
+ return None
652
  clips = []
653
+ for idx, (media_elem, tts_elem) in enumerate(paired_elements):
654
+ print(f"Processing segment {idx+1} with prompt: '{media_elem['prompt']}'")
655
+ media_asset = generate_media(media_elem['prompt'])
656
+ if not media_asset:
657
+ continue
658
+ tts_path = generate_tts(tts_elem['text'], tts_elem['voice'])
659
+ if not tts_path:
660
+ continue
661
+ clip = create_clip(
662
+ media_path=media_asset['path'],
663
+ asset_type=media_asset['asset_type'],
664
+ tts_path=tts_path,
665
+ duration=tts_elem['duration'],
666
+ effects=media_elem.get('effects', 'fade-in'),
667
+ narration_text=tts_elem['text'],
668
+ segment_index=idx
669
+ )
670
+ if clip:
671
+ clips.append(clip)
672
  if not clips:
673
+ shutil.rmtree(TEMP_FOLDER)
674
  return None
675
  final_video = concatenate_videoclips(clips, method="compose")
676
+ final_video = add_background_music(final_video, bg_music_volume=0.08)
677
+ final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=24, preset='veryfast')
678
  shutil.rmtree(TEMP_FOLDER)
679
  return OUTPUT_VIDEO_FILENAME
680
 
681
+ # --------- NEW CALLBACK FUNCTIONS FOR THE NEW UI ---------
682
+ # Global variable to store clip data from script-generation.
683
+ generated_clip_data = []
684
+
685
+ def generate_script_clips(topic_input, full_script):
686
+ """
687
+ Callback when the user clicks "πŸ“ Generate Script & Load Clips".
688
+ Uses full_script if provided, else generates a script based on topic_input.
689
+ Returns generated raw script and a JSON string representing clip data.
690
+ """
691
+ input_text = full_script if full_script.strip() != "" else topic_input
692
+ script = generate_script(input_text)
693
+ if not script:
694
+ return "Error: failed to generate script", "{}"
695
+ elements = parse_script(script)
696
+ clip_list = []
697
+ for i in range(0, len(elements), 2):
698
+ if i + 1 < len(elements):
699
+ media_elem = elements[i]
700
+ tts_elem = elements[i+1]
701
+ clip_info = {
702
+ "prompt": media_elem.get("prompt", ""),
703
+ "narration": tts_elem.get("text", ""),
704
+ "custom_media": None
705
+ }
706
+ clip_list.append(clip_info)
707
+ global generated_clip_data
708
+ generated_clip_data = clip_list
709
+ return script, json.dumps(clip_list)
710
+
711
+ def update_clip_editor(clip_json):
712
+ """
713
+ Generate a dynamic interface (using Gradio Blocks) for editing clip details.
714
+ Returns a list of Accordion components.
715
+ """
716
+ clip_list = json.loads(clip_json)
717
+ editors = []
718
+ for idx, clip in enumerate(clip_list, start=1):
719
+ accordion_title = f"Clip {idx}: {clip['prompt']}"
720
+ # Create an accordion for each clip.
721
+ with gr.Accordion(label=accordion_title, open=(idx<=2)) as accordion:
722
+ prompt_box = gr.Textbox(label="Visual Prompt", value=clip["prompt"])
723
+ narration_box = gr.Textbox(label="Narration Text", value=clip["narration"], lines=3)
724
+ custom_media_box = gr.File(label="Custom Media Upload (overrides prompt)", file_types=[".jpg", ".png", ".mp4"])
725
+ # Pack into a dictionary structure.
726
+ editors.append({
727
+ "prompt": prompt_box,
728
+ "narration": narration_box,
729
+ "custom_media": custom_media_box
730
+ })
731
+ editors.append(accordion)
732
+ return editors
733
+
734
+ def generate_final_video(topic_input, full_script, clip_data_json, resolution, render_speed,
735
+ video_clip_percent, zoom_pan, bg_music_file, bg_music_volume,
736
+ subtitle_enabled, font_dropdown, font_size, outline_width,
737
+ font_color, outline_color, subtitle_position):
738
+ """
739
+ Callback for "🎬 Generate Video" button.
740
+ Here we would combine the user-edited clip data with settings and call video generation.
741
+ Note: For demonstration, we will use the original generate_video function.
742
+ """
743
+ # In a real integration you would process clip_data_json to update each clip with custom overrides.
744
+ print("Final settings:")
745
+ print(f"Resolution: {resolution}, Render Speed: {render_speed}, Video Clip %: {video_clip_percent}, Zoom/Pan: {zoom_pan}")
746
+ if bg_music_file is not None:
747
+ print("Custom background music provided.")
748
+ # For now, we simply call generate_video using topic_input and full_script.
749
+ video_file = generate_video(topic_input, resolution, "Yes")
750
+ return video_file
751
+
752
+ # --------- GRADIO BLOCKS UI ---------
753
+ with gr.Blocks(title="πŸš€ Orbit Video Engine") as demo:
754
  with gr.Row():
755
  # Column 1: Content Input & Script Generation
756
+ with gr.Column(scale=1):
757
+ gr.Markdown("## 1. Content Input")
758
+ topic_input = gr.Textbox(label="Topic Input", placeholder="Enter your video topic here...")
759
+ full_script = gr.Textbox(label="Or Paste Full Script", placeholder="Paste full script here...", lines=5)
760
  generate_script_btn = gr.Button("πŸ“ Generate Script & Load Clips")
761
+ generated_script_disp = gr.Textbox(label="Generated Script", interactive=False, visible=False)
762
+ clip_data_storage = gr.Textbox(visible=False)
763
+ # Column 2: Clip Editor (Dynamic)
764
+ with gr.Column(scale=1):
765
+ gr.Markdown("## 2. Edit Clips")
766
+ gr.Markdown("Modify each clip's visual prompt, narration or upload custom media.")
767
+ clip_editor_container = gr.Column(visible=False)
 
 
 
 
 
 
 
 
 
768
  # Column 3: Settings & Output
769
+ with gr.Column(scale=1):
770
+ gr.Markdown("## 3. Video Settings")
771
+ resolution = gr.Radio(choices=["Short (1080x1920)", "Full HD (1920x1080)"],
772
+ label="Resolution", value="Full HD (1920x1080)")
773
+ render_speed = gr.Dropdown(choices=["ultrafast", "veryfast", "fast", "medium", "slow", "veryslow"],
774
+ label="Render Speed", value="fast")
775
+ video_clip_percent = gr.Slider(0, 100, step=5, label="Video Clip Percentage", value=25)
776
+ zoom_pan = gr.Checkbox(label="Add Zoom/Pan Effect (Images)", value=True)
777
  with gr.Accordion("Background Music", open=False):
778
+ bg_music_file = gr.File(label="Upload Background Music (MP3)", file_types=[".mp3"])
779
+ bg_music_volume = gr.Slider(0.0, 1.0, step=0.05, label="BGM Volume", value=0.15)
780
  with gr.Accordion("Subtitle Settings", open=True):
781
+ subtitle_enabled = gr.Checkbox(label="Enable Subtitles", value=True)
782
+ font_dropdown = gr.Dropdown(choices=["Impact", "Arial", "Helvetica", "Times New Roman"],
783
+ label="Font", value="Arial")
784
  font_size = gr.Number(label="Font Size", value=45)
785
  outline_width = gr.Number(label="Outline Width", value=2)
786
  font_color = gr.ColorPicker(label="Font Color", value="#FFFFFF")
787
  outline_color = gr.ColorPicker(label="Outline Color", value="#000000")
788
+ subtitle_position = gr.Radio(choices=["center", "bottom", "top"], label="Subtitle Position", value="center")
789
+ gr.Markdown("## 4. Output")
790
  generate_video_btn = gr.Button("🎬 Generate Video")
791
+ video_preview = gr.Video(label="Generated Video")
792
+ download_video_file = gr.File(label="Download Video", interactive=False)
793
+ # Interactions
 
794
  generate_script_btn.click(
795
+ fn=generate_script_clips,
796
+ inputs=[topic_input, full_script],
797
+ outputs=[generated_script_disp, clip_data_storage]
798
+ ).then(
799
+ fn=lambda script: gr.update(visible=True),
800
+ inputs=[generated_script_disp],
801
+ outputs=[generated_script_disp]
802
+ ).then(
803
+ fn=update_clip_editor,
804
+ inputs=[clip_data_storage],
805
+ outputs=[clip_editor_container]
806
+ ).then(
807
+ fn=lambda _: gr.update(visible=True),
808
+ inputs=[clip_editor_container],
809
+ outputs=[clip_editor_container]
810
  )
811
  generate_video_btn.click(
812
+ fn=generate_final_video,
813
+ inputs=[topic_input, full_script, clip_data_storage, resolution, render_speed,
814
+ video_clip_percent, zoom_pan, bg_music_file, bg_music_volume,
815
+ subtitle_enabled, font_dropdown, font_size, outline_width, font_color, outline_color, subtitle_position],
816
+ outputs=[video_preview]
817
  )
818
+
819
+ demo.launch(share=True)