mirror of
https://github.com/R0m1k3/Socialflow.git
synced 2026-10-23 15:08:13 +02:00
feat: Implement a FastAPI-based FFmpeg service for video processing, including API key authentication, video/audio input handling, text overlay, and audio mixing.
This commit is contained in:
1 parent
e29048d374
commit
430187cbad
2 files changed
+25
-19
No files matched your search
+24
-18
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import FastAPI, HTTPException, Header, Body
|
from fastapi import FastAPI, HTTPException, Header
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import uvicorn
|
import uvicorn
|
||||||
@@ -7,7 +7,6 @@ import os
|
|||||||
import uuid
|
import uuid
|
||||||
import base64
|
import base64
|
||||||
import requests
|
import requests
|
||||||
import json
|
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -17,6 +16,7 @@ API_KEY = os.environ.get("API_KEY", "default-key")
|
|||||||
TEMP_DIR = Path("/tmp/ffmpeg_processing")
|
TEMP_DIR = Path("/tmp/ffmpeg_processing")
|
||||||
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
TEMP_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class ReelRequest(BaseModel):
|
class ReelRequest(BaseModel):
|
||||||
video_base64: Optional[str] = None
|
video_base64: Optional[str] = None
|
||||||
video_url: Optional[str] = None
|
video_url: Optional[str] = None
|
||||||
@@ -27,18 +27,21 @@ class ReelRequest(BaseModel):
|
|||||||
font_size: int = 60
|
font_size: int = 60
|
||||||
music_volume: float = 0.25
|
music_volume: float = 0.25
|
||||||
|
|
||||||
|
|
||||||
class ReelResponse(BaseModel):
|
class ReelResponse(BaseModel):
|
||||||
success: boolean
|
success: bool
|
||||||
output_base64: Optional[str] = None
|
output_base64: Optional[str] = None
|
||||||
duration: Optional[float] = None
|
duration: Optional[float] = None
|
||||||
detail: Optional[str] = None
|
detail: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health_check(x_api_key: str = Header(None)):
|
def health_check(x_api_key: str = Header(None)):
|
||||||
if x_api_key != API_KEY:
|
if x_api_key != API_KEY:
|
||||||
raise HTTPException(status_code=401, detail="Invalid API Key")
|
raise HTTPException(status_code=401, detail="Invalid API Key")
|
||||||
return {"status": "healthy"}
|
return {"status": "healthy"}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/process-reel")
|
@app.post("/process-reel")
|
||||||
async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
||||||
if x_api_key != API_KEY:
|
if x_api_key != API_KEY:
|
||||||
@@ -70,7 +73,7 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
if request.music_url:
|
if request.music_url:
|
||||||
try:
|
try:
|
||||||
# Add User-Agent to avoid 403 on some CDNs
|
# Add User-Agent to avoid 403 on some CDNs
|
||||||
headers = {'User-Agent': 'Mozilla/5.0'}
|
headers = {"User-Agent": "Mozilla/5.0"}
|
||||||
response = requests.get(request.music_url, headers=headers, stream=True)
|
response = requests.get(request.music_url, headers=headers, stream=True)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
with open(input_audio_path, "wb") as f:
|
with open(input_audio_path, "wb") as f:
|
||||||
@@ -134,13 +137,13 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
filter_str = ",".join(filters)
|
filter_str = ",".join(filters)
|
||||||
|
|
||||||
if filters:
|
if filters:
|
||||||
cmd.extend(["-vf", filter_str])
|
cmd.extend(["-vf", filter_str])
|
||||||
|
|
||||||
if has_music:
|
if has_music:
|
||||||
# Map video from 0, audio from 1 (music), shortest (cut music to video len)
|
# Map video from 0, audio from 1 (music), shortest (cut music to video len)
|
||||||
cmd.extend(["-map", "0:v", "-map", "1:a", "-shortest"])
|
cmd.extend(["-map", "0:v", "-map", "1:a", "-shortest"])
|
||||||
else:
|
else:
|
||||||
cmd.extend(["-map", "0:v", "-map", "0:a?"]) # Keep original audio if exists
|
cmd.extend(["-map", "0:v", "-map", "0:a?"]) # Keep original audio if exists
|
||||||
|
|
||||||
cmd.append(str(output_video_path))
|
cmd.append(str(output_video_path))
|
||||||
|
|
||||||
@@ -152,31 +155,34 @@ async def process_reel(request: ReelRequest, x_api_key: str = Header(None)):
|
|||||||
raise Exception(f"FFmpeg encoding failed: {process.stderr.decode()}")
|
raise Exception(f"FFmpeg encoding failed: {process.stderr.decode()}")
|
||||||
|
|
||||||
# 4. Get Duration (ffprobe)
|
# 4. Get Duration (ffprobe)
|
||||||
duration_cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", str(output_video_path)]
|
duration_cmd = [
|
||||||
|
"ffprobe",
|
||||||
|
"-v",
|
||||||
|
"error",
|
||||||
|
"-show_entries",
|
||||||
|
"format=duration",
|
||||||
|
"-of",
|
||||||
|
"default=noprint_wrappers=1:nokey=1",
|
||||||
|
str(output_video_path),
|
||||||
|
]
|
||||||
dur_proc = subprocess.run(duration_cmd, stdout=subprocess.PIPE)
|
dur_proc = subprocess.run(duration_cmd, stdout=subprocess.PIPE)
|
||||||
duration = float(dur_proc.stdout.decode().strip() or 0)
|
duration = float(dur_proc.stdout.decode().strip() or 0)
|
||||||
|
|
||||||
# 5. Read Output
|
# 5. Read Output
|
||||||
with open(output_video_path, "rb") as f:
|
with open(output_video_path, "rb") as f:
|
||||||
out_bytes = f.read()
|
out_bytes = f.read()
|
||||||
out_b64 = base64.b64encode(out_bytes).decode('utf-8')
|
out_b64 = base64.b64encode(out_bytes).decode("utf-8")
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
shutil.rmtree(job_dir)
|
shutil.rmtree(job_dir)
|
||||||
|
|
||||||
return {
|
return {"success": True, "output_base64": out_b64, "duration": duration}
|
||||||
"success": True,
|
|
||||||
"output_base64": out_b64,
|
|
||||||
"duration": duration
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if 'job_dir' in locals():
|
if "job_dir" in locals():
|
||||||
shutil.rmtree(job_dir, ignore_errors=True)
|
shutil.rmtree(job_dir, ignore_errors=True)
|
||||||
return {
|
return {"success": False, "detail": str(e)}
|
||||||
"success": False,
|
|
||||||
"detail": str(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||||
@@ -3,4 +3,4 @@ uvicorn==0.27.0
|
|||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
pydantic==2.6.0
|
pydantic==2.6.0
|
||||||
shutil
|
pydantic==2.6.0
|
||||||
Reference in new issue
Block a user