AIビデオAPIの呼び出しはデモではシンプルに見えます:
- プロンプトを送信する。
- レスポンスを待つ。
- ビデオを表示する。
本番環境では、モデルが元のHTTPリクエストでビデオを返すことはほとんどありません。リモートジョブを作成し、外部タスクIDを渡し、プロバイダー固有の状態を遷移させ、最終的にメディアURLまたはエラーを返します。アプリケーションは、ユーザーの状態、課金の状態、モデレーションの状態、プロバイダーの状態をその間一貫したものに保つ必要があります。
この記事では、MICTのようなAI画像・ショートビデオワークスペース向けのプロバイダー中立のリファレンスアーキテクチャを紹介します。コードサンプルはMICTの本番実装の逐語的な説明ではなく、例示です。有用な部分は特定のAPI呼び出しではなく、その呼び出しを囲む境界のセットです。
実際のワークフローはステートマシンである
信頼性の高い生成リクエストには、「読み込み中」と「完了」以上のステージがあります:
request
-> authenticate
-> validate model settings
-> moderate input
-> calculate cost
-> create and persist local task
-> reserve or deduct credits
-> create provider task
-> attach external task ID
-> poll provider
-> normalize provider state
-> moderate output
-> complete or fail
-> refund when required
Enter fullscreen mode Exit fullscreen mode
これを明示的なステートマシンとして扱うことで、すべてのレイヤーが共有の語彙を持つことができます。通常、コンパクトな内部セットで十分です:
type GenerationStatus =
| "running"
| "processing"
| "completed"
| "failed";
Enter fullscreen mode Exit fullscreen mode
プロバイダーはエッジで独自の語彙を保持できます:
function mapProviderState(state: string): GenerationStatus {
switch (state) {
case "waiting":
case "queuing":
return "running";
case "generating":
return "processing";
case "success":
return "completed";
case "fail":
return "failed";
default:
return "running";
}
}
Enter fullscreen mode Exit fullscreen mode
重要な設計上の選択は、製品の残りの部分がすべてのプロバイダーの表記法で分岐しないことです。アダプターが一度だけそれを行います。
ローカルと外部のタスクIDを分離する
プロバイダーを呼び出す前に、独自のタスクを作成して永続化します。
const taskId = createLocalTaskId();
await persistGeneration({
taskId,
userId,
status: "running",
});
const externalTask = await provider.createTask(providerModel, input);
await attachExternalTaskId({
taskId,
externalTaskId: externalTask.id,
});
Enter fullscreen mode Exit fullscreen mode
ローカルIDは製品に属します。以下に使用します:
- 認可チェック
- 生成履歴
- クレジット取引
- サポート参照
- 分析
- 冪等性
外部IDはプロバイダーに属します。リモートジョブのクエリやキャンセル時にのみ使用します。
この分離は、プロバイダーが変更された場合、一時的にプロバイダーが置き換えられた場合、または外部タスクIDを受け取らなかったローカルジョブをサポートチケットが参照する場合に重要です。
金銭が動く前に設定を検証する
異なるビデオモデルは、モード、継続時間、解像度、アスペクト比、出力数、オーディオ設定の異なる組み合わせを受け入れます。クライアントにこれらの組み合わせを発明させないでください。
機能をサーバーサイドのモデル設定に保持します:
type ModelConfig = {
modes: Array<"t2v" | "i2v">;
durations: number[];
resolutions: string[];
aspectRatios: string[];
calculateCredits(input: {
duration: number;
resolution: string;
outputCount: number;
}): number;
};
Enter fullscreen mode Exit fullscreen mode
次に、モデレーション、課金、プロバイダーへの送信前にサポートされていない値を拒否します:
if (!config.modes.includes(mode)) {
throw new RequestError("Unsupported generation mode");
}
if (!config.resolutions.includes(resolution)) {
throw new RequestError("Unsupported resolution");
}
Enter fullscreen mode Exit fullscreen mode
クライアントサイドの検証はフィードバックに役立ちます。サーバーサイドの検証は、請求書とデータを保護する境界です。
生成前にモデレーションを行う
入力モデレーションは、プロバイダータスクが作成される前、クレジットが課金される前に実行する必要があります。
const moderation = await moderateRequest({
userId,
model,
mode,
prompt,
imageUrl,
});
if (!moderation.allowed) {
return {
ok: false,
code: moderation.code,
creditsCharged: false,
};
}
Enter fullscreen mode Exit fullscreen mode
これにより、独自のポリシーが拒否するリクエストに対して上流プロバイダーに支払うことを避けられます。また、UIがポリシー決定と技術的失敗を区別できるようになります。
出力モデレーションは別のゲートです。プロバイダーはアプリケーションがリリースすべきでないメディアを正常に生成する可能性があります。出力は、そのゲートを通過するまでcompletedになってはなりません。
provider success
-> extract result URL
-> moderate result
-> completed
provider success
-> extract result URL
-> output blocked
-> failed with a policy-safe message
Enter fullscreen mode Exit fullscreen mode
入力モデレーション、プロバイダーのポリシーエラー、出力モデレーションを1つの汎用的な「生成失敗」トーストにまとめないでください。それらは異なる再試行ルールとサポートパスを持っています。
クレジット操作を冪等にする
AI生成製品で最も被害の大きいバグは、しばしば財務状態のバグです:
- プロバイダーがリクエストを拒否したが、クレジットが差し引かれたままだった
- ポーリングが2回実行され、2回返金された
- プロバイダータスク開始後にデータベース書き込みが失敗した
- ネットワークタイムアウト後にブラウザがリクエストを再試行した
すべての課金には安定したソースキーを持つ必要があります:
await decreaseCredits({
userId,
amount: creditCost,
sourceType: "generation_charge",
sourceId: taskId,
});
Enter fullscreen mode Exit fullscreen mode
返金は同じソースを参照する必要があります:
await refundCreditsBySource({
sourceType: "generation_charge",
sourceId: taskId,
transactionType: "refund",
});
Enter fullscreen mode Exit fullscreen mode
データベースは関連するソースまたはトランザクションキーの一意性を強制する必要があります。アプリケーションレベルのif (!refunded)チェックは役立ちますが、同時ポーリングの下では十分ではありません。
少なくとも2つの返金パスが必要です:
- プロバイダータスクを作成できなかった。
- プロバイダーがタスクを受け入れたが、後で終端障害に達した。
try {
const external = await provider.createTask(modelId, input);
await attachExternalTaskId({ taskId, externalTaskId: external.id });
} catch (error) {
await refundByGenerationSource(taskId);
throw error;
}
Enter fullscreen mode Exit fullscreen mode
ポーリング中の終端障害の場合:
if (status === "failed") {
await refundByGenerationSource(taskId);
await markGenerationFailed(taskId, publicErrorMessage);
}
Enter fullscreen mode Exit fullscreen mode
返金が冪等であれば、繰り返しのポーリングははるかに恐ろしいものではなくなります。
ポーリングは退屈であるべき
WebSocketは知覚される応答性を向上させることができますが、数秒または数分かかるプロバイダージョブの場合、ポーリングが最も堅牢な最初の選択肢であることが多いです。
ステータスエンドポイントは以下を行う必要があります:
- リクエストを認証する
- ローカル生成を読み込む
- ユーザーに属することを検証する
- ローカルの終端状態に対して即座に返す
- 非終端ジョブに対してのみプロバイダーをクエリする
- 結果を正規化する
- ローカル状態を更新する
- 小さく安定したレスポンスを返す
type StatusResponse = {
status: GenerationStatus;
progress?: number;
resultUrl?: string;
errorCode?: string;
errorMessage?: string;
};
Enter fullscreen mode Exit fullscreen mode
クライアントはプロバイダーの生のペイロードを必要としません。生のペイロードは実装の詳細を漏らし、フロントエンドの動作を不安定なサードパーティスキーマに依存させます。
合理的なブラウザループは次のようになります:
async function waitForGeneration(taskId: string) {
while (true) {
const result = await getStatus(taskId);
if (result.status === "completed") return result;
if (result.status === "failed") throw new Error(result.errorMessage);
await delay(5000);
}
}
Enter fullscreen mode Exit fullscreen mode
本番コードは、コンポーネントのアンマウント時にポーリングを停止し、非表示のタブで一時停止または減速し、クライアントサイドの最大待機時間を適用する必要があります。ブラウザが質問を停止しても、ジョブはサーバー上で継続できます。
プロバイダーの結果を防御的に解析する
1つのプロバイダーでも、モデル間で異なる結果形状を返す場合があります:
{ "resultUrls": ["https://..."] }
Enter fullscreen mode Exit fullscreen mode
{ "video_url": "https://..." }
Enter fullscreen mode Exit fullscreen mode
{ "output": [{ "url": "https://..." }] }
Enter fullscreen mode Exit fullscreen mode
結果抽出をアダプター内に保持し、空でない文字列のみを受け入れます:
function extractResultUrl(raw: string): string | undefined {
const result = JSON.parse(raw);
const candidates = [
result.resultUrls?.[0],
result.urls?.[0],
result.output?.[0]?.url,
result.videoUrl,
result.video_url,
];
return candidates.find(
(value): value is string =>
typeof value === "string" && value.length > 0
);
}
Enter fullscreen mode Exit fullscreen mode
使用可能な結果URLなしにsuccessと返すプロバイダーは、完了したユーザー体験ではありません。ジョブを非終端のままにするか、明確に診断された失敗パスに移動します。
遅れた成功が失敗を上書きしないようにする
非同期システムは厄介な順序を生む可能性があります:
- ジョブが成功したように見える
- 出力モデレーションが開始する
- 別のプロセスが生成を失敗とマークする
- 遅れた更新が完了とマークしようとする
条件付き更新を使用します:
update generations
set status = 'completed', result_url = $1
where task_id = $2
and status <> 'failed';
Enter fullscreen mode Exit fullscreen mode
更新が0行に影響する場合、現在の状態を再読み込みしてその状態を返します。終端の安全決定は、遅れた成功イベントよりも優先されるべきです。
エラーカテゴリを保持する
ユーザーは異なるエラーに対して異なるアクションを必要とします:
| エラーカテゴリ | ユーザーのアクション |
|---|---|
| クレジット不足 | クレジットを追加するか、より安価な設定を選択する |
| プロバイダー利用不可 | 後で再試行する |
| 入力ポリシーでブロック | リクエストを修正する |
| プロバイダーポリシーでブロック | リクエストを修正する |
| 出力ポリシーでブロック | 決定が間違っていると思われる場合のみサポートに連絡する |
| 不明な技術的失敗 | 再試行するか、タスクIDを添えてサポートに連絡する |
安定したエラーコードを返し、ユーザー向けの安全なメッセージを別途管理します:
throw new RequestError(
"We could not start this generation right now.",
"provider_unavailable",
{ creditsCharged: false }
);
Enter fullscreen mode Exit fullscreen mode
上流のアカウント残高、内部モデルルート、または生のポリシーメッセージをブラウザに公開しないでください。
テストする内容
ハッピーパスのビデオだけでは十分ではありません。状態遷移をテストします:
- 無効なモデル設定はクレジットを課金しない
- 入力モデレーションの拒否はクレジットを課金しない
- プロバイダー作成の失敗は1回返金する
- プロバイダーの終端障害は1回返金する
- 繰り返しのステータスポーリングは返金を重複させない
- 1人のユーザーが別のユーザーのタスクを読み取れない
- URLなしのプロバイダー成功は完了にならない
- 出力モデレーションの失敗は遅れた成功によって上書きできない
- 終端のローカル状態はプロバイダーにクエリし続けない
- 不明なプロバイダー状態は回復可能に保つ
最良のテストは不変条件を対象とします:
one accepted generation <= one charge
one failed charged generation <= one refund
completed => usable result URL
failed-by-policy => result URL is not released
task owner => only user allowed to read status
Enter fullscreen mode Exit fullscreen mode
より大きな教訓
AIビデオインターフェースは、クリエイティブなUIが接続された分散システムです。モデル呼び出しは1つのステップに過ぎません。製品は、検証、認可、課金、ポーリング、モデレーション、障害回復といった他のすべてのステップに明示的な境界があるときに信頼できるものになります。
小さな内部ステートマシンから始めます。プロバイダーの語彙をエッジに保持します。すべての課金に安定したソースを与えます。返金を冪等にします。モデレーションをライフサイクルの一部として扱い、事前チェックとして扱わないようにします。そして、ポーリングを意図的に退屈なものにします。
その基盤は生成デモほど魅力的ではありませんが、デモを製品にできるものです。
免責事項:この記事はMICTのために作成され、このアーキテクチャの製品コンテキストとして1回参照されています。AIツールがドラフトと編集を支援しました。コード、主張、最終テキストは公開前にレビューされました。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.