testdeep123 commited on
Commit
47a0253
Β·
verified Β·
1 Parent(s): 37089c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -667
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # -*- coding: utf-8 -*-
2
  from kokoro import KPipeline
3
  import soundfile as sf
4
- import torch # Keep torch import if Kokoro needs it implicitly
5
  import os
6
  from moviepy.editor import (
7
  VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips,
@@ -22,7 +22,7 @@ from bs4 import BeautifulSoup
22
  from urllib.parse import quote
23
  import gradio as gr
24
  import shutil
25
- import traceback # For detailed error logging
26
 
27
  # --- Initialize Kokoro TTS pipeline ---
28
  pipeline = None
@@ -42,7 +42,6 @@ def initialize_kokoro():
42
  # Attempt initialization once at the start
43
  initialize_kokoro()
44
 
45
-
46
  # --- Configure ImageMagick ---
47
  try:
48
  imagemagick_path = None
@@ -54,22 +53,15 @@ try:
54
  if imagemagick_path:
55
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": imagemagick_path})
56
  print(f"ImageMagick path set to: {imagemagick_path}")
57
- elif not any(shutil.which(cmd) for cmd in ["convert", "magick"]): # Check PATH using shutil.which
58
  print("Warning: ImageMagick 'convert' or 'magick' command not found in common paths or system PATH.")
59
  print(" TextClip captions requiring ImageMagick may fail.")
60
  except Exception as e:
61
  print(f"Warning: Error configuring ImageMagick: {e}. TextClip captions might fail.")
62
 
63
-
64
  # --- Global Configuration ---
65
- PEXELS_API_KEY = os.getenv('PEXELS_API_KEY', 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna') # Replace/Use Env Var
66
- OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', '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') # Replace/Use Env Var
67
-
68
- # Check if API keys look like defaults
69
- if not PEXELS_API_KEY or 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna' in PEXELS_API_KEY:
70
- print("\nWARNING: Pexels API Key seems to be the default or not set. Media fetching might fail.\n")
71
- if not OPENROUTER_API_KEY or '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' in OPENROUTER_API_KEY:
72
- print("\nWARNING: OpenRouter API Key seems to be the default or not set. Script generation might fail.\n")
73
 
74
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
75
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
@@ -83,66 +75,46 @@ TEMP_FOLDER = None
83
 
84
  def generate_script(user_input):
85
  """Generates script using OpenRouter API."""
86
- if not OPENROUTER_API_KEY or 'sk-or-v1-' not in OPENROUTER_API_KEY:
87
  print("Error: OpenRouter API Key not set or looks invalid.")
88
  return "Error: OpenRouter API Key not configured."
 
89
  headers = {
90
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
91
- 'HTTP-Referer': 'http://localhost:7860', # Referer helps OpenRouter identify usage
92
  'X-Title': 'AI Documentary Maker'
93
  }
94
- # (Keep the detailed prompt as it was)
95
  prompt = f"""Short Documentary Script GeneratorInstructions:
96
- If I say "use this," just output the script exactly as I gave it.
97
- If I only give topics, generate a script based on them.
98
- 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
99
- 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
100
- Formatting Rules:
101
- Title in Square Brackets:
102
- Each section starts with a one-word title inside [ ] (max two words if necessary).
103
- This title will be used as a search term for Pexels footage.
104
- Casual & Funny Narration:
105
- Each section has 5-10 words of narration.
106
- Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
107
- No Special Formatting:
108
- 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
109
- Generalized Search Terms:
110
- If a term is too specific, make it more general for Pexels search.
111
- Scene-Specific Writing:
112
- Each section describes only what should be shown in the video.
113
- 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 .....
114
- No extra text, just the script.
115
- Example Output:
116
- [North Korea]
117
- Top 5 unknown facts about North Korea.
118
- [Invisibility]
119
- North Korea’s internet speed is so fast… it doesn’t exist.
120
- [Leadership]
121
- Kim Jong-un once won an election with 100% votes… against himself.
122
- [Magic]
123
- North Korea discovered time travel. That’s why their news is always from the past.
124
- [Warning]
125
- Subscribe now, or Kim Jong-un will send you a free one-way ticket… to North Korea.
126
- [Freedom]
127
- North Korean citizens can do anything… as long as it's government-approved.
128
  Now here is the Topic/scrip: {user_input}
129
  """
130
- data = {'model': OPENROUTER_MODEL, 'messages': [{'role': 'user', 'content': prompt}], 'temperature': 0.4, 'max_tokens': 1024}
 
 
 
 
 
 
 
131
  try:
132
- response = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, json=data, timeout=60)
 
133
  response.raise_for_status()
134
  response_data = response.json()
 
135
  if 'choices' in response_data and len(response_data['choices']) > 0:
136
  script_content = response_data['choices'][0]['message']['content'].strip()
137
  if '[' in script_content and ']' in script_content:
138
- print("Script generated successfully.")
139
- return script_content
140
  else:
141
- print("Warning: Generated script missing expected format '[Title] Narration'.")
142
- return script_content # Return anyway, user might fix it
143
  else:
144
  print(f"API Error: Unexpected response format from OpenRouter: {response_data}")
145
  return "Error: Could not parse script from API response."
 
146
  except requests.exceptions.Timeout:
147
  print("API Error: Request to OpenRouter timed out.")
148
  return "Error: Script generation timed out."
@@ -152,659 +124,186 @@ Now here is the Topic/scrip: {user_input}
152
  print(f" Details: {error_details}")
153
  return f"Error: Failed connect to script generation service ({e.response.status_code if e.response else 'N/A'})."
154
  except Exception as e:
155
- print(f"Error during script generation: {e}"); traceback.print_exc()
156
- return f"Error: Unexpected error during script generation."
157
-
158
- def parse_script(script_text):
159
- """Parses the script text into media and TTS elements."""
160
- sections = {}
161
- current_title = None
162
- current_text = ""
163
- if not script_text or not isinstance(script_text, str): return []
164
- try:
165
- lines = script_text.strip().splitlines()
166
- for line in lines:
167
- line = line.strip()
168
- if not line: continue
169
- match = re.match(r'^\s*\[([^\]]+)\]\s*(.*)', line)
170
- if match:
171
- if current_title is not None and current_text: sections[current_title] = current_text.strip()
172
- current_title = match.group(1).strip()
173
- current_text = match.group(2).strip() + " "
174
- elif current_title: current_text += line + " "
175
- if current_title is not None and current_text: sections[current_title] = current_text.strip()
176
- if not sections: print("Warning: No sections found in script. Check format."); return []
177
- elements = []
178
- for title, narration in sections.items():
179
- if not title or not narration: print(f"Warning: Skipping section with empty title/narration."); continue
180
- media_element = {"type": "media", "prompt": title, "effects": "fade-in"}
181
- words = narration.split(); duration = max(2.0, len(words) * 0.4) # Estimate
182
- tts_element = {"type": "tts", "text": narration, "voice": "en", "duration": duration}
183
- elements.append(media_element); elements.append(tts_element)
184
- print(f"Script parsed into {len(elements)//2} sections.")
185
- return elements
186
- except Exception as e: print(f"Error parsing script: {e}"); traceback.print_exc(); return []
187
-
188
- def search_pexels_videos(query, pexels_api_key):
189
- """Searches Pexels for videos matching the query."""
190
- if not pexels_api_key or 'YOUR_PEXELS_API_KEY' in pexels_api_key: print("Error: Pexels API Key invalid."); return None
191
- headers = {'Authorization': pexels_api_key}
192
- base_url = "https://api.pexels.com/videos/search"
193
- params = {"query": query, "per_page": 15, "page": 1, "orientation": "landscape"} # Start with page 1
194
- max_retries = 3; retry_delay = 2; all_videos = []
195
- print(f"Searching Pexels videos for: '{query}'...")
196
- for page in range(1, 4): # Search first 3 pages
197
- params["page"] = page
198
- print(f" Fetching page {page}...")
199
- for attempt in range(max_retries):
200
- try:
201
- response = requests.get(base_url, headers=headers, params=params, timeout=20)
202
- response.raise_for_status()
203
- data = response.json(); videos = data.get("videos", [])
204
- if not videos: print(f" No videos found on page {page}."); break # Stop searching this query if no results on a page
205
- found_count = 0
206
- for video in videos:
207
- video_files = video.get("video_files", [])
208
- hd_link, fhd_link, best_link = None, None, None
209
- for file in video_files:
210
- q, link, w = file.get("quality"), file.get("link"), file.get("width", 0)
211
- if not link: continue
212
- if not best_link: best_link = link
213
- if q == "hd" and w >= 1280: hd_link = link
214
- if q == "fhd" and w >= 1920: fhd_link = link
215
- chosen_link = fhd_link or hd_link or best_link
216
- if chosen_link: all_videos.append(chosen_link); found_count += 1
217
- print(f" Found {found_count} links on page {page}.")
218
- break # Success for this page
219
- except requests.exceptions.Timeout: print(f"Timeout (attempt {attempt+1}/{max_retries}). Retrying..."); time.sleep(retry_delay); retry_delay *= 1.5
220
- except requests.exceptions.HTTPError as e:
221
- if e.response.status_code == 429: print(f"Rate limit (attempt {attempt+1}/{max_retries}). Retrying..."); time.sleep(retry_delay); retry_delay *= 1.5
222
- elif e.response.status_code in [400, 404]: print(f"Pexels API Error {e.response.status_code}. Skipping. Resp: {e.response.text}"); return None
223
- else: print(f"HTTP Error {e.response.status_code} (attempt {attempt+1}/{max_retries})"); time.sleep(retry_delay); retry_delay *= 1.5;
224
- if attempt == max_retries - 1: return None # Failed after retries
225
- except requests.exceptions.RequestException as e: print(f"Network error: {e} (attempt {attempt+1}/{max_retries})"); time.sleep(retry_delay); retry_delay *= 1.5;
226
- if attempt == max_retries - 1: return None # Failed after retries
227
- time.sleep(0.5) # Pause between pages
228
- if all_videos: return random.choice(all_videos)
229
- else: print(f"No suitable Pexels videos found for '{query}'."); return None
230
-
231
- def search_pexels_images(query, pexels_api_key):
232
- """Searches Pexels for images matching the query."""
233
- if not pexels_api_key or 'YOUR_PEXELS_API_KEY' in pexels_api_key: print("Error: Pexels API Key invalid."); return None
234
- headers = {'Authorization': pexels_api_key}
235
- url = "https://api.pexels.com/v1/search"
236
- params = {"query": query, "per_page": 15, "orientation": "landscape"}
237
- max_retries = 3; retry_delay = 2
238
- print(f"Searching Pexels images for: '{query}'...")
239
- for attempt in range(max_retries):
240
- try:
241
- response = requests.get(url, headers=headers, params=params, timeout=15)
242
- response.raise_for_status()
243
- data = response.json(); photos = data.get("photos", [])
244
- if photos:
245
- potential_photos = photos[:min(10, len(photos))]
246
- chosen_photo = random.choice(potential_photos)
247
- img_url = chosen_photo.get("src", {}).get("large2x") or chosen_photo.get("src", {}).get("original") or chosen_photo.get("src", {}).get("large")
248
- if img_url: print(f"Found {len(photos)} images, selected one."); return img_url
249
- else: break # Chosen photo invalid, retry or fail
250
- else: print(f"No Pexels images found for '{query}'."); return None
251
- except requests.exceptions.Timeout: print(f"Timeout (attempt {attempt+1}/{max_retries}). Retrying..."); time.sleep(retry_delay); retry_delay *= 1.5
252
- except requests.exceptions.HTTPError as e:
253
- if e.response.status_code == 429: print(f"Rate limit (attempt {attempt+1}/{max_retries}). Retrying..."); time.sleep(retry_delay); retry_delay *= 1.5
254
- elif e.response.status_code in [400, 404]: print(f"Pexels API Error {e.response.status_code}. Skipping. Resp: {e.response.text}"); return None
255
- else: print(f"HTTP Error {e.response.status_code} (attempt {attempt+1}/{max_retries})"); time.sleep(retry_delay); retry_delay *= 1.5;
256
- if attempt == max_retries - 1: return None
257
- except requests.exceptions.RequestException as e: print(f"Network error: {e} (attempt {attempt+1}/{max_retries})"); time.sleep(retry_delay); retry_delay *= 1.5;
258
- if attempt == max_retries - 1: return None
259
- print(f"Failed to find Pexels images for '{query}'."); return None
260
-
261
- def search_google_images(query):
262
- """Searches Google Images (caution: scraping)."""
263
- print(f"Attempting Google Images search for: '{query}' (use with caution)...")
264
- try:
265
- search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch&hl=en&safe=active"
266
- headers = {"User-Agent": USER_AGENT}
267
- response = requests.get(search_url, headers=headers, timeout=15)
268
- response.raise_for_status()
269
- soup = BeautifulSoup(response.text, "html.parser")
270
- img_tags = soup.find_all("img")
271
- image_urls = [img.get("src") or img.get("data-src") for img in img_tags]
272
- valid_urls = [src for src in image_urls if src and src.startswith("http") and "gstatic.com/images" not in src]
273
- if valid_urls: return random.choice(valid_urls[:min(len(valid_urls), 10)])
274
- else: print(f"No suitable Google Images found for '{query}'."); return None
275
- except requests.exceptions.RequestException as e: print(f"Error during Google Images request: {e}"); return None
276
- except Exception as e: print(f"Error parsing Google Images results: {e}"); return None
277
-
278
- # Corrected download_image with SyntaxError Fix
279
- def download_image(image_url, filename):
280
- """Downloads and validates an image."""
281
- if not TEMP_FOLDER or not os.path.exists(TEMP_FOLDER):
282
- print("Error: Temp folder invalid.")
283
- return None
284
-
285
- full_path = os.path.join(TEMP_FOLDER, os.path.basename(filename))
286
- try:
287
- headers = {"User-Agent": USER_AGENT}
288
- print(f"Downloading image: {image_url} -> {os.path.basename(full_path)}")
289
- response = requests.get(image_url, headers=headers, stream=True, timeout=20, verify=True)
290
- response.raise_for_status()
291
-
292
- with open(full_path, 'wb') as f:
293
- for chunk in response.iter_content(chunk_size=8192):
294
- f.write(chunk)
295
- print("Image downloaded.")
296
-
297
- if os.path.getsize(full_path) < 1024:
298
- print(f"Warning: Downloaded file is small ({os.path.getsize(full_path)} bytes).")
299
-
300
- # Validation
301
- try:
302
- img = Image.open(full_path)
303
- img.verify()
304
- img.close() # Verify structure
305
-
306
- img = Image.open(full_path) # Reopen to process
307
- if img.mode != 'RGB':
308
- print(f"Converting image from {img.mode} to RGB.")
309
- img = img.convert('RGB')
310
- img.save(full_path, format='JPEG')
311
- img.close()
312
- print(f"Image validated (RGB): {os.path.basename(full_path)}")
313
- return full_path
314
-
315
- except (IOError, SyntaxError, Image.UnidentifiedImageError) as e_validate:
316
- print(f"Error: Downloaded file invalid image: {e_validate}")
317
- if os.path.exists(full_path):
318
- try:
319
- os.remove(full_path)
320
- except OSError:
321
- pass
322
- return None
323
-
324
- except Exception as e_img_proc:
325
- print(f"Error processing downloaded image: {e_img_proc}")
326
- if os.path.exists(full_path):
327
- try:
328
- os.remove(full_path)
329
- except OSError:
330
- pass
331
- return None
332
-
333
- except requests.exceptions.Timeout:
334
- print(f"Error: Timeout downloading {image_url}")
335
- return None
336
-
337
- except requests.exceptions.RequestException as e_download:
338
- print(f"Error: Download failed: {e_download}")
339
- if os.path.exists(full_path):
340
- try:
341
- os.remove(full_path)
342
- except OSError:
343
- pass
344
- return None
345
-
346
- except Exception as e_general:
347
- print(f"Error during image download/processing: {e_general}")
348
- if os.path.exists(full_path):
349
- try:
350
- os.remove(full_path)
351
- except OSError:
352
- pass
353
- return None
354
-
355
-
356
- def download_video(video_url, filename):
357
- """Downloads a video file."""
358
- if not TEMP_FOLDER or not os.path.exists(TEMP_FOLDER):
359
- print("Error: Temp folder invalid.")
360
- return None
361
-
362
- full_path = os.path.join(TEMP_FOLDER, os.path.basename(filename))
363
- try:
364
- headers = {"User-Agent": USER_AGENT}
365
- print(f"Downloading video: {video_url} -> {os.path.basename(full_path)}")
366
- response = requests.get(video_url, stream=True, timeout=60, verify=True)
367
- response.raise_for_status()
368
-
369
- with open(full_path, 'wb') as f:
370
- for chunk in response.iter_content(chunk_size=8192 * 10):
371
- f.write(chunk)
372
- print("Video downloaded successfully.")
373
-
374
- min_video_size = 100 * 1024
375
- if os.path.getsize(full_path) < min_video_size:
376
- print(f"Warning: Downloaded video is small ({os.path.getsize(full_path)} bytes).")
377
- return full_path
378
-
379
- except requests.exceptions.Timeout:
380
- print(f"Error: Timeout downloading {video_url}")
381
- return None
382
-
383
- except requests.exceptions.RequestException as e:
384
- print(f"Error: Video download failed: {e}")
385
- if os.path.exists(full_path):
386
- try:
387
- os.remove(full_path)
388
- except OSError:
389
- pass
390
- return None
391
-
392
- except Exception as e:
393
- print(f"Error during video download: {e}")
394
- if os.path.exists(full_path):
395
- try:
396
- os.remove(full_path)
397
- except OSError:
398
- pass
399
- return None
400
-
401
- # Corrected generate_tts with SyntaxError Fix and Kokoro Default Voice
402
- def generate_tts(text, voice): # voice parameter ignored for now
403
- """Generates TTS using Kokoro default voice for lang_code='a'."""
404
- global pipeline
405
- if pipeline is None:
406
- if not initialize_kokoro():
407
- print("Error: Failed Kokoro init.")
408
- return None
409
- if not TEMP_FOLDER or not os.path.exists(TEMP_FOLDER):
410
- print("Error: Temp folder invalid.")
411
- return None
412
-
413
- safe_text_prefix = re.sub(r'[^\w\s-]', '', text[:20]).strip().replace(' ', '_')
414
- file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text_prefix}.wav")
415
-
416
- if os.path.exists(file_path):
417
- if os.path.getsize(file_path) > 100:
418
- print(f"Using cached TTS: {os.path.basename(file_path)}")
419
- return file_path
420
- else:
421
- print("Cached TTS invalid. Regenerating.")
422
- try:
423
- os.remove(file_path)
424
- except OSError:
425
- pass
426
-
427
- print(f"Generating TTS for: '{text[:50]}...'")
428
- try:
429
- # Use default voice for lang_code='a' by not specifying 'voice' param
430
- generator = pipeline(text, speed=1.0)
431
- audio_segments = [chunk[-1] if isinstance(chunk, tuple) else chunk
432
- for chunk in generator if isinstance(chunk, (np.ndarray, tuple))]
433
- if not audio_segments:
434
- print("Error: TTS produced no audio segments.")
435
- return None
436
- full_audio = np.concatenate(audio_segments)
437
- sf.write(file_path, full_audio, 24000) # Kokoro uses 24kHz
438
- print(f"TTS audio saved: {os.path.basename(file_path)}")
439
- if os.path.exists(file_path) and os.path.getsize(file_path) > 100:
440
- return file_path
441
- else:
442
- print("Error: TTS file invalid post-generation.")
443
- if os.path.exists(file_path):
444
- try:
445
- os.remove(file_path)
446
- except OSError:
447
- pass
448
- return None
449
- except requests.exceptions.RequestException as req_err:
450
- print(f"Error during Kokoro TTS (Network/Download): {req_err}")
451
- traceback.print_exc()
452
- if os.path.exists(file_path):
453
- try:
454
- os.remove(file_path)
455
- except OSError:
456
- pass
457
- return None
458
- except Exception as e:
459
- print(f"Error during Kokoro TTS generation: {e}")
460
  traceback.print_exc()
461
- if os.path.exists(file_path):
462
- try:
463
- os.remove(file_path)
464
- except OSError:
465
- pass
466
- return None
467
-
468
- def apply_kenburns_effect(clip, target_resolution, effect_type=None):
469
- """Applies Ken Burns effect to an ImageClip."""
470
- resized_clip = None # Initialize
471
- try:
472
- target_w, target_h = target_resolution
473
- if not isinstance(clip, ImageClip) or clip.duration is None or clip.duration <= 0:
474
- print("Warning: Ken Burns needs ImageClip with duration."); return resize_to_fill(clip.set_duration(3.0), target_resolution)
475
- base_scale = 1.20; initial_w, initial_h = int(target_w * base_scale), int(target_h * base_scale)
476
- img_aspect = clip.w / clip.h; initial_aspect = initial_w / initial_h
477
- if img_aspect > initial_aspect: scaled_h, scaled_w = initial_h, int(initial_h * img_aspect)
478
- else: scaled_w, scaled_h = initial_w, int(initial_w / img_aspect)
479
- scaled_w, scaled_h = max(initial_w, scaled_w), max(initial_h, scaled_h)
480
- resized_clip = clip.resize(newsize=(scaled_w, scaled_h)) # Keep ref for closing
481
- nw, nh = scaled_w, scaled_h; max_offset_x, max_offset_y = max(0, nw - target_w), max(0, nh - target_h)
482
- effects = { "zoom-in": {"sZ": 1.0, "eZ": base_scale, "sP": (0.5, 0.5), "eP": (0.5, 0.5)}, "zoom-out": {"sZ": base_scale, "eZ": 1.0, "sP": (0.5, 0.5), "eP": (0.5, 0.5)}, "pan-left": {"sZ": base_scale, "eZ": base_scale, "sP": (1.0, 0.5), "eP": (0.0, 0.5)}, "pan-right": {"sZ": base_scale, "eZ": base_scale, "sP": (0.0, 0.5), "eP": (1.0, 0.5)}, "pan-up": {"sZ": base_scale, "eZ": base_scale, "sP": (0.5, 1.0), "eP": (0.5, 0.0)}, "pan-down": {"sZ": base_scale, "eZ": base_scale, "sP": (0.5, 0.0), "eP": (0.5, 1.0)}, "diag-tl-br": {"sZ": base_scale, "eZ": base_scale, "sP": (0.0, 0.0), "eP": (1.0, 1.0)}, "diag-tr-bl": {"sZ": base_scale, "eZ": base_scale, "sP": (1.0, 0.0), "eP": (0.0, 1.0)}, }
483
- if effect_type is None or effect_type == "random" or effect_type not in effects: effect_type = random.choice(list(effects.keys()))
484
- print(f"Applying Ken Burns effect: {effect_type}")
485
- p = effects[effect_type] # Params shortcut
486
- def transform_frame(get_frame, t):
487
- img_frame = resized_clip.get_frame(0); ratio = t / clip.duration if clip.duration > 0 else 0
488
- curr_Z = p["sZ"] + (p["eZ"] - p["sZ"]) * ratio; pX = p["sP"][0] + (p["eP"][0] - p["sP"][0]) * ratio; pY = p["sP"][1] + (p["eP"][1] - p["sP"][1]) * ratio
489
- crop_w, crop_h = int(target_w / curr_Z), int(target_h / curr_Z); crop_w, crop_h = min(crop_w, nw), min(crop_h, nh)
490
- if crop_w <= 0 or crop_h <= 0: return cv2.resize(img_frame, (target_w, target_h), interpolation=cv2.INTER_AREA)
491
- cX = (nw / 2) + (pX - 0.5) * max_offset_x; cY = (nh / 2) + (pY - 0.5) * max_offset_y
492
- min_cX, max_cX = crop_w / 2, nw - crop_w / 2; min_cY, max_cY = crop_h / 2, nh - crop_h / 2
493
- cX = max(min_cX, min(cX, max_cX)); cY = max(min_cY, min(cY, max_cY))
494
- cropped = cv2.getRectSubPix(img_frame, (crop_w, crop_h), (cX, cY))
495
- return cv2.resize(cropped, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
496
- final_effect_clip = clip.set_duration(clip.duration).fl(transform_frame) # Apply effect
497
- return final_effect_clip
498
- except Exception as e: print(f"Error applying Ken Burns: {e}. Falling back."); traceback.print_exc(); return resize_to_fill(clip, target_resolution)
499
- finally:
500
- if resized_clip: try: resized_clip.close(); except Exception: pass # Close intermediate clip
501
-
502
- def resize_to_fill(clip, target_resolution):
503
- """Resizes and crops clip to fill target resolution."""
504
- resized_clip = None # Initialize
505
- try:
506
- target_w, target_h = target_resolution; target_aspect = target_w / target_h
507
- clip_w, clip_h = clip.w, clip.h
508
- if clip_w is None or clip_h is None or clip_w == 0 or clip_h == 0:
509
- try: frame = clip.get_frame(0); clip_h, clip_w, _ = frame.shape;
510
- except Exception as size_err: print(f"Error: Cannot get clip size: {size_err}. Skipping resize."); return clip
511
- if clip_w == 0 or clip_h == 0: print("Error: Clip has zero dimension. Skipping resize."); return clip
512
-
513
- clip_aspect = clip_w / clip_h
514
- if abs(clip_aspect - target_aspect) < 0.01: # Aspect ratios match, just resize
515
- final_clip = clip.resize(newsize=target_resolution)
516
- elif clip_aspect > target_aspect: # Wider than target
517
- resized_clip = clip.resize(height=target_h)
518
- crop = (resized_clip.w - target_w) / 2; x1, x2 = max(0, math.floor(crop)), min(resized_clip.w, resized_clip.w - math.ceil(crop))
519
- final_clip = resized_clip.crop(x1=x1, y1=0, x2=x2, y2=target_h)
520
- else: # Taller than target
521
- resized_clip = clip.resize(width=target_w)
522
- crop = (resized_clip.h - target_h) / 2; y1, y2 = max(0, math.floor(crop)), min(resized_clip.h, resized_clip.h - math.ceil(crop))
523
- final_clip = resized_clip.crop(x1=0, y1=y1, x2=target_w, y2=y2)
524
-
525
- # Ensure final size is exact if crop/resize was done
526
- if final_clip is not clip and (final_clip.w != target_w or final_clip.h != target_h):
527
- final_clip = final_clip.resize(newsize=target_resolution)
528
-
529
- return final_clip
530
- except Exception as e: print(f"Error during resize_to_fill: {e}. Returning original."); traceback.print_exc(); return clip
531
- finally:
532
- if resized_clip: try: resized_clip.close(); except Exception: pass # Close intermediate clip if created
533
-
534
- # Enhanced add_background_music for debugging
535
- def add_background_music(final_video, music_file, bg_music_volume=0.08):
536
- """Adds background music, enhanced with debugging logs."""
537
- if not music_file: print("No background music file provided. Skipping."); return final_video
538
- if not os.path.exists(music_file): print(f"Error: BG music file not found: {music_file}. Skipping."); return final_video
539
-
540
- print(f"\n--- Adding Background Music ---")
541
- print(f" Source File: {music_file}")
542
- bg_music = None; final_video_with_music = final_video # Start with original
543
- original_video_audio = final_video.audio # Store original ref
544
-
545
- try:
546
- print(f" DEBUG: Loading AudioFileClip...")
547
- bg_music = AudioFileClip(music_file)
548
- print(f" DEBUG: Music loaded. Duration: {bg_music.duration:.2f}s")
549
-
550
- video_duration = final_video.duration
551
- if video_duration <= 0: print(" WARNING: Video duration is zero. Cannot add music."); return final_video
552
- if bg_music.duration <= 0: print(" WARNING: Music duration is zero. Skipping music."); return final_video
553
-
554
- print(f" DEBUG: Video duration: {video_duration:.2f}s. Adjusting music...")
555
- if bg_music.duration < video_duration:
556
- print(f" DEBUG: Looping music...")
557
- looped_bg = bg_music.loop(duration=video_duration)
558
- bg_music.close(); bg_music = looped_bg # Replace and close original
559
- print(f" DEBUG: Music looped.")
560
- elif bg_music.duration > video_duration:
561
- print(f" DEBUG: Trimming music...")
562
- trimmed_bg = bg_music.subclip(0, video_duration)
563
- bg_music.close(); bg_music = trimmed_bg # Replace and close original
564
- print(f" DEBUG: Music trimmed.")
565
-
566
- print(f" DEBUG: Adjusting volume to {bg_music_volume}...")
567
- bg_music = bg_music.volumex(bg_music_volume)
568
-
569
- mixed_audio = None
570
- if original_video_audio:
571
- print(f" DEBUG: Compositing with existing video audio (Duration: {original_video_audio.duration:.2f}s)...")
572
- try:
573
- mixed_audio = CompositeAudioClip([original_video_audio, bg_music])
574
- print(" DEBUG: Audio composited.")
575
- except Exception as composite_error:
576
- print(f" ERROR: Failed to composite audio clips: {composite_error}"); traceback.print_exc()
577
- print(" FALLBACK: Returning video with original audio.")
578
- return final_video # Return original video (original_video_audio is still attached)
579
- print(f" DEBUG: Setting composite audio on video...")
580
- final_video_with_music = final_video.set_audio(mixed_audio)
581
- print(" DEBUG: Composite audio set.")
582
- else:
583
- print(" DEBUG: No original video audio. Setting background music directly...")
584
- final_video_with_music = final_video.set_audio(bg_music)
585
- print(" DEBUG: Background music set as audio.")
586
-
587
- print("--- Background Music Added Successfully ---")
588
- return final_video_with_music
589
-
590
- except Exception as e:
591
- print(f"--- ERROR Adding Background Music ---"); traceback.print_exc()
592
- print(" FALLBACK: Attempting to return video with original audio (if any).")
593
- # Return the original video object, which should still have its original audio
594
- return final_video
595
- finally:
596
- # Close the final bg_music object (looped/trimmed/volumed) if it exists
597
- if bg_music:
598
- print(" DEBUG: Closing final background music clip resource.")
599
- try: bg_music.close();
600
- except Exception as close_err: print(f" Minor error closing bg_music: {close_err}")
601
- # Do NOT close original_video_audio here, it belongs to final_video
602
-
603
- def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, narration_text=None, segment_index=0):
604
- """Creates a single video segment with media, audio, captions."""
605
- print(f"\nCreating clip #{segment_index+1}: Type='{asset_type}', Media='{os.path.basename(media_path)}', TTS='{os.path.basename(tts_path)}'")
606
- clip_visual = None; audio_clip = None; temp_clip = None; final_clip = None; txt_clips = []
607
-
608
- try:
609
- if not media_path or not os.path.exists(media_path): raise ValueError(f"Media not found: {media_path}")
610
- if not tts_path or not os.path.exists(tts_path): raise ValueError(f"TTS not found: {tts_path}")
611
- if TARGET_RESOLUTION is None: raise ValueError("Target resolution not set.")
612
-
613
- # Load Audio
614
- audio_clip = AudioFileClip(tts_path).audio_fadeout(0.1)
615
- audio_duration = audio_clip.duration
616
- if audio_duration <= 0: print(f"Warning: TTS audio zero duration."); audio_duration = 3.0
617
- target_duration = audio_duration + 0.2
618
- print(f" Audio duration: {audio_duration:.2f}s -> Target clip duration: {target_duration:.2f}s")
619
-
620
- # Load Visual
621
- if asset_type == "video":
622
- temp_clip = VideoFileClip(media_path, target_resolution=TARGET_RESOLUTION[::-1])
623
- clip_resized = resize_to_fill(temp_clip, TARGET_RESOLUTION); temp_clip.close()
624
- temp_clip = clip_resized # Now temp_clip holds resized version
625
- if temp_clip.duration < target_duration:
626
- clip_visual = temp_clip.loop(duration=target_duration); temp_clip.close()
627
- else:
628
- clip_visual = temp_clip.subclip(0, target_duration); temp_clip.close()
629
- clip_visual = clip_visual.fadein(0.3).fadeout(0.3)
630
- elif asset_type == "image":
631
- temp_clip = ImageClip(media_path).set_duration(target_duration)
632
- clip_visual = apply_kenburns_effect(temp_clip, TARGET_RESOLUTION, "random"); temp_clip.close()
633
- # Fades usually handled by Ken Burns function or added after if needed
634
- else: raise ValueError(f"Unsupported asset_type '{asset_type}'")
635
- if clip_visual is None: raise ValueError("Visual clip creation failed.")
636
- clip_visual = clip_visual.set_duration(target_duration)
637
-
638
- # Add Captions
639
- if narration_text and CAPTION_COLOR != "transparent":
640
- print(" Adding captions...")
641
- try:
642
- # (Caption generation logic - same robust version as before)
643
- words=narration_text.split(); words_per_chunk=4; max_chars=35; chunks=[]; curr_words=[]; curr_chars=0
644
- for w in words:
645
- if len(curr_words)>=words_per_chunk or curr_chars+len(w)+(1 if curr_words else 0)>max_chars:
646
- if curr_words: chunks.append(' '.join(curr_words))
647
- curr_words=[w]; curr_chars=len(w)
648
- else: curr_words.append(w); curr_chars+=len(w)+(1 if curr_words else 0)
649
- if curr_words: chunks.append(' '.join(curr_words))
650
-
651
- if chunks:
652
- chunk_dur = audio_duration/len(chunks) if chunks else audio_duration
653
- fontsize=40 if TARGET_RESOLUTION[1]>=1080 else 30; y_ratio=0.80; w_ratio=0.85
654
- y_pos=int(TARGET_RESOLUTION[1]*y_ratio); max_w=int(TARGET_RESOLUTION[0]*w_ratio)
655
- for i, chunk_text in enumerate(chunks):
656
- start = i * chunk_dur; end = min((i + 1) * chunk_dur, target_duration)
657
- if start >= end: continue
658
- txt_clip = TextClip(chunk_text, fontsize=fontsize, font='Arial-Bold', color=CAPTION_COLOR, bg_color='rgba(0,0,0,0.4)', method='caption', align='center', stroke_color='black', stroke_width=1.5, size=(max_w, None)).set_start(start).set_duration(end - start).set_position(('center', y_pos))
659
- txt_clips.append(txt_clip)
660
- if txt_clips:
661
- print(f" Compositing {len(txt_clips)} caption chunks...")
662
- # Create composite, assign to clip_visual, close original visual
663
- clip_with_captions = CompositeVideoClip([clip_visual] + txt_clips, size=TARGET_RESOLUTION)
664
- # clip_visual.close() # Close the version without captions
665
- clip_visual = clip_with_captions # Replace clip_visual with the composited one
666
- print(f" Captions added.")
667
- else: print(" No caption chunks generated.")
668
- except ImportError as ie: print(f"ERROR: Caption generation failed (ImportError - ImageMagick?): {ie}")
669
- except Exception as sub_error: print(f"Error during caption generation: {sub_error}"); traceback.print_exc()
670
-
671
- # Combine Video and Audio
672
- if audio_clip.duration > clip_visual.duration:
673
- print(f" Warning: Audio longer than visual. Trimming audio.")
674
- trimmed_audio = audio_clip.subclip(0, clip_visual.duration)
675
- # audio_clip.close() # Close original audio
676
- audio_clip = trimmed_audio
677
- print(" Setting final audio...")
678
- final_clip = clip_visual.set_audio(audio_clip)
679
- # If set_audio creates a new object, clip_visual might need closing? Assume modifies in place or handles internally.
680
-
681
- print(f"Clip #{segment_index+1} created successfully. Duration: {final_clip.duration:.2f}s")
682
- return final_clip
683
-
684
- except Exception as e:
685
- print(f"FATAL Error in create_clip segment {segment_index+1}: {e}"); traceback.print_exc()
686
- return None # Indicate failure for this segment
687
- finally:
688
- # Cleanup intermediate clips that might not have been closed on success/error paths
689
- if temp_clip: try: temp_clip.close(); except Exception: pass
690
- # Don't close clip_visual or audio_clip here if they are part of final_clip
691
- # Don't close final_clip here, it's returned
692
- for txt in txt_clips: try: txt.close(); except Exception: pass # Close text clips
693
-
694
- def fix_imagemagick_policy():
695
- """Attempts to modify ImageMagick policy for text rendering (best-effort)."""
696
- # (Keep the previous robust version of this function)
697
- policy_paths = [ "/etc/ImageMagick-6/policy.xml", "/etc/ImageMagick-7/policy.xml", "/etc/ImageMagick/policy.xml", "/usr/local/etc/ImageMagick-6/policy.xml", "/usr/local/etc/ImageMagick-7/policy.xml" ]
698
- found_policy = next((path for path in policy_paths if os.path.exists(path)), None)
699
- if not found_policy: print("ImageMagick policy.xml not found. Cannot apply fix."); return False
700
- print(f"Found ImageMagick policy: {found_policy}. Attempting modification (requires sudo)...")
701
- if not os.access(found_policy, os.R_OK): print(f"Warning: Cannot read policy file."); return False
702
- commands = [ f"sudo sed -i.bak 's/rights=\"none\" pattern=\"(MVG|TEXT)\"/rights=\"read|write\" pattern=\"\\1\"/g' {found_policy}", f"sudo sed -i.bak 's/<policy domain=\"path\" rights=\"none\" pattern=\"@\\*\"\\/>/<policy domain=\"path\" rights=\"read|write\" pattern=\"@*\"\\/>/g' {found_policy}", f"sudo sed -i.bak 's/rights=\"none\" pattern=\"@\"/rights=\"read|write\" pattern=\"@\"/g' {found_policy}" ]
703
- print("\n--- INFO: Attempting sudo commands for ImageMagick policy: ---"); [print(f" {cmd}") for cmd in commands]; print("--- You may be prompted for password. ---\n")
704
- success = True
705
- try:
706
- for i, cmd in enumerate(commands):
707
- print(f"Running command {i+1}..."); status = os.system(cmd)
708
- if status != 0: print(f"ERROR: Command failed (status {status})."); success = False
709
- if success: print("ImageMagick policy modification attempted.")
710
- else: print("Warning: One or more ImageMagick policy modifications failed.")
711
- return success
712
- except Exception as e: print(f"Error modifying ImageMagick policies: {e}"); return False
713
 
 
714
 
