Short answer: choose a text-to-image API only after it passes a repeatable test built from your own marketing briefs, target poster and social-ad dimensions, style rules, upscale path, and campaign traffic shape. The useful output is not the prettiest sample. It is a verified, publishable asset delivered inside an end-to-end SLO, with a rollback target already proved against the same corpus.
Don't let a gallery make this decision.
How should a marketing app test text-to-image API resolution and style control?
Start with a frozen evaluation corpus, not an open-ended prompt contest. Each case should describe the intended subject, composition, palette, negative space, placement, crop, and final pixel dimensions. Include the awkward work that exposes weak controls: a product beside reflective packaging, a subject that must remain clear after a vertical crop, a poster with a quiet region reserved for copy, and a campaign family that needs to look related across several aspect ratios. Strip customer data from this corpus. If an approved workflow could contain regulated information, classification and access controls belong before evaluation, not after a provider has been selected; for US healthcare data, the security and privacy requirements in 45 CFR Part 164 are a concrete review boundary, not a marketing checkbox.
Generate several candidates per case because one lucky output says little about repeatability. Preserve the request envelope, model or capability identifier when exposed, timestamps, original bytes, transformed bytes, reviewer result, and rejection reason. Blind the review so provider labels cannot influence it. A reviewer should grade separate properties: adherence to the brief, composition, visual defects, brand-style consistency, crop survival, and editability. "High quality" is otherwise too vague to operate. It can mean attractive at first glance, faithful to a product reference, clean at final size, or fast enough for an approval session; those are different indicators and they fail for different reasons.
Resolution needs the same discipline. Requested dimensions are an input. Decoded dimensions are evidence. Usable resolution is a reviewer judgment made after the exact resize, crop, and compression chain that production will apply. An upscale step belongs in its own test cell, paired with the non-upscaled output, because enlargement can change edges and texture without fixing composition or brief adherence. Render the final ad with deterministic typography and brand assets outside the generated image unless the experiment is specifically measuring embedded text. This keeps spelling, legal copy, hierarchy, and logo placement under application control.
Use explicit denominators. Brief acceptance rate is accepted generations divided by completed generations. Workflow completion rate is verified publishable assets divided by submitted jobs. Deadline attainment is publishable assets ready before the internal cutoff divided by submitted jobs. Track p50, p95, and p99 time to a verified asset, but set the objective on the percentile that reflects the real approval workflow. A median hides the long queue that wakes the on-call engineer during a campaign launch.
I would also require a minimum sample count per creative class before treating a difference as meaningful. I'm not sure a universal sample size exists here; variance depends on prompt breadth, reviewer agreement, and how close candidates are to the acceptance boundary. Record those three inputs and expand the sample until the decision stops moving materially. Your mileage may vary. The uncertainty is useful because it tells the team what another evaluation run could resolve.
Make "ready" an observable state
An API response is one step in a media pipeline. The application still has to decode the object, verify its type and dimensions, apply deterministic transforms, store it, attach policy and provenance metadata, and make the resulting asset readable by the publisher. Define ready at that boundary. A 200 from generation does not prove that the campaign can retrieve the final object, and a successful upscale job does not prove that the result survived the placement crop.
Keep the provider boundary narrow β boring is good here β and translate business intent through an adapter. The internal request should carry the brief, target geometry, style reference identifier, and an idempotency key owned by the application. Vendor-specific options stay inside the adapter rather than leaking into every workflow. If routing decisions are produced by a model through structured tool calls, validate the chosen function and every argument in application code before dispatch; the function-calling guide in Further reading documents that separation between model output and application-side execution.
The following Go boundary validates bytes before a workflow may move to ready. It deliberately avoids any commercial route or SDK type.
package creative
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io"
)
const maxImageBytes = 40 << 20
type Request struct {
Brief string
Width int
Height int
StyleRef string
IdempotencyKey string
}
type Generator interface {
Generate(context.Context, Request) (io.ReadCloser, error)
}
type Asset struct {
Bytes []byte
Format string
Width int
Height int
SHA256 string
}
func GenerateAndVerify(ctx context.Context, g Generator, req Request) (Asset, error) {
if req.Brief == "" || req.Width <= 0 || req.Height <= 0 || req.IdempotencyKey == "" {
return Asset{}, errors.New("incomplete generation request")
}
body, err := g.Generate(ctx, req)
if err != nil {
return Asset{}, fmt.Errorf("generate image: %w", err)
}
defer body.Close()
data, err := io.ReadAll(io.LimitReader(body, maxImageBytes+1))
if err != nil {
return Asset{}, fmt.Errorf("read image: %w", err)
}
if len(data) > maxImageBytes {
return Asset{}, errors.New("image exceeds size limit")
}
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return Asset{}, fmt.Errorf("decode image: %w", err)
}
if format != "jpeg" && format != "png" {
return Asset{}, fmt.Errorf("unsupported image format %q", format)
}
if cfg.Width != req.Width || cfg.Height != req.Height {
return Asset{}, fmt.Errorf(
"dimensions %dx%d; want %dx%d",
cfg.Width, cfg.Height, req.Width, req.Height,
)
}
sum := sha256.Sum256(data)
return Asset{
Bytes: data, Format: format, Width: cfg.Width, Height: cfg.Height,
SHA256: hex.EncodeToString(sum[:]),
}, nil
}
The byte limit is application policy, so set it from measured assets and memory budgets rather than copying 40 MiB blindly. Production code should then write the verified bytes to durable storage, read the object back through the same identity used by the publishing stage, and atomically record the checksum, dimensions, transform version, and storage location. Only that record may advance the job. Keep generated artwork and overlaid copy as separate lineage inputs; when legal text changes, the system can render a new final asset without asking a probabilistic generator to recreate the scene.
Retries need a state machine, not a loop around Generate. Distinguish a request that is safe to retry from a transform or publication step whose side effect may already exist. Use the application idempotency key across attempts, bound the attempt count, obey dependency retry guidance, and add jitter where appropriate. Reconciliation should scan for contradictions such as a job marked ready with no readable object, or a stored object whose checksum differs from the publication record. Those are user-visible workflow failures even if every individual request appeared healthy.
Small point, big consequence.
Capacity-plan the operating model
Run the quality corpus through a load profile that resembles campaign work: bounded bursts, concurrent variants, human review pauses, rejected candidates, and upscale tasks. Average requests per second won't reveal a queue that grows faster than workers can drain it. Measure queue age, active jobs, attempt count, dependency latency, bytes held in memory, transform CPU, storage-write duration, acceptance rate, and end-to-end completion time. Then raise offered load until the deadline SLO breaks, identify the actual bottleneck, and establish headroom from that observed limit. Do this independently for generation and upscale; they may have different concurrency and resource constraints.
The operating model determines which team owns that headroom. There is no default winner.
| Model | Platform team owns | Suitable when | The catch is |
|---|---|---|---|
| Managed API | Evaluation, adapter, queue, asset verification, policy integration, and dependency budgets | Demand varies and the team wants to avoid operating model serving | External quotas, policy changes, and capability lifecycle remain dependencies |
| Self-hosted inference | All application controls plus accelerators, serving, scaling, artifacts, safety operations, and upgrades | Utilization is sustained and the organization can staff inference operations | Spare capacity, rollouts, and incident response become on-call work |
| Split path | Two adapters, routing policy, cross-path evaluation, and reconciliation | A measured continuity objective justifies independent paths | Test combinations and operational surface grow quickly |
This is a buy-versus-build review with an SLO attached. A managed path is not suitable when its data boundary, deployment isolation, required controls, or dependency objective cannot satisfy policy. Self-hosting is not suitable when accelerator capacity and model-serving incidents have no durable owner. Stick with one narrow managed adapter when traffic is uneven and dependency risk fits the error budget; consider self-hosting only when measured utilization, control requirements, staffing, and upgrade ownership make the extra system rational. A split design earns its keep only if the continuity objective is valuable enough to fund two integrations and continuous equivalence testing.
Cost belongs in the same worksheet, but inference charges alone are a misleading numerator. Include rejected generations, upscale work, storage, egress, accelerator idle capacity, reviewer time, evaluation runs, integration maintenance, and on-call ownership. Use your observed acceptance rate and traffic distribution in the model. Don't publish a universal cost ranking from a synthetic prompt set; commercial terms move, while labor and rejection costs vary by workflow.
Before approval, write the decision as assumptions rather than adjectives: expected submissions per campaign, burst concurrency, final dimensions, acceptance target, deadline percentile, maximum queue age, data classification, recovery objective, owner, and review date. That record makes later reconsideration possible without reopening a debate about which demo looked better.
Verify releases and rehearse rollback
Treat a change to the model, provider adapter, prompt template, style reference, safety policy, crop, encoder, or upscale stage as a release. Run the frozen corpus first, compare outputs through blinded review, and allow a bounded canary only after the offline gate passes. The canary dashboard must terminate at the verified publication asset. Generation latency is diagnostic; time to readable, correctly sized, approved creative is the service indicator.
Predeclare stop conditions. Roll back when the canary spends the error budget for time to verified asset, acceptance drops below the approved baseline, queue age breaches its limit, or reconciliation finds an invalid ready state. Route new jobs to the last accepted configuration through versioned configuration, while in-flight work follows a documented finish-or-expire rule. Never make rollback depend on editing code during the incident.
Run the rollback before launch. Verify that the prior configuration still has capacity, that its credentials and model reference remain valid, that old and new request envelopes can be read, and that replay cannot publish twice. Preserve requests, checksums, transform versions, reviewer outcomes, and state transitions long enough to separate creative variance from adapter, crop, storage, or publishing changes during review. If the alternate path cannot meet the same final-asset gate, it is not a rollback target; it is another experiment.
The final selection should remain conditional. Choose the API and operating model that pass this workload's quality, resolution, style-control, upscale, data, capacity, and recovery gates, then rerun the decision when the briefs, placements, traffic shape, policy boundary, or staffing model changes.
Top comments (0)