OrbitMC commited on
Commit
5411b6e
·
verified ·
1 Parent(s): bdac94f

Update app (1).py

Browse files
Files changed (1) hide show
  1. app (1).py +99 -234
app (1).py CHANGED
@@ -1,56 +1,59 @@
1
-
2
-
3
  # Import necessary libraries
4
- from kokoro import KPipeline
5
-
6
  import soundfile as sf
7
  import torch
8
-
9
- import soundfile as sf
10
- import os
11
- from moviepy.editor import VideoFileClip, AudioFileClip, ImageClip
12
- from PIL import Image
13
  import tempfile
14
  import random
15
  import cv2
16
  import math
17
- import os, requests, io, time, re, random
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  from moviepy.editor import (
19
  VideoFileClip, concatenate_videoclips, AudioFileClip, ImageClip,
20
- CompositeVideoClip, TextClip, CompositeAudioClip
21
  )
22
- import gradio as gr
23
- import shutil
24
- import os
25
  import moviepy.video.fx.all as vfx
26
- import moviepy.config as mpy_config
27
  from pydub import AudioSegment
28
  from pydub.generators import Sine
29
 
30
- from PIL import Image, ImageDraw, ImageFont
31
- import numpy as np
32
- from bs4 import BeautifulSoup
33
- import base64
34
- from urllib.parse import quote
35
- import pysrt
36
- from gtts import gTTS
37
- import gradio as gr # Import Gradio
38
 
39
- # Initialize Kokoro TTS pipeline (using American English)
40
- pipeline = KPipeline(lang_code='a') # Use voice 'af_heart' for American English
41
- # Ensure ImageMagick binary is set
42
- mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
43
 
44
  # ---------------- Global Configuration ---------------- #
45
- PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
46
- OPENROUTER_API_KEY = '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'
47
- OPENROUTER_MODEL = "moonshotai/kimi-k2:free"
48
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
49
  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"
50
 
 
 
 
 
 
 
51
 
52
-
53
- # Additional global variables needed for the Gradio interface
54
  selected_voice = 'af_heart' # Default voice
55
  voice_speed = 0.9 # Default voice speed
56
  font_size = 45 # Default font size
@@ -62,24 +65,17 @@ TARGET_RESOLUTION = None
62
  CAPTION_COLOR = None
63
  TEMP_FOLDER = None
64
 
65
-
66
  # ---------------- Helper Functions ---------------- #
67
- # (Your existing helper functions remain unchanged: generate_script, parse_script,
68
- # search_pexels_videos, search_pexels_images, search_google_images, download_image,
69
- # download_video, generate_media, generate_tts, apply_kenburns_effect,
70
- # resize_to_fill, find_mp3_files, add_background_music, create_clip,
71
- # fix_imagemagick_policy)
72
-
73
- # Define these globally as they were in your original code but will be set per run
74
- TARGET_RESOLUTION = None
75
- CAPTION_COLOR = None
76
- TEMP_FOLDER = None
77
 
78
  def generate_script(user_input):
79
  """Generate documentary script with proper OpenRouter handling."""
 
 
 
 
80
  headers = {
81
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
82
- 'HTTP-Referer': 'https://your-domain.com',
83
  'X-Title': 'AI Documentary Maker'
84
  }
85
 
@@ -93,7 +89,7 @@ Structure:
93
  - The full script should make sense as one connected narration — no randomness.
94
  - Use natural, formal English. No slang, no fake AI language, and no robotic tone.
95
  - Do not use humor, sarcasm, or casual language. This is a serious narration.
96
- - No emotion-sound words like aww, eww, whoa, etc.
97
  - Do not use numbers like 1, 2, 3 — write them out as one, two, three.
98
  - At the end, add a [Subscribe] tag with a formal or respectful reason to follow or subscribe.
99
 
@@ -125,9 +121,7 @@ Rising temperatures are causing coral bleaching and habitat loss.
125
 
126
  Follow to explore more about the changing planet we live on.
127
 
