grab primary colors from a bsky pfp

README.md

bsky-primary #

A tiny FastAPI service that extracts two prominent colors from a Bluesky user's avatar. Point it at one or more handles/DIDs and get back a light and dark hex color per actor — handy for theming UI to match someone's profile picture.

API #

POST /colors #

{ "actors": ["@alice.bsky.social", "did:plc:xxxx", "bob.example.com"] }

Response — keyed by the exact strings you sent:

{
  "@alice.bsky.social": { "light": "#e2c48f", "dark": "#5a3b21" },
  "did:plc:xxxx":        { "light": "#88c0d0", "dark": "#2e3440" },
  "bob.example.com":     null
}
  • The two colors are sorted by luminance; dark is suitable for strokes/text.
  • A value is null when the avatar can't be fetched, decoded, or yields no usable color — callers should fall back to their own defaults.
  • Up to 60 actors per request. Results are cached in-memory for 24h (failures retried after 5 min).

GET /colors/{actor} #

Convenience lookup for a single handle or DID. Add ?refresh=true to bypass the cache and re-fetch the avatar.

GET /colors/@bsky.app
GET /colors/did:plc:z72i7hdynmk6r22z27h6tvur?refresh=true
{ "light": "#e2c48f", "dark": "#5a3b21" }

Returns null if the actor can't be resolved or yields no usable color.

GET /healthz #

{ "ok": true, "cache_size": 12 }

OpenAPI #

A static spec lives at openapi.json (OpenAPI 3.1). The running service also serves it live at /openapi.json, with interactive docs at /docs.

How it works #

  1. Resolve avatars in batches via app.bsky.actor.getProfiles on the public Bluesky API (no auth required).
  2. Fetch each avatar (5 MB / 5 s caps), resample to 32×32.
  3. Histogram pixels into 4-bit-per-channel buckets, skipping near-black, near-white, and transparent pixels.
  4. Pick the two most-populous buckets at least MIN_COLOR_DIST apart, average each bucket, and sort by luminance.

Tuning constants live at the top of main.py.

Running #

Local #

pip install -r requirements.txt
uvicorn main:app --host 0.0.0.0 --port 8000

Docker #

cd deploy
# optional: cp .env.example .env  and set HOST_PORT
docker compose up --build -d

The container listens on 8000; the compose file publishes it on HOST_PORT (default 8000).

Example #

curl -s localhost:8000/colors \
  -H 'content-type: application/json' \
  -d '{"actors":["@bsky.app"]}'