Cách tạo video AI có âm thanh gốc bằng Gemini Omni Flash qua Google Flow API
8 phút đọc • 15 tháng 6 năm 2026 (12 tháng 11 năm 2026)
useapi.net là API của bên thứ ba, không liên kết với Google và không được Google bảo trợ.
Mục lục
- Giới thiệu
- Model được hỗ trợ
- Giá
- Tạo video bằng hai lệnh gọi API
- Các chế độ tham chiếu
- Giọng nói và nhân vật
- Kéo dài video bằng Omni 1.2
- Tạo hàng loạt bằng script
- Ví dụ
- Câu hỏi thường gặp
- Kết luận
Giới thiệu
Gemini Omni Flash tạo video AI có âm thanh gốc chỉ với một lệnh curl qua Google Flow API của useapi.net, có giọng nói đồng bộ, nhân vật và chỉnh sửa video sang video, trên gói đăng ký website Google Flow của bạn (Google AI: Omni 1.1 Flash trên Plus, Pro hoặc Ultra, Omni 1.2 Flash trên mọi gói, tài khoản miễn phí tối đa 10 giây). Một video 720p dài 10 giây tốn $0,12 trên Google AI Ultra $199,99, so với $1,01 trên Gemini API chính thức (tính đến tháng 11/2026), và không cần dự án Google Cloud hay API key. Cùng gói đăng ký đó cũng tạo được video Omni trong Google Vids mà không tốn credit Flow.
Trong giao diện, Google Flow gọi model này là Omni 1.1 Flash, còn giá trị trong API là model: "omni-flash". Omni 1.2 Flash, được Google phát hành ngày 5 tháng 11 năm 2026, là một giá trị riêng, model: "omni-flash-1.2": độ dài bất kỳ từ 3 đến 15 giây, credit tính theo giây, và là model kéo dài video thêm tối đa 10 giây mỗi lần. Code gửi omni-flash vẫn tiếp tục nhận bản 1.1. Về video 15 giây, kéo dài có giọng nói và giá mỗi video trong Google Flow và Google Vids, xem Cách dùng Gemini Omni 1.2 Flash API.
Omni Flash dùng cùng Google Flow API với Veo, được chọn bằng một trường duy nhất — model: "omni-flash-1.2" hoặc model: "omni-flash" — nhưng là một loại model khác. Veo mạnh ở việc tạo video thuần từ văn bản, còn Omni Flash có âm thanh gốc và nhận rất nhiều loại tham chiếu:
- Âm thanh được tạo cùng với video — lời thuyết minh, hội thoại và âm thanh nền đến từ chính model đó, không phải ghép vào sau.
- Giọng nói — 30 giọng hệ thống có sẵn, cộng với giọng tùy chỉnh bạn tạo từ lời thoại và ghi chú diễn xuất của riêng mình, truyền vào qua
referenceAudio_1..5. - Tham chiếu ảnh và video — tối đa 10 ảnh tham chiếu trên Omni 1.2 Flash (7 trên Omni 1.1 Flash) để tạo video từ ảnh tham chiếu (R2V), và chỉnh sửa video sang video (V2V) một video tải lên hoặc đã tạo qua
referenceVideo_1. - Nhân vật dùng lại được — gộp ảnh tham chiếu và một giọng thành một nhân vật có tên rồi dùng lại trong nhiều video.
- Video dài hơn — Omni 1.1 Flash làm video 4, 6, 8 hoặc 10 giây, Omni 1.2 Flash làm độ dài bất kỳ từ 3 đến 15 (Veo tối đa là
8), và Omni 1.2 kéo dài video theo từng bước tối đa 10 giây.
Bài hướng dẫn này trình bày quy trình hai lệnh gọi với curl sao chép là chạy, các chế độ tham chiếu làm nên nét riêng của Omni Flash, rồi một script Node.js chạy được để tạo hàng loạt từ danh sách prompt. Để tạo video thuần từ văn bản, xem hướng dẫn Veo 3.1 cùng loạt.
Model được hỗ trợ
Bài này dùng các model video có âm thanh gốc của Google, Omni 1.1 Flash (model: "omni-flash") và Omni 1.2 Flash (model: "omni-flash-1.2"). Với Omni 1.1, số credit tăng theo độ dài video:
| Độ dài | Credit mỗi video |
|---|---|
| 4s | 7 |
| 6s | 10 |
| 8s | 12 |
| 10s | 15 |
| Chỉnh sửa V2V | 20 |
Đó là mức tốn ở 720p mặc định. Truyền resolution: "360p" tốn khoảng một nửa — 4 / 5 / 6 / 7 credit theo độ dài, 10 cho một lần chỉnh sửa V2V — và video 360p có thể nâng lên 720p sau đó bằng POST /videos/upscale.
Omni 1.2 Flash (model: "omni-flash-1.2") nhận mọi số giây nguyên từ 3 đến 15 và tính giá theo giây, như nhau trên mọi gói:
| Độ dài | 720p | 360p |
|---|---|---|
| 3s | 9 | 3 |
| 8s | 24 | 8 |
| 10s | 30 | 10 |
| 15s | 60 | 20 |
Tới 10 giây, mức tốn là 3 credit mỗi giây ở 720p và 1 ở 360p, và mỗi giây sau giây thứ 10 tốn gấp đôi. Một lần chỉnh sửa V2V tốn bằng một video có cùng độ dài với phần được chỉnh, và bản 1.2 chỉnh sửa được tối đa 15 giây của một clip. Tài khoản Google miễn phí dùng được 1.2 cho video tối đa 10 giây.
Omni Flash nhận năm chế độ trên POST /videos: tạo video thuần từ văn bản (T2V), tạo video từ ảnh tham chiếu (R2V) với tối đa 10 referenceImage_1..10 trên Omni 1.2 Flash (7 trên Omni 1.1 Flash), tạo video từ một khung đầu qua startImage (I2V), tạo video giữa khung đầu và khung cuối qua startImage + endImage (I2V-FL), và chỉnh sửa video sang video (V2V) một file MP4 tải lên qua referenceVideo_1. Xem Các chế độ tham chiếu bên dưới.
Giá
Omni 1.1 Flash cần gói Google AI trả phí (Plus, Pro hoặc Ultra), Omni 1.2 Flash chạy trên tài khoản miễn phí với video tối đa 10 giây, và useapi.net không thu phí theo video. Mỗi video dùng credit Flow theo độ dài (xem Model được hỗ trợ). Giá mỗi video trên từng gói, so với Gemini API chính thức, có trong bảng giá Google Flow.
Gói Google AI còn đi kèm một hạn mức hằng tháng trong Google Vids, nơi tạo video dài tới 15 giây bằng Gemini Omni 1.2 Flash (thông báo của Google) mà không dùng credit Flow. Trên Ultra $199,99 (Google gọi là Ultra 20x), hạn mức là 10.000 giây video mỗi tháng — thêm khoảng 1.000 clip 10 giây ở 1080p, ngoài khoảng 1.666 clip Omni 1.1 Flash dài 10 giây (hoặc khoảng 833 clip Omni 1.2 Flash ở 720p) mà 25.000 credit của Flow mua được. Số giây này được dùng chung bởi mọi người trong gói gia đình Google. Các gói khác được ít hơn (xem trang Gemini trong Google Vids của Google), và các tài khoản miễn phí chúng tôi thử nghiệm không có giây nào. Google Vids API dùng hạn mức này mà không cần captcha. Kết nối tài khoản theo Setup Google Vids.
Tạo video bằng hai lệnh gọi API
Bạn cần một API token của useapi.net và một tài khoản Google Flow đã kết nối, dùng gói Google AI trả phí (Plus, Pro hoặc Ultra) cho ví dụ Omni 1.1 Flash bên dưới (Omni 1.2 Flash cũng chạy trên tài khoản miễn phí, tối đa 10 giây). Job video chạy bất đồng bộ: truyền async: true và lệnh tạo trả về jobid ngay, sau đó bạn polling cho tới khi video sẵn sàng.
- Gửi job —
POST https://api.useapi.net/v1/google-flow/videosvớimodel: "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 có âm thanh gốc, nên nó tạo luôn phần âm thanh — lời thuyết minh của người gác hải đăng và tiếng sóng vỗ — thẳng từ prompt, không cần tham số âm thanh nào. Ở chế độ async, phản hồi trả về ngay với 201 Created, một jobid viết thường và 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 lấy kết quả —
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 chuyển qua created → started → completed. Khi tới completed, file MP4 hoàn chỉnh nằm ở response.media[].videoUrl (một prompt với count > 1 trả về một mục cho mỗi 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 thường xong trong 60–180 giây. videoUrl có chữ ký dùng được khoảng 6 giờ, nên hãy tải về sớm. Không muốn polling? Truyền replyUrl trong body của lệnh tạo để nhận webhook khi job xong.
Các chế độ tham chiếu
Giá trị riêng của Omni Flash nằm ở chỗ bạn đưa vào được rất nhiều tham chiếu. Mọi tham chiếu đều đến từ endpoint tải lên POST /assets/email (hoặc, với nhân vật, từ POST /characters). Phản hồi tải lên đặt id tham chiếu lồng ở mediaGenerationId.mediaGenerationId — chuỗi lồng đó là thứ bạn truyền lại. Các chế độ omni-flash nhận (theo tài liệu POST /videos):
| Chế độ | Tham số | Giới hạn của Omni Flash |
|---|---|---|
| Video từ ảnh tham chiếu (R2V) | referenceImage_1..10 | tối đa 10 ảnh tham chiếu trên omni-flash-1.2, 7 trên omni-flash |
| Video từ ảnh (I2V) | startImage | một khung đầu |
| Video từ ảnh, khung đầu + cuối (I2V-FL) | startImage + endImage | chỉ hai khung — không kèm tham chiếu khác |
| Chỉnh sửa video sang video (V2V) | referenceVideo_1 (+ tùy chọn startFrameIndex_1 / endFrameIndex_1) | một file MP4 tải lên |
| Nhân vật | character_1..10 (từ POST /characters) | tổng cộng 10 khi gộp với referenceImage_* trên omni-flash-1.2, 7 trên omni-flash |
| Thuyết minh bằng giọng | referenceAudio_1..5 | tối đa 5 (R2V) / tối đa 3 (chỉnh sửa V2V) |
Video từ ảnh tham chiếu (R2V) — tải lên một hoặc nhiều ảnh, rồi truyền chúng làm referenceImage_1, referenceImage_2, … (các ô _4.._7 chỉ có trên Omni Flash). Ảnh tham chiếu không kết hợp được với 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"
}'
Chỉnh sửa video sang video (V2V) — tải một file MP4 lên POST /assets với Content-Type: video/mp4, rồi truyền id nhận được làm referenceVideo_1 để chỉnh sửa video đó. Chế độ này chỉ có trên Omni Flash và chuyển request sang chế độ V2V. Không nhận duration (độ dài đầu ra bằng đoạn cắt của đầu vào, tối đa 10 s trên omni-flash và 15 s trên omni-flash-1.2). Bạn có thể cắt video nguồn trên một dòng thời gian ảo 24 fps bằng startFrameIndex_1 (0–239) và endFrameIndex_1 (1–240, trong đó 240 = 10 s), hoặc tới 359 và 360 (15 s) trên 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
}'
Bạn cũng có thể đặt các dấu @ ngay trong prompt, trỏ tới các ô bạn truyền vào — ví dụ @referenceImage_1, @character_1, @referenceAudio_1. Dấu nhắc là tùy chọn, và mỗi dấu phải có một tham số tương ứng trong body. (@referenceVideo_* không dùng được trong prompt — hãy truyền nó làm một trường trong body.) Xem Dấu nhắc @ trong prompt.
Giọng nói và nhân vật
Hai lệnh gọi thiết lập ngắn giúp bạn dùng tính năng âm thanh và nhận diện nhân vật của Omni Flash.
Giọng tùy chỉnh — POST /voices lưu một giọng dựa trên một trong 30 giọng hệ thống, với dialog và voicePerformance của riêng bạn. Trường voice trong phản hồi là một id tham chiếu mà bạn truyền làm referenceAudio_1..5 trên 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:...", ... }
Mỗi ô referenceAudio_* nhận tên một giọng hệ thống (không phân biệt hoa thường, một trong 30 giọng có sẵn như Aoede, Kore, Puck, Zephyr) hoặc id giọng của người dùng từ lệnh gọi trên. Tạo giọng cần reCAPTCHA (được giải tự động — xem phần câu hỏi thường gặp).
Nhân vật dùng lại được — POST /characters gộp 1–2 ảnh tham chiếu cùng một giọng tùy chọn thành một nhân vật có tên. Trường character trong phản hồi là thứ bạn truyền làm 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:...", ... }
Trên Omni Flash, nhân vật dùng được với mọi độ dài và chia chung giới hạn tổng 10 ảnh tham chiếu với referenceImage_* (referenceImages.length + Σ(character.imgs) ≤ 10, Omni 1.1: 7). POST /characters không cần captcha. Nhân vật và giọng nói cũng dùng được với Veo, nhưng giới hạn chặt hơn (3 ảnh, 1 giọng, chỉ 8 s) — xem What can use a character?.
Kéo dài video bằng Omni 1.2
POST /videos/extend thêm 3 đến 10 giây mỗi lần bằng Omni 1.2 Flash vào bất kỳ video nào ngắn hơn 30 giây, với tối đa 3 giọng, ảnh tham chiếu hoặc nhân vật, và trả về một clip liền mạch (chỉ gói trả phí). Phần hướng dẫn trong Cách dùng Gemini Omni 1.2 Flash API kéo dài một video 10 giây hai lần thành 30 giây với hai giọng, chính là ví dụ bên dưới.
Tạo hàng loạt bằng script
Tìm được cảnh quay ưng ý cần nhiều lần thử, và chạy từng lần bằng tay rất mất công. Script Node.js dưới đây đọc danh sách prompt từ prompts.json, tải lên ảnh tham chiếu, khung đầu và khung cuối hoặc video nguồn nếu có, truyền thẳng các id character_* và resolution, gửi từng job ở chế độ async với model: "omni-flash", rồi polling và tải về mọi file MP4 đã xong — nên bạn có thể xếp hàng cả loạt rồi quay lại chọn video đẹp nhất. Script dừng lại kèm cảnh báo trước khi gửi một tổ hợp mà API sẽ từ chối, chẳng hạn khung cuối mà không có khung đầu.
Bạn cần Node.js v21 trở lên. Đặt prompts.json và omni-flash.mjs cùng một thư mục và chạy node ./omni-flash.mjs API_TOKEN EMAIL, trong đó API_TOKEN là API token useapi.net của bạn và EMAIL là email của tài khoản Google Flow đã kết nối. Script tự tìm tài khoản theo email và kiểm tra trường health trước khi gửi.
Muốn clone và chạy trên máy? File omni-flash.mjs và prompts.json đầy đủ có trên GitHub ở useapi/google-flow-api.
Bạn làm việc trong Claude Code hoặc Codex? Agent skill omni-flash-video để agent làm hết việc này cho bạn: mô tả video, chỉ cho nó ảnh của bạn, và nó tải ảnh lên, viết request, chờ và lưu file .mp4. Cài bằng npx skills add useapi/google-flow-api --skill omni-flash-video.
Mở 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"
}
]
Mở 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();
Ví dụ
Các video dưới đây là kết quả Gemini Omni Flash thật, làm qua Google Flow API này — model: "omni-flash-1.2" và model: "omni-flash", kéo dài, giọng nói, tham chiếu, nhân vật và chỉnh sửa V2V.
Kéo dài bằng Omni 1.2 — video 10 giây được kéo dài hai lần thành 30 giây, với hai giọng
Từ Google Flow: Omni 1.2 Flash, 15-Second Videos, and Extend with Voices — omni-flash-1.2 cho 10 giây đầu, rồi hai lệnh gọi POST /videos/extend với referenceAudio_1.
Chỉnh sửa V2V — đổi phong cách một file MP4 tải lên thành hoạt hình, giữ nguyên chuyển động và khung hình
Từ Google Flow: Gemini Omni Flash — chỉnh sửa bằng referenceVideo_1.
Dấu nhắc @ trong prompt — video noir R2V với @character_1, @referenceImage_1, @referenceImage_2 và một giọng tùy chỉnh
Từ Google Flow: Inline @-mentions — dấu nhắc @ trong prompt gắn từng tham chiếu vào đúng chỗ.
Nhân vật + chỉnh sửa V2V — ghép hai nhân vật dùng lại được vào video noir
Từ Google Flow: Inline @-mentions — chỉnh sửa V2V bằng referenceVideo_1 với @character_* trong prompt. Xem thêm Reusable Voices and Characters về quy trình thiết lập giọng nói và nhân vật.
Câu hỏi thường gặp
- Gemini Omni Flash là gì? Qua useapi.net, bạn chạy Gemini Omni Flash, model video có âm thanh gốc của Google trong Google Flow, bằng cách đặt
model: "omni-flash-1.2"(Omni 1.2 Flash) hoặcmodel: "omni-flash"(Omni 1.1 Flash) trên Google Flow API. Khác với model video không tiếng, nó tạo phần âm thanh, lời thuyết minh, hội thoại và âm thanh nền ngay trong cùng một video, và nhận tham chiếu ảnh, video, âm thanh cùng nhân vật dùng lại được. - Omni Flash khác Veo 3.1 thế nào? Qua useapi.net, cả hai chạy trên cùng Google Flow API, được chọn bằng trường
model. Veo là engine tạo video thuần từ văn bản và từ ảnh với độ chân thực cao hơn, còn Omni Flash có âm thanh gốc và nhận nhiều tham chiếu, với chỉnh sửa video sang video (referenceVideo_1), nhiều giọng trong một video, nhân vật dùng lại được ở mọi độ dài và video dài hơn Veo. Xem Các chế độ tham chiếu để biết giới hạn chính xác. - Có dùng được giọng của riêng tôi, video tham chiếu hoặc âm thanh tham chiếu không? Qua useapi.net thì được. Thuyết minh bằng các giọng hệ thống có sẵn hoặc giọng tùy chỉnh từ POST /voices, truyền làm
referenceAudio_1..5, và tải một file MP4 lên bằng POST /assets để chỉnh sửa nó làmreferenceVideo_1(V2V). Xem Các chế độ tham chiếu và Giọng nói và nhân vật để biết giới hạn của từng chế độ. - Omni Flash có API chính thức không? Qua useapi.net, Omni 1.1 Flash rẻ hơn nhiều so với trên API của chính Google: một video 720p dài 8 giây tốn $0,10 trên Google AI Ultra $199,99, so với $0,81 trên Gemini API chính thức, nơi bán model này theo giây từ ngày 30 tháng 6 năm 2026 (tính đến tháng 11/2026). Nó chạy qua một endpoint REST tiêu chuẩn trên gói đăng ký website Google Flow của bạn.
- Omni Flash có làm được video dài hơn 10 giây không? Qua useapi.net thì được: Omni 1.2 Flash (
model: "omni-flash-1.2") làm tối đa 15 giây trong một lệnh gọi, và POST /videos/extend thêm tối đa 10 giây mỗi lần kèm giọng nói, với giá $0,19 cho một video 720p dài 8 giây trên Google AI Ultra $199,99, so với $0,56 trên API của chính Google (tính đến tháng 11/2026). Mọi giới hạn độ dài và mức giá trong Google Flow và Google Vids có ở Cách dùng Gemini Omni 1.2 Flash API. - Tôi có phải tự giải captcha không? Qua useapi.net, bạn không bao giờ phải tự giải. Google Flow yêu cầu reCAPTCHA cho mỗi video và mỗi giọng mới, và worker của useapi.net tự động giải: tài khoản Google Flow đầu tiên của bạn được 300 lần giải miễn phí bằng key AntiCaptcha, CapSolver và EzCaptcha của chúng tôi, sau đó bạn thêm key dịch vụ của riêng mình bằng POST /accounts/captcha-providers. POST /characters không cần captcha.
Kết luận
Ghé Discord Server hoặc
Telegram Channel của chúng tôi cho mọi câu hỏi hỗ trợ.
Xem repo GitHub của chúng tôi với các ví dụ code.
Một trợ lý AI cũng có thể làm việc với Google Flow thay bạn: MCP server Google Flow được host sẵn cho phép Claude Code, Cursor và VS Code trò chuyện với Google Flow Agent, và Agent tạo ảnh và video trên tài khoản của bạn.
Bắt đầu dùng API
Một gói đăng ký $15/tháng dùng được mọi API trên useapi.net: một token cho tất cả dịch vụ. Mỗi dịch vụ bạn kết nối tối đa 3 tài khoản của riêng mình, còn useapi.net lo REST API, cân bằng tải và polling. Cần thêm tài khoản? Mỗi gói đăng ký thêm giá $10/tháng và cho thêm 3 tài khoản ở mỗi dịch vụ.
Đăng ký — $15/tháng Thanh toán bằng tiền mã hóa
Không có thẻ thanh toán quốc tế? Bạn có thể đăng ký bằng tiền mã hóa (thanh toán bằng tiền mã hóa không được hoàn tiền). Giá và các gói số lượng lớn có trên trang đăng ký. Nếu thanh toán qua Stripe, bạn được hoàn tiền toàn bộ trong 14 ngày kể từ lần mua đầu tiên khi có dưới 50 lượt tạo thành công. Đã đăng ký rồi? Hãy đổi số lượng trong Stripe Customer Portal thay vì mua lại.