OrbitMC commited on
Commit
a4c0061
·
verified ·
1 Parent(s): 06e6d8b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -332
app.py CHANGED
@@ -36,11 +36,9 @@ except Exception as e:
36
 
37
  # ---------------- Global Configuration ---------------- #
38
 
39
- # Fetch environment API credentials safely
40
  PEXELS_API_KEY = os.environ.get('PEXELS_API_KEY', '')
41
  GROQ_API_KEY = os.environ.get('GROQ_API_KEY', '')
42
 
43
- # Local operational defaults
44
  if not PEXELS_API_KEY:
45
  PEXELS_API_KEY = 'YOUR_PEXELS_KEY_HERE'
46
  if not GROQ_API_KEY:
@@ -63,66 +61,59 @@ CAPTION_COLOR = "white"
63
  TEMP_FOLDER = None
64
 
65
  VOICE_CHOICES = {
66
- 'Emma (Female)': 'af_heart',
67
- 'Bella (Female)': 'af_bella',
68
- 'Nicole (Female)': 'af_nicole',
69
- 'Aoede (Female)': 'af_aoede',
70
- 'Kore (Female)': 'af_kore',
71
- 'Sarah (Female)': 'af_sarah',
72
- 'Nova (Female)': 'af_nova',
73
- 'Sky (Female)': 'af_sky',
74
- 'Alloy (Female)': 'af_alloy',
75
- 'Jessica (Female)': 'af_jessica',
76
- 'River (Female)': 'af_river',
77
- 'Michael (Male)': 'am_michael',
78
- 'Fenrir (Male)': 'am_fenrir',
79
- 'Puck (Male)': 'am_puck',
80
- 'Echo (Male)': 'am_echo',
81
- 'Eric (Male)': 'am_eric',
82
- 'Liam (Male)': 'am_liam',
83
- 'Onyx (Male)': 'am_onyx',
84
- 'Santa (Male)': 'am_santa',
85
- 'Adam (Male)': 'am_adam',
86
- 'Emma 🇬🇧 (Female)': 'bf_emma',
87
- 'Isabella 🇬🇧 (Female)': 'bf_isabella',
88
- 'Alice 🇬🇧 (Female)': 'bf_alice',
89
- 'Lily 🇬🇧 (Female)': 'bf_lily',
90
- 'George 🇬🇧 (Male)': 'bm_george',
91
- 'Fable 🇬🇧 (Male)': 'bm_fable',
92
- 'Lewis 🇬🇧 (Male)': 'bm_lewis',
93
  'Daniel 🇬🇧 (Male)': 'bm_daniel'
94
  }
95
 
96
  # ---------------- Core Support Functions ---------------- #
97
 
98
  def check_api_keys():
99
- """Verify system infrastructure credentials are populated."""
100
  if not PEXELS_API_KEY or PEXELS_API_KEY == 'YOUR_PEXELS_KEY_HERE':
101
  return False, "PEXELS_API_KEY is not configured in environment variables."
102
  if not GROQ_API_KEY or GROQ_API_KEY == 'YOUR_GROQ_KEY_HERE':
103
  return False, "GROQ_API_KEY is not configured in environment variables."
104
  return True, "API keys configured successfully."
105
 
106
- def generate_script(user_input):
107
- """Generate high-quality, humanized documentary scripts using the Groq API endpoint."""
108
  headers = {
109
  'Authorization': f'Bearer {GROQ_API_KEY}',
110
  'Content-Type': 'application/json'
111
  }
112
 
113
- prompt = f"""You are a witty, charismatic, and authentic human documentary narrator. Your task is to write an engaging video script based on the topic provided.
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
  CRITICAL INSTRUCTIONS FOR NATURAL HUMAN TONE:
116
- - Avoid ALL standard robotic AI tropes, filler terms, and clichés.
117
  - Strictly DO NOT use words like: "delve", "tapestry", "testament", "furthermore", "moreover", "in conclusion", "look no further", "nestled", "beacon", or "revolutionize".
118
- - Write with organic variety in sentence structure. Mix short, punchy statements with casual commentary.
119
- - The humor should feel effortless, dry, and conversational—like a real person sharing funny, unexpected facts.
120
 
121
  Format Requirements:
122
  - Break the script into distinct scenes using structural brackets: [Tag].
123
- - The Tag must be a simple 1-2 word search query suitable for finding background stock video/images (e.g., [Penguin], [City Traffic]).
124
- - Directly beneath each tag, write exactly one engaging sentence (maximum 15 words) continuing the narrative.
125
- - Conclude the piece with a [Subscribe] tag containing a clever, unexpected parting remark.
126
 
127
  Topic: {user_input}
128
  """
@@ -130,7 +121,7 @@ Topic: {user_input}
130
  data = {
131
  'model': GROQ_MODEL,
132
  'messages': [{'role': 'user', 'content': prompt}],
133
- 'temperature': 0.65,
134
  'max_tokens': 1500
135
  }
136
 
@@ -141,23 +132,18 @@ Topic: {user_input}
141
  json=data,
142
  timeout=25
143
  )
144
-
145
  if response.status_code == 200:
146
- response_data = response.json()
147
- return response_data['choices'][0]['message']['content'].strip()
148
  else:
149
- print(f"Groq API Execution Error {response.status_code}: {response.text}")
150
  return None
151
  except Exception as e:
152
  print(f"Network processing exception during script generation: {str(e)}")
153
  return None
154
 
155
  def parse_script(script_text):
156
- """Robust regex parser extracting asset cues and voiceover text blocks."""
157
  if not script_text:
158
  return []
159
-
160
- # Matches tags in brackets and extracts all text leading up to the next bracket setup
161
  pattern = r'\[(.*?)\]\s*([^\[]+)'
162
  matches = re.findall(pattern, script_text)
163
 
@@ -165,14 +151,10 @@ def parse_script(script_text):
165
  for tag, narration in matches:
166
  clean_tag = tag.strip()
167
  clean_narration = re.sub(r'\s+', ' ', narration.strip())
168
-
169
  if not clean_tag or not clean_narration:
170
  continue
171
 
172
- # Register video/visual search directive block
173
  elements.append({"type": "media", "prompt": clean_tag})
174
-
175
- # Determine temporal timing properties safely
176
  words = clean_narration.split()
177
  calculated_duration = max(3.5, len(words) * 0.45)
178
  elements.append({
@@ -180,83 +162,67 @@ def parse_script(script_text):
180
  "text": clean_narration,
181
  "duration": calculated_duration
182
  })
183
-
184
  return elements
185
 
186
  def search_pexels_videos(query, pexels_api_key):
187
- """Query Pexels video directories for optimized landscape video content assets."""
188
  headers = {'Authorization': pexels_api_key}
189
  url = "https://api.pexels.com/videos/search"
190
  params = {"query": query, "per_page": 8, "orientation": "landscape"}
191
-
192
  try:
193
  response = requests.get(url, headers=headers, params=params, timeout=12)
194
  if response.status_code == 200:
195
- data = response.json()
196
- videos = data.get("videos", [])
197
  if videos:
198
  selected_video = random.choice(videos)
199
  video_files = selected_video.get("video_files", [])
200
- # Prioritize HD quality assets within typical delivery parameters
201
  for file in video_files:
202
  if file.get("quality") == "hd" and file.get("width", 0) >= 1280:
203
  return file.get("link")
204
  if video_files:
