Vo Hoang Minh commited on
Commit ·
cc72d85
1
Parent(s): 1d83642
- app.py +186 -147
- motion_processor.py +4 -6
- video_processor.py +7 -23
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
from motion_processor import MotionProcessor
|
| 3 |
from file_handler import FileHandler
|
| 4 |
from video_processor import VideoProcessor
|
|
@@ -8,7 +9,7 @@ motion = MotionProcessor()
|
|
| 8 |
files = FileHandler()
|
| 9 |
processor = VideoProcessor(motion, files)
|
| 10 |
|
| 11 |
-
|
| 12 |
|
| 13 |
def preview_files(uploaded_files):
|
| 14 |
if not uploaded_files:
|
|
@@ -25,74 +26,63 @@ def filter_templates_by_tag(tag_filter):
|
|
| 25 |
else:
|
| 26 |
filtered = motion.get_templates_by_tag(tag_filter)
|
| 27 |
|
| 28 |
-
choices = [f"{t['name']}
|
| 29 |
return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
|
| 30 |
|
| 31 |
def search_templates(search_query):
|
| 32 |
if not search_query.strip():
|
| 33 |
-
choices = [f"{t['name']}
|
| 34 |
else:
|
| 35 |
filtered = motion.search_templates(search_query)
|
| 36 |
-
choices = [f"{t['name']}
|
| 37 |
|
| 38 |
return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
|
| 39 |
|
| 40 |
-
def
|
| 41 |
-
if not test_file or not template_choice:
|
| 42 |
-
return None, "Please upload a test file and select a template"
|
| 43 |
-
|
| 44 |
-
template_name = template_choice.split(" (")[0] # Extract name from "name (tags)"
|
| 45 |
-
|
| 46 |
-
result, status = processor.test_effect(test_file.name, template_name, aspect)
|
| 47 |
-
return result, status
|
| 48 |
-
|
| 49 |
-
def create_motion_video(input_files, aspect, output_format, use_random, selected_template,
|
| 50 |
-
workers, quality, fps, progress=gr.Progress()):
|
| 51 |
def progress_fn(val, desc):
|
| 52 |
progress(val, desc=desc)
|
| 53 |
|
|
|
|
|
|
|
|
|
|
| 54 |
# Prepare template
|
| 55 |
template = None
|
| 56 |
if not use_random and selected_template:
|
| 57 |
-
template_name = selected_template.split("
|
| 58 |
template = next((t for t in motion.templates if t['name'] == template_name), None)
|
| 59 |
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
if result:
|
| 72 |
-
return result, status, gr.DownloadButton(label="📥 Download", value=result, visible=True)
|
| 73 |
-
return None, status, gr.DownloadButton(visible=False)
|
| 74 |
|
| 75 |
def load_template(template_choice):
|
| 76 |
if not template_choice:
|
| 77 |
return "", 4, 1.0, 1.3, 0, 0, 0, 0, "", "", 0, 0
|
| 78 |
|
| 79 |
-
template_name = template_choice.split("
|
| 80 |
template = next((t for t in motion.templates if t['name'] == template_name), motion.templates[0])
|
| 81 |
|
| 82 |
return (
|
| 83 |
template['name'],
|
| 84 |
template['duration'],
|
| 85 |
-
template['scale'][0], template['scale'][1],
|
| 86 |
-
template['pan'][0], template['pan'][1],
|
| 87 |
-
template['pan'][2], template['pan'][3],
|
| 88 |
', '.join(template.get('tags', [])),
|
| 89 |
template.get('desc', ''),
|
| 90 |
-
template['rotate'][0], template['rotate'][1]
|
| 91 |
)
|
| 92 |
|
| 93 |
def save_template(name, duration, scale_start, scale_end, x1, y1, x2, y2, tags, desc, rot_start, rot_end):
|
| 94 |
if not name:
|
| 95 |
-
return "Enter template name", gr.Dropdown()
|
| 96 |
|
| 97 |
tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] if tags else []
|
| 98 |
|
|
@@ -106,7 +96,6 @@ def save_template(name, duration, scale_start, scale_end, x1, y1, x2, y2, tags,
|
|
| 106 |
'desc': desc or f"Custom template: {name}"
|
| 107 |
}
|
| 108 |
|
| 109 |
-
# Update or add
|
| 110 |
existing = next((i for i, t in enumerate(motion.templates) if t['name'] == name), None)
|
| 111 |
if existing is not None:
|
| 112 |
motion.templates[existing] = new_template
|
|
@@ -114,8 +103,8 @@ def save_template(name, duration, scale_start, scale_end, x1, y1, x2, y2, tags,
|
|
| 114 |
motion.templates.append(new_template)
|
| 115 |
|
| 116 |
motion.save_templates(motion.templates)
|
| 117 |
-
choices = [f"{t['name']}
|
| 118 |
-
return f"Saved '{name}'!", gr.Dropdown(choices=choices, value=f"{name}
|
| 119 |
|
| 120 |
# Get all unique tags
|
| 121 |
all_tags = set()
|
|
@@ -124,99 +113,98 @@ for template in motion.templates:
|
|
| 124 |
tag_choices = ["all"] + sorted(list(all_tags))
|
| 125 |
|
| 126 |
# Interface
|
| 127 |
-
with gr.Blocks(title="Ken Burns Story Creator",
|
| 128 |
-
.
|
| 129 |
-
.
|
| 130 |
-
""") as app:
|
| 131 |
-
gr.Markdown("# 🎬 Ken Burns Story Creator\nCreate motion videos for YouTube, TikTok, Reels with 30+ effects")
|
| 132 |
|
| 133 |
with gr.Tabs():
|
|
|
|
| 134 |
with gr.Tab("📹 Create Video"):
|
| 135 |
with gr.Row():
|
| 136 |
with gr.Column(scale=2):
|
| 137 |
-
#
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
# Preview
|
| 142 |
with gr.Group():
|
| 143 |
-
gr.
|
| 144 |
-
|
| 145 |
-
preview_info = gr.Textbox(label="Info", interactive=False)
|
| 146 |
-
|
| 147 |
-
# Effect Selection
|
| 148 |
-
with gr.Group():
|
| 149 |
-
gr.Markdown("### 🎭 Effect Selection")
|
| 150 |
-
|
| 151 |
-
with gr.Row():
|
| 152 |
-
use_random = gr.Checkbox(label="Random Effects", value=True)
|
| 153 |
-
|
| 154 |
-
with gr.Column(visible=False) as effect_selector:
|
| 155 |
-
with gr.Row():
|
| 156 |
-
tag_filter = gr.Dropdown(
|
| 157 |
-
choices=tag_choices, value="all",
|
| 158 |
-
label="Filter by Tag", scale=1
|
| 159 |
-
)
|
| 160 |
-
search_box = gr.Textbox(
|
| 161 |
-
label="Search Effects", placeholder="zoom, pan, dramatic...", scale=2
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
template_dropdown = gr.Dropdown(
|
| 165 |
-
choices=[f"{t['name']} ({', '.join(t.get('tags', []))})" for t in motion.templates],
|
| 166 |
-
label="Select Effect"
|
| 167 |
-
)
|
| 168 |
|
| 169 |
-
#
|
|
|
|
| 170 |
with gr.Row():
|
|
|
|
| 171 |
aspect_ratio = gr.Dropdown(
|
| 172 |
choices=['reels', 'tiktok', 'youtube', 'square', 'widescreen'],
|
| 173 |
value='reels',
|
| 174 |
label="📱 Platform"
|
| 175 |
)
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
with gr.Column(scale=1):
|
| 179 |
-
#
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
|
| 187 |
-
# Create
|
| 188 |
-
create_btn = gr.Button(
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
value='fast',
|
| 204 |
-
label="🏆 Quality Preset"
|
| 205 |
-
)
|
| 206 |
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
| 209 |
|
| 210 |
with gr.Row():
|
| 211 |
with gr.Column(scale=2):
|
| 212 |
template_dropdown_edit = gr.Dropdown(
|
| 213 |
-
choices=[f"{t['name']}
|
| 214 |
-
value=f"{motion.templates[0]['name']}
|
| 215 |
-
label="Template"
|
| 216 |
)
|
| 217 |
|
| 218 |
with gr.Row():
|
| 219 |
-
name_input = gr.Textbox(label="Name", placeholder="
|
| 220 |
duration_input = gr.Number(label="Duration (s)", value=4, minimum=2, maximum=10)
|
| 221 |
|
| 222 |
with gr.Row():
|
|
@@ -235,8 +223,7 @@ with gr.Blocks(title="Ken Burns Story Creator", css="""
|
|
| 235 |
|
| 236 |
tags_input = gr.Textbox(
|
| 237 |
label="Tags (comma separated)",
|
| 238 |
-
placeholder="zoom,
|
| 239 |
-
value=""
|
| 240 |
)
|
| 241 |
|
| 242 |
desc_input = gr.Textbox(
|
|
@@ -249,57 +236,109 @@ with gr.Blocks(title="Ken Burns Story Creator", css="""
|
|
| 249 |
template_status = gr.Textbox(label="Status", interactive=False)
|
| 250 |
|
| 251 |
with gr.Column(scale=1):
|
| 252 |
-
gr.Markdown("###
|
| 253 |
-
gr.Markdown(f"**{len(tag_choices)-1} unique tags:**")
|
| 254 |
-
gr.Markdown(", ".join(sorted(tag_choices[1:]))) # Exclude "all"
|
| 255 |
-
|
| 256 |
-
gr.Markdown("### 📖 Template Guide")
|
| 257 |
gr.Markdown("""
|
| 258 |
-
**Scale**: Zoom level
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
""")
|
|
|
|
|
|
|
|
|
|
| 264 |
|
|
|
|
| 265 |
with gr.Tab("ℹ️ Help"):
|
| 266 |
gr.Markdown(f"""
|
| 267 |
### 📖 How to Use
|
| 268 |
-
1. **Upload** your images/videos
|
| 269 |
-
2. **Choose effects**: Random or specific template
|
| 270 |
-
3. **Test effects** on single image first
|
| 271 |
-
4. **Select platform** aspect ratio
|
| 272 |
-
5. **Create** your story video
|
| 273 |
|
| 274 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
-
|
| 277 |
-
- Center zoom in/out
|
| 278 |
-
- Directional zooms (left, right, top, bottom)
|
| 279 |
-
- Dramatic push/pull effects
|
| 280 |
|
| 281 |
-
**
|
| 282 |
-
-
|
| 283 |
-
-
|
| 284 |
-
-
|
| 285 |
|
| 286 |
-
|
| 287 |
-
- Slow drifts and breathing
|
| 288 |
-
- Focus shifts and reveals
|
| 289 |
-
- Documentary style movements
|
| 290 |
|
| 291 |
-
|
| 292 |
-
-
|
| 293 |
-
-
|
| 294 |
-
-
|
| 295 |
|
| 296 |
-
###
|
| 297 |
-
|
| 298 |
-
-
|
| 299 |
-
-
|
|
|
|
|
|
|
| 300 |
""")
|
| 301 |
|
| 302 |
-
#
|
| 303 |
-
files_input.change(
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
app.launch()
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import atexit
|
| 3 |
from motion_processor import MotionProcessor
|
| 4 |
from file_handler import FileHandler
|
| 5 |
from video_processor import VideoProcessor
|
|
|
|
| 9 |
files = FileHandler()
|
| 10 |
processor = VideoProcessor(motion, files)
|
| 11 |
|
| 12 |
+
atexit.register(files.cleanup)
|
| 13 |
|
| 14 |
def preview_files(uploaded_files):
|
| 15 |
if not uploaded_files:
|
|
|
|
| 26 |
else:
|
| 27 |
filtered = motion.get_templates_by_tag(tag_filter)
|
| 28 |
|
| 29 |
+
choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in filtered]
|
| 30 |
return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
|
| 31 |
|
| 32 |
def search_templates(search_query):
|
| 33 |
if not search_query.strip():
|
| 34 |
+
choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in motion.templates]
|
| 35 |
else:
|
| 36 |
filtered = motion.search_templates(search_query)
|
| 37 |
+
choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in filtered]
|
| 38 |
|
| 39 |
return gr.Dropdown(choices=choices, value=choices[0] if choices else None)
|
| 40 |
|
| 41 |
+
def create_motion_video(input_files, aspect, output_format, use_random, selected_template, progress=gr.Progress()):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
def progress_fn(val, desc):
|
| 43 |
progress(val, desc=desc)
|
| 44 |
|
| 45 |
+
if not input_files:
|
| 46 |
+
return None, "❌ No files provided", gr.DownloadButton(visible=False)
|
| 47 |
+
|
| 48 |
# Prepare template
|
| 49 |
template = None
|
| 50 |
if not use_random and selected_template:
|
| 51 |
+
template_name = selected_template.split(" - ")[0]
|
| 52 |
template = next((t for t in motion.templates if t['name'] == template_name), None)
|
| 53 |
|
| 54 |
+
try:
|
| 55 |
+
result, status = processor.create_video(
|
| 56 |
+
input_files, aspect, output_format, progress_fn, template
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
if result:
|
| 60 |
+
return result, f"✅ {status}", gr.DownloadButton(label="📥 Download Video", value=result, visible=True)
|
| 61 |
+
return None, f"❌ {status}", gr.DownloadButton(visible=False)
|
| 62 |
+
except Exception as e:
|
| 63 |
+
return None, f"❌ Error: {str(e)}", gr.DownloadButton(visible=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
|
| 65 |
def load_template(template_choice):
|
| 66 |
if not template_choice:
|
| 67 |
return "", 4, 1.0, 1.3, 0, 0, 0, 0, "", "", 0, 0
|
| 68 |
|
| 69 |
+
template_name = template_choice.split(" - ")[0]
|
| 70 |
template = next((t for t in motion.templates if t['name'] == template_name), motion.templates[0])
|
| 71 |
|
| 72 |
return (
|
| 73 |
template['name'],
|
| 74 |
template['duration'],
|
| 75 |
+
template['scale'][0], template['scale'][1],
|
| 76 |
+
template['pan'][0], template['pan'][1],
|
| 77 |
+
template['pan'][2], template['pan'][3],
|
| 78 |
', '.join(template.get('tags', [])),
|
| 79 |
template.get('desc', ''),
|
| 80 |
+
template['rotate'][0], template['rotate'][1]
|
| 81 |
)
|
| 82 |
|
| 83 |
def save_template(name, duration, scale_start, scale_end, x1, y1, x2, y2, tags, desc, rot_start, rot_end):
|
| 84 |
if not name:
|
| 85 |
+
return "❌ Enter template name", gr.Dropdown()
|
| 86 |
|
| 87 |
tag_list = [tag.strip() for tag in tags.split(',') if tag.strip()] if tags else []
|
| 88 |
|
|
|
|
| 96 |
'desc': desc or f"Custom template: {name}"
|
| 97 |
}
|
| 98 |
|
|
|
|
| 99 |
existing = next((i for i, t in enumerate(motion.templates) if t['name'] == name), None)
|
| 100 |
if existing is not None:
|
| 101 |
motion.templates[existing] = new_template
|
|
|
|
| 103 |
motion.templates.append(new_template)
|
| 104 |
|
| 105 |
motion.save_templates(motion.templates)
|
| 106 |
+
choices = [f"{t['name']} - {t.get('desc', '')[:50]}..." for t in motion.templates]
|
| 107 |
+
return f"✅ Saved '{name}'!", gr.Dropdown(choices=choices, value=f"{name} - {desc[:50]}...")
|
| 108 |
|
| 109 |
# Get all unique tags
|
| 110 |
all_tags = set()
|
|
|
|
| 113 |
tag_choices = ["all"] + sorted(list(all_tags))
|
| 114 |
|
| 115 |
# Interface
|
| 116 |
+
with gr.Blocks(title="Ken Burns Story Creator", theme=gr.themes.Soft()) as app:
|
| 117 |
+
gr.Markdown("# 🎬 Ken Burns Story Creator", elem_classes="text-center")
|
| 118 |
+
gr.Markdown("Create motion videos for YouTube, TikTok, Reels with 35+ professional effects", elem_classes="text-center")
|
|
|
|
|
|
|
| 119 |
|
| 120 |
with gr.Tabs():
|
| 121 |
+
# Main tab - Create Video
|
| 122 |
with gr.Tab("📹 Create Video"):
|
| 123 |
with gr.Row():
|
| 124 |
with gr.Column(scale=2):
|
| 125 |
+
# File input
|
| 126 |
+
gr.Markdown("### 📁 Upload Your Media")
|
| 127 |
+
files_input = gr.File(
|
| 128 |
+
label="Select Images/Videos",
|
| 129 |
+
file_count="multiple",
|
| 130 |
+
file_types=["image", "video"]
|
| 131 |
+
)
|
| 132 |
|
| 133 |
# Preview
|
| 134 |
with gr.Group():
|
| 135 |
+
preview_img = gr.Image(label="📸 Preview", height=200, show_label=False)
|
| 136 |
+
preview_info = gr.Textbox(label="Info", interactive=False, show_label=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
+
# Motion settings
|
| 139 |
+
gr.Markdown("### 🎭 Motion Effects")
|
| 140 |
with gr.Row():
|
| 141 |
+
use_random = gr.Checkbox(label="🎲 Random Effects", value=True)
|
| 142 |
aspect_ratio = gr.Dropdown(
|
| 143 |
choices=['reels', 'tiktok', 'youtube', 'square', 'widescreen'],
|
| 144 |
value='reels',
|
| 145 |
label="📱 Platform"
|
| 146 |
)
|
| 147 |
+
|
| 148 |
+
# Effect selector (hidden by default)
|
| 149 |
+
with gr.Column(visible=False) as effect_selector:
|
| 150 |
+
with gr.Row():
|
| 151 |
+
tag_filter = gr.Dropdown(
|
| 152 |
+
choices=tag_choices,
|
| 153 |
+
value="all",
|
| 154 |
+
label="🏷️ Filter by Tag"
|
| 155 |
+
)
|
| 156 |
+
search_box = gr.Textbox(
|
| 157 |
+
label="🔍 Search",
|
| 158 |
+
placeholder="zoom, pan, dramatic..."
|
| 159 |
+
)
|
| 160 |
+
|
| 161 |
+
template_dropdown = gr.Dropdown(
|
| 162 |
+
choices=[f"{t['name']} - {t.get('desc', '')[:50]}..." for t in motion.templates],
|
| 163 |
+
label="🎬 Select Effect",
|
| 164 |
+
interactive=True
|
| 165 |
+
)
|
| 166 |
|
| 167 |
with gr.Column(scale=1):
|
| 168 |
+
# Settings
|
| 169 |
+
gr.Markdown("### ⚙️ Settings")
|
| 170 |
+
output_format = gr.Radio(
|
| 171 |
+
choices=['mp4', 'gif'],
|
| 172 |
+
value='mp4',
|
| 173 |
+
label="📄 Output Format"
|
| 174 |
+
)
|
| 175 |
|
| 176 |
+
# Create button
|
| 177 |
+
create_btn = gr.Button(
|
| 178 |
+
"🚀 Create Motion Video",
|
| 179 |
+
variant="primary",
|
| 180 |
+
size="lg",
|
| 181 |
+
scale=2
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
# Results
|
| 185 |
+
status = gr.Textbox(label="📊 Status", interactive=False)
|
| 186 |
+
output_video = gr.Video(label="🎥 Result", height=300)
|
| 187 |
+
download_btn = gr.DownloadButton(
|
| 188 |
+
label="📥 Download Video",
|
| 189 |
+
visible=False,
|
| 190 |
+
variant="secondary"
|
| 191 |
+
)
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
+
# Templates tab
|
| 194 |
+
with gr.Tab("🎭 Motion Templates"):
|
| 195 |
+
gr.Markdown("### 🛠️ Template Editor")
|
| 196 |
+
gr.Markdown(f"**{len(motion.templates)} available templates** - Create and customize motion effects")
|
| 197 |
|
| 198 |
with gr.Row():
|
| 199 |
with gr.Column(scale=2):
|
| 200 |
template_dropdown_edit = gr.Dropdown(
|
| 201 |
+
choices=[f"{t['name']} - {t.get('desc', '')[:50]}..." for t in motion.templates],
|
| 202 |
+
value=f"{motion.templates[0]['name']} - {motion.templates[0].get('desc', '')[:50]}..." if motion.templates else None,
|
| 203 |
+
label="📋 Select Template to Edit"
|
| 204 |
)
|
| 205 |
|
| 206 |
with gr.Row():
|
| 207 |
+
name_input = gr.Textbox(label="Name", placeholder="my_effect")
|
| 208 |
duration_input = gr.Number(label="Duration (s)", value=4, minimum=2, maximum=10)
|
| 209 |
|
| 210 |
with gr.Row():
|
|
|
|
| 223 |
|
| 224 |
tags_input = gr.Textbox(
|
| 225 |
label="Tags (comma separated)",
|
| 226 |
+
placeholder="zoom, dramatic, smooth"
|
|
|
|
| 227 |
)
|
| 228 |
|
| 229 |
desc_input = gr.Textbox(
|
|
|
|
| 236 |
template_status = gr.Textbox(label="Status", interactive=False)
|
| 237 |
|
| 238 |
with gr.Column(scale=1):
|
| 239 |
+
gr.Markdown("### 📖 Quick Guide")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
gr.Markdown("""
|
| 241 |
+
**Scale**: Zoom level
|
| 242 |
+
- 1.0 = normal size
|
| 243 |
+
- 1.3 = 30% bigger
|
| 244 |
+
- 0.8 = 20% smaller
|
| 245 |
+
|
| 246 |
+
**Pan**: Movement direction
|
| 247 |
+
- X: left (-) / right (+)
|
| 248 |
+
- Y: up (-) / down (+)
|
| 249 |
+
|
| 250 |
+
**Rotate**: Angle in degrees
|
| 251 |
+
- Positive = clockwise
|
| 252 |
+
- Negative = counter-clockwise
|
| 253 |
+
|
| 254 |
+
**Duration**: 3-7s optimal for social media
|
| 255 |
""")
|
| 256 |
+
|
| 257 |
+
gr.Markdown("### 🏷️ Popular Tags")
|
| 258 |
+
gr.Markdown(", ".join(sorted(tag_choices[1:8]))) # Show first 7 tags
|
| 259 |
|
| 260 |
+
# Help tab
|
| 261 |
with gr.Tab("ℹ️ Help"):
|
| 262 |
gr.Markdown(f"""
|
| 263 |
### 📖 How to Use
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
|
| 265 |
+
1. **📁 Upload**: Select your images or videos
|
| 266 |
+
2. **📱 Platform**: Choose aspect ratio (Reels 9:16, YouTube 16:9, Square 1:1)
|
| 267 |
+
3. **🎭 Effects**: Use random or select specific motion templates
|
| 268 |
+
4. **🚀 Create**: Click the create button and wait
|
| 269 |
+
5. **📥 Download**: Get your final video
|
| 270 |
+
|
| 271 |
+
### 🎬 Motion Effects ({len(motion.templates)} total)
|
| 272 |
+
|
| 273 |
+
**🔍 Zoom Effects**: Center, corners, dramatic, slow cinematic
|
| 274 |
+
**↔️ Pan Effects**: Horizontal, vertical, diagonal movements
|
| 275 |
+
**🌀 Complex**: Zoom+pan, parallax, spiral, documentary style
|
| 276 |
+
**🎨 Special**: Breathing, pendulum, tilt, intimate close-ups
|
| 277 |
|
| 278 |
+
### 📱 Perfect for Social Media
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
+
- **TikTok/Reels**: 9:16 vertical format
|
| 281 |
+
- **YouTube Shorts**: 16:9 or 9:16
|
| 282 |
+
- **Instagram Posts**: 1:1 square format
|
| 283 |
+
- **Stories**: Any format works
|
| 284 |
|
| 285 |
+
### ⚡ Performance Tips
|
|
|
|
|
|
|
|
|
|
| 286 |
|
| 287 |
+
- Use high-resolution images (1080p+)
|
| 288 |
+
- Keep videos under 60 seconds for social media
|
| 289 |
+
- MP4 for quality, GIF for lightweight sharing
|
| 290 |
+
- Random effects create dynamic variety
|
| 291 |
|
| 292 |
+
### 🛠️ Technical
|
| 293 |
+
|
| 294 |
+
- Processing: FFmpeg with Ken Burns effects
|
| 295 |
+
- Parallel processing for speed
|
| 296 |
+
- Optimized for 25-30 FPS output
|
| 297 |
+
- Quality presets: fast/medium/slow
|
| 298 |
""")
|
| 299 |
|
| 300 |
+
# Event handlers
|
| 301 |
+
files_input.change(
|
| 302 |
+
fn=preview_files,
|
| 303 |
+
inputs=[files_input],
|
| 304 |
+
outputs=[preview_img, preview_info]
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
create_btn.click(
|
| 308 |
+
fn=create_motion_video,
|
| 309 |
+
inputs=[files_input, aspect_ratio, output_format, use_random, template_dropdown],
|
| 310 |
+
outputs=[output_video, status, download_btn]
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
use_random.change(
|
| 314 |
+
fn=lambda x: gr.update(visible=not x),
|
| 315 |
+
inputs=[use_random],
|
| 316 |
+
outputs=[effect_selector]
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
tag_filter.change(
|
| 320 |
+
fn=filter_templates_by_tag,
|
| 321 |
+
inputs=[tag_filter],
|
| 322 |
+
outputs=[template_dropdown]
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
search_box.change(
|
| 326 |
+
fn=search_templates,
|
| 327 |
+
inputs=[search_box],
|
| 328 |
+
outputs=[template_dropdown]
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
template_dropdown_edit.change(
|
| 332 |
+
fn=load_template,
|
| 333 |
+
inputs=[template_dropdown_edit],
|
| 334 |
+
outputs=[name_input, duration_input, scale_start, scale_end, x1, y1, x2, y2, tags_input, desc_input, rot_start, rot_end]
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
save_btn.click(
|
| 338 |
+
fn=save_template,
|
| 339 |
+
inputs=[name_input, duration_input, scale_start, scale_end, x1, y1, x2, y2, tags_input, desc_input, rot_start, rot_end],
|
| 340 |
+
outputs=[template_status, template_dropdown_edit]
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
app.launch()
|
motion_processor.py
CHANGED
|
@@ -1,7 +1,4 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import random
|
| 3 |
-
import subprocess
|
| 4 |
-
import math
|
| 5 |
from pathlib import Path
|
| 6 |
|
| 7 |
class MotionProcessor:
|
|
@@ -38,13 +35,14 @@ class MotionProcessor:
|
|
| 38 |
if query in t['name'].lower() or
|
| 39 |
any(query in tag.lower() for tag in t.get('tags', []))]
|
| 40 |
|
| 41 |
-
def apply_motion(self, input_path, output_path, template=None, aspect='youtube'
|
| 42 |
if not template:
|
| 43 |
template = self.get_random_template()
|
| 44 |
|
| 45 |
resolution = self.aspect_ratios[aspect]
|
| 46 |
w, h = map(int, resolution.split(':'))
|
| 47 |
duration = template['duration']
|
|
|
|
| 48 |
|
| 49 |
# Extract values - đơn giản
|
| 50 |
scale_start, scale_end = template['scale']
|
|
@@ -87,7 +85,7 @@ class MotionProcessor:
|
|
| 87 |
'ffmpeg', '-y', '-i', input_path,
|
| 88 |
'-filter_complex', filter_complex,
|
| 89 |
'-map', '[v]', '-t', str(duration),
|
| 90 |
-
'-c:v', 'libx264', '-preset',
|
| 91 |
output_path
|
| 92 |
]
|
| 93 |
|
|
|
|
| 1 |
+
import json, random, subprocess, math
|
|
|
|
|
|
|
|
|
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
class MotionProcessor:
|
|
|
|
| 35 |
if query in t['name'].lower() or
|
| 36 |
any(query in tag.lower() for tag in t.get('tags', []))]
|
| 37 |
|
| 38 |
+
def apply_motion(self, input_path, output_path, template=None, aspect='youtube'):
|
| 39 |
if not template:
|
| 40 |
template = self.get_random_template()
|
| 41 |
|
| 42 |
resolution = self.aspect_ratios[aspect]
|
| 43 |
w, h = map(int, resolution.split(':'))
|
| 44 |
duration = template['duration']
|
| 45 |
+
fps = 25 # Fixed FPS
|
| 46 |
|
| 47 |
# Extract values - đơn giản
|
| 48 |
scale_start, scale_end = template['scale']
|
|
|
|
| 85 |
'ffmpeg', '-y', '-i', input_path,
|
| 86 |
'-filter_complex', filter_complex,
|
| 87 |
'-map', '[v]', '-t', str(duration),
|
| 88 |
+
'-c:v', 'libx264', '-preset', 'fast', '-crf', '23',
|
| 89 |
output_path
|
| 90 |
]
|
| 91 |
|
video_processor.py
CHANGED
|
@@ -15,7 +15,7 @@ class VideoProcessor:
|
|
| 15 |
return None
|
| 16 |
|
| 17 |
def create_video(self, input_files, aspect='youtube', output_format='mp4', progress_fn=None,
|
| 18 |
-
selected_template=None
|
| 19 |
files = self.files.get_files(input_files)
|
| 20 |
if not files:
|
| 21 |
return None, "No valid files found"
|
|
@@ -27,19 +27,14 @@ class VideoProcessor:
|
|
| 27 |
segments_dir = self.files.temp_path("segments")
|
| 28 |
os.makedirs(segments_dir, exist_ok=True)
|
| 29 |
|
| 30 |
-
# Advanced settings
|
| 31 |
-
workers = advanced_settings.get('workers', 4) if advanced_settings else 4
|
| 32 |
-
quality = advanced_settings.get('quality', 'fast') if advanced_settings else 'fast'
|
| 33 |
-
fps = advanced_settings.get('fps', 25) if advanced_settings else 25
|
| 34 |
-
|
| 35 |
processed = []
|
| 36 |
-
with ThreadPoolExecutor(max_workers=
|
| 37 |
futures = []
|
| 38 |
for i, file_path in enumerate(files):
|
| 39 |
output_path = os.path.join(segments_dir, f"seg_{i:03d}.mp4")
|
| 40 |
# Use selected template or random
|
| 41 |
template = selected_template if selected_template else None
|
| 42 |
-
future = executor.submit(self.process_single, file_path, output_path, aspect, template
|
| 43 |
futures.append(future)
|
| 44 |
|
| 45 |
for future in futures:
|
|
@@ -71,20 +66,9 @@ class VideoProcessor:
|
|
| 71 |
|
| 72 |
return final_output, f"Created video from {len(files)} files"
|
| 73 |
|
| 74 |
-
|
| 75 |
-
def test_effect(self, test_file, template_name, aspect='youtube'):
|
| 76 |
-
"""Test single effect on one file"""
|
| 77 |
-
if not test_file:
|
| 78 |
-
return None, "No test file provided"
|
| 79 |
-
|
| 80 |
-
template = next((t for t in self.motion.templates if t['name'] == template_name), None)
|
| 81 |
-
if not template:
|
| 82 |
-
return None, f"Template '{template_name}' not found"
|
| 83 |
-
|
| 84 |
-
test_output = self.files.temp_path(f"test_{template_name}.mp4")
|
| 85 |
-
|
| 86 |
try:
|
| 87 |
-
|
| 88 |
-
return result, f"Test completed with '{template_name}' effect"
|
| 89 |
except Exception as e:
|
| 90 |
-
|
|
|
|
|
|
| 15 |
return None
|
| 16 |
|
| 17 |
def create_video(self, input_files, aspect='youtube', output_format='mp4', progress_fn=None,
|
| 18 |
+
selected_template=None):
|
| 19 |
files = self.files.get_files(input_files)
|
| 20 |
if not files:
|
| 21 |
return None, "No valid files found"
|
|
|
|
| 27 |
segments_dir = self.files.temp_path("segments")
|
| 28 |
os.makedirs(segments_dir, exist_ok=True)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
processed = []
|
| 31 |
+
with ThreadPoolExecutor(max_workers=4) as executor:
|
| 32 |
futures = []
|
| 33 |
for i, file_path in enumerate(files):
|
| 34 |
output_path = os.path.join(segments_dir, f"seg_{i:03d}.mp4")
|
| 35 |
# Use selected template or random
|
| 36 |
template = selected_template if selected_template else None
|
| 37 |
+
future = executor.submit(self.process_single, file_path, output_path, aspect, template)
|
| 38 |
futures.append(future)
|
| 39 |
|
| 40 |
for future in futures:
|
|
|
|
| 66 |
|
| 67 |
return final_output, f"Created video from {len(files)} files"
|
| 68 |
|
| 69 |
+
def process_single(self, file_path, output_path, aspect, template=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
try:
|
| 71 |
+
return self.motion.apply_motion(file_path, output_path, template, aspect)
|
|
|
|
| 72 |
except Exception as e:
|
| 73 |
+
print(f"Error processing {file_path}: {e}")
|
| 74 |
+
return None
|