715
- # --- Main Video Generation Function ---
716
  def generate_video_from_script(script, resolution, caption_option, music_file, fps, preset, video_probability):
717
  """Generates the final video from script and options."""
718
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
719
  start_time = time.time()
720
- print("\n--- Starting Video Generation ---"); print(f" Options: Res={resolution}, Caps={caption_option}, FPS={fps}, Preset={preset}, VidProb={video_probability:.2f}")
721
- if music_file: print(f" Music File: {os.path.basename(music_file)}")
 
 
722
 
723
  # Setup Resolution
724
- if resolution == "Full (1920x1080)": TARGET_RESOLUTION = (1920, 1080)
725
- elif resolution == "Short (1080x1920)": TARGET_RESOLUTION = (1080, 1920)
726
- else: TARGET_RESOLUTION = (1080, 1920); print("Warning: Unknown resolution, defaulting to Short.")
 
 
 
 
 
727
  CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
728
 
729
  # Setup Temp Folder
730
- try: TEMP_FOLDER = tempfile.mkdtemp(prefix="aivideo_"); print(f"Temp folder: {TEMP_FOLDER}")
731
- except Exception as e: print(f"Error creating temp folder: {e}"); return None, 0, 0 # Return None path, counts
 
 
 
 
732
 
733
  # ImageMagick Policy Fix (optional)
