PosterScorer / app.py
jackstev's picture
Deploy Poster Scorer
b33be9e verified
Raw History Blame Contribute Delete
5.73 kB
"""Poster Scorer: Gradio app that scores an uploaded poster and suggests one change.
Pipeline: poster image -> OCR + pixel analysis (features.py) -> 14 numeric
features -> trained AutoGluon regressor -> PosterIQ design score on its 1-10
scale, plus one model-estimated feedback sentence (poster_inference.py).
"""
from pathlib import Path
from threading import Lock
import gradio as gr
import pandas as pd
import pytesseract
from PIL import Image
from features import FEATURE_COLUMNS, MIN_SIDE_PX, extract_features
from poster_inference import PosterScorer
EXAMPLES_DIR = Path(__file__).resolve().parent / "examples"
LOWER_QUARTILE = 4.0 # Dataset quartiles of overall_design_score
UPPER_QUARTILE = 5.9
MIN_CONTENT_FRAC = 0.01 # Below this the poster is treated as blank
MAX_PIXELS = 40_000_000 # Same upload limit as api.py
# Load once per process; a broken bundle should fail startup, never fall back to a heuristic
SCORER = PosterScorer()
MODEL_SOURCE = f"Trained model: {SCORER.predictor.model_best}"
SCORING_LOCK = Lock() # OCR runs in parallel, model calls one at a time as in api.py
SCOPE_NOTE = (
"The model predicts PosterIQ overall design quality from text and layout measurements. "
"The feedback is the change the model estimates would raise its score most, "
"not a proven cause or a guaranteed improvement."
)
def design_band(score):
"""Map a 1-10 predicted design score to its dataset quartile band."""
if score < LOWER_QUARTILE:
return "Below typical"
if score < UPPER_QUARTILE:
return "Typical"
return "Above typical"
def lean_note(row):
"""Describe which side dominates, straight from the measurements."""
if row["word_count"] == 0:
return "No readable text was detected."
if row["text_share"] > 0.6:
return "Leans text-heavy."
if row["text_share"] < 0.2:
return "Leans graphic-heavy."
return "Text and graphics are roughly comparable in area."
def empty_outputs(message):
"""Every early exit returns the same six outputs as a success."""
return None, "", "", pd.DataFrame(columns=["feature", "value"]), None, message
def score_poster(image):
"""Callback: poster image in, score + verdict + feedback + features + overlay + status out."""
if image is None:
return empty_outputs("Upload a poster image, then select Score poster.")
if not isinstance(image, Image.Image):
return empty_outputs("Use a PNG, JPG, or WEBP image file.")
if min(image.size) < MIN_SIDE_PX:
return empty_outputs(f"Image is too small; use at least {MIN_SIDE_PX}px on the shortest side.")
if image.width * image.height > MAX_PIXELS:
return empty_outputs("Image is too large; use at most 40 million pixels.")
try:
row, overlay = extract_features(image) # OCR once; the scorer reuses these measurements
except pytesseract.TesseractNotFoundError:
return empty_outputs("OCR engine not found. Install Tesseract or set TESSERACT_CMD.")
# Long format table reads better than one very wide row
table = pd.DataFrame({"feature": FEATURE_COLUMNS, "value": [round(float(row[c]), 4) for c in FEATURE_COLUMNS]})
if row["text_area_frac"] + row["graphic_area_frac"] < MIN_CONTENT_FRAC:
# Nearly blank input: a score here would be meaningless
return None, "", "", table, overlay, "Almost no text or graphics were detected. Check that the image is a poster."
with SCORING_LOCK:
result = SCORER.score_features(row)
score = round(result["score"], 1) # Already clipped to 1..10; round before banding so both agree
status = f"{lean_note(row)} Source: {MODEL_SOURCE}. {SCOPE_NOTE}"
return score, design_band(score), result["feedback_sentence"], table, overlay, status
with gr.Blocks(title="Poster Scorer") as demo:
gr.Markdown(
"# Poster Scorer\n"
"Upload a poster to predict its PosterIQ overall design score on a 1 to 10 scale "
"and get one suggested layout change from the model. "
"The overlay shows what was measured: **orange boxes** are detected text, "
"**blue tint** is graphic area."
)
with gr.Row():
with gr.Column():
image_in = gr.Image(type="pil", sources=["upload", "clipboard"], label="Poster")
score_btn = gr.Button("Score poster", variant="primary")
with gr.Column():
score_out = gr.Number(label="Predicted design score (1-10)", precision=1, interactive=False)
band_out = gr.Textbox(label="Verdict", interactive=False)
feedback_out = gr.Textbox(label="Model feedback", interactive=False, lines=2)
status_out = gr.Textbox(label="Interpretation", interactive=False, lines=3)
with gr.Row():
overlay_out = gr.Image(label="What the model measured", interactive=False)
table_out = gr.Dataframe(label="Extracted features (model input)", interactive=False)
outputs = [score_out, band_out, feedback_out, table_out, overlay_out, status_out]
score_btn.click(score_poster, image_in, outputs, api_name="score_poster", concurrency_limit=2)
# Clearing the input also clears stale results
image_in.clear(lambda: empty_outputs("Upload another poster."), outputs=outputs, api_name="clear_poster")
example_paths = sorted(p for p in EXAMPLES_DIR.glob("*") if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"})
if example_paths:
gr.Examples(
examples=[[str(p)] for p in example_paths], inputs=image_in, outputs=outputs,
fn=score_poster, run_on_click=True, cache_examples=False,
label="Click an example to score it",
)
if __name__ == "__main__":
demo.queue().launch()