File size: 3,650 Bytes
b48e580
532cbb3
b48e580
532cbb3
 
b48e580
 
 
 
 
532cbb3
 
 
 
 
 
fc144fa
b48e580
 
532cbb3
fc144fa
 
 
73caa3e
fc144fa
 
 
 
 
 
73caa3e
 
 
 
 
 
 
 
 
 
 
 
532cbb3
73caa3e
 
 
532cbb3
 
 
 
 
 
 
73caa3e
b48e580
 
532cbb3
 
 
 
 
 
 
73caa3e
 
 
 
532cbb3
 
 
 
 
 
 
 
73caa3e
b48e580
73caa3e
532cbb3
73caa3e
532cbb3
 
 
 
 
fc144fa
73caa3e
532cbb3
 
 
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
"""
Nawah-Router — FastAPI backend for the interactive routing demo.

One endpoint. The model scores every supplied category in a single forward pass, so the frontend
can re-route on each keystroke without a per-category cost.
"""
import os
import time

import torch
from fastapi import FastAPI
from fastapi.responses import FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from transformers import AutoTokenizer

from routing_model import MAX_ROUTES, RouterModel, build_text, detect_lang, route

HF_TOKEN = os.environ.get("MODEL_HF_TOKEN") or os.environ.get("HF_TOKEN")

# Three backbones on the same head and the same task. The two 6M's are bidirectional BERT
# encoders (one Arabic-only, one pretrained jointly on Arabic+English); the 52M is a Llama
# decoder. Measured on the same eval sets in one session each - see the model cards.
REGISTRY = {
    "52M":         {"repo": "oddadmix/Nawah-Router-v3",
                     "label": "Nawah-Router-v3 (Llama decoder, Arabic)"},
    "6M-BERT":     {"repo": "oddadmix/Nawah-Router-BERT-6M-v2",
                     "label": "Nawah-Router-BERT-6M-v2 (BERT encoder, Arabic)"},
    "6M-BILINGUAL": {"repo": "oddadmix/Nawah-Router-BERT-6M-bilingual-pretrained",
                      "label": "Nawah-Router-BERT-6M-bilingual-pretrained (BERT encoder, English+Arabic)"},
}
DEFAULT = os.environ.get("MODEL_KEY", "6M-BERT")

LOADED = {}
for key, spec in REGISTRY.items():
    print(f"[*] loading {spec['repo']}", flush=True)
    tok = AutoTokenizer.from_pretrained(spec["repo"], token=HF_TOKEN)
    mdl = RouterModel.from_pretrained(spec["repo"], token=HF_TOKEN)
    LOADED[key] = {"tok": tok, "model": mdl, "label": spec["label"],
                   "repo": spec["repo"],
                   "params": sum(p.numel() for p in mdl.parameters())}
    print(f"[+] {key}: {LOADED[key]['params']/1e6:.2f}M params", flush=True)
torch.set_num_threads(int(os.environ.get("OMP_NUM_THREADS", 4)))

MODEL_ID = REGISTRY[DEFAULT]["repo"]
TOK, MODEL = LOADED[DEFAULT]["tok"], LOADED[DEFAULT]["model"]

app = FastAPI()


class RouteReq(BaseModel):
    text: str = ""
    cats: list[str] = []
    model: str = DEFAULT


@app.get("/")
def index():
    return FileResponse("static/index.html")


@app.get("/api/ready")
def ready():
    return {"ready": True, "model": MODEL_ID, "default": DEFAULT,
            "params": LOADED[DEFAULT]["params"], "max_routes": MAX_ROUTES,
            "models": [{"key": k, "label": v["label"], "repo": v["repo"],
                        "params": v["params"]} for k, v in LOADED.items()]}


@app.post("/api/route")
def api_route(req: RouteReq):
    cats = [c.strip() for c in req.cats if c and c.strip()][:MAX_ROUTES]
    text = (req.text or "").strip()
    if not text or not cats:
        return JSONResponse({"results": [], "ms": 0, "tokens": 0})
    sel = LOADED.get(req.model) or LOADED[DEFAULT]
    t0 = time.perf_counter()
    res = route(sel["model"], sel["tok"], text, cats)
    full, _ = build_text(text, cats)
    ntok = len(sel["tok"].encode(full, add_special_tokens=False))
    order = {r["route"]: r["score"] for r in res}
    return JSONResponse({
        # returned in the caller's order so the UI does not reshuffle rows under the cursor
        "results": [{"route": c, "score": order.get(c, 0.0)} for c in cats],
        "top": max(range(len(cats)), key=lambda i: order.get(cats[i], 0.0)),
        "ms": round((time.perf_counter() - t0) * 1000), "tokens": ntok, "lang": detect_lang(text),
        "model": sel["label"], "params": sel["params"]})


app.mount("/static", StaticFiles(directory="static"), name="static")