import os import sys import subprocess import base64 import datetime from io import BytesIO import streamlit as st from PIL import Image # Set Streamlit page configuration (centered content via CSS) st.set_page_config( page_title="Metamorph: DiffMorpher + LCM-LoRA + FILM", layout="wide", page_icon="🌀" ) def save_uploaded_file(uploaded_file, dst_path): """Save an uploaded file to a destination path.""" with open(dst_path, "wb") as f: f.write(uploaded_file.getbuffer()) def get_img_as_base64(img): """Convert PIL Image to base64 for embedding in HTML.""" buffered = BytesIO() img.save(buffered, format="PNG") return base64.b64encode(buffered.getvalue()).decode("utf-8") def ensure_scripts_exist(): """Check if the required script files exist.""" required_scripts = ["run_morphing.py", "FILM.py"] missing_scripts = [script for script in required_scripts if not os.path.exists(script)] if missing_scripts: error_msg = f"Missing required script(s): {', '.join(missing_scripts)}" return False, error_msg return True, "" def create_temp_folder(): """Create a persistent temporary folder in the repo for processing.""" base_folder = os.path.join(os.getcwd(), "temp_run") os.makedirs(base_folder, exist_ok=True) # Create a subfolder with a timestamp to avoid collisions run_folder = os.path.join(base_folder, datetime.datetime.now().strftime("run_%Y%m%d_%H%M%S")) os.makedirs(run_folder) return run_folder def main(): # ---------------- CUSTOM CSS FOR A PROFESSIONAL, DARK THEME ---------------- st.markdown( """ """, unsafe_allow_html=True ) # Check if required scripts exist scripts_exist, error_msg = ensure_scripts_exist() if not scripts_exist: st.error(error_msg) st.error("Please make sure all required scripts are in the same directory as this Streamlit app.") return # ---------------- HEADER & LOGO ---------------- logo_path = "metamorphLogo_nobg.png" if os.path.exists(logo_path): try: logo = Image.open(logo_path) logo_base64 = get_img_as_base64(logo) st.markdown( f"""
Metamorph Logo
""", unsafe_allow_html=True ) except Exception as e: st.warning(f"Logo could not be loaded: {e}") st.markdown("

Metamorph Web App

", unsafe_allow_html=True) st.markdown( """

DiffMorpher is used for keyframe generation by default, with FILM for interpolation. Optionally, you can enable LCM-LoRA for accelerated inference (with slight decrease in quality). Upload two images, optionally provide textual prompts, and fine-tune the settings to create a smooth, high-quality morphing video.


""", unsafe_allow_html=True ) # ---------------- SECTION 1: IMAGE & PROMPT INPUTS ---------------- st.subheader("1. Upload Source Images & Prompts") col_imgA, col_imgB = st.columns(2) with col_imgA: st.markdown("#### Image A") uploaded_image_A = st.file_uploader("Upload your first image", type=["png", "jpg", "jpeg"], key="imgA") if uploaded_image_A is not None: st.image(uploaded_image_A, caption="Preview - Image A", use_container_width=True) prompt_A = st.text_input("Prompt for Image A (optional)", value="", key="promptA") with col_imgB: st.markdown("#### Image B") uploaded_image_B = st.file_uploader("Upload your second image", type=["png", "jpg", "jpeg"], key="imgB") if uploaded_image_B is not None: st.image(uploaded_image_B, caption="Preview - Image B", use_container_width=True) prompt_B = st.text_input("Prompt for Image B (optional)", value="", key="promptB") st.markdown("
", unsafe_allow_html=True) # ---------------- SECTION 2: CONFIGURE MORPHING PIPELINE ---------------- st.subheader("2. Configure Morphing Pipeline") st.markdown( """

Select a preset below to automatically adjust quality and inference time. If you choose Custom ⚙️, the advanced settings will automatically expand so you can fine-tune the configuration.

