testdeep123 commited on
Commit
48a1d98
·
verified ·
1 Parent(s): 48a6754

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +760 -248
app.py CHANGED
@@ -1,353 +1,865 @@
1
  # Import necessary libraries
2
- import gradio as gr
 
 
 
 
 
3
  import os
4
- import shutil
 
5
  import tempfile
6
  import random
7
- import requests
8
- import soundfile as sf
 
9
  from moviepy.editor import (
10
- VideoFileClip, concatenate_videoclips, AudioFileClip, ImageClip, CompositeVideoClip, TextClip
 
11
  )
12
  import moviepy.video.fx.all as vfx
13
- from kokoro import KPipeline
14
- from gtts import gTTS
15
  from pydub import AudioSegment
16
- import math
17
- import re
18
- from PIL import Image
 
 
 
 
 
 
 
19
 
20
  # Initialize Kokoro TTS pipeline (using American English)
21
- pipeline = KPipeline(lang_code='a')
 
 
22
 
23
- # Global Configuration
24
  PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
25
  OPENROUTER_API_KEY = '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'
26
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
27
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
28
  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"
29
 
30
- # Helper Functions
 
 
 
 
 
 
 
 
 
 
 
31
  def generate_script(user_input):
32
- """Generate a documentary script using OpenRouter API."""
33
  headers = {
34
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
35
  'HTTP-Referer': 'https://your-domain.com',
36
  'X-Title': 'AI Documentary Maker'
37
  }
 
38
  prompt = f"""Short Documentary Script GeneratorInstructions:
39
- If I say "use this," output the script exactly as given.
40
- If I give topics, generate a script based on them.
41
- If I provide a full script, rewrite it unchanged. Keep it short, simple, humorous, and serious but funny. Use normal conversational text.
 
 
42
  Formatting Rules:
43
- - Title in square brackets: [Title]
44
- - Each section starts with a one-word title in [ ] (max two words).
45
- - Narration: 5-10 words, casual, funny, unpredictable.
46
- - No special formatting, just script text.
47
- - Generalized search terms for Pexels.
48
- - End with a funny subscribe statement.
49
- Example:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  [North Korea]
 
51
  Top 5 unknown facts about North Korea.
 
52
  [Invisibility]
53
- North Korea’s internet speed doesn’t exist.
 
 
54
  [Leadership]
55
- Kim Jong-un won 100% votes… against himself.
56
- [Subscribe]
57
- Subscribe, or Kim sends you a ticket to nowhere.
58
- Topic: {user_input}
 
 
 
 
 
 
 
 
 
 
 
59
  """
 
60
  data = {
61
  'model': OPENROUTER_MODEL,
62
  'messages': [{'role': 'user', 'content': prompt}],
63
  'temperature': 0.4,
64
  'max_tokens': 5000
65
  }
 
66
  try:
67
- response = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, json=data, timeout=30)
68
- response.raise_for_status()
69
- return response.json()['choices'][0]['message']['content']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  except Exception as e:
71
- print(f"Script generation failed: {e}")
72
  return None
73
 
74
  def parse_script(script_text):
75
- """Parse script into a list of elements with media prompts and TTS text."""
76
- elements = []
77
- lines = script_text.splitlines()
78
- for i in range(0, len(lines), 2):
79
- if i + 1 < len(lines) and lines[i].startswith('[') and lines[i].endswith(']'):
80
- title = lines[i][1:-1].strip()
81
- text = lines[i + 1].strip()
82
- if title and text:
83
- elements.append({'type': 'media', 'prompt': title})
84
- elements.append({'type': 'tts', 'text': text, 'voice': 'en'})
85
- return elements
86
-
87
- def search_pexels_videos(query, api_key):
88
- """Search Pexels for a random HD video."""
89
- headers = {'Authorization': api_key}
90
- params = {"query": query, "per_page": 15}
91
  try:
