Spaces:
Sleeping
Sleeping
| """ | |
| PhotoMotion Studio | |
| Upload a photo. Choose a motion style. Get a short social video. | |
| A Gradio-based photo-to-video generator for small businesses, creators, | |
| real estate teams, and product brands. | |
| """ | |
| from __future__ import annotations | |
| import spaces | |
| import gradio as gr | |
| import numpy as np | |
| from PIL import Image | |
| import tempfile | |
| import os | |
| import random | |
| from typing import Optional | |
| import imageio | |
| # --------------------------------------------------------------------------- | |
| # Preset definitions | |
| # --------------------------------------------------------------------------- | |
| PRESET_PROMPTS = { | |
| # Main presets | |
| "Product Spotlight": ( | |
| "Animate this product photo with a clean studio ad look, subtle floating motion, " | |
| "soft lighting, gentle camera movement, and a professional commercial feel." | |
| ), | |
| "Coffee Bag Commercial": ( | |
| "Animate this coffee product photo into a cozy morning commercial. Add subtle camera " | |
| "push-in, warm lighting, gentle steam, and premium product-ad movement." | |
| ), | |
| "New Home Cinematic Pan": ( | |
| "Animate this new construction home exterior with a slow cinematic push-in, realistic " | |
| "daylight, subtle sky movement, and polished real estate ad style." | |
| ), | |
| "Claymation-Style Motion": ( | |
| "Animate this image in a playful claymation-inspired style with handmade texture, " | |
| "gentle movement, warm colors, and a fun social media ad feel." | |
| ), | |
| "Social Media Ad Zoom": ( | |
| "Animate this image with an attention-grabbing zoom, smooth motion, subtle depth, " | |
| "and a polished social media ad style." | |
| ), | |
| "Real Estate Listing Reveal": ( | |
| "Animate this real estate image with a smooth reveal, slow camera movement, bright " | |
| "natural lighting, and a professional listing video feel." | |
| ), | |
| "Cozy Lifestyle Animation": ( | |
| "Animate this photo with warm lighting, gentle movement, cozy lifestyle atmosphere, " | |
| "and soft cinematic motion." | |
| ), | |
| "Before-and-After Reveal": ( | |
| "Create a short animated reveal effect using this image, with smooth transition-style " | |
| "movement, clean framing, and social media friendly pacing." | |
| ), | |
| "Slow 3D Push-In": ( | |
| "Animate this image with a slow 3D push-in effect, subtle parallax, realistic depth, " | |
| "and smooth cinematic camera movement." | |
| ), | |
| "Floating Camera Tour": ( | |
| "Animate this image as if the camera is floating gently through the scene, with smooth " | |
| "motion, realistic depth, and professional video pacing." | |
| ), | |
| # Coffee niche presets | |
| "Steam Rising": ( | |
| "Animate this coffee image with gentle steam rising, warm morning light, soft background " | |
| "movement, and a cozy fresh-brewed feeling." | |
| ), | |
| "Beans Falling": ( | |
| "Animate this coffee product photo with coffee beans gently falling around the product, " | |
| "subtle camera movement, warm lighting, and a polished ad style." | |
| ), | |
| "Cozy Morning Table": ( | |
| "Animate this image into a cozy morning coffee scene with soft sunlight, gentle camera " | |
| "push-in, subtle steam, and peaceful lifestyle motion." | |
| ), | |
| "Bag Rotates Slightly": ( | |
| "Animate this coffee bag with a slight product rotation, subtle studio lighting changes, " | |
| "smooth camera motion, and a premium product showcase feel." | |
| ), | |
| "Claymation Coffee Ad": ( | |
| "Animate this coffee product photo in a playful claymation-inspired style with handmade " | |
| "texture, gentle product movement, warm colors, and a fun social media ad feel." | |
| ), | |
| "Pour-Over Scene": ( | |
| "Animate this coffee image with a slow pour-over inspired motion, warm light, subtle " | |
| "steam, and smooth cinematic movement." | |
| ), | |
| "Product Hero Shot": ( | |
| "Animate this coffee product as a premium hero shot with elegant lighting, slow push-in, " | |
| "subtle background movement, and a clean commercial look." | |
| ), | |
| } | |
| ALL_PRESETS = list(PRESET_PROMPTS.keys()) | |
| # Map presets to motion types for the Ken Burns fallback | |
| PRESET_MOTION_MAP = { | |
| "Product Spotlight": "zoom_in", | |
| "Coffee Bag Commercial": "push_in", | |
| "New Home Cinematic Pan": "pan_right", | |
| "Claymation-Style Motion": "zoom_in_wobble", | |
| "Social Media Ad Zoom": "zoom_in_fast", | |
| "Real Estate Listing Reveal": "pan_right_zoom", | |
| "Cozy Lifestyle Animation": "drift", | |
| "Before-and-After Reveal": "pan_left", | |
| "Slow 3D Push-In": "push_in", | |
| "Floating Camera Tour": "float_through", | |
| "Steam Rising": "drift_up", | |
| "Beans Falling": "zoom_in", | |
| "Cozy Morning Table": "push_in", | |
| "Bag Rotates Slightly": "drift", | |
| "Claymation Coffee Ad": "zoom_in_wobble", | |
| "Pour-Over Scene": "drift_up", | |
| "Product Hero Shot": "push_in", | |
| } | |
| # Preset categories for caption/hashtag generation | |
| COFFEE_PRESETS = { | |
| "Coffee Bag Commercial", "Steam Rising", "Beans Falling", | |
| "Cozy Morning Table", "Bag Rotates Slightly", "Claymation Coffee Ad", | |
| "Pour-Over Scene", "Product Hero Shot", | |
| } | |
| REAL_ESTATE_PRESETS = { | |
| "New Home Cinematic Pan", "Real Estate Listing Reveal", "Floating Camera Tour", | |
| } | |
| # Aspect ratio to resolution mapping | |
| ASPECT_RATIOS = { | |
| "1:1": (512, 512), | |
| "4:5": (512, 640), | |
| "9:16": (576, 1024), | |
| "16:9": (1024, 576), | |
| } | |
| MOTION_STRENGTH_MAP = {"Low": 0.4, "Medium": 0.7, "High": 1.0} | |
| FPS = 24 | |
| # --------------------------------------------------------------------------- | |
| # Ken Burns / demo video generation | |
| # --------------------------------------------------------------------------- | |
| def generate_ken_burns_video( | |
| image: Image.Image, | |
| target_w: int, | |
| target_h: int, | |
| num_frames: int, | |
| motion_type: str, | |
| strength: float, | |
| ) -> list: | |
| """Create a Ken Burns style animation from a still image.""" | |
| img = image.convert("RGB") | |
| # Scale image so we have room to pan/zoom | |
| padding = 1.4 | |
| scale = max(target_w / img.width, target_h / img.height) * padding | |
| new_w = int(img.width * scale) | |
| new_h = int(img.height * scale) | |
| img = img.resize((new_w, new_h), Image.LANCZOS) | |
| cx_start, cy_start = new_w / 2, new_h / 2 | |
| frames = [] | |
| for i in range(num_frames): | |
| t = i / max(num_frames - 1, 1) # 0..1 | |
| s = strength | |
| if motion_type == "zoom_in": | |
| zoom = 1.0 + t * 0.25 * s | |
| cx, cy = cx_start, cy_start | |
| elif motion_type == "zoom_in_fast": | |
| zoom = 1.0 + t * 0.4 * s | |
| cx, cy = cx_start, cy_start | |
| elif motion_type == "push_in": | |
| zoom = 1.0 + t * 0.2 * s | |
| cx = cx_start + t * 15 * s | |
| cy = cy_start - t * 10 * s | |
| elif motion_type == "pan_right": | |
| zoom = 1.0 + t * 0.05 * s | |
| cx = cx_start - (new_w * 0.08 * s) + t * (new_w * 0.16 * s) | |
| cy = cy_start | |
| elif motion_type == "pan_left": | |
| zoom = 1.0 + t * 0.05 * s | |
| cx = cx_start + (new_w * 0.08 * s) - t * (new_w * 0.16 * s) | |
| cy = cy_start | |
| elif motion_type == "pan_right_zoom": | |
| zoom = 1.0 + t * 0.15 * s | |
| cx = cx_start - (new_w * 0.06 * s) + t * (new_w * 0.12 * s) | |
| cy = cy_start | |
| elif motion_type == "drift": | |
| zoom = 1.0 + t * 0.1 * s | |
| cx = cx_start + np.sin(t * np.pi) * 20 * s | |
| cy = cy_start + np.cos(t * np.pi * 0.5) * 10 * s | |
| elif motion_type == "drift_up": | |
| zoom = 1.0 + t * 0.12 * s | |
| cx = cx_start | |
| cy = cy_start + (new_h * 0.04 * s) - t * (new_h * 0.08 * s) | |
| elif motion_type == "float_through": | |
| zoom = 1.0 + t * 0.18 * s | |
| cx = cx_start + np.sin(t * np.pi * 0.7) * 25 * s | |
| cy = cy_start - t * 15 * s | |
| elif motion_type == "zoom_in_wobble": | |
| zoom = 1.0 + t * 0.2 * s | |
| cx = cx_start + np.sin(t * np.pi * 3) * 8 * s | |
| cy = cy_start + np.cos(t * np.pi * 2) * 6 * s | |
| else: | |
| zoom = 1.0 + t * 0.15 * s | |
| cx, cy = cx_start, cy_start | |
| crop_w = target_w / zoom | |
| crop_h = target_h / zoom | |
| x1 = max(0, cx - crop_w / 2) | |
| y1 = max(0, cy - crop_h / 2) | |
| x2 = min(new_w, x1 + crop_w) | |
| y2 = min(new_h, y1 + crop_h) | |
| # Adjust if hitting edges | |
| if x2 - x1 < crop_w: | |
| x1 = max(0, x2 - crop_w) | |
| if y2 - y1 < crop_h: | |
| y1 = max(0, y2 - crop_h) | |
| cropped = img.crop((int(x1), int(y1), int(x2), int(y2))) | |
| frame = cropped.resize((target_w, target_h), Image.LANCZOS) | |
| # Add subtle vignette for cinematic feel | |
| frame_arr = np.array(frame, dtype=np.float32) | |
| rows, cols = frame_arr.shape[:2] | |
| Y, X = np.ogrid[:rows, :cols] | |
| center_y, center_x = rows / 2, cols / 2 | |
| dist = np.sqrt((X - center_x) ** 2 + (Y - center_y) ** 2) | |
| max_dist = np.sqrt(center_x**2 + center_y**2) | |
| vignette = 1.0 - 0.3 * (dist / max_dist) ** 2 | |
| frame_arr = frame_arr * vignette[:, :, np.newaxis] | |
| frame_arr = np.clip(frame_arr, 0, 255).astype(np.uint8) | |
| frames.append(frame_arr) | |
| return frames | |
| def create_demo_video( | |
| image: Image.Image, | |
| preset: str, | |
| duration: int, | |
| aspect_ratio: str, | |
| strength_label: str, | |
| seed: int, | |
| ) -> str: | |
| """Generate a demo video using Ken Burns effect and return the file path.""" | |
| random.seed(seed) | |
| np.random.seed(seed % (2**31)) | |
| target_w, target_h = ASPECT_RATIOS.get(aspect_ratio, (512, 512)) | |
| num_frames = duration * FPS | |
| motion_type = PRESET_MOTION_MAP.get(preset, "zoom_in") | |
| strength = MOTION_STRENGTH_MAP.get(strength_label, 0.7) | |
| frames = generate_ken_burns_video( | |
| image, target_w, target_h, num_frames, motion_type, strength | |
| ) | |
| # Add a subtle warm color grade for coffee/lifestyle presets | |
| if preset in COFFEE_PRESETS or preset in { | |
| "Cozy Lifestyle Animation", "Claymation-Style Motion" | |
| }: | |
| graded = [] | |
| for f in frames: | |
| f = f.astype(np.float32) | |
| f[:, :, 0] = np.clip(f[:, :, 0] * 1.04, 0, 255) # slight red boost | |
| f[:, :, 2] = np.clip(f[:, :, 2] * 0.95, 0, 255) # slight blue reduce | |
| graded.append(f.astype(np.uint8)) | |
| frames = graded | |
| # Write video | |
| tmp = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) | |
| tmp_path = tmp.name | |
| tmp.close() | |
| writer = imageio.get_writer(tmp_path, fps=FPS, codec="libx264", | |
| quality=8, pixelformat="yuv420p") | |
| for frame in frames: | |
| writer.append_data(frame) | |
| writer.close() | |
| return tmp_path | |
| # --------------------------------------------------------------------------- | |
| # Attempt real model inference via HF Inference API | |
| # --------------------------------------------------------------------------- | |
| def try_inference_api(image: Image.Image, prompt: str, seed: int) -> Optional[str]: | |
| """Try to generate video using Hugging Face Inference API. Returns path or None.""" | |
| try: | |
| from huggingface_hub import InferenceClient | |
| token = os.environ.get("HF_TOKEN") | |
| if not token: | |
| return None | |
| client = InferenceClient(token=token) | |
| # Resize image for the API | |
| img = image.convert("RGB").resize((1024, 576), Image.LANCZOS) | |
| tmp_img = tempfile.NamedTemporaryFile(suffix=".png", delete=False) | |
| img.save(tmp_img.name) | |
| tmp_img.close() | |
| result = client.image_to_video( | |
| tmp_img.name, | |
| model="stabilityai/stable-video-diffusion-img2vid-xt", | |
| ) | |
| tmp_vid = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) | |
| tmp_vid.write(result) | |
| tmp_vid.close() | |
| os.unlink(tmp_img.name) | |
| return tmp_vid.name | |
| except Exception: | |
| return None | |
| # --------------------------------------------------------------------------- | |
| # Caption, hashtag, and ad headline generators | |
| # --------------------------------------------------------------------------- | |
| CAPTION_TEMPLATES = { | |
| "coffee": [ | |
| "Nothing beats that first sip. Fresh roasted, always ready.", | |
| "Small batch. Big flavor. Your morning just got better.", | |
| "Crafted with care, brewed with love. Try our latest roast.", | |
| "Start your day the right way. Premium coffee, delivered fresh.", | |
| "From bean to cup, every detail matters. Taste the difference.", | |
| "Good mornings start here. Freshly roasted, small-batch perfection.", | |
| ], | |
| "real_estate": [ | |
| "Welcome home. This stunning property is ready for its new owner.", | |
| "New listing alert! Schedule your tour today.", | |
| "Dream home, meet dream buyer. Now available.", | |
| "Modern living at its finest. Explore this beautiful new build.", | |
| "Just listed! This one won't last long. DM for details.", | |
| "Your next chapter starts here. Tour this beauty today.", | |
| ], | |
| "product": [ | |
| "New drop alert! Check out our latest must-have.", | |
| "Designed for you. Shop the collection now.", | |
| "Quality you can see, style you can feel. Now available.", | |
| "Your new favorite is here. Link in bio to shop.", | |
| "Elevate your everyday. Discover what's new in store.", | |
| "Made with intention. Built to last. Shop now.", | |
| ], | |
| "lifestyle": [ | |
| "The little moments make the biggest memories.", | |
| "Creating spaces that feel like home.", | |
| "Life's better when you slow down and enjoy the details.", | |
| "Curated vibes for your everyday. Follow for more inspo.", | |
| "Find beauty in the simple things.", | |
| "Living intentionally, one moment at a time.", | |
| ], | |
| } | |
| HASHTAG_SETS = { | |
| "coffee": [ | |
| "#coffee", "#coffeelover", "#coffeetime", "#morningcoffee", | |
| "#smallbatchcoffee", "#coffeeaddict", "#freshroasted", "#coffeeshop", | |
| "#barista", "#smallbusiness", "#shopsmall", "#coffeegram", | |
| "#coffeeroaster", "#productphotography", "#socialmediamarketing", | |
| ], | |
| "real_estate": [ | |
| "#realestate", "#newhome", "#newconstruction", "#justlisted", | |
| "#dreamhome", "#hometour", "#realtor", "#househunting", | |
| "#homeforsale", "#luxuryhomes", "#openhouse", "#property", | |
| "#homebuyers", "#realestateagent", "#localrealestate", | |
| ], | |
| "product": [ | |
| "#product", "#shopsmall", "#smallbusiness", "#newproduct", | |
| "#productphotography", "#ecommerce", "#shoplocal", "#handmade", | |
| "#branddesign", "#socialmediamarketing", "#entrepreneur", | |
| "#instashop", "#productlaunch", | |
| ], | |
| "lifestyle": [ | |
| "#lifestyle", "#aesthetic", "#vibes", "#inspo", | |
| "#dailyinspiration", "#cozy", "#minimal", "#homedecor", | |
| "#slowliving", "#intentionalliving", "#contentcreator", | |
| "#socialmedia", "#creator", | |
| ], | |
| } | |
| AD_HEADLINES = { | |
| "coffee": [ | |
| "Freshly Roasted. Delivered to Your Door.", | |
| "Your Morning Deserves Better Coffee.", | |
| "Small Batch. Big Flavor. Order Now.", | |
| "Premium Coffee for Coffee Lovers.", | |
| "Start Every Morning Right.", | |
| "Taste the Craft in Every Cup.", | |
| ], | |
| "real_estate": [ | |
| "Your Dream Home Awaits.", | |
| "New Listing. Schedule a Tour Today.", | |
| "Modern Living Starts Here.", | |
| "Find Your Perfect Home Now.", | |
| "Luxury Meets Comfort. Explore Today.", | |
| "New Build. Move-In Ready.", | |
| ], | |
| "product": [ | |
| "New Arrivals Just Dropped. Shop Now.", | |
| "Quality Crafted for Everyday Life.", | |
| "Discover Your New Favorite Product.", | |
| "Designed for You. Built to Last.", | |
| "Shop the Latest Collection Today.", | |
| "Upgrade Your Everyday Essentials.", | |
| ], | |
| "lifestyle": [ | |
| "Elevate Your Everyday Routine.", | |
| "Live Better. Feel Better.", | |
| "Curated for the Life You Love.", | |
| "Simple. Beautiful. Intentional.", | |
| "Create Your Perfect Space.", | |
| "Inspire Your Next Chapter.", | |
| ], | |
| } | |
| def get_category(preset: str) -> str: | |
| if preset in COFFEE_PRESETS: | |
| return "coffee" | |
| if preset in REAL_ESTATE_PRESETS: | |
| return "real_estate" | |
| if preset in {"Product Spotlight", "Social Media Ad Zoom", "Before-and-After Reveal"}: | |
| return "product" | |
| return "lifestyle" | |
| def generate_caption(preset: str, seed: int) -> str: | |
| rng = random.Random(seed) | |
| category = get_category(preset) | |
| templates = CAPTION_TEMPLATES.get(category, CAPTION_TEMPLATES["lifestyle"]) | |
| return rng.choice(templates) | |
| def generate_hashtags(preset: str, seed: int) -> str: | |
| rng = random.Random(seed + 1) | |
| category = get_category(preset) | |
| pool = HASHTAG_SETS.get(category, HASHTAG_SETS["lifestyle"]) | |
| count = rng.randint(8, 12) | |
| selected = rng.sample(pool, min(count, len(pool))) | |
| return " ".join(selected) | |
| def generate_ad_headlines(preset: str, seed: int) -> str: | |
| rng = random.Random(seed + 2) | |
| category = get_category(preset) | |
| pool = AD_HEADLINES.get(category, AD_HEADLINES["lifestyle"]) | |
| selected = rng.sample(pool, min(3, len(pool))) | |
| lines = [f"{i+1}. {h}" for i, h in enumerate(selected)] | |
| return "\n".join(lines) | |
| # --------------------------------------------------------------------------- | |
| # Main generation pipeline | |
| # --------------------------------------------------------------------------- | |
| def generate_video( | |
| image, | |
| preset: str, | |
| custom_prompt: str, | |
| video_length: str, | |
| aspect_ratio: str, | |
| motion_strength: str, | |
| seed_value, | |
| progress=gr.Progress(), | |
| ): | |
| """Main generation function called by the Gradio interface.""" | |
| if image is None: | |
| raise gr.Error("Please upload an image before generating a video.") | |
| if not preset: | |
| raise gr.Error("Please select a motion style preset.") | |
| # Parse inputs | |
| duration = int(video_length.split()[0]) | |
| seed_val = int(seed_value) if seed_value else 0 | |
| seed = seed_val if seed_val > 0 else random.randint(1, 999999) | |
| # Build the prompt | |
| base_prompt = PRESET_PROMPTS.get(preset, "") | |
| if custom_prompt and custom_prompt.strip(): | |
| full_prompt = f"{base_prompt} {custom_prompt.strip()}" | |
| else: | |
| full_prompt = base_prompt | |
| pil_image = Image.fromarray(image) if isinstance(image, np.ndarray) else image | |
| progress(0.1, desc="Preparing image...") | |
| # Try real inference first | |
| progress(0.2, desc="Attempting AI video generation...") | |
| video_path = try_inference_api(pil_image, full_prompt, seed) | |
| if video_path is None: | |
| # Fallback to demo mode | |
| progress(0.3, desc="Using motion effect generator...") | |
| progress(0.5, desc="Generating frames...") | |
| video_path = create_demo_video( | |
| pil_image, preset, duration, aspect_ratio, motion_strength, seed | |
| ) | |
| progress(0.8, desc="Generating caption and hashtags...") | |
| caption = generate_caption(preset, seed) | |
| hashtags = generate_hashtags(preset, seed) | |
| headlines = generate_ad_headlines(preset, seed) | |
| progress(0.95, desc="Finalizing...") | |
| info_text = ( | |
| f"Preset: {preset}\n" | |
| f"Duration: {duration}s | Aspect Ratio: {aspect_ratio} | " | |
| f"Motion Strength: {motion_strength}\n" | |
| f"Seed: {seed}\n" | |
| f"Prompt: {full_prompt[:120]}..." | |
| ) | |
| progress(1.0, desc="Done!") | |
| return video_path, video_path, caption, hashtags, headlines, info_text | |
| # --------------------------------------------------------------------------- | |
| # Build presets reference text | |
| # --------------------------------------------------------------------------- | |
| def build_presets_reference() -> str: | |
| sections = [] | |
| sections.append("## Main Motion Style Presets\n") | |
| main_presets = [ | |
| "Product Spotlight", "Coffee Bag Commercial", "New Home Cinematic Pan", | |
| "Claymation-Style Motion", "Social Media Ad Zoom", "Real Estate Listing Reveal", | |
| "Cozy Lifestyle Animation", "Before-and-After Reveal", "Slow 3D Push-In", | |
| "Floating Camera Tour", | |
| ] | |
| for p in main_presets: | |
| sections.append(f"**{p}**\n{PRESET_PROMPTS[p]}\n") | |
| sections.append("\n---\n## Coffee Niche Presets\n") | |
| coffee_presets = [ | |
| "Steam Rising", "Beans Falling", "Cozy Morning Table", | |
| "Bag Rotates Slightly", "Claymation Coffee Ad", "Pour-Over Scene", | |
| "Product Hero Shot", | |
| ] | |
| for p in coffee_presets: | |
| sections.append(f"**{p}**\n{PRESET_PROMPTS[p]}\n") | |
| return "\n".join(sections) | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # --------------------------------------------------------------------------- | |
| CUSTOM_CSS = """ | |
| .main-title { | |
| text-align: center; | |
| margin-bottom: 0; | |
| } | |
| .subtitle { | |
| text-align: center; | |
| color: #666; | |
| font-size: 1.1em; | |
| margin-top: 0; | |
| margin-bottom: 20px; | |
| } | |
| footer { | |
| text-align: center; | |
| padding: 20px; | |
| color: #999; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="PhotoMotion Studio", | |
| css=CUSTOM_CSS, | |
| theme=gr.themes.Soft( | |
| primary_hue="indigo", | |
| secondary_hue="slate", | |
| ), | |
| ) as demo: | |
| gr.Markdown( | |
| "# PhotoMotion Studio", | |
| elem_classes=["main-title"], | |
| ) | |
| gr.Markdown( | |
| "Upload a photo. Choose a motion style. Get a short social video.", | |
| elem_classes=["subtitle"], | |
| ) | |
| with gr.Tabs(): | |
| # ---- Tab 1: Generate Video ---- | |
| with gr.TabItem("Generate Video"): | |
| with gr.Row(): | |
| # Left column: inputs | |
| with gr.Column(scale=1): | |
| image_input = gr.Image( | |
| label="Upload Image", | |
| type="pil", | |
| height=300, | |
| ) | |
| preset_dropdown = gr.Dropdown( | |
| choices=ALL_PRESETS, | |
| value="Product Spotlight", | |
| label="Motion Style Preset", | |
| info="Choose a preset that matches your content.", | |
| ) | |
| custom_prompt = gr.Textbox( | |
| label="Custom Motion Prompt (Optional)", | |
| placeholder="Add extra motion details here, e.g. 'with golden hour lighting and slow rotation'", | |
| lines=2, | |
| ) | |
| with gr.Row(): | |
| video_length = gr.Dropdown( | |
| choices=["4 seconds", "5 seconds", "6 seconds"], | |
| value="4 seconds", | |
| label="Video Length", | |
| ) | |
| aspect_ratio = gr.Dropdown( | |
| choices=["1:1", "4:5", "9:16", "16:9"], | |
| value="1:1", | |
| label="Aspect Ratio", | |
| ) | |
| with gr.Row(): | |
| motion_strength = gr.Radio( | |
| choices=["Low", "Medium", "High"], | |
| value="Medium", | |
| label="Motion Strength", | |
| ) | |
| seed_input = gr.Number( | |
| label="Seed (for repeatable results)", | |
| value=0, | |
| precision=0, | |
| info="Set to 0 for random. Use a specific number to reproduce results.", | |
| ) | |
| generate_btn = gr.Button( | |
| "Generate Video", | |
| variant="primary", | |
| size="lg", | |
| ) | |
| # Right column: outputs | |
| with gr.Column(scale=1): | |
| video_output = gr.Video( | |
| label="Generated Video", | |
| height=400, | |
| ) | |
| download_file = gr.File( | |
| label="Download Video", | |
| ) | |
| generation_info = gr.Textbox( | |
| label="Generation Info", | |
| interactive=False, | |
| lines=4, | |
| ) | |
| # Caption, hashtags, headlines below | |
| with gr.Row(): | |
| with gr.Column(): | |
| caption_output = gr.Textbox( | |
| label="Suggested Social Media Caption", | |
| interactive=False, | |
| lines=2, | |
| ) | |
| with gr.Column(): | |
| hashtag_output = gr.Textbox( | |
| label="Suggested Hashtags", | |
| interactive=False, | |
| lines=2, | |
| ) | |
| with gr.Column(): | |
| headline_output = gr.Textbox( | |
| label="Ad Headlines", | |
| interactive=False, | |
| lines=3, | |
| ) | |
| generate_btn.click( | |
| fn=generate_video, | |
| inputs=[ | |
| image_input, | |
| preset_dropdown, | |
| custom_prompt, | |
| video_length, | |
| aspect_ratio, | |
| motion_strength, | |
| seed_input, | |
| ], | |
| outputs=[ | |
| video_output, | |
| download_file, | |
| caption_output, | |
| hashtag_output, | |
| headline_output, | |
| generation_info, | |
| ], | |
| ) | |
| # ---- Tab 2: Prompt Presets ---- | |
| with gr.TabItem("Prompt Presets"): | |
| gr.Markdown("# Motion Style Preset Library") | |
| gr.Markdown( | |
| "Browse all available presets and their prompts below. " | |
| "You can copy any prompt text and paste it into the custom prompt field " | |
| "on the Generate Video tab for further customization." | |
| ) | |
| gr.Markdown(build_presets_reference()) | |
| # ---- Tab 3: Caption + Hashtags ---- | |
| with gr.TabItem("Caption + Hashtags"): | |
| gr.Markdown("# Caption and Hashtag Guide") | |
| gr.Markdown( | |
| "PhotoMotion Studio automatically generates captions, hashtags, and ad " | |
| "headlines when you create a video. Here's how they work:" | |
| ) | |
| gr.Markdown( | |
| """ | |
| ## Captions | |
| Captions are tailored to your selected preset category: | |
| - **Coffee presets** generate warm, inviting captions about fresh roasts and morning routines. | |
| - **Real estate presets** generate professional listing-style captions. | |
| - **Product presets** generate shop-ready captions with calls to action. | |
| - **Lifestyle presets** generate aesthetic, relatable captions. | |
| ## Hashtags | |
| 8-12 relevant hashtags are generated based on your content niche: | |
| - **Coffee**: #coffee, #coffeelover, #smallbatchcoffee, #shopsmall, and more. | |
| - **Real Estate**: #realestate, #newhome, #justlisted, #hometour, and more. | |
| - **Product**: #product, #ecommerce, #shoplocal, #productlaunch, and more. | |
| - **Lifestyle**: #lifestyle, #aesthetic, #cozy, #intentionalliving, and more. | |
| ## Ad Headlines | |
| 3 short ad headlines (under 10 words each) are generated, suitable for: | |
| - Facebook Ads | |
| - Instagram Promoted Posts | |
| - TikTok Spark Ads | |
| ## Tips | |
| - Use the **seed** field to regenerate with different caption/hashtag combinations. | |
| - Copy and customize the generated text to match your brand voice. | |
| - Pair the video with the caption for a complete social media post. | |
| """ | |
| ) | |
| # ---- Tab 4: About ---- | |
| with gr.TabItem("About"): | |
| gr.Markdown( | |
| """ | |
| # About PhotoMotion Studio | |
| **PhotoMotion Studio** is a simple photo-to-video generator designed for | |
| small businesses, creators, real estate teams, and product brands. | |
| ## How It Works | |
| 1. **Upload** a still image (product photo, home exterior, coffee bag, etc.). | |
| 2. **Choose** a motion style preset that matches your content. | |
| 3. **Customize** with an optional prompt and adjust settings. | |
| 4. **Generate** a short 4-6 second social media video. | |
| 5. **Download** your video and copy the suggested caption and hashtags. | |
| ## Use Cases | |
| - **Coffee brands**: Turn product photos into cozy animated ads. | |
| - **Real estate agents**: Create cinematic listing videos from photos. | |
| - **E-commerce stores**: Make product spotlight videos for social media. | |
| - **Content creators**: Generate engaging lifestyle content quickly. | |
| - **Small businesses**: Create professional-looking social videos without a video team. | |
| ## Supported Platforms | |
| Your generated videos are optimized for: | |
| - Instagram Reels (9:16, 1:1) | |
| - TikTok (9:16) | |
| - Facebook Ads (1:1, 16:9) | |
| - YouTube Shorts (9:16) | |
| - Shopify product pages (1:1, 16:9) | |
| - Email marketing (16:9) | |
| ## Technical Details | |
| - Videos are generated at 24 FPS. | |
| - The app uses AI-powered video generation when available, with a Ken Burns | |
| motion effect as a reliable fallback. | |
| - All presets include carefully crafted motion prompts optimized for each | |
| content niche. | |
| ## Future Features | |
| - Logo and text overlays | |
| - Brand color customization | |
| - Music suggestions | |
| - Batch video generation | |
| - Shopify product video mode | |
| - Export presets per platform | |
| --- | |
| Built with Gradio on Hugging Face Spaces. | |
| """ | |
| ) | |
| gr.Markdown( | |
| "<center style='color: #999; padding: 10px;'>" | |
| "PhotoMotion Studio v1.0 | Built for small businesses and creators" | |
| "</center>" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |