本文へスキップ

AI APIがアセットキーを返したら、メディアを長期保存しましょう

生成結果は約14日、アップロードした参照素材は1日保存されます。有効期限と、コールバックから自分のストレージへコピーする方法を解説します。

執筆
SpicyAPI Engineering
公開
読了時間
約 16 分
更新
砂時計のある一時保管ドックから、編集モニター横のドライブベイへ光る写真カードを滑らせる手

生成に成功しても、一時的な配信URLを保存するだけでファイルをコピーしなければ、製品内の画像が表示できなくなることがあります。署名付きリンクは数分で失効しますが、APIの出力本体は約2週間保存されます。

メディアAPIは、同じファイルを複数の識別情報で扱います。入力の参照、生成出力のキー、そして数分間だけデータを取得できる署名付きURLです。それぞれ異なる期限で失効し、どれも永久的なアドレスではありません。このガイドではSpicyAPIの具体的な保存期間を示し、大切な結果を自分で管理するストレージへコピーする方法を説明します。保存期間の設定により、これらの期間を短縮できます。

まずPlaygroundで試す

Z-Imageのテキストから画像ページを開き、サインインします。

  1. Promptに短い場面の説明を入力し、Aspect ratioを選びます。
  2. Run modelを押します。結果がOutputに表示されます。
  3. 画像をダウンロードし、自分で管理できる場所に保存します。
  4. 後から保存したファイルを開き直します。元のページや一時的な結果リンクがなくても開けます。このガイドの残りの部分では、本番環境でこの操作を自動化します。

重要なポイント

  • 完了時にワーカーでコピーします。 大きなファイルのダウンロードはコールバックリクエストの処理外で行います。
  • 署名付きリンクを非公開にします。 リンクを持つ人は一時的にファイルへアクセスできます。
  • 自分のアセットIDと配信URLを公開します。 タスクの記録は、そのメディアより長く残ることがあります。

何がいつ失効するか

対象有効期間期限後に起きること
アップロードチケット(uploadUrl)約20分。この時間内にPUTと確定処理の両方を完了する必要がありますチケット失効の応答が返ったら、新しいチケットが必要です。確定処理が混雑している場合は409が返ることもあります
確定済みの参照(spicy://f/fil_...)確定から1日。確定応答のexpiresAtまでです新規タスクと再試行でURIが拒否されます
結果リンク(output.assets[].url)約20分。ただしメディア本体の期限を超えることはありませんストレージが403を返します。タスクを再取得して新しいリンクを得ます
生成した画像、動画、音声完了から約14日(短く設定した場合はそれより短い期間)削除され、リンクを発行できなくなります(410がdownload-urlから返ります)
タスク内のプロンプトテキストと結果ファイルのパス30日inputとoutputが消去され、タスクの行は残ります
タスクの記録(ID、モデル、状態、費用、タイムスタンプ)無期限に保存内容はなくなっても照会できます

Spicy Studioのプロジェクト内で作成した短編動画のメディアには、約60日という独自の長い保存期間があります。API、SDK、Playgroundでの生成には、上記の14日上限が適用されます。生成物の保存期間はリクエストごと、またはアカウントごとに短縮することもできます。詳しくは保存期間設定の記事をご覧ください。これらの上限より短くすることはできますが、長くすることはできません。

14日は送信時ではなく、完了時から数えます。タスクが終わる前のタイムスタンプは暫定値で、完了時に基準が更新されます。

「約」としているのには理由があります。ストレージの削除はライフサイクルルールで実行され、期限から最大1日遅れる場合があります。ただし、この遅延を追加の猶予として使うことはできません。保存期間が終了すると、ダウンロードリンクは発行されなくなります。

1つのファイル、4つの表現

text
local reference image
  │
  ├─ POST /api/v1/common/upload-url       → upload ticket (about 20 min)
  ├─ PUT to the returned uploadUrl         → temporary object
  ├─ POST /api/v1/files/{fileId}/commit    → spicy://f/fil_...  (usable 1 day)
  ├─ POST /api/v1/jobs/createTask          → task
  └─ terminal callback or recordInfo
       └─ output.assets[]                  → key + url + expiresAt (about 20 min)
            └─ stream bytes to your storage → your asset ID and delivery URL

SDK、CLI、Playgroundはアップロード手順を代行するため、手作業で実装することはあまりありません。それぞれの表現には役割があります。チケットは有効期間内の直接書き込みを許可し、確定処理は受け付けたデータを固定します。spicy:// URIは、アカウントに紐づく確定済み入力をタスクが1日間参照できるようにします。出力のkeyは生成結果を識別します。urlは一時的な読み取りを許可します。製品が公開すべき長期的な識別情報は、自分のアセットIDだけです。

SpicyAPIへの参照素材のアップロードは無料です。料金がかかるのは、その素材を使う生成タスクです。既存の出力リンクを更新しても、新たな生成料金は発生しません。出力を失い、再生成することを選んだ場合は、その新規タスクにモデルの料金がかかります。長期保存用バケットの保存、リクエスト、転送費用は、ストレージサービスのプランに沿って別途予算に計上してください。

入力:アップロード後、1日での失効に備える

元の参照画像、動画、音声は、自分のストレージに保存してください。ローカルの入力をSDK、CLI、Playgroundからアップロードし、確定済みURIが有効な間は再利用します。失効後に必要になったら、再アップロードしてください。一時的なURIはアーカイブではありません。

モデルへ入力を渡す方法は3つあります。

  • 公開HTTPSアドレス。 モデルのフィールドがURLを受け付け、アドレスがファイルそのものを返す場合に使えます。クラウドドライブ、アルバム、SNSのリンクはウェブページを返すため、タスクがinvalid_assetで失敗します。その場合はファイルをダウンロードしてからアップロードしてください。
  • 小さな埋め込み画像。 JPEG、PNG、WebP、GIFをbase64のデータURIとして渡せます。各画像の上限は1 MiBです。
  • アップロード。 ローカルファイル用です。画像(JPEG、PNG、WebP、GIF)は最大10 MiB、WAV、MP3、WebM、MP4、MOVの音声・動画は最大90 MiBかつ600秒です。モデル側の上限がこれより小さい場合があるため、モデルページでフィールドの制限を確認してください。TypeScript SDKでは、await client.uploadFile("./reference.png")がspicy:// URIをfile.uriとして返します。

SpicyAPIキーをブラウザーに公開してはいけません。サーバーからアップロードチケットを発行してください。メディアのドキュメントでは、ブラウザーへの組み込みとすべての制限を説明しています。

出力:完了した結果の状態

タスクが完了していることと、ファイルを取得できることは別の状態です。

  • 取得可能: ファイルを自分のストレージへコピーし、タスクID、形式、サイズを記録します。
  • 準備中: 同じタスクを保持し、再度確認します。ファイルを得るために別の生成を購入してはいけません。
  • 取得不可: プラットフォーム上のコピーはなくなっており、待っても復元されません。自分で保存したコピーを使ってください。なければ再生成し、通常どおり課金されます。料金が発生しないのは、初回の保存に失敗してファイルが1つも配信されなかった場合だけです。

一時的なリンクは、その所有者にアクセスを許可します。公開ページやアクセス解析に含めず、コピー用ワーカーの準備ができた時点で最新のリンクを取得してください。

保存期間の境界

タスクの記録、出力キー、更新したダウンロードリンクは、データ本体を保存しません。メディアの期限前にコピーしてください。

保存期間が終了すると、recordInfoはcontentState: "expired"を報告し、inputとoutputは遅くとも30日後に消去されます。タスクの内容を自分で破棄した場合、状態はpurgedです。一方、タスク状態のexpiredは別の意味で、生成がタイムアウトし、何も生成されなかったことを示します。UIで両者に同じラベルを表示しないでください。一方は再試行が必要ですが、もう一方は再試行では復元できません。

コピー期限は保存期限より十分前に設定してください。最初の成功イベントでコピーし、その後は、成功したのに長期保存済みアセットがないタスクを列挙する照合ジョブを実行します。結果の残り期間がまだ数日あるうちに警告してください。14日目は復旧のための余裕であり、コピーの予定日ではありません。アカウントやリクエストの設定で保存期間を短縮すると、outputsExpireAtが14日よりずっと早くなる場合があります。警告の基準にはこのフィールドを使い、固定値は使わないでください。アカウント設定の1つは、過去のタスクにも作用します。プロンプトテキストの保存期間を短縮すると、すでに完了したタスクの結果が、当初割り当てられたoutputsExpireAtより前に削除されます。アカウント設定を変更する前に残したいものをダウンロードし、照合ジョブではこうしたタスクがパイプラインの不具合ではなく、消失したものとして報告されることを想定してください。

再試行と大きな動画に対応するコールバック処理

アプリケーションでは、完了イベントを検証して永続化し、コピーをキューに入れてから応答します。イベントが繰り返されてもギャラリー項目が重複しないよう、コピーを冪等にしてください。コピーされていない結果は、受け入れ済みの期限に達する前に警告します。

この単体ワーカーは、既存タスクのファイルを非公開のローカルディレクトリに保存します。spicyapiをインストールし、SPICY_API_KEY、TASK_ID、必要に応じてARCHIVE_DIRを設定して実行してください。生成は行いません。そのディレクトリを永続ストレージ上に配置し、バックアップ計画に含めてください。一時的なサーバーディスクは長期保存用のアーカイブにはなりません。

python
import hashlib
import json
import os
from pathlib import Path
import tempfile
import urllib.request
from spicyapi import SpicyClient, output_assets

client = SpicyClient()
task_id = os.environ["TASK_ID"]
root = Path(os.environ.get("ARCHIVE_DIR", "media-archive"))
root.mkdir(parents=True, exist_ok=True, mode=0o700)
task = client.get_task(task_id)
if task["state"] != "succeeded":
    raise RuntimeError("Resume the existing task before archiving it.")
for index, asset in enumerate(output_assets(task)):
    if asset.get("pending") or asset.get("unavailable") or not asset.get("url"):
        raise RuntimeError("Asset is not ready; retain the task ID.")
    request = urllib.request.Request(asset["url"], headers={"User-Agent": "SpicyAPI-Blog-Example/1.0"})
    temporary = None
    try:
        with urllib.request.urlopen(request, timeout=300) as response:
            mime = response.headers.get_content_type()
            suffix = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp",
                      "video/mp4": ".mp4", "video/webm": ".webm", "audio/mpeg": ".mp3", "audio/wav": ".wav"}.get(mime)
            if response.status != 200 or suffix is None:
                raise RuntimeError("Unexpected response format; do not guess an extension.")
            digest, size = hashlib.sha256(), 0
            with tempfile.NamedTemporaryFile(dir=root, delete=False) as output:
                temporary = Path(output.name)
                while chunk := response.read(1024 * 1024):
                    output.write(chunk)
                    digest.update(chunk)
                    size += len(chunk)
        if not size or (asset.get("bytes") and size != int(asset["bytes"])):
            raise RuntimeError("Empty or incomplete transfer.")
        filename = root / (digest.hexdigest() + suffix)
        temporary.replace(filename)
        record = {"taskId": task_id, "index": index, "file": filename.name,
                  "mime": mime, "bytes": size, "sha256": digest.hexdigest()}
        (root / (digest.hexdigest() + ".json")).write_text(json.dumps(record, indent=2))
        print(filename)
    finally:
        if temporary is not None:
            temporary.unlink(missing_ok=True)

オブジェクトストレージから配信する場合は、非公開の一時オブジェクトへストリーミングし、転送がすべて完了してから公開してください。メディアとWebhookのドキュメントに、結果の更新と認証済みイベントの説明があります。届かなかったコールバックに備え、照合手段としてポーリングを残してください。

長期保存アセットは生成経路を隠す

コピーした後、製品が配信するオブジェクトには独自の識別情報があります。

text
asset_01k...                     database identity
generated/job_.../....mp4        private storage key
https://media.example.com/a/...  controlled delivery URL

バケットを非公開にし、自分のCDN、認証付きメディアエンドポイント、または自分で発行する短期間有効なリンクで配信してください。SpicyAPIの署名付きURLを公開URLとして再利用しないでください。これにより、一時リンクやストレージの詳細がHTML、ログ、エクスポートに入ることを防げます。また、タスク履歴を書き換えずにアクセスルールやドメインを変更でき、ユーザーのアセット削除を自分の記録に対する操作として扱えます。由来の情報は非公開で保持してください。元のタスクID、元のアセットキー、コンテンツタイプ、サイズ、コピー時刻、コピー先キーです。

障害と復旧方法

障害観測される状況正しい復旧方法
アップロード完了前にチケットが失効、または未確定ファイルをタスクで使用アップロードや確定処理が拒否される、またはタスク作成時に参照が拒否される新しいチケットを取得して再アップロードします。確定処理の混雑による409/429なら、同じ確定処理を再試行します
保存したspicy:// URIを翌日に使用タスク作成時に400、または再試行に失敗元のファイルを再アップロードします。原本は自分のストレージに保存します
コールバックが2回届く2つのワーカーが同じコピーを開始request_idで重複を排除し、キューとコピー先ではタスクIDとアセットインデックスの組み合わせでも排除します
コールバックエンドポイントが遅いダウンロード中に再送が届く永続化し、キューに入れ、メディアの転送前に2xxを返します
キュー待ち中に結果リンクが失効コピー元の取得が403を返すタスクを再取得して新しいurlを得て、ストリーミングを再試行します
アセットがpending: truekeyとurlが空数秒待って再度照会します
アセットがunavailable: true、またはdownload-urlが410を返す失効、削除、または保存失敗検証済みの非公開コピーを使います。なければユーザーに伝え、新規の有料タスクとして再生成します
コピー完了後にデータベースへの書き込みが失敗非公開オブジェクトはあるが、取得可能なアセットの行がない既存のコピー先を検証してメタデータの書き込みを再試行します。有効なコピーを削除したり再生成したりしてはいけません
データベースでは取得可能だが、オブジェクトがないページが表示できない、またはCDNが404を返す取得可能と記録する前に、データベースの行とバケットを照合します
署名付きURLがログに出る一時的な読み取り権限が露出クエリ文字列と応答本文を伏せます。リンクは約20分で失効しますが、取り消しはできません
大きな動画をメモリに蓄積ワーカーが再起動する、またはメモリ不足になる同時実行数を制限し、バックプレッシャーを使ってストリーミングします

