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
    }