File size: 5,688 Bytes
b416e9f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# main.py

import requests
import uuid
import time
import brotli
import json
import asyncio
from typing import Literal

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

# --- FastAPI App Initialization ---
app = FastAPI(
    title="Sora Video Generation API",
    description="An unofficial API wrapper to generate videos using the Bylo.ai Sora model.",
    version="1.0.0",
)

# --- Pydantic Models for Request and Response ---
class SoraRequest(BaseModel):
    prompt: str = Field(..., description="The text prompt to generate the video from.")
    ratio: Literal["portrait", "landscape"] = Field(
        "portrait", 
        description="The aspect ratio of the generated video."
    )

# --- Core Logic (Adapted from your script) ---

def decode_response(resp: requests.Response):
    """
    Safely decode response: Brotli β†’ UTF-8 β†’ JSON, with automatic fallback if Brotli fails.
    """
    raw_data = resp.content
    encoding = resp.headers.get("Content-Encoding", "").lower()

    if encoding == "br":
        try:
            raw_data = brotli.decompress(raw_data)
        except brotli.error:
            pass  # Fall back to raw UTF-8 if not actually Brotli compressed

    try:
        return json.loads(raw_data.decode("utf-8"))
    except Exception as je:
        snippet = raw_data[:500]
        raise RuntimeError(f"Failed to decode JSON: {je}\nRaw response snippet:\n{snippet}")

def sora(prompt: str, ratio: str = "portrait"):
    """
    The core function to interact with the Bylo.ai Sora API.
    This is a synchronous function intended to be run in a thread.
    """
    if not prompt:
        raise ValueError("Prompt is required.")
    if ratio not in ["portrait", "landscape"]:
        raise ValueError("Available ratios: portrait, landscape.")

    base_url = "https://api.bylo.ai/aimodels/api/v1/ai"

    headers = {
        "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
        "accept-language": "id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7",
        "accept-encoding": "gzip, deflate, br",
        "cache-control": "max-age=0",
        "connection": "keep-alive",
        "content-type": "application/json; charset=UTF-8",
        "dnt": "1",
        "origin": "https://bylo.ai",
        "pragma": "no-cache",
        "referer": "https://bylo.ai/features/sora-2",
        "sec-ch-prefers-color-scheme": "dark",
        "sec-ch-ua": '"Chromium";v="137", "Not/A)Brand";v="24"',
        "sec-ch-ua-mobile": "?1",
        "user-agent": "Mozilla/5.0 (Linux; Android 15; SM-F958 Build/AP3A.240905.015) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.86 Mobile Safari/537.36",
        "x-requested-with": "XMLHttpRequest",
        "uniqueId": str(uuid.uuid4()).replace("-", "")
    }

    payload = {
        "prompt": prompt,
        "channel": "SORA2",
        "pageId": 536,
        "source": "bylo.ai",
        "watermarkFlag": True,
        "privateFlag": True,
        "isTemp": True,
        "vipFlag": True,
        "model": "sora_video2",
        "videoType": "text-to-video",
        "aspectRatio": ratio
    }

    try:
        # STEP 1: Create generation task
        create_resp = requests.post(f"{base_url}/video/create", headers=headers, json=payload)
        create_resp.raise_for_status()
        task_data = decode_response(create_resp)

        task_id = task_data.get("data")
        if not task_id:
            raise RuntimeError(f"API did not return task_id. Full response:\n{json.dumps(task_data, indent=2)}")

        # STEP 2: Poll until done
        # Set a reasonable timeout to prevent infinite loops
        polling_start_time = time.time()
        timeout_seconds = 300  # 5 minutes

        while True:
            if time.time() - polling_start_time > timeout_seconds:
                raise RuntimeError("Polling timed out after 5 minutes.")

            poll_resp = requests.get(f"{base_url}/{task_id}?channel=SORA2", headers=headers)
            poll_resp.raise_for_status()
            poll_json = decode_response(poll_resp)

            state = poll_json.get("data", {}).get("state", 0)
            if state > 0:
                complete_data_str = poll_json.get("data", {}).get("completeData")
                if not complete_data_str:
                    raise RuntimeError(f"Generation completed but no 'completeData' found. Full response: \n{poll_json}")
                return json.loads(complete_data_str)

            time.sleep(5) # Increased sleep time for polling

    except requests.exceptions.RequestException as re:
        raise RuntimeError(f"Network error during Sora generation: {re}")
    except Exception as e:
        # Re-raise exceptions to be caught by the endpoint handler
        raise RuntimeError(f"Sora generation failed: {e}")


# --- API Endpoint ---
@app.post("/generate", summary="Create a new Sora video")
async def generate_video(request: SoraRequest):
    """
    Accepts a prompt and ratio, then generates a video by polling the Bylo.ai API.
    """
    try:
        # Run the synchronous, blocking sora function in a separate thread
        result = await asyncio.to_thread(sora, request.prompt, request.ratio)
        return result
    except ValueError as ve:
        # Handle bad input from the user
        raise HTTPException(status_code=400, detail=str(ve))
    except RuntimeError as re:
        # Handle errors from the external API or other processing issues
        raise HTTPException(status_code=500, detail=str(re))

@app.get("/", include_in_schema=False)
def root():
    return {"message": "Sora API is running. See /docs for usage."}