saq1b commited on
Commit
4401c86
·
verified ·
1 Parent(s): d535c7b

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +114 -494
main.py CHANGED
@@ -1,517 +1,137 @@
1
- from fastapi import FastAPI, HTTPException, Request
2
  from fastapi.staticfiles import StaticFiles
3
- from concurrent.futures import ThreadPoolExecutor
4
- import asyncio
5
- import aiohttp
6
- import tempfile
7
  import uuid
 
 
 
8
  import shutil
9
- import os
10
- import random
11
- import traceback
12
- import string
13
 
 
14
  app = FastAPI()
15
 
16
- def generate_hash(length=12):
17
- # Characters that can appear in the hash
18
- characters = string.ascii_lowercase + string.digits
19
- # Generate a random string of the specified length
20
- hash_string = ''.join(random.choice(characters) for _ in range(length))
21
- return hash_string
22
-
23
- @app.get("/")
24
- async def read_root():
25
- return {"message": "Saqib's API"}
26
-
27
- # Create a directory to store MP3 files if it doesn't exist
28
- AUDIO_DIR = "audio_files"
29
- os.makedirs(AUDIO_DIR, exist_ok=True)
30
-
31
- # Create a directory for storing output files
32
- OUTPUT_DIR = "output"
33
- os.makedirs(OUTPUT_DIR, exist_ok=True)
34
-
35
- # Mount the audio directory
36
- app.mount("/audio", StaticFiles(directory=AUDIO_DIR), name="audio")
37
 
38
- # Mount the output directory
39
- app.mount("/output", StaticFiles(directory=OUTPUT_DIR), name="output")
 
 
 
40
 
41
- thread_pool = ThreadPoolExecutor(max_workers=2)
 
 
 
 
42
 
43
- async def run_ffmpeg_async(ffmpeg_command):
44
- loop = asyncio.get_running_loop()
45
- await loop.run_in_executor(thread_pool, ffmpeg_command)
46
-
47
- async def download_file(url: str, suffix: str):
48
- async with aiohttp.ClientSession() as session:
49
- async with session.get(url) as response:
50
- if response.status != 200:
51
- raise HTTPException(status_code=400, detail=f"Failed to download file from {url}")
52
- with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
53
- temp_file.write(await response.read())
54
- return temp_file.name
55
-
56
-
57
- @app.post("/add_audio_to_image")
58
- async def add_audio_to_image(request: Request):
59
  try:
60
- # Generate a unique filename
61
- output_filename = f"{uuid.uuid4()}.mp4"
62
- output_path = os.path.join(OUTPUT_DIR, output_filename)
63
-
64
- # Call the modal API with the request data and download the output file
65
- data = await request.json()
66
- image_url = data.get("image_url")
67
- audio_url = data.get("audio_url")
68
-
69
- if not image_url or not audio_url:
70
- raise HTTPException(status_code=400, detail="Missing image_url or audio_url in request")
71
-
72
- image_file = await download_file(image_url, ".jpg")
73
- audio_file = await download_file(audio_url, ".mp3")
74
-
75
- # Run ffmpeg command with improved audio detection parameters
76
- ffmpeg_cmd = f"ffmpeg -loop 1 -i {image_file} -analyzeduration 10000000 -probesize 10000000 -i {audio_file} -c:v libx264 -tune stillimage -c:a aac -b:a 192k -strict experimental -shortest -pix_fmt yuv420p {output_path}"
77
- process = await asyncio.create_subprocess_shell(
78
- ffmpeg_cmd,
79
- stdout=asyncio.subprocess.PIPE,
80
- stderr=asyncio.subprocess.PIPE
81
- )
82
- _, stderr = await process.communicate()
83
-
84
- if process.returncode != 0:
85
- print(f"FFmpeg error: {stderr.decode()}")
86
- raise HTTPException(status_code=500, detail=f"FFmpeg failed: {stderr.decode()}")
87
-
88
- # Clean up temporary files
89
- os.remove(image_file)
90
- os.remove(audio_file)
91
-
92
- # Return the URL path to the output file
93
- return f"/output/{output_filename}"
94
- except Exception as e:
95
- print(f"An error occurred: {str(e)}")
96
- print(traceback.format_exc())
97
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
98
-
99
- @app.post("/add_audio_to_video")
100
- async def add_audio_to_video(request: Request):
101
- try:
102
- # Generate a unique filename
103
- output_filename = f"{uuid.uuid4()}.mp4"
104
- output_path = os.path.join(OUTPUT_DIR, output_filename)
105
-
106
- # Call the modal API with the request data and download the output file
107
- data = await request.json()
108
- video_url = data.get("video_url")
109
- audio_url = data.get("audio_url")
110
-
111
- if not video_url or not audio_url:
112
- raise HTTPException(status_code=400, detail="Missing video_url or audio_url in request")
113
-
114
- video_file = await download_file(video_url, ".mp4")
115
- audio_file = await download_file(audio_url, ".mp3")
116
-
117
- # Run ffmpeg command with improved audio detection parameters
118
- ffmpeg_cmd = f"ffmpeg -analyzeduration 10000000 -probesize 10000000 -i {video_file} -analyzeduration 10000000 -probesize 10000000 -i {audio_file} -c:v copy -c:a aac -strict experimental -shortest {output_path}"
119
- process = await asyncio.create_subprocess_shell(
120
- ffmpeg_cmd,
121
- stdout=asyncio.subprocess.PIPE,
122
- stderr=asyncio.subprocess.PIPE
123
- )
124
- _, stderr = await process.communicate()
125
-
126
- if process.returncode != 0:
127
- print(f"FFmpeg error: {stderr.decode()}")
128
- raise HTTPException(status_code=500, detail=f"FFmpeg failed: {stderr.decode()}")
129
-
130
- # Clean up temporary files
131
- os.remove(video_file)
132
- os.remove(audio_file)
133
-
134
- # Return the URL path to the output file
135
- return f"/output/{output_filename}"
136
  except Exception as e:
137
- print(f"An error occurred: {str(e)}")
138
- print(traceback.format_exc())
139
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
140
-
141
- @app.post("/concatenate_videos")
142
- async def concatenate_videos(request: Request):
143
- try:
144
- # Generate a unique filename for the output
145
- output_filename = f"{uuid.uuid4()}.mp4"
146
- output_path = os.path.join(OUTPUT_DIR, output_filename)
147
-
148
- # Call the modal API with the request data and download the output file
149
- data = await request.json()
150
- video_urls = data.get("video_urls")
151
 
152
- if not video_urls or not isinstance(video_urls, list):
153
- raise HTTPException(status_code=400, detail="Invalid video_urls in request. Must be a list of URLs.")
154
-
155
- # Download the video files
156
- video_files = []
157
- for i, url in enumerate(video_urls):
158
- video_file = await download_file(url, f"_{i}.mp4")
159
- video_files.append(video_file)
160
-
161
- # Create a temporary file with the list of input files
162
- concat_list_path = os.path.join(OUTPUT_DIR, "concat_list.txt")
163
- with open(concat_list_path, "w") as f:
164
- for file in video_files:
165
- f.write(f"file '{file}'\n")
166
-
167
- # Run ffmpeg command
168
- ffmpeg_cmd = f"ffmpeg -f concat -safe 0 -i {concat_list_path} -c copy {output_path}"
169
- process = await asyncio.create_subprocess_shell(
170
- ffmpeg_cmd,
171
- stdout=asyncio.subprocess.PIPE,
172
- stderr=asyncio.subprocess.PIPE
173
- )
174
- _, stderr = await process.communicate()
175
-
176
- if process.returncode != 0:
177
- print(f"FFmpeg error: {stderr.decode()}")
178
- raise HTTPException(status_code=500, detail=f"FFmpeg failed: {stderr.decode()}")
179
-
180
- # Clean up temporary files
181
- for file in video_files:
182
- os.remove(file)
183
- os.remove(concat_list_path)
184
-
185
- # Return the URL path to the output file
186
- return f"/output/{output_filename}"
187
-
188
- except Exception as e:
189
- print(f"An error occurred: {str(e)}")
190
- print(traceback.format_exc())
191
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
192
 
193
- @app.post("/concatenate_audio")
194
- async def concatenate_audio(request: Request):
195
  try:
196
- # Generate a unique filename for the output
197
- output_filename = f"{uuid.uuid4()}.mp3"
198
- output_path = os.path.join(AUDIO_DIR, output_filename)
199
-
200
- # Call the modal API with the request data and download the output file
201
- data = await request.json()
202
- audio_urls = data.get("audio_urls")
203
-
204
- if not audio_urls or not isinstance(audio_urls, list):
205
- raise HTTPException(status_code=400, detail="Invalid audio_urls in request. Must be a list of URLs.")
206
-
207
- # Download the audio files
208
- audio_files = []
209
- for i, url in enumerate(audio_urls):
210
- audio_file = await download_file(url, f"_{i}.mp3")
211
- audio_files.append(audio_file)
212
-
213
- # Create a temporary file with the list of input files
214
- concat_list_path = os.path.join(AUDIO_DIR, "concat_list.txt")
215
- with open(concat_list_path, "w") as f:
216
- for file in audio_files:
217
- f.write(f"file '{file}'\n")
218
-
219
- # Run ffmpeg command with improved audio parameters
220
- ffmpeg_cmd = f"ffmpeg -f concat -safe 0 -i {concat_list_path} -c:a aac -b:a 192k -strict experimental {output_path}"
221
- process = await asyncio.create_subprocess_shell(
222
- ffmpeg_cmd,
223
- stdout=asyncio.subprocess.PIPE,
224
- stderr=asyncio.subprocess.PIPE
225
- )
226
- _, stderr = await process.communicate()
227
-
228
- if process.returncode != 0:
229
- print(f"FFmpeg error: {stderr.decode()}")
230
- raise HTTPException(status_code=500, detail=f"FFmpeg failed: {stderr.decode()}")
231
-
232
- # Clean up temporary files
233
- for file in audio_files:
234
- os.remove(file)
235
- os.remove(concat_list_path)
236
-
237
- # Return the URL path to the output file
238
- return f"/audio/{output_filename}"
239
-
240
- except Exception as e:
241
- print(f"An error occurred: {str(e)}")
242
- print(traceback.format_exc())
243
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
244
-
245
- @app.post("/make_video")
246
- async def make_video(request: Request):
247
  try:
248
- # Generate a unique filename for the output
249
- output_filename = f"{uuid.uuid4()}.mp4"
250
- output_path = os.path.join(OUTPUT_DIR, output_filename)
251
- temp_dir = os.path.join(tempfile.gettempdir(), generate_hash())
252
- os.makedirs(temp_dir, exist_ok=True)
253
-
254
- data = await request.json()
255
- assets = data.get("assets", {})
256
- clips = assets.get("clips", [])
257
- music_url = assets.get("music_url")
258
- volume_adjustment = data.get("volume_adjustment", 1.0) # Default to normal volume if not specified
259
-
260
- if not clips or not isinstance(clips, list):
261
- raise HTTPException(status_code=400, detail="Invalid clips in request.")
262
-
263
- # Create a list to hold clip video files that we'll concatenate later
264
- clip_videos = []
265
- segment_list_path = os.path.join(temp_dir, "segment_list.txt")
266
 
