작업을 놓치지 않고 GPT Image 2 요청을 배치로 처리하기
여러 GPT Image 2 프롬프트를 제출하고, 작업 ID를 저장하고, 중단 후에도 폴링을 재개하여 재제출 없이 완료된 이미지를 내려받습니다.
Markdown으로 읽기SeedRouter에서의 GPT Image 2 배치 생성이란, 일반적인 이미지 요청을 제출하고 반환되는 작업 ID를 각각 추적하는 것을 뜻합니다. 이 가이드는 별도의 Batch API나 배치 할인 상품이 아니라 작은 클라이언트 측 스크립트를 사용합니다. 프롬프트마다 별개의 작업이며, 스크립트가 진행 상황을 저장하므로 중단 후에도 그 작업들을 다시 확인할 수 있습니다.
여기에는 서로 다른 두 가지 일이 있습니다. 이미지를 생성하는 일과 내려받는 일입니다. 다운로드 실패가 새로운 생성을 요구하지는 않습니다. 폴링 기한에 도달했다고 해서 이미지 작업이 실패한 것도 아닙니다. 이 구분을 스크립트에 남겨 두면 수고와 혼란을 모두 줄일 수 있습니다.
시작 전에 무엇이 필요한가요?
Node.js 22 이상, 서버 측 SEEDROUTER_API_KEY 환경 변수, 그리고 테스트 묶음을 감당할 계정 잔액이 필요합니다. 프롬프트 두 개로 시작하세요. 장수를 늘리기 전에 현재 요금을 확인하세요.
아래 코드를 비공개 작업 디렉터리에 images.mjs로 저장하세요. 그 위치에 image-tasks.json을 만들고 PNG 파일을 images/에 내려받습니다. 이 상태 파일은 보관하세요. 로컬 라벨과 접수된 작업을 이어 주는 것이 바로 이 파일입니다. 비밀 정보나 고객 민감 정보가 담긴 프롬프트를 공개 저장소에 올리지 마세요.
예시는 프롬프트마다 한 장을 요청합니다. 같은 프롬프트로 여러 결과가 필요하면 n 매개변수를 사용하세요. 서로 다른 프롬프트를 제출하는 것과 혼동하지 마세요. 허용 범위와 그 밖의 제약은 모델 레퍼런스에 정리되어 있습니다.
한 번 제출하고 작업 ID를 저장하세요
스크립트는 요청을 보내기 전에 unconfirmed 항목을 기록합니다. 응답을 저장하기 전에 프로세스가 멈춰도, 다음 실행에서 그 항목을 조용히 다시 제출하지 않습니다. unconfirmed 항목을 어떻게 처리할지 결정하기 전에 계정에서 작업 상태를 확인하세요.
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));node images.mjs submit을 실행한 뒤 node images.mjs poll을 실행하세요. 스크립트는 한 번에 하나만 돌리고, 상태 파일의 라벨 이름은 바꾸지 마세요. 이것은 작은 로컬 예시이지 다중 프로세스용 저장소가 아닙니다. 애플리케이션 인스턴스가 여러 개인 서비스라면, 이 JSON 파일을 공유하는 대신 각 제출의 소유권이 분명한 영속 저장소를 사용하세요.
생성을 다시 시작하지 않고 폴링만 재개하기
로컬 기한이나 네트워크 중단 뒤에는 node images.mjs poll을 다시 실행하세요. 저장된 작업 ID를 읽어 GET 요청으로 확인합니다. 다른 생성 요청은 보내지 않습니다. 완료되었지만 다운로드가 끝나지 않은 작업은 다시 확인되고, PNG 파일이 동일하게 정해진 이름으로 내려받아집니다.
10분이라는 기한은 이 예시 스크립트의 것입니다. 약속된 완료 시간도, 서버 측 취소도 아닙니다. 진행 중인 네트워크 요청 하나가 그 루프 기한 이후에 끝날 수도 있습니다. 실제 상태는 작업 수명 주기 레퍼런스에 정의되어 있습니다. processing, completed, failed 세 가지입니다.
unconfirmed 항목은 스크립트에 저장된 작업 ID가 없으므로 사람이 확인해야 합니다. 두 번째에는 되는지 보려고 그 항목을 지우고 제출을 다시 실행하지 마세요. 먼저 계정의 작업 기록을 확인하세요. ID를 되찾았다면 저장된 항목에 그 ID를 넣고 폴링을 사용하고, 그렇지 않다면 다시 생성하기로 결정하기 전에 불확실한 제출부터 정리하세요.
동시 실행은 얼마나 해야 하나요?
이 예시처럼 순차 제출과 작은 테스트 묶음으로 시작하세요. 접수된 작업은 스크립트가 다음 요청을 제출하는 동안에도 계속 진행되지만, 요청이 무제한으로 한꺼번에 나가지는 않습니다. 이 코드가 어디서나 안전한 동시 실행 수를 뜻하는 것은 아닙니다.
규모가 큰 애플리케이션에서는 미완료 작업의 최대 개수를 명시적인 설정으로 두세요. 그 한도에 도달하면 새 작업 접수를 멈추고, 이미 접수된 작업은 계속 확인합니다. 계정 한도는 POST를 무작정 반복하지 말고 문서화된 오류 안내에 따라 처리하세요.
다시 만들어야 하는 결과는 따로 기록하세요. 마음에 들지 않는 완성 이미지는 실패한 API 작업이 아닙니다. 그것은 새로운 편집상의 판단이며, 요청을 한 번 더 보낸다면 예산에 넣어야 할 새 생성입니다.
완료라고 말하기 전에 묶음을 확인하세요
입력한 라벨을 상태 파일 및 내려받은 파일과 대조하세요. 모든 라벨에는 설명이 있어야 합니다. 다운로드 완료, 처리 중, 실패, 또는 제출 확인 대기 중입니다. 파일 개수만 세면 끝나지 않은 작업이나 중간에 멈춘 다운로드를 놓칠 수 있습니다.
이미지도 직접 열어 보세요. 스크립트는 HTTP 상태와 PNG 콘텐츠 타입을 확인하지만, 머그가 요청한 색인지, 라벨을 읽을 수 있는지는 판단하지 못합니다. 시각적 요구사항은 전송 성공 여부와 분리해서 검토하세요.
output_format을 바꾼다면 기대하는 콘텐츠 타입과 파일 확장자도 함께 바꾸세요. 내려받은 JPEG의 이름을 .png로 바꾼다고 변환되지 않습니다. 이 예시가 PNG로 고정한 것은, 형식 처리라는 별도의 문제가 작업 복구라는 본줄기를 가리지 않도록 하기 위해서입니다.
자주 묻는 질문
이것이 OpenAI의 Batch API인가요?
아닙니다. 스크립트에서 일반적인 SeedRouter 이미지 작업을 제출할 뿐입니다. Batch API 엔드포인트를 쓰지 않으며, 특별한 배치 요금을 뜻하지도 않습니다.
다운로드만 재개할 수 있나요?
가능합니다. 저장된 상태로 폴링 명령을 실행하세요. 완료된 작업의 출력을 다시 가져와 빠진 결과를 내려받으며, 새 이미지 작업을 만들지 않습니다.
한 작업이 실패하면 다른 작업도 멈추나요?
멈추지 않습니다. 폴링 루프는 그 작업을 실패로 기록하고 나머지 확인을 계속합니다. 실패한 항목은 따로 검토하세요. 실패한 작업은 과금되지 않으며, 로컬 시간 초과만으로는 실패로 단정할 수 없습니다.
작업 기록을 이미지와 함께 보관하세요
믿을 수 있는 GPT Image 2 배치 생성은 각 프롬프트와 그 작업 ID의 연결을 지키는 데 달려 있습니다. 그 기록을 보존하고, 기존 작업을 재개하고, 다운로드 재시도는 생성과 분리해서 처리하세요. 작은 묶음이 기대대로 동작하면, 요금 가이드로 모니터링할 수 있는 예산 안에서 작업량을 늘리세요.



