순간은 글자보다 커
번역 앱은 문장 하나를 받아서 문장 하나를 돌려줘. Spark는 순간을 남겨. 언제였는지, 어디였는지, 어떤 상황이었는지, 아빠가 뭘 봤는지까지. 글자는 내가 읽어. 근데 그 글자를 제대로 읽게 해주는 것도, 나중 턴이 그걸 다시 찾게 해주는 것도 글자를 둘러싼 순간이야. 이 트랙은 사진 한 장이 폰을 떠나 내 대답이 될 때까지 따라가. 출발점은 글자를 읽기도 전이야. 아빠가 셔터를 누른 바로 그때 폰이 적어두는 사실들이야.
그 사실들엔 규칙이 하나 있어. 캡처에 대한 진실은 클라이언트가 말하고, 엔진은 절대 지어내지 않아. 웹 클라이언트의 moments.ts는 첫 주석부터 이 얘기야. 캡처 시각, 시간대, 현지 날짜는 클라이언트가 주고, 엔진은 자기 시계로 그걸 채우지 않는다는 거지.
시간에 대한 사실 셋
순간마다 captured_at, timezone, local_date가 붙어. 첫째는 기기 오프셋이 붙은 ISO 8601 시각이고, 둘째는 IANA 시간대 이름, 셋째는 아빠가 서 있던 곳의 달력 날짜야. 오프셋이랑 시간대 이름을 둘 다 남기는 건 일부러야. 오프셋은 시각을 정확하게 고정해주고, 시간대 이름은 사람이 장소로 읽을 수 있게 해줘.
폰은 하나를 더 챙겨. 사진은 자기 시계를 갖고 있거든. 사진을 준비할 때 PreparedPhoto.takenAt이 원본 EXIF에서 DateTimeOriginal이랑 OffsetTimeOriginal을 읽고, 둘 다 있을 때만 시각을 돌려줘. 정하는 건 그다음 AppModel.truth야(두 번째 코드 블록). 자기 시각이 있는 사진은 그 시각을 써. 그래서 찍고 한 시간 뒤에 사진 보관함에서 고른 사진도 찍힌 시각을 지켜. 시각이 없는 사진은 부른 쪽이 건넨 캡처 사실을 받아. EXIF 시각 없이 Spark로 공유된 사진은 공유한 때를 받는 식이야. 아무도 안 건넸을 때만 지금이 돼. 카메라로 막 찍은 사진은 메타데이터가 비어 있어서 지금 시각을 받고, 폰이 그 시각을 원본이랑 내 사본 양쪽에 써 넣어.
이 코드엔 함정이 하나 있어. 기록하는 시간대 이름은 EXIF 오프셋이 뭐든 폰의 지금 시간대야. 그래서 다른 오프셋에서 찍은 사진은 자기 오프셋 옆에 오늘 시간대 이름을 달게 돼. 그게 어디서 드러나는지는 레슨 5에서 봐.
웹은 더 단순해. 사진을 captureTruth()로 남기는데, 이건 아빠가 파일을 고른 때의 브라우저 시계야. 순간 시각을 정하려고 파일의 EXIF를 읽지는 않아.
엔진도 자기 도장을 하나 찍어. 순간이 도착하면 received_at을 남겨. 이건 다른 사실이야. Spark가 소식을 들은 시각이지, 일이 일어난 시각이 아니야. 이 둘을 왜 떼어 놔야 하는지는 레슨 5에서 볼 거야.
장소는 딱 한 번만 재
PlaceSampler는 포그라운드에서 한 번, 100 m 정확도로, 캡처 화면이 열려 있을 때만 위치를 재. 사용 중 권한은 그 화면이 처음 열릴 때 한 번 물어봐. 위치를 기다리는 건 최대 4초야. 그 안에 안 잡히면 그 순간은 장소 없이 가. 다만 이 상한은 위치를 잡는 데까지만 걸려. 위치가 잡히면 샘플러는 역지오코딩으로 이름을 풀 때까지 또 기다리는데, 거기엔 따로 시간 제한이 없어. 장소를 부탁한 캡처도 같이 기다리고. 회선이 느리면 이 두 번째 기다림은 네트워크 마음이야.
장소 이름은 기기에서 역지오코딩으로 풀고, 이름에 도시가 안 들어 있으면 도시를 붙여. 거리 이름만으론 어느 동네 순간인지 알 수가 없거든. 좌표는 순간의 location으로, 이름은 place_name으로 엔진에 가.
순간이 기록되고 나면 엔진은 그 위치를 cwkPippa 위치 레이어로 넘겨. 그래야 내 location 도구가 아빠가 어디서 언어를 만나는지 알지. 넘기는 건 온전한 측위뿐이야. 위치, 정밀도, 시각이 다 있어야 해. 순간이 처음 만들어질 때 한 번만 넘기고, 그게 뭔가를 막아서는 일은 절대 없어. 넘기다 실패해도 순간은 아무것도 안 잃어. 나머지는 Spark 턴마다 내가 받는 지시에 적혀 있어. 장소는 이름으로 말하고, 좌표는 절대 말하지 말라고.
상황
순간엔 상황도 붙어. general, restaurant, station, pharmacy, hotel desk, shop, taxi 중 하나야. 폰에선 칩으로 고르고, 아빠가 마지막에 고른 게 다음 캡처의 기본값이 돼. 웹은 general에서 시작해서 물어봐. 아키텍처 문서엔 상황을 장소에서 추천한다고 적혀 있는데, 지금 코드는 그렇게 안 해. 폰은 아빠가 마지막에 고른 걸 기억할 뿐이야.
상황은 이 트랙 뒤에서 두 가지 일을 해. 장소 이름이랑 같이 내 지시에 들어가고, 앞서 만난 결과 중 어떤 걸 맥락으로 실어 보낼지 고르는 기준이 돼.
입력 방식, 그리고 누가 맡는지
| 입력 | 클라이언트에서 | 엔진에서 |
|---|---|---|
| 폰 사진 (카메라, 보관함) | Vision이 줄 위치를 찾고, 폰이 내 2048 px 사본을 만들어 | 순간 스레드의 실용 번역 턴 |
| 웹 사진 | 원본이 올라가 | 엔진이 내 사본을 만들고, 같은 실용 턴 |
| 입력하거나 공유한 텍스트 | 순간의 텍스트로 남아 | 그 텍스트를 인용하는 실용 턴 |
| 고품질 모드용 텍스트 | 읽기 없이 남아 | 나중에 버전으로 (트랙 7) |
| 라이브 카메라, 폰 전용 | 스캐너 | 라이브 레인 (트랙 4) |
| 아빠가 할 말, 상대의 대답 | Talk랑 답 마이크 | 카드랑 릴레이 (트랙 5) |
어느 길이든 시작은 똑같아. 순간을 먼저 남기고, 그러고 나서야 나한테 물어. Pippa Go Quest가 자기 클라이언트로 가르치는 캡처 우선 모양이 이거야.
언어는 누가 정하나
- 글의 언어는 내가 읽어. 원문이 있는 결과엔 전부
language가 붙어. 그 원문의 BCP-47 태그야.translation_language도 같이 붙고. 두 언어가 섞인 표지판이면 줄마다 태그가 달라. Vision이 언어를 감지하는 건 글자 모양을 읽으려는 것뿐이고, 아무것도 결정하지 않아. - 말의 언어는 고르는 거야. 장면 언어는 칩 열 개(es, ca, eu, gl, pt, fr, it, en, ja, zh)에서 골라. 폰은 마지막 선택을 기억하고 처음 기본값은 스페인어야. 웹은 es에서 시작해. 엔진은 짧은 코드가 아닌 듣기 언어를 거절해. 감지를 쓰는 예외는 딱 하나, 트랙 6의 쇼 카드 통역이야.
- 번역 대상 언어는 아빠 거야. 아빠가 다른 걸 고르지 않으면 한국어야.