CoolFace
Apppublic

Dhrona1421/multimodal-content-moderation

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
App README

πŸ›‘οΈ Multimodal Content Moderation Environment

OpenEnv RL Environment v2 Β· Meta Γ— Hugging Face Γ— PyTorch Hackathon

A production-grade reinforcement learning environment that simulates real-world social media content moderation β€” the same class of problem Meta's Trust & Safety teams solve at billions-of-posts-per-day scale. An agent observes posts (text + image classification + user trust metadata) and calls the standard OpenEnv API with a single action payload like {"action": "flag", "confidence": 0.78}.


πŸ“‹ Table of Contents

  1. 1.Problem Statement
  2. 2.Architecture Overview
  3. 3.Environment Specification
  4. 4.Feature Extractor
  5. 5.Policy Network
  6. 6.Training Algorithm
  7. 7.Reward System
  8. 8.Novel Features
  9. 9.Dataset
  10. 10.Tasks
  11. 11.Metrics
  12. 12.Baseline Results
  13. 13.Quick Start
  14. 14.Validation
  15. 15.API Reference
  16. 16.File Structure
  17. 17.Deployment

🎯 Problem Statement

Content moderation is one of the most consequential AI applications today:

  • β€”Platforms process hundreds of millions of posts per day
  • β€”Wrong decisions cause real harm β€” missed hate speech, undetected scams, false removal of legitimate content
  • β€”Human reviewers cannot scale; AI agents must make calibrated decisions and know when to escalate
  • β€”Moderation is inherently multimodal β€” text and image signals frequently conflict

This environment provides a reproducible RL sandbox for training and evaluating moderation agents across all of these challenges.


πŸ—οΈ Architecture Overview

Raw Post (text + image_tag + user_type + history)
          β”‚
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  features.py  β€”  64-dim Multimodal Extractor    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ one-hot  β”‚ β”‚ keyword  β”‚ β”‚ cross-modal      β”‚ β”‚
β”‚  β”‚ encodingsβ”‚ β”‚ TF scoresβ”‚ β”‚ interaction termsβ”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ (64,) ∈ [0,1]
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  network.py  β€”  Actor-Critic MLP (19,172 params)β”‚
β”‚  64β†’128(LN+Drop)β†’64(LN+Drop)β†’32                β”‚
β”‚       β”‚ Actor head          β”‚ Critic head       β”‚
β”‚  FC(3)β†’softmax β†’ Ο€(a|s)   FC(1) β†’ V(s)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ action, confidence, value
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  env.py  β€”  ContentModerationEnv                β”‚
β”‚  Multi-objective reward Β· Severity weighting    β”‚
β”‚  Confidence-gated escalation Β· User history     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          β”‚ reward ∈ [0.0, 1.0]
          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  train.py  β€”  PPO-Clip + GAE                    β”‚
β”‚  Curriculum: easy β†’ medium β†’ hard               β”‚
β”‚  Adam + Cosine LR Β· KL early-stop               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” Environment Specification

Observation Space

FieldTypeValues / Range
post_idint1–41
textstrPost caption / body
image_tagcategoricalsafe Β· nudity Β· violence Β· drugs Β· misleading
user_typecategoricalnew Β· trusted Β· suspicious
difficultycategoricaleasy Β· medium Β· hard
stepint1 – max_steps
max_stepsint12 (fixed)
user_historyfloat0.0–5.0 recency-decayed session flag count
session_statsdict{correct, wrong, flagged, removed} running totals
featuresndarray(64,) ∈ [0,1] pre-computed multimodal feature vector

Action Space

The canonical step() input is a single action object:

FieldTypeValues / Range
actionenumallow Β· flag Β· remove
confidencefloat0.0–1.0 (default 1.0)

Episode

  • β€”12 steps per episode (posts sampled from task pool)
  • β€”reset() β†’ first observation
  • β€”step(action_payload) β†’ (obs, reward, done, info)
  • β€”state() β†’ current observation without advancing (OpenEnv spec)

🧠 Feature Extractor (64-dim)

features.py converts a raw observation dict into a 64-dimensional float32 vector where every value ∈ [0, 1].

DimsGroupDescription
0–4Image tag one-hot5 categories
5–7User type one-hotnew / trusted / suspicious
8–10Difficulty one-hoteasy / medium / hard
11–19Keyword group TF scores9 semantic buckets (spam, hate, violence, drugs, …)
20–24Bigram phrase signals5 phrase-level detectors (credible threat, coded sales, …)
25–30Surface text statisticslength, ALL-CAPS ratio, emoji density, URLs, …
31–40Cross-modal interactions10 conflict terms β€” safe text + harmful image, trusted + misinfo, …
41–45Session / history signalsrecency-decayed flags, repeat-offender binary
46–63Reserved (zero-padded)for future modalities

The cross-modal interaction terms are the key innovation β€” they explicitly encode the conflicting signals that make hard-tier posts difficult:

python
feat[31] = high_risk_img  * safe_score        # safe text + harmful image
feat[32] = trusted_user   * misinfo_score     # trusted user spreading misinfo
feat[33] = suspicious_usr * safe_score        # suspicious user + safe content
feat[38] = mislead_img    * trusted_user      # trusted + misleading image
feat[39] = high_risk_img  * suspicious_usr    # highest-risk combination

🧬 Policy Network

network.py implements a Deep Actor-Critic MLP in pure NumPy with a PyTorch-compatible API (direct port requires only replacing @ with torch.matmul).

Input (64)
  β†’ Linear(128) β†’ LayerNorm β†’ ReLU β†’ Dropout(0.10)
  β†’ Linear(64)  β†’ LayerNorm β†’ ReLU β†’ Dropout(0.10)
  β†’ Linear(32)  β†’ ReLU
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Actor head β†’ Linear(3) β†’ Softmax β†’ Ο€(a|s)  β”‚
  β”‚ Critic head β†’ Linear(1)          β†’ V(s)     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Parameters: 19,172  |  Init: He normal (actor/critic: Οƒ=0.01)

Design choices:

  • β€”LayerNorm prevents internal covariate shift without batch statistics
  • β€”Small actor/critic init (Οƒ=0.01) gives uniform initial action probs
  • β€”Separate heads on shared trunk = standard Actor-Critic architecture
  • β€”Confidence output = max(Ο€(a|s)) β€” directly interpretable

πŸš€ Training Algorithm (PPO-Clip)

train.py implements Proximal Policy Optimisation with Clip (Schulman et al. 2017) in pure NumPy β€” no external RL libraries.

Algorithm

for stage in [easy, medium, hard]:             # curriculum
  for update in range(n_updates):
    collect n_steps transitions                 # rollout
    compute GAE advantages (Ξ»=0.95, Ξ³=0.99)
    for epoch in range(4):                      # PPO epochs
      for minibatch in shuffle(rollout):        # 4 mini-batches
        compute L_clip + c1Β·L_VF - c2Β·H        # PPO objective
        compute analytic gradients via backprop
        clip gradients (max_norm=0.5)
        Adam step (lr=3e-4, cosine annealed)
        if KL > 0.02: early stop epoch          # stability

Hyperparameters

ParameterValueRationale
clip_eps0.2Standard PPO clip ratio
gae_lambda0.95High Ξ» = low bias, moderate variance
gamma0.99Near-1 for 8-step episodes
n_epochs4Standard PPO
n_minibatches416 steps/batch from 64-step rollout
ent_coef0.02Encourages exploration across action space
target_kl0.02Prevents catastrophic policy updates
lr3e-4Standard Adam LR
weight_decay1e-4L2 regularisation

Curriculum Learning

Training proceeds through three stages: easy β†’ medium β†’ hard. Each stage uses the dataset pool for that difficulty level and inherits the policy weights from the previous stage. This mirrors established curriculum learning practice and ensures the agent first learns obvious cases before facing adversarial edge cases.


πŸ’° Reward System

Multi-objective reward decomposition

reward = base_accuracy + severity_modifier + context_modifier + calibration_bonus

Base accuracy matrix (correctaction, agentaction):

allowflagremove
allow+1.00βˆ’0.50βˆ’1.00
flagβˆ’1.00+1.00+0.50
removeβˆ’1.00+0.50+1.00

Severity amplifier β€” applied when agent allows harmful content:

Image TagSeverityFalse-negative penalty
nudity1.5Γ—up to βˆ’1.45
violence1.4Γ—up to βˆ’1.42
drugs1.3Γ—up to βˆ’1.39
misleading1.2Γ—up to βˆ’1.36

Confidence-gated escalation (confidence < 0.45 β†’ human review):

