本文へスキップ

createTaskの次は?非同期生成タスクのポーリングとWebhook

画像・動画モデルは結果ではなくtaskIdを返します。ポーリングとWebhookの選び方、タイムアウト後の再開、二重課金の回避を解説します。

執筆
SpicyAPI Engineering
公開
読了時間
約 9 分
更新
円形のチェックポイントが並ぶ曲がりくねった道と、光る球体へまっすぐ伸びる金色の道。

createTaskを呼び出すと、画像ではなくtaskIdが返ってきました。ここから結果を取得する必要があります。数分間接続を開いたままにしたり、タイムアウト後に二重で支払ったり、プロセスの再起動でタスクを見失ったりしない方法が必要です。

要点は次のとおりです。

  • スクリプトで高速な画像モデルを使う場合: ?wait=30をcreateTaskに付ければ、多くの場合はポーリング自体を省けます。
  • スクリプト、ローカル開発、公開サーバーがない場合: 以下の例のように、回数に上限のあるループでタスクの状態を確認します。
  • ユーザーが待つ処理、動画、大量の処理: callBackUrlを使い、署名を検証し、すぐに受信を確認してからキューで処理します。
  • 本番環境: 両方を使います。コールバックで素早く結果を受け取り、定期的な状態確認で届かなかったコールバックを補います。

APIを直接呼び出す場合、?wait=<seconds>(最大60)はタスクが終了するまでcreateTaskの接続を維持します。数秒で完了する画像モデルに向いていますが、動画には向きません。callBackUrl(Bは大文字)は結果を受信する公開HTTPSアドレスです。recordInfoは状態確認の呼び出しで、SDKのget_taskはこれをラップしています。タスクのリファレンスに、この3つの説明があります。

まずPlaygroundで試す

seedream-5-0-pro Playgroundを開きます。短いPromptを入力し、Resolutionを1kに設定してRun modelを押し、Outputで結果を確認してください。Historyには過去の実行が表示されます。ここで見られる待ち時間を、アプリケーション向けに自動化するのがポーリングとWebhookです。別の画像をもう1枚生成するわけではありません。

完了結果の受け取り方を選ぶ

アプリケーション最初に使う方法理由
ローカルスクリプトや時々実行するバッチSDKによるポーリング公開の受信先を用意する必要がない
対話型の画像・動画アプリケーション検証済みのWebhookタスクごとに待機プロセスを維持せず、生成完了時に対応できる
本番サービスWebhookと定期的な照合コールバックを受け損ねても、受け付け済みタスクが放置されない

ポーリングを選ぶのは、 タスク数が少ない場合や、実行マシンが外部からのHTTPS呼び出しを受信できない場合です。Webhookを選ぶのは、 対話型アプリケーションでユーザーが待っている場合や、長い動画タスクがワーカーを占有してしまう場合です。

画像入力を使うタスクでは、SDKのアップロード処理が/files/{fileId}/commitを実行し、spicy:// URIを返します。メディアのドキュメントを参照してください。

SDKでタスクを1件実行する

spicyapiをインストールし、SPICY_API_KEYを設定して、例を実行します。中断後に同じタスクを再開できるように、map-image.jsonを保存してください。意図的に別の画像を生成する場合は、新しい実行名を使います。Python SDKがAPIメソッドを提供し、タスクのリファレンスがライフサイクル全体を説明しています。

python
import json
import os
import time
import uuid
from pathlib import Path
from spicyapi import SpicyClient, output_assets

client = SpicyClient()

