Skip to content

nuretadream/nureta-python-sdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

nureta — Python SDK for the Nureta Generation API

Python client for the Nureta ("tokenstore") Generation API — a Seedance-compatible platform for generating video, images, and scripts from text and reference images, with first-class support for explicit / NSFW content.

  • Website & dashboard: https://developer.nureta.ai/
  • Base URL: https://developer.nureta.ai
  • Auth: API key as a bearer token (Authorization: Bearer sk-...)
  • Billing: pay-per-generation against your balance (USDC top-ups on Base); failed jobs are auto-refunded
  • Async by design: you create a task, then poll it (or receive a webhook) until it's succeeded/failed

Generated from openapi.yml with openapi-generator (-g python). Regenerate with the command at the bottom.


Install

pip install -e .            # from this repo
# runtime deps: urllib3, pydantic v2, python-dateutil, typing-extensions

Authenticate

import nureta

client = nureta.Nureta(api_key="sk-...")   # base_url defaults to https://developer.nureta.ai
# ... or pass base_url="https://staging..." to point elsewhere

Create an API key in the API keys section of the dashboard. New keys default to 10 req/s (adjustable 1–100). Every call — task creation, polling, uploads — counts toward that key's rate limit; a 429 RateLimitExceeded means back off and retry.


What this SDK can do

Capability Resource Endpoint Output
Video generation VideosApi POST /api/v3/contents/generations/tasks content.video_url
Image generation ImagesApi POST /api/v3/images/generations/tasks content.image_url
Script (storyboard) ScriptsApi POST /api/v3/contents/scripts content.script
Reference-image upload UploadsApi POST /api/v3/contents/uploads signed uploadUrl + assetUrl
Models & pricing ModelsApi / ImagesApi / ScriptsApi GET .../models model list

The video path also supports multi-shot "scenes" (16–180s, stitched), an editable storyboard workflow (draft → tweak → render), and per-shot explicit control (see NSFW below).


Quickstart — generate a video

import time
import nureta

with nureta.Nureta(api_key="sk-...") as client:      # base_url defaults to production
    created = client.videos.create(nureta.CreateVideoTaskParams.from_dict({
        "model": "seahorse-1080p",
        "content": [
            {"type": "text", "text": "a seahorse dancing through neon waves"},
            # optional reference / start / end frames:
            # {"type": "image_url", "image_url": {"url": "https://.../ref.jpg"}},
        ],
        "ratio": "16:9",
        "duration": 8,          # single clip: 5/8/10/12/15s
    }))

    task_id = created.id        # "cgt-..."
    while True:
        task = client.videos.retrieve(task_id)
        if task.status in ("succeeded", "failed"):
            break
        # task.step.code = queued | generating_script | rendering | stitching | ...
        time.sleep(3)

    if task.status == "succeeded":
        print(task.content.video_url)
    else:
        print("failed:", task.error.message)   # auto-refunded

client.videos.create(...) is the openai-style short alias; the generated client.videos.create_video_task(...) name works too.

Params: pass a dict or a model

Every create / edit_* method accepts a plain dict / TypedDict or the pydantic model — pick your style:

from nureta.types import CreateVideoTaskParamsDict   # typed dict, for IDE autocomplete

params: CreateVideoTaskParamsDict = {"model": "seahorse-1080p",
                                     "content": [{"type": "text", "text": "..."}]}
client.videos.create(params)                                   # dict
client.videos.create(nureta.CreateVideoTaskParams.from_dict(params))  # model — same result

Retries & timeouts

Both clients retry with exponential backoff and take a default timeout, configured once on the client (openai-style):

client = nureta.Nureta(api_key="sk-...", max_retries=2, timeout=60)

Retries fire on 429 and 5xx — but only for idempotent methods (GET polling, PUT edits), so a billed create/render POST is never silently re-submitted. A numeric Retry-After header is honored. Set max_retries=0 to disable.

Async

nureta.AsyncNureta mirrors Nureta exactly — same methods and types, just await each call (httpx under the hood):

import asyncio, nureta