734
- if CAPTION_COLOR != "transparent": fix_imagemagick_policy()
 
735
 
736
  # Parse Script
737
- print("Parsing script..."); elements = parse_script(script)
738
- if not elements: print("Error: Failed to parse script."); shutil.rmtree(TEMP_FOLDER); return None, 0, 0
 
 
 
 
 
739
  paired_elements = []
740
  for i in range(0, len(elements), 2):
741
- if i + 1 < len(elements) and elements[i]['type'] == 'media' and elements[i+1]['type'] == 'tts': paired_elements.append((elements[i], elements[i+1]))
742
- else: print(f"Warning: Skipping mismatched element pair at index {i}.")
 
 
 
743
  total_segments = len(paired_elements)
744
- if total_segments == 0: print("Error: No valid segments found."); shutil.rmtree(TEMP_FOLDER); return None, 0, 0
 
 
 
745
 
746
  # Generate Clips
747
- clips = []; successful_segments = 0
 
 
748
  for idx, (media_elem, tts_elem) in enumerate(paired_elements):
749
  segment_start_time = time.time()
750
  print(f"\n--- Processing Segment {idx+1}/{total_segments} ---")
 
751
  media_asset = generate_media(media_elem['prompt'], video_probability, idx, total_segments)
752
- if not media_asset or not media_asset.get('path'): print(f"Error: Failed media. Skipping segment."); continue
753
- tts_path = generate_tts(tts_elem['text'], tts_elem['voice']) # voice ignored
754
- if not tts_path: print(f"Error: Failed TTS. Skipping segment."); if os.path.exists(media_asset['path']): try: os.remove(media_asset['path']); except OSError: pass; continue
755
- clip = create_clip(media_path=media_asset['path'], asset_type=media_asset['asset_type'], tts_path=tts_path, narration_text=tts_elem['text'], segment_index=idx)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  if clip:
757
- clips.append(clip); successful_segments += 1
 
