Gemini Omni Flash の使い方:Google Flow API で音声付きの AI 動画を生成する
8分で読めます • 2026年6月15日(2026年11月12日更新)
useapi.net はサードパーティの API です。Google とは提携しておらず、Google の承認も受けていません。
目次
はじめに
Gemini Omni Flash は、useapi.net の Google Flow API を通して、curl 1回で音声付きの AI 動画を作ります。声の同期、キャラクター、動画から動画への編集にも対応し、使うのはお使いの Google Flow の Web サイトのプランです(Google AI:Omni 1.1 Flash は Plus、Pro、Ultra、Omni 1.2 Flash はすべてのプランで使え、無料アカウントでは10秒まで)。10秒・720p の動画は Google AI Ultra $199.99 で $0.12 で、公式の Gemini API では $1.01 です(2026年11月時点)。Google Cloud プロジェクトも API キーも必要ありません。同じプランで Google Vids でも Omni の動画を作れ、こちらは Flow クレジットを使いません。
Google Flow の画面ではこのモデルは Omni 1.1 Flash と表示され、API の値は model: "omni-flash" です。Google が2026年11月5日に公開した Omni 1.2 Flash は別の値 model: "omni-flash-1.2" で、3〜15秒の任意の長さに対応し、クレジットは秒単位で、1回最大10秒の動画の延長もこのモデルで行います。omni-flash を送るコードは引き続き 1.1 を使います。15秒の動画、声を付けた延長、Google Flow と Google Vids での動画1本あたりの料金については、Gemini Omni 1.2 Flash API の使い方を参照してください。
Omni Flash は Veo と同じ Google Flow API で、model: "omni-flash-1.2" または model: "omni-flash" という1つのフィールドで選びます。ただし、モデルの性格は違います。Veo がテキストから動画を作る強力なエンジンなのに対し、Omni Flash は音声を最初から生成し、参照素材を豊富に受け付けます。
- 音は動画の一部として生成されます。ナレーション、セリフ、環境音が同じモデルから出てくるので、後から付け足す必要はありません。
- 声は、内蔵の30種類のシステムの声のプリセットに加え、ご自身のセリフと演技の指示から作るカスタムの声も使え、
referenceAudio_1..5で渡します。 - 画像と動画の参照として、参照画像から動画(R2V)には Omni 1.2 Flash で最大10枚(Omni 1.1 Flash では7枚)の参照画像を使え、
referenceVideo_1でアップロードした動画や生成した動画を動画から動画(V2V)で編集できます。 - 再利用できるキャラクターでは、参照画像と声を名前付きのキャラクターにまとめ、複数の動画で使い回せます。
- 長い動画も作れます。Omni 1.1 Flash は4、6、8、10秒、Omni 1.2 Flash は3〜15秒の任意の長さで(Veo は最長
8)、Omni 1.2 は1回最大10秒ずつ動画を延長できます。
このガイドでは、コピーしてそのまま使える curl で2回の呼び出しの流れを示し、Omni Flash ならではの参照モードを紹介したあと、プロンプトのリストから一括生成する Node.js スクリプトを載せます。テキストから動画を作るだけなら、姉妹編の Veo 3.1 のチュートリアルを参照してください。
対応モデル
このガイドでは、Google の音声付き動画モデル Omni 1.1 Flash(model: "omni-flash")と Omni 1.2 Flash(model: "omni-flash-1.2")を使います。Omni 1.1 のクレジット数は動画の長さで決まります。
| 長さ | 動画1本のクレジット |
|---|---|
| 4秒 | 7 |
| 6秒 | 10 |
| 8秒 | 12 |
| 10秒 | 15 |
| V2V 編集 | 20 |
これはデフォルトの 720p での料金です。resolution: "360p" を渡すと約半分になり、長さ順に4、5、6、7クレジット、V2V 編集は10クレジットです。360p の動画は、あとから POST /videos/upscale で 720p にできます。
Omni 1.2 Flash(model: "omni-flash-1.2")は3〜15の任意の整数秒を受け付け、秒単位で料金がかかります。料金はどのプランでも同じです。
| 長さ | 720p | 360p |
|---|---|---|
| 3秒 | 9 | 3 |
| 8秒 | 24 | 8 |
| 10秒 | 30 | 10 |
| 15秒 | 60 | 20 |
10秒までは 720p で1秒3クレジット、360p で1クレジットで、10秒を超える分は1秒あたり2倍になります。V2V 編集は編集した長さの動画と同じ料金で、1.2 では最大15秒の動画を編集できます。無料の Google アカウントでも、10秒以下の動画なら 1.2 を使えます。
Omni Flash は POST /videos で5つのモードに対応しています。テキストから動画(T2V)、Omni 1.2 Flash で最大10枚(Omni 1.1 Flash では7枚)の referenceImage_1..10 を使う参照画像から動画(R2V)、startImage の開始フレームから作る画像から動画(I2V)、startImage と endImage の開始・終了フレームの間をつなぐ画像から動画(I2V-FL)、そして referenceVideo_1 でアップロードした MP4 を編集する動画から動画(V2V)です。詳しくは下の参照モードを参照してください。
料金
Omni 1.1 Flash には有料の Google AI のプラン(Plus、Pro、Ultra)が必要で、Omni 1.2 Flash は無料アカウントでも10秒まで使えます。useapi.net が動画ごとに料金を取ることはありません。動画は長さに応じて Flow クレジットを使います(対応モデルを参照)。各プランでの動画1本の料金と公式の Gemini API との比較は、Google Flow の料金表にあります。
Google AI のプランには、Google Vids の月間利用枠も付いています。Google Vids は Gemini Omni 1.2 Flash で最長15秒の動画を作り(Google の発表)、Flow クレジットは使いません。Ultra $199.99(Google の呼び方では Ultra 20x)では月10,000秒で、1080p の10秒の動画が約1,000本です。これは Flow の25,000クレジットで作れる Omni 1.1 Flash の10秒の動画約1,666本(720p の Omni 1.2 Flash なら約833本)とは別に使えます。この秒数は Google のファミリー プランのメンバー全員で共有されます。ほかのプランの利用枠はもっと少なく(Google の Google Vids の Gemini のページを参照)、当社がテストした無料アカウントではゼロでした。Google Vids API はこの利用枠を CAPTCHA なしで使います。アカウントは Google Vids のセットアップで接続します。
2回の API 呼び出しで動画を生成する
useapi.net の API トークンと、接続済みの Google Flow アカウントが必要です。下の Omni 1.1 Flash の例には有料の Google AI のプラン(Plus、Pro、Ultra)が必要です(Omni 1.2 Flash は無料アカウントでも10秒まで動きます)。動画のジョブは非同期です。async: true を渡すと作成の呼び出しはすぐに jobid を返すので、動画ができるまでポーリングします。
- ジョブを送信する —
model: "omni-flash"を指定してPOST https://api.useapi.net/v1/google-flow/videos:
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 は音声も生成するモデルなので、灯台守のナレーションと打ち寄せる波の音をプロンプトだけから作ります。音声のパラメーターは必要ありません。非同期モードでは、応答はすぐに 201 Created で返り、小文字の jobid と 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
}
}
- 結果をポーリングする —
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 は created → started → completed と進みます。completed になると、完成した MP4 が response.media[].videoUrl に入ります(1つのプロンプトで count > 1 を指定すると、動画ごとに1つずつエントリーが返ります)。
{
"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 は通常60〜180秒で完了します。署名付きの videoUrl は約6時間有効なので、早めにダウンロードしてください。ポーリングしたくない場合は、作成のボディに replyUrl を渡すと、ジョブの完了時に Webhook で通知を受け取れます。
参照モード
Omni Flash の最大の特長は、参照として渡せる素材の多さです。参照はすべて POST /assets/email のアップロード用エンドポイントから取得します(キャラクターは POST /characters から)。アップロードの応答では、参照 ID が mediaGenerationId.mediaGenerationId に入れ子で入っており、この入れ子の文字列を渡します。omni-flash が受け付けるモードは次のとおりです(POST /videos のドキュメントより)。
| モード | パラメーター | Omni Flash の上限 |
|---|---|---|
| 参照画像から動画(R2V) | referenceImage_1..10 | omni-flash-1.2 で画像最大10枚、omni-flash で7枚 |
| 画像から動画(I2V) | startImage | 開始フレーム1枚 |
| 画像から動画、開始・終了(I2V-FL) | startImage + endImage | 2枚のフレームのみ、ほかの参照は不可 |
| 動画から動画の編集(V2V) | referenceVideo_1(+ 任意で startFrameIndex_1 / endFrameIndex_1) | アップロードした MP4 1本 |
| キャラクター | character_1..10(POST /characters から) | referenceImage_* と合わせて omni-flash-1.2 で計10、omni-flash で7 |
| 声のナレーション | referenceAudio_1..5 | 最大5(R2V)/ 最大3(V2V 編集) |
参照画像から動画(R2V)では、1枚以上の画像をアップロードし、referenceImage_1、referenceImage_2 … として渡します(_4〜_7 の枠は Omni Flash のみ)。参照画像は 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"
}'
動画から動画の編集(V2V)では、Content-Type: video/mp4 で MP4 を POST /assets にアップロードし、返ってきた ID を referenceVideo_1 として渡すと、その動画を編集できます。これは Omni Flash のみの機能で、リクエストは V2V モードになります。duration は受け付けません(出力の長さは入力のトリム範囲と同じで、omni-flash で最大10秒、omni-flash-1.2 で最大15秒)。元の動画は、24 fps の仮想タイムライン上で startFrameIndex_1(0〜239)と endFrameIndex_1(1〜240、240 = 10秒)でトリムでき、omni-flash-1.2 では359と360(15秒)まで指定できます。
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
}'
プロンプトには、渡した枠を指すインラインの @ マーカーも書けます。たとえば @referenceImage_1、@character_1、@referenceAudio_1 です。マーカーは任意で、それぞれに対応するボディのパラメーターが必要です(@referenceVideo_* はインラインでは使えないので、ボディのフィールドとして渡します)。詳しくは Inline @-mention markers を参照してください。
声とキャラクター
Omni Flash の音声と人物の機能は、短いセットアップの呼び出し2つで使えます。
カスタムの声は、POST /voices で、30種類のシステムのプリセットの1つをもとに、ご自身の dialog と voicePerformance を加えて保存します。応答の voice フィールドは参照 ID で、これを POST /videos に referenceAudio_1..5 として渡します。
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:...", ... }
referenceAudio_* の各枠には、システムの声の名前(大文字・小文字は区別せず、Aoede、Kore、Puck、Zephyr など30種類のプリセットのいずれか)か、上の呼び出しで作ったユーザーの声の ID を渡せます。声の作成には reCAPTCHA が必要です(自動で解決されます。よくある質問を参照)。
再利用できるキャラクターは、POST /characters で、参照画像1〜2枚と任意の声を名前付きのキャラクターにまとめたものです。応答の character フィールドを 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:...", ... }
Omni Flash では、キャラクターはどの長さでも使え、画像の参照枠の合計10枚を referenceImage_* と共有します(referenceImages.length + Σ(character.imgs) ≤ 10, Omni 1.1: 7)。POST /characters には CAPTCHA は不要です。キャラクターと声は Veo でも使えますが、上限はより厳しくなります(画像3枚、声1つ、8秒のみ)。詳しくは What can use a character? を参照してください。
Omni 1.2 で動画を延長する
POST /videos/extend は、30秒未満の動画に Omni 1.2 Flash で1回3〜10秒を追加します。声、参照画像、キャラクターを最大3つまで指定でき、1本のつながった動画を返します(有料プランのみ)。Gemini Omni 1.2 Flash API の使い方の手順では、10秒の動画を2人の声で2回延長して30秒にしています。その動画は下の例にあります。
スクリプトで一括生成する
良いテイクを見つけるには何度も試す必要があり、それを手作業で行うのは面倒です。下の Node.js スクリプトは、prompts.json からプロンプトのリストを読み込み、参照画像、開始・終了フレーム、元の動画をアップロードし、character_* の ID と resolution はそのまま渡します。各ジョブを model: "omni-flash" の非同期モードで送信し、ポーリングして、完成した MP4 をすべてダウンロードします。まとめてキューに入れておき、あとで良いものだけを選べます。開始フレームなしの終了フレームのように、API が拒否する組み合わせは送信前に警告を出して止まります。
Node.js v21 以降が必要です。prompts.json と omni-flash.mjs を同じフォルダーに置き、node ./omni-flash.mjs API_TOKEN EMAIL を実行します。API_TOKEN は useapi.net の API トークン、EMAIL は接続済みの Google Flow アカウントのメールアドレスです。スクリプトはメールアドレスからアカウントを自動で探し、送信前にその health フィールドを確認します。
クローンしてローカルで実行したい場合は、omni-flash.mjs と prompts.json の完全版が GitHub の useapi/google-flow-api にあります。
Claude Code や Codex を使っているなら、omni-flash-video エージェントスキルでこの作業をすべてエージェントに任せられます。作りたい動画を説明して画像の場所を伝えると、画像のアップロード、リクエストの作成、完了の待機、.mp4 の保存まで行います。インストールは npx skills add useapi/google-flow-api --skill omni-flash-video です。
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"
}
]
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();
例
この Google Flow API で作った実際の Gemini Omni Flash の動画です。model: "omni-flash-1.2" と model: "omni-flash" で、延長、声、参照、キャラクター、V2V 編集を使っています。
Omni 1.2 の延長 » 10秒の動画を2回延長して30秒に、2人の声で
出典は Google Flow: Omni 1.2 Flash, 15-Second Videos, and Extend with Voices(英語)です。最初の10秒を omni-flash-1.2 で作り、referenceAudio_1 を付けて POST /videos/extend を2回呼びました。
V2V 編集 » アップロードした MP4 を、動きと構図はそのままでアニメ風に
出典は Google Flow: Gemini Omni Flash(英語)です。referenceVideo_1 で編集しました。
インラインの @ メンション » @character_1、@referenceImage_1、@referenceImage_2 とカスタムの声で作るノワール風の R2V 動画
出典は Google Flow: Inline @-mentions(英語)です。インラインの @ メンションのマーカーで、それぞれの参照を指定しました。
キャラクター + V2V 編集 » 再利用できる2人のキャラクターをノワール風の動画に合成
出典は Google Flow: Inline @-mentions(英語)です。インラインの @character_* を使い、referenceVideo_1 で V2V 編集しました。声とキャラクターを準備する流れは Reusable Voices and Characters(英語)も参照してください。
よくある質問
- Gemini Omni Flash とは何ですか? useapi.net を使えば、Google Flow にある Google の音声付き動画モデル Gemini Omni Flash を、Google Flow API で
model: "omni-flash-1.2"(Omni 1.2 Flash)またはmodel: "omni-flash"(Omni 1.1 Flash)を指定して動かせます。音のない動画モデルと違い、サウンドトラック、ナレーション、セリフ、環境音を同じ動画の一部として作り、画像、動画、音声の参照と再利用できるキャラクターを受け付けます。 - Omni Flash と Veo 3.1 の違いは? useapi.net を使えば、どちらも同じ Google Flow API で動き、
modelフィールドで選べます。Veo はテキストや画像から動画を作る、より忠実度の高いエンジンです。一方 Omni Flash は音声を生成し、参照素材を豊富に受け付けます。動画から動画の編集(referenceVideo_1)、1本の動画に複数の声、どの長さでも使える再利用可能なキャラクター、Veo より長い動画に対応しています。正確な上限は参照モードを参照してください。 - 自分の声、参照動画、参照音声を使えますか? useapi.net を使えば、使えます。内蔵のシステムの声か、POST /voices で作ったカスタムの声を
referenceAudio_1..5として渡してナレーションを付けられ、POST /assets で MP4 をアップロードすればreferenceVideo_1として編集できます(V2V)。各モードの上限は参照モードと声とキャラクターを参照してください。 - Omni Flash の公式 API はありますか? useapi.net を使えば、Omni 1.1 Flash は Google 自身の API よりはるかに安く使えます。8秒・720p の動画は Google AI Ultra $199.99 で $0.10 で、公式の Gemini API では $0.81 です。Gemini API は2026年6月30日からこのモデルを秒単位で販売しています(2026年11月時点)。useapi.net では、お使いの Google Flow の Web サイトのプランで、標準的な REST エンドポイントから動きます。
- Omni Flash で10秒より長い動画を作れますか? useapi.net を使えば、作れます。Omni 1.2 Flash(
model: "omni-flash-1.2")は1回の呼び出しで最大15秒を作り、POST /videos/extend は声を付けて1回最大10秒を追加します。8秒・720p の動画は Google AI Ultra $199.99 で $0.19、Google 自身の API では $0.56 です(2026年11月時点)。Google Flow と Google Vids での長さの上限と料金はすべて Gemini Omni 1.2 Flash API の使い方にあります。 - CAPTCHA を解く必要はありますか? useapi.net を使えば、ご自身で解く必要はありません。Google Flow は動画ごと、新しい声ごとに reCAPTCHA を求めますが、useapi.net のワーカーが自動で解決します。最初の Google Flow アカウントには、当社の AntiCaptcha、CapSolver、EzCaptcha のキーで300回の無料解決が付き、その後は POST /accounts/captcha-providers でご自身のプロバイダーのキーを追加します。POST /characters には CAPTCHA は不要です。
おわりに
ご質問は Discord サーバー または
Telegram チャンネルへどうぞ。
コード例は GitHub リポジトリにあります。
AI アシスタントに Google Flow の作業を任せることもできます。ホスト型の Google Flow MCP サーバーを使うと、Claude Code、Cursor、VS Code から Google Flow Agent とチャットでき、Agent がお使いのアカウントで画像や動画を作ります。
API を使い始める
月額15ドルのサブスクリプション1つで、useapi.net のすべての API を使えます。トークン1つで、すべてのサービスに対応します。各サービスにご自身のアカウントを最大3つまで接続でき、REST API、負荷分散、ポーリングは useapi.net が担当します。アカウントを増やしたい場合は、サブスクリプションを1つ追加するごとに月額10ドルで、各サービスのアカウントが3つずつ増えます。
海外で使えるクレジットカードがない場合は、暗号資産で登録することもできます(暗号資産での支払いは返金できません)。料金とボリュームプランはサブスクリプションのページをご覧ください。Stripe でお支払いの場合、初回購入から14日以内で、成功した生成が50回未満であれば全額返金されます。すでに登録済みの場合は、新たに購入せず Stripe カスタマーポータルで数量を変更してください。