205
  return video_files[0].get("link")
206
  except Exception as e:
207
- print(f"Pexels video fetch sequence exception: {e}")
208
  return None
209
 
210
  def search_pexels_images(query, pexels_api_key):
211
- """Query Pexels asset directories for matching photographic components."""
212
  headers = {'Authorization': pexels_api_key}
213
  url = "https://api.pexels.com/v1/search"
214
  params = {"query": query, "per_page": 8, "orientation": "landscape"}
215
-
216
  try:
217
  response = requests.get(url, headers=headers, params=params, timeout=12)
218
  if response.status_code == 200:
219
- data = response.json()
220
- photos = data.get("photos", [])
221
  if photos:
222
  return random.choice(photos).get("src", {}).get("large2x")
223
  except Exception as e:
224
- print(f"Pexels image search exception lookup trace: {e}")
225
  return None
226
 
227
  def download_asset_file(url, local_path):
228
- """Safely streams network binary assets into local storage blocks with validation context."""
229
  try:
230
  headers = {"User-Agent": USER_AGENT}
231
  with requests.get(url, headers=headers, stream=True, timeout=20) as r:
232
  r.raise_for_status()
233
  with open(local_path, 'wb') as f:
234
  for chunk in r.iter_content(chunk_size=16384):
235
- if chunk:
236
- f.write(chunk)
237
  return local_path
238
  except Exception as e:
239
- print(f"Error handling network asset download pipeline: {e}")
240
- if os.path.exists(local_path):
241
- os.remove(local_path)
242
  return None
243
 
244
  def generate_solid_fallback(prompt, target_res):
245
- """Produces custom atmospheric placeholder graphics to prevent render failure workflows."""
246
  w, h = target_res
247
  random.seed(prompt)
248
  base_color = (random.randint(15, 35), random.randint(20, 40), random.randint(30, 55))
249
-
250
  img = Image.new('RGB', (w, h), base_color)
251
  fallback_path = os.path.join(TEMP_FOLDER, f"fallback_{int(time.time())}_{random.randint(0,99)}.jpg")
252
  img.save(fallback_path, quality=90)
253
  return fallback_path
254
 
255
  def generate_media_asset(prompt):
256
- """Coordinates search matrices to fetch optimized video or graphic components."""
257
  safe_name = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
258
-
259
- # Attempt high quality stock video stream compilation paths
260
  if random.random() < video_clip_probability:
261
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
262
  if video_url:
@@ -264,82 +230,55 @@ def generate_media_asset(prompt):
264
  if download_asset_file(video_url, local_video_path):
265
  return {"path": local_video_path, "type": "video"}
266
 
267
- # Image processing search fallback tracks
268
  img_url = search_pexels_images(prompt, PEXELS_API_KEY)
269
  if img_url:
270
  local_img_path = os.path.join(TEMP_FOLDER, f"img_{safe_name}_{int(time.time())}.jpg")
271
  if download_asset_file(img_url, local_img_path):
272
  return {"path": local_img_path, "type": "image"}
273
 
274
- # Absolute safe fallback execution block
275
- fallback_image = generate_solid_fallback(prompt, TARGET_RESOLUTION)
276
- return {"path": fallback_image, "type": "image"}
277
 
278
  def generate_tts_audio(text):
279
- """Converts script blocks into high quality WAV audio streams via Kokoro or gTTS fallbacks."""
280
  safe_name = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
281
  output_audio_path = os.path.join(TEMP_FOLDER, f"tts_{safe_name}_{int(time.time())}.wav")
282
-
283
  try:
284
- # Pipeline generation handling through Kokoro system models
285
  generator = pipeline(text, voice=selected_voice, speed=voice_speed, split_pattern=r'\n+')
286
  audio_blocks = [audio for _, _, audio in generator]
287
-
288
  if audio_blocks:
289
  merged_audio = np.concatenate(audio_blocks) if len(audio_blocks) > 1 else audio_blocks[0]
290
  sf.write(output_audio_path, merged_audio, 24000)
291
  return output_audio_path
292
  except Exception as e:
293
- print(f"Kokoro engine synthesis bypass. Invoking alternative gTTS framework: {e}")
294
 
295
  try:
296
- # Alternative global synthesis path engines
297
  tts = gTTS(text=text, lang='en', slow=False)
298
  temp_mp3 = os.path.join(TEMP_FOLDER, f"gtts_{int(time.time())}.mp3")
299
  tts.save(temp_mp3)
300
-
301
- normalized_sound = AudioSegment.from_mp3(temp_mp3)
302
- normalized_sound.export(output_audio_path, format="wav")
303
-
304
- if os.path.exists(temp_mp3):
305
- os.remove(temp_mp3)
306
  return output_audio_path
307
  except Exception as fail_err:
308
- print(f"Critical error: Synthesis framework down. Creating safe padding elements: {fail_err}")
309
-
310
- # Generate clean programmatic silence block to prevent pipeline execution crash
311
  duration_sec = max(3, len(text.split()) * 0.5)
312
- silent_samples = int(duration_sec * 24000)
313
- sf.write(output_audio_path, np.zeros(silent_samples, dtype=np.float32), 24000)
314
  return output_audio_path
315
 
316
  def build_wrapped_subtitle_layer(text, canvas_resolution, font_size_target):
317
- """Calculates adaptive dynamic line wrapping parameters to print polished captions onto video matrices."""
318
  width, height = canvas_resolution
319
  max_text_boundary_width = int(width * 0.85)
320
-
321
- # Establish dynamic font structures safely across Linux/Windows hosting architectures
322
  font_engine = None
323
- font_options = [
324
- "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
325
- "C:\\Windows\\Fonts\\arialbd.ttf",
326
- "/usr/share/fonts/Arial.ttf"
327
- ]
328
  for path in font_options:
329
  if os.path.exists(path):
330
  try:
331
  font_engine = ImageFont.truetype(path, font_size_target)
332
  break
333
- except:
334
- pass
335
- if font_engine is None:
336
- font_engine = ImageFont.load_default()
337
 
338
- # Form text line arrays matching specific horizontal screen boundaries
339
  words = text.split()
340
- compiled_lines = []
341
- current_line_build = []
342
-
343
  measurement_canvas = Image.new('RGBA', (1, 1))
344
  draw_inspector = ImageDraw.Draw(measurement_canvas)
345
 
@@ -348,22 +287,17 @@ def build_wrapped_subtitle_layer(text, canvas_resolution, font_size_target):
348
  try:
349
  box = draw_inspector.textbbox((0, 0), test_string, font=font_engine)
350
  calculated_w = box[2] - box[0]
351
- except:
352
- calculated_w = len(test_string) * (font_size_target * 0.55)
353
 
354
  if calculated_w <= max_text_boundary_width:
355
  current_line_build.append(word)
356
  else:
357
- if current_line_build:
358
- compiled_lines.append(" ".join(current_line_build))
359
  current_line_build = [word]
360
- if current_line_build:
361
- compiled_lines.append(" ".join(current_line_build))
362
 
363
- # Construct the graphic layout layer
364
  line_stride = font_size_target + 12
365
  computed_box_height = (len(compiled_lines) * line_stride) + 40
366
-
367
  subtitle_strip_canvas = Image.new('RGBA', (width, computed_box_height), (0, 0, 0, 0))
368
  context_drawer = ImageDraw.Draw(subtitle_strip_canvas)
369
 