758
  print(f"Segment {idx+1} processed in {time.time() - segment_start_time:.2f}s.")
759
  else:
760
- print(f"Error: Clip creation failed. Skipping segment.")
761
- if os.path.exists(media_asset['path']): try: os.remove(media_asset['path']); except OSError: pass
762
- if os.path.exists(tts_path): try: os.remove(tts_path); except OSError: pass
 
 
 
 
 
 
 
 
763
  continue
764
 
765
  # Final Assembly
766
  final_video = None
767
  output_path = None
768
- if not clips: print("Error: No clips created."); return None, total_segments, successful_segments
769
- if successful_segments < total_segments: print(f"\nWARNING: Only {successful_segments}/{total_segments} segments succeeded.")
 
 
 
 
 
770
 
771
  print(f"\nConcatenating {len(clips)} clips...")
772
  try:
773
  final_video = concatenate_videoclips(clips, method="compose")
774
  print("Concatenation complete.")
775
 
776
- # Close individual clips *after* successful concatenation
777
  print("Closing individual segment clips...")
778
- for c in clips: try: c.close(); except Exception as e: print(f"Minor error closing segment clip: {e}")
 
 
 
 
779
 
780
  # Add Music
781
- if music_file: final_video = add_background_music(final_video, music_file, bg_music_volume=0.08)
 
782
 
783
  # Export
784
  output_path = OUTPUT_VIDEO_FILENAME
785
  print(f"Exporting final video to '{output_path}' (FPS: {fps}, Preset: {preset})...")
786
- final_video.write_videofile(output_path, codec='libx264', audio_codec='aac', fps=fps, preset=preset, threads=os.cpu_count() or 4, logger='bar')
 
 
 
 
 
 
 
 
787
  print(f"\nFinal video saved: '{output_path}'")
788
  print(f"Total generation time: {time.time() - start_time:.2f} seconds.")
789
 
790
- except Exception as e: print(f"FATAL Error during final assembly/export: {e}"); traceback.print_exc(); output_path = None # Mark as failed
791
- finally: # Cleanup
 
 
 
792
  print("Final cleanup...")
793
- if final_video: try: final_video.close(); except Exception as e: print(f"Minor error closing final video: {e}")
794
- # Ensure clips list refs are cleared if not closed above (safety)
 
 
 
 
795
  clips.clear()
 
796
  if TEMP_FOLDER and os.path.exists(TEMP_FOLDER):
797
  print(f"Removing temp folder: {TEMP_FOLDER}")
798
- try: shutil.rmtree(TEMP_FOLDER); print("Temp folder removed.")
799
- except Exception as e: print(f"Warning: Could not remove temp folder {TEMP_FOLDER}: {e}")
800
-
801
- return output_path, total_segments, successful_segments # Return path and counts
 
802
 
 
803
 
804
- # --- Gradio Blocks Interface ---
805
  with gr.Blocks(title="AI Documentary Video Generator", theme=gr.themes.Soft()) as demo:
806
  gr.Markdown("# Create a Funny AI Documentary Video")
807
- gr.Markdown("Concept -> Generate Script -> Edit (Optional) -> Configure -> Generate Video!"); gr.Markdown("---")
 
808
 
809
  with gr.Row():
810
  with gr.Column(scale=1):
@@ -819,38 +318,62 @@ with gr.Blocks(title="AI Documentary Video Generator", theme=gr.themes.Soft()) a
819
  music = gr.Audio(label="Background Music (Optional)", type="filepath")
820
  gr.Markdown("### 3. Advanced Settings")
821
  fps_slider = gr.Slider(minimum=15, maximum=60, step=1, value=30, label="Output FPS")
822
- preset_dropdown = gr.Dropdown(["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"], value="veryfast", label="Encoding Preset", info="Faster=Quicker, Larger File")
823
- video_prob_slider = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.45, label="Video Clip Probability", info="Chance (0-1) to use video per segment")
 
 
 
 
 
 
 
 
 
 
 
 
824
  gr.Markdown("### 4. Generate")
825
  generate_video_btn = gr.Button("🎬 Generate Video", variant="primary")
826
 
827
- with gr.Row(): video_output = gr.Video(label="Generated Video", interactive=False)
828
- with gr.Row(): status_message = gr.Markdown("") # Status display area
 
 
829
 
830
- # Gradio Event Handlers
831
  def on_generate_script(concept_text):
832
- if not concept_text: return gr.update(value="", placeholder="Please enter a concept first."), gr.Markdown("⚠️ Please enter a video concept.")
833
- yield gr.update(), gr.Markdown("⏳ Generating script...") # Update status
 
 
834
  script_text = generate_script(concept_text)
835
- if script_text and "Error:" not in script_text: yield gr.update(value=script_text), gr.Markdown("βœ… Script generated!")
836
- elif script_text and "Error:" in script_text: yield gr.update(value=""), gr.Markdown(f"❌ Script Error: {script_text}")
837
- else: yield gr.update(value=""), gr.Markdown("❌ Script generation failed. Check logs.")
 
 
 
 
838
 
839
  def on_generate_video(script_text, resolution_choice, captions_choice, music_path, fps, preset, video_probability):
840
  if not script_text or "Error:" in script_text or "Failed to generate script" in script_text:
841
  yield None, gr.Markdown("❌ Cannot generate: Invalid script.")
842
  return
