Spaces:
Sleeping
Sleeping
Commit
·
15a5b8a
1
Parent(s):
0c43391
add: endpoint product video gen --> one combined
Browse files- app.py +108 -4
- src/components/each_necklace_video_gen.py +182 -0
app.py
CHANGED
@@ -2,6 +2,7 @@ import mimetypes
|
|
2 |
import os
|
3 |
import tempfile
|
4 |
import time
|
|
|
5 |
|
6 |
import requests
|
7 |
from fastapi import FastAPI
|
@@ -9,6 +10,7 @@ from pydantic import BaseModel
|
|
9 |
from starlette.responses import JSONResponse
|
10 |
from supabase import create_client
|
11 |
|
|
|
12 |
from src.components.vidgen import VideoCreator
|
13 |
|
14 |
supabase_url = os.getenv('SUPABASE_URL')
|
@@ -16,7 +18,7 @@ supabase_key = os.getenv('SUPABASE_KEY')
|
|
16 |
supabase = create_client(supabase_url, supabase_key)
|
17 |
app = FastAPI()
|
18 |
|
19 |
-
RESOURCES_DIR = "
|
20 |
|
21 |
os.makedirs(RESOURCES_DIR, exist_ok=True)
|
22 |
TEMP_VIDEO_DIR = f"{RESOURCES_DIR}/temp_video"
|
@@ -94,7 +96,7 @@ class VideoGenerator(BaseModel):
|
|
94 |
makeup_image_title: str = "Makeup Try-On"
|
95 |
|
96 |
|
97 |
-
@app.post("/create-video/")
|
98 |
async def create_video(request: VideoGenerator):
|
99 |
start_time = time.time()
|
100 |
try:
|
@@ -134,11 +136,9 @@ async def create_video(request: VideoGenerator):
|
|
134 |
|
135 |
)
|
136 |
|
137 |
-
# Generate video
|
138 |
video_creator.create_final_video()
|
139 |
video_creation_time = time.time() - start_time - file_download_time
|
140 |
|
141 |
-
# Upload to Supabase
|
142 |
url = upload_to_supabase(video_path=output_path)
|
143 |
supabase_upload_time = time.time() - start_time - file_download_time - video_creation_time
|
144 |
|
@@ -168,6 +168,110 @@ async def create_video(request: VideoGenerator):
|
|
168 |
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500)
|
169 |
|
170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
171 |
@app.get("/resources")
|
172 |
async def get_infromation():
|
173 |
music = os.listdir(RESOURCES_DIR + "/audio")
|
|
|
2 |
import os
|
3 |
import tempfile
|
4 |
import time
|
5 |
+
from typing import List
|
6 |
|
7 |
import requests
|
8 |
from fastapi import FastAPI
|
|
|
10 |
from starlette.responses import JSONResponse
|
11 |
from supabase import create_client
|
12 |
|
13 |
+
from src.components.each_necklace_video_gen import EachVideoCreator
|
14 |
from src.components.vidgen import VideoCreator
|
15 |
|
16 |
supabase_url = os.getenv('SUPABASE_URL')
|
|
|
18 |
supabase = create_client(supabase_url, supabase_key)
|
19 |
app = FastAPI()
|
20 |
|
21 |
+
RESOURCES_DIR = "resources"
|
22 |
|
23 |
os.makedirs(RESOURCES_DIR, exist_ok=True)
|
24 |
TEMP_VIDEO_DIR = f"{RESOURCES_DIR}/temp_video"
|
|
|
96 |
makeup_image_title: str = "Makeup Try-On"
|
97 |
|
98 |
|
99 |
+
@app.post("/create-single-product-video/")
|
100 |
async def create_video(request: VideoGenerator):
|
101 |
start_time = time.time()
|
102 |
try:
|
|
|
136 |
|
137 |
)
|
138 |
|
|
|
139 |
video_creator.create_final_video()
|
140 |
video_creation_time = time.time() - start_time - file_download_time
|
141 |
|
|
|
142 |
url = upload_to_supabase(video_path=output_path)
|
143 |
supabase_upload_time = time.time() - start_time - file_download_time - video_creation_time
|
144 |
|
|
|
168 |
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500)
|
169 |
|
170 |
|
171 |
+
class EachNecklaceVideoGeneratorRequest(BaseModel):
|
172 |
+
intro_video_path: str = "JewelMirror_intro.mp4"
|
173 |
+
font_path: str = "PlayfairDisplay-VariableFont_wght.ttf"
|
174 |
+
background_audio_path: str = "TraditionalIndianVlogMusic.mp3"
|
175 |
+
image_display_duration: float = 2.5
|
176 |
+
fps: int = 10
|
177 |
+
necklace_title: List[str]
|
178 |
+
nto_image_title: List[str]
|
179 |
+
nto_cto_image_title: List[str]
|
180 |
+
makeup_image_title: List[str]
|
181 |
+
necklace_images: List[str]
|
182 |
+
necklace_try_on_output_images: List[List[str]]
|
183 |
+
clothing_output_images: List[List[str]]
|
184 |
+
makeup_output_images: List[List[str]]
|
185 |
+
|
186 |
+
|
187 |
+
@app.post("/create-multiple-product-video/")
|
188 |
+
async def create_video(request: EachNecklaceVideoGeneratorRequest):
|
189 |
+
start_time = time.time()
|
190 |
+
try:
|
191 |
+
def process_images(image_list):
|
192 |
+
if isinstance(image_list, list):
|
193 |
+
return [process_images(item) for item in image_list]
|
194 |
+
return download_image(image_list)
|
195 |
+
|
196 |
+
temp_files = {
|
197 |
+
'necklaces': process_images(request.necklace_images),
|
198 |
+
'necklace_tryon': process_images(request.necklace_try_on_output_images),
|
199 |
+
'clothing': process_images(request.clothing_output_images),
|
200 |
+
'makeup': process_images(request.makeup_output_images)
|
201 |
+
}
|
202 |
+
file_download_time = time.time() - start_time
|
203 |
+
|
204 |
+
def verify_files(files):
|
205 |
+
if isinstance(files, list):
|
206 |
+
return all(verify_files(item) for item in files) # Recurse for nested lists
|
207 |
+
return files is not None # Check single file
|
208 |
+
|
209 |
+
if not all(verify_files(files) for files in temp_files.values()):
|
210 |
+
return JSONResponse(
|
211 |
+
content={"status": "error", "message": "Failed to download all required images."},
|
212 |
+
status_code=400
|
213 |
+
)
|
214 |
+
|
215 |
+
# Prepare paths
|
216 |
+
intro_path = f"{RESOURCES_DIR}/intro/{request.intro_video_path}"
|
217 |
+
output_path = f"{TEMP_VIDEO_DIR}/video_{os.urandom(8).hex()}.mp4"
|
218 |
+
font_path = f"{RESOURCES_DIR}/fonts/{request.font_path}"
|
219 |
+
audio_path = f"{RESOURCES_DIR}/audio/{request.background_audio_path}"
|
220 |
+
|
221 |
+
video_creator = EachVideoCreator(
|
222 |
+
intro_video_path=intro_path,
|
223 |
+
necklace_image=temp_files['necklaces'],
|
224 |
+
nto_outputs=temp_files['necklace_tryon'],
|
225 |
+
nto_cto_outputs=temp_files['clothing'],
|
226 |
+
makeup_outputs=temp_files['makeup'],
|
227 |
+
font_path=font_path,
|
228 |
+
output_path=output_path,
|
229 |
+
audio_path=audio_path,
|
230 |
+
image_display_duration=request.image_display_duration,
|
231 |
+
fps=request.fps,
|
232 |
+
necklace_title=request.necklace_title,
|
233 |
+
nto_title=request.nto_image_title,
|
234 |
+
|
235 |
+
cto_title=request.nto_cto_image_title,
|
236 |
+
makeup_title=request.makeup_image_title
|
237 |
+
|
238 |
+
)
|
239 |
+
|
240 |
+
# Generate video
|
241 |
+
video_creator.create_final_video()
|
242 |
+
video_creation_time = time.time() - start_time - file_download_time
|
243 |
+
|
244 |
+
# Upload video to Supabase
|
245 |
+
url = upload_to_supabase(video_path=output_path)
|
246 |
+
supabase_upload_time = time.time() - start_time - file_download_time - video_creation_time
|
247 |
+
|
248 |
+
response = {
|
249 |
+
"status": "success",
|
250 |
+
"message": "Video created successfully.",
|
251 |
+
"video_url": url,
|
252 |
+
"timings": {
|
253 |
+
"file_download_time": file_download_time,
|
254 |
+
"video_creation_time": video_creation_time,
|
255 |
+
"supabase_upload_time": supabase_upload_time
|
256 |
+
}
|
257 |
+
}
|
258 |
+
|
259 |
+
def cleanup_files(files):
|
260 |
+
if isinstance(files, list):
|
261 |
+
for item in files:
|
262 |
+
cleanup_files(item)
|
263 |
+
elif os.path.exists(files):
|
264 |
+
os.unlink(files)
|
265 |
+
|
266 |
+
if os.path.exists(output_path):
|
267 |
+
os.remove(output_path)
|
268 |
+
|
269 |
+
return JSONResponse(content=response, status_code=200)
|
270 |
+
|
271 |
+
except Exception as e:
|
272 |
+
return JSONResponse(content={"status": "error", "message": str(e)}, status_code=500)
|
273 |
+
|
274 |
+
|
275 |
@app.get("/resources")
|
276 |
async def get_infromation():
|
277 |
music = os.listdir(RESOURCES_DIR + "/audio")
|
src/components/each_necklace_video_gen.py
ADDED
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
project @ images_to_video
|
3 |
+
created @ 2024-12-17
|
4 |
+
author @ github.com/ishworrsubedii
|
5 |
+
"""
|
6 |
+
import os
|
7 |
+
|
8 |
+
from moviepy.audio.io.AudioFileClip import AudioFileClip
|
9 |
+
from moviepy.video.VideoClip import ImageClip, ColorClip, TextClip
|
10 |
+
from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip
|
11 |
+
from moviepy.video.compositing.concatenate import concatenate_videoclips
|
12 |
+
from moviepy.video.fx.all import resize
|
13 |
+
from moviepy.video.io.VideoFileClip import VideoFileClip
|
14 |
+
|
15 |
+
|
16 |
+
class EachVideoCreator:
|
17 |
+
def __init__(self, necklace_title, nto_title, cto_title, makeup_title, intro_video_path=None, necklace_image=None,
|
18 |
+
nto_outputs=None,
|
19 |
+
nto_cto_outputs=None, makeup_outputs=None, font_path=None, output_path=None,
|
20 |
+
audio_path=None, image_display_duration=2.5, box_color=(131, 42, 48), box_opacity=0.8,
|
21 |
+
font_size=28, text_color="white", fps=1):
|
22 |
+
self.intro_video_path = intro_video_path
|
23 |
+
self.necklace_images = necklace_image if necklace_image else []
|
24 |
+
self.nto_outputs = nto_outputs if nto_outputs else []
|
25 |
+
self.nto_cto_outputs = nto_cto_outputs if nto_cto_outputs else []
|
26 |
+
self.makeup_outputs = makeup_outputs if makeup_outputs else []
|
27 |
+
self.output_video_path = output_path
|
28 |
+
self.font_path = font_path
|
29 |
+
self.audio_path = audio_path
|
30 |
+
self.image_display_duration = image_display_duration
|
31 |
+
self.box_color = box_color
|
32 |
+
self.box_opacity = box_opacity
|
33 |
+
self.font_size = font_size
|
34 |
+
self.text_color = text_color
|
35 |
+
self.fps = fps
|
36 |
+
self.necklace_title = necklace_title
|
37 |
+
self.nto_title = nto_title
|
38 |
+
self.cto_title = cto_title
|
39 |
+
self.makeup_title = makeup_title
|
40 |
+
|
41 |
+
def create_necklace_clips(self, necklace_image, index, label):
|
42 |
+
if not necklace_image:
|
43 |
+
print(f"Skipping necklace {index + 1}: No image provided.")
|
44 |
+
return []
|
45 |
+
|
46 |
+
backgrounds = [
|
47 |
+
(245, 245, 245), # Soft White
|
48 |
+
(220, 245, 245), # Light Blue
|
49 |
+
(230, 230, 235) # Pearl Gray
|
50 |
+
]
|
51 |
+
necklace_clips = []
|
52 |
+
for bg_color in backgrounds:
|
53 |
+
bg_clip = ColorClip((1080, 1080), col=bg_color, duration=self.image_display_duration)
|
54 |
+
necklace = resize(ImageClip(necklace_image), width=650)
|
55 |
+
necklace = necklace.set_duration(self.image_display_duration).set_position('center')
|
56 |
+
txt_overlay = self.create_text_overlay(f"{label}")
|
57 |
+
final_clip = CompositeVideoClip([bg_clip, necklace, txt_overlay.set_position(('center', 'bottom'))])
|
58 |
+
necklace_clips.append(final_clip)
|
59 |
+
return necklace_clips
|
60 |
+
|
61 |
+
def create_grouped_clips(self, grouped_images, label):
|
62 |
+
clips = []
|
63 |
+
for idx, group in enumerate(grouped_images):
|
64 |
+
for img_path in group:
|
65 |
+
if os.path.exists(img_path) and img_path.lower().endswith(('.png', '.jpg', '.jpeg')):
|
66 |
+
print(f"Processing {label} image: {img_path}")
|
67 |
+
img_clip = resize(ImageClip(img_path), (1080, 1080))
|
68 |
+
txt_overlay = self.create_text_overlay(f"{label} {idx + 1}")
|
69 |
+
final_clip = CompositeVideoClip([
|
70 |
+
img_clip.set_duration(self.image_display_duration),
|
71 |
+
txt_overlay.set_position(('center', 'bottom'))
|
72 |
+
])
|
73 |
+
clips.append(final_clip)
|
74 |
+
return clips
|
75 |
+
|
76 |
+
def create_text_overlay(self, text, duration=None):
|
77 |
+
box = ColorClip((1080, 80), col=self.box_color, duration=duration or self.image_display_duration)
|
78 |
+
box = box.set_opacity(self.box_opacity)
|
79 |
+
txt = TextClip(text, font=self.font_path, fontsize=self.font_size, color=self.text_color)
|
80 |
+
return CompositeVideoClip([box, txt.set_position('center')])
|
81 |
+
|
82 |
+
def create_final_video(self):
|
83 |
+
try:
|
84 |
+
print("Starting video creation...")
|
85 |
+
clips = []
|
86 |
+
|
87 |
+
# Step 1: Process Intro Video
|
88 |
+
if self.intro_video_path and os.path.exists(self.intro_video_path):
|
89 |
+
print(f"Adding intro video from path: {self.intro_video_path}")
|
90 |
+
intro_clip = resize(VideoFileClip(self.intro_video_path), (1080, 1080))
|
91 |
+
clips.append(intro_clip)
|
92 |
+
else:
|
93 |
+
print("Skipping intro video: Path not provided or invalid.")
|
94 |
+
|
95 |
+
# Step 2: Process Necklaces and Associated Outputs
|
96 |
+
for idx, necklace_image in enumerate(self.necklace_images):
|
97 |
+
print(f"Processing Necklace {idx + 1}...")
|
98 |
+
# Necklace preview clips
|
99 |
+
necklace_clips = self.create_necklace_clips(necklace_image, idx, self.necklace_title[idx])
|
100 |
+
if necklace_clips:
|
101 |
+
clips.extend(necklace_clips)
|
102 |
+
else:
|
103 |
+
print(f"Skipping Necklace {idx + 1} preview: No valid clips created.")
|
104 |
+
|
105 |
+
# NTO outputs
|
106 |
+
if idx < len(self.nto_outputs):
|
107 |
+
print(f"Adding NTO outputs for Necklace {idx + 1}")
|
108 |
+
nto_clips = self.create_grouped_clips([self.nto_outputs[idx]], self.nto_title[idx])
|
109 |
+
if nto_clips:
|
110 |
+
|
111 |
+
clips.extend(nto_clips)
|
112 |
+
else:
|
113 |
+
print(f"No valid NTO clips for Necklace {idx + 1}")
|
114 |
+
|
115 |
+
# CTO outputs
|
116 |
+
if idx < len(self.nto_cto_outputs):
|
117 |
+
print(f"Adding CTO outputs for Necklace {idx + 1}")
|
118 |
+
cto_clips = self.create_grouped_clips([self.nto_cto_outputs[idx]], self.cto_title[idx])
|
119 |
+
if cto_clips:
|
120 |
+
clips.extend(cto_clips)
|
121 |
+
else:
|
122 |
+
print(f"No valid CTO clips for Necklace {idx + 1}")
|
123 |
+
|
124 |
+
# Makeup outputs
|
125 |
+
if idx < len(self.makeup_outputs):
|
126 |
+
print(f"Adding Makeup outputs for Necklace {idx + 1}")
|
127 |
+
makeup_clips = self.create_grouped_clips([self.makeup_outputs[idx]], self.makeup_title[idx])
|
128 |
+
if makeup_clips:
|
129 |
+
clips.extend(makeup_clips)
|
130 |
+
else:
|
131 |
+
print(f"No valid Makeup clips for Necklace {idx + 1}")
|
132 |
+
|
133 |
+
final_clips = []
|
134 |
+
for clip in clips:
|
135 |
+
final_clips.append(clip.set_duration(self.image_display_duration))
|
136 |
+
clips = final_clips
|
137 |
+
|
138 |
+
if not clips:
|
139 |
+
print("No valid clips to combine. Exiting.")
|
140 |
+
return
|
141 |
+
|
142 |
+
print(f"Total clips to concatenate: {len(clips)}")
|
143 |
+
final_video = concatenate_videoclips(clips, method="compose")
|
144 |
+
|
145 |
+
# Step 4: Add Background Audio
|
146 |
+
if self.audio_path and os.path.exists(self.audio_path):
|
147 |
+
print(f"Adding background audio from path: {self.audio_path}")
|
148 |
+
try:
|
149 |
+
audio = AudioFileClip(self.audio_path).subclip(0, final_video.duration)
|
150 |
+
final_video = final_video.set_audio(audio)
|
151 |
+
except Exception as e:
|
152 |
+
print(f"Error adding audio: {e}")
|
153 |
+
else:
|
154 |
+
print("Skipping background audio: Path not provided or invalid.")
|
155 |
+
|
156 |
+
# Step 5: Export Final Video
|
157 |
+
print("Rendering final video...")
|
158 |
+
final_video.write_videofile(
|
159 |
+
self.output_video_path,
|
160 |
+
fps=self.fps,
|
161 |
+
codec="libx264",
|
162 |
+
audio_codec="aac",
|
163 |
+
threads=4,
|
164 |
+
preset="ultrafast"
|
165 |
+
)
|
166 |
+
print(f"Video successfully saved to: {self.output_video_path}")
|
167 |
+
|
168 |
+
except Exception as e:
|
169 |
+
print(f"An error occurred during video creation: {e}")
|
170 |
+
|
171 |
+
# if __name__ == "__main__":
|
172 |
+
# creator = EachVideoCreator(
|
173 |
+
# intro_video_path="intro.mp4",
|
174 |
+
# necklace_image=["necklace.jpg", "necklace2.png"],
|
175 |
+
# nto_outputs=[["nto1.jpg", "nto2.jpg"], ["nto3.jpg", "nto4.jpg"]],
|
176 |
+
# nto_cto_outputs=[["cto1.jpg", "cto2.jpg"], ["cto3.jpg", "cto4.jpg"]],
|
177 |
+
# makeup_outputs=[["makeup1.jpg", "makeup2.jpg"], ["makeup3.jpg", "makeup4.jpg"]],
|
178 |
+
# font_path="font.ttf",
|
179 |
+
# output_path="output/final_video.mp4",
|
180 |
+
# audio_path="audio.mp3"
|
181 |
+
# )
|
182 |
+
# creator.create_final_video()
|