testdeep123 commited on
Commit
3e3c550
Β·
verified Β·
1 Parent(s): 0212e21

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +185 -767
app.py CHANGED
@@ -1,807 +1,225 @@
1
- """
2
- Full Code: Orbit Video Engine with Dynamic Clip Editor and Video Output Download
3
-
4
- This script implements a Gradio Blocks UI that:
5
- 1. Accepts a video topic or a full script.
6
- 2. Generates a documentary-style script using an AI API.
7
- 3. Parses the script into clip data.
8
- 4. Dynamically creates an editor for each clip, where users can:
9
- - Edit the visual prompt.
10
- - Edit the TTS (narration) text.
11
- - Upload their own image/video to override the generated media.
12
- 5. Provides video settings (resolution, render speed, clip preferences, background music, subtitle settings).
13
- 6. Renders the video and displays it so you can preview it and download the final MP4 file.
14
-
15
- Make sure you have all required dependencies installed.
16
- """
17
-
18
- # ------------------- IMPORTS -------------------
19
- from kokoro import KPipeline
20
- import soundfile as sf
21
- import torch
22
- import os, time, random, math, json, tempfile, shutil, re, requests
23
- from moviepy.editor import (VideoFileClip, AudioFileClip, ImageClip, concatenate_videoclips,
24
- CompositeVideoClip, TextClip, CompositeAudioClip)
25
- import moviepy.video.fx.all as vfx
26
- import moviepy.config as mpy_config
27
- from pydub import AudioSegment
28
- from PIL import Image
29
- import numpy as np
30
- from bs4 import BeautifulSoup
31
- from urllib.parse import quote
32
- import pysrt
33
- from gtts import gTTS
34
- import cv2
35
  import gradio as gr
 
 
 
 
36
 
