XuanShu API
Генерация и редактирование изображений
Используйте gpt-image-2 для text-to-image, image-to-image и редактирования изображений: рабочие потоковые примеры на Python и Node.js плюс устранение неполадок.
1. Получите API Key
- Если у вас ещё нет аккаунта, зарегистрируйтесь на /register.
- Создайте key на /keys и замените <YOUR_API_KEY> в примерах на полное значение.
- Проверьте на /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")
breakimport { writeFile } from "node:fs/promises";
const API_URL = "https://www.xuanshuapi.com/v1/images/generations";
const API_KEY = "<YOUR_API_KEY>";
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 response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: "Bearer " + API_KEY,
"Content-Type": "application/json",
Accept: "text/event-stream",
},
body: JSON.stringify({
model: "gpt-image-2",
prompt: "一只在太空里漂浮的猫,科技感插画风格",
n: 1,
size: "1024x1024",
stream: true,
response_format: "b64_json",
}),
});
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("generated-image.png", Buffer.from(image, "base64"));
console.log("\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")
break4. Редактирование изображения (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. | Проверьте все три варианта, как в примерах; запишите файл и выйдите из цикла при первом найденном. |