Spaces:
Running
Running
nalin0503
commited on
Commit
Β·
792f5e9
1
Parent(s):
8b4471b
add app.py and reqs.txt
Browse files- app.py +340 -0
- requirements.txt +16 -0
app.py
ADDED
@@ -0,0 +1,340 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
import subprocess
|
4 |
+
import tempfile
|
5 |
+
import base64
|
6 |
+
from io import BytesIO
|
7 |
+
|
8 |
+
import streamlit as st
|
9 |
+
from PIL import Image
|
10 |
+
|
11 |
+
# Set Streamlit page configuration (centered content via CSS)
|
12 |
+
st.set_page_config(
|
13 |
+
page_title="Metamorph: DiffMorpher + LCM-LoRA + FILM",
|
14 |
+
layout="wide",
|
15 |
+
page_icon="π"
|
16 |
+
)
|
17 |
+
|
18 |
+
def save_uploaded_file(uploaded_file, dst_path):
|
19 |
+
with open(dst_path, "wb") as f:
|
20 |
+
f.write(uploaded_file.getbuffer())
|
21 |
+
|
22 |
+
def get_img_as_base64(img):
|
23 |
+
buffered = BytesIO()
|
24 |
+
img.save(buffered, format="PNG")
|
25 |
+
return base64.b64encode(buffered.getvalue()).decode("utf-8")
|
26 |
+
|
27 |
+
def main():
|
28 |
+
# ---------------- CUSTOM CSS FOR A PROFESSIONAL, DARK THEME ----------------
|
29 |
+
st.markdown(
|
30 |
+
"""
|
31 |
+
<style>
|
32 |
+
/* Import Google Font */
|
33 |
+
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
34 |
+
|
35 |
+
/* Global styling */
|
36 |
+
body {
|
37 |
+
font-family: 'Roboto', sans-serif;
|
38 |
+
color: #f1f1f1;
|
39 |
+
}
|
40 |
+
h1, h2, h3, h4 {
|
41 |
+
color: #ffffff;
|
42 |
+
}
|
43 |
+
p, span, label {
|
44 |
+
color: #f1f1f1;
|
45 |
+
}
|
46 |
+
body, p {
|
47 |
+
line-height: 1.6;
|
48 |
+
letter-spacing: 0.3px;
|
49 |
+
}
|
50 |
+
/* Header: Centered large logo and title */
|
51 |
+
.header-logo-large {
|
52 |
+
display: block;
|
53 |
+
margin-left: auto;
|
54 |
+
margin-right: auto;
|
55 |
+
width: 200px;
|
56 |
+
}
|
57 |
+
.header-title {
|
58 |
+
text-align: center;
|
59 |
+
font-size: 2.8rem;
|
60 |
+
font-weight: bold;
|
61 |
+
color: #ffffff;
|
62 |
+
margin-top: 0.5rem;
|
63 |
+
}
|
64 |
+
/* Dark animated background */
|
65 |
+
.stApp {
|
66 |
+
background: linear-gradient(315deg, #000428, #004e92);
|
67 |
+
animation: gradient 30s ease infinite;
|
68 |
+
background-size: 400% 400%;
|
69 |
+
background-attachment: fixed;
|
70 |
+
}
|
71 |
+
@keyframes gradient {
|
72 |
+
0% { background-position: 0% 0%; }
|
73 |
+
50% { background-position: 100% 100%; }
|
74 |
+
100% { background-position: 0% 0%; }
|
75 |
+
}
|
76 |
+
/* Main container styling */
|
77 |
+
.main .block-container {
|
78 |
+
max-width: 900px;
|
79 |
+
margin: 0 auto;
|
80 |
+
padding: 2rem 1rem;
|
81 |
+
background-color: transparent;
|
82 |
+
color: #f1f1f1;
|
83 |
+
}
|
84 |
+
/* Run button styling */
|
85 |
+
div.stButton > button {
|
86 |
+
background-image: linear-gradient(45deg, #8e44ad, #732d91);
|
87 |
+
box-shadow: 0 0 10px rgba(142,68,173,0.6), 0 0 20px rgba(114,45,145,0.4);
|
88 |
+
border: none;
|
89 |
+
color: #ffffff;
|
90 |
+
padding: 0.6rem 1.2rem;
|
91 |
+
border-radius: 5px;
|
92 |
+
cursor: pointer;
|
93 |
+
font-family: 'Roboto', sans-serif;
|
94 |
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
95 |
+
}
|
96 |
+
div.stButton > button:hover {
|
97 |
+
transform: scale(1.02);
|
98 |
+
box-shadow: 0 0 20px rgba(142,68,173,0.8), 0 0 30px rgba(114,45,145,0.6);
|
99 |
+
}
|
100 |
+
/* File uploader label styling */
|
101 |
+
.stFileUploader label {
|
102 |
+
font-size: 1rem;
|
103 |
+
color: #f1f1f1;
|
104 |
+
}
|
105 |
+
/* Advanced Options divider styling */
|
106 |
+
.right-column-divider {
|
107 |
+
border-left: 2px solid #f1f1f1;
|
108 |
+
padding-left: 1rem;
|
109 |
+
margin-left: 1rem;
|
110 |
+
}
|
111 |
+
</style>
|
112 |
+
""",
|
113 |
+
unsafe_allow_html=True
|
114 |
+
)
|
115 |
+
|
116 |
+
# ---------------- HEADER & LOGO ----------------
|
117 |
+
logo_path = os.path.join("lcm-lora", "metamorphLogo_nobg.png")
|
118 |
+
if os.path.exists(logo_path):
|
119 |
+
try:
|
120 |
+
logo = Image.open(logo_path)
|
121 |
+
logo_base64 = get_img_as_base64(logo)
|
122 |
+
st.markdown(
|
123 |
+
f"""
|
124 |
+
<div style="text-align: center;">
|
125 |
+
<img src="data:image/png;base64,{logo_base64}" class="header-logo-large" alt="Metamorph Logo">
|
126 |
+
</div>
|
127 |
+
""",
|
128 |
+
unsafe_allow_html=True
|
129 |
+
)
|
130 |
+
except Exception:
|
131 |
+
pass
|
132 |
+
|
133 |
+
st.markdown("<h1 class='header-title'>Metamorph Web App</h1>", unsafe_allow_html=True)
|
134 |
+
st.markdown(
|
135 |
+
"""
|
136 |
+
<p style='text-align: center; font-size: 1.1rem;'>
|
137 |
+
DiffMorpher is used for keyframe generation by default, with FILM for interpolation.
|
138 |
+
Optionally, you can enable LCM-LoRA for accelerated inference (with slight decrease in quality).
|
139 |
+
Upload two images, optionally provide textual prompts, and fine-tune the settings to create a smooth, high-quality morphing video.
|
140 |
+
</p>
|
141 |
+
<hr>
|
142 |
+
""",
|
143 |
+
unsafe_allow_html=True
|
144 |
+
)
|
145 |
+
|
146 |
+
# ---------------- SECTION 1: IMAGE & PROMPT INPUTS ----------------
|
147 |
+
st.subheader("1. Upload Source Images & Prompts")
|
148 |
+
col_imgA, col_imgB = st.columns(2)
|
149 |
+
with col_imgA:
|
150 |
+
st.markdown("#### Image A")
|
151 |
+
uploaded_image_A = st.file_uploader("Upload your first image", type=["png", "jpg", "jpeg"], key="imgA")
|
152 |
+
if uploaded_image_A is not None:
|
153 |
+
# use_container_width instead of use_column_width
|
154 |
+
st.image(uploaded_image_A, caption="Preview - Image A", use_container_width=True)
|
155 |
+
prompt_A = st.text_input("Prompt for Image A (optional)", value="", key="promptA")
|
156 |
+
with col_imgB:
|
157 |
+
st.markdown("#### Image B")
|
158 |
+
uploaded_image_B = st.file_uploader("Upload your second image", type=["png", "jpg", "jpeg"], key="imgB")
|
159 |
+
if uploaded_image_B is not None:
|
160 |
+
# use_container_width instead of use_column_width
|
161 |
+
st.image(uploaded_image_B, caption="Preview - Image B", use_container_width=True)
|
162 |
+
prompt_B = st.text_input("Prompt for Image B (optional)", value="", key="promptB")
|
163 |
+
|
164 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
165 |
+
|
166 |
+
# ---------------- SECTION 2: CONFIGURE MORPHING PIPELINE ----------------
|
167 |
+
st.subheader("2. Configure Morphing Pipeline")
|
168 |
+
st.markdown(
|
169 |
+
"""
|
170 |
+
<p style="font-size: 1rem;">
|
171 |
+
Select a preset below to automatically adjust quality and inference time.
|
172 |
+
If you choose <strong>Custom βοΈ</strong>, the advanced settings will automatically expand so you can fine-tune the configuration.
|
173 |
+
</p>
|
174 |
+
""",
|
175 |
+
unsafe_allow_html=True
|
176 |
+
)
|
177 |
+
|
178 |
+
# Preset Options (Dropdown)
|
179 |
+
st.markdown("**Preset Options**")
|
180 |
+
preset_option = st.selectbox(
|
181 |
+
"Select a preset for quality and inference time",
|
182 |
+
options=[
|
183 |
+
"Maximum quality, highest inference time π",
|
184 |
+
"Medium quality, medium inference time βοΈ",
|
185 |
+
"Low quality, lowest inference time β‘",
|
186 |
+
"Creative morph π¨",
|
187 |
+
"Custom βοΈ"
|
188 |
+
],
|
189 |
+
index=0,
|
190 |
+
label_visibility="collapsed" # Hide the label in the UI but keep it for accessibility
|
191 |
+
)
|
192 |
+
|
193 |
+
# Determine preset defaults based on selection
|
194 |
+
if preset_option.startswith("Maximum quality"):
|
195 |
+
# "Maximum quality, highest inference time π"
|
196 |
+
preset_model = "Base Stable Diffusion V2-1 (No LCM-LoRA support)"
|
197 |
+
preset_film = True
|
198 |
+
preset_lcm = False
|
199 |
+
elif preset_option.startswith("Medium quality"):
|
200 |
+
# "Medium quality, medium inference time βοΈ"
|
201 |
+
preset_model = "Base Stable Diffusion V2-1 (No LCM-LoRA support)"
|
202 |
+
preset_film = False
|
203 |
+
preset_lcm = False
|
204 |
+
elif preset_option.startswith("Low quality"):
|
205 |
+
# "Low quality, lowest inference time β‘"
|
206 |
+
preset_model = "Base Stable Diffusion V1-5"
|
207 |
+
preset_film = False
|
208 |
+
preset_lcm = True
|
209 |
+
elif preset_option.startswith("Creative morph"):
|
210 |
+
# "Creative morph π¨"
|
211 |
+
preset_model = "Dreamshaper-7 (fine-tuned SD V1-5)"
|
212 |
+
preset_film = True
|
213 |
+
preset_lcm = True
|
214 |
+
else:
|
215 |
+
# "Custom βοΈ"
|
216 |
+
preset_model = None
|
217 |
+
preset_film = None
|
218 |
+
preset_lcm = None
|
219 |
+
|
220 |
+
# Auto-expand advanced options if "Custom βοΈ" is chosen
|
221 |
+
advanced_expanded = True if preset_option.endswith("βοΈ") else False
|
222 |
+
|
223 |
+
# Advanced Options for fine-tuning
|
224 |
+
with st.expander("Advanced Options", expanded=advanced_expanded):
|
225 |
+
options_list = [
|
226 |
+
"Base Stable Diffusion V1-5",
|
227 |
+
"Dreamshaper-7 (fine-tuned SD V1-5)",
|
228 |
+
"Base Stable Diffusion V2-1 (No LCM-LoRA support)"
|
229 |
+
]
|
230 |
+
default_model = preset_model if preset_model is not None else "Base Stable Diffusion V1-5"
|
231 |
+
default_index = options_list.index(default_model)
|
232 |
+
model_option = st.selectbox("Select Model Card", options=options_list, index=default_index)
|
233 |
+
|
234 |
+
col_left, col_right = st.columns(2)
|
235 |
+
# Left Column: Keyframe Generator Parameters
|
236 |
+
with col_left:
|
237 |
+
st.markdown("##### Keyframe Generator Parameters")
|
238 |
+
num_frames = st.number_input("Number of keyframes (2β200)", min_value=2, max_value=200, value=20)
|
239 |
+
if model_option == "Base Stable Diffusion V2-1 (No LCM-LoRA support)":
|
240 |
+
enable_lcm_lora = st.checkbox(
|
241 |
+
"Enable LCM-LoRA (accelerated inference, slight decrease in quality)",
|
242 |
+
value=False,
|
243 |
+
disabled=True,
|
244 |
+
help="LCM-LoRA is not available for the selected model card."
|
245 |
+
)
|
246 |
+
else:
|
247 |
+
lcm_default = preset_lcm if preset_lcm is not None else False
|
248 |
+
enable_lcm_lora = st.checkbox(
|
249 |
+
"Enable LCM-LoRA (accelerated inference, slight decrease in quality)",
|
250 |
+
value=lcm_default
|
251 |
+
)
|
252 |
+
use_adain = st.checkbox("Use AdaIN", value=True)
|
253 |
+
use_reschedule = st.checkbox("Use reschedule sampling", value=True)
|
254 |
+
keyframe_duration = st.number_input("Keyframe Duration (seconds, only if not using FILM)", min_value=0.01, max_value=5.0, value=0.1, step=0.01)
|
255 |
+
# Right Column: Inter-frame Interpolator Parameters (FILM)
|
256 |
+
with col_right:
|
257 |
+
st.markdown("<div class='right-column-divider'>", unsafe_allow_html=True)
|
258 |
+
st.markdown("##### Inter-frame Interpolator Parameters")
|
259 |
+
default_use_film = preset_film if preset_film is not None else True
|
260 |
+
use_film = st.checkbox("Use FILM interpolation", value=default_use_film)
|
261 |
+
film_fps = st.number_input("FILM FPS (1β120)", min_value=1, max_value=120, value=30)
|
262 |
+
film_recursions = st.number_input("FILM recursion passes (1β6)", min_value=1, max_value=6, value=3)
|
263 |
+
st.markdown("</div>", unsafe_allow_html=True)
|
264 |
+
|
265 |
+
st.markdown("<hr>", unsafe_allow_html=True)
|
266 |
+
|
267 |
+
# ---------------- SECTION 3: EXECUTE MORPH PIPELINE ----------------
|
268 |
+
st.subheader("3. Generate Morphing Video")
|
269 |
+
st.markdown("Once satisfied with your inputs, click below to start the process.")
|
270 |
+
if st.button("Run Morphing Pipeline", key="run_pipeline"):
|
271 |
+
if not (uploaded_image_A and uploaded_image_B):
|
272 |
+
st.error("Please upload both images before running the morphing pipeline.")
|
273 |
+
return
|
274 |
+
|
275 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
276 |
+
imgA_path = os.path.join(temp_dir, "imageA.png")
|
277 |
+
imgB_path = os.path.join(temp_dir, "imageB.png")
|
278 |
+
save_uploaded_file(uploaded_image_A, imgA_path)
|
279 |
+
save_uploaded_file(uploaded_image_B, imgB_path)
|
280 |
+
|
281 |
+
output_dir = os.path.join(temp_dir, "morph_results")
|
282 |
+
film_output_dir = os.path.join(temp_dir, "film_output")
|
283 |
+
os.makedirs(output_dir, exist_ok=True)
|
284 |
+
os.makedirs(film_output_dir, exist_ok=True)
|
285 |
+
|
286 |
+
# Build the CLI command. Note: numeric parameters are converted to strings for CLI compatibility.
|
287 |
+
cmd = [
|
288 |
+
sys.executable, "run_morphing.py",
|
289 |
+
"--model_path", model_option if model_option != "Dreamshaper-7 (fine-tuned SD V1-5)" else "lykon/dreamshaper-7",
|
290 |
+
"--image_path_0", imgA_path,
|
291 |
+
"--image_path_1", imgB_path,
|
292 |
+
"--prompt_0", prompt_A,
|
293 |
+
"--prompt_1", prompt_B,
|
294 |
+
"--output_path", output_dir,
|
295 |
+
"--film_output_folder", film_output_dir,
|
296 |
+
"--num_frames", str(num_frames),
|
297 |
+
"--keyframe_duration", str(keyframe_duration)
|
298 |
+
]
|
299 |
+
if enable_lcm_lora:
|
300 |
+
cmd.append("--use_lcm")
|
301 |
+
if use_adain:
|
302 |
+
cmd.append("--use_adain")
|
303 |
+
if use_reschedule:
|
304 |
+
cmd.append("--use_reschedule")
|
305 |
+
if use_film:
|
306 |
+
cmd.append("--use_film")
|
307 |
+
cmd.extend(["--film_fps", str(film_fps)])
|
308 |
+
cmd.extend(["--film_num_recursions", str(film_recursions)])
|
309 |
+
|
310 |
+
st.info("Initializing pipeline. Please wait...")
|
311 |
+
with st.spinner("Generating morph..."):
|
312 |
+
try:
|
313 |
+
subprocess.run(cmd, check=True)
|
314 |
+
except subprocess.CalledProcessError as e:
|
315 |
+
st.error(f"Error running pipeline: {e}")
|
316 |
+
return
|
317 |
+
|
318 |
+
possible_outputs = [f for f in os.listdir(film_output_dir) if f.endswith(".mp4")]
|
319 |
+
if not possible_outputs:
|
320 |
+
possible_outputs = [f for f in os.listdir(output_dir) if f.endswith(".mp4")]
|
321 |
+
|
322 |
+
if possible_outputs:
|
323 |
+
final_video_path = os.path.join(
|
324 |
+
film_output_dir if os.listdir(film_output_dir) else output_dir,
|
325 |
+
possible_outputs[0]
|
326 |
+
)
|
327 |
+
st.success("Morphing complete! π")
|
328 |
+
st.video(final_video_path)
|
329 |
+
with open(final_video_path, "rb") as f:
|
330 |
+
st.download_button(
|
331 |
+
"Download Result Video",
|
332 |
+
data=f.read(),
|
333 |
+
file_name="morph_result.mp4",
|
334 |
+
mime="video/mp4"
|
335 |
+
)
|
336 |
+
else:
|
337 |
+
st.warning("No .mp4 output found. Check logs for details.")
|
338 |
+
|
339 |
+
if __name__ == "__main__":
|
340 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
streamlit
|
2 |
+
accelerate==0.23.0
|
3 |
+
diffusers==0.17.1
|
4 |
+
einops==0.7.0
|
5 |
+
gradio==4.7.1
|
6 |
+
numpy==1.26.1
|
7 |
+
opencv_python==4.5.5.64
|
8 |
+
packaging==23.2
|
9 |
+
Pillow==10.1.0
|
10 |
+
safetensors==0.4.0
|
11 |
+
tqdm==4.65.0
|
12 |
+
transformers==4.34.1
|
13 |
+
torch
|
14 |
+
torchvision
|
15 |
+
lpips
|
16 |
+
# peft
|