Update app.py
Browse files
app.py
CHANGED
@@ -15,54 +15,103 @@ load_dotenv()
|
|
15 |
B_KEY = os.getenv("B_KEY")
|
16 |
|
17 |
# URLs
|
18 |
-
API_URL =
|
19 |
-
UPLOAD_URL = os.getenv("UPLOAD_URL") # Make sure to add this to your .env file
|
20 |
|
21 |
def lipsync_api_call(video_url, audio_url):
|
|
|
|
|
|
|
|
|
22 |
headers = {
|
23 |
"Content-Type": "application/json",
|
24 |
"x-api-key": B_KEY
|
25 |
}
|
26 |
|
27 |
data = {
|
28 |
-
"audioUrl": audio_url,
|
29 |
-
"videoUrl": video_url,
|
30 |
-
"maxCredits": 1000,
|
31 |
"model": "lipsync-1.8.0-beta",
|
32 |
-
"
|
33 |
-
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
}
|
36 |
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
|
40 |
def check_job_status(job_id):
|
|
|
41 |
headers = {"x-api-key": B_KEY}
|
42 |
-
max_attempts = 3000
|
|
|
43 |
|
44 |
-
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
return None
|
52 |
-
|
53 |
-
|
54 |
return None
|
55 |
|
56 |
def get_media_duration(file_path):
|
|
|
57 |
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path]
|
58 |
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
59 |
-
|
|
|
|
|
60 |
|
61 |
def combine_audio_video(video_path, audio_path, output_path):
|
|
|
|
|
|
|
|
|
|
|
62 |
video_duration = get_media_duration(video_path)
|
63 |
audio_duration = get_media_duration(audio_path)
|
64 |
|
65 |
if video_duration > audio_duration:
|
|
|
66 |
cmd = [
|
67 |
'ffmpeg', '-i', video_path, '-i', audio_path,
|
68 |
'-t', str(audio_duration),
|
@@ -71,6 +120,7 @@ def combine_audio_video(video_path, audio_path, output_path):
|
|
71 |
'-y', output_path
|
72 |
]
|
73 |
else:
|
|
|
74 |
loop_count = int(audio_duration // video_duration) + 1
|
75 |
cmd = [
|
76 |
'ffmpeg', '-stream_loop', str(loop_count), '-i', video_path, '-i', audio_path,
|
@@ -80,54 +130,84 @@ def combine_audio_video(video_path, audio_path, output_path):
|
|
80 |
'-shortest', '-y', output_path
|
81 |
]
|
82 |
|
83 |
-
|
|
|
|
|
|
|
84 |
|
85 |
def is_image_url(url):
|
86 |
parsed = urlparse(url)
|
87 |
path = parsed.path.lower()
|
88 |
-
|
|
|
|
|
|
|
89 |
|
90 |
def create_video_from_image(image_url, output_path, duration=10):
|
91 |
-
|
|
|
|
|
|
|
92 |
response = requests.get(image_url)
|
93 |
if response.status_code != 200:
|
|
|
94 |
raise Exception("Failed to download the image")
|
95 |
|
96 |
temp_image_path = f"temp_image_{uuid.uuid4()}.jpg"
|
|
|
|
|
97 |
with open(temp_image_path, 'wb') as f:
|
98 |
f.write(response.content)
|
99 |
|
100 |
-
# Create a 10-second video from the image
|
101 |
cmd = [
|
102 |
'ffmpeg', '-loop', '1', '-i', temp_image_path,
|
103 |
'-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p',
|
104 |
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
105 |
'-y', output_path
|
106 |
]
|
107 |
-
subprocess.run(cmd, check=True)
|
108 |
|
109 |
-
|
|
|
|
|
|
|
|
|
110 |
os.remove(temp_image_path)
|
|
|
111 |
|
112 |
return output_path
|
113 |
|
114 |
def upload_file(file_path):
|
|
|
|
|
115 |
with open(file_path, 'rb') as file:
|
116 |
files = {'fileToUpload': (os.path.basename(file_path), file)}
|
117 |
data = {'reqtype': 'fileupload'}
|
118 |
-
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
def process_video(video_url, audio_url, progress=gr.Progress()):
|
|
|
|
|
|
|
|
|
125 |
if not audio_url:
|
|
|
126 |
return None, "No audio URL provided"
|
127 |
if not video_url:
|
|
|
128 |
return None, "No video URL provided"
|
129 |
|
130 |
session_id = str(uuid.uuid4())
|
|
|
131 |
|
132 |
progress(0.2, desc="Processing media...")
|
133 |
|
@@ -146,27 +226,36 @@ def process_video(video_url, audio_url, progress=gr.Progress()):
|
|
146 |
job_data = lipsync_api_call(video_url, audio_url)
|
147 |
|
148 |
if "error" in job_data or "message" in job_data:
|
149 |
-
|
|
|
|
|
150 |
|
151 |
job_id = job_data["id"]
|
|
|
152 |
|
153 |
progress(0.6, desc="Processing lipsync...")
|
154 |
result_url = check_job_status(job_id)
|
155 |
|
156 |
if result_url:
|
157 |
progress(0.9, desc="Downloading result...")
|
|
|
158 |
response = requests.get(result_url)
|
159 |
output_path = f"output_{session_id}.mp4"
|
|
|
160 |
with open(output_path, "wb") as f:
|
161 |
f.write(response.content)
|
|
|
|
|
162 |
progress(1.0, desc="Complete!")
|
163 |
return output_path, "Lipsync completed successfully!"
|
164 |
else:
|
165 |
raise Exception("Lipsync processing failed or timed out")
|
166 |
|
167 |
except Exception as e:
|
|
|
168 |
progress(0.8, desc="Falling back to simple combination...")
|
169 |
try:
|
|
|
170 |
video_response = requests.get(video_url)
|
171 |
temp_video_path = f"temp_video_{session_id}.mp4"
|
172 |
with open(temp_video_path, "wb") as f:
|
@@ -186,6 +275,7 @@ def process_video(video_url, audio_url, progress=gr.Progress()):
|
|
186 |
progress(1.0, desc="Complete!")
|
187 |
return output_path, f"Used fallback method. Original error: {str(e)}"
|
188 |
except Exception as fallback_error:
|
|
|
189 |
return None, f"All methods failed. Error: {str(fallback_error)}"
|
190 |
|
191 |
def create_interface():
|
@@ -193,7 +283,7 @@ def create_interface():
|
|
193 |
|
194 |
"""
|
195 |
with gr.Blocks(css=css) as app:
|
196 |
-
gr.Markdown("#
|
197 |
with gr.Row():
|
198 |
with gr.Column():
|
199 |
video_url_input = gr.Textbox(label="Video or Image URL")
|
@@ -212,5 +302,6 @@ def create_interface():
|
|
212 |
return app
|
213 |
|
214 |
if __name__ == "__main__":
|
|
|
215 |
app = create_interface()
|
216 |
app.launch()
|
|
|
15 |
B_KEY = os.getenv("B_KEY")
|
16 |
|
17 |
# URLs
|
18 |
+
API_URL = "https://api.sync.so/v2/generate" # Updated API endpoint
|
|
|
19 |
|
20 |
def lipsync_api_call(video_url, audio_url):
|
21 |
+
print(f"\n[DEBUG] Starting lipsync_api_call")
|
22 |
+
print(f"[DEBUG] Video URL: {video_url}")
|
23 |
+
print(f"[DEBUG] Audio URL: {audio_url}")
|
24 |
+
|
25 |
headers = {
|
26 |
"Content-Type": "application/json",
|
27 |
"x-api-key": B_KEY
|
28 |
}
|
29 |
|
30 |
data = {
|
|
|
|
|
|
|
31 |
"model": "lipsync-1.8.0-beta",
|
32 |
+
"input": [
|
33 |
+
{
|
34 |
+
"type": "video",
|
35 |
+
"url": video_url
|
36 |
+
},
|
37 |
+
{
|
38 |
+
"type": "audio",
|
39 |
+
"url": audio_url
|
40 |
+
}
|
41 |
+
],
|
42 |
+
"options": {
|
43 |
+
"pads": [0, 5, 0, 0],
|
44 |
+
"speedup": 1,
|
45 |
+
"output_format": "mp4",
|
46 |
+
"sync_mode": "bounce",
|
47 |
+
"fps": 24,
|
48 |
+
"output_resolution": [1280, 720]
|
49 |
+
}
|
50 |
}
|
51 |
|
52 |
+
print(f"[DEBUG] Request payload: {json.dumps(data, indent=2)}")
|
53 |
+
|
54 |
+
try:
|
55 |
+
response = requests.post(API_URL, headers=headers, data=json.dumps(data))
|
56 |
+
print(f"[DEBUG] API Response status code: {response.status_code}")
|
57 |
+
print(f"[DEBUG] API Response: {response.text}")
|
58 |
+
return response.json()
|
59 |
+
except Exception as e:
|
60 |
+
print(f"[ERROR] API call failed: {str(e)}")
|
61 |
+
raise
|
62 |
|
63 |
def check_job_status(job_id):
|
64 |
+
print(f"\n[DEBUG] Checking job status for ID: {job_id}")
|
65 |
headers = {"x-api-key": B_KEY}
|
66 |
+
max_attempts = 3000
|
67 |
+
attempt = 0
|
68 |
|
69 |
+
while attempt < max_attempts:
|
70 |
+
try:
|
71 |
+
response = requests.get(f"{API_URL}/{job_id}", headers=headers)
|
72 |
+
print(f"[DEBUG] Status check attempt {attempt + 1}")
|
73 |
+
print(f"[DEBUG] Status response: {response.text}")
|
74 |
+
|
75 |
+
data = response.json()
|
76 |
+
status = data.get("status")
|
77 |
+
print(f"[DEBUG] Current status: {status}")
|
78 |
+
|
79 |
+
if status == "COMPLETED":
|
80 |
+
print(f"[DEBUG] Job completed. Output URL: {data.get('outputUrl')}")
|
81 |
+
return data.get("outputUrl")
|
82 |
+
elif status == "FAILED" or status == "CANCELED":
|
83 |
+
print(f"[ERROR] Job failed or was canceled. Error: {data.get('error')}")
|
84 |
+
return None
|
85 |
+
|
86 |
+
attempt += 1
|
87 |
+
time.sleep(10)
|
88 |
+
|
89 |
+
except Exception as e:
|
90 |
+
print(f"[ERROR] Status check failed: {str(e)}")
|
91 |
return None
|
92 |
+
|
93 |
+
print("[ERROR] Max attempts reached")
|
94 |
return None
|
95 |
|
96 |
def get_media_duration(file_path):
|
97 |
+
print(f"\n[DEBUG] Getting duration for: {file_path}")
|
98 |
cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', file_path]
|
99 |
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
100 |
+
duration = float(result.stdout.strip())
|
101 |
+
print(f"[DEBUG] Media duration: {duration} seconds")
|
102 |
+
return duration
|
103 |
|
104 |
def combine_audio_video(video_path, audio_path, output_path):
|
105 |
+
print(f"\n[DEBUG] Combining audio and video")
|
106 |
+
print(f"[DEBUG] Video path: {video_path}")
|
107 |
+
print(f"[DEBUG] Audio path: {audio_path}")
|
108 |
+
print(f"[DEBUG] Output path: {output_path}")
|
109 |
+
|
110 |
video_duration = get_media_duration(video_path)
|
111 |
audio_duration = get_media_duration(audio_path)
|
112 |
|
113 |
if video_duration > audio_duration:
|
114 |
+
print("[DEBUG] Video longer than audio - trimming video")
|
115 |
cmd = [
|
116 |
'ffmpeg', '-i', video_path, '-i', audio_path,
|
117 |
'-t', str(audio_duration),
|
|
|
120 |
'-y', output_path
|
121 |
]
|
122 |
else:
|
123 |
+
print("[DEBUG] Audio longer than video - looping video")
|
124 |
loop_count = int(audio_duration // video_duration) + 1
|
125 |
cmd = [
|
126 |
'ffmpeg', '-stream_loop', str(loop_count), '-i', video_path, '-i', audio_path,
|
|
|
130 |
'-shortest', '-y', output_path
|
131 |
]
|
132 |
|
133 |
+
print(f"[DEBUG] FFmpeg command: {' '.join(cmd)}")
|
134 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
135 |
+
print(f"[DEBUG] FFmpeg stdout: {result.stdout}")
|
136 |
+
print(f"[DEBUG] FFmpeg stderr: {result.stderr}")
|
137 |
|
138 |
def is_image_url(url):
|
139 |
parsed = urlparse(url)
|
140 |
path = parsed.path.lower()
|
141 |
+
result = path.endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp', '.heic', '.svg', '.ico'))
|
142 |
+
print(f"\n[DEBUG] Checking if URL is image: {url}")
|
143 |
+
print(f"[DEBUG] Result: {result}")
|
144 |
+
return result
|
145 |
|
146 |
def create_video_from_image(image_url, output_path, duration=10):
|
147 |
+
print(f"\n[DEBUG] Creating video from image")
|
148 |
+
print(f"[DEBUG] Image URL: {image_url}")
|
149 |
+
print(f"[DEBUG] Output path: {output_path}")
|
150 |
+
|
151 |
response = requests.get(image_url)
|
152 |
if response.status_code != 200:
|
153 |
+
print(f"[ERROR] Failed to download image. Status code: {response.status_code}")
|
154 |
raise Exception("Failed to download the image")
|
155 |
|
156 |
temp_image_path = f"temp_image_{uuid.uuid4()}.jpg"
|
157 |
+
print(f"[DEBUG] Temporary image path: {temp_image_path}")
|
158 |
+
|
159 |
with open(temp_image_path, 'wb') as f:
|
160 |
f.write(response.content)
|
161 |
|
|
|
162 |
cmd = [
|
163 |
'ffmpeg', '-loop', '1', '-i', temp_image_path,
|
164 |
'-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p',
|
165 |
'-vf', 'scale=trunc(iw/2)*2:trunc(ih/2)*2',
|
166 |
'-y', output_path
|
167 |
]
|
|
|
168 |
|
169 |
+
print(f"[DEBUG] FFmpeg command: {' '.join(cmd)}")
|
170 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
171 |
+
print(f"[DEBUG] FFmpeg stdout: {result.stdout}")
|
172 |
+
print(f"[DEBUG] FFmpeg stderr: {result.stderr}")
|
173 |
+
|
174 |
os.remove(temp_image_path)
|
175 |
+
print(f"[DEBUG] Temporary image removed")
|
176 |
|
177 |
return output_path
|
178 |
|
179 |
def upload_file(file_path):
|
180 |
+
print(f"\n[DEBUG] Uploading file: {file_path}")
|
181 |
+
|
182 |
with open(file_path, 'rb') as file:
|
183 |
files = {'fileToUpload': (os.path.basename(file_path), file)}
|
184 |
data = {'reqtype': 'fileupload'}
|
185 |
+
try:
|
186 |
+
response = requests.post(UPLOAD_URL, files=files, data=data)
|
187 |
+
print(f"[DEBUG] Upload response status code: {response.status_code}")
|
188 |
+
print(f"[DEBUG] Upload response: {response.text}")
|
189 |
+
|
190 |
+
if response.status_code == 200:
|
191 |
+
return response.text.strip()
|
192 |
+
return None
|
193 |
+
except Exception as e:
|
194 |
+
print(f"[ERROR] File upload failed: {str(e)}")
|
195 |
+
return None
|
196 |
|
197 |
def process_video(video_url, audio_url, progress=gr.Progress()):
|
198 |
+
print(f"\n[DEBUG] Starting video processing")
|
199 |
+
print(f"[DEBUG] Video URL: {video_url}")
|
200 |
+
print(f"[DEBUG] Audio URL: {audio_url}")
|
201 |
+
|
202 |
if not audio_url:
|
203 |
+
print("[ERROR] No audio URL provided")
|
204 |
return None, "No audio URL provided"
|
205 |
if not video_url:
|
206 |
+
print("[ERROR] No video URL provided")
|
207 |
return None, "No video URL provided"
|
208 |
|
209 |
session_id = str(uuid.uuid4())
|
210 |
+
print(f"[DEBUG] Session ID: {session_id}")
|
211 |
|
212 |
progress(0.2, desc="Processing media...")
|
213 |
|
|
|
226 |
job_data = lipsync_api_call(video_url, audio_url)
|
227 |
|
228 |
if "error" in job_data or "message" in job_data:
|
229 |
+
error_msg = job_data.get("error", job_data.get("message", "Unknown error"))
|
230 |
+
print(f"[ERROR] API error: {error_msg}")
|
231 |
+
raise Exception(error_msg)
|
232 |
|
233 |
job_id = job_data["id"]
|
234 |
+
print(f"[DEBUG] Job ID: {job_id}")
|
235 |
|
236 |
progress(0.6, desc="Processing lipsync...")
|
237 |
result_url = check_job_status(job_id)
|
238 |
|
239 |
if result_url:
|
240 |
progress(0.9, desc="Downloading result...")
|
241 |
+
print(f"[DEBUG] Downloading from: {result_url}")
|
242 |
response = requests.get(result_url)
|
243 |
output_path = f"output_{session_id}.mp4"
|
244 |
+
|
245 |
with open(output_path, "wb") as f:
|
246 |
f.write(response.content)
|
247 |
+
|
248 |
+
print(f"[DEBUG] Result saved to: {output_path}")
|
249 |
progress(1.0, desc="Complete!")
|
250 |
return output_path, "Lipsync completed successfully!"
|
251 |
else:
|
252 |
raise Exception("Lipsync processing failed or timed out")
|
253 |
|
254 |
except Exception as e:
|
255 |
+
print(f"[ERROR] Main process failed: {str(e)}")
|
256 |
progress(0.8, desc="Falling back to simple combination...")
|
257 |
try:
|
258 |
+
print("[DEBUG] Attempting fallback method")
|
259 |
video_response = requests.get(video_url)
|
260 |
temp_video_path = f"temp_video_{session_id}.mp4"
|
261 |
with open(temp_video_path, "wb") as f:
|
|
|
275 |
progress(1.0, desc="Complete!")
|
276 |
return output_path, f"Used fallback method. Original error: {str(e)}"
|
277 |
except Exception as fallback_error:
|
278 |
+
print(f"[ERROR] Fallback method failed: {str(fallback_error)}")
|
279 |
return None, f"All methods failed. Error: {str(fallback_error)}"
|
280 |
|
281 |
def create_interface():
|
|
|
283 |
|
284 |
"""
|
285 |
with gr.Blocks(css=css) as app:
|
286 |
+
gr.Markdown("# Lipsync Video Generator")
|
287 |
with gr.Row():
|
288 |
with gr.Column():
|
289 |
video_url_input = gr.Textbox(label="Video or Image URL")
|
|
|
302 |
return app
|
303 |
|
304 |
if __name__ == "__main__":
|
305 |
+
print("[DEBUG] Starting application")
|
306 |
app = create_interface()
|
307 |
app.launch()
|