> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apimart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ベストプラクティス

> Midjourney 接続のポーリングパターン、Prompt 設計、参照画像、エラーリトライ戦略、並行処理とトラブルシューティングの推奨事項

よくある問題、パフォーマンス最適化、エラー処理のベストプラクティスをまとめています。**接続前に一読することを推奨します。**

## タスク送信とポーリング

送信系のエンドポイントはすべて非同期タスクです。送信後に `task_id` が返り、その後 `GET /v1/midjourney/{task_id}` を定期的に照会してステータスを取得し、`SUCCESS` / `FAILURE` になるまで繰り返します。

```python theme={null}
import time, httpx

def wait_task(task_id, timeout=300):
    deadline = time.time() + timeout
    while time.time() < deadline:
        resp = httpx.get(f"{HOST}/v1/midjourney/{task_id}",
                         headers={"Authorization": f"Bearer {API_KEY}"}).json()
        if resp["status"] in ("SUCCESS", "FAILURE"):
            return resp
        if resp["status"] == "MODAL":
            raise RuntimeError(f"task {task_id} は /modal でパラメータ補完が必要")
        time.sleep(3)
    raise TimeoutError(task_id)
```

* **ポーリング間隔**：3〜5 秒に 1 回を推奨。これより高頻度にしても意味がなく、クォータの無駄になります。
* **web リクエスト内で同期的にブロックして**タスク完了を待たないでください。送信後はすぐに `task_id` を返し、フロントエンドで非同期にポーリングさせます。

## Prompt 設計

**良い prompt：**

```text theme={null}
a serene mountain lake at sunrise, photorealistic, soft golden light,
mist rising from water, snow-capped peaks in distance --ar 16:9 --v 8.1 --s 100
```

* **主体を先頭に**：まず主体、次にシーンの描写、最後に修飾語。
* **構造化パラメータを明示**：`--ar` / `--v` / `--s`（または対応する body フィールド）を使う方が、デフォルト値に依存するよりも制御しやすくなります。
* **曖昧な語を避ける**：`photorealistic` は `realistic` よりも明確です。

**避けるべきこと：** 抽象的すぎる指定（"make it good"）、主体が散漫（複数の並列オブジェクトに主従の区別がない）、語に引用符を付ける（リテラル値として扱われます）。

**Niji アニメ：** `niji: true` + `version: "7"` を渡すと、プラットフォームが `--niji 7` に正規化し、課金は `midjourney@imagine-niji7` になります。

## 参照画像のベストプラクティス

| ソース             | 推奨される方法                             | 注意                                      |
| --------------- | ----------------------------------- | --------------------------------------- |
| ユーザーアップロード      | まず自分の OSS / CDN に保存し、送信時にその URL を渡す | base64 を直接渡さない（帯域の無駄）                   |
| 公開 URL          | そのまま渡す                              | SSRF（公開ネットワークから到達可能であること）と 12 MiB 制限に注意 |
| サードパーティ / 他の生成物 | まず自分の OSS に保存する                     | サードパーティの URL は期限切れになる可能性がある             |

* **5 MiB 未満に圧縮**：プラットフォームの上限は 12 MiB ですが、小さい画像の方が転送・処理ともに高速です。
* 形式は PNG / JPG / WebP のいずれも可。高品質な JPG を推奨します。
* 解像度は 1024〜2048 px で十分。それ以上は無駄になります。
* 参照画像のウェイト `iw`（0〜3、既定 1）：>1 で元画像に近づき、\<1 でより自由になります。

## エラー処理とリトライ戦略

| code                  | 意味                              | リトライ戦略                                         |
| --------------------- | ------------------------------- | ---------------------------------------------- |
| `1` / `200`           | 成功                              | ✅                                              |
| `4` VALIDATION\_ERROR | パラメータ誤り                         | ❌ リトライせず、パラメータを修正                              |
| `3` NOT\_FOUND        | 利用可能なインスタンスなし / task\_id が存在しない | インスタンス利用不可なら後ほどリトライ可。task\_id が存在しない場合はリトライしない |
| `9` FAILURE           | サービスによる拒否 / 内部エラー               | ⏳ リトライ可、指数バックオフ（1s, 4s, 16s）                   |
| `21` MODAL            | 非終了状態                           | ✅ 引き続き `/modal` を呼ぶ                            |
| `24` BANNED\_PROMPT   | 禁止ワード                           | ❌ リトライせず prompt を変更。**自動返金済み**                 |
| `429`                 | レート制限                           | ⏳ 指数バックオフ + jitter                             |
| `5xx` / ネットワークエラー     | サーバ / ネットワーク                    | ⏳ 指数バックオフ。ネットワークエラーは即時に 1 回リトライ可               |

