Metadata-Version: 2.4
Name: xyran
Version: 1.2.0
Summary: Fast, local-first content safety SDK with bundled Owen-S ONNX inference.
Author: Xyran Contributors
License-Expression: Apache-2.0
Keywords: moderation,nsfw,content-safety,onnx,offline,image-classification,gif,webp,batch,folder-scan,runtime-repair
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: THIRD_PARTY_NOTICES.md
License-File: src/xyran/third_party/OWEN_MODEL_MIT_LICENSE.txt
Requires-Dist: numpy<3,>=1.26
Requires-Dist: Pillow<13,>=10.4
Requires-Dist: pyvips[binary]<4,>=3.2
Requires-Dist: onnxruntime-gpu[cuda,cudnn]<2,>=1.29; (sys_platform == "win32" and platform_machine == "AMD64") or (sys_platform == "linux" and platform_machine == "x86_64")
Requires-Dist: onnxruntime<2,>=1.29; sys_platform == "darwin" or (sys_platform == "win32" and platform_machine != "AMD64") or (sys_platform == "linux" and platform_machine != "x86_64")
Dynamic: license-file

# 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

```bash
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:

```bash
xyran repair-runtime
```

Xyran will:

1. inspect the **current Python interpreter/environment**;
2. remove all overlapping ORT distributions (`onnxruntime`,
   `onnxruntime-gpu`, DirectML and OpenVINO variants);
3. reinstall exactly one Xyran-compatible runtime;
4. verify the result in a **fresh Python process**;
5. run the bundled model smoke test and dynamic-batch smoke test.

Preview the exact commands without changing anything:

```bash
xyran repair-runtime --dry-run
```

For CI/Docker/non-interactive environments:

```bash
xyran repair-runtime --yes
```

Force CPU runtime:

```bash
xyran repair-runtime --runtime cpu
```

Force the NVIDIA GPU runtime on supported Windows/Linux x64 systems:

```bash
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

```python
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:

```python
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:

```python
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:

1. **endpoints** — always protects intro/outro coverage;
2. **duration-aware time coverage** — samples by playback time rather than only
   frame number, so long-dwell frames receive appropriate representation;
3. **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;
4. **long-dwell frames** — prioritizes frames visible for longer periods;
5. **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:

```python
result = mod.scan_animation("animation.gif", sampling="all")
```

CLI equivalent:

```bash
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:

```python
results = mod.scan_batch(paths, batch_size=16)
```

## Folder scanning

Scan a complete directory recursively:

```python
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:

```python
report.write_reports(
    "./reports/xyran-report",
    formats=("json", "md", "txt", "csv"),
)
```

This creates:

```text
xyran-report.json
xyran-report.md
xyran-report.txt
xyran-report.csv
```

CLI:

```bash
xyran scan-folder ./uploads \
  --output ./reports/xyran-report \
  --report-formats json md txt csv
```

Windows CMD can use one line:

```bat
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-unknown` when 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

```text
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:

```python
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:

```bash
xyran formats
xyran formats --json
```

## Animation safety limits

Defaults protect against unexpectedly large or adversarial animations:

```text
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:

```python
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:

```python
mod = Moderator(resident=False)
```

## Device selection

```python
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:

```text
NSFL -> graphic
NSFW -> sexual
SFW  -> safe
```

Development defaults:

```text
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.

```python
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

```bash
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:

```text
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:

```text
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.
