253 lines
7.9 KiB
Python
253 lines
7.9 KiB
Python
from fastapi import FastAPI, Form # <-- Form is imported here
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.responses import StreamingResponse, JSONResponse
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from tortoise.contrib.fastapi import register_tortoise
|
||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||
from apscheduler.triggers.cron import CronTrigger
|
||
from .config import settings
|
||
from .tasks.user import free_credit
|
||
from .services import storage as storage_service, tool as tool_service
|
||
from .routes import register_routers
|
||
from .models import SunoTask
|
||
from pytz import timezone
|
||
from typing import Annotated # We keep the import but adjust usage to fix the NameError
|
||
import fal_client
|
||
import os
|
||
import httpx
|
||
import asyncio
|
||
import json
|
||
# مدل مورد نظر Fal AI برای تولید ویدئو از متن
|
||
FAL_VIDEO_MODEL = "fal-ai/pixverse/v3.5/text-to-video"
|
||
FAL_API_KEY = os.getenv("FAL_KEY") # کلید Fal API از محیط
|
||
FAL_BASE_URL = "https://queue.fal.run/fal-ai/pixverse"
|
||
|
||
app = FastAPI()
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=["*"],
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
allow_credentials=True,
|
||
)
|
||
|
||
|
||
# Create scheduler
|
||
scheduler = AsyncIOScheduler()
|
||
|
||
|
||
# Add a job to the scheduler
|
||
tehran_tz = timezone('Asia/Tehran')
|
||
|
||
# scheduler.add_job(
|
||
# free_credit,
|
||
# CronTrigger(hour=0, minute=0, second=0, timezone=tehran_tz)
|
||
# )
|
||
|
||
# Start the scheduler
|
||
scheduler.start()
|
||
|
||
register_tortoise(
|
||
app,
|
||
db_url=settings.database_url,
|
||
modules={"models": ["src.models"]},
|
||
generate_schemas=False,
|
||
add_exception_handlers=True,
|
||
)
|
||
|
||
app.mount(
|
||
"/.well-known",
|
||
StaticFiles(directory="static/.well-known"),
|
||
name=".well-known",
|
||
)
|
||
|
||
register_routers(app)
|
||
|
||
|
||
@app.get("/", tags=["home"])
|
||
async def info():
|
||
return {
|
||
"image": "https://www.instagram.com/houshan.ai/",
|
||
"audio": "https://www.instagram.com/houshan.ai/",
|
||
"video": "https://www.instagram.com/houshan.ai/",
|
||
}
|
||
|
||
|
||
@app.get("/apk", tags=["app"])
|
||
async def apk():
|
||
file_size = await storage_service.get_file_size("apk/app-release.apk")
|
||
|
||
headers = {
|
||
"Content-Disposition": 'attachment; filename="houshan.apk"',
|
||
"Content-Length": str(file_size),
|
||
}
|
||
|
||
return StreamingResponse(
|
||
storage_service.get("apk/app-release.apk"),
|
||
media_type="application/octet-stream",
|
||
headers=headers,
|
||
)
|
||
|
||
|
||
# ------------------------------------
|
||
## 🎥 ر: تولید ویدئو از متن (با استفاده مستقیم از fal_client)
|
||
# ----------------------------------------------------------------------
|
||
@app.post("/public/generate-video", tags=["public"])
|
||
async def generate_public_video(
|
||
prompt: str = Form(..., description="The text prompt to generate the video from."),
|
||
length: int = Form(5),
|
||
negative_prompt: str = Form("blurry, low quality"),
|
||
):
|
||
if not FAL_API_KEY:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "FAL_KEY environment variable is not set."
|
||
}, 500)
|
||
|
||
headers = {
|
||
"Authorization": f"Key {FAL_API_KEY}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
# مرحله ۱: ارسال درخواست تولید ویدیو
|
||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||
try:
|
||
submit_url = f"{FAL_BASE_URL}/v3.5/text-to-video"
|
||
response = await client.post(
|
||
submit_url,
|
||
headers=headers,
|
||
json={
|
||
"prompt": prompt,
|
||
"duration": "5",
|
||
"resolution":"540p",
|
||
"negative_prompt": negative_prompt
|
||
}
|
||
)
|
||
|
||
if response.status_code != 200:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "Failed to submit video generation request.",
|
||
"details": response.text
|
||
}, response.status_code)
|
||
|
||
data = response.json()
|
||
request_id = data.get("request_id")
|
||
|
||
if not request_id:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "No request_id returned from Fal API.",
|
||
"details": data
|
||
}, 500)
|
||
|
||
print(f"✅ Fal request submitted. Request ID: {request_id}")
|
||
|
||
except Exception as e:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "Failed to connect to Fal API.",
|
||
"details": str(e)
|
||
}, 500)
|
||
|
||
# مرحله ۲: بررسی وضعیت تا زمان تکمیل
|
||
status_url = f"{FAL_BASE_URL}/requests/{request_id}/status"
|
||
for attempt in range(30):
|
||
await asyncio.sleep(3)
|
||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||
status_response = await client.get(status_url, headers=headers)
|
||
status_data = status_response.json()
|
||
status = status_data.get("status")
|
||
|
||
print(f"🔄 Fal Status Check {attempt+1}: {status}")
|
||
|
||
if status == "COMPLETED":
|
||
break
|
||
elif status in ["FAILED", "ERROR"]:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "Video generation failed.",
|
||
"details": status_data
|
||
}, 500)
|
||
else:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "Timeout: Video not completed in expected time."
|
||
}, 504)
|
||
|
||
# مرحله ۳: دریافت نتیجه نهایی
|
||
result_url = f"{FAL_BASE_URL}/requests/{request_id}"
|
||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||
final_response = await client.get(result_url, headers=headers)
|
||
|
||
if final_response.status_code != 200:
|
||
return JSONResponse({
|
||
"status": "error",
|
||
"message": "Failed to retrieve final video result.",
|
||
"details": final_response.text
|
||
}, final_response.status_code)
|
||
|
||
final_data = final_response.json()
|
||
|
||
return JSONResponse({
|
||
"status": "success",
|
||
"message": "Video generated successfully.",
|
||
"video_result": final_data
|
||
})
|
||
|
||
@app.post("/suno/callback")
|
||
async def suno_callback(payload: dict):
|
||
print("=== Suno Callback Received ===")
|
||
print("Payload:", json.dumps(payload, ensure_ascii=False, indent=2))
|
||
|
||
task_id = payload.get("data", {}).get("task_id")
|
||
callback_type = payload.get("data", {}).get("callbackType")
|
||
|
||
if not task_id:
|
||
print("WARNING: task_id missing in callback")
|
||
return {"ok": True}
|
||
|
||
suno_task = await SunoTask.get_or_none(task_id=task_id)
|
||
if not suno_task:
|
||
print(f"WARNING: Task {task_id} not found in DB")
|
||
return {"ok": True}
|
||
|
||
suno_task.raw_callback = payload
|
||
|
||
if callback_type in ("text", "first"):
|
||
suno_task.status = "PROCESSING"
|
||
print(f"Task {task_id} status updated to PROCESSING")
|
||
|
||
elif callback_type == "complete":
|
||
items = payload.get("data", {}).get("data", [])
|
||
if items:
|
||
suno_task.audio_url = items[0].get("audio_url")
|
||
suno_task.image_url = items[0].get("image_url")
|
||
print(f"Task {task_id} audio_url: {suno_task.audio_url}")
|
||
print(f"Task {task_id} image_url: {suno_task.image_url}")
|
||
|
||
suno_task.status = "COMPLETE"
|
||
print(f"Task {task_id} status updated to COMPLETE")
|
||
|
||
await suno_task.save()
|
||
print(f"Task {task_id} saved to DB")
|
||
return {"ok": True}
|
||
|
||
|
||
@app.get("/api/suno/tasks/{task_id}")
|
||
async def get_suno_task(task_id: str):
|
||
suno_task = await SunoTask.get_or_none(task_id=task_id)
|
||
|
||
if not suno_task:
|
||
print(f"Task {task_id} not found")
|
||
raise HTTPException(404, "Task not found")
|
||
|
||
result = {
|
||
"task_id": suno_task.task_id,
|
||
"status": suno_task.status,
|
||
"audio_url": suno_task.audio_url,
|
||
"image_url": suno_task.image_url,
|
||
}
|
||
print(f"Returning task {task_id}:", json.dumps(result, ensure_ascii=False))
|
||
return result |