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

Pippa Chooses What Is Shown

~14 min · structured-output, prompting, shared-components, bcp-47

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

Show is said to someone

Every phrase in Spark can be heard: Listen plays it in my voice for Dad. Show is a different act. It opens a full-screen card meant for the other person, turned toward them, with the words large. A card on a stranger's side of the table is a statement made in Dad's name, so choosing which words get Show is part of speaking for him.

Spark got this wrong once, briefly. The shared full-screen card arrived, and a few minutes later another commit offered Show on every interpretation expression. Within the hour the next commit took that back and limited Show to phrases I designate for the other person (todo #343). The problem with Show everywhere is easy to picture. A menu reading has a dozen lines and a few expressions. With a Show button on each, Dad has to decide at the table which of them are for the server. I can make that decision while I write, because I know which line is the order and which is a dish name he only wanted to understand.

The rule: by kind, or by an explicit flag

Two kinds are meant for the other person by definition: card, which Dad asked me to write for someone, and answer, a reply he can play back. Everything else, a line read off a sign or an expression, gets Show only when I mark it with the boolean show: true. Ordinary translations, read signs, heard replies and learning examples keep Listen and have no Show.

The decision is taught in the Spark frame that every turn about a moment carries (first code block). It says what qualifies, a complete, context-appropriate message for that person, and it gives two concrete boundaries. In restaurant guidance, designate just the ordering request. In taxi advice, designate just the destination or the driver-facing question. Explanations and pronunciation stay outside the words the other person sees.

Both clients project the rule in one line. On the phone, canShow on the result model is kind == "card" || kind == "answer" || show == true. The web's canShowResult is the same expression in TypeScript. Neither one looks at paragraphs, language or length to guess.

A strict boolean that survives revision

The engine's normalize keeps show only when it is a real JSON boolean. The string "true", the number 1, an empty object and null are all dropped (second code block). A model that writes a string has not designated anything, and the code refuses to read intent into it.

Show is also part of a result's content. content_of, which the engine uses to tell a repeated result from a revised one, includes it. If I write the same expression again in a later stream with show added, it is kept as a revision of the first one, not skipped as a duplicate. The second test in the block guards the prompt side: if an edit to the frame ever drops the restaurant or taxi boundary, it fails.

Lines written before the rule existed stay unmarked. Nothing backfills a designation or infers one, and a kept list does not treat a star as one. An expression's own detail page shows the designation of the result it came from.

One card for the whole family

The full-screen presentation is not Spark's. It belongs to cwkSiblingKit: KitPhraseCard on the web and PhraseCard in the CwkKitMedia Swift package, which cwkPippa's own apps use too. The split is written at the top of Spark's iOS ResultPhraseButton: Spark owns language direction, the kit owns the full-screen presentation. Spark's conversation BigCard delegates to the same component and keeps only its own voice action.

The kit's card fits portrait and landscape, offers a small meaning that can be hidden, and scrolls very long text instead of cutting it. Showing a phrase that is already on screen does no network work. The native card holds the idle timer while it is up and restores it when dismissed. On the web, wake lock is best effort, and the card still works when the browser says no. The two platforms take the kit differently. The web card is vendored into Spark as a checked copy, and Sibling Kit Quest explains why. The iOS card is not copied at all: the app's project lists the kit's own Swift package as a local package, by a relative path to the sibling repository, and links its CwkKitMedia product.

Which words go big: the reverse-card rule

One more decision sits between a result and its card: which of its two texts is shown and read. Usually it is source, the foreign words. Some results run the other way. When Dad asks in Korean how to say something, the Korean can end up as the source and the foreign sentence as the translation. Showing the Korean to a server would be useless.

SourceReading.resolve (third code block, with a web twin in sourceReading.ts) settles it. It takes each text's language from my tags, language and translation_language. A result without tags falls back to a Hangul check over the syllable and Jamo ranges, and then to the scene's language. If a line has a Korean source and a non-Korean translation, the reading starts on the translation. Listen and Show use the same reading, so the big card shows the foreign words and the small meaning under it is Dad's Korean. A translation gets its own player id, so its audio is a separate take from the source's.

The tags decide which text a line starts on and the card's display language. They are never sent to the voice. The rule is a compatibility layer, and it rewrites nothing in the corpus.

Principle: Let the author of a result decide who it is for, while the context is still in hand, and make every reader project that decision the same way instead of guessing it again.

Code

cwkSpark, backend/turns.py: the Spark frame teaches deliberate designation (verbatim, lines 127-136)·python
    lines.append(
        "Choose what is intended for the person Dad is speaking to. Only `card` and `answer` "
        "results, or a line/expression explicitly marked with boolean `show: true`, get a Show action. "
        "Use `show: true` only when the source (or the foreign translation of a Korean source) is "
        "a complete, context-appropriate message for that person. Do not mark every translation, "
        "sign, menu item, heard reply or learning example. In restaurant guidance, designate just "
        "the ordering request; in taxi advice, designate just the destination or driver-facing question. "
        "Keep explanations and pronunciation outside the recipient-facing words. Omit `show` otherwise. "
        "Use Spark result fields for this designation, never a separate `show` fence."
    )
cwkSpark, backend/tests/test_show_designation.py: a strict boolean that counts as content, and a prompt that keeps its boundaries (verbatim, lines 1-21)·python
from backend.results import normalize, content_of
from backend.turns import _spark_frame


def test_show_requires_boolean_and_survives_revision_identity():
    base = dict(type='expression', source='La cuenta, por favor.', translation='계산서 주세요.')
    ordinary = normalize(base)
    chosen = normalize(dict(base, show=True))
    assert 'show' not in ordinary
    assert chosen['show'] is True
    assert content_of(chosen) != content_of(ordinary)
    assert content_of(normalize(dict(base, show=False)))['show'] is False
    for invalid in ('true', 1, {}, None):
        assert 'show' not in normalize(dict(base, show=invalid))


def test_every_spark_frame_teaches_deliberate_selection():
    prompt = _spark_frame({'id': 'moment'})
    assert 'show: true' in prompt
    assert 'Do not mark every translation' in prompt
    assert 'ordering request' in prompt and 'driver-facing question' in prompt
cwkSpark, clients/apple/SparkMobile/Sources/SourceReading.swift: which text a result reads and shows (verbatim, lines 13-32)·swift
struct SourceReading {
    var text: String?
    var language: String
    var translated: Bool

    static func hasHangul(_ text: String?) -> Bool {
        (text ?? "").unicodeScalars.contains {
            (0xAC00...0xD7AF).contains($0.value) || (0x1100...0x11FF).contains($0.value)
        }
    }

    static func resolve(_ item: PippaResult, scene: String, translated: Bool? = nil) -> SourceReading {
        let sourceLanguage = item.language?.nonEmpty ?? (hasHangul(item.source) ? "ko-KR" : scene)
        let targetLanguage = item.translationLanguage?.nonEmpty ?? (hasHangul(item.translation) ? "ko-KR" : scene)
        let reverse = item.kind == "line" && SourceLanguage.normalized(sourceLanguage).hasPrefix("ko")
            && !SourceLanguage.normalized(targetLanguage).hasPrefix("ko") && item.translation?.nonEmpty != nil
        let readTranslation = translated ?? reverse
        return SourceReading(text: readTranslation ? item.translation : item.source,
                             language: readTranslation ? targetLanguage : sourceLanguage, translated: readTranslation)
    }

External links

Exercise

Add a recipient-facing designation to the structured output of any model-backed app. Define it as an optional strict boolean on a result, drop any non-boolean value at parse time, include it in whatever you use to detect a revised result, and show a Show action only for results that carry it or whose kind is recipient-facing by definition. Write the prompt with two concrete boundary cases from your own domain, plus a test that fails if either case disappears from the prompt.
Hint
Feed the parser the string true, the number 1, an empty object and null: all four must vanish. Then write one result twice with only the flag changed and check that the second is stored as a revision. If your UI starts inferring the flag from length or language, the decision has moved out of the model and into a guess.

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.