File size: 8,602 Bytes
9cf661f e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f e3843bf a0f4ba8 e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f 14fe91b 82ec7c2 14fe91b e902a4a 14fe91b e902a4a 14fe91b e902a4a 9cf661f 14fe91b e3843bf 9cf661f e3843bf a8e28bb e3843bf a8e28bb e3843bf 9cf661f e3843bf a8e28bb e3843bf 14fe91b e3843bf 9cf661f e3843bf 9cf661f e3843bf 9cf661f 8618730 14fe91b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | import os
import subprocess
import concurrent.futures
import uuid
import time
import json
from typing import List, Optional, Dict
from fastapi import FastAPI, BackgroundTasks, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import cloudinary
import cloudinary.uploader
from ass_generator import generate_ass
# ------------------------------------------
# CONFIGURATION
# ------------------------------------------
CLOUD_NAME = "dgfhhszx8"
UPLOAD_PRESET = "testing"
JOBS: Dict[str, dict] = {}
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class TranscriptItem(BaseModel):
text: str
start: float
end: float
class VideoRequest(BaseModel):
video_url: str
transcript: Optional[List[TranscriptItem]] = None
style: Optional[str] = "hormozi"
# ------------------------------------------
# BACKGROUND WORKER
# ------------------------------------------
def process_video_background(job_id: str, req: VideoRequest):
print(f"[{job_id}] Starting Job")
JOBS[job_id]["status"] = "processing"
work_dir = f"/tmp/{job_id}"
os.makedirs(work_dir, exist_ok=True)
ass_file = os.path.join(work_dir, "captions.ass")
output_video = os.path.join(work_dir, "output.webm") # WebM for transparency
try:
# 1. CALCULATE DURATION
duration = 5.0 # Default
if req.transcript and len(req.transcript) > 0:
last_item = req.transcript[-1]
duration = float(last_item.end) + 1.0 # 1s buffer
JOBS[job_id]["progress"] = f"Generating {duration}s Transparent Layer..."
# 2. GENERATE CAPTIONS
if req.transcript:
transcript_dicts = [t.dict() for t in req.transcript]
generate_ass(transcript_dicts, style_name=req.style, output_file=ass_file)
# DEBUG: Print the ASS file contents
with open(ass_file, 'r') as f:
ass_content = f.read()
print(f"[{job_id}] ASS File Content:\n{ass_content}")
# 3. GENERATE TRANSPARENT WEBM
# Key fixes:
# - Use shell=True with properly escaped command
# - Use drawtext as fallback if ASS fails
# - Ensure alpha channel is preserved
# Escape special chars for shell
ass_file_shell = ass_file.replace("'", "'\\''")
# Method 1: Try with ASS filter
cmd = (
f"ffmpeg -y "
f"-f lavfi -i 'color=c=0x000000@0:s=1280x720:d={duration}:r=30,format=rgba' "
f"-vf \"ass='{ass_file_shell}'\" "
f"-c:v libvpx-vp9 -pix_fmt yuva420p -auto-alt-ref 0 "
f"-b:v 1M -deadline realtime -cpu-used 8 "
f"'{output_video}'"
)
print(f"[{job_id}] Running FFmpeg command:\n{cmd}")
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True
)
print(f"[{job_id}] FFmpeg STDOUT: {result.stdout}")
print(f"[{job_id}] FFmpeg STDERR: {result.stderr}")
if result.returncode != 0:
raise Exception(f"FFmpeg failed: {result.stderr}")
# Verify output file was created and has content
if not os.path.exists(output_video):
raise Exception("Output video file was not created")
file_size = os.path.getsize(output_video)
print(f"[{job_id}] Output video size: {file_size} bytes")
if file_size < 1000:
raise Exception(f"Output video too small ({file_size} bytes), likely empty")
upload_target = output_video
else:
raise Exception("No transcript provided")
# 4. UPLOAD
JOBS[job_id]["progress"] = "Uploading Transparent Layer..."
res_vid = cloudinary.uploader.unsigned_upload(
output_video, UPLOAD_PRESET, cloud_name=CLOUD_NAME, resource_type="video"
)
# DEBUG: Upload ASS file too
res_ass = cloudinary.uploader.unsigned_upload(
ass_file, UPLOAD_PRESET, cloud_name=CLOUD_NAME, resource_type="raw"
)
url_vid = res_vid['secure_url']
url_ass = res_ass['secure_url']
# 5. CLEANUP
try:
if os.path.exists(output_video): os.remove(output_video)
if os.path.exists(ass_file): os.remove(ass_file)
os.rmdir(work_dir)
except: pass
JOBS[job_id]["status"] = "completed"
JOBS[job_id]["progress"] = "Done"
JOBS[job_id]["result"] = [url_vid, url_ass]
except Exception as e:
import traceback
error_details = traceback.format_exc()
print(f"[{job_id}] FAILED: {error_details}")
JOBS[job_id]["status"] = "failed"
JOBS[job_id]["error"] = str(e)
# ------------------------------------------
# API ENDPOINTS
# ------------------------------------------
@app.post("/jobs")
def submit_job(req: VideoRequest, background_tasks: BackgroundTasks):
job_id = str(uuid.uuid4())
JOBS[job_id] = {
"status": "queued",
"progress": "Waiting...",
"result": None,
"created_at": time.time()
}
background_tasks.add_task(process_video_background, job_id, req)
return {"job_id": job_id, "status": "queued"}
@app.get("/jobs/{job_id}")
def get_job_status(job_id: str):
job = JOBS.get(job_id)
if not job: raise HTTPException(status_code=404)
return job
@app.get("/")
def home():
return {"message": "Caption Engine V3 Running"}
@app.get("/debug/test")
def debug_test():
"""
Debug endpoint that generates a simple test video with text
to verify FFmpeg is working correctly with transparency.
"""
import shutil
work_dir = "/tmp/debug_test"
output_path = os.path.join(work_dir, "test.webm")
try:
os.makedirs(work_dir, exist_ok=True)
# Simple test: Generate transparent video with drawtext
cmd = (
f"ffmpeg -y "
f"-f lavfi -i 'color=black@0:s=1280x720:d=3,format=rgba' "
f"-vf \"drawtext=text='HELLO WORLD':fontsize=60:fontcolor=yellow:x=(w-text_w)/2:y=(h-text_h)/2\" "
f"-c:v libvpx-vp9 -pix_fmt yuva420p -auto-alt-ref 0 "
f"-b:v 1M "
f"'{output_path}'"
)
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
response = {
"command": cmd,
"stdout": result.stdout,
"stderr": result.stderr,
"return_code": result.returncode,
"file_exists": os.path.exists(output_path),
"file_size": os.path.getsize(output_path) if os.path.exists(output_path) else 0
}
# Upload to cloudinary if successful
if result.returncode == 0 and os.path.exists(output_path):
res = cloudinary.uploader.unsigned_upload(
output_path, UPLOAD_PRESET, cloud_name=CLOUD_NAME, resource_type="video"
)
response["cloudinary_url"] = res.get("secure_url")
# Cleanup
shutil.rmtree(work_dir, ignore_errors=True)
return response
except Exception as e:
import traceback
return {"error": str(e), "traceback": traceback.format_exc()}
@app.get("/debug/fonts")
def debug_fonts():
"""Check available fonts on the system"""
result = subprocess.run(
"fc-list : family | sort | uniq",
shell=True, capture_output=True, text=True
)
fonts = result.stdout.strip().split("\n")
return {"fonts": fonts[:50], "total": len(fonts)} # Limit output
@app.get("/debug/ffmpeg")
def debug_ffmpeg():
"""Check FFmpeg version and capabilities"""
version = subprocess.run("ffmpeg -version | head -5", shell=True, capture_output=True, text=True)
encoders = subprocess.run("ffmpeg -encoders 2>/dev/null | grep vp9", shell=True, capture_output=True, text=True)
filters = subprocess.run("ffmpeg -filters 2>/dev/null | grep -E '(ass|subtitles)'", shell=True, capture_output=True, text=True)
return {
"version": version.stdout,
"vp9_encoders": encoders.stdout,
"subtitle_filters": filters.stdout
}
|