Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7rnC7jppGGdHGESvy
46 lines
4.4 KiB
Markdown
46 lines
4.4 KiB
Markdown
# image-gen
|
|
|
|
Companion code for [Image Generation and Vision in Spring AI: ImageModel, Prompt Options and Safe Storage](https://ankurm.com/spring-ai-image-generation-imagemodel-s3-minio-safe-storage/), part of the [Spring AI series](../README.md) on ankurm.com. It is the generation half of [Multimodal Spring AI: Extract Structured Data from Images](https://ankurm.com/multimodal-spring-ai-extract-structured-data-from-images-receipts-java-records/).
|
|
|
|
A prompt goes through our own checks, into `ImageModel`, and the bytes that come back are verified and written to an S3-compatible bucket under a key we choose.
|
|
|
|
**No image provider was called.** The tests use two hand-written fakes -- `ScriptedImageModel` (an `ImageModel` that returns what it was told to) and `ScriptedModerationModel` -- and, for the wire tests, the real `OpenAiImageModel` against a local mock of `/v1/images/generations` (`FakeImagesServer`) that returns a fixed 64x64 PNG whatever the prompt says. Nothing here measures image quality, latency, or whether a real model would refuse a prompt. **The storage is real:** the `ImageStore` tests run against a MinIO server in Docker through Testcontainers (the image is `chainguard/minio`, an upstream MinIO build, because `minio/minio` and `quay.io/minio/minio` could not be pulled from the machine these were captured on; the exact version is in `output/05-store-minio.txt`). Prices in `CostModel` were copied from the providers' pages on 2026-10-28 and are not checked by any test.
|
|
|
|
| | |
|
|
|---|---|
|
|
| Spring Boot | 4.1.2 (parent; GA on Maven Central 2026-10-22) |
|
|
| Spring AI | 2.0.2 (`spring-ai-model`, `spring-ai-openai`, `spring-ai-stability-ai`, `spring-ai-google-genai-image`) |
|
|
| AWS SDK for Java | 2.57.5 (`s3`) |
|
|
| Testcontainers | 2.0.5 |
|
|
| Java | 25 |
|
|
| Run | `scripts/run-all.sh` (about 20 s, needs Docker for the MinIO tests) |
|
|
|
|
## Source
|
|
|
|
| File | What it is |
|
|
|---|---|
|
|
| [`ImageService`](src/main/java/com/ankurm/imagegen/ImageService.java) | prompt gate -> `ImageModel` -> bytes -> verify -> store; one bad image in a batch stores nothing |
|
|
| [`PromptGate`](src/main/java/com/ankurm/imagegen/PromptGate.java) | length and control-character checks, then `ModerationModel`; fails closed when moderation is down |
|
|
| [`OpenAiOptionsFactory`](src/main/java/com/ankurm/imagegen/OpenAiOptionsFactory.java) | size / quality / n checked against allow-lists, then `OpenAiImageOptions` with the model set explicitly |
|
|
| [`ImageSniffer`](src/main/java/com/ankurm/imagegen/ImageSniffer.java), [`ImageVerifier`](src/main/java/com/ankurm/imagegen/ImageVerifier.java), [`ImagePolicy`](src/main/java/com/ankurm/imagegen/ImagePolicy.java) | magic-number sniffing, size cap, pixel cap read from the header, a real decode, allow-list of types |
|
|
| [`RemoteFetcher`](src/main/java/com/ankurm/imagegen/RemoteFetcher.java) | downloads a provider-returned URL: https and listed hosts only, no redirects, hard byte cap |
|
|
| [`ImageStore`](src/main/java/com/ankurm/imagegen/ImageStore.java) | S3/MinIO writer: server-chosen key, pinned `Content-Type`, `Content-Disposition: attachment`, presigned GET |
|
|
| [`CostModel`](src/main/java/com/ankurm/imagegen/CostModel.java) | per-image prices with the date they were read and the page they came from |
|
|
|
|
## Captured output (`output/`)
|
|
|
|
| File | Shows |
|
|
|---|---|
|
|
| `01-api-shape.txt` | class names, portable vs provider options, what is *not* there (moderation option, image moderation) |
|
|
| `02-wire-requests.txt` | JSON body sent by the real `OpenAiImageModel` for six option shapes, including the model reset and the `ImagePrompt(List)` failure |
|
|
| `03-wire-response.txt` | what `ImageResponse` carries back (b64 or URL, revised prompt, no usage) |
|
|
| `04-wire-refusal.txt` | a provider 400 `content_policy_violation` arrives as an SDK exception |
|
|
| `05-store-minio.txt`, `05b-presigned.txt` | the object as MinIO holds it; presigned URL behaviour |
|
|
| `06-verifier.txt`, `07-trailing-bytes.txt`, `08-base64-cap.txt` | twelve inputs against the verifier; the limit of sniffing; base64 refused before decoding |
|
|
| `09-fetcher.txt` | redirect, scheme, host list, endless body |
|
|
| `10-keys.txt`, `11-naive-vs-pinned.txt` | owner and id validation; trusting the sender's `Content-Type` |
|
|
| `12-service-happy.txt` .. `15-url-results.txt` | the service end to end; refusals before any paid call; a batch with one bad image; URL results |
|
|
| `16-cost.txt` | the per-image price table |
|
|
|
|
Keys, ports and timestamps differ between runs; everything else is stable.
|