Metadata-Version: 2.4
Name: fluidghost
Version: 2.0.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 does one thing: **spoof media with your API key**, over the **submit → poll →
download** loop. It handles a single photo, a video, or a whole carousel.

- Full API docs: <https://ghost.fluidvip.com/app/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. An API key is **spoof-only**: it can
submit jobs and read/download their results, and nothing else.

## Spoof a photo or video

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

## Settings

Provide exactly one recipe source (or bind a default preset to your key and omit all three):

| 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
