How to Generate Audio-Native AI Video with Gemini Omni Flash via the Google Flow API

7 min read β€’ June 15, 2026

Table of contents

  1. Introduction
  2. Supported model
  3. Pricing
  4. Generate a clip in two API calls
  5. Reference modes
  6. Voices & characters
  7. Batch-generate with a script
  8. Examples
  9. Frequently asked questions
  10. Conclusion

Introduction

Gemini Omni Flash generates audio-native AI video from a single curl through useapi.net. It handles synced voices, characters, and video-to-video edits on your own Google Flow account, with no Google Cloud project or API key. Omni Flash is Flow-only β€” the metered Gemini API doesn’t expose it at all, so a flat subscription is the only way to reach it programmatically.

Omni Flash is the same Google Flow API as Veo, picked with one field β€” model: "omni-flash" β€” but it is a different kind of model. Where Veo is a strong plain text-to-video engine, Omni Flash is audio-native and reference-rich:

  • Sound is generated as part of the clip β€” narration, dialog, and ambience come out of the same model, not bolted on after.
  • Voices β€” 30 built-in system voice presets plus custom voices you create from your own dialog and performance notes, fed in via referenceAudio_1..5.
  • Image and video references β€” up to 7 reference images for reference-to-video (R2V), and video-to-video (V2V) editing of an uploaded or generated clip via referenceVideo_1.
  • Reusable characters β€” bundle reference images + a voice into a named character and reuse it across generations.
  • 10-second clips β€” Omni Flash is the only Google Flow model that goes to 10 seconds (Veo tops out at 8).

This guide shows the two-call workflow with copy-paste curl, the reference modes that make Omni Flash distinctive, then a runnable Node.js script that batch-generates from a list of prompts. For plain text-to-video, see the sibling Veo 3.1 tutorial.

Supported model

This guide uses one model β€” omni-flash, Google’s audio-native video model β€” selected with model: "omni-flash". Credit cost scales with clip length:

Duration Credits per generation
4s 15
6s 20
8s 25
10s 30
V2V edit 40

Omni Flash accepts four modes on POST /videos: plain text-to-video (T2V), reference-to-video (R2V) with up to 7 referenceImage_1..7, image-to-video from a single start frame via startImage (first frame only, no endImage), and video-to-video editing (V2V) of an uploaded MP4 via referenceVideo_1. See Reference modes below.

Pricing

Omni Flash video needs a paid Google AI plan (Plus, Pro, or Ultra) plus a flat $15/month to useapi.net for API access β€” no per-call surcharge. Each clip spends Flow credits from your plan, scaling with clip length (see the Supported model table above).

Omni Flash is Flow-only β€” Google’s official Gemini video API lists only Veo β€” so the value is running it on your existing Flow subscription with no per-second metering. How Google Flow pricing through useapi.net compares to the official Gemini API across the catalog:

Third-party Google Flow API by useapi.net vs. the official Gemini API β€” drive your own Google Flow subscription instead of metered, per-call API billing:

Model Official Gemini API useapi.net (Flow Pro) useapi.net (Flow Ultra)
Veo 3.1 Fast β€” 8s clip $0.80 ~$0.40 ~$0.10
Veo 3.1 Quality β€” 8s clip $3.20 ~$2.00 ~$1.00
Veo 3.1 Lite β€” 8s clip $0.40 ~$0.20 ~$0.05
Veo 3.1 Lite, lower priority β€” β€” $0 (Ultra $199)
Gemini Omni Flash β€” 8s clip β€” (Flow only) ~$0.50 ~$0.25
Nano Banana Pro β€” per image $0.134 included included
Nano Banana β€” per image $0.039 included included
Imagen 4 β€” per image metered included included

Expected daily output β€” Ultra ($199/mo) plan. Daily averages observed on top real accounts. These are expected, not guaranteed: Google governs the underlying Flow allowances and they vary with demand.

Free workload (no Flow credits spent) Avg generations / day*
Veo 3.1 Lite β€” lower priority video ~1,000
Images β€” Nano Banana, Nano Banana Pro, Imagen 4 up to ~500

