Spaces:
Build error
Build error
| 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.") | |
| async def root(): | |
| return { | |
| "service": "Précis API", | |
| "docs": "/docs", | |
| "health": "/health", | |
| "status": "/status", | |
| } | |
| async def health(): | |
| return {"status": "healthy", "service": "precis"} | |
| 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, | |
| } | |
| 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} | |
| 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) | |
| 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) | |
| 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) | |