Start6 of 7
Installing from a clone
On this page
Do this if you want to change Kotoba. If you only want to run her, pip install is simpler and gives
you a compiled web UI you do not have to build.
What you need
- Python 3.11 or newer
- Node 22 or newer, for the frontend.
.nvmrcpins the version, sonvm usepicks it up
Two processes, two terminals
git clone https://github.com/rodhnin/kotoba-companion
cd kotoba-companion
# terminal 1 — backend
python -m venv .venv && source .venv/bin/activate
pip install -e "api/[dev]"
uvicorn kotoba.server:app --reload --port 8000
# terminal 2 — frontend
npm install
npm run dev # http://localhost:3000
kotoba serve works from a clone too and starts both, but two terminals give you two separate logs
and two separate restarts, which is what you want while editing.
The package is a src-layout under api/: the code lives in api/src/kotoba/, the ASGI target is
kotoba.server:app, and api/pyproject.toml is the manifest. pip install -e "api/[dev]" installs
the package plus the test tools. Installing requirements.txt alone leaves kotoba unimportable and
uvicorn fails with ModuleNotFoundError.
The backend boots with no API keys at all — the loop returns an offline reply — so you can work on the UI and the event pipeline before wiring up a model or ElevenLabs.
Keys in a clone
cp api/.env.example api/.env
Two lines cover the default path:
OPENAI_API_KEY=<your OpenAI key>
ELEVENLABS_API_KEY=<your ElevenLabs key>
Replace both placeholders. The values shipped in api/.env.example — sk-... and el_... — are
recognised as placeholders and treated as no key at all, deliberately, so an unedited copy fails with
a sentence instead of a 401 buried in a log.
Or just run kotoba setup, which stores them encrypted in the database instead. Both routes work and
both are read by the same code.
You do not need .env.local. It exists for the split-origin case, where the browser has to call
the backend on a different origin. Creating it has a cost worth knowing: NEXT_PUBLIC_* values are
frozen into the compiled bundle, so the web build script refuses to run while that file exists rather
than baking your address into everybody's copy.
Where things live in a clone
This is the one place a clone differs from an install, and it surprises people:
- The database stays with the checkout, at
api/kotoba.db, when that file exists. A relativeDATABASE_URLresolves againstapi/and not against whatever directory you happen to be standing in — otherwisecdwould silently open a different, empty database. - Her personality is
soul/default.mdin the checkout, not the copy in your home. - Everything else — memory, files, models, the master key, settings, logs — is still under
~/.kotoba/.
Building the packaged web UI
python scripts/build_web.py
Needs Node. It compiles the Next app, checks it, and copies the result into api/src/kotoba/web/,
which is gitignored — the repository never carries a build. It refuses to run when a root .env or
.env.local exists, when a Live2D model is sitting under public/, and when the finished build
contains a private hostname, an IP literal, a tunnel domain, a home directory path, a source map or
image metadata.
You rarely need it. kotoba serve prefers the development server whenever npm is available, because
your edits have to show; the packaged copy is what an install without Node gets. But building a wheel
requires it: the build backend refuses a distribution whose web UI is absent, stale, or stamped for a
different version.
Before you open a pull request
The four things CI runs, in the same form:
pytest api/tests -q
npx tsc --noEmit --noUnusedLocals --noUnusedParameters
npm test
python scripts/build_web.py
A few backend tests fetch and then run somebody else's package, so they are deselected by default and refuse to run even when selected. Ask for them explicitly when you touch MCP installation or the Docker sandbox:
KOTOBA_ALLOW_NETWORK=1 pytest api/tests -m network
To exercise the first-run screens without touching your own install, run ./try-setup.sh. It points
kotoba setup at a throwaway home, so your own database, keystore, settings, memory and personality
files are never opened, and it prints the sandbox path and how to delete it.
For any backend change, confirm GET /health returns {"status": "ok"}.
Docker
cp api/.env.example api/.env
docker compose up --build
# → http://localhost:3000
Compose reads api/.env and will not start without it. Both services come up on the compose network
and the frontend proxies /api/* to the backend.
One caveat that is not obvious: the browser's voice socket cannot go through that proxy, so its
address is baked into the frontend image at build time as ws://localhost:<backend port>. Open the
stack from the Docker host and voice works. Open it from another machine on your network and the
browser dials its own localhost — the page loads and voice silently does not. Text is unaffected.
Ports are baked in at build time, so pass --build whenever you change KOTOBA_BACKEND_PORT or
KOTOBA_WEB_PORT.
