| 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 |
|
|
| |
| 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) |
| |
| |
| 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 = 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]}...") |
|
|
| |
| all_tags = set() |
| for t in motion.templates: |
| all_tags.update(t.get('tags', [])) |
| tag_choices = ["all"] + sorted(list(all_tags)) |
|
|
| |
| 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 |
| """) |
| |
| |
| 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() |