DEV Community

Adela for BetterToken.ai

Posted on Originally published at bettertoken.ai

Template-Based Image Generation with Genviso and BetterToken

Template-Based Image Generation with Genviso and BetterToken

A good image is not yet a production workflow. For a single asset, you can rewrite a prompt several times, inspect the outputs, and choose one manually. With hundreds of SKUs, that habit becomes a long series of costly experiments. Every change to the light, camera angle, or material triggers another request, while the reason an image worked remains in one person's head.

Split the work into two loops. First, the team tests a visual direction and records the rules that can be repeated. Then the backend inserts business data into the approved template, sends requests, stores results, and handles errors. Creative exploration stays outside the production queue, and composition edits no longer require changes to server code.

Why prompt debugging in code gets out of control

Too many connected variables

An image model responds to the subject, environment, lighting, camera position, material, depth of field, and palette at the same time. A serum-bottle photograph changes substantially when you switch between:

  • a straight-on view and a 45-degree top view;
  • hard directional light and soft diffused light;
  • reflective glass and a matte finish;
  • travertine, metal, or seamless paper backgrounds;
  • an 85 mm macro look and a wide-angle composition.

Change several variables together and you cannot tell which wording helped. Change them one at a time and the request count grows quickly. A backend using BetterToken can execute a template through the Image API, but starting the queue too early only reproduces an untested visual hypothesis at scale.

Each business task needs its own visual grammar

A product card needs a readable silhouette, controlled reflections, and room for layout. A 3D illustration has different constraints on shape and material. A social poster depends on hierarchy, contrast, and safe areas. One universal prompt usually accumulates contradictory adjectives.

It is more practical to keep several template families:

skincare_product
luxury_watch
food_photography
3d_illustration
social_poster
Enter fullscreen mode Exit fullscreen mode

Each family defines its own required fields and acceptance criteria. The application selects a template by category and then fills it with data for the specific product or campaign.

Exploration and production need different rules

Exploration allows many variants and subjective comparison. Production needs a predictable contract, a template version, bounded retries, a job identifier, and an explicit acceptance result.

A fragile loop looks like this:

edit the prompt in code
→ send an API request
→ open the image
→ edit the code again
→ send another request
Enter fullscreen mode Exit fullscreen mode

There is no point at which the visual decision becomes approved. As a result, every design discussion reaches into the backend and its task queue.

Workflow architecture: from visual hypothesis to a CMS asset

During visual exploration, the team uses Genviso to compare options in its visual prompt gallery, test composition, light, and style, and preserve the structure of a successful prompt. During server execution, the application uses BetterToken's OpenAI-compatible Base URL with the user's own API key, fills the variables, calls a currently available model, and records the output. The handoff between the two stages is a versioned Prompt Template—not a selected image or a set of verbal instructions.

To test this boundary before connecting a queue, create your own API key, run one control request with the approved template, and immediately reconcile its model, status, and actual charge in the Dashboard. This proves the server route without turning visual exploration into a stream of production requests.

visual exploration
    ↓
validate the Prompt Template
    ↓
lock variables and constraints
    ↓
insert PIM / CMS / SKU data
    ↓
backend Image API request
    ↓
store file, status, and metadata
    ↓
visual acceptance
Enter fullscreen mode Exit fullscreen mode

Every transition should produce an artifact that can be checked:

Stage Output Exit condition
Visual exploration Successful and rejected variants The influential parameters are understood
Template validation A prompt with named variables It works across several representative products
Integration A render function and input schema Required fields are validated before the API request
Test request One stored file and request record The file opens and model/status match the test
Production A task with template_id, version, and job_id Retries are bounded and the output maps to its SKU

Stage 1: turn the visual decision into a template

A starting structure for studio skincare photography could be:

Commercial studio product photography of {subject}.

Environment:
{environment}

Visual style:
{visual_style}

Lighting:
{lighting}

Composition:
{composition}

Color palette:
{color_palette}

Crisp reflections, premium material texture,
high-end commercial editorial photography.
Enter fullscreen mode Exit fullscreen mode

The order and visual dimensions remain stable while their values change independently:

subject
environment
visual_style
lighting
composition
color_palette
Enter fullscreen mode Exit fullscreen mode

Before developers receive the template, record four more things:

  1. Required fields. Do not send a request without subject or composition.
  2. Allowed values. If the camera angle has three approved options, use an enum instead of free-form CMS text.
  3. Forbidden combinations. Transparent packaging on a mirror may require a separate template.
  4. Acceptance criteria. The silhouette is readable, the logo is not distorted, the product is not cropped, and the background supports the final layout.

