Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,5 @@ GUARD_SPACE_ID=
#GUARD_ENV_FILE=.env.staging

# (OPTIONAL) Default media file to analyze
# Used only by scripts/create_activity.py
# Used only by scripts/create_prediction.py
#GUARD_MEDIA=path/to/photo.jpg
24 changes: 12 additions & 12 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The `docs` group is also excluded since it is only used for building documentati

To keep the repository organized, please use descriptive branch names based on the type of work you are doing:

* **Features:** `feature/activity-pagination` or `feat/share-expiry-filters`
* **Features:** `feature/prediction-pagination` or `feat/share-expiry-filters`
* **Bug Fixes:** `fix/presigned-upload-retry`
* **Documentation:** `docs/update-readme`

Expand Down Expand Up @@ -127,23 +127,23 @@ you are finished, running `uv sync` returns you to the default environment.
### Testing against a Live API

The test suite is fully mocked and never touches the network. To exercise the real lifecycle end-to-end, create an
activity. **This runs against production by default and spends real tokens** — one activity per run. You can point it
prediction. **This runs against production by default and spends real tokens** — one prediction per run. You can point it
elsewhere using the `GUARD_BASE_URL` environment variable.

```bash
export GUARD_API_KEY=... # a token_raw from POST /api/v1/tokens/
export GUARD_SPACE_ID=...

# see the space and the projected cost, creating nothing
uv run python scripts/create_activity.py path/to/photo.jpg --list-only
uv run python scripts/create_prediction.py path/to/photo.jpg --list-only

uv run python scripts/create_activity.py path/to/photo.jpg
uv run python scripts/create_prediction.py path/to/photo.jpg

# against a local dev server instead
GUARD_BASE_URL=http://localhost:8000 uv run python scripts/create_activity.py path/to/photo.jpg
GUARD_BASE_URL=http://localhost:8000 uv run python scripts/create_prediction.py path/to/photo.jpg

# on-device, which needs no space, no network and no tokens
uv run python scripts/create_activity.py path/to/photo.jpg --engine local
uv run python scripts/create_prediction.py path/to/photo.jpg --engine local
```

Credentials resolve the same way everywhere in this client: explicit arguments, followed by `GUARD_* `environment
Expand All @@ -157,15 +157,15 @@ If you do not have a `space_id` yet, four more scripts can help:
# list the spaces your key can see, with their ids
uv run python scripts/list_spaces.py

# the two ids a new space needs: a predictor, and optionally some tasks
# the two ids a new space needs: a predictor, and optionally some targets
uv run python scripts/list_predictors.py
uv run python scripts/list_tasks.py --predictor-id <uuid>
uv run python scripts/list_targets.py --predictor-id <uuid>

# walk predictors -> tasks -> create a space (--list-only creates nothing)
# walk predictors -> targets -> create a space (--list-only creates nothing)
uv run python scripts/create_space.py --list-only

# and then, with a space id in hand, analyze something in it
uv run python scripts/create_activity.py path/to/photo.jpg --list-only
uv run python scripts/create_prediction.py path/to/photo.jpg --list-only
```

## What to Watch Out For
Expand All @@ -190,8 +190,8 @@ exceptions do not subclass it, so `local.py` translates each one before it escap
that does not come from `guard_local` propagates untouched. This is intentional, as a bug in the engine should surface
exactly as the bug it is.

**The task labels are a public API.** `_adapt` derives each local result's id from `uuid5(namespace, label)`. Renaming a
label silently changes the ID that callers rely on. These labels also mirror the tasks the cloud API seeds, allowing
**The target labels are a public API.** `_adapt` derives each local result's id from `uuid5(namespace, label)`. Renaming a
label silently changes the ID that callers rely on. These labels also mirror the targets the cloud API seeds, allowing
users to test locally and then route to the cloud without changing their code.

**Engine scores must be floats between `0.0` and `1.0`.** The `_score_to_int` function rescales by value rather than by
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ and test locally without an API key. Both return the exact same strictly typed m
**⚡ Sync & Async Support:** Natively supports both synchronous operations and `async/await` out of the box, making it
a perfect fit for high-performance frameworks like FastAPI.

**🛠️ Comprehensive API Bindings:** Typed bindings for the entire API including spaces, activities, tasks, predictors,
**🛠️ Comprehensive API Bindings:** Typed bindings for the entire API including spaces, predictions, targets, predictors,
runners, shares, and reactions. We also include token estimation, so you can price a job before you spend on it.

## Installation
Expand Down Expand Up @@ -51,10 +51,10 @@ pip install "guard-client[local]"

## Quick Start

The `analyze()` method runs the entire detection lifecycle for you. It creates an activity, uploads the media, confirms
The `analyze()` method runs the entire detection lifecycle for you. It creates a prediction, uploads the media, confirms
the upload, polls until processing finishes, and returns the result.

You will need an **API key** and a space to create the activity in. You can create a new access token in the **Elhio
You will need an **API key** and a space to create the prediction in. You can create a new access token in the **Elhio
dashboard** (under Settings > Account Settings > Access Tokens). If you do not know your space ID yet, you can
ask the API for it. The `spaces.list()` method returns every space the key can see.

Expand All @@ -66,15 +66,15 @@ with GuardClient(api_key="your_api_key_here") as client:
# 1. Get an available space
space = client.spaces.list()[0]

# 2. Analyze the media (creates activity, uploads, and polls for results)
# 2. Analyze the media (creates prediction, uploads, and polls for results)
result = client.analyze("photo.jpg", space_id=space.id)

# 3. Print the detection results
for item in result.results:
print(f"{item.label}: {item.score}/100")

# AI-Generated: 87/100
# Violence: 2/100
# Violent: 2/100
# Explicit: 1/100
```

Expand Down Expand Up @@ -110,11 +110,11 @@ with GuardClient(engine="local") as client:
print(f"{item.label}: {item.score}/100")

# AI-Generated: 90/100
# Violence: 2/100
# Violent: 2/100
# Explicit: 1/100
```

Two fields easily tell the engines apart. The `result.activity_id` is `None` because a local run creates nothing
Two fields easily tell the engines apart. The `result.prediction_id` is `None` because a local run creates nothing
server-side, meaning there is nothing to share or react to. The `result.detected` field carries the engine's own
per-category threshold verdict, which the cloud API does not report. Reading it means you are opting into extra detail,
not into a different data shape.
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "guard-client"
version = "0.0.1"
version = "0.0.2"
description = "A Python client for seamlessly integrating visual safety filters into your applications"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -42,7 +42,7 @@ dependencies = [
]

[project.optional-dependencies]
local = ["guard-local-detector>=0.0.1"]
local = ["guard-local-detector>=0.0.2"]

[project.urls]
Homepage = "https://elhio.com/"
Expand Down
26 changes: 14 additions & 12 deletions scripts/create_activity.py → scripts/create_prediction.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
"""
Create one activity after showing the space and its estimated cost.
Create one prediction after showing the space and its estimated cost.

This script runs the complete detection lifecycle through `analyze()`. It creates the
activity, uploads the media, confirms it, polls until completion, and prints the final
prediction, uploads the media, confirms it, polls until completion, and prints the final
scores. Be aware that running this against a live API will spend real tokens. You can
pass `--list-only` to see the cost estimate without creating anything, or use
`--engine local` to run the detection on your device for free.
Expand All @@ -15,14 +15,14 @@
Example:
```bash
# preview the cost without creating anything
uv run python scripts/create_activity.py photo.jpg --list-only
uv run python scripts/create_prediction.py photo.jpg --list-only

# run the activity against the cloud API
uv run python scripts/create_activity.py photo.jpg
uv run python scripts/create_activity.py photo.jpg --space-id <uuid>
# run the prediction against the cloud API
uv run python scripts/create_prediction.py photo.jpg
uv run python scripts/create_prediction.py photo.jpg --space-id <uuid>

