本文へスキップ

AI コンパニオンアプリに画像と動画の配信を組み込む

承認済みのコンパニオン画像と短い動画を作成します。場面を固定し、見積りを取り、非同期ジョブを復旧し、確認済み素材を重複タスクなしで届けます。

執筆
Lydia
公開
読了時間
約 17 分
更新
成人男性が望遠鏡の接眼レンズをのぞき込み、そのそばで赤いコートの女性が、暖かく照らされた扉の近くで微笑んでいる。

ユーザーが、架空の成人コンパニオンキャラクターのローワンに「今夜、天文台を見せて」と話しかけます。チャットモデルはキャラクターとして返答できますが、画像を作り、場面の一貫性を保ち、実際の素材ができたらそれを添付するのはアプリケーションの役目です。画像と動画のジョブは別に実行し、その間もチャットは応答し続けるようにします。

ユーザーには、2 つの購入を自分で決めてもらいます。

  • スチルを承認する: 無料のシーンカードを見せ、画像の見積りを取り、ユーザーが価格を承諾してから初めて作成します。
  • クリップを承認する: 完成したスチルを見せ、そのアニメーションの見積りを取り、ユーザーがその画像と価格を承諾してから初めて動画を作成します。

各リクエストは送信前に保存し、段階ごとに別のキーを使い、配信は自前で保存したコピーから行います。ツール呼び出しはシーンカードを開始するだけで、購入を承認することも、存在しない添付を告知することもありません。

具体的な予算として、1k の Qwen 編集($0.036)と 5 秒の 720p Seedance 2.5 クリップを合わせると $1.4225 です。追加の参照画像の料金、チャットトークン、再試行は含みません。ユーザーに価格を見せる前に、必ず見積り(quote)を取ってください。

全体の流れは次のとおりです。

text
1. Chat model calls request_scene_image(scene, details)
2. Server validates owner and scene -> free draft card -> tool result back to the chat
3. Gate 1: user approves the still's price -> Qwen edit task (own idempotency key)
4. Callback or poll -> still ready -> copy it to your own storage
5. Gate 2: user accepts the still -> quote the clip -> user approves its price
6. Seedance task (own key) -> callback or poll -> ready_for_review
7. Review the whole clip -> copy to your storage -> delivered

チャットの依頼を、アプリが実際に作れる画像に変える

ユーザーが「緑のコートでもう 1 枚送って」と書いたとき、紺色のコートの動画がすでに生成中かもしれません。チャットの返答とメディアのリクエストが、書き換え可能な 1 つの説明を共有してはいけません。アプリは、どの架空の成人か、どのコートか、どの場所か、スチルと動画のどちらがほしいのかを把握している必要があります。そうしないと「もう 1 枚」が、意図せず別の顔を作ったり、古い場面を再利用したり、写真を頼まれたのに動画を購入したりしかねません。

恒久的なキャラクターデータは小さく保ちます。承認済みのポートレート、成人であること、安定した外見のメモ、ユーザーが選んだキャラクターです。現在の場面は別に持ちます。コート、部屋、カメラの見え方、依頼された動作です。会話から場面を提案できますが、保存済みのキャラクターと承認済みの元素材を決めるのはアプリケーションです。チャットモデルに、参照 URL やタスク ID を作らせたり、画像が存在すると言わせたりしてはいけません。

天文台の例では、見積りを取る前に、依頼を明示的な場面の説明にします。ローワン、承認済みのポートレート、紺色のコート、天文台のバルコニー、ミディアムショット、スチル 1 枚です。後で「動かして」と頼まれたら、承認済みの天文台のスチルと、小さな首の動きを使います。会話全体を画像モデルに送ってはいけません。別のキャラクターや以前の服装に関する無関係な説明が、現在の依頼と衝突することがあります。

画像の生成中は、インターフェースに場面の簡単な要約を表示できます。ユーザーが緑を紺に変えたら新しいリビジョンにし、保留中だった以前の結果にその旨のラベルを付けます。完成済みの古いリビジョンは本人の非公開履歴に残してかまいませんが、最新の依頼への答えとして黙って表示してはいけません。