128
-
129
-
130
- Now here is the Topic/scrip: {user_input}
131
  """
132
 
133
  data = {
@@ -161,12 +155,7 @@ Now here is the Topic/scrip: {user_input}
161
  return None
162
 
163
  def parse_script(script_text):
164
- """
165
- Parse the generated script into a list of elements.
166
- For each section, create two elements:
167
- - A 'media' element using the section title as the visual prompt.
168
- - A 'tts' element with the narration text, voice info, and computed duration.
169
- """
170
  sections = {}
171
  current_title = None
172
  current_text = ""
@@ -183,7 +172,7 @@ def parse_script(script_text):
183
  current_title = line[bracket_start+1:bracket_end]
184
  current_text = line[bracket_end+1:].strip()
185
  elif current_title:
186
- current_text += line + " "
187
 
188
  if current_title:
189
  sections[current_title] = current_text.strip()
@@ -207,6 +196,10 @@ def parse_script(script_text):
207
 
208
  def search_pexels_videos(query, pexels_api_key):
209
  """Search for a video on Pexels by query and return a random HD video."""
 
 
 
 
210
  headers = {'Authorization': pexels_api_key}
211
  base_url = "https://api.pexels.com/videos/search"
212
  num_pages = 3
@@ -247,21 +240,11 @@ def search_pexels_videos(query, pexels_api_key):
247
  retry_delay *= 2
248
  else:
249
  print(f"Error fetching videos: {response.status_code} {response.text}")
250
- if attempt < max_retries - 1:
251
- print(f"Retrying in {retry_delay} seconds...")
252
- time.sleep(retry_delay)
253
- retry_delay *= 2
254
- else:
255
- break
256
 
257
  except requests.exceptions.RequestException as e:
258
  print(f"Request exception: {e}")
259
- if attempt < max_retries - 1:
260
- print(f"Retrying in {retry_delay} seconds...")
261
- time.sleep(retry_delay)
262
- retry_delay *= 2
263
- else:
264
- break
265
 
266
  if all_videos:
267
  random_video = random.choice(all_videos)
@@ -273,6 +256,10 @@ def search_pexels_videos(query, pexels_api_key):
273
 
274
  def search_pexels_images(query, pexels_api_key):
275
  """Search for an image on Pexels by query."""
 
 
 
 
276
  headers = {'Authorization': pexels_api_key}
277
  url = "https://api.pexels.com/v1/search"
278
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
@@ -301,17 +288,11 @@ def search_pexels_images(query, pexels_api_key):
301
  retry_delay *= 2
302
  else:
303
  print(f"Error fetching images: {response.status_code} {response.text}")
304
- if attempt < max_retries - 1:
305
- print(f"Retrying in {retry_delay} seconds...")
306
- time.sleep(retry_delay)
307
- retry_delay *= 2
308
 
309
  except requests.exceptions.RequestException as e:
310
  print(f"Request exception: {e}")
311
- if attempt < max_retries - 1:
312
- print(f"Retrying in {retry_delay} seconds...")
313
- time.sleep(retry_delay)
314
- retry_delay *= 2
315
 
316
  print(f"No Pexels images found for query: {query} after all attempts")
317
  return None
@@ -344,7 +325,7 @@ def download_image(image_url, filename):
344
  """Download an image from a URL to a local file with enhanced error handling."""
345
  try:
346
  headers = {"User-Agent": USER_AGENT}
347
- print(f"Downloading image from: {image_url} to {filename}")
348
  response = requests.get(image_url, headers=headers, stream=True, timeout=15)
349
  response.raise_for_status()
350
 
@@ -397,11 +378,7 @@ def download_video(video_url, filename):
397
  return None
398
 
399
  def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
400
- """
401
- Generate a visual asset by first searching for a video or using a specific search strategy.
402
- For news-related queries, use Google Images.
403
- Returns a dict: {'path': <file_path>, 'asset_type': 'video' or 'image'}.
404
- """
405
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
406
 
407
  if "news" in prompt.lower():
@@ -413,8 +390,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
413
  if downloaded_image:
414
  print(f"News image saved to {downloaded_image}")
415
  return {"path": downloaded_image, "asset_type": "image"}
416
- else:
417
- print(f"Google Images search failed for prompt: {prompt}")
418
 
419
  if random.random() < video_clip_probability:
420
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
@@ -424,8 +399,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
424
  if downloaded_video:
425
  print(f"Video asset saved to {downloaded_video}")
426
  return {"path": downloaded_video, "asset_type": "video"}
427
- else:
428
- print(f"Pexels video search failed for prompt: {prompt}")
429
 
430
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
431
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
@@ -434,9 +407,8 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
434
  if downloaded_image:
435
  print(f"Image asset saved to {downloaded_image}")
436
  return {"path": downloaded_image, "asset_type": "image"}
437
- else:
438
- print(f"Pexels image download failed for prompt: {prompt}")
439
 
 
440
  fallback_terms = ["nature", "people", "landscape", "technology", "business"]
441
  for term in fallback_terms:
442
  print(f"Trying fallback image search with term: {term}")
@@ -447,10 +419,6 @@ def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
447
  if downloaded_fallback:
448
  print(f"Fallback image saved to {downloaded_fallback}")
449
  return {"path": downloaded_fallback, "asset_type": "image"}
450
- else:
451
- print(f"Fallback image download failed for term: {term}")
452
- else:
453
- print(f"Fallback image search failed for term: {term}")
454
 
455
  print(f"Failed to generate visual asset for prompt: {prompt}")
456
  return None
@@ -465,9 +433,7 @@ def generate_silent_audio(duration, sample_rate=24000):
465
  return silent_path
466
 
467
  def generate_tts(text, voice):
468
- """
469
- Generate TTS audio using Kokoro, falling back to gTTS or silent audio if needed.
470
- """
471
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
472
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
473
 
@@ -475,31 +441,35 @@ def generate_tts(text, voice):
475
  print(f"Using cached TTS for text '{text[:10]}...'")
476
  return file_path
477
 
478
- try:
479
- kokoro_voice = selected_voice if voice == 'en' else voice
480
- generator = pipeline(text, voice=kokoro_voice, speed=voice_speed, split_pattern=r'\n+')
481
- audio_segments = []
482
- for i, (gs, ps, audio) in enumerate(generator):
483
- audio_segments.append(audio)
484
- full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
485
- sf.write(file_path, full_audio, 24000)
486
- print(f"TTS audio saved to {file_path} (Kokoro)")
487
- return file_path
488
- except Exception as e:
489
- print(f"Error with Kokoro TTS: {e}")
490
  try:
491
- print("Falling back to gTTS...")
492
- tts = gTTS(text=text, lang='en')
493
- mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
494
- tts.save(mp3_path)
495
- audio = AudioSegment.from_mp3(mp3_path)
496
- audio.export(file_path, format="wav")
497
- os.remove(mp3_path)
498
- print(f"Fallback TTS saved to {file_path} (gTTS)")
499
  return file_path
500
- except Exception as fallback_error:
501
- print(f"Both TTS methods failed: {fallback_error}")
502
- return generate_silent_audio(duration=max(3, len(text.split()) * 0.5))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
  def apply_kenburns_effect(clip, target_resolution, effect_type=None):
505
  """Apply a smooth Ken Burns effect with a single movement pattern."""
@@ -593,17 +563,6 @@ def resize_to_fill(clip, target_resolution):
593
 
594
  return clip
595
 
596
- def find_mp3_files():
597
- """Search for any MP3 files in the current directory and subdirectories."""
598
- mp3_files = []
599
- for root, dirs, files in os.walk('.'):
600
- for file in files:
601
- if file.endswith('.mp3'):
602
- mp3_path = os.path.join(root, file)
603
- mp3_files.append(mp3_path)
604
- print(f"Found MP3 file: {mp3_path}")
605
- return mp3_files[0] if mp3_files else None
606
-
607
  def add_background_music(final_video, bg_music_volume=0.10):
608
  """Add background music to the final video using any MP3 file found."""
609
  try:
@@ -661,116 +620,23 @@ def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, n
661
  else:
662
  return None
663
 
664
- if narration_text and CAPTION_COLOR != "transparent":
665
- try:
666
- words = narration_text.split()
667
- chunks = []
668
- current_chunk = []
669
- for word in words:
670
- current_chunk.append(word)
671
- if len(current_chunk) >= 5:
672
- chunks.append(' '.join(current_chunk))
673
- current_chunk = []
674
- if current_chunk:
675
- chunks.append(' '.join(current_chunk))
676
-
677
- chunk_duration = audio_duration / len(chunks)
678
- subtitle_clips = []
679
- subtitle_y_position = int(TARGET_RESOLUTION[1] * 0.70)
680
-
681
- for i, chunk_text in enumerate(chunks):
682
- start_time = i * chunk_duration
683
- end_time = (i + 1) * chunk_duration
684
- txt_clip = TextClip(
685
- chunk_text,
686
- fontsize=45,
687
- font='Arial-Bold',
688
- color=CAPTION_COLOR,
689
- bg_color='rgba(0, 0, 0, 0.25)',
690
- method='caption',
691
- align='center',
692
- stroke_width=2,
693
- stroke_color=CAPTION_COLOR,
694
- size=(TARGET_RESOLUTION[0] * 0.8, None)
695
- ).set_start(start_time).set_end(end_time)
696
- txt_clip = txt_clip.set_position(('center', subtitle_y_position))
697
- subtitle_clips.append(txt_clip)
698
-
699
- clip = CompositeVideoClip([clip] + subtitle_clips)
700
- except Exception as sub_error:
701
- print(f"Subtitle error: {sub_error}")
702
- txt_clip = TextClip(
703
- narration_text,
704
- fontsize=font_size,
705
- color=CAPTION_COLOR,
706
- align='center',
707
- size=(TARGET_RESOLUTION[0] * 0.7, None)
708
- ).set_position(('center', int(TARGET_RESOLUTION[1] / 3))).set_duration(clip.duration)
709
- clip = CompositeVideoClip([clip, txt_clip])
710
-
711
  clip = clip.set_audio(audio_clip)
712
  print(f"Clip created: {clip.duration:.1f}s")
713
  return clip
714
  except Exception as e:
715
  print(f"Error in create_clip: {str(e)}")
716
  return None
717
-
718
- def fix_imagemagick_policy():
719
- """Fix ImageMagick security policies."""
720
- try:
721
- print("Attempting to fix ImageMagick security policies...")
722
- policy_paths = [
723
- "/etc/ImageMagick-6/policy.xml",
724
- "/etc/ImageMagick-7/policy.xml",
725
- "/etc/ImageMagick/policy.xml",
726
- "/usr/local/etc/ImageMagick-7/policy.xml"
727
- ]
728
- found_policy = next((path for path in policy_paths if os.path.exists(path)), None)
729
- if not found_policy:
730
- print("No policy.xml found. Using alternative subtitle method.")
731
- return False
732
- print(f"Modifying policy file at {found_policy}")
733
- os.system(f"sudo cp {found_policy} {found_policy}.bak")
734
- os.system(f"sudo sed -i 's/rights=\"none\"/rights=\"read|write\"/g' {found_policy}")
735
- os.system(f"sudo sed -i 's/<policy domain=\"path\" pattern=\"@\*\"[^>]*>/<policy domain=\"path\" pattern=\"@*\" rights=\"read|write\"/g' {found_policy}")
736
- os.system(f"sudo sed -i 's/<policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"[^>]*>/<!-- <policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"> -->/g' {found_policy}")
737
- print("ImageMagick policies updated successfully.")
738
- return True
739
- except Exception as e:
740
- print(f"Error fixing policies: {e}")
741
- return False
742
-
743
-
744
-
745
-
746
-
747
-
748
-
749
-
750
-
751
-
752
-
753
-
754
-
755
-
756
-
757
-
758
-
759
-
760
-
761
-
762
-
763
-
764
-
765
-
766
-
767
-
768
 
769
  # ---------------- Main Video Generation Function ---------------- #
770
  def generate_video(user_input, resolution, caption_option):
771
  """Generate a video based on user input via Gradio."""
772
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
773
 
 
 
 
 
774
  # Set resolution
775
  if resolution == "Full":
776
  TARGET_RESOLUTION = (1920, 1080)
@@ -785,11 +651,6 @@ def generate_video(user_input, resolution, caption_option):
785
  # Create a unique temporary folder
786
  TEMP_FOLDER = tempfile.mkdtemp()
787
 
788
- # Fix ImageMagick policy
789
- fix_success = fix_imagemagick_policy()
790
- if not fix_success:
791
- print("Will use alternative methods if needed")
792
-
793
  print("Generating script from API...")
794
  script = generate_script(user_input)
795
  if not script:
@@ -894,6 +755,10 @@ VOICE_CHOICES = {
894
  def generate_video_with_options(user_input, resolution, caption_option, music_file, voice, vclip_prob, bg_vol, video_fps, video_preset, v_speed, caption_size):
895
  global selected_voice, voice_speed, font_size, video_clip_probability, bg_music_volume, fps, preset
896
 
 
 
 
 
897
  # Update global variables with user selections
898
  selected_voice = VOICE_CHOICES[voice]
899
  voice_speed = v_speed
@@ -916,7 +781,7 @@ def generate_video_with_options(user_input, resolution, caption_option, music_fi
916
  iface = gr.Interface(
917
  fn=generate_video_with_options,
918
  inputs=[
919
- gr.Textbox(label="Video Concept", placeholder="Enter your video concept here..."),
920
  gr.Radio(["Full", "Short"], label="Resolution", value="Full"),
921
  gr.Radio(["No"], label="Captions (Coming Soon)", value="No"),
922
  gr.File(label="Upload Background Music (MP3)", file_types=[".mp3"]),
@@ -931,9 +796,9 @@ iface = gr.Interface(
931
  ],
932
  outputs=gr.Video(label="Generated Video"),
933
  title="AI Documentary Video Generator",
934
- description="Create short documentary videos with AI. Upload music, choose voice, and customize settings."
935
  )
936
 
937
  # Launch the interface
938
  if __name__ == "__main__":
939
- iface.launch(share=True)
 
 
 
1
  # Import necessary libraries
2
+ import os
 
3
  import soundfile as sf
4
  import torch
 
 
 
 
 
5
  import tempfile
6
  import random
7
  import cv2
8
  import math
9
+ import time
10
+ import re
11
+ import requests
12
+ import io
13
+ import shutil
14
+ import numpy as np
15
+ from bs4 import BeautifulSoup
16
+ from urllib.parse import quote
17
+ import pysrt
18
+ from gtts import gTTS
19
+ import gradio as gr
20
+
21
+ # Import Kokoro
22
+ from kokoro import KPipeline
23
+
24
+ # Import moviepy components
25
  from moviepy.editor import (
26
  VideoFileClip, concatenate_videoclips, AudioFileClip, ImageClip,
27
+ CompositeVideoClip, TextClip, CompositeAudioClip, concatenate_audioclips
28
  )
 
 
 
29
  import moviepy.video.fx.all as vfx
30
+ from PIL import Image, ImageDraw, ImageFont
31
  from pydub import AudioSegment
32
  from pydub.generators import Sine
33
 
34
+ # ---------------- Secret Management ---------------- #
35
+ # Get secrets from environment variables (set in Gradio Space settings)
36
+ PEXELS_API_KEY = os.getenv('PEXELS_API_KEY', '')
37
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '')
38
+ OPENROUTER_MODEL = os.getenv('OPENROUTER_MODEL', 'moonshotai/kimi-k2:free')
 
 
 
39
 
40
+ # Check if secrets are loaded
41
+ if not PEXELS_API_KEY or not OPENROUTER_API_KEY:
42
+ print("WARNING: API keys not found in environment variables!")
43
+ print("Please set PEXELS_API_KEY and OPENROUTER_API_KEY in your Gradio Space secrets.")
44
 
45
  # ---------------- Global Configuration ---------------- #
 
 
 
46
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
47
  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"
48
 
49
+ # Initialize Kokoro TTS pipeline (using American English)
50
+ try:
51
+ pipeline = KPipeline(lang_code='a')
52
+ except Exception as e:
53
+ print(f"Warning: Kokoro initialization failed: {e}")
54
+ pipeline = None
55
 
56
+ # Global variables for video generation
 
57
  selected_voice = 'af_heart' # Default voice
58
  voice_speed = 0.9 # Default voice speed
59
  font_size = 45 # Default font size
 
65
  CAPTION_COLOR = None
66
  TEMP_FOLDER = None
67
 
 
68
  # ---------------- Helper Functions ---------------- #
 
 
 
 
 
 
 
 
 
 
69
 
70
  def generate_script(user_input):
71
  """Generate documentary script with proper OpenRouter handling."""
72
+ if not OPENROUTER_API_KEY:
73
+ print("OpenRouter API key not configured")
74
+ return None
75
+
76
  headers = {
77
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
78
+ 'HTTP-Referer': 'https://huggingface.co', # Use HuggingFace as referer
79
  'X-Title': 'AI Documentary Maker'
80
  }
81
 
 
89
  - The full script should make sense as one connected narration — no randomness.
90
  - Use natural, formal English. No slang, no fake AI language, and no robotic tone.
91
  - Do not use humor, sarcasm, or casual language. This is a serious narration.
92
+ - No emotion-sound words like "aww," "eww," "whoa," etc.
93
  - Do not use numbers like 1, 2, 3 — write them out as one, two, three.
94
  - At the end, add a [Subscribe] tag with a formal or respectful reason to follow or subscribe.
95
 
 
121
 
122
  Follow to explore more about the changing planet we live on.
123
 
124
+ Now here is the Topic/script: {user_input}
 
 
125
  """
