בקשות GPT Image 2 באצווה בלי לאבד משימות
שלח אוסף פרומפטים של GPT Image 2, שמור מזהי משימות, חדש את התשאול אחרי הפסקה והורד את התמונות בלי לשלוח בקשה נוספת.
קריאה כ-Markdownיצירה באצווה עם GPT Image 2 ב-SeedRouter פירושה שליחת בקשות תמונה רגילות ומעקב אחרי כל מזהה משימה שמוחזר. המדריך הזה משתמש בסקריפט קטן בצד הלקוח, ולא ב-Batch API נפרד או במוצר עם הנחת אצווה. כל פרומפט הוא משימה בפני עצמה; הסקריפט שומר את ההתקדמות כדי שתוכל לבדוק את המשימות האלה שוב אחרי הפרעה.
מדובר בשתי עבודות נפרדות: יצירת התמונה והורדתה. הורדה שנכשלה אינה מחייבת יצירה חדשה. פקיעת מועד בתשאול אינה אומרת שמשימת התמונה נכשלה. שמירה על ההבחנות האלה בתוך הסקריפט חוסכת גם עבודה וגם בלבול.
מה צריך לפני שמתחילים?
תזדקק ל-Node.js בגרסה 22 ומעלה, למשתנה סביבה SEEDROUTER_API_KEY בצד השרת וליתרה מספקת בחשבון עבור ערכת הבדיקה. התחל בשני פרומפטים. בדוק את המחירים הנוכחיים לפני שאתה מגדיל את מספר התמונות.
שמור את הקוד הבא בשם images.mjs בתיקיית עבודה פרטית. הוא יוצר שם את image-tasks.json ומוריד קובצי PNG אל images/. שמור על קובץ המצב: הוא מה שמקשר בין התוויות המקומיות שלך למשימות שהתקבלו. אל תוסיף סודות או פרומפטים עם נתוני לקוחות רגישים למאגר ציבורי.
הדוגמאות מבקשות תמונה אחת לכל פרומפט. השתמש בפרמטר n כשאתה רוצה כמה פלטים לאותו פרומפט; אל תבלבל בין זה לבין שליחת פרומפטים שונים. SeedRouter מתעדת את הטווח הקביל ואת יתר האילוצים בתיעוד המודל.
שלח פעם אחת, ואז שמור את מזהי המשימות
הסקריפט כותב רשומה במצב 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 אחרי פקיעת מועד מקומית או ניתוק רשת. הסקריפט קורא את מזהי המשימות השמורים ובודק אותם בבקשות GET. הוא אינו שולח בקשת יצירה נוספת. משימה שהושלמה אך ההורדה שלה לא הסתיימה נבדקת שוב, וקובצי ה-PNG שלה יורדים לאותם שמות קבועים.
מועד עשר הדקות שייך לסקריפט הדוגמה הזה בלבד. הוא אינו זמן השלמה מובטח ואינו ביטול מצד השרת. בקשת רשת בודדת שכבר רצה יכולה להסתיים אחרי פקיעת מועד הלולאה. תיעוד מחזור החיים של המשימות מגדיר את המצבים האמיתיים: processing, completed ו-failed.
פריט במצב unconfirmed דורש בדיקה ידנית מפני שלסקריפט אין מזהה משימה שמור. אל תמחק את הרשומה ותשלח שוב רק כדי לראות אם בפעם השנייה זה יעבוד. בדוק קודם את היסטוריית המשימות בחשבון שלך. אם תשחזר את המזהה, הכנס אותו לרשומה השמורה והשתמש בתשאול; אחרת, ברר את השליחה הלא ודאית לפני שתחליט ליצור מחדש.
כמה מקביליות כדאי להפעיל?
התחל בשליחה עוקבת ובערכת בדיקה קטנה, כמו בדוגמה הזאת. כל משימה שהתקבלה יכולה להמשיך בזמן שהסקריפט שולח את הבאה, אבל הבקשות אינן יוצאות במכה אחת ובלי גבול. הקוד אינו רומז על מספר מקביליות בטוח שמתאים לכולם.
באפליקציה גדולה יותר, הפוך את המספר המרבי של משימות לא גמורות להגדרה מפורשת. הפסק לקלוט עבודה חדשה כשמגיעים למגבלה, והמשך לבדוק משימות שכבר התקבלו. טפל במגבלות החשבון לפי הנחיות השגיאות המתועדות, ולא באמצעות חזרה עיוורת על בקשות POST.
נהל רישום נפרד לתוצאות שדורשות תיקון יצירתי. תמונה שהושלמה ולא מצאה חן בעיניך אינה משימת API שנכשלה. זו החלטה עריכתית חדשה, ואם תשלח בקשה נוספת — גם יצירה חדשה שצריך לכלול בתקציב.
בדוק את כל הערכה לפני שאתה מכריז שסיימת
השווה את תוויות הקלט שלך לקובץ המצב ולקבצים שהורדת. לכל תווית צריך להיות הסבר: הורדה, עדיין בעיבוד, נכשלה או ממתינה לבדיקת השליחה. ספירת קבצים בלבד עלולה להסתיר משימה שלא הסתיימה או הורדה שנעצרה באמצע.
פתח גם את התמונות עצמן. הסקריפט בודק את סטטוס ה-HTTP ואת סוג התוכן PNG, אבל הוא לא יכול להכריע אם הספל בצבע המבוקש או אם התווית קריאה. הערך את הבריף החזותי בנפרד מהצלחת ההעברה.
אם אתה משנה את output_format, עדכן יחד גם את סוג התוכן הצפוי וגם את סיומת הקובץ. שינוי שם של קובץ JPEG שהורד ל-.png אינו ממיר אותו. הדוגמה מקבעת PNG בכוונה, כדי שבעיית הפורמטים הנוספת לא תסתיר את נושא שחזור המשימות.
שאלות נפוצות
האם זה ה-Batch API של OpenAI?
לא. הסקריפט שולח משימות תמונה רגילות של SeedRouter. הוא אינו משתמש בנקודת קצה של Batch API ואינו מרמז על תמחור מיוחד לאצווה.
אפשר לחדש רק את ההורדות?
כן. הרץ את פקודת התשאול עם קובץ המצב השמור. היא שולפת שוב את הפלט של המשימות שהושלמו ומורידה את התוצאות החסרות בלי ליצור משימת תמונה נוספת.
האם משימה שנכשלה עוצרת את כל היתר?
לולאת התשאול מסמנת את המשימה הזאת ככושלת וממשיכה לבדוק את האחרות. בדוק את הפריט שנכשל בנפרד. משימה שנכשלת אינה מחויבת; ופקיעת זמן מקומית לבדה אינה מוכיחה כישלון.
שמור את רישום המשימות יחד עם התמונות
יצירה אמינה באצווה עם GPT Image 2 תלויה בשמירת הקשר בין כל פרומפט למזהה המשימה שלו. שמור את הרישום הזה, חדש משימות קיימות, ונסה שוב הורדות בנפרד מהיצירה. ברגע שערכה קטנה מתנהגת כמצופה, הגדל את העומס בתוך תקציב שאתה יכול לעקוב אחריו בעזרת מדריך המחירים.



