the_shape_of_words / docs /make_spec_figures.py
resakemal's picture
Update spec and add field notes
a4417cf
Raw
History Blame Contribute Delete
6.55 kB
"""
docs/make_spec_figures.py — regenerate the illustrations embedded in app_spec.md.
These are rendered with the app's OWN engine (engine/mappings.py + renderer.py),
so the figures are accurate to what the toy actually draws and reproducible:
python docs/make_spec_figures.py
Outputs PNGs into docs/figures/. The PAD (valence/arousal/dominance) values for
the example sentences are the model-anchored few-shot values from
prompts/prompt_core.md (so no model call is needed to regenerate).
"""
import os, sys
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon
# import the real engine
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from engine.mappings import affect_to_geometry, affect_to_color, hex_of
from engine.renderer import geom_to_points, geom_to_points_segmented
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "figures")
os.makedirs(OUT, exist_ok=True)
INK = "#11131A"
PAPER = "#ECE7DB"
MIST = "#9AA3B2"
GREY = "#8A8F9C" # the app's greyscale-during-play fill
ACCENT = "#E8A33D"
def _shape(ax, pts, fill, edge, lw=1.6, alpha=0.92):
ax.add_patch(Polygon(pts, closed=True, facecolor=fill, edgecolor=edge,
lw=lw, alpha=alpha, joinstyle="round"))
ax.set_xlim(0.02, 0.98); ax.set_ylim(0.02, 0.98)
ax.set_aspect("equal"); ax.axis("off")
def fig_sentence_to_shape():
"""Hero: four beats -> their shapes, greyscale (during play) then revealed."""
examples = [
('"The blade flashed once\nin the dark."', 0.12, 0.88, 0.80),
('"Snow covered everything,\nsoft and still."', 0.82, 0.15, 0.40),
('"She waited by the phone\nthat never rang."', 0.25, 0.18, 0.30),
('"Fireworks burst over\nthe laughing crowd."', 0.85, 0.82, 0.62),
]
fig, axes = plt.subplots(2, len(examples), figsize=(len(examples) * 2.5, 5.4))
fig.patch.set_facecolor(INK)
for col, (sent, v, a, d) in enumerate(examples):
g = affect_to_geometry(v, a, d)
pts = geom_to_points(g, seed_key=sent)
col_hex = hex_of(affect_to_color(v, a, d))
# row 0 — during play (greyscale, colour hidden)
_shape(axes[0, col], pts, GREY, "#C9CDD6")
axes[0, col].set_title(sent, color=PAPER, fontsize=10, pad=8)
# row 1 — revealed (affect-driven colour)
_shape(axes[1, col], pts, col_hex, "#FFFFFF")
axes[1, col].text(0.5, -0.04, f"V {v:.2f} A {a:.2f} D {d:.2f}",
color=MIST, fontsize=8, ha="center", va="top",
family="monospace", transform=axes[1, col].transAxes)
axes[0, 0].text(-0.12, 0.5, "during play", rotation=90, va="center", ha="center",
color=MIST, fontsize=10, family="monospace",
transform=axes[0, 0].transAxes)
axes[1, 0].text(-0.12, 0.5, "revealed", rotation=90, va="center", ha="center",
color=ACCENT, fontsize=10, family="monospace",
transform=axes[1, 0].transAxes)
fig.suptitle("Sentence → essence → shape (colour hidden until the reveal)",
color=PAPER, fontsize=13, y=0.99)
fig.tight_layout(rect=(0.02, 0, 1, 0.96))
_save(fig, "sentence_to_shape.png")
def fig_color_corners():
"""The four emotion-corner hue anchors, each as a shape in its colour."""
corners = [
("calm · pleasant\n(low A, high V)", 0.85, 0.12, 0.45), # green
("joyful\n(high A, high V)", 0.88, 0.85, 0.60), # yellow-orange
("angry\n(high A, low V)", 0.12, 0.88, 0.80), # red
("sad\n(low A, low V)", 0.20, 0.16, 0.30), # blue
]
fig, axes = plt.subplots(1, 4, figsize=(10, 3.0))
fig.patch.set_facecolor(INK)
for ax, (label, v, a, d) in zip(axes, corners):
g = affect_to_geometry(v, a, d)
pts = geom_to_points(g, seed_key=label)
_shape(ax, pts, hex_of(affect_to_color(v, a, d)), "#FFFFFF")
ax.set_title(label, color=PAPER, fontsize=10, pad=8)
fig.suptitle("Affect → colour: four hue anchors, interpolated circularly",
color=PAPER, fontsize=13, y=1.02)
fig.tight_layout()
_save(fig, "color_corners.png")
def fig_segmented():
"""Per-segment essence: 'calm at first, then suddenly sharp' in one form."""
calm = affect_to_geometry(0.75, 0.15, 0.45)
sharp = affect_to_geometry(0.15, 0.90, 0.75)
pts = geom_to_points_segmented([calm, sharp], [1.0, 1.0], seed_key="calm-then-sharp")
fig, ax = plt.subplots(figsize=(4.4, 4.4))
fig.patch.set_facecolor(INK)
_shape(ax, pts, "#7F77DD", "#FFFFFF")
ax.set_title('"Calm at first,\nthen suddenly sharp"', color=PAPER, fontsize=12, pad=10)
fig.tight_layout()
_save(fig, "segmented_essence.png")
def fig_geometry_axes():
"""Each scored geometric axis swept 0.0 -> 1.0 (one axis per row)."""
baseline = {"spikiness": 0.15, "compactness": 0.7, "segmentability": 0.1,
"symmetry": 0.9, "scale": 1.0}
rows = ["spikiness", "compactness", "segmentability"]
steps = np.round(np.arange(0.0, 1.01, 0.2), 1)
fig, axes = plt.subplots(len(rows), len(steps),
figsize=(len(steps) * 1.5, len(rows) * 1.6))
fig.patch.set_facecolor(INK)
for r, axis in enumerate(rows):
for c, val in enumerate(steps):
g = dict(baseline); g[axis] = float(val)
pts = geom_to_points(g, seed_key=f"{axis}{val}")
_shape(axes[r, c], pts, "#7F77DD", "#26215C", lw=1.2)
if r == 0:
axes[r, c].set_title(f"{val:.1f}", color=MIST, fontsize=9)
if c == 0:
axes[r, c].text(-0.2, 0.5, axis, rotation=90, va="center", ha="center",
color=PAPER, fontsize=10, weight="bold",
transform=axes[r, c].transAxes)
fig.suptitle("Scored geometric axes (Huang 2020) — each row varies ONE axis 0→1",
color=PAPER, fontsize=12, y=1.0)
fig.tight_layout(rect=(0.02, 0, 1, 0.95))
_save(fig, "geometry_axes.png")
def _save(fig, name):
path = os.path.join(OUT, name)
fig.savefig(path, dpi=120, facecolor=fig.get_facecolor(), bbox_inches="tight")
plt.close(fig)
print("saved", os.path.relpath(path))
if __name__ == "__main__":
fig_sentence_to_shape()
fig_color_corners()
fig_segmented()
fig_geometry_axes()
print("done")