248 lines
7.9 KiB
Python
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)
|