Como gerar vídeo com IA e áudio nativo com o Gemini Omni Flash pela Google Flow API
8 min de leitura • 15 de junho de 2026 (12 de novembro de 2026)
A useapi.net é uma API de terceiros. Não tem afiliação com o Google nem é endossada por ele.
Sumário
- Introdução
- Modelo suportado
- Preços
- Gere um vídeo em duas chamadas de API
- Modos de referência
- Vozes e personagens
- Estenda um vídeo com o Omni 1.2
- Gere em lote com um script
- Exemplos
- Perguntas frequentes
- Conclusão
Introdução
O Gemini Omni Flash gera vídeo com IA e áudio nativo com um único curl pela Google Flow API da useapi.net, com vozes sincronizadas, personagens e edições de vídeo para vídeo, na sua assinatura do site Google Flow (Google AI: Omni 1.1 Flash no Plus, Pro ou Ultra, Omni 1.2 Flash em todos os planos, e em contas gratuitas até 10 segundos). Um vídeo de 10 segundos em 720p custa $0,12 no Google AI Ultra $199,99, contra $1,01 na Gemini API oficial (em novembro de 2026), sem projeto no Google Cloud nem chave de API. A mesma assinatura também gera vídeos Omni no Google Vids, sem gastar créditos do Flow.
Na interface, o Google Flow chama este modelo de Omni 1.1 Flash, e o valor na API é model: "omni-flash". O Omni 1.2 Flash, que o Google lançou em 5 de novembro de 2026, é outro valor, model: "omni-flash-1.2": qualquer duração de 3 a 15 segundos, créditos por segundo, e é o modelo que estende um vídeo em até 10 segundos por vez. Código que envia omni-flash continua recebendo o 1.1. Para vídeos de 15 segundos, extensão com vozes e o preço por vídeo no Google Flow e no Google Vids, veja Como usar a API do Gemini Omni 1.2 Flash.
O Omni Flash é a mesma Google Flow API do Veo, escolhido com um único campo — model: "omni-flash-1.2" ou model: "omni-flash" —, mas é outro tipo de modelo. O Veo é um motor forte de texto para vídeo simples, e o Omni Flash tem áudio nativo e aceita muitas referências:
- O som é gerado junto com o vídeo — narração, diálogo e ambiente saem do mesmo modelo, não são acrescentados depois.
- Vozes — 30 presets de voz do sistema, mais vozes personalizadas que você cria com o seu próprio diálogo e notas de interpretação, passadas em
referenceAudio_1..5. - Referências de imagem e de vídeo — até 10 imagens de referência no Omni 1.2 Flash (7 no Omni 1.1 Flash) para referência para vídeo (R2V), e edição de vídeo para vídeo (V2V) de um vídeo enviado ou gerado com
referenceVideo_1. - Personagens reutilizáveis — junte imagens de referência e uma voz em um personagem com nome e use-o em vários vídeos.
- Vídeos mais longos — o Omni 1.1 Flash gera 4, 6, 8 ou 10 segundos e o Omni 1.2 Flash qualquer duração de 3 a 15 (o Veo para em
8), e o Omni 1.2 estende um vídeo em etapas de até 10 segundos.
Este guia mostra o fluxo em duas chamadas com curl para copiar e colar, os modos de referência que tornam o Omni Flash diferente e, depois, um script Node.js pronto para rodar que gera em lote a partir de uma lista de prompts. Para texto para vídeo simples, veja o tutorial do Veo 3.1 irmão deste.
Modelo suportado
Este guia usa os modelos de vídeo com áudio nativo do Google, Omni 1.1 Flash (model: "omni-flash") e Omni 1.2 Flash (model: "omni-flash-1.2"). No Omni 1.1, o custo em créditos cresce com a duração do vídeo:
| Duração | Créditos por vídeo |
|---|---|
| 4s | 7 |
| 6s | 10 |
| 8s | 12 |
| 10s | 15 |
| Edição V2V | 20 |
Esses são os custos no 720p padrão. Passar resolution: "360p" custa cerca da metade — 4 / 5 / 6 / 7 créditos pela duração, 10 por uma edição V2V —, e um vídeo em 360p pode ser levado a 720p depois com POST /videos/upscale.
O Omni 1.2 Flash (model: "omni-flash-1.2") aceita qualquer número inteiro de segundos de 3 a 15 e cobra por segundo, igual em todos os planos:
| Duração | 720p | 360p |
|---|---|---|
| 3s | 9 | 3 |
| 8s | 24 | 8 |
| 10s | 30 | 10 |
| 15s | 60 | 20 |
Até 10 segundos são 3 créditos por segundo em 720p e 1 em 360p, e cada segundo acima de 10 custa o dobro. Uma edição V2V custa o mesmo que um vídeo com a duração editada, e o 1.2 edita até 15 segundos de um clipe. Contas Google gratuitas podem usar o 1.2 para vídeos de até 10 segundos.
O Omni Flash aceita cinco modos no POST /videos: texto para vídeo simples (T2V), referência para vídeo (R2V) com até 10 referenceImage_1..10 no Omni 1.2 Flash (7 no Omni 1.1 Flash), imagem para vídeo a partir de um quadro inicial com startImage (I2V), imagem para vídeo entre um quadro inicial e um final com startImage + endImage (I2V-FL), e edição de vídeo para vídeo (V2V) de um MP4 enviado com referenceVideo_1. Veja Modos de referência abaixo.
Preços
O Omni 1.1 Flash exige uma assinatura paga do Google AI (Plus, Pro ou Ultra), o Omni 1.2 Flash funciona em contas gratuitas até 10 segundos, e a useapi.net não cobra nada por vídeo. Cada vídeo gasta créditos do Flow pela duração (veja Modelo suportado). O preço por vídeo em cada plano, comparado com a Gemini API oficial, está na tabela de preços do Google Flow.
Um plano Google AI também vem com uma cota mensal no Google Vids, que gera vídeos de até 15 segundos com o Gemini Omni 1.2 Flash (anúncio do Google) e não usa créditos do Flow. No Ultra $199,99 (que o Google chama de Ultra 20x) são 10.000 segundos de vídeo por mês — cerca de 1.000 clipes de 10 segundos em 1080p a mais, além dos ~1.666 clipes Omni 1.1 Flash de dez segundos (ou ~833 clipes Omni 1.2 Flash em 720p) que os 25.000 créditos do Flow compram. Esses segundos são divididos entre todos de um plano família do Google. Os outros planos recebem menos (veja a página Gemini no Google Vids do Google), e as contas gratuitas que testamos não tinham nenhum. A Google Vids API usa essa cota sem captcha. Conecte a conta com Setup Google Vids.
Gere um vídeo em duas chamadas de API
Você precisa de um token da API da useapi.net e de uma conta Google Flow conectada, com uma assinatura paga do Google AI (Plus, Pro ou Ultra) para o exemplo com Omni 1.1 Flash abaixo (o Omni 1.2 Flash também roda em conta gratuita, até 10 segundos). Jobs de vídeo são assíncronos: passe async: true e a chamada de criação devolve um jobid na hora, depois você consulta até o vídeo ficar pronto.
- Envie o job —
POST https://api.useapi.net/v1/google-flow/videoscommodel: "omni-flash":
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"prompt": "A lighthouse keeper narrates the coming storm, waves crashing below",
"model": "omni-flash",
"aspectRatio": "landscape",
"duration": 10,
"async": true
}'
Como o Omni Flash tem áudio nativo, ele gera a trilha sonora — a narração do faroleiro e as ondas quebrando — direto do prompt, sem nenhum parâmetro de áudio. No modo assíncrono, a resposta volta na hora com 201 Created, um jobid em minúsculas e status: "created":
{
"jobid": "j1731859234567v-u12345-email:jo***@gmail.com-bot:google-flow",
"type": "video",
"status": "created",
"created": "2026-06-15T12:34:56.789Z",
"request": {
"async": true,
"prompt": "A lighthouse keeper narrates the coming storm, waves crashing below",
"model": "omni-flash",
"aspectRatio": "landscape",
"duration": 10
}
}
- Consulte o resultado —
GET https://api.useapi.net/v1/google-flow/jobs/{jobId}:
curl "https://api.useapi.net/v1/google-flow/jobs/JOBID" \
-H "Authorization: Bearer $USEAPI_TOKEN"
O status passa por created → started → completed. Quando chega a completed, o MP4 pronto está em response.media[].videoUrl (um único prompt com count > 1 devolve uma entrada por vídeo):
{
"jobid": "j1731859234567v-...",
"type": "video",
"status": "completed",
"response": {
"media": [
{
"mediaGenerationId": "user:12345-email:6a6f...-video:a1d95d21-...",
"videoUrl": "https://flow-content.google/video/a1d95d21-...?Expires=...",
"thumbnailUrl": "https://flow-content.google/image/a1d95d21-...?Expires=..."
}
],
"remainingCredits": 18760
}
}
O Omni Flash costuma terminar em 60–180 segundos. O videoUrl assinado vale por cerca de 6 horas, então baixe logo. Não quer ficar consultando? Passe um replyUrl no corpo da criação para receber um webhook quando o job terminar.
Modos de referência
O grande diferencial do Omni Flash é quanta coisa você pode passar como referência. Todas as referências vêm do endpoint de upload POST /assets/email (ou, no caso de personagens, do POST /characters). A resposta do upload traz o id de referência aninhado em mediaGenerationId.mediaGenerationId — é essa string aninhada que você passa de volta. Os modos aceitos pelo omni-flash (da documentação do POST /videos):
| Modo | Parâmetro(s) | Limite no Omni Flash |
|---|---|---|
| Referência para vídeo (R2V) | referenceImage_1..10 | até 10 imagens no omni-flash-1.2, 7 no omni-flash |
| Imagem para vídeo (I2V) | startImage | um quadro inicial |
| Imagem para vídeo, inicial + final (I2V-FL) | startImage + endImage | só os dois quadros — sem outras referências |
| Edição de vídeo para vídeo (V2V) | referenceVideo_1 (+ startFrameIndex_1 / endFrameIndex_1 opcionais) | um MP4 enviado |
| Personagens | character_1..10 (do POST /characters) | 10 no total com referenceImage_* no omni-flash-1.2, 7 no omni-flash |
| Narração por voz | referenceAudio_1..5 | até 5 (R2V) / até 3 (edição V2V) |
Referência para vídeo (R2V) — envie uma ou mais imagens e passe-as como referenceImage_1, referenceImage_2, … (os slots _4.._7 são só do Omni Flash). Imagens de referência não podem ser combinadas com startImage/endImage.
# 1. Upload a reference image — the id is nested one level deep:
curl -X POST "https://api.useapi.net/v1/google-flow/assets/john%40gmail.com" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: image/jpeg" \
--data-binary @subject.jpeg
# Response: { "mediaGenerationId": { "mediaGenerationId": "user:12345-email:...-image:..." }, ... }
# 2. Generate, narrated by a system voice
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "omni-flash",
"prompt": "the subject walks toward camera and introduces herself",
"duration": 8,
"referenceImage_1": "user:12345-email:...-image:...",
"referenceAudio_1": "Kore"
}'
Edição de vídeo para vídeo (V2V) — envie um MP4 com Content-Type: video/mp4 para o POST /assets e passe o id devolvido como referenceVideo_1 para editar esse vídeo. Isso é só do Omni Flash e coloca a requisição no modo V2V. duration não é aceito (a duração da saída segue a janela de corte da entrada, no máximo 10 s no omni-flash e 15 s no omni-flash-1.2). Se quiser, corte a origem em uma linha do tempo virtual de 24 fps com startFrameIndex_1 (0–239) e endFrameIndex_1 (1–240, em que 240 = 10 s), ou até 359 e 360 (15 s) no omni-flash-1.2:
curl -X POST "https://api.useapi.net/v1/google-flow/videos" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "omni-flash",
"prompt": "restyle the scene as a rainy neon-lit night, add ambient city sound",
"referenceVideo_1": "user:12345-email:...-video:...",
"startFrameIndex_1": 0,
"endFrameIndex_1": 240,
"async": true
}'
Você também pode colocar marcadores @ inline no prompt que apontam para os slots que você passa — por exemplo @referenceImage_1, @character_1, @referenceAudio_1. Os marcadores são opcionais e cada um precisa de um parâmetro correspondente no corpo. (@referenceVideo_* não funciona inline — passe-o como campo do corpo.) Veja Inline @-mention markers.
Vozes e personagens
Duas chamadas curtas de configuração alimentam os recursos de áudio e identidade do Omni Flash.
Vozes personalizadas — o POST /voices salva uma voz baseada em um dos 30 presets do sistema, com o seu próprio dialog e voicePerformance. O campo voice da resposta é um id de referência que você passa depois como referenceAudio_1..5 no POST /videos:
curl -X POST "https://api.useapi.net/v1/google-flow/voices" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"voice": "Achernar",
"dialog": "Welcome to the demo.",
"voicePerformance": "Cheerful, energetic delivery",
"displayName": "Cheerful Narrator"
}'
# Response: { "voice": "user:12345-email:...-voice:...-mid:...", ... }
Cada slot referenceAudio_* aceita o nome de uma voz do sistema (sem diferenciar maiúsculas, um dos 30 presets, como Aoede, Kore, Puck, Zephyr) ou o id de uma voz de usuário da chamada acima. Criar uma voz exige reCAPTCHA (resolvido automaticamente — veja as perguntas frequentes).
Personagens reutilizáveis — o POST /characters junta 1–2 imagens de referência e uma voz opcional em um personagem com nome. O campo character da resposta é o que você passa como character_1..10:
curl -X POST "https://api.useapi.net/v1/google-flow/characters" \
-H "Authorization: Bearer $USEAPI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"displayName": "Carol",
"imageReference_1": "user:12345-email:...-image:abc123...",
"voice": "user:12345-email:...-voice:...-mid:..."
}'
# Response: { "character": "user:12345-email:...-character:...-imgs:1-voice:...", ... }
No Omni Flash, os personagens funcionam em todas as durações e dividem o limite total de 10 imagens de referência com referenceImage_* (referenceImages.length + Σ(character.imgs) ≤ 10, Omni 1.1: 7). O POST /characters não exige captcha. Personagens e vozes também funcionam no Veo, mas com limites mais apertados (3 imagens, 1 voz, só 8 s) — veja What can use a character?.
Estenda um vídeo com o Omni 1.2
O POST /videos/extend acrescenta de 3 a 10 segundos por vez com o Omni 1.2 Flash a qualquer vídeo com menos de 30 segundos, com até 3 vozes, imagens de referência ou personagens, e devolve um único clipe contínuo (só em planos pagos). O passo a passo em Como usar a API do Gemini Omni 1.2 Flash estende um vídeo de 10 segundos duas vezes até 30 segundos, com duas vozes, o exemplo abaixo.
Gere em lote com um script
Achar a tomada certa leva muitas tentativas, e rodar tudo à mão cansa. O script Node.js abaixo lê uma lista de prompts do prompts.json, envia as imagens de referência, os quadros inicial e final ou os vídeos de origem, repassa direto os ids character_* e a resolution, envia cada job no modo assíncrono com model: "omni-flash" e depois consulta e baixa cada MP4 pronto — assim você coloca um lote na fila e volta depois para escolher os melhores. Ele para com um aviso antes de enviar uma combinação que a API recusaria, como um quadro final sem quadro inicial.
Você precisa do Node.js v21 ou mais novo. Coloque o prompts.json e o omni-flash.mjs na mesma pasta e rode node ./omni-flash.mjs API_TOKEN EMAIL, em que API_TOKEN é o seu token da API da useapi.net e EMAIL é o email da sua conta Google Flow conectada. O script encontra a conta pelo email automaticamente e verifica o campo health dela antes de enviar.
Prefere clonar e rodar localmente? O omni-flash.mjs e o prompts.json completos estão no GitHub, em useapi/google-flow-api.
Trabalha no Claude Code ou no Codex? A agent skill omni-flash-video deixa o agente fazer tudo isso por você: descreva o vídeo, aponte as suas imagens, e ele as envia, escreve a requisição, espera e salva o .mp4. Instale com npx skills add useapi/google-flow-api --skill omni-flash-video.
Abrir prompts.json
[
{
"prompt": "Audio-native text-to-video. By default model omni-flash with a landscape 8-second clip will be used."
},
{
"duration": 10,
"prompt": "A 10-second clip (omni-flash only). Omni Flash is audio-native, so narration, dialogue and sound come from the prompt. To pin a specific voice, add referenceAudio_1 together with a referenceImage_* or character_*, as in the next prompt."
},
{
"duration": 8,
"referenceImage_1": "./subject.jpeg",
"referenceAudio_1": "Kore",
"prompt": "Reference-to-video (R2V): the uploaded image is a reference, narrated by 'Kore'. Up to 7 referenceImage_* on omni-flash, 10 on omni-flash-1.2."
},
{
"duration": 8,
"startImage": "./first_image.jpeg",
"endImage": "./last_image.jpeg",
"prompt": "Image-to-video with a start frame (startImage) and an end frame (endImage). The two frames are the only input — no referenceImage_*, character_* or referenceAudio_* alongside them."
},
{
"referenceVideo_1": "./source.mp4",
"startFrameIndex_1": 0,
"endFrameIndex_1": 240,
"prompt": "Video-to-video edit (V2V): omni-flash restyles the uploaded MP4. No duration here — output matches the trim window (max 10 s). For all parameters see https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos"
}
]
Abrir o script omni-flash.mjs
/*
Script version 1.3, November 11, 2026
Script to batch-generate audio-native videos with Gemini Omni Flash using the Google Flow API by useapi.net
Uses the POST /videos endpoint in async mode (model: omni-flash by default, or omni-flash-1.2 per prompt) and polls GET /jobs/{jobId}.
For more details visit https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos
Installation Instructions:
==========================
You need Node.js v21 or newer installed to run this script. Download and install Node.js from:
- Windows, macOS, Linux: https://nodejs.org/
After installation, verify by running the following command in a terminal:
node -v
Running the Script:
===================
Usage: node omni-flash.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]
Replace API_TOKEN with your actual useapi.net API token, see https://useapi.net/docs/start-here/setup-useapi
Replace EMAIL with configured Google Flow email account, see https://useapi.net/docs/start-here/setup-google-flow
If optional PROMPTS_FILE not provided prompts.json will be used.
Example:
--------
node omni-flash.mjs user:1234-abcdefhijklmnopqrstuv [email protected]
This command executes the script using API token user:1234-abcdefhijklmnopqrstuv with [email protected] Google Flow account email.
Changelog:
==========
- June 15, 2026: Initial release.
- September 11, 2026: Adds resolution, startImage / endImage (start and end frames, local files uploaded for you) and character_1..7. Accepts .jpg and upper-case file extensions. Stops with a warning before submitting combinations the API rejects, such as endImage without startImage or referenceAudio_* on its own. A prompt whose local startImage, endImage, referenceImage_* or referenceVideo_1 file fails to upload is skipped and logged to omni-flash_errors.txt instead of being submitted without it. Saves a downloaded video under its final name only once it has been fully received, so a failed download leaves no partial MP4 for the next run to skip as "already exists".
- November 11, 2026: referenceImage_8..10 and character_8..10: omni-flash-1.2 takes up to 10 references (omni-flash 7).
*/
import readline from 'node:readline';
import fs from 'fs/promises';
import path from 'path';
import { writeFile } from 'node:fs/promises';
import { Readable } from 'node:stream';
// Constants
const RESULTS_FILE = 'omni-flash_results.txt';
const ERRORS_FILE = 'omni-flash_errors.txt';
const DEFAULT_PROMPTS_FILE = 'prompts.json';
const MODEL = 'omni-flash';
// Reference slots: up to 10 on omni-flash-1.2, 7 on omni-flash, 3 on Veo (referenceImage_* and character_* together).
const imageRefKeys = Array.from({ length: 10 }, (_, i) => `referenceImage_${i + 1}`);
const characterKeys = Array.from({ length: 10 }, (_, i) => `character_${i + 1}`);
const SLEEP_429 = 30 * 1000; // in milliseconds
const SLEEP_POLL = 15 * 1000; // in milliseconds
const urlAccounts = 'https://api.useapi.net/v1/google-flow/accounts';
const urlVideos = 'https://api.useapi.net/v1/google-flow/videos';
const urlJobs = 'https://api.useapi.net/v1/google-flow/jobs/';
const urlUploadAsset = 'https://api.useapi.net/v1/google-flow/assets/';
// Google Flow accepts png, jpeg and webp images, and mp4 video (for omni-flash V2V edit).
const supportedImageExtensions = ['png', 'jpg', 'jpeg', 'webp'];
const supportedVideoExtensions = ['mp4'];
// { filename: mediaGenerationId }
const uploadedFiles = {};
// Utility to sleep for given milliseconds
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Function to fetch configured Google Flow API accounts
async function fetchAccounts(apiToken) {
const response = await fetch(urlAccounts, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.error(`⛔ Error fetching accounts (HTTP ${response.status}): ${response.statusText}`);
process.exit(1);
}
return response.json();
}
const elapsedTimeSec = (start) => (Date.now() - start) / 1000;
// Map a file extension to the Content-Type required by POST /assets
const contentTypeForExt = (ext) =>
ext === 'png' ? 'image/png' :
ext === 'webp' ? 'image/webp' :
ext === 'mp4' ? 'video/mp4' :
'image/jpeg';
async function uploadAsset(apiToken, email, filename) {
// Check if already uploaded
if (uploadedFiles.hasOwnProperty(filename))
return uploadedFiles[filename];
const startTime = Date.now();
console.log(`⬆️ Account ${email} uploading file…`, filename);
const body = new Blob([await fs.readFile(filename)]);
const fileExt = filename.split('.').pop().toLowerCase();
const response = await fetch(`${urlUploadAsset}${encodeURIComponent(email)}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`,
'Content-Type': contentTypeForExt(fileExt)
},
body
});
if (response.ok) {
const json = await response.json();
// POST /assets returns the reference id nested as mediaGenerationId.mediaGenerationId
const mediaGenerationId = json?.mediaGenerationId?.mediaGenerationId;
console.log(`🆗 mediaGenerationId (${elapsedTimeSec(startTime)} sec)`, mediaGenerationId);
uploadedFiles[filename] = mediaGenerationId;
}
else {
console.error(`❗ Unable to upload file HTTP ${response.status} (${elapsedTimeSec(startTime)} sec)`, await response.text());
// Do not attempt to upload failed file again
uploadedFiles[filename] = undefined;
}
return uploadedFiles[filename];
}
async function submit(apiToken, url, body, index, prompt) {
const createResponse = await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiToken}`
},
body
});
const createBody = await createResponse.text();
// Async POST /videos returns 201 Created with a jobid to poll.
if (createResponse.status == 201) {
const json = JSON.parse(createBody);
// 201 async payload uses lowercase "jobid"; sync 200 uses "jobId". Accept either.
const jobId = json.jobid ?? json.jobId;
if (jobId) {
await fs.appendFile(RESULTS_FILE, `${jobId},#${index}:${prompt}\n`);
console.log(`✅ jobId`, jobId);
return 201;
} else {
const error = `No jobid found in HTTP 201 response`;
console.log(`❓ ${error}`, createBody);
await fs.appendFile(ERRORS_FILE, `${error},#${index}:${prompt}\n`);
return 500;
}
} else {
switch (createResponse.status) {
case 429:
console.log(`🔄️ Retry on HTTP ${createResponse.status}`, createBody);
break;
case 503:
console.log(`🔄️ Service unavailable, retry on HTTP ${createResponse.status}`, createBody);
break;
case 400:
console.log(`🛑 Rejected request (validation or content policy)`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
break;
case 402:
console.log(`🛑 No subscription / insufficient credits`, createBody);
break;
default:
console.log(`❗ FAILED with HTTP ${createResponse.status}`, createBody);
await fs.appendFile(ERRORS_FILE, `${createResponse.status},#${index}:${prompt}\n`);
}
return createResponse.status;
}
}
// Submit a single prompt to POST /videos with model omni-flash (or the prompt's own model) in async mode.
// startImage = start frame (I2V), endImage = end frame (I2V-FL, requires startImage) — the frames are the only input;
// referenceImage_1..10 = R2V image refs; character_1..10 = POST /characters ids (10 on omni-flash-1.2, 7 on omni-flash);
// referenceVideo_1 = V2V edit (no duration); referenceAudio_1..5 = system voice name or POST /voices user-voice id;
// resolution = 720p (default) or 360p; model = 'omni-flash' (Omni 1.1, the default) or 'omni-flash-1.2' (3–15 s, per-second credits).
async function submitVideo(apiToken, email, prompt, index) {
const {
prompt: text, model = MODEL, aspectRatio, duration, resolution, count,
startImage, endImage,
referenceVideo_1, startFrameIndex_1, endFrameIndex_1,
referenceAudio_1, referenceAudio_2, referenceAudio_3, referenceAudio_4, referenceAudio_5
} = prompt;
console.log(`🚀 ${model} » Prompt #${index} • account ${email} …`);
// Upload any local image/video files and swap the paths for their mediaGenerationId.
const resolveRef = async (ref) =>
ref && (ref.startsWith('./') || ref.includes('.'))
&& [...supportedImageExtensions, ...supportedVideoExtensions].includes(ref.split('.').pop().toLowerCase())
? await uploadAsset(apiToken, email, ref)
: ref;
const startImageId = await resolveRef(startImage);
const endImageId = await resolveRef(endImage);
const refImages = {};
for (const key of imageRefKeys)
refImages[key] = await resolveRef(prompt[key]);
const refVideo_1 = await resolveRef(referenceVideo_1);
// A local file that failed to upload would be left out of the body, and the prompt would still be
// generated (and charged) without it. Skip the prompt instead. uploadAsset remembers the failed file,
// so a later prompt using it is skipped too, without another upload attempt.
const failedUploads = [
['startImage', startImage, startImageId], ['endImage', endImage, endImageId],
...imageRefKeys.map(key => [key, prompt[key], refImages[key]]),
['referenceVideo_1', referenceVideo_1, refVideo_1]
].filter(([, file, id]) => file && !id);
for (const [param, file] of failedUploads) {
console.error(`🛑 Prompt #${index} skipped: ${param} ${file} failed to upload`);
await fs.appendFile(ERRORS_FILE, `Upload failed for ${param} ${file},#${index}:${text}\n`);
}
if (failedUploads.length)
return;
// V2V edit does not accept duration — output matches the trim window.
const isV2V = !!refVideo_1;
const body = JSON.stringify({
model,
email,
prompt: text,
aspectRatio,
duration: isV2V ? undefined : duration,
resolution,
count,
startImage: startImageId,
endImage: endImageId,
...refImages,
...Object.fromEntries(characterKeys.map(key => [key, prompt[key]])),
referenceVideo_1: refVideo_1,
startFrameIndex_1,
endFrameIndex_1,
referenceAudio_1,
referenceAudio_2,
referenceAudio_3,
referenceAudio_4,
referenceAudio_5,
async: true
});
return await submit(apiToken, urlVideos, body, index, text);
}
// Function to download videos
async function download(apiToken) {
if (! await fileExists(RESULTS_FILE)) return;
try {
const resultsContent = await fs.readFile(RESULTS_FILE, 'utf8');
const lines = resultsContent.trim().split('\n');
for (const line of lines) {
const [jobId, prompt] = line.split(',');
console.log(`👉 ${jobId}`);
while (true) {
const response = await fetch(`${urlJobs}${jobId}`, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${apiToken}`
}
});
if (!response.ok) {
console.log(`🛑 Poll failed ${jobId} (HTTP ${response.status}):\n${prompt}\n`, await response.text());
break;
}
const jobBody = await response.json();
const { status, error, response: jobResponse } = jobBody;
if (status == 'failed') {
console.error(`🛑 FAILED ${jobId} (${error}):\n${prompt}\n`);
break;
}
if (status == 'completed') {
const media = jobResponse?.media ?? [];
if (media.length == 0)
console.error(`🛑 Completed but no media for ${jobId}:\n${prompt}\n`);
// A single prompt with count > 1 returns multiple videos.
for (let i = 0; i < media.length; i++) {
const url = media[i]?.videoUrl;
const videoFilename = `${jobId.replace(/[:*]/g, '_')}_${i + 1}.mp4`;
try {
await fs.access(videoFilename);
console.log(`⚠️ ${videoFilename} already exists. Skipping download.`);
continue;
} catch {
// File does not exist, proceed with downloading
}
if (url) {
console.log(`✅ Downloading ${url} to ${videoFilename}`);
// Stream into a temporary file and rename it once the whole video has arrived, so a download
// that drops mid-stream leaves no partial file behind for the next run to skip as "already exists".
const tempFilename = `${videoFilename}.part`;
try {
const videoResponse = await fetch(url);
if (!videoResponse.ok) {
console.error(`⛔ Unable to download ${jobId} (HTTP ${videoResponse.status}):\n${prompt}\n`, url);
continue;
}
const stream = Readable.fromWeb(videoResponse.body);
await writeFile(tempFilename, stream);
await fs.rename(tempFilename, videoFilename);
} catch (err) {
await fs.rm(tempFilename, { force: true });
console.error(`⛔ Error during download: ${err}`);
}
} else
console.error(`🛑 No videoUrl for ${jobId} media #${i + 1}:\n${prompt}\n`);
}
break;
}
console.log(`⌛ ${jobId} status (${status}) and is still in progress, waiting…`);
await sleep(SLEEP_POLL);
}
}
} catch (error) {
console.log(`⛔ Error during download:`, error.stack || error);
}
}
// Main function
async function main() {
const apiToken = process.argv[2];
const email = process.argv[3];
const promptFile = process.argv[4] || DEFAULT_PROMPTS_FILE;
if (!apiToken || !email) {
console.error('Usage: node omni-flash.mjs <API_TOKEN> <EMAIL> [PROMPTS_FILE]');
process.exit(1);
}
console.info('Script v1.3');
console.info('Node version is: ' + process.version);
try {
if (await fileExists(RESULTS_FILE)) {
let user_input;
while (!['y', 'n'].includes(user_input)) {
user_input = (await promptUser(`❔ ${RESULTS_FILE} file detected. Do you want to download the results now? (y/n): `))?.toLowerCase();
if (user_input == 'y') {
await download(apiToken);
await fs.unlink(RESULTS_FILE);
}
}
}
const start = new Date();
try {
console.info('START EXECUTION', start);
await execute(apiToken, email, promptFile); // Pass the promptFile to execute function
}
finally {
console.info('COMPLETED', new Date());
console.info('EXECUTION ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
try {
console.info('START DOWNLOAD', start);
await download(apiToken);
}
finally {
console.info('TOTAL ELAPSED', diffInMinutesAndSeconds(start, new Date()));
}
} catch (error) {
console.error('⛔ Error during execution:', error.stack || error);
}
}
// Modify the execute function to accept promptFile as a parameter
async function execute(apiToken, email, promptFile) {
const accounts = await fetchAccounts(apiToken);
console.info(`Configured Google Flow API accounts (${Object.keys(accounts).length}):`, Object.keys(accounts).join(', '));
if (Object.keys(accounts).length <= 0) {
console.error(`⛔ No configured Google Flow accounts found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
if (!accounts[email]) {
console.error(`⛔ Account ${email} not found. Please refer to https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
if (accounts[email].health && accounts[email].health !== 'OK') {
console.error(`⛔ Account ${email} health is '${accounts[email].health}'. Please resolve and update the account, see https://useapi.net/docs/start-here/setup-google-flow`);
process.exit(1);
}
const promptData = await fs.readFile(promptFile, 'utf8');
const prompts = JSON.parse(promptData);
console.log(`Total number of prompts to process`, prompts.length);
let warnings = [];
// Parameters accepted by this script for the omni-flash POST /videos endpoint.
// See https://useapi.net/docs/api-google-flow-v1/post-google-flow-videos for the full parameter set.
const supportedParams = [
'prompt', 'model', 'aspectRatio', 'duration', 'resolution', 'count',
'startImage', 'endImage',
...imageRefKeys, ...characterKeys,
'referenceVideo_1', 'startFrameIndex_1', 'endFrameIndex_1',
'referenceAudio_1', 'referenceAudio_2', 'referenceAudio_3', 'referenceAudio_4', 'referenceAudio_5'
];
const audioRefKeys = ['referenceAudio_1', 'referenceAudio_2', 'referenceAudio_3', 'referenceAudio_4', 'referenceAudio_5'];
const invalidKeys = (prompt) => Object.keys(prompt).filter(key => !key.startsWith('__') && !supportedParams.includes(key))
// Validate a local file path reference (skip already-uploaded mediaGenerationId strings).
const looksLikePath = (ref) => ref && (ref.startsWith('./') || (ref.includes('.') && !ref.startsWith('user:')));
for (let i = 1; i <= prompts.length; i++) {
const prompt = prompts[i - 1];
const { prompt: text, referenceVideo_1, duration, startImage, endImage } = prompt;
const validateFile = async (file, allowedExts) => {
if (looksLikePath(file)) {
try {
await fs.access(file);
} catch {
warnings.push(`⚠️ File '${file}' does not exist. Prompt ${i}`);
}
const ext = file.split('.').pop().toLowerCase();
if (!allowedExts.includes(ext))
warnings.push(`⚠️ File ${file} extension ${ext} not supported. Prompt ${i}`);
}
};
const notSupported = invalidKeys(prompt);
if (notSupported.length)
warnings.push(`⚠️ Following params not supported: ${notSupported.join(',')}. Prompt ${i}`);
if (!text)
warnings.push(`⚠️ prompt is required. Prompt ${i}`);
// V2V edit (referenceVideo_1) does not accept duration — output matches the trim window.
if (referenceVideo_1 && duration)
warnings.push(`⚠️ duration is not accepted with referenceVideo_1 (V2V edit). Prompt ${i}`);
if (endImage && !startImage)
warnings.push(`⚠️ endImage requires startImage (end-frame-only is not supported). Prompt ${i}`);
// Start/end frames (I2V, I2V-FL) are the only input in that mode — the API rejects anything alongside them.
if (startImage || endImage) {
if (referenceVideo_1)
warnings.push(`⚠️ startImage/endImage cannot be combined with referenceVideo_1 (V2V edit). Prompt ${i}`);
if ([...imageRefKeys, ...characterKeys].some(k => prompt[k]))
warnings.push(`⚠️ startImage/endImage cannot be combined with referenceImage_* or character_*. Prompt ${i}`);
if (audioRefKeys.some(k => prompt[k]))
warnings.push(`⚠️ startImage/endImage cannot be combined with referenceAudio_*. Prompt ${i}`);
}
// A voice needs a referenceImage_* or character_* to attach to (or a V2V edit) — the API rejects referenceAudio_* on its own.
// With start/end frames the check above already applies.
if (!startImage && !endImage && !referenceVideo_1 && audioRefKeys.some(k => prompt[k]) && ![...imageRefKeys, ...characterKeys].some(k => prompt[k]))
warnings.push(`⚠️ referenceAudio_* requires a referenceImage_* or character_* (or referenceVideo_1 for a V2V edit). Prompt ${i}`);
await Promise.all([
validateFile(referenceVideo_1, supportedVideoExtensions),
...['startImage', 'endImage', ...imageRefKeys].map(k => validateFile(prompt[k], supportedImageExtensions))
]);
}
if (warnings.length > 0) {
warnings.forEach(warning => console.warn(warning));
console.error(`⛔ Execution stopped due to warnings.`);
process.exit(1);
}
for (let i = 0; i < prompts.length; i++) {
const prompt = prompts[i];
while (true) {
const responseCode = await submitVideo(apiToken, email, prompt, i + 1);
if (responseCode == 429 || responseCode == 503)
await sleep(SLEEP_429);
else
if (responseCode == 402) {
process.exit(1);
} else
break;
}
}
}
// Utility function to check if a file exists
async function fileExists(path) {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
// Function to prompt user input
async function promptUser(query) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => rl.question(query, answer => {
rl.close();
resolve(answer);
}));
}
function diffInMinutesAndSeconds(date1, date2) {
const diffInSeconds = Math.floor((date2 - date1) / 1000);
return `${Math.floor(diffInSeconds / 60)} minutes ${diffInSeconds % 60} seconds`;
};
main();
Exemplos
Os vídeos abaixo são resultados reais do Gemini Omni Flash feitos por esta Google Flow API — model: "omni-flash-1.2" e model: "omni-flash", extensão, vozes, referências, personagens e edição V2V.
Extensão no Omni 1.2 — um vídeo de 10 segundos estendido duas vezes até 30 segundos, com duas vozes
De Google Flow: Omni 1.2 Flash, 15-Second Videos, and Extend with Voices (em inglês) — omni-flash-1.2 nos primeiros 10 segundos, depois duas chamadas POST /videos/extend com referenceAudio_1.
Edição V2V — transforma um MP4 enviado em desenho animado, com o mesmo movimento e enquadramento
De Google Flow: Gemini Omni Flash (em inglês) — edição com referenceVideo_1.
Menções @ inline — vídeo noir R2V com @character_1, @referenceImage_1, @referenceImage_2 e uma voz personalizada
De Google Flow: Inline @-mentions (em inglês) — marcadores de menção @ inline ancorando cada referência.
Personagens + edição V2V — coloca dois personagens reutilizáveis no vídeo noir
De Google Flow: Inline @-mentions (em inglês) — edição V2V com referenceVideo_1 e @character_* inline. Veja também Reusable Voices and Characters (em inglês) para o fluxo de criação de vozes e personagens.
Perguntas frequentes
- O que é o Gemini Omni Flash? Com a useapi.net você roda o Gemini Omni Flash, o modelo de vídeo com áudio nativo do Google no Google Flow, definindo
model: "omni-flash-1.2"(Omni 1.2 Flash) oumodel: "omni-flash"(Omni 1.1 Flash) na Google Flow API. Ao contrário de um modelo de vídeo mudo, ele gera a trilha sonora, a narração, o diálogo e o ambiente como parte do mesmo vídeo, e aceita referências de imagem, vídeo e áudio, além de personagens reutilizáveis. - Qual a diferença entre o Omni Flash e o Veo 3.1? Com a useapi.net os dois rodam na mesma Google Flow API, escolhidos pelo campo
model. O Veo é o motor de texto para vídeo e imagem para vídeo simples com mais fidelidade, e o Omni Flash tem áudio nativo e aceita muitas referências, com edição de vídeo para vídeo (referenceVideo_1), várias vozes por vídeo, personagens reutilizáveis em qualquer duração e vídeos mais longos que os do Veo. Veja os limites exatos em Modos de referência. - Dá para usar a minha própria voz, um vídeo de referência ou um áudio de referência? Com a useapi.net, sim. Narre com as vozes prontas do sistema ou com vozes personalizadas do POST /voices, passadas como
referenceAudio_1..5, e envie um MP4 com POST /assets para editá-lo comoreferenceVideo_1(V2V). Veja os limites de cada modo em Modos de referência e Vozes e personagens. - Existe uma API oficial do Omni Flash? Com a useapi.net, o Omni 1.1 Flash custa bem menos do que na API do próprio Google: um vídeo de 8 segundos em 720p custa $0,10 no Google AI Ultra $199,99, contra $0,81 na Gemini API oficial, que vende o modelo por segundo desde 30 de junho de 2026 (em novembro de 2026). Ele roda por um endpoint REST padrão na sua assinatura do site Google Flow.
- O Omni Flash faz vídeos com mais de 10 segundos? Com a useapi.net, sim: o Omni 1.2 Flash (
model: "omni-flash-1.2") gera até 15 segundos em uma chamada, e o POST /videos/extend acrescenta até 10 segundos por vez com vozes, a $0,19 por um vídeo de 8 segundos em 720p no Google AI Ultra $199,99, contra $0,56 na API do próprio Google (em novembro de 2026). Todos os limites de duração e preços no Google Flow e no Google Vids estão em Como usar a API do Gemini Omni 1.2 Flash. - Preciso resolver captcha? Com a useapi.net você nunca resolve um captcha. O Google Flow pede um reCAPTCHA em cada vídeo e em cada voz nova, e o worker da useapi.net resolve automaticamente: a sua primeira conta Google Flow vem com 300 resoluções grátis nas nossas próprias chaves de AntiCaptcha, CapSolver e EzCaptcha, e depois disso você acrescenta as chaves do seu próprio provedor com POST /accounts/captcha-providers. O POST /characters não precisa de captcha.
Conclusão
Visite o nosso servidor no Discord ou o
canal no Telegram para qualquer dúvida de suporte.
Veja o nosso repositório no GitHub com exemplos de código.
Um assistente de IA também pode trabalhar com o Google Flow por você: o servidor MCP do Google Flow hospedado permite que o Claude Code, o Cursor e o VS Code conversem com o Google Flow Agent, que gera imagens e vídeos na sua conta.
Comece a usar a API
Uma assinatura de US$ 15 por mês dá acesso a todas as APIs da useapi.net: um token, todos os serviços. Você conecta até 3 contas suas por serviço, e a useapi.net cuida da API REST, do balanceamento de carga e do polling. Precisa de mais contas? Cada assinatura extra custa US$ 10 por mês e acrescenta mais 3 contas por serviço.
Assinar — US$ 15/mês Pagar com cripto
Não tem cartão de crédito internacional? Você também pode assinar com criptomoedas (pagamentos em cripto não são reembolsáveis). Preços e planos de volume estão na página de assinatura. Pagando pelo Stripe, você recebe reembolso integral em até 14 dias após a primeira compra, se tiver menos de 50 gerações bem-sucedidas. Já é assinante? Altere a quantidade no portal do cliente Stripe em vez de comprar de novo.