DifficultyEscalation rewardRationale
hard+0.30Smart to admit uncertainty on adversarial cases
medium+0.20Acceptable
easyβˆ’0.10Unnecessary β€” agent should be decisive

Reward range: [0.0, 1.0] Score range: [0.0, 1.0] (normalised over episode)


✨ Novel Features

1. Confidence-Gated Human Escalation

When an agent's confidence < 0.45, the action is treated as "route to human review" rather than a committed decision. This earns partial credit proportional to difficulty β€” mirrors how Meta's actual Trust & Safety pipeline handles low-confidence cases. No other OpenEnv submission models this mechanism.

2. Cross-Modal Interaction Features

The feature extractor explicitly computes 10 product terms between image signals and text signals, directly encoding the conflicts that define hard-tier posts. A naive bag-of-words or one-hot approach misses these.

3. Severity-Weighted Reward

False negatives are not all equally bad. Allowing nudity content is penalised 1.5Γ— harder than allowing misleading content β€” matching the real prioritisation hierarchy in Trust & Safety.

4. Multi-Metric Grading

Beyond accuracy, the grader computes:

  • β€”ECE (Expected Calibration Error) β€” is the agent's stated confidence reliable?
  • β€”FNR on high-risk content β€” the safety-critical miss rate
  • β€”Fairness gap β€” max accuracy disparity across new/trusted/suspicious users
  • β€”Per-class F1 β€” does the agent know when to flag vs remove vs allow?

5. Vectorised Environment

VecContentModerationEnv runs N independent environments in lockstep for PPO batch rollout collection β€” standard in modern RL but unusual in OpenEnv submissions.

6. Decision Reasoning (The "Why")

The environment now captures and displays Agent Reasoning for every step. Agents evaluate:

  • β€”Image Signal: Direct visual violation check.
  • β€”User Signal: Contextual trust-level analysis.
  • β€”Text Signal: Semantic intent and keyword analysis.
  • β€”Cross-Modal Conflict: Detecting when text masks harmful imagery. This reasoning is visible live in the Auto-Pilot and Play tabs of the Gradio demo, transforming the agent from a "black box" into a transparent moderator.

7. Procedural Episode Variants

Medium and hard episodes apply deterministic, seed-controlled text perturbations (context distractors, conflicting clauses, light formatting noise). This prevents pure memorization of static samples while preserving reproducibility.


πŸ“Š Dataset (41 Posts)

Hand-crafted posts across 3 difficulty tiers designed to test reasoning, not keyword matching.

DifficultyCountDesign intent
Easy14All signals align β€” blatant spam or clearly safe content
Medium13Coded language, trust-level reasoning, health misinfo
Hard14Intentionally conflicting signals (see edge cases below)

Hard-Tier Edge Cases

PostConflictCorrect action
#28Uplifting text + nudity image + trusted userremove β€” image overrides everything
#29Historical Jefferson quote as political threat + trustedflag β€” intent context matters
#30Baby's first steps + suspicious accountallow β€” suspicion β‰  guilt
#31ICU nurse explaining acetaminophen thresholds + drugs imageallow β€” medical education
#37"Fresh batch dropped πŸͺπŸ”₯ DM if you know" + trustedflag β€” coded trafficking
#38Mental health medication advocacy + drugs imageallow β€” destigmatisation
#40Agent actively debunking misinformation + misleading imageallow β€” content of the image β‰  stance
#41Arabic-language prayer + suspicious accountallow β€” multilingual fairness test

Distribution

DimensionBreakdown
Correct actionallow: 14 Β· flag: 18 Β· remove: 9
Image tagsafe: 16 Β· misleading: 10 Β· drugs: 6 Β· violence: 6 Β· nudity: 3
User typetrusted: 16 Β· new: 13 Β· suspicious: 12

🧩 Tasks

python
from tasks import make_task

env = make_task("easy")    # easy-only pool (14 posts), sampled to episode length
env = make_task("medium")  # medium-only pool (13 posts), sampled to episode length
env = make_task("hard")    # hard-only pool (14 posts), sampled to episode length

πŸ“ Metrics

The grader (grader.py) computes the following for every agent Γ— task combination:

MetricDescriptionDirection
scoreNormalised episode reward (0–1), primary rank↑
accuracyFraction of steps with correct action↑
macro_f1Unweighted mean F1 across allow/flag/remove↑
weighted_f1Support-weighted F1↑
eceExpected Calibration Error↓
fnr_high_riskFalse-negative rate on nudity/violence/drugs↓
fairness_gapMax accuracy gap across user trust types↓
confusion_matrix3Γ—3 true Γ— predicted action matrixβ€”