*Averages from the busiest real accounts β€” not guarantees. They are set by Google’s own allowances and fluctuate with demand. Applies only to the free lower-priority video queue and image generation. Credit-metered models (Veo 3.1 Fast / Quality and Gemini Omni Flash) are not shown here β€” their volume is bounded by your plan’s monthly Flow credits (see the credit table), not a free allowance.

Veo is billed per second on the Gemini API (an 8-second 720p clip shown) and images are priced per image. Through useapi.net you spend your own Flow plan credits and pay a flat $15/month β€” image generation is included on any Google AI plan, and there is no Google Cloud project, API key, or per-call metering.

Full per-tier credit costs are on the Google Flow overview.

Generate a clip in two API calls

You need a useapi.net API token and a connected Google Flow account on a paid Google AI plan (Plus, Pro, or Ultra). Generation is asynchronous: pass async: true and the create call returns a jobid immediately, then you poll until the video is ready.

  1. Submit the job β€” POST https://api.useapi.net/v1/google-flow/videos with model: "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
  }'

Omni Flash is audio-native, so it generates the soundtrack β€” the keeper’s narration and the crashing waves β€” straight from the prompt, no audio parameter required. In async mode the response returns immediately with 201 Created, a lowercase jobid, and 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
  }
}
  1. Poll for the result β€” 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"

status moves through created β†’ started β†’ completed. When it reaches completed, the finished MP4 is in response.media[].videoUrl (a single prompt with count > 1 returns one entry per clip):

{
  "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
  }
}

Omni Flash typically finishes in 60–180 seconds. The signed videoUrl is valid for about 24 hours, so download promptly. Prefer not to poll? Pass a replyUrl in the create body to receive a webhook callback when the job completes.

Reference modes

Omni Flash’s distinctive value is how much you can feed it as a reference. All references come from the POST /assets/email upload endpoint (or, for characters, from POST /characters). The upload response nests the reference id at mediaGenerationId.mediaGenerationId β€” that nested string is what you pass back. The modes accepted by omni-flash (from the POST /videos docs):

Mode Parameter(s) Omni Flash limit
Reference-to-video (R2V) referenceImage_1..7 up to 7 image refs
Image-to-video (I2V) startImage first frame only (no endImage)
Video-to-video edit (V2V) referenceVideo_1 (+ optional startFrameIndex_1 / endFrameIndex_1) one uploaded MP4
Characters character_1..7 (from POST /characters) 7 total combined with referenceImage_*
Voice narration referenceAudio_1..5 up to 5 (R2V) / up to 3 (V2V edit)

Reference-to-video (R2V) β€” upload one or more images, then pass them as referenceImage_1, referenceImage_2, … (slots _4.._7 are Omni Flash only). Image references cannot be combined with 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"
  }'

Video-to-video edit (V2V) β€” upload an MP4 with Content-Type: video/mp4 to POST /assets, then pass the returned id as referenceVideo_1 to edit that clip. This is Omni Flash only and switches the request into V2V mode. duration is not accepted (output length matches the input trim window, max 10 s). Optionally trim the source on a 24 fps virtual timeline with startFrameIndex_1 (0–239) and endFrameIndex_1 (1–240; 240 = 10 s):

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
  }'

You can also place inline @-markers in the prompt that resolve to the slots you supply β€” e.g. @referenceImage_1, @character_1, @referenceAudio_1. Markers are opt-in and each must have a matching body param. (@referenceVideo_* is not supported inline β€” pass it as a body field.) See Inline @-mention markers.

Voices & characters

Two short setup calls power Omni Flash’s audio and identity features.

Custom voices β€” POST /voices saves a voice built on one of the 30 system presets with your own dialog and voicePerformance. The response voice field is a reference-id you then pass as referenceAudio_1..5 on 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:...", ... }