92
- response = requests.get("https://api.pexels.com/videos/search", headers=headers, params=params, timeout=10)
93
- response.raise_for_status()
94
- videos = response.json().get("videos", [])
95
- hd_videos = [v["video_files"][0]["link"] for v in videos if v["video_files"] and v["video_files"][0]["quality"] == "hd"]
96
- return random.choice(hd_videos) if hd_videos else None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  except Exception as e:
98
- print(f"Pexels video search failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  return None
100
 
101
- def search_pexels_images(query, api_key):
102
- """Search Pexels for a random image."""
103
- headers = {'Authorization': api_key}
 
104
  params = {"query": query, "per_page": 5, "orientation": "landscape"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  try:
106
- response = requests.get("https://api.pexels.com/v1/search", headers=headers, params=params, timeout=10)
107
- response.raise_for_status()
108
- photos = response.json().get("photos", [])
109
- return random.choice(photos)["src"]["original"] if photos else None
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  except Exception as e:
111
- print(f"Pexels image search failed: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  return None
113
 
114
- def download_file(url, filename):
115
- """Download a file from a URL."""
116
  try:
117
- response = requests.get(url, stream=True, timeout=15)
118
  response.raise_for_status()
119
  with open(filename, 'wb') as f:
120
  for chunk in response.iter_content(chunk_size=8192):
121
  f.write(chunk)
 
122
  return filename
123
  except Exception as e:
124
- print(f"Download failed: {e}")
 
 
125
  return None
126
 
127
- def generate_media(prompt, video_percentage, temp_folder):
128
- """Generate media based on prompt and video percentage."""
 
 
 
 
129
  safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
130
- if random.random() < video_percentage / 100:
131
- video_file = os.path.join(temp_folder, f"{safe_prompt}_video.mp4")
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
133
- if video_url and download_file(video_url, video_file):
134
- return {"path": video_file, "asset_type": "video"}
135
- image_file = os.path.join(temp_folder, f"{safe_prompt}.jpg")
 
 
 
 
 
 
136
  image_url = search_pexels_images(prompt, PEXELS_API_KEY)
137
- if image_url and download_file(image_url, image_file):
138
- return {"path": image_file, "asset_type": "image"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
  return None
140
 
141
- def generate_tts(text, voice, temp_folder):
142
- """Generate TTS audio with fallback."""
 
 
 
 
 
 
 
 
 
 
 
143
  safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
144
- file_path = os.path.join(temp_folder, f"tts_{safe_text}.wav")
 
 
 
 
 
145
  try:
146
- generator = pipeline(text, voice='af_heart', speed=0.9)
147
- audio = next(generator)[2]
148
- sf.write(file_path, audio, 24000)
 
 
 
 
 
149
  return file_path
150
- except Exception:
 
151
  try:
 
152
  tts = gTTS(text=text, lang='en')
153
- mp3_path = os.path.join(temp_folder, f"tts_{safe_text}.mp3")
154
  tts.save(mp3_path)
155
  audio = AudioSegment.from_mp3(mp3_path)
156
  audio.export(file_path, format="wav")
157
  os.remove(mp3_path)
 
158
  return file_path
159
- except Exception as e:
160
- print(f"TTS generation failed: {e}")
161
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
 
163
  def resize_to_fill(clip, target_resolution):
164
- """Resize and crop clip to fill target resolution."""
165
  target_w, target_h = target_resolution
166
  clip_aspect = clip.w / clip.h
167
  target_aspect = target_w / target_h
 
168
  if clip_aspect > target_aspect:
169
  clip = clip.resize(height=target_h)
170
  crop_amount = (clip.w - target_w) / 2
171
- clip = clip.crop(x1=crop_amount, x2=clip.w - crop_amount)
172
  else:
173
  clip = clip.resize(width=target_w)
174
  crop_amount = (clip.h - target_h) / 2
175
- clip = clip.crop(y1=crop_amount, y2=clip.h - crop_amount)
 
176
  return clip
177
 
178
- def create_clip(media_path, asset_type, tts_path, duration, narration_text, text_color, text_size, caption_bg, target_resolution):
179
- """Create a video clip with media, TTS, and subtitles."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  try:
 
 
 
 
 
181
  audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2)
182
- target_duration = audio_clip.duration + 0.2
 
 
183
  if asset_type == "video":
184
  clip = VideoFileClip(media_path)
185
- clip = resize_to_fill(clip, target_resolution)
186
- clip = clip.loop(duration=target_duration) if clip.duration < target_duration else clip.subclip(0, target_duration)
187
- else: # image
188
- clip = ImageClip(media_path).set_duration(target_duration).resize(target_resolution).fadein(0.3).fadeout(0.3)
189
-
190
- if narration_text and caption_bg != "transparent":
191
- words = narration_text.split()
192
- chunks = [' '.join(words[i:i+5]) for i in range(0, len(words), 5)]
193
- chunk_duration = audio_clip.duration / len(chunks)
194
- subtitle_clips = [
195
- TextClip(
196
- chunk,
197
- fontsize=text_size,
198
- color=text_color,
199
- bg_color=caption_bg,
200
- size=(target_resolution[0] * 0.8, None),
201
- method='caption',
202
- align='center'
203
- ).set_position(('center', target_resolution[1] * 0.7)).set_start(i * chunk_duration).set_end((i + 1) * chunk_duration)
204
- for i, chunk in enumerate(chunks)
205
- ]
206
- clip = CompositeVideoClip([clip] + subtitle_clips)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  clip = clip.set_audio(audio_clip)
 
209
  return clip
210
  except Exception as e:
211
- print(f"Clip creation failed: {e}")
212
  return None
213
 
214
- def add_background_music(final_video, custom_music_path, music_volume):
215
- """Add background music to the video."""
216
  try:
217
- if custom_music_path and os.path.exists(custom_music_path):
218
- bg_music = AudioFileClip(custom_music_path)
219
- else:
220
- bg_music = AudioFileClip("default_music.mp3") # Assume a default music file exists
221
- if bg_music.duration < final_video.duration:
222
- bg_music = concatenate_audioclips([bg_music] * math.ceil(final_video.duration / bg_music.duration))
223
- bg_music = bg_music.subclip(0, final_video.duration).volumex(music_volume)
224
- final_video = final_video.set_audio(CompositeAudioClip([final_video.audio, bg_music]))
225
- return final_video
 
 
 
 
 
 
 
 
 
226
  except Exception as e:
227
- print(f"Background music failed: {e}")
228
- return final_video
229
 
230
- # Gradio Interface
231
- with gr.Blocks(title="AI Documentary Video Generator") as app:
232
- ### Initial Inputs
233
- with gr.Column():
234
- concept = gr.Textbox(label="Video Concept", placeholder="Enter your video concept...")
235
- resolution = gr.Radio(["Full", "Short"], label="Resolution", value="Full")
236
- captions = gr.Radio(["Yes", "No"], label="Captions", value="Yes")
237
- video_percentage = gr.Slider(0, 100, label="Video Percentage", value=50)
238
- text_color = gr.ColorPicker(label="Text Color", value="#FFFFFF")
239
- text_size = gr.Slider(20, 60, label="Text Size", value=28)
240
- caption_bg = gr.ColorPicker(label="Caption Background Color", value="transparent")
241
- music_volume = gr.Slider(0, 1, label="Music Volume", value=0.08)
242
- custom_music = gr.File(label="Upload Custom Background Music", type="file")
243
- generate_script_btn = gr.Button("Generate Script")
244
-
245
- ### States
246
- num_clips = gr.State(value=0)
247
- titles_state = gr.State(value=[])
248
- initial_texts_state = gr.State(value=[])
249
-
250
- ### Clip Editing Section
251
- with gr.Column(visible=False) as clip_section:
252
- clip_textboxes = []
253
- clip_files = []
254
- for i in range(10): # Max 10 clips
255
- with gr.Row():
256
- text_box = gr.Textbox(label=f"Clip {i+1} Text", visible=False)
257
- file_upload = gr.File(label=f"Upload Media for Clip {i+1}", type="file", visible=False)
258
- clip_textboxes.append(text_box)
259
- clip_files.append(file_upload)
260
- generate_video_btn = gr.Button("Generate Video", visible=False)
261
-
262
- ### Output
263
- video_output = gr.Video(label="Generated Video")
264
-
265
- ### Script Generation Logic
266
- def generate_script_fn(concept):
267
- script = generate_script(concept)
268
- if not script:
269
- return 0, [], []
270
- elements = parse_script(script)
271
- titles = [e['prompt'] for e in elements if e['type'] == 'media']
272
- texts = [e['text'] for e in elements if e['type'] == 'tts']
273
- return len(titles), titles, texts
274
-
275
- def update_textboxes(texts):
276
- return [gr.update(value=texts[i] if i < len(texts) else "", visible=i < len(texts)) for i in range(10)]
277
-
278
- def update_files(n):
279
- return [gr.update(visible=i < n) for i in range(10)]
280
-
281
- generate_script_btn.click(
282
- fn=generate_script_fn,
283
- inputs=[concept],
284
- outputs=[num_clips, titles_state, initial_texts_state]
285
- ).then(
286
- fn=update_textboxes,
287
- inputs=[initial_texts_state],
288
- outputs=clip_textboxes
289
- ).then(
290
- fn=update_files,
291
- inputs=[num_clips],
292
- outputs=clip_files
293
- ).then(
294
- fn=lambda: gr.update(visible=True),
295
- outputs=[clip_section]
296
- ).then(
297
- fn=lambda: gr.update(visible=True),
298
- outputs=[generate_video_btn]
299
- )
300
-
301
- ### Video Generation Logic
302
- def generate_video_fn(resolution, captions, video_percentage, text_color, text_size, caption_bg, music_volume, custom_music, num_clips, titles, *clip_data):
303
- texts = clip_data[:10]
304
- files = clip_data[10:]
305
- temp_folder = tempfile.mkdtemp()
306
- target_resolution = (1920, 1080) if resolution == "Full" else (1080, 1920)
307
- clips = []
308
-
309
- for i in range(num_clips):
310
- text = texts[i]
311
- media_file = files[i]
312
- title = titles[i]
313
- if media_file:
314
- ext = os.path.splitext(media_file)[1].lower()
315
- media_path = os.path.join(temp_folder, f"clip_{i}{ext}")
316
- shutil.copy(media_file, media_path)
317
- asset_type = "video" if ext in ['.mp4', '.avi', '.mov'] else "image"
318
- else:
319
- media_asset = generate_media(title, video_percentage, temp_folder)
320
- if not media_asset:
321
- continue
322
- media_path = media_asset['path']
323
- asset_type = media_asset['asset_type']
324
-
325
- tts_path = generate_tts(text, 'en', temp_folder)
326
- if not tts_path:
327
- continue
328
 
329
- duration = max(3, len(text.split()) * 0.5)
330
- clip = create_clip(
331
- media_path, asset_type, tts_path, duration, text,
332
- text_color, text_size, caption_bg if captions == "Yes" else "transparent", target_resolution
333
- )
334
- if clip:
335
- clips.append(clip)
336
 
337
- if not clips:
338
- shutil.rmtree(temp_folder)
339
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
 
341
- final_video = concatenate_videoclips(clips, method="compose")
342
- final_video = add_background_music(final_video, custom_music, music_volume)
343
- final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=24)
344
- shutil.rmtree(temp_folder)
345
- return OUTPUT_VIDEO_FILENAME
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
- generate_video_btn.click(
348
- fn=generate_video_fn,
349
- inputs=[resolution, captions, video_percentage, text_color, text_size, caption_bg, music_volume, custom_music, num_clips, titles_state] + clip_textboxes + clip_files,
350
- outputs=[video_output]
351
- )
352
 
353
- app.launch(share=True)
 
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:
164
+ return response_data['choices'][0]['message']['content']
165
+ else:
166
+ print("Unexpected response format:", response_data)
167
+ return None
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()
190
+ if line.startswith("[") and "]" in line:
191
+ bracket_start = line.find("[")
192
+ bracket_end = line.find("]", bracket_start)
193
+ if bracket_start != -1 and bracket_end != -1:
194
+ if current_title is not None:
195
+ sections[current_title] = current_text.strip()
196
+ current_title = line[bracket_start+1:bracket_end]
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)
260
+ retry_delay *= 2
261
+ else:
262
+ print(f"Error fetching videos: {response.status_code} {response.text}")
263
+ if attempt < max_retries - 1:
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")
282
+ return random_video
283
+ else:
284
+ print("No suitable videos found after searching all pages.")
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", [])
303
+ if photos:
304
+ photo = random.choice(photos[:min(5, len(photos))])
305
+ img_url = photo.get("src", {}).get("original")
306
+ return img_url
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)
314
+ retry_delay *= 2
315
+ else:
316
+ print(f"Error fetching images: {response.status_code} {response.text}")
317
+ if attempt < max_retries - 1:
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:
350
+ print(f"No Google Images found for query: {query}")
351
+ return None
352
  except Exception as e:
353
+ print(f"Error in Google Images search: {e}")
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()
373
+ img = Image.open(filename)
374
+ if img.mode != 'RGB':
375
+ img = img.convert('RGB')
376
+ img.save(filename)
377
+ print(f"Image validated and processed: {filename}")
378
+ return filename
379
+ except Exception as e_validate:
380
+ print(f"Downloaded file is not a valid image: {e_validate}")
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):
388
+ os.remove(filename)
389
+ return None
390
+ except Exception as e_general:
391
+ print(f"General error during image processing: {e_general}")
392
+ if os.path.exists(filename):
393
+ os.remove(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()
401
  with open(filename, 'wb') as f:
402
  for chunk in response.iter_content(chunk_size=8192):
403
  f.write(chunk)
404
+ print(f"Video downloaded successfully to: {filename}")
405
  return filename
406
  except Exception as e:
407
+ print(f"Video download error: {e}")
408
+ if os.path.exists(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")
423
+ image_url = search_google_images(prompt)
424
+ if image_url:
425
+ downloaded_image = download_image(image_url, image_file)
426
+ if downloaded_image:
427
+ print(f"News image saved to {downloaded_image}")
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.25:
433
+ video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
434
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
435
+ if video_url:
436
+ downloaded_video = download_video(video_url, video_file)
437
+ if downloaded_video:
438
+ print(f"Video asset saved to {downloaded_video}")
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:
446
+ downloaded_image = download_image(image_url, image_file)
447
+ if downloaded_image:
448
+ print(f"Image asset saved to {downloaded_image}")
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}")
456
+ fallback_file = os.path.join(TEMP_FOLDER, f"fallback_{term}.jpg")
457
+ fallback_url = search_pexels_images(term, PEXELS_API_KEY)
458
+ if fallback_url:
459
+ downloaded_fallback = download_image(fallback_url, fallback_file)
460
+ if downloaded_fallback:
461
+ print(f"Fallback image saved to {downloaded_fallback}")
462
+ return {"path": downloaded_fallback, "asset_type": "image"}
463
+ else:
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+')
494
+ audio_segments = []
495
+ for i, (gs, ps, audio) in enumerate(generator):
496
+ audio_segments.append(audio)
497
+ full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
498
+ sf.write(file_path, full_audio, 24000)
499
+ print(f"TTS audio saved to {file_path} (Kokoro)")
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
546
+ start_center = (new_width / 2, new_height / 2)
547
+ end_center = start_center
548
+ elif effect_type == "zoom-out":
549
+ start_zoom = 1.1
550
+ end_zoom = 0.9
551
+ start_center = (new_width / 2, new_height / 2)
552
+ end_center = start_center
553
+ elif effect_type == "pan-left":
554
+ start_zoom = 1.0
555
+ end_zoom = 1.0
556
+ start_center = (max_offset_x + target_w / 2, (max_offset_y // 2) + target_h / 2)
557
+ end_center = (target_w / 2, (max_offset_y // 2) + target_h / 2)
558
+ elif effect_type == "pan-right":
559
+ start_zoom = 1.0
560
+ end_zoom = 1.0
561
+ start_center = (target_w / 2, (max_offset_y // 2) + target_h / 2)
562
+ end_center = (max_offset_x + target_w / 2, (max_offset_y // 2) + target_h / 2)
563
+ elif effect_type == "up-left":
564
+ start_zoom = 1.0
565
+ end_zoom = 1.0
566
+ start_center = (max_offset_x + target_w / 2, max_offset_y + target_h / 2)
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
574
+ ratio = 0.5 - 0.5 * math.cos(math.pi * ratio)
575
+ current_zoom = start_zoom + (end_zoom - start_zoom) * ratio
576
+ crop_w = int(target_w / current_zoom)
577
+ crop_h = int(target_h / current_zoom)
578
+ current_center_x = start_center[0] + (end_center[0] - start_center[0]) * ratio
579
+ current_center_y = start_center[1] + (end_center[1] - start_center[1]) * ratio
580
+ min_center_x = crop_w / 2
581
+ max_center_x = new_width - crop_w / 2
582
+ min_center_y = crop_h / 2
583
+ max_center_y = new_height - crop_h / 2
584
+ current_center_x = max(min_center_x, min(current_center_x, max_center_x))
585
+ current_center_y = max(min_center_y, min(current_center_y, max_center_y))
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
601
+ clip = clip.crop(x1=crop_amount, x2=clip.w - crop_amount, y1=0, y2=clip.h)
602
  else:
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)
660
+ if clip.duration < target_duration:
661
+ clip = clip.loop(duration=target_duration)
662
+ else:
663
+ clip = clip.subclip(0, target_duration)
664
+ elif asset_type == "image":
665
+ img = Image.open(media_path)
666
+ if img.mode != 'RGB':
667
+ with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp:
668
+ img.convert('RGB').save(temp.name)
669
+ media_path = temp.name
670
+ img.close()
671
+ clip = ImageClip(media_path).set_duration(target_duration)
672
+ clip = apply_kenburns_effect(clip, TARGET_RESOLUTION)
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()
680
+ chunks = []
681
+ current_chunk = []
682
+ for word in words:
683
+ current_chunk.append(word)
684
+ if len(current_chunk) >= 5:
685
+ chunks.append(' '.join(current_chunk))
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
697
+ txt_clip = TextClip(
698
+ chunk_text,
699
+ fontsize=45,
700
+ font='Arial-Bold',
701
+ color=CAPTION_COLOR,
702
+ bg_color='rgba(0, 0, 0, 0.25)',
703
+ method='caption',
704
+ align='center',
705
+ stroke_width=2,
706
+ stroke_color=CAPTION_COLOR,
707
+ size=(TARGET_RESOLUTION[0] * 0.8, None)
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}")
715
+ txt_clip = TextClip(
716
+ narration_text,
717
+ fontsize=28,
718
+ color=CAPTION_COLOR,
719
+ align='center',
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
727
  except Exception as e:
728
+ print(f"Error in create_clip: {str(e)}")
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 = [
736
+ "/etc/ImageMagick-6/policy.xml",
737
+ "/etc/ImageMagick-7/policy.xml",
738
+ "/etc/ImageMagick/policy.xml",
739
+ "/usr/local/etc/ImageMagick-7/policy.xml"
740
+ ]
741
+ found_policy = next((path for path in policy_paths if os.path.exists(path)), None)
742
+ if not found_policy:
743
+ print("No policy.xml found. Using alternative subtitle method.")
744
+ return False
745
+ print(f"Modifying policy file at {found_policy}")
746
+ os.system(f"sudo cp {found_policy} {found_policy}.bak")
747
+ os.system(f"sudo sed -i 's/rights=\"none\"/rights=\"read|write\"/g' {found_policy}")
748
+ os.system(f"sudo sed -i 's/<policy domain=\"path\" pattern=\"@\*\"[^>]*>/<policy domain=\"path\" pattern=\"@*\" rights=\"read|write\"/g' {found_policy}")
749
+ os.system(f"sudo sed -i 's/<policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"[^>]*>/<!-- <policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"> -->/g' {found_policy}")
750
+ print("ImageMagick policies updated successfully.")
751
+ return True
752
  except Exception as e:
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']}'")
808
+ media_asset = generate_media(media_elem['prompt'], current_index=idx, total_segments=len(paired_elements))
809
+ if not media_asset:
810
+ print(f"Skipping segment {idx+1} due to missing media asset.")
811
+ continue
812
+ tts_path = generate_tts(tts_elem['text'], tts_elem['voice'])
813
+ if not tts_path:
814
+ print(f"Skipping segment {idx+1} due to TTS generation failure.")
815
+ continue
816
+ clip = create_clip(
817
+ media_path=media_asset['path'],
818
+ asset_type=media_asset['asset_type'],
819
+ tts_path=tts_path,
820
+ duration=tts_elem['duration'],
821
+ effects=media_elem.get('effects', 'fade-in'),
822
+ narration_text=tts_elem['text'],
823
+ segment_index=idx
824
+ )
825
+ if clip:
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=24, 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="Full"),
856
+ gr.Radio(["Yes", "No"], label="Captions", value="Yes")
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