Cara Membuat Video AI dengan Audio Bawaan memakai Gemini Omni Flash lewat Google Flow API
8 menit baca β’ 15 Juni 2026 (12 November 2026)
useapi.net adalah API pihak ketiga. Kami tidak berafiliasi dengan Google dan tidak disahkan oleh Google.
Daftar isi
- Pendahuluan
- Model yang didukung
- Harga
- Membuat video dalam dua panggilan API
- Mode referensi
- Suara & karakter
- Memperpanjang video dengan Omni 1.2
- Generate batch dengan skrip
- Contoh
- Pertanyaan yang sering diajukan
- Kesimpulan
Pendahuluan
Gemini Omni Flash membuat video AI dengan audio bawaan dari satu curl lewat Google Flow API useapi.net, lengkap dengan suara yang tersinkron, karakter, dan edit video-ke-video, di langganan situs Google Flow Anda (Google AI: Omni 1.1 Flash di Plus, Pro atau Ultra, Omni 1.2 Flash di semua paket, akun gratis hingga 10 detik). Satu video 720p 10 detik berbiaya $0,12 di Google AI Ultra $199,99, dibanding $1,01 di Gemini API resmi (per November 2026), tanpa project Google Cloud maupun API key. Langganan yang sama juga membuat video Omni di Google Vids, tanpa memakai kredit Flow.
Di antarmukanya Google Flow menamai model ini Omni 1.1 Flash, dan nilainya di API adalah model: "omni-flash". Omni 1.2 Flash, yang dirilis Google pada 5 November 2026, memakai nilai terpisah, model: "omni-flash-1.2": durasi berapa pun dari 3 sampai 15 detik, kredit per detik, dan model inilah yang memperpanjang video hingga 10 detik setiap kali. Kode yang mengirim omni-flash tetap mendapat 1.1. Untuk video 15 detik, extend dengan suara, dan harga per video di Google Flow dan Google Vids, lihat Cara Memakai Gemini Omni 1.2 Flash API.
Omni Flash memakai Google Flow API yang sama dengan Veo, dipilih dengan satu field β model: "omni-flash-1.2" atau model: "omni-flash" β tetapi jenis modelnya berbeda. Veo adalah mesin text-to-video biasa yang kuat, sedangkan Omni Flash membuat audio sendiri dan menerima banyak referensi:
- Suara dibuat sebagai bagian dari video β narasi, dialog, dan suasana keluar dari model yang sama, bukan ditempelkan belakangan.
- Suara pembicara β 30 preset suara sistem bawaan ditambah suara kustom yang Anda buat dari dialog dan catatan pembawaan Anda sendiri, dikirim lewat
referenceAudio_1..5. - Referensi gambar dan video β hingga 10 gambar referensi di Omni 1.2 Flash (7 di Omni 1.1 Flash) untuk reference-to-video (R2V), serta edit video-ke-video (V2V) dari video hasil upload atau hasil generate lewat
referenceVideo_1. - Karakter yang bisa dipakai ulang β gabungkan gambar referensi + satu suara menjadi karakter bernama dan pakai lagi di banyak video.
- Video lebih panjang β Omni 1.1 Flash membuat 4, 6, 8 atau 10 detik dan Omni 1.2 Flash durasi berapa pun dari 3 sampai 15 (Veo maksimal
8), dan Omni 1.2 memperpanjang video bertahap hingga 10 detik per langkah.
Panduan ini menunjukkan alur kerja dua panggilan dengan curl yang siap disalin, mode-mode referensi yang membuat Omni Flash istimewa, lalu skrip Node.js siap jalan yang membuat video secara batch dari daftar prompt. Untuk text-to-video biasa, lihat tutorial Veo 3.1 yang menjadi pasangannya.
Model yang didukung
Panduan ini memakai model video Google dengan audio bawaan, Omni 1.1 Flash (model: "omni-flash") dan Omni 1.2 Flash (model: "omni-flash-1.2"). Untuk Omni 1.1, biaya kredit naik sesuai durasi video:
| Durasi | Kredit per video |
|---|---|
| 4s | 7 |
| 6s | 10 |
| 8s | 12 |
| 10s | 15 |
| Edit V2V | 20 |
Itu biaya di resolusi default 720p. Dengan resolution: "360p" biayanya sekitar separuh β 4 / 5 / 6 / 7 kredit menurut durasi, 10 untuk edit V2V β dan video 360p bisa dinaikkan ke 720p sesudahnya dengan POST /videos/upscale.
Omni 1.2 Flash (model: "omni-flash-1.2") menerima jumlah detik bulat berapa pun dari 3 sampai 15 dan menagihnya per detik, sama di setiap paket:
| Durasi | 720p | 360p |
|---|---|---|
| 3s | 9 | 3 |
| 8s | 24 | 8 |
| 10s | 30 | 10 |
| 15s | 60 | 20 |
Hingga 10 detik biayanya 3 kredit per detik di 720p dan 1 di 360p, dan setiap detik setelah detik ke-10 berbiaya dua kali lipat. Edit V2V berbiaya sama dengan video sepanjang bagian yang diedit, dan 1.2 bisa mengedit hingga 15 detik dari sebuah klip. Akun Google gratis bisa memakai 1.2 untuk video hingga 10 detik.
Omni Flash menerima lima mode di POST /videos: text-to-video biasa (T2V), reference-to-video (R2V) dengan hingga 10 referenceImage_1..10 di Omni 1.2 Flash (7 di Omni 1.1 Flash), image-to-video dari frame awal lewat startImage (I2V), image-to-video di antara frame awal dan frame akhir lewat startImage + endImage (I2V-FL), dan edit video-ke-video (V2V) dari MP4 yang di-upload lewat referenceVideo_1. Lihat Mode referensi di bawah.
Harga
Omni 1.1 Flash butuh langganan Google AI berbayar (Plus, Pro atau Ultra), Omni 1.2 Flash berjalan di akun gratis hingga 10 detik, dan useapi.net tidak menambahkan biaya per video. Setiap video memakai kredit Flow sesuai durasinya (lihat Model yang didukung). Harga per video di setiap paket, dibandingkan dengan Gemini API resmi, ada di tabel harga Google Flow.
Paket Google AI juga berisi kuota bulanan di Google Vids, yang membuat video hingga 15 detik dengan Gemini Omni 1.2 Flash (pengumuman Google) dan tidak memakai kredit Flow. Di Ultra $199,99 (Google menyebutnya Ultra 20x) kuotanya 10.000 detik video per bulan β sekitar 1.000 klip 10 detik 1080p tambahan di luar ~1.666 klip Omni 1.1 Flash 10 detik (atau ~833 klip Omni 1.2 Flash di 720p) yang bisa dibeli dengan 25.000 kredit Flow. Detik-detik itu dipakai bersama oleh semua anggota paket keluarga Google. Paket lain mendapat lebih sedikit (lihat halaman Gemini di Google Vids milik Google), dan akun gratis yang kami uji tidak mendapat kuota sama sekali. Google Vids API memakai kuota ini tanpa captcha. Hubungkan akunnya lewat Setup Google Vids.
Membuat video dalam dua panggilan API
Anda butuh API token useapi.net dan akun Google Flow yang sudah terhubung dengan langganan Google AI berbayar (Plus, Pro atau Ultra) untuk contoh Omni 1.1 Flash di bawah (Omni 1.2 Flash juga berjalan di akun gratis, hingga 10 detik). Job video berjalan secara asynchronous: kirim async: true dan panggilan create langsung mengembalikan jobid, lalu Anda melakukan polling sampai videonya siap.
- Kirim job β
POST https://api.useapi.net/v1/google-flow/videosdenganmodel: "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 membuat audionya sendiri, jadi soundtrack β narasi penjaga mercusuar dan deburan ombak β langsung dibuat dari prompt, tanpa parameter audio. Dalam mode async, respons langsung kembali dengan 201 Created, jobid berhuruf kecil, dan 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
}
}
- Polling hasilnya β
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 berpindah dari created β started β completed. Saat mencapai completed, MP4 jadi ada di response.media[].videoUrl (satu prompt dengan count > 1 mengembalikan satu entri per video):
{
"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 biasanya selesai dalam 60β180 detik. videoUrl yang ditandatangani berlaku sekitar 6 jam, jadi segera unduh. Tidak mau polling? Kirim replyUrl di body create untuk menerima callback webhook saat job selesai.
Mode referensi
Keunggulan khas Omni Flash adalah banyaknya referensi yang bisa Anda berikan. Semua referensi berasal dari endpoint upload POST /assets/email (atau, untuk karakter, dari POST /characters). Respons upload menyimpan id referensi secara bersarang di mediaGenerationId.mediaGenerationId β string bersarang itulah yang Anda kirim kembali. Mode yang diterima omni-flash (dari dokumentasi POST /videos):
| Mode | Parameter | Batas Omni Flash |
|---|---|---|
| Reference-to-video (R2V) | referenceImage_1..10 | hingga 10 gambar referensi di omni-flash-1.2, 7 di omni-flash |
| Image-to-video (I2V) | startImage | satu frame awal |
| Image-to-video, awal + akhir (I2V-FL) | startImage + endImage | hanya kedua frame itu β tanpa referensi lain |
| Edit video-ke-video (V2V) | referenceVideo_1 (+ opsional startFrameIndex_1 / endFrameIndex_1) | satu MP4 hasil upload |
| Karakter | character_1..10 (dari POST /characters) | total 10 bersama referenceImage_* di omni-flash-1.2, 7 di omni-flash |
| Narasi suara | referenceAudio_1..5 | hingga 5 (R2V) / hingga 3 (edit V2V) |
Reference-to-video (R2V) β upload satu atau beberapa gambar, lalu kirim sebagai referenceImage_1, referenceImage_2, β¦ (slot _4.._7 hanya untuk Omni Flash). Referensi gambar tidak bisa digabung dengan 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"
}'
Edit video-ke-video (V2V) β upload MP4 dengan Content-Type: video/mp4 ke POST /assets, lalu kirim id yang dikembalikan sebagai referenceVideo_1 untuk mengedit video itu. Mode ini hanya ada di Omni Flash dan mengubah request ke mode V2V. duration tidak diterima (panjang hasil mengikuti jendela potongan input, maksimal 10 detik di omni-flash dan 15 detik di omni-flash-1.2). Jika perlu, potong sumbernya pada timeline virtual 24 fps dengan startFrameIndex_1 (0β239) dan endFrameIndex_1 (1β240, dengan 240 = 10 detik), atau hingga 359 dan 360 (15 detik) di 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
}'
Anda juga bisa menaruh penanda @ inline di prompt yang merujuk ke slot yang Anda isi β misalnya @referenceImage_1, @character_1, @referenceAudio_1. Penanda bersifat opsional dan masing-masing harus punya parameter body yang cocok. (@referenceVideo_* tidak didukung secara inline β kirim sebagai field body.) Lihat penanda @-mention inline.
Suara & karakter
Dua panggilan setup singkat menjalankan fitur audio dan identitas Omni Flash.
Suara kustom β POST /voices menyimpan suara yang dibangun dari salah satu dari 30 preset sistem dengan dialog dan voicePerformance milik Anda sendiri. Field voice di respons adalah id referensi yang lalu Anda kirim sebagai referenceAudio_1..5 di 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:...", ... }
Setiap slot referenceAudio_* menerima nama suara sistem (tidak peka huruf besar-kecil, salah satu dari 30 preset seperti Aoede, Kore, Puck, Zephyr) atau id suara pengguna dari panggilan di atas. Pembuatan suara butuh reCAPTCHA (diselesaikan otomatis β lihat FAQ).
Karakter yang bisa dipakai ulang β POST /characters menggabungkan 1β2 gambar referensi plus suara opsional menjadi karakter bernama. Field character di respons adalah nilai yang Anda kirim sebagai 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:...", ... }
Di Omni Flash, karakter bisa dipakai di semua durasi dan berbagi batas total 10 referensi gambar dengan referenceImage_* (referenceImages.length + Ξ£(character.imgs) β€ 10, Omni 1.1: 7). POST /characters tidak butuh captcha. Karakter dan suara juga bisa dipakai di Veo, tetapi dengan batas lebih ketat (3 gambar, 1 suara, hanya 8 detik) β lihat What can use a character?.
Memperpanjang video dengan Omni 1.2
POST /videos/extend menambahkan 3 sampai 10 detik setiap kali dengan Omni 1.2 Flash ke video apa pun yang lebih pendek dari 30 detik, dengan hingga 3 suara, gambar referensi atau karakter, dan mengembalikan satu klip utuh (hanya paket berbayar). Panduan di Cara Memakai Gemini Omni 1.2 Flash API memperpanjang video 10 detik dua kali menjadi 30 detik dengan dua suara, yaitu contoh di bawah.
Generate batch dengan skrip
Menemukan hasil yang pas butuh banyak percobaan, dan menjalankannya satu per satu dengan tangan itu melelahkan. Skrip Node.js di bawah membaca daftar prompt dari prompts.json, meng-upload gambar referensi, frame awal dan akhir, atau video sumber jika ada, meneruskan id character_* dan resolution apa adanya, mengirim setiap job dalam mode async dengan model: "omni-flash", lalu melakukan polling dan mengunduh setiap MP4 yang sudah jadi β jadi Anda bisa mengantrekan satu batch lalu kembali untuk memilih yang terbaik. Skrip berhenti dengan peringatan sebelum mengirim kombinasi yang akan ditolak API, misalnya frame akhir tanpa frame awal.
Anda butuh Node.js v21 atau lebih baru. Taruh prompts.json dan omni-flash.mjs di folder yang sama dan jalankan node ./omni-flash.mjs API_TOKEN EMAIL, dengan API_TOKEN berisi API token useapi.net Anda dan EMAIL berisi email akun Google Flow yang terhubung. Skrip mencari akun berdasarkan email secara otomatis dan memeriksa field health-nya sebelum mengirim.
Lebih suka clone dan menjalankannya di komputer sendiri? omni-flash.mjs dan prompts.json lengkap ada di GitHub di useapi/google-flow-api.
Bekerja di Claude Code atau Codex? Agent skill omni-flash-video membuat agent mengerjakan semuanya untuk Anda: jelaskan videonya, tunjukkan gambar Anda, dan agent meng-upload gambar itu, menulis request, menunggu, lalu menyimpan .mp4-nya. Pasang dengan npx skills add useapi/google-flow-api --skill omni-flash-video.
Buka 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"
}
]
Buka skrip 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();
Contoh
Video di bawah adalah hasil asli Gemini Omni Flash yang dibuat lewat Google Flow API ini β model: "omni-flash-1.2" dan model: "omni-flash", extend, suara, referensi, karakter, dan edit V2V.
Extend Omni 1.2 β video 10 detik diperpanjang dua kali menjadi 30 detik, dengan dua suara
Dari Google Flow: Omni 1.2 Flash, 15-Second Videos, and Extend with Voices (bahasa Inggris) β omni-flash-1.2 untuk 10 detik pertama, lalu dua panggilan POST /videos/extend dengan referenceAudio_1.
Edit V2V β mengubah gaya MP4 hasil upload menjadi kartun, dengan gerakan dan framing yang sama
Dari Google Flow: Gemini Omni Flash (bahasa Inggris) β edit dengan referenceVideo_1.
@-mention inline β video noir R2V dengan @character_1, @referenceImage_1, @referenceImage_2 dan suara kustom
Dari Google Flow: Inline @-mentions (bahasa Inggris) β penanda @-mention inline yang mengaitkan setiap referensi.
Karakter + edit V2V β memasukkan dua karakter yang bisa dipakai ulang ke dalam video noir
Dari Google Flow: Inline @-mentions (bahasa Inggris) β edit V2V dengan referenceVideo_1 dan @character_* inline. Lihat juga Reusable Voices and Characters (bahasa Inggris) untuk alur setup suara dan karakter.
Pertanyaan yang sering diajukan
- Apa itu Gemini Omni Flash? Lewat useapi.net Anda menjalankan Gemini Omni Flash, model video dengan audio bawaan milik Google di Google Flow, dengan mengisi
model: "omni-flash-1.2"(Omni 1.2 Flash) ataumodel: "omni-flash"(Omni 1.1 Flash) di Google Flow API. Berbeda dari model video bisu, model ini membuat soundtrack, narasi, dialog, dan suasana sebagai bagian dari video yang sama, dan menerima referensi gambar, video, dan audio plus karakter yang bisa dipakai ulang. - Apa beda Omni Flash dengan Veo 3.1? Lewat useapi.net keduanya berjalan di Google Flow API yang sama, dipilih dengan field
model. Veo adalah mesin text-to-video dan image-to-video biasa dengan fidelitas lebih tinggi, sedangkan Omni Flash membuat audio sendiri dan menerima banyak referensi, dengan edit video-ke-video (referenceVideo_1), beberapa suara per video, karakter yang bisa dipakai ulang di durasi berapa pun, dan video yang lebih panjang daripada Veo. Lihat Mode referensi untuk batas persisnya. - Bisakah memakai suara saya sendiri, video referensi, atau audio referensi? Lewat useapi.net, bisa. Buat narasi dengan suara sistem bawaan atau suara kustom dari POST /voices, dikirim sebagai
referenceAudio_1..5, dan upload MP4 dengan POST /assets untuk mengeditnya sebagaireferenceVideo_1(V2V). Lihat Mode referensi dan Suara & karakter untuk batas setiap mode. - Apakah ada API Omni Flash resmi? Lewat useapi.net, Omni 1.1 Flash jauh lebih murah daripada di API milik Google sendiri: satu video 720p 8 detik berbiaya $0,10 di Google AI Ultra $199,99, dibanding $0,81 di Gemini API resmi, yang menjual model ini per detik sejak 30 Juni 2026 (per November 2026). Model ini berjalan lewat endpoint REST standar di langganan situs Google Flow Anda.
- Bisakah Omni Flash membuat video lebih dari 10 detik? Lewat useapi.net, bisa: Omni 1.2 Flash (
model: "omni-flash-1.2") membuat hingga 15 detik dalam satu panggilan, dan POST /videos/extend menambahkan hingga 10 detik setiap kali dengan suara, seharga $0,19 untuk video 720p 8 detik di Google AI Ultra $199,99, dibanding $0,56 di API milik Google sendiri (per November 2026). Semua batas durasi dan harga di Google Flow dan Google Vids ada di Cara Memakai Gemini Omni 1.2 Flash API. - Apakah saya harus menyelesaikan captcha? Lewat useapi.net Anda tidak pernah menyelesaikannya sendiri. Google Flow meminta reCAPTCHA untuk setiap video dan setiap suara baru, dan worker useapi.net menyelesaikannya otomatis: akun Google Flow pertama Anda mendapat 300 penyelesaian gratis dengan key AntiCaptcha, CapSolver dan EzCaptcha milik kami, dan setelah itu Anda menambahkan key penyedia sendiri dengan POST /accounts/captcha-providers. POST /characters tidak butuh captcha.
Kesimpulan
Kunjungi Server Discord atau
Channel Telegram kami untuk pertanyaan dan masalah dukungan.
Lihat repo GitHub kami berisi contoh kode.
Asisten AI juga bisa bekerja dengan Google Flow untuk Anda: MCP server Google Flow yang di-hosting memungkinkan Claude Code, Cursor dan VS Code mengobrol dengan Google Flow Agent, yang membuat gambar dan video di akun Anda.
Mulai memakai API
Satu langganan US$15/bulan mencakup semua API di useapi.net: satu token untuk semua layanan. Di setiap layanan Anda bisa menghubungkan hingga 3 akun milik Anda sendiri, dan useapi.net menangani REST API, load balancing, dan polling. Butuh lebih banyak akun? Setiap langganan tambahan US$10/bulan dan menambah 3 akun lagi per layanan.
Berlangganan β US$15/bulan Bayar dengan kripto
Tidak punya kartu kredit internasional? Anda bisa berlangganan dengan kripto (pembayaran kripto tidak bisa di-refund). Harga dan paket volume ada di halaman langganan. Jika membayar lewat Stripe, Anda mendapat refund penuh dalam 14 hari sejak pembelian pertama, selama jumlah generasi yang berhasil kurang dari 50. Sudah berlangganan? Ubah jumlahnya di Stripe Customer Portal, tidak perlu membeli lagi.