Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import os | |
| import numpy as np | |
| # MoviePy 2.x Imports | |
| from moviepy import ImageClip, AudioFileClip, CompositeVideoClip, TextClip, ColorClip, concatenate_videoclips, VideoFileClip | |
| from gtts import gTTS | |
| import tempfile | |
| from PIL import Image | |
| # --- Helper: Resolution & Bitrate --- | |
| def get_specs(ratio, quality): | |
| res_map = { | |
| "144p": (256, 144, "300k"), | |
| "240p": (426, 240, "600k"), | |
| "360p": (640, 360, "1000k"), | |
| "720p": (1280, 720, "2500k"), | |
| "1080p": (1920, 1080, "5000k") | |
| } | |
| w, h, b = res_map.get(quality, res_map["360p"]) | |
| if ratio == "9:16 (Shorts)": return h, w, b | |
| elif ratio == "1:1 (Square)": return h, h, b | |
| else: return w, h, b # 16:9 | |
| # --- TTS Function with Language --- | |
| def text_to_audio(text, lang='en'): | |
| if not text: return None | |
| try: | |
| t = tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') | |
| gTTS(text=text, lang=lang).save(t.name) | |
| return t.name | |
| except Exception as e: | |
| print(f"TTS Error: {e}") | |
| return None | |
| # --- Function 1: Create Ad --- | |
| def create_controlled_ad(bg_img, char_img, prod_img, audio_file, text_input, lang_select, ratio, quality, overlay_text, | |
| c_x, c_y, c_zoom, p_x, p_y, p_zoom): | |
| # 1. Audio Handling | |
| audio_path = audio_file | |
| if not audio_path and text_input: | |
| audio_path = text_to_audio(text_input, lang=lang_select) | |
| if not audio_path: | |
| return None, "Please provide Audio File or Text to Speak." | |
| try: | |
| W, H, bitrate = get_specs(ratio, quality) | |
| audio_clip = AudioFileClip(audio_path) | |
| duration = audio_clip.duration | |
| clips = [] | |
| # 2. Background Layer | |
| if bg_img is not None: | |
| if isinstance(bg_img, np.ndarray): | |
| bg_pil = Image.fromarray(bg_img) | |
| else: | |
| bg_pil = Image.open(bg_img) | |
| bg_resized = bg_pil.resize((W, H), Image.LANCZOS) | |
| bg_clip = ImageClip(np.array(bg_resized)).with_duration(duration) | |
| else: | |
| bg_clip = ColorClip(size=(W, H), color=(20, 20, 20)).with_duration(duration) | |
| clips.append(bg_clip) | |
| # Helper to place images | |
| def place_image(img_input, pos_x, pos_y, zoom_factor, default_size_pct): | |
| if img_input is None: return | |
| if isinstance(img_input, np.ndarray): | |
| pil_img = Image.fromarray(img_input) | |
| else: | |
| pil_img = Image.open(img_input) | |
| base_h = int(H * default_size_pct) | |
| base_w = int(base_h * (pil_img.width / pil_img.height)) | |
| final_h = int(base_h * zoom_factor) | |
| final_w = int(base_w * zoom_factor) | |
| if final_w <= 0: final_w = 1 | |
| if final_h <= 0: final_h = 1 | |
| resized_pil = pil_img.resize((final_w, final_h), Image.LANCZOS) | |
| clip = ImageClip(np.array(resized_pil)).with_duration(duration) | |
| center_x = (W - final_w) // 2 | |
| center_y = (H - final_h) // 2 | |
| offset_x = int((pos_x / 100) * W) | |
| offset_y = int((pos_y / 100) * H) | |
| final_pos = (center_x + offset_x, center_y + offset_y) | |
| clips.append(clip.with_position(final_pos)) | |
| # 3. Add Character & Product | |
| place_image(char_img, c_x, c_y, c_zoom, 0.50) | |
| place_image(prod_img, p_x, p_y, p_zoom, 0.30) | |
| # 4. Add Text Overlay (FIXED for MoviePy 2.x) | |
| if overlay_text and len(str(overlay_text).strip()) > 0: | |
| fontsize = max(12, int(W * 0.05)) | |
| try: | |
| # Try simple label first | |
| txt_clip = TextClip( | |
| str(overlay_text), | |
| font_size=fontsize, | |
| color='white', | |
| stroke_color='black', | |
| stroke_width=2, | |
| font='Arial-Bold' | |
| ) | |
| # Wrap text if too wide | |
| if txt_clip.w > W - 40: | |
| txt_clip = TextClip( | |
| str(overlay_text), | |
| font_size=fontsize, | |
| color='white', | |
| stroke_color='black', | |
| stroke_width=2, | |
| size=(W-40, None), | |
| method='caption' | |
| ) | |
| txt_y = H - txt_clip.h - 20 | |
| clips.append(txt_clip.with_position(('center', txt_y)).with_duration(duration)) | |
| except Exception as text_err: | |
| print(f"Text Clip Error: {text_err}") | |
| pass | |
| # 5. Render Video | |
| final_video = CompositeVideoClip(clips, size=(W, H)).with_audio(audio_clip) | |
| output_file = "ad_part.mp4" | |
| final_video.write_videofile( | |
| output_file, | |
| fps=24, | |
| codec='libx264', | |
| audio_codec='aac', | |
| preset='ultrafast', | |
| threads=4, | |
| logger=None | |
| ) | |
| final_video.close() | |
| audio_clip.close() | |
| if not audio_file and audio_path and os.path.exists(audio_path): | |
| os.remove(audio_path) | |
| return output_file, f"Part Created! Use Merge Tab to join more." | |
| except Exception as e: | |
| import traceback | |
| return None, f"Error: {str(e)}\n{traceback.format_exc()}" | |
| # --- Function 2: Merge Videos --- | |
| def merge_videos(video_list): | |
| if not video_list or len(video_list) < 2: | |
| return None, "Please upload at least 2 videos to merge." | |
| clips = [] | |
| try: | |
| for video_file in video_list: | |
| clips.append(VideoFileClip(str(video_file))) | |
| final_clip = concatenate_videoclips(clips, method="compose") | |
| output_file = "merged_final.mp4" | |
| final_clip.write_videofile( | |
| output_file, | |
| fps=24, | |
| codec='libx264', | |
| audio_codec='aac', | |
| preset='ultrafast', | |
| threads=4, | |
| logger=None | |
| ) | |
| for c in clips: c.close() | |
| return output_file, "Videos Merged Successfully!" | |
| except Exception as e: | |
| for c in clips: | |
| try: c.close() | |
| except: pass | |
| return None, f"Merge Error: {str(e)}" | |
| # --- UI Design (Gradio 6 Compatible) --- | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# 🎛️ Pro Ad Maker & Merger (Final)") | |
| with gr.Tabs(): | |
| # TAB 1: CREATE AD | |
| with gr.TabItem("1. Create Ad Part"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| bg_in = gr.Image(label="1. Background", type="numpy") | |
| char_in = gr.Image(label="2. Character", type="numpy") | |
| prod_in = gr.Image(label="3. Product", type="numpy") | |
| with gr.Accordion("Settings", open=True): | |
| ratio_in = gr.Radio(["16:9", "9:16", "1:1"], value="9:16", label="Ratio") | |
| qual_in = gr.Radio(["144p", "360p", "720p", "1080p"], value="360p", label="Quality") | |
| lang_in = gr.Dropdown(choices=[("English", "en"), ("Hindi", "hi")], value="hi", label="Language") | |
| txt_in = gr.Textbox( | |
| lines=2, | |
| placeholder="Script... (Copy-Paste allowed)", | |
| label="Text to Speech" | |
| ) | |
| aud_in = gr.Audio(label="OR Upload Audio") | |
| gen_btn = gr.Button("🎥 Generate Part", variant="primary") | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 🕹️ Adjust Position") | |
| with gr.Tab("Character"): | |
| c_zoom = gr.Slider(0.1, 2.0, value=1.0, label="Zoom") | |
| c_x = gr.Slider(-50, 50, value=-20, label="Horiz (Left/Right)") | |
| c_y = gr.Slider(-50, 50, value=10, label="Vert (Up/Down)") | |
| with gr.Tab("Product"): | |
| p_zoom = gr.Slider(0.1, 2.0, value=1.0, label="Zoom") | |
| p_x = gr.Slider(-50, 50, value=20, label="Horiz (Left/Right)") | |
| p_y = gr.Slider(-50, 50, value=0, label="Vert (Up/Down)") | |
| vid_out_1 = gr.Video(label="Generated Part") | |
| status_1 = gr.Textbox(label="Status") | |
| gen_btn.click( | |
| fn=create_controlled_ad, | |
| inputs=[bg_in, char_in, prod_in, aud_in, txt_in, lang_in, ratio_in, qual_in, txt_in, c_x, c_y, c_zoom, p_x, p_y, p_zoom], | |
| outputs=[vid_out_1, status_1] | |
| ) | |
| # TAB 2: MERGE VIDEOS | |
| with gr.TabItem("2. Merge Parts"): | |
| gr.Markdown("Upload multiple generated parts to join them into one long video.") | |
| with gr.Row(): | |
| with gr.Column(): | |
| files_in = gr.File(file_count="multiple", label="Select Video Parts") | |
| merge_btn = gr.Button("🔗 Merge Videos", variant="primary") | |
| with gr.Column(): | |
| vid_out_2 = gr.Video(label="Final Long Video") | |
| status_2 = gr.Textbox(label="Status") | |
| merge_btn.click(fn=merge_videos, inputs=[files_in], outputs=[vid_out_2, status_2]) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| theme=gr.themes.Soft(), | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=True | |
| ) |