File size: 3,460 Bytes
795547d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from pathlib import Path
import os
import json
import unicodedata
import numpy as np
import sentencepiece as spm
import torch
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

ROOT = Path(__file__).resolve().parent
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
MODEL = torch.jit.load(str(ROOT / "splinet_embedder.ts"), map_location=DEVICE).eval()
SP = spm.SentencePieceProcessor(model_file=str(ROOT / "spiece.model"))

PAD_ID = 3
CLS_ID = 4
SEP_ID = 5
MAX_LENGTH = 512
DEFAULT_BATCH = int(os.environ.get("SPLINET_BATCH_SIZE", "256" if DEVICE.type == "cuda" else "8"))

def normalize_text(text):
    text = unicodedata.normalize("NFKC", text or "").lower()
    return " ".join(text.split())

def tokenize(texts):
    ids = torch.full((len(texts), MAX_LENGTH), PAD_ID, dtype=torch.long)
    mask = torch.zeros((len(texts), MAX_LENGTH), dtype=torch.long)
    for i, text in enumerate(texts):
        pieces = list(SP.encode(normalize_text(text), out_type=int))[:MAX_LENGTH - 2]
        seq = [CLS_ID] + pieces + [SEP_ID]
        n = len(seq)
        ids[i, :n] = torch.tensor(seq, dtype=torch.long)
        mask[i, :n] = 1
    return ids, mask

def encode(texts, batch_size=None):
    if isinstance(texts, str):
        texts = [texts]
    if not texts:
        return np.empty((0, 768), dtype=np.float32)
    bs = max(1, int(batch_size or DEFAULT_BATCH))
    output = []
    start = 0
    while start < len(texts):
        current = min(bs, len(texts) - start)
        try:
            ids, mask = tokenize(texts[start:start + current])
            ids = ids.to(DEVICE)
            mask = mask.to(DEVICE)
            with torch.inference_mode():
                if DEVICE.type == "cuda":
                    with torch.autocast(device_type="cuda", dtype=torch.float16):
                        z = MODEL(ids, mask)
                else:
                    z = MODEL(ids, mask)
            output.append(z.float().cpu().numpy())
            start += current
        except torch.OutOfMemoryError:
            if DEVICE.type != "cuda" or current == 1:
                raise
            torch.cuda.empty_cache()
            bs = max(1, current // 2)
    return np.concatenate(output, axis=0).astype(np.float32)

def similarity(text_a, text_b):
    z = encode([text_a, text_b])
    return float(np.dot(z[0], z[1]))

class EmbedRequest(BaseModel):
    texts: list[str]
    batch_size: int | None = None

class SimilarityRequest(BaseModel):
    text_a: str
    text_b: str

app = FastAPI(title="SpliNet Embedder API", version="1.0.0")

@app.get("/health")
def health():
    return {"status": "ok", "device": str(DEVICE)}

@app.get("/model-info")
def model_info():
    with open(ROOT / "model_info.json", "r", encoding="utf-8") as f:
        return json.load(f)

@app.post("/embed")
def embed_endpoint(req: EmbedRequest):
    try:
        z = encode(req.texts, req.batch_size)
        return {"count": len(req.texts), "dimension": 768, "embeddings": z.tolist()}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/similarity")
def similarity_endpoint(req: SimilarityRequest):
    try:
        return {"cosine_similarity": similarity(req.text_a, req.text_b)}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))