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