```python theme={null}
import time, random, httpx

def submit_with_retry(payload, max_attempts=5):
    for attempt in range(max_attempts):
        try:
            r = httpx.post(f"{HOST}/v1/midjourney/generations/imagine",
                           json=payload,
                           headers={"Authorization": f"Bearer {API_KEY}"},
                           timeout=30)
            data = r.json()
            if r.status_code == 200 and data["code"] in (1, 200):
                return data
            if data["code"] in (4, 24):
                raise ValueError(data["description"])      # リトライ不可
            if data["code"] == 3 and "task" in data["description"]:
                raise ValueError(data["description"])      # task_id が存在しない
            # その他（9 / 429 / 5xx）はリトライ可
        except httpx.RequestError:
            pass
        time.sleep((4 ** attempt) + random.uniform(0, 1))  # 1s / 4s / 16s ...
    raise RuntimeError(f"最大リトライ回数に到達 {max_attempts}")
```

## 二次操作のフロー

```python theme={null}
# imagine → ポーリング → upscale
imagine_id = submit({"prompt": "a cat"})["data"][0]["task_id"]
result = wait_task(imagine_id)           # grid_image_url + 4 枚の image_urls + buttons
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 2})["data"][0]["task_id"]
final = wait_task(upscale_id)            # upscale はローカル合成、1〜2s
single_image = final["image_urls"][0]
```

局部リペイント（inpaint → modal の 2 ステップ）：

```python theme={null}
imagine_id = submit({"prompt": "a portrait"})["data"][0]["task_id"]; wait_task(imagine_id)
upscale_id = submit_to("/upscale", {"task_id": imagine_id, "index": 1})["data"][0]["task_id"]; wait_task(upscale_id)

inpaint_id = submit_to("/inpaint", {"task_id": upscale_id})["data"][0]["task_id"]  # status=modal
# フロントエンドで mask を描き（透明=リペイント領域）、自分の OSS にアップロードして mask_url を取得
final = submit_to("/modal", {
    "task_id": inpaint_id,
    "prompt": "replace the eyes with cybernetic blue eyes",
    "mask_url": "https://your-oss.com/mask.png"
})
wait_task(final["data"][0]["task_id"])
```

> ⚠️ inpaint が MODAL に入った後、**30 分以内**に `/modal` を呼ぶ必要があります。そうしないとバックエンドが自動的に CANCEL + 返金します。

## video の課金コントロール

* 単一クリップ：`batch_size: 1` → 1 × `midjourney@video` を消費
* バッチ 4 クリップ：`batch_size: 4` → 4 × `midjourney@video` を消費
* 高解像度の単一クリップ：`video_type: "vid_1.1_i2v_720"` + `batch_size: 1` → 1 × `midjourney@video-720p` を消費

**推奨**：完成品として 1 クリップだけ必要なら `batch_size=1` を使い、バッチで比較検討する場合のみ 4 を使ってください。デフォルトで 4 を有効にしないこと（コストが N 倍になります）。

## 並行処理とスループット

```python theme={null}
import asyncio
sem = asyncio.Semaphore(10)  # クライアント側の同時送信は最大 10 件

async def submit_one(prompt):
    async with sem:
        return await submit({"prompt": prompt})
```

* プラットフォームには 1 分あたりの送信数に上限があり、超過すると `429` が返るためバックオフしてリトライしてください。
* 実際の生成の並行数はシステム容量によって決まり、超過分はキューに入ります。タスクが長時間 `SUBMITTED` のままの場合、通常はキュー待ちです。
* ポーリングには必ず `sleep` を入れ、sleep なしの無限ループにしないでください。

## モニタリングの推奨

| 指標                   | 参考しきい値 | 意味                                 |
| -------------------- | ------ | ---------------------------------- |
| タスク SUCCESS 率（直近 1h） | > 95%  | 低い場合はサービス / ネットワークの異常を示す           |
| 平均完了時間               | \< 90s | 高い場合はキュー待ちを示す                      |
| MODAL 滞留タスク数         | 0 に近い  | 多い場合はクライアントが `/modal` を呼んでいないことを示す |
| `code=24` の比率        | \< 5%  | 高い場合は prompt が頻繁に禁止ワードに触れていることを示す  |

## トラブルシューティングチェックリスト

| 現象                     | 確認すべき方向                                           |
| ---------------------- | ------------------------------------------------- |
| タスクが長時間 `SUBMITTED`    | システムでキュー待ち中。後ほど再照会                                |
| タスクが長時間 `NOT_START`    | プラットフォームが後ほど自動でタイムアウト返金する。手動対応は不要                 |
| タスクが `MODAL` で 30 分超過  | クライアントが `/modal` を呼んでいない。すでに自動 CANCEL + 返金済み      |
| `prompt` フィールドが空       | describe タスクのテキスト結果は `description` フィールドにある       |
| `image_urls` が 1 枚足りない | 内容審査で一部の画像がブロックされた。`fail_reason` を確認              |
| 課金が想定を超える              | `quota` フィールドを確認。video は `batch_size` を掛けることを忘れずに |