267
- with open(segment_list_path, "w") as segment_file:
268
- # Process each clip: combine image with its audio
269
- for i, clip in enumerate(clips):
270
- image_url = clip.get("image_url")
271
- audio_url = clip.get("audio_url")
272
-
273
- if not image_url or not audio_url:
274
- raise HTTPException(status_code=400, detail=f"Missing image_url or audio_url in clip {i}")
275
-
276
- # Download files
277
- image_file = await download_file(image_url, ".jpg")
278
- audio_file = await download_file(audio_url, ".mp3")
279
-
280
- # Create segment video
281
- segment_output = os.path.join(temp_dir, f"segment_{i}.mp4")
282
-
283
- # Run ffmpeg command to create video segment
284
- segment_cmd = f'ffmpeg -loop 1 -i {image_file} -i {audio_file} -c:v libx264 -tune stillimage -c:a aac -b:a 192k -shortest -pix_fmt yuv420p {segment_output}'
285
- proc = await asyncio.create_subprocess_shell(
286
- segment_cmd,
287
- stdout=asyncio.subprocess.PIPE,
288
- stderr=asyncio.subprocess.PIPE
289
- )
290
- _, stderr = await proc.communicate()
291
-
292
- if proc.returncode != 0:
293
- print(f"FFmpeg error for segment {i}: {stderr.decode()}")
294
- raise HTTPException(status_code=500, detail=f"FFmpeg failed for segment {i}: {stderr.decode()}")
295
-
296
- # Add to list for concatenation
297
- clip_videos.append(segment_output)
298
- segment_file.write(f"file '{segment_output}'\n")
299
-
300
- # Clean up individual files
301
- os.remove(image_file)
302
- os.remove(audio_file)
303
 
304
- # Concatenate all segment videos
305
- concat_output = os.path.join(temp_dir, "concat_output.mp4")
306
- concat_cmd = f'ffmpeg -f concat -safe 0 -i {segment_list_path} -c copy {concat_output}'
307
 
308
- proc = await asyncio.create_subprocess_shell(
309
- concat_cmd,
310
- stdout=asyncio.subprocess.PIPE,
311
- stderr=asyncio.subprocess.PIPE
312
- )
313
- _, stderr = await proc.communicate()
314
 
315
- if proc.returncode != 0:
316
- print(f"FFmpeg concat error: {stderr.decode()}")
317
- raise HTTPException(status_code=500, detail=f"FFmpeg concat failed: {stderr.decode()}")
318
-
319
- # If there's a music URL, download it and mix with the video
320
- if music_url:
321
- music_file = await download_file(music_url, ".wav")
322
-
323
- # Final command to mix music with video at specified volume
324
- final_cmd = (
325
- f'ffmpeg -i {concat_output} -i {music_file} -filter_complex '
326
- f'"[1:a]volume={volume_adjustment}[music];[0:a][music]amix=inputs=2:duration=longest" '
327
- f'-c:v copy {output_path}'
328
- )
329
-
330
- proc = await asyncio.create_subprocess_shell(
331
- final_cmd,
332
- stdout=asyncio.subprocess.PIPE,
333
- stderr=asyncio.subprocess.PIPE
334
- )
335
- _, stderr = await proc.communicate()
336
-
337
- if proc.returncode != 0:
338
- print(f"FFmpeg final mix error: {stderr.decode()}")
339
- raise HTTPException(status_code=500, detail=f"FFmpeg music mixing failed: {stderr.decode()}")
340
-
341
- os.remove(music_file)
342
- else:
343
- # If no music, just copy the concatenated output
344
- shutil.copy(concat_output, output_path)
345
-
346
- # Clean up temporary files and directory
347
- for video_file in clip_videos:
348
- os.remove(video_file)
349
- os.remove(segment_list_path)
350
- os.remove(concat_output)
351
- os.rmdir(temp_dir)
352
-
353
- # Return the URL path to the output file
354
- return f"/output/{output_filename}"
355
-
356
- except Exception as e:
357
- print(f"An error occurred: {str(e)}")
358
- print(traceback.format_exc())
359
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
360
-
361
- @app.post("/create_slideshow")
362
- async def create_slideshow(request: Request):
363
- try:
364
- # Generate a unique filename for the output
365
- output_filename = f"{uuid.uuid4()}.mp4"
366
- output_path = os.path.join(OUTPUT_DIR, output_filename)
367
- temp_dir = os.path.join(tempfile.gettempdir(), generate_hash())
368
- os.makedirs(temp_dir, exist_ok=True)
369
-
370
- data = await request.json()
371
- image_urls = data.get("image_urls")
372
- audio_url = data.get("audio_url")
373
- duration = data.get("duration", 4) # Default to 4 seconds per image if not specified
374
-
375
- if not image_urls or not isinstance(image_urls, list):
376
- raise HTTPException(status_code=400, detail="Invalid image_urls in request. Must be a list of URLs.")
377
-
378
- if not audio_url:
379
- raise HTTPException(status_code=400, detail="Missing audio_url in request.")
380
-
381
- # Download the images
382
- image_files = []
383
- for i, url in enumerate(image_urls):
384
- image_file = await download_file(url, f"_{i}.jpg")
385
- image_files.append(image_file)
386
-
387
- # Download audio file
388
- audio_file = await download_file(audio_url, ".mp3")
389
-
390
- # Pre-process audio file to ensure it's valid
391
- normalized_audio = os.path.join(temp_dir, "normalized_audio.wav")
392
- audio_check_cmd = (
393
- f'ffmpeg -i {audio_file} -af "aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo" '
394
- f'-y {normalized_audio}'
395
- )
396
- audio_process = await asyncio.create_subprocess_shell(
397
- audio_check_cmd,
398
- stdout=asyncio.subprocess.PIPE,
399
- stderr=asyncio.subprocess.PIPE
400
- )
401
- _, audio_stderr = await audio_process.communicate()
402
-
403
- # If audio normalization fails, create a silent audio track
404
- if audio_process.returncode != 0:
405
- print(f"Audio normalization failed: {audio_stderr.decode()}")
406
- print("Creating silent audio track instead...")
407
-
408
- # Calculate total video duration based on number of images and duration per image
409
- total_duration = len(image_urls) * duration
410
-
411
- silent_cmd = (
412
- f'ffmpeg -f lavfi -i anullsrc=r=44100:cl=stereo -t {total_duration} '
413
- f'-y {normalized_audio}'
414
- )
415
- silent_process = await asyncio.create_subprocess_shell(
416
- silent_cmd,
417
- stdout=asyncio.subprocess.PIPE,
418
- stderr=asyncio.subprocess.PIPE
419
- )
420
- _, silent_stderr = await silent_process.communicate()
421
-
422
- if silent_process.returncode != 0:
423
- print(f"Silent audio creation failed: {silent_stderr.decode()}")
424
- # Fall back to no audio if even silent audio fails
425
- normalized_audio = None
426
-
427
- # Create a text file with the list of images and their duration
428
- images_list_path = os.path.join(temp_dir, "images_list.txt")
429
- with open(images_list_path, "w") as f:
430
- for file in image_files:
431
- f.write(f"file '{file}'\n")
432
- f.write(f"duration {duration}\n")
433
- # Add the last image again with a small duration to prevent ffmpeg from cutting it off
434
- f.write(f"file '{image_files[-1]}'\n")
435
- f.write(f"duration 0.1\n")
436
-
437
- # Create intermediate video without audio
438
- intermediate_video = os.path.join(temp_dir, "intermediate.mp4")
439
- # Use the complex concat demuxer for images to create a slideshow
440
- concat_cmd = (
441
- f'ffmpeg -f concat -safe 0 -i {images_list_path} -vsync vfr '
442
- f'-pix_fmt yuv420p -c:v libx264 -r 30 {intermediate_video}'
443
- )
444
- process = await asyncio.create_subprocess_shell(
445
- concat_cmd,
446
- stdout=asyncio.subprocess.PIPE,
447
- stderr=asyncio.subprocess.PIPE
448
- )
449
- _, stderr = await process.communicate()
450
-
451
- if process.returncode != 0:
452
- print(f"FFmpeg slideshow creation error: {stderr.decode()}")
453
- raise HTTPException(status_code=500, detail=f"FFmpeg slideshow creation failed: {stderr.decode()}")
454
-
455
- # Add audio to the slideshow only if normalized_audio is available
456
- if normalized_audio:
457
- final_cmd = (
458
- f'ffmpeg -i {intermediate_video} -i {normalized_audio} '
459
- f'-c:v copy -c:a aac -b:a 192k -shortest {output_path}'
460
- )
461
- process = await asyncio.create_subprocess_shell(
462
- final_cmd,
463
- stdout=asyncio.subprocess.PIPE,
464
- stderr=asyncio.subprocess.PIPE
465
- )
466
- _, stderr = await process.communicate()
467
-
468
- if process.returncode != 0:
469
- print(f"FFmpeg audio addition error: {stderr.decode()}")
470
- # If adding audio fails, use just the video
471
- shutil.copy(intermediate_video, output_path)
472
- else:
473
- # If no normalized audio available, use just the video
474
- shutil.copy(intermediate_video, output_path)
475
-
476
- # Clean up temporary files
477
- for file in image_files:
478
- if os.path.exists(file):
479
- os.remove(file)
480
-
481
- if os.path.exists(images_list_path):
482
- os.remove(images_list_path)
483
-
484
- if os.path.exists(intermediate_video):
485
- os.remove(intermediate_video)
486
-
487
- if os.path.exists(audio_file):
488
- os.remove(audio_file)
489
-
490
- if normalized_audio and os.path.exists(normalized_audio):
491
- os.remove(normalized_audio)
492
-
493
- try:
494
- os.rmdir(temp_dir)
495
- except OSError:
496
- # Directory might not be empty, try to clean up remaining files
497
- for remaining_file in os.listdir(temp_dir):
498
- try:
499
- os.remove(os.path.join(temp_dir, remaining_file))
500
- except:
501
- pass
502
- try:
503
- os.rmdir(temp_dir)
504
- except:
505
- pass
506
-
507
- # Return the URL path to the output file
508
- return f"/output/{output_filename}"
509
-
510
  except Exception as e:
511
- print(f"An error occurred: {str(e)}")
512
- print(traceback.format_exc())
513
- raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
 
514
 
515
  if __name__ == "__main__":
516
  import uvicorn
517
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ from fastapi import FastAPI, HTTPException
2
  from fastapi.staticfiles import StaticFiles
3
+ from pydantic import BaseModel, HttpUrl
4
+ from typing import List
5
+ import os
6
+ import subprocess
7
  import uuid
8
+ import requests
9
+ import re
10
+ from urllib.parse import urlparse
11
  import shutil
 
 
 
 
12
 
13
+ # Create FastAPI app
14
  app = FastAPI()
15
 
16
+ # Create and mount staticfiles directory
17
+ os.makedirs("staticfiles", exist_ok=True)
18
+ app.mount("/static", StaticFiles(directory="staticfiles"), name="static")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
+ # Define input model for the request
21
+ class SlideshowRequest(BaseModel):
22
+ image_urls: List[HttpUrl]
23
+ audio_url: HttpUrl
24
+ duration: int
25
 
26
+ def extract_google_drive_id(url):
27
+ """Extract file ID from a Google Drive URL"""
28
+ pattern = r'(?:/file/d/|id=|/open\?id=)([^/&]+)'
29
+ match = re.search(pattern, str(url))
30
+ return match.group(1) if match else None
31
 
32
+ def download_file(url, local_path):
33
+ """Download a file from URL to local path"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  try:
35
+ # Handle Google Drive URLs
36
+ if "drive.google.com" in str(url):
37
+ file_id = extract_google_drive_id(url)
38
+ if file_id:
39
+ url = f"https://drive.google.com/uc?export=download&id={file_id}"
40
+
41
+ response = requests.get(str(url), stream=True)
42
+ response.raise_for_status()
43
+
44
+ with open(local_path, 'wb') as f:
45
+ for chunk in response.iter_content(chunk_size=8192):
46
+ f.write(chunk)
47
+ return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  except Exception as e:
49
+ print(f"Error downloading {url}: {str(e)}")
50
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ def create_slideshow(image_paths, audio_path, output_path, duration):
53
+ """Generate slideshow from images and audio using ffmpeg"""
54
+ # Create temporary file list for ffmpeg concat
55
+ concat_file = "temp_concat.txt"
56
+
57
+ with open(concat_file, "w") as f:
58
+ for img in image_paths:
59
+ f.write(f"file '{img}'\n")
60
+ f.write(f"duration {duration}\n")
61
+
62
+ # Add the last image again without duration (required by ffmpeg)
63
+ if image_paths:
64
+ f.write(f"file '{image_paths[-1]}'\n")
65
+
66
+ # Run ffmpeg command to create slideshow with audio
67
+ cmd = [
68
+ "ffmpeg",
69
+ "-f", "concat",
70
+ "-safe", "0",
71
+ "-i", concat_file,
72
+ "-i", audio_path,
73
+ "-c:v", "libx264",
74
+ "-pix_fmt", "yuv420p",
75
+ "-c:a", "aac",
76
+ "-shortest",
77
+ "-y",
78
+ output_path
79
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
80
 
 
 
81
  try:
82
+ subprocess.run(cmd, check=True, capture_output=True)
83
+ os.remove(concat_file)
84
+ return True
85
+ except subprocess.CalledProcessError as e:
86
+ print(f"FFmpeg error: {e.stderr.decode()}")
87
+ if os.path.exists(concat_file):
88
+ os.remove(concat_file)
89
+ return False
90
+
91
+ @app.post("/make_slideshow")
92
+ async def make_slideshow(request: SlideshowRequest):
93
+ """
94
+ Create a slideshow from images and audio with specified duration per image.
95
+ Returns the URL of the generated video.
96
+ """
97
+ # Create unique directory for this request
98
+ request_id = str(uuid.uuid4())
99
+ request_dir = os.path.join("staticfiles", request_id)
100
+ os.makedirs(request_dir, exist_ok=True)
101
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  try:
103
+ # Download images
104
+ image_paths = []
105
+ for i, url in enumerate(request.image_urls):
106
+ image_path = os.path.join(request_dir, f"image_{i:03d}.png")
107
+ if download_file(url, image_path):
108
+ image_paths.append(image_path)
109
+ else:
110
+ raise HTTPException(status_code=400, detail=f"Failed to download image: {url}")
 
 
 
 
 
 
 
 
 
 
111
 
112
+ # Download audio
113
+ audio_path = os.path.join(request_dir, "audio.mp3")
114
+ if not download_file(request.audio_url, audio_path):
115
+ raise HTTPException(status_code=400, detail=f"Failed to download audio: {request.audio_url}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
+ # Output video path
118
+ output_path = os.path.join(request_dir, "slideshow.mp4")
 
119
 
120
+ # Generate slideshow
121
+ if not create_slideshow(image_paths, audio_path, output_path, request.duration):
122
+ raise HTTPException(status_code=500, detail="Failed to create slideshow")
 
 
 
123
 
124
+ # Return URL to the video
125
+ base_url = "/static"
126
+ video_url = f"{base_url}/{request_id}/slideshow.mp4"
127
+ return {"url": video_url}
128
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
+ # Clean up on error
131
+ if os.path.exists(request_dir):
132
+ shutil.rmtree(request_dir)
133
+ raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
134
 
135
  if __name__ == "__main__":
136
  import uvicorn
137
+ uvicorn.run(app, host="0.0.0.0", port=8000)