testdeep123 commited on
Commit
c7ff119
·
verified ·
1 Parent(s): a7922d3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +66 -271
app.py CHANGED
@@ -1,163 +1,89 @@
1
- # Import necessary libraries
2
  from kokoro import KPipeline
3
-
4
  import soundfile as sf
5
  import torch
6
-
7
- import soundfile as sf
8
  import os
9
- from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip
 
 
10
  from PIL import Image
11
  import tempfile
12
  import random
13
  import cv2
14
  import math
15
- import os, requests, io, time, re, random
16
- from moviepy.editor import (
17
- VideoFileClip, concatenate_videoclips, AudioFileClip, ImageClip,
18
- CompositeVideoClip, TextClip
19
- )
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
-
25
- from PIL import Image, ImageDraw, ImageFont
26
  import numpy as np
27
  from bs4 import BeautifulSoup
28
- import base64
29
  from urllib.parse import quote
30
- import pysrt
31
- from gtts import gTTS
32
- import gradio as gr # Import Gradio
33
 
34
- # Initialize Kokoro TTS pipeline (using American English)
35
- pipeline = KPipeline(lang_code='a') # Use voice 'af_heart' for American English
36
- # Ensure ImageMagick binary is set
37
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
38
 
39
- # ---------------- Global Configuration ---------------- #
40
  PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
41
  OPENROUTER_API_KEY = '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'
42
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
43
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
44
  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"
45
-
46
- # ---------------- Helper Functions ---------------- #
47
- # (Your existing helper functions remain unchanged: generate_script, parse_script,
48
- # search_pexels_videos, search_pexels_images, search_google_images, download_image,
49
- # download_video, generate_media, generate_tts, apply_kenburns_effect,
50
- # resize_to_fill, find_mp3_files, add_background_music, create_clip,
51
- # fix_imagemagick_policy)
52
-
53
- # Define these globally as they were in your original code but will be set per run
54
  TARGET_RESOLUTION = None
55
  CAPTION_COLOR = None
56
  TEMP_FOLDER = None
57
 
 
58
  def generate_script(user_input):
59
- """Generate documentary script with proper OpenRouter handling."""
60
  headers = {
61
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
62
  'HTTP-Referer': 'https://your-domain.com',
63
  'X-Title': 'AI Documentary Maker'
64
  }
65
-
66
  prompt = f"""Short Documentary Script GeneratorInstructions:
67
-
68
  If I say "use this," just output the script exactly as I gave it.
69
  If I only give topics, generate a script based on them.
70
  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
71
  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
72
  Formatting Rules:
73
-
74
-
75
  Title in Square Brackets:
76
-
77
-
78
  Each section starts with a one-word title inside [ ] (max two words if necessary).
79
  This title will be used as a search term for Pexels footage.
80
-
81
-
82
-
83
  Casual & Funny Narration:
84
-
85
-
86
  Each section has 5-10 words of narration.
87
  Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
88
-
89
-
90
-
91
  No Special Formatting:
92
-
93
-
94
  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
95
-
96
-
97
-
98
  Generalized Search Terms:
99
-
100
-
101
  If a term is too specific, make it more general for Pexels search.
102
-
103
-
104
-
105
  Scene-Specific Writing:
106
-
107
-
108
  Each section describes only what should be shown in the video.
109
-
110
-
111
-
112
  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 .....
113
-
114
-
115
  No extra text, just the script.
116
-
117
-
118
-
119
  Example Output:
120
  [North Korea]
121
-
122
  Top 5 unknown facts about North Korea.
123
-
124
  [Invisibility]
125
-
126
  North Korea’s internet speed is so fast… it doesn’t exist.
127
-
128
  [Leadership]
129
-
130
  Kim Jong-un once won an election with 100% votes… against himself.
131
-
132
  [Magic]
133
-
134
  North Korea discovered time travel. That’s why their news is always from the past.
135
-
136
  [Warning]
137
-
138
  Subscribe now, or Kim Jong-un will send you a free one-way ticket… to North Korea.
139
-
140
  [Freedom]
141
-
142
  North Korean citizens can do anything… as long as it's government-approved.
143
  Now here is the Topic/scrip: {user_input}
144
  """
145
-
146
  data = {
147
  'model': OPENROUTER_MODEL,
148
  'messages': [{'role': 'user', 'content': prompt}],
149
  'temperature': 0.4,
150
  'max_tokens': 5000
151
  }
152
-
153
  try:
154
- response = requests.post(
155
- 'https://openrouter.ai/api/v1/chat/completions',
156
- headers=headers,
157
- json=data,
158
- timeout=30
159
- )
160
-
161
  if response.status_code == 200:
162
  response_data = response.json()
163
  if 'choices' in response_data and len(response_data['choices']) > 0:
@@ -168,22 +94,14 @@ Now here is the Topic/scrip: {user_input}
168
  else:
169
  print(f"API Error {response.status_code}: {response.text}")
170
  return None
171
-
172
  except Exception as e:
173
  print(f"Request failed: {str(e)}")
174
  return None
175
 
176
  def parse_script(script_text):
177
- """
178
- Parse the generated script into a list of elements.
179
- For each section, create two elements:
180
- - A 'media' element using the section title as the visual prompt.
181
- - A 'tts' element with the narration text, voice info, and computed duration.
182
- """
183
  sections = {}
184
  current_title = None
185
  current_text = ""
186
-
187
  try:
188
  for line in script_text.splitlines():
189
  line = line.strip()
@@ -197,63 +115,50 @@ def parse_script(script_text):
197
  current_text = line[bracket_end+1:].strip()
198
  elif current_title:
199
  current_text += line + " "
200
-
201
  if current_title:
202
  sections[current_title] = current_text.strip()
203
-
204
  elements = []
205
  for title, narration in sections.items():
206
  if not title or not narration:
207
  continue