def run(model, inputs, name):
    path = Path(name + ".json")
    request = {"model": model, "input": inputs}
    saved = json.loads(path.read_text()) if path.exists() else {"request": request, "key": str(uuid.uuid4())}
    if saved["request"] != request:
        raise ValueError("Changed input needs a new run name; preserve the old attempt.")
    def save():
        temporary = path.with_suffix(".tmp")
        temporary.write_text(json.dumps(saved))
        temporary.chmod(0o600)
        temporary.replace(path)
    if not saved.get("taskId"):
        if time.time() - saved.get("sentAt", time.time()) >= 23 * 3600:
            raise RuntimeError("Reconcile the saved attempt before submitting again.")
        if "quote" not in saved:
            saved["quote"] = client.quote_task(model=model, input_data=inputs)
            if input(f"Maximum ${saved['quote']['maxCharge']}. Create? [y/N] ") != "y":
                raise SystemExit("Stopped without creating this task.")
        saved.setdefault("sentAt", 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()
    print("Task:", saved["taskId"])
    for _ in range(360):
        task = client.get_task(saved["taskId"])
        if task["state"] not in {"queued", "running"} and task["state"] != "succeeded":
            raise RuntimeError(str(task.get("errorMessage", task["state"])))
        if task["state"] == "succeeded":
            assets = output_assets(task)
            if assets and assets[0].get("unavailable"):
                raise RuntimeError("Output unavailable; keep the task ID for reconciliation.")
            if assets and assets[0].get("url") and not assets[0].get("pending"):
                return assets[0]
        time.sleep(5)
    raise TimeoutError("Rerun the same saved attempt to resume; no new task is needed.")

asset = run("bytedance/seedream-5.0-pro/text-to-image", {
    "prompt": "a folded paper map on a wooden table, overhead", "resolution": "1k", "output_format": "png"
}, "map-image")
print(asset["url"])

レスポンスを正しく読む

タスクは常にqueued、running、succeeded、failed、expiredのいずれかの状態です。処理が進行中なのは最初の2つだけなので、succeededだけを待つのではなく、「queuedでもrunningでもない」ときにループを終了します。そうしないと、失敗したタスクを永久に確認し続けてしまいます(古いレコードには読み取り専用のcanceledが表示されることもあります)。結果のリンクはoutput.assets[0].urlにあります。APIを直接呼び出す場合は、レスポンスのルートではなく、ラッパー内のdataオブジェクトからタスクを読み取ります。受け付け済みのタスクはキャンセルできません。課金されるのはsucceededだけで、failedとexpiredでは通常、確保された金額が解放されます。

タイムアウトがアプリケーションに与える意味

HTTP 202はタスクが受け付けられたことを示し、生成完了を意味しません。ローカルのタイムアウトは待機を止めますが、受け付け済みタスクをキャンセルするわけではありません。タスクIDを保存し、別の生成を購入するのではなく、そのタスクを再開してください。タスクが返す期限は復旧可能な期間の境界であり、レンダリング時間の予測ではありません。

  • 送信へのレスポンスが失われた場合: 元のタスクを復旧時に見つけられるよう、リクエストとそのキーを保持します。
  • 生成に失敗した場合: モデルのエラーを表示し、ユーザーが明示的に次の試行を選べるようにします。
  • 生成は成功したもののファイルが準備中の場合: 同じタスクで待機を続けます。画像や動画は、まだ配信用に準備されています。

新しいリクエストには新しい冪等性キーを使います。同じリクエストを復旧する場合は、キーと入力を変えません。冪等性のリファレンスに、24時間の再実行可能期間が説明されています。

Webhookで素早く結果を受け取る

完了イベントをアプリケーションのバックエンドに送り、通常の通知経路でユーザーインターフェースを更新します。コールバックはユーザーのブラウザーへの直接メッセージではありません。

受信側では次の4つを行います。

  1. 公式SDKとリクエストの生の本文を使ってイベントを検証します。
  2. タスクが自分のアプリケーションの処理に属することを確認します。
  3. 受信確認を返す前に、イベントを永続化し、重複を排除します。
  4. HTTPレスポンスとは別に、出力のコピーとアプリケーションの更新をキューに入れます。

選ぶべき場合: すでにバックエンドを運用していて、完了をすぐに通知したい場合です。別の方法が向く場合: 公開の受信先がない短いローカルスクリプトなら、ポーリングのほうが簡単です。

タスクの完了とメディアの配信は、別々に確認してください。形式が正しいイベントでも、失敗したタスクを通知していることがあります。一方、成功したタスクでも、アプリケーションが準備完了を知らせる前にファイル処理が必要な場合があります。Webhookのドキュメントに、検証と配信の取り決めが記載されています。

定期的な照合を続ける

受け付け済みタスクのIDをすべて保存してください。アプリケーションで未解決のタスクを定期的に再確認します。特に、デプロイ、ネットワーク障害、署名用シークレットの変更後は重要です。復旧した結果も、Webhookと同じ冪等な完了ハンドラーに渡せば、ギャラリーに同じファイルが二重に追加されません。

未確定の課金は、精算が完了するまで記録に残します。ローカルの記録が失われても、現在のキーに対応するタスク履歴からIDを復旧できます。本番環境への統合に、この手順が説明されています。

期限切れになる前に結果をコピーする

準備済みの出力リンクは一時的なものです。User-Agentリクエストヘッダーを付け、返された形式に合うファイル名で、自分が管理するストレージにファイルをコピーしてください。ダウンロード失敗時は既存の結果を復旧します。再生成する理由にはなりません。

期限はメディアの保持と配信、コピーの手順は永続ストレージを参照してください。

選び方の結論

小さなスクリプトではSDKのポーリングから始めます。対話型サービスを運用するようになったら検証済みのWebhookを追加し、復旧のための定期確認も続けてください。保存したタスクIDが、この3つの経路をつなぎます。いずれも代わりのタスクを送信するのではなく、1回の生成結果を取得する方法です。

同時に処理する件数を増やす場合は、上限を設けたバッチ送信を追加します。初めて画像と動画を組み合わせる場合は、入門ガイドを使ってください。

Agentに呼び出しを任せる

入力の準備と結果の取得を任せ、自分は料金や創作上の選択を承認したい場合は、Agentを使います。

  • CLI: {"prompt": "a folded paper map on a wooden table, overhead", "resolution": "1k", "output_format": "png"}をinput.jsonとして保存し、npx @spicyapi/cli tasks create --model bytedance/seedream-5.0-pro/text-to-image --input-file input.json --waitを使います。CLIで料金を確認・承認します。返されたタスクIDは復旧のために保存してください。
  • MCP: アシスタントに、spicyapi_task_createをbytedance/seedream-5.0-pro/text-to-image向けに使い、その後spicyapi_task_waitを使うよう依頼します。MCP serverは作成前に料金の承認を求めます。
  • Skill: npx skills add spicyapi-ai/spicy-skillでインストールし、$spicyapiに同じ手順を実行して承認のために停止するよう依頼します。Skillは設定済みのCLIまたはMCP接続を使います。

他の言語 English · 한국어 · Deutsch · Français · Español · Português (Brasil) · Русский · Italiano · Polski

実際に試してみる

支払いは実行した分だけ。プランもサブスクリプションもありません。

使用モデル:MiniMax H3 · ByteDance Seedance 2.0 Fast