Tarifs+7% bonus

Comment générer des images produits en masse avec l'API Seedream 5.0 Pro

Créez un flux de travail API Seedream 5.0 Pro piloté par CSV avec des entrées multi-références, une concurrence contrôlée, des nouvelles tentatives, des estimations de coûts et une revue humaine.

Comment générer des images produits en masse avec l'API Seedream 5.0 Pro

Un catalogue de 100 SKU n'est pas une seule requête d'image. Chaque produit a besoin de ses propres images de référence, variables de prompt, nom de fichier, enregistrement des tentatives et état d'approbation. Copier des prompts dans un playground peut produire des images, mais cela ne peut pas vous dire quelle version appartient à MUG-001 ou si une image de sac ayant échoué doit être relancée.

La solution pratique consiste en une requête Seedream par SKU, exécutée via un petit pool de workers. C'est ce que « bulk » signifie ici. BytePlus répertorie actuellement text-to-multiple-images comme non pris en charge pour Seedream 5.0 Pro, donc un seul prompt ne renvoie pas un catalogue entier. À la place, un CSV fournit les données produit, Python envoie plusieurs requêtes indépendantes à un rythme contrôlé, et un manifeste enregistre chaque résultat.

Le workflow final utilise l'API Seedream 5.0 Pro sur GPTProto et produit :

  • products.csv, avec une ligne par image produit ;

  • bulk_seedream_products.py, qui crée et exécute les tâches ;

  • generated/, avec des noms de fichiers liés aux SKU ; et

  • manifest.json, avec les tentatives, les erreurs, les emplacements de sortie et le statut de révision.

Table des matières

What “Seedream 5.0 Pro API Batch” Actually Means

Three phrases are often treated as synonyms. They are not.

Term Meaning Current behavior
Multi-reference input Several source images guide one edit The GPT Proto image-edit endpoint accepts up to 10 references
Multiple outputs One prompt returns several separate images Not exposed for Seedream 5.0 Pro
Bulk generation Many SKU-specific requests run through a queue The workflow built below

Multi-reference input improves one result. It does not create ten results. A product front view, a material close-up, and a lighting reference can all guide one final image. To create images for 100 products, the client still sends 100 requests.

That distinction affects everything downstream. One SKU per request gives each output an owner, filename, cost record, and retry history. Concurrency makes the queue move faster; it does not turn the model into a multi-output endpoint. The underlying model behavior is documented in BytePlus’s image-generation matrix, while GPT Proto exposes the model through its own /api/v3/ routes.

What You Will Build

The data moves through five stages:

CSV catalog → prompt and references → controlled request pool → downloaded files → human review

The script uses synchronous requests with enable_sync_mode: true. That choice is deliberate. GPT Proto’s live model page documents an asynchronous result_id workflow, but the complete status and output response schema is not published on that page. A production tutorial should not invent those fields.

Synchronous mode still supports bulk generation. Each worker handles one product from request through download, while ThreadPoolExecutor lets a small number of workers run at the same time. The manifest is written by the main thread after each worker finishes, so interrupted runs can skip files that are already present.

For a later high-throughput version, set enable_sync_mode to false only after you have captured a live response and confirmed the current task ID, status, output, and error fields. Then store the returned result_id before polling /api/v3/predictions/{result_id}/result.

What You Need Before Starting

Install Python and the single dependency:

python --version
python -m pip install requests

Create a GPT Proto API key, then keep it in an environment variable:

export GPTPROTO_API_KEY="your-key-here"

The current live API examples use this header format:

Authorization: Bearer YOUR_API_KEY

You also need product references at public HTTPS URLs. A path such as /Users/me/photos/mug.jpg exists only on your computer. The remote API cannot fetch it. Put the files on a CDN, object-storage bucket, or another HTTPS location that the API can reach without a login cookie.

Start with a small catalog slice. Pick products with different shapes, materials, and label complexity. A simple mug can pass while a bag with multiple straps fails, so one easy test is not enough to approve a shared prompt.

Estimate the Batch Cost Before You Run It

GPT Proto’s live Seedream 5.0 Pro pricing was checked on September 17, 2026. Use the model page for per-image rates and the GPT Proto pricing page for account-level pricing information.

Output Base price, including the first reference Each additional reference
1K $0.0405 per image +$0.0027
2K $0.0810 per image +$0.0027

The current Seedream 5.0 Pro Image Edit API accepts up to 10 reference images. The first is included in the base price. References two through ten add $0.0027 each.

That produces these planning figures before retries or rejected generations:

Batch Estimated API cost
100 one-reference images at 1K $4.05
100 one-reference images at 2K $8.10
1,000 one-reference images at 1K $40.50
1,000 one-reference images at 2K $81.00
100 images with three references at 1K $4.59
100 images with three references at 2K $8.64

The raw-generation bill is easy to estimate. The approved-image cost is not. If a product needs several attempts, every generated image counts. Use this formula for a more realistic budget:

estimated catalog cost =
  number of SKUs
  × average attempts per approved image
  × (base output price + paid reference-image cost)

Use 1K to test the prompt system across representative products. Do not treat it as a guaranteed preview of a later 2K call. The 2K request creates a new image, so composition and details can change. Review the final-resolution result again.

Step 1: Create a Product CSV

Save this as products.csv:

sku,name,color,material,angle,background,product_image_url,detail_image_url,style_reference_url,avoid
MUG-001,Stackable coffee mug,matte navy,stoneware,three-quarter,soft warm gray,https://cdn.example.com/mug-front.jpg,https://cdn.example.com/mug-glaze.jpg,,steam or hands
BAG-014,Compact crossbody bag,forest green,pebbled leather,front three-quarter,light beige,https://cdn.example.com/bag-front.jpg,https://cdn.example.com/bag-texture.jpg,https://cdn.example.com/style-neutral.jpg,model or extra straps

Replace every cdn.example.com URL before running the script. The CSV fields serve different jobs:

  • sku is the permanent product key. It also becomes part of the output filename.

  • name, color, material, angle, and background hold facts that change by product.

  • product_image_url is the identity reference and is required.

  • detail_image_url can show material, controls, hardware, or a label close-up.

  • style_reference_url controls lighting and background treatment, not product identity.

  • avoid lists a few visible mistakes that matter for that SKU.

Keep values short and factual. Do not put an entire creative brief into one cell. Do not put the API key in the CSV, source code, or manifest.

The script below rejects duplicate SKUs, missing product URLs, non-HTTPS references, and more than ten references before sending a paid request.

Step 2: Give Each Reference Image One Job

A Seedream 5.0 Pro API multi-reference image request works better when the prompt explains why each image exists. Use a stable order:

  1. Product identity reference: silhouette, proportions, visible design, and packaging.

  2. Detail reference: material, hardware, controls, or label treatment.

  3. Style reference: lighting, surface, palette, and background only.

The generated prompt will include instructions such as:

Image 1 is the product identity reference. Preserve its silhouette, proportions, visible design, and packaging. Image 2 is the detail reference. Use it for material and hardware details. Image 3 is the lighting and background reference only. Do not copy objects from it.

Start with one to three useful references. Ten unlabeled inputs cost more and can conflict with one another. More images are not automatically more accurate.

A ComfyUI launch post describes stable product features across scene variations as a production use case. Treat that as a workflow claim, not an independent benchmark. Product identity still needs a side-by-side review against the source.

Step 3: Build One Reusable Product Prompt

Keep catalog-wide rules in code and SKU facts in the CSV. A useful order is:

asset type
→ product identity and protected details
→ requested background
→ camera and composition
→ lighting and shadow
→ material behavior
→ exclusions

For the mug row, the script produces instructions close to this:

Create one square e-commerce hero photograph of the Stackable coffee mug.
The product is matte navy stoneware. Preserve the product identity, silhouette,
proportions, visible design, color family, material, and packaging details from
the references. Show a three-quarter view on a soft warm gray background.
Use a large diffused key light from the upper left and a natural contact shadow.
Keep the whole product inside the frame with crop-safe space. Return one product
image, not a collage or contact sheet. Do not add steam or hands. Do not invent
logos, labels, accessories, duplicate products, handles, straps, or controls.

Name observable details. “Premium quality” gives the model little physical information. “Soft contact shadow falling back-right” does. Keep exclusions short because the current GPT Proto request does not expose a separate negative_prompt field.

For more prompt patterns, use the Seedream 5.0 Pro Prompt Guide.

Step 4: Test One Product Before Starting the Queue

Run one synchronous image-edit request first:

curl --request POST \
  --url "https://gptproto.com/api/v3/doubao/dola-seedream-5-0-pro-260628/image-edit" \
  --header "Authorization: Bearer $GPTPROTO_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "images": [
      "https://your-cdn.example.com/mug-front.jpg",
      "https://your-cdn.example.com/mug-glaze.jpg"
    ],
    "prompt": "Create one square e-commerce hero photograph of the referenced mug. Image 1 is the product identity reference. Preserve its silhouette, proportions, handle, color, and visible design. Image 2 is the glaze detail reference. Show a three-quarter view on a soft warm gray background with a natural contact shadow. Keep the entire product inside the frame. No hands, steam, extra text, duplicate products, or collage.",
    "size": "1024x1024",
    "enable_base64_output": false,
    "enable_sync_mode": true,
    "output_format": "png"
  }'

The endpoint and current Bearer header match the live GPT Proto model example. Print and retain the first successful JSON response before adding more automation. Check that it contains an output your client can download.

Do not continue because the image merely looks attractive. Compare it with the references:

  • Is the silhouette recognizable?

  • Are the color and material acceptably close?

  • Did the model add or remove a handle, strap, button, cap, or label?

  • Is required text correct character by character?

  • Does the product make physical contact with the surface?

  • Is there enough crop-safe space for the intended marketplace placement?

Test one representative product and one difficult product. Both should pass before the full queue starts.

Step 5: Run One Request per SKU

The batch script sends the same image-edit payload used in the cURL test. The variables come from each CSV row. MAX_WORKERS controls how many independent requests can be active at once.

Start with a small value such as 2 or 3. The BytePlus documentation may publish limits for direct BytePlus accounts, but those numbers are not automatically your GPT Proto account limits. If the API returns 429, reduce the worker count and follow the response’s retry guidance.

The script uses deterministic names:

{sku}_hero_{attempt:02d}.{extension}

Examples include MUG-001_hero_01.png and BAG-014_hero_02.png. The attempt remains visible, so a reviewer can trace the selected file back to the manifest.

Step 6: Add Retries and Resume Support

Not every failure should trigger another paid request.

The script retries explicit 429 and 5xx responses with exponential backoff and jitter. It also honors a numeric Retry-After header when present. Authentication, invalid-parameter, balance, and moderation errors are recorded without an unchanged resubmission.

Read timeouts and dropped connections are different. The server may have accepted the request even though the client did not receive the response. Without a documented idempotency key, an automatic resubmission could create a second image and a second charge. The script marks those cases manual_check instead. That is slower than blind retrying. It is also safer for a production budget.

After each worker finishes, the main thread updates manifest.json through an atomic file replacement. On restart:

  • an item with downloaded, approved, or manual_review status and an existing local file is skipped;

  • a failed_retryable item continues only if attempts remain;

  • a manual_check item is not submitted again automatically; and

  • a failed item retains its error and is skipped until an operator fixes the cause, then removes that SKU entry from the manifest or changes its status to failed_retryable.

This synchronous design cannot save an asynchronous task ID. If you later adopt the documented background workflow, add task_id to the same manifest and save the live result_id immediately after submission.

Complete Python Script for Bulk Seedream Product Images

Save the following as bulk_seedream_products.py beside products.csv.

from __future__ import annotations

import base64
import csv
import json
import os
import random
import re
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import urlparse

import requests

API_URL = (
    "https://gptproto.com/api/v3/doubao/"
    "dola-seedream-5-0-pro-260628/image-edit"
)
CSV_PATH = Path(os.getenv("PRODUCTS_CSV", "products.csv"))
OUTPUT_DIR = Path(os.getenv("OUTPUT_DIR", "generated"))
MANIFEST_PATH = Path(os.getenv("MANIFEST_PATH", "manifest.json"))

SIZE = os.getenv("SEEDREAM_SIZE", "1024x1024")
OUTPUT_FORMAT = os.getenv("SEEDREAM_OUTPUT_FORMAT", "png")
MAX_WORKERS = int(os.getenv("MAX_WORKERS", "3"))
MAX_ATTEMPTS = int(os.getenv("MAX_ATTEMPTS", "4"))
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "300"))
PROMPT_VERSION = os.getenv("PROMPT_VERSION", "product-hero-v1")

REFERENCE_FIELDS = (
    (
        "product_image_url",
        "the product identity reference. Preserve its silhouette, proportions, "
        "visible design, color family, material, and packaging",
    ),
    (
        "detail_image_url",
        "the detail reference. Use it for material, hardware, controls, and label details",
    ),
    (
        "style_reference_url",
        "the lighting and background reference only. Do not copy products or props from it",
    ),
)

def now_utc() -> str:
    return datetime.now(timezone.utc).isoformat()

def is_https_url(value: str) -> bool:
    parsed = urlparse(value)
    return parsed.scheme == "https" and bool(parsed.netloc)

def clean_filename(value: str) -> str:
    cleaned = re.sub(r"[^A-Za-z0-9._-]+", "-", value.strip())
    return cleaned.strip("-._") or "product"