208
-
209
  media_element = {"type": "media", "prompt": title, "effects": "fade-in"}
210
  words = narration.split()
211
  duration = max(3, len(words) * 0.5)
212
  tts_element = {"type": "tts", "text": narration, "voice": "en", "duration": duration}
213
  elements.append(media_element)
214
  elements.append(tts_element)
215
-
216
  return elements
217
  except Exception as e:
218
  print(f"Error parsing script: {e}")
219
  return []
220
 
221
  def search_pexels_videos(query, pexels_api_key):
222
- """Search for a video on Pexels by query and return a random HD video."""
223
  headers = {'Authorization': pexels_api_key}
224
  base_url = "https://api.pexels.com/videos/search"
225
  num_pages = 3
226
  videos_per_page = 15
227
-
228
  max_retries = 3
229
  retry_delay = 1
230
-
231
  search_query = query
232
  all_videos = []
233
-
234
  for page in range(1, num_pages + 1):
235
  for attempt in range(max_retries):
236
  try:
237
  params = {"query": search_query, "per_page": videos_per_page, "page": page}
238
  response = requests.get(base_url, headers=headers, params=params, timeout=10)
239
-
240
  if response.status_code == 200:
241
  data = response.json()
242
  videos = data.get("videos", [])
243
-
244
  if not videos:
245
  print(f"No videos found on page {page}.")
246
  break
247
-
248
  for video in videos:
249
  video_files = video.get("video_files", [])
250
  for file in video_files:
251
  if file.get("quality") == "hd":
252
  all_videos.append(file.get("link"))
253
  break
254
-
255
  break
256
-
257
  elif response.status_code == 429:
258
  print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying in {retry_delay} seconds...")
259
  time.sleep(retry_delay)
@@ -264,18 +169,12 @@ def search_pexels_videos(query, pexels_api_key):
264
  print(f"Retrying in {retry_delay} seconds...")
265
  time.sleep(retry_delay)
266
  retry_delay *= 2
267
- else:
268
- break
269
-
270
  except requests.exceptions.RequestException as e:
271
  print(f"Request exception: {e}")
272
  if attempt < max_retries - 1:
273
  print(f"Retrying in {retry_delay} seconds...")
274
  time.sleep(retry_delay)
275
  retry_delay *= 2
276
- else:
277
- break
278
-
279
  if all_videos:
280
  random_video = random.choice(all_videos)
281
  print(f"Selected random video from {len(all_videos)} HD videos")
@@ -285,18 +184,14 @@ def search_pexels_videos(query, pexels_api_key):
285
  return None
286
 
287
  def search_pexels_images(query, pexels_api_key):
288
- """Search for an image on Pexels by query."""
289
  headers = {'Authorization': pexels_api_key}
290
  url = "https://api.pexels.com/v1/search"
291
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
292
-
293
  max_retries = 3
294
  retry_delay = 1
295
-
296
  for attempt in range(max_retries):
297
  try:
298
  response = requests.get(url, headers=headers, params=params, timeout=10)
299
-
300
  if response.status_code == 200:
301
  data = response.json()
302
  photos = data.get("photos", [])
@@ -307,7 +202,6 @@ def search_pexels_images(query, pexels_api_key):
307
  else:
308
  print(f"No images found for query: {query}")
309
  return None
310
-
311
  elif response.status_code == 429:
312
  print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying in {retry_delay} seconds...")
313
  time.sleep(retry_delay)
@@ -318,32 +212,27 @@ def search_pexels_images(query, pexels_api_key):
318
  print(f"Retrying in {retry_delay} seconds...")
319
  time.sleep(retry_delay)
320
  retry_delay *= 2
321
-
322
  except requests.exceptions.RequestException as e:
323
  print(f"Request exception: {e}")
324
  if attempt < max_retries - 1:
325
  print(f"Retrying in {retry_delay} seconds...")
326
  time.sleep(retry_delay)
327
  retry_delay *= 2
328
-
329
  print(f"No Pexels images found for query: {query} after all attempts")
330
  return None
331
 
332
  def search_google_images(query):
333
- """Search for images on Google Images (for news-related queries)"""
334
  try:
335
  search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch"
336
  headers = {"User-Agent": USER_AGENT}
337
  response = requests.get(search_url, headers=headers, timeout=10)
338
  soup = BeautifulSoup(response.text, "html.parser")
339
-
340
  img_tags = soup.find_all("img")
341
  image_urls = []
342
  for img in img_tags:
343
  src = img.get("src", "")
344
  if src.startswith("http") and "gstatic" not in src:
345
  image_urls.append(src)
346
-
347
  if image_urls:
348
  return random.choice(image_urls[:5]) if len(image_urls) >= 5 else image_urls[0]
349
  else:
@@ -354,19 +243,15 @@ def search_google_images(query):
354
  return None
355
 
356
  def download_image(image_url, filename):
357
- """Download an image from a URL to a local file with enhanced error handling."""
358
  try:
359
  headers = {"User-Agent": USER_AGENT}
360
  print(f"Downloading image from: {image_url} to {filename}")
361
  response = requests.get(image_url, headers=headers, stream=True, timeout=15)
362
  response.raise_for_status()
363
-
364
  with open(filename, 'wb') as f:
365
  for chunk in response.iter_content(chunk_size=8192):
366
  f.write(chunk)
367
-
368
  print(f"Image downloaded successfully to: {filename}")
369
-
370
  try:
371
  img = Image.open(filename)
372
  img.verify()
@@ -381,7 +266,6 @@ def download_image(image_url, filename):
381
  if os.path.exists(filename):
382
  os.remove(filename)
383
  return None
384
-
385
  except requests.exceptions.RequestException as e_download:
386
  print(f"Image download error: {e_download}")
387
  if os.path.exists(filename):
@@ -394,7 +278,6 @@ def download_image(image_url, filename):
394
  return None
395
 
396
  def download_video(video_url, filename):
397
- """Download a video from a URL to a local file."""
398
  try:
399
  response = requests.get(video_url, stream=True, timeout=30)
400
  response.raise_for_status()
@@ -409,14 +292,8 @@ def download_video(video_url, filename):
409
  os.remove(filename)
410
  return None
411
 
412
- def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
413
- """
414
- Generate a visual asset by first searching for a video or using a specific search strategy.
415
- For news-related queries, use Google Images.
416
- Returns a dict: {'path': <file_path>, 'asset_type': 'video' or 'image'}.
417
- """
418
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
419
-
420
  if "news" in prompt.lower():
421
  print(f"News-related query detected: {prompt}. Using Google Images...")
422
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_news.jpg")
@@ -428,7 +305,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
428
  return {"path": downloaded_image, "asset_type": "image"}
429
  else:
430
  print(f"Google Images search failed for prompt: {prompt}")
431
-
432
  if random.random() < 0.45:
433
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
434
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
@@ -439,7 +315,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
439
  return {"path": downloaded_video, "asset_type": "video"}
440
  else:
441
  print(f"Pexels video search failed for prompt: {prompt}")
442
-
443
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
444
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
445
  if image_url:
@@ -449,7 +324,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
449
  return {"path": downloaded_image, "asset_type": "image"}
450
  else:
451
  print(f"Pexels image download failed for prompt: {prompt}")
452
-
453
  fallback_terms = ["nature", "people", "landscape", "technology", "business"]
454
  for term in fallback_terms:
455
  print(f"Trying fallback image search with term: {term}")
@@ -464,30 +338,15 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
464
  print(f"Fallback image download failed for term: {term}")
465
  else:
466
  print(f"Fallback image search failed for term: {term}")
467
-
468
  print(f"Failed to generate visual asset for prompt: {prompt}")
469
  return None
470
 
471
- def generate_silent_audio(duration, sample_rate=24000):
472
- """Generate a silent WAV audio file lasting 'duration' seconds."""
473
- num_samples = int(duration * sample_rate)
474
- silence = np.zeros(num_samples, dtype=np.float32)
475
- silent_path = os.path.join(TEMP_FOLDER, f"silent_{int(time.time())}.wav")
476
- sf.write(silent_path, silence, sample_rate)
477
- print(f"Silent audio generated: {silent_path}")
478
- return silent_path
479
-
480
  def generate_tts(text, voice):
481
- """
482
- Generate TTS audio using Kokoro, falling back to gTTS or silent audio if needed.
483
- """
484
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
485
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
486
-
487
  if os.path.exists(file_path):
488
  print(f"Using cached TTS for text '{text[:10]}...'")
489
  return file_path
490
-
491
  try:
492
  kokoro_voice = 'af_heart' if voice == 'en' else voice
493
  generator = pipeline(text, voice=kokoro_voice, speed=0.9, split_pattern=r'\n+')
@@ -500,46 +359,28 @@ def generate_tts(text, voice):
500
  return file_path
501
  except Exception as e:
502
  print(f"Error with Kokoro TTS: {e}")
503
- try:
504
- print("Falling back to gTTS...")
505
- tts = gTTS(text=text, lang='en')
506
- mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
507
- tts.save(mp3_path)
508
- audio = AudioSegment.from_mp3(mp3_path)
509
- audio.export(file_path, format="wav")
510
- os.remove(mp3_path)
511
- print(f"Fallback TTS saved to {file_path} (gTTS)")
512
- return file_path
513
- except Exception as fallback_error:
514
- print(f"Both TTS methods failed: {fallback_error}")
515
- return generate_silent_audio(duration=max(3, len(text.split()) * 0.5))
516
 
517
  def apply_kenburns_effect(clip, target_resolution, effect_type=None):
518
- """Apply a smooth Ken Burns effect with a single movement pattern."""
519
  target_w, target_h = target_resolution
520
  clip_aspect = clip.w / clip.h
521
  target_aspect = target_w / target_h
522
-
523
  if clip_aspect > target_aspect:
524
  new_height = target_h
525
  new_width = int(new_height * clip_aspect)
526
  else:
527
  new_width = target_w
528
  new_height = int(new_width / clip_aspect)
529
-
530
  clip = clip.resize(newsize=(new_width, new_height))
531
  base_scale = 1.15
532
  new_width = int(new_width * base_scale)
533
  new_height = int(new_height * base_scale)
534
  clip = clip.resize(newsize=(new_width, new_height))
535
-
536
  max_offset_x = new_width - target_w
537
  max_offset_y = new_height - target_h
538
-
539
  available_effects = ["zoom-in", "zoom-out", "pan-left", "pan-right", "up-left"]
540
  if effect_type is None or effect_type == "random":
541
  effect_type = random.choice(available_effects)
542
-
543
  if effect_type == "zoom-in":
544
  start_zoom = 0.9
545
  end_zoom = 1.1
@@ -567,7 +408,6 @@ def apply_kenburns_effect(clip, target_resolution, effect_type=None):
567
  end_center = (target_w / 2, target_h / 2)
568
  else:
569
  raise ValueError(f"Unsupported effect_type: {effect_type}")
570
-
571
  def transform_frame(get_frame, t):
572
  frame = get_frame(t)
573
  ratio = t / clip.duration if clip.duration > 0 else 0
@@ -586,15 +426,12 @@ def apply_kenburns_effect(clip, target_resolution, effect_type=None):
586
  cropped_frame = cv2.getRectSubPix(frame, (crop_w, crop_h), (current_center_x, current_center_y))
587
  resized_frame = cv2.resize(cropped_frame, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
588
  return resized_frame
589
-
590
  return clip.fl(transform_frame)
591
 
592
  def resize_to_fill(clip, target_resolution):
593
- """Resize and crop a clip to fill the target resolution while maintaining aspect ratio."""
594
  target_w, target_h = target_resolution
595
  clip_aspect = clip.w / clip.h
596
  target_aspect = target_w / target_h
597
-
598
  if clip_aspect > target_aspect:
599
  clip = clip.resize(height=target_h)
600
  crop_amount = (clip.w - target_w) / 2
@@ -603,57 +440,35 @@ def resize_to_fill(clip, target_resolution):
603
  clip = clip.resize(width=target_w)
604
  crop_amount = (clip.h - target_h) / 2
605
  clip = clip.crop(x1=0, x2=clip.w, y1=crop_amount, y2=clip.h - crop_amount)
606
-
607
  return clip
608
 
609
- def find_mp3_files():
610
- """Search for any MP3 files in the current directory and subdirectories."""
611
- mp3_files = []
612
- for root, dirs, files in os.walk('.'):
613
- for file in files:
614
- if file.endswith('.mp3'):
615
- mp3_path = os.path.join(root, file)
616
- mp3_files.append(mp3_path)
617
- print(f"Found MP3 file: {mp3_path}")
618
- return mp3_files[0] if mp3_files else None
619
-
620
- def add_background_music(final_video, bg_music_volume=0.08):
621
- """Add background music to the final video using any MP3 file found."""
622
- try:
623
- bg_music_path = find_mp3_files()
624
- if bg_music_path and os.path.exists(bg_music_path):
625
- print(f"Adding background music from: {bg_music_path}")
626
- bg_music = AudioFileClip(bg_music_path)
627
- if bg_music.duration < final_video.duration:
628
- loops_needed = math.ceil(final_video.duration / bg_music.duration)
629
- bg_segments = [bg_music] * loops_needed
630
- bg_music = concatenate_audioclips(bg_segments)
631
- bg_music = bg_music.subclip(0, final_video.duration)
632
- bg_music = bg_music.volumex(bg_music_volume)
633
- video_audio = final_video.audio
634
- mixed_audio = CompositeAudioClip([video_audio, bg_music])
635
- final_video = final_video.set_audio(mixed_audio)
636
- print("Background music added successfully")
637
- else:
638
- print("No MP3 files found, skipping background music")
639
- return final_video
640
- except Exception as e:
641
- print(f"Error adding background music: {e}")
642
- print("Continuing without background music")
643
- return final_video
644
 
645
  def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, narration_text=None, segment_index=0):
646
- """Create a video clip with synchronized subtitles and narration."""
647
  try:
648
  print(f"Creating clip #{segment_index} with asset_type: {asset_type}, media_path: {media_path}")
649
  if not os.path.exists(media_path) or not os.path.exists(tts_path):
650
  print("Missing media or TTS file")
651
  return None
652
-
653
  audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2)
654
  audio_duration = audio_clip.duration
655
  target_duration = audio_duration + 0.2
656
-
657
  if asset_type == "video":
658
  clip = VideoFileClip(media_path)
659
  clip = resize_to_fill(clip, TARGET_RESOLUTION)
@@ -673,7 +488,6 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
673
  clip = clip.fadein(0.3).fadeout(0.3)
674
  else:
675
  return None
676
-
677
  if narration_text and CAPTION_COLOR != "transparent":
678
  try:
679
  words = narration_text.split()
@@ -686,11 +500,9 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
686
  current_chunk = []
687
  if current_chunk:
688
  chunks.append(' '.join(current_chunk))
689
-
690
  chunk_duration = audio_duration / len(chunks)
691
  subtitle_clips = []
692
  subtitle_y_position = int(TARGET_RESOLUTION[1] * 0.70)
693
-
694
  for i, chunk_text in enumerate(chunks):
695
  start_time = i * chunk_duration
696
  end_time = (i + 1) * chunk_duration
@@ -708,7 +520,6 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
708
  ).set_start(start_time).set_end(end_time)
709
  txt_clip = txt_clip.set_position(('center', subtitle_y_position))
710
  subtitle_clips.append(txt_clip)
711
-
712
  clip = CompositeVideoClip([clip] + subtitle_clips)
713
  except Exception as sub_error:
714
  print(f"Subtitle error: {sub_error}")
@@ -720,7 +531,6 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
720
  size=(TARGET_RESOLUTION[0] * 0.7, None)
721
  ).set_position(('center', int(TARGET_RESOLUTION[1] / 3))).set_duration(clip.duration)
722
  clip = CompositeVideoClip([clip, txt_clip])
723
-
724
  clip = clip.set_audio(audio_clip)
725
  print(f"Clip created: {clip.duration:.1f}s")
726
  return clip
@@ -729,7 +539,6 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
729
  return None
730
 
731
  def fix_imagemagick_policy():
732
- """Fix ImageMagick security policies."""
733
  try:
734
  print("Attempting to fix ImageMagick security policies...")
