The Obscuro FastAPI server provides a REST API for programmatic video anonymization.
blur-api --host 0.0.0.0 --port 8000uvicorn blur_api.serve:app --reload --host 0.0.0.0 --port 8000Host address to bind to.
Default: 0.0.0.0
Port to bind to.
Default: 8000
Logging level. Choices: DEBUG, INFO, WARNING, ERROR, CRITICAL
Default: INFO
Enable JSON logging to the specified file.
Enable auto-reload for development (watches for file changes).
Path to TOML configuration file to load before starting.
# Build the CPU image locally (optional if you pull from GHCR)
docker build -t ghcr.io/<your-account>/blur-gui-backend:latest .
# Run the container
docker run --rm -p 8000:8000 \
-e BLUR_MODELS_DIR=/data/models \
-v "$(pwd)/models:/data/models" \
ghcr.io/<your-account>/blur-gui-backend:latestPublished builds are available on GitHub Container Registry via the release workflow:
# Authenticate once (public images can skip --password-stdin if anonymous pull is enabled)
echo "${GHCR_TOKEN}" | docker login ghcr.io -u <your-account> --password-stdin
# Pull the pre-built backend
docker pull ghcr.io/<your-account>/blur-gui-backend:latest
docker pull ghcr.io/<your-account>/blur-gui-backend:gpuUse the GPU tag together with --gpus all if you have the NVIDIA container runtime installed.
All endpoints are relative to the server URL:
http://localhost:8000
Get available configuration options and current settings.
Response:
{
"model": {
"available": ["1280_nano", "640_nano"],
"current": "1280_nano",
"files": [
{
"name": "1280_nano",
"filename": "1280_nano.onnx",
"size_bytes": 12345678,
"modified_at": "2025-11-06T10:30:00Z",
"immutable": true
}
]
},
"blur": {
"types": ["gaussian", "pixelate", "blackout", "debug"],
"current_type": "gaussian",
"current_strength": 10,
"strength_range": [1, 100]
},
"detection": {
"current_confidence_threshold": 0.5,
"current_low_score_threshold": 0.1,
"current_batch_size": 4,
"threshold_range": [0.0, 1.0],
"use_sahi": true,
"current_inference_size": 1920,
"inference_size_range": [256, 8192],
"current_sahi_overlap": 0.2,
"sahi_overlap_range": [0.0, 0.99],
"current_disable_masks": false,
"current_single_pass": false,
"classes_to_blur": ["plate", "head"]
},
"tracking": {
"types": ["dummy", "bytetrack", "botsort", "hybrid_sot", "oc_sort"],
"current_type": "bytetrack",
"params": {
"distance_gate": 0.05,
"confirm_after_N": 2,
"max_misses_M": 10,
...
},
"use_offline_linker": true,
"ranges": {
"distance_gate": [0.05, 1.0],
"confirm_after_N": [1, 5],
"max_misses_M": [1, 30],
...
}
},
"video": {
"codecs": ["h264", "hevc", "vp8", "vp9"],
"current_codec": "h264",
"current_quality": null,
"quality_range": [1, 51]
},
"global": {
"log_levels": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
"current_debug": false,
"current_log_level": "INFO"
}
}Get the current active configuration.
Response:
{
"model": {
"name": "1280_nano",
"file": null
},
"blur": {
"type": "gaussian",
"strength": 10
},
"detection": {
"confidence_threshold": 0.5,
"low_score_threshold": 0.1,
"batch_size": 4,
"use_sahi": true,
"inference_size": 1920,
"sahi_overlap_ratio": 0.2,
"disable_masks": false,
"single_pass": false,
"classes_to_blur": ["plate", "head"]
},
"tracking": {
"type": "bytetrack",
"params": {...},
"use_offline_linker": true
},
"video": {
"codec": "h264",
"quality": null
},
"debug": false,
"log_level": "INFO"
}List all available ONNX detection models (files under <models root>/detection).
Response:
{
"models": [
{
"name": "1280_nano",
"filename": "1280_nano.onnx",
"size_bytes": 12345678,
"modified_at": "2025-11-06T10:30:00Z",
"immutable": true
}
]
}Upload a new ONNX detection model.
Request:
- Content-Type:
multipart/form-data - Fields:
file(file, required) - ONNX model filename(string, optional) - Model name (without .onnx extension)
Example using curl:
curl -X POST http://localhost:8000/blur/models \
-F "file=@my_model.onnx" \
-F "name=custom_model"Response:
{
"models": [...],
"added": {
"name": "custom_model",
"filename": "custom_model.onnx",
"size_bytes": 12345678,
"modified_at": "2025-11-06T10:35:00Z"
}
}Delete a detection model by name.
Parameters:
model_name(path) - Model name (with or without .onnx extension)
Example:
curl -X DELETE http://localhost:8000/blur/models/custom_modelResponse:
{
"models": [...]
}Blur a single frame/image (for real-time preview).
Request:
- Content-Type:
multipart/form-data - Fields:
input(file, required) - Image fileconfig(string, optional) - JSON configuration overrides
Configuration Override Format:
The config field accepts a JSON string with nested configuration overrides:
{
"blur": {
"type": "pixelate",
"strength": 20
},
"detection": {
"confidence_threshold": 0.3,
"low_score_threshold": 0.1,
"disable_masks": false,
"single_pass": false,
"classes_to_blur": ["plate", "head"]
}
}Example using curl:
curl -X POST http://localhost:8000/blur/frame \
-F "input=@frame.jpg" \
-F 'config={"blur":{"type":"pixelate","strength":25}}'Response:
- Content-Type:
image/jpeg - Body: Blurred image as JPEG
Note: This endpoint automatically uses CPU processing if GPU is busy, making it suitable for real-time preview while video processing is running.
Blur an image file on the server's filesystem.
Request:
- Content-Type:
multipart/form-data - Fields:
input(string, required) - Path to input image file on serveroutput_path(string, required) - Path to save output image on serverconfig(string, optional) - JSON configuration overrides
Example using curl:
curl -X POST http://localhost:8000/blur/image_file \
-F "input=/path/to/input.jpg" \
-F "output_path=/path/to/output.jpg" \
-F 'config={}'Response:
{
"message": "Image blurred and saved successfully."
}Start asynchronous video processing job.
Request:
- Content-Type:
multipart/form-data - Fields:
input(file, required) - Video fileconfig(string, optional) - JSON configuration overridesoutput_filename(string, optional) - Desired output filename
Example using curl:
curl -X POST http://localhost:8000/blur/video_file \
-F "input=@dashcam.mp4" \
-F 'config={"blur":{"type":"pixelate"},"tracking":{"type":"botsort"}}' \
-F "output_filename=result.mp4"Response:
{
"job_id": "550e8400-e29b-41d4-a716-446655440000"
}Use the job_id to track progress and download results.
Get real-time progress updates for a video processing job (Server-Sent Events).
Parameters:
job_id(path) - Job ID returned from/blur/video_file
Response:
Stream of Server-Sent Events with JSON data:
data: {"job_id":"550e8400-...","progress":25,"message":"Detection: Processing frame 128/500","stage":"Detection","stage_message":"Processing frame 128/500","status":"running","error":null,"sequence":15,"updated_at":1699272345.123,"output_path":null}
data: {"job_id":"550e8400-...","progress":50,"message":"Tracking: Associating detections","stage":"Tracking","stage_message":"Associating detections","status":"running","error":null,"sequence":16,"updated_at":1699272350.456,"output_path":null}
data: {"job_id":"550e8400-...","progress":100,"message":"Blurring: Complete","stage":"Blurring","stage_message":"Complete","status":"done","error":null,"sequence":20,"updated_at":1699272360.789,"output_path":"/tmp/obscuro_jobs/session_abc123/550e8400-..._output.mp4"}
Job Status Values:
running- Job is processingdone- Job completed successfullyerror- Job failedcancelled- Job was cancelled
Example using curl:
curl -N http://localhost:8000/blur/video_progress/550e8400-e29b-41d4-a716-446655440000Example using JavaScript:
const eventSource = new EventSource('/blur/video_progress/550e8400-...');
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(`Progress: ${data.progress}%`);
console.log(`Status: ${data.status}`);
if (data.status === 'done') {
eventSource.close();
// Download the result
}
};Cancel a running video processing job.
Parameters:
job_id(path) - Job ID to cancel
Example:
curl -X POST http://localhost:8000/blur/cancel_video/550e8400-e29b-41d4-a716-446655440000Response:
{
"message": "Job cancelled successfully"
}Error Responses:
404 Not Found- Job ID not found400 Bad Request- Job cannot be cancelled (already completed)
Download the processed video result.
Parameters:
job_id(path) - Job ID
Response:
- Content-Type:
video/mp4 - Content-Disposition:
attachment; filename="blurred_video_{job_id}.mp4" - Body: Processed video file
Example:
curl -O -J http://localhost:8000/blur/download/550e8400-e29b-41d4-a716-446655440000Note: The job is automatically cleaned up after download, and the temporary files are deleted.
Error Responses:
404 Not Found- Job ID not found or output file missing400 Bad Request- Job not completed yet
Check API health and backend status.
Response:
{
"status": "ok",
"status_code": 0,
"execution_provider": "CUDAExecutionProvider",
"requested_providers": ["CUDAExecutionProvider", "CPUExecutionProvider"],
"active_providers": ["CUDAExecutionProvider", "CPUExecutionProvider"]
}Status Values:
ok(status_code: 0) - Backend is ready and GPU availabledegraded(status_code: 1) - Backend running but GPU not available (using CPU)busy(status_code: 1) - Backend is currently processingerror(status_code: -1) - Backend initialization error
Example:
curl http://localhost:8000/healthzMost processing endpoints accept a config parameter for runtime overrides. The configuration must be a nested JSON object matching the AnonymizerConfig structure:
{
"model": {
"name": "1280_nano"
},
"blur": {
"type": "pixelate",
"strength": 25
},
"detection": {
"confidence_threshold": 0.3,
"low_score_threshold": 0.1,
"batch_size": 8,
"use_sahi": true,
"inference_size": 2560,
"sahi_overlap_ratio": 0.25,
"disable_masks": false,
"single_pass": false,
"classes_to_blur": ["plate", "head"]
},
"tracking": {
"type": "botsort",
"use_offline_linker": true,
"params": {
"distance_gate": 0.1,
"max_misses_M": 8
}
},
"video": {
"codec": "h264",
"quality": 23
},
"debug": false,
"log_level": "INFO"
}You can override any subset of these values. Unspecified fields use the server's current configuration.
All endpoints return standard HTTP error codes:
400 Bad Request- Invalid input or configuration404 Not Found- Resource not found500 Internal Server Error- Processing error
Error response format:
{
"detail": "Error message describing the issue"
}The API does not currently implement rate limiting, but be aware that:
- Only one GPU-backed video job can run at a time
- Frame preview requests use CPU when GPU is busy
- Multiple concurrent requests may impact performance
The API allows cross-origin requests from any origin (*). Configure allow_origins in production for security.
import requests
import json
import time
API_BASE = "http://localhost:8000"
# 1. Check available models
response = requests.get(f"{API_BASE}/blur/config/options")
options = response.json()
print(f"Available models: {options['model']['available']}")
# 2. Upload a video and start processing
config = {
"blur": {"type": "pixelate", "strength": 20},
"tracking": {"type": "botsort"}
}
with open("dashcam.mp4", "rb") as f:
files = {"input": f}
data = {
"config": json.dumps(config),
"output_filename": "result.mp4"
}
response = requests.post(f"{API_BASE}/blur/video_file", files=files, data=data)
job_id = response.json()["job_id"]
print(f"Job ID: {job_id}")
# 3. Poll for progress
while True:
response = requests.get(f"{API_BASE}/blur/video_progress/{job_id}")
# Note: This is simplified; use SSE for real-time updates
time.sleep(2)
# Check if done (see SSE example above for proper implementation)
# 4. Download result
response = requests.get(f"{API_BASE}/blur/download/{job_id}")
with open("blurred_output.mp4", "wb") as f:
f.write(response.content)
print("Processing complete!")const API_BASE = 'http://localhost:8000';
async function processVideo(file: File) {
// 1. Upload and start processing
const formData = new FormData();
formData.append('input', file);
formData.append('config', JSON.stringify({
blur: { type: 'pixelate', strength: 20 },
tracking: { type: 'botsort' }
}));
const response = await fetch(`${API_BASE}/blur/video_file`, {
method: 'POST',
body: formData
});
const { job_id } = await response.json();
console.log(`Job ID: ${job_id}`);
// 2. Monitor progress with SSE
const eventSource = new EventSource(`${API_BASE}/blur/video_progress/${job_id}`);
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log(`Progress: ${data.progress}% - ${data.message}`);
if (data.status === 'done') {
eventSource.close();
downloadResult(job_id);
} else if (data.status === 'error') {
eventSource.close();
console.error(`Error: ${data.error}`);
}
};
}
async function downloadResult(job_id: string) {
const response = await fetch(`${API_BASE}/blur/download/${job_id}`);
const blob = await response.blob();
// Trigger download
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `blurred_${job_id}.mp4`;
a.click();
window.URL.revokeObjectURL(url);
}- Use
/blur/framefor real-time preview before processing full videos - Monitor
/healthzto check GPU availability before submitting jobs - Store uploaded models persistently using Docker volumes
- Use Server-Sent Events for real-time progress updates
- Cancel long-running jobs with
/blur/cancel_video/{job_id} - Adjust
batch_sizeandinference_sizefor optimal GPU utilization