""", unsafe_allow_html=True ) # Preset Options (Dropdown) st.markdown("**Preset Options**") preset_option = st.selectbox( "Select a preset for quality and inference time", options=[ "Maximum quality, longest inference time 🏆", "Medium quality, medium inference time ⚖️", "Low quality, shortest inference time ⚡", "Creative morph 🎨", "Custom ⚙️" ], index=0, label_visibility="collapsed" # Hide the label in the UI but keep it for accessibility ) # Determine preset defaults based on selection if preset_option.startswith("Maximum quality"): preset_model = "Base Stable Diffusion V2-1" preset_film = True preset_lcm = False elif preset_option.startswith("Medium quality"): preset_model = "Base Stable Diffusion V2-1" preset_film = False preset_lcm = False elif preset_option.startswith("Low quality"): preset_model = "Base Stable Diffusion V1-5" preset_film = False preset_lcm = True elif preset_option.startswith("Creative morph"): preset_model = "Dreamshaper-7 (fine-tuned SD V1-5)" preset_film = True preset_lcm = True else: # "Custom" preset_model = None preset_film = None preset_lcm = None advanced_expanded = True if preset_option.endswith("⚙️") else False # Advanced Options for fine-tuning with st.expander("Advanced Options", expanded=advanced_expanded): options_list = [ "Base Stable Diffusion V1-5", "Dreamshaper-7 (fine-tuned SD V1-5)", "Base Stable Diffusion V2-1" ] default_model = preset_model if preset_model is not None else "Base Stable Diffusion V1-5" default_index = options_list.index(default_model) model_option = st.selectbox("Select Model Card", options=options_list, index=default_index) col_left, col_right = st.columns(2) # Left Column: Keyframe Generator Parameters with col_left: st.markdown("##### Keyframe Generator Parameters") num_frames = st.number_input("Number of keyframes (2–50)", min_value=2, max_value=50, value=16) lcm_default = preset_lcm if preset_lcm is not None else False enable_lcm_lora = st.checkbox( "Enable LCM-LoRA", value=lcm_default, help="Accelerates inference with slight quality decrease" ) use_adain = st.checkbox("Use AdaIN", value=True, help="Adaptive Instance Normalization for improved generation") use_reschedule = st.checkbox("Use reschedule sampling", value=True, help="Better sampling strategy") # Right Column: Inter-frame Interpolator Parameters (FILM) with col_right: st.markdown("
", unsafe_allow_html=True) st.markdown("##### Inter-frame Interpolator Parameters") default_use_film = preset_film if preset_film is not None else True use_film = st.checkbox("Use FILM interpolation", value=default_use_film, help="Frame Interpolation for Large Motion - creates smooth transitions") film_recursions = st.number_input("FILM recursion passes (1–6)", min_value=1, max_value=6, value=3, help="Higher values create more intermediate frames (smoother but slower)") # Set default FPS based on whether FILM is enabled default_fps = 30 if use_film else 4 output_fps = st.number_input("Output FPS (1–120)", min_value=1, max_value=120, value=default_fps, help="Output video frames per second") st.markdown("
", unsafe_allow_html=True) st.markdown("
", unsafe_allow_html=True) # ---------------- SECTION 3: EXECUTE MORPH PIPELINE ---------------- st.subheader("3. Generate Morphing Video") st.markdown("Once satisfied with your inputs, click below to start the process.") # New checkbox for SLAB execution toggle # using_slab = st.checkbox("Using SLAB GPU Cluster?", value=False, help="If enabled, the pipeline command will be prefixed with SLAB cluster execution parameters.") if st.button("Run Morphing Pipeline", key="run_pipeline"): if not (uploaded_image_A and uploaded_image_B): st.error("Please upload both images before running the morphing pipeline.") return # Instead of using /tmp, create a folder in the repo for temporary processing. temp_dir = create_temp_folder() try: # Save uploaded images imgA_path = os.path.join(temp_dir, "imageA.png") imgB_path = os.path.join(temp_dir, "imageB.png") save_uploaded_file(uploaded_image_A, imgA_path) save_uploaded_file(uploaded_image_B, imgB_path) # Create output directories output_dir = os.path.join(temp_dir, "morph_results") film_output_dir = os.path.join(temp_dir, "film_output") os.makedirs(output_dir, exist_ok=True) os.makedirs(film_output_dir, exist_ok=True) actual_model_path = ( "lykon/dreamshaper-7" if model_option == "Dreamshaper-7 (fine-tuned SD V1-5)" else "stabilityai/stable-diffusion-2-1-base" if model_option == "Base Stable Diffusion V2-1" else "sd-legacy/stable-diffusion-v1-5" ) # Build the command for run_morphing.py cmd = [ sys.executable, "run_morphing.py", "--model_path", actual_model_path, "--image_path_0", imgA_path, "--image_path_1", imgB_path, "--prompt_0", prompt_A, "--prompt_1", prompt_B, "--output_path", output_dir, "--film_output_folder", film_output_dir, "--num_frames", str(num_frames), "--fps", str(output_fps) ] if enable_lcm_lora: cmd.append("--use_lcm") if use_adain: cmd.append("--use_adain") if use_reschedule: cmd.append("--use_reschedule") if use_film: cmd.append("--use_film") # Add film recursion parameter cmd.extend(["--film_num_recursions", str(film_recursions)]) # If SLAB execution is enabled, prepend the srun command prefix. # if using_slab: # slab_prefix = [ # "srun", "-p", "rtx3090_slab", "-w", "slabgpu05", "--gres=gpu:1", # "--job-name=test", "--kill-on-bad-exit=1" # ] # cmd = slab_prefix + cmd st.info("Initializing pipeline. This may take a few minutes...") progress_bar = st.progress(0) status_text = st.empty() # Update progress status for i in range(1, 11): status_text.text(f"Step {i}/10: {'Preparing images' if i <= 2 else 'Generating keyframes' if i <= 6 else 'Interpolating frames' if i <= 9 else 'Finalizing video'}") progress_bar.progress(i * 10) if i == 3: # Start processing! try: subprocess.run(cmd, check=True) except subprocess.CalledProcessError as e: st.error(f"Error running morphing pipeline: {e}") return break # Set to 100% when done progress_bar.progress(100) status_text.text("Processing complete!") # Check for output video video_found = False possible_outputs = [f for f in os.listdir(film_output_dir) if f.endswith(".mp4")] if possible_outputs: final_video_path = os.path.join(film_output_dir, possible_outputs[0]) video_found = True if not video_found: possible_outputs = [f for f in os.listdir(output_dir) if f.endswith(".mp4")] if possible_outputs: final_video_path = os.path.join(output_dir, possible_outputs[0]) video_found = True if video_found: st.success("Morphing complete! 🎉") # st.video(final_video_path) # Comment to remove video preview, buggy try: with open(final_video_path, "rb") as f: video_bytes = f.read() st.download_button( "Download Result Video", data=video_bytes, file_name="metamorph_result.mp4", mime="video/mp4" ) except Exception as e: st.error(f"Error preparing video for download: {e}") else: st.warning("No output video was generated. Check logs for details.") except Exception as e: st.error(f"An error occurred during processing: {e}") if __name__ == "__main__": main()