37
- # ------------------- GLOBAL CONFIGURATION -------------------
38
- pipeline = KPipeline(lang_code='a') # American English voice (uses 'af_heart')
39
- mpy_config.change_settings({"IMAGEMAGICK_BINARY": "/usr/bin/convert"})
40
-
41
- PEXELS_API_KEY = 'BhJqbcdm9Vi90KqzXKAhnEHGsuFNv4irXuOjWtT761U49lRzo03qBGna'
42
- OPENROUTER_API_KEY = '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'
43
- OPENROUTER_MODEL = "mistralai/mistral-small-3.1-24b-instruct:free"
44
- OUTPUT_VIDEO_FILENAME = "final_video.mp4"
45
- USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/91.0.4472.124 Safari/537.36"
46
-
47
- # These globals will be set for each run
48
- TARGET_RESOLUTION = None
49
- CAPTION_COLOR = None
50
- TEMP_FOLDER = None
51
-
52
- # ------------------- HELPER FUNCTIONS -------------------
53
-
54
- def generate_script(user_input):
55
- """Generate a documentary script using the OpenRouter API."""
56
- headers = {
57
- 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
58
- 'HTTP-Referer': 'https://your-domain.com',
59
- 'X-Title': 'AI Documentary Maker'
60
- }
61
- prompt = f"""Short Documentary Script GeneratorInstructions:
62
-
63
- If I say "use this," just output the script exactly as I gave it.
64
- If I only give topics, generate a script based on them.
65
- 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
66
- 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
67
- Formatting Rules:
68
-
69
- Title in Square Brackets:
70
-
71
- Each section starts with a one-word title inside [ ] (max two words if necessary).
72
- This title will be used as a search term for Pexels footage.
73
-
74
- Casual & Funny Narration:
75
-
76
- Each section has 5-10 words of narration.
77
- Keep it natural, funny, and unpredictable (not robotic, poetic, or rhythmic).
78
-
79
- No Special Formatting:
80
-
81
- No bold, italics, or special characters. You are an assistant AI your task is to create script. You aren't a chatbot. So, don't write extra text
82
-
83
- Generalized Search Terms:
84
-
85
- If a term is too specific, make it more general for Pexels search.
86
-
87
- Scene-Specific Writing:
88
-
89
- Each section describes only what should be shown in the video.
90
-
91
- 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 .....
92
 
93
- No extra text, just the script.
94
-
95
- Example Output:
96
- [North Korea]
97
- Top 5 unknown facts about North Korea.
98
- [Invisibility]
99
- North Korea’s internet speed is so fast… it doesn’t exist.
100
- [Leadership]
101
- Kim Jong-un once won an election with 100% votes… against himself.
102
- [Magic]
103
- North Korea discovered time travel. That’s why their news is always from the past.
104
- [Warning]
105
- Subscribe now, or Kim Jong-un will send you a free one-way ticket… to North Korea.
106
- [Freedom]
107
- North Korean citizens can do anything… as long as it's government-approved.
108
- Now here is the Topic/scrip: {user_input}
109
- """
110
- data = {
111
- 'model': OPENROUTER_MODEL,
112
- 'messages': [{'role': 'user', 'content': prompt}],
113
- 'temperature': 0.4,
114
- 'max_tokens': 5000
115
- }
116
- try:
117
- response = requests.post(
118
- 'https://openrouter.ai/api/v1/chat/completions',
119
- headers=headers,
120
- json=data,
121
- timeout=30
122
- )
123
- if response.status_code == 200:
124
- response_data = response.json()
125
- if 'choices' in response_data and len(response_data['choices']) > 0:
126
- return response_data['choices'][0]['message']['content']
127
- else:
128
- print("Unexpected response format:", response_data)
129
- return None
130
- else:
131
- print(f"API Error {response.status_code}: {response.text}")
132
- return None
133
- except Exception as e:
134
- print(f"Request failed: {str(e)}")
135
- return None
136
-
137
- def parse_script(script_text):
138
- """
139
- Parse the generated script into a list of elements.
140
- Each clip (segment) is represented as a pair of elements:
141
- - A media element with a 'prompt'
142
- - A TTS element with narration text and duration
143
- """
144
- sections = {}
145
- current_title = None
146
- current_text = ""
147
- try:
148
- for line in script_text.splitlines():
149
- line = line.strip()
150
- if line.startswith("[") and "]" in line:
151
- bracket_start = line.find("[")
152
- bracket_end = line.find("]", bracket_start)
153
- if bracket_start != -1 and bracket_end != -1:
154
- if current_title is not None:
155
- sections[current_title] = current_text.strip()
156
- current_title = line[bracket_start+1:bracket_end]
157
- current_text = line[bracket_end+1:].strip()
158
- elif current_title:
159
- current_text += line + " "
160
- if current_title:
161
- sections[current_title] = current_text.strip()
162
- elements = []
163
- for title, narration in sections.items():
164
- if not title or not narration:
165
- continue
166
- media_element = {"type": "media", "prompt": title, "effects": "fade-in"}
167
- words = narration.split()
168
- duration = max(3, len(words) * 0.5)
169
- tts_element = {"type": "tts", "text": narration, "voice": "en", "duration": duration}
170
- elements.append(media_element)
171
- elements.append(tts_element)
172
- return elements
173
- except Exception as e:
174
- print(f"Error parsing script: {e}")
175
- return []
176
-
177
- def search_pexels_videos(query, pexels_api_key):
178
- """Search for a video on Pexels by query and return a random HD video link."""
179
- headers = {'Authorization': pexels_api_key}
180
- base_url = "https://api.pexels.com/videos/search"
181
- num_pages = 3
182
- videos_per_page = 15
183
- max_retries = 3
184
- retry_delay = 1
185
- search_query = query
186
- all_videos = []
187
- for page in range(1, num_pages + 1):
188
- for attempt in range(max_retries):
189
- try:
190
- params = {"query": search_query, "per_page": videos_per_page, "page": page}
191
- response = requests.get(base_url, headers=headers, params=params, timeout=10)
192
- if response.status_code == 200:
193
- data = response.json()
194
- videos = data.get("videos", [])
195
- if not videos:
196
- print(f"No videos found on page {page}.")
197
- break
198
- for video in videos:
199
- video_files = video.get("video_files", [])
200
- for file in video_files:
201
- if file.get("quality") == "hd":
202
- all_videos.append(file.get("link"))
203
- break
204
- break
205
- elif response.status_code == 429:
206
- print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying...")
207
- time.sleep(retry_delay)
208
- retry_delay *= 2
209
- else:
210
- print(f"Error fetching videos: {response.status_code} {response.text}")
211
- if attempt < max_retries - 1:
212
- time.sleep(retry_delay)
213
- retry_delay *= 2
214
- else:
215
- break
216
- except requests.exceptions.RequestException as e:
217
- print(f"Request exception: {e}")
218
- if attempt < max_retries - 1:
219
- time.sleep(retry_delay)
220
- retry_delay *= 2
221
- else:
222
- break
223
- if all_videos:
224
- random_video = random.choice(all_videos)
225
- print(f"Selected random video from {len(all_videos)} HD videos")
226
- return random_video
227
- else:
228
- print("No suitable videos found.")
229
- return None
230
-
231
- def search_pexels_images(query, pexels_api_key):
232
- """Search for an image on Pexels by query."""
233
- headers = {'Authorization': pexels_api_key}
234
- url = "https://api.pexels.com/v1/search"
235
- params = {"query": query, "per_page": 5, "orientation": "landscape"}
236
- max_retries = 3
237
- retry_delay = 1
238
- for attempt in range(max_retries):
239
- try:
240
- response = requests.get(url, headers=headers, params=params, timeout=10)
241
- if response.status_code == 200:
242
- data = response.json()
243
- photos = data.get("photos", [])
244
- if photos:
245
- photo = random.choice(photos[:min(5, len(photos))])
246
- img_url = photo.get("src", {}).get("original")
247
- return img_url
248
- else:
249
- print(f"No images found for query: {query}")
250
- return None
251
- elif response.status_code == 429:
252
- print(f"Rate limit hit (attempt {attempt+1}/{max_retries}). Retrying...")
253
- time.sleep(retry_delay)
254
- retry_delay *= 2
255
- else:
256
- print(f"Error fetching images: {response.status_code} {response.text}")
257
- if attempt < max_retries - 1:
258
- time.sleep(retry_delay)
259
- retry_delay *= 2
260
- except requests.exceptions.RequestException as e:
261
- print(f"Request exception: {e}")
262
- if attempt < max_retries - 1:
263
- time.sleep(retry_delay)
264
- retry_delay *= 2
265
- print(f"No Pexels images found for query: {query}")
266
- return None
267
-
268
- def search_google_images(query):
269
- """Search for images on Google Images (useful for news-like queries)."""
270
- try:
271
- search_url = f"https://www.google.com/search?q={quote(query)}&tbm=isch"
272
- headers = {"User-Agent": USER_AGENT}
273
- response = requests.get(search_url, headers=headers, timeout=10)
274
- soup = BeautifulSoup(response.text, "html.parser")
275
- img_tags = soup.find_all("img")
276
- image_urls = []
277
- for img in img_tags:
278
- src = img.get("src", "")
279
- if src.startswith("http") and "gstatic" not in src:
280
- image_urls.append(src)
281
- if image_urls:
282
- return random.choice(image_urls[:5]) if len(image_urls) >= 5 else image_urls[0]
283
- else:
284
- print(f"No Google Images found for query: {query}")
285
- return None
286
- except Exception as e:
287
- print(f"Error in Google Images search: {e}")
288
- return None
289
-
290
- def download_image(image_url, filename):
291
- """Download an image from a URL to a local file and verify it."""
292
- try:
293
- headers = {"User-Agent": USER_AGENT}
294
- print(f"Downloading image from: {image_url}")
295
- response = requests.get(image_url, headers=headers, stream=True, timeout=15)
296
- response.raise_for_status()
297
- with open(filename, 'wb') as f:
298
- for chunk in response.iter_content(chunk_size=8192):
299
- f.write(chunk)
300
- print(f"Image downloaded to: {filename}")
301
- try:
302
- img = Image.open(filename)
303
- img.verify()
304
- img = Image.open(filename)
305
- if img.mode != 'RGB':
306
- img = img.convert('RGB')
307
- img.save(filename)
308
- return filename
309
- except Exception as e_validate:
310
- print(f"Invalid image file: {e_validate}")
311
- if os.path.exists(filename):
312
- os.remove(filename)
313
- return None
314
- except Exception as e:
315
- print(f"Image download error: {e}")
316
- if os.path.exists(filename):
317
- os.remove(filename)
318
- return None
319
-
320
- def download_video(video_url, filename):
321
- """Download a video from a URL to a local file."""
322
- try:
323
- response = requests.get(video_url, stream=True, timeout=30)
324
- response.raise_for_status()
325
- with open(filename, 'wb') as f:
326
- for chunk in response.iter_content(chunk_size=8192):
327
- f.write(chunk)
328
- return filename
329
- except Exception as e:
330
- print(f"Video download error: {e}")
331
- if os.path.exists(filename):
332
- os.remove(filename)
333
- return None
334
-
335
- def generate_media(prompt, user_image=None, current_index=0, total_segments=1):
336
- """
337
- Generate a visual asset for the clip.
338
- If a user_image (custom media) is provided, it is used directly.
339
- Otherwise, use Pexels (or Google Images for news queries) with fallbacks.
340
- """
341
- safe_prompt = re.sub(r'[^\w\s-]', '', prompt).strip().replace(' ', '_')
342
- if user_image is not None:
343
- print(f"Using custom media for prompt: {prompt}")
344
- return {"path": user_image, "asset_type": "image"}
345
- if "news" in prompt.lower():
346
- image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_news.jpg")
347
- image_url = search_google_images(prompt)
348
- if image_url:
349
- downloaded_image = download_image(image_url, image_file)
350
- if downloaded_image:
351
- return {"path": downloaded_image, "asset_type": "image"}
352
- if random.random() < 0.25:
353
- video_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}_video.mp4")
354
- video_url = search_pexels_videos(prompt, PEXELS_API_KEY)
355
- if video_url:
356
- downloaded_video = download_video(video_url, video_file)
357
- if downloaded_video:
358
- return {"path": downloaded_video, "asset_type": "video"}
359
- image_file = os.path.join(TEMP_FOLDER, f"{safe_prompt}.jpg")
360
- image_url = search_pexels_images(prompt, PEXELS_API_KEY)
361
- if image_url:
362
- downloaded_image = download_image(image_url, image_file)
363
- if downloaded_image:
364
- return {"path": downloaded_image, "asset_type": "image"}
365
- fallback_terms = ["nature", "people", "landscape", "technology", "business"]
366
- for term in fallback_terms:
367
- fallback_file = os.path.join(TEMP_FOLDER, f"fallback_{term}.jpg")
368
- fallback_url = search_pexels_images(term, PEXELS_API_KEY)
369
- if fallback_url:
370
- downloaded_fallback = download_image(fallback_url, fallback_file)
371
- if downloaded_fallback:
372
- return {"path": downloaded_fallback, "asset_type": "image"}
373
- print(f"Failed to generate asset for prompt: {prompt}")
374
- return None
375
-
376
- def generate_silent_audio(duration, sample_rate=24000):
377
- """Generate a silent WAV audio file of given duration."""
378
- num_samples = int(duration * sample_rate)
379
- silence = np.zeros(num_samples, dtype=np.float32)
380
- silent_path = os.path.join(TEMP_FOLDER, f"silent_{int(time.time())}.wav")
381
- sf.write(silent_path, silence, sample_rate)
382
- return silent_path
383
-
384
- def generate_tts(text, voice):
385
- """Generate TTS audio using Kokoro with fallback to gTTS."""
386
- safe_text = re.sub(r'[^\w\s-]', '', text[:10]).strip().replace(' ', '_')
387
- file_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.wav")
388
- if os.path.exists(file_path):
389
- return file_path
390
- try:
391
- kokoro_voice = 'af_heart' if voice == 'en' else voice
392
- generator = pipeline(text, voice=kokoro_voice, speed=0.9, split_pattern=r'\n+')
393
- audio_segments = []
394
- for i, (gs, ps, audio) in enumerate(generator):
395
- audio_segments.append(audio)
396
- full_audio = np.concatenate(audio_segments) if len(audio_segments) > 1 else audio_segments[0]
397
- sf.write(file_path, full_audio, 24000)
398
- return file_path
399
- except Exception as e:
400
- try:
401
- tts = gTTS(text=text, lang='en')
402
- mp3_path = os.path.join(TEMP_FOLDER, f"tts_{safe_text}.mp3")
403
- tts.save(mp3_path)
404
- audio = AudioSegment.from_mp3(mp3_path)
405
- audio.export(file_path, format="wav")
406
- os.remove(mp3_path)
407
- return file_path
408
- except Exception as fallback_error:
409
- return generate_silent_audio(duration=max(3, len(text.split()) * 0.5))
410
-
411
- def apply_kenburns_effect(clip, target_resolution, effect_type=None):
412
- """Apply a Ken Burns effect to the clip (used for images)."""
413
- target_w, target_h = target_resolution
414
- clip_aspect = clip.w / clip.h
415
- target_aspect = target_w / target_h
416
- if clip_aspect > target_aspect:
417
- new_height = target_h
418
- new_width = int(new_height * clip_aspect)
419
- else:
420
- new_width = target_w
421
- new_height = int(new_width / clip_aspect)
422
- clip = clip.resize(newsize=(new_width, new_height))
423
- base_scale = 1.15
424
- new_width = int(new_width * base_scale)
425
- new_height = int(new_height * base_scale)
426
- clip = clip.resize(newsize=(new_width, new_height))
427
- max_offset_x = new_width - target_w
428
- max_offset_y = new_height - target_h
429
- available_effects = ["zoom-in", "zoom-out", "pan-left", "pan-right", "up-left"]
430
- if effect_type is None or effect_type == "random":
431
- effect_type = random.choice(available_effects)
432
- if effect_type == "zoom-in":
433
- start_zoom = 0.9; end_zoom = 1.1; start_center = (new_width/2, new_height/2); end_center = start_center
434
- elif effect_type == "zoom-out":
435
- start_zoom = 1.1; end_zoom = 0.9; start_center = (new_width/2, new_height/2); end_center = start_center
436
- elif effect_type == "pan-left":
437
- start_zoom = 1.0; end_zoom = 1.0; start_center = (max_offset_x+target_w/2, (max_offset_y//2)+target_h/2); end_center = (target_w/2, (max_offset_y//2)+target_h/2)
438
- elif effect_type == "pan-right":
439
- start_zoom = 1.0; end_zoom = 1.0; start_center = (target_w/2, (max_offset_y//2)+target_h/2); end_center = (max_offset_x+target_w/2, (max_offset_y//2)+target_h/2)
440
- elif effect_type == "up-left":
441
- start_zoom = 1.0; end_zoom = 1.0; start_center = (max_offset_x+target_w/2, max_offset_y+target_h/2); end_center = (target_w/2, target_h/2)
442
- else:
443
- raise ValueError(f"Unsupported effect: {effect_type}")
444
- def transform_frame(get_frame, t):
445
- frame = get_frame(t)
446
- ratio = t / clip.duration if clip.duration > 0 else 0
447
- ratio = 0.5 - 0.5 * math.cos(math.pi * ratio)
448
- current_zoom = start_zoom + (end_zoom - start_zoom) * ratio
449
- crop_w = int(target_w / current_zoom)
450
- crop_h = int(target_h / current_zoom)
451
- current_center_x = start_center[0] + (end_center[0] - start_center[0]) * ratio
452
- current_center_y = start_center[1] + (end_center[1] - start_center[1]) * ratio
453
- min_center_x = crop_w / 2; max_center_x = new_width - crop_w / 2
454
- min_center_y = crop_h / 2; max_center_y = new_height - crop_h / 2
455
- current_center_x = max(min_center_x, min(current_center_x, max_center_x))
456
- current_center_y = max(min_center_y, min(current_center_y, max_center_y))
457
- cropped_frame = cv2.getRectSubPix(frame, (crop_w, crop_h), (current_center_x, current_center_y))
458
- resized_frame = cv2.resize(cropped_frame, (target_w, target_h), interpolation=cv2.INTER_LANCZOS4)
459
- return resized_frame
460
- return clip.fl(transform_frame)
461
 
462
- def resize_to_fill(clip, target_resolution):
463
- """Resize and crop a clip to fill the target resolution."""
464
- target_w, target_h = target_resolution
465
- clip_aspect = clip.w / clip.h
466
- target_aspect = target_w / target_h
467
- if clip_aspect > target_aspect:
468
- clip = clip.resize(height=target_h)
469
- crop_amount = (clip.w - target_w) / 2
470
- clip = clip.crop(x1=crop_amount, x2=clip.w - crop_amount, y1=0, y2=clip.h)
471
  else:
472
- clip = clip.resize(width=target_w)
473
- crop_amount = (clip.h - target_h) / 2
474
- clip = clip.crop(x1=0, x2=clip.w, y1=crop_amount, y2=clip.h - crop_amount)
475
- return clip
476
-
477
- def find_mp3_files():
478
- """Search for an MP3 file in the current directory (for background music)."""
479
- mp3_files = []
480
- for root, dirs, files in os.walk('.'):
481
- for file in files:
482
- if file.endswith('.mp3'):
483
- mp3_files.append(os.path.join(root, file))
484
- return mp3_files[0] if mp3_files else None
485
-
486
- def add_background_music(final_video, bg_music_volume=0.08):
487
- """Add background music to the final video if an MP3 file is found."""
488
- try:
489
- bg_music_path = find_mp3_files()
490
- if bg_music_path and os.path.exists(bg_music_path):
491
- bg_music = AudioFileClip(bg_music_path)
492
- if bg_music.duration < final_video.duration:
493
- loops_needed = math.ceil(final_video.duration / bg_music.duration)
494
- bg_segments = [bg_music] * loops_needed
495
- bg_music = concatenate_videoclips(bg_segments)
496
- bg_music = bg_music.subclip(0, final_video.duration)
497
- bg_music = bg_music.volumex(bg_music_volume)
498
- video_audio = final_video.audio
499
- mixed_audio = CompositeAudioClip([video_audio, bg_music])
500
- final_video = final_video.set_audio(mixed_audio)
501
- return final_video
502
- except Exception as e:
503
- print(f"Error adding background music: {e}")
504
- return final_video
505
-
506
- def create_clip(media_path, asset_type, tts_path, duration=None, effects=None, narration_text=None, segment_index=0):
507
- """Create a video clip from a media asset and TTS audio, and add subtitles if enabled."""
508
- try:
509
- if not os.path.exists(media_path) or not os.path.exists(tts_path):
510
- return None
511
- audio_clip = AudioFileClip(tts_path).audio_fadeout(0.2)
512
- audio_duration = audio_clip.duration
513
- target_duration = audio_duration + 0.2
514
- if asset_type == "video":
515
- clip = VideoFileClip(media_path)
516
- clip = resize_to_fill(clip, TARGET_RESOLUTION)
517
- if clip.duration < target_duration:
518
- clip = clip.loop(duration=target_duration)
519
- else:
520
- clip = clip.subclip(0, target_duration)
521
- elif asset_type == "image":
522
- img = Image.open(media_path)
523
- if img.mode != 'RGB':
524
- with tempfile.NamedTemporaryFile(suffix='.jpg', delete=False) as temp:
525
- img.convert('RGB').save(temp.name)
526
- media_path = temp.name
527
- img.close()
528
- clip = ImageClip(media_path).set_duration(target_duration)
529
- clip = apply_kenburns_effect(clip, TARGET_RESOLUTION)
530
- clip = clip.fadein(0.3).fadeout(0.3)
531
  else:
532
- return None
533
- if narration_text and CAPTION_COLOR != "transparent":
534
- try:
535
- words = narration_text.split()
536
- chunks = []
537
- current_chunk = []
538
- for word in words:
539
- current_chunk.append(word)
540
- if len(current_chunk) >= 5:
541
- chunks.append(' '.join(current_chunk))
542
- current_chunk = []
543
- if current_chunk:
544
- chunks.append(' '.join(current_chunk))
545
- chunk_duration = audio_duration / len(chunks)
546
- subtitle_clips = []
547
- subtitle_y_position = int(TARGET_RESOLUTION[1] * 0.70)
548
- for i, chunk_text in enumerate(chunks):
549
- start_time = i * chunk_duration
550
- end_time = (i + 1) * chunk_duration
551
- txt_clip = TextClip(
552
- chunk_text,
553
- fontsize=45,
554
- font='Arial-Bold',
555
- color=CAPTION_COLOR,
556
- bg_color='rgba(0, 0, 0, 0.25)',
557
- method='caption',
558
- align='center',
559
- stroke_width=2,
560
- stroke_color=CAPTION_COLOR,
561
- size=(TARGET_RESOLUTION[0] * 0.8, None)
562
- ).set_start(start_time).set_end(end_time)
563
- txt_clip = txt_clip.set_position(('center', subtitle_y_position))
564
- subtitle_clips.append(txt_clip)
565
- clip = CompositeVideoClip([clip] + subtitle_clips)
566
- except Exception as sub_error:
567
- txt_clip = TextClip(
568
- narration_text,
569
- fontsize=28,
570
- color=CAPTION_COLOR,
571
- align='center',
572
- size=(TARGET_RESOLUTION[0] * 0.7, None)
573
- ).set_position(('center', int(TARGET_RESOLUTION[1] / 3))).set_duration(clip.duration)
574
- clip = CompositeVideoClip([clip, txt_clip])
575
- clip = clip.set_audio(audio_clip)
576
- return clip
577
- except Exception as e:
578
- print(f"Error in create_clip: {str(e)}")
579
- return None
580
-
581
- def fix_imagemagick_policy():
582
- """Attempt to modify ImageMagick security policies if needed."""
583
- try:
584
- policy_paths = [
585
- "/etc/ImageMagick-6/policy.xml",
586
- "/etc/ImageMagick-7/policy.xml",
587
- "/etc/ImageMagick/policy.xml",
588
- "/usr/local/etc/ImageMagick-7/policy.xml"
589
- ]
590
- found_policy = next((path for path in policy_paths if os.path.exists(path)), None)
591
- if not found_policy:
592
- return False
593
- os.system(f"sudo cp {found_policy} {found_policy}.bak")
594
- os.system(f"sudo sed -i 's/rights=\"none\"/rights=\"read|write\"/g' {found_policy}")
595
- os.system(f"sudo sed -i 's/<policy domain=\"path\" pattern=\"@\\*\"[^>]*>/<policy domain=\"path\" pattern=\"@*\" rights=\"read|write\"/g' {found_policy}")
596
- os.system(f"sudo sed -i 's/<policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"[^>]*>/<!-- <policy domain=\"coder\" rights=\"none\" pattern=\"PDF\"> -->/g' {found_policy}")
597
- return True
598
- except Exception as e:
599
- return False
600
-
601
- def generate_video(user_input, resolution, caption_option):
602
- """
603
- Original video generation function.
604
- This version uses the generated script to create video clips.
605
- (Integration with custom clip edits could be extended further.)
606
- """
607
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
608
- if resolution == "Full HD (1920x1080)":
609
- TARGET_RESOLUTION = (1920, 1080)
610
- elif resolution == "Short (1080x1920)":
611
- TARGET_RESOLUTION = (1080, 1920)
612
- else:
613
- TARGET_RESOLUTION = (1920, 1080)
614
- CAPTION_COLOR = "white" if caption_option == "Yes" else "transparent"
 
615
  TEMP_FOLDER = tempfile.mkdtemp()
616
- fix_imagemagick_policy()
617
- print("Generating script from API...")
618
- script = generate_script(user_input)
619
- if not script:
620
- shutil.rmtree(TEMP_FOLDER)
621
- return None
622
- print("Generated Script:\n", script)
623
- elements = parse_script(script)
624
- if not elements:
625
- shutil.rmtree(TEMP_FOLDER)
626
- return None
627
- paired_elements = []
628
- for i in range(0, len(elements), 2):
629
- if i + 1 < len(elements):
630
- paired_elements.append((elements[i], elements[i + 1]))
631
- if not paired_elements:
632
- shutil.rmtree(TEMP_FOLDER)
633
- return None
634
  clips = []
635
- for idx, (media_elem, tts_elem) in enumerate(paired_elements):
636
- print(f"Processing segment {idx+1} with prompt: '{media_elem['prompt']}'")
637
- media_asset = generate_media(media_elem['prompt'])
638
- if not media_asset:
639
- continue
640
- tts_path = generate_tts(tts_elem['text'], tts_elem['voice'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  if not tts_path:
642
  continue
 
 
 
643
  clip = create_clip(
644
- media_path=media_asset['path'],
645
- asset_type=media_asset['asset_type'],
646
  tts_path=tts_path,
647
- duration=tts_elem['duration'],
648
- effects=media_elem.get('effects', 'fade-in'),
649
- narration_text=tts_elem['text'],
650
  segment_index=idx
651
  )
 
 
652
  if clip:
653
  clips.append(clip)
 
654
  if not clips:
655
  shutil.rmtree(TEMP_FOLDER)
656
- return None
657
- final_video = concatenate_videoclips(clips, method="compose")
658
- final_video = add_background_music(final_video, bg_music_volume=0.08)
659
- final_video.write_videofile(OUTPUT_VIDEO_FILENAME, codec='libx264', fps=24, preset='veryfast')
660
- shutil.rmtree(TEMP_FOLDER)
661
- return OUTPUT_VIDEO_FILENAME
662
 
663
- # ------------------- NEW CALLBACKS FOR THE ADVANCED UI -------------------
 
664
 
665
- # Global variable to store clip data from the script generation.
666
- generated_clip_data = []
 
 
 
 
 
 
667
 
668
- def generate_script_clips(topic_input, full_script):
669
- """
670
- Callback when "πŸ“ Generate Script & Load Clips" is clicked.
671
- Uses the full_script if provided; otherwise, generates based on the topic.
672
- Returns the raw generated script and a JSON string containing the clip data.
673
- """
674
- input_text = full_script if full_script.strip() != "" else topic_input
675
- script = generate_script(input_text)
676
- if not script:
677
- return "Error: failed to generate script", "{}"
678
- elements = parse_script(script)
679
- clip_list = []
680
- for i in range(0, len(elements), 2):
681
- if i + 1 < len(elements):
682
- media_elem = elements[i]
683
- tts_elem = elements[i+1]
684
- clip_info = {
685
- "prompt": media_elem.get("prompt", ""),
686
- "narration": tts_elem.get("text", ""),
687
- "custom_media": "" # initially empty; user may upload a file later
688
- }
689
- clip_list.append(clip_info)
690
- global generated_clip_data
691
- generated_clip_data = clip_list
692
- return script, json.dumps(clip_list)
693
 
694
- def update_clip_editor(clip_json):
695
- """
696
- Dynamically build a UI editor for each clip.
697
- Each editor contains:
698
- - A textbox to edit the visual prompt.
699
- - A textbox to edit the TTS text.
700
- - A File uploader to allow custom media upload.
701
- Returns a list of dynamically generated Accordions.
702
- """
703
- clip_list = json.loads(clip_json)
704
- editors = []
705
- # Create a container for all clip editors.
706
- for idx, clip in enumerate(clip_list, start=1):
707
- with gr.Accordion(label=f"Clip {idx}: {clip['prompt']}", open=(idx<=2)) as acc:
708
- prompt_box = gr.Textbox(label="Visual Prompt", value=clip["prompt"])
709
- tts_box = gr.Textbox(label="TTS Text", value=clip["narration"], lines=3)
710
- custom_media_box = gr.File(label="Upload custom image/video", file_types=[".jpg", ".png", ".mp4"])
711
- # Pack values in a dictionary and return them later (here we simply display the components).
712
- # In a full integration you could use additional state to track these values.
713
- editors.append(acc)
714
- return editors
715
 
716
- def generate_final_video(topic_input, full_script, clip_data_json, resolution, render_speed,
717
- video_clip_percent, zoom_pan, bg_music_file, bg_music_volume,
718
- subtitle_enabled, font_dropdown, font_size, outline_width,
719
- font_color, outline_color, subtitle_position):
720
- """
721
- Callback when "🎬 Generate Video" is clicked.
722
- In a full implementation you would update each clip from the dynamic editors.
723
- Here, for demonstration purposes, we simply call the original generate_video function.
724
- The final video file is returned and used both for preview and download.
725
- """
726
- print("Final settings:")
727
- print(f"Resolution: {resolution}, Render Speed: {render_speed}, Video Clip %: {video_clip_percent}, Zoom/Pan: {zoom_pan}")
728
- if bg_music_file is not None:
729
- print("Custom background music provided.")
730
- video_file = generate_video(topic_input, resolution, "Yes")
731
- # Return the file path for both the video preview and the download component.
732
- return video_file, video_file
733
 
734
- # ------------------- GRADIO BLOCKS UI -------------------
735
  with gr.Blocks(title="πŸš€ Orbit Video Engine") as demo:
 
 
 
736
  with gr.Row():
737
  # Column 1: Content Input & Script Generation
738
  with gr.Column(scale=1):
739
- gr.Markdown("## 1. Content Input")
740
- topic_input = gr.Textbox(label="Topic Input", placeholder="Enter your video topic here...")
741
- full_script = gr.Textbox(label="Or Paste Full Script", placeholder="Paste full script (using [Title] etc.)", lines=5)
742
- generate_script_btn = gr.Button("πŸ“ Generate Script & Load Clips")
743
- generated_script_disp = gr.Textbox(label="Generated Script", interactive=False, visible=False)
744
- # Hidden storage for clip JSON data
745
- clip_data_storage = gr.Textbox(visible=False)
746
- # Column 2: Dynamic Clip Editor
 
 
 
 
 
 
 
 
 
 
 
 
747
  with gr.Column(scale=1):
748
- gr.Markdown("## 2. Edit Clips")
749
- gr.Markdown("Modify each clip's Visual Prompt, TTS Text or upload your own media.")
750
- clip_editor_container = gr.Column(visible=False)
751
- # Column 3: Video Settings & Output
752
- with gr.Column(scale=1):
753
- gr.Markdown("## 3. Video Settings")
754
- resolution = gr.Radio(choices=["Short (1080x1920)", "Full HD (1920x1080)"],
755
- label="Resolution", value="Full HD (1920x1080)")
756
- render_speed = gr.Dropdown(choices=["ultrafast", "veryfast", "fast", "medium", "slow", "veryslow"],
757
- label="Render Speed", value="fast")
758
- video_clip_percent = gr.Slider(0, 100, step=5, label="Video Clip Percentage", value=25)
759
- zoom_pan = gr.Checkbox(label="Add Zoom/Pan Effect (Images)", value=True)
760
  with gr.Accordion("Background Music", open=False):
761
- bg_music_file = gr.File(label="Upload Background Music (MP3)", file_types=[".mp3"])
762
- bg_music_volume = gr.Slider(0.0, 1.0, step=0.05, label="BGM Volume", value=0.15)
 
763
  with gr.Accordion("Subtitle Settings", open=True):
764
- subtitle_enabled = gr.Checkbox(label="Enable Subtitles", value=True)
765
- font_dropdown = gr.Dropdown(choices=["Impact", "Arial", "Helvetica", "Times New Roman"],
766
- label="Font", value="Arial")
767
  font_size = gr.Number(label="Font Size", value=45)
768
  outline_width = gr.Number(label="Outline Width", value=2)
769
  font_color = gr.ColorPicker(label="Font Color", value="#FFFFFF")
770
  outline_color = gr.ColorPicker(label="Outline Color", value="#000000")
771
- subtitle_position = gr.Radio(choices=["center", "bottom", "top"], label="Subtitle Position", value="center")
772
- gr.Markdown("## 4. Output")
773
- generate_video_btn = gr.Button("🎬 Generate Video")
774
- # Two outputs: one for video preview, one for file download
775
- video_preview = gr.Video(label="Generated Video")
776
- download_video_file = gr.File(label="Download Video", interactive=False)
777
-
778
- # ------------------- INTERACTIONS -------------------
779
- # When "Generate Script & Load Clips" is clicked:
780
- generate_script_btn.click(
781
- fn=generate_script_clips,
782
- inputs=[topic_input, full_script],
783
- outputs=[generated_script_disp, clip_data_storage]
 
 
 
 
 
784
  ).then(
785
- fn=lambda script: gr.update(visible=True),
786
- inputs=[generated_script_disp],
787
- outputs=[generated_script_disp]
788
- ).then(
789
- fn=update_clip_editor,
790
- inputs=[clip_data_storage],
791
- outputs=[clip_editor_container]
792
- ).then(
793
- fn=lambda _: gr.update(visible=True),
794
- inputs=[clip_editor_container],
795
- outputs=[clip_editor_container]
796
  )
797
-
798
- # When "Generate Video" is clicked, call the video generation callback.
799
- generate_video_btn.click(
800
- fn=generate_final_video,
801
- inputs=[topic_input, full_script, clip_data_storage, resolution, render_speed,
802
- video_clip_percent, zoom_pan, bg_music_file, bg_music_volume,
803
- subtitle_enabled, font_dropdown, font_size, outline_width, font_color, outline_color, subtitle_position],
804
- outputs=[video_preview, download_video_file]
805
  )
806
 
807
- demo.launch(share=True)
 
 
1
+ # Import necessary libraries (assuming all your imports remain the same)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import gradio as gr
3
+ import os
4
+ import tempfile
5
+ import shutil
6
+ from moviepy.editor import concatenate_videoclips, CompositeVideoClip, AudioFileClip, TextClip
7
 
8
+ # Your existing helper functions (generate_script, parse_script, etc.) remain unchanged
9
+ # Ensure TEMP_FOLDER, TARGET_RESOLUTION, and CAPTION_COLOR are set within functions as needed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ # Define maximum number of clips to handle in the UI
12
+ MAX_CLIPS = 10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ def process_script(topic, script_input):
15
+ """Process the topic or script and return updates for the UI."""
16
+ if script_input.strip():
17
+ raw_script = script_input
 
 
 
 
 
18
  else:
19
+ raw_script = generate_script(topic)
20
+ if not raw_script:
21
+ return "Failed to generate script", 0, [], [], [], []
22
+
23
+ elements = parse_script(raw_script)
24
+ paired_elements = [(elements[i], elements[i + 1]) for i in range(0, len(elements) - 1, 2)]
25
+ num_clips = min(len(paired_elements), MAX_CLIPS)
26
+
27
+ # Prepare updates for clip editor
28
+ accordion_updates = []
29
+ prompt_updates = []
30
+ narration_updates = []
31
+ media_updates = []
32
+ for i in range(MAX_CLIPS):
33
+ if i < num_clips:
34
+ media_elem, tts_elem = paired_elements[i]
35
+ accordion_updates.append(gr.update(visible=True, label=f"Clip {i+1}: {media_elem['prompt'][:20]}..."))
36
+ prompt_updates.append(gr.update(value=media_elem['prompt']))
37
+ narration_updates.append(gr.update(value=tts_elem['text']))
38
+ media_updates.append(gr.update(value=None)) # Reset file upload
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  else:
40
+ accordion_updates.append(gr.update(visible=False))
41
+ prompt_updates.append(gr.update(value=""))
42
+ narration_updates.append(gr.update(value=""))
43
+ media_updates.append(gr.update(value=None))
44
+
45
+ return raw_script, num_clips, accordion_updates, prompt_updates, narration_updates, media_updates
46
+
47
+ def generate_video_full(resolution, render_speed, video_clip_percent, zoom_pan_effect,
48
+ bgm_upload, bgm_volume, subtitles_enabled, font, font_size,
49
+ outline_width, font_color, outline_color, position, num_clips,
50
+ *clip_inputs):
51
+ """Generate the video using all settings and edited clip data."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  global TARGET_RESOLUTION, CAPTION_COLOR, TEMP_FOLDER
53
+
54
+ # Set resolution
55
+ TARGET_RESOLUTION = (1080, 1920) if resolution == "Short (1080x1920)" else (1920, 1080)
56
+
57
+ # Set caption settings
58
+ CAPTION_COLOR = font_color if subtitles_enabled else "transparent"
59
+
60
+ # Create temporary folder
61
  TEMP_FOLDER = tempfile.mkdtemp()
62
+
63
+ # Parse clip inputs (visual_prompt, narration, custom_media for each clip)
64
+ clips_data = []
65
+ for i in range(num_clips):
66
+ idx = i * 3
67
+ visual_prompt = clip_inputs[idx]
68
+ narration = clip_inputs[idx + 1]
69
+ custom_media = clip_inputs[idx + 2]
70
+ clips_data.append({
71
+ 'visual_prompt': visual_prompt,
72
+ 'narration': narration,
73
+ 'custom_media': custom_media
74
+ })
75
+
76
+ # Generate clips
 
 
 
77
  clips = []
78
+ for idx, clip_data in enumerate(clips_data):
79
+ # Use custom media if provided, otherwise generate media
80
+ if clip_data['custom_media']:
81
+ media_path = clip_data['custom_media']
82
+ asset_type = 'video' if media_path.endswith(('.mp4', '.avi', '.mov')) else 'image'
83
+ else:
84
+ media_asset = generate_media(clip_data['visual_prompt'], current_index=idx, total_segments=num_clips)
85
+ if not media_asset:
86
+ continue
87
+ media_path = media_asset['path']
88
+ asset_type = media_asset['asset_type']
89
+
90
+ # Adjust video clip percentage
91
+ original_random = random.random()
92
+ adjusted_random = original_random * (video_clip_percent / 100)
93
+ if adjusted_random < (video_clip_percent / 100) and not clip_data['custom_media']:
94
+ media_asset = generate_media(clip_data['visual_prompt'], current_index=idx, total_segments=num_clips)
95
+ if media_asset and media_asset['asset_type'] == 'video':
96
+ media_path = media_asset['path']
97
+ asset_type = 'video'
98
+
99
+ # Generate TTS
100
+ tts_path = generate_tts(clip_data['narration'], 'en')
101
  if not tts_path:
102
  continue
103
+
104
+ # Create clip
105
+ duration = max(3, len(clip_data['narration'].split()) * 0.5)
106
  clip = create_clip(
107
+ media_path=media_path,
108
+ asset_type=asset_type,
109
  tts_path=tts_path,
110
+ duration=duration,
111
+ effects='fade-in',
112
+ narration_text=clip_data['narration'],
113
  segment_index=idx
114
  )
115
+ if clip and zoom_pan_effect and asset_type == 'image':
116
+ clip = apply_kenburns_effect(clip, TARGET_RESOLUTION)
117
  if clip:
118
  clips.append(clip)
119
+
120
  if not clips:
121
  shutil.rmtree(TEMP_FOLDER)
122
+ return None, None
 
 
 
 
 
123
 
124
+ # Concatenate clips
125
+ final_video = concatenate_videoclips(clips, method="compose")
126
 
127
+ # Add background music if uploaded
128
+ if bgm_upload:
129
+ bg_music = AudioFileClip(bgm_upload).volumex(bgm_volume)
130
+ if bg_music.duration < final_video.duration:
131
+ bg_music = bg_music.loop(duration=final_video.duration)
132
+ else:
133
+ bg_music = bg_music.subclip(0, final_video.duration)
134
+ final_video = final_video.set_audio(CompositeVideoClip([final_video.audio, bg_music]))
135
 
136
+ # Export video
137
+ output_path = "final_video.mp4"
138
+ final_video.write_videofile(output_path, codec='libx264', fps=24, preset=render_speed)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ # Clean up
141
+ shutil.rmtree(TEMP_FOLDER)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
+ return output_path, output_path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
+ # Gradio Blocks Interface
146
  with gr.Blocks(title="πŸš€ Orbit Video Engine") as demo:
147
+ gr.Markdown("# πŸš€ Orbit Video Engine")
148
+ gr.Markdown("Create funny documentary-style videos with ease!")
149
+
150
  with gr.Row():
151
  # Column 1: Content Input & Script Generation
152
  with gr.Column(scale=1):
153
+ gr.Markdown("### 1. Content Input")
154
+ topic_input = gr.Textbox(label="Topic", placeholder="e.g., Funny Cat Facts")
155
+ script_input = gr.Textbox(label="Or Paste Full Script", lines=10, placeholder="[Title]\nNarration...")
156
+ generate_button = gr.Button("πŸ“ Generate Script & Load Clips")
157
+ script_display = gr.Textbox(label="Generated Script", interactive=False, visible=False)
158
+
159
+ # Column 2: Clip Editor
160
+ with gr.Column(scale=2):
161
+ gr.Markdown("### 2. Edit Clips")
162
+ gr.Markdown("Modify prompts, narration, or upload custom media below.")
163
+ with gr.Column() as clip_editor:
164
+ clip_accordions = []
165
+ for i in range(MAX_CLIPS):
166
+ with gr.Accordion(f"Clip {i+1}", visible=False) as acc:
167
+ visual_prompt = gr.Textbox(label="Visual Prompt")
168
+ narration = gr.Textbox(label="Narration", lines=3)
169
+ custom_media = gr.File(label="Upload Custom Media (Image/Video)")
170
+ clip_accordions.append((acc, visual_prompt, narration, custom_media))
171
+
172
+ # Column 3: Settings & Output
173
  with gr.Column(scale=1):
174
+ gr.Markdown("### 3. Video Settings")
175
+ resolution = gr.Radio(["Short (1080x1920)", "Full HD (1920x1080)"], label="Resolution", value="Full HD (1920x1080)")
176
+ render_speed = gr.Dropdown(["ultrafast", "faster", "fast", "medium", "slow", "slower", "veryslow"], label="Render Speed", value="fast")
177
+ video_clip_percent = gr.Slider(0, 100, value=25, label="Video Clip Percentage")
178
+ zoom_pan_effect = gr.Checkbox(label="Add Zoom/Pan Effect (Images)", value=True)
179
+
 
 
 
 
 
 
180
  with gr.Accordion("Background Music", open=False):
181
+ bgm_upload = gr.Audio(label="Upload Background Music", type="filepath")
182
+ bgm_volume = gr.Slider(0.0, 1.0, value=0.15, label="BGM Volume")
183
+
184
  with gr.Accordion("Subtitle Settings", open=True):
185
+ subtitles_enabled = gr.Checkbox(label="Enable Subtitles", value=True)
186
+ font = gr.Dropdown(["Impact", "Arial", "Times New Roman"], label="Font", value="Arial")
 
187
  font_size = gr.Number(label="Font Size", value=45)
188
  outline_width = gr.Number(label="Outline Width", value=2)
189
  font_color = gr.ColorPicker(label="Font Color", value="#FFFFFF")
190
  outline_color = gr.ColorPicker(label="Outline Color", value="#000000")
191
+ position = gr.Radio(["center", "bottom", "top"], label="Position", value="bottom")
192
+
193
+ generate_video_button = gr.Button("🎬 Generate Video")
194
+
195
+ gr.Markdown("### 4. Output")
196
+ output_video = gr.Video(label="Generated Video")
197
+ download_button = gr.File(label="Download Video")
198
+
199
+ # State to track number of clips
200
+ num_clips_state = gr.State(value=0)
201
+
202
+ # Event handlers
203
+ generate_button.click(
204
+ fn=process_script,
205
+ inputs=[topic_input, script_input],
206
+ outputs=[script_display, num_clips_state] +
207
+ [comp for acc in clip_accordions for comp in [acc[0], acc[1], acc[2], acc[3]]],
208
+ _js="() => {return [document.querySelector('#topic_input textarea').value, document.querySelector('#script_input textarea').value]}"
209
  ).then(
210
+ fn=lambda x: gr.update(visible=True),
211
+ inputs=[script_display],
212
+ outputs=[script_display]
 
 
 
 
 
 
 
 
213
  )
214
+
215
+ generate_video_button.click(
216
+ fn=generate_video_full,
217
+ inputs=[resolution, render_speed, video_clip_percent, zoom_pan_effect,
218
+ bgm_upload, bgm_volume, subtitles_enabled, font, font_size,
219
+ outline_width, font_color, outline_color, position, num_clips_state] +
220
+ [comp for acc in clip_accordions for comp in acc[1:]], # visual_prompt, narration, custom_media
221
+ outputs=[output_video, download_button]
222
  )
223
 
224
+ # Launch the interface
225
+ demo.launch(share=True)