@@ -371,18 +305,12 @@ def build_wrapped_subtitle_layer(text, canvas_resolution, font_size_target):
371
  try:
372
  box = context_drawer.textbbox((0, 0), line, font=font_engine)
373
  w = box[2] - box[0]
374
- except:
375
- w = len(line) * (font_size_target * 0.55)
376
 
377
  target_x = (width - w) // 2
378
  target_y = 20 + (idx * line_stride)
379
-
380
- # High-contrast text layout shadow processing loops
381
- shadow_offsets = [(-2, -2), (-2, 2), (2, -2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)]
382
- for dx, dy in shadow_offsets:
383
  context_drawer.text((target_x + dx, target_y + dy), line, font=font_engine, fill=(0, 0, 0, 225))
384
-
385
- # Draw text top presentation layer
386
  context_drawer.text((target_x, target_y), line, font=font_engine, fill=(255, 255, 255, 255))
387
 
388
  local_subtitle_png_path = os.path.join(TEMP_FOLDER, f"sub_{int(time.time())}_{random.randint(100,999)}.png")
@@ -390,137 +318,89 @@ def build_wrapped_subtitle_layer(text, canvas_resolution, font_size_target):
390
  return local_subtitle_png_path
391
 
392
  def resize_and_crop_to_fill(clip, target_resolution):
393
- """Crops background tracks seamlessly to fit target composition frames without aspect stretching distortion."""
394
  tw, th = target_resolution
395
  clip_w, clip_h = clip.w, clip.h
396
-
397
  aspect_target = tw / th
398
  aspect_clip = clip_w / clip_h
399
 
400
  if aspect_clip > aspect_target:
401
  scale_factor = th / clip_h
402
- scaled_w = int(clip_w * scale_factor)
403
- resized_clip = clip.resize(newsize=(scaled_w, th))
404
- crop_start_x = (scaled_w - tw) / 2
405
- return resized_clip.crop(x1=crop_start_x, x2=crop_start_x + tw, y1=0, y2=th)
406
  else:
407
  scale_factor = tw / clip_w
408
- scaled_h = int(clip_h * scale_factor)
409
- resized_clip = clip.resize(newsize=(tw, scaled_h))
410
- crop_start_y = (scaled_h - th) / 2
411
- return resized_clip.crop(x1=0, x2=tw, y1=crop_start_y, y2=crop_start_y + th)
412
 
413
  def apply_cinematic_motion_effect(clip, target_resolution):
414
- """Injects steady cinematic animation tracking routines over static photo frames."""
415
  tw, th = target_resolution
416
-
417
- # Scale canvas boundaries slightly to establish buffer tracks for pan scanning operations
418
  base_clip = clip.resize(newsize=(int(tw * 1.2), int(th * 1.2)))
419
  max_delta_x = base_clip.w - tw
420
  max_delta_y = base_clip.h - th
421
-
422
  motion_style = random.choice(["zoom-in", "zoom-out", "pan-right", "pan-left"])
423
  clip_duration = clip.duration
424
 
425
  def frame_transformation_matrix(get_frame, t):
426
  frame = get_frame(t)
427
  progress = (t / clip_duration) if clip_duration > 0 else 0
428
- # Smooth sinusoidal mapping curves
429
  smooth_step = 0.5 * (1.0 - math.cos(math.pi * progress))
430
 
431
  if motion_style == "zoom-in":
432
- scale_current = 1.0 + (0.12 * smooth_step)
433
- curr_w, curr_h = int(tw * scale_current), int(th * scale_current)
434
- resized_f = cv2.resize(frame, (curr_w, curr_h), interpolation=cv2.INTER_LINEAR)
435
- x_start = (curr_w - tw) // 2
436
- y_start = (curr_h - th) // 2
437
- return resized_f[y_start:y_start+th, x_start:x_start+tw]
438
-
439
  elif motion_style == "zoom-out":
440
- scale_current = 1.15 - (0.12 * smooth_step)
441
- curr_w, curr_h = int(tw * scale_current), int(th * scale_current)
442
- resized_f = cv2.resize(frame, (curr_w, curr_h), interpolation=cv2.INTER_LINEAR)
443
- x_start = (curr_w - tw) // 2
444
- y_start = (curr_h - th) // 2
445
- return resized_f[y_start:y_start+th, x_start:x_start+tw]
446
-
447
  elif motion_style == "pan-right":
448
  x_offset = int(max_delta_x * smooth_step)
449
- y_center = max_delta_y // 2
450
- return frame[y_center:y_center+th, x_offset:x_offset+tw]
451
-
452
- else: # pan-left
453
  x_offset = int(max_delta_x * (1.0 - smooth_step))
454
- y_center = max_delta_y // 2
455
- return frame[y_center:y_center+th, x_offset:x_offset+tw]
456
 
457
  return base_clip.fl(frame_transformation_matrix)
458
 
459
  def compile_individual_segment(media_path, asset_type, audio_path, script_text, segment_id):
460
- """Combines voice, subtitles, and background visual elements into a single composite sequence block."""
461
- video_sequence_clip = None
462
- voice_track_clip = None
463
- subtitle_overlay_clip = None
464
- composite_block = None
465
-
466
  try:
467
- if not os.path.exists(media_path) or not os.path.exists(audio_path):
468
- return None
469
-
470
  voice_track_clip = AudioFileClip(audio_path).fx(vfx.audio_fadeout, 0.15)
471
  allocated_duration = voice_track_clip.duration + 0.35
472
 
473
  if asset_type == "video":
474
  raw_video = VideoFileClip(media_path, audio=False)
475
  normalized_video = resize_and_crop_to_fill(raw_video, TARGET_RESOLUTION)
476
-
477
- if normalized_video.duration < allocated_duration:
478
- video_sequence_clip = normalized_video.fx(vfx.loop, duration=allocated_duration)
479
- raw_video.close()
480
- normalized_video.close()
481
- else:
482
- video_sequence_clip = normalized_video.subclip(0, allocated_duration)
483
- raw_video.close()
484
- normalized_video.close()
485
  else:
486
  base_image = ImageClip(media_path).set_duration(allocated_duration)
487
- animated_image = apply_cinematic_motion_effect(base_image, TARGET_RESOLUTION)
488
- video_sequence_clip = animated_image.fx(vfx.fadein, 0.25).fx(vfx.fadeout, 0.25)
489
  base_image.close()
490
 
491
  video_sequence_clip = video_sequence_clip.set_audio(voice_track_clip)
492
 
493
- # Append caption overlays dynamically if requested
494
  if CAPTION_COLOR == "white" and script_text:
495
- subtitle_image_file = build_wrapped_subtitle_layer(script_text, TARGET_RESOLUTION, font_size)
496
- subtitle_overlay_clip = (ImageClip(subtitle_image_file)
497
- .set_duration(allocated_duration)
498
- .set_position(('center', int(TARGET_RESOLUTION[1] * 0.78))))
499
- composite_block = CompositeVideoClip([video_sequence_clip, subtitle_overlay_clip], size=TARGET_RESOLUTION)
500
- return composite_block
501
- else:
502
- return video_sequence_clip
503
-
504
  except Exception as err:
505
- print(f"Exception encountered while processing sequence segment block assembly #{segment_id}: {err}")
506
- # Clean execution leaks
507
  try:
508
- if video_sequence_clip: video_sequence_clip.close()
509
- if voice_track_clip: voice_track_clip.close()
510
- if subtitle_overlay_clip: subtitle_overlay_clip.close()
511
- except:
512
- pass
513
  return None
514
 
515
  # ---------------- Pipeline Processing Entry ---------------- #
516
 
517
- def generate_video(user_input, resolution_mode, enable_captions):
518
- """Main orchestrator function that controls script generation, file handling, asset searches, and rendering."""
519
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
520
 
521
  api_ok, check_msg = check_api_keys()
522
- if not api_ok:
523
- return None, f"Configuration Error: {check_msg}"
524
 
525
  TARGET_RESOLUTION = (1920, 1080) if resolution_mode == "Full (16:9)" else (1080, 1920)
526
  CAPTION_COLOR = "white" if enable_captions == "Yes" else "transparent"
@@ -530,185 +410,96 @@ def generate_video(user_input, resolution_mode, enable_captions):
530
  final_output_composition = None
531
 
532
  try:
533
- print("Contacting Groq processing endpoints for narrative compilation...")
534
- generated_script_text = generate_script(user_input)
535
-
536
- if not generated_script_text:
537
- return None, "Failed to retrieve processing directives from Groq script modules."
538
 
539
- print(f"\n--- Output Script Generated ---\n{generated_script_text}\n-------------------------------")
540
-
541
  script_execution_steps = parse_script(generated_script_text)
542
- if not script_execution_steps:
543
- return None, "Parser structural error. Could not read tags from the generated script structure."
544
-
545
- # Connect matching narrative scripts to corresponding scene prompts
546
- paired_segments = []
547
- for i in range(0, len(script_execution_steps), 2):
548
- if i + 1 < len(script_execution_steps):
549
- paired_segments.append((script_execution_steps[i], script_execution_steps[i+1]))
550
 
551
- print(f"Beginning rendering sequences for {len(paired_segments)} active scenes.")
552
 
553
  for idx, (media_cue, audio_cue) in enumerate(paired_segments):
554
- print(f"Rendering scene progression track ({idx + 1}/{len(paired_segments)}) -> Tag: {media_cue['prompt']}")
555
-
556
  media_asset = generate_media_asset(media_cue['prompt'])
557
  audio_track_file = generate_tts_audio(audio_cue['text'])
558
-
559
- constructed_segment = compile_individual_segment(
560
- media_path=media_asset['path'],
561
- asset_type=media_asset['type'],
562
- audio_path=audio_track_file,
563
- script_text=audio_cue['text'],
564
- segment_id=idx
565
- )
566
-
567
- if constructed_segment:
568
- master_clip_list.append(constructed_segment)
569
 
570
- if not master_clip_list:
571
- return None, "Pipeline Error: Could not successfully render any independent scene tracks."
572
 
573
- print("Assembling timeline segments into master stream...")
574
  final_output_composition = concatenate_videoclips(master_clip_list, method="compose")
575
 
576
- # Mix background audio files if available
577
  bg_music_source = "music.mp3"
578
  if os.path.exists(bg_music_source):
579
  try:
580
- print("Injecting background music matrix layer...")
581
  ambient_music_clip = AudioFileClip(bg_music_source)
582
-
583
  if ambient_music_clip.duration < final_output_composition.duration:
584
- loop_iterations = math.ceil(final_output_composition.duration / ambient_music_clip.duration)
585
- ambient_music_clip = concatenate_audioclips([ambient_music_clip] * loop_iterations)
586
-
587
- ambient_music_clip = (ambient_music_clip
588
- .subclip(0, final_output_composition.duration)
589
- .fx(vfx.volumex, bg_music_volume))
590
-
591
- mixed_audio_output = CompositeAudioClip([final_output_composition.audio, ambient_music_clip])
592
- final_output_composition = final_output_composition.set_audio(mixed_audio_output)
593
- except Exception as music_err:
594
- print(f"Background ambient track mixing exception bypassed: {music_err}")
595
-
596
- print(f"Encoding final MP4 video stream layer ({fps} FPS)...")
597
- final_output_composition.write_videofile(
598
- OUTPUT_VIDEO_FILENAME,
599
- codec='libx264',
600
- audio_codec='aac',
601
- fps=fps,
602
- preset=preset,
603
- threads=4,
604
- logger=None
605
- )
606
-
607
  return OUTPUT_VIDEO_FILENAME, "Cinematic video generation completed successfully!"
608
 
609
  except Exception as pipeline_fault:
610
- print(f"Critical execution failure inside video generation pipeline: {pipeline_fault}")
611
  return None, f"Execution Failure: {str(pipeline_fault)}"
612
-
613
  finally:
614
- print("Executing memory storage cleanup tracks...")
615
  try:
616
- if final_output_composition:
617
- final_output_composition.close()
618
- for structural_clip in master_clip_list:
619
- if structural_clip:
620
- structural_clip.close()
621
- except Exception as close_err:
622
- print(f"Resource lock release trace warning: {close_err}")
623
-
624
- time.sleep(1.2)
625
  if TEMP_FOLDER and os.path.exists(TEMP_FOLDER):
626
- try:
627
- shutil.rmtree(TEMP_FOLDER)
628
- print("Temporary working directory flushed successfully.")
629
- except Exception as flush_err:
630
- print(f"Warning clearing temporary directory blocks: {flush_err}")
631
 
632
  # ---------------- Gradio Interface Binding Maps ---------------- #
633
 
634
- def UI_interaction_bridge(prompt, res_mode, caption_flag, music_upload, voice, v_prob, music_vol, frames_per_sec, speed_preset, tts_speed, size_font):
635
- """Synchronizes UI settings variables with background script parameters."""
636
  global selected_voice, voice_speed, font_size, video_clip_probability, bg_music_volume, fps, preset
637
-
638
  selected_voice = VOICE_CHOICES.get(voice, 'am_michael')
639
- voice_speed = tts_speed
640
- font_size = size_font
641
- video_clip_probability = v_prob / 100.0
642
- bg_music_volume = music_vol
643
- fps = frames_per_sec
644
- preset = speed_preset
645
 
646
  if music_upload is not None:
647
- destination_path = "music.mp3"
648
- try:
649
- shutil.copy(music_upload.name, destination_path)
650
- print(f"New ambient track file successfully mounted: {destination_path}")
651
- except Exception as e:
652
- print(f"Error mounting audio file resource track: {e}")
653
 
654
- return generate_video(prompt, res_mode, caption_flag)
655
 
656
- # Constructing layout structures
657
  with gr.Blocks(title="AI Cinematic Documentary Generator") as app_interface:
658
  gr.Markdown("# 🎬 AI Cinematic Documentary Video Generator")
659
- gr.Markdown("Instantly build automated documentary videos fueled by high-performance Groq Llama-3.3 intelligence and automated stock footage tracking matrices.")
660
 
661
  with gr.Row():
662
  with gr.Column(scale=1):
663
- user_prompt_input = gr.Textbox(
664
- label="Documentary Video Concept / Prompt",
665
- placeholder="Ex: The secret comedic lives of household cats when owners go to work...",
666
- lines=4
667
- )
668
 
669
  with gr.Row():
670
- ui_resolution = gr.Radio(["Full (16:9)", "Shorts (9:16)"], label="Video Output Dimension", value="Full (16:9)")
671
- ui_captions = gr.Radio(["Yes", "No"], label="Render Subtitle Captions", value="Yes")
672
 