# run on-device instead, which requires no space and consumes no tokens
uv run python scripts/create_activity.py photo.jpg --engine local
uv run python scripts/create_prediction.py photo.jpg --engine local
```
"""

Expand All @@ -41,8 +41,10 @@ def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("media", nargs="?", help="media file; defaults to GUARD_MEDIA")
parser.add_argument("--space-id", help="the space to run in; or GUARD_SPACE_ID")
parser.add_argument("--user-id", help="create a user-owned activity")
parser.add_argument("--account-id", help="create a service-account-owned activity")
parser.add_argument("--user-id", help="create a user-owned prediction")
parser.add_argument(
"--account-id", help="create a service-account-owned prediction"
)
parser.add_argument(
"--engine", choices=["cloud", "local"], help="client default: cloud"
)
Expand Down Expand Up @@ -87,13 +89,13 @@ def print_media(path: Path) -> None:
def print_space(client: GuardClient, space_id: str, media: Path) -> None:
"""Show the space this will run in, and what it is expected to cost."""
space = client.spaces.get(space_id)
tasks = ", ".join(task.name for task in space.enabled_tasks) or "-"
targets = ", ".join(target.name for target in space.enabled_targets) or "-"

print(f"\nSpace: {space.name}")
print(f" id {space.id}")
print(f" predictor {space.predictor_name} (x{space.predictor_multiplier})")
print(f" max media {space.max_media_size or '-'} bytes")
print(f" tasks {tasks}")
print(f" targets {targets}")

print(f"\nEstimated cost: {client.estimate_tokens(media, space_id=space_id)}")
print(" An estimate: the API reserves the minimum, payed_tokens is final.")
Expand Down Expand Up @@ -133,7 +135,7 @@ def main() -> int:
)

print(f"\nDone: engine={result.engine.value}")
print(f" activity {result.activity_id or '-'}")
print(f" prediction {result.prediction_id or '-'}")
for item in result.results:
print(f" - {item.label}: {item.score}/100")
if not result.results:
Expand Down
30 changes: 15 additions & 15 deletions scripts/create_space.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#!/usr/bin/env python3
"""
Create a space after showing the available predictors and tasks.
Create a space after showing the available predictors and targets.

A space needs a predictor and optionally a set of tasks. This script walks through the
entire flow. It lists the predictors, lists the tasks for the chosen predictor, creates
the space, and prints the new space ID.
A space needs a predictor and optionally a set of targets. This script walks through the
entire flow. It lists the predictors, lists the targets for the chosen predictor,
creates the space, and prints the new space ID.

Only GUARD_API_KEY is required. Everything else resolves through the standard client
precedence using arguments, the environment, or a `.env` file.
Expand Down Expand Up @@ -44,14 +44,14 @@ def parse_args() -> argparse.Namespace:

parser.add_argument("--public", action="store_true", help="make the space public")
parser.add_argument(
"--all-tasks",
"--all-targets",
action="store_true",
help="enable every task the predictor supports (default: none)",
help="enable every target the predictor supports (default: none)",
)
parser.add_argument(
"--list-only",
action="store_true",
help="print predictors and tasks, then exit without creating anything",
help="print predictors and targets, then exit without creating anything",
)
return parser.parse_args()

Expand All @@ -74,12 +74,12 @@ def main() -> int:
print(f" {predictor.id} {predictor.name:28} media={media}")

predictor_id = args.predictor_id or str(predictors[0].id)
tasks = client.tasks.list(predictor_id=predictor_id)
targets = client.targets.list(predictor_id=predictor_id)

print(f"\nTasks for predictor {predictor_id}:")
for task in tasks:
print(f" {task.id} {task.name}")
if not tasks:
print(f"\nTargets for predictor {predictor_id}:")
for target in targets:
print(f" {target.id} {target.name}")
if not targets:
print(" (none)")

if args.list_only:
Expand All @@ -90,7 +90,7 @@ def main() -> int:
print("\nPass --name to create a space.", file=sys.stderr)
return 1

task_ids = [t.id for t in tasks] if args.all_tasks else None
target_ids = [t.id for t in targets] if args.all_targets else None
print(f"\nCreating space {args.name!r}...")

space = client.spaces.create(
Expand All @@ -100,13 +100,13 @@ def main() -> int:
is_public=args.public,
user_id=args.user_id,
organization_id=args.organization_id,
enabled_task_ids=task_ids,
enabled_target_ids=target_ids,
)

print(f"\nCreated: {space.id}")
print(f" name {space.name}")
print(f" public {space.is_public}")
print(f" tasks {', '.join(space.enabled_task_names) or '-'}")
print(f" targets {', '.join(space.enabled_target_names) or '-'}")
print(f"\nPut this in .env as GUARD_SPACE_ID={space.id}")
except GuardError as exc:
print(f"\nFAILED: {type(exc).__name__}: {exc}", file=sys.stderr)
Expand Down
12 changes: 6 additions & 6 deletions scripts/list_predictors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
required. Everything else resolves through the standard client precedence using
arguments, the environment, or a `.env` file.

The `--task-id` argument may be repeated. It narrows the results to predictors
supporting all of the given tasks. Run `scripts/list_tasks.py` to find those IDs.
The `--target-id` argument may be repeated. It narrows the results to predictors
supporting all of the given targets. Run `scripts/list_targets.py` to find those IDs.

Example:
```bash
uv run python scripts/list_predictors.py
uv run python scripts/list_predictors.py --sort-by created_at --sort-order desc
uv run python scripts/list_predictors.py --task-id <uuid> --task-id <uuid>
uv run python scripts/list_predictors.py --target-id <uuid> --target-id <uuid>
```
"""

Expand All @@ -35,10 +35,10 @@ def parse_args() -> argparse.Namespace:
)

parser.add_argument(
"--task-id",
"--target-id",
action="append",
metavar="UUID",
help="only predictors supporting this task; repeatable",
help="only predictors supporting this target; repeatable",
)
parser.add_argument(
"--sort-by", choices=["name", "created_at"], help="server default: name"
Expand All @@ -58,7 +58,7 @@ def main() -> int:
filters = {
"user_id": args.user_id,
"organization_id": args.organization_id,
"supported_task_ids": args.task_id,
"supported_target_ids": args.target_id,
"sort_by": args.sort_by,
"sort_order": args.sort_order,
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/list_spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"""
Print the spaces available to your API key along with their IDs.

Creating an activity requires a `space_id`. This script helps you find one to place in
Creating a prediction requires a `space_id`. This script helps you find one to place in
your `.env` file as `GUARD_SPACE_ID`. Only the `GUARD_API_KEY` is required. Everything
else resolves through the standard client precedence using arguments, the environment,
or a `.env` file.
Expand Down
Loading
Loading