Metadata-Version: 2.4
Name: fluidghost
Version: 2.1.0
Summary: Official Python client for the FluidGhost API — spoof a photo, video, or carousel with your API key and get back distinct variants with device-authentic metadata.
Author: FluidGhost
License: MIT
Project-URL: Homepage, https://ghost.fluidvip.com
Project-URL: Documentation, https://ghost.fluidvip.com/app/docs/
Keywords: fluidghost,metadata,exif,image,video,uniquify,sdk
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Intended Audience :: Developers
Classifier: Topic :: Multimedia :: Graphics
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.25
Dynamic: license-file

# fluidghost

Official Python client for the [FluidGhost API](https://ghost.fluidvip.com).

FluidGhost turns one photo or video into a set of **distinct variants** — each a fresh,
standalone capture with its own metadata, its own pixels, and a device-authentic filename.
This SDK **spoofs media with your API key**, over the **submit → poll → download** loop
(a single photo, a video, or a whole carousel), and **inspects metadata** (see
[Inspect metadata](#inspect-metadata)).

- Full API docs: <https://ghost.fluidvip.com/docs/>
- Python **3.8+** (depends on `requests`).

## Install

```bash
pip install fluidghost
```

## Authentication

Mint an API key in the dashboard (**Settings → API keys** at <https://ghost.fluidvip.com>).
Keep it server-side — never ship it in client code. A secret API key can
submit jobs, read and download their results, and inspect metadata (management keys only provision).

## Spoof a photo or video

If your API key has a saved preset bound to it (pick it when you create the key in the dashboard),
the key already carries the settings — the whole call is just the image:

```python
job = fg.run(image="photo.jpg")
```

Otherwise name the settings on the call, as below. Note that `recipe_id`, `recipe_config`
and `preset_id` **override** the key's bound preset.

`run()` submits a job and waits for it to finish. Results are **one-time links**:
`download_variant()` streams the bytes and the object is deleted on a successful download
(a second call raises `FluidGhostError` with status `410`). Photos and videos both go
through the same call — the type is detected from the file.

```python
import os
from fluidghost import FluidGhost

fg = FluidGhost(api_key=os.environ["FLUIDGHOST_KEY"])

job = fg.run(
    image="photo.jpg",                 # or a video: "clip.mp4"
    recipe_id="full-refresh",
    copies=3,
    options={"noise": 35, "ultraSafe": True},
    on_poll=lambda j: print(j["status"], j.get("progress")),
)

for v in job["variants"]:
    if v["status"] != "completed":
        continue
    data = fg.download_variant(job["jobId"], v["index"])
    with open(v.get("fileName") or f"variant-{v['index']}.jpg", "wb") as f:
        f.write(data)
    print(v.get("fileName"), "risk", v.get("detectionRisk"), v.get("riskBand"))
```

You can pass in-memory bytes instead of a path with a `(filename, bytes)` tuple:

```python
fg.create_job(image=("photo.jpg", my_bytes), recipe_id="full-refresh")
```

## Spoof a carousel (matched set)

A carousel is a set of photos posted together. `spoof_carousel()` submits one linked job
per file — sharing a session and a running filename sequence so the outputs read as one
cohesive post — and waits for all of them.

```python
jobs = fg.spoof_carousel(
    ["slide1.jpg", "slide2.jpg", "slide3.jpg"],
    recipe_id="full-refresh",
)

for job in jobs:
    v = job["variants"][0]
    data = fg.download_variant(job["jobId"], v["index"])
    with open(v["fileName"], "wb") as f:
        f.write(data)
```

> For a fully cohesive set (the same device across every slide), pass a `recipe_config`
> or `preset_id` that pins a device — otherwise each slide picks its own device family.

## Inspect metadata

Four read-only calls read metadata without creating a job. They need the `inspect:read`
scope (secret API keys have it by default) and share one rate limit of 60 requests per
minute per owner (a `429` raises `FluidGhostError` with `retry_after`). Every file argument
takes a path or a `(filename, bytes)` tuple, like `run()`.

```python
# Metadata of an uploaded photo or video, grouped by container.  POST /api/inspect
meta = fg.inspect("photo.jpg")

# Deep forensic verdict (metadata, container, C2PA).  POST /api/inspect/forensics
# Fail-open: a missing engine returns {"status": "unavailable"} or {"status": "error"}.
report = fg.inspect_forensics("photo.jpg")

# Perceptual (Meta-PDQ) compare of an original and a variant.  POST /api/inspect/compare
cmp = fg.inspect_compare("original.jpg", "variant.jpg")
print(cmp["detectionRisk"], cmp["band"], cmp["matchThreshold"])

# Metadata of a finished variant, read server-side (does not consume the one-time
# download link).  GET /api/inspect/result/{jobId}/{index}
variant_meta = fg.inspect_result(job["jobId"], 0)
```

## Settings

Provide exactly one recipe source (or bind a default preset to your key and omit all three;
any of the three you do send overrides the key's preset):

| Argument | Meaning |
| --- | --- |
| `recipe_id` | A bundled starter, e.g. `"full-refresh"`. |
| `preset_id` | A saved preset created in the dashboard (e.g. `"sys:full-refresh"`). |
| `recipe_config` | A full recipe graph (advanced). |
| `copies` | Number of variants, 1–50. |
| `strength` | Global strength 0–100. |
| `options` | Extra knobs merged onto the recipe, e.g. `{"noise": 35, "ultraSafe": True}`. |

## Errors

Every non-2xx response raises `FluidGhostError`, mirroring the API's `{error, message, details}`:

```python
from fluidghost import FluidGhost, FluidGhostError

try:
    fg.run(image="photo.jpg", recipe_id="full-refresh", copies=3)
except FluidGhostError as e:
    print(e.status, e.code, e.details)
    if e.code == "rate_limited":
        time.sleep(e.retry_after or 5)
    if e.code == "insufficient_balance":
        ...  # top up in the dashboard
```

## Configuration

```python
FluidGhost(
    api_key="fg_sk_live_…",
    base_url="https://api-ghost.fluidvip.com",  # default; override for a proxy
    timeout=60.0,
)
```

## License

MIT
