saq1b commited on
Commit
6586de4
·
1 Parent(s): 2eead49

Add video processing features: create video from images and audio, concatenate clips, and adjust audio volume

Browse files
Files changed (1) hide show
  1. main.py +170 -0
main.py CHANGED
@@ -1,11 +1,13 @@
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 asyncio
7
  import uuid
8
  import aiohttp
 
9
  import re
10
  from urllib.parse import urlparse
11
  import shutil
@@ -25,6 +27,10 @@ class SlideshowRequest(BaseModel):
25
  duration: int
26
  zoom: bool = False
27
 
 
 
 
 
28
  def extract_google_drive_id(url):
29
  """Extract file ID from a Google Drive URL"""
30
  pattern = r'(?:/file/d/|id=|/open\?id=)([^/&]+)'
@@ -197,6 +203,170 @@ async def make_slideshow(request: SlideshowRequest):
197
  shutil.rmtree(request_dir)
198
  raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
199
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
  if __name__ == "__main__":
201
  import uvicorn
202
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
  from fastapi import FastAPI, HTTPException
2
  from fastapi.staticfiles import StaticFiles
3
+ from fastapi.responses import FileResponse
4
  from pydantic import BaseModel, HttpUrl
5
  from typing import List
6
  import os
7
  import asyncio
8
  import uuid
9
  import aiohttp
10
+ import traceback
11
  import re
12
  from urllib.parse import urlparse
13
  import shutil
 
27
  duration: int
28
  zoom: bool = False
29
 
30
+ class MakeVideoInput(BaseModel):
31
+ assets: dict
32
+ volume_adjustment: float = 1.0
33
+
34
  def extract_google_drive_id(url):
35
  """Extract file ID from a Google Drive URL"""
36
  pattern = r'(?:/file/d/|id=|/open\?id=)([^/&]+)'
 
203
  shutil.rmtree(request_dir)
204
  raise HTTPException(status_code=500, detail=f"Error: {str(e)}")
205
 