735
  policy_paths = [
@@ -753,55 +562,33 @@ def fix_imagemagick_policy():
753
  print(f"Error fixing policies: {e}")
754
  return False
755
 
756
- # ---------------- Main Function with Gradio Integration ---------------- #
757
- def generate_video(user_input, resolution, caption_option):
758
- """Generate a video based on user input via Gradio."""
759
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
760
- import shutil
761
-
762
- # Set resolution
763
  if resolution == "Full":
764
  TARGET_RESOLUTION = (1920, 1080)
765
  elif resolution == "Short":
766
  TARGET_RESOLUTION = (1080, 1920)
767
  else:
768
- TARGET_RESOLUTION = (1920, 1080) # Default
769
-
770
- # Set caption color
771
  CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
772
-
773
- # Create a unique temporary folder
774
  TEMP_FOLDER = tempfile.mkdtemp()
775
-
776
- # Fix ImageMagick policy
777
  fix_success = fix_imagemagick_policy()
778
  if not fix_success:
779
  print("Will use alternative methods if needed")
780
-
781
- print("Generating script from API...")
782
- script = generate_script(user_input)
783
- if not script:
784
- print("Failed to generate script.")
785
- shutil.rmtree(TEMP_FOLDER)
786
- return None
787
- print("Generated Script:\n", script)
788
  elements = parse_script(script)
789
  if not elements:
790
  print("Failed to parse script into elements.")
791
  shutil.rmtree(TEMP_FOLDER)
792
  return None
793
- print(f"Parsed {len(elements)//2} script segments.")
794
-
795
  paired_elements = []
796
  for i in range(0, len(elements), 2):
797
  if i + 1 < len(elements):
798
  paired_elements.append((elements[i], elements[i + 1]))
799
-
800
  if not paired_elements:
801
  print("No valid script segments found.")
802
  shutil.rmtree(TEMP_FOLDER)
803
  return None
804
-
805
  clips = []
806
  for idx, (media_elem, tts_elem) in enumerate(paired_elements):
807
  print(f"\nProcessing segment {idx+1}/{len(paired_elements)} with prompt: '{media_elem['prompt']}'")
@@ -826,40 +613,48 @@ def generate_video(user_input, resolution, caption_option):
826
  clips.append(clip)
827
  else:
828
  print(f"Clip creation failed for segment {idx+1}.")
829
-
830
  if not clips:
831
  print("No clips were successfully created.")
832
  shutil.rmtree(TEMP_FOLDER)
833
  return None
834
-
835
  print("\nConcatenating clips...")
836
  final_video = concatenate_videoclips(clips, method="compose")
837
- final_video = add_background_music(final_video, bg_music_volume=0.08)
838
-
839
  print(f"Exporting final video to {OUTPUT_VIDEO_FILENAME}...")
840
  final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=60, preset='veryfast')
841
  print(f"Final video saved as {OUTPUT_VIDEO_FILENAME}")
842
-
843
- # Clean up
844
- print("Cleaning up temporary files...")
845
  shutil.rmtree(TEMP_FOLDER)
846
  print("Temporary files removed.")
847
-
848
  return OUTPUT_VIDEO_FILENAME
849
 
850
- # ---------------- Gradio Interface ---------------- #
851
- iface = gr.Interface(
852
- fn=generate_video,
853
- inputs=[
854
- gr.Textbox(label="Video Concept", placeholder="Enter your video concept here..."),
855
- gr.Radio(["Full", "Short"], label="Resolution", value="Short"),
856
- gr.Radio(["Yes", "No"], label="Captions", value="No")
857
- ],
858
- outputs=gr.Video(label="Generated Video"),
859
- title="AI Documentary Video Generator",
860
- description="Create a funny documentary-style video based on your concept. Note: Generation may take several minutes on CPU."
861
- )
862
-
863
- # Launch the interface
864
- iface.launch(share=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
865
 
 
 
 
1
  from kokoro import KPipeline
 
2
  import soundfile as sf
3
  import torch
 
 
4
  import os
5
+ from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips, CompositeVideoClip, TextClip
6
+ import moviepy.video.fx.all as vfx
7
+ import moviepy.config as mpy_config
8
  from PIL import Image
9
  import tempfile
10
  import random
11
  import cv2
12
  import math
13
+ import requests
14
+ import re
15
+ import time
 
 
 
 
16
  from pydub import AudioSegment
 
 
 
17
  import numpy as np
18
  from bs4 import BeautifulSoup
 
19
  from urllib.parse import quote
20
+ import gradio as gr
21
+ import shutil
 
22
 
23
+ # Initialize Kokoro TTS pipeline (American English)
24
+ pipeline = KPipeline(lang_code='a')
 
25
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
26
 
27
+ # Global Configuration
28
  PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
29
  OPENROUTER_API_KEY = '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'
30
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
31
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
32
  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"
 
 
 
 
 
 
 
 
 
33
  TARGET_RESOLUTION = None
34
  CAPTION_COLOR = None
35
  TEMP_FOLDER = None
36
 
37
+ # Helper Functions (unchanged from original, included for completeness)
38
  def generate_script(user_input):
 
39
  headers = {
40
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
41
  'HTTP-Referer': 'https://your-domain.com',
42
  'X-Title': 'AI Documentary Maker'
43
  }
 
44
  prompt = f"""Short Documentary Script GeneratorInstructions:
 
45
  If I say "use this," just output the script exactly as I gave it.
46
  If I only give topics, generate a script based on them.
47
  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
48
  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
49
  Formatting Rules:
 
 
50
  Title in Square Brackets:
 
 
51
  Each section starts with a one-word title inside [ ] (max two words if necessary).
52
  This title will be used as a search term for Pexels footage.
 
 
 
53
  Casual & Funny Narration:
 
 
54
  Each section has 5-10 words of narration.
55
  Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
 
 
 
56
  No Special Formatting:
 
 
57
  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
 
 
 
58
  Generalized Search Terms:
 
 
59
  If a term is too specific, make it more general for Pexels search.
 
 
 
60
  Scene-Specific Writing:
 
 
61
  Each section describes only what should be shown in the video.
 
 
 
62
  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 .....
 
 
63
  No extra text, just the script.
 
 
 
64
  Example Output:
65
  [North Korea]
 
66
  Top 5 unknown facts about North Korea.
 
67
  [Invisibility]
 
68
  North Korea’s internet speed is so fast… it doesn’t exist.
 
69
  [Leadership]
 
70
  Kim Jong-un once won an election with 100% votes… against himself.
 
71
  [Magic]
 
72
  North Korea discovered time travel. That’s why their news is always from the past.
 
73
  [Warning]
 
74
  Subscribe now, or Kim Jong-un will send you a free one-way ticket… to North Korea.
 
75
  [Freedom]
 
76
  North Korean citizens can do anything… as long as it's government-approved.
77
  Now here is the Topic/scrip: {user_input}
78
  """
 
79
  data = {
80
  'model': OPENROUTER_MODEL,
81
  'messages': [{'role': 'user', 'content': prompt}],
82
  'temperature': 0.4,
83
  'max_tokens': 5000
84
  }
 
85
  try:
86
+ response = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, json=data, timeout=30)
 
 
 
 
 
 
87
  if response.status_code == 200:
88
  response_data = response.json()
89
  if 'choices' in response_data and len(response_data['choices']) > 0:
 
94
  else:
95
  print(f"API Error {response.status_code}: {response.text}")
96
  return None
 
97
  except Exception as e:
98
  print(f"Request failed: {str(e)}")
99
  return None
100
 
101
  def parse_script(script_text):
 
 
 
 
 
 
102
  sections = {}
103
  current_title = None
104
  current_text = ""
 
105
  try:
106
  for line in script_text.splitlines():
107
  line = line.strip()
 
115
  current_text = line[bracket_end+1:].strip()
116
  elif current_title:
117
  current_text += line + " "
 
118
  if current_title:
119
  sections[current_title] = current_text.strip()
 
120
  elements = []
121
  for title, narration in sections.items():
122
  if not title or not narration:
123
  continue
 
124
  media_element = {"type": "media", "prompt": title, "effects": "fade-in"}
125
  words = narration.split()
126
  duration = max(3, len(words) * 0.5)
127
  tts_element = {"type": "tts", "text": narration, "voice": "en", "duration": duration}
128
  elements.append(media_element)
129
  elements.append(tts_element)
 
130
  return elements
131
  except Exception as e:
132
  print(f"Error parsing script: {e}")
133
  return []
134
 
135
  def search_pexels_videos(query, pexels_api_key):
 
136
  headers = {'Authorization': pexels_api_key}
137
  base_url = "https://api.pexels.com/videos/search"
138
  num_pages = 3
139
  videos_per_page = 15
 
140
  max_retries = 3
141
  retry_delay = 1
 
142
  search_query = query
143
  all_videos = []
 
144
  for page in range(1, num_pages + 1):
145
  for attempt in range(max_retries):
146
  try:
147
  params = {"query": search_query, "per_page": videos_per_page, "page": page}
148
  response = requests.get(base_url, headers=headers, params=params, timeout=10)
 
149
  if response.status_code == 200:
150
  data = response.json()
151
  videos = data.get("videos", [])
 
152
  if not videos:
153
  print(f"No videos found on page {page}.")
154
  break
 
155
  for video in videos:
156
  video_files = video.get("video_files", [])
157
  for file in video_files:
158
  if file.get("quality") == "hd":
159
  all_videos.append(file.get("link"))
160
  break
 
161
  break
 
162
  elif response.status_code == 429:
163
  print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying in {retry_delay} seconds...")
164
  time.sleep(retry_delay)
 
169
  print(f"Retrying in {retry_delay} seconds...")
170
  time.sleep(retry_delay)
171
  retry_delay *= 2
 
 
 
172
  except requests.exceptions.RequestException as e:
173
  print(f"Request exception: {e}")
174
  if attempt < max_retries - 1:
175
  print(f"Retrying in {retry_delay} seconds...")
176
  time.sleep(retry_delay)
177
  retry_delay *= 2
 
 
 
178
  if all_videos:
179
  random_video = random.choice(all_videos)
180
  print(f"Selected random video from {len(all_videos)} HD videos")
 
184
  return None
185
 
186
  def search_pexels_images(query, pexels_api_key):
 
187
  headers = {'Authorization': pexels_api_key}
188
  url = "https://api.pexels.com/v1/search"
189
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
 
190
  max_retries = 3
191
  retry_delay = 1
 
192
  for attempt in range(max_retries):
193
  try:
194
  response = requests.get(url, headers=headers, params=params, timeout=10)
 
195
  if response.status_code == 200:
196
  data = response.json()
197
  photos = data.get("photos", [])
 
202
  else:
203
  print(f"No images found for query: {query}")
204
  return None
 
205
  elif response.status_code == 429:
206
  print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying in {retry_delay} seconds...")
207
  time.sleep(retry_delay)
 
212
  print(f"Retrying in {retry_delay} seconds...")
213
  time.sleep(retry_delay)
214
  retry_delay *= 2
 
215
  except requests.exceptions.RequestException as e:
216
  print(f"Request exception: {e}")
217
  if attempt < max_retries - 1:
218
  print(f"Retrying in {retry_delay} seconds...")
219
  time.sleep(retry_delay)
220
  retry_delay *= 2
 
221
  print(f"No Pexels images found for query: {query} after all attempts")
222
  return None
223
 
224
  def search_google_images(query):
 
225
  try:
226
  search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch"
227
  headers = {"User-Agent": USER_AGENT}
228
  response = requests.get(search_url, headers=headers, timeout=10)
229
  soup = BeautifulSoup(response.text, "html.parser")
 
230
  img_tags = soup.find_all("img")
231
  image_urls = []
232
  for img in img_tags:
233
  src = img.get("src", "")
234
  if src.startswith("http") and "gstatic" not in src:
235
  image_urls.append(src)
 
236
  if image_urls:
237
  return random.choice(image_urls[:5]) if len(image_urls) >= 5 else image_urls[0]
238
  else:
 
243
  return None
244
 
245
  def download_image(image_url, filename):
 
246
  try:
247
  headers = {"User-Agent": USER_AGENT}
248
  print(f"Downloading image from: {image_url} to {filename}")
249
  response = requests.get(image_url, headers=headers, stream=True, timeout=15)
250
  response.raise_for_status()
 
251
  with open(filename, 'wb') as f:
252
  for chunk in response.iter_content(chunk_size=8192):
253
  f.write(chunk)
 
254
  print(f"Image downloaded successfully to: {filename}")
 
255
  try:
256
  img = Image.open(filename)
257
  img.verify()
 
266
  if os.path.exists(filename):
267
  os.remove(filename)
268
  return None
 
