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 )