File size: 5,029 Bytes
3a2cb1d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""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()