A ideia é simples: você escreve uma lista de cenas, define o formato de cada uso e deixa um script gerar o pacote visual inteiro. No fim, uma pasta reúne a capa de um post, o banner de uma página e a versão quadrada para uma rede social, cada uma salva localmente.

Vamos construir isso em duas etapas. Primeiro, uma única imagem para provar que a chamada funciona. Depois, um lote com concorrência controlada, validação dos arquivos e uma regra importante: a máquina produz opções, mas uma pessoa ainda escolhe o que merece ser publicado.

A menor versão que já resolve o problema

Crie uma pasta e salve este arquivo como one-image.mjs:

import { writeFile } from "node:fs/promises";

const apiKey = process.env.HINOW_API_KEY;

if (!apiKey) {
  throw new Error("Set HINOW_API_KEY before running the script");
}

const response = await fetch("https://api.hinow.ai/v1/images", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + apiKey,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "hinow/himegia",
    prompt: "A cozy desk beside a window, a notebook, warm morning light, editorial photography, muted blue and amber palette",
    parameters: {
      aspect_ratio: "16:9",
      output_format: "jpeg"
    }
  }),
  signal: AbortSignal.timeout(120000)
});

if (!response.ok) {
  throw new Error(await response.text());
}

const data = await response.json();
const imageUrl = data.data.urls[0];
const imageResponse = await fetch(imageUrl);

if (!imageResponse.ok) {
  throw new Error("Could not download generated image");
}

const imageBytes = Buffer.from(await imageResponse.arrayBuffer());
await writeFile("cover.jpeg", imageBytes);
console.log("Saved cover.jpeg");

Execute com uma versão recente do Node.js que ofereça suporte a fetch e módulos ES:

HINOW_API_KEY="sua-chave" node one-image.mjs

No Windows PowerShell, a forma equivalente é:

$env:HINOW_API_KEY="sua-chave"; node one-image.mjs

O script faz quatro coisas. Envia a descrição para a rota de geração, espera a resposta, extrai a URL em data.data.urls[0] e baixa os bytes para um arquivo. A chave fica em uma variável de ambiente para não parar no código nem no histórico do projeto.

A descrição também tem uma estrutura deliberada. Em vez de empilhar palavras soltas, ela informa cena, estilo, iluminação e paleta. Esse formato dá ao modelo de imagem mais elementos para organizar. Evite pedir letreiros ou frases exatas dentro da arte. Texto literal ainda sai com erros de grafia com frequência.

A geração leva de alguns segundos a mais de meio minuto por imagem. Por isso o timeout é de 120 segundos, não o padrão curto de muitas bibliotecas HTTP.

Uma lista vira um pacote visual

Agora vamos substituir a chamada única por um lote. Crie image-factory.mjs:

import { mkdir, stat, writeFile } from "node:fs/promises";

const apiKey = process.env.HINOW_API_KEY;

if (!apiKey) {
  throw new Error("Set HINOW_API_KEY before running the script");
}

const jobs = [
  {
    name: "post-cover",
    prompt: "A curious developer exploring a glowing abstract interface, editorial illustration, soft contrast, deep violet and cyan palette",
    aspectRatio: "16:9",
    format: "jpeg"
  },
  {
    name: "website-banner",
    prompt: "A clean futuristic workspace with floating geometric forms, wide cinematic composition, soft studio lighting, black and lime palette",
    aspectRatio: "21:9",
    format: "webp"
  },
  {
    name: "social-square",
    prompt: "A compact robot arranging colorful blocks on a table, friendly editorial illustration, bright daylight, coral and navy palette",
    aspectRatio: "1:1",
    format: "png"
  }
];

const outputDirectory = "generated-images";
const concurrency = 3;

await mkdir(outputDirectory, { recursive: true });

async function generateImage(job) {
  const response = await fetch("https://api.hinow.ai/v1/images", {
    method: "POST",
    headers: {
      Authorization: "Bearer " + apiKey,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "hinow/himegia",
      prompt: job.prompt,
      parameters: {
        aspect_ratio: job.aspectRatio,
        output_format: job.format
      }
    }),
    signal: AbortSignal.timeout(120000)
  });

  if (!response.ok) {
    throw new Error(`${job.name}: ${await response.text()}`);
  }

  const data = await response.json();
  const imageUrl = data.data.urls[0];
  const imageResponse = await fetch(imageUrl);

  if (!imageResponse.ok) {
    throw new Error(`${job.name}: could not download image`);
  }

  const imageBytes = Buffer.from(await imageResponse.arrayBuffer());
  const filePath = `${outputDirectory}/${job.name}.${job.format}`;
  await writeFile(filePath, imageBytes);
  console.log(`Saved ${filePath}`);
}

