Kotoba

Where to start

What Kotoba isWho she is and where she runs, in one page
InstallingOne package, two commands
First runA key, a model, her first words
The approval gateHow she asks before she acts
The two voice modesLocal voice, or the agent tunnel
The soul fileChange who she is
RoadmapWhat grows next, and what was cut on purpose

Or a section

↑↓ move openesc close124 pages
All pages

The web app1 of 12

First run in the browser

About 7 minutes to read

On this page

The first time you open Kotoba in a browser it does not open the companion. It opens /setup, a nine-step wizard that asks the questions the backend needs answered before she can think, and writes each answer the moment you give it.

Screenshot placeholder: the /setup brain step — the step strip across the top, the chibi and her speech bubble on the left, the two provider cards on the right, and the dashed "About my brain" note underneath.

How you get there

/app asks GET /api/setup/status before it mounts anything. If the answer is needed: true it replaces the URL with /setup (app/app/page.tsx:52-59). This is the same predicate the terminal wizard runs, so a machine set up from a terminal and a machine set up from a browser can never disagree about whether somebody has been set up (lib/first-run.ts:2-9).

The check has a 2-second deadline and fails open (lib/first-run.ts:11, 96-118). A refusal, a thrown fetch and a backend that never answers all read as "already configured". Being wrong that way costs you one visit to an app that reports its own trouble; being wrong the other way would trap a configured user in a setup screen they do not need.

What each step asks

The nine steps are declared in one list (components/Onboarding.tsx:140-150).

StepLabelWhat it writes
1Her brainPOST /api/setup/provider
2The modelPOST /api/setup/model
3The keyPOST /api/setup/provider, then POST /api/settings/llm-key
4Your namePOST /api/settings/user-name
5Her namePOST /api/settings/soul (name)
6LanguagePOST /api/settings/soul (language)
7Her facePOST /api/models/install/default or /api/models/install/upload
8Her voicePOST /api/settings/voice-key
9Readynothing — it only navigates

1. Her brain

Two cards: OpenAI and xAI (Grok). A dashed panel below them reads "more brains soon~" (Onboarding.tsx:1235-1263). Choosing one posts the provider, and the backend answers with a model it actually serves; the screen adopts that answer rather than the one you clicked (Onboarding.tsx:663-682).

2. The model

The model list comes from the backend's own catalogue, read at the door from GET /api/setup/status (Onboarding.tsx:554, 559-560). A hard-coded copy of the same table stands in until that answer arrives (Onboarding.tsx:108-133) — the frontend renders before setup status can answer — and a backend test parses that copy out of the component and fails if it has drifted from the provider table (api/tests/test_setup_model_step.py:41-60).

Each row carries a price hint relative to the cheapest model and a context size.

The model is chosen before the key, and the order is load-bearing: the key check validates against the current companion model, so a key with no access to it fails at the key step — where you can still paste another — rather than on your first real sentence (api/tests/test_setup_model_step.py:1-9, api/src/kotoba/server.py:1193-1197).

3. The key

A masked field. What the button means is decided before anything is sent (lib/key-step.ts:16-20):

  • Something typed → check it, and file the verdict under the provider the attempt carried, not the one on screen when the answer comes back.
  • Empty, over a key the backend already holds → keep it. Nothing is sent at all.
  • Empty, with no key held → the button is disabled (Onboarding.tsx:1320).

The label and the placeholder tell you which of those three you are in: one is saved; type to replace it, or she is using $VAR; type to save one here instead, or nothing (Onboarding.tsx:406-420). An environment variable counts as a key she has.

If a key is refused

The backend's own words are classified into five causes (lib/key-errors.ts:8, 17-25) and she says the one that fits (Onboarding.tsx:281-286):

CauseWhat she says
quotaThe key is real; the account behind it has nothing left. Top it up and the same key works.
networkIt never reached them. A proxy, a firewall or a VPN on this machine — not your key.
installShe could not store it safely on this machine, so she did not keep it.
modelThe key opened the door; the model you picked was not behind it. Go back one step.
pasteIt was probably copied incomplete, or belongs to a different account.

Only a paste failure clears the field (Onboarding.tsx:737-738). Emptying the box over a key with no credit behind it, or over a proxy that ate the request, would throw away something that was right.

Nothing is stored on a refusal. A refused key check is a non-2xx from the backend, not a 200 carrying a falsy flag.

4 and 5. The names

Your name is capped at 40 characters, hers at 24 (Onboarding.tsx:1346, 1374). Both writes are read back: the backend silently refuses a value that does not look like a name, so the screen prints what it ended up with, never what was clicked (lib/setup-writes.ts:33-37). A refused write is undone on screen and says so.

6. Language

Six chips: Auto — match you, English, Español, 日本語, Français, Português (Onboarding.tsx:315). The chips are labels; a two-letter code is what gets stored (Onboarding.tsx:318-325). Language decides both halves — how she answers and how she hears you.

7. Her face

No Live2D model ships with Kotoba. Two doors, and both land a model on your own disk:

  • Live2D's sample. The offer, its name and its licence link come from GET /api/models/default (Onboarding.tsx:555, 561-564). The licence link is drawn before the button is offered. Pressing the button posts accept_license: true (Onboarding.tsx:846).
  • A .zip of yours. Drag one in or pick one. The archive is the request body — there is no multipart and no filename, because the installed folder is named from the model's own entry file (Onboarding.tsx:57-59). Anything that is not a .zip is refused in the browser before a request is made (Onboarding.tsx:862-875), and more than one file at a time is refused too.

A finished install draws a receipt card: the folder name, the file count and byte size, and the path on your disk — with her actually drawn inside it from the files that just landed (components/FaceReceipt.tsx). Files in the archive that she cannot wear are reported as left behind (Onboarding.tsx:2186-2193). You leave the step with Keep this face.

The step has no skip. A way out appears only after a door has actually refused her, so it is never an invitation (Onboarding.tsx:817, 886-893, 1501).

8. Her voice

An ElevenLabs key, optional and masked. Empty over a key the backend already holds keeps it and sends nothing. Skip does not clear anything: it is declining to give a new key, not a claim about the old one (Onboarding.tsx:930-940).

9. Ready

A recap of eight rows — Brain, Model, Key, You, Her, Language, Face, Voice (Onboarding.tsx:2484-2513). The two secrets show as dots or as "not yet"; neither is ever printed.

Everything was already saved. The button only opens the door: it shows a loading screen for about four seconds and then replaces the URL with /app (Onboarding.tsx:1602, 2662-2672).

When something is already configured

/setup is not reachable by accident on a configured machine. SetupDoor asks the same question /app asks, and a machine that does not need first run is sent to /app with replace, so Back does not bounce off it (components/SetupDoor.tsx:30-38).

The one way in is Settings → Brain → Run setup again, which navigates to /setup?reconfigure=1 (components/panels/SettingsPanel.tsx:719-721). That flag is guessable, which is honest: it stops an accident — a bookmark, browser history, an old link — rather than a person, and /setup sits behind the same password gate as /app (lib/first-run.ts:16-21).

Reconfiguring differs from a first run in three ways:

  1. The recap and the steps are seeded from what the install already holds, so you do not see seven dashes over answers you are keeping (lib/setup-nav.ts:82-96, Onboarding.tsx:571-572).
  2. The step strip lets you jump to any step, not only the ones behind you (lib/setup-nav.ts:9-12). On a first run you can only reopen a step you have already answered.
  3. The note on the first screen says so plainly: only the answers you actually give are overwritten, and a step you walk past keeps exactly what it had (Onboarding.tsx:391-397).

There is a second, narrower door: Settings → Brain → Give her a face goes straight to the face step (/setup?reconfigure=1&step=face). It exists because an install done from a terminal cannot fetch a model — the terminal wizard has no browser to draw the licence in — and walking the other eight questions to reach that one step is why nobody did (lib/first-run.ts:27-32).

/app offers the face step itself, once, if she has no model. It is offered once per browser and not once per tab, and the flag is kept in localStorage under kotoba.sent-for-a-face (lib/first-run.ts:34-63). Declining is final until you use the Settings button.