speechify-clone/main.py

248 lines
7.9 KiB
Python

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)