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

Her character3 of 8

Language

About 6 minutes to read

On this page

One value, two jobs. soul_config.language decides what language she answers in and what language the transcriber listens for, and those are separate code paths that once disagreed. This page is mostly about the ways they can still disagree.

The shipped value is auto.


What auto means

auto means "no pin", on both sides.

Her replies. The value is interpolated into the first paragraph of the system prompt:

Speak in the first person as yourself. Language: auto (auto = match the user).

That is the whole mechanism. There is no language detector, no post-check, no filter that rewrites a reply in the wrong language. It is an instruction to the model and nothing enforces it.

The transcriber. stt_language_for() returns "" for auto, and the empty string is passed as language_code=None to the ElevenLabs speech-to-text client, which is that API's "detect it yourself".


What a code does

Set es, and both sides pin. Measured:

python
>>> stt_language_for("es")
'es'

and the prompt line becomes Language: es (auto = match the user).

The transcriber pin is the half that matters most, and the reason it exists is worth stating, because it is not obvious that a reply setting should touch a microphone. From core/voice/config.py:

"Settings → Personality has always shown a Language control, and it only ever chose how she REPLIES. Transcription kept auto-detecting, so picking 'Spanish' did nothing about EL hearing a short Spanish sentence as Portuguese — and the transcript is the whole of what she gets, so she answered in Portuguese. A visible control that cannot fix the thing it is named after is worse than no control."

Three consecutive Spanish sentences came back labelled Portuguese in live testing. She answered in Portuguese, correctly, because the transcript was all she had.


The order of authority

stt_language_for() consults, in order:

  1. KOTOBA_STT_LANGUAGE — the environment override. Wins over everything.
  2. soul_config.language — what Settings, kotoba setup or the soul file's first boot set.
  3. "" — auto-detect.

Measured, exhaustively:

soul_config.languageKOTOBA_STT_LANGUAGETranscriber listens for
autounsetauto-detect
esunsetes
jaunsetja
esjaja
autojaja
esespanol (junk)es — the junk override steps aside

A refused KOTOBA_STT_LANGUAGE is logged and ignored; it does not shadow the soul's pin.


The shape a code has to have

The check is ^[A-Za-z]{2,3}$ — two or three letters, nothing else. Measured:

ValueTranscriber getsNote
eses
jaja
ESescase and padding tolerated
Spanish`` (auto)refused
español`` (auto)refused
es-ES`` (auto)refused — a region tag is one character too many
zzzzzzaccepted, and it is not a language

The two rows at the bottom are the traps. es-ES looks more correct than es and is worse. zzz passes the shape check and is sent to ElevenLabs, which will make of it what it will — nothing here validates that a code names a real language.

A refused value logs a warning and falls through to auto:

soul language 'Spanish' is not an ISO 639-1/639-3 code — transcription stays on auto

The gap that is still reachable

The database coerces a junk language to auto, but its bar is low: any value up to 20 characters that does not open with a bracket is stored as typed. So Spanish survives the database and is then refused by the transcriber. Measured, end to end, on a real row:

You setStoredPrompt saysTranscriber listens for
SpanishSpanishSpanishauto
eseseses
autoautoautoauto

Row one is the old bug, still reachable — not through the UI, which offers a fixed list of codes, but by hand-editing soul/default.md on a first boot, or by POSTing a word to /api/settings/soul. Her replies move to Spanish; her ears stay on auto-detect.

Always write a code, never a language name. kotoba doctor does not check this. The only signal is that warning line in the log.


Where each surface gets its language

SurfaceReply languageTranscription
Browser, voice_mode=localprompt linestt_language_for(soul.language)core/voice/session.py
Browser, voice_mode=agentprompt linenot ours — the ElevenLabs dashboard agent transcribes; this setting cannot reach it
Browser, typedprompt linen/a
Terminal (kotoba)prompt linen/a — it is text in, text out
Discord voiceprompt linestt_language_for(soul.language)discord/client.py
Discord textprompt line, plus a per-message rulen/a

Discord text channels add an instruction of their own, because a room can hold several languages at once:

"[LANGUAGE] Answer in the language of the message you are answering, and in no other. … Somebody else in this room speaking a different language changes nothing about THIS reply."

That is also an instruction, not an enforcement.


What is enforced, and what is asked

Because the distinction matters more here than anywhere else on this site:

Enforced in code:

  • The transcriber's language_code parameter. A pin is a pin; the audio is transcribed against that language whatever the model would have preferred.
  • The shape check on the code, and the auto fallback when it fails.
  • The precedence of KOTOBA_STT_LANGUAGE over the soul value.
  • One canned English line she can be given — the replacement text the forbidden-phrase filter substitutes for a false "I can't reach the web" claim — is deliberately English-only, and test_reaction_language.py pins that the filter removes rather than substitutes when the claim arrived in another language, so English cannot leak into a Spanish turn that way.

Asked of the model, and nothing more:

  • That she reply in the pinned language at all. One line of the prompt.
  • That she keep audio tags in English while speaking another language. The tag vocabulary is English-only and the prompt says so at length. The filter knows only the nineteen English tags, so a translated one is never corrected: measured, [emocionada] Que bien! is removed from speech and sets no face, and on a text surface it is removed only when it opens the reply — mid-line, ya dije [emocionada] mid prints the bracket. A tag in a non-Latin script ([笑い]) is dropped from speech and printed on a text surface wherever it sits.

Enforced in code, but only for Spanish:

  • Memory facts must be in English, because the duplicate detector only works within one language. memory_write runs _looks_spanish() on the fact and refuses it with an instruction to translate. The check is deliberately conservative — a Spanish-only word, or a Spanish verb plus one more signal — and it covers no other language. The code says so: "Spanish is the only non-English this store has ever received; anything else passes as before." A fact in French or Japanese is saved as written. See What she remembers.