Skip to content
C.W.K.
Lesson 02 of 05 · published

Who Spoke, and Which Way

~14 min · interpreter, language-detection, direction, testing

Level 0First Word
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

The narrowest question

Anyone at the table may talk into the card: Dad, the person serving him, a friend leaning in. Before Pippa writes a word, the engine has to decide which way this sentence goes. That sounds like language identification, a hard problem with a long tail. It is much smaller than that, because Dad speaks Korean. The rule only needs to know one thing: is this Korean or not?

  • Korean in is Dad speaking. It goes out in the other person's language, as a card for them.
  • Anything else in is theirs. It comes back in Korean, for Dad.

Two signals answer the question, in a fixed order. First, the language Scribe named when it committed the transcript. When Scribe named none, the script decides: Hangul anywhere in the words means Korean. The returned direction records which one decided, in by: scribe, script, or dad when Dad flipped it.

Reading the function

The first excerpt below is the whole rule. A few details deserve a slow read.

_primary lowercases a tag, keeps only its first subtag, and folds three-letter codes to two. That folding exists because Scribe speaks two dialects. Its realtime model and every card use ISO 639-1 (es), while its batch model, used when the engine transcribes a kept recording itself, names languages in ISO 639-3 (spa). The sitting keeps the short form, so spa, es and es-ES all compare equal.

The Hangul pattern covers three Unicode ranges: the precomposed syllables, the conjoining jamo, and the compatibility jamo. A word typed on a phone and a transcript that came out as loose jamo both count.

When the speaker is Dad, the target is the counterpart's language, taken from the sitting's scene language. When the speaker is the other person, the source is whatever was detected, and the target is always Korean. One odd case is handled on purpose: a flip that turns Korean words into "theirs" would leave the source as Korean, so the function substitutes the counterpart's language instead.

And when nothing names a language and there is no Hangul, the answer is "other, source unknown, into Korean". The engine does not guess the name. Pippa's refined result carries its own BCP-47 language tag, and that names it.

The counterpart's language moves

The sitting's scene language is a live value. It starts as the card's language, or a blank card's best guess. When the other person speaks a language Scribe names, and it differs from the scene language, the engine writes it to the moment before anything else happens, so Dad's next card is in it. Ask for the bill in Spanish, get an answer in English, and the next thing Dad says comes out in English.

A blank sitting where Dad speaks first works the other way round: Pippa picks "the language of the place" for his first card, and the language she wrote it in becomes the sitting's.

One tap to flip

Any rule this small will sometimes be wrong. A Korean speaker answering Dad in Korean, or Dad trying out a phrase in the other language. So every exchange shows its direction as a chip, and a tap posts a new utterance with flip_of set to the earlier exchange. That new turn reads the same words and language from the turn it flips, and inverts the speaker. The original stays in the record. The test for it asserts that the sitting now holds two heard results with the same words, which is the honest history: it was heard once, read once each way.

Note what is not in the rule. The design first named two tie-breakers: Pippa's own reading of the words, and the speaker ear, cwkPippa's local check of whether a voice is Dad's. Neither was needed for a yes-or-no question about Korean, and leaving the ear out keeps a stranger's voice away from the one service that embeds voices.

The design also measured whether to hint Scribe. With no fixed language and detection on, six utterances spoken by macOS voices (three Spanish, two Korean, one English), each run twice, were labeled right 12 times out of 12. Adding Korean and Spanish as secondary languages made the English one come back labeled Korean: one wrong out of six. So the socket opens with no hint at all.

Principle: Shrink the question to the decision. The interpreter does not need to know what language someone spoke; it needs to know which way to translate. A binary question with a visible answer and a one-tap correction beats a clever classifier nobody can overrule.

Dad practicing

The next-to-last assertion in the test reads Quiero agua, tagged Spanish, and expects Korean as the target. Dad practicing his Spanish is read back to him in Korean, which is the useful direction: he hears what he actually said. If he meant it as a card for the waiter, the chip is right there.

Code

