Repository navigation
Uploads
Bring your own image, GIF, or MP4 (or M4V — the same container, stored as MP4) into your library so it can be used as a
generation reference or animated into a video — or an MP3 that becomes the
audio reference of a Gen8 Preview video clip. The same moderation pass as the studio
runs server-side, so a flagged upload throws NsfwRejectedError.
Requires the generate scope.
const up = await client.uploads.image({ path: './portrait.jpg' });
console.log(up.creationId, up.width, up.height);const file = input.files[0]; // <input type="file">
const up = await client.uploads.image({ data: file });await client.uploads.image({ data: myUint8Array, filename: 'ref.png' });
await client.uploads.image({ base64: '...', filename: 'ref.jpg', contentType: 'image/jpeg' });UploadInput accepts exactly one source: path (Node), data
(Uint8Array | ArrayBuffer | Blob), or base64. filename and contentType
are optional (inferred when possible).
By default an upload lands unfiled at the drive root. targetFolderId files
it directly into a folder instead — no separate move call, and the
duplicate check (below) is scoped to that folder:
const folder = await client.folders.create({ title: 'Refs' });
const up = await client.uploads.image({ path: './ref.jpg', targetFolderId: folder.folderId });
up.folderId; // folder.folderIdThe folder must be one you can write to — your own, or one shared with you
with editor rights (system folders are excluded). Anything else answers 403
INVALID_FOLDER and the upload is discarded, not filed at the root.
If the file you're uploading is an edit of a creation you already own — a crop you made locally, a background you erased — say so, and the platform links the two:
await client.uploads.image({ path: './cropped.png', croppedFromCreationId: original.creationId });
await client.uploads.image({ path: './cutout.png', erasedFromCreationId: original.creationId });The two fields are mutually exclusive (an upload is one kind of edit), and
both are images-only, on creations you own. The new creation counts as
lineage of the original — it shows up in creations.list({ usedRef: original })
— and is filed with the studio's crop / erase edits (crops feed the crop
smart collection, erase cut-outs with real transparency feed alpha).
const up = await client.uploads.image({ path: './face.jpg' });
await client.generate.imageWithRefs({
tool: 'character-ref',
prompt: 'wearing a wizard hat',
refCreationIds: [up.creationId],
});Uploaded source files can't be published to the public feed — only your own generated creations can.
An .mp4 or .m4v clip (the same ISO BMFF container — the platform stores and
serves it as .mp4, outputExtension: '.mp4', mediaKind: 'video', duration
in seconds) needs an account allowed to upload videos (403 VIDEOS_NOT_ALLOWED
otherwise). A file the platform cannot read is refused with 400 INVALID_MEDIA;
a DRM-protected one — an iTunes / FairPlay purchase, a CENC-encrypted MP4 — with
400 PROTECTED_MEDIA. Both are ValidationErrors: the file is the problem, not
the service, so do not retry them.
const clip = await client.uploads.image({ path: './clip.m4v' }); // contentType inferred: video/mp4
clip.mediaKind; // 'video'uploads.audio() takes an MP3 — 5 minutes max (400 AUDIO_TOO_LONG), Holder
tier and up (403 AUDIO_NOT_ALLOWED) — and returns an audio creation
(mediaKind: 'audio', duration in seconds, no pixels so the file name and
length label it). Pass its id to generate.video with videoModel: 'next':
audioUse: 'music' lays it under the clip as soundtrack / sound design,
'voice' makes it the voice sample the speaking character follows. The audio
counts as one more reference for the price.
const track = await client.uploads.audio({ path: './theme.mp3' }); // Node
const track2 = await client.uploads.audio({ data: mp3Blob, filename: 'theme.mp3' }); // Browser
const clip = await client.generate.video({
videoModel: 'next',
inputCreationId: frame.creationId,
prompt: 'the band walks on stage as the intro swells',
audioCreationId: track.creationId,
audioUse: 'music',
duration: 8,
});The server de-duplicates uploads per folder by content hash: sending the
exact same pixels into the same folder returns 409 DUPLICATE_IN_FOLDER
(a ValidationError). Re-uploading the same file into a different folder is
fine, as is uploading different bytes. With targetFolderId, the check runs
against that folder.
checkDuplicate asks the question before you pay for the transfer — an
md5 of the file bytes instead of the file itself:
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
const bytes = await readFile('./ref.jpg');
const md5 = createHash('md5').update(bytes).digest('hex');
const probe = await client.uploads.checkDuplicate(md5, folder.folderId);
// { duplicate: true, existingCreationId: '…', folderId: '…' } — or duplicate: false
if (!probe.duplicate) {
await client.uploads.image({ data: bytes, filename: 'ref.jpg', targetFolderId: folder.folderId });
}Omit folderId to check against the drive root. The probe only needs the
read scope, and it is advisory: the upload itself still re-checks, so a
concurrent upload can race your false — treat it as a hint that saves the
round-trip, not a lock.