async function runWithLimit(items, limit) {
  let nextIndex = 0;

  async function worker() {
    while (nextIndex < items.length) {
      const currentIndex = nextIndex++;
      await generateImage(items[currentIndex]);
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, items.length) },
    () => worker()
  );

  await Promise.all(workers);
}

await runWithLimit(jobs, concurrency);

for (const job of jobs) {
  const filePath = `${outputDirectory}/${job.name}.${job.format}`;
  const fileInfo = await stat(filePath);

  if (fileInfo.size === 0) {
    throw new Error(`${filePath} is empty`);
  }
}

console.log(`Validated ${jobs.length} generated files`);

A lista jobs separa conteúdo de infraestrutura. Cada item descreve o que produzir, o nome do arquivo, o formato e a proporção. Para acrescentar uma quarta peça, você não duplica a chamada HTTP. Apenas adiciona outro item.

Os formatos aceitos são jpeg, png e webp. As proporções incluem 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9 e 9:21. A escolha deve seguir o destino: uma imagem quadrada serve para uma grade, uma proporção larga ocupa um banner e uma vertical aproveita melhor uma tela de celular.

Por que não disparar tudo de uma vez?

Três cilindros de vidro contendo fluxos de areia colorida fluindo independentemente uns dos outros
Controle de fluxo: cada processo opera no seu ritmo sem engarrafar o sistema.

Promise.all puro é tentador: basta mapear a lista para promessas e esperar todas. O problema aparece quando o lote cresce. Muitas requisições simultâneas pressionam a API, dificultam a recuperação de erros e podem esbarrar nos limites de uso.

O runWithLimit mantém no máximo três trabalhos ativos. Ele cria trabalhadores que retiram o próximo item da fila e só avançam quando terminam o atual. Assim, o lote continua paralelo sem virar uma avalanche.

A conta de tempo deixa a decisão concreta. Suponha que cada imagem leve 20 segundos e que existam 12 imagens. Em fila única, o total fica em torno de 240 segundos, ou 4 minutos. Com concorrência limitada, o tempo estimado cai aproximadamente na proporção do número de trabalhos simultâneos. Essa é uma conta ilustrativa baseada nessa premissa, não uma promessa de latência: a geração real varia por imagem e pode levar de segundos a mais de meio minuto.

O limite três também é um ponto de partida, não uma lei. Se a API aceitar mais concorrência no seu caso, aumente gradualmente. Se houver erros intermitentes ou saturação, reduza.

O teste que evita uma pasta enganosa

Uma lâmina de vidro fosco filtrando uma luz colorida e caótica ao fundo
A tela de vidro: o que brilha na máquina nem sempre é a imagem que o olho precisa ver.

Rodar o comando deve criar três arquivos em generated-images: post-cover.jpeg, website-banner.webp e social-square.png. A mensagem final informa quantos arquivos da lista foram verificados.

A validação automática verifica existência e tamanho maior que zero. Ela pega falhas básicas, como uma resposta vazia ou um caminho incorreto. Não prova que a imagem ficou boa. Para isso, abra os arquivos. Veja se o assunto aparece, se a composição deixa espaço para o uso previsto e se a paleta combina com o projeto.

Essa inspeção é parte do sistema, não uma etapa opcional. A geração é probabilística. A mesma descrição pode produzir imagens diferentes em chamadas diferentes. Se uma versão for aprovada, guarde o arquivo aprovado. Não dependa apenas do prompt para reproduzi-la depois.

A fábrica começa depois da primeira pasta

O próximo passo natural é transformar a lista em dados externos, como um arquivo JSON revisado pelo time. Depois, você pode criar variações por produto, gerar tamanhos por canal ou separar prompts por campanha. Também pode registrar quais arquivos foram aprovados e impedir que uma nova execução os substitua.

Uma extensão útil é adicionar tentativas para falhas temporárias, mas sem repetir indiscriminadamente uma geração que já terminou. Outra é salvar metadados junto das imagens, incluindo a descrição, a proporção e o momento da criação. Isso facilita auditoria e comparação.

A fronteira importante está na consistência. Para uma biblioteca de peças independentes, gerar em lote resolve muito. Para personagens idênticos, embalagens precisas ou texto legível, a seleção humana continua indispensável. A fábrica acelera a produção de alternativas. Ela não transforma uma alternativa em decisão.