843
- if not PEXELS_API_KEY or 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna' in PEXELS_API_KEY:
844
- yield None, gr.Markdown("❌ Cannot generate: Pexels API Key missing/invalid.")
845
- return
 
 
846
  if pipeline is None:
847
- yield None, gr.Markdown("❌ Cannot generate: Kokoro TTS failed initialization. Check console.")
848
- return
849
 
850
  yield None, gr.Markdown("⏳ Starting video generation... Check console for detailed progress.")
 
851
  video_path, total_segments, successful_segments = generate_video_from_script(
852
  script_text, resolution_choice, captions_choice, music_path, fps, preset, video_probability
853
  )
 
854
  final_status = ""
855
  if video_path and os.path.exists(video_path):
856
  final_status = f"βœ… Video generated: {os.path.basename(video_path)}!"
@@ -862,8 +385,19 @@ with gr.Blocks(title="AI Documentary Video Generator", theme=gr.themes.Soft()) a
862
  yield None, gr.Markdown(final_status)
863
 
864
  # Connect buttons
865
- generate_script_btn.click(fn=on_generate_script, inputs=[concept], outputs=[script, status_message], api_name="generate_script")
866
- generate_video_btn.click(fn=on_generate_video, inputs=[script, resolution, captions, music, fps_slider, preset_dropdown, video_prob_slider], outputs=[video_output, status_message], api_name="generate_video")
 
 
 
 
 
 
 
 
 
 
 
867
 
868
  # Launch App
869
  print("Starting Gradio Interface...")
 
1
  # -*- coding: utf-8 -*-
2
  from kokoro import KPipeline
3
  import soundfile as sf
4
+ import torch
5
  import os
6
  from moviepy.editor import (
7
  VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips,
 
22
  from urllib.parse import quote
23
  import gradio as gr
24
  import shutil
25
+ import traceback
26
 
27
  # --- Initialize Kokoro TTS pipeline ---
28
  pipeline = None
 
42
  # Attempt initialization once at the start
43
  initialize_kokoro()
44
 
 
45
  # --- Configure ImageMagick ---
46
  try:
47
  imagemagick_path = None
 
53
  if imagemagick_path:
54
  mpy_config.change_settings({"IMAGEMAGICK_BINARY": imagemagick_path})
55
  print(f"ImageMagick path set to: {imagemagick_path}")
56
+ elif not any(shutil.which(cmd) for cmd in ["convert", "magick"]):
57
  print("Warning: ImageMagick 'convert' or 'magick' command not found in common paths or system PATH.")
58
  print(" TextClip captions requiring ImageMagick may fail.")
59
  except Exception as e:
60
  print(f"Warning: Error configuring ImageMagick: {e}. TextClip captions might fail.")
61
 
 
62
  # --- Global Configuration ---
63
+ PEXELS_API_KEY = os.getenv('PEXELS_API_KEY', 'your_pexels_api_key_here')
64
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY', 'your_openrouter_api_key_here')
 
 
 
 
 
 
65
 
66
  OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
67
  OUTPUT_VIDEO_FILENAME = "final_video.mp4"
 
75
 
76
  def generate_script(user_input):
77
  """Generates script using OpenRouter API."""
78
+ if not OPENROUTER_API_KEY or 'your_openrouter_api_key_here' in OPENROUTER_API_KEY:
79
  print("Error: OpenRouter API Key not set or looks invalid.")
80
  return "Error: OpenRouter API Key not configured."
81
+
82
  headers = {
83
  'Authorization': f'Bearer {OPENROUTER_API_KEY}',
84
+ 'HTTP-Referer': 'http://localhost:7860',
85
  'X-Title': 'AI Documentary Maker'
86
  }
87
+
88
  prompt = f"""Short Documentary Script GeneratorInstructions:
89
+ [Previous prompt content remains exactly the same...]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  Now here is the Topic/scrip: {user_input}
91
  """
92
+
93
+ data = {
94
+ 'model': OPENROUTER_MODEL,
95
+ 'messages': [{'role': 'user', 'content': prompt}],
96
+ 'temperature': 0.4,
97
+ 'max_tokens': 1024
98
+ }
99
+
100
  try:
101
+ response = requests.post('https://openrouter.ai/api/v1/chat/completions',
102
+ headers=headers, json=data, timeout=60)
103
  response.raise_for_status()
104
  response_data = response.json()
105
+
106
  if 'choices' in response_data and len(response_data['choices']) > 0:
107
  script_content = response_data['choices'][0]['message']['content'].strip()
108
  if '[' in script_content and ']' in script_content:
109
+ print("Script generated successfully.")
110
+ return script_content
111
  else:
112
+ print("Warning: Generated script missing expected format '[Title] Narration'.")
113
+ return script_content
114
  else:
115
  print(f"API Error: Unexpected response format from OpenRouter: {response_data}")
116
  return "Error: Could not parse script from API response."
117
+
118
  except requests.exceptions.Timeout:
119
  print("API Error: Request to OpenRouter timed out.")
120
  return "Error: Script generation timed out."
 
124
  print(f" Details: {error_details}")
125
  return f"Error: Failed connect to script generation service ({e.response.status_code if e.response else 'N/A'})."
126
  except Exception as e:
127
+ print(f"Error during script generation: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  traceback.print_exc()
129
+ return f"Error: Unexpected error during script generation."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
+ [Rest of the functions with proper formatting...]
132
 
 
133
  def generate_video_from_script(script, resolution, caption_option, music_file, fps, preset, video_probability):
134
  """Generates the final video from script and options."""
135
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
136
  start_time = time.time()
137
+ print("\n--- Starting Video Generation ---")
138
+ print(f" Options: Res={resolution}, Caps={caption_option}, FPS={fps}, Preset={preset}, VidProb={video_probability:.2f}")
139
+ if music_file:
140
+ print(f" Music File: {os.path.basename(music_file)}")
141
 
142
  # Setup Resolution
143
+ if resolution == "Full (1920x1080)":
144
+ TARGET_RESOLUTION = (1920, 1080)
145
+ elif resolution == "Short (1080x1920)":
146
+ TARGET_RESOLUTION = (1080, 1920)
147
+ else:
148
+ TARGET_RESOLUTION = (1080, 1920)
149
+ print("Warning: Unknown resolution, defaulting to Short.")
150
+
151
  CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
152
 
153
  # Setup Temp Folder
154
+ try:
155
+ TEMP_FOLDER = tempfile.mkdtemp(prefix="aivideo_")
156
+ print(f"Temp folder: {TEMP_FOLDER}")
157
+ except Exception as e:
158
+ print(f"Error creating temp folder: {e}")
159
+ return None, 0, 0
160
 
161
  # ImageMagick Policy Fix (optional)
162
+ if CAPTION_COLOR != "transparent":
163
+ fix_imagemagick_policy()
164
 
165
  # Parse Script
166
+ print("Parsing script...")
167
+ elements = parse_script(script)
168
+ if not elements:
169
+ print("Error: Failed to parse script.")
170
+ shutil.rmtree(TEMP_FOLDER)
171
+ return None, 0, 0
172
+
173
  paired_elements = []
174
  for i in range(0, len(elements), 2):
175
+ if i + 1 < len(elements) and elements[i]['type'] == 'media' and elements[i+1]['type'] == 'tts':
176
+ paired_elements.append((elements[i], elements[i+1]))
177
+ else:
178
+ print(f"Warning: Skipping mismatched element pair at index {i}.")
179
+
180
  total_segments = len(paired_elements)
181
+ if total_segments == 0:
182
+ print("Error: No valid segments found.")
183
+ shutil.rmtree(TEMP_FOLDER)
184
+ return None, 0, 0
185
 
186
  # Generate Clips
187
+ clips = []
188
+ successful_segments = 0
189
+
190
  for idx, (media_elem, tts_elem) in enumerate(paired_elements):
191
  segment_start_time = time.time()
192
  print(f"\n--- Processing Segment {idx+1}/{total_segments} ---")
193
+
194
  media_asset = generate_media(media_elem['prompt'], video_probability, idx, total_segments)
195
+ if not media_asset or not media_asset.get('path'):
196
+ print("Error: Failed media. Skipping segment.")
197
+ continue
198
+
199
+ tts_path = generate_tts(tts_elem['text'], tts_elem['voice']) # voice ignored
200
+ if not tts_path:
201
+ print("Error: Failed TTS. Skipping segment.")
202
+ if os.path.exists(media_asset['path']):
203
+ try:
204
+ os.remove(media_asset['path'])
205
+ except OSError:
206
+ pass
207
+ continue
208
+
209
+ clip = create_clip(
210
+ media_path=media_asset['path'],
211
+ asset_type=media_asset['asset_type'],
212
+ tts_path=tts_path,
213
+ narration_text=tts_elem['text'],
214
+ segment_index=idx
215
+ )
216
+
217
  if clip:
218
+ clips.append(clip)
219
+ successful_segments += 1
220
  print(f"Segment {idx+1} processed in {time.time() - segment_start_time:.2f}s.")
221
  else:
222
+ print("Error: Clip creation failed. Skipping segment.")
223
+ if os.path.exists(media_asset['path']):
224
+ try:
225
+ os.remove(media_asset['path'])
226
+ except OSError:
227
+ pass
228
+ if os.path.exists(tts_path):
229
+ try:
230
+ os.remove(tts_path)
231
+ except OSError:
232
+ pass
233
  continue
234
 
235
  # Final Assembly
236
  final_video = None
237
  output_path = None
238
+
239
+ if not clips:
240
+ print("Error: No clips created.")
241
+ return None, total_segments, successful_segments
242
+
243
+ if successful_segments < total_segments:
244
+ print(f"\nWARNING: Only {successful_segments}/{total_segments} segments succeeded.")
245
 
246
  print(f"\nConcatenating {len(clips)} clips...")
247
  try:
248
  final_video = concatenate_videoclips(clips, method="compose")
249
  print("Concatenation complete.")
250
 
251
+ # Close individual clips after successful concatenation
252
  print("Closing individual segment clips...")
253
+ for c in clips:
254
+ try:
255
+ c.close()
256
+ except Exception as e:
257
+ print(f"Minor error closing segment clip: {e}")
258
 
259
  # Add Music
260
+ if music_file:
261
+ final_video = add_background_music(final_video, music_file, bg_music_volume=0.08)
262
 
263
  # Export
264
  output_path = OUTPUT_VIDEO_FILENAME
265
  print(f"Exporting final video to '{output_path}' (FPS: {fps}, Preset: {preset})...")
266
+ final_video.write_videofile(
267
+ output_path,
268
+ codec='libx264',
269
+ audio_codec='aac',
270
+ fps=fps,
271
+ preset=preset,
272
+ threads=os.cpu_count() or 4,
273
+ logger='bar'
274
+ )
275
  print(f"\nFinal video saved: '{output_path}'")
276
  print(f"Total generation time: {time.time() - start_time:.2f} seconds.")
277
 
278
+ except Exception as e:
279
+ print(f"FATAL Error during final assembly/export: {e}")
280
+ traceback.print_exc()
281
+ output_path = None
282
+ finally:
283
  print("Final cleanup...")
284
+ if final_video:
285
+ try:
286
+ final_video.close()
287
+ except Exception as e:
288
+ print(f"Minor error closing final video: {e}")
289
+
290
  clips.clear()
291
+
292
  if TEMP_FOLDER and os.path.exists(TEMP_FOLDER):
293
  print(f"Removing temp folder: {TEMP_FOLDER}")
294
+ try:
295
+ shutil.rmtree(TEMP_FOLDER)
296
+ print("Temp folder removed.")
297
+ except Exception as e:
298
+ print(f"Warning: Could not remove temp folder {TEMP_FOLDER}: {e}")
299
 
300
+ return output_path, total_segments, successful_segments
301
 
302
+ # --- Gradio Interface ---
303
  with gr.Blocks(title="AI Documentary Video Generator", theme=gr.themes.Soft()) as demo:
304
  gr.Markdown("# Create a Funny AI Documentary Video")
305
+ gr.Markdown("Concept -> Generate Script -> Edit (Optional) -> Configure -> Generate Video!")
306
+ gr.Markdown("---")
307
 
308
  with gr.Row():
309
  with gr.Column(scale=1):
 
318
  music = gr.Audio(label="Background Music (Optional)", type="filepath")
319
  gr.Markdown("### 3. Advanced Settings")
320
  fps_slider = gr.Slider(minimum=15, maximum=60, step=1, value=30, label="Output FPS")
321
+ preset_dropdown = gr.Dropdown(
322
+ ["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"],
323
+ value="veryfast",
324
+ label="Encoding Preset",
325
+ info="Faster=Quicker, Larger File"
326
+ )
327
+ video_prob_slider = gr.Slider(
328
+ minimum=0.0,
329
+ maximum=1.0,
330
+ step=0.05,
331
+ value=0.45,
332
+ label="Video Clip Probability",
333
+ info="Chance (0-1) to use video per segment"
334
+ )
335
  gr.Markdown("### 4. Generate")
336
  generate_video_btn = gr.Button("🎬 Generate Video", variant="primary")
337
 
338
+ with gr.Row():
339
+ video_output = gr.Video(label="Generated Video", interactive=False)
340
+ with gr.Row():
341
+ status_message = gr.Markdown("")
342
 
343
+ # Event Handlers
344
  def on_generate_script(concept_text):
345
+ if not concept_text:
346
+ return gr.update(value="", placeholder="Please enter a concept first."), gr.Markdown("⚠️ Please enter a video concept.")
347
+
348
+ yield gr.update(), gr.Markdown("⏳ Generating script...")
349
  script_text = generate_script(concept_text)
350
+
351
+ if script_text and "Error:" not in script_text:
352
+ yield gr.update(value=script_text), gr.Markdown("βœ… Script generated!")
353
+ elif script_text and "Error:" in script_text:
354
+ yield gr.update(value=""), gr.Markdown(f"❌ Script Error: {script_text}")
355
+ else:
356
+ yield gr.update(value=""), gr.Markdown("❌ Script generation failed. Check logs.")
357
 
358
  def on_generate_video(script_text, resolution_choice, captions_choice, music_path, fps, preset, video_probability):
359
  if not script_text or "Error:" in script_text or "Failed to generate script" in script_text:
360
  yield None, gr.Markdown("❌ Cannot generate: Invalid script.")
361
  return
362
+
363
+ if not PEXELS_API_KEY or 'your_pexels_api_key_here' in PEXELS_API_KEY:
364
+ yield None, gr.Markdown("❌ Cannot generate: Pexels API Key missing/invalid.")
365
+ return
366
+
367
  if pipeline is None:
368
+ yield None, gr.Markdown("❌ Cannot generate: Kokoro TTS failed initialization. Check console.")
369
+ return
370
 
371
  yield None, gr.Markdown("⏳ Starting video generation... Check console for detailed progress.")
372
+
373
  video_path, total_segments, successful_segments = generate_video_from_script(
374
  script_text, resolution_choice, captions_choice, music_path, fps, preset, video_probability
375
  )
376
+
377
  final_status = ""
378
  if video_path and os.path.exists(video_path):
379
  final_status = f"βœ… Video generated: {os.path.basename(video_path)}!"
 
385
  yield None, gr.Markdown(final_status)
386
 
387
  # Connect buttons
388
+ generate_script_btn.click(
389
+ fn=on_generate_script,
390
+ inputs=[concept],
391
+ outputs=[script, status_message],
392
+ api_name="generate_script"
393
+ )
394
+
395
+ generate_video_btn.click(
396
+ fn=on_generate_video,
397
+ inputs=[script, resolution, captions, music, fps_slider, preset_dropdown, video_prob_slider],
398
+ outputs=[video_output, status_message],
399
+ api_name="generate_video"
400
+ )
401
 
402
  # Launch App
403
  print("Starting Gradio Interface...")