""" 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 # --------------------------------------------------------------------------- @spaces.GPU 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( "
" "PhotoMotion Studio v1.0 | Built for small businesses and creators" "
" ) if __name__ == "__main__": demo.launch()