NSFW のコンパニオン製品では、画像と動画の適合性を別々に確認します。画像タスクが成人向けコンテンツを受け付けても、次の動画モデルが同じ場面を受け付けるとは限りません。段階ごとに選んだモデルのページを確認し、選んだモデルが対応しない成人向けクリップを約束せず、利用できないタスクは正直に伝えます。

まず Playground で試す

Qwen Image 2.1 edit の Playground を開きます。Images で Add a file を選んで承認済みのキャラクターポートレートをアップロードし、Prompt に新しい場面を書き(下のプロンプトが使えます)、Resolution を 1k、Aspect ratio を 16:9 に設定して Run model を押し、スチルを確認します。その構図を承認したら、Seedance 2.5 image-to-video の Playground を開きます。スチルを Input image に追加し、動きを Prompt に書いて、もう一度 Run model を押します。Closing frame は任意です。

チャットにツール呼び出しでメディアを依頼させる

チャットモデルに、アプリケーションへシーンカードの準備を頼む request_scene_image ツールを渡します。使うのは、チャットの返答で作られた ID ではなく、アプリケーション側のサインイン中のユーザーと承認済みのキャラクターポートレートです。

  • チャットモデル: 場面を説明し、作成するかどうかをユーザーに尋ねます。
  • アプリケーション: キャラクターの所有権を確認し、価格を表示し、承認を待ちます。
  • メディアタスク: 承認後にのみ画像を作成します。アニメーションは別の購入です。

Grok 4.6 chat は OpenAI 形式のツール呼び出しに対応しています。API は会話の記憶を保持しないため、リクエストのたびにキャラクターカードとフィクション執筆の文脈を再送してください。ツールメッセージの形式はチャットレシピにあります。

json
{
  "type": "function",
  "function": {
    "name": "request_scene_image",
    "description": "Ask the app to prepare a picture of the character. Call only when the user asks to see something. The app shows a free scene card first; nothing is made or charged until the user approves a quoted price.",
    "parameters": {
      "type": "object",
      "properties": {
        "scene": {"type": "string", "enum": ["observatory-balcony", "kitchen-morning", "rooftop-dusk"]},
        "details": {"type": "string", "description": "Clothing, props and mood the user asked for, in one sentence."}
      },
      "required": ["scene"]
    }
  }
}

この呼び出しには、ドラフトカードが承認待ちであることを伝える tool メッセージで応答します。そうすれば、モデルは納品したと言い張らず、「シーンカードです。作りますか?」のような正直な一言を書けます。

キャラクターと場面を別々のレコードにする

承認済みのポートレートと、依頼された場面を分けて持ちます。紺色のコート、天文台のバルコニー、望遠鏡に向けた小さな振り向きです。ポートレートはキャラクターの基準になり、承認された場面画像は動画の最初のフレームになります。新しい場面のたびにキャラクターのポートレートを置き換えないでください。

リクエストごとに、送信前にレコードを 1 つ保存します(これはアプリケーションのフィールドで、API のペイロードではありません)。

json
{
  "mediaRequestId": "scene-104-r1",
  "ownerId": "application-user-id",
  "sceneRevision": 1,
  "referenceAssetId": "approved-rowan-portrait",
  "approvedStillAssetId": null,
  "model": "alibaba/qwen-image-2.1/edit",
  "inputSnapshot": {},
  "idempotencyKey": "<uuid generated for this request>",
  "taskId": null,
  "state": "draft",
  "deliveredAssetId": null
}

ユーザーがコートを緑に変えたら、新しいキーでリビジョン 2 を作ります。青いコートのリクエストが実行中のあいだは、決して編集しないでください。

承認したスチルは自分で保存します。 タスクの結果リンクの有効期間は約 20 分、生成ファイルの保持は約 14 日なので、承認したスチルを自前のストレージにコピーし、画像ファイルそのものを返す安定した公開 HTTPS URL を Seedance に渡します(共有ページへのリンクは失敗します)。Qwen の結果リンクを動画の段階にそのまま渡しても、有効期限が切れるまでしか使えません。動きのリビジョンには保存済みのスチルを再利用してください。ポートレートと動きを同時に変えると、どちらも直しにくくなります。

画像と動画の設定は別々に行う

