Classifying Images
The image classification endpoint runs a Vision Transformer over an input image and returns a set of labels with confidence scores. The recommended model is Freepik/nsfw_image_detector, an EVA-02 based classifier that produces a four-bucket severity breakdown — far more useful for tunable moderation than a single normal / nsfw flag.
The model returns probabilities across four labels that sum to 1.0:
| Label | Meaning |
|---|---|
neutral | Safe — no NSFW content |
low | Mildly suggestive |
medium | Suggestive or borderline |
high | Explicit |
Two endpoints are exposed:
inference.vit.img2label.v1— returns a single JSON document. Labels are embedded underconfig.labelson the returned job.inference.vit.img2label.v2— returns a multipart response containinglabels.jsonand the original input image. Useful when chaining classification into a wider workflow that also needs the image bytes downstream.
Project Setup
Section titled “Project Setup”# Create a project directory.mkdir prodia-classifying-imagescd prodia-classifying-imagesInstall Node (if not already installed):
brew install node# Close the current terminal and open a new one so that node is available.apt install node# Close the current terminal and open a new one so that node is available.winget install -e --id OpenJS.NodeJS.LTS# Close the current terminal and open a new one so that node is available.Create project skeleton:
# Requires node --version >= 18# Initialize the project with npm.npm init -y
# Install the prodia-js library.npm install prodia --saveInstall Python (if not already installed):
brew install python# Close the current terminal and open a new one so that python is available.apt install python3 python3-venv python-is-python3# Close the current terminal and open a new one so that python is available.winget install -e --id Python.Python.3.12# Close the current terminal and open a new one so that python is available.# Requires python --version >= 3.12python -m venv venvsource venv/bin/activatepip install requestsInstall curl (if not already installed):
brew install curl# Close the current terminal and open a new one so that curl is available.apt install curl# Close the current terminal and open a new one so that curl is available.# NOTE: Windows 10 and up have curl installed by default and this can be# skipped.winget install -e --id cURL.cURL# Close the current terminal and open a new one so that curl is available.# Export your token so it can be used by the main code.export PRODIA_TOKEN=your-token-hereYour token is exported to an environment variable. If you close or switch your
shell you’ll need to run export PRODIA_TOKEN=your-token-here again.
Create a main file for your project:
const { createProdia } = require("prodia/v2");
const prodia = createProdia({ token: process.env.PRODIA_TOKEN // get it from environment});Create the following main.py
from requests.adapters import HTTPAdapter, Retryimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})set -euo pipefailYou’re now ready to make some API calls!
Classify an image
Section titled “Classify an image”Pass the input image and request Freepik/nsfw_image_detector. The response contains a labels object with one score per severity bucket.
const { createProdia } = require("prodia/v2");
const prodia = createProdia({ token: process.env.PRODIA_TOKEN,});
(async () => { // get input image const inputBuffer = await (await fetch("https://docs.prodia.com/sunny-day.jpg")).arrayBuffer();
const { job } = await prodia.job({ type: "inference.vit.img2label.v1", config: { model: "Freepik/nsfw_image_detector", }, }, { inputs: [ inputBuffer ], });
console.log(job.config.labels); // => { neutral: 0.9995, high: 0.00038, low: 0.000087, medium: 0.000056 }})();node main.jsfrom requests.adapters import HTTPAdapter, Retryfrom io import BytesIOimport jsonimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})
try: with open('sunny-day.jpg', 'rb') as f: input_image = f.read()except FileNotFoundError: res = requests.get('https://docs.prodia.com/sunny-day.jpg') input_image = res.content with open('sunny-day.jpg', 'wb') as f: f.write(res.content)
headers = { 'Accept': 'application/json',}
job = { 'type': 'inference.vit.img2label.v1', 'config': { 'model': 'Freepik/nsfw_image_detector', },}
files = [ ('job', ('job.json', BytesIO(json.dumps(job).encode('utf-8')), 'application/json')), ('input', ('sunny-day.jpg', input_image, 'image/jpeg')),]
res = session.post(prodia_url, headers=headers, files=files)print(f"Request ID: {res.headers['x-request-id']}")print(f"Status: {res.status_code}")
if res.status_code != 200: print(res.text) sys.exit(1)
labels = res.json()['config']['labels']print(labels)# => {'neutral': 0.9995, 'high': 0.00038, 'low': 8.7e-05, 'medium': 5.6e-05}python main.pyset -euo pipefail
cat <<EOF > job.json{ "type": "inference.vit.img2label.v1", "config": { "model": "Freepik/nsfw_image_detector" }}EOF
if [[ ! -f sunny-day.jpg ]]; then curl -Lo sunny-day.jpg 'https://docs.prodia.com/sunny-day.jpg'fi
curl -sSf --retry 3 \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -H 'Accept: application/json' \ https://inference.prodia.com/v2/jobbash main.shThe full response looks like this:
{ "type": "inference.vit.img2label.v1", "id": "65a10fac-b90f-40a8-8f5c-e41dbfbb4991", "state": { "current": "completed" }, "config": { "model": "Freepik/nsfw_image_detector", "labels": { "neutral": 0.9994731545448303, "high": 0.0003829085035249591, "low": 0.00008746454113861546, "medium": 0.00005647135549224913 } }, "metrics": { "elapsed": 0.16 }}Choosing a threshold
Section titled “Choosing a threshold”The four scores sum to 1.0 and represent the model’s belief that the image primarily belongs to that severity bucket. To turn them into an allow/block decision, the Freepik model card recommends a cumulative scheme: pick the lowest severity you want to flag, sum that bucket and all higher ones, then compare against a threshold (typically 0.5).
| Policy | Score formula | Use case |
|---|---|---|
| Strict — block only explicit | P(high) | Permissive platforms; reject only the most extreme content |
| Moderate — block suggestive and up | P(medium) + P(high) | General audiences; common default |
| Lenient — block anything non-neutral | P(low) + P(medium) + P(high) | Family-safe / under-13 surfaces |
The closer the threshold is to 0, the more aggressively borderline images are flagged (more false positives). The closer to 1, the more permissive (more false negatives).
A minimal moderate-policy check looks like this:
const { neutral, low, medium, high } = job.config.labels;const flagged = (medium + high) > 0.5;Keep the image alongside the labels (v2)
Section titled “Keep the image alongside the labels (v2)”If you want both the labels and a pass-through copy of the original image in a single response — for example, to pipe straight into a downstream job in a workflow — use the v2 endpoint. It returns a multipart body containing labels.json followed by the original image bytes.
const { createProdia } = require("prodia/v2");const fs = require("node:fs/promises");
const prodia = createProdia({ token: process.env.PRODIA_TOKEN,});
(async () => { const inputBuffer = await (await fetch("https://docs.prodia.com/sunny-day.jpg")).arrayBuffer();
const result = await prodia.job({ type: "inference.vit.img2label.v2", config: { model: "Freepik/nsfw_image_detector" }, }, { accept: "multipart/form-data", inputs: [ inputBuffer ], });
const form = await result.formData(); const outputs = form.getAll("output"); // outputs[0] is labels.json, outputs[1] is the original image
const labels = JSON.parse(await outputs[0].text()); console.log(labels);
const imageBuffer = await outputs[1].arrayBuffer(); await fs.writeFile("passthrough.jpg", new Uint8Array(imageBuffer));})();node main.jspip install requests-toolbeltfrom requests.adapters import HTTPAdapter, Retryfrom requests_toolbelt.multipart import decoderfrom io import BytesIOimport jsonimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})
with open('sunny-day.jpg', 'rb') as f: input_image = f.read()
job = { 'type': 'inference.vit.img2label.v2', 'config': {'model': 'Freepik/nsfw_image_detector'},}
files = [ ('job', ('job.json', BytesIO(json.dumps(job).encode('utf-8')), 'application/json')), ('input', ('sunny-day.jpg', input_image, 'image/jpeg')),]
res = session.post(prodia_url, headers={'Accept': 'multipart/form-data'}, files=files)if res.status_code != 200: print(res.text); sys.exit(1)
multipart = decoder.MultipartDecoder.from_response(res)for part in multipart.parts: disposition = part.headers[b'Content-Disposition'].decode() if 'filename="labels.json"' in disposition: labels = json.loads(part.content) print(labels) elif 'filename="sunny-day.jpg"' in disposition: with open('passthrough.jpg', 'wb') as f: f.write(part.content)python main.pyset -euo pipefail
cat <<EOF > job.json{ "type": "inference.vit.img2label.v2", "config": { "model": "Freepik/nsfw_image_detector" }}EOF
curl -sSf --retry 3 \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -H 'Accept: multipart/form-data' \ --output response.bin \ https://inference.prodia.com/v2/job
# response.bin is a multipart body containing job.json, labels.json, and the image.bash main.shInput requirements
Section titled “Input requirements”| Constraint | Value |
|---|---|
| Accepted formats | PNG, JPEG, WebP |
| Minimum dimensions | 128 x 128 |
| Maximum dimensions | 2048 x 2048 |
| Maximum file size | 10 MB |