126
 
127
  data = {
 
155
  return None
156
 
157
  def parse_script(script_text):
158
+ """Parse the generated script into a list of elements."""
 
 
 
 
 
159
  sections = {}
160
  current_title = None
161
  current_text = ""
 
172
  current_title = line[bracket_start+1:bracket_end]
173
  current_text = line[bracket_end+1:].strip()
174
  elif current_title:
175
+ current_text += " " + line
176
 
177
  if current_title:
178
  sections[current_title] = current_text.strip()
 
196
 
197
  def search_pexels_videos(query, pexels_api_key):
198
  """Search for a video on Pexels by query and return a random HD video."""
199
+ if not pexels_api_key:
200
+ print("Pexels API key not configured")
201
+ return None
202
+
203
  headers = {'Authorization': pexels_api_key}
204
  base_url = "https://api.pexels.com/videos/search"
205
  num_pages = 3
 
240
  retry_delay *= 2
241
  else:
242
  print(f"Error fetching videos: {response.status_code} {response.text}")
243
+ break
 
 
 
 
 
244
 
245
  except requests.exceptions.RequestException as e:
246
  print(f"Request exception: {e}")
247
+ break
 
 
 
 
 
248
 
249
  if all_videos:
250
  random_video = random.choice(all_videos)
 
256
 
257
  def search_pexels_images(query, pexels_api_key):
258
  """Search for an image on Pexels by query."""
259
+ if not pexels_api_key:
260
+ print("Pexels API key not configured")
261
+ return None
262
+
263
  headers = {'Authorization': pexels_api_key}
264
  url = "https://api.pexels.com/v1/search"
265
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
 
288
  retry_delay *= 2
289
  else:
290
  print(f"Error fetching images: {response.status_code} {response.text}")
291
+ break
 
 
 
292
 
293
  except requests.exceptions.RequestException as e:
294
  print(f"Request exception: {e}")
295
+ break
 
 
 
296
 
297
  print(f"No Pexels images found for query: {query} after all attempts")
298
  return None
 
325
  """Download an image from a URL to a local file with enhanced error handling."""
326
  try:
327
  headers = {"User-Agent": USER_AGENT}
328
+ print(f"Downloading image from: {image_url}")
329
  response = requests.get(image_url, headers=headers, stream=True, timeout=15)
330
  response.raise_for_status()
331
 
 
378
  return None
379
 
380
  def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
381
+ """Generate a visual asset by searching for videos or images."""
 
 
 
 
382
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
383
 
384
  if "news" in prompt.lower():
 
390
  if downloaded_image:
391
  print(f"News image saved to {downloaded_image}")
392
  return {"path": downloaded_image, "asset_type": "image"}
 
 
393
 
394
  if random.random() < video_clip_probability:
395
  video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
 
399
  if downloaded_video:
400
  print(f"Video asset saved to {downloaded_video}")
401
  return {"path": downloaded_video, "asset_type": "video"}
 
 
402
 
403
  image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
404
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
 
407
  if downloaded_image:
408
  print(f"Image asset saved to {downloaded_image}")
409
  return {"path": downloaded_image, "asset_type": "image"}
 
 
410
 
411
+ # Fallback to generic images
412
  fallback_terms = ["nature", "people", "landscape", "technology", "business"]
413
  for term in fallback_terms:
414
  print(f"Trying fallback image search with term: {term}")
 
419
  if downloaded_fallback:
420
  print(f"Fallback image saved to {downloaded_fallback}")
421
  return {"path": downloaded_fallback, "asset_type": "image"}
 
 
 
 
422
 
423
  print(f"Failed to generate visual asset for prompt: {prompt}")
424
  return None
 
433
  return silent_path
434
 
435
  def generate_tts(text, voice):
436
+ """Generate TTS audio using Kokoro, falling back to gTTS or silent audio if needed."""
 
 
437
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
438
  file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
439
 
 
441
  print(f"Using cached TTS for text '{text[:10]}...'")
442
  return file_path
443
 
444
+ # Try Kokoro if available
445
+ if pipeline:
 
 
 
 
 
 
 
 
 
 
446
  try:
447
+ kokoro_voice = selected_voice if voice == 'en' else voice
448
+ generator = pipeline(text, voice=kokoro_voice, speed=voice_speed, split_pattern=r'\n+')
449
+ audio_segments = []
450
+ for i, (gs, ps, audio) in enumerate(generator):
451
+ audio_segments.append(audio)
452
+ full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
453
+ sf.write(file_path, full_audio, 24000)
454
+ print(f"TTS audio saved to {file_path} (Kokoro)")
455
  return file_path
456
+ except Exception as e:
457
+ print(f"Error with Kokoro TTS: {e}")
458
+
459
+ # Fallback to gTTS
460
+ try:
461
+ print("Falling back to gTTS...")
462
+ tts = gTTS(text=text, lang='en')
463
+ mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
464
+ tts.save(mp3_path)
465
+ audio = AudioSegment.from_mp3(mp3_path)
466
+ audio.export(file_path, format="wav")
467
+ os.remove(mp3_path)
468
+ print(f"Fallback TTS saved to {file_path} (gTTS)")
469
+ return file_path
470
+ except Exception as fallback_error:
471
+ print(f"Both TTS methods failed: {fallback_error}")
472
+ return generate_silent_audio(duration=max(3, len(text.split()) * 0.5))
473
 
474
  def apply_kenburns_effect(clip, target_resolution, effect_type=None):
475
  """Apply a smooth Ken Burns effect with a single movement pattern."""
 
563
 
564
  return clip
565
 
 
 
 
 
 
 
 
 
 
 
 
566
  def add_background_music(final_video, bg_music_volume=0.10):
567
  """Add background music to the final video using any MP3 file found."""
568
  try:
 
620
  else:
621
  return None
622
 
623
+ # Skip subtitles for now since captions are disabled
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  clip = clip.set_audio(audio_clip)
625
  print(f"Clip created: {clip.duration:.1f}s")
626
  return clip
627
  except Exception as e:
628
  print(f"Error in create_clip: {str(e)}")
629
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
 
631
  # ---------------- Main Video Generation Function ---------------- #
632
  def generate_video(user_input, resolution, caption_option):
633
  """Generate a video based on user input via Gradio."""
634
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
635
 
636
+ # Check if API keys are configured
637
+ if not PEXELS_API_KEY or not OPENROUTER_API_KEY:
638
+ return None
639
+
640
  # Set resolution
641
  if resolution == "Full":
642
  TARGET_RESOLUTION = (1920, 1080)
 
651
  # Create a unique temporary folder
652
  TEMP_FOLDER = tempfile.mkdtemp()
653
 
 
 
 
 
 
654
  print("Generating script from API...")
655
  script = generate_script(user_input)
656
  if not script:
 
755
  def generate_video_with_options(user_input, resolution, caption_option, music_file, voice, vclip_prob, bg_vol, video_fps, video_preset, v_speed, caption_size):
756
  global selected_voice, voice_speed, font_size, video_clip_probability, bg_music_volume, fps, preset
757
 
758
+ # Check if API keys are configured
759
+ if not PEXELS_API_KEY or not OPENROUTER_API_KEY:
760
+ return gr.Error("API keys not configured. Please set PEXELS_API_KEY and OPENROUTER_API_KEY in Space secrets.")
761
+
762
  # Update global variables with user selections
763
  selected_voice = VOICE_CHOICES[voice]
764
  voice_speed = v_speed
 
781
  iface = gr.Interface(
782
  fn=generate_video_with_options,
783
  inputs=[
784
+ gr.Textbox(label="Video Concept", placeholder="Enter your video concept here...", lines=3),
785
  gr.Radio(["Full", "Short"], label="Resolution", value="Full"),
786
  gr.Radio(["No"], label="Captions (Coming Soon)", value="No"),
787
  gr.File(label="Upload Background Music (MP3)", file_types=[".mp3"]),
 
796
  ],
797
  outputs=gr.Video(label="Generated Video"),
798
  title="AI Documentary Video Generator",
799
+ description="Create short documentary videos with AI. Upload music, choose voice, and customize settings.\n\n⚠️ **Important**: Make sure to set your API keys in the Space secrets!"
800
  )
801
 
802
  # Launch the interface
803
  if __name__ == "__main__":
804
+ iface.launch()