673
- ui_audio_upload = gr.File(label="Optional Background Music Tracking (MP3 Only)", file_types=[".mp3"])
674
 
675
  with gr.Accordion("Advanced Cinema Tuning Configuration", open=False):
676
- ui_voice_selection = gr.Dropdown(
677
- choices=list(VOICE_CHOICES.keys()),
678
- label="Narration Voice Model Selection",
679
- value="Michael (Male)"
680
- )
681
- ui_video_probability = gr.Slider(0, 100, value=70, step=5, label="Target Video vs. Photo Probability (%)")
682
- ui_music_level = gr.Slider(0.00, 0.40, value=0.06, step=0.01, label="Background Music Volume Mix Factor")
683
- ui_fps_target = gr.Slider(15, 60, value=24, step=1, label="Target Output Encoding FPS")
684
- ui_encoding_speed = gr.Dropdown(
685
- choices=["ultrafast", "superfast", "veryfast", "medium", "slow"],
686
- value="veryfast",
687
- label="FFmpeg Encoding Pipeline Speed Preset"
688
- )
689
- ui_voice_tempo = gr.Slider(0.6, 1.4, value=1.05, step=0.05, label="Narration Voice Pace Speed")
690
- ui_caption_font_size = gr.Slider(20, 90, value=48, step=2, label="Caption Title Text Size Scaling")
691
 
692
  trigger_generation_button = gr.Button("🎬 Render Cinematic Video Sequence", variant="primary")
693
 
694
  with gr.Column(scale=1):
695
- video_output_display = gr.Video(label="Final Render Output Component Preview")
696
  pipeline_status_log = gr.Textbox(label="System Operational Pipeline Logs", interactive=False)
697
-
698
- gr.Markdown("""
699
- ### ⚙️ Production Guidelines & Prerequisites:
700
- 1. Verify your operational environment contains verified **GROQ_API_KEY** and **PEXELS_API_KEY** secret strings.
701
- 2. Optional backing tracks look for or utilize uploaded `.mp3` files mapped directly into working context spaces.
702
- 3. The subtitle caption generator functions natively on a zero-dependency Pillow overlay architecture to maximize compatibility with Hugging Face deployment spaces.
703
- """)
704
 
705
  trigger_generation_button.click(
706
  fn=UI_interaction_bridge,
707
- inputs=[
708
- user_prompt_input, ui_resolution, ui_captions, ui_audio_upload,
709
- ui_voice_selection, ui_video_probability, ui_music_level, ui_fps_target,
710
- ui_encoding_speed, ui_voice_tempo, ui_caption_font_size
711
- ],
712
  outputs=[video_output_display, pipeline_status_log]
713
  )
714
 
 
36
 
37
  # ---------------- Global Configuration ---------------- #
38
 
 
39
  PEXELS_API_KEY = os.environ.get('PEXELS_API_KEY', '')
40
  GROQ_API_KEY = os.environ.get('GROQ_API_KEY', '')
41
 
 
42
  if not PEXELS_API_KEY:
43
  PEXELS_API_KEY = 'YOUR_PEXELS_KEY_HERE'
44
  if not GROQ_API_KEY:
 
61
  TEMP_FOLDER = None
62
 
63
  VOICE_CHOICES = {
64
+ 'Emma (Female)': 'af_heart', 'Bella (Female)': 'af_bella', 'Nicole (Female)': 'af_nicole',
65
+ 'Aoede (Female)': 'af_aoede', 'Kore (Female)': 'af_kore', 'Sarah (Female)': 'af_sarah',
66
+ 'Nova (Female)': 'af_nova', 'Sky (Female)': 'af_sky', 'Alloy (Female)': 'af_alloy',
67
+ 'Jessica (Female)': 'af_jessica', 'River (Female)': 'af_river', 'Michael (Male)': 'am_michael',
68
+ 'Fenrir (Male)': 'am_fenrir', 'Puck (Male)': 'am_puck', 'Echo (Male)': 'am_echo',
69
+ 'Eric (Male)': 'am_eric', 'Liam (Male)': 'am_liam', 'Onyx (Male)': 'am_onyx',
70
+ 'Santa (Male)': 'am_santa', 'Adam (Male)': 'am_adam', 'Emma 🇬🇧 (Female)': 'bf_emma',
71
+ 'Isabella 🇬🇧 (Female)': 'bf_isabella', 'Alice 🇬🇧 (Female)': 'bf_alice', 'Lily 🇬🇧 (Female)': 'bf_lily',
72
+ 'George 🇬🇧 (Male)': 'bm_george', 'Fable 🇬🇧 (Male)': 'bm_fable', 'Lewis 🇬🇧 (Male)': 'bm_lewis',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  'Daniel 🇬🇧 (Male)': 'bm_daniel'
74
  }
75
 
76
  # ---------------- Core Support Functions ---------------- #
77
 
78
  def check_api_keys():
 
79
  if not PEXELS_API_KEY or PEXELS_API_KEY == 'YOUR_PEXELS_KEY_HERE':
80
  return False, "PEXELS_API_KEY is not configured in environment variables."
81
  if not GROQ_API_KEY or GROQ_API_KEY == 'YOUR_GROQ_KEY_HERE':
82
  return False, "GROQ_API_KEY is not configured in environment variables."
83
  return True, "API keys configured successfully."
84
 
85
+ def generate_script(user_input, tone_style):
86
+ """Generate high-quality, humanized documentary scripts using style presets via Groq."""
87
  headers = {
88
  'Authorization': f'Bearer {GROQ_API_KEY}',
89
  'Content-Type': 'application/json'
90
  }
91
 
92
+ # Tone definition matrix map
93
+ tone_directives = {
94
+ "Funny / Humorous": "Write with sharp comedic timing, witty punchlines, and playful sarcasm. Make light of the topic like a stand-up comedian presenting a documentary.",
95
+ "Serious / Dramatic": "Write with an authoritative, compelling, and intense cinematic tone. Focus on raw emotion, gravity, and high stakes without being dry.",
96
+ "Informal / Casual": "Write like a close friend talking over coffee. Use relaxed phrasing, casual observations, and a warm, approachable delivery.",
97
+ "Educational / Insightful": "Write with curiosity and infectious enthusiasm for facts. Keep it deeply engaging, clear, intellectual, yet completely accessible."
98
+ }
99
+
100
+ selected_tone_prompt = tone_directives.get(tone_style, tone_directives["Informal / Casual"])
101
+
102
+ prompt = f"""You are an authentic human video narrator. Your task is to write an engaging video script based on the topic provided.
103
+
104
+ TONE SETTING:
105
+ {selected_tone_prompt}
106
 
107
  CRITICAL INSTRUCTIONS FOR NATURAL HUMAN TONE:
108
+ - Avoid ALL standard robotic AI tropes, rigid filler terms, and artificial structural transitions.
109
  - Strictly DO NOT use words like: "delve", "tapestry", "testament", "furthermore", "moreover", "in conclusion", "look no further", "nestled", "beacon", or "revolutionize".
110
+ - Write with organic variety in sentence structure. Mix short, punchy phrases with authentic commentary.
 
111
 
112
  Format Requirements:
113
  - Break the script into distinct scenes using structural brackets: [Tag].
114
+ - The Tag must be a simple 1-2 word search query suitable for finding background stock video/images (e.g., [Cat Sleeping], [Stormy Sky]).
115
+ - Directly beneath each tag, write exactly one engaging sentence (maximum 15 words) continuing the narrative stream.
116
+ - Conclude the piece with a [Subscribe] tag containing a clever, customized parting remark matching your assigned tone.
117
 
118
  Topic: {user_input}
119
  """
 
