GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-12Intermediate

Automating Pre-Release UI Checks with Gemini 3.2 Flash Vision — A Personal Dev Story

How I used Gemini 3.2 Flash's multimodal input to automatically QA iOS/Android app screenshots before each release. Detect text overflow, dark mode contrast issues, and layout breaks with a 30-line Python script — and why this works for indie developers without a QA team.

Gemini 3.2 Flashmultimodal44mobile appQAscreenshotindie dev5Python38

Every few releases, something slips through. A button label that wraps on iPhone SE. A white text on white background in dark mode that nobody caught before shipping.

I've been building and maintaining wallpaper and wellness apps since 2014 — they've reached over 50 million downloads combined — and release QA has always been the part that doesn't scale well when you're working alone. The number of device-size and dark/light mode combinations grows, but the hours in the day don't.

What finally helped me plug most of those gaps was Gemini 3.2 Flash's vision capability. Pass it a screenshot, ask if anything looks wrong, and it gives you a surprisingly specific answer.

What I Wanted to Catch

Before explaining the code, here's what I was actually looking for:

  • Text clipping and overflow: Labels that escape their containers when the user's font size is set to large
  • Dark mode contrast failures: The white-text-on-white-background scenario I embarrassingly shipped once
  • Safe area violations: Elements overlapping the status bar or home indicator

Checking these manually across a matrix of device × dark/light × locale means reviewing dozens of screenshots. Handing them to Gemini is faster, and it doesn't get tired.

The Basic Implementation

This is the core of what I built — one screenshot in, a structured review out.

import google.generativeai as genai
from pathlib import Path
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-3.2-flash")
 
def review_screenshot(image_path: str, context: str = "") -> dict:
    """
    Review a single app screenshot for UI issues.
    Pass 'context' to describe the screen (e.g. "Settings screen, dark mode").
    Better context = better accuracy.
    """
    image_data = Path(image_path).read_bytes()
    
    prompt = f"""
This is a mobile app screenshot.
{f"Screen description: {context}" if context else ""}
 
Check the UI quality for the following:
1. Text clipping or overflow outside its container
2. Element overlap or layout breaks
3. Insufficient contrast between text and background (especially in dark mode)
4. Buttons or tap targets that appear too small
 
If you find issues, list them. If everything looks fine, say so.
Respond in JSON: {{"status": "ok" or "issues_found", "issues": ["..."]}}
"""
    
    response = model.generate_content([
        prompt,
        {"mime_type": "image/png", "data": image_data}
    ])
    
    import json, re
    json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
    if json_match:
        return json.loads(json_match.group())
    return {"status": "parse_error", "issues": [response.text]}
 
# Usage
result = review_screenshot(
    "screenshots/settings_dark_iphone15pro.png",
    context="Settings screen, dark mode, iPhone 15 Pro"
)
print(result)
# {"status": "issues_found", "issues": ["Label on row 3 appears to extend beyond its container"]}

Batch Processing Before a Release

Once the single-image version worked, I added batch processing to handle a full release screenshot set.

import google.generativeai as genai
from pathlib import Path
import json, re, time
from typing import NamedTuple
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
model = genai.GenerativeModel("gemini-3.2-flash")
 
class ScreenshotCheckResult(NamedTuple):
    file: str
    status: str
    issues: list[str]
 
def batch_review(screenshot_dir: str, config_json: str = None) -> list[ScreenshotCheckResult]:
    """
    Review all PNG screenshots in a directory.
    config_json: {"filename": "screen description"} mapping for better accuracy.
    """
    configs = {}
    if config_json:
        with open(config_json) as f:
            configs = json.load(f)
    
    screenshots = sorted(Path(screenshot_dir).glob("*.png"))
    results = []
    
    for i, path in enumerate(screenshots):
        context = configs.get(path.name, "")
        print(f"[{i+1}/{len(screenshots)}] Checking {path.name}...")
        
        image_data = path.read_bytes()
        prompt = f"""
Mobile app screenshot QA review.
{f"Screen: {context}" if context else ""}
 
Check for:
- Text clipping or overflow
- Layout breaks or element overlap
- Contrast issues (especially dark mode)
- Tap target sizing problems
 
JSON response only: {{"status": "ok" or "issues_found", "issues": ["..."]}}
"""
        
        try:
            response = model.generate_content([
                prompt,
                {"mime_type": "image/png", "data": image_data}
            ])
            json_match = re.search(r'\{.*\}', response.text, re.DOTALL)
            if json_match:
                data = json.loads(json_match.group())
                results.append(ScreenshotCheckResult(
                    file=path.name,
                    status=data.get("status", "unknown"),
                    issues=data.get("issues", [])
                ))
        except Exception as e:
            results.append(ScreenshotCheckResult(
                file=path.name, status="error", issues=[str(e)]
            ))
        
        # Rate limit buffer
        if i < len(screenshots) - 1:
            time.sleep(1)
    
    return results
 
def print_report(results: list[ScreenshotCheckResult]):
    ok_count = sum(1 for r in results if r.status == "ok")
    issue_count = sum(1 for r in results if r.status == "issues_found")
    
    print(f"\n{'='*50}")
    print(f"Screenshot QA Report")
    print(f"{'='*50}")
    print(f"✅ No issues: {ok_count}")
    print(f"❌ Issues found: {issue_count}")
    
    for r in results:
        if r.status == "issues_found":
            print(f"\n📱 {r.file}")
            for issue in r.issues:
                print(f"  → {issue}")
 
# Run it
results = batch_review("./release_screenshots/", "./screenshot_configs.json")
print_report(results)

Your screenshot_configs.json looks like this:

{
  "home_light_iphone15.png": "Home screen, light mode, iPhone 15",
  "home_dark_iphone15pro.png": "Home screen, dark mode, iPhone 15 Pro",
  "settings_light.png": "Settings screen, light mode",
  "settings_dark.png": "Settings screen, dark mode"
}

What I Learned After Three Months of Using This

Accuracy is better than I expected for the things that matter most. It won't catch a 2px misalignment, but the "white text on white background" class of problems — the ones that actually land in your crash reports and reviews — it catches reliably. That covers around 90% of the visual bugs I used to ship.

Context descriptions are the biggest lever. The single most impactful improvement I made was adding proper screen descriptions to each file. Without knowing it's a dark mode screenshot, Gemini sometimes interprets it as light mode and misses contrast issues entirely. Always include light mode or dark mode in the context string.

Why Gemini 3.2 Flash specifically. I tested this on 2.5 Flash first. Both work, but 3.2 Flash gives more specific descriptions of what's wrong — sometimes including which UI element is affected. The cost difference between the two is small enough that 3.2 Flash is the better choice here.

One image per request. You can technically pass multiple images in one API call, but the responses get mixed together and accuracy drops. One image per request is slower but consistently more accurate.

Hooking It Into GitHub Actions

Here's the GitHub Actions config I use for release branches:

# .github/workflows/ui-qa.yml
name: UI Screenshot QA
 
on:
  workflow_dispatch:
  push:
    branches: [release/*]
 
jobs:
  screenshot-qa:
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Capture screenshots
        run: |
          xcodebuild test \
            -scheme "MyApp" \
            -destination "platform=iOS Simulator,name=iPhone 15" \
            -testPlan "ScreenshotCapture"
          
      - name: Run Gemini Vision QA
        env:
          GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
        run: |
          pip install google-generativeai --quiet
          python scripts/qa_screenshots.py ./screenshots/ --config screenshot_configs.json
          
      - name: Upload QA report
        uses: actions/upload-artifact@v4
        with:
          name: qa-report
          path: qa_report.txt

The report artifact only matters when something fails, so it doesn't add noise to the normal workflow.

Where This Doesn't Help

I want to be honest about the limits. This approach doesn't catch animation problems, interaction state issues, or anything that requires actually tapping through the app. VoiceOver accessibility testing also can't be done from a static screenshot.

Think of it as automating the part of your QA that currently means scrolling through screenshots in Finder. It's not a replacement for XCTest or Espresso — it's what happens before those, when you're deciding whether the build is worth deeper testing at all.

Where to Start

Set up the single-image version first and run it on your last 10 screenshots. See what Gemini catches and whether it matches what you would have flagged manually. Once you have a feel for how it interprets your app's UI style, adding the batch runner takes another 15 minutes.

The script is small enough that the real investment is building screenshot_configs.json with accurate descriptions for your screens — that's where the quality improvement actually comes from.


Related reading:

Share

Thank You for Reading

Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-03-28
Lyria 3 Pro API Complete Implementation Guide — Generate Professional Full-Length Tracks from Text and Images
Learn how to generate full-length music tracks using Google DeepMind's Lyria 3 Pro. Covers Clip/Pro/RealTime model differences, Interactions API, prompt engineering, and monetization strategies.
API / SDK2026-07-18
How Well Does Omni Flash Hear 'Rotate the Camera 30 Degrees Right'? Measuring Where Conversational Edits Land
Public-preview Gemini Omni Flash lets you re-edit a generated video in plain language. 'Make the lighting evening' lands; 'rotate the camera 30 degrees' often misses. Here is a running log of where instructions land, sorted mechanically by comparing before/after frames.
API / SDK2026-07-16
I Asked Gemini to Grade My App Store Screenshots. Everything Scored 78–85.
Ask Gemini Vision to grade App Store screenshots out of 100 and good candidates and deliberately broken ones both land at 78–85. Here is how I measured the judge's discrimination power, dropped absolute scoring, and rebuilt it as debiased pairwise comparison.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →