Vamos construir um pipeline que recebe uma ideia em texto, transforma essa ideia em um roteiro estruturado, gera uma imagem base e envia essa imagem para uma etapa de animação. O resultado final é um arquivo de vídeo, mas o aprendizado mais importante está no caminho: cada modelo faz uma tarefa estreita e entrega um artefato verificável para a etapa seguinte.

A versão mínima terá três blocos. O primeiro produz roteiro e descrição visual. O segundo gera a imagem principal. O terceiro anima essa imagem. Como a rota de vídeo não tem um formato de exemplo canônico aqui, a última chamada será descrita como uma integração genérica. O texto e a imagem usarão chamadas HTTP completas para uma API de geração multimodal.

A primeira decisão é separar as saídas

Um prompt único parece mais rápido, mas mistura decisões diferentes. O modelo precisa inventar a história, escolher enquadramento, desenhar personagens e ainda manter movimento coerente. Quando algo dá errado, fica difícil saber qual etapa causou o problema.

Vamos pedir ao primeiro modelo um JSON com quatro campos: title, narration, visual_prompt e motion_prompt. O roteiro fica separado da instrução visual porque a narração será usada como texto e o prompt visual será usado para gerar a imagem. Essa separação é o contrato entre as etapas.

Crie uma pasta vazia e salve este arquivo como 01-plan.mjs:

const response = await fetch("https://api.provedor.example/v1/chat/completions", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "MODELO_DE_TEXTO",
    response_format: { type: "json_object" },
    messages: [{
      role: "user",
      content: `Crie um plano curto para um vídeo vertical sobre uma bicicleta elétrica atravessando uma cidade ao amanhecer.

Retorne somente JSON válido com estes campos:
{
  "title": "título",
  "narration": "narração em português do Brasil, com duas frases",
  "visual_prompt": "descrição visual detalhada em inglês",
  "motion_prompt": "instrução de movimento em inglês"
}

A bicicleta deve ser preta, ter uma faixa azul no quadro e aparecer em todas as etapas. Não inclua texto, logotipos ou marcas na imagem.`
    }]
  })
});

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

const data = await response.json();
const plan = JSON.parse(data.choices[0].message.content);

console.log(JSON.stringify(plan, null, 2));

Execute com:

API_KEY="sua-chave" node 01-plan.mjs

No Windows PowerShell, use $env:API_KEY="sua-chave" antes do comando. O resultado esperado é um objeto com os quatro campos. Se o JSON falhar, o primeiro lugar para investigar é o prompt: exigir response_format reduz o problema, mas não substitui a validação do programa.

Gere uma imagem que carregue identidade

Agora o visual_prompt vira uma imagem. A consistência começa antes do vídeo, com uma descrição que fixa os elementos que não podem mudar: cor, objeto principal, ambiente, câmera e iluminação.

Salve como 02-image.mjs:

const plan = {
  title: "Electric bicycle at dawn",
  visual_prompt: "A black electric bicycle with a blue stripe on the frame crossing a quiet modern city street at dawn, soft golden light, low camera angle, realistic cinematic photography, no text, no logos, no brands"
};

const response = await fetch("https://api.provedor.example/v1/images", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "MODELO_DE_IMAGEM",
    prompt: plan.visual_prompt,
    parameters: {
      aspect_ratio: "9:16",
      output_format: "jpeg"
    }
  })
});

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

const data = await response.json();
console.log(data.data.urls[0]);

A geração pode levar tempo; em um sistema real, configure um timeout adequado. Neste exemplo, a URL é exibida para inspeção. Baixe o arquivo ou abra a URL e verifique três coisas: a bicicleta é preta, a faixa é azul e o enquadramento é vertical. Se uma dessas propriedades falhar, regenere a imagem antes de avançar.

Essa pausa é uma decisão de engenharia. Não adianta mandar uma imagem inconsistente para a animação e esperar que o terceiro modelo corrija identidade, composição e movimento ao mesmo tempo.

O handoff para vídeo precisa ser explícito

A etapa de animação recebe dois dados: a URL da imagem aprovada e o motion_prompt. O prompt de movimento não deve redescrever toda a cena. Ele deve dizer o que muda no tempo: a bicicleta avança, a câmera acompanha suavemente e a luz permanece estável.

A estrutura genérica da chamada é:

const response = await fetch("https://api.provedor.example/v1/videos", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "MODELO_DE_VIDEO",
    image_url: "URL_DA_IMAGEM_APROVADA",
    prompt: "Animate the bicycle moving forward while the camera follows smoothly. Preserve the black frame, blue stripe, composition and lighting.",
    duration: "DURACAO_DO_VIDEO"
  })
});

Os campos exatos variam conforme o serviço de vídeo escolhido. Consulte a documentação da rota antes de preencher model, image_url e duration. O ponto arquitetural permanece igual: a imagem aprovada entra como referência e o texto descreve apenas o movimento.

Se o serviço responder com uma tarefa assíncrona, o programa terá de consultar o status até a conclusão. Use espera progressiva, não um loop apertado. A espera precisa fazer parte do desenho do produto, porque a geração e a consulta de status ocupam tempo e afetam a experiência do usuário.

Una tudo sem perder rastreabilidade

Quando a versão mínima funcionar, substitua os valores fixos por um objeto plan produzido pela primeira etapa. Salve também um manifesto com o prompt usado, a URL da imagem, o status de aprovação e a URL do vídeo. O manifesto pode ser um JSON comum.

A razão é simples: uma geração de vídeo não é reproduzível apenas pelo arquivo final. Você precisa saber qual roteiro gerou a imagem, qual imagem foi aprovada e qual instrução de movimento produziu o clipe. Sem isso, uma revisão vira uma nova tentativa às cegas.

Um fluxo confiável tem estados claros: draft, needs_review, approved e failed. A pessoa revisa a imagem antes da animação e revisa o vídeo antes da publicação. Essa intervenção não é um remendo. Ela é o mecanismo que impede uma escolha ruim na primeira etapa de se multiplicar nas seguintes.

Meça antes de chamar isso de produto

Conte as chamadas e separe o tempo de cada uma. Neste exemplo mínimo, considere uma chamada de texto, uma de imagem e uma de vídeo. Se a imagem precisar de novas tentativas, o custo e a espera da etapa visual aumentam, enquanto a etapa de texto permanece igual.

Também registre o motivo da rejeição. Se as falhas forem de identidade visual, melhore o visual_prompt. Se forem de movimento, preserve a imagem e refine somente o motion_prompt. Essa separação evita regenerar tudo por causa de um problema localizado.

O que você construiu aqui não é um botão mágico. É uma linha de produção pequena, com contratos entre etapas. Uma possível evolução é gerar vários planos de cena, aprovar um conjunto de imagens, animar apenas as aprovadas e montar os clipes com narração e legendas. Quando cada artefato tem estado, origem e critério de validação, combinar modalidades deixa de ser demonstração e começa a parecer software.