Qwen Image 2.1 edit のルートは、プロンプトと 1〜10 枚の image_urls を受け付けます。参照画像を追加すると 1k の価格が上がります。2k は参照画像 1〜10 枚で $0.048 です。最初の場面では、承認済みのポートレート 1 枚、1k、16:9、PNG、プロンプト拡張オフを使います。

Use the supplied portrait as the identity reference for the same fictional adult Rowan. Create a medium shot on an observatory balcony, wearing a navy coat. Keep both hands relaxed and visible. A telescope rests on its tripod beside Rowan. Leave open space on the right for a small head turn. Warm doorway light, natural night background, no text or extra people.

スチルをユーザーに見せる前に、顔と服装を承認済みのポートレートと見比べます。ゲート 2 では、アニメーションの前に、ユーザーがそのスチルとクリップの価格を承諾します。望遠鏡が浮いている、右側に動きの余白がないといった場合は、画像の段階で直してください。動画では、悪い最初のフレームを修復できません。

紺色のコートを着た架空の成人男性が、天文台のバルコニーで手の力を抜いて立ち、地面にしっかり据えた三脚の望遠鏡の隣にいる。

提案する最初のフレームのイラストです。キャラクターには小さく振り向く余白があり、望遠鏡は物理的に支えられています。これは Qwen の出力でも生成テストでもありません。

Seedance 2.5 image-to-video のルートは、image_url、4〜30 秒、480p・720p・1080p、音声のオン/オフ、任意のシードを受け付けます。aspect_ratio は adaptive のみなので、クリップはスチルの形に従います。先にスチルを配信フォーマットへクロップしておいてください。reference-to-video のルートは契約が異なるため、そのフィールドを混ぜないでください。

判断の場面で見積りを取る

Qwen の編集は、参照画像 1 枚込みで、1k が $0.036、2k が $0.048、1.5k が $0.072 です。参照画像を追加すると、1k と 1.5k の価格が上がります。Seedance は 1 秒あたり、480p が $0.1234、720p が $0.2773、1080p が $0.6823 で、音声のオンとオフで同じ料金です。

動きをやり直すときは、承認済みのスチルを再利用します。

プラン費用
1k スチルのみ$0.036
スチル + 5 秒クリップ(480p)$0.653
スチル + 5 秒クリップ(720p)$1.4225
スチル + 5 秒クリップ(1080p)$3.4475
スチル + 720p クリップ 2 回$2.809

これらには 1k 編集の参照画像 1 枚が含まれ、追加の参照画像の料金、チャットトークン、スチルの追加編集は含まれません。

スチルの価格は生成前に、クリップの価格はユーザーがスチルを承諾した後に表示します。チャットのたびに表示するものではありません。見積りは無料で、何も確保せず、そのアカウント、API キー、モデル、入力に対して 5 分間有効です。長さ、解像度、参照画像を変えると、新しい見積りが必要です。送信したタスクは推定費用をすぐに保留し、その保留額が請求されうる上限です。承認済みの画像を再利用すれば、動きのリビジョンは動画分の料金だけで済みます。

応答が失われても耐える動画段階のクライアント

spicyapi を python -m pip install spicyapi でインストールし、SPICY_API_KEY と APPROVED_STILL_URL を設定して、スチルを承認した後にこれを実行します。保存された実行ファイルを残しておけば、同じ試行を再開できます。別のクリエイティブなテイクにするときは、新しい実行名を使います。

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"] in {"failed", "expired", "canceled"}:
            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/seedance-2.5/image-to-video", {
    "image_url": os.environ["APPROVED_STILL_URL"],
    "prompt": "The adult character slowly turns toward the telescope. Hold the camera steady. Keep the navy coat and balcony unchanged.",
    "duration_seconds": 5, "resolution": "720p", "aspect_ratio": "adaptive", "generate_audio": True,
}, "companion-video")
print(asset["url"])

本番への組み込みについては、Python SDK とタスクのドキュメントを参照してください。

ポーリングとコールバックを 1 つの更新関数で処理する

ポーリングで得た結果と、検証済みの完了イベントの両方に、アプリケーションの同じハンドラーを使います。素材を 1 回だけ添付し、非公開のまま保ち、確認できる状態になったときだけチャットに通知します。重複したイベントでギャラリーのエントリーが増えてはいけません。

