precis / backend /app.py
compendious's picture
more data cleaning. Tuning data and then tuning the model is next
f179148
import asyncio
from typing import Optional
import httpx
from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from config import (
OLLAMA_BASE_URL,
DEFAULT_MODEL,
AVAILABLE_MODELS,
ALLOWED_ORIGINS,
API_KEY,
MAX_UPLOAD_BYTES,
)
from schemas import TranscriptRequest, YouTubeRequest
from ollama import stream_summary
from youtube import extract_video_id, fetch_transcript
app = FastAPI(
title="Précis API",
description="Content summarisation service powered by Ollama",
version="0.4.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS,
allow_credentials=False,
allow_methods=["POST", "GET", "OPTIONS"],
allow_headers=["Content-Type", "X-API-Key"],
)
# Only mount frontend in production when dist/ exists
import os
if os.path.isdir("frontend/dist"):
app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
def verify_api_key(x_api_key: Optional[str] = Header(default=None, alias="X-API-Key")):
if not API_KEY:
raise HTTPException(
status_code=500,
detail="Server misconfigured: PRECIS_API_KEY must be set.",
)
if x_api_key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid API key.")
@app.get("/")
async def root():
return {
"service": "Précis API",
"docs": "/docs",
"health": "/health",
"status": "/status",
}
@app.get("/health")
async def health():
return {"status": "healthy", "service": "precis"}
@app.get("/status")
async def status():
ollama_ok = False
try:
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
ollama_ok = r.status_code == 200
except Exception:
pass
return {
"service": "Précis API",
"version": "0.4.0",
"default_model": DEFAULT_MODEL,
"available_models": AVAILABLE_MODELS,
"ollama_reachable": ollama_ok,
}
@app.get("/models")
async def list_models():
try:
async with httpx.AsyncClient(timeout=5.0) as client:
r = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
r.raise_for_status()
payload = r.json() if r.content else {}
installed = [m.get("name") for m in payload.get("models", []) if m.get("name")]
if installed:
default = DEFAULT_MODEL if DEFAULT_MODEL in installed else installed[0]
return {"default": default, "available": installed}
except Exception:
pass
return {"default": DEFAULT_MODEL, "available": AVAILABLE_MODELS}
@app.post("/summarize/transcript")
async def summarize_transcript(
request: TranscriptRequest,
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
):
verify_api_key(x_api_key)
if not request.text.strip():
raise HTTPException(status_code=400, detail="Text must not be empty.")
return stream_summary(request.text, title=request.title, model=request.model)
@app.post("/summarize/youtube")
async def summarize_youtube(
request: YouTubeRequest,
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
):
verify_api_key(x_api_key)
video_id = extract_video_id(request.url)
text = await asyncio.to_thread(fetch_transcript, video_id)
return stream_summary(text, model=request.model)
@app.post("/summarize/file")
async def summarize_file(
req: Request,
file: UploadFile = File(...),
model: Optional[str] = None,
x_api_key: Optional[str] = Header(default=None, alias="X-API-Key"),
):
verify_api_key(x_api_key)
content_length = req.headers.get("content-length")
if content_length and int(content_length) > MAX_UPLOAD_BYTES:
raise HTTPException(status_code=413, detail="Uploaded file is too large.")
if not file.filename.endswith(".txt"):
raise HTTPException(status_code=400, detail="Only .txt files are supported.")
content = await file.read()
if len(content) > MAX_UPLOAD_BYTES:
raise HTTPException(status_code=413, detail="Uploaded file is too large.")
try:
text = content.decode("utf-8")
except UnicodeDecodeError:
raise HTTPException(status_code=400, detail="File must be valid UTF-8 text.")
if not text.strip():
raise HTTPException(status_code=400, detail="Uploaded file is empty.")
return stream_summary(text, title=file.filename, model=model)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)