Generating - 2DAICommunity/2dai-cloud-sdk GitHub Wiki

Generating

All generation goes through an async queue. By default the SDK polls to completion and resolves the finished creation; pass { wait: false } to get the queue ticket immediately.

Images

const gen = await client.generate.image({
  prompt: 'a neon city street after rain, cinematic',
  aspectRatio: '16:9',
  // style + quality default to 'auto' β€” the server picks by your tier
});
// gen: { creationId, cdnId, downloadUrl, width, height, costUsd, queueId, ... }

Parameters (ImageParams):

Field Notes
prompt required
imageModel 'default' (Gen 7.2, default) or 'next' β€” Gen8 Preview images, see below
style style id, or 'auto' (default)
quality quality id, or 'auto' (default). Recommended: 'max' (best balance of detail and price); 'ultimate' for the highest resolution. ultra / ultimate are tier-gated β€” client.me() lists the presets your account may submit
aspectRatio '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3'
width / height explicit dims (override aspectRatio)
negativePrompt things to avoid β€” optional; applied by Gen8 Preview images, the Gen7 image engine renders without it
enhanced rewrite the prompt with TIXI first β€” see Prompt enhancement
allowNSFW opt in where your account permits
refCreationIds soft-conditioning references: up to 3, up to 6 with imageModel: 'next' (strict β€” TOO_MANY_REFS); each adds a surcharge

Gen8 Preview images

imageModel: 'next' runs Gen8 Preview images, the preview of 2DAI's next image engine. It keeps up to 6 refCreationIds natively (characters, products, places that stay recognisable), renders legible text in the picture and honours long, literal prompts β€” describe the scene plainly rather than in keyword lists, and refer to each reference by what it shows ("the singer with the red braids"), never by its position ("the second reference"). The quality ids are the same (fast … ultimate; auto picks between high and max), each preset priced on render time (roughly four to seven times the Gen 7.2 price), and every reference adds a surcharge on both models β€” costUsd on the ticket is the truth. The channel opens at Holder: read me().canUseImageNext and me().qualities.imageNext (same ids, allowed resolved for the account and the key's channel policy) instead of guessing; below the gate the submit is a 403 TOOL_NOT_ALLOWED_FOR_TIER (tool: 'image-next', with the tier that opens it), a preset above the tier a 403 QUALITY_NOT_ALLOWED_FOR_TIER, a key whose policy stops at default a 403 KEY_POLICY_CHANNEL, and an engine that is off a 400 IMAGE_MODEL_UNAVAILABLE β€” all before any charge. Finished creations are stamped aiModel: 'gen8.0' with imageModel: 'next'; generate.similar on such a row re-runs it on Gen8 Preview images. The other image tools (imageWithRefs, artisticStyle, wallpaper) stay on Gen 7.2 for now.

const me = await client.me();
if (me.canUseImageNext) {
  const poster = await client.generate.image({
    imageModel: 'next',
    prompt: 'a concert poster titled "NEON TIDE": the singer with the red braids on the neon-lit festival stage, bold white typography, rain and neon',
    refCreationIds: [singer.creationId, venue.creationId],   // ≀6, kept recognisable
    quality: 'max',
    aspectRatio: '2:3',
  });
}

Prompt enhancement (TIXI)

enhanced: true sends your prompt through TIXI, the 2DAI prompt agent, before it reaches the image model. TIXI is an LLM that expands a short brief into a full image-gen prompt: camera, lens and lighting, composition and framing, style anchoring, and β€” importantly β€” it groups related details together, because scattering them through the prompt is what makes image models produce anatomy errors.

// "a warrior" comes back as a paragraph describing a specific scene
const gen = await client.generate.image({ prompt: 'a warrior', enhanced: true });
console.log(gen.prompt);            // still "a warrior" β€” what you sent
console.log(gen.raw?.finalPrompt);  // what TIXI wrote

It costs about 2 s of extra latency and nothing in credit. Reach for it on short or vague briefs; leave it off when you have already written a precise prompt and want it honoured verbatim.

Worth knowing: enhanced is not a global switch. character-ref and wallpaper-resize always run TIXI β€” their directives are built around it, so the flag is redundant there. On generate.video it only matters with style: 'raw': every other video style (and 'auto') runs TIXI anyway, because the style's guidance only exists through that rewrite (see Video).

Aspect ratios

The aspectRatio sugar maps to sensible ~1MP dimensions (all multiples of 16). Pass explicit width/height if you need exact control.

Content rating

Every output is rated by the platform's classifier and the creation carries the result: nsfwRate (one of 0, 0.6, 0.8, 0.99, 1) and nsfwFlagged (true from 0.8 up).

nsfwRate Tier What it means for the creation
0 / 0.6 SFW / Suggestive nothing changes
0.8 / 0.99 Near-nude / Adult kept and charged as usual, but masked in the owner's drive until they reveal it, excluded from public feeds and folder previews, and refused by creations.publish (NSFW_NOT_PUBLISHABLE)
1 Prohibited discarded before it is stored β€” the generation fails with NSFW_MAX_EXCEEDED and nothing is charged

A flagged result is not an error: the job completes, creations.get returns the row, and viewUrl opens it in the owner's drive for review. Show that to your user instead of retrying. The rating follows the request as much as the picture: a prompt the platform judges explicit makes the result explicit even when the render looks tame. A generation that ends in NSFW_MAX_EXCEEDED is the only case where a reworded prompt is the way forward.

Reference tools

imageWithRefs covers the ref-driven tools. All three render on the Gen8 image engine, which reads every reference natively (up to 6 per tool):

// character-ref: keep the same character across a new scene β€” 1–6 refs
// (face shots, full-body shots or a character sheet); face photos lock the
// exact same face, the prompt drives outfit, pose and scene
await client.generate.imageWithRefs({
  tool: 'character-ref',
  prompt: 'as an astronaut on the moon',
  refCreationIds: [gen.creationId],
});

// style-transfer: refs are the STYLE source
await client.generate.imageWithRefs({
  tool: 'style-transfer',
  prompt: 'a fox',
  refCreationIds: [styleImageId],
  extractionDirective: 'style', // optional, style-transfer only
});

// smart-edit: refs[0] is the image to EDIT, the prompt is the instruction
await client.generate.imageWithRefs({
  tool: 'smart-edit',
  prompt: 'remove the crowd in the background, keep the lighting',
  refCreationIds: [gen.creationId],   // + up to 5 supporting refs
});

tool is one of 'character-ref', 'style-transfer', 'smart-edit' ('face-ref' was merged into 'character-ref' in 3.0.0 and is now a 400 INVALID_TOOL). Some tools are tier-gated β€” a TierError tells you if your tier can't use one.

Smart edit edits an existing image in place of generating a fresh one: refCreationIds[0] is the image to edit, the prompt is the edit instruction, and up to 5 more refs can ride along as visual support (a logo to insert, a garment to borrow…).

Reference caps

Each tool caps its reference list, and the caps are strict β€” exceeding one rejects with a 400 TOO_MANY_REFS (carrying max and got) before anything is charged, rather than silently dropping the extras:

Tool Max refs
generate.image (refCreationIds) 3 Β· 6 with imageModel: 'next'
character-ref 6
style-transfer 6
smart-edit 6 (refs[0] = the image to edit)
generate.wallpaper (refCreationIds) 3

Wallpaper-resize

Expand a stored creation into a wallpaper dimension β€” the same tool as the studio's wallpaper maker. Quality is always Ultra (there is no quality picker), and the dimension is what drives the price:

const wall = await client.generate.wallpaper({
  inputCreationId: gen.creationId,
  dimension: 'widescreen',        // 'standard' | 'photo' | 'widescreen' | 'ultrawide'
  prompt: 'extend the sky',       // optional guidance for the expansion
  refCreationIds: [refId],        // optional, up to 3 extra soft refs
});

An unknown dimension is rejected (fail-closed) rather than priced at a default. Wallpaper outputs are the one generated kind generate.similar can't re-run β€” their pricing dimension isn't stored on the creation.

Artist Painter

Paint your subject as a new work in a curated artistic style. List the styles first, then pass an id β€” or leave artisticStyleId out to let the server pick the best match for your prompt.

const styles = await client.artisticStyles.list();
// [{ artisticStyleId, title, blurb, thumbnailCdnId }, …]

const painting = await client.generate.artisticStyle({
  prompt: 'a lighthouse on a stormy coast',
  artisticStyleId: styles[0]?.artisticStyleId,   // or 'auto' (default)
  refCreationIds: [gen.creationId],              // optional subject images, ≀6
});
console.log(painting.artisticStyleName);          // the style it was painted in

A prompt OR at least one subject ref is required (400 ARTIST_PAINTER_NEEDS_PROMPT_OR_REF). Works are unsigned unless the prompt asks for a signature. Billed like style-transfer; the tool is Explorer+.

Video

Animate an existing still creation:

const vid = await client.generate.video({
  prompt: 'slow camera push-in, gentle parallax',
  inputCreationId: gen.creationId,
  duration: 5,        // seconds; longer durations are tier-gated
});
await client.download(vid, { savePath: './clip' }); // β†’ ./clip.mp4

Video lengths are 5, 6 or 7 s (7 needs Supporter+); 5 s is the recommended shot. frameInterpolation: true doubles the frame rate for twice the price.

Styles and TIXI. Every video style except 'raw' (and 'auto') runs TIXI on your prompt β€” the style's guidance only exists through that rewrite, so it cannot be switched off. style: 'raw' sends the prompt verbatim; add enhanced: true to have TIXI write the motion scenario (Video) or the full Gen8 Preview video brief (shots, pictures, sound) from a short idea. A prompt starting with tixi adds a thinking pass (the word is dropped from what TIXI reads). No extra credit either way; your text stays in Creation.prompt, TIXI's version in raw.finalPrompt.

Gen8 Preview video

videoModel: 'next' runs Gen8 Preview video, the preview of 2DAI's next video engine. Clips come out with sound, from a first frame (inputCreationId) plus up to 6 refCreationIds β€” characters, props and places that stay consistent through the clip (each adds a surcharge) β€” with an optional aspectRatio ('auto' keeps the first frame's ratio; frameInterpolation does not apply). Lengths and presets follow the account: fast quality and 1, 5, 8 or 10 s from Holder, every preset and 12, 15, 17 or 20 s at Founder β€” read me().videoDurationsNext and me().qualities.videoNext instead of guessing; a length or preset the account may not submit is refused with a 403 (INSUFFICIENT_TIER / QUALITY_NOT_ALLOWED_FOR_TIER) before any charge. Price follows the length (8 s β‰ˆ 1.6Γ—, 10 s β‰ˆ 2Γ—, 20 s β‰ˆ 4Γ— the 5 s price), +33 % per reference after the first frame, the audio reference included. A 1 s clip carries almost no sound. Long clips render for several minutes: the SDK waits up to 20 minutes on a next clip (timeoutMs overrides it), or pass { wait: false } and collect the ticket. The clip can follow an audio reference of yours: uploads.audio() an MP3 (5 min max, Holder+) and pass audioCreationId with audioUse: 'music' (soundtrack / sound design) or 'voice' (a voice sample the speaking character follows) β€” it counts as one more reference for the price; 400 AUDIO_REF_NEXT_ONLY on the default model, INVALID_AUDIO_REF when the creation is not an audio file. With style: 'raw' you bring the scenario yourself β€” the opening frame, the shots in order, and the sound you expect; add enhanced: true (or any other style) and TIXI writes the full brief around your idea and the audio.

const clip = await client.generate.video({
  videoModel: 'next',
  prompt: 'the kite surfer lands on the beach, waves at the camera, the crowd cheers',
  inputCreationId: beach.creationId,        // the opening frame
  refCreationIds: [surferId, dogId],        // optional, ≀6 β€” kept consistent
  duration: 10,                             // one of me().videoDurationsNext
  aspectRatio: '16:9',                      // or 'auto'
});
await client.download(clip, { savePath: './landing' }); // β†’ ./landing.mp4

// With an audio reference: your MP3 as the soundtrack, or 'voice' for a voice sample
const track = await client.uploads.audio({ path: './surf-theme.mp3' });
const scored = await client.generate.video({
  videoModel: 'next',
  prompt: 'the surfer lands as the chorus hits, the crowd cheers',
  inputCreationId: beach.creationId,
  audioCreationId: track.creationId,
  audioUse: 'music',
  duration: 8,
});

Generate similar

Re-run any of your stored creations without re-assembling its parameters β€” the server re-derives the original tool's submit (prompt, style, quality, dimensions, references) from the stored row and queues a fresh generation at the usual price:

const again = await client.generate.similar(gen);            // Creation, result or id
const ticket = await client.generate.similar(gen, { wait: false });

The new creation records its parent in raw.retriedFromCreationId. Works for image, ref-tool, edit and video rows. Two kinds of rows can't be re-derived and reject with a 400 ValidationError: raw uploads (nothing was generated) and wallpaper-resize outputs (REGEN_UNSUPPORTED_TOOL). There's no params override β€” similar is deliberately "the same again"; to change anything, submit through the normal generate.* method instead.

Non-blocking + the queue

// Get the ticket now
const { queueId, costUsd } = await client.generate.image({ prompt: '…' }, { wait: false });

// Poll yourself
const state = await client.queue.waitFor(queueId, { intervalMs: 2000, timeoutMs: 300_000 });
if (state.status === 'completed') {
  console.log(state.downloadUrl);
}

// Or a one-off status read
const now = await client.queue.get(queueId);

waitFor resolves on any terminal status (completed, failed, cancelled, dismissed, timeout, expired) β€” inspect .status. When you use the blocking generate.* (default), a non-completed result throws GenerationFailedError, and exceeding your deadline throws TimeoutError. A job that ended failed, timeout or expired carries errorCode (to branch on) and errorMessage (to show) β€” see Errors β†’ Why a generation failed.

Queue lifecycle

A generation moves through: pending (or pendingCharge for a fraction of a second while the charge lands) β†’ processing/inProgress β†’ one terminal state: completed (β†’ creationId + downloadUrl), failed, cancelled, dismissed, timeout, or expired. costUsd appears on the ticket as soon as a non-zero charge was applied at submit. Aborting a client-side wait does NOT cancel the job β€” it keeps running server-side; find its result later via queue.get(queueId) or creations.list().

Waiting for render capacity. When every render slot is busy, a job stays pending and starts as soon as capacity frees up; it can also move from processing back to pending while it waits for a free slot. If no capacity frees up before its queue deadline (at least 30 minutes), it ends expired and is refunded. A render that runs far too long is stopped (timeout, refunded). Such waits can outlast the default 5-minute timeoutMs of a blocking generate.* β€” for long waits, submit with { wait: false } and poll with queue.waitFor on your own deadline.

Settled queue rows past your newest ~200 are eventually moved to an archive (nothing is deleted), and queue.get reads the archive transparently β€” a stored queueId keeps answering with its terminal status and creationId indefinitely. Only genuinely unknown ids raise NotFoundError.

Reference lifetimes

References are vetted at submit: an inputCreationId / refCreationIds entry (style sources included) that is unknown, in the trash, deleted, or not visible to the account (neither yours nor public) is refused with a 404 CREATION_NOT_FOUND naming the ids in missingCreationIds, before any charge. The worker then reads the files when it picks the ticket up: an optional reference you trash or delete while the ticket is still pending (Image-tool refs, identity refs, extra Gen8 Preview video refs) is left out of the render β€” the job completes without it and the price is unchanged (image channels bill every reference at submit); a primary the tool cannot render without (the first frame of a video, the wallpaper source, the smart-edit target) makes the job fail, refunded. Settle your tickets before touching their inputs:

const ticket = await client.generate.imageWithRefs(
  { tool: 'character-ref', prompt: '…', refCreationIds: [ref.creationId] },
  { wait: false },
);
// … later, BEFORE deleting `ref`:
await client.queue.waitFor(ticket.queueId).catch(() => {});

Cancellation

Two different things answer to "cancel", and they do different jobs:

Stop waiting β€” pass an AbortSignal. The queued job keeps running server-side; you just stop polling it:

const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 10_000);
const gen = await client.generate.image({ prompt: '…' }, { signal: ctrl.signal });

Cancel the job itself β€” queue.cancel. While a ticket is still pending (no worker has picked it up), the server can withdraw it: the charge is refunded and the queue slot released:

const ticket = await client.generate.image({ prompt: '…' }, { wait: false });
await client.queue.cancel(ticket.queueId);   // β†’ { queueId, status: 'cancelled' }

Once a worker has the job there is no abort hook β€” cancel answers 409 NOT_PENDING with the current status in err.details.status. That makes the race benign: either you cancelled in time and were refunded, or the generation completes and you keep it. Needs the generate scope, same as the submit.