Each referenceAudio_* slot accepts either a system voice name (case-insensitive, one of the 30 presets such as Aoede, Kore, Puck, Zephyr) or a user voice id from the call above. Voice creation requires reCAPTCHA (solved automatically β€” see the FAQ).

Reusable characters β€” POST /characters bundles 1–2 reference images plus an optional voice into a named character. The response character field is what you pass as character_1..7:

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:...", ... }

On Omni Flash, characters work in all durations and share the 7-total image-ref budget with referenceImage_* (referenceImages.length + Ξ£(character.imgs) ≀ 7). POST /characters does not require a captcha. Characters and voices are also usable from Veo, but with tighter budgets (3 images, 1 voice, 8 s only) β€” see What can use a character?.

Batch-generate with a script

Finding the right take takes many attempts, and running them by hand is tedious. The Node.js script below reads a list of prompts from prompts.json, uploads any reference images or source videos, submits each job in async mode with model: "omni-flash", then polls and downloads every finished MP4 β€” so you can queue a batch and come back to the winners.

You need Node.js v21 or newer. Put prompts.json and omni-flash.mjs in the same folder and run node ./omni-flash.mjs API_TOKEN EMAIL, where API_TOKEN is your useapi.net API token and EMAIL is your connected Google Flow account email. The script looks the account up by email automatically and checks its health field before submitting.

Prefer to clone and run it locally? The complete omni-flash.mjs and prompts.json are on GitHub in useapi/google-flow-api.

Expand prompts.json
[
    {
        "prompt": "Audio-native text-to-video. By default model omni-flash with a landscape 8-second clip will be used."
    },
    {
        "duration": 10,
        "referenceAudio_1": "Charon",
        "prompt": "A 10-second clip (omni-flash only) narrated by the system voice 'Charon'. referenceAudio_1 accepts a preset name or a POST /voices user-voice id."
    },
    {
        "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."
    },
    {
        "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"
    }
]
Expand omni-flash.mjs script
/*

Script version 1.0, June 15, 2026

Script to batch-generate audio-native videos with Gemini Omni Flash using the Google Flow API v1 by useapi.net πŸš€
Uses the POST /videos endpoint in async mode (model: omni-flash) 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.

*/

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';
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', '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();

    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 in async mode.