def reference_data(row: dict[str, str]) -> tuple[list[str], list[str]]:
    urls: list[str] = []
    role_lines: list[str] = []
    for field, role in REFERENCE_FIELDS:
        url = row.get(field, "").strip()
        if not url:
            continue
        urls.append(url)
        role_lines.append(f"Image {len(urls)} is {role}.")
    return urls, role_lines

def validate_and_load_rows(path: Path) -> list[dict[str, str]]:
    required = {
        "sku",
        "name",
        "color",
        "material",
        "angle",
        "background",
        "product_image_url",
        "detail_image_url",
        "style_reference_url",
        "avoid",
    }

    with path.open("r", encoding="utf-8-sig", newline="") as handle:
        reader = csv.DictReader(handle)
        fields = set(reader.fieldnames or [])
        missing = required - fields
        if missing:
            raise ValueError(f"CSV is missing columns: {sorted(missing)}")

        rows: list[dict[str, str]] = []
        seen_skus: set[str] = set()

        for line_number, raw_row in enumerate(reader, start=2):
            row = {key: (value or "").strip() for key, value in raw_row.items()}
            sku = row["sku"]
            if not sku:
                raise ValueError(f"Line {line_number}: sku is required")
            if sku in seen_skus:
                raise ValueError(f"Line {line_number}: duplicate sku {sku!r}")
            seen_skus.add(sku)

            urls, _ = reference_data(row)
            if not row["product_image_url"]:
                raise ValueError(
                    f"Line {line_number}: product_image_url is required"
                )
            if len(urls) > 10:
                raise ValueError(
                    f"Line {line_number}: {sku} has more than 10 references"
                )
            invalid_urls = [url for url in urls if not is_https_url(url)]
            if invalid_urls:
                raise ValueError(
                    f"Line {line_number}: references must be public HTTPS URLs: "
                    f"{invalid_urls}"
                )
            rows.append(row)

    if not rows:
        raise ValueError("The CSV contains no product rows")
    return rows

def build_prompt(row: dict[str, str], role_lines: list[str]) -> str:
    avoid = row.get("avoid") or "extra products or unrelated props"
    references = " ".join(role_lines)
    return (
        f"Create one square e-commerce hero photograph of the {row['name']}. "
        f"{references} "
        f"The product is {row['color']} {row['material']}. "
        "Preserve the product identity, silhouette, proportions, visible design, "
        "color family, material, and packaging details from the references. "
        f"Show a {row['angle']} view on a {row['background']} background. "
        "Use a large diffused key light from the upper left and a natural contact "
        "shadow falling back-right. Keep the whole product inside the frame with "
        "crop-safe space. Return one product image, not a collage or contact sheet. "
        f"Do not add {avoid}. Do not invent logos, labels, accessories, duplicate "
        "products, handles, straps, buttons, caps, or controls."
    )

def load_manifest(path: Path) -> dict[str, Any]:
    if not path.exists():
        return {"prompt_version": PROMPT_VERSION, "items": {}}
    with path.open("r", encoding="utf-8") as handle:
        data = json.load(handle)
    if not isinstance(data.get("items"), dict):
        raise ValueError("manifest.json must contain an object named 'items'")
    return data

def save_manifest(path: Path, manifest: dict[str, Any]) -> None:
    temporary = path.with_suffix(path.suffix + ".tmp")
    with temporary.open("w", encoding="utf-8") as handle:
        json.dump(manifest, handle, indent=2, ensure_ascii=False)
        handle.write("\n")
    os.replace(temporary, path)

def output_items(payload: dict[str, Any]) -> list[Any]:
    data = payload.get("data")
    containers = [data, payload] if isinstance(data, dict) else [payload]
    for container in containers:
        for key in ("outputs", "output", "images", "image"):
            value = container.get(key)
            if value:
                return value if isinstance(value, list) else [value]
    raise ValueError(
        "No output field found. Inspect the saved response and update output_items() "
        "to match the current API response."
    )

def output_value(item: Any) -> str:
    if isinstance(item, str):
        return item
    if isinstance(item, dict):
        for key in ("url", "image_url", "b64_json", "base64"):
            value = item.get(key)
            if isinstance(value, str) and value:
                return value
    raise ValueError(f"Unsupported output item: {item!r}")

def extension_from_content_type(content_type: str) -> str:
    mime = content_type.split(";", 1)[0].strip().lower()
    return {
        "image/png": ".png",
        "image/jpeg": ".jpg",
        "image/webp": ".webp",
    }.get(mime, f".{OUTPUT_FORMAT}")

