Update app.py
Browse files
app.py
CHANGED
@@ -15,13 +15,59 @@ load_dotenv()
|
|
15 |
B_KEY = os.getenv("B_KEY")
|
16 |
|
17 |
# URLs
|
18 |
-
API_URL = "https://api.sync.so/v2/generate"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
@@ -45,7 +91,7 @@ def lipsync_api_call(video_url, audio_url):
|
|
45 |
"output_format": "mp4",
|
46 |
"sync_mode": "bounce",
|
47 |
"fps": 24,
|
48 |
-
"output_resolution":
|
49 |
}
|
50 |
}
|
51 |
|
@@ -60,94 +106,14 @@ def lipsync_api_call(video_url, audio_url):
|
|
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),
|
118 |
-
'-map', '0:v', '-map', '1:a',
|
119 |
-
'-c:v', 'copy', '-c:a', 'aac',
|
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,
|
127 |
-
'-t', str(audio_duration),
|
128 |
-
'-map', '0:v', '-map', '1:a',
|
129 |
-
'-c:v', 'copy', '-c:a', 'aac',
|
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}")
|
@@ -162,7 +128,7 @@ def create_video_from_image(image_url, output_path, duration=10):
|
|
162 |
cmd = [
|
163 |
'ffmpeg', '-loop', '1', '-i', temp_image_path,
|
164 |
'-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p',
|
165 |
-
'-vf', 'scale=
|
166 |
'-y', output_path
|
167 |
]
|
168 |
|
|
|
15 |
B_KEY = os.getenv("B_KEY")
|
16 |
|
17 |
# URLs
|
18 |
+
API_URL = "https://api.sync.so/v2/generate"
|
19 |
+
|
20 |
+
def get_media_resolution(url):
|
21 |
+
print(f"\n[DEBUG] Getting resolution for: {url}")
|
22 |
+
|
23 |
+
# Download the file to a temporary location
|
24 |
+
response = requests.get(url)
|
25 |
+
if response.status_code != 200:
|
26 |
+
print(f"[ERROR] Failed to download media. Status code: {response.status_code}")
|
27 |
+
return None
|
28 |
+
|
29 |
+
temp_path = f"temp_media_{uuid.uuid4()}"
|
30 |
+
with open(temp_path, 'wb') as f:
|
31 |
+
f.write(response.content)
|
32 |
+
|
33 |
+
# Get resolution using FFprobe
|
34 |
+
cmd = [
|
35 |
+
'ffprobe',
|
36 |
+
'-v', 'error',
|
37 |
+
'-select_streams', 'v:0',
|
38 |
+
'-show_entries', 'stream=width,height',
|
39 |
+
'-of', 'json',
|
40 |
+
temp_path
|
41 |
+
]
|
42 |
+
|
43 |
+
try:
|
44 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
45 |
+
os.remove(temp_path) # Clean up temp file
|
46 |
+
|
47 |
+
if result.returncode == 0:
|
48 |
+
data = json.loads(result.stdout)
|
49 |
+
if 'streams' in data and data['streams']:
|
50 |
+
width = data['streams'][0].get('width')
|
51 |
+
height = data['streams'][0].get('height')
|
52 |
+
if width and height:
|
53 |
+
print(f"[DEBUG] Detected resolution: {width}x{height}")
|
54 |
+
return [width, height]
|
55 |
+
except Exception as e:
|
56 |
+
print(f"[ERROR] Failed to get resolution: {str(e)}")
|
57 |
+
if os.path.exists(temp_path):
|
58 |
+
os.remove(temp_path)
|
59 |
+
|
60 |
+
print("[DEBUG] Failed to detect resolution, using default")
|
61 |
+
return [1280, 720] # Default resolution
|
62 |
|
63 |
def lipsync_api_call(video_url, audio_url):
|
64 |
print(f"\n[DEBUG] Starting lipsync_api_call")
|
65 |
print(f"[DEBUG] Video URL: {video_url}")
|
66 |
print(f"[DEBUG] Audio URL: {audio_url}")
|
67 |
|
68 |
+
# Get the resolution of the input video/image
|
69 |
+
resolution = get_media_resolution(video_url)
|
70 |
+
|
71 |
headers = {
|
72 |
"Content-Type": "application/json",
|
73 |
"x-api-key": B_KEY
|
|
|
91 |
"output_format": "mp4",
|
92 |
"sync_mode": "bounce",
|
93 |
"fps": 24,
|
94 |
+
"output_resolution": resolution
|
95 |
}
|
96 |
}
|
97 |
|
|
|
106 |
print(f"[ERROR] API call failed: {str(e)}")
|
107 |
raise
|
108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
109 |
def create_video_from_image(image_url, output_path, duration=10):
|
110 |
print(f"\n[DEBUG] Creating video from image")
|
111 |
print(f"[DEBUG] Image URL: {image_url}")
|
112 |
print(f"[DEBUG] Output path: {output_path}")
|
113 |
|
114 |
+
# Get the resolution before creating the video
|
115 |
+
resolution = get_media_resolution(image_url)
|
116 |
+
|
117 |
response = requests.get(image_url)
|
118 |
if response.status_code != 200:
|
119 |
print(f"[ERROR] Failed to download image. Status code: {response.status_code}")
|
|
|
128 |
cmd = [
|
129 |
'ffmpeg', '-loop', '1', '-i', temp_image_path,
|
130 |
'-c:v', 'libx264', '-t', str(duration), '-pix_fmt', 'yuv420p',
|
131 |
+
'-vf', f'scale={resolution[0]}:{resolution[1]}',
|
132 |
'-y', output_path
|
133 |
]
|
134 |
|