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."} |