def save_output(value: str, file_stem: Path) -> tuple[Path, str | None]:
    if value.startswith("data:image/"):
        header, encoded = value.split(",", 1)
        mime = header.split(";", 1)[0].removeprefix("data:")
        destination = file_stem.with_suffix(extension_from_content_type(mime))
        destination.write_bytes(base64.b64decode(encoded))
        return destination, None

    if value.startswith("https://") or value.startswith("http://"):
        response = requests.get(value, timeout=REQUEST_TIMEOUT)
        response.raise_for_status()
        extension = extension_from_content_type(
            response.headers.get("Content-Type", "")
        )
        destination = file_stem.with_suffix(extension)
        destination.write_bytes(response.content)
        return destination, value

# Some responses return raw Base64 without a data URI.
    try:
        decoded = base64.b64decode(value, validate=True)
    except ValueError as exc:
        raise ValueError("Output is neither a URL nor valid Base64") from exc
    destination = file_stem.with_suffix(f".{OUTPUT_FORMAT}")
    destination.write_bytes(decoded)
    return destination, None

def backoff_seconds(attempt: int, response: requests.Response | None) -> float:
    if response is not None:
        retry_after = response.headers.get("Retry-After")
        if retry_after:
            try:
                return max(0.0, float(retry_after))
            except ValueError:
                pass
    return min(60.0, (2 ** (attempt - 1)) + random.uniform(0.0, 1.0))

def result_record(
    row: dict[str, str],
    references: list[str],
    status: str,
    attempt: int,
    *,
    output_url: str | None = None,
    local_path: str | None = None,
    error: str | None = None,
    created_at: str | None = None,
) -> dict[str, Any]:
    timestamp = now_utc()
    return {
        "sku": row["sku"],
        "asset_type": "hero",
        "status": status,
        "attempt": attempt,
        "prompt_version": PROMPT_VERSION,
        "reference_urls": references,
        "output_url": output_url,
        "local_path": local_path,
        "error": error,
        "requester": "",
        "reviewer": "",
        "approval_status": "pending" if status == "downloaded" else "",
        "created_at": created_at or timestamp,
        "updated_at": timestamp,
    }

def process_product(
    row: dict[str, str], previous: dict[str, Any] | None
) -> dict[str, Any]:
    api_key = os.environ["GPTPROTO_API_KEY"]
    references, role_lines = reference_data(row)
    prompt = build_prompt(row, role_lines)
    created_at = (previous or {}).get("created_at") or now_utc()
    first_attempt = int((previous or {}).get("attempt", 0)) + 1

    if first_attempt > MAX_ATTEMPTS:
        return result_record(
            row,
            references,
            "failed",
            first_attempt - 1,
            error="Maximum attempts already reached",
            created_at=created_at,
        )

    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    body = {
        "images": references,
        "prompt": prompt,
        "size": SIZE,
        "enable_base64_output": False,
        "enable_sync_mode": True,
        "output_format": OUTPUT_FORMAT,
    }

    for attempt in range(first_attempt, MAX_ATTEMPTS + 1):
        response: requests.Response | None = None
        try:
            response = requests.post(
                API_URL,
                headers=headers,
                json=body,
                timeout=REQUEST_TIMEOUT,
            )

            if response.status_code == 429 or response.status_code >= 500:
                message = f"HTTP {response.status_code}: {response.text[:500]}"
                if attempt == MAX_ATTEMPTS:
                    return result_record(
                        row,
                        references,
                        "failed_retryable",
                        attempt,
                        error=message,
                        created_at=created_at,
                    )
                time.sleep(backoff_seconds(attempt, response))
                continue

            response.raise_for_status()
            payload = response.json()
            item = output_items(payload)[0]
            value = output_value(item)
            file_stem = OUTPUT_DIR / (
                f"{clean_filename(row['sku'])}_hero_{attempt:02d}"
            )
            local_file, output_url = save_output(value, file_stem)
            return result_record(
                row,
                references,
                "downloaded",
                attempt,
                output_url=output_url,
                local_path=str(local_file),
                created_at=created_at,
            )

        except requests.exceptions.ConnectTimeout as exc:

# No connection was established; a bounded retry is reasonable.
            if attempt == MAX_ATTEMPTS:
                return result_record(
                    row,
                    references,
                    "failed_retryable",
                    attempt,
                    error=str(exc),
                    created_at=created_at,
                )
            time.sleep(backoff_seconds(attempt, response))

        except (
            requests.exceptions.ReadTimeout,
            requests.exceptions.ConnectionError,
        ) as exc:

