Xyran V1
Fast, local-first content safety for Python.
Xyran is an offline image and animation moderation SDK with bundled Owen-S ONNX inference. After installation, inference requires no API key, cloud moderation service, telemetry, Hugging Face login, or model download.
Xyran 1.1 adds frame-aware animated GIF/WebP moderation, real ONNX batching, recursive folder scanning, and JSON/Markdown/TXT/CSV reports.
Install
pip install xyran
On Windows x64 and Linux x86_64, the package intentionally installs
onnxruntime-gpu[cuda,cudnn] so NVIDIA CUDA can work out of the box while the
same runtime can fall back to CPU. On macOS and non-x64 Windows/Linux targets,
the CPU ONNX Runtime package is selected.
ONNX Runtime's CPU and GPU Python distributions share the same import namespace. Use a clean virtual environment for the most predictable install.
Runtime conflict repair
If xyran doctor reports multiple ONNX Runtime distributions or a damaged
onnxruntime namespace, use Xyran's built-in repair command:
xyran repair-runtime
Xyran will:
- inspect the current Python interpreter/environment;
- remove all overlapping ORT distributions (
onnxruntime,onnxruntime-gpu, DirectML and OpenVINO variants); - reinstall exactly one Xyran-compatible runtime;
- verify the result in a fresh Python process;
- run the bundled model smoke test and dynamic-batch smoke test.
Preview the exact commands without changing anything:
xyran repair-runtime --dry-run
For CI/Docker/non-interactive environments:
xyran repair-runtime --yes
Force CPU runtime:
xyran repair-runtime --runtime cpu
Force the NVIDIA GPU runtime on supported Windows/Linux x64 systems:
xyran repair-runtime --runtime gpu
The repair command always uses the interpreter that is currently running
Xyran (sys.executable -m pip), so it does not silently repair a different
Python installation. Network/package-index access is required to reinstall ORT.
A clean virtual environment remains the recommended production deployment.
Static image usage
from xyran import Moderator
mod = Moderator() # model/session resident by default
result = mod.scan("image.jpg")
print(result.decision) # ALLOW / REVIEW / BLOCK
print(result.scores.sexual)
print(result.scores.graphic)
print(result.scores.safe)
Animated GIF / WebP
scan() now auto-detects multi-frame GIF/WebP and switches to frame-aware
moderation:
from xyran import Moderator, AnimationModerationResult
mod = Moderator()
result = mod.scan("animation.gif")
if isinstance(result, AnimationModerationResult):
print(result.decision)
print(result.processing.sampled_frames, result.processing.total_frames)
print(result.processing.exhaustive)
print(result.worst_frame.frame_index)
print(result.worst_frame.scores)
Explicit animation API:
result = mod.scan_animation(
"animation.webp",
sampling="smart", # smart | uniform | all
max_samples=32,
batch_size=16,
)
Smart sampling design
The default sampling="smart" is deterministic and combines:
- endpoints — always protects intro/outro coverage;
- duration-aware time coverage — samples by playback time rather than only frame number, so long-dwell frames receive appropriate representation;
- visual-change peaks — a cheap 48x48 RGB signature combines whole-frame change with extra weight on the most-changed local regions, improving sensitivity to smaller abrupt inserts;
- long-dwell frames — prioritizes frames visible for longer periods;
- coverage fill — fills any collisions with deterministic frame coverage.
If the animation has 32 frames or fewer, the default is automatically exhaustive. Longer animations are smart-sampled to at most 32 model-scanned frames by default.
Smart sampling reduces model inference cost, but it is not a mathematical guarantee that every unsafe frame is inspected. High-assurance workflows can request every frame:
result = mod.scan_animation("animation.gif", sampling="all")
CLI equivalent:
xyran scan-animation animation.gif --sampling all
Real frame batching
Selected frames are preprocessed independently, stacked into dynamic
[batch, 3, 224, 224] tensors, and sent through one ONNX Runtime call per
batch. The default frame batch size is 16.
The same real batching is available for static inputs:
results = mod.scan_batch(paths, batch_size=16)
Folder scanning
Scan a complete directory recursively:
from xyran import Moderator
mod = Moderator()
report = mod.scan_folder(
"./uploads",
recursive=True,
batch_size=16,
animation_sampling="smart",
animation_max_samples=32,
)
print(report.summary)
Write multiple report formats from the same scan result:
report.write_reports(
"./reports/xyran-report",
formats=("json", "md", "txt", "csv"),
)
This creates:
xyran-report.json
xyran-report.md
xyran-report.txt
xyran-report.csv
CLI:
xyran scan-folder ./uploads \
--output ./reports/xyran-report \
--report-formats json md txt csv
Windows CMD can use one line:
xyran scan-folder .\uploads --output .\reports\xyran-report --report-formats json md txt csv
Folder scanning:
- scans known raster-image extensions by default;
- recursively scans subdirectories by default;
- uses real ONNX batching for static images;
- uses frame batching inside each animated GIF/WebP;
- records per-file decode/runtime failures without silently dropping them;
- can optionally probe every regular file with
probe_unknown=True/--probe-unknownwhen filename extensions are untrusted.
The JSON report contains the full structured results, including sampled frame indices, selection reasons, per-frame scores, animation timing metadata, error details, provider/fallback status, and whether each animation scan was exhaustive.
Markdown/TXT provide human-readable summaries. CSV is intentionally flattened to one row per file; for animations it reports the worst sampled frame.
V1.1 defaults
preprocess BlurPad + Lanczos3
input 224 x 224 (generated internally)
tiling disabled
resident true
device auto
animation sampling smart
animation max samples 32
animation batch size 16
folder static batch 16
runtime ONNX Runtime
network after install not required
telemetry disabled
cloud API none
The default BlurPad + Lanczos3 preprocessing preserves source aspect ratio, places the fitted sharp image over a blurred full-canvas background, and sends a single 224x224 tensor per image/frame to Owen-S.
Optional alternate preprocessing:
mod = Moderator(preprocess="warp") # pyvips Warp + Linear
No tiling is used in V1.1 inference.
Inputs and image formats
scan() accepts:
- local file paths (
str/pathlib.Path) - encoded image bytes (
bytes/bytearray) PIL.Image.Image
Xyran identifies common image families from file content where possible rather than trusting only filename extensions. EXIF orientation is applied. Alpha is flattened onto white consistently before preprocessing.
Core raster formats:
| Format | Extensions | Xyran 1.1 policy |
|---|---|---|
| JPEG | .jpg, .jpeg, .jpe |
static supported |
| PNG | .png |
static supported; multi-frame/APNG remains fail-closed |
| WebP | .webp |
static + animated supported |
| BMP | .bmp, .dib |
static supported |
| TIFF | .tif, .tiff |
single-page only |
| GIF | .gif |
static + animated supported |
Extended raster formats are accepted when the installed decoder exposes the codec:
- HEIC / HEIF
- AVIF
- JPEG 2000
- ICO
Multi-page families other than GIF/WebP remain fail-closed in V1.1. SVG, PDF and PSD are intentionally outside the normal raster-image moderation contract.
Inspect the actual local decoder/runtime support:
xyran formats
xyran formats --json
Animation safety limits
Defaults protect against unexpectedly large or adversarial animations:
maximum source frames 5,000
maximum effective time 600,000 ms
maximum pixels/frame 80,000,000
These are hard decode/analysis limits, not policy thresholds. They can be
customized in scan_animation() when the caller explicitly needs larger input.
Model residency
Default:
mod = Moderator(resident=True)
The ONNX session is created once and remains resident until close()/unload().
This is recommended for servers, desktop apps, animation scanning and directory
batches.
Memory-sensitive mode:
mod = Moderator(resident=False)
Device selection
Moderator(device="auto") # default: CUDA when it really works, otherwise CPU
Moderator(device="cpu") # strict CPU
Moderator(device="cuda") # strict CUDA; raises if CUDA is unusable
device="auto" can fall back to CPU if CUDA is visible but unusable.
Policy
The bundled classifier produces:
NSFL -> graphic
NSFW -> sexual
SFW -> safe
Development defaults:
sexual REVIEW >= 0.35
sexual BLOCK >= 0.85
graphic REVIEW >= 0.35
graphic BLOCK >= 0.85
These thresholds are not universal safety policy. Real moderation policy is application-specific.
from xyran import Moderator, ModerationPolicy
policy = ModerationPolicy(
sexual_review=0.40,
sexual_block=0.90,
graphic_review=0.35,
graphic_block=0.85,
)
mod = Moderator(policy=policy)
For animations, each sampled frame receives an ordinary policy decision. The
animation decision is the worst sampled frame by severity. worst_frame.scores
is a real model probability vector. peak_scores is an aggregate convenience
field: sexual/graphic are maxima across sampled frames and safe is the minimum,
so peak_scores is not itself one probability distribution.
CLI
xyran doctor
xyran formats
xyran scan image.jpg
xyran scan animation.gif
xyran scan animation.webp --animation-sampling all
xyran scan-animation animation.gif --sampling smart --max-samples 32 --batch-size 16
xyran scan-folder ./uploads --output xyran-report --report-formats json md txt csv
Bundled model
Xyran V1 pins:
Repository: OwenElliott/image-safety-classifier-s
Source commit: eb8b0b203952b70db191e990217174af4af39767
File: onnx/image-safety-classifier-s.onnx
Size: 23,701,765 bytes
SHA256: fef443ed68ae25ed693b6fef9e456071692ed3963cff4168acb39c3de6f017e7
License metadata: MIT
See THIRD_PARTY_NOTICES.md and package xyran/third_party/.
Offline guarantee
After pip install xyran finishes successfully:
model download during inference: NO
API key: NO
cloud moderation API: NO
telemetry: NO
network required for inference: NO
Limitations
Xyran helps classify content-safety risk. It is not a guarantee that every
unsafe image/frame will be detected, nor that every flagged image/frame is
unsafe. Smart animation sampling is deliberately non-exhaustive on long
animations; use sampling="all" when exhaustive frame inspection is required.
For high-stakes moderation, use human review and dataset-specific evaluation.
Metadata
Release files for xyran 1.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| xyran-1.2.0.tar.gz | 21.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| xyran-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 42.2 MB
Release files / xyran-1.2.0.tar.gz
| Download URL | xyran-1.2.0.tar.gz |
|---|---|
| Size | 21.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9ea4b44e26c2a856bdbf80cd171dd3af48509eb2a95a6a3fe513b59af46d7d47
|
|
BLAKE2b-256 checksum How to use checksums |
bd4f6ba09be6af8fde2f3740ee3429f0069e1d0d4cd9100f572026a80b5ac54a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|
Release files / xyran-1.2.0-py3-none-any.whl
| Download URL | xyran-1.2.0-py3-none-any.whl |
|---|---|
| Size | 21.1 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
d50c7dac8091d0abc29db746e652fb370ed033d2a10fd393d6a5eda89fb556c1
|
|
BLAKE2b-256 checksum How to use checksums |
4ef199371dc140d519b8ad9f2047a3ce26d9e0ddb96febf1851e5a315ea3a6dd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|