画像を1枚作り、確認して承認してから、短い動画にします。この手順は、2026年10月に実施した青いマグカップのテストに基づいています。使用するのは$0.010のZ-Image静止画と、$0.185のMiniMax H3動画(768p、5秒)です。静止画のプロンプト拡張を無効にすると、各段階を1回ずつ試す費用は$0.195です。まずPlaygroundで同じシーンを試し、入力がうまく機能したらPythonの例を使うか、エージェントに呼び出しを任せてください。
目指す結果は具体的です。作業台の上に形の整ったマグカップがあり、湯気が右に流れ、カメラがゆっくり近づいて最後に止まる映像です。モデルのデプロイやGPUサーバーの設定は不要です。
Playgroundで一連の手順を試す
アカウントを作成して入金し、メールアドレスを確認してください。Playgroundがリクエストを処理するため、APIキーが必要なのは、後述するコードやエージェント用ツールを使う場合だけです。
1. 最初の画像を生成する
Z-Imageのテキストから画像を開き、Playgroundを使います。次のプロンプトを貼り付けてください。
Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench in a pottery studio, a thin curl of steam rising from the mug. Soft window light from the left, shallow depth of field, shelves of pots softly out of focus behind. The mug sits left of centre with empty bench to its right. Natural colours, no text, no logos.
16:9、PNG、シード42を選び、プロンプト拡張を無効にします。表示価格を確認してRun modelを押してください。基本の画像は$0.010で、拡張を有効にすると$0.016になります。プロンプトの変更を比較する間は、シード42を固定します。
先に結果を開いてから次に進みます。以下を確認してください。
- 被写体: マグカップが1つ、取っ手が1つで、陶器の形状が整っていること。
- 動きの余地: 湯気が見え、右に流れるための空間があること。
- 構図: マグカップが中央より左にあり、棚が控えめな背景になっていること。
- 不要な要素: 意図しない文字、ロゴ、追加のカップがないこと。
承認したPNGをimage.pngとしてダウンロードします。マグカップが不適切なら、動画に費用をかける前に1文を変えて画像を作り直してください。基本の画像を作り直すたびに$0.010が追加されます。
2. 承認した画像を動画にする
MiniMax H3の画像から動画を開き、Input imageにimage.pngを追加し、Promptに以下を貼り付けます。
Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.
Durationを5秒、Resolutionを768pに設定します。価格を確認してRun modelを押してください。この動画は$0.185で、2kを選ぶと$0.290になります。
このプロンプトは湯気の動き、カメラの動き、音を分けて指定しています。マグカップを改めて描写せず、その場に留まるよう指示しています。マグカップが作業台の上を移動する場合は、被写体の動作を増やすのではなく、カメラの指示を簡単にしてください。ゆっくりした前進はカメラを近づける動きであり、マグカップを動かす指示ではありません。
3. 動画を確認し、次のテイクを選ぶ
結果を最後まで音付きで再生します。マグカップが滑ったり変形したりしないこと、カメラが近づいてから止まること、不要なカットがないこと、音が静かな工房に合うことを確認してください。想定する成果物は音声トラック付きの約5秒のMP4です。ファイルが再生できるだけでは、使えるショットとはいえません。
画像は適切でも動きが不適切なら、承認済みの最初の画像を保ち、動きのプロンプトを修正します。最初の画像自体が不適切なら、先に画像を差し替えてください。H3にはシードがありません。2kでの生成は新しいテイクとなり、気に入った768p版と動きが異なる場合があります。
青いマグカップのテストで得られた結果
静止画の費用は$0.010で、1280 × 720のPNGとして届きました。5秒の動画リクエストは$0.125(480pでの費用です。480pは現在提供されておらず、同じ動画は768pで$0.185です)で、音声トラック付きの832 × 480のMP4が返されました。合計は$0.135でした(768pなら$0.195です)。
マグカップは作業台に留まり、湯気は上と右へ動き、カメラの前進は控えめでした。静止画ではマグカップが中央よりわずかに左に配置され、終盤には棚の近くで湯気がぼやけました。より寄った商品ショットにしたい場合は、動画化する前に、静止画のプロンプトでマグカップの配置を明確にしてください。
最初の試行にこの2つのモデルが適している理由
Z-Imageでは、被写体と構図を決める画像を低価格で作れます。H3はその画像を受け取り、動きと音声を生成します。入力には以下の違いがあります。
| Z-Imageのテキストから画像 | H3の画像から動画 | |
|---|---|---|
| モデルID | alibaba/z-image-turbo/text-to-image | minimax/h3/image-to-video |
| 必須入力 | prompt(1~1,000文字) | prompt(最大7,000)とimage_url |
| ここで使う設定 | aspect_ratio(1:1、4:3、3:4、16:9または9:16)、output_format(pngのみ)、seed、enable_prompt_expansion | duration_seconds(4~15)、resolution(768pまたは2k) |
| 使用できないもの | その他すべての出力形式 | seedとaspect_ratio:最初の画像で形が決まります |
| 出力 | PNGを1つ | 生成した音声付きのMP4を1つ |
| 価格 | 画像あたり$0.010 | 768pでは毎秒$0.037 |
最初の画像にはPNG、JPEG、WebPまたはGIFを使い、各辺を256~5,760ピクセル、アスペクト比を2:5~5:2、ファイルサイズを30 MB未満にします。PNGのアップロードが最も簡単です。公開画像リンクを使う場合は、画像そのものが返される必要があります。クラウドストレージの共有ページは画像入力ではありません。
同じ手順をPythonから実行する
Python 3.11以降を使い、python -m pip install spicyapiで公式パッケージをインストールし、fileユーティリティを使えるようにします。自分のターミナルでSPICY_API_KEYを設定し、次のコードをblue-mug.pyとして保存して、python blue-mug.pyを実行してください。有料の両段階で承認を求め、画像の確認時に停止します。再開時は生成された実行フォルダーを保ち、意図的に新しい試行を行う場合だけ新しいRUN_NAMEを選んでください。APIの仕様とSDKのオプションはPython SDKドキュメントを参照してください。
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tempfile
import time
import urllib.request
import uuid
from spicyapi import SpicyClient, output_assets
# Change RUN_NAME for a deliberately new pair of paid generations.
folder = Path(os.getenv("RUN_NAME", "blue-mug-1"))
folder.mkdir(exist_ok=True, mode=0o700)
state_file = folder / "state.json"
state = json.loads(state_file.read_text()) if state_file.exists() else {}
identity = hashlib.sha256(os.environ["SPICY_API_KEY"].encode()).hexdigest()
if state.get("identity", identity) != identity:
raise RuntimeError("This saved run belongs to a different API key.")
state["identity"] = identity
client = SpicyClient()
def save() -> None:
temp = state_file.with_suffix(".tmp")
temp.write_text(json.dumps(state, indent=2))
temp.chmod(0o600)
temp.replace(state_file)
def run(stage: str, model: str, inputs: dict) -> dict:
request = {"model": model, "input": inputs}
if stage not in state:
state[stage] = {"request": request, "key": str(uuid.uuid4())}
saved = state[stage]
if saved["request"] != request:
raise RuntimeError("Input changed: choose a new RUN_NAME, not a new retry key.")
save() # The same request and key survive a lost create response.
if not saved.get("taskId"):
if "first_sent" in saved and time.time() - saved["first_sent"] >= 23 * 3600:
raise RuntimeError("Unknown acceptance: reconcile task history before retrying.")
if "quote" not in saved:
quote = client.quote_task(model=model, input_data=inputs)
if input(f"{stage}: quoted ${quote['estimatedCost']}. Create? [y/N] ") != "y":
raise SystemExit("No task created for this stage.")
saved["quote"] = quote
saved.setdefault("first_sent", time.time())
save()
quote = saved["quote"]
task = client.create_task(model=model, input_data=inputs,
idempotency_key=saved["key"],
quote_id=quote["quoteId"],
expected_cost=quote["estimatedCost"])
saved["taskId"] = task["taskId"]
save()
task_id = saved["taskId"]
print(stage, "task:", task_id)
deadline = time.monotonic() + 1200
while time.monotonic() < deadline:
task = client.get_task(task_id)
if task["state"] in {"failed", "canceled", "expired"}:
raise RuntimeError(f"{task_id}: {task.get('errorCode')} {task.get('errorMessage')}")
if task["state"] == "succeeded":
assets = output_assets(task)
if not assets or assets[0].get("unavailable"):
raise RuntimeError(f"{task_id}: no retrievable output; keep the task ID.")
asset = assets[0]
if not asset.get("pending") and asset.get("url"):
print("Charge:", task.get("cost"), "settled:", task.get("settled"))
return asset
time.sleep(5)
raise TimeoutError(f"Resume {task_id} by rerunning this file; do not start another task.")
def download(asset: dict, destination: Path, mime: str) -> None:
temporary = None
try:
request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
with urllib.request.urlopen(request, timeout=300) as response:
if response.status != 200 or response.headers.get_content_type() != mime:
raise RuntimeError("Unexpected download status or content type.")
with tempfile.NamedTemporaryFile(dir=folder, delete=False) as handle:
temporary = Path(handle.name)
while chunk := response.read(1024 * 1024):
handle.write(chunk)
size = temporary.stat().st_size
expected = int(asset.get("bytes") or 0)
detected = subprocess.check_output(["file", "-b", "--mime-type", str(temporary)], text=True).strip()
if not size or (expected and size != expected) or detected != mime:
raise RuntimeError("Incomplete or incorrectly formatted download.")
temporary.replace(destination) # An interrupted transfer leaves the old file alone.
finally:
if temporary is not None:
temporary.unlink(missing_ok=True)
image_input = {
"prompt": (
'Photograph of a handmade blue ceramic mug of tea on a worn wooden workbench '
'in a pottery studio, a thin curl of steam rising from the mug. Soft window '
'light from the left, shallow depth of field, shelves of pots softly out of '
'focus behind. The mug sits left of centre with empty bench to its right. '
'Natural colours, no text, no logos.'
),
"aspect_ratio": "16:9",
"output_format": "png",
"seed": 42,
"enable_prompt_expansion": False,
}
image_asset = run("image", "alibaba/z-image-turbo/text-to-image", image_input)
download(image_asset, folder / "image.png", "image/png")
if input(f"Open {folder / 'image.png'}. Is the mug/frame approved? [y/N] ") != "y":
raise SystemExit("Stopped before video generation.")
if not state.get("image_uri"):
state["image_uri"] = client.upload_file(str(folder / "image.png"))["uri"]
save()
video_input = {"image_url": state["image_uri"], "prompt": 'Steam rises from the mug and drifts to the right. The camera pushes in slowly toward the mug, then holds. The mug, bench and shelves stay where they are. Quiet room, a faint ceramic clink.',
"duration_seconds": 5, "resolution": "768p"}
video_asset = run("video", "minimax/h3/image-to-video", video_input)
download(video_asset, folder / "video.mp4", "video/mp4")
print("Play", folder / "video.mp4", "with sound and review it before using it.")APIキーは自分のサーバーか管理するマシンに保管し、公開ウェブページやモバイルアプリには決して入れないでください。別のモデルを組み込む前には、上記のモデルページで現在の提供状況と受け付ける入力を確認します。
エージェントに呼び出しを任せる
入力の準備と結果の取得を任せ、価格と制作上の選択を自分で承認したい場合はAgentを使います。動画生成を許可する前に、最初の画像を自分で確認してください。
- CLI: 同じ入力オブジェクトを
input.jsonとして保存し、npx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --waitを使います。CLIが価格を確認します。復旧に備えて返されたタスクIDを保管してください。動画では、承認済み静止画をnpx @spicyapi/cli files upload image.pngでアップロードし、同じ方法でminimax/h3/image-to-videoを作成し、返されたspicy://URIをimage_urlに指定します。 - MCP: アシスタントに
spicyapi_task_createでalibaba/z-image-turbo/text-to-imageを呼び出し、続いてspicyapi_task_waitを使うよう依頼します。動画ではspicyapi_upload_fileで承認済みのimage.pngをアップロードし、続いてspicyapi_task_createでminimax/h3/image-to-videoを呼び出し、5秒、768p、上記の動きのプロンプトを指定します。MCPサーバーは作成前に価格の確認を求めます。 - Skill:
npx skills add spicyapi-ai/spicy-skillでインストールし、$spicyapiに同じ手順を実行して承認のために停止するよう依頼します。Skillは設定済みのCLIまたはMCP接続を使います。
完成動画だけでなく試行の予算も組む
以下は米ドルでの見積もりです。生成前に現在の価格を確認してください。
| リクエスト | 見積価格 |
|---|---|
| Z-Image静止画1枚、16:9 PNG | $0.010 |
| 同じ静止画でプロンプト拡張を有効化 | $0.016 |
| H3、768pで4 s | $0.148 |
H3、768pで5 s(resolution省略時の既定値) | $0.185 |
| H3、2kで5 s | $0.290 |
| H3、768pで10 s | $0.370 |
| H3、2kで15 s | $0.870 |
- 長さと解像度: H3は768p、2kで毎秒それぞれ$0.037、$0.058です。
- 非対応の組み合わせ: 2kで4秒の動画を作る場合は、エラー
40004で拒否されます。 - 試作2本: 静止画1枚と768pの5秒動画2本で$0.380です。
- 試作と納品用テイク: 承認済み静止画を再利用し、静止画1枚、768pの5秒確認用動画、2kの5秒動画を作ると、$0.010 + $0.185 + $0.290 = $0.485です。
タスクが受け付けられた時点で資金が確保され、終了時に精算されます。失敗または期限切れのタスクでは通常、この確保が解除されます。一部のコンテンツ拒否には、モデルが明示する方針に従って課金されます。請求を調べる際は、タスクの実際のcostとsettledの状態を確認してください。
適切な段階で問題を直す
| 症状 | 変更すること |
|---|---|
| マグカップに取っ手が2つある、または不要な文字がある | 画像プロンプトを修正し、動画生成前に新しい最初の画像を承認します |
| カメラを動かしたかったのにマグカップが動く | 画像は保ち、カメラの動きを1つだけ指定し、マグカップを静止させると明記します |
| 768pでは良かったが新しい2kのテイクが異なる | 新しいショットとして扱い、承認前に動きを確認します |
| 画像リンクが使えない | 画像ファイルをアップロードするか、共有ページではなく直接アクセスできる画像URLを使います |
アカウントが401または40310を返す | キーを確認するか、メールアドレスの確認を完了します |
40301 / 40302 / 40303 | モデルの権限、キーのIP許可リスト、地域別の提供状況を確認します |
40201 / 40202 | 利用可能残高やアカウント/キーの支出上限を確認し、送信失敗の状況を照合する間は保存したリクエストを保持します |
リクエストがseed、aspect_ratio、output_formatを拒否する | 選択したタスクの対応入力を確認し、Z-ImageのフィールドをH3にコピーしないでください |
40004 | 2kと4秒という非対応の組み合わせを変更します |
40901または冪等性の競合 | 再見積もりします。タスクIDを保存していなければ、state.jsonのその段階の項目を削除して再実行します。以前の試行が受け付けられた可能性があれば、何かを新しく作る前にタスク一覧を確認してください。エラーリファレンスも参照してください |
429または接続エラー | 実行フォルダーとタスクIDを保ち、結果を回収するためだけに別の有料試行を始めないでください |
タスクがcontent_rejectedまたはtimeoutを報告する | エラーと課金を読み、プロンプトの変更、元画像の変更、ショットの短縮のいずれかを意図的に選びます |
| 生成は成功したが結果がまだ準備できていない | 保存済みの同じタスクを再開し、ダウンロードのために別の動画を生成しないでください |
最初のワークフローは小さく保つ
ショットがうまくいったら、納品用の長さと解像度を決め、そのテイクを生成して確認します。必要に応じてH3のlast_image_urlに終了画像を指定すると、終わり方を誘導できます。ただし、最後のフレームの完全一致やフレーム間の動きは保証されません。
アプリケーションでは2つのタスクIDを保管し、承認したファイルを自分のストレージに保存します。結果の取得方法にはポーリングとWebhookの比較、ファイルの保持にはメディア保存、多くのショットが必要な場合にはバッチ生成を参考にしてください。最初の目標は変わりません。承認済みの画像1枚と、意図したショットに実際に沿う動画1本です。







