import gradio as gr import atexit from typing import List, Tuple, Optional, Dict, Any from src.motion_processor import MotionProcessor from src.file_handler import FileHandler from src.video_processor import VideoProcessor from src.utils import estimate_output # Init motion = MotionProcessor() files = FileHandler() processor = VideoProcessor(motion, files) atexit.register(files.cleanup) def preview_files(uploaded_files: Optional[List[Any]]) -> Tuple[Optional[str], str]: if not uploaded_files: return None, "No files selected" file_list, info = files.files_info(uploaded_files) if not file_list: return None, "No valid media files found" preview = files.preview(file_list) # Info display parts = [ f"📊 Total: {info['total']} files ({info['size_mb']} MB)", f"đŸ–ŧī¸ Images: {info['images']} | đŸŽĨ Videos: {info['videos']}" ] if info['details']: est = estimate_output(file_list) parts.append(f"âąī¸ Est. duration: {est['duration_fmt']}") parts.append(f"đŸ“Ļ Est. size: ~{est['size_mb']} MB") return preview, "\n".join(parts) def template_choices() -> List[str]: return [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in motion.templates] def filter_by_tag(tag: str) -> gr.Dropdown: if tag == "all": filtered = motion.templates else: filtered = motion.by_tag(tag) choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in filtered] return gr.Dropdown(choices=choices, value=choices[0] if choices else None) def search_templates(q: str) -> gr.Dropdown: if not q.strip(): choices = template_choices() else: filtered = motion.search(q) choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in filtered] return gr.Dropdown(choices=choices, value=choices[0] if choices else None) def create_video(input_files: Optional[List[Any]], aspect: str, fmt: str, use_random: bool, selected: Optional[str], quality: str, progress: gr.Progress = gr.Progress()) -> Tuple[Optional[str], str, gr.DownloadButton]: def progress_fn(val: float, desc: str) -> None: progress(val, desc=desc) if not input_files: return None, "❌ No files provided", gr.DownloadButton(visible=False) # Template template = None if not use_random and selected: name = selected.split(" - ")[0] template = next((t for t in motion.templates if t['name'] == name), None) result, status = processor.create(input_files, aspect, fmt, progress_fn, template, quality) if result: return result, f"✅ {status}", gr.DownloadButton(label="đŸ“Ĩ Download", value=result, visible=True) return None, f"❌ {status}", gr.DownloadButton(visible=False) def load_template(choice: Optional[str]) -> Tuple: if not choice: return "", 4, 1.0, 1.3, 0, 0, 0, 0, "", "", 0, 0 name = choice.split(" - ")[0] t = next((t for t in motion.templates if t['name'] == name), motion.templates[0]) return ( t['name'], t['duration'], t['scale'][0], t['scale'][1], t['pan'][0], t['pan'][1], t['pan'][2], t['pan'][3], ', '.join(t.get('tags', [])), t.get('desc', ''), t['rotate'][0], t['rotate'][1] ) def save_template(name: str, dur: int, s1: float, s2: float, x1: float, y1: float, x2: float, y2: float, tags: str, desc: str, r1: float, r2: float) -> Tuple[str, gr.Dropdown]: if not name: return "❌ Enter name", gr.Dropdown() tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] if tags else [] template = { 'name': name, 'duration': dur, 'scale': [s1, s2], 'pan': [x1, y1, x2, y2], 'rotate': [r1, r2], 'tags': tag_list, 'desc': desc or f"Custom: {name}" } existing = next((i for i, t in enumerate(motion.templates) if t['name'] == name), None) if existing is not None: motion.templates[existing] = template else: motion.templates.append(template) motion.save(motion.templates) choices = template_choices() return f"✅ Saved '{name}'!", gr.Dropdown(choices=choices, value=f"{name} - {desc[:50]}...") # Tags all_tags = set() for t in motion.templates: all_tags.update(t.get('tags', [])) tag_choices = ["all"] + sorted(list(all_tags)) # UI with gr.Blocks(title="Ken Burns Creator", theme=gr.themes.Soft()) as app: gr.Markdown("# đŸŽŦ Ken Burns Story Creator") gr.Markdown("Create motion videos for social media with 35+ effects") with gr.Tabs(): with gr.Tab("📹 Create"): with gr.Row(): with gr.Column(scale=2): gr.Markdown("### 📁 Upload Media") files_input = gr.File( label="Images/Videos", file_count="multiple", file_types=["image", "video"], height=120 ) with gr.Group(): preview_img = gr.Image(label="Preview", height=150, show_label=False) preview_info = gr.Textbox(label="Info", interactive=False, lines=4, show_label=False) gr.Markdown("### 🎭 Effects") with gr.Row(): use_random = gr.Checkbox(label="🎲 Random", value=True) aspect_ratio = gr.Dropdown( choices=['reels', 'tiktok', 'youtube', 'square', 'wide'], value='reels', label="📱 Platform" ) with gr.Column(visible=False) as effects: with gr.Row(): tag_filter = gr.Dropdown(choices=tag_choices, value="all", label="đŸˇī¸ Tag") search_box = gr.Textbox(label="🔍 Search", placeholder="zoom, pan...") template_dropdown = gr.Dropdown( choices=template_choices(), label="đŸŽŦ Effect" ) with gr.Column(scale=1): gr.Markdown("### âš™ī¸ Settings") with gr.Row(): fmt = gr.Radio(['mp4', 'gif'], value='mp4', label="📄 Format") quality = gr.Dropdown([ ('⚡ Fast', 'fast'), ('âš–ī¸ Balanced', 'balanced'), ('đŸŽ¯ Quality', 'quality'), ('💎 Best', 'best') ], value='balanced', label="đŸŽ›ī¸ Quality") create_btn = gr.Button("🚀 Create Video", variant="primary", size="lg") status = gr.Textbox(label="Status", interactive=False) output_video = gr.Video(label="Result", height=300) download_btn = gr.DownloadButton(label="đŸ“Ĩ Download", visible=False) with gr.Tab("🎭 Templates"): gr.Markdown(f"### Template Editor ({len(motion.templates)} available)") with gr.Row(): with gr.Column(scale=2): edit_dropdown = gr.Dropdown( choices=template_choices(), value=template_choices()[0] if motion.templates else None, label="Select Template" ) with gr.Row(): name_input = gr.Textbox(label="Name", placeholder="my_effect") dur_input = gr.Number(label="Duration (s)", value=4, minimum=2, maximum=10) with gr.Row(): s1 = gr.Number(label="Scale Start", value=1.0, step=0.1) s2 = gr.Number(label="Scale End", value=1.3, step=0.1) with gr.Row(): x1 = gr.Number(label="Pan X1", value=0) y1 = gr.Number(label="Pan Y1", value=0) x2 = gr.Number(label="Pan X2", value=0) y2 = gr.Number(label="Pan Y2", value=0) with gr.Row(): r1 = gr.Number(label="Rotate Start", value=0) r2 = gr.Number(label="Rotate End", value=0) tags_input = gr.Textbox(label="Tags", placeholder="zoom, smooth, dramatic") desc_input = gr.Textbox(label="Description", lines=2) save_btn = gr.Button("💾 Save", variant="primary") save_status = gr.Textbox(label="Status", interactive=False) with gr.Column(scale=1): gr.Markdown("### Guide") gr.Markdown(""" **Scale**: 1.0=normal, 1.3=30% bigger **Pan**: X/Y movement (-=left/up, +=right/down) **Rotate**: Degrees (+=clockwise) **Duration**: 3-7s optimal for social """) with gr.Tab("â„šī¸ Help"): gr.Markdown(f""" ### Quick Start 1. Upload images/videos 2. Choose platform (Reels=9:16, YouTube=16:9) 3. Select random or specific effects 4. Create and download ### {len(motion.templates)} Motion Effects - **Zoom**: Center, corners, dramatic - **Pan**: Horizontal, vertical, diagonal - **Complex**: Parallax, spiral, documentary - **Special**: Breathing, tilt, intimate ### Tips - Use 1080p+ images for best quality - MP4 for quality, GIF for sharing - 3-7s per image works well - Mix effects for variety """) # Events files_input.change(preview_files, [files_input], [preview_img, preview_info]) create_btn.click( create_video, [files_input, aspect_ratio, fmt, use_random, template_dropdown, quality], [output_video, status, download_btn] ) use_random.change(lambda x: gr.update(visible=not x), [use_random], [effects]) tag_filter.change(filter_by_tag, [tag_filter], [template_dropdown]) search_box.change(search_templates, [search_box], [template_dropdown]) edit_dropdown.change( load_template, [edit_dropdown], [name_input, dur_input, s1, s2, x1, y1, x2, y2, tags_input, desc_input, r1, r2] ) save_btn.click( save_template, [name_input, dur_input, s1, s2, x1, y1, x2, y2, tags_input, desc_input, r1, r2], [save_status, edit_dropdown] ) if __name__ == "__main__": app.launch()