121
  data = {
122
  'model': GROQ_MODEL,
123
  'messages': [{'role': 'user', 'content': prompt}],
124
+ 'temperature': 0.72,
125
  'max_tokens': 1500
126
  }
127
 
 
132
  json=data,
133
  timeout=25
134
  )
 
135
  if response.status_code == 200:
136
+ return response.json()['choices'][0]['message']['content'].strip()
 
137
  else:
138
+ print(f"Groq API Error {response.status_code}: {response.text}")
139
  return None
140
  except Exception as e:
141
  print(f"Network processing exception during script generation: {str(e)}")
142
  return None
143
 
144
  def parse_script(script_text):
 
145
  if not script_text:
146
  return []
 
 
147
  pattern = r'\[(.*?)\]\s*([^\[]+)'
148
  matches = re.findall(pattern, script_text)
149
 
 
151
  for tag, narration in matches:
152
  clean_tag = tag.strip()
153
  clean_narration = re.sub(r'\s+', ' ', narration.strip())
 
154
  if not clean_tag or not clean_narration:
155
  continue
156
 
 
157
  elements.append({"type": "media", "prompt": clean_tag})
 
 
158
  words = clean_narration.split()
159
  calculated_duration = max(3.5, len(words) * 0.45)
160
  elements.append({
 
162
  "text": clean_narration,
163
  "duration": calculated_duration
164
  })
 
165
  return elements
166
 
167
  def search_pexels_videos(query, pexels_api_key):
 
168
  headers = {'Authorization': pexels_api_key}
169
  url = "https://api.pexels.com/videos/search"
170
  params = {"query": query, "per_page": 8, "orientation": "landscape"}
 
171
  try:
172
  response = requests.get(url, headers=headers, params=params, timeout=12)
173
  if response.status_code == 200:
174
+ videos = response.json().get("videos", [])
 
175
  if videos:
176
  selected_video = random.choice(videos)
177
  video_files = selected_video.get("video_files", [])
 
178
  for file in video_files:
179
  if file.get("quality") == "hd" and file.get("width", 0) >= 1280:
180
  return file.get("link")
181
  if video_files:
182
  return video_files[0].get("link")
183
  except Exception as e:
184
+ print(f"Pexels video fetch exception: {e}")
185
  return None
186
 
187
  def search_pexels_images(query, pexels_api_key):
 
188
  headers = {'Authorization': pexels_api_key}
189
  url = "https://api.pexels.com/v1/search"
190
  params = {"query": query, "per_page": 8, "orientation": "landscape"}
 
191
  try:
192
  response = requests.get(url, headers=headers, params=params, timeout=12)
193
  if response.status_code == 200:
194
+ photos = response.json().get("photos", [])
 
195
  if photos:
196
  return random.choice(photos).get("src", {}).get("large2x")
197
  except Exception as e:
198
+ print(f"Pexels image search exception: {e}")
199
  return None
200
 
201
  def download_asset_file(url, local_path):
 
202
  try:
203
  headers = {"User-Agent": USER_AGENT}
204
  with requests.get(url, headers=headers, stream=True, timeout=20) as r:
205
  r.raise_for_status()
206
  with open(local_path, 'wb') as f:
207
  for chunk in r.iter_content(chunk_size=16384):
208
+ if chunk: f.write(chunk)
 
209
  return local_path
210
  except Exception as e:
211
+ print(f"Error handling asset download: {e}")
212
+ if os.path.exists(local_path): os.remove(local_path)
 
213
  return None
214
 
215
  def generate_solid_fallback(prompt, target_res):
 
216
  w, h = target_res
217
  random.seed(prompt)
218
  base_color = (random.randint(15, 35), random.randint(20, 40), random.randint(30, 55))
 
219
  img = Image.new('RGB', (w, h), base_color)
220
  fallback_path = os.path.join(TEMP_FOLDER, f"fallback_{int(time.time())}_{random.randint(0,99)}.jpg")
221
  img.save(fallback_path, quality=90)
222
  return fallback_path
223
 
224
  def generate_media_asset(prompt):
 
225
  safe_name = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
 
 
226
  if random.random() < video_clip_probability:
227
  video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
228
  if video_url:
 
230
  if download_asset_file(video_url, local_video_path):
231
  return {"path": local_video_path, "type": "video"}
232
 
 
233
  img_url = search_pexels_images(prompt, PEXELS_API_KEY)
234
  if img_url:
235
  local_img_path = os.path.join(TEMP_FOLDER, f"img_{safe_name}_{int(time.time())}.jpg")
236
  if download_asset_file(img_url, local_img_path):
237
  return {"path": local_img_path, "type": "image"}
238
 
239
+ return {"path": generate_solid_fallback(prompt, TARGET_RESOLUTION), "type": "image"}
 
 
240
 
241
  def generate_tts_audio(text):
 
242
  safe_name = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
243
  output_audio_path = os.path.join(TEMP_FOLDER, f"tts_{safe_name}_{int(time.time())}.wav")
 
244
  try:
 
245
  generator = pipeline(text, voice=selected_voice, speed=voice_speed, split_pattern=r'\n+')
246
  audio_blocks = [audio for _, _, audio in generator]
 
247
  if audio_blocks:
248
  merged_audio = np.concatenate(audio_blocks) if len(audio_blocks) > 1 else audio_blocks[0]
249
  sf.write(output_audio_path, merged_audio, 24000)
250
  return output_audio_path
251
  except Exception as e:
252
+ print(f"Kokoro engine bypass, trying gTTS fallback: {e}")
253
 
254
  try:
 
255
  tts = gTTS(text=text, lang='en', slow=False)
256
  temp_mp3 = os.path.join(TEMP_FOLDER, f"gtts_{int(time.time())}.mp3")
257
  tts.save(temp_mp3)
258
+ AudioSegment.from_mp3(temp_mp3).export(output_audio_path, format="wav")
259
+ if os.path.exists(temp_mp3): os.remove(temp_mp3)
 
 
 
 
260
  return output_audio_path
261
  except Exception as fail_err:
262
+ print(f"Critical synthesis failure. Using silence audio pad: {fail_err}")
 
 
263
  duration_sec = max(3, len(text.split()) * 0.5)
264
+ sf.write(output_audio_path, np.zeros(int(duration_sec * 24000), dtype=np.float32), 24000)
 
265
  return output_audio_path
266
 
267
  def build_wrapped_subtitle_layer(text, canvas_resolution, font_size_target):
 
268
  width, height = canvas_resolution
269
  max_text_boundary_width = int(width * 0.85)
 
 
270
  font_engine = None
271
+ font_options = ["/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", "C:\\Windows\\Fonts\\arialbd.ttf", "/usr/share/fonts/Arial.ttf"]
 
 
 
 
272
  for path in font_options:
273
  if os.path.exists(path):
274
  try:
275
  font_engine = ImageFont.truetype(path, font_size_target)
276
  break
277
+ except: pass
278
+ if font_engine is None: font_engine = ImageFont.load_default()
 
 
279
 
 
280
  words = text.split()
281
+ compiled_lines, current_line_build = [], []
 
 
282
  measurement_canvas = Image.new('RGBA', (1, 1))
