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():
# Initialize session state variables
if 'page' not in st.session_state:
st.session_state.page = 'input' # States: 'input', 'processing', 'result'
if 'temp_dir' not in st.session_state:
st.session_state.temp_dir = None
if 'final_video_path' not in st.session_state:
st.session_state.final_video_path = None
if 'process_started' not in st.session_state:
st.session_state.process_started = False
# Function to switch to processing page and start morphing
def start_processing():
st.session_state.page = 'processing'
st.session_state.process_started = False # Will be set to True when processing starts
# Function to return to input page
def return_to_input():
st.session_state.page = 'input'
st.session_state.temp_dir = None
st.session_state.final_video_path = None
st.session_state.process_started = False
# ---------------- 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
# Load logo path for all pages
logo_path = "metamorphLogo_nobg.png"
logo_exists = os.path.exists(logo_path)
logo_base64 = None
if logo_exists:
try:
logo = Image.open(logo_path)
logo_base64 = get_img_as_base64(logo)
except Exception as e:
st.warning(f"Logo could not be loaded: {e}")
# =============== INPUT PAGE ===============
if st.session_state.page == 'input':
# Display centered logo and title for input page
if logo_exists and logo_base64:
st.markdown(
f"""
""",
unsafe_allow_html=True
)
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 descriptions, and fine-tune the settings to create a smooth, high-quality morphing video.
For further information on how to configure the parameters, please refer to the User Documentation.
""",
unsafe_allow_html=True
)
# ---------------- SECTION 1: IMAGE & PROMPT INPUTS ----------------
st.subheader("1. Upload Source Images & Prompts")
st.markdown("**Note:** Your uploaded images must be of similar topology and same size to achieve the best results.")
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("Short Description for Image A (optional)", value="", key="promptA",
help="For added interpolation between the two descriptions")
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("Short Description for Image B (optional)", value="", key="promptB",
help="For added interpolation between the two descriptions")
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"
)
# Determine preset defaults based on selection
if preset_option.startswith("Maximum quality"):
preset_model = "Base Stable Diffusion V2-1"
preset_film = False # Changed to False as FILM is disabled
preset_lcm = False
preset_frames = 48 # Increased for maximum quality
preset_fps = 16 # Increased for maximum quality
elif preset_option.startswith("Medium quality"):
preset_model = "Base Stable Diffusion V2-1"
preset_film = False
preset_lcm = False
preset_frames = 24 # Default frame count
preset_fps = 10 # Default FPS
elif preset_option.startswith("Low quality"):
preset_model = "Base Stable Diffusion V2-1"
preset_film = False
preset_lcm = True
preset_frames = 24 # Default frame count
preset_fps = 10 # Default FPS
elif preset_option.startswith("Creative morph"):
preset_model = "Dreamshaper-7 (fine-tuned SD V1-5)"
preset_film = False # Changed to False as FILM is disabled
preset_lcm = True
preset_frames = 24 # Default frame count
preset_fps = 10 # Default FPS
else:
# "Custom"
preset_model = None
preset_film = None
preset_lcm = None
preset_frames = None
preset_fps = 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")
# Set default based on preset
default_frames = preset_frames if preset_frames is not None else 24
num_frames = st.number_input("Number of keyframes (2–50)", min_value=2, max_value=50, value=default_frames)
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")
# Disabled FILM checkbox with warning message
st.markdown(
"""
Use FILM interpolation
""",
unsafe_allow_html=True
)
# Always set use_film to False since it's disabled
use_film = False
# Disabled FILM recursion parameter with warning message
st.markdown(
"""
FILM recursion passes (1–6)
""",
unsafe_allow_html=True
)
st.info("Unfortunately, FILM is not available for use on the HF Demo, please select other choices.")
film_recursions = 3 # placeholder value, but it won't be used since FILM is disabled
# Set default FPS based on preset
default_fps = preset_fps if preset_fps is not None else 10
output_fps = st.number_input("Output FPS (1–60)", min_value=1, max_value=60, 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.")
# Create a container for the run button
run_container = st.container()
with run_container:
# Save values to session state so we can access them in the processing page
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.")
else:
# Save all settings to session state
st.session_state.uploaded_image_A = uploaded_image_A
st.session_state.uploaded_image_B = uploaded_image_B
st.session_state.prompt_A = prompt_A
st.session_state.prompt_B = prompt_B
st.session_state.model_option = model_option
st.session_state.num_frames = num_frames
st.session_state.enable_lcm_lora = enable_lcm_lora
st.session_state.use_adain = use_adain
st.session_state.use_reschedule = use_reschedule
st.session_state.use_film = use_film # Always False now
st.session_state.film_recursions = film_recursions
st.session_state.output_fps = output_fps
# Switch to processing page
start_processing()
st.rerun()
# =============== PROCESSING PAGE ===============
elif st.session_state.page == 'processing':
# Display centered logo for processing page
if logo_exists and logo_base64:
st.markdown(
f"""
""",
unsafe_allow_html=True
)
st.markdown("
Metamorph Web App
", unsafe_allow_html=True)
st.markdown(
"""
Processing Your Morphing Request
Please wait while we generate your morphing video...
""",
unsafe_allow_html=True
)
# Use a progress bar for visual feedback
progress_bar = st.progress(0)
# Only start processing if not already started
if not st.session_state.process_started:
st.session_state.process_started = True
# Create a temporary folder for processing
temp_dir = create_temp_folder()
st.session_state.temp_dir = temp_dir
try:
# Update progress
progress_bar.progress(10)
# Extract variables from session state
uploaded_image_A = st.session_state.uploaded_image_A
uploaded_image_B = st.session_state.uploaded_image_B
prompt_A = st.session_state.prompt_A
prompt_B = st.session_state.prompt_B
model_option = st.session_state.model_option
num_frames = st.session_state.num_frames
enable_lcm_lora = st.session_state.enable_lcm_lora
use_adain = st.session_state.use_adain
use_reschedule = st.session_state.use_reschedule
use_film = st.session_state.use_film # Always False now
film_recursions = st.session_state.film_recursions
output_fps = st.session_state.output_fps
# 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)
# Update progress
progress_bar.progress(20)
# Create output directories
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
output_dir = os.path.join(temp_dir, f"morph_results_{timestamp}")
film_output_dir = os.path.join(temp_dir, f"film_output_{timestamp}")
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"
)
# Update progress
progress_bar.progress(30)
# 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: # disabled, no cudnn on hf
cmd.append("--use_film")
# Add film recursion parameter
cmd.extend(["--film_num_recursions", str(film_recursions)])
# Run the morphing process
try:
# Update progress - processing takes the longest
progress_bar.progress(40)
subprocess.run(cmd, check=True)
# Update progress
progress_bar.progress(90)
# 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.session_state.final_video_path = final_video_path
st.session_state.page = 'result'
progress_bar.progress(100)
st.rerun()
else:
st.error("No output video was generated. Check logs for details.")
except subprocess.CalledProcessError as e:
st.error(f"Error running morphing pipeline: {e}")
except Exception as e:
st.error(f"An error occurred during processing: {e}")
# =============== RESULT PAGE ===============
elif st.session_state.page == 'result':
# Display left-aligned logo for results page (no title)
if logo_exists and logo_base64:
st.markdown(
f"""
Your morphing video has been successfully generated. You can download it below.
""",
unsafe_allow_html=True
)
# Show the result video and download button
try:
if st.session_state.final_video_path:
# Display video preview
video_file = open(st.session_state.final_video_path, 'rb')
video_bytes = video_file.read()
video_file.close()
# st.video(video_bytes)
# Download button
st.download_button(
"Download Morphing 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}")
# Button to start a new project
if st.button("Start New Morphing Project"):
return_to_input()
st.rerun()
if __name__ == "__main__":
main()