Her character3 of 8
Language
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:
>>> 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:
KOTOBA_STT_LANGUAGE— the environment override. Wins over everything.soul_config.language— what Settings,kotoba setupor the soul file's first boot set.""— auto-detect.
Measured, exhaustively:
soul_config.language | KOTOBA_STT_LANGUAGE | Transcriber listens for |
|---|---|---|
auto | unset | auto-detect |
es | unset | es |
ja | unset | ja |
es | ja | ja |
auto | ja | ja |
es | espanol (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:
| Value | Transcriber gets | Note |
|---|---|---|
es | es | |
ja | ja | |
ES | es | case and padding tolerated |
Spanish | `` (auto) | refused |
español | `` (auto) | refused |
es-ES | `` (auto) | refused — a region tag is one character too many |
zzz | zzz | accepted, 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 set | Stored | Prompt says | Transcriber listens for |
|---|---|---|---|
Spanish | Spanish | Spanish | auto |
es | es | es | es |
auto | auto | auto | auto |
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
| Surface | Reply language | Transcription |
|---|---|---|
Browser, voice_mode=local | prompt line | stt_language_for(soul.language) — core/voice/session.py |
Browser, voice_mode=agent | prompt line | not ours — the ElevenLabs dashboard agent transcribes; this setting cannot reach it |
| Browser, typed | prompt line | n/a |
Terminal (kotoba) | prompt line | n/a — it is text in, text out |
| Discord voice | prompt line | stt_language_for(soul.language) — discord/client.py |
| Discord text | prompt line, plus a per-message rule | n/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_codeparameter. 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
autofallback when it fails. - The precedence of
KOTOBA_STT_LANGUAGEover 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.pypins 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] midprints 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_writeruns_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.