cwkSpark, backend/turns.py: the direction rule (verbatim, lines 456-484)·python
def exchange_direction(
    text: str, heard_in: str | None, moment: dict[str, Any], *, flip: bool = False
) -> dict[str, Any]:
    """Which way one utterance goes (docs/PIPPA-LIVE-INTERPRETER.md §3.1).

    Korean in is Dad speaking: it goes out in the counterpart's language as a
    card for them. Anything else in comes back in Korean for Dad. Scribe's
    language decides; when it named none, Hangul in the words decides. A flip
    is Dad's tap on the direction chip: the earlier reading was the wrong way.
    The counterpart's language is the sitting's scene language (the card's,
    then whatever they last spoke), else Pippa picks the place's."""
    source = _primary(heard_in)
    by = "scribe"
    if source is None:
        source = "ko" if _HANGUL.search(text) else None
        by = "script"
    dad = source == "ko"
    if flip:
        dad = not dad
        by = "dad"
    counterpart = _primary(moment.get("scene_language"))
    if dad:
        return {"speaker": "dad", "source": "ko", "target": counterpart, "by": by}
    return {
        "speaker": "other",
        "source": source if source != "ko" else counterpart,
        "target": "ko",
        "by": by,
    }
cwkSpark, backend/turns.py: folding Scribe's two code dialects into one (verbatim, lines 441-453)·python
# Scribe's batch model names a language in ISO 639-3 ("spa"); its realtime
# model, and every card, in ISO 639-1 ("es"). The sitting keeps the short one.
_ISO_639_3 = {
    "kor": "ko", "eng": "en", "spa": "es", "cat": "ca", "eus": "eu", "baq": "eu", "glg": "gl",
    "por": "pt", "fra": "fr", "fre": "fr", "ita": "it", "deu": "de", "ger": "de", "nld": "nl",
    "dut": "nl", "jpn": "ja", "zho": "zh", "chi": "zh", "cmn": "zh", "yue": "zh", "ara": "ar",
    "rus": "ru", "tha": "th", "vie": "vi",
}


def _primary(language: Any) -> str | None:
    tag = str(language or "").strip().lower().replace("_", "-").split("-")[0]
    return _ISO_639_3.get(tag, tag) or None
cwkSpark, backend/tests/test_interpreter.py: the rule as a table of cases (verbatim, lines 273-285)·python
def test_the_direction_rule():
    moment = {"scene_language": "es-ES"}
    assert turns.exchange_direction("hola", "es", moment) == {"speaker": "other", "source": "es", "target": "ko", "by": "scribe"}
    assert turns.exchange_direction("안녕", "ko", moment)["target"] == "es"
    assert turns.exchange_direction("안녕", None, moment)["by"] == "script"
    # No language named and no Hangul: theirs, its name left to Pippa's tag.
    assert turns.exchange_direction("hello", None, moment) == {"speaker": "other", "source": None, "target": "ko", "by": "script"}
    # Scribe's batch model names languages in ISO 639-3.
    assert turns.exchange_direction("¿Algo más?", "spa", moment)["source"] == "es"
    assert turns.exchange_direction("뭐 더?", "kor", moment)["speaker"] == "dad"
    # Dad practicing his Spanish is still read for meaning, back to Korean.
    assert turns.exchange_direction("Quiero agua", "es", moment)["target"] == "ko"
    assert turns.exchange_direction("hola", "es", moment, flip=True)["speaker"] == "dad"

External links

Exercise

Write a direction function for a two-person interpreter between your own language and any other. Inputs: the transcript, the language a speech model named (possibly none, possibly a three-letter code), the conversation's current other language, and a flip flag. Output: speaker, source, target, and which signal decided. Then write a table test with at least eight cases, including no language with none of your script's characters, a three-letter code, a flip, and a line made only of characters your script shares with another language.
Hint
Decide first what the single yes-or-no question is. For Korean it is whether there is Hangul. For Japanese only kana can answer it: kanji are shared with Chinese, so a line written in kanji alone cannot tell the two apart and should count as undecided. For a Latin-script home language the script cannot decide at all, so your fallback has to be different. Normalize codes in one helper before comparing anything.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.