Mídia e uploads
Envie arquivos ao Nodaro, liste sua biblioteca, baixe vídeos de redes sociais, corte clipes, embuta legendas e componha imagens e vídeos em TypeScript.
client.uploads armazena os seus arquivos no Nodaro, client.library lista as mídias que você já tem, e client.media prepara e edita mídias: baixa vídeos de redes sociais, corta clipes, embute legendas e compõe imagens e vídeos. A maioria dos métodos de client.media inicia um job e retorna o jobId dele, que você consulta periodicamente com client.jobs.getStatus(). Os métodos chamam os endpoints das APIs REST de Envio de arquivos e de Voz e mídia.
Métodos
| Método | O que faz |
|---|---|
uploads.upload(file) | Envia um arquivo e obtém a URL pública dele |
library.list(params?) | Lista as suas mídias armazenadas |
media.downloadVideo(input) | Baixa um vídeo de rede social para o seu armazenamento |
media.downloadVideoProgress(downloadId, opts?) | Acompanha o progresso de um download |
media.videoMetadata(input) | Lê a duração e o tamanho de um vídeo de rede social sem baixá-lo |
media.saveToStorage(input) | Copia a URL de uma mídia para o seu armazenamento |
media.process(input) | Corta ou recorta um arquivo armazenado, sem custo |
media.trimVideo(input) | Corta um vídeo, mantendo um intervalo |
media.trimAudio(input) | Corta um áudio ou o extrai de um vídeo |
media.addCaptions(input) | Embute legendas em um vídeo |
media.stillToVideo(input) | Transforma uma imagem e uma faixa de áudio em um vídeo |
media.slideshow(input) | Transforma imagens e um áudio opcional em uma apresentação de slides |
media.videoOverlay(input) | Posiciona camadas de imagem temporizadas sobre um vídeo |
media.imageCollage(input) | Combina de 2 a 30 imagens em uma única colagem |
media.imageOverlay(input) | Posiciona camadas de imagem, texto, QR e forma sobre uma imagem |
media.suggestOverlayPlacement(input) | Pergunta a um modelo de visão onde uma camada deve ficar |
client.uploads
uploads.upload(file)
Envia um arquivo (POST /v1/upload, multipart) e retorna a URL pública e os detalhes de armazenamento dele. O SDK envia o arquivo como form data e deixa o runtime definir o boundary do multipart.
upload(file: File): Promise<UploadResult>Prop
Type
const result = await client.uploads.upload(file)
const clip = await client.nodes.runAndWait("generate-video", {
prompt: "The camera slowly pushes in",
imageUrl: result.url,
})| Campo | Tipo | Descrição |
|---|---|---|
url | string | A URL pública do arquivo armazenado. Passe-a como imageUrl, videoUrl ou audioUrl. |
assetId | string | null | O ID do item armazenado, ou null para um upload anônimo. |
thumbnailUrl | string | null | Uma miniatura para imagens e vídeo, ou null. |
category | string | image, video ou audio. |
filename | string | O nome de arquivo exibido. |
mimeType | string | O tipo de mídia que o servidor definiu. Pode ser diferente do que o navegador declarou, por exemplo video/mp4 para um arquivo .mp4 enviado como application/octet-stream. |
sizeBytes | number | O tamanho armazenado. |
r2Key | string | A chave de armazenamento do arquivo. |
Lança StorageExceededError quando o seu armazenamento está cheio. Os nós Enviar imagem (Upload Image), Enviar vídeo (Upload Video) e Enviar áudio (Upload Audio) usam o mesmo armazenamento.
client.library
library.list(params?)
Lista as suas mídias armazenadas com um cursor (GET /v1/library). Por padrão, retorna os itens salvos na sua biblioteca mais os itens compartilhados com você, como o seletor de mídia do editor os mostra.
list(params?: ListLibraryParams): Promise<{ data: LibraryAsset[]; nextCursor: string | null; totalCount?: number }>Prop
Type
const { data: videos, nextCursor } = await client.library.list({ type: "video", limit: 20 })
for (const asset of videos) console.log(asset.filename, asset.url)Cada LibraryAsset tem id, type, filename, mimeType, sizeBytes, url, thumbnailUrl, metadata, isLibraryItem, uploadSource, source, sourceDetail e createdAt. totalCount só aparece na primeira página.
client.media: importar e preparar
media.downloadVideo(input)
Baixa um vídeo do YouTube, TikTok, Instagram, X ou Facebook para o seu armazenamento (POST /v1/download-video). Retorna um downloadId, não um ID de job. Acompanhe-o com downloadVideoProgress(). O arquivo final vai para a sua biblioteca.
downloadVideo(input: {
url: string
maxHeight?: number
sectionStartSec?: number
sectionEndSec?: number
requireAudio?: boolean
}): Promise<{ downloadId: string }>Prop
Type
const { downloadId } = await client.media.downloadVideo({
url: "https://youtu.be/dQw4w9WgXcQ",
maxHeight: 720,
})media.downloadVideoProgress(downloadId, opts?)
Transmite ao vivo o progresso de um download como um iterador assíncrono (GET /v1/download-video/progress/:id, server-sent events). Emite um evento a cada 500 ms, aproximadamente, até o download terminar ou falhar, e então se encerra.
downloadVideoProgress(downloadId: string, opts?: { signal?: AbortSignal }): AsyncGenerator<DownloadVideoProgress>Prop
Type
for await (const event of client.media.downloadVideoProgress(downloadId)) {
console.log(`${event.phase} ${event.percent}%`)
if (event.phase === "completed") console.log("Stored at", event.videoUrl)
if (event.phase === "failed") console.error(event.error)
}Cada evento é { phase, percent, videoUrl?, thumbnailUrl?, error? }. Comece a iterar logo depois que downloadVideo() retornar, porque o registro de progresso expira pouco depois do fim do download. O timeoutMs do cliente não se aplica, porque downloads grandes levam minutos.
media.videoMetadata(input)
Lê a duração, as dimensões, o título e o status de transmissão ao vivo de um vídeo de rede social sem baixá-lo (POST /v1/video-metadata). Responde diretamente, sem job. Use-o para decidir se vale buscar apenas um trecho.
videoMetadata(input: { url: string }): Promise<VideoMetadata>Prop
Type
const meta = await client.media.videoMetadata({ url: "https://youtu.be/dQw4w9WgXcQ" })Os campos são preenchidos na medida do possível: uma plataforma pode não informar todos.
media.saveToStorage(input)
Copia um arquivo de mídia de uma URL para o seu armazenamento no Nodaro (POST /v1/save-to-storage). O servidor busca o arquivo, então nada passa pelo seu cliente.
saveToStorage(input: { mediaUrl: string; filename?: string; mediaType?: "image" | "video" | "audio" }): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.saveToStorage({ mediaUrl: "https://example.com/intro.mp4", mediaType: "video" })O nó Salvar no armazenamento (Save to Storage) faz o mesmo dentro de um workflow.
media.process(input)
Corta ou recorta um arquivo que já está no seu armazenamento (POST /v1/media/process). Responde diretamente e não custa nada. Use-o para preparar uma fonte antes de uma etapa paga.
process(input: {
sourceUrl: string
type: "video" | "audio"
trim?: { startTime: number; endTime: number }
crop?: { x: number; y: number; width: number; height: number }
format?: "mp4" | "webm" | "mp3" | "wav" | "m4a" | "aac"
deleteSource?: boolean
}): Promise<{ data: MediaProcessResult }>Prop
Type
const { data } = await client.media.process({
sourceUrl: stored.url,
type: "video",
trim: { startTime: 12, endTime: 42 },
})
console.log(data.url, data.sizeBytes)O resultado tem url, thumbnailUrl, assetId, sizeBytes, mimeType e metadata.
media.trimVideo(input)
Corta um vídeo, mantendo um intervalo (POST /v1/trim-video), como faz o nó Cortar vídeo (Trim Video). Informe o intervalo na unidade que for mais conveniente.
trimVideo(input: {
videoUrl: string
startTime?: number
endTime?: number
trimStartFrames?: number
trimEndFrames?: number
trimStartSeconds?: number
trimEndSeconds?: number
keepFirstSeconds?: number
keepLastSeconds?: number
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.trimVideo({ videoUrl, keepFirstSeconds: 15 })media.trimAudio(input)
Corta um áudio, mantendo um intervalo, ou o extrai de um vídeo (POST /v1/trim-audio), como faz o nó Cortar áudio (Trim Audio).
trimAudio(input: {
videoUrl?: string
audioUrl?: string
audioFormat?: "mp3" | "wav" | "aac"
startTime?: number
endTime?: number
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.trimAudio({ videoUrl, startTime: 5, endTime: 35 })client.media: legendas
media.addCaptions(input)
Embute legendas em um vídeo (POST /v1/add-captions), como faz o nó Adicionar legendas (Add Captions). Informe as palavras como text, informe captions com tempos por palavra ou deixe o Nodaro transcrever a fala, que é o padrão.
addCaptions(input: AddCaptionsInput): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.addCaptions({
videoUrl: "https://example.com/talk.mp4",
style: "word-highlight",
maxWordsPerLine: 2,
})De onde vêm as palavras. Quando uma chamada traz várias fontes, as captions com tempos por palavra têm prioridade, depois o text no estilo subtitle e, por último, a transcrição. No subtitle, text é a legenda: ela é embutida como um único bloco estático durante o vídeo inteiro e nunca é substituída por uma transcrição. Use \n no texto para forçar uma quebra de linha. Em um estilo cinético, text é apenas um texto de reserva. Ele é usado quando a transcrição não encontra nada ou autoTranscribe é false, e então as palavras dele são distribuídas por igual ao longo do vídeo.
Estilos e looks. Nos estilos cinéticos, um look não definido é renderizado como outline. No subtitle, é renderizado como clean. Os ajustes explícitos substituem campos individuais do look. Um segmento que define o próprio look parte dessa predefinição e não herda os ajustes do nível superior.
Ajustes no subtitle. Os ajustes de estilo (look, fontFamily, fontWeight, strokeColor, strokeWidth, uppercase, positionY e maxWordsPerLine) também funcionam no subtitle. Um subtitle com qualquer um deles é cobrado pelo preço cinético, e um subtitle de texto simples é cobrado pelo preço mais baixo. highlightColor e animate são exclusivos dos estilos cinéticos e são recusados com um 400 no subtitle. Um subtitle transcrito automaticamente também é cobrado pelo preço cinético.
Linhas. word-highlight, karaoke e bouncy mostram uma linha por vez. Uma linha termina no fim de uma frase, em uma pausa de 0,5 segundo ou mais, quando ocupa cerca de 85% da largura do quadro ou ao chegar a maxWordsPerLine palavras. word-pop mostra uma palavra por vez, então maxWordsPerLine não tem efeito nesse estilo. Uma página de tiktok-words nunca atravessa o fim de uma frase nem uma pausa. Ambas ficam na tela por no máximo 1,5 segundo depois da última palavra falada. O startMs e o endMs de uma palavra marcam o tempo do efeito dela, não por quanto tempo ela fica visível.
Mecanismos de transcrição. Um estilo cinético precisa de tempos por palavra, então transcribeProvider deve ser incredibly-fast-whisper, o padrão aqui, ou elevenlabs-stt. whisper não tem tempos por palavra. Ele é recusado com 400 validation_error apenas quando a transcrição é a única fonte possível das palavras. No subtitle, qualquer mecanismo funciona, porque um subtitle só precisa dos tempos de cada frase.
Taxa de quadros. Uma renderização estilizada mantém a taxa de quadros do vídeo de origem, arredondada para um número inteiro entre 15 e 60. Uma fonte com taxa de quadros variável, ou um clipe longo demais para o limite de quadros, é renderizado a 30 fps.
Transcrever, corrigir e embutir
As words de um job de client.audio.transcribe() têm exatamente o formato que captions aceita. Corrija uma palavra e depois embuta as legendas sem uma segunda transcrição:
import type { TranscribeJobOutput } from "@nodaro/sdk"
const { jobId } = await client.audio.transcribe({
audioUrl: "https://example.com/talk.mp3",
provider: "elevenlabs-stt", // always returns word timings
})
// ...poll until the job completes, then:
const { data: job } = await client.jobs.get(jobId)
const { words = [] } = job.output_data as TranscribeJobOutput
const corrected = words.map((w, i) => (i === 7 ? { ...w, text: "Nodaro" } : w))
await client.media.addCaptions({
videoUrl: "https://example.com/talk.mp4",
captions: corrected,
autoTranscribe: false,
style: "word-highlight",
})client.media: renderizar e compor
media.stillToVideo(input)
Transforma uma imagem fixa e uma faixa de áudio em um MP4 (POST /v1/still-to-video), como faz o nó Imagem fixa para vídeo (Still to Video). É renderizado no servidor, sem modelo de IA, e não custa créditos. O vídeo dura o mesmo que o áudio; não há campo de duração.
stillToVideo(input: {
imageUrl: string
audioUrl: string
motion?: "none" | "zoom-in" | "zoom-out" | "pan-left" | "pan-right" | "ken-burns"
intensity?: number
resolution?: "720p" | "1080p" | "4K"
aspectRatio?: "16:9" | "9:16" | "1:1" | "4:3"
fps?: 24 | 30
fit?: "cover" | "contain"
padColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.stillToVideo({ imageUrl: coverUrl, audioUrl: podcastUrl, motion: "ken-burns" })media.slideshow(input)
Transforma de 2 a 100 imagens e uma faixa de áudio opcional em uma apresentação de slides em MP4 (POST /v1/slideshow), como faz o nó Apresentação de slides (Slideshow). Não custa créditos. Para uma única imagem, use stillToVideo().
slideshow(input: {
imageUrls: string[]
audioUrl?: string
imageDurations?: Array<number | null>
perImageDuration?: number
transition?: string
transitionDuration?: number
motion?: "none" | "zoom-in" | "zoom-out" | "ken-burns" | "alternate"
intensity?: number
resolution?: "720p" | "1080p" | "4K"
aspectRatio?: "16:9" | "9:16" | "1:1" | "4:3"
fps?: 24 | 30
fit?: "cover" | "contain"
padColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.slideshow({ imageUrls: frames, audioUrl: musicUrl, motion: "alternate" })Com áudio, o tempo é dividido por igual, a menos que imageDurations fixe a duração de algumas imagens. Sem áudio, o vídeo dura o número de imagens vezes perImageDuration e fica sem som.
media.videoOverlay(input)
Posiciona de 1 a 20 camadas de imagem temporizadas sobre um vídeo em uma única passada (POST /v1/video-overlay), como faz o nó Sobreposição em vídeo (Video Overlay). Custa 22 créditos por execução, seja qual for o número de camadas ou a duração. O áudio do próprio vídeo é mantido como está.
videoOverlay(input: VideoOverlayRequest): Promise<{ jobId: string }>Prop
Type
Cada camada recebe imageUrl e start, em segundos, de 0 a 3.600, e um end opcional; sem end, ela fica até o fim do vídeo. preset é card, corner-badge ou full-frame. Os campos da caixa, x e y, vão de -100 a 100, e width e height, de 1 a 100, todos em porcentagem do quadro de saída. Um campo de caixa explícito substitui a predefinição. Uma camada sem nenhum dos dois é um selo de canto, no canto inferior direito ou no corner que ela indicar. opacity vai de 0 a 1, animate vem ativado por padrão e zIndex vai de 0 a 100.
const { jobId } = await client.media.videoOverlay({
videoUrl,
layers: [
{ imageUrl: logoUrl, start: 0, preset: "corner-badge", corner: "top-right" },
{ imageUrl: offerCardUrl, start: 8, end: 14, preset: "card" },
],
})A saída do job concluído tem videoUrl, thumbnailUrl, width, height, durationSec e warnings. Cada aviso é { layer?, slot?, code, detail }, com um código como clipped, skipped, animated_first_frame ou audio_reencoded.
media.imageCollage(input)
Combina de 2 a 30 imagens em uma única colagem grande, em 2K ou 4K (POST /v1/image-collage), como faz o nó Colagem de imagens (Image Collage).
imageCollage(input: {
imageUrls: string[]
imageSizes?: Array<0 | 1 | 2 | 3>
numbered?: boolean
imageLabels?: Array<string | null>
badgePosition?: "top-left" | "top-right"
layout?: "smart" | "grid"
resolution?: "2K" | "4K"
aspectRatio?: string
gap?: number
backgroundColor?: string
}): Promise<{ jobId: string }>Prop
Type
const { jobId } = await client.media.imageCollage({
imageUrls: shotUrls,
numbered: true,
imageLabels: ["Wide", "Medium", "Close-up"],
})Números e rótulos nunca mudam o layout, o tamanho de saída nem o preço. Um rótulo longo demais para a imagem é encurtado com reticências.
media.imageOverlay(input)
Posiciona de 1 a 12 camadas sobre uma imagem base, com precisão de pixel (POST /v1/image-overlay), como faz o nó Sobreposição em imagem (Image Overlay). É uma composição sem modelo de IA. Custa 11 créditos, e cada tamanho de plataforma extra em variants aumenta o preço.
imageOverlay(input: {
imageUrl: string
layers: Array<{
kind?: "image" | "text" | "qr" | "shape"
imageUrl?: string
anchor?: "top-left" | "top" | "top-right" | "left" | "center" | "right" | "bottom-left" | "bottom" | "bottom-right"
x?: number
y?: number
width?: number
height?: number
opacity?: number
rotation?: number
blend?: "over" | "multiply" | "screen"
fit?: "contain" | "cover" | "stretch"
shadow?: { blur: number; offsetX: number; offsetY: number; color: string; opacity: number }
roundedCorners?: number
zIndex?: number
text?: OverlayTextStyle
qr?: OverlayQrStyle
shape?: OverlayShapeStyle
effects?: OverlayImageEffects
}>
canvas?: { width: number; height: number; backgroundColor?: string }
baseFit?: "contain" | "cover"
outputFormat?: "png" | "jpg" | "webp"
variants?: string[]
maskMode?: "none" | "layers" | "around" | "outside"
maskSpread?: number
qrText?: string
}): Promise<{ jobId: string }>Prop
Type
Camadas. Uma camada é de um destes quatro tipos:
- Uma imagem:
kind: "image", o padrão, comimageUrl. - Texto real:
kind: "text"com um objetotext. Ele define o conteúdo, a fonte, o peso, a cor, o alinhamento, o contorno e a caixa de fundo, além de um tamanho em porcentagem da altura da base. - Um QR code:
kind: "qr"comqr: { text }. - Uma forma chapada:
kind: "shape"comshape: { shape, color }.
As posições são porcentagens da imagem base, então a mesma chamada funciona em uma prévia em 1K e em uma renderização em 4K:
anchoré uma de nove posições,centerpor padrão.xeydeslocam a camada a partir da âncora, em porcentagem da largura e da altura da base. Com uma âncora à direita ou embaixo, um valor negativo a desloca para dentro.widthé em porcentagem da largura da base, 25 por padrão. A altura segue a proporção da camada, a menos que você definaheight, e entãofitdecide como a camada preenche a caixa.opacityvai de 0 a 1,rotationé em graus em torno do centro da camada, eblendéover(o padrão),multiplyouscreen.shadowadiciona uma sombra suave,roundedCornersarredonda os cantos, em pixels, ezIndexdefine a ordem de empilhamento.
const { jobId } = await client.media.imageOverlay({
imageUrl: productShotUrl,
layers: [{ imageUrl: logoUrl, anchor: "bottom-right", x: -4, y: -6, width: 12, opacity: 0.95 }],
variants: ["youtube-thumbnail"],
})A saída do job concluído tem imageUrl, com width e height, maskUrl e variants, cada uma no formato { id, label, width, height, url }. As camadas SVG são desenhadas com nitidez no tamanho final. Uma camada QR com qr.fromInput: true obtém o conteúdo de qrText, e uma execução sem qrText é recusada com um 400 que o indica.
media.suggestOverlayPlacement(input)
Pergunta a um modelo de visão onde uma camada deve ficar sobre uma imagem base (POST /v1/image-overlay/suggest-placement). O modelo mantém a camada longe de rostos, do assunto principal e de texturas carregadas. Responde diretamente, sem um job a consultar periodicamente, e é cobrado como uma chamada do nó Descrever imagem (Describe Image). Nada é composto: você aplica o posicionamento.
suggestOverlayPlacement(input: {
imageUrl: string
layerAspect?: number
intent?: string
safeArea?: { x: number; y: number; w: number; h: number }
llmModel?: string
}): Promise<{ jobId: string; placement: OverlayPlacement }>Prop
Type
const { placement } = await client.media.suggestOverlayPlacement({
imageUrl: baseUrl,
intent: "a logo",
layerAspect: 2.5,
})
const { reason, ...box } = placement // anchor, x, y and width, in imageOverlay's units
await client.media.imageOverlay({ imageUrl: baseUrl, layers: [{ imageUrl: logoUrl, ...box }] })placement tem anchor, x, y e width nas unidades em porcentagem de imageOverlay, além de um reason de uma frase que você pode mostrar ao usuário. jobId é o registro de cobrança.
Perguntas frequentes
Páginas relacionadas
Vozes e áudio
Jobs e execuções
Envio de arquivos
Voz e mídia
Adicionar legendas
Última atualização
LLM e Reduce
Obtenha JSON validado de um modelo de linguagem com client.llm e use client.reduce para escolher o melhor de vários resultados, votar, juntar ou mesclar.
Vozes e áudio
Navegue pelas vozes, troque e substitua vozes, crie novas vozes, duble vídeos e separe, mixe e transcreva áudio em TypeScript com o SDK do Nodaro.