πŸ“ˆ Baseline Results

All values below are verified against the bundled artifacts at seed=42, 12 steps per episode.

Verified score (primary metric, 0–1)

AgentEasyMediumHard**Aggregate**
PPO (ppo_checkpoint_best.npz)0.51280.73080.66670.6368
Rule-Based0.94260.80370.85200.8661

Additional metrics (Rule-Based, Hard task)

MetricValue
Accuracy83.33%
Macro-F10.8778
ECE0.2649
FNR (high-risk)0.0000
Fairness gap0.2857

Reproduce the verified numbers with:

bash
python inference_eval.py --rule-based --seed 42 --verbose
python train.py --eval-only --checkpoint ppo_checkpoint --seed 42
Why does rule-based beat PPO? The reward function strongly penalizes confident false negatives on harmful content while still giving modest partial credit for cautious escalation. The shipped PPO checkpoint converges toward a conservative flag-heavy policy, which is safer than random but weaker than the hand-authored moderation heuristic. That is acceptable for this benchmark: the baseline is deterministic, reproducible, and the hard tier remains nontrivial for learned agents. External LLM scores are intentionally omitted from this fixed table because they depend on the provider, model, and token configuration.

πŸš€ Quick Start

Local (no Docker)

bash
git clone <your-repo-url>
cd multimodal-content-moderation
python3.11 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt

# Run strict hackathon inference loop (rule-based, no API key needed)
python inference.py --task hard --rule-based --seed 42 --max-steps 12

# Run with LLM agent
export API_BASE_URL=https://api.openai.com/v1
export MODEL_NAME=gpt-4o-mini
export HF_TOKEN=your_api_key_here
python inference.py --task hard --seed 42 --max-steps 12

# Single task
python inference.py --task hard --seed 42 --max-steps 12

# Full evaluation report (table metrics + results.json)
python inference_eval.py --rule-based --seed 42 --verbose

# Run through deployed/local HTTP API endpoints instead of direct env calls
python api_inference.py --base-url http://127.0.0.1:7860 --task hard --agent rule-based

# Run local submission smoke checks
python validate_submission.py

# Run the official OpenEnv validator
openenv validate --verbose

# Run the OpenEnv server entrypoint directly
uv run server

# Launch API-only server entrypoint
python api.py

# Train PPO from scratch (full curriculum)
python train.py --updates 200

# Evaluate saved checkpoint
python train.py --eval-only --checkpoint ppo_checkpoint

# Launch Gradio UI + API (default mode)
python app.py

# Force API-only mode from app.py if needed
OPENENV_API_ONLY=1 \
python app.py

Docker

bash
# Build
docker build -t content-moderation-env .

# Run interactive demo (rule-based, no key needed)
docker run -p 7860:7860 content-moderation-env

# With LLM agent
docker run -e HF_TOKEN=your_key -p 7860:7860 content-moderation-env

# CLI strict inference
docker run content-moderation-env python inference.py --task hard --rule-based --seed 42 --max-steps 12

# Full evaluation report
docker run content-moderation-env python inference_eval.py --rule-based --seed 42 --verbose

# Submission smoke checks
docker run content-moderation-env python validate_submission.py

# Train inside container
docker run content-moderation-env python train.py --updates 200

Hugging Face Spaces

  1. 1.Create a new Space with the Docker SDK
  2. 2.Push this repository
  3. 3.Add the required runtime configuration:
  4. 4.variable API_BASE_URL
  5. 5.variable MODEL_NAME
  6. 6.secret HF_TOKEN
  7. 7.Restart the Space after saving variables/secrets
  8. 8.Verify the public Space responds with 200 on POST /reset
  9. 9.The Space launches app.py automatically on port 7860

πŸ”Œ Plug in Your Own Agent

The grader accepts any callable that returns either an OpenEnv action payload or a legacy (action, confidence) tuple:

python
from grader import ModerationGrader
from features import extract_features

def my_agent(obs):
    # obs keys: post_id, text, image_tag, user_type, difficulty,
    #           step, max_steps, user_history, session_stats, features (64,)
    features = obs["features"]    # pre-computed 64-dim feature list
    action     = "flag"           # your logic here
    confidence = 0.80             # calibrated confidence [0, 1]
    return {"action": action, "confidence": confidence}

