본문 바로가기
C.W.K.
Lesson 01 of 05 · published

시팅은 대화 순간이야

~14 min · 통역, 아키텍처, 소유권, 레포 간 경계

Level 0첫 단어
0 XP0/47 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

버튼 하나를 잘못 읽은 일

쇼 카드는 아빠가 상대한테 돌려 보여주는 문장이야. 상대 언어로 쓴 피파의 문장이 크게 있고, 아빠용 한국어 한 줄이 작게 붙고, 피파 목소리를 트는 버튼이 하나 있어. 그 버튼 이름이 원래 "Speak"였어. 어떤 웨이터가 그걸 보고 여기 대고 말하면 번역되는 줄 알고 폰에 대고 말을 했지. 버튼 이름은 Listen으로 바뀌었고, 아빠는 그 오해를 기능으로 만들었어. 사람들이 어차피 카드한테 말을 거니까, 카드가 들으면 되잖아.

실제로 그런 기능을 넣는 건 어때? 이건 아빠한테도 유용할 거야. 카드에서 바로 말을 하면 (어떤 언어로 입력되든 번역어를 판단해서 A -> B 언어로) 실제 번역해주는 기능. 아빠가 쓰면 바로 주문 카드가 되는 식이고. 상대 원어민이 쓰면 아빠가 볼 수 있는 우리말이 나오는 식이고. 실시간 번역인 거지. 대신 피파가 문맥을 알아서 판단하는 거고.

조각 다섯, 카드 하나

이 기능은 레포 하나에 다 안 담겨. 그러니까 누가 뭘 가졌는지부터 보자.

조각주인
화면 위 카드: Talk, 실시간 자막, 방향 칩, 다듬은 문장Sibling Kit의 phrase card와 통역 모듈(웹은 KitPhraseCard랑 kitInterpreter.ts, iOS는 CwkKitInterpreter). 그리는 건 cwkPippa 웹이랑 피파 아이폰 앱
들어도 된다는 허가cwkPippa의 /api/stt/realtime-session, language_code="auto". Scribe v2 Realtime 토큰은 cwkBellows가 찍어 주고, 기기가 Scribe에 직접 연결해
문cwkPippa의 /api/interpret. 루프백으로 Spark에 넘기기만 하는 얇은 문
시팅과 그 교환들Spark 엔진. 대화 순간 하나, 발화마다 exchange 턴 하나, 녹음, 이벤트
카드에 입힐 피파 목소리cwkBellows. Spark가 자기 음성 바인딩으로 불러

문은 독스트링에 자기가 넘지 않는 선을 적어 뒀어. 여기선 아무것도 번역하지 않고, 아무것도 저장하지 않는다. 하는 일은 id가 ULID 모양인지 보고, 녹음의 역할을 두 가지 중 하나로 제한하고, Spark가 준 답이나 거절을 그대로 돌려주는 거야. Spark가 죽어 있으면 카드가 두 언어로 그렇다고 말하고, Listen은 그래도 돌아가.

듣는 쪽은 Voice Mode Quest 영역이야. Scribe 실시간 소켓, 말하는 중간에 뜨는 단어, 말이 끝났다고 보는 시간 창은 거기서 배워. 통역사는 그 귀를 그대로 쓰고 딱 하나만 바꿨어. 언어를 미리 정하지 않는다는 거. cwkPippa 쪽 집 구조는 Pippa Quest가 다뤄.

왜 순간이야

통역사가 생기기 전에도 Spark엔 대화 순간이 있었어. 거기엔 아빠가 미리 써 두는 카드, 답 마이크, 상대 말을 피파가 옮겨 주는 릴레이가 들어 있었지. 테이블에 앉아 있는 시간도 그런 순간 하나로 들어가. 아빠가 카드에서 Talk를 누르면 시팅이 열리고, 엔진은 시작한 카드를 순간의 origin으로 들고 있어. 거기엔 카드 문장, 아빠가 뜻한 것, 그 언어, 카드가 나온 cwkPippa 대화까지 담겨. 시팅의 장면 언어는 카드 언어로 시작해.

새 종류의 기록을 만들지 않고 순간으로 둔 덕에 세 가지를 한꺼번에 얻어. 시팅이 사진이나 간판처럼 코퍼스에 들어가니까 검색되고 기억돼. 아빠의 베일 씌운 기록은 원래 낯선 사람 목소리를 둘 사적인 공간이라, 녹음이 안전하게 머물 데가 이미 있어. 그리고 뒤에 오는 턴이 순간을 맥락으로 읽으니까, 테이블의 다음 문장이 앞 문장을 알아.