Keep the prompt beside a machine-readable contract:

{
  "template_id": "skincare_product_v3",
  "required_variables": [
    "subject",
    "environment",
    "visual_style",
    "lighting",
    "composition",
    "color_palette"
  ],
  "output_size": "1024x1024"
}
Enter fullscreen mode Exit fullscreen mode

The version in template_id makes a result reproducible. When a designer changes the lighting or composition, new jobs receive a new version while existing assets remain tied to the old one.

Stage 2: connect the template to the backend

For a first test, you need the official openai Python SDK, your own BetterToken API key, and a current model ID from the Image API documentation. Keep the key and model ID in the environment:

python -m pip install openai

export BETTERTOKEN_API_KEY="your_api_key_here"
export BETTERTOKEN_IMAGE_MODEL="current_image_model_id"
Enter fullscreen mode Exit fullscreen mode

Never put a real key in source control, prompts, screenshots, or logs. In production, use a secret manager and separate keys for different applications or environments.

The following example renders the template, sends one request, and saves a PNG from b64_json:

import base64
import os
from pathlib import Path
from typing import Mapping

from openai import OpenAI


client = OpenAI(
    base_url="https://www.bettertoken.ai/v1",
    api_key=os.environ["BETTERTOKEN_API_KEY"],
)


def render_product_prompt(variables: Mapping[str, str]) -> str:
    return f"""
Commercial studio product photography of {variables['subject']}.

Environment:
{variables['environment']}

Visual style:
{variables['visual_style']}

Lighting:
{variables['lighting']}

Composition:
{variables['composition']}

Color palette:
{variables['color_palette']}

Crisp reflections, premium material texture,
high-end commercial editorial photography.
""".strip()


product = {
    "subject": (
        "frosted amber glass serum bottle "
        "with a minimalist gold dropper"
    ),
    "environment": (
        "organic travertine pedestal "
        "surrounded by subtle water ripples"
    ),
    "visual_style": "high-end botanical skincare editorial",
    "lighting": (
        "warm directional morning rim light "
        "with soft diffused fill"
    ),
    "composition": (
        "centered 85mm macro product photography "
        "with shallow depth of field"
    ),
    "color_palette": "earthy amber, warm beige and subtle gold",
}


response = client.images.generate(
    model=os.environ["BETTERTOKEN_IMAGE_MODEL"],
    prompt=render_product_prompt(product),
    size="1024x1024",
    n=1,
)

image_base64 = response.data[0].b64_json
if not image_base64:
    raise RuntimeError("Image API response does not contain b64_json")

output_path = Path("serum-product.png")
output_path.write_bytes(base64.b64decode(image_base64))
print(f"Saved: {output_path}")
Enter fullscreen mode Exit fullscreen mode

The client.images.generate(...) call and b64_json decoding follow the current OpenAI Python SDK contract. The model ID stays in BETTERTOKEN_IMAGE_MODEL because available models and parameters should be checked before a run. A model change then does not require a rewrite of the Prompt Template or business logic.

A minimal batch-job loop

The following is deliberately explicit integration pseudocode. save_job, generate_image, and ApiError represent adapters for your storage and API client; they are not extra SDK methods. The important part is the sequence of states and decisions:

MAX_ATTEMPTS = 3
RETRYABLE_STATUS = {429, 500, 502, 503, 504}

for sku in sku_rows:
    variables = validate_variables(sku)  # before any API call
    prompt = render_product_prompt(variables)
    job_id = uuid4().hex
    prompt_hash = sha256(prompt.encode()).hexdigest()

    save_job(
        job_id=job_id,
        sku_id=sku["id"],
        template_id="skincare_product_v3",
        prompt_hash=prompt_hash,
        status="pending",
    )

    for attempt in range(1, MAX_ATTEMPTS + 1):
        save_job(job_id=job_id, status="running", attempt=attempt)
        try:
            result = generate_image(prompt)
        except ApiError as error:
            if error.status_code in {400, 401}:
                save_job(job_id=job_id, status="failed", error_code=error.status_code)
                break  # correct data, key, or configuration first
            if error.status_code not in RETRYABLE_STATUS:
                save_job(job_id=job_id, status="failed", error_code=error.status_code)
                break
            if attempt == MAX_ATTEMPTS:
                save_job(job_id=job_id, status="failed", error_code=error.status_code)
                break
            sleep(min(2 ** attempt, 8))
            continue
        except TimeoutError:
            save_job(job_id=job_id, status="unknown", error_code="timeout")
            break  # inspect Dashboard and storage before resubmitting

        if not result.b64_json:
            save_job(job_id=job_id, status="failed", error_code="empty_output")
            break

        output_path = persist_png(job_id, result.b64_json)
        save_job(
            job_id=job_id,
            status="succeeded",
            output_path=output_path,
            model=result.model,
            attempt=attempt,
        )
        break
