Prisme generates synthetic personas with a large language model and lets you probe how they react to things. You describe a population, Prisme creates rich, individual personas (demographics, life story, opinions, fears, aspirations, and a 9-axis personality/ideology profile), groups them, and lets you run "stimuli" past them to see how different slices of a synthetic public might respond. It supports personas from France, the USA, and Belgium.
It is a full-stack app: a FastAPI + PostgreSQL backend that orchestrates the generation pipeline and talks to Google's Gemini API, and a React frontend that visualizes personas and their axes as radar charts.
Status: a personal project, actively built. The product was previously named "Hibiki"; any leftover reference to that name is a rename in progress.
- Persona - one synthetic individual with a structured portrait: name,
age, gender, location, occupation, family situation, religion, life story,
opinions, media consumption, hobbies, fears, aspirations, and more. Each
persona is scored on 9 axes:
age,education,ses,openness,agreeableness,economic,societal,institution,ambiguity_tolerance. - Bubble - a population definition: the filters and intent that describe the kind of people to generate.
- Cluster - a grouping of bubbles.
- Job - one generation run. Jobs are checkpointed stage by stage, so a backend restart resumes where it left off.
- Stimulus - something you want the personas to evaluate (the thing a survey shows them).
- Country - every persona belongs to one of
France,USA,Belgium, which drives its context and the language of its generated content.
Generation is a text-first, batched pipeline (one batched LLM call per persona per stage; concurrent jobs hitting the same model share a batch):
- sketch - Verbalized Sampling: the model returns several candidate sketches with self-assessed probabilities, and one is drawn by weighted-random sampling (this is what gives the population diversity).
- enrich - flesh the chosen sketch into a richer narrative.
- structure - emit the final structured persona portrait.
- naming - a classifier picks name attributes, then a sampler draws an actual first and last name.
Every LLM call uses a strict response schema so the output parses reliably.
| Layer | Stack |
|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy + Alembic, async batched LLM pipeline |
| Database | PostgreSQL 16 (pgvector extension, for persona similarity) |
| LLM | Google Gemini (Generative Language API), structured output |
| Frontend | React 19, Vite, TanStack Query, Recharts (radar charts) |
| Dev/infra | Docker Compose, GitHub Actions CI |
You need Docker and Docker Compose.
# 1. configure the local environment
cp .env.example .env
# edit .env: choose a local POSTGRES_PASSWORD; optionally set GEMINI_API_KEY
# (you can also set the key later from the in-app Settings page)
# 2. build and start the stack
docker compose up --buildThen open:
- Frontend: http://localhost:4174
- Backend health: http://localhost:8002/health
- PostgreSQL: localhost:5435
Day to day, docker compose up is enough. The frontend source is bind-mounted,
so editing .tsx/.css hot-reloads with no rebuild. Rebuild the backend
after any backend change (docker compose up -d --build backend): it has no
bind mount, its code is copied into the image at build time. The database schema
is managed by Alembic and migrated automatically on backend startup.
To stop: docker compose down (add --volumes to also drop the Postgres data).
Read from .env (see .env.example):
| Variable | Default | Purpose |
|---|---|---|
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB |
- | local database credentials |
GEMINI_API_KEY |
empty | Gemini key; if blank, set it from the in-app Settings page |
MODEL_NAME |
gemini-3-flash-preview |
the Gemini model used for generation |
GEMINI_MAX_CONCURRENCY |
10 |
max concurrent in-flight LLM requests |
.env is gitignored and never committed.
# backend (pytest, against the prisme_test database)
docker compose run --rm -v "$PWD/backend/tests:/app/tests:ro" backend pytest
# frontend (vitest)
docker compose exec frontend ./node_modules/.bin/vitest runThe backend test suite guards itself against running on any database whose name
does not end in _test (the tests truncate tables).
The backend image ships production deps only, so lint and type-check run on the host in a virtualenv:
cd backend
python3 -m venv .venv # one time
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
.venv/bin/ruff check . # lint
.venv/bin/ruff format --check . # formatter (drop --check to apply)
.venv/bin/mypy app # type-check (every function in app/ is annotated)Frontend: docker compose exec frontend npm run typecheck and npm run lint.
CI (GitHub Actions, .github/workflows/ci.yml) runs the Alembic migration
round-trip, ruff, mypy, and pytest for the backend, plus typecheck, lint, and
vitest for the frontend, on every push and pull request.
Prisme/
compose.yml # dev stack: db (Postgres+pgvector), backend, frontend
backend/ # FastAPI app
app/
api/ # routes: bubbles, clusters, personas, stimuli, jobs, ...
services/ # generation pipeline, Gemini client, batching
persona_schema.py # source of truth for persona enums
llm_schemas.py # Gemini-safe response schemas
migrations/ # Alembic schema migrations
tests/
frontend/ # React + Vite app (radar visualizations, persona UI)
db-init/ # creates the prisme_test database on first boot
specs/ # design specs (some early ones are French archives)
CLAUDE.md # coding conventions and the full commands reference
CLAUDE.mdis the source of truth for coding conventions: the English-only rule, naming conventions, the generation pipeline, multi-country support, and the complete ports/commands/Alembic reference. Read it before contributing.specs/holds the design specs.prisme_phase1_spec.mdandprisme_phase2_spec.mdare kept as historical French archives; the others are the current English specs.