{"slug": "18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand", "title": "18 AI wallpapers for my game, and the three things I had to fix by hand", "summary": "A developer behind the browser game Cá Viên Chiên generated 36 AI wallpapers (18 scenes in both wide and tall compositions) using Codex image generation, then upscaled them 4× with Real-ESRGAN and processed them with sharp into 162 JPEG downloads across 4K, 2K and phone sizes for release 1.38.0. The developer reports three hand fixes were needed: generating separate tall compositions because a 9:19.5 phone crop keeps only 31% of a landscape image versus 69% of a portrait one, and manually retouching seven images where the model produced near-text scribbles that became visible after upscaling.", "body_md": "[Cá Viên Chiên](https://cavienchien.net/en/game/) is a small browser game about running a Vietnamese fried fish ball cart. The game already had painted art (the stall, 44 customers, ten night scenes behind the game page), so when I wanted something people could take home, wallpapers were the obvious gift: nine pictures of the cart's world and nine of Vietnam at night.\n\nThe [wallpapers page](https://cavienchien.net/en/wallpapers/) went live in release 1.38.0. Each wallpaper has its own page with a short story, a phone preview and nine downloads: 4K (3840 × 2160), 2K (2560 × 1440) and phone (1290 × 2796), each with a small logo, a QR code to the game, or no watermark. That's 162 JPEG files from 36 generated images.\n\nThe pipeline itself is short:\n\n```\nCodex image generation (1536 × 1024 and 1024 × 1536, one at a time)\n  → my eyes, at full size\n  → retouch.sh (a feathered blur over one box, original kept)\n  → Real-ESRGAN 4× (realesrgan-ncnn-vulkan, local GPU)\n  → sharp: cover-resize, watermark, JPEG q84 → public/wallpapers/\n```\n\nThree parts of it didn't work the way I first planned.\n\nMy first plan was cheap: generate 18 wide pictures, upscale them, and cut the phone version out of the middle. I did the arithmetic before spending any image quota on it, and that saved the plan from itself.\n\nA phone lock screen is 9 : 19.5. Out of a 3 : 2 picture, a full-height 9 : 19.5 window keeps `(9 / 19.5) / (3 / 2) = 0.31` of the width. Out of a 2 : 3 picture drawn tall, it keeps `(9 / 19.5) / (2 / 3) = 0.69`.\n\n📷 IMAGE: upload phone-crop-31-vs-69.webp here, image description: \"Left: a wide painting of a fish ball cart with a narrow yellow 9:19.5 window over its middle, labelled 'From the wide picture: 31 % of its width'; the cart is cut in half. Right: the same scene drawn tall, the window keeps most of it, labelled 'Drawn tall: 69 %'.\"\n\n31 % of a landscape is a sliver: half a cart, a slice of sky. So every wallpaper was generated twice, wide and tall, with a different composition prompt for each. The tall prompt asks for \"a calm uncluttered area across the top 30 % where the clock will sit\" and \"nothing important within 5 % of the left and right edges\", because the phone version still gets a cover crop from 2 : 3 down to 9 : 19.5. It doubled the generation count from 18 to 36, which is a real cost when every image comes out of a usage quota. It ran out once, at image 29, and the rest waited for the reset.\n\nEvery prompt ended with the same line: \"Absolutely no text, no letters, no numbers, no signs with writing, no logos\". At thumbnail size, all 36 images obeyed. At full size, seven of them didn't:\n\nNone of it is readable. That's the problem: it's *almost* text, and after a 4× upscale \"almost\" becomes very visible on a 27-inch screen.\n\n📷 IMAGE: upload pseudo-text-temple-board.webp here, image description: \"Zoomed crop of a temple gate at night. Left, labelled 'Generated': a glowing name board with letter-like scribbles. Right, labelled 'Retouched': the same board as a soft plain glow.\"\n\nRegenerating would cost quota and could just move the scribble somewhere else, so I wrote a tiny retouch step instead. It builds a mask with rounded white boxes, feathers it with a blur, and composites a blurred copy of the picture through it:\n\n```\nfix() {\n  local id=\"$1\" blur=\"$2\"; shift 2\n  [ -f \"orig-$id.png\" ] || cp \"$id.png\" \"orig-$id.png\"     # (a later version also handles redraws)\n  local size; size=\"$(magick identify -format '%wx%h' \"orig-$id.png\")\"\n  local draw=(); for box in \"$@\"; do draw+=(-draw \"roundrectangle $box 4,4\"); done\n  magick -size \"$size\" xc:black -fill white \"${draw[@]}\" -blur 0x5 .mask.png\n  magick \"orig-$id.png\" -blur \"0x$blur\" .mask.png -alpha off -compose CopyOpacity -composite .layer.png\n  magick \"orig-$id.png\" .layer.png -compose Over -composite \"$id.png\"\n}\nfix hoan-kiem-tall 6 \"290,770 332,794\"                                          # the gate's name board\nfix tet-stall-tall 6 \"668,712 704,748\" \"750,676 778,704\" \"796,800 824,828\"     # three ornaments\n```\n\nMy first attempt passed the mask as a third image to a plain `-composite`, and ImageMagick blurred the whole picture. `CopyOpacity` puts the mask into the blurred layer's alpha first, so the final `Over` only shows the blur inside the boxes.\n\n📷 IMAGE: upload pseudo-text-tet-ornaments.webp here, image description: \"Three red diamond-shaped Tết ornaments. Left, labelled 'Generated': each has a gold pseudo-character in the middle. Right, labelled 'Retouched': plain red diamonds with their gold border.\"\n\n📷 IMAGE: upload pseudo-text-plates.webp here, image description: \"Rear of three parked motorbikes. Left, labelled 'Generated': number plates with fake letters and digits. Right, labelled 'Retouched': blank, softly blurred plates.\"\n\nThe only check that found these was looking: each image at full size, then zoomed crops of every sign, plate and board. An automated test can confirm that a file exists and has the right size, but it can't tell you that a temple says \"HENYOD\".\n\nThe step after the retouch was the slow one. Real-ESRGAN takes a few seconds per image on the GPU, so my build script cached its output next to the source:\n\n``` js\n// before\nfunction upscaled(file) {\n  const out = file.replace(/\\.png$/, '@4x.png');\n  if (!existsSync(out)) execFileSync(ESRGAN, ['-i', file, '-o', out, /* … */]);\n  return out;\n}\n```\n\nThe previews on the site are made straight from the source PNG; the 4K, 2K and phone downloads are made from the `@4x` file. I retouched the Saigon skyline *after* its first build, so the previews showed the clean signs and the downloads kept the scribbles. Every check I had run looked at the pages, and the pages were right.\n\nAn independent review of the diff caught it before the release: inside the six retouched boxes, the downscaled upscale was 24.6 levels per channel away from the retouched source and only 9.0 from the original. Every other retouched image measured 2–5 against its source. The fix is to treat the upscale as stale when the source is newer:\n\n```\n// after\nif (!existsSync(out) || statSync(file).mtimeMs > statSync(out).mtimeMs) { /* upscale again */ }\n```\n\nI also dropped the cached upscale of every retouched image and rebuilt them, and the retouch step now records the hash of what it wrote, so a picture that has since been regenerated becomes the new original instead of being overwritten by an old blurred copy.\n\n`https://cavienchien.net` in a unit test.`ImageObject` (license, `acquireLicensePage`, `creditText`) inside an `ImageGallery`, and each page's `og:image` is its own wallpaper, so a shared link shows the picture.\nThe lesson I'm keeping is about caches: if a step is slow enough to cache, its cache key has to include everything that can change its input. A file that merely *exists* was never enough, and the pages couldn't tell me, because they were built from a different input.", "url": "https://wpnews.pro/news/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand", "canonical_source": "https://dev.to/ocxers/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand-3g6c", "published_at": "2026-10-06 13:36:49+00:00", "updated_at": "2026-10-06 13:48:31.873778+00:00", "lang": "en", "topics": ["generative-ai", "ai-tools"], "entities": ["Cá Viên Chiên", "Codex", "Real-ESRGAN", "sharp"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand", "markdown": "https://wpnews.pro/news/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand.md", "text": "https://wpnews.pro/news/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand.txt", "jsonld": "https://wpnews.pro/news/18-ai-wallpapers-for-my-game-and-the-three-things-i-had-to-fix-by-hand.jsonld"}}