Chạy GPT Image 2 theo lô mà không mất tác vụ
Gửi một loạt prompt GPT Image 2, lưu mã tác vụ, tiếp tục hỏi trạng thái sau khi gián đoạn và tải ảnh về mà không phải gửi lại yêu cầu.
Đọc dạng MarkdownTạo ảnh theo lô với GPT Image 2 trên SeedRouter nghĩa là gửi những yêu cầu ảnh thông thường và theo dõi từng mã tác vụ được trả về. Hướng dẫn này dùng một script nhỏ chạy phía client, không phải một Batch API riêng hay gói giảm giá theo lô. Mỗi prompt là một tác vụ độc lập; script lưu tiến độ để bạn kiểm tra lại các tác vụ đó sau khi bị gián đoạn.
Có hai việc tách biệt: tạo ảnh và tải ảnh về. Một lần tải hỏng không đòi hỏi tạo lại ảnh. Hết hạn vòng hỏi trạng thái không có nghĩa là tác vụ tạo ảnh đã thất bại. Giữ những phân biệt đó trong script sẽ tiết kiệm cả công sức lẫn sự rối rắm.
Cần chuẩn bị gì trước khi bắt đầu?
Bạn cần Node.js 22 trở lên, một biến môi trường SEEDROUTER_API_KEY phía máy chủ và đủ số dư tài khoản cho bộ thử nghiệm. Hãy bắt đầu với hai prompt. Xem biểu giá hiện tại trước khi tăng số lượng ảnh.
Hãy lưu đoạn mã sau thành images.mjs trong một thư mục làm việc riêng tư. Nó tạo image-tasks.json ở đó và tải các tệp PNG vào images/. Giữ lại tệp trạng thái: chính nó nối các nhãn cục bộ của bạn với những tác vụ đã được nhận. Đừng đưa khóa bí mật hay prompt chứa dữ liệu nhạy cảm của khách hàng lên kho mã công khai.
Các ví dụ yêu cầu một ảnh cho mỗi prompt. Hãy dùng tham số n khi bạn muốn nhiều kết quả cho cùng một prompt; đừng nhầm điều đó với việc gửi các prompt khác nhau. SeedRouter ghi rõ khoảng giá trị được chấp nhận và các ràng buộc khác trong tài liệu mô hình.
Gửi một lần rồi lưu mã tác vụ
Script ghi một mục unconfirmed trước khi gửi yêu cầu. Nếu tiến trình dừng trước khi kịp lưu phản hồi, lần chạy sau sẽ không lặng lẽ gửi lại mục đó. Hãy xem tác vụ trong tài khoản của bạn trước khi quyết định xử lý một mục chưa xác nhận.
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
const mode = process.argv[2];
if (!['submit', 'poll'].includes(mode)) {
throw new Error('Use: node images.mjs submit | poll');
}
const key = process.env.SEEDROUTER_API_KEY;
if (!key) throw new Error('Set SEEDROUTER_API_KEY on your server.');
const base = 'https://api.seedrouter.ai/v1';
const headers = { Authorization: `Bearer ${key}` };
const file = 'image-tasks.json';
let state;
try {
state = JSON.parse(await readFile(file, 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
state = {};
}
const save = async () => {
await writeFile(`${file}.tmp`, JSON.stringify(state, null, 2), { mode: 0o600 });
await rename(`${file}.tmp`, file);
};
const pause = () => new Promise((resolve) => setTimeout(resolve, 3000));
const jobs = [
['blue-mug', 'A cobalt-blue ceramic mug on a pale gray tabletop.'],
['green-bowl', 'A green ceramic bowl on a pale gray tabletop.'],
];
if (mode === 'submit') {
for (const [name, prompt] of jobs) {
if (state[name]) continue;
state[name] = { status: 'unconfirmed' };
await save();
try {
const response = await fetch(`${base}/images/generations`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-image-2', prompt,
size: '1024x1024', quality: 'low', output_format: 'png', n: 1,
}),
signal: AbortSignal.timeout(60000),
});
const task = await response.json();
if (typeof task.id === 'string' && task.id) {
state[name] = { id: task.id, status: 'processing' };
await save();
}
if (!response.ok || !state[name].id) {
throw new Error('Submission needs review.');
}
} catch {
console.error(`${name}: stopped; inspect the saved state before continuing.`);
process.exitCode = 1;
break;
}
}
} else {
await mkdir('images', { recursive: true });
const deadline = Date.now() + 600000;
do {
for (const [name, entry] of Object.entries(state)) {
if (!entry.id || entry.status === 'failed' || entry.downloaded) continue;
try {
const response = await fetch(`${base}/tasks/${encodeURIComponent(entry.id)}`, {
headers, signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error('Task check failed.');
const task = await response.json();
if (!['processing', 'completed', 'failed'].includes(task.status)) {
throw new Error('Unexpected task status.');
}
entry.status = task.status;
await save();
if (task.status !== 'completed') continue;
const images = task.output?.data;
if (!Array.isArray(images) || !images.length) {
throw new Error('Completed task has no image URLs.');
}
for (const [index, image] of images.entries()) {
const url = new URL(image.url);
if (url.protocol !== 'https:') throw new Error('Expected an HTTPS image URL.');
// The download is a separate request: never attach the API key.
const result = await fetch(url, { signal: AbortSignal.timeout(60000) });
if (!result.ok || !result.headers.get('content-type')?.startsWith('image/png')) {
throw new Error('PNG download failed.');
}
await writeFile(`images/${name}-${index}.png`, Buffer.from(await result.arrayBuffer()));
}
entry.downloaded = true;
await save();
} catch {
console.error(`${name}: check or download incomplete; task ID retained.`);
}
}
if (!Object.values(state).some((entry) => entry.id && entry.status !== 'failed' && !entry.downloaded)) break;
await pause();
} while (Date.now() < deadline);
}
console.log(JSON.stringify(state, null, 2));Chạy node images.mjs submit, rồi node images.mjs poll. Mỗi lúc chỉ chạy một bản sao của script và đừng đổi tên nhãn trong tệp trạng thái. Đây là một ví dụ cục bộ nhỏ, không phải hệ lưu trữ nhiều tiến trình. Với dịch vụ có nhiều bản chạy của ứng dụng, hãy dùng kho lưu trữ bền vững nắm quyền sở hữu riêng cho từng lần gửi thay vì chia sẻ chung tệp JSON này.
Nối lại vòng hỏi mà không tạo ảnh mới
Hãy chạy lại node images.mjs poll sau khi hết hạn cục bộ hoặc đứt mạng. Nó đọc các mã tác vụ đã lưu và gửi yêu cầu GET để kiểm tra. Nó không gửi thêm yêu cầu tạo ảnh nào. Một tác vụ đã xong nhưng chưa tải hết sẽ được kiểm tra lại, và các tệp PNG của nó được tải về với đúng những tên tệp xác định như trước.
Mốc mười phút là của riêng script ví dụ này. Đó không phải thời gian hoàn thành được cam kết, cũng không phải lệnh hủy phía máy chủ. Một yêu cầu mạng đang dở có thể kết thúc sau khi vòng lặp hết hạn. Tài liệu vòng đời tác vụ định nghĩa các trạng thái thật: processing, completed và failed.
Một mục unconfirmed cần được xem thủ công vì script không có mã tác vụ nào được lưu. Đừng xóa mục đó rồi gửi lại chỉ để xem lần thứ hai có chạy không. Trước hết hãy tra lịch sử tác vụ trong tài khoản của bạn. Nếu tìm lại được mã, hãy điền mã đó vào mục đã lưu rồi dùng vòng hỏi trạng thái; nếu không, hãy làm rõ lần gửi chưa chắc chắn trước khi quyết định tạo lại.
Nên chạy song song bao nhiêu?
Hãy bắt đầu bằng cách gửi tuần tự với một bộ thử nhỏ, như ví dụ này làm. Mỗi tác vụ đã được nhận vẫn chạy tiếp trong lúc script gửi tác vụ kế, nhưng các yêu cầu không bắn ra ồ ạt không giới hạn. Đoạn mã không ngụ ý một con số song song an toàn cho mọi trường hợp.
Với ứng dụng lớn hơn, hãy biến số tác vụ chưa xong tối đa thành một thiết lập tường minh. Khi chạm ngưỡng đó thì ngừng nhận việc mới và tiếp tục kiểm tra những tác vụ đã nhận. Hãy xử lý giới hạn tài khoản theo hướng dẫn về lỗi đã ghi trong tài liệu, chứ đừng lặp lại lệnh POST một cách mù quáng.
Hãy ghi riêng những kết quả cần sửa về mặt sáng tạo. Một tấm ảnh đã xong mà bạn không ưng không phải là tác vụ API thất bại. Đó là một quyết định biên tập mới và, nếu bạn gửi thêm yêu cầu, là một lần tạo ảnh mới cần tính vào ngân sách.
Kiểm tra cả bộ trước khi coi là đã xong
Hãy đối chiếu các nhãn đầu vào với tệp trạng thái và các tệp đã tải. Mỗi nhãn đều phải có lời giải thích: đã tải, đang chạy, thất bại hoặc chờ xem lại lần gửi. Chỉ đếm số tệp có thể che mất một tác vụ chưa xong hoặc một lần tải đứt giữa chừng.
Hãy mở cả các tấm ảnh ra xem. Script kiểm tra mã trạng thái HTTP và kiểu nội dung PNG, nhưng nó không quyết định được chiếc ly có đúng màu đã yêu cầu hay nhãn có đọc được hay không. Hãy đánh giá yêu cầu hình ảnh tách khỏi chuyện truyền dữ liệu thành công.
Nếu đổi output_format, hãy cập nhật cùng lúc kiểu nội dung mong đợi và phần mở rộng tệp. Đổi tên một tệp JPEG thành .png không chuyển đổi được nó. Ví dụ cố tình cố định PNG để vấn đề xử lý định dạng không che lấp chủ đề khôi phục tác vụ.
Câu hỏi thường gặp
Đây có phải Batch API của OpenAI không?
Không. Script gửi các tác vụ tạo ảnh thông thường của SeedRouter. Nó không dùng endpoint Batch API và cũng không ngụ ý có mức giá riêng cho lô.
Tôi có thể chỉ tải lại phần còn thiếu không?
Được. Hãy chạy lệnh hỏi trạng thái với tệp trạng thái đã lưu. Nó lấy lại kết quả của các tác vụ đã xong và tải những tệp còn thiếu mà không tạo thêm tác vụ ảnh nào.
Một tác vụ hỏng có chặn hết các tác vụ khác không?
Vòng hỏi trạng thái ghi tác vụ đó là thất bại rồi tiếp tục kiểm tra những tác vụ còn lại. Hãy xem riêng mục bị hỏng. Tác vụ thất bại không bị tính phí; riêng việc hết thời gian chờ ở máy bạn thì chưa chứng minh được là hỏng.
Giữ bản ghi tác vụ cùng với ảnh
Tạo ảnh theo lô đáng tin cậy với GPT Image 2 phụ thuộc vào việc giữ đúng liên kết giữa mỗi prompt và mã tác vụ của nó. Hãy bảo toàn bản ghi đó, nối lại các tác vụ đang có và thử tải lại tách khỏi việc tạo ảnh. Khi một bộ nhỏ đã chạy đúng như mong đợi, hãy tăng khối lượng trong một ngân sách mà bạn theo dõi được nhờ hướng dẫn về giá.