grader = ModerationGrader(seed=42)
report = grader.grade_all_tasks(my_agent)
grader.print_report(report, verbose=True)
print(f"Aggregate: {report['aggregate_score']:.4f}")

πŸ“š API Reference

ContentModerationEnv

python
env = ContentModerationEnv(
    dataset_path   = "moderation_dataset.json",
    task           = "hard",        # easy | medium | hard
    max_steps      = 12,
    seed           = 42,
    severity_scale = 0.3,           # weight of severity penalty
    calib_weight   = 0.15,          # weight of calibration bonus
)

obs            = env.reset()
obs, r, done, info = env.step({"action": "flag", "confidence": 0.75})
obs            = env.state()        # OpenEnv spec: non-advancing read
score          = env.compute_score()
print(env.render())

OpenEnvModerationEnv (strict canonical API)

python
from openenv_env import OpenEnvModerationEnv

env = OpenEnvModerationEnv(task="hard", seed=42, max_steps=12)
obs = env.reset()
obs, reward, done, info = env.step({"action": "flag", "confidence": 0.8})
obs = env.state()

VecContentModerationEnv

python
from env import VecContentModerationEnv

vec      = VecContentModerationEnv(n_envs=4, task="hard", seed=0)
obs_list = vec.reset()
obs_list, rewards, dones, infos = vec.step(
    actions=[
        {"action": "allow", "confidence": 0.9},
        {"action": "flag", "confidence": 0.7},
        {"action": "remove", "confidence": 0.95},
        {"action": "flag", "confidence": 0.6},
    ],
)

ActorCriticNetwork

python
from network import ActorCriticNetwork
from features import extract_features

net = ActorCriticNetwork()              # 19,172 parameters
net.load("ppo_final")                   # load checkpoint

feat = extract_features(obs)            # (64,) ndarray
probs, value, cache = net.forward(feat) # probs sums to 1.0
action_idx, conf, val = net.act(feat, greedy=True)

net.save("my_checkpoint")              # saves .npz file

ModerationGrader

python
from grader import ModerationGrader

grader = ModerationGrader(seed=42)

# Grade one task
result = grader.grade_single_task("hard", my_agent)
print(result["score"])               # 0–1
print(result["classification"])      # per-class precision/recall/F1
print(result["confusion_matrix"])    # 3Γ—3 list
print(result["fnr_high_risk"])       # false-negative rate on harmful content
print(result["fairness_gap"])        # accuracy gap across user types

# Grade all tasks
report = grader.grade_all_tasks(my_agent)
grader.print_report(report, verbose=True)
print(report["aggregate_score"])

PPOTrainer

python
from train import PPOConfig, PPOTrainer, make_ppo_agent
from network import ActorCriticNetwork

cfg = PPOConfig()
cfg.n_steps   = 64
cfg.n_epochs  = 4
cfg.lr        = 3e-4

net     = ActorCriticNetwork()
trainer = PPOTrainer(net, cfg)
env     = make_task("hard")

# One update cycle
rollout_stats = trainer.collect_rollout(env)
update_stats  = trainer.update(rollout_stats)

# Wrap as grader-compatible agent
agent = make_ppo_agent(net, greedy=True)

πŸ“ File Structure

