Spaces:
Runtime error
Runtime error
Deploy standalone ML service
Browse files- .dockerignore +10 -0
- .env.example +8 -0
- Dockerfile +20 -0
- README.md +51 -4
- app/__init__.py +1 -0
- app/config.py +41 -0
- app/language.py +136 -0
- app/main.py +97 -0
- app/model.py +110 -0
- app/normalization.py +21 -0
- app/schemas.py +72 -0
- deploy_to_hf_space.py +57 -0
- models/muril-balanced-30k-v1/config.json +43 -0
- models/muril-balanced-30k-v1/model.safetensors +3 -0
- models/muril-balanced-30k-v1/special_tokens_map.json +7 -0
- models/muril-balanced-30k-v1/tokenizer.json +0 -0
- models/muril-balanced-30k-v1/tokenizer_config.json +58 -0
- models/muril-balanced-30k-v1/vocab.txt +0 -0
- requirements.txt +9 -0
- smoke_test.py +38 -0
.dockerignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
.gitignore
|
| 3 |
+
.env
|
| 4 |
+
.tmp
|
| 5 |
+
__pycache__
|
| 6 |
+
*.pyc
|
| 7 |
+
*.pyo
|
| 8 |
+
*.pyd
|
| 9 |
+
smoke_test.py
|
| 10 |
+
deploy_to_hf_space.py
|
.env.example
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
APP_NAME=Standalone Content Safety ML Service
|
| 2 |
+
APP_VERSION=0.1.0
|
| 3 |
+
API_PREFIX=/api/v1
|
| 4 |
+
MODEL_ARTIFACT_PATH=./models/muril-balanced-30k-v1
|
| 5 |
+
MODEL_VERSION=muril-balanced-30k-v1
|
| 6 |
+
MODEL_MIN_SCORE=0.35
|
| 7 |
+
MODEL_MAX_LENGTH=256
|
| 8 |
+
CORS_ALLOW_ORIGINS=http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
ENV PIP_NO_CACHE_DIR=1
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip install --upgrade pip
|
| 11 |
+
RUN pip install --extra-index-url https://download.pytorch.org/whl/cpu -r requirements.txt
|
| 12 |
+
|
| 13 |
+
COPY app ./app
|
| 14 |
+
COPY .env.example ./.env
|
| 15 |
+
COPY models ./models
|
| 16 |
+
COPY README.md ./README.md
|
| 17 |
+
|
| 18 |
+
EXPOSE 8000
|
| 19 |
+
|
| 20 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
README.md
CHANGED
|
@@ -1,10 +1,57 @@
|
|
| 1 |
---
|
| 2 |
-
title: Context
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Context-Aware Safety ML API
|
| 3 |
+
emoji: 🛡️
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 8000
|
| 8 |
pinned: false
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Standalone multilingual safety inference API.
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# Standalone ML Service
|
| 14 |
+
|
| 15 |
+
This folder contains a minimal ML-only inference API extracted from the larger project.
|
| 16 |
+
|
| 17 |
+
It includes:
|
| 18 |
+
|
| 19 |
+
- a FastAPI app for transformer inference
|
| 20 |
+
- the local MuRIL checkpoint under `models/`
|
| 21 |
+
- basic language detection and text normalization
|
| 22 |
+
- CORS support for frontend clients
|
| 23 |
+
- a smoke test for local verification
|
| 24 |
+
- Docker metadata ready for Hugging Face Spaces
|
| 25 |
+
|
| 26 |
+
## Run locally
|
| 27 |
+
|
| 28 |
+
```powershell
|
| 29 |
+
python -m venv .venv
|
| 30 |
+
.venv\Scripts\activate
|
| 31 |
+
pip install -r requirements.txt
|
| 32 |
+
Copy-Item .env.example .env
|
| 33 |
+
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
## Smoke test
|
| 37 |
+
|
| 38 |
+
```powershell
|
| 39 |
+
python smoke_test.py
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
## API
|
| 43 |
+
|
| 44 |
+
- `GET /api/v1/health`
|
| 45 |
+
- `POST /api/v1/moderate`
|
| 46 |
+
|
| 47 |
+
## Deploy to Hugging Face Spaces
|
| 48 |
+
|
| 49 |
+
Create a Docker Space and upload this folder:
|
| 50 |
+
|
| 51 |
+
```powershell
|
| 52 |
+
.\.venv\Scripts\python.exe standalone-ml-service\deploy_to_hf_space.py --repo-id your-username/your-space-name --token hf_xxx
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
Optional flags:
|
| 56 |
+
|
| 57 |
+
- `--private`
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Standalone ML inference service package."""
|
app/config.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from functools import lru_cache
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from pydantic import field_validator
|
| 5 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 6 |
+
|
| 7 |
+
ENV_FILE = Path(__file__).resolve().parents[1] / ".env"
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class Settings(BaseSettings):
|
| 11 |
+
model_config = SettingsConfigDict(
|
| 12 |
+
env_file=str(ENV_FILE),
|
| 13 |
+
env_file_encoding="utf-8",
|
| 14 |
+
extra="ignore",
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
app_name: str = "Standalone Content Safety ML Service"
|
| 18 |
+
app_version: str = "0.1.0"
|
| 19 |
+
api_prefix: str = "/api/v1"
|
| 20 |
+
model_artifact_path: str = "./models/muril-balanced-30k-v1"
|
| 21 |
+
model_version: str = "muril-balanced-30k-v1"
|
| 22 |
+
model_min_score: float = 0.35
|
| 23 |
+
model_max_length: int = 256
|
| 24 |
+
cors_allow_origins: list[str] = [
|
| 25 |
+
"http://localhost:3000",
|
| 26 |
+
"http://127.0.0.1:3000",
|
| 27 |
+
"http://localhost:5173",
|
| 28 |
+
"http://127.0.0.1:5173",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
@field_validator("cors_allow_origins", mode="before")
|
| 32 |
+
@classmethod
|
| 33 |
+
def _split_origins(cls, value):
|
| 34 |
+
if isinstance(value, str):
|
| 35 |
+
return [item.strip() for item in value.split(",") if item.strip()]
|
| 36 |
+
return value
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@lru_cache
|
| 40 |
+
def get_settings() -> Settings:
|
| 41 |
+
return Settings()
|
app/language.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unicodedata
|
| 2 |
+
from collections import Counter
|
| 3 |
+
|
| 4 |
+
from app.schemas import LanguageProfile, LanguageSignal, ScriptRatio
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class HeuristicLanguageDetector:
|
| 8 |
+
roman_hindi_markers = {
|
| 9 |
+
"hai",
|
| 10 |
+
"nahi",
|
| 11 |
+
"nahin",
|
| 12 |
+
"tum",
|
| 13 |
+
"aap",
|
| 14 |
+
"mera",
|
| 15 |
+
"meri",
|
| 16 |
+
"kya",
|
| 17 |
+
"kyu",
|
| 18 |
+
"kyun",
|
| 19 |
+
"mat",
|
| 20 |
+
"kar",
|
| 21 |
+
"kr",
|
| 22 |
+
"bhai",
|
| 23 |
+
"yaar",
|
| 24 |
+
"acha",
|
| 25 |
+
"accha",
|
| 26 |
+
"bakwas",
|
| 27 |
+
"bewakoof",
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
def detect(self, text: str) -> LanguageProfile:
|
| 31 |
+
script_counts = Counter()
|
| 32 |
+
|
| 33 |
+
for char in text:
|
| 34 |
+
script_name = self._script_name(char)
|
| 35 |
+
if script_name is not None:
|
| 36 |
+
script_counts[script_name] += 1
|
| 37 |
+
|
| 38 |
+
total = sum(script_counts.values())
|
| 39 |
+
scripts = []
|
| 40 |
+
if total:
|
| 41 |
+
scripts = [
|
| 42 |
+
ScriptRatio(name=name, ratio=round(count / total, 3))
|
| 43 |
+
for name, count in script_counts.most_common()
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
lowered_tokens = {token.strip(".,!?;:()[]{}\"'").lower() for token in text.split()}
|
| 47 |
+
roman_hindi_hits = len(self.roman_hindi_markers.intersection(lowered_tokens))
|
| 48 |
+
|
| 49 |
+
has_latin = script_counts["latin"] > 0
|
| 50 |
+
has_devanagari = script_counts["devanagari"] > 0
|
| 51 |
+
has_other_indic = script_counts["indic_other"] > 0
|
| 52 |
+
|
| 53 |
+
if has_devanagari and has_latin:
|
| 54 |
+
return LanguageProfile(
|
| 55 |
+
primary_language="hinglish",
|
| 56 |
+
code_mixed=True,
|
| 57 |
+
scripts=scripts,
|
| 58 |
+
candidates=[
|
| 59 |
+
LanguageSignal(name="hinglish", confidence=0.9),
|
| 60 |
+
LanguageSignal(name="hindi", confidence=0.72),
|
| 61 |
+
LanguageSignal(name="english", confidence=0.63),
|
| 62 |
+
],
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
if has_devanagari:
|
| 66 |
+
return LanguageProfile(
|
| 67 |
+
primary_language="hindi",
|
| 68 |
+
code_mixed=False,
|
| 69 |
+
scripts=scripts,
|
| 70 |
+
candidates=[
|
| 71 |
+
LanguageSignal(name="hindi", confidence=0.92),
|
| 72 |
+
LanguageSignal(name="hinglish", confidence=0.28),
|
| 73 |
+
],
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
if has_latin and roman_hindi_hits >= 2:
|
| 77 |
+
return LanguageProfile(
|
| 78 |
+
primary_language="hinglish",
|
| 79 |
+
code_mixed=True,
|
| 80 |
+
scripts=scripts,
|
| 81 |
+
candidates=[
|
| 82 |
+
LanguageSignal(name="hinglish", confidence=0.82),
|
| 83 |
+
LanguageSignal(name="english", confidence=0.58),
|
| 84 |
+
],
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
if has_latin:
|
| 88 |
+
return LanguageProfile(
|
| 89 |
+
primary_language="english",
|
| 90 |
+
code_mixed=False,
|
| 91 |
+
scripts=scripts,
|
| 92 |
+
candidates=[
|
| 93 |
+
LanguageSignal(name="english", confidence=0.9),
|
| 94 |
+
LanguageSignal(name="hinglish", confidence=0.25),
|
| 95 |
+
],
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
if has_other_indic:
|
| 99 |
+
return LanguageProfile(
|
| 100 |
+
primary_language="indic_other",
|
| 101 |
+
code_mixed=False,
|
| 102 |
+
scripts=scripts,
|
| 103 |
+
candidates=[LanguageSignal(name="indic_other", confidence=0.8)],
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
return LanguageProfile(
|
| 107 |
+
primary_language="unknown",
|
| 108 |
+
code_mixed=False,
|
| 109 |
+
scripts=scripts,
|
| 110 |
+
candidates=[LanguageSignal(name="unknown", confidence=0.4)],
|
| 111 |
+
)
|
| 112 |
+
|
| 113 |
+
def _script_name(self, char: str) -> str | None:
|
| 114 |
+
if not char.isalpha():
|
| 115 |
+
return None
|
| 116 |
+
|
| 117 |
+
name = unicodedata.name(char, "")
|
| 118 |
+
if "LATIN" in name:
|
| 119 |
+
return "latin"
|
| 120 |
+
if "DEVANAGARI" in name:
|
| 121 |
+
return "devanagari"
|
| 122 |
+
if any(
|
| 123 |
+
block in name
|
| 124 |
+
for block in (
|
| 125 |
+
"BENGALI",
|
| 126 |
+
"GURMUKHI",
|
| 127 |
+
"GUJARATI",
|
| 128 |
+
"ORIYA",
|
| 129 |
+
"TAMIL",
|
| 130 |
+
"TELUGU",
|
| 131 |
+
"KANNADA",
|
| 132 |
+
"MALAYALAM",
|
| 133 |
+
)
|
| 134 |
+
):
|
| 135 |
+
return "indic_other"
|
| 136 |
+
return "other"
|
app/main.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from contextlib import asynccontextmanager
|
| 2 |
+
from time import perf_counter
|
| 3 |
+
|
| 4 |
+
from fastapi import APIRouter, FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
|
| 7 |
+
from app.config import get_settings
|
| 8 |
+
from app.language import HeuristicLanguageDetector
|
| 9 |
+
from app.model import TransformerModerationModel
|
| 10 |
+
from app.normalization import TextNormalizer
|
| 11 |
+
from app.schemas import HealthResponse, ModerateRequest, ModerateResponse
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@asynccontextmanager
|
| 15 |
+
async def lifespan(app: FastAPI):
|
| 16 |
+
settings = get_settings()
|
| 17 |
+
model = TransformerModerationModel(
|
| 18 |
+
artifact_path=settings.model_artifact_path,
|
| 19 |
+
max_length=settings.model_max_length,
|
| 20 |
+
min_score=settings.model_min_score,
|
| 21 |
+
)
|
| 22 |
+
model.warmup()
|
| 23 |
+
app.state.model = model
|
| 24 |
+
app.state.normalizer = TextNormalizer()
|
| 25 |
+
app.state.language_detector = HeuristicLanguageDetector()
|
| 26 |
+
app.state.settings = settings
|
| 27 |
+
yield
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def create_app() -> FastAPI:
|
| 31 |
+
settings = get_settings()
|
| 32 |
+
app = FastAPI(
|
| 33 |
+
title=settings.app_name,
|
| 34 |
+
version=settings.app_version,
|
| 35 |
+
docs_url="/docs",
|
| 36 |
+
redoc_url="/redoc",
|
| 37 |
+
lifespan=lifespan,
|
| 38 |
+
)
|
| 39 |
+
app.add_middleware(
|
| 40 |
+
CORSMiddleware,
|
| 41 |
+
allow_origins=settings.cors_allow_origins,
|
| 42 |
+
allow_credentials=True,
|
| 43 |
+
allow_methods=["*"],
|
| 44 |
+
allow_headers=["*"],
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
router = APIRouter(prefix=settings.api_prefix)
|
| 48 |
+
|
| 49 |
+
@router.get("/health", response_model=HealthResponse)
|
| 50 |
+
async def health() -> HealthResponse:
|
| 51 |
+
model = app.state.model
|
| 52 |
+
app_settings = app.state.settings
|
| 53 |
+
return HealthResponse(
|
| 54 |
+
status="ok",
|
| 55 |
+
model_backend=model.backend_name,
|
| 56 |
+
model_version=app_settings.model_version,
|
| 57 |
+
model_artifact_path=app_settings.model_artifact_path,
|
| 58 |
+
model_loaded=model.is_loaded,
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
@router.post("/moderate", response_model=ModerateResponse)
|
| 62 |
+
async def moderate(payload: ModerateRequest) -> ModerateResponse:
|
| 63 |
+
started_at = perf_counter()
|
| 64 |
+
normalizer = app.state.normalizer
|
| 65 |
+
detector = app.state.language_detector
|
| 66 |
+
model = app.state.model
|
| 67 |
+
settings = app.state.settings
|
| 68 |
+
|
| 69 |
+
normalized_text = normalizer.normalize(payload.text)
|
| 70 |
+
language = detector.detect(normalized_text)
|
| 71 |
+
categories = model.predict(normalized_text, payload.context)
|
| 72 |
+
latency_ms = round((perf_counter() - started_at) * 1000, 2)
|
| 73 |
+
|
| 74 |
+
return ModerateResponse(
|
| 75 |
+
normalized_text=normalized_text,
|
| 76 |
+
categories=categories,
|
| 77 |
+
language=language,
|
| 78 |
+
model_backend=model.backend_name,
|
| 79 |
+
model_version=settings.model_version,
|
| 80 |
+
latency_ms=latency_ms,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
app.include_router(router)
|
| 84 |
+
|
| 85 |
+
@app.get("/", tags=["root"])
|
| 86 |
+
async def root():
|
| 87 |
+
return {
|
| 88 |
+
"service": settings.app_name,
|
| 89 |
+
"docs": "/docs",
|
| 90 |
+
"health": f"{settings.api_prefix}/health",
|
| 91 |
+
"moderate": f"{settings.api_prefix}/moderate",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return app
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
app = create_app()
|
app/model.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from functools import cached_property
|
| 5 |
+
|
| 6 |
+
from app.schemas import CategoryName, CategoryScore, ConversationTurn
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class TransformerModerationModel:
|
| 11 |
+
artifact_path: str
|
| 12 |
+
max_length: int = 256
|
| 13 |
+
min_score: float = 0.35
|
| 14 |
+
backend_name: str = "transformer"
|
| 15 |
+
|
| 16 |
+
@cached_property
|
| 17 |
+
def tokenizer(self):
|
| 18 |
+
from transformers import AutoTokenizer
|
| 19 |
+
|
| 20 |
+
return AutoTokenizer.from_pretrained(self.artifact_path)
|
| 21 |
+
|
| 22 |
+
@cached_property
|
| 23 |
+
def model(self):
|
| 24 |
+
from transformers import AutoModelForSequenceClassification
|
| 25 |
+
|
| 26 |
+
return AutoModelForSequenceClassification.from_pretrained(self.artifact_path)
|
| 27 |
+
|
| 28 |
+
@property
|
| 29 |
+
def is_loaded(self) -> bool:
|
| 30 |
+
return "model" in self.__dict__ and "tokenizer" in self.__dict__
|
| 31 |
+
|
| 32 |
+
def warmup(self) -> None:
|
| 33 |
+
_ = self.tokenizer
|
| 34 |
+
_ = self.model
|
| 35 |
+
|
| 36 |
+
def predict(
|
| 37 |
+
self,
|
| 38 |
+
text: str,
|
| 39 |
+
context: list[ConversationTurn],
|
| 40 |
+
) -> list[CategoryScore]:
|
| 41 |
+
import torch
|
| 42 |
+
|
| 43 |
+
packed_text = self._pack_input(text=text, context=context)
|
| 44 |
+
encoded = self.tokenizer(
|
| 45 |
+
packed_text,
|
| 46 |
+
truncation=True,
|
| 47 |
+
padding=False,
|
| 48 |
+
max_length=self.max_length,
|
| 49 |
+
return_tensors="pt",
|
| 50 |
+
)
|
| 51 |
+
with torch.no_grad():
|
| 52 |
+
logits = self.model(**encoded).logits[0]
|
| 53 |
+
probabilities = torch.sigmoid(logits).tolist()
|
| 54 |
+
|
| 55 |
+
results: list[CategoryScore] = []
|
| 56 |
+
id_to_label = getattr(self.model.config, "id2label", {})
|
| 57 |
+
for index, score in enumerate(probabilities):
|
| 58 |
+
label = id_to_label.get(index, str(index))
|
| 59 |
+
normalized_label = normalize_label(label)
|
| 60 |
+
if normalized_label is None or score < self.min_score:
|
| 61 |
+
continue
|
| 62 |
+
results.append(
|
| 63 |
+
CategoryScore(
|
| 64 |
+
name=normalized_label,
|
| 65 |
+
score=round(float(score), 4),
|
| 66 |
+
source="transformer",
|
| 67 |
+
rationale=f"Model logits crossed threshold for {normalized_label.value}.",
|
| 68 |
+
)
|
| 69 |
+
)
|
| 70 |
+
return sorted(results, key=lambda category: category.score, reverse=True)
|
| 71 |
+
|
| 72 |
+
def _pack_input(
|
| 73 |
+
self,
|
| 74 |
+
text: str,
|
| 75 |
+
context: list[ConversationTurn],
|
| 76 |
+
) -> str:
|
| 77 |
+
turns = []
|
| 78 |
+
for offset, turn in enumerate(context[-5:], start=1):
|
| 79 |
+
turns.append(f"[TURN-{offset}] {turn.role.value}: {turn.text}")
|
| 80 |
+
turns.append(f"[CURRENT] user: {text}")
|
| 81 |
+
return "\n".join(turns)
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
def normalize_label(label: str) -> CategoryName | None:
|
| 85 |
+
normalized = (
|
| 86 |
+
label.strip()
|
| 87 |
+
.lower()
|
| 88 |
+
.replace("-", "_")
|
| 89 |
+
.replace("/", "_")
|
| 90 |
+
.replace(" ", "_")
|
| 91 |
+
)
|
| 92 |
+
mapping = {
|
| 93 |
+
"harassment_or_insult": CategoryName.HARASSMENT_OR_INSULT,
|
| 94 |
+
"insult": CategoryName.HARASSMENT_OR_INSULT,
|
| 95 |
+
"harassment": CategoryName.HARASSMENT_OR_INSULT,
|
| 96 |
+
"toxic": CategoryName.HARASSMENT_OR_INSULT,
|
| 97 |
+
"severe_toxic": CategoryName.HARASSMENT_OR_INSULT,
|
| 98 |
+
"threat_or_violence": CategoryName.THREAT_OR_VIOLENCE,
|
| 99 |
+
"threat": CategoryName.THREAT_OR_VIOLENCE,
|
| 100 |
+
"violence": CategoryName.THREAT_OR_VIOLENCE,
|
| 101 |
+
"hate": CategoryName.HATE,
|
| 102 |
+
"identity_hate": CategoryName.HATE,
|
| 103 |
+
"self_harm": CategoryName.SELF_HARM,
|
| 104 |
+
"sexual_explicit": CategoryName.SEXUAL_EXPLICIT,
|
| 105 |
+
"sexual": CategoryName.SEXUAL_EXPLICIT,
|
| 106 |
+
"obscene": CategoryName.PROFANITY,
|
| 107 |
+
"profanity": CategoryName.PROFANITY,
|
| 108 |
+
"spam": CategoryName.SPAM,
|
| 109 |
+
}
|
| 110 |
+
return mapping.get(normalized)
|
app/normalization.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import unicodedata
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class TextNormalizer:
|
| 6 |
+
url_pattern = re.compile(r"https?://\S+|www\.\S+", re.IGNORECASE)
|
| 7 |
+
mention_pattern = re.compile(r"@\w+")
|
| 8 |
+
whitespace_pattern = re.compile(r"\s+")
|
| 9 |
+
repeated_latin_pattern = re.compile(r"([A-Za-z])\1{2,}")
|
| 10 |
+
zero_width_pattern = re.compile(r"[\u200b-\u200f\u2060\ufeff]")
|
| 11 |
+
repeated_punctuation_pattern = re.compile(r"([!?.,])\1{2,}")
|
| 12 |
+
|
| 13 |
+
def normalize(self, text: str) -> str:
|
| 14 |
+
normalized = unicodedata.normalize("NFKC", text).strip()
|
| 15 |
+
normalized = self.zero_width_pattern.sub("", normalized)
|
| 16 |
+
normalized = self.url_pattern.sub("<URL>", normalized)
|
| 17 |
+
normalized = self.mention_pattern.sub("<USER>", normalized)
|
| 18 |
+
normalized = self.repeated_latin_pattern.sub(r"\1\1", normalized)
|
| 19 |
+
normalized = self.repeated_punctuation_pattern.sub(r"\1\1", normalized)
|
| 20 |
+
normalized = self.whitespace_pattern.sub(" ", normalized)
|
| 21 |
+
return normalized
|
app/schemas.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import StrEnum
|
| 2 |
+
|
| 3 |
+
from pydantic import BaseModel, Field
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class CategoryName(StrEnum):
|
| 7 |
+
HARASSMENT_OR_INSULT = "harassment_or_insult"
|
| 8 |
+
THREAT_OR_VIOLENCE = "threat_or_violence"
|
| 9 |
+
HATE = "hate"
|
| 10 |
+
SELF_HARM = "self_harm"
|
| 11 |
+
SEXUAL_EXPLICIT = "sexual_explicit"
|
| 12 |
+
PROFANITY = "profanity"
|
| 13 |
+
SPAM = "spam"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ConversationRole(StrEnum):
|
| 17 |
+
USER = "user"
|
| 18 |
+
ASSISTANT = "assistant"
|
| 19 |
+
MODERATOR = "moderator"
|
| 20 |
+
SYSTEM = "system"
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class ConversationTurn(BaseModel):
|
| 24 |
+
role: ConversationRole
|
| 25 |
+
text: str = Field(min_length=1, max_length=2000)
|
| 26 |
+
user_id: str | None = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class CategoryScore(BaseModel):
|
| 30 |
+
name: CategoryName
|
| 31 |
+
score: float = Field(ge=0.0, le=1.0)
|
| 32 |
+
source: str
|
| 33 |
+
rationale: str
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class ScriptRatio(BaseModel):
|
| 37 |
+
name: str
|
| 38 |
+
ratio: float = Field(ge=0.0, le=1.0)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class LanguageSignal(BaseModel):
|
| 42 |
+
name: str
|
| 43 |
+
confidence: float = Field(ge=0.0, le=1.0)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class LanguageProfile(BaseModel):
|
| 47 |
+
primary_language: str
|
| 48 |
+
code_mixed: bool
|
| 49 |
+
scripts: list[ScriptRatio] = Field(default_factory=list)
|
| 50 |
+
candidates: list[LanguageSignal] = Field(default_factory=list)
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class ModerateRequest(BaseModel):
|
| 54 |
+
text: str = Field(min_length=1, max_length=4000)
|
| 55 |
+
context: list[ConversationTurn] = Field(default_factory=list, max_length=10)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class ModerateResponse(BaseModel):
|
| 59 |
+
normalized_text: str
|
| 60 |
+
categories: list[CategoryScore] = Field(default_factory=list)
|
| 61 |
+
language: LanguageProfile
|
| 62 |
+
model_backend: str
|
| 63 |
+
model_version: str
|
| 64 |
+
latency_ms: float = Field(ge=0.0)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class HealthResponse(BaseModel):
|
| 68 |
+
status: str
|
| 69 |
+
model_backend: str
|
| 70 |
+
model_version: str
|
| 71 |
+
model_artifact_path: str
|
| 72 |
+
model_loaded: bool
|
deploy_to_hf_space.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from huggingface_hub import HfApi
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
SPACE_DIR = Path(__file__).resolve().parent
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def build_parser() -> argparse.ArgumentParser:
|
| 13 |
+
parser = argparse.ArgumentParser(description="Deploy the standalone ML service to Hugging Face Spaces.")
|
| 14 |
+
parser.add_argument("--repo-id", required=True, help="Space repo id, for example: username/space-name")
|
| 15 |
+
parser.add_argument("--token", required=True, help="Hugging Face access token")
|
| 16 |
+
parser.add_argument("--private", action="store_true", help="Create the Space as private")
|
| 17 |
+
return parser
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def main() -> None:
|
| 21 |
+
args = build_parser().parse_args()
|
| 22 |
+
api = HfApi(token=args.token)
|
| 23 |
+
|
| 24 |
+
create_kwargs = {
|
| 25 |
+
"repo_id": args.repo_id,
|
| 26 |
+
"token": args.token,
|
| 27 |
+
"repo_type": "space",
|
| 28 |
+
"space_sdk": "docker",
|
| 29 |
+
"private": args.private,
|
| 30 |
+
"exist_ok": True,
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
api.create_repo(**create_kwargs)
|
| 34 |
+
|
| 35 |
+
ignore_patterns = [
|
| 36 |
+
".env",
|
| 37 |
+
".tmp/*",
|
| 38 |
+
"__pycache__/*",
|
| 39 |
+
"*.pyc",
|
| 40 |
+
"*.pyo",
|
| 41 |
+
"*.pyd",
|
| 42 |
+
]
|
| 43 |
+
|
| 44 |
+
api.upload_folder(
|
| 45 |
+
repo_id=args.repo_id,
|
| 46 |
+
folder_path=str(SPACE_DIR),
|
| 47 |
+
repo_type="space",
|
| 48 |
+
token=args.token,
|
| 49 |
+
commit_message="Deploy standalone ML service",
|
| 50 |
+
ignore_patterns=ignore_patterns,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
print(f"Deployment uploaded to https://huggingface.co/spaces/{args.repo_id}")
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
if __name__ == "__main__":
|
| 57 |
+
main()
|
models/muril-balanced-30k-v1/config.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_name_or_path": "google/muril-base-cased",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"BertForSequenceClassification"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.1,
|
| 7 |
+
"classifier_dropout": null,
|
| 8 |
+
"embedding_size": 768,
|
| 9 |
+
"hidden_act": "gelu",
|
| 10 |
+
"hidden_dropout_prob": 0.1,
|
| 11 |
+
"hidden_size": 768,
|
| 12 |
+
"id2label": {
|
| 13 |
+
"0": "toxic",
|
| 14 |
+
"1": "severe_toxic",
|
| 15 |
+
"2": "obscene",
|
| 16 |
+
"3": "threat",
|
| 17 |
+
"4": "insult",
|
| 18 |
+
"5": "identity_hate"
|
| 19 |
+
},
|
| 20 |
+
"initializer_range": 0.02,
|
| 21 |
+
"intermediate_size": 3072,
|
| 22 |
+
"label2id": {
|
| 23 |
+
"identity_hate": 5,
|
| 24 |
+
"insult": 4,
|
| 25 |
+
"obscene": 2,
|
| 26 |
+
"severe_toxic": 1,
|
| 27 |
+
"threat": 3,
|
| 28 |
+
"toxic": 0
|
| 29 |
+
},
|
| 30 |
+
"layer_norm_eps": 1e-12,
|
| 31 |
+
"max_position_embeddings": 512,
|
| 32 |
+
"model_type": "bert",
|
| 33 |
+
"num_attention_heads": 12,
|
| 34 |
+
"num_hidden_layers": 12,
|
| 35 |
+
"pad_token_id": 0,
|
| 36 |
+
"position_embedding_type": "absolute",
|
| 37 |
+
"problem_type": "multi_label_classification",
|
| 38 |
+
"torch_dtype": "float32",
|
| 39 |
+
"transformers_version": "4.40.0",
|
| 40 |
+
"type_vocab_size": 2,
|
| 41 |
+
"use_cache": true,
|
| 42 |
+
"vocab_size": 197285
|
| 43 |
+
}
|
models/muril-balanced-30k-v1/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6749cec301b96e2238f2d0bfc980a24eb70841e98c198f53feaae3972e72a2dd
|
| 3 |
+
size 950266896
|
models/muril-balanced-30k-v1/special_tokens_map.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cls_token": "[CLS]",
|
| 3 |
+
"mask_token": "[MASK]",
|
| 4 |
+
"pad_token": "[PAD]",
|
| 5 |
+
"sep_token": "[SEP]",
|
| 6 |
+
"unk_token": "[UNK]"
|
| 7 |
+
}
|
models/muril-balanced-30k-v1/tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
models/muril-balanced-30k-v1/tokenizer_config.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"added_tokens_decoder": {
|
| 3 |
+
"0": {
|
| 4 |
+
"content": "[PAD]",
|
| 5 |
+
"lstrip": false,
|
| 6 |
+
"normalized": false,
|
| 7 |
+
"rstrip": false,
|
| 8 |
+
"single_word": false,
|
| 9 |
+
"special": true
|
| 10 |
+
},
|
| 11 |
+
"100": {
|
| 12 |
+
"content": "[UNK]",
|
| 13 |
+
"lstrip": false,
|
| 14 |
+
"normalized": false,
|
| 15 |
+
"rstrip": false,
|
| 16 |
+
"single_word": false,
|
| 17 |
+
"special": true
|
| 18 |
+
},
|
| 19 |
+
"103": {
|
| 20 |
+
"content": "[MASK]",
|
| 21 |
+
"lstrip": false,
|
| 22 |
+
"normalized": false,
|
| 23 |
+
"rstrip": false,
|
| 24 |
+
"single_word": false,
|
| 25 |
+
"special": true
|
| 26 |
+
},
|
| 27 |
+
"104": {
|
| 28 |
+
"content": "[CLS]",
|
| 29 |
+
"lstrip": false,
|
| 30 |
+
"normalized": false,
|
| 31 |
+
"rstrip": false,
|
| 32 |
+
"single_word": false,
|
| 33 |
+
"special": true
|
| 34 |
+
},
|
| 35 |
+
"105": {
|
| 36 |
+
"content": "[SEP]",
|
| 37 |
+
"lstrip": false,
|
| 38 |
+
"normalized": false,
|
| 39 |
+
"rstrip": false,
|
| 40 |
+
"single_word": false,
|
| 41 |
+
"special": true
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"clean_up_tokenization_spaces": true,
|
| 45 |
+
"cls_token": "[CLS]",
|
| 46 |
+
"do_basic_tokenize": true,
|
| 47 |
+
"do_lower_case": false,
|
| 48 |
+
"lowercase": false,
|
| 49 |
+
"mask_token": "[MASK]",
|
| 50 |
+
"model_max_length": 512,
|
| 51 |
+
"never_split": null,
|
| 52 |
+
"pad_token": "[PAD]",
|
| 53 |
+
"sep_token": "[SEP]",
|
| 54 |
+
"strip_accents": false,
|
| 55 |
+
"tokenize_chinese_chars": true,
|
| 56 |
+
"tokenizer_class": "BertTokenizer",
|
| 57 |
+
"unk_token": "[UNK]"
|
| 58 |
+
}
|
models/muril-balanced-30k-v1/vocab.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.135.2
|
| 2 |
+
uvicorn[standard]==0.42.0
|
| 3 |
+
pydantic==2.12.5
|
| 4 |
+
pydantic-settings==2.13.1
|
| 5 |
+
httpx==0.28.1
|
| 6 |
+
torch==2.5.1
|
| 7 |
+
transformers==4.46.3
|
| 8 |
+
sentencepiece==0.2.1
|
| 9 |
+
safetensors==0.7.0
|
smoke_test.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from fastapi.testclient import TestClient
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main() -> None:
|
| 8 |
+
project_root = Path(__file__).resolve().parent
|
| 9 |
+
os.environ["MODEL_ARTIFACT_PATH"] = str(project_root / "models" / "muril-balanced-30k-v1")
|
| 10 |
+
|
| 11 |
+
from app.config import get_settings
|
| 12 |
+
from app.main import create_app
|
| 13 |
+
|
| 14 |
+
get_settings.cache_clear()
|
| 15 |
+
app = create_app()
|
| 16 |
+
|
| 17 |
+
with TestClient(app) as client:
|
| 18 |
+
health = client.get("/api/v1/health")
|
| 19 |
+
assert health.status_code == 200, health.text
|
| 20 |
+
|
| 21 |
+
response = client.post(
|
| 22 |
+
"/api/v1/moderate",
|
| 23 |
+
json={
|
| 24 |
+
"text": "You are an idiot and I will hurt you",
|
| 25 |
+
"context": [],
|
| 26 |
+
},
|
| 27 |
+
)
|
| 28 |
+
assert response.status_code == 200, response.text
|
| 29 |
+
payload = response.json()
|
| 30 |
+
assert payload["model_backend"] == "transformer"
|
| 31 |
+
assert payload["normalized_text"]
|
| 32 |
+
assert payload["latency_ms"] >= 0
|
| 33 |
+
|
| 34 |
+
print("Standalone ML service smoke test passed.")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
main()
|