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.