| """ |
| eval_felt_quality.py — eyeball the model's affect judgments on a fixed, varied |
| sentence set, and render each as its shape+color. Use it to (a) sanity-check that |
| judgments feel right, and (b) A/B models (Qwen3 8B vs MiniCPM4.1-8B vs ...). |
| |
| Run (model comes from the same env vars as the app): |
| STORY_SHAPES_MODEL=qwen3:8b python eval_felt_quality.py |
| STORY_SHAPES_MODEL=openbmb/MiniCPM4.1-8B python eval_felt_quality.py # if pulled in Ollama |
| |
| Outputs: |
| eval_<model>.png a grid: sentence -> judged V/A/D -> rendered shape+color |
| eval_<model>.json raw judgments for diffing between models |
| |
| The sentence set spans the space ON PURPOSE: calm/sad/angry/joyful, literal vs |
| poetic vs absurd, understated vs loaded — so sameness or mis-judgment shows up. |
| """ |
| import os, json, sys |
|
|
| SENTENCES = [ |
| |
| "The lake lay perfectly still under the morning light.", |
| "She sipped her tea and watched the snow fall.", |
| |
| "The last letter sat unopened on the empty table.", |
| "He walked home alone in the grey rain.", |
| |
| "The blade flashed once and the room went silent.", |
| "Glass shattered as the door slammed off its hinges.", |
| |
| "Children spilled into the square, laughing and shrieking with delight.", |
| "Fireworks burst gold across the whole sky at once.", |
| |
| "Something shifted in the dark just beyond the candlelight.", |
| |
| "The mountain loomed over the valley, vast and unmoving.", |
| |
| "A small mouse trembled at the edge of the floorboard.", |
| |
| "A flying rhinoceros with fire wings sneezes Tuesday.", |
| "Purple seven runs sideways into the soft idea of soup.", |
| |
| "Dusk folded itself quietly over the rooftops.", |
| |
| "Rage and grief and joy and terror crashed through her all at once.", |
| ] |
|
|
| def main(): |
| from model import backend |
| model = backend.MODEL |
| safe = model.replace("/", "_").replace(":", "_") |
| results = [] |
| print(f"Evaluating model: {model}\n") |
| for s in SENTENCES: |
| try: |
| j = backend.judge_beat(s, story="", mode="exploration") |
| v, a, d = j["valence"], j["arousal"], j["dominance"] |
| print(f" V{v:.2f} A{a:.2f} D{d:.2f} deserve={str(j.get('deserves_shape')):5s} {s[:55]}") |
| results.append({"sentence": s, **j}) |
| except Exception as e: |
| print(f" [FAILED] {s[:55]} ({e})") |
| results.append({"sentence": s, "error": str(e)}) |
|
|
| with open(f"eval_{safe}.json", "w") as f: |
| json.dump({"model": model, "results": results}, f, indent=2) |
| print(f"\nwrote eval_{safe}.json") |
|
|
| |
| try: |
| import matplotlib; matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| from engine.mappings import affect_to_geometry, affect_to_color |
| from engine.renderer import geom_to_points |
| ok = [r for r in results if "valence" in r] |
| n = len(ok); cols = 4; rows = (n + cols - 1) // cols |
| fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3.2)) |
| axes = axes.ravel() |
| for ax, r in zip(axes, ok): |
| g = affect_to_geometry(r["valence"], r["arousal"], r["dominance"]) |
| col = affect_to_color(r["valence"], r["arousal"], r["dominance"]) |
| pts = geom_to_points(g, seed_key=r["sentence"]) |
| ax.add_patch(plt.Polygon(pts, closed=True, facecolor=col, edgecolor="#222", lw=1.2)) |
| ax.set_xlim(0, 1); ax.set_ylim(1, 0); ax.set_aspect("equal"); ax.axis("off") |
| ax.set_title(r["sentence"][:38] + ("…" if len(r["sentence"]) > 38 else ""), fontsize=7) |
| ax.text(0.5, -0.04, f"V{r['valence']:.2f} A{r['arousal']:.2f} D{r['dominance']:.2f}", |
| fontsize=6.5, ha="center", va="top", transform=ax.transAxes, color="#777") |
| for ax in axes[n:]: |
| ax.axis("off") |
| plt.suptitle(f"Felt-quality eval — {model}", fontsize=11, y=1.0) |
| plt.tight_layout() |
| plt.savefig(f"eval_{safe}.png", dpi=90, bbox_inches="tight") |
| print(f"wrote eval_{safe}.png") |
| except Exception as e: |
| print(f"(skipped grid render: {e})") |
|
|
| if __name__ == "__main__": |
| main() |
|
|