// referenceImage_1..7 = R2V image refs; referenceVideo_1 = V2V edit (no duration);
// referenceAudio_1..5 = system voice name or POST /voices user-voice id.
async function submitVideo(apiToken, email, prompt, index) {
    const {
        prompt: text, aspectRatio, duration, count, seed,
        referenceImage_1, referenceImage_2, referenceImage_3, referenceImage_4,
        referenceImage_5, referenceImage_6, referenceImage_7,
        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.includes(ref.split('.').pop()) || supportedVideoExtensions.includes(ref.split('.').pop()))
            ? await uploadAsset(apiToken, email, ref)
            : ref;

    const refImage_1 = await resolveRef(referenceImage_1);
    const refImage_2 = await resolveRef(referenceImage_2);
    const refImage_3 = await resolveRef(referenceImage_3);
    const refImage_4 = await resolveRef(referenceImage_4);
    const refImage_5 = await resolveRef(referenceImage_5);
    const refImage_6 = await resolveRef(referenceImage_6);
    const refImage_7 = await resolveRef(referenceImage_7);
    const refVideo_1 = await resolveRef(referenceVideo_1);

    // V2V edit does not accept duration β€” output matches the trim window.
    const isV2V = !!refVideo_1;

    const body = JSON.stringify({
        model: MODEL,
        email,
        prompt: text,
        aspectRatio,
        duration: isV2V ? undefined : duration,
        count,
        seed,
        referenceImage_1: refImage_1,
        referenceImage_2: refImage_2,
        referenceImage_3: refImage_3,
        referenceImage_4: refImage_4,
        referenceImage_5: refImage_5,
        referenceImage_6: refImage_6,
        referenceImage_7: refImage_7,
        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}`);
                            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(videoFilename, stream);
                            } catch (err) {
                                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.0');

    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', 'aspectRatio', 'duration', 'count', 'seed',
        'referenceImage_1', 'referenceImage_2', 'referenceImage_3', 'referenceImage_4',
        'referenceImage_5', 'referenceImage_6', 'referenceImage_7',
        'referenceVideo_1', 'startFrameIndex_1', 'endFrameIndex_1',
        'referenceAudio_1', 'referenceAudio_2', 'referenceAudio_3', 'referenceAudio_4', 'referenceAudio_5'
    ];

    const imageRefKeys = ['referenceImage_1', 'referenceImage_2', 'referenceImage_3', 'referenceImage_4', 'referenceImage_5', 'referenceImage_6', 'referenceImage_7'];

    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 } = 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();

                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}`);

        await Promise.all([
            validateFile(referenceVideo_1, supportedVideoExtensions),
            ...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();

Examples

The clips below are real Gemini Omni Flash generations made through this Google Flow API β€” model: "omni-flash", voices, references, characters, and V2V editing.

V2V edit β€” restyle an uploaded MP4 into a cartoon, same motion and framing

From Google Flow: Gemini Omni Flash β€” referenceVideo_1 editing.

Inline @-mentions β€” R2V noir clip with @character_1, @referenceImage_1, @referenceImage_2 and a custom voice

From Google Flow: Inline @-mentions β€” inline @-mention markers grounding each reference.

Characters + V2V edit β€” composites two reusable characters into the noir clip

From Google Flow: Inline @-mentions β€” referenceVideo_1 V2V edit with inline @character_*. See also Reusable Voices and Characters for the voice/character setup pipeline.

Frequently asked questions

  • What is Gemini Omni Flash? Gemini Omni Flash is Google’s audio-native video model in Google Flow. Unlike a silent video model, it generates the soundtrack β€” narration, dialog, ambience β€” as part of the same clip, and it accepts image, video, and audio references plus reusable characters. Through useapi.net you reach it by setting model: "omni-flash" on the Google Flow API.
  • How is Omni Flash different from Veo 3.1? Both run on the same Google Flow API. Veo is the higher-fidelity plain text-to-video / image-to-video engine; Omni Flash is audio-native and reference-rich β€” image references, video-to-video editing (referenceVideo_1), multiple voices per clip, reusable characters in any duration, and longer clips than Veo. See Reference modes for the exact limits and Supported model for durations.
  • Can it use my own voice, a reference video, or a reference audio? Yes β€” audio is one of Omni Flash’s core strengths: narrate with the built-in system voices or custom voices from POST /voices (passed as referenceAudio_1..5), and upload an MP4 via POST /assets to edit it as referenceVideo_1 (V2V). See Reference modes and Voices & characters for the per-mode limits.
  • Is there an official Omni Flash API? Omni Flash is offered through Google Flow, the consumer app β€” not as a standalone metered SKU on Google’s developer platform, whose official video API currently lists Veo. useapi.net bridges that gap: it drives your own consumer Google Flow account through a standard REST endpoint, so you can call Omni Flash programmatically while spending the Flow credits you already pay for.
  • Do I need to solve a captcha? Google Flow video generation (and voice creation) requires reCAPTCHA, but you do not solve it yourself. The useapi.net worker solves it automatically β€” your first Google Flow account comes with 100 free captcha credits (a one-time allotment, not per account, powered by CapSolver), and after that you configure your own provider keys via POST /accounts/captcha-providers. The script in this guide sends no captchaToken and relies on this automatic solving. (POST /characters does not require a captcha.)
  • How much does it cost? You keep your paid Google AI subscription (Plus, Pro, or Ultra β€” needed for Omni Flash video) and add a flat monthly useapi.net subscription for API access to all services. Each generation then spends Flow credits from your Google plan, scaling with clip length (a V2V edit costs more). See Pricing for the dollar figures and Supported model for the per-duration credits.

Conclusion

Visit our Discord Server or Telegram Channel for any support questions and concerns.

We regularly post guides and tutorials on the YouTube Channel.

Check our GitHub repo with code examples.

Cross posted