269
  except requests.exceptions.RequestException as e_download:
270
  print(f"Image download error: {e_download}")
271
  if os.path.exists(filename):
 
278
  return None
279
 
280
  def download_video(video_url, filename):
 
281
  try:
282
  response = requests.get(video_url, stream=True, timeout=30)
283
  response.raise_for_status()
 
292
  os.remove(filename)
293
  return None
294
 
295
+ def generate_media(prompt, current_index=0, total_segments=1):
 
 
 
 
 
296
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
 
297
  if "news" in prompt.lower():
298
  print(f"News-related query detected: {prompt}. Using Google Images...")
299
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_news.jpg")
 
305
  return {"path": downloaded_image, "asset_type": "image"}
306
  else:
307
  print(f"Google Images search failed for prompt: {prompt}")
 
308
  if random.random() < 0.45:
309
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
310
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
 
315
  return {"path": downloaded_video, "asset_type": "video"}
316
  else:
317
  print(f"Pexels video search failed for prompt: {prompt}")
 
318
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
319
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
320
  if image_url:
 
324
  return {"path": downloaded_image, "asset_type": "image"}
325
  else:
326
  print(f"Pexels image download failed for prompt: {prompt}")
 
327
  fallback_terms = ["nature", "people", "landscape", "technology", "business"]
328
  for term in fallback_terms:
329
  print(f"Trying fallback image search with term: {term}")
 
338
  print(f"Fallback image download failed for term: {term}")
339
  else:
340
  print(f"Fallback image search failed for term: {term}")
 
341
  print(f"Failed to generate visual asset for prompt: {prompt}")
342
  return None
343
 
 
 
 
 
 
 
 
 
 
344
  def generate_tts(text, voice):
 
 
 
345
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
346
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
 
347
  if os.path.exists(file_path):
348
  print(f"Using cached TTS for text '{text[:10]}...'")
349
  return file_path
 
350
  try:
351
  kokoro_voice = 'af_heart' if voice == 'en' else voice
352
  generator = pipeline(text, voice=kokoro_voice, speed=0.9, split_pattern=r'\n+')
 
359
  return file_path
360
  except Exception as e:
361
  print(f"Error with Kokoro TTS: {e}")
362
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
  def apply_kenburns_effect(clip, target_resolution, effect_type=None):
 
365
  target_w, target_h = target_resolution
366
  clip_aspect = clip.w / clip.h
367
  target_aspect = target_w / target_h
 
368
  if clip_aspect > target_aspect:
369
  new_height = target_h
370
  new_width = int(new_height * clip_aspect)
371
  else:
372
  new_width = target_w
373
  new_height = int(new_width / clip_aspect)
 
374
  clip = clip.resize(newsize=(new_width, new_height))
375
  base_scale = 1.15
376
  new_width = int(new_width * base_scale)
377
  new_height = int(new_height * base_scale)
378
  clip = clip.resize(newsize=(new_width, new_height))
 
379
  max_offset_x = new_width - target_w
380
  max_offset_y = new_height - target_h
 
381
  available_effects = ["zoom-in", "zoom-out", "pan-left", "pan-right", "up-left"]
382
  if effect_type is None or effect_type == "random":
383
  effect_type = random.choice(available_effects)
 
384
  if effect_type == "zoom-in":
385
  start_zoom = 0.9
386
  end_zoom = 1.1
 
408
  end_center = (target_w / 2, target_h / 2)
409
  else:
410
  raise ValueError(f"Unsupported effect_type: {effect_type}")
 
411
  def transform_frame(get_frame, t):
412
  frame = get_frame(t)
413
  ratio = t / clip.duration if clip.duration > 0 else 0
 
426
  cropped_frame = cv2.getRectSubPix(frame, (crop_w, crop_h), (current_center_x, current_center_y))
427
  resized_frame = cv2.resize(cropped_frame, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
428
  return resized_frame
 
429
  return clip.fl(transform_frame)
430
 
431
  def resize_to_fill(clip, target_resolution):
 
432
  target_w, target_h = target_resolution
433
  clip_aspect = clip.w / clip.h
434
  target_aspect = target_w / target_h
 
435
  if clip_aspect > target_aspect:
436
  clip = clip.resize(height=target_h)
437
  crop_amount = (clip.w - target_w) / 2
 
440
  clip = clip.resize(width=target_w)
441
  crop_amount = (clip.h - target_h) / 2
442
  clip = clip.crop(x1=0, x2=clip.w, y1=crop_amount, y2=clip.h - crop_amount)
 
443
  return clip
444
 
445
+ def add_background_music(final_video, music_file, bg_music_volume=0.08):
446
+ if music_file and os.path.exists(music_file):
447
+ print(f"Adding background music from: {music_file}")
448
+ bg_music = AudioFileClip(music_file)
449
+ if bg_music.duration < final_video.duration:
450
+ loops_needed = math.ceil(final_video.duration / bg_music.duration)
451
+ bg_segments = [bg_music] * loops_needed
452
+ bg_music = concatenate_audioclips(bg_segments)
453
+ bg_music = bg_music.subclip(0, final_video.duration)
454
+ bg_music = bg_music.volumex(bg_music_volume)
455
+ video_audio = final_video.audio
456
+ mixed_audio = CompositeAudioClip([video_audio, bg_music])
457
+ final_video = final_video.set_audio(mixed_audio)
458
+ print("Background music added successfully")
459
+ else:
460
+ print("No music file provided, skipping background music")
461
+ return final_video
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
462
 
463
  def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, narration_text=None, segment_index=0):
 
464
  try:
465
  print(f"Creating clip #{segment_index} with asset_type: {asset_type}, media_path: {media_path}")
466
  if not os.path.exists(media_path) or not os.path.exists(tts_path):
467
  print("Missing media or TTS file")
468
  return None
 
469
  audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2)
470
  audio_duration = audio_clip.duration
471
  target_duration = audio_duration + 0.2
 
