Node.jsでGoogle Veo2 APIを使って動画を生成してみた
2025年4月10日、Google Cloud Next 25の際に発表された時点で使えるようになっていたGoogleの動画生成AIである「Veo2」。OpenAIのSoraに対する対抗馬であり、Runway Gen-3やDream Machineの対抗馬でもあります。
このVeo2のAPIが既にもう使えるようになっていたので、Node.jsにてテキストから動画を生成してみました。
目次
今回利用するツール等
今回はテキストから動画作成なので、画像から動画作成ではありませんが、APIでは画像からの動画生成にも対応しているようです。動画ですので画像の生成よりもプロンプトによる影響が大きいので、注意が必要です。OpenAIは現時点でSoraのAPI提供を考えていないようなので(ただし過去に流出騒動で存在自体はあるみたい)、Geminiがここは一歩先に出た感じですが、そのクオリティは如何に?
利用料金について
OpenAIではSoraをChatGPTで利用するにはChatGPT Plusに課金が必要です。月額2000円で1000クレジット分が使えるようで、その利用料金は以下のようになっています。
- 720pの動画5秒で60クレジットを消費(1クレジット当たり単純計算で2円。)
- 1000クレジットでおよそ16本/月の動画生成が可能
- クレジットが枯渇すると追加課金しない限り翌月まで待たなければならない
一方、Veo2のAPI利用料金は結構お高いです。
- 1秒あたり$0.35の料金(シンプルだけど高い。ドル円140円のレートだと、1秒当たり50円)
- 1回につき8秒の動画が生成されるので、400円も掛かる計算になる。
- デフォルトのAPIリクエストだと2本生成されるので、1度に800円も消費する。
- Soraと同じ5秒での金額計算では、250円/1本も掛かる計算になる
故に開発する際には、間違ってリクエストして生成出来たけどローカルにダウンロード失敗でも当然課金されるので、とんでもプライスになりかねません。今後安くなっていくとは思いますが、現時点では積極的に使える金額ではないなぁと思います(4本 vs 16本なので4倍高いという計算になる)。
図:あっという間に高額請求になります。
準備とコード
Vertex AIのMedia Studioで試せる
Google Cloudのコンソール内にあるMedia Studioで課金が必要ですが、プロンプトを使って生成することが可能です。画像をアップの方法がわからなかったですが、テキストからの動画生成は可能となっていて、右サイドパネルから細かい生成用の設定を行うことが可能です。
ただし、前述にもあるようにかなり高額なので、利用にあたっては十分な資金力と注意が必要になります。何度も気に食わないなぁと言って生成を繰り返しているととんでもないことになってしまいます。
Youtubeショートから既存のアップロード動画に対してVeo2の動画生成が出来るみたいですが、まだ日本は提供エリアに入っていなかったと思います。
図:Media Studioで作成
出力
今回の動画出力はリファレンスのデフォルトの指定のまま行っています。
- 1280x720の720p出力で設定(アスペクト比としては16:9)
- 1本あたり8秒でおよそ3MB程度のファイルサイズの動画が生成されます。
- 生成に掛かる時間は1本当たり30秒未満で高速生成されます。
- デフォルトだと2本生成されるので要注意。
といった感じです。GoogleスライドなどにGASで組み込んでリクエストし、直接スライドに動画を差し込むなんて使い方も出来ると思います。
事前準備
モジュールの追加
今回のコードはNode.jsでリクエストを投げますが、npmにてモジュールを1つ追加する必要があります。「Google Gen AI SDK for TypeScript and JavaScript」というモジュールを追加しておいてください。
Node.jsにて新規プロジェクトを作成したら、以下のコマンドを実行してモジュールを追加します。
1 |
npm install @google/genai |
また、APIキーも必要になるので、こちらのサイトからキーを生成しておいてください。APIキーは大切なものなので流出しないように細心の注意が必要です。できれば環境変数などに入れてprocess.envで呼び出して使うようにしましょう。
ソースコード
veo-2.0-generate-001が利用するモデル名になります。リクエストオプションを指定すると細かく制御できます。Operationのconfig内で指定します。
- durationSecondsで生成する秒数指定(デフォルトが8秒)
- aspectRatioで縦横比を指定(デフォルトは16:9)
- sampleCountで生成する数を指定(デフォルトは2のようです)
Cloud Storageに保存も指定できますがデフォルトだとBase64にエンコードされたものが返ってくるようです。このコードではindex.jsのコードと同じフォルダ内にvideo0.mp4といった形でダウンロードして生成させています。
※ReadableStream is lockedというエラー対策の為にReadableStreamには1個ずつ接続するように、stream.pipelineを使って制御を加えています(エラーになってもとりあえず出力はされますが)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
// @google/genai パッケージから GoogleGenAI クラスをインポート const { GoogleGenAI } = require("@google/genai"); //標準のモジュールを追加する const { createWriteStream } = require("fs"); const { Readable } = require("stream"); const { pipeline } = require('stream/promises'); //APIキーの指定 const googleApiKey = "APIキーをここに指定する"; // APIキーが設定されているか確認 if (!googleApiKey) { console.error("エラー: APIキーが指定されていません。"); process.exit(1); } //GoogleGenAI クライアントを初期化 const ai = new GoogleGenAI({ apiKey: googleApiKey }); async function main() { try { // 動画生成APIを呼び出し、オペレーションを開始 let operation = await ai.models.generateVideos({ model: "veo-2.0-generate-001", // 使用するモデル名 prompt: "ここに動画生成用のプロンプト文を指定する", // 動画の内容を指定するプロンプト config: { personGeneration: "dont_allow", // 人物の生成を許可しない設定 aspectRatio: "16:9", // 動画のアスペクト比 }, }); //オペレーションのIDを表示 console.log("オペレーションID:", operation.name); console.log("生成完了までポーリングします (10秒ごと)..."); // オペレーションが完了するまでポーリング while (!operation.done) { // 10秒待機 await new Promise((resolve) => setTimeout(resolve, 10000)); // オペレーションの最新の状態を取得 (API呼び出しは要確認) operation = await ai.operations.getVideosOperation({ operation: operation, }); console.log("ポーリング中... 完了:", operation.done); } console.log("動画生成が完了しました。"); // オペレーションの結果に生成された動画が含まれているか確認 if (operation.response?.generatedVideos && operation.response.generatedVideos.length > 0) { console.log(`${operation.response.generatedVideos.length} 個の動画をダウンロードします...`); // 生成された各動画を処理 // Promise.all を使って非同期処理を並列実行し、完了を待つ await Promise.all(operation.response.generatedVideos.map(async (generatedVideo, n) => { try { //VideoのURLを取得する const videoUri = generatedVideo.video?.uri; if (!videoUri) { console.warn(`動画 ${n} のURIが見つかりません。スキップします。`); return; // この動画の処理をスキップ } // 動画データを取得するためのURLを構築 (変数から読み込んだAPIキーを付与) const fetchUrl = `${videoUri}&key=${googleApiKey}`; console.log(`動画 ${n} をダウンロード中: ${videoUri}`); //ダウンロードの実行 const resp = await fetch(fetchUrl); //ダウンロードリクエストの結果 if (!resp.ok) { throw new Error(`動画 ${n} のダウンロードに失敗しました: ${resp.status} ${resp.statusText}`); } if (!resp.body) { throw new Error(`動画 ${n} のレスポンスボディが空です。`); } //ファイル名の指定とローカルにMP4の生成 const outputFilename = `video${n}.mp4`; const writer = createWriteStream(outputFilename); // Readable.fromWeb が存在するか確認 (Node.jsバージョン互換性のため) if (typeof Readable.fromWeb !== 'function') { throw new Error("Readable.fromWeb はこの Node.js バージョンでは利用できません。Node.js v17 以降を使用するか、ストリームを別の方法で処理してください。"); } const nodeReadableStream = Readable.fromWeb(resp.body); //pipeline を使用したストリーム処理 try { // nodeReadableStream から writer へデータをパイプライン処理 await pipeline(nodeReadableStream, writer); // pipeline が正常に完了したらメッセージを表示 console.log(`動画 ${n} が ${outputFilename} として保存されました。`); } catch (pipelineError) { // pipeline 中にエラーが発生した場合 (読み込み、書き込み双方のエラーを捕捉) console.error(`ストリームパイプラインエラー (動画 ${n}):`, pipelineError); } } catch (downloadError) { //エラーは個別にログ出力する console.error(`動画 ${n} の処理中にエラーが発生しました:`, downloadError.message || downloadError); } })); console.log("すべての動画ダウンロード処理が試行されました。"); } else { console.log("生成された動画が見つかりませんでした。"); if (operation.error) { console.error("オペレーションエラー:", operation.error); } } } catch (error) { console.error("メイン処理中にエラーが発生しました:", error); } } // メイン関数を実行 main(); |
生成してみた動画
いちごの開花の様子のタイムラプスと、猫の昼寝の様子をそれぞれ8秒間、4本作成したものを作りました。かなりハイレベルだと思います。自分もイチゴは栽培していますが、細部に渡ってよく出来てるなと感じます。