新しい短尺動画を作るたびに、いちばん手が止まるのは動画本体ではなく「告知文をどう書くか」でした。30秒のクリップを何度も見返して、見せ場の秒数をメモして、キャプションとハッシュタグを起こす。この一連の作業が、私自身の個人開発では毎回ボトルネックになっていました。App Store と Google Play の両方でアプリを配信していると、告知動画を出す機会も増え、そのたびに同じ手起こしを繰り返していたのです。
Omni Flash が公開プレビューに入って、この手起こしを1回のAPI呼び出しに畳めるようになりました。動画をフレームに割ってから個別に投げるのではなく、動画そのものをネイティブに受け取り、告知に必要な要素を構造化 JSON で返してもらう。今日はその最小構成を、実際に動くコードとコスト試算とともに置いていきます。
フレーム抽出を挟まない、という選択
これまでの動画理解は、ffmpeg で数秒おきにフレームを切り出し、静止画として個別に投げるのが定番でした。安定はしますが、抜き出した瞬間の間に起きた動きは落ちますし、呼び出し回数がそのままコストと待ち時間になります。
Omni Flash はネイティブに動画を1本受け取り、時間軸を保ったまま解釈します。フレーム抽出をどこで手放すべきか、その境界そのものはOmni Flash で動画理解を1パスに畳む設計 で実測ベースに整理しました。本記事はその先、「1パスで受け取った理解を、告知に使える構造化データとして取り出す」ところに焦点を当てます。
告知メタデータの抽出は、実は動画理解のなかでも Omni Flash の相性が良い部類です。理由は、欲しい出力が最初から決まっているからです。キャプション、ハッシュタグ、チャプター、見せ場のタイムスタンプ。出力の形を先に固定できるなら、モデルに自由記述をさせず、スキーマに流し込むのが最も安定します。
欲しい出力の形を先に決める
まずは受け取りたい JSON の形を Pydantic で宣言します。ここを曖昧にすると、モデルは毎回違う構造で返してきて、後段のパースが崩れます。
from pydantic import BaseModel
class Chapter ( BaseModel ):
start: str # "0:07" のような mm:ss
title: str # そのチャプターの短い見出し
class Highlight ( BaseModel ):
timestamp: str # 見せ場の秒数 "0:12"
reason: str # なぜここが見せ場なのか
class PromoMeta ( BaseModel ):
hook_caption: str # 最初の1行に置く短い引き
long_caption: str # 本文用のやや長いキャプション
hashtags: list[ str ] # 5〜8個を想定
chapters: list[Chapter] # 動画の流れ
highlights: list[Highlight] # サムネや切り抜きに使う候補
ポイントは、timestamp を数値ではなく mm:ss の文字列にしていることです。動画の時間表現をモデルに数値化させると、単位(秒かフレームか)が揺れます。表示に使う形のまま受け取り、必要なら後で秒に直すほうが安定します。
動画を1回渡して構造化メタデータを受け取る
Files API に動画をアップロードし、ACTIVE になってから response_schema 付きで1回だけ呼び出します。
import time
from google import genai
from google.genai import types
client = genai.Client( api_key = "YOUR_API_KEY" )
video_file = client.files.upload( file = "promo_clip.mp4" )
# アップロード直後は PROCESSING。ACTIVE になるまで待つ
while video_file.state.name == "PROCESSING" :
time.sleep( 2 )
video_file = client.files.get( name = video_file.name)
prompt = (
"この動画はモバイルアプリの告知用クリップです。"
"SNS 投稿に使う告知文・ハッシュタグ・チャプター・見せ場を日本語で作ってください。"
"hook_caption は 40 文字以内、long_caption は 120 文字前後にしてください。"
)
resp = client.models.generate_content(
model = "gemini-omni-flash-latest" , # 公開プレビューの実体は changelog で確認
contents = [video_file, prompt],
config = types.GenerateContentConfig(
response_mime_type = "application/json" ,
response_schema = PromoMeta,
media_resolution = types.MediaResolution. MEDIA_RESOLUTION_LOW ,
),
)
meta = resp.parsed
print (meta.hook_caption)
for h in meta.highlights:
print (h.timestamp, h.reason)
resp.parsed は response_schema に沿って検証済みのオブジェクトです。ここが空や None で返るときは、たいてい response_mime_type を application/json にし忘れているか、スキーマに必須項目が多すぎてモデルが埋めきれていないケースです。スキーマ検証まわりでつまずいたときの切り分けはGemini API の構造化出力エラーを直す手順 にまとめています。
モデル ID は公開プレビュー段階で表記が動きます。私は運用に載せるときは、モデル名を環境変数に逃がして直書きしないようにしています。プレビューの実体はGemini API changelog で都度確認するのが確実です。
見せ場の秒数がずれるときは fps を疑う
構造化メタデータのうち、いちばん精度が問われるのが highlights のタイムスタンプです。ここが数秒ずれると、切り抜きやサムネの指示として使えなくなります。
Omni Flash に渡す動画は、既定では毎秒1フレーム相当でサンプリングされます。テンポの速いクリップでは、この粒度だと見せ場を1〜2秒取りこぼすことがあります。動きの速い動画では、Part 側で fps を上げて渡します。
part = types.Part(
file_data = types.FileData(
file_uri = video_file.uri,
mime_type = video_file.mime_type,
),
video_metadata = types.VideoMetadata( fps = 2 ), # 既定1 → 2 で細かい動きを拾う
)
resp = client.models.generate_content(
model = "gemini-omni-flash-latest" ,
contents = [part, prompt],
config = types.GenerateContentConfig(
response_mime_type = "application/json" ,
response_schema = PromoMeta,
),
)
fps を上げるほどタイムスタンプは細かく取れますが、入力トークンはほぼ比例で増えます。私の環境では、30秒のアプリ告知クリップで fps を 1 から 2 に上げると、見せ場のずれは体感で 2 秒前後から 1 秒以内に収まる一方、入力トークンは概ね倍になりました。落としどころは動画のテンポ次第です。ゆっくりした癒し系のクリップなら fps 1 のまま、動きの速いゲーム画面なら 2〜3 が目安になります。fps とトークンの関係をもう一段詰めたいときは動画理解のタイムスタンプ精度と fps 制御 を併せてご覧ください。
1本あたりのコストを見積もる
告知作業を自動化する判断は、結局「1本いくらで回るか」で決まります。動画入力のトークンは、おおまかに 秒数 × fps × 1フレームあたりのトークン で効いてきます。
設定 30秒クリップの入力トークン目安 用途
media_resolution LOW / fps 1 最小 癒し系・スライドショー系の告知
media_resolution LOW / fps 2 約2倍 動きのあるアプリ画面
media_resolution MEDIUM / fps 2 さらに増加 細部の視認が重要なクリップ
私の運用では、告知メタデータの用途なら media_resolution は LOW で十分でした。告知文とハッシュタグに、フレーム内の細かい文字までは要らないからです。細部の視認が必要になるのは、動画内の UI テキストを読ませたいときくらいです。告知メタデータ用途なら、まずは LOW / fps 1 から始めることを推奨します。そのうえで、見せ場がずれるクリップだけ fps を上げる、という順に削っていくのがコスト面では素直です。
本番運用で崩れやすい落とし穴と、その回避策
1パス抽出は快適ですが、崩れ方には癖があります。本番運用に載せる前に、私自身が告知パイプラインを回すなかで踏んだ落とし穴を3つ挙げておきます。いずれも回避策はシンプルです。
長尺動画で見せ場が前半に偏る
長尺動画では、highlights が動画前半に偏ることがあります。モデルは早い段階で「見せ場を見つけた」と判断しがちだからです。3分を超える動画なら、前半・後半で2回に分けて投げ、あとで結合するほうが均等に拾えます。この分割は本番運用でも安定して効きました。
無音区間で告知文が抽象的になる
無音区間の長いクリップでは、long_caption が動画の雰囲気だけを述べた抽象的な文になりやすいです。プロンプトに「画面に映る具体的な要素を1つ以上入れる」と条件を足すと、この落とし穴を回避でき、手直しの量が減りました。
縦動画のテロップを読み落とす
縦動画(9:16)でも動作しますが、テロップが画面端に寄っていると読み落とすことがあります。テロップ由来の情報が告知に必須なら、その文言だけはこちらで用意し、モデルには構成とタイムスタンプに専念させて読み落としを回避するほうが確実です。
生成物は、必ず人の目を通してから投稿する
最後に、私自身が守っている線引きを共有させてください。この記事の手法は、告知文やハッシュタグといった「作品を世に出す作業」を助けるものです。作品そのもの、つまりアプリの中身や動画の映像表現は、これまで通り手で作ります。
告知の下書きを AI に任せ、投稿前に自分で読み直して整える。この順番だけは崩さないようにしています。生成された long_caption の言い回しが自分の声とずれていたら、そこは書き直す。AI に告知を回してもらうのは、創作そのものの時間を確保するためだ、というのが個人開発とアート活動を並行してきたなかで固まった使い方です。
次の一歩
まずは手元の短尺動画を1本、media_resolution LOW / fps 1 で PromoMeta スキーマに流してみてください。返ってきた highlights のタイムスタンプが実際の見せ場とどれだけ合っているかを見れば、そのクリップに必要な fps が一目で分かります。そこから削るか足すかを決めれば、あなたの告知作業に合った最小構成が見つかります。
動画からの告知づくりで詰まっていた時間が、少しでも軽くなれば嬉しいです。お読みいただきありがとうございました。