472
  if asset_type == "video":
473
  clip = VideoFileClip(media_path)
474
  clip = resize_to_fill(clip, TARGET_RESOLUTION)
 
488
  clip = clip.fadein(0.3).fadeout(0.3)
489
  else:
490
  return None
 
491
  if narration_text and CAPTION_COLOR != "transparent":
492
  try:
493
  words = narration_text.split()
 
500
  current_chunk = []
501
  if current_chunk:
502
  chunks.append(' '.join(current_chunk))
 
503
  chunk_duration = audio_duration / len(chunks)
504
  subtitle_clips = []
505
  subtitle_y_position = int(TARGET_RESOLUTION[1] * 0.70)
 
506
  for i, chunk_text in enumerate(chunks):
507
  start_time = i * chunk_duration
508
  end_time = (i + 1) * chunk_duration
 
520
  ).set_start(start_time).set_end(end_time)
521
  txt_clip = txt_clip.set_position(('center', subtitle_y_position))
522
  subtitle_clips.append(txt_clip)
 
523
  clip = CompositeVideoClip([clip] + subtitle_clips)
524
  except Exception as sub_error:
525
  print(f"Subtitle error: {sub_error}")
 
531
  size=(TARGET_RESOLUTION[0] * 0.7, None)
532
  ).set_position(('center', int(TARGET_RESOLUTION[1] / 3))).set_duration(clip.duration)
533
  clip = CompositeVideoClip([clip, txt_clip])
 
534
  clip = clip.set_audio(audio_clip)
535
  print(f"Clip created: {clip.duration:.1f}s")
536
  return clip
 
539
  return None
540
 
541
  def fix_imagemagick_policy():
 
542
  try:
543
  print("Attempting to fix ImageMagick security policies...")
544
  policy_paths = [
 
562
  print(f"Error fixing policies: {e}")
563
  return False
564
 
565
+ # Main Video Generation Function with Script Input
566
+ def generate_video_from_script(script, resolution, caption_option, music_file):
 
567
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
 
 
 
568
  if resolution == "Full":
569
  TARGET_RESOLUTION = (1920, 1080)
570
  elif resolution == "Short":
571
  TARGET_RESOLUTION = (1080, 1920)
572
  else:
573
+ TARGET_RESOLUTION = (1920, 1080)
 
 
574
  CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
 
 
575
  TEMP_FOLDER = tempfile.mkdtemp()
 
 
576
  fix_success = fix_imagemagick_policy()
577
  if not fix_success:
578
  print("Will use alternative methods if needed")
 
 
 
 
 
 
 
 
579
  elements = parse_script(script)
580
  if not elements:
581
  print("Failed to parse script into elements.")
582
  shutil.rmtree(TEMP_FOLDER)
583
  return None
 
 
584
  paired_elements = []
585
  for i in range(0, len(elements), 2):
586
  if i + 1 < len(elements):
587
  paired_elements.append((elements[i], elements[i + 1]))
 
588
  if not paired_elements:
589
  print("No valid script segments found.")
590
  shutil.rmtree(TEMP_FOLDER)
591
  return None
 
592
  clips = []
593
  for idx, (media_elem, tts_elem) in enumerate(paired_elements):
594
  print(f"\nProcessing segment {idx+1}/{len(paired_elements)} with prompt: '{media_elem['prompt']}'")
 
613
  clips.append(clip)
614
  else:
615
  print(f"Clip creation failed for segment {idx+1}.")
 
616
  if not clips:
617
  print("No clips were successfully created.")
618
  shutil.rmtree(TEMP_FOLDER)
619
  return None
 
620
  print("\nConcatenating clips...")
621
  final_video = concatenate_videoclips(clips, method="compose")
622
+ final_video = add_background_music(final_video, music_file, bg_music_volume=0.08)
 
623
  print(f"Exporting final video to {OUTPUT_VIDEO_FILENAME}...")
624
  final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=60, preset='veryfast')
625
  print(f"Final video saved as {OUTPUT_VIDEO_FILENAME}")
 
 
 
626
  shutil.rmtree(TEMP_FOLDER)
627
  print("Temporary files removed.")
 
628
  return OUTPUT_VIDEO_FILENAME
629
 
630
+ # Gradio Blocks Interface
631
+ with gr.Blocks(title="AI Documentary Video Generator") as demo:
632
+ gr.Markdown("### Create a Funny Documentary Video")
633
+ gr.Markdown("Enter a concept, generate a script, edit it, upload music, and generate your video!")
634
+ gr.Markdown("**Note**: Generation may take several minutes on CPU.")
635
+
636
+ concept = gr.Textbox(label="Video Concept", placeholder="Enter your video concept here...")
637
+ resolution = gr.Radio(["Full", "Short"], label="Resolution", value="Short")
638
+ captions = gr.Radio(["Yes", "No"], label="Captions", value="No")
639
+ script = gr.Textbox(label="Script", lines=10, placeholder="Generated script will appear here...")
640
+ music = gr.Audio(label="Background Music (Optional)", type="filepath")
641
+
642
+ generate_script_btn = gr.Button("Generate Script")
643
+ generate_video_btn = gr.Button("Generate Video")
644
+
645
+ video_output = gr.Video(label="Generated Video")
646
+
647
+ def on_generate_script(concept):
648
+ script_text = generate_script(concept)
649
+ return script_text if script_text else "Failed to generate script."
650
+
651
+ def on_generate_video(script, resolution, captions, music):
652
+ if not script or "Failed" in script:
653
+ return None
654
+ video_path = generate_video_from_script(script, resolution, captions, music)
655
+ return video_path
656
+
657
+ generate_script_btn.click(on_generate_script, inputs=concept, outputs=script)
658
+ generate_video_btn.click(on_generate_video, inputs=[script, resolution, captions, music], outputs=video_output)
659
 
660
+ demo.launch(share=True)