async def main():
    async with nureta.AsyncNureta(api_key="sk-...") as client:
        created = await client.videos.create({"model": "seahorse-1080p",
                                              "content": [{"type": "text", "text": "..."}]})
        while True:
            task = await client.videos.retrieve(created.id)
            if task.status in ("succeeded", "failed"):
                break
            await asyncio.sleep(3)
        print(task.content.video_url)

asyncio.run(main())

Content items

content is a list mixing exactly one text prompt with up to 9 image_url references and at most one audio_url:

content = [
    {"type": "text", "text": "cinematic, moody lighting"},
    {"type": "image_url", "image_url": {"url": "https://.../ref.jpg"}},
    {"type": "image_url", "image_url": {"url": "https://.../start.jpg"}, "role": "first_frame"},
    {"type": "image_url", "image_url": {"url": "https://.../end.jpg"},   "role": "last_frame"},
]

A pinned first_frame/last_frame is mutually exclusive with reference images — send a frame or references, not both.

Don't host your reference yet? Presign an upload:

signed = client.uploads.create(nureta.CreateUploadParams(file_name="ref.jpg", content_type="image/jpeg"))
# PUT the bytes to signed.upload_url with the same Content-Type, then use signed.asset_url as image_url.url

Images

created = client.images.create(nureta.CreateImageTaskParams.from_dict({
    "model": "seahorse-image",
    "content": [{"type": "text", "text": "a clean studio product shot on marble"}],
    "size": "4K",                 # 1K / 2K / 4K
}))
task = client.images.retrieve(created.id)   # poll → task.content.image_url

Flat-priced: one image, one charge. Same content shape as video (text prompt + up to 9 refs; a text-only prompt is allowed).

Scripts

Generate just a storyboard (title, characters, per-scene beats) — the same v3 director as video, stopped before rendering. Flat $0.01 regardless of length.

created = client.scripts.create(nureta.CreateScriptTaskParams.from_dict({
    "model": "seahorse-script",
    "content": [{"type": "text", "text": "a lonely lighthouse keeper finds a message in a bottle"}],
    "duration": 30,               # 5–180s; scales scene count, not price
}))
script = client.scripts.retrieve(created.id).content.script
# script.title, script.characters, script.scenes[].{setting, beat, duration_seconds}

NSFW / explicit content

Nureta is built to render explicit adult video, not just soft/suggestive motion. The underlying model (Seedance) has no built-in knowledge of sex acts or anatomy, so explicit shots are routed through a separate anatomy-reference pipeline that attaches real anatomy references at render time. You opt into that pipeline per shot with the explicit flag.

Best model for NSFW

seahorse-1080p — the flagship. All three video models (seahorse-480p / 720p / 1080p) support the explicit pipeline, but 1080p gives the highest-fidelity anatomy and detail, which is exactly where explicit content lives or dies. Use 720p to cut cost during iteration, then final-render at 1080p. For explicit stills, use seahorse-image at 4K.

How to actually get explicit output (this matters)

