from fastapi import FastAPI, HTTPException, Form, BackgroundTasks from fastapi.responses import FileResponse from kokoro_onnx import Kokoro import tempfile import os import bcrypt from datetime import datetime, timedelta from supabase import create_client, Client import soundfile as sf # ============== CONFIG ============== SUPABASE_URL = os.getenv("SUPABASE_URL") SUPABASE_KEY = os.getenv("SUPABASE_KEY") if not SUPABASE_URL or not SUPABASE_KEY: raise ValueError("SUPABASE_URL and SUPABASE_KEY environment variables must be set") DAILY_QUOTA = 50 # Increased since Kokoro is much faster MAX_CHARS = 4500 # ~5 minutes of audio (speaking rate: ~900 chars/min) MIN_CHARS = 5 MAX_AUDIO_DURATION = 300 # 5 minutes of audio # Admin credentials ADMIN_USERNAME = "madhab" ADMIN_PASSWORD = "Madhab@Studify2024!" # ============== SUPABASE ============== supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) def init_admin(): """Create admin user if not exists""" try: result = supabase.table("tts_users").select("username").eq("username", ADMIN_USERNAME).execute() if not result.data: password_hash = bcrypt.hashpw(ADMIN_PASSWORD.encode(), bcrypt.gensalt()).decode() supabase.table("tts_users").insert({ "username": ADMIN_USERNAME, "password_hash": password_hash, "role": "admin", "daily_limit": -1, "is_active": True }).execute() print(f"✅ Admin user created: {ADMIN_USERNAME}") else: print(f"✅ Admin user exists: {ADMIN_USERNAME}") except Exception as e: # Ignore RLS errors - user may already exist or require manual creation print(f"⚠️ Note: {e}") print(f"ℹ️ Tip: Create admin user manually in Supabase if needed") # ============== KOKORO TTS MODEL ============== print("🎤 Loading Kokoro TTS model...") try: kokoro = Kokoro("kokoro-v0_19.onnx", "voices") print("✅ Kokoro TTS loaded successfully!") except Exception as e: print(f"⚠️ Kokoro not found locally. Will download on first use.") kokoro = None app = FastAPI(title="Kokoro TTS API - Professional & Fast") @app.on_event("startup") def startup(): global kokoro if kokoro is None: import urllib.request import os print("📥 Downloading Kokoro TTS model files...") # Create directory for voices os.makedirs("voices", exist_ok=True) # Download voices file voices_file = "voices/voices.bin" if not os.path.exists(voices_file): print("Downloading voices.bin...") urllib.request.urlretrieve( "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/voices.bin", voices_file ) print("✅ Voices downloaded!") # Download ONNX model model_file = "kokoro-v0_19.onnx" if not os.path.exists(model_file): print("Downloading kokoro-v0_19.onnx...") urllib.request.urlretrieve( "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files/kokoro-v0_19.onnx", model_file ) print("✅ Model downloaded!") print("🎤 Initializing Kokoro TTS...") kokoro = Kokoro(model_file, voices_file) print("✅ Kokoro TTS loaded!") init_admin() # ============== AUTH ============== def verify_password(plain_password: str, hashed_password: str) -> bool: try: return bcrypt.checkpw(plain_password.encode(), hashed_password.encode()) except: return False def authenticate_user(username: str, password: str) -> dict: result = supabase.table("tts_users").select("*").eq("username", username).execute() if not result.data or len(result.data) == 0: raise HTTPException(status_code=401, detail="Access denied. User not found in database.") user = result.data[0] if not user.get('is_active', True): raise HTTPException(status_code=403, detail="Account is disabled. Contact admin.") if not verify_password(password, user['password_hash']): raise HTTPException(status_code=401, detail="Invalid credentials.") return user def check_quota(username: str, daily_limit: int, role: str) -> dict: if role == 'admin' or daily_limit == -1: return {"used": 0, "remaining": -1, "is_unlimited": True} since = (datetime.utcnow() - timedelta(hours=24)).isoformat() result = supabase.table("tts_usage_logs").select("id", count="exact").eq("username", username).gte("created_at", since).execute() used = result.count or 0 remaining = daily_limit - used if remaining <= 0: raise HTTPException(status_code=429, detail=f"Daily quota exceeded. Used {used}/{daily_limit}. Resets in 24h.") return {"used": used, "remaining": remaining, "is_unlimited": False} def log_usage(username: str, text_length: int, language: str): supabase.table("tts_usage_logs").insert({ "username": username, "text_length": text_length, "language": language, "created_at": datetime.utcnow().isoformat() }).execute() # ============== HELPERS ============== def cleanup_file(path: str): try: if os.path.exists(path): os.unlink(path) except: pass def generate_speech(text: str, voice: str = "af_heart", speed: float = 1.0) -> str: """ Generate speech using Kokoro TTS Available voices: af (American Female), am (American Male), bf (British Female), etc. """ if len(text) < MIN_CHARS: raise ValueError(f"Text too short. Minimum {MIN_CHARS} characters.") if len(text) > MAX_CHARS: raise ValueError(f"Text too long. Maximum {MAX_CHARS} characters.") # Generate audio samples samples, sample_rate = kokoro.create( text=text, voice=voice, speed=speed, lang="en-us" # Kokoro supports: en-us, en-gb, ja, etc. ) # Save to temporary file with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp: sf.write(tmp.name, samples, sample_rate) return tmp.name # ============== API ENDPOINTS ============== @app.get("/health") def health(): return { "status": "healthy", "model": "Kokoro TTS 82M", "speed": "10x faster than XTTS", "authentication": "required", "default_quota": DAILY_QUOTA } @app.post("/api/generate") async def generate_tts( background_tasks: BackgroundTasks, username: str = Form(...), password: str = Form(...), text: str = Form(...), voice: str = Form("af_heart"), # American Female - Heart speed: float = Form(1.0) ): """ Generate TTS with Kokoro (Fast & Emotional) Performance: - Max audio length: 5 minutes - Speaking rate: ~900 chars/minute - Max chars: 4500 (~5 min audio) - Generation time: ~20-30 seconds on CPU Available voices: - af_heart: American Female (warm) - af_bella: American Female (professional) - am_adam: American Male (confident) - am_michael: American Male (friendly) - bf_emma: British Female (elegant) - bf_isabella: British Female (storytelling) ⭐ Usage: curl -X POST https://your-service.hf.space/api/generate \ -F "username=madhab" \ -F "password=Madhab@Studify2024!" \ -F "text=Hello world. This is much faster!" \ -F "voice=bf_isabella" \ -F "speed=1.0" \ --output output.wav """ user = authenticate_user(username, password) quota = check_quota(user['username'], user['daily_limit'], user['role']) try: output_path = generate_speech(text.strip(), voice, speed) if not quota['is_unlimited']: log_usage(user['username'], len(text), "en") background_tasks.add_task(cleanup_file, output_path) response = FileResponse(output_path, media_type="audio/wav", filename="kokoro_tts.wav") response.headers["X-Quota-Used"] = str(quota["used"] + (0 if quota["is_unlimited"] else 1)) response.headers["X-Quota-Remaining"] = "unlimited" if quota["is_unlimited"] else str(quota["remaining"] - 1) response.headers["X-Model"] = "Kokoro-82M" return response except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) except Exception as e: raise HTTPException(status_code=500, detail=f"TTS generation failed: {str(e)}") @app.post("/api/quota") async def check_user_quota(username: str = Form(...), password: str = Form(...)): user = authenticate_user(username, password) quota = check_quota(user['username'], user['daily_limit'], user['role']) return { "username": user['username'], "role": user['role'], "used_today": quota["used"], "remaining": "unlimited" if quota["is_unlimited"] else quota["remaining"], "daily_limit": "unlimited" if quota["is_unlimited"] else user['daily_limit'] } @app.post("/api/admin/create-user") async def create_user( admin_username: str = Form(...), admin_password: str = Form(...), new_username: str = Form(...), new_password: str = Form(...), role: str = Form("user"), daily_limit: int = Form(50) ): admin = authenticate_user(admin_username, admin_password) if admin['role'] != 'admin': raise HTTPException(status_code=403, detail="Admin access required") existing = supabase.table("tts_users").select("username").eq("username", new_username).execute() if existing.data: raise HTTPException(status_code=400, detail="Username already exists") password_hash = bcrypt.hashpw(new_password.encode(), bcrypt.gensalt()).decode() supabase.table("tts_users").insert({ "username": new_username, "password_hash": password_hash, "role": role, "daily_limit": daily_limit, "is_active": True }).execute() return {"success": True, "username": new_username, "role": role, "daily_limit": daily_limit} @app.post("/api/admin/list-users") async def list_users(admin_username: str = Form(...), admin_password: str = Form(...)): admin = authenticate_user(admin_username, admin_password) if admin['role'] != 'admin': raise HTTPException(status_code=403, detail="Admin access required") result = supabase.table("tts_users").select("username, role, daily_limit, is_active, created_at").execute() return {"users": result.data} # ============== ROOT ENDPOINT ============== @app.get("/") def root(): return { "service": "Kokoro TTS API", "status": "running", "model": "Kokoro-82M", "version": "1.0", "endpoints": { "health": "/health", "generate": "/api/generate (POST)", "quota": "/api/quota (POST)", "docs": "/docs" }, "usage": "Visit /docs for interactive API documentation" } if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=7860)