206
+
207
+ async def download_file_with_extension(url, extension, output_dir):
208
+ """Download file and save with the provided extension"""
209
+ file_id = str(uuid.uuid4())
210
+ file_path = os.path.join(output_dir, f"{file_id}{extension}")
211
+ success = await download_file(url, file_path)
212
+ if not success:
213
+ raise HTTPException(status_code=400, detail=f"Failed to download file from {url}")
214
+ return file_path
215
+
216
+ async def add_audio_to_image(image_path, audio_path, output_dir):
217
+ """Create a video from an image and audio file"""
218
+ output_id = str(uuid.uuid4())
219
+ output_path = os.path.join(output_dir, f"{output_id}.mp4")
220
+
221
+ cmd = [
222
+ "ffmpeg", "-y",
223
+ "-loop", "1",
224
+ "-i", image_path,
225
+ "-i", audio_path,
226
+ "-c:v", "libx264",
227
+ "-tune", "stillimage",
228
+ "-c:a", "aac",
229
+ "-pix_fmt", "yuv420p",
230
+ "-shortest",
231
+ output_path
232
+ ]
233
+
234
+ process = await asyncio.create_subprocess_exec(
235
+ *cmd,
236
+ stdout=asyncio.subprocess.PIPE,
237
+ stderr=asyncio.subprocess.PIPE
238
+ )
239
+ stdout, stderr = await process.communicate()
240
+
241
+ if process.returncode != 0:
242
+ print(f"FFmpeg error: {stderr.decode()}")
243
+ raise HTTPException(status_code=500, detail="Failed to add audio to image")
244
+
245
+ return {"output_path": output_path}
246
+
247
+ async def concatenate_videos(clip_paths, output_dir):
248
+ """Concatenate multiple video clips"""
249
+ output_id = str(uuid.uuid4())
250
+ output_path = os.path.join(output_dir, f"{output_id}.mp4")
251
+
252
+ # Create temporary file list for ffmpeg concat
253
+ concat_file = os.path.join(output_dir, "concat_list.txt")
254
+
255
+ async with aiofiles.open(concat_file, "w") as f:
256
+ for clip in clip_paths:
257
+ await f.write(f"file '{clip}'\n")
258
+
259
+ cmd = [
260
+ "ffmpeg", "-y",
261
+ "-f", "concat",
262
+ "-safe", "0",
263
+ "-i", concat_file,
264
+ "-c", "copy",
265
+ output_path
266
+ ]
267
+
268
+ process = await asyncio.create_subprocess_exec(
269
+ *cmd,
270
+ stdout=asyncio.subprocess.PIPE,
271
+ stderr=asyncio.subprocess.PIPE
272
+ )
273
+ stdout, stderr = await process.communicate()
274
+
275
+ # Clean up
276
+ if os.path.exists(concat_file):
277
+ os.remove(concat_file)
278
+
279
+ if process.returncode != 0:
280
+ print(f"FFmpeg error: {stderr.decode()}")
281
+ raise HTTPException(status_code=500, detail="Failed to concatenate videos")
282
+
283
+ return {"output_path": output_path}
284
+
285
+ async def add_audio_to_video(video_path, audio_path, volume_adjustment, output_dir):
286
+ """Add background music to video with volume adjustment"""
287
+ output_filename = f"final_video_{str(uuid.uuid4())}.mp4"
288
+ output_path = os.path.join(output_dir, output_filename)
289
+
290
+ cmd = [
291
+ "ffmpeg", "-y",
292
+ "-i", video_path,
293
+ "-i", audio_path,
294
+ "-filter_complex", f"[1:a]volume={volume_adjustment}[a1];[0:a][a1]amix=inputs=2:duration=longest[a]",
295
+ "-map", "0:v",
296
+ "-map", "[a]",
297
+ "-c:v", "copy",
298
+ "-c:a", "aac",
299
+ "-b:a", "192k",
300
+ output_path
301
+ ]
302
+
303
+ process = await asyncio.create_subprocess_exec(
304
+ *cmd,
305
+ stdout=asyncio.subprocess.PIPE,
306
+ stderr=asyncio.subprocess.PIPE
307
+ )
308
+ stdout, stderr = await process.communicate()
309
+
310
+ if process.returncode != 0:
311
+ print(f"FFmpeg error: {stderr.decode()}")
312
+ raise HTTPException(status_code=500, detail="Failed to add audio to video")
313
+
314
+ return {"output_path": output_path, "output_filename": output_filename}
315
+
316
+ @app.post("/make_video")
317
+ async def make_video(input_data: MakeVideoInput):
318
+ try:
319
+ # Create unique directory for this request
320
+ request_id = str(uuid.uuid4())
321
+ request_dir = os.path.join("staticfiles", request_id)
322
+ os.makedirs(request_dir, exist_ok=True)
323
+
324
+ assets = input_data.assets
325
+ volume_adjustment = input_data.volume_adjustment
326
+
327
+ # Download music
328
+ music_url = assets["music_url"]
329
+ music_path = await download_file_with_extension(music_url, ".mp3", request_dir)
330
+
331
+ # Process each clip
332
+ clips = []
333
+ for clip_data in assets["clips"]:
334
+ image_url = clip_data["image_url"]
335
+ audio_url = clip_data["audio_url"]
336
+
337
+ image_path = await download_file_with_extension(image_url, ".png", request_dir)
338
+ audio_path = await download_file_with_extension(audio_url, ".mp3", request_dir)
339
+
340
+ add_audio_to_image_result = await add_audio_to_image(image_path, audio_path, request_dir)
341
+
342
+ clips.append(add_audio_to_image_result["output_path"])
343
+
344
+ # Concatenate all the clips
345
+ concatenate_videos_result = await concatenate_videos(clips, request_dir)
346
+ concatenate_videos_output_path = concatenate_videos_result["output_path"]
347
+
348
+ # Add background music
349
+ add_audio_to_video_result = await add_audio_to_video(
350
+ concatenate_videos_output_path,
351
+ music_path,
352
+ volume_adjustment,
353
+ request_dir
354
+ )
355
+
356
+ # Return the final video
357
+ return FileResponse(
358
+ add_audio_to_video_result["output_path"],
359
+ media_type="video/mp4",
360
+ filename=add_audio_to_video_result["output_filename"]
361
+ )
362
+ except Exception as e:
363
+ # Clean up on error
364
+ if os.path.exists(request_dir):
365
+ shutil.rmtree(request_dir)
366
+ print(f"An error occurred: {str(e)}")
367
+ print(traceback.format_exc())
368
+ raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
369
+
370
  if __name__ == "__main__":
371
  import uvicorn
372
  uvicorn.run(app, host="0.0.0.0", port=7860)