text
multimodal-content-moderation/
|-- moderation_dataset.json   # 41 posts with ground-truth labels and reasons
|-- features.py               # 64-dim multimodal feature extractor
|-- network.py                # Deep Actor-Critic MLP + Adam optimiser
|-- env.py                    # OpenEnv-compliant RL environment (+ VecEnv)
|-- openenv_env.py            # Strict reset()/step()/state() OpenEnv adapter
|-- tasks.py                  # Task registry and make_task() factory
|-- grader.py                 # Full grading engine with F1, ECE, FNR, fairness
|-- inference.py              # Strict [START]/[STEP]/[END] submission inference runner
|-- inference_eval.py         # Full local evaluation runner (tables + results.json)
|-- api_inference.py          # HTTP /reset+/step+/state agent loop runner
|-- train.py                  # PPO-Clip trainer with GAE and curriculum learning
|-- app.py                    # API + optional 6-tab Gradio UI runtime
|-- api.py                    # Dedicated API-only entrypoint
|-- server/
|   |-- __init__.py           # OpenEnv server package exports
|   `-- app.py                # OpenEnv-compatible server entry point (main)
|-- __init__.py               # Package init - public API exports
|-- pyproject.toml            # OpenEnv packaging metadata + `server` script
|-- uv.lock                   # Locked dependency resolution for uv/openenv
|-- openenv.yaml              # OpenEnv metadata specification
|-- requirements.txt          # Python dependencies
|-- Dockerfile                # Multi-stage production Docker build
|-- scripts/
|   `-- validate-submission.sh # End-to-end HF + Docker + openenv validator
|-- LICENSE                   # MIT license
|-- ppo_checkpoint_best.npz   # Bundled PPO checkpoint used by demo + evaluation
|-- ppo_checkpoint_final.npz  # Final PPO checkpoint after training
|-- ppo_final.npz             # Legacy bundled PPO checkpoint alias
|-- training_log.csv          # Training metrics CSV (generated by train.py)
`-- results.json              # Last evaluation results (generated by inference_eval.py)

🚒 Deployment

Hugging Face Spaces (recommended)

The Dockerfile is configured for HF Spaces:

  • β€”Exposes port 7860 (FastAPI API default)
  • β€”Serves validator-compatible HTTP endpoints: POST /reset, POST /step, GET /state
  • β€”Health-check validates environment integrity
  • β€”Bundles the trained PPO checkpoints used by the UI and CLI evaluation
  • β€”API_BASE_URL, MODEL_NAME, and HF_TOKEN enable the LLM agent path required by the hackathon
  • β€”Falls back to rule-based agent automatically if no token

Environment Variables

VariableDefaultPurpose
HF_TOKEN(unset)API key for LLM agent
OPENAI_API_KEY(unset)Fallback API key alias
MODEL_NAMEQwen/Qwen2.5-72B-InstructModel identifier for OpenAI-compat API
API_BASE_URLhttps://router.huggingface.co/v1API base URL (supports any OpenAI-compat endpoint)
LOCAL_IMAGE_NAME(unset)Docker image name when using fromdockerimage() style env launch
OPENENV_API_ONLY0If 1, runs API-only mode (no Gradio UI)

Submission Validator

Run the local validator script against the public Space URL:

bash
./scripts/validate-submission.sh https://dhrona1421-multimodal-content-moderation.hf.space .

HTTP API

The deployed Space exposes validator-compatible API endpoints by default:

  • β€”POST /reset with optional JSON body: {"task":"easy|medium|hard","seed":42,"max_steps":12,"env_id":"optional"}
  • β€”POST /step with action payload: {"action":"flag","confidence":0.78}
  • β€”GET /state to read the current observation without advancing
  • β€”GET /healthz for a basic service check
  • β€”GET /health for the OpenEnv runtime validator
  • β€”GET /metadata for environment metadata
  • β€”GET /schema for action, observation, and state schemas
  • β€”POST /mcp for the JSON-RPC compatibility check used by the OpenEnv runtime validator
  • β€”Optional multi-session routing via X-Env-Id header (or env_id in request body/query)
  • β€”Use env_id: "new" on /reset to request a generated session id returned in X-Env-Id

These endpoints return standard OpenEnv-style JSON responses and allow the official submission validator to ping the Space directly.


πŸ† What Makes This Submission Stand Out

FeatureDetail
Confidence-gated escalationModels human review queue; novel in OpenEnv
Cross-modal interaction features10 explicit conflict terms in the 64-dim feature vector
Severity-weighted rewardsNudity/violence false-negatives penalised 1.5Γ— harder
Multi-metric gradingECE, FNR, fairness gap, per-class F1 alongside accuracy
PPO-Clip from scratchFull backprop, GAE, KL stop β€” no external RL library
Vectorised environmentBatch rollout collection for scalable RL training
Curriculum learningeasy β†’ medium β†’ hard difficulty progression
Adversarial hard tier14 edge cases designed to break keyword matching
Multilingual fairnessArabic-language post in dataset (post #41)
Chain-of-thought LLM3-shot CoT with 4-signal reasoning protocol
6-tab Gradio demoPlay / Auto-Pilot / Training / Dataset / Leaderboard / About
Multi-stage DockerfileLean production image, health check, pre-trained checkpoint

πŸ“„ License

MIT β€” see LICENSE.


Built for the Meta Γ— Hugging Face Γ— PyTorch OpenEnv Hackathon.