Explicit is a per-segment control, so it applies to multi-shot scenes — i.e. either advanced mode (segments) or edit mode, both video-only with a total duration > 15s.

  • explicit: truethis shot depicts sex. You must describe the act concretely in the shot's script/beat (positions, penetration, what each body is doing). A vague line like "they get closer" on an explicit shot attaches no references and renders soft.
  • explicit: false — build-up / story shot (a look, a touch, undressing, afterglow). Rendered from text only; no anatomy references (and you don't want sex bleeding into a build-up shot).

Mark explicit: true only on the shots that should actually show sex, and make those shots' text spell out the act.

Advanced mode — bring your own explicit storyboard

created = client.videos.create(nureta.CreateVideoTaskParams.from_dict({
    "model": "seahorse-1080p",
    "ratio": "9:16",                       # vertical
    "content": [{"type": "text", "text": "a moonlit rooftop encounter"}],
    "segments": [                          # must total 16–180s; per-segment 4–15s
        {"script": "she steps onto the rooftop, city lights behind her", "duration_seconds": 8},
        {"script": "he joins her at the railing; they kiss", "duration_seconds": 10, "explicit": False},
        {"script": "<concrete description of the act>", "duration_seconds": 12, "explicit": True},
    ],
}))
# poll created.id to succeeded → content.video_url

Edit mode — review the storyboard before you spend on the full render

Submit with mode: "edit"; the task charges at submit and parks at status: "planned" with a scene draft (the AI director sets explicit per shot for you). Then:

  • client.videos.edit_plan(task_id, EditPlanParams(...)) — tweak a shot's script, duration_seconds, or pin a first_frame/last_frame (matched by 1-based index).
  • client.videos.edit_plan_structured(task_id, EditPlanStructuredParams(...)) — edit each shot's short beat (and override explicit) and let the server re-expand it; top-level world / wardrobe / character set a shared style. Returns 202; poll until scene.reexpand_status == "ready".
  • client.videos.render(task_id) — commit to the full render.
  • client.videos.cancel(task_id) — cancel a not-yet-rendered draft for an immediate full refund (otherwise an un-rendered draft auto-refunds after 24h).
created = client.videos.create(nureta.CreateVideoTaskParams.from_dict({
    "model": "seahorse-1080p", "duration": 60, "mode": "edit",
    "content": [{"type": "text", "text": "a slow bedroom scene"}],
}))
# ... poll created.id to status "planned", inspect task.scene.segments, edit, then:
client.videos.render(created.id)

Legal/safety: explicit generation is for adult (18+) content you are authorized to create. callback_urls must be HTTPS and must not target private/loopback ranges.


Task lifecycle & progress

Every video task carries a step so you can show real progress while status is still pending:

task.step.code        # queued | generating_script | generating_visuals | pending_rendering
                      # | rendering | stitching | done | failed | cancelled | reexpanding | reexpand_failed
task.step.done, task.step.total   # clips completed / total (while rendering)

status is one of pending | planned | succeeded | failed | cancelled. The step lags the true phase by up to ~30s.

Callbacks

Pass callback_url (and optional callback_secret) on creation and Nureta POSTs the terminal task object to you when it finishes — same JSON as retrieve_*. Delivery is at-least-once (dedupe by id); when a secret is set, verify the X-Tokenstore-Signature HMAC-SHA256 over timestamp + "." + rawBody. Webhook receiving is your web server's job, not this SDK's.

Errors

API errors raise nureta.exceptions.ApiException subclasses; the JSON body carries a code:

AuthenticationError · InvalidParameter · ModelNotFound · AccountOverdueError · RateLimitExceeded (429) · InternalServiceError (generation failed — you're refunded).

from nureta.exceptions import ApiException
try:
    client.videos.create(params)
except ApiException as e:
    print(e.status, e.body)

Models & pricing

Prices are live from the public model endpoints (no auth):

for m in client.models.list().models:
    print(m.id, m.resolution, m.price_usd_per_sec)
# image / script model lists: client.images.list_models(), client.scripts.list_models()
Model Resolution USD/sec USD/5s
seahorse-480p 480p $0.1888 $0.9440
seahorse-720p 720p $0.2773 $1.3864
seahorse-1080p 1080p $0.5428 $2.7139
seahorse-image 1K/2K/4K $0.05 / image
seahorse-script $0.01 / script

Video price scales linearly with duration. Legacy seahorse-fast-<res> ids still work as aliases.


Repo layout & regeneration

src/nureta/
  _base.py          shared base layer: config, retries/timeout policy, param coercion
  client.py         hand-written sync client (Nureta)
  _async_client.py  hand-written async client (AsyncNureta) — reuses generated serialize/deserialize
  types.py          TypedDict request params
  api/ models/ ...  generated from openapi.yml (low-level transport: api_client.py, rest.py)
openapi.yml         API spec — source of truth
api.md              resource/method reference
scripts/generate.sh regenerates src/nureta/ (preserves the 4 hand-written files)
tests/              offline tests (incl. async + retries)

Regenerate after editing the spec:

./scripts/generate.sh        # needs: brew install openapi-generator

It generates into a temp dir and copies only the package into src/nureta/, so the repo root stays clean and the hand-written client.py is preserved.

About

official python sdk for nureta APIs https://developer.nureta.ai/

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors