Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Content Moderation API

A small FastAPI service that puts two Hugging Face models behind an HTTP API: one suggests a minimum age rating for a text, the other flags hate speech.

Scope

This is a proof of concept, not a reference implementation. There were two things worth validating:

  • Whether a layered structure (domain / infrastructure / presentation) is worth the extra indirection when the interesting part of the app is a model call.
  • Whether running the models locally is practical: no external inference API, no per-request cost, everything on the machine that serves the requests, and what that costs in performance (cold-start download and load time, latency per request, memory held by a process that keeps the models resident).

It is kept deliberately small, so anything a production service would need, persistence, auth, rate limiting, batching, monitoring, model evaluation, is out of scope on purpose.

Setup

python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

Run

uvicorn main:app --reload

Swagger UI: http://127.0.0.1:8000/docs

The models are downloaded from Hugging Face and loaded on the first request that needs them, so that request is slow (a few GB on a cold cache). Every request after it reuses the loaded model.

Tests

pip install -r requirements-dev.txt
pytest

The tests replace both services with fakes through FastAPI's dependency overrides, so they never download or run a model.

Endpoints

Age Classification

POST /v1/age-classification

{
  "text": "string"
}
{
  "rating": 12,
  "label": "12+"
}

Hate Speech Detection

POST /v1/hate-speech/detect

{
  "text": "string"
}
{
  "is_hate_speech": true,
  "confidence_score": 0.9712
}

Hate Speech Analysis

POST /v1/hate-speech/analyze

Same request, plus the score of every toxicity category:

{
  "text": "string",
  "is_hate_speech": true,
  "confidence_score": 0.9712,
  "detected_categories": ["toxic"],
  "categories": [
    { "category": "toxic", "confidence": 0.9712, "above_threshold": true },
    { "category": "insult", "confidence": 0.1043, "above_threshold": false }
  ],
  "model": "unitary/toxic-bert",
  "analyzed_at": "2026-01-01T00:00:00Z"
}

Health

GET /health: liveness check that does not touch the models.

Models

Feature Model Task
Age classification facebook/bart-large-mnli zero-shot classification against the age labels in HuggingFaceAgeService
Hate speech unitary/toxic-bert multi-label toxicity classification

Both are English-language models. Configurable through environment variables: AGE_MODEL, HATE_SPEECH_MODEL, HATE_SPEECH_THRESHOLD, TORCH_DEVICE (auto-detects cuda / mps / cpu when unset). See app/config.py.

Limitations

  • The age scale is a hand-written mapping from zero-shot labels to ages. It was never evaluated against a labelled dataset, and zero-shot confidence on these labels is often low.
  • Hate speech detection inherits the biases and the English-only training data of unitary/toxic-bert.
  • Models are held in memory in a single process. One worker per machine is the practical limit.

Structure

fastapi-content-moderation/
├── app/
│   ├── config.py                  # Settings, read from environment variables
│   │
│   ├── domain/                    # Core of the app, no framework or ML imports
│   │   ├── entities/              # Objects with identity and behaviour
│   │   ├── services/              # Interfaces (ABCs) the domain depends on
│   │   ├── usecases/              # One class per business operation
│   │   ├── value_objects/         # Immutable, self-validating values
│   │   └── errors.py              # Expected failures, mapped to HTTP in main.py
│   │
│   ├── infrastructure/            # Hugging Face implementations of the interfaces
│   │   └── providers.py           # Composition root: builds each service once
│   │
│   └── presentation/              # HTTP layer (FastAPI)
│       ├── age_classification/    # routes.py, controller.py, schemas.py
│       └── hate_speech/
│
├── tests/                         # pytest, with fake services
└── main.py                        # App instance, routers, exception handlers

Each feature in presentation/ is split the same way: routes.py declares the endpoints and their dependencies, controller.py maps between schemas and the domain, schemas.py holds the request/response models.

graph LR
  main[main.py] --> presentation
  presentation[presentation<br/>routes, controllers, schemas] --> usecases
  usecases[domain<br/>usecases, entities, value objects] --> interfaces
  interfaces[domain<br/>service interfaces]
  infrastructure[infrastructure<br/>Hugging Face services] -.implements.-> interfaces
  providers[infrastructure<br/>providers.py] --> infrastructure
  presentation -.depends on.-> providers
Loading

The domain declares what it needs as abstract classes and never imports transformers or fastapi. providers.py is the only place that knows which concrete implementation is used, which is what lets the tests swap in fakes.

About

FastAPI service running local Hugging Face models for hate-speech detection, age classification and sentiment analysis, in DDD layers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages