Cortex-0 / src /api.py
Frankenstein-Labs's picture
release: Cortex-0 (Phases 0-12)
3a2cb1d verified
Raw History Blame Contribute Delete
5.03 kB
"""Cortex-0 — API FastAPI (Phase 12).
Expose le moteur multimédia maison (`src/inference.CortexEngine`) :
* ``GET /health`` : état du service + si un checkpoint entraîné est chargé ;
* ``GET /capabilities`` : tailles / modalités supportées ;
* ``POST /chat`` : texte -> texte (JSON) ;
* ``POST /vision`` : image (upload) -> légende ;
* ``POST /audio`` : WAV (upload) -> transcription ;
* ``POST /video`` : frames + audio optionnel -> description ;
* ``POST /memory/ingest`` : ingérer un cours (RAG, mode A) ;
* ``POST /memory/query`` : interroger la mémoire d'un cours (réponse extractive).
Aucun secret en dur ; le checkpoint est choisi via ``CORTEX_CHECKPOINT``. Aucun poids
pré-entraîné n'est téléchargé : seuls nos propres checkpoints sont chargés.
"""
from __future__ import annotations
import os
from pathlib import Path
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from pydantic import BaseModel, Field
from src.inference import CortexEngine
from src.memory.rag import LessonMemory
app = FastAPI(title="Cortex-0", version="0.1.0",
description="IA multimodale from scratch (texte/image/audio/vidéo).")
_engine: CortexEngine | None = None
_memory = LessonMemory(dim=512, max_words=40)
def get_engine() -> CortexEngine:
global _engine
if _engine is None:
ckpt = os.environ.get("CORTEX_CHECKPOINT") or None
_engine = CortexEngine(checkpoint=ckpt)
return _engine
# ---------------------------------------------------------------------------
# Schémas
# ---------------------------------------------------------------------------
class ChatRequest(BaseModel):
prompt: str = Field(..., min_length=1)
max_new_tokens: int = Field(40, ge=1, le=512)
class IngestRequest(BaseModel):
text: str = Field(..., min_length=1)
source: str = "manual"
class QueryRequest(BaseModel):
question: str = Field(..., min_length=1)
k: int = Field(3, ge=1, le=10)
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/health")
def health() -> dict:
eng = get_engine()
return {"status": "ok", "trained": eng.trained, "device": str(eng.device),
"vocab_size": eng.tokenizer.vocab_size}
@app.get("/capabilities")
def capabilities() -> dict:
eng = get_engine()
return {
"modalities": ["text", "image", "audio", "video"],
"image_tokens": eng.n_image_tokens,
"audio_tokens": eng.n_audio_tokens,
"video_tokens": eng.n_video_tokens,
"from_scratch": True,
"external_weights": False,
}
@app.post("/chat")
def chat(req: ChatRequest) -> dict:
return get_engine().chat(req.prompt, max_new_tokens=req.max_new_tokens)
@app.post("/vision")
async def vision(file: UploadFile = File(...),
max_new_tokens: int = Form(20)) -> dict:
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="image vide")
try:
return get_engine().describe_image(data, max_new_tokens=max_new_tokens)
except TypeError as exc:
raise HTTPException(status_code=415, detail=str(exc)) from exc
@app.post("/audio")
async def audio(file: UploadFile = File(...),
max_new_tokens: int = Form(20)) -> dict:
data = await file.read()
if not data:
raise HTTPException(status_code=400, detail="audio vide")
try:
return get_engine().transcribe_audio(data, max_new_tokens=max_new_tokens)
except TypeError as exc:
raise HTTPException(status_code=415, detail=str(exc)) from exc
@app.post("/video")
async def video(frames: UploadFile = File(...),
audio_file: UploadFile | None = File(None),
max_new_tokens: int = Form(24)) -> dict:
"""``frames`` = archive numpy ``.npy`` de forme ``(T, H, W, 3)``."""
import io
import numpy as np
raw = await frames.read()
try:
arr = np.load(io.BytesIO(raw), allow_pickle=False)
except Exception as exc: # fichier invalide
raise HTTPException(status_code=400, detail=f"frames .npy invalides : {exc}") from exc
aud = await audio_file.read() if audio_file is not None else None
return get_engine().describe_video(arr, audio=aud, max_new_tokens=max_new_tokens)
@app.post("/memory/ingest")
def memory_ingest(req: IngestRequest) -> dict:
_memory.add_text(req.text, source=req.source)
return {"passages": len(_memory.passages), "source": req.source}
@app.post("/memory/query")
def memory_query(req: QueryRequest) -> dict:
return _memory.answer(req.question, k=req.k)
def main() -> None:
import uvicorn
ckpt = os.environ.get("CORTEX_CHECKPOINT")
if ckpt and Path(ckpt).is_file():
get_engine()
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))
if __name__ == "__main__":
main()