olivas/backend/app/services/insights.py
DJP c1b80eb9a7 Replace entropy score with composite Design Effectiveness Score
The pure Shannon entropy score penalized well-designed ads with multiple
intentional visual elements (e.g. hero product + text + logo scored ~8/100).

New composite score (0-100) weights four components:
- Peak Dominance (30%): strength of #1 hotspot vs rest
- Hierarchy Clarity (25%): monotonic intensity ordering
- Gaze Coherence (25%): smooth spatial gaze path
- Entropy Concentration (20%): sqrt-softened entropy

The raw entropy score is preserved as entropy_score for users who want it,
visible in the ScoreCard hover tooltip and PDF report.

Also adds auto-create DB tables on startup for fresh Docker deploys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 22:50:12 -05:00

201 lines
7.3 KiB
Python

"""Rule-based insights engine for visual attention analysis.
Generates actionable text insights from analysis metrics —
no AI needed, just conditional logic on the computed data.
"""
def generate_insights(analysis) -> list[dict]:
"""Generate insights from a completed analysis.
Args:
analysis: Analysis ORM object with overall_score, hotspots, gaze_sequence,
image_width, image_height.
Returns:
List of dicts with keys: type (info|success|warning), title, description.
"""
insights = []
score = analysis.overall_score
hotspots = analysis.hotspots or []
gaze_seq = analysis.gaze_sequence or []
w = analysis.image_width
h = analysis.image_height
if score is None or not hotspots:
return insights
# 1. Design Effectiveness
if score >= 55:
insights.append({
"type": "success",
"title": "Strong design effectiveness",
"description": (
f"Design effectiveness is high (score {score:.0f}/100). "
"Your design has a clear visual hierarchy with a dominant focal point "
"and coherent gaze flow."
),
})
elif score >= 35:
insights.append({
"type": "info",
"title": "Moderate design effectiveness",
"description": (
f"Design effectiveness is moderate (score {score:.0f}/100). "
"Viewers will notice several areas. Consider whether your primary message "
"is prominent enough to stand out from competing elements."
),
})
else:
insights.append({
"type": "warning",
"title": "Low design effectiveness",
"description": (
f"Design effectiveness is low (score {score:.0f}/100). "
"No single element dominates and attention may scatter without clear hierarchy. "
"Consider increasing contrast, size, or whitespace around your hero element."
),
})
# 2. Dominant Focal Point
if len(hotspots) >= 2:
top = hotspots[0]["intensity"]
second = hotspots[1]["intensity"]
if top > 0 and second > 0:
ratio = top / second
if ratio >= 3:
insights.append({
"type": "success",
"title": "Clear dominant element",
"description": (
f"The top hotspot is {ratio:.1f}x stronger than the second — "
"your design has one unmistakable focal point. This is ideal for "
"ads with a single hero product or CTA."
),
})
elif ratio >= 1.5:
insights.append({
"type": "info",
"title": "Moderate focal dominance",
"description": (
f"The top hotspot is {ratio:.1f}x stronger than the second. "
"There's a primary focus but competing elements may split attention."
),
})
else:
insights.append({
"type": "warning",
"title": "Competing focal points",
"description": (
"The top two hotspots have similar intensity — viewers' eyes will "
"bounce between them. If one element is your priority, consider "
"making it larger, brighter, or more isolated."
),
})
# 3. Gaze Entry Point
if gaze_seq:
first = gaze_seq[0]
x_pct = first.get("x_pct", first["x"] / w * 100 if w else 50)
y_pct = first.get("y_pct", first["y"] / h * 100 if h else 50)
if y_pct < 33:
v_zone = "top"
elif y_pct < 66:
v_zone = "middle"
else:
v_zone = "bottom"
if x_pct < 33:
h_zone = "left"
elif x_pct < 66:
h_zone = "center"
else:
h_zone = "right"
position = f"{v_zone}-{h_zone}"
insights.append({
"type": "info",
"title": f"First fixation: {position}",
"description": (
f"Viewers are predicted to look at the {position} area first "
f"({x_pct:.0f}% from left, {y_pct:.0f}% from top). "
"Place your most important message or brand element here for maximum impact."
),
})
# 4. Spatial Balance
if len(hotspots) >= 3:
quadrants = {"TL": 0, "TR": 0, "BL": 0, "BR": 0}
for hs in hotspots:
cx = hs.get("center_x", hs["x"])
cy = hs.get("center_y", hs["y"])
q_h = "L" if cx < w / 2 else "R"
q_v = "T" if cy < h / 2 else "B"
quadrants[q_v + q_h] += 1
max_q = max(quadrants.values())
max_q_name = {
"TL": "top-left", "TR": "top-right",
"BL": "bottom-left", "BR": "bottom-right",
}
dominant = [k for k, v in quadrants.items() if v == max_q]
if max_q >= 3:
zone = max_q_name[dominant[0]]
insights.append({
"type": "warning",
"title": f"Attention clusters in {zone}",
"description": (
f"{max_q} of {len(hotspots)} hotspots fall in the {zone} quadrant. "
"The opposite areas of your design may go largely unnoticed. "
"Consider rebalancing if key information is in the neglected zones."
),
})
# 5. Edge Risk
edge_threshold = 0.10 # 10% from edge
edge_hotspots = []
for hs in hotspots[:3]: # check top 3
cx = hs.get("center_x", hs["x"])
cy = hs.get("center_y", hs["y"])
near_edge = (
cx < w * edge_threshold
or cx > w * (1 - edge_threshold)
or cy < h * edge_threshold
or cy > h * (1 - edge_threshold)
)
if near_edge:
edge_hotspots.append(hs["rank"])
if edge_hotspots:
ranks = ", ".join(f"#{r}" for r in edge_hotspots)
insights.append({
"type": "warning",
"title": "Key attention near edge",
"description": (
f"Hotspot {ranks} {'is' if len(edge_hotspots) == 1 else 'are'} close to the "
"image edge. When printed or cropped, this attention area may be partially cut off. "
"Consider adding safe margins around critical content."
),
})
# 6. Attention Drop-off
if len(hotspots) >= 3:
intensities = [hs["intensity"] for hs in hotspots[:5]]
if intensities[0] > 0:
dropoff = intensities[-1] / intensities[0]
if dropoff < 0.2:
insights.append({
"type": "info",
"title": "Steep attention drop-off",
"description": (
f"Attention drops sharply from hotspot #1 ({intensities[0]:.0%}) to "
f"#{len(intensities)} ({intensities[-1]:.0%}). This means the design "
"has a very strong primary focus but secondary elements get little attention."
),
})
return insights