the_shape_of_words / README.md
resakemal's picture
Update field notes
281848b
|
Raw
History Blame Contribute Delete
8.88 kB

A newer version of the Gradio SDK is available: 6.20.0

Upgrade
metadata
title: The Shape Of Words
emoji: πŸ”₯
colorFrom: indigo
colorTo: purple
sdk: gradio
sdk_version: 6.18.0
python_version: '3.12'
app_file: app.py
pinned: false
short_description: Tell a story. Watch its feelings take shape.
license: mit
tags:
  - track:wood
  - sponsor:openbmb
  - sponsor:modal
  - achievement:offbrand
  - achievement:llama
  - achievement:fieldnotes

The Shape of Words

A collaborative storytelling toy: tell a story one beat at a time, and the model turns the feeling of each beat into an abstract shape. At the end, hidden colors bloom through the shapes one by one, the model adds its own companion shapes, and everything composes into a single arrangement you can rearrange, sonify, save, or turn into a painting.

Built on gradio.Server (FastAPI + Gradio's API engine, gradio 6.x) so the custom HTML/JS frontend gets queuing, streaming, and Hugging Face Spaces hosting.


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Browser β€” static/index.html (custom HTML/JS, no build step) β”‚
β”‚  β€’ renderer Β· mappings Β· layout Β· sound Β· share card Β· UI   β”‚
β”‚  β€’ all rendering is client-side (JS port of engine/)        β”‚
β”‚  β€’ calls the backend for MODEL JUDGMENT only                β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                β”‚  /gradio_api/call/<name>   (queued, SSE)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ app.py β€” gradio.Server                                      β”‚
β”‚  @app.api  judge_beat Β· judge_beat_segmented Β·              β”‚
β”‚            continue_story Β· reveal Β· title_story Β· paint    β”‚
β”‚  @app.get "/"  serves the frontend                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
        β”‚ model/backend.py                     β”‚ model/painter.py
        β”‚  STORY_SHAPES_BACKEND                β”‚  STORY_SHAPES_PAINT_BACKEND
        β–Ό                                      β–Ό
  llamacpp  GGUF in-process (llama.cpp)   modal       Modal endpoint (FLUX.2 Klein)
  modal_llm Modal endpoint (transformers) flux_local  in-process diffusers

The model only judges affect (valence / arousal / dominance + a few flags + a voice line). Everything visual β€” geometry, color (OKLCH), layout, scoring β€” is deterministic code (in engine/, mirrored client-side for rendering), grounded in perception research (see the spec's references).


Run locally (llama.cpp + MiniCPM4.1-8B)

The default backend runs the LLM in-process via llama-cpp-python β€” no daemon, no separate model server.

pip install -r requirements.txt
python app.py            # serves http://localhost:7860

On first run it pulls a quantized GGUF (default openbmb/MiniCPM4.1-8B-GGUF, Q4_K_M, ~5 GB). With a GPU it offloads all layers automatically; CPU-only? set STORY_SHAPES_LLAMACPP_GPU_LAYERS=0. Then open http://localhost:7860.

Config via env vars (all optional):

var default meaning
STORY_SHAPES_BACKEND llamacpp llamacpp (in-process) or modal_llm (Modal endpoint)
STORY_SHAPES_LLAMACPP_REPO openbmb/MiniCPM4.1-8B-GGUF GGUF Hub repo
STORY_SHAPES_LLAMACPP_FILE *Q4_K_M.gguf GGUF filename glob
STORY_SHAPES_LLAMACPP_GPU_LAYERS -1 layers on GPU (-1 all, 0 CPU-only)
PORT 7860 server port

The frontend falls back to a built-in keyword stub if the backend is unreachable, so static/index.html also opens standalone for quick UI testing.


Deploy to a Hugging Face Space

The README YAML frontmatter above is the HF Space config (sdk: gradio, sdk_version: 6.18.0, app_file: app.py). The gradio version is pinned there because gr.Server requires gradio 6.x. Push this repo to a Space and set the env vars below.

Recommended setup β€” both models on one 24 GB GPU (e.g. a dedicated L4):

var value notes
STORY_SHAPES_BACKEND llamacpp MiniCPM4.1-8B as a GGUF via llama.cpp (~5 GB at Q4_K_M)
STORY_SHAPES_PAINT_BACKEND flux_local FLUX.2 Klein 4B in-process with CPU offload

A quantized LLM (5 GB) + cpu-offloaded FLUX (8–12 GB peak) fit comfortably together on 24 GB. A dedicated GPU Space is also not ZeroGPU, which avoids ZeroGPU's per-request quota attribution.

Alternative β€” offload the heavy work to Modal (Space then needs no GPU):

var value notes
STORY_SHAPES_BACKEND modal_llm LLM on a Modal GPU (modal deploy modal_llm.py)
STORY_SHAPES_LLM_MODAL_URL (from modal deploy) the printed modal_llm URL
STORY_SHAPES_PAINT_BACKEND modal painter on a Modal GPU (modal deploy modal_painter.py)
STORY_SHAPES_PAINT_MODAL_URL (from modal deploy) the printed modal_painter URL

DISCLAIMER: I was attempting the Off the Grid challenge (local models only) but I could not test the model deployment on ZeroGPU due to consistently encountering 429 Too Many Requests error (more details in the "deployment story" in the Architecture section in field_notes.md). For stable/expected results on the Space I'm routing both models to Modal (shape judgment & painter; this will increase initial inference time due to cold start); I apologize for the inconvenience. I have attached a local_run_logs.txt for proof that the shape judgment model runs locally.

Why not run MiniCPM4.1 in-process under plain transformers? gradio 6 forces transformers β‰₯ 5.0, and MiniCPM4.1's trust_remote_code modeling code is incompatible with transformers 5.x (it loads with a shim but crashes during generation). That's exactly why the LLM runs via llama.cpp (no transformers at all) or on Modal with transformers pinned to 4.x.

See requirements.txt for the llama.cpp install (a prebuilt CUDA wheel, with a one-comment CPU-fallback toggle) and the diffusers-from-source line FLUX needs.


Layout

app.py                 gradio.Server: API endpoints + serves frontend
requirements.txt
engine/                deterministic core (also mirrored in the frontend JS)
  mappings.py          affect -> geometry, affect -> color (OKLCH, research-grounded)
  renderer.py          geometry -> shape points
  scorer.py            zone-based puzzle scoring (+ band + hint)
model/
  backend.py           LLM abstraction: llamacpp / modal_llm
  painter.py           painting abstraction: modal / flux_local
modal_llm.py           Modal GPU endpoint for the LLM (deploy once)
modal_painter.py       Modal GPU endpoint for FLUX.2 Klein (deploy once)
prompts/               system prompts + JSON schemas / GBNF grammars per call
static/
  index.html           the entire frontend (custom HTML/JS)
docs/
  make_spec_figures.py renders the spec/field-notes illustrations from the engine

Features

  • βœ… Exploration mode: full loop, hidden-color reveal, AI companion shapes, narrative composition, drag / rotate / layers / hide / save.
  • βœ… Per-segment essence: shape morphs through each phrase's felt character.
  • βœ… OKLCH color: perceptually-uniform lightness (Python + JS parity).
  • βœ… Reveal sound (Web Audio): pentatonic chimes, shape-born tick, atmospheric pad, replay β€” togglable in Settings.
  • βœ… Share card (canvas + painting): model-named title, popup, PNG export.
  • βœ… Origin tooltip: hover/tap a shape or layer row to see its source beat.
  • βœ… Painting step (FLUX.2 Klein img2img): "Paint this"; free style field + preset styles + 🎲 Surprise me; randomized seed; modal or flux_local.
  • βœ… LLM backends: llamacpp (GGUF, Llama Champion) and modal_llm. Both produce schema-constrained JSON.
  • βœ… Felt-quality eval (eval_felt_quality.py): A/B models on a fixed set.
  • βœ… HF Space deployment (gradio 6.18, gr.Server).