> ## 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–5s 一次，更高频无意义且浪费配额。
* **不要在 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 两步）：

```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})
```

* 平台对每分钟提交数有上限，超出返回 `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` 少一张   | 内容审核拦了部分图，看 `fail_reason`            |
| 计费超预期              | 看 `quota` 字段；video 记得 × `batch_size` |