稼働中のコンパニオンアプリでは、完了通知を検証済みの Webhook に送り、届かないコールバックに備えて定期的な状態スイープも残します。コールバックの本文にはプロンプトと署名付きの結果リンクが含まれるため、非公開として扱ってください。設定はタスクのドキュメントにあります。照合スイープ付きの Webhook(記事「async-task-api-polling-vs-webhook」を参照)は本番向きで、試作ならポーリングで十分です。多数のユーザーが同時にメディアを依頼する場合は、制限付きディスパッチを使います。

配信の状態、失敗、キャンセル

draft、submitted、generating、ready_for_review、delivered、failed、rejected を使います。API の succeeded は結果の処理を始める合図であり、ユーザーに見えるという意味ではありません。delivered にする前に、クリップ全体を見て、顔のドリフト、動く小物、依頼していないカットがないか確認します。

API の結果表示する内容課金
succeeded、素材の準備完了確認してから配信settled と cost を読む
succeeded、素材が pending「まだ準備中です」と表示し、数秒後に再取得まだ確定ではない
succeeded、素材が unavailable保存済みのコピーを復元するか、改めて生成することを提案cost を読む
failed または expired分かりやすい失敗表示と再試行ボタン保留は通常解放。モデルページに記載がある場合、一部のコンテンツ拒否は課金される

期限切れの見積りには、新しい見積りと承認が必要です。すでに承認されたリクエストは、元のキーで復旧します。受理済みのタスクはキャンセルできません。ユーザーが「やっぱりいい」と言ったら、保留中の添付を非表示にして、ジョブはそのまま完了させます。チャットを閉じれば課金が解除されると約束してはいけません。再試行は、独自の見積りを持つ新しいリクエストです。

コンパニオンの非公開メディアを非公開に保つ

配信する前に、その場面がサインイン中のユーザーのものであることを確認します。承認済みのメディアは自前のアクセスルールを持つストレージにコピーし、一時的な結果リンクを公開ギャラリーのアドレスにしてはいけません。

削除では、プラットフォーム側のコピーと自前のコピーを別々に扱います。タスクごとの保持設定でプラットフォームが結果を保持する期間を短くし、ユーザーが場面やアカウントを削除したら、完了した各タスクの保存済みコンテンツを破棄します(請求記録は残ります)。保持を 0 にすると、ワーカーがコピーする前に結果が消える場合があります。自前のコピーの削除と、その配信アクセスの取り消しは別に行います。詳細は保持のドキュメントにあります。製品に合った保持の制御と耐久性のあるストレージパイプラインを使ってください。

フィクスチャーで 4 つの失敗をテストする

ダブルクリック、承認応答の消失、成功イベントの重複、利用できない素材を再現します。結果として、論理的なリクエストは 1 件、元のキーでの復旧、ギャラリーへの添付は 1 件、そして「利用不可」の正直な状態になるはずです。どれも有料の生成は必要ありません。スチルとクリップをまたいだ顔の一貫性は目で確かめるしかないため、すべての出力を確認してください。「動画ができました」というチャットメッセージが、実際のタスクや素材の状態より優先されないようにします。天文台のイラストは編集用の場面であり、Qwen や Seedance の出力ではありません。

呼び出しを Agent に任せる

入力の準備と結果の返却を Agent に任せ、価格とクリエイティブな判断は自分で承認したいときに使います。

  • CLI: input.json を、{"image_urls": ["<approved portrait URL>"], "prompt": "<the scene prompt above>", "resolution": "1k", "aspect_ratio": "16:9", "output_format": "png", "enable_prompt_expansion": false} のような内容で用意し、npx @spicyapi/cli tasks create --model alibaba/qwen-image-2.1/edit --input-file input.json --wait を実行します。CLI が価格を確認します。復旧のために、返されたタスク ID を保管してください。
  • MCP: アシスタントに、spicyapi_task_create を alibaba/qwen-image-2.1/edit に使い、続けて spicyapi_task_wait を使うよう頼みます。MCP サーバーは、作成前に価格の確認を求めます。
  • 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