◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-07-16Advanced

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.

Gemini API249Vision4ASOApp Store6LLM-as-judgePairwise ComparisonPython51Indie Dev7

✦ Premium Article

Before replacing all five of my App Store screenshots, I asked Gemini to grade the new set. It came back with 82. I fed it the old set as a control: 79.

Three points apart. That told me nothing, so I built a deliberately broken version — every headline erased, contrast crushed, panels shuffled — and fed that in too.

If a set with zero readable copy earns a 76, then 82 means nothing either. That was the day I stopped asking Vision for a score.

What follows is how I proved the grader was unusable at my decision granularity, and what I replaced it with. As an indie developer who has rebuilt a wallpaper app's store listing more times than I'd like to admit, this was the single change that made the tooling worth running.

The minimum setup for doubting your own grader

pip install google-genai pillow --break-system-packages
import os
import json
import statistics
from pathlib import Path
from itertools import combinations
 
from google import genai
from google.genai import types
 
client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
 
# Verification burns a lot of calls, so Flash. Only the final head-to-head goes to Pro.
JUDGE_MODEL = "gemini-flash-latest"
 
 
def load_image(path: str) -> types.Part:
    """Load a local image into a Part, inferring MIME from the extension."""
    suffix = Path(path).suffix.lower()
    mime = {
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".png": "image/png",
        ".webp": "image/webp",
    }.get(suffix, "image/jpeg")
 
    with open(path, "rb") as f:
        return types.Part.from_bytes(data=f.read(), mime_type=mime)

One caveat on that model string. gemini-flash-latest is an alias, and as of July 2026 it resolves to Gemini 3.5 Flash. If you run scheduled jobs against an alias, the thing doing your grading can change underneath you without a single line of your code moving. Pin down what the alias currently points at before you trust any measurement you take today.

The grader itself is conventional. The thing under suspicion is not the function — it's the numbers coming out of it.

def score_absolute(image_paths: list[str], app_name: str, market: str = "United States") -> dict:
    """Grade a screenshot set from 0-100 (the conventional absolute approach)."""
    parts = [
        types.Part.from_text(
            f"""You are an App Store optimization expert.
Evaluate the following screenshot set.
 
App: {app_name}
Target market: {market}
 
Return exactly this JSON:
{{
  "overall_score": <integer 0-100>,
  "message_clarity": <0-100>,
  "visual_hierarchy": <0-100>,
  "reason": "one sentence justifying the score"
}}"""
        )
    ]
    for i, p in enumerate(image_paths):
        parts.append(types.Part.from_text(f"Screenshot {i + 1}:"))
        parts.append(load_image(p))
 
    resp = client.models.generate_content(
        model=JUDGE_MODEL,
        contents=parts,
        config=types.GenerateContentConfig(
            response_mime_type="application/json",
            temperature=0.2,
        ),
    )
    return json.loads(resp.text)

Measure the noise first: ten runs, one image

You cannot judge a grader from a single output. Send the same input repeatedly and find out how much the answer moves on its own.

def measure_noise(image_paths: list[str], app_name: str, n: int = 10) -> dict:
    """Grade identical input n times and measure the spread."""
    scores = []
    for i in range(n):
        result = score_absolute(image_paths, app_name)
        scores.append(result["overall_score"])
        print(f"  run {i + 1}: {result['overall_score']}")
 
    return {
        "scores": scores,
        "mean": statistics.mean(scores),
        "stdev": statistics.stdev(scores) if len(scores) > 1 else 0.0,
        "range": max(scores) - min(scores),
    }

At temperature=0.2 I expected near-identical numbers. On my set — five portrait screenshots, wallpaper category — here is what came back.

MetricMeasured
Mean81.4
Standard deviation2.3
Min–max78–85 (7-point spread)

Seven points of movement on an unchanged image. Which means the 3-point gap between my new set and my old set was sitting entirely inside the noise floor.

Dropping to temperature=0 only narrowed the spread to 3 points. Multimodal inputs carry variance in tiling and tokenization that temperature does not reach. This is not a problem you turn down the temperature to fix, so I stopped trying.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A repeatable way to decide whether an LLM judge is usable at all: measure run-to-run noise, then divide your signal gap by it
✦Ablation code that deliberately degrades your own screenshots to test what the grader can and cannot detect
✦Full pairwise comparison implementation with order-swap debiasing, plus win-rate ranking and a token cost breakdown
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
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 →

Related Articles

◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.
◈ API / SDK2026-10-08
When a Deprecation Has No Shutdown Date: Re-Picking an Image Model on Three Conditions
gemini-nano-banana-2.1 went GA on October 6, and gemini-3.1-flash-image was deprecated with no shutdown date. Here is a table and a short script for choosing a replacement by resolution, reference images, and search grounding.
◈ API / SDK2026-09-22
When Function Calls Turn Into Plain Text, Suspect the Property Names in Your Tool Declarations
A record of chasing MALFORMED_FUNCTION_CALL after output limits and schema conflicts were ruled out, bisecting nine tool declarations down to one property name, and adding a linter that stops bad names before they ship.