editor / app.py
Vo Hoang Minh
up
ed85b4d
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()