Docs do Nodaro
DocumentaçãoReferência de nósModelosAgentes de IA (MCP)DesenvolvedoresSelf-hostingPesquisa
SDK para TypeScript

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étodoO 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,
})
CampoTipoDescrição
urlstringA URL pública do arquivo armazenado. Passe-a como imageUrl, videoUrl ou audioUrl.
assetIdstring | nullO ID do item armazenado, ou null para um upload anônimo.
thumbnailUrlstring | nullUma miniatura para imagens e vídeo, ou null.
categorystringimage, video ou audio.
filenamestringO nome de arquivo exibido.
mimeTypestringO 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.
sizeBytesnumberO tamanho armazenado.
r2KeystringA 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, com imageUrl.
  • Texto real: kind: "text" com um objeto text. 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" com qr: { text }.
  • Uma forma chapada: kind: "shape" com shape: { 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, center por padrão.
  • x e y deslocam 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ê defina height, e então fit decide como a camada preenche a caixa.
  • opacity vai de 0 a 1, rotation é em graus em torno do centro da camada, e blend é over (o padrão), multiply ou screen.
  • shadow adiciona uma sombra suave, roundedCorners arredonda os cantos, em pixels, e zIndex define 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

Última atualização

Nesta página