Enter fullscreen mode Exit fullscreen mode

The local job_id links the SKU, template, and file; it does not make the remote request idempotent. After a timeout, leave the state as unknown, check the Dashboard by time and inspect object storage, and only then decide whether one resubmission is necessary. This keeps the queue from hiding a possible duplicate.

Troubleshooting order

Symptom Check first Fix Recheck
400 Required variables, current model ID, supported size Correct data or the parameter; do not repeat the same request automatically Run one control SKU and open the PNG
401 Whether the key variable loaded, belongs to the user, and matches the protocol Replace or recreate the key without logging it Send the minimal request and find its status in the Dashboard
429 Current task rate and concurrency Stop admitting new jobs, reduce concurrency, apply bounded backoff Let one request pass before restoring load gradually
5xx Request time and attempts already made Retry only up to MAX_ATTEMPTS; preserve time and status if it persists Test one request after a delay without changing the approved template
Timeout Dashboard and storage; an output may exist without a client response Keep unknown instead of assuming failure If neither record nor file exists, allow one resubmission with the same local job_id
Empty b64_json or decode failure Current response format, model, and documented parameters Store the error code without key data; correct parsing or configuration Retry one SKU and verify that the PNG decodes and opens

What to add before batch generation

One successful file proves the basic route, not that the queue is ready for hundreds of jobs.

Validate data before the request

An empty material, unexpected markup in product_name, or free-form text in place of an approved palette changes the prompt. Check required fields, lengths, and allowed values before calling the model. Store the final prompt hash, template_id, and SKU identifier with the job.

Bound retries

A retry after a timeout can create another image even when the application missed the first response. Set a finite attempt count, add backoff, and assign a job_id to every run. Do not retry 400, 401, or model-configuration errors indefinitely; correct the data, key, or configuration first.

Separate technical and visual acceptance

HTTP 200 and a valid PNG prove technical success. Composition, product distortion, and brand fit are separate checks. The automated job stores the file and metadata; the next stage applies the template's visual criteria.

Reconcile the request with usage

After the control generation, locate the request in the Dashboard by time. Check the model, status, and charge; available usage fields cover input, output, and cache tokens. The Dashboard stores usage and spending metadata, not the full prompt or response. Use the current models and pricing page for budget planning, and use the request record for the actual test charge.

Example flow for a product catalog

For a store with several hundred SKUs, the job can move through this chain:

PIM / SKU database
↓
product category → template_id
↓
name / material / color / background
↓
required-field validation
↓
render Prompt Template
↓
generation job with job_id
↓
Image API
↓
object storage
↓
visual acceptance
↓
CMS / media library
Enter fullscreen mode Exit fullscreen mode

The prompt is now a versioned production object. You can see which template created a file, compare rejection rates by version, and roll back a poor change without rewriting the entire integration.

Pre-launch checklist

  • The Prompt Template has been tested on representative products and difficult edge cases.
  • template_id, required variables, and acceptance criteria are defined.
  • The API key stays outside source code and logs.
  • The model ID comes from the environment or configuration.
  • One test request creates an openable file of the expected size.
  • A 400/401 error triggers a data or configuration fix before another request.
  • 429/5xx errors have a bounded retry policy.
  • Every job maps to an SKU, job_id, template version, and storage location.
  • Technical validation and visual acceptance are separate.
  • The model, status, and test charge have been checked in the Dashboard.

How the role split improves collaboration

Genviso owns the interactive loop: finding a visual direction quickly, comparing prompts, and validating the template before developer handoff. BetterToken owns the server loop: API key management, the OpenAI-compatible connection, calling a currently available model, and recording usage. Both teams agree on one contract—a Prompt Template with variables, a version, and acceptance criteria.

To move an approved template into a working backend and verify its cost on one representative SKU, create your own API key, run the smallest request from the Image API reference, and check the model, status, and charge in the Dashboard before connecting the queue. This boundary keeps visual exploration interactive and production execution reproducible.


Originally published on the BetterToken blog.

BetterToken provides pay-as-you-go access to AI model APIs through
OpenAI-compatible and Anthropic-compatible endpoints — useful if you are wiring
Claude Code, Codex, or your own tooling to a custom base URL.
See the docs to get started.

Top comments (0)