# The server may have accepted the request. Avoid a blind resubmission.
            return result_record(
                row,
                references,
                "manual_check",
                attempt,
                error=f"Ambiguous network failure; verify account history: {exc}",
                created_at=created_at,
            )

        except requests.exceptions.HTTPError as exc:
            status_code = response.status_code if response is not None else "unknown"
            details = response.text[:500] if response is not None else str(exc)
            return result_record(
                row,
                references,
                "failed",
                attempt,
                error=f"HTTP {status_code}: {details}",
                created_at=created_at,
            )

        except (ValueError, KeyError, json.JSONDecodeError) as exc:
            return result_record(
                row,
                references,
                "manual_check",
                attempt,
                error=str(exc),
                created_at=created_at,
            )

    return result_record(
        row,
        references,
        "failed",
        MAX_ATTEMPTS,
        error="Request ended without a result",
        created_at=created_at,
    )

def should_skip(entry: dict[str, Any] | None) -> bool:
    if not entry:
        return False
    if entry.get("status") in {"manual_check", "failed"}:
        return True
    if entry.get("status") not in {"downloaded", "manual_review", "approved"}:
        return False
    local_path = entry.get("local_path")
    return bool(local_path and Path(local_path).exists())

def main() -> None:
    if not os.getenv("GPTPROTO_API_KEY"):
        raise SystemExit("Set GPTPROTO_API_KEY before running the script")
    if MAX_WORKERS < 1:
        raise SystemExit("MAX_WORKERS must be at least 1")

    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
    rows = validate_and_load_rows(CSV_PATH)
    manifest = load_manifest(MANIFEST_PATH)
    items: dict[str, Any] = manifest["items"]

    pending: list[dict[str, str]] = []
    for row in rows:
        entry = items.get(row["sku"])
        if should_skip(entry):
            print(f"SKIP {row['sku']}: {entry['status']}")
        else:
            pending.append(row)

    if not pending:
        print("Nothing to submit")
        return

    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
        future_to_row = {
            executor.submit(process_product, row, items.get(row["sku"])): row
            for row in pending
        }
        for future in as_completed(future_to_row):
            row = future_to_row[future]
            try:
                record = future.result()
            except Exception as exc:  # Preserve an unexpected worker failure.
                references, _ = reference_data(row)
                record = result_record(
                    row,
                    references,
                    "failed",
                    int(items.get(row["sku"], {}).get("attempt", 0)),
                    error=f"Unexpected worker error: {exc}",
                )
            items[row["sku"]] = record
            manifest["prompt_version"] = PROMPT_VERSION
            manifest["updated_at"] = now_utc()
            save_manifest(MANIFEST_PATH, manifest)
            print(f"{record['status'].upper()} {row['sku']}: {record.get('local_path') or record.get('error')}")

if __name__ == "__main__":
    main()

Run it with:

python bulk_seedream_products.py

You can change the worker count and output size without editing the file:

MAX_WORKERS=2 SEEDREAM_SIZE=2048x2048 python bulk_seedream_products.py

The console reports each SKU as it finishes. Open manifest.json to see which files are downloaded, failed, or held for a manual check. If the live response uses a field shape not covered by output_items(), the script records the mismatch instead of silently treating the job as complete. Update that small parser using the JSON captured during the single-product test.

Review Every Image Before It Reaches the Store

A downloaded image is not an approved image. Keep generation status and approval status separate.

Check Common failure
Geometry Changed cap, strap, button, handle, or proportions
Color Wrong shade, saturation, or surface finish
Material Plastic replaces metal; leather texture becomes flat
Packaging Invented logo, misspelled label, or changed volume
Scene Floating object or implausible contact shadow
Crop Product is clipped in marketplace thumbnails
Duplication An extra product or accessory appears

Move a record from pending to approved, rejected, or needs_edit only after a person compares the output with the source images. If label copy must be legally exact, keep the source label in post-production rather than trusting generated text without verification.

For a first-party quality benchmark, run a small matrix before scaling:

  • a labeled bottle across a catalog background, lifestyle scene, and campaign layout;

  • a leather bag across the same three scene types; and

  • a lamp with geometry and control details visible.

Record first-pass approval rate, attempts per approved image, identity failures, label errors, and API cost per approved image. Those measurements describe your catalog better than a generic model demo.

Make the Workflow Usable by a Team

The manifest is the handoff contract between the operator, designer, and reviewer. Keep these fields even if one person currently does every job:

Field Why it matters
sku Permanent primary key for the product
prompt_version Identifies the shared rules used for the output
reference_urls Reconstructs the exact source set
attempt Separates reruns and supports cost analysis
requester and reviewer Assigns ownership
approval_status Keeps generated and publishable assets distinct
output_url and local_path Connects the API result to the stored file
error Preserves the reason a row stopped
timestamps Shows when the record changed

Treat the SKU as immutable. If the shared prompt changes, increment PROMPT_VERSION rather than overwriting the history. Rerun rejected rows, not the whole catalog. Store approved assets separately from drafts, and never commit an API key to the same repository.

This is the difference between a batch script and a team workflow. The script produces files. The records explain how those files were produced and whether they are allowed to ship.

Common Problems and Fixes

Problem Likely cause Fix
401 or 403 Key, header, account access, or balance Confirm the current Bearer header on the live model page and check the account
Reference does not load Private, local, expired, or non-HTTPS URL Use a stable public HTTPS URL and test it outside your signed-in browser
429 Too many concurrent requests Lower MAX_WORKERS; honor the documented retry response
Request returns a task but no image Asynchronous mode is active Use enable_sync_mode: true for this script, or implement polling from a verified live response
Possible duplicate after timeout Client lost a response after submission Do not blindly retry; check account history and mark the row manually
Product changes too much Reference roles or protected details are vague Name the identity reference and list the visible parts that must remain
Extra objects or collage Deliverable is underspecified Ask for one product image and explicitly reject collages and duplicates
Label text is wrong Generative rendering is not exact Check every character; preserve exact regulated text in post-production
Output parser finds no image Current JSON shape differs Inspect the single-test response and update output_items()

Do not add retries until you know whether the failure happened before or after the server accepted the request. Reliability is not the same as resubmitting everything.

When Seedream 5.0 Pro Is the Right Choice

Seedream 5.0 Pro fits product lifestyle scenes, campaign layouts, localized creative, and catalog variations guided by reference images. Its flat per-image tiers also make the raw generation budget straightforward to calculate.

It is not a substitute for product photography or post-production when the output must preserve pixel-identical geometry, legally exact packaging, or regulated label copy. It is also the wrong fit for a system that publishes every generated image without review, or for a requirement that one request return many separate product files.

Use it for scalable creative production. Keep a person between generation and publication.

Start with a Representative Catalog Slice

Choose a small set of SKUs that covers different shapes, materials, labels, and accessories. Lock the CSV schema and prompt version. Measure attempts, rejection reasons, and cost per approved image. Expand only after the resume and review flow works.

Review the current parameters and price on the Seedream 5.0 Pro API page, then run one product before funding a full catalog batch. For product references rather than text-only generation, open the Seedream 5.0 Pro Image Edit API.

FAQ

Comment créer plusieurs images produit avec l’API Seedream 5.0 Pro ?

Créez une requête par SKU. Lisez les variables produit et les URL de référence depuis un CSV, exécutez un petit nombre de requêtes en parallèle, utilisez des noms de fichiers liés aux SKU et consignez chaque résultat dans un manifeste. Le script ci-dessus met en œuvre ce modèle.

Seedream 5.0 Pro peut-il générer plusieurs images à la fois ?

Pas avec une seule requête renvoyant un catalogue d’images distinctes. Le modèle Pro actuel n’expose pas de fonction texte-vers-plusieurs-images. Vous pouvez tout de même générer plusieurs images dans Seedream 5 Pro en exécutant des requêtes indépendantes en parallèle via une file d’attente contrôlée.

Combien d’images de référence puis-je utiliser avec Seedream 5.0 Pro sur GPTProto ?

Le point de terminaison actuel d’édition d’image accepte jusqu’à 10 références. La première est incluse dans le prix de base, et chaque référence supplémentaire ajoute $0.0027. Attribuez à chaque référence un rôle explicite dans le prompt.

Combien coûte un lot d’API Seedream 5.0 Pro ?

Au prix vérifié le 17 septembre 2026, une image 1K coûte $0.0405 et une image 2K coûte $0.081. Ajoutez $0.0027 pour chaque référence après la première, puis multipliez par le nombre moyen de tentatives nécessaires pour obtenir un résultat approuvé. Consultez la page du modèle en direct avant de budgétiser une exécution de production.

Seedream 5.0 Pro convient-il à un flux de travail professionnel ou d’équipe ?

Oui, lorsque la génération est associée à un mappage stable des SKU, des versions de prompt, un manifeste reprenable, des clés API protégées, des réviseurs nommés et des états d’approbation. L’API crée des ressources brouillon. Votre flux de travail décide quelles ressources peuvent être publiées.

Dois-je générer des images produit en 1K ou en 2K ?

Utilisez la 1K pour tester le prompt partagé sur des SKU représentatifs. Une fois le prompt stable, utilisez la résolution requise par l’emplacement final. Traitez chaque requête 2K comme une nouvelle génération et révisez-la à nouveau plutôt que de supposer qu’elle correspondra à la composition 1K.

Articles associés

