Skip to main content
valen

Score photos once, weigh them per storefront

A marketplace asks sellers for a good main photo. "Is this a good photo?" hides what makes it good, and different storefronts care about different things. Valen scores each photo once on three dimensions; code combines them with weights each storefront can change without another model call.

StudioKitchen counterDimCropped
Mug on a studio backdropMug on a cluttered counterMug in dim lightMug cut off at the edge

How it works​

Three score questions, each with three levels described as situations a person could recognize:

DimensionLevels
lightingpoor · fair · good
backgroundcluttered · plain · studio
framingcut off · loose · centered

A score runs from 0 to 2 over three levels; code divides by 2 so each dimension is on 0–1, then takes a weighted sum per storefront:

Profilelightingbackgroundframing
marketplace0.30.20.5
premium_shop0.30.50.2

Run it​

Download the bundle once. The recorded answers in caches/composite_scoring.json let the script run without an API key; --replay off calls the free hosted API instead.

Bash
curl -fLO https://valen-team.github.io/cookbook/valen-cookbook.zip
python -m zipfile -e valen-cookbook.zip .
cd valen-cookbook && python -m pip install -r requirements.txt
python composite_scoring.py
python composite_scoring.py --replay off --base-url https://api.valen-team.workers.dev/v1

Code​

composite_scoring.py
"""Score listing photos once per dimension, then let code weigh the dimensions for each storefront. Apache-2.0."""
import json

from common import HERE, client, emit, image_bytes, image_input, parser, request

DIMENSIONS = {
"lighting": ("How well lit is the mug?", [
("poor", "Dark, heavily shadowed or color-cast; the glaze is hard to see"),
("fair", "Ordinary indoor light; the glaze is visible but flat"),
("good", "Bright, even light that shows the glaze clearly")]),
"background": ("How clean is the background behind the mug?", [
("cluttered", "Other household objects compete with the mug"),
("plain", "A table or cloth with nothing else in view"),
("studio", "A seamless studio backdrop")]),
"framing": ("How much of the mug is in the frame?", [
("cut_off", "Part of the mug, such as the handle, is outside the frame"),
("loose", "The whole mug is visible but small or off-center"),
("centered", "The whole mug is centered and fills the frame")]),
}
# Weights are business choices in code; changing them never needs another model call.
PROFILES = {"marketplace": {"lighting": 0.3, "background": 0.2, "framing": 0.5},
"premium_shop": {"lighting": 0.3, "background": 0.5, "framing": 0.2}}


def score(predict, photo):
questions = [{"name": name, "type": "score", "instructions": text,
"levels": [{"label": label, "description": description} for label, description in levels]}
for name, (text, levels) in DIMENSIONS.items()]
answers = request(predict, image_input(image_bytes(HERE / "media" / photo)), questions)
# Scores run 0-2 over three levels; divide by 2 so every dimension is on 0-1.
return {"photo": photo, **{name: answers[name]["score"] / 2 for name in DIMENSIONS}}


def rank(scores, weights):
total = lambda s: round(sum(weights[name] * s[name] for name in DIMENSIONS), 3)
return sorted(({"photo": s["photo"], "score": total(s)} for s in scores), key=lambda x: x["score"], reverse=True)


def main():
args = parser(__doc__).parse_args()
photos = json.loads((HERE / "composite_scoring_data.json").read_text())
with client(args) as predict:
scores = [score(predict, photo) for photo in photos]
emit({"dimensions": scores, "rankings": {name: rank(scores, w) for name, w in PROFILES.items()}}, args)


if __name__ == "__main__":
main()

Output​

The kitchen-counter photo is well lit and fully framed, so the marketplace ranks it second; the premium shop weighs the clutter and ranks it last.

Recorded output
{
"dimensions": [
{
"photo": "mug-studio.jpg",
"lighting": 0.9991636368077078,
"background": 0.9938775610787292,
"framing": 0.9752801720575722
},
{
"photo": "mug-counter.jpg",
"lighting": 0.9860608196005157,
"background": 0.006156532209375251,
"framing": 0.9074895660257324
},
{
"photo": "mug-dim.jpg",
"lighting": 0.17881670806358135,
"background": 0.641231147103601,
"framing": 0.8176063590643421
},
{
"photo": "mug-cropped.jpg",
"lighting": 0.9932250863251958,
"background": 0.5433575647609814,
"framing": 0.29707197970072985
}
],
"rankings": {
"marketplace": [
{
"photo": "mug-studio.jpg",
"score": 0.986
},
{
"photo": "mug-counter.jpg",
"score": 0.751
},
{
"photo": "mug-dim.jpg",
"score": 0.591
},
{
"photo": "mug-cropped.jpg",
"score": 0.555
}
],
"premium_shop": [
{
"photo": "mug-studio.jpg",
"score": 0.992
},
{
"photo": "mug-cropped.jpg",
"score": 0.629
},
{
"photo": "mug-dim.jpg",
"score": 0.538
},
{
"photo": "mug-counter.jpg",
"score": 0.48
}
]
}
}

Make it yours​

  • Store the per-dimension scores, not only the total. New weights, filters or a learned model can use them later without calling Valen again.
  • The photos are generated for this recipe.