OpenAI Sora 2は、テキストまたは画像から高精細な動画を自動生成できる最先端のマルチモーダルAIである。2026年現在、広告クリエイティブや商品紹介動画の自動生成に活用されている。本記事では、renueが自社プロダクトとして実装しているSora 2 API統合の本番アーキテクチャをもとに、非同期ジョブ管理・image-to-video・Azure Storage統合の実装パターンを解説する。なお、OpenAIは2026年9月24日にSora 2 APIの提供終了を発表しており、本記事は廃止前の最終実装ガイドとしての性格も持つ。
重要: Sora 2 API廃止予定(2026年9月24日)
OpenAIは公式に、以下のモデルとAPIを2026年9月24日に廃止すると発表している。
- Videos API
- sora-2
- sora-2-pro
- sora-2-2025-10-06
- sora-2-2025-12-08
- sora-2-pro-2025-10-06
2026年4月時点の実装は依然として有効だが、長期的には後継モデルへの移行計画を準備しておく必要がある。本記事で解説する実装パターンは、将来的に別の動画生成APIにも応用可能な汎用性の高い設計である。
Sora 2 APIの基本仕様
| 項目 | 仕様 |
|---|---|
| エンドポイント | POST /v1/videos (ジョブ作成) |
| ステータス確認 | GET /v1/videos/{id} |
| ダウンロード | GET /v1/videos/{id}/content |
| 動画長(seconds) | 4 / 8 / 12 のいずれか |
| サイズ(size) | 720x1280 (縦) / 1280x720 (横) / 1024x1024 (正方形) 等 |
| 入力モード | text-to-video / image-to-video |
| ジョブ方式 | 非同期(ステータスをポーリング) |
| 音声 | 対応(natural language生成) |
本番品質Sora 2統合に必要な5レイヤー
レイヤー1: ジョブベース非同期処理
Sora 2 APIは同期APIではなく、ジョブベース非同期APIである。動画生成には数秒〜数分かかるため、以下の3ステップで処理する。
- POST /v1/videos: ジョブ作成、IDを返す
- GET /v1/videos/{id}: ステータスをポーリング(queued / in_progress / completed / failed)
- GET /v1/videos/{id}/content: 完了後にダウンロード
実装パターン(renueの`SoraVideoGenerator`)
class SoraVideoGenerator:
def __init__(self):
self.api_key = env_setting.OPENAI_API_KEY
self.base_url = "https://api.openai.com/v1"
VALID_SECONDS = ("4", "8", "12")
def create_video_job(self, prompt, model="sora-2", size="720x1280", seconds=8):
seconds_str = str(seconds)
if seconds_str not in self.VALID_SECONDS:
raise ValueError(f"seconds must be one of {self.VALID_SECONDS}")
resp = requests.post(
f"{self.base_url}/videos",
headers=self._headers(),
json={
"model": model,
"prompt": prompt,
"size": size,
"seconds": seconds_str,
},
timeout=60,
)
return resp.json()["id"]
def get_video_status(self, job_id):
resp = requests.get(
f"{self.base_url}/videos/{job_id}",
headers=self._headers(),
timeout=30,
)
return resp.json()
def download_video(self, job_id):
resp = requests.get(
f"{self.base_url}/videos/{job_id}/content",
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=120,
)
return resp.content
非同期処理の実装ポイント
- create_video_job のtimeout: 60秒(軽いジョブ作成のみ)
- download_video のtimeout: 120秒(動画ダウンロードは時間がかかる)
- get_video_status のtimeout: 30秒(軽い確認のみ)
- ポーリング間隔: 5〜10秒が推奨(早すぎるとレート制限、遅すぎるとUX悪化)
レイヤー2: image-to-video機能
Sora 2は「テキストから生成」だけでなく「画像から動画を生成」もサポートする。既存のバナー画像を動画化する用途で強力である。
image-to-video の制約
image-to-videoには重要な制約がある。**参照画像のサイズは動画サイズと一致していなければならない**。これを無視すると「Invalid input image size」エラーが発生する。
画像リサイズ前処理の実装
renueの実装では、参照画像を自動的にリサイズして動画サイズに合わせる処理を実装している。単純なリサイズではなく、**Fit + pad(黒背景)**方式を採用する。
def _download_and_resize_image(self, url, target_size):
from PIL import Image
import io
resp = requests.get(url, timeout=30)
if not resp.ok:
raise ValueError(f"Image download failed: {resp.status_code}")
target_w, target_h = (int(x) for x in target_size.split("x"))
img = Image.open(io.BytesIO(resp.content))
if img.mode != "RGB":
img = img.convert("RGB")
# Fit + pad(black background)
ratio = min(target_w / img.width, target_h / img.height)
new_w, new_h = int(img.width * ratio), int(img.height * ratio)
resized = img.resize((new_w, new_h), Image.LANCZOS)
canvas = Image.new("RGB", (target_w, target_h), (0, 0, 0))
canvas.paste(resized, ((target_w - new_w) // 2, (target_h - new_h) // 2))
buf = io.BytesIO()
canvas.save(buf, format="JPEG", quality=95)
return buf.getvalue(), "image/jpeg"
Fit + Pad方式の理由
- Crop方式: 元画像の重要部分を切り落とすリスクあり
- Stretch方式: 元画像のアスペクト比が崩れる
- Fit + Pad方式(採用): アスペクト比を維持し、余白を黒背景で埋める
広告クリエイティブでは元画像の意図を崩さないことが最優先のため、Fit + Pad方式が最適である。
multipart form-dataでの送信
image-to-videoではAPIリクエストがJSONではなくmultipart form-dataになる。JSONとファイルを混在させるパターン。
if input_image_url:
image_bytes, mime_type = self._download_and_resize_image(input_image_url, size)
data = {
"model": model,
"prompt": prompt,
"size": size,
"seconds": seconds_str,
}
files = {
"input_reference": ("reference.jpg", image_bytes, mime_type),
}
resp = requests.post(
f"{self.base_url}/videos",
headers={"Authorization": f"Bearer {self.api_key}"},
data=data,
files=files,
timeout=120,
)
重要な点: `headers`に`Content-Type`を指定しない。`requests`ライブラリがmultipart用のboundaryを自動設定してくれる。
レイヤー3: SSRF対策(セキュリティ重要)
image-to-videoで最も危険なセキュリティリスクは**SSRF(Server-Side Request Forgery)**である。ユーザーが任意のURLを指定できると、サーバーから内部ネットワークのリソースにアクセスされる可能性がある。
対策の原則
renueの実装では「ユーザー入力のURLは絶対に受け付けない」原則を徹底している。
- DB保存URLのみ: `input_image_url`は必ずDBから取得した信頼済みURLを使う
- ルーター層での検証: クライアントからは`creative_id`を受け取り、ルーター内部でDBからURLを取得
- コメントでの明記: 開発者が誤って書き換えないよう、`# nosec: URL is fetched from DB, not user input`のコメントで意図を明示
実装の注意点
# 信頼済みURL(DBから取得)のみを受け付ける
# ルーター側でSSRF対策済み
def _download_and_resize_image(self, url, target_size):
resp = requests.get(url, timeout=30) # nosec: URL is fetched from DB, not user input
...
静的解析ツール(Bandit等)でrequestsの使用が警告される場合があるため、`# nosec`コメントで明示的に「DBからの信頼済みURL」であることを示す。
レイヤー4: Azure Storage統合
Sora 2から取得した動画をそのままレスポンスで返すのではなく、永続化のためにオブジェクトストレージにアップロードする。renueの実装ではAzure Blob Storageを使用している。
アップロード処理の設計
def _upload_video(self, video_bytes, mime_type, prefix="creatives/videos"):
if not video_bytes:
raise ValueError("Empty video bytes")
extension = "mp4"
if "/" in mime_type:
ext = mime_type.split("/", 1)[-1]
if ext:
extension = ext
storage_config = get_storage_config()
has_storage = all([
storage_config.get("account_name"),
storage_config.get("account_key"),
storage_config.get("container_name"),
])
if not has_storage:
if env_setting.PY_ENV.upper() == "LOCAL":
encoded = base64.b64encode(video_bytes).decode("utf-8")
return f"data:{mime_type};base64,{encoded}"
raise ValueError("Azure storage is not configured")
file_name = f"{prefix}/{uuid.uuid4().hex}.{extension}"
response = upload_bytes_to_azure(
file_bytes=video_bytes,
file_name=file_name,
custom_name=file_name,
make_public=True,
expired_at=datetime.datetime.utcnow() + datetime.timedelta(days=365),
content_type=mime_type,
)
return response["file_url"]
実装の工夫点
- 拡張子の自動判定: mime_typeから拡張子を抽出(`video/mp4` → `mp4`)
- UUID命名: `uuid.uuid4().hex`でユニークなファイル名を生成
- プレフィックス整理: `creatives/videos/`のようにカテゴリ別にフォルダ分け
- 1年間の有効期限: `expired_at`を1年後に設定してコスト管理
- LOCAL環境での代替: Azure Storage未設定時はbase64データURLで返す(ローカル開発用)
レイヤー5: データクラスによる型安全性
生成された動画のメタデータを管理するため、`GeneratedVideo` dataclassを定義する。
@dataclass
class GeneratedVideo:
video_bytes: bytes
mime_type: str
url: Optional[str] = None
prompt_used: str = ""
duration: int = 0
各フィールドの意義
- video_bytes: 生バイナリ(必要に応じて再処理)
- mime_type: 拡張子判定や適切な配信ヘッダーの設定
- url: アップロード後のアクセスURL(Azure Blob Storage)
- prompt_used: 生成に使ったプロンプト(履歴管理)
- duration: 動画の長さ(秒)
dataclassを使うことで、型ヒント・イミュータブル性・デフォルト値管理が統一できる。
非同期ジョブ管理のワーカーパターン
Sora 2のジョブが完了するまで待つ間、Webリクエストをブロックしてはいけない。renueの実装では以下のパターンを採用している。
Celeryワーカーでのポーリング
- フロントエンドがPOST /creatives/generate-videoを叩く
- FastAPIがSora 2にジョブ作成リクエスト、job_idを取得
- Celeryタスクをenqueue(job_idを渡す)
- フロントエンドには即座に「生成開始」を返す
- Celeryワーカーが5秒ごとにステータス確認
- completed になったら動画をダウンロード、Azure Blobにアップロード
- DB更新(creatives.video_url を設定)
- WebSocket/Pollingでフロントエンドに完了通知
タイムアウト対策
- ジョブ作成のタイムアウト: 60秒(通常は即応答)
- ポーリング最大時間: 10分(これを超えたら失敗扱い)
- ダウンロードのタイムアウト: 120秒(動画ファイルサイズによる)
エラーハンドリング
Sora 2 APIのエラー種別
- 400 Bad Request: プロンプト違反、sizeまたはsecondsが不正
- 401 Unauthorized: APIキーが無効
- 429 Too Many Requests: レート制限(リトライが必要)
- 500 Internal Server Error: OpenAI側の問題(リトライで解決する場合が多い)
- job failed: ジョブは作成されたが生成中にエラー
リトライ戦略
if not resp.ok:
logger.error("Sora video job creation failed: %s %s", resp.status_code, resp.text)
raise ValueError(f"Sora API error: {resp.status_code} {resp.text}")
ログに詳細(status_code + text)を必ず残す。本番運用では、500系エラーは自動リトライ(指数バックオフ)、400系はユーザー通知、401/429は管理者通知、という戦略が効果的である。
コンテンツポリシー
Sora 2は強力な生成AIだが、OpenAIのコンテンツポリシーに従う必要がある。以下の内容は生成できない。
- 有名人の顔や声を模倣
- 暴力的・性的・差別的なコンテンツ
- 他者の著作権を侵害する内容
- 虚偽情報の動画
本番運用ではプロンプトの事前フィルタリング(禁止ワードチェック)を実装し、ユーザーに違反の可能性を事前に通知する設計が推奨される。
renueの実装特徴
renueは「Self-DX First」の方針のもと、Sora 2統合を自社プロダクトとして実装している。社内12業務を553のAIツールで自動化済み(2026年1月時点)であり、Sora 2統合は広告代理AIエージェントの動画生成機能として組み込まれている(全て公開情報)。
技術スタック
- 言語: Python 3.11
- HTTPクライアント: requests
- 画像処理: Pillow(Image.LANCZOS でリサイズ)
- ストレージ: Azure Blob Storage
- 非同期ジョブ: Celery
- dataclass: `GeneratedVideo`
Sora 2廃止後の移行計画
2026年9月24日のSora 2廃止に備え、以下の準備が推奨される。
移行候補
- OpenAI後継モデル: OpenAIが新しい動画生成APIを提供する可能性
- Google Veo: Google DeepMindの動画生成モデル
- Runway Gen-3: プロ向け動画生成AI
- Luma Dream Machine: 商用利用可の動画生成
- Kling AI: 中国発の高品質動画生成
- Azure OpenAI Videos: Microsoft経由でのアクセス
移行を楽にする設計
API固有のロジックを`SoraVideoGenerator`クラスに閉じ込めることで、将来的に別のAPIに切り替える際も最小限の修正で済む。Strategy Patternで動画生成プロバイダーを抽象化しておくとさらに移行が容易になる。
業界別の活用パターン
| 業界 | 主な活用 |
|---|---|
| EC/D2C | 商品紹介動画、SNS広告動画 |
| 不動産 | 物件内覧動画、周辺環境の可視化 |
| 教育 | 講義の視覚化、実験デモ動画 |
| 観光 | 観光地紹介動画、宿泊施設PR |
| 採用 | 会社紹介動画、社員インタビュー風動画 |
| マーケティング | キャンペーン動画、A/Bテスト用バリエーション |
導入時のよくある失敗パターン
- 同期APIと勘違いする: タイムアウトで失敗
- 画像サイズを事前リサイズしない: image-to-videoでエラー
- SSRF対策を怠る: セキュリティインシデント
- Azure Storageを設定しない: 動画を永続化できない
- ポーリング間隔が短すぎる: レート制限に当たる
- エラー種別を区別しない: 不要なリトライでコスト増
- コンテンツポリシーを無視: API利用停止のリスク
- 廃止予定を考慮しない: 2026年9月後に動かなくなる
よくある質問
Sora 2のコストはどれくらい?
動画の長さとサイズによるが、1本あたり数ドル〜十数ドル程度。text-to-videoとimage-to-videoで料金が異なる場合がある。最新情報はOpenAI公式の料金ページで確認すること。
動画生成の所要時間は?
通常は30秒〜数分。Sora 2の負荷状況による。本番運用では最大10分のタイムアウトを設定し、それを超えたら失敗扱いにする設計が推奨される。
image-to-videoで元画像はどこまで反映される?
構図・主要な被写体・色調は概ね反映される。ただし細部は動画化の過程で変化することが多い。元画像を厳密に保持したい場合は、Sora 2以外の選択肢も検討すべき。
seconds=12より長い動画は?
Sora 2のAPIでは4/8/12秒のみサポート。より長い動画が必要な場合は、複数の動画を生成して編集でつなぐか、別のプロダクト(Runway Gen-3等)を検討する。
2026年9月以降はどうする?
OpenAIの後継モデル発表を待つか、Google Veo・Runway・Klingなどの代替APIへ移行する。本記事で解説した設計パターン(非同期ジョブ・画像前処理・Azure Storage統合)は、どの動画生成APIにも応用可能である。