Plus de blogs
5 meilleures API vidéo IA abordables en 2026 : tarification, e-commerce et mini-séries dramatiques

5 meilleures API vidéo IA abordables en 2026 : tarification, e-commerce et mini-séries dramatiques

Vidu Q3 Turbo est la meilleure API vidéo IA abordable pour la plupart des développeurs en 2026. Un clip de cinq secondes en 720p coûte environ 0,24 $, tandis que la 1080p coûte 0,056 $ par seconde générée. Seedance 2.0 Mini est plus adapté aux brouillons économiques, Hailuo 2.3 Standard aux clips d’action fixes de six secondes, Kling 3.0 Standard au dialogue, et Wan 3.0 aux histoires plus longues en plusieurs plans. Ces gagnants changent lorsque vous ajoutez la résolution, la durée minimale des clips, l’audio et les tentatives échouées. Cette comparaison va au-delà du tarif le plus bas annoncé pour estimer ce que chaque API coûte pour le plan que vous pouvez réellement utiliser. Remarque sur les prix : Les prix GPTProto dans ce guide ont été vérifiés le 15 septembre 2026. Les tarifs des API vidéo et les paramètres disponibles peuvent changer, alors consultez la page du modèle en direct avant de budgétiser une production. Essayez Vidu Q3 Turbo

Tiffany Layne | 2026-09-16

6 meilleures API LLM abordables pour les agents IA en 2026

6 meilleures API LLM abordables pour les agents IA en 2026

Une API LLM abordable pour un agent IA n'est pas forcément le modèle avec le prix le plus bas par token d'entrée. Un agent peut choisir un outil, construire des arguments, lire le résultat, réviser son plan et appeler un autre outil avant de produire une réponse utile. Un modèle bon marché qui effectue des appels invalides ou nécessite plusieurs tentatives peut donc coûter plus cher qu'un modèle légèrement plus cher qui termine la tâche du premier coup. Ce guide compare six modèles prêts pour les agents disponibles via GPTProto. Le classement prend en compte le prix de l'API, l'utilisation d'outils, les preuves de performance indépendantes, la vitesse, les limites de contexte et le risque pratique de payer pour des boucles d'agent inutiles. Il s'agit d'une comparaison de benchmarks publics et de tarifs—et non d'une affirmation selon laquelle nous aurions réalisé un test comparatif privé en tête-à-tête. Une clé pour votre équipe Réponse rapide : GLM-5.3 Flash est le choix par défaut le plus solide pour la plupart des agents sensibles aux coûts. DeepSeek Flash est l'alternative open-weight plus rapide, tandis que GPT-5.6 Luna est prometteur pour les charges légères et à gros volume une fois son prix de route en direct confirmé. MiniMax M3 convient aux longues sessions documentaires, Gemini 3.8 Flash mène en vitesse multimodale, et Grok 4.6 est mieux traité comme modèle d'escalade pour les tâches plus difficiles.

Michael Johnson | 2026-09-15

Comment créer un fond d’écran animé par IA avec Midjourney et Seedance 2.5

Comment créer un fond d’écran animé par IA avec Midjourney et Seedance 2.5

Vous pouvez suivre le même processus sans écrire de code : créer ou choisir une image, demander une invite de mouvement à un modèle de chat capable de traiter les images, animer l'image et télécharger le résultat. La dernière étape dépend de votre appareil. Windows et Android peuvent utiliser des applications de fond d'écran vidéo ; iPhone a besoin d'une Live Photo compatible pour son écran de verrouillage animé.

Tiffany Layne | 2026-09-09

Générer plusieurs images à la fois dans ChatGPT

Générer plusieurs images à la fois dans ChatGPT

En bref Maîtriser la génération de plusieurs images à la fois dans ChatGPT implique une combinaison de prompts structurés dans l'interface de chat et l'utilisation de paramètres spécifiques ou de boucles via l'API OpenAI. Bien que l'interface standard produise par défaut des sorties uniques, vous pouvez contourner ce goulot d'étranglement avec des dispositions en grille et des commandes par lots. La plupart des utilisateurs rencontrent des difficultés parce que l'interface web est conçue pour la simplicité conversationnelle, et non pour la production à haut volume. En comprenant les mécanismes sous-jacents de DALL-E 3, vous pouvez commencer à considérer l'outil comme une usine créative plutôt que comme un simple chatbot. Ce guide explore la transition des demandes manuelles ponctuelles vers des flux de travail par lots automatisés ou structurés, afin que vous n'ayez jamais à attendre le rendu d'une seule image avant de commencer le prochain concept créatif.

Schuyler Stacy | 2026-08-31