묶음은 엔진이 먼저 봐
폰이 보낸 묶음은 POST /api/live/translate에 떨어져. 라이브 순간 id 하나에 항목이 1개에서 40개까지 실려 있고, 항목마다 키 하나와 500자 이하 글자 하나가 들어 있어. 이 경로는 라이브가 아닌 순간은 거절해(평범한 순간을 보내고 400을 기대하는 테스트가 있어). 거기를 통과하면 live_lane 슬롯에 어느 두뇌가 배정됐는지 확인하고, 전부 턴 실행기로 넘겨. 실행기는 어떤 두뇌도 부르기 전에 항목마다 코퍼스한테 먼저 물어봐.
"같은 글자"를 가리키는 키 하나
맞춰보기는 source_key에서 시작해. 공백을 한 칸으로 접고, casefold()를 걸어. casefold()는 대소문자 구분 없이 비교하라고 만든 거라, 파이썬의 그냥 소문자화보다 세. 원문 글자가 있는 결과는 사진에서 왔든 대화에서 왔든 라이브 카메라에서 왔든, 기록될 때 이 키를 달아. 미러의 results 테이블엔 (source_key, target_language, seq) 인덱스가 있고, known_translation은 그 위에서 도는 인덱스 쿼리 한 번이야. 키와 목표 언어가 같고 종류가 line, live-line, expression 중 하나인 결과 가운데 제일 최근 걸 찾아.
핵심은 그 종류 셋이야. 사진 찍은 메뉴판에서 내가 읽어준 줄, 대화 중에 내가 건넨 표현, 지난주에 라이브로 본 간판. 그중 뭐든 오늘 카메라한테 답이 돼. 그리고 쿼리가 seq 기준으로 제일 최근 걸 가져오니까, 나중에 고친 답이 다음 조회의 답이 돼. 트랙 3에서 본 대로 수정은 덮어쓰기가 아니라 새 결과거든. 함정은 그 다음 조회가 언제냐야. 폰은 세션 동안 받은 답을 메모리에 들고 있고, 같은 글자는 엔진에 두 번 안 물어(레슨 1). 그래서 카메라가 켜져 있는 동안 고친 답은 지금 세션엔 안 닿고, 다음 라이브 세션에 가서야 닿아. 설계 문서엔 그 효과가 숫자로 적혀 있어. 측정에 쓴 간판 넷이 코퍼스에서 20 ms 만에 돌아왔대.
캐시에서 찾아도 기록은 남겨
캐시에서 찾았다고 문자열만 돌려주고 끝내진 않아. 실행기는 이 라이브 순간에 새 live-line 결과를 써. 거기엔 스캐너가 읽은 글자 그대로, 이미 아는 번역과 발음, 그리고 베껴 온 결과를 가리키는 cached_from이 들어가. 두뇌가 안 돌았으니 턴은 없어. 그래서 라이브 순간엔 답을 받은 글자가 전부 남아. 빌려 온 답엔 출처도 붙어 있고. 언제 어디서는 순간 자체가 들고 있는 값이야. 세션을 열 때 폰이 잰 장소와 시각이지, 글자마다 따로 잰 게 아니야.
독스트링이 약속하는 건 셋이야. 이미 만난 글자는 코퍼스에서 바로 답하고, 나머지는 짧은 호출 한 번으로 피파한테 가고, 글자는 전부 이 라이브 순간에 코퍼스로 남는다는 거. 근데 두 군데는 적힌 것보다 느슨해. 끝까지 번역 못 한 글자는 결과가 아예 안 생겨서 코퍼스에 안 남아(아래). 그리고 바로라는 말은 조회까지만 맞아. 경로는 묶음 전체가 끝나야 폰한테 답해. 그래서 아는 글자랑 새 글자가 섞인 묶음에선, 아는 답도 내 호출이 끝날 때까지 기다렸다가 내 답이랑 같이 도착해.
못 찾은 건 짧은 호출 한 번
코퍼스가 모르는 건 한꺼번에 나한테 와. 실행기는 두뇌와 항목 수를 적은 live 턴을 기록하고, cwkPippa 형제 유틸리티 문에 보낼 원샷 요청을 만들어. 요청엔 번호를 붙인 못 찾은 글자 목록, 시스템 프롬프트, 슬롯의 두뇌와 노력 수준이 들어가. 거기에 max_tokens: 0(Spark 쪽 상한 없음), persona_soul_id: "pippa", source: "spark-live"가 붙어. 문은 대화 없이, 도구도 끈 채로 이걸 돌려. 엔진은 60초까지 기다려.
시스템 프롬프트는 짧고 엄해. 목표 언어를 말하고, 순간에 상황이랑 장소가 있으면 덧붙여. 한 줄에 JSON 객체 하나씩 답하라고 하는데, 거기엔 번호와 번역, 그리고 아빠가 소리 내서 말할 만한 짧은 것에만 발음을 넣으라고 해. 번역은 원래 간판만큼 짧게 하라는 주문도 있어. 답은 parse_json_lines가 읽어. 중괄호로 시작하는 줄을 다 가져오고, 펜스나 끝에 붙은 쉼표는 봐주고, 나머지는 건너뛰어. 객체마다 n으로 원래 항목을 찾아가.
번역된 건 전부 그 턴 아래 live-line이 돼. 끝까지 번역 안 된 건 pending으로 돌려보내고, 폰은 그 키를 풀어서 나중에 다시 물어(레슨 1). pending인 글자는 결과를 하나도 안 남겨서 순간에도 없어. 물어봤다는 흔적은 기껏해야 턴에 적힌 항목 수야. 턴은 하나라도 돌아왔으면 completed, 하나도 없으면 failed로 끝나. 걸린 시간, 모델, 노력 수준, 오류도 같이 남아.
기계 번역으로 메우지 않아
설계하던 날, 이 모드엔 아직 못 정한 질문이 하나 있었어. 기계 번역이라고 표시한 Apple 온디바이스 번역으로 빈칸부터 채워 두고, 몇 초 뒤에 내 답이 오면 갈아끼울까? 아빠 답은 두 마디였어.
A로 가.
여기서도 피파만 쓴다는 뜻이야. 그래서 레인엔 두 번째 길이 없어. 배정된 두뇌가 없거나 문이 거절하면, 못 찾은 글자는 오류와 함께 pending으로 돌아가. 폰은 그 오류를 상태 캡슐에 띄우고. 짐작으로 채우는 건 없어. 새 글자는 몇 초 기다리고, 이미 만난 글자는 묶음에 새 글자가 없으면 바로 나와. 섞여 있으면 내 답이랑 같이 오고. 이 둘이 한 쌍이고, 이 레인이 약속하는 건 그게 다야.
재시작도 같은 생각으로 다뤄. 엔진이 멈출 때 돌고 있던 live 턴은 다음에 켜질 때 interrupted로 적히고, 다시 확인하지 않아. 물어볼 cwkPippa 대화가 없거든. 그 글자가 화면에 다시 들어오면 카메라가 그냥 또 물어.
테스트 test_live_text_already_met_answers_from_the_corpus_and_the_rest_from_pippa가 처음부터 끝까지 증명해. 먼저 La cuenta를 담은 텍스트 순간을 번역해 둬. 이어서 라이브 묶음이 공백 두 칸에 대문자인 La CUENTA랑 Salida를 보내. 앞엣것은 원래 줄을 가리키는 cached_from을 달고 코퍼스에서 나오고, 가짜 문까지 가는 건 라이브 슬롯의 low 노력 수준을 단 1. Salida 하나뿐이야. 두 번째 묶음의 소문자 salida도 코퍼스에서 나오고, 문은 여전히 한 번만 불렸어. 인덱스 쪽 이야기는 SQLite Quest에 자세히 있어.