283
  draw_inspector = ImageDraw.Draw(measurement_canvas)
284
 
 
287
  try:
288
  box = draw_inspector.textbbox((0, 0), test_string, font=font_engine)
289
  calculated_w = box[2] - box[0]
290
+ except: calculated_w = len(test_string) * (font_size_target * 0.55)
 
291
 
292
  if calculated_w <= max_text_boundary_width:
293
  current_line_build.append(word)
294
  else:
295
+ if current_line_build: compiled_lines.append(" ".join(current_line_build))
 
296
  current_line_build = [word]
297
+ if current_line_build: compiled_lines.append(" ".join(current_line_build))
 
298
 
 
299
  line_stride = font_size_target + 12
300
  computed_box_height = (len(compiled_lines) * line_stride) + 40
 
301
  subtitle_strip_canvas = Image.new('RGBA', (width, computed_box_height), (0, 0, 0, 0))
302
  context_drawer = ImageDraw.Draw(subtitle_strip_canvas)
303
 
 
305
  try:
306
  box = context_drawer.textbbox((0, 0), line, font=font_engine)
307
  w = box[2] - box[0]
308
+ except: w = len(line) * (font_size_target * 0.55)
 
309
 
310
  target_x = (width - w) // 2
311
  target_y = 20 + (idx * line_stride)
312
+ for dx, dy in [(-2, -2), (-2, 2), (2, -2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)]:
 
 
 
313
  context_drawer.text((target_x + dx, target_y + dy), line, font=font_engine, fill=(0, 0, 0, 225))
 
 
314
  context_drawer.text((target_x, target_y), line, font=font_engine, fill=(255, 255, 255, 255))
315
 
316
  local_subtitle_png_path = os.path.join(TEMP_FOLDER, f"sub_{int(time.time())}_{random.randint(100,999)}.png")
 
318
  return local_subtitle_png_path
319
 
320
  def resize_and_crop_to_fill(clip, target_resolution):
 
321
  tw, th = target_resolution
322
  clip_w, clip_h = clip.w, clip.h
 
323
  aspect_target = tw / th
324
  aspect_clip = clip_w / clip_h
325
 
326
  if aspect_clip > aspect_target:
327
  scale_factor = th / clip_h
328
+ resized_clip = clip.resize(newsize=(int(clip_w * scale_factor), th))
329
+ return resized_clip.crop(x1=(resized_clip.w - tw) / 2, x2=((resized_clip.w - tw) / 2) + tw, y1=0, y2=th)
 
 
330
  else:
331
  scale_factor = tw / clip_w
332
+ resized_clip = clip.resize(newsize=(tw, int(clip_h * scale_factor)))
333
+ return resized_clip.crop(x1=0, x2=tw, y1=(resized_clip.h - th) / 2, y2=((resized_clip.h - th) / 2) + th)
 
 
334
 
335
  def apply_cinematic_motion_effect(clip, target_resolution):
 
336
  tw, th = target_resolution
 
 
337
  base_clip = clip.resize(newsize=(int(tw * 1.2), int(th * 1.2)))
338
  max_delta_x = base_clip.w - tw
339
  max_delta_y = base_clip.h - th
 
340
  motion_style = random.choice(["zoom-in", "zoom-out", "pan-right", "pan-left"])
341
  clip_duration = clip.duration
342
 
343
  def frame_transformation_matrix(get_frame, t):
344
  frame = get_frame(t)
345
  progress = (t / clip_duration) if clip_duration > 0 else 0
 
346
  smooth_step = 0.5 * (1.0 - math.cos(math.pi * progress))
347
 
348
  if motion_style == "zoom-in":
