Metadata-Version: 2.4
Name: fcaai
Version: 0.4.0
Summary: Official Python client for the fcaai API — image, video, audio, 3D and chat generation behind one key.
Author: FCA Studio LLC
License-Expression: MIT
Project-URL: Homepage, https://fcaai.ai
Project-URL: Documentation, https://fcaai.ai/docs
Project-URL: Source, https://fcaai.ai/developers#sdk
Project-URL: Changelog, https://fcaai.ai/developers#sdk
Keywords: fcaai,ai,api,sdk,image-generation,video-generation,text-to-speech,3d
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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 :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# fcaai (Python)

Official Python client for the [fcaai API](https://api.fcaai.ai/docs). Standard library only (no `requests`/`httpx`), Python 3.9+. All prices in USD.

```bash
pip install fcaai
export FCAAI_API_KEY=fcaai_live_…
```

```python
from fcaai import Fcaai

client = Fcaai()  # reads FCAAI_API_KEY

print(client.models.get("google/nano-banana-pro", resolution="2k")["estimate"]["price_display"])

gen = client.generations.create("google/nano-banana-pro", {"prompt": "a paper plane on a wooden desk", "aspect_ratio": "16:9"})
gen.wait()                       # long-polls /v1/generations/{id}?wait=60
print(gen.status, gen.urls, gen.price_usd)

# Files: ≤ 50 MB in one request, bigger ones chunked (8 MB, 3 parallel, resumable with session_id=)
img = client.uploads.create("room.jpg", "image/jpeg")
video = client.uploads.create_chunked("walkthrough.mp4", mime="video/mp4", on_progress=lambda s, n: print(s * 100 // n, "%"))

clip = client.generations.create("fca/magic-v3", {"image": img["url"], "prompt": "slow dolly in", "duration": 5}, wait=600)
```

## Audio, 3D, 4K images and templates (0.2)

```python
# Text to speech (MP3). Voices: [v.id for v in client.models.voices("minimax/speech-2.8")] → narrator, calm, …
clip = client.audio.speech("Welcome to the studio.", voice="narrator", wait=True)

# Sound effects: duration in seconds, seamless loop
rain = client.audio.sfx("rain on a tin roof", duration=6, loop=True, wait=True)

# One object photo → GLB (takes minutes)
chair = client.model3d.from_image(img["url"], detail="detailed", surface="textured", wait=1800)

# 4K on local models (2K render + detail upscaler), up to 4 outputs
shots = client.images.create("glass villa, blue hour", model="fca/magic-pro", resolution="4k", count=2, wait=True)
for o in shots.outputs:          # Output dataclass: url, kind, mime, width, height, thumb_url, card_url, poster_url
    print(o.url, o.thumb_url)

# Templates
tpl = client.templates.list()[0]
print([(i.key, i.type, i.required) for i in tpl.inputs])
run = client.templates.run(tpl.id, {"photo": img["url"]}, wait=True)
```

## Canvas: timelines, farm, community, approvals, plans, panoramas, video edits (0.3)

```python
# Scene timeline: storyboard → cheap previews → approve → full quality → one MP4
tl = client.timelines.create([{"prompt": "Lina walks into the empty gallery at dawn", "duration": 5}],
                             {"model": "bytedance/seedance-2.0-fast", "imageModel": "fca/magic-pro", "aspect": "16:9"}, preview=True)
client.timelines.wait(tl["id"]); client.timelines.approve(tl["id"]); client.timelines.final(tl["id"])
client.timelines.wait(tl["id"], timeout=3600); client.timelines.cut(tl["id"])
print(client.timelines.wait(tl["id"])["cut"]["url"])

# Render farm (scenes sent from 3ds Max with the FCA plugin; FcaaiError not_enabled when the farm is off)
for j in client.farm.jobs.list(status="active"):
    print(j["id"], j["status"], j["progress"])
client.farm.jobs.pause("fj_…")    # the reserved amount stays on hold; paused time is not charged
client.farm.jobs.resume("fj_…")

page = client.community.list(type="flow", category="architecture", sort="popular")
res = client.approvals.list("<board-id>", status="pending")
client.approvals.decide("<board-id>", "<request-id>", "<option-id>", "changes", note="Warmer light, please.")
plan = client.plans.read(img["url"])            # billed per read, never retried
pano = client.panoramas.finish("https://…/pano.png")  # free
edit = client.video_edits.create([{"url": "https://fcaai.ai/o/…/clip-1.mp4", "start": 0.5, "end": 4}], transition="fade", wait=True)
print(edit.urls[0])
```

## Voices, multi-step templates, elements, style kits, reseller (0.4)

```python
# Clone your own voice (free; speech is billed as usual). Consent is required.
info = client.voices.list()
me = client.voices.clone(["take1.mp3", "take2.mp3"], name="My voice", consent=True,
                         consent_version=info["clone"]["consent_version"])
client.audio.speech("Hello from my own voice.", voice=me["id"], model="<speech-model-with-cloned-voices>", wait=True)
styles = client.models.voice_styles(locale="en")    # ready-made speaking styles → styles["param"]

# Multi-step templates (Template.is_package) run as one template run
tpl = client.templates.get("<package-slug>")
run = client.templates.run_package(tpl.id, {"photo": img["url"]}, wait=True)
print(run["status"], run["price_usd"], [o["url"] for o in run["outputs"]])
# client.templates.get_run(run_id, wait=30) · client.templates.cancel_run(run_id)

# Single-step template on another allowed model, with a style kit, filed into an Architecture Studio project
kit = client.style_kits.list()[0]
g = client.templates.run("<slug>", {"photo": img["url"]}, model=tpl.models[-1]["id"], brand_kit_id=kit["id"], project="<project-id>")

# Element library (characters, products, places, materials) → references added server-side
lina = client.elements.list(kind="character", q="lina")["data"][0]
client.generations.create("fca/magic-pro", {"prompt": "@lina reading in a cafe", "element_ids": [lina["id"]]},
                          brand_kit_id=kit["id"], external_user_id="customer-42")

# Reseller keys (scope reseller:manage)
acme = client.reseller.subtenants.create("Acme Studio", retail_markup=1.5)
key = client.reseller.subtenants.create_key(acme["id"], "server", mode="live")   # full key shown only here
print(client.reseller.usage(from_="2026-10-01", to="2026-10-31")["totals"])
```

`images.generate(prompt, size=…, n=…)` stays OpenAI-compatible (returns a dict); passing `resolution`, `count`, `aspect_ratio`, `images` or `wait` makes it a native generation.

Also: `generations.get/list(status=, limit=)/cancel`, `account.get/balance/usage(from_=, to=)`, `uploads.create_session/get_session/put_chunk/complete_session/delete_session` (low-level resumable upload), `webhooks.list/create/delete/deliveries`, `chat.completions.create(messages)` (OpenAI schema, `fca/chat`), `images.generate(prompt, size=…)`.

Errors raise `fcaai.FcaaiError` (`status`, `type`, `code`, `message`, `request_id`, `doc_url`, `retry_after`, `body`). 429/5xx/network errors are retried (`max_retries=2`) honoring `Retry-After`. `create()` always sends an `Idempotency-Key`, so retries never double-charge.

## Webhooks

```python
from fcaai import verify_webhook_signature

ok = verify_webhook_signature(secret, request.headers["X-FCAAI-Signature"], request.get_data(), tolerance_sec=300)
```

## Changelog

Full history in `CHANGELOG.md` (included in the source distribution). **0.4.0** — full `/v1` parity: multi-step template runs, voice cloning + voice styles,
elements, style kits, reseller sub-tenants/keys/usage, `brand_kit_id` / `external_user_id` on generations, typed package (`py.typed`).

## Tests

```bash
python -m pytest -q        # or: python -m unittest
```

License: MIT © 2026 FCA Studio LLC.
