Template-Based Image Generation with Genviso and BetterToken

A practical way to separate visual exploration from backend execution and turn a promising prompt into a versioned production template.

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

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

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

Every transition should produce an artifact that can be checked:

StageOutputExit condition
Visual explorationSuccessful and rejected variantsThe influential parameters are understood
Template validationA prompt with named variablesIt works across several representative products
IntegrationA render function and input schemaRequired fields are validated before the API request
Test requestOne stored file and request recordThe file opens and model/status match the test
ProductionA task with template_id, version, and job_idRetries 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.

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

subject environment visual_style lighting composition color_palette

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" }

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"

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}")

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

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

SymptomCheck firstFixRecheck
400Required variables, current model ID, supported sizeCorrect data or the parameter; do not repeat the same request automaticallyRun one control SKU and open the PNG
401Whether the key variable loaded, belongs to the user, and matches the protocolReplace or recreate the key without logging itSend the minimal request and find its status in the Dashboard
429Current task rate and concurrencyStop admitting new jobs, reduce concurrency, apply bounded backoffLet one request pass before restoring load gradually
5xxRequest time and attempts already madeRetry only up to MAX_ATTEMPTS; preserve time and status if it persistsTest one request after a delay without changing the approved template
TimeoutDashboard and storage; an output may exist without a client responseKeep unknown instead of assuming failureIf neither record nor file exists, allow one resubmission with the same local job_id
Empty b64_json or decode failureCurrent response format, model, and documented parametersStore the error code without key data; correct parsing or configurationRetry 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

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.

Ready to optimize your LLM workflow?

Join thousands of developers building faster, smarter, and more cost-effective AI applications with BetterToken.