本番公開前のチェックリスト

  • 期限前に復旧する: 各タスクの受け入れ済み期限を使って、コピー漏れを警告します。
  • 所有者を明確にする: 非公開アセットは所有者だけに配信し、APIキーをローテーションする前に結果を回収します。
  • すべてのシステムから削除する: バケット、配信キャッシュ、バックアップを削除方針に含めます。

よくある質問

output.assets[].urlで返されたURLを保存してもよいですか?

コピーそのもののためだけに使ってください。自分のアセット記録を保存し、データを転送する必要があるときに新しいリンクを取得します。

download-urlは引き続き必要ですか?

保存期間内にtaskIdとkeyを使ってリンクを更新するときだけ必要です。取得可能なアセットにはすでにurlとexpiresAtが含まれます。

リンクを更新するとメディアの保存期間も延びますか?

いいえ。新しいリンクが更新するのは読み取りアクセスだけです。オブジェクトは引き続きretention.outputsExpireAtで削除されます。

アップロードせず、公開画像URLをモデルに渡せますか?

はい。モデルのスキーマがそのフィールドでURLを許可し、アドレスがファイル自体を返し、タスク実行中もアクセスできる場合に使えます。ローカルファイル、大きな入力、一般公開されていない素材にはアップロード手順を使ってください。

来週も同じ参照画像が必要です。どうすればよいですか?

来週必要になったら、自分のコピーから再アップロードしてください。確定済み参照が使えるのは1日だけですが、先に削除されない限り、有効期間中は複数のタスクで使えます。

コールバックリクエスト内で結果をコピーすべきですか?

いいえ。リクエスト内で大きな転送を行うと、コールバックの15秒タイムアウトを超えて重複配信が発生します。キューに入れたワーカーなら、安全に再試行できます。

結果をまったく保存したくない場合は?

コピーせずに期限切れを待つか、保存期間を短縮するか、必要に応じてタスクの内容を破棄してください。設定方法はAIメディアのアップロード、保存、配信と保存期間のドキュメントで説明しています。内容を破棄しても、課金額は変わりません。

Agentに呼び出しを任せる

Agentを使って、既存の出力を再生成せずに回収しましょう。

  • CLI: npx @spicyapi/cli tasks get "$TASK_ID"でタスクを取得します。CLIガイドをご覧ください。
  • MCP: spicyapi_task_getに現在の出力を要求し、返されたファイルを保存します。MCPガイドをご覧ください。
  • Skill: npx skills add spicyapi-ai/spicy-skillをインストールし、$spicyapiにそのタスクの結果を選んだディレクトリへ保存するよう依頼します。Skillガイドをご覧ください。

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

実際に試してみる

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

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