from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, Response from pydantic import BaseModel import httpx import re import os app = FastAPI(title="Speechify Clone") LITELLM_BASE = "http://localhost:4000" LITELLM_KEY = "sk-c5c58adcc8dda288067f034ee927be23509142d274fe48418c25009dfb507a52" TTS_MODELS = { "openai-tts-1": { "name": "OpenAI TTS", "quality": "Standard", "voices": [ {"id": "alloy", "name": "Alloy"}, {"id": "echo", "name": "Echo"}, {"id": "fable", "name": "Fable"}, {"id": "onyx", "name": "Onyx"}, {"id": "nova", "name": "Nova"}, {"id": "shimmer", "name": "Shimmer"}, ] }, "openai-tts-1-hd": { "name": "OpenAI TTS HD", "quality": "High Definition", "voices": [ {"id": "alloy", "name": "Alloy"}, {"id": "echo", "name": "Echo"}, {"id": "fable", "name": "Fable"}, {"id": "onyx", "name": "Onyx"}, {"id": "nova", "name": "Nova"}, {"id": "shimmer", "name": "Shimmer"}, ] }, "elevenlabs-v3": { "name": "ElevenLabs v3", "quality": "Premium", "voices": [ {"id": "Rachel", "name": "Rachel"}, {"id": "Drew", "name": "Drew"}, {"id": "Clyde", "name": "Clyde"}, {"id": "Paul", "name": "Paul"}, {"id": "Domi", "name": "Domi"}, {"id": "Dave", "name": "Dave"}, {"id": "Fin", "name": "Fin"}, {"id": "Bella", "name": "Bella"}, {"id": "Antoni", "name": "Antoni"}, {"id": "Thomas", "name": "Thomas"}, {"id": "Charlie", "name": "Charlie"}, {"id": "Emily", "name": "Emily"}, {"id": "Elli", "name": "Elli"}, {"id": "Callum", "name": "Callum"}, {"id": "Patrick", "name": "Patrick"}, {"id": "Harry", "name": "Harry"}, {"id": "Liam", "name": "Liam"}, {"id": "Dorothy", "name": "Dorothy"}, {"id": "Josh", "name": "Josh"}, {"id": "Arnold", "name": "Arnold"}, {"id": "Adam", "name": "Adam"}, {"id": "Sam", "name": "Sam"}, ] }, "elevenlabs-multilingual": { "name": "ElevenLabs Multilingual", "quality": "Premium Multilingual", "voices": [ {"id": "Rachel", "name": "Rachel"}, {"id": "Drew", "name": "Drew"}, {"id": "Clyde", "name": "Clyde"}, {"id": "Paul", "name": "Paul"}, {"id": "Domi", "name": "Domi"}, {"id": "Dave", "name": "Dave"}, {"id": "Fin", "name": "Fin"}, {"id": "Bella", "name": "Bella"}, {"id": "Antoni", "name": "Antoni"}, {"id": "Thomas", "name": "Thomas"}, {"id": "Charlie", "name": "Charlie"}, {"id": "Emily", "name": "Emily"}, {"id": "Elli", "name": "Elli"}, {"id": "Callum", "name": "Callum"}, {"id": "Patrick", "name": "Patrick"}, {"id": "Harry", "name": "Harry"}, {"id": "Liam", "name": "Liam"}, {"id": "Dorothy", "name": "Dorothy"}, {"id": "Josh", "name": "Josh"}, {"id": "Arnold", "name": "Arnold"}, {"id": "Adam", "name": "Adam"}, {"id": "Sam", "name": "Sam"}, ] }, "vertex-gemini-2.5-flash-tts": { "name": "Gemini 2.5 Flash TTS", "quality": "Fast", "voices": [ {"id": "Aoede", "name": "Aoede"}, {"id": "Charon", "name": "Charon"}, {"id": "Fenrir", "name": "Fenrir"}, {"id": "Kore", "name": "Kore"}, {"id": "Puck", "name": "Puck"}, {"id": "Orbit", "name": "Orbit"}, {"id": "Zephyr", "name": "Zephyr"}, {"id": "Autonoe", "name": "Autonoe"}, {"id": "Enceladus", "name": "Enceladus"}, {"id": "Laomedeia", "name": "Laomedeia"}, {"id": "Achernar", "name": "Achernar"}, {"id": "Rasalgethi", "name": "Rasalgethi"}, {"id": "Schedar", "name": "Schedar"}, {"id": "Sulafat", "name": "Sulafat"}, ] }, "vertex-gemini-2.5-pro-tts": { "name": "Gemini 2.5 Pro TTS", "quality": "High Quality", "voices": [ {"id": "Aoede", "name": "Aoede"}, {"id": "Charon", "name": "Charon"}, {"id": "Fenrir", "name": "Fenrir"}, {"id": "Kore", "name": "Kore"}, {"id": "Puck", "name": "Puck"}, {"id": "Orbit", "name": "Orbit"}, {"id": "Zephyr", "name": "Zephyr"}, {"id": "Autonoe", "name": "Autonoe"}, {"id": "Enceladus", "name": "Enceladus"}, {"id": "Laomedeia", "name": "Laomedeia"}, {"id": "Achernar", "name": "Achernar"}, {"id": "Rasalgethi", "name": "Rasalgethi"}, {"id": "Schedar", "name": "Schedar"}, {"id": "Sulafat", "name": "Sulafat"}, ] }, } class TTSRequest(BaseModel): text: str model: str voice: str speed: float = 1.0 class URLRequest(BaseModel): url: str @app.get("/api/voices") async def get_voices(): return TTS_MODELS @app.post("/api/tts") async def text_to_speech(req: TTSRequest): payload = { "model": req.model, "input": req.text, "voice": req.voice, } if req.speed != 1.0: payload["speed"] = req.speed async with httpx.AsyncClient(timeout=60.0) as client: resp = await client.post( f"{LITELLM_BASE}/v1/audio/speech", json=payload, headers={"Authorization": f"Bearer {LITELLM_KEY}"}, ) if resp.status_code != 200: raise HTTPException(status_code=resp.status_code, detail=resp.text) return Response( content=resp.content, media_type="audio/mpeg", ) @app.post("/api/extract-url") async def extract_from_url(req: URLRequest): try: from bs4 import BeautifulSoup import html2text async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client: resp = await client.get( req.url, headers={"User-Agent": "Mozilla/5.0 (compatible; SpeechifyClone/1.0)"}, ) soup = BeautifulSoup(resp.text, "html.parser") for tag in soup(["script", "style", "nav", "footer", "header", "aside", "iframe"]): tag.decompose() main = ( soup.find("article") or soup.find("main") or soup.find(id="content") or soup.find(class_="content") or soup.body ) h = html2text.HTML2Text() h.ignore_links = True h.ignore_images = True h.body_width = 0 text = h.handle(str(main)) text = re.sub(r'\n{3,}', '\n\n', text).strip() title = soup.title.string.strip() if soup.title else req.url return {"text": text, "title": title} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) @app.post("/api/extract-pdf") async def extract_from_pdf(file: UploadFile = File(...)): try: import fitz # PyMuPDF content = await file.read() doc = fitz.open(stream=content, filetype="pdf") pages = [] for page in doc: pages.append(page.get_text()) text = "\n\n".join(pages) text = re.sub(r'\n{3,}', '\n\n', text).strip() return {"text": text, "title": file.filename} except Exception as e: raise HTTPException(status_code=400, detail=str(e)) app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/") async def root(): return FileResponse("static/index.html") if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8080, reload=True)