349
+ sc = 1.0 + (0.12 * smooth_step)
350
+ rf = cv2.resize(frame, (int(tw * sc), int(th * sc)), interpolation=cv2.INTER_LINEAR)
351
+ return rf[((rf.shape[0]-th)//2):((rf.shape[0]-th)//2)+th, ((rf.shape[1]-tw)//2):((rf.shape[1]-tw)//2)+tw]
 
 
 
 
352
  elif motion_style == "zoom-out":
353
+ sc = 1.15 - (0.12 * smooth_step)
354
+ rf = cv2.resize(frame, (int(tw * sc), int(th * sc)), interpolation=cv2.INTER_LINEAR)
355
+ return rf[((rf.shape[0]-th)//2):((rf.shape[0]-th)//2)+th, ((rf.shape[1]-tw)//2):((rf.shape[1]-tw)//2)+tw]
 
 
 
 
356
  elif motion_style == "pan-right":
357
  x_offset = int(max_delta_x * smooth_step)
358
+ return frame[max_delta_y//2:(max_delta_y//2)+th, x_offset:x_offset+tw]
359
+ else:
 
 
360
  x_offset = int(max_delta_x * (1.0 - smooth_step))
361
+ return frame[max_delta_y//2:(max_delta_y//2)+th, x_offset:x_offset+tw]
 
362
 
363
  return base_clip.fl(frame_transformation_matrix)
364
 
365
  def compile_individual_segment(media_path, asset_type, audio_path, script_text, segment_id):
366
+ video_sequence_clip, voice_track_clip, subtitle_overlay_clip = None, None, None
 
 
 
 
 
367
  try:
368
+ if not os.path.exists(media_path) or not os.path.exists(audio_path): return None
 
 
369
  voice_track_clip = AudioFileClip(audio_path).fx(vfx.audio_fadeout, 0.15)
370
  allocated_duration = voice_track_clip.duration + 0.35
371
 
372
  if asset_type == "video":
373
  raw_video = VideoFileClip(media_path, audio=False)
374
  normalized_video = resize_and_crop_to_fill(raw_video, TARGET_RESOLUTION)
375
+ video_sequence_clip = normalized_video.fx(vfx.loop, duration=allocated_duration) if normalized_video.duration < allocated_duration else normalized_video.subclip(0, allocated_duration)
376
+ raw_video.close()
 
 
 
 
 
 
 
377
  else:
378
  base_image = ImageClip(media_path).set_duration(allocated_duration)
379
+ video_sequence_clip = apply_cinematic_motion_effect(base_image, TARGET_RESOLUTION).fx(vfx.fadein, 0.25).fx(vfx.fadeout, 0.25)
 
380
  base_image.close()
381
 
382
  video_sequence_clip = video_sequence_clip.set_audio(voice_track_clip)
383
 
 
384
  if CAPTION_COLOR == "white" and script_text:
385
+ sub_file = build_wrapped_subtitle_layer(script_text, TARGET_RESOLUTION, font_size)
386
+ subtitle_overlay_clip = ImageClip(sub_file).set_duration(allocated_duration).set_position(('center', int(TARGET_RESOLUTION[1] * 0.78)))
387
+ return CompositeVideoClip([video_sequence_clip, subtitle_overlay_clip], size=TARGET_RESOLUTION)
388
+ return video_sequence_clip
 
 
 
 
 
389
  except Exception as err:
390
+ print(f"Segment compilation error #{segment_id}: {err}")
 
391
  try:
392
+ for c in [video_sequence_clip, voice_track_clip, subtitle_overlay_clip]:
393
+ if c: c.close()
394
+ except: pass
 
 
395
  return None
396
 
397
  # ---------------- Pipeline Processing Entry ---------------- #
398
 
399
+ def generate_video(user_input, tone_style, resolution_mode, enable_captions):
 
400
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
401
 
402
  api_ok, check_msg = check_api_keys()
403
+ if not api_ok: return None, f"Configuration Error: {check_msg}"
 
404
 
405
  TARGET_RESOLUTION = (1920, 1080) if resolution_mode == "Full (16:9)" else (1080, 1920)
406
  CAPTION_COLOR = "white" if enable_captions == "Yes" else "transparent"
 
410
  final_output_composition = None
411
 
412
  try:
413
+ print(f"Requesting '{tone_style}' narrative track from Groq AI engines...")
414
+ generated_script_text = generate_script(user_input, tone_style)
415
+ if not generated_script_text: return None, "Failed to retrieve script from Groq endpoints."
 
 
416
 
 
 
417
  script_execution_steps = parse_script(generated_script_text)
418
+ if not script_execution_steps: return None, "Parser layout error. Check script tags."
 
 
 
 
 
 
 
419
 
420
+ paired_segments = [(script_execution_steps[i], script_execution_steps[i+1]) for i in range(0, len(script_execution_steps), 2) if i + 1 < len(script_execution_steps)]
421
 
422
  for idx, (media_cue, audio_cue) in enumerate(paired_segments):
 
 
423
  media_asset = generate_media_asset(media_cue['prompt'])
424
  audio_track_file = generate_tts_audio(audio_cue['text'])
425
+ constructed_segment = compile_individual_segment(media_asset['path'], media_asset['type'], audio_track_file, audio_cue['text'], idx)
426
+ if constructed_segment: master_clip_list.append(constructed_segment)
 
 
 
 
 
 
 
 
 
427
 
428
+ if not master_clip_list: return None, "Pipeline Error: Could not render individual timeline tracks."
 
429
 
 
430
  final_output_composition = concatenate_videoclips(master_clip_list, method="compose")
431
 
 
432
  bg_music_source = "music.mp3"
433
  if os.path.exists(bg_music_source):
434
  try:
 
435
  ambient_music_clip = AudioFileClip(bg_music_source)
 
436
  if ambient_music_clip.duration < final_output_composition.duration:
437
+ ambient_music_clip = concatenate_audioclips([ambient_music_clip] * math.ceil(final_output_composition.duration / ambient_music_clip.duration))
438
+ ambient_music_clip = ambient_music_clip.subclip(0, final_output_composition.duration).fx(vfx.volumex, bg_music_volume)
439
+ final_output_composition = final_output_composition.set_audio(CompositeAudioClip([final_output_composition.audio, ambient_music_clip]))
440
+ except Exception as music_err: print(f"Background audio mixing bypassed: {music_err}")
441
+
442
+ final_output_composition.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', audio_codec='aac', fps=fps, preset=preset, threads=4, logger=None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
443
  return OUTPUT_VIDEO_FILENAME, "Cinematic video generation completed successfully!"
444
 
445
  except Exception as pipeline_fault:
 
446
  return None, f"Execution Failure: {str(pipeline_fault)}"
 
447
  finally:
 
448
  try:
449
+ if final_output_composition: final_output_composition.close()
450
+ for clip in master_clip_list:
451
+ if clip: clip.close()
452
+ except: pass
453
+ time.sleep(1.0)
 
 
 
 
454
  if TEMP_FOLDER and os.path.exists(TEMP_FOLDER):
455
+ try: shutil.rmtree(TEMP_FOLDER)
456
+ except: pass
 
 
 
457
 
458
  # ---------------- Gradio Interface Binding Maps ---------------- #
459
 
460
+ def UI_interaction_bridge(prompt, tone, res_mode, caption_flag, music_upload, voice, v_prob, music_vol, frames_per_sec, speed_preset, tts_speed, size_font):
 
461
  global selected_voice, voice_speed, font_size, video_clip_probability, bg_music_volume, fps, preset
 
462
  selected_voice = VOICE_CHOICES.get(voice, 'am_michael')
463
+ voice_speed, font_size, video_clip_probability, bg_music_volume, fps, preset = tts_speed, size_font, v_prob / 100.0, music_vol, frames_per_sec, speed_preset
 
 
 
 
 
464
 
465
  if music_upload is not None:
466
+ try: shutil.copy(music_upload.name, "music.mp3")
467
+ except Exception as e: print(f"Error mounting audio file: {e}")
 
 
 
 
468
 
469
+ return generate_video(prompt, tone, res_mode, caption_flag)
470
 
 
471
  with gr.Blocks(title="AI Cinematic Documentary Generator") as app_interface:
472
  gr.Markdown("# 🎬 AI Cinematic Documentary Video Generator")
 
473
 
474
  with gr.Row():
475
  with gr.Column(scale=1):
476
+ user_prompt_input = gr.Textbox(label="Documentary Video Concept / Prompt", placeholder="Ex: The secret life of honeybees...", lines=3)
477
+ ui_tone = gr.Dropdown(choices=["Funny / Humorous", "Serious / Dramatic", "Informal / Casual", "Educational / Insightful"], value="Informal / Casual", label="Narrator Presentation Tone Preset")
 
 
 
478
 
479
  with gr.Row():
480
+ ui_resolution = gr.Radio(["Full (16:9)", "Shorts (9:16)"], label="Video Dimensions", value="Full (16:9)")
481
+ ui_captions = gr.Radio(["Yes", "No"], label="Render Captions", value="Yes")
482
 
483
+ ui_audio_upload = gr.File(label="Optional Background Music (MP3)", file_types=[".mp3"])
484
 
485
  with gr.Accordion("Advanced Cinema Tuning Configuration", open=False):
486
+ ui_voice_selection = gr.Dropdown(choices=list(VOICE_CHOICES.keys()), label="Voice Model", value="Michael (Male)")
487
+ ui_video_probability = gr.Slider(0, 100, value=70, step=5, label="Video vs Photo Ratio (%)")
488
+ ui_music_level = gr.Slider(0.00, 0.40, value=0.06, step=0.01, label="Music Mix Volume")
489
+ ui_fps_target = gr.Slider(15, 60, value=24, step=1, label="Target Output FPS")
490
+ ui_encoding_speed = gr.Dropdown(choices=["ultrafast", "superfast", "veryfast", "medium", "slow"], value="veryfast", label="FFmpeg Speed Preset")
491
+ ui_voice_tempo = gr.Slider(0.6, 1.4, value=1.05, step=0.05, label="Voice Tempo Scale")
492
+ ui_caption_font_size = gr.Slider(20, 90, value=48, step=2, label="Caption Size")
 
 
 
 
 
 
 
 
493
 
494
  trigger_generation_button = gr.Button("🎬 Render Cinematic Video Sequence", variant="primary")
495
 
496
  with gr.Column(scale=1):
497
+ video_output_display = gr.Video(label="Final Render Preview")
498
  pipeline_status_log = gr.Textbox(label="System Operational Pipeline Logs", interactive=False)
 
 
 
 
 
 
 
499
 
500
  trigger_generation_button.click(
501
  fn=UI_interaction_bridge,
502
+ inputs=[user_prompt_input, ui_tone, ui_resolution, ui_captions, ui_audio_upload, ui_voice_selection, ui_video_probability, ui_music_level, ui_fps_target, ui_encoding_speed, ui_voice_tempo, ui_caption_font_size],
 
 
 
 
503
  outputs=[video_output_display, pipeline_status_log]
504
  )
505