XuanShu API

Генерация и редактирование изображений

Используйте gpt-image-2 для text-to-image, image-to-image и редактирования изображений: рабочие потоковые примеры на Python и Node.js плюс устранение неполадок.

1. Получите API Key

  1. Если у вас ещё нет аккаунта, зарегистрируйтесь на /register.
  2. Создайте key на /keys и замените <YOUR_API_KEY> в примерах на полное значение.
  3. Проверьте на /available-channels, что gpt-image-2 виден для текущего key.

2. Text-to-image

Передайте только текстовый запрос, без референсного изображения. Ответ идёт потоком по SSE: события partial_image показывают прогресс, а в финальном событии изображение приходит как b64_json.

import base64
import json
import urllib.request
from pathlib import Path

API_URL = "https://www.xuanshuapi.com/v1/images/generations"
API_KEY = "<YOUR_API_KEY>"


def iter_sse(response):
    buffer = ""
    while chunk := response.read(4096):
        buffer += chunk.decode("utf-8", errors="replace")
        frames = buffer.split("\n\n")
        buffer = frames.pop()
        for frame in frames:
            payload = [
                line[5:].strip()
                for line in frame.splitlines()
                if line.startswith("data:")
            ]
            data = "\n".join(payload).strip()
            if data and data != "[DONE]":
                yield data


body = {
    "model": "gpt-image-2",
    "prompt": "一只在太空里漂浮的猫,科技感插画风格",
    "n": 1,
    "size": "1024x1024",
    "stream": True,
    "response_format": "b64_json",
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(body).encode("utf-8"),
    method="POST",
    headers={
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
)

with urllib.request.urlopen(request, timeout=900) as response:
    for data in iter_sse(response):
        event = json.loads(data)
        if event.get("type") == "image_generation.partial_image":
            print(".", end="", flush=True)
        image = (
            event.get("b64_json")
            or ((event.get("data") or [{}])[0]).get("b64_json")
            or (event.get("item") or {}).get("result")
        )
        if image:
            Path("generated-image.png").write_bytes(base64.b64decode(image))
            print("\n已保存:generated-image.png")
            break

3. Image-to-image (референс через JSON)

Референсное изображение передаётся через images[].image_url; поддерживается несколько изображений, а также data URL. images[].file_id не поддерживается и вызывает ошибку.

import base64
import json
import urllib.request
from pathlib import Path

API_URL = "https://www.xuanshuapi.com/v1/images/edits"
API_KEY = "<YOUR_API_KEY>"


def iter_sse(response):
    buffer = ""
    while chunk := response.read(4096):
        buffer += chunk.decode("utf-8", errors="replace")
        frames = buffer.split("\n\n")
        buffer = frames.pop()
        for frame in frames:
            payload = [
                line[5:].strip()
                for line in frame.splitlines()
                if line.startswith("data:")
            ]
            data = "\n".join(payload).strip()
            if data and data != "[DONE]":
                yield data


# 参考图用 images[].image_url 传入,支持多张,也可传 data URL。
# 注意:images[].file_id 不受支持。
body = {
    "model": "gpt-image-2",
    "prompt": "参考这张图,生成一张更精致的科技风品牌图。",
    "images": [{"image_url": "https://www.xuanshuapi.com/brand/og-cover.png"}],
    "n": 1,
    "size": "1024x1024",
    "quality": "auto",
    "stream": True,
    "response_format": "b64_json",
}

request = urllib.request.Request(
    API_URL,
    data=json.dumps(body).encode("utf-8"),
    method="POST",
    headers={
        "Authorization": "Bearer " + API_KEY,
        "Content-Type": "application/json",
        "Accept": "text/event-stream",
    },
)

with urllib.request.urlopen(request, timeout=900) as response:
    for data in iter_sse(response):
        event = json.loads(data)
        if event.get("type") == "image_generation.partial_image":
            print(".", end="", flush=True)
        image = (
            event.get("b64_json")
            or ((event.get("data") or [{}])[0]).get("b64_json")
            or (event.get("item") or {}).get("result")
        )
        if image:
            Path("generated-image.png").write_bytes(base64.b64decode(image))
            print("\n已保存:generated-image.png")
            break

4. Редактирование изображения (multipart)

Загрузите файл изображения напрямую и измените его по текстовому запросу. Этот путь использует multipart/form-data; не задавайте Content-Type вручную — FormData сама генерирует boundary.

import { writeFile } from "node:fs/promises";

const API_URL = "https://www.xuanshuapi.com/v1/images/edits";
const API_KEY = "<YOUR_API_KEY>";
const SOURCE_URL = "https://www.xuanshuapi.com/brand/og-cover.png";

async function* readSse(response) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    const frames = buffer.split(/\r?\n\r?\n/);
    buffer = frames.pop() || "";
    for (const frame of frames) {
      const data = frame
        .split(/\r?\n/)
        .filter((line) => line.startsWith("data:"))
        .map((line) => line.slice(5).trim())
        .join("\n");
      if (data && data !== "[DONE]") yield data;
    }
  }
}

const source = await fetch(SOURCE_URL);
const sourceType = source.headers.get("content-type") || "image/png";
const sourceFile = new File(
  [await source.arrayBuffer()], "source.png", { type: sourceType });

const form = new FormData();
form.append("image", sourceFile);
form.append("prompt", "把图片整体色调改为蓝色。");
form.append("model", "gpt-image-2");
form.append("n", "1");
form.append("quality", "auto");
form.append("size", "1024x1024");
form.append("stream", "true");
form.append("response_format", "b64_json");

const response = await fetch(API_URL, {
  method: "POST",
  headers: { Authorization: "Bearer " + API_KEY, Accept: "text/event-stream" },
  body: form,
});
if (!response.ok) throw new Error(await response.text());

for await (const data of readSse(response)) {
  const event = JSON.parse(data);
  if (event.type === "image_generation.partial_image") process.stdout.write(".");
  const image = event.b64_json ?? event.data?.[0]?.b64_json ?? event.item?.result;
  if (image) {
    await writeFile("edited-image.png", Buffer.from(image, "base64"));
    console.log("\n已保存:edited-image.png");
    break;
  }
}

5. Критерии успеха

Команда завершается без ошибок и создаёт generated-image.png или edited-image.png в текущей директории, файл открывается как обычное изображение; в /usage появляется успешный вызов gpt-image-2 без 404, 401/403 или 429.

6. Устранение неполадок

СимптомПроверкаРешение
404Эндпоинт text-to-image — /v1/images/generations; image-to-image и редактирование используют /v1/images/edits. Оба включают /v1.Исправьте URL и повторите запрос.
400 нет референсного изображенияПри вызове edits через JSON поле images[].image_url обязательно.Добавьте массив images или перейдите на multipart-загрузку файла изображения.
Ошибка file_idНи images[].file_id, ни mask.file_id не поддерживаются.Используйте image_url или data URL.
Модель недоступнаПроверьте на /available-channels, что gpt-image-2 виден для текущего key.Выберите другую модель изображений, видимую в консоли.
429Проверьте баланс, лимиты key и конкурентность на /usage.Снизьте конкурентность и подождите восстановления лимита.
Изображение не найдено в ответеВ потоковом ответе итоговое изображение может быть в b64_json, data[0].b64_json или item.result.Проверьте все три варианта, как в примерах; запишите файл и выйдите из цикла при первом найденном.