Spaces:
Running
Running
Commit ·
84bb476
1
Parent(s): 2a5dd49
Refactor: reorganize into core/, api/, web/, templates/
Browse filesCo-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app/{routers → api}/__init__.py +0 -0
- app/{routers → api}/inference.py +4 -4
- app/{routers → api}/meta.py +2 -2
- app/core/__init__.py +0 -0
- app/{cache.py → core/cache.py} +0 -0
- app/{category_meta.py → core/category_meta.py} +0 -0
- app/{model_loader.py → core/model_loader.py} +3 -1
- app/{schemas.py → core/schemas.py} +4 -4
- app/main.py +4 -3
- app/services/classifier.py +6 -4
- app/{static → templates}/batch.html +0 -0
- app/{static → templates}/categories.html +0 -0
- app/{static → templates}/home.html +0 -0
- app/{static → templates}/index.html +0 -0
- app/utils.py +3 -2
- app/web/__init__.py +0 -0
- app/{routers → web}/pages.py +0 -0
app/{routers → api}/__init__.py
RENAMED
|
File without changes
|
app/{routers → api}/inference.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import APIRouter, HTTPException
|
| 2 |
-
from ..model_loader import get_classifier
|
| 3 |
-
from ..schemas import PredictRequest, PredictResponse, ClassifyBatchRequest, ClassifyBatchResponse
|
| 4 |
from ..services.classifier import classify_one, classify_many
|
| 5 |
from ..utils import normalize
|
| 6 |
|
|
@@ -10,14 +10,14 @@ router = APIRouter(tags=["inference"])
|
|
| 10 |
@router.post("/classify", response_model=PredictResponse)
|
| 11 |
def classify(request: PredictRequest):
|
| 12 |
if get_classifier() is None:
|
| 13 |
-
raise HTTPException(status_code=503, detail="
|
| 14 |
return classify_one(normalize(request.text), request.text)
|
| 15 |
|
| 16 |
|
| 17 |
@router.post("/classify/batch", response_model=ClassifyBatchResponse)
|
| 18 |
def classify_batch(request: ClassifyBatchRequest):
|
| 19 |
if get_classifier() is None:
|
| 20 |
-
raise HTTPException(status_code=503, detail="
|
| 21 |
normalized = [normalize(t) for t in request.texts]
|
| 22 |
results, from_cache = classify_many(normalized, request.texts)
|
| 23 |
return ClassifyBatchResponse(results=results, total=len(results), from_cache=from_cache)
|
|
|
|
| 1 |
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from ..core.model_loader import get_classifier
|
| 3 |
+
from ..core.schemas import PredictRequest, PredictResponse, ClassifyBatchRequest, ClassifyBatchResponse
|
| 4 |
from ..services.classifier import classify_one, classify_many
|
| 5 |
from ..utils import normalize
|
| 6 |
|
|
|
|
| 10 |
@router.post("/classify", response_model=PredictResponse)
|
| 11 |
def classify(request: PredictRequest):
|
| 12 |
if get_classifier() is None:
|
| 13 |
+
raise HTTPException(status_code=503, detail="Model not available")
|
| 14 |
return classify_one(normalize(request.text), request.text)
|
| 15 |
|
| 16 |
|
| 17 |
@router.post("/classify/batch", response_model=ClassifyBatchResponse)
|
| 18 |
def classify_batch(request: ClassifyBatchRequest):
|
| 19 |
if get_classifier() is None:
|
| 20 |
+
raise HTTPException(status_code=503, detail="Model not available")
|
| 21 |
normalized = [normalize(t) for t in request.texts]
|
| 22 |
results, from_cache = classify_many(normalized, request.texts)
|
| 23 |
return ClassifyBatchResponse(results=results, total=len(results), from_cache=from_cache)
|
app/{routers → api}/meta.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
-
from ..model_loader import get_categories
|
| 3 |
-
from ..category_meta import CATEGORY_META
|
| 4 |
from ..services.classifier import get_cache
|
| 5 |
|
| 6 |
router = APIRouter(tags=["meta"])
|
|
|
|
| 1 |
from fastapi import APIRouter
|
| 2 |
+
from ..core.model_loader import get_categories
|
| 3 |
+
from ..core.category_meta import CATEGORY_META
|
| 4 |
from ..services.classifier import get_cache
|
| 5 |
|
| 6 |
router = APIRouter(tags=["meta"])
|
app/core/__init__.py
ADDED
|
File without changes
|
app/{cache.py → core/cache.py}
RENAMED
|
File without changes
|
app/{category_meta.py → core/category_meta.py}
RENAMED
|
File without changes
|
app/{model_loader.py → core/model_loader.py}
RENAMED
|
@@ -4,13 +4,14 @@ from transformers import pipeline
|
|
| 4 |
from huggingface_hub import hf_hub_download
|
| 5 |
|
| 6 |
HF_REPO = "cmeneses99/sms-classifier"
|
| 7 |
-
MODEL_DIR = Path(__file__).parent.parent / "model"
|
| 8 |
|
| 9 |
_classifier = None
|
| 10 |
_categories: list[str] = []
|
| 11 |
|
| 12 |
|
| 13 |
def _ensure_model() -> Path:
|
|
|
|
| 14 |
if (MODEL_DIR / "config.json").exists():
|
| 15 |
return MODEL_DIR
|
| 16 |
|
|
@@ -30,6 +31,7 @@ def _ensure_model() -> Path:
|
|
| 30 |
|
| 31 |
|
| 32 |
def load_model() -> None:
|
|
|
|
| 33 |
global _classifier, _categories
|
| 34 |
|
| 35 |
model_path = _ensure_model()
|
|
|
|
| 4 |
from huggingface_hub import hf_hub_download
|
| 5 |
|
| 6 |
HF_REPO = "cmeneses99/sms-classifier"
|
| 7 |
+
MODEL_DIR = Path(__file__).parent.parent.parent / "model"
|
| 8 |
|
| 9 |
_classifier = None
|
| 10 |
_categories: list[str] = []
|
| 11 |
|
| 12 |
|
| 13 |
def _ensure_model() -> Path:
|
| 14 |
+
"""Download model files from HF Hub if not present locally."""
|
| 15 |
if (MODEL_DIR / "config.json").exists():
|
| 16 |
return MODEL_DIR
|
| 17 |
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def load_model() -> None:
|
| 34 |
+
"""Load the classifier pipeline and category labels into module-level state."""
|
| 35 |
global _classifier, _categories
|
| 36 |
|
| 37 |
model_path = _ensure_model()
|
app/{schemas.py → core/schemas.py}
RENAMED
|
@@ -9,8 +9,8 @@ class PredictRequest(BaseModel):
|
|
| 9 |
def sanitize(cls, v: str) -> str:
|
| 10 |
v = v.strip()
|
| 11 |
if not v:
|
| 12 |
-
raise ValueError("
|
| 13 |
-
#
|
| 14 |
v = " ".join(v.split())
|
| 15 |
return v
|
| 16 |
|
|
@@ -37,9 +37,9 @@ class ClassifyBatchRequest(BaseModel):
|
|
| 37 |
for text in v:
|
| 38 |
text = " ".join(text.strip().split())
|
| 39 |
if not text:
|
| 40 |
-
raise ValueError("
|
| 41 |
if len(text) > 512:
|
| 42 |
-
raise ValueError("
|
| 43 |
result.append(text)
|
| 44 |
return result
|
| 45 |
|
|
|
|
| 9 |
def sanitize(cls, v: str) -> str:
|
| 10 |
v = v.strip()
|
| 11 |
if not v:
|
| 12 |
+
raise ValueError("Text cannot be empty or contain only whitespace")
|
| 13 |
+
# collapse excessive whitespace (tabs, multiple spaces, newlines)
|
| 14 |
v = " ".join(v.split())
|
| 15 |
return v
|
| 16 |
|
|
|
|
| 37 |
for text in v:
|
| 38 |
text = " ".join(text.strip().split())
|
| 39 |
if not text:
|
| 40 |
+
raise ValueError("Each text must have at least one character")
|
| 41 |
if len(text) > 512:
|
| 42 |
+
raise ValueError("Each text must have at most 512 characters")
|
| 43 |
result.append(text)
|
| 44 |
return result
|
| 45 |
|
app/main.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
from contextlib import asynccontextmanager
|
| 2 |
from fastapi import FastAPI
|
| 3 |
-
from .model_loader import load_model
|
| 4 |
-
from .
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
@asynccontextmanager
|
|
@@ -12,7 +13,7 @@ async def lifespan(app: FastAPI):
|
|
| 12 |
|
| 13 |
app = FastAPI(
|
| 14 |
title="SMS Classifier API",
|
| 15 |
-
description="
|
| 16 |
version="2.0.0",
|
| 17 |
lifespan=lifespan,
|
| 18 |
)
|
|
|
|
| 1 |
from contextlib import asynccontextmanager
|
| 2 |
from fastapi import FastAPI
|
| 3 |
+
from .core.model_loader import load_model
|
| 4 |
+
from .api import inference, meta
|
| 5 |
+
from .web import pages
|
| 6 |
|
| 7 |
|
| 8 |
@asynccontextmanager
|
|
|
|
| 13 |
|
| 14 |
app = FastAPI(
|
| 15 |
title="SMS Classifier API",
|
| 16 |
+
description="Classifies SMS messages into categories using fine-tuned DistilBERT.",
|
| 17 |
version="2.0.0",
|
| 18 |
lifespan=lifespan,
|
| 19 |
)
|
app/services/classifier.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
from ..cache import LRUCache
|
| 2 |
-
from ..model_loader import get_classifier
|
| 3 |
-
from ..schemas import PredictResponse, IntentPrediction
|
| 4 |
|
| 5 |
_cache = LRUCache(max_size=512)
|
| 6 |
|
|
@@ -10,6 +10,7 @@ def get_cache() -> LRUCache:
|
|
| 10 |
|
| 11 |
|
| 12 |
def run_inference(texts: list[str]) -> list[PredictResponse]:
|
|
|
|
| 13 |
classifier = get_classifier()
|
| 14 |
raw = classifier(texts, batch_size=16)
|
| 15 |
responses = []
|
|
@@ -24,6 +25,7 @@ def run_inference(texts: list[str]) -> list[PredictResponse]:
|
|
| 24 |
|
| 25 |
|
| 26 |
def classify_one(normalized_text: str, original_text: str) -> PredictResponse:
|
|
|
|
| 27 |
cached = _cache.get(normalized_text)
|
| 28 |
if cached:
|
| 29 |
return cached.model_copy(update={"cached": True})
|
|
@@ -34,7 +36,7 @@ def classify_one(normalized_text: str, original_text: str) -> PredictResponse:
|
|
| 34 |
|
| 35 |
|
| 36 |
def classify_many(normalized_texts: list[str], original_texts: list[str]) -> tuple[list[PredictResponse], int]:
|
| 37 |
-
"""Returns (results, from_cache_count)."""
|
| 38 |
results: list[PredictResponse | None] = [None] * len(normalized_texts)
|
| 39 |
from_cache = 0
|
| 40 |
pending = []
|
|
|
|
| 1 |
+
from ..core.cache import LRUCache
|
| 2 |
+
from ..core.model_loader import get_classifier
|
| 3 |
+
from ..core.schemas import PredictResponse, IntentPrediction
|
| 4 |
|
| 5 |
_cache = LRUCache(max_size=512)
|
| 6 |
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
def run_inference(texts: list[str]) -> list[PredictResponse]:
|
| 13 |
+
"""Run the model on a list of texts and return structured responses."""
|
| 14 |
classifier = get_classifier()
|
| 15 |
raw = classifier(texts, batch_size=16)
|
| 16 |
responses = []
|
|
|
|
| 25 |
|
| 26 |
|
| 27 |
def classify_one(normalized_text: str, original_text: str) -> PredictResponse:
|
| 28 |
+
"""Classify a single message, using cache when available."""
|
| 29 |
cached = _cache.get(normalized_text)
|
| 30 |
if cached:
|
| 31 |
return cached.model_copy(update={"cached": True})
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def classify_many(normalized_texts: list[str], original_texts: list[str]) -> tuple[list[PredictResponse], int]:
|
| 39 |
+
"""Classify a batch of messages. Returns (results, from_cache_count)."""
|
| 40 |
results: list[PredictResponse | None] = [None] * len(normalized_texts)
|
| 41 |
from_cache = 0
|
| 42 |
pending = []
|
app/{static → templates}/batch.html
RENAMED
|
File without changes
|
app/{static → templates}/categories.html
RENAMED
|
File without changes
|
app/{static → templates}/home.html
RENAMED
|
File without changes
|
app/{static → templates}/index.html
RENAMED
|
File without changes
|
app/utils.py
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
import unicodedata
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
-
|
| 5 |
|
| 6 |
|
| 7 |
def normalize(text: str) -> str:
|
|
|
|
| 8 |
text = text.lower()
|
| 9 |
text = unicodedata.normalize("NFD", text)
|
| 10 |
return "".join(c for c in text if unicodedata.category(c) != "Mn")
|
| 11 |
|
| 12 |
|
| 13 |
def read_static(filename: str) -> str:
|
| 14 |
-
return (
|
|
|
|
| 1 |
import unicodedata
|
| 2 |
from pathlib import Path
|
| 3 |
|
| 4 |
+
_TEMPLATES = Path(__file__).parent / "templates"
|
| 5 |
|
| 6 |
|
| 7 |
def normalize(text: str) -> str:
|
| 8 |
+
"""Lowercase and strip accents for cache-key normalization."""
|
| 9 |
text = text.lower()
|
| 10 |
text = unicodedata.normalize("NFD", text)
|
| 11 |
return "".join(c for c in text if unicodedata.category(c) != "Mn")
|
| 12 |
|
| 13 |
|
| 14 |
def read_static(filename: str) -> str:
|
| 15 |
+
return (_TEMPLATES / filename).read_text(encoding="utf-8")
|
app/web/__init__.py
ADDED
|
File without changes
|
app/{routers → web}/pages.py
RENAMED
|
File without changes
|