나중에 아빠가 빈 카드를 달라고 했어. 문장 없이 대화를 시작하는 방법이 필요했던 거야. 빈 시팅엔 origin이 없어. 그럴 땐 cwkPippa 문이 아빠의 가장 최근 위치 기록을 봐. 여섯 시간 안쪽 것만 쓰고, 역지오코딩으로 나라를 알아낸 다음 그 나라를 언어로 바꿔. 일본이면 일본어, 포르투갈이면 포르투갈어. 쓸 만한 위치가 없으면 아빠의 첫 문장이 정해. 피파가 아빠 첫 카드를 쓴 언어가 시팅의 언어가 돼.

Spark 자체 앱이 아직 안 하는 것

여기 경계를 똑바로 잡아 둬. 시팅과 교환은 Spark 엔진 거야. 그런데 지금 /exchanges를 부르는 건 Spark 웹도 SparkMobile도 아니야. 부르는 쪽은 전부 cwkPippa 화면이고, 위의 문을 지나와. Spark 웹에도 킷의 phrase card가 통역 transport까지 통째로 들어와 있긴 해. 근데 transport를 넘겨주는 Spark 화면이 하나도 없어서, Spark 카드는 예전 그대로야.

이 순서는 아빠가 정했어. 아빠가 원래 카드를 보여주던 cwkPippa 웹이랑 피파 아이폰 앱이 먼저고, Spark 웹이 두 번째, SparkMobile이 맨 끝이야. SparkMobile이 끝인 데엔 구조적인 이유가 있어. Spark 폰은 Spark 엔진하고만 말하고 제공자 자격 증명을 하나도 안 들고 있어. 그래서 cwkPippa 화면처럼 ElevenLabs에 직접 연결할 수 없고, 먼저 엔진을 거치는 중계가 있어야 해.

원칙: 기록은 나머지 기록이 이미 사는 곳에 둬. 통역사 화면은 아빠가 손에 쥐고 있던 화면으로 갔고, 그 기억은 언어를 기억하는 게 본업인 앱으로 갔어.

그러니까 이 트랙에서 엔진 발췌는 다 Spark 거로 읽고, 카드 동작은 cwkPippa가 그리는 Sibling Kit 카드로 읽어.

Code

cwkPippa, backend/routes/interpret.py: 문이 발화 하나를 Spark로 넘긴다 (원문 그대로, 176-184행)·python
@router.post("/sessions/{moment_id}/utterances")
async def utterance(moment_id: str, body: UtteranceIn) -> dict[str, Any]:
    """One utterance: Spark decides the direction at once and streams the
    translation through the sitting's events."""
    return await _forward(
        "POST",
        f"/api/moments/{_moment_id(moment_id)}/exchanges",
        json=body.model_dump(exclude_none=True),
    )
cwkSpark, backend/corpus.py: 시팅을 연 카드를 텍스트로 남긴다 (원문 그대로, 134-141행)·python
def _origin(value: Any) -> dict[str, Any] | None:
    """The show card an interpreter sitting was opened on, kept as text."""
    if value is None:
        return None
    if not isinstance(value, dict) or not str(value.get("text") or "").strip():
        raise CorpusError("an origin is the card's text, with its translation and language")
    fields = ("text", "translation", "language", "pippa_conversation_id")
    return {key: str(value[key]).strip() for key in fields if value.get(key) and str(value[key]).strip()}
발화 하나를 누가 나르나 (이 레슨용으로 그린 그림)·text
device: cwkPippa web or the Pippa iPhone app
  the card (Sibling Kit phrase card + interpreter module)
    1. grant   -> cwkPippa /api/stt/realtime-session  (language_code "auto")
    2. audio   -> Scribe v2 Realtime, dialed by the device itself
    3. words   -> cwkPippa /api/interpret/sessions/{id}/utterances
                      |
                      v  loopback, nothing translated, nothing stored
Spark engine
  conversation moment = the sitting   (origin = the card it began on)
  exchange turn       = one utterance (direction, draft, refined result)
  events              -> back through the door to the card

External links

Exercise

만들고 있는 기능 중에 사용자 화면과 직접 운영하는 백엔드에 걸쳐 있는 걸 하나 골라. 이 레슨 표처럼 조각마다 한 줄씩(화면, 권한, 문, 기록, 부수 효과) 쓰고 줄마다 주인을 하나만 적어. 그런 다음 화면을 띄우는 쪽과 기록 주인 사이에 둘 수 있는 가장 얇은 문을 설계해 봐. 문이 검사하는 걸 빠짐없이 적고, 아무것도 저장하거나 바꾸지 않는지 확인해.
Hint
주인이 둘인 줄이 있으면, 그 기능엔 서로 어긋날 날만 기다리는 진실이 둘 있는 거야. 문에 데이터베이스 테이블이 필요해지면, 그 테이블이 진짜 어느 주인 건지 물어봐. 검사하고 넘기기만 하는 문은 오후 한나절이면 지우고 다시 쓸 수 있어. 그게 바로 노리는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.