minimax-hailuo-video について
MiniMax Hailuo API による有料動画生成リクエストを準備・管理するスキルです。リージョン別の接続先、モデルと生成方式の制約、事前の費用承認、結果が不明なタスクの扱いを重視します。
主な機能
- 01サービスリージョン別の接続先選択
- 02対応する生成方式とモデルの選定
- 03監査可能なリクエスト記録の作成
- 04生成前の費用承認
- 05タスク確認と不確実な結果への対応
活用シーン
- 01費用承認付きのテキスト動画生成依頼
- 02画像動画生成で静止画を動かす
- 03タイムアウトしたタスクの再試行前の照合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ calesthio/ generative-media-skills --skill minimax-hailuo-videoインストール後、Claude Code で/minimax-hailuo-videoを入力して読み込まれたか確認します。
同梱ファイルEVAL.md
詳細
MiniMax Hailuo 動画
確認済みのリクエストを作成し、正確な費用の承認を得てから、有料タスクを最大1件だけ作成し、結果とその証拠を保存します。ドキュメントに記載がないことは、既定値ではなく不明として扱ってください。
まず利用範囲を確認する
サービスリージョンを1つ選び、そのアカウント、キー、課金、ホスト、利用規約を一貫して使用してください。
| リージョン | コンソール/ドキュメント | API ホスト | 課金通貨 |
|---|---|---|---|
| グローバル | platform.minimax.io | https://api.minimax.io | USD |
| 中国本土 | platform.minimaxi.com | https://api.minimaxi.com | CNY |
2つのプラットフォームは別々のサービスリージョンです。一方で購入したキーやプランが、もう一方でも使えると想定してはいけません。401 はリージョン違いの症状である可能性があります。以下の例では従量課金 API キーを使用してください。Token Plan/Subscription Key には、別個のクォータとモデルアクセス権があります。
以下の製品は、このワークフローに含めないでください。
hailuoai.videoは一般向け Hailuo 製品であり、利用規約、プライバシーに関する挙動、機能、商用利用への影響が別途定められています。- Video Agent/テンプレート生成は、別のテンプレート API です。非推奨のクエリーエンドポイントと9時間のリンクに関する記述は、一般的な動画生成の仕様を定めるものではありません。
- 画像、音声、音楽、テキストの各エンドポイントは、それぞれ別のモダリティです。一般的な Hailuo 動画生成では、音声入力、音声プロンプト、生成音声の制御はドキュメントに記載されていません。
- ゲートウェイや再販業者は、それぞれ独自のスキーマ、保持期間、料金、サポートを設けています。それらを「MiniMax API」と呼ばないでください。
モデルを選ぶ前にモードを決める
現在、ファーストパーティの一般向け動画生成では、4つのモードに POST /v1/video_generation を使用します。API 概要では Hailuo 2.3 ファミリーと Hailuo-02 が取り上げられており、詳細なエンドポイントのページには旧モデルも記載されています。
| モード | 必須のメディアフィールド | ドキュメントに記載されたモデル | ドキュメントに記載された出力の選択肢 |
|---|---|---|---|
| テキストから動画(T2V) | なし。prompt は必須 | MiniMax-Hailuo-2.3、MiniMax-Hailuo-02。T2V エンドポイントには T2V-01-Director、T2V-01 も記載 | 2.3/02:768P は6秒または10秒、1080P は6秒。24 fps |
| 画像から動画(I2V) | first_frame_image | MiniMax-Hailuo-2.3、MiniMax-Hailuo-2.3-Fast、MiniMax-Hailuo-02。エンドポイントには I2V-01-Director、I2V-01-live、I2V-01 も記載 | 2.3/Fast/02:768P は6秒または10秒、1080P は6秒。02 にはさらに512P の6秒または10秒もあります。24 fps |
| 最初/最後のフレーム | first_frame_image、last_frame_image | MiniMax-Hailuo-02 | 768P は6秒または10秒、1080P は6秒。24 fps。エンドポイントには512P 非対応と明記されています |
| 被写体参照 | subject_reference 配列 | S2V-01 | 不明: 専用エンドポイントには長さ、解像度、現在の価格が記載されていません。一方、一般ガイドの例では6秒と1080Pが指定されています |
MiniMax-Hailuo-2.3-Fast は I2V 専用です。T2V には選択しないでください。詳細ページでは旧 *-01* モデルを720P/6秒と説明していますが、現在の従量課金価格には記載されていません。有料利用の前に、正しいコンソールで利用可否と価格を確認してください。他のモデルの価格を流用せず、S2V-01 についても同様に確認してください。
2026-07-10に確認した現在の従量課金価格:
| 組み合わせ | グローバル | 中国本土 |
|---|---|---|
| 2.3-Fast、768P、6秒 | $0.19 | ¥1.35 |
| 2.3-Fast、768P、10秒 | $0.32 | ¥2.25 |
| 2.3-Fast、1080P、6秒 | $0.33 | ¥2.31 |
| 2.3 または 02、768P、6秒 | $0.28 | ¥2.00 |
| 2.3 または 02、768P、10秒 | $0.56 | ¥4.00 |
| 2.3 または 02、1080P、6秒 | $0.49 | ¥3.50 |
| 02 I2V、512P、6秒 | $0.10 | ¥0.60 |
| 02 I2V、512P、10秒 | $0.15 | ¥1.00 |
これらは生成動画1本あたりの価格であり、恒久的な定額ではありません。承認の直前に、該当リージョンの公式料金ページを再確認してください。パッケージではユニットが消費され、RPM が上がります。Token Plan ではプランのクォータが使われます。パッケージの平均単価や別リージョンの通貨を、従量課金の見積もりとして使用しないでください。
リクエストの記録を作成する
create を呼び出す前に、次の内容を記録します。
- リージョン、コンソール/アカウント識別子、キーの種類、モデル、モード、解像度、尺、バリアント数、通貨、単価、料金ページの取得時刻;
- 正確なプロンプト、
prompt_optimizer、fast_pretreatment、およびデフォルト以外のすべてのパラメーター; - ソースファイルの SHA-256、MIME タイプ、バイト長、寸法、アスペクト比、出所、ライセンス/同意、顔/本人識別情報の有無;
- 想定する対象者、管轄区域、AI 表示/ウォーターマークの計画、保持クラス、レビュー担当者;
- POST の前にローカルで生成した試行 ID。
最初/最後のフレームと I2V の入力に関する公式の制約は、JPG/JPEG、PNG、または WebP、20 MB 未満、短辺が 300 px 超、アスペクト比が 2:5 から 5:2、公開 URL または Base64 データ URL です。最初の画像によって出力のアスペクト比/解像度が決まります。最初/最後モードでは、サイズが異なる最後の画像は最初の画像に合わせて切り抜かれます。不自然な遷移を避けるため、構図、被写体の大きさ、照明、トポロジーをあらかじめ揃えてください。
S2V のリファレンスには、異なる形式が正確に記載されています:"subject_reference": [{"type": "character", "image": ["https://authorized.example/reference.jpg"]}]。type は character、image は許可された顔参照 URL を含む配列です。I2V のフィールドを流用したり、S2V のページに記載がないのに I2V のメディア転送ルールが適用されると想定したりしないでください。
公開ソース URL を使うと、MiniMax がそのアセットを取得できます。また、その URL が URL ログやアクセス制御を通じて露出する可能性があります。ポリシーで許可される場合に限り、適切な範囲に制限した URL またはサイズを制限した Base64 を使用してください。SSRF 対策なしに、ユーザーから任意の URL を受け取って自前のダウンローダーで取得してはいけません。元のアセットをハッシュ化し、Base64 の本文や署名付きダウンロード URL はログに記録しないでください。
このモーションシステム向けのプロンプトの作成
プロンプトの上限は2,000文字です。Hailuo は、ドキュメントに記載された 2.3、02、Director の各ワークフローで、明示的なカメラコマンドをサポートしています。
[Truck left]、[Truck right]、[Pan left]、[Pan right]、[Push in]、[Pull out]、[Pedestal up]、[Pedestal down]、[Tilt up]、[Tilt down]、[Zoom in]、[Zoom out]、[Shake]、[Tracking shot]、[Static shot]。
同じ角括弧内のコマンドは同時に実行されます。公式ガイダンスでは、組み合わせるコマンドは最大3つまでが推奨されています。コマンドを順番に配置すると、一連の動きを表します。これらは方向を指定するコントロールであり、フレーム単位で正確なタイムコードではありません。
次の簡潔な順序でプロンプトを作成します。
- 被写体と最初の構図を明確にします。
- 物理的に一貫した動作の流れを1つ説明します。
- 主となるカメラコマンド、または単純な一連の動きを指定します。
- 目に見える最終状態と、維持すべき安定したディテールを指定します。
I2V では、静止画の見た目と矛盾する内容ではなく、提供されたフレームからの変化を説明します。最初と最後のフレームを使って生成する場合は、既知の2つの状態をつなぐ、もっともらしい動きを説明します。意図したショットにするには、prompt_optimizer: false を設定します。設定しない場合、最適化機能によって文言やカメラの意図が書き換えられることがあります。prompt_optimizer のデフォルトは true です。fast_pretreatment のデフォルトは false で、最適化機能が必要です。また、現行の Hailuo I2V モデルで利用できると記載されています。
以下は作成上のヒューリスティックであり、API が保証するものではありません。6〜10秒のクリップでは、動作を詰め込むよりも、主要な動作を1つ、主となるカメラの動きを1つにすると、通常は適しています。最終状態を明示すると、レビューしやすくなります。生成された物理表現、顔、テキスト、手、遮蔽、人物の同一性については、引き続きフレームごとに確認してください。モーション、表情、リアリズム、指示への準拠が向上するという MiniMax の主張は、プロバイダーによる主張であり、独立したベンチマークの結果ではありません。
一般エンドポイントのドキュメントには、シード、ネガティブプロンプト、任意のアスペクト比パラメーター、音声入力、音声プロンプト、音声出力の切り替えは記載されていません。これらを独自に追加しないでください。実際のファイルを確認するまでは、出力音声トラックの有無と性質を不明として扱ってください。必要な場合は、別途ライセンスされた音声を後工程で追加します。
各モードのリファレンスでは、model が必須です。T2V では prompt が必須ですが、I2V と最初/最後のフレームを使うモードでは、テキストプロンプトは任意と記載されています。プロンプトの上限は2,000文字です。first_frame_image、last_frame_image、subject_reference は、それぞれ対応するモードでのみ必須です。callback_url は任意です。duration と resolution は、汎用 CLI のデフォルトではなく、上記のモデル/モードのマトリックスに従う必要があります。現行の中国本土向け T2V ページには、デフォルトが false のブール値 aigc_watermark も記載されていますが、現行のグローバル T2V ページには記載されていません。
有料となる境界を明確にする
デフォルトではドライランにします。マスキング済みのリクエスト、動画1本あたりの正確な費用、バリアント数による倍率、通貨、価格の根拠となる最新のタイムスタンプ/証拠ダイジェスト、ソースハッシュ、ローカルジョブキー、出力先、ダウンロードを許可するホストの厳密なポリシーを表示します。このエンベロープ全体を正規化してハッシュ化します。人間によるダイジェストと、同じ通貨建ての有限かつ正の上限額の承認を必須にします。金額のみの承認では、モデル、プロンプト、ソース、リージョン、管理ポリシーが確定しないため不十分です。この組み合わせに対する現在の公開価格がない場合は、UNKNOWN で停止し、オペレーターを適切なコンソールまたは MiniMax サポートに案内します。
POST の前に、排他的作成セマンティクスでジョブ台帳を取得し、intent-recorded エントリーを永続化します。応答を受け取った後は、許可リストに含まれる結果の概要のみを永続化します。具体的には、タスク ID、プロバイダーコード、ステータスメッセージ/本文のダイジェスト、タイムスタンプです。生のプロンプト、Base64、エラー本文、署名付き URL は保存しません。MiniMax のドキュメントには、動画作成用の冪等性キーが記載されていません。したがって、次のようにします。
- 認証済みの GET/ポーリング/ダウンロード操作は、上限付きのバックオフで再試行します。
- 作成 POST は自動的に再試行しません。
- 作成がタイムアウトするか、
task_idを取得する前に接続が切断された場合は、create-outcome-unknownとして記録します。代替タスクを承認する前に、請求/コンソール/サポートで状況を照合します。 - 5xx、プロバイダーのタイムアウト/内部コード、不正なエンベロープ、または形式上は成功でも使用できないタスク ID を返した場合は、いずれも同様に曖昧な状態として扱い、停止する前にその状態を永続化します。
- 以前の作成が存在しないと確実に判明していない限り、再実行は新たな有料となる可能性のある動画作成として扱います。
HTTP の成功だけでは、プロバイダー側の成功を意味しません。base_resp.status_code == 0 かつ空でない task_id を必須にします。診断用に base_resp.status_code と status_msg を保持します。よくあるコードには、1001(タイムアウト)、1002(レート制限)、1004(認証)、1008(残高不足)、1026(入力がセンシティブ)、1027(出力がセンシティブ)、2013(無効なパラメーター)、2049(無効なキー)、2056(利用上限)があります。コンテンツ安全性に関する応答を回避してはいけません。
現在公開されているデフォルトのグローバル動画制限は、Hailuo 2.3 シリーズと 02 シリーズ全体で毎分5リクエストです。パッケージによっては、より高い制限が公開されています。これはアカウント/プラン固有のものとして扱い、コンソールで再確認します。ポーリングの間隔は別途設定し、ガイドが推奨する10秒間隔を使用します。
インシデント発生時は https://status.minimax.io/ を確認します。ただし、動画のステータスが個別に表示されていない場合、別のコンポーネントがステータス一覧にあるというだけで、動画生成が正常だと推測してはいけません。
ドライランを優先する完全なクライアント
この Python 3.11 以降のクライアントは、両方のリージョンで現在価格が設定されている T2V、I2V、および最初/最後のフレームの組み合わせに対応しています。S2V-01 と価格未設定の旧モデルは実行しません。create を再試行せず、create の前に排他的な試行台帳を取得し、既知のタスク ID から再開し、MiniMax のエンベロープを確認し、10 秒ごとにポーリングします。また、ベアラートークンを使わず、厳密なホスト/パブリック DNS ポリシーに従ってダウンロードし、クラッシュ後も再開可能な形で、ffprobe と ffmpeg による完全なデコードを使って MP4 を検証します。スキルディレクトリの外に hailuo_generate.py として保存してください。Python 3.11 以降、ffprobe、ffmpeg が必要です。
#!/usr/bin/env python3
import argparse, base64, hashlib, http.client, ipaddress, json, os, pathlib, random, socket, ssl, subprocess, sys, tempfile, threading, time, urllib.error, urllib.parse, urllib.request
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
HOSTS = {"global": "https://api.minimax.io", "cn": "https://api.minimaxi.com"}
PRICES = {
"global": {"currency": "USD", "table": {
("MiniMax-Hailuo-2.3-Fast", "768P", 6): "0.19", ("MiniMax-Hailuo-2.3-Fast", "768P", 10): "0.32",
("MiniMax-Hailuo-2.3-Fast", "1080P", 6): "0.33", ("MiniMax-Hailuo-2.3", "768P", 6): "0.28",
("MiniMax-Hailuo-2.3", "768P", 10): "0.56", ("MiniMax-Hailuo-2.3", "1080P", 6): "0.49",
("MiniMax-Hailuo-02", "512P", 6): "0.10", ("MiniMax-Hailuo-02", "512P", 10): "0.15",
("MiniMax-Hailuo-02", "768P", 6): "0.28", ("MiniMax-Hailuo-02", "768P", 10): "0.56",
("MiniMax-Hailuo-02", "1080P", 6): "0.49"}},
"cn": {"currency": "CNY", "table": {
("MiniMax-Hailuo-2.3-Fast", "768P", 6): "1.35", ("MiniMax-Hailuo-2.3-Fast", "768P", 10): "2.25",
("MiniMax-Hailuo-2.3-Fast", "1080P", 6): "2.31", ("MiniMax-Hailuo-2.3", "768P", 6): "2.00",
("MiniMax-Hailuo-2.3", "768P", 10): "4.00", ("MiniMax-Hailuo-2.3", "1080P", 6): "3.50",
("MiniMax-Hailuo-02", "512P", 6): "0.60", ("MiniMax-Hailuo-02", "512P", 10): "1.00",
("MiniMax-Hailuo-02", "768P", 6): "2.00", ("MiniMax-Hailuo-02", "768P", 10): "4.00",
("MiniMax-Hailuo-02", "1080P", 6): "3.50"}}
}
MODELS = {
"text": {"MiniMax-Hailuo-2.3", "MiniMax-Hailuo-02"},
"image": {"MiniMax-Hailuo-2.3", "MiniMax-Hailuo-2.3-Fast", "MiniMax-Hailuo-02"},
"first-last": {"MiniMax-Hailuo-02"},
}
MIME = {".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".webp": "image/webp"}
MAX_JSON = 256 * 1024
MAX_VIDEO = 250 * 1024 * 1024
MAX_TOOL_OUTPUT = 512 * 1024
NONTERMINAL = {"Preparing", "Queueing", "Processing"}
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl): return None
NO_REDIRECT = urllib.request.build_opener(NoRedirect)
class APIProblem(Exception):
def __init__(self, kind, http_status=None, body_sha256=None):
super().__init__(kind); self.kind = kind; self.http_status = http_status; self.body_sha256 = body_sha256
def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
def sha256_bytes(value): return hashlib.sha256(value).hexdigest()
def utc_now(): return datetime.now(timezone.utc).isoformat()
def fsync_directory(path):
if os.name != "nt":
fd = os.open(path, os.O_RDONLY)
try: os.fsync(fd)
finally: os.close(fd)
def image_dimensions(data, suffix):
if suffix == ".png" and data[:8] == b"\x89PNG\r\n\x1a\n" and data[12:16] == b"IHDR":
return int.from_bytes(data[16:20], "big"), int.from_bytes(data[20:24], "big")
if suffix in {".jpg", ".jpeg"} and data[:2] == b"\xff\xd8":
i = 2
while i + 9 < len(data):
if data[i] != 0xff: i += 1; continue
marker = data[i + 1]; i += 2
if marker in {0xd8, 0xd9} or 0xd0 <= marker <= 0xd7: continue
if i + 2 > len(data): break
length = int.from_bytes(data[i:i + 2], "big")
if marker in {0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf}:
return int.from_bytes(data[i + 5:i + 7], "big"), int.from_bytes(data[i + 3:i + 5], "big")
if length < 2: break
i += length
if suffix == ".webp" and data[:4] == b"RIFF" and data[8:12] == b"WEBP":
kind = data[12:16]
if kind == b"VP8X" and len(data) >= 30:
return 1 + int.from_bytes(data[24:27], "little"), 1 + int.from_bytes(data[27:30], "little")
if kind == b"VP8L" and len(data) >= 25 and data[20] == 0x2f:
b0, b1, b2, b3 = data[21:25]
return 1 + b0 + ((b1 & 0x3f) << 8), 1 + (b1 >> 6) + (b2 << 2) + ((b3 & 0x0f) << 10)
if kind == b"VP8 " and len(data) >= 30 and data[23:26] == b"\x9d\x01\x2a":
return int.from_bytes(data[26:28], "little") & 0x3fff, int.from_bytes(data[28:30], "little") & 0x3fff
raise SystemExit("Image signature or dimensions are invalid/unsupported")
def atomic_json(path, value):
path = pathlib.Path(path); path.parent.mkdir(parents=True, exist_ok=True)
fd, name = tempfile.mkstemp(prefix=".hailuo-", suffix=".json", dir=path.parent)
try:
os.fchmod(fd, 0o600)
with os.fdopen(fd, "wb") as out:
out.write(canonical(value) + b"\n"); out.flush(); os.fsync(out.fileno())
os.replace(name, path); fsync_directory(path.parent)
finally:
if os.path.exists(name): os.unlink(name)
def exclusive_json(path, value):
path = pathlib.Path(path); path.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
with os.fdopen(fd, "wb") as out:
out.write(canonical(value) + b"\n"); out.flush(); os.fsync(out.fileno())
fsync_directory(path.parent)
def read_json(path):
raw = pathlib.Path(path).read_bytes()
if len(raw) > MAX_JSON: raise SystemExit("Ledger exceeds JSON cap")
value = json.loads(raw)
if not isinstance(value, dict): raise SystemExit("Ledger is not an object")
return value
def bounded_process(argv, timeout, cap=MAX_TOOL_OUTPUT):
process = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
chunks, total, overflow, lock = [], [0], threading.Event(), threading.Lock()
def drain(stream):
while True:
data = stream.read(65536)
if not data: return
with lock:
total[0] += len(data)
if total[0] > cap:
overflow.set(); process.kill(); return
chunks.append(data)
threads = [threading.Thread(target=drain, args=(stream,), daemon=True) for stream in (process.stdout, process.stderr)]
for thread in threads: thread.start()
try: code = process.wait(timeout=timeout)
except subprocess.TimeoutExpired:
process.kill(); process.wait(); raise RuntimeError(f"tool timeout: {pathlib.Path(argv[0]).name}")
for thread in threads: thread.join(timeout=2)
output = b"".join(chunks)
if overflow.is_set(): raise RuntimeError(f"tool output cap exceeded: {pathlib.Path(argv[0]).name}")
if code: raise RuntimeError(f"tool failed: {pathlib.Path(argv[0]).name}; diagnostics_sha256={sha256_bytes(output)}")
return output
def media(path_text):
path = pathlib.Path(path_text)
if not path.is_file() or path.suffix.lower() not in MIME or path.stat().st_size >= 20 * 1024 * 1024:
raise SystemExit("Image must be JPG/JPEG, PNG, or WebP and strictly under 20 MB")
data = path.read_bytes()
width, height = image_dimensions(data, path.suffix.lower())
ratio = width / height
if min(width, height) <= 300 or not 0.4 <= ratio <= 2.5:
raise SystemExit("Image short edge must exceed 300 px and aspect ratio must be within 2:5–5:2")
bounded_process(["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), "-f", "null", "-"], 60)
return {
"wire": f"data:{MIME[path.suffix.lower()]};base64,{base64.b64encode(data).decode('ascii')}",
"evidence": {"path": str(path.resolve()), "sha256": hashlib.sha256(data).hexdigest(),
"bytes": len(data), "mime": MIME[path.suffix.lower()],
"width": width, "height": height, "aspect_ratio": ratio}
}
def api_json(method, url, key, body=None, retries=0):
encoded = None if body is None else canonical(body)
for attempt in range(retries + 1):
req = urllib.request.Request(url, data=encoded, method=method,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json", "Accept": "application/json"})
try:
with NO_REDIRECT.open(req, timeout=40) as response:
raw = response.read(MAX_JSON + 1)
if len(raw) > MAX_JSON: raise APIProblem("response-too-large")
try: result = json.loads(raw)
except json.JSONDecodeError: raise APIProblem("invalid-json", response.status, sha256_bytes(raw))
if not isinstance(result, dict): raise APIProblem("non-object-json", response.status, sha256_bytes(raw))
return result
except urllib.error.HTTPError as exc:
raw = exc.read(MAX_JSON + 1)
problem = APIProblem("http-error", exc.code, sha256_bytes(raw))
if attempt == retries or exc.code not in {408, 429, 500, 502, 503, 504}: raise problem
except APIProblem: raise
except (urllib.error.URLError, TimeoutError, OSError) as exc:
if attempt == retries: raise APIProblem("transport-" + type(exc).__name__)
time.sleep(min(8, 2 ** attempt) * (1 + random.random() * 0.25))
raise AssertionError("unreachable")
def provider_result(result):
base = result.get("base_resp")
if not isinstance(base, dict) or not isinstance(base.get("status_code"), int):
raise APIProblem("missing-provider-envelope", body_sha256=sha256_bytes(canonical(result)))
return base["status_code"], sha256_bytes(str(base.get("status_msg", "")).encode("utf-8"))
def exact_hosts(raw):
hosts = []
for item in raw.split(","):
host = item.strip().rstrip(".").lower()
if not host: continue
if "*" in host or "/" in host or ":" in host: raise SystemExit("Output hosts must be exact DNS names")
try: ipaddress.ip_address(host)
except ValueError: pass
else: raise SystemExit("Output host policy rejects IP literals")
hosts.append(host)
if not hosts: raise SystemExit("Provide at least one reviewed exact --output-host")
return tuple(sorted(set(hosts)))
def validate_output_url(url, allowed_hosts):
parsed = urllib.parse.urlsplit(url)
if parsed.scheme != "https" or not parsed.hostname or parsed.username or parsed.password or parsed.fragment:
raise RuntimeError("Output URL must be HTTPS with hostname, no userinfo, and no fragment")
if parsed.port not in (None, 443): raise RuntimeError("Output URL must use default HTTPS port")
host = parsed.hostname.rstrip(".").lower()
try: ipaddress.ip_address(host)
except ValueError: pass
else: raise RuntimeError("Output URL rejects IP literals")
if host not in allowed_hosts: raise RuntimeError("Output host is outside the approved exact-host policy")
addresses = []
for item in socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM):
address = ipaddress.ip_address(item[4][0])
if not address.is_global: raise RuntimeError("Output host resolves to a non-public address")
addresses.append(str(address))
if not addresses: raise RuntimeError("Output host did not resolve")
target = parsed.path or "/"
if parsed.query: target += "?" + parsed.query
return host, target, sorted(set(addresses))
class PinnedHTTPSConnection(http.client.HTTPSConnection):
def __init__(self, host, address, timeout):
super().__init__(host, 443, timeout=timeout, context=ssl.create_default_context()); self.address = address
def connect(self):
raw = socket.create_connection((self.address, 443), self.timeout)
try: self.sock = self._context.wrap_socket(raw, server_hostname=self.host)
except Exception: raw.close(); raise
def verify_mp4(path):
path = pathlib.Path(path); size = path.stat().st_size
if size <= 0 or size > MAX_VIDEO: raise RuntimeError("Artifact size is outside policy")
digest = hashlib.sha256()
with path.open("rb") as stream:
header = stream.read(32)
if len(header) < 12 or header[4:8] != b"ftyp": raise RuntimeError("Artifact lacks ISO-BMFF ftyp signature")
stream.seek(0)
for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk)
raw = bounded_process(["ffprobe", "-v", "error", "-show_streams", "-show_format", "-of", "json", str(path)], 30, MAX_JSON)
metadata = json.loads(raw); streams = metadata.get("streams", [])
video = next((item for item in streams if item.get("codec_type") == "video"), None)
if not video: raise RuntimeError("Artifact has no video stream")
bounded_process(["ffmpeg", "-nostdin", "-v", "error", "-i", str(path), "-f", "null", "-"], 300)
return {"bytes": size, "sha256": digest.hexdigest(), "qa": {
"container": metadata.get("format", {}).get("format_name"), "video_codec": video.get("codec_name"),
"width": video.get("width"), "height": video.get("height"), "fps": video.get("avg_frame_rate"),
"duration": metadata.get("format", {}).get("duration"),
"audio_present": any(item.get("codec_type") == "audio" for item in streams), "full_decode": "passed"}}
def safe_download(url, destination, allowed_hosts, ledger_path, record):
destination = pathlib.Path(destination); destination.parent.mkdir(parents=True, exist_ok=True)
stage = destination.with_name("." + destination.name + ".hailuo-stage")
expected = record.get("artifact_staged") or record.get("artifact")
if destination.exists():
if not isinstance(expected, dict): raise RuntimeError("Existing artifact has no durable expected digest")
artifact = verify_mp4(destination)
if (artifact["sha256"], artifact["bytes"]) != (expected.get("sha256"), expected.get("bytes")):
raise RuntimeError("Existing artifact does not match durable evidence")
return artifact
if stage.exists() and isinstance(expected, dict):
artifact = verify_mp4(stage)
if (artifact["sha256"], artifact["bytes"]) != (expected.get("sha256"), expected.get("bytes")):
raise RuntimeError("Staged artifact does not match durable evidence")
os.replace(stage, destination); fsync_directory(destination.parent); return artifact
host, target, addresses = validate_output_url(url, allowed_hosts)
url_hash = sha256_bytes(url.encode("utf-8"))
if record.get("download_url_sha256") not in (None, url_hash): raise RuntimeError("Download URL changed during an incomplete transfer")
record.update(state="downloading", download_url_sha256=url_hash, download_host=host, download_started_at=record.get("download_started_at") or utc_now())
atomic_json(ledger_path, record)
if stage.exists(): stage.unlink()
connection = PinnedHTTPSConnection(host, addresses[0], timeout=90)
try:
connection.request("GET", target, headers={"Host": host, "Accept": "video/mp4", "User-Agent": "hailuo-artifact-client/2"})
response = connection.getresponse()
if response.status != 200: raise RuntimeError(f"Output returned HTTP {response.status}")
media_type = response.getheader("Content-Type", "").split(";", 1)[0].lower()
if media_type not in {"video/mp4", "application/octet-stream"}: raise RuntimeError("Unexpected output media type")
announced = response.getheader("Content-Length")
if announced is not None and (not announced.isdigit() or int(announced) > MAX_VIDEO): raise RuntimeError("Invalid or excessive Content-Length")
fd = os.open(stage, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600); total = 0
try:
with os.fdopen(fd, "wb") as out:
while True:
chunk = response.read(1024 * 1024)
if not chunk: break
total += len(chunk)
if total > MAX_VIDEO: raise RuntimeError("Download exceeded byte cap")
out.write(chunk)
out.flush(); os.fsync(out.fileno())
except BaseException:
stage.unlink(missing_ok=True); raise
finally: connection.close()
artifact = verify_mp4(stage)
record.update(state="artifact-staged", artifact_staged=artifact, artifact_validated_at=utc_now())
atomic_json(ledger_path, record)
os.replace(stage, destination); fsync_directory(destination.parent)
return artifact
def main():
p = argparse.ArgumentParser()
p.add_argument("--region", choices=HOSTS, required=True)
p.add_argument("--mode", choices=MODELS, required=True)
p.add_argument("--model", required=True); p.add_argument("--prompt", required=True)
p.add_argument("--resolution", choices=["512P", "768P", "1080P"], required=True)
p.add_argument("--duration", type=int, choices=[6, 10], required=True)
p.add_argument("--first-frame"); p.add_argument("--last-frame")
p.add_argument("--optimizer", action=argparse.BooleanOptionalAction, default=False)
p.add_argument("--fast-pretreatment", action="store_true")
p.add_argument("--execute", action="store_true")
p.add_argument("--job-key", required=True)
p.add_argument("--output-host", required=True, help="Comma-separated reviewed exact DNS hostnames")
p.add_argument("--pricing-checked-at", required=True, help="Timezone-aware ISO-8601 time of the live regional price check")
p.add_argument("--pricing-evidence-sha256", required=True)
p.add_argument("--rights-sha256", required=True); p.add_argument("--moderation-sha256", required=True)
p.add_argument("--qa-plan-sha256", required=True)
p.add_argument("--approval-sha256")
p.add_argument("--max-cost"); p.add_argument("--max-cost-currency", choices=["USD", "CNY"])
p.add_argument("--show-full-plan", action="store_true")
p.add_argument("--ledger", default="hailuo-attempt.json")
p.add_argument("--output", default="hailuo-output.mp4")
a = p.parse_args()
if not a.job_key or any(not (character.isalnum() or character in "-_") for character in a.job_key):
raise SystemExit("job-key accepts letters, digits, hyphen, and underscore")
if a.model not in MODELS[a.mode]: raise SystemExit("Model is not valid for the selected mode")
if len(a.prompt) > 2000: raise SystemExit("Prompt exceeds 2,000 characters")
if a.fast_pretreatment and not a.optimizer: raise SystemExit("fast_pretreatment requires optimizer")
if a.mode in {"image", "first-last"} and not a.first_frame: raise SystemExit("First frame required")
if a.mode == "first-last" and not a.last_frame: raise SystemExit("Last frame required")
key = (a.model, a.resolution, a.duration)
unit = PRICES[a.region]["table"].get(key)
if unit is None: raise SystemExit("UNKNOWN current price/combination; verify in the correct MiniMax console")
if a.model == "MiniMax-Hailuo-02" and a.mode == "text" and a.resolution == "512P":
raise SystemExit("512P pricing/availability is documented for Hailuo-02 I2V, not T2V")
if a.mode == "first-last" and a.resolution == "512P":
raise SystemExit("The first/last-frame endpoint explicitly does not support 512P")
request = {"model": a.model, "prompt": a.prompt, "resolution": a.resolution,
"duration": a.duration, "prompt_optimizer": a.optimizer}
evidence = {}
if a.mode in {"image", "first-last"}:
item = media(a.first_frame); request["first_frame_image"] = item["wire"]; evidence["first_frame"] = item["evidence"]
if a.mode == "first-last":
item = media(a.last_frame); request["last_frame_image"] = item["wire"]; evidence["last_frame"] = item["evidence"]
if a.fast_pretreatment: request["fast_pretreatment"] = True
currency = PRICES[a.region]["currency"]
output_hosts = exact_hosts(a.output_host)
for name, value in (("pricing evidence", a.pricing_evidence_sha256), ("rights", a.rights_sha256),
("moderation", a.moderation_sha256), ("QA plan", a.qa_plan_sha256)):
if len(value) != 64 or any(character not in "0123456789abcdefABCDEF" for character in value):
raise SystemExit(f"{name} must be a SHA-256 digest")
redacted = dict(request)
for field in ("first_frame_image", "last_frame_image"):
if field in redacted: redacted[field] = f"<base64 redacted; sha256={evidence[field.split('_image')[0]].get('sha256', 'see evidence')}>"
redacted["prompt"] = "sha256:" + sha256_bytes(a.prompt.encode("utf-8"))
request_hash = sha256_bytes(canonical(request))
output_policy = {"exact_hosts": list(output_hosts), "https": True, "redirects": "reject",
"reject_ip_literals": True, "public_dns": True,
"dns_pinning": "connect-IP-with-TLS-SNI-and-host-certificate",
"max_bytes": MAX_VIDEO, "container": "mp4", "full_decode": True}
envelope = {"schema_version": 1, "job_key": a.job_key, "region": a.region, "api_origin": HOSTS[a.region],
"endpoint": "/v1/video_generation", "mode": a.mode, "model": a.model,
"request_sha256": request_hash, "redacted_request": redacted, "source_evidence": evidence,
"output": str(pathlib.Path(a.output).resolve()), "output_policy": output_policy,
"price": {"amount": unit, "currency": currency, "checked_at": a.pricing_checked_at,
"evidence_sha256": a.pricing_evidence_sha256.lower()},
"governance": {"rights_sha256": a.rights_sha256.lower(),
"moderation_sha256": a.moderation_sha256.lower(),
"qa_plan_sha256": a.qa_plan_sha256.lower()}}
approval = sha256_bytes(canonical(envelope))
print(json.dumps({"approval_sha256": approval, "plan": envelope}, indent=2, ensure_ascii=False))
if a.show_full_plan: print(json.dumps({"protected_payload": request}, indent=2, ensure_ascii=False))
print(f"DRY RUN: one task, exact maximum {currency} {unit}; no network request sent", file=sys.stderr)
if not a.execute: return
if a.approval_sha256 != approval: raise SystemExit("Exact approval digest mismatch")
try: maximum = Decimal(a.max_cost or "")
except InvalidOperation: raise SystemExit("max-cost must be a decimal")
if not maximum.is_finite() or maximum <= 0 or maximum < Decimal(unit):
raise SystemExit("max-cost must be finite, positive, and at least the quoted amount")
if a.max_cost_currency != currency: raise SystemExit("max-cost currency does not match the selected region")
try: checked_at = datetime.fromisoformat(a.pricing_checked_at.replace("Z", "+00:00"))
except ValueError: raise SystemExit("pricing-checked-at must be ISO-8601")
if checked_at.tzinfo is None: raise SystemExit("pricing-checked-at must include a timezone")
age = datetime.now(timezone.utc) - checked_at.astimezone(timezone.utc)
if age.total_seconds() < -300 or age.total_seconds() > 86400:
raise SystemExit("Live regional pricing evidence must be no more than 24 hours old")
api_key = os.environ.get("MINIMAX_API_KEY")
if not api_key: raise SystemExit("Set MINIMAX_API_KEY only in the environment")
ledger_path = pathlib.Path(a.ledger)
if ledger_path.exists():
intent = read_json(ledger_path)
if intent.get("request_sha256") != request_hash or intent.get("approval_sha256") != approval:
raise SystemExit("Existing job ledger belongs to a different approved request")
task_id = intent.get("task_id")
if not task_id: raise SystemExit(f"Ledger state {intent.get('state')!r} has no task ID; never replay this job key")
else:
intent = {"schema_version": 1, "state": "intent-recorded", "created_at": utc_now(),
"job_key": a.job_key, "request_sha256": request_hash, "approval_sha256": approval,
"region": a.region, "host": HOSTS[a.region], "mode": a.mode, "model": a.model,
"redacted_request": redacted, "source_evidence": evidence, "output_policy": output_policy,
"price": envelope["price"], "governance": envelope["governance"]}
try: exclusive_json(ledger_path, intent)
except FileExistsError: raise SystemExit("Another process acquired this job ledger")
intent.update(state="create-started", create_started_at=utc_now()); atomic_json(ledger_path, intent)
try: created = api_json("POST", HOSTS[a.region] + "/v1/video_generation", api_key, request, retries=0)
except APIProblem as exc:
intent.update(state="create-outcome-unknown", create_outcome_at=utc_now(), error_kind=exc.kind,
http_status=exc.http_status, error_body_sha256=exc.body_sha256, replay_allowed=False)
atomic_json(ledger_path, intent)
raise SystemExit("Create outcome unknown; do not replay until billing/console/support is reconciled")
try: provider_code, provider_message_sha256 = provider_result(created)
except APIProblem as exc:
intent.update(state="create-outcome-unknown", create_outcome_at=utc_now(), error_kind=exc.kind,
response_sha256=exc.body_sha256, replay_allowed=False); atomic_json(ledger_path, intent)
raise SystemExit("Create success envelope is unusable; do not replay")
if provider_code != 0:
uncertain = provider_code in {1001, 1024, 1033}
intent.update(state="create-outcome-unknown" if uncertain else "create-rejected",
create_outcome_at=utc_now(), provider_status_code=provider_code,
provider_status_message_sha256=provider_message_sha256, replay_allowed=False)
atomic_json(ledger_path, intent)
raise SystemExit("MiniMax rejected or could not confirm create; this job key cannot be replayed")
task_id = created.get("task_id")
if not isinstance(task_id, (str, int)) or not str(task_id).isdigit():
intent.update(state="create-outcome-unknown", create_outcome_at=utc_now(),
error_kind="unusable-success-task-id", response_sha256=sha256_bytes(canonical(created)),
replay_allowed=False); atomic_json(ledger_path, intent)
raise SystemExit("Create returned an unusable task ID; do not replay")
task_id = str(task_id)
intent.update(state="task-created", task_id=task_id, submitted_at=utc_now(),
provider_status_code=0, provider_status_message_sha256=provider_message_sha256)
atomic_json(ledger_path, intent)
deadline = time.monotonic() + 30 * 60
while time.monotonic() < deadline:
query = HOSTS[a.region] + "/v1/query/video_generation?" + urllib.parse.urlencode({"task_id": task_id})
try: status = api_json("GET", query, api_key, retries=3)
except APIProblem as exc:
intent.update(poll_error_kind=exc.kind, poll_http_status=exc.http_status,
poll_error_body_sha256=exc.body_sha256); atomic_json(ledger_path, intent); raise SystemExit("Polling failed; resume this task ID later")
provider_code, provider_message_sha256 = provider_result(status)
if provider_code != 0:
intent.update(poll_provider_status_code=provider_code,
poll_provider_status_message_sha256=provider_message_sha256); atomic_json(ledger_path, intent)
raise SystemExit("MiniMax query returned a provider error; resume after diagnosis")
if status.get("task_id") not in (None, task_id, int(task_id)):
raise SystemExit("Query task ID mismatch")
name = status.get("status")
intent.update(state="polling", last_status=name, last_polled_at=utc_now()); atomic_json(ledger_path, intent)
if name == "Success":
file_id = status.get("file_id")
if not isinstance(file_id, (str, int)) or not str(file_id).isdigit(): raise RuntimeError("Success returned invalid file_id")
file_id = str(file_id)
retrieve = HOSTS[a.region] + "/v1/files/retrieve?" + urllib.parse.urlencode({"file_id": file_id})
retrieved = api_json("GET", retrieve, api_key, retries=3)
retrieve_code, retrieve_message_sha256 = provider_result(retrieved)
if retrieve_code != 0: raise RuntimeError("MiniMax retrieve returned a provider error")
file_record = retrieved.get("file") or {}
if str(file_record.get("file_id")) not in {"None", file_id}: raise RuntimeError("Retrieve file ID mismatch")
url = file_record.get("download_url") or retrieved.get("download_url")
if not url: raise RuntimeError("Retrieve returned no download_url")
artifact = safe_download(url, a.output, set(output_hosts), ledger_path, intent)
expected_dimensions = (status.get("video_width"), status.get("video_height"))
actual_dimensions = (artifact["qa"].get("width"), artifact["qa"].get("height"))
try: actual_duration = Decimal(str(artifact["qa"].get("duration")))
except InvalidOperation: actual_duration = Decimal("NaN")
qa_state = "automated-passed"
if expected_dimensions != actual_dimensions or not actual_duration.is_finite() or abs(actual_duration - Decimal(a.duration)) > Decimal("1"):
qa_state = "quarantined-metadata-mismatch"
intent.update(state="artifact-downloaded" if qa_state == "automated-passed" else "artifact-quarantined",
file_id=file_id, retrieve_provider_message_sha256=retrieve_message_sha256,
artifact=dict(artifact, path=str(pathlib.Path(a.output).resolve())),
expected_dimensions={"width": expected_dimensions[0], "height": expected_dimensions[1]},
qa_state=qa_state, human_review="pending", stored_at=utc_now())
atomic_json(ledger_path, intent); print(json.dumps({"artifact": intent["artifact"], "qa_state": qa_state}, indent=2)); return
if name == "Fail":
intent.update(state="generation-failed", terminal_at=utc_now(),
failure_sha256=sha256_bytes(str(status.get("error_message", "")).encode("utf-8")))
atomic_json(ledger_path, intent); raise SystemExit("MiniMax generation failed")
if name not in NONTERMINAL: raise RuntimeError("Unknown task status; fail closed")
time.sleep(10)
intent.update(state="poll-deadline-reached", deadline_at=utc_now()); atomic_json(ledger_path, intent)
raise SystemExit("Polling deadline reached; task may still be running—resume query, do not recreate")
if __name__ == "__main__": main()
キーもネットワーク呼び出しも使わずに、グローバル向けの 6 秒 768P T2V リクエストをドライランします。まず、グローバルの従量課金ページを再度開き、現在のタイムゾーン対応の確認時刻を記録し、その証拠の保護済みキャプチャをハッシュ化して、保護済みの権利/モデレーション/QA 証拠のダイジェストを提示してください。出力ホストには、機微情報を含まないステージングワークフローまたはプロバイダーの確認で判明した、確認済み CDN ホスト名を厳密に指定してください。ワイルドカードは使用しないでください。
python hailuo_generate.py --region global --mode text --model MiniMax-Hailuo-2.3 --resolution 768P --duration 6 --job-key kite-v1 --output-host '<reviewed-exact-download-host>' --pricing-checked-at '2026-07-10T12:00:00Z' --pricing-evidence-sha256 '<64-hex-digest>' --rights-sha256 '<64-hex-digest>' --moderation-sha256 '<64-hex-digest>' --qa-plan-sha256 '<64-hex-digest>' --prompt "A red paper kite rises above a quiet salt flat. [Tracking shot] The camera follows low and steady; the kite settles centered against the horizon."
保護されたペイロードと変更されていない証拠一式を確認した後、同じ引数を指定して、表示されたダイジェスト、通貨が一致する上限額、および明示的な実行ゲートを追加してください。
export MINIMAX_API_KEY='replace-in-your-shell-only'
python hailuo_generate.py <the-exact-unchanged-dry-run-arguments> --execute --approval-sha256 '<printed-digest>' --max-cost 0.28 --max-cost-currency USD
PowerShell では、キーを $env:MINIMAX_API_KEY = '...' で設定してください。実際のキーをスクリプト、ノートブック、ログ、チャット、ブラウザーコード、またはバージョン管理に貼り付けないでください。このクライアントでは、中国向けの aigc_watermark フィールドを意図的に追加していません。現在の中国向け T2V ドキュメントには記載されていますが、現在のグローバル向けドキュメントには記載されていません。中国向けルートで使用する場合は、該当するモードの最新の中国向けリファレンスを確認してから追加してください。ただし、これは管轄区域固有の公開表示に代わるものではありません。
非同期ライフサイクルを完了する
ポーリング状態は大文字と小文字を区別します。Preparing、Queueing、Processing、Success、Fail です。成功したら file_id を取得し、GET /v1/files/retrieve?file_id=... を呼び出して、すぐにダウンロードしてください。現在の一般的なファイルドキュメントには、ダウンロード URL の有効期間や生成ファイルの保持期間は記載されていません。過去のページと別のテンプレート API には 9 時間と記載されていますが、これは現在の一般的な動画に対する保証ではありません。
認証済みの retrieve 応答に含まれる署名付き URL は、信頼できないものとして扱ってください。ベアラートークンを使わずにダウンロードし、HTTPS、ユーザー情報/フラグメントなし、デフォルトポート、承認済みの厳密なホスト名、すべての DNS レコードがパブリックであること、IP リテラルの拒否、承認済みホスト名を対象とした TLS SNI/証明書検証付きの DNS 固定接続、リダイレクトなし、厳格な型/バイト数/時間の上限を必須にしてください。永続的にステージングし、ハッシュを計算して、出力を制限した ffmpeg を使い、ffprobe による検証と完全なデコードを行ってから、アトミックに昇格させてください。再起動時は、永続化された台帳の証拠とサイズおよびダイジェストが一致する場合に限り、既存の最終ファイルまたはステージングファイルを採用してください。
クリップ全体を確認し、安全性、アイデンティティの変化、物理的な整合性、受け入れ基準を検証してください。コンテナ/コーデック、長さ、寸法、フレームレート、オーディオストリーム、バイト長、SHA-256、プロンプトのハッシュまたは保護されたプロンプト、リクエスト/承認ダイジェスト、タスク/ファイル ID、モデル、ソースハッシュと権利証拠、価格の証拠、自動レビューと人手によるレビュー、後続のリネージュを記録してください。署名付き URL とキーは通常のログとは別に保管してください。リファレンスクライアントが記録するのは URL のハッシュと厳密なホスト名だけです。
コールバックは任意です。MiniMax は challenge を POST してコールバックを検証します。エンドポイントは 3 秒以内にこれをそのまま返す必要があります。更新ペイロードでは、ポーリング応答と異なり、processing、success、failed が小文字で使われます。コールバック署名や共有シークレットの仕組みは文書化されていません。TLS、推測困難なパス、本文/レートの上限、想定されるタスク ID の許可リストを使用してください。コールバックは再確認のきっかけとしてのみ扱い、終端処理やダウンロードの前に、認証済みのステータスエンドポイントを再照会してください。
権利、肖像、開示、データ管理を適切に行う
送信する権限のあるプロンプトとメディアのみを使用してください。個人を特定できる人物については、使用目的、地域、対象者、期間、編集、合成変換に関するインフォームド・コンセントを記録し、該当する場合は、パブリシティ権、出演者、労働関係、未成年者/保護者の承認を得てください。支持を受けていると誤解させたり、詐欺目的でなりすましたり、同意なしに親密な画像を作成したり、安全フィルターを回避したりしないでください。S2V-01で使用する顔写真は、API上では単なる参照画像とされていても、生体情報に準ずる機微な情報です。
MiniMaxの現行API規約では、法令の遵守、ディープシンセシスコンテンツの一般公開時の表示、および該当する技術的識別情報/届出が求められています。ルートにaigc_watermarkフラグがある場合でも、目に見える開示を追加し、来歴情報を保持してください。公開前に、地域の選挙、広告、児童安全、プライバシー、生体情報、AI表示に関する規則を再確認してください。
「学習には使用しない」と約束したり、削除時期を断定したりしないでください。現行のグローバルAPI規約では、サービスの提供、維持、開発、改善、安全確保、規約の執行を目的として、入力/出力の処理が認められています。プライバシーポリシーには目的に基づく保持と米国のデータセンターについて記載されていますが、一般的な動画入力に対する固定の削除期間は定められていません。プロバイダーは保存済み/生成済み情報を削除する場合があります。個人データを最小限に抑え、独自の保持/削除スケジュールを設定し、台帳/成果物へのアクセスを制限し、地域、対象者、または法令に応じてデータ移転評価を実施してください。機微な制作作業に入る前に、該当する地域の最新の規約とプライバシー通知を改めて確認してください。
事実と矛盾を慎重に確認する
重要な記述は、次のいずれかに分類してください。
- 事実: 現行の一次情報であるリファレンス、料金、規約、またはステータスに関するドキュメントに、直接記載されています。
- プロバイダーの主張: MiniMaxが、独立した検証なしにモデルの品質や機能について説明しています。
- 経験則: 経験やシステム上の制約から導かれた、運用上または創作上の実践です。
- 不明: 現行の一次資料に記載がない、資料間で矛盾している、または該当する組み合わせの料金が示されていません。
2026-07-10時点で確認されているドキュメント上の矛盾/制限:
- S2Vのリファレンスには尺/解像度の記載がありませんが、ガイドの例では6 s/1080Pが指定されています。料金は記載されていません。
- 概要ページでは2.3/Fast/02が強調されていますが、エンドポイントのリファレンスには旧モデル名が残っており、現行の従量課金料金は確認できません。
- 中国向けT2Vのリファレンスには
aigc_watermarkが記載されていますが、グローバル版T2Vのリファレンスにはありません。 - 一般的なファイルのドキュメントにはURLの有効期間が記載されていません。過去のリリースや非推奨のテンプレートエンドポイントにおける9時間を引き継がないでください。
- ポーリングのステータスはタイトルケースですが、コールバックのステータスは小文字です。
矛盾が費用、権利、互換性、または保持期間に影響する場合は、作業を中断し、適切なコンソールまたはMiniMaxサポートで確認してください。都合のよい解釈を黙って選ぶのではなく、回答と取得時刻を記録してください。
ファーストパーティの情報源マップ
実装に関する情報には、該当するリージョンの最新のファーストパーティページのみを使用してください。
- グローバルドキュメントのインデックス:https://platform.minimax.io/docs/llms.txt
- グローバルの動画ガイドと API リファレンス:https://platform.minimax.io/docs/guides/video-generation、https://platform.minimax.io/docs/api-reference/video-generation-t2v、https://platform.minimax.io/docs/api-reference/video-generation-i2v、https://platform.minimax.io/docs/api-reference/video-generation-fl2v、https://platform.minimax.io/docs/api-reference/video-generation-s2v、https://platform.minimax.io/docs/api-reference/video-generation-query、https://platform.minimax.io/docs/api-reference/video-generation-download
- グローバルの料金、制限、エラー、リリースノート:https://platform.minimax.io/docs/guides/pricing-paygo、https://platform.minimax.io/docs/guides/pricing-video、https://platform.minimax.io/docs/guides/rate-limits、https://platform.minimax.io/docs/api-reference/errorcode、https://platform.minimax.io/docs/release-notes/apis、https://platform.minimax.io/docs/release-notes/models
- グローバルの API 利用規約とプライバシーポリシー:https://platform.minimax.io/protocol/terms-of-service、https://platform.minimax.io/protocol/privacy-policy
- 中国本土向けドキュメントのインデックスと従量課金制の料金:https://platform.minimaxi.com/docs/llms.txt、https://platform.minimaxi.com/docs/guides/pricing-paygo
- リージョンとキーの違い:https://platform.minimax.io/docs/token-plan/minimax-cli
- 公式サービスの稼働状況:https://status.minimax.io/
- 別途定められている消費者向けの利用規約とプライバシーポリシー:https://hailuoai.video/doc/terms-of-service.html、https://hailuoai.video/doc/privacy-policy.html
現在のドキュメントインデックスには、Hailuo 2.3 専用の技術モデルカードや安全性カードはありません。MiniMax がファーストパーティの資料を公開しない限り、アーキテクチャ、トレーニングコーパス、ベンチマークの手法、体系的な安全性評価、人口統計学的属性ごとの性能は不明として扱ってください。