""" Plain-English rationales — OpenAI / HF Inference / template fallback. """ from __future__ import annotations import json import urllib.request from .config import HF_TOKEN, OPENAI_API_KEY def _level(v: float) -> str: if v >= 0.72: return "high" if v >= 0.45: return "moderate" return "low" def _template_rationale(dim: dict, laion_score: float) -> str: v = dim["value"] lvl = _level(v) dim_id = dim["id"] detail = dim.get("detail", "") templates = { "color_harmony": { "high": f"Wide hue spread with strong light–dark separation ({detail}) — reads as a structured palette.", "moderate": f"Balanced color structure ({detail}) — neither flat nor chaotic.", "low": f"Narrow palette or low contrast ({detail}) — may read as muted or monochrome.", }, "composition_balance": { "high": f"Visual mass near center ({detail}) — symmetric, stable framing.", "moderate": f"Mild asymmetry ({detail}) — subject slightly off-center; can feel dynamic.", "low": f"Strong asymmetry ({detail}) — edge-weighted or rule-breaking layout (not inherently bad).", }, "saturation_intensity": { "high": f"Vivid chroma ({detail}) — bold, energetic color presence.", "moderate": f"Moderate vividness ({detail}) — controlled color energy.", "low": f"Muted or achromatic ({detail}) — restrained, minimal, or grayscale palette.", }, "edge_complexity": { "high": f"Rich edge structure ({detail}) — textured, detailed surfaces.", "moderate": f"Moderate texture ({detail}) — visible structure without noise.", "low": f"Smooth surfaces ({detail}) — minimal fine detail; clean or flat depending on intent.", }, "warm_cool": { "high": f"Warm hue bias ({detail}) — reds/oranges/yellows dominate the mood.", "moderate": f"Neutral temperature ({detail}) — mixed hues, flexible mood.", "low": f"Cool hue bias ({detail}) — blues/greens dominate the mood.", }, } interpretation = dim.get("interpretation", "") base = templates.get(dim_id, {}).get(lvl, f"{dim['label']} is {lvl} ({v:.0%}). {detail}") if interpretation and interpretation not in base: base = f"{interpretation.capitalize()}. {base}" if laion_score >= 6.5 and v >= 0.7: return base if laion_score < 5.0 and v < 0.4: return base + " Aligns with the lower LAION aesthetic score." return base def _openai_rationales(dims: list[dict], laion_score: float) -> list[str] | None: if not OPENAI_API_KEY: return None try: from openai import OpenAI client = OpenAI(api_key=OPENAI_API_KEY) payload = { "laion_score": laion_score, "dimensions": [ {"label": d["label"], "value_0_1": d["value"], "raw": d.get("raw"), "detail": d.get("detail")} for d in dims ], } resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ { "role": "system", "content": ( "You are an art director writing one-sentence plain-English rationales for aesthetic " "diagnostics. Be specific, cite direction (+/-). Return JSON: {\"rationales\": [str×N]} " "same order as input dimensions." ), }, {"role": "user", "content": json.dumps(payload)}, ], response_format={"type": "json_object"}, max_tokens=600, temperature=0.4, ) data = json.loads(resp.choices[0].message.content) lines = data.get("rationales", []) if len(lines) == len(dims): return lines except Exception: pass return None def _hf_rationales(dims: list[dict], laion_score: float) -> list[str] | None: if not HF_TOKEN: return None try: import urllib.parse prompt = ( f"LAION aesthetic score: {laion_score}/10. " f"For each dimension write ONE short plain-English rationale sentence:\n" + "\n".join(f"- {d['label']}: {d['value']:.2f} ({d.get('detail')})" for d in dims) + "\nReturn only numbered lines 1-5." ) body = json.dumps({"inputs": prompt, "parameters": {"max_new_tokens": 400}}).encode() req = urllib.request.Request( "https://api-inference.huggingface.co/models/HuggingFaceH4/zephyr-7b-beta", data=body, headers={"Authorization": f"Bearer {HF_TOKEN}", "Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=30) as resp: out = json.loads(resp.read().decode()) text = out[0]["generated_text"] if isinstance(out, list) else out.get("generated_text", "") lines = [ln.strip().lstrip("0123456789.) ") for ln in text.split("\n") if ln.strip()] if len(lines) >= len(dims): return lines[: len(dims)] except Exception: pass return None def generate_rationales(dims: list[dict], laion_score: float) -> tuple[list[str], str]: """Return (rationales, source_label).""" for fn, label in [ (_openai_rationales, "GPT-4o mini"), (_hf_rationales, "HF Inference"), ]: result = fn(dims, laion_score) if result: return result, label return [_template_rationale(d, laion_score) for d in dims], "rule-based template"