cmeneses99 Claude Sonnet 4.6 commited on
Commit
84bb476
·
1 Parent(s): 2a5dd49

Refactor: reorganize into core/, api/, web/, templates/

Browse files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

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="Modelo no disponible")
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="Modelo no disponible")
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("El texto no puede estar vacío o contener solo espacios")
13
- # Colapsa whitespace excesivo (tabs, múltiples espacios, newlines)
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("Cada texto debe tener al menos un carácter")
41
  if len(text) > 512:
42
- raise ValueError("Cada texto debe tener máximo 512 caracteres")
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 .routers import pages, inference, meta
 
5
 
6
 
7
  @asynccontextmanager
@@ -12,7 +13,7 @@ async def lifespan(app: FastAPI):
12
 
13
  app = FastAPI(
14
  title="SMS Classifier API",
15
- description="Clasifica mensajes SMS en categorías usando DistilBERT fine-tuned.",
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
- _STATIC = Path(__file__).parent / "static"
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 (_STATIC / filename).read_text(encoding="utf-8")
 
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