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

음성 차선: 말라가에서 잰 것

~17 min · latency, reranking, timeline, measurement

Level 0음소거
0 XP0/61 lessons0/14 achievements
0/100 XP to next level100 XP to go0% complete
"레이턴시는 확정적이다. 타이핑하는 턴은 거의 느껴지지 않아. 폰이든 웹이든 보이스 모드로 채팅할 때만 느려." — 아빠, 말라가, 2026-10-18

또 길 위에서

이 트랙 첫 레슨은 책상 앞에서, 오피스의 자기 네트워크 위에서 턴을 쟀어. 3주 뒤 아빠는 스페인의 호텔 와이파이와 LTE로 피파랑 얘기하고 있었고, 아빠가 겪는 숫자는 그 레슨의 숫자가 아니었어. Lightning(트랙 3의 새 레슨)을 써 본 아빠의 첫 평가는 직설적이었어. 아무 소용이 없다고. 어느 브레인이든 생각을 시작하기 전의 기다림부터 길다고. 그래서 2026-10-18 밤에 이 집은 단계마다 다시 쟀어. 이번엔 오피스 쪽만이 아니라 요청 양쪽을 다.

말 턴의 시간은 어디로 갔나

단계무엇그날 밤 든 시간
말 끝Scribe의 침묵 창, 아빠가 정한 값기본 3초
시끄러운 곳 다시 읽기배치 모델이 발화를 한 번 더 읽어1초쯤, 시끄러운 곳 모드에서만
화자 확인녹음을 cwkBellows의 귀로(트랙 8)1.4~3.9초
기억 검색임베딩 0.1초, 그다음 스레드 하나 위의 4B 재정렬기2~9초
시스템 프롬프트 만들기캐시해 둔 볼트 바탕0.3~0.8초, 검색 뒤에 가려져
Claude CLI 시작띄우기, 연결, CLI 자체 초기화초기화가 13.7~41초 지점
제공자에서 첫 토큰까지마스터 치프의 Lightning(약 28만 토큰), 캐시 식음 / 데워짐12~13초 / 2.3~5초
마스터 치프의 Claude Opus(약 40만 토큰, 거의 다 캐시에서)초기화 뒤 7.5~10초
도구 호출 하나Claude의 셸이나 MCP 도구10초쯤
첫 소리prepare와 스트림의 첫 바이트0.9~1.6초

이 중 두 줄은 피파 코드 탓이 아니고, 설계 문서도 그렇게 적어. CLI의 느린 초기화와 도구 하나에 10초씩 드는 건 둘 다 오피스의 파일 감시 데몬에서 왔어. 그건 맥 자체의 문제라 플릿의 이슈 기록으로 갔어. 피파 코드 몫인 줄들은 한 표에 놓고 보니 모양이 훤히 보였어. 화자 확인과 기억 검색이 하나씩 차례로 돌았던 거야. 그래서 마스터 치프 음성 턴의 준비가 5.4~8.7초였어.

나란히, 그리고 일찍 듣기

이제 채팅 라우트는 전부 화자 확인보다 먼저 기억 검색을 시작해(나중에 라우트가 받아 가는 EarlyRag). 그래서 준비 시간이 둘을 더한 값이 아니라 둘 중 긴 쪽이 돼. 중요한 순서는 그대로야. 화자 확인은 여전히 아빠의 행이 저장되기 전에 턴을 판정해. 그리고 최종 질의나 범위가 검색을 시작할 때와 다르면, 미리 받은 답은 버리고 라우트가 자기 검색을 다시 돌려. 확인 자체도 앞으로 당겨졌어. 폰은 16 kHz 녹음을 턴보다 20~45초 먼저 올려. 그래서 이제 업로드 라우트가 그 자리에서 들어 두고, 턴은 그 답이 오면 받아 가. 웹에선 업로드와 턴이 같은 초에 도착하니까, /api/stt가 Scribe를 돌리기 전에 녹음을 남기고 그 옆에서 확인을 시작해. 둘 다 바꾼 뒤 라우트 자신의 줄은 준비 1.1~3.3초, 화자 확인 0초였어. 음성 턴이든 친 턴이든.

기억에도 음성 차선을

이 트랙 두 번째 레슨은 결정 하나로 끝났어. 기억 품질이 먼저고, 0.5초에서 2초 빨라지는 건 보이스 모드가 아니면 의미 없다고. 4B 재정렬기는 남았고, 더 빠른 선택지는 재 보고 거절했어. 2026-10-18, 아빠는 그 결정에 스스로 적어 둔 단서를 썼어. 재정렬기가 Admin에서 고르는 값이 됐고, 말 턴엔 따로 고르는 칸이 생겼어. 쓰는 턴은 rag_rerank_model(비우면 서버에 설정된 모델), 말 턴은 Lightning까지 포함해서 rag_rerank_voice_model(비우면 쓰는 턴의 선택). 고를 수 있는 모델은 모델 서버에서 그때그때 읽어 오고, 서버에 없는 모델은 거절해. 폰의 Settings에도 같은 두 칸이 있어. 아빠는 음성 차선을 0.6B 재정렬기로 정했어. 짧은 말 턴의 검색이 4B에서 2.74초, 0.6B에서 0.63초였어. 쓰는 턴은 여전히 4B로 매겨.

값은 그 레슨이 잰 그대로야. 같은 프롬프트 20개에서 0.6B는 4B가 찾은 조각의 4분의 3쯤만 남겼어. 이제 그 값은 말 턴에서만 치르고, 그걸 치르는 사람이 직접 골랐고, 턴마다 뭘 치렀는지 스스로 말해. 라우트의 [turn-prep] 줄에 그 턴이 쓴 재정렬기 차선과 모델이 적히거든.

음성 턴마다 타임라인 하나

오피스 자신의 줄은 요청이 도착할 때 시작해서 모델이 끝날 때 끝나. 그 양쪽에서 아빠가 겪은 건 아무것도 안 남았어. 녹음이 링크를 건너가는 시간도, 피파 목소리가 돌아오는 시간도. 이제 음성 턴은 타임라인을 하나씩 보내. 클라이언트가 피파의 첫 소리가 날 때 한 번 보내. POST 전의 시간(말 끝, 통째 받아쓰기), 녹음마다 건너간 시간(바이트와 초, 오피스 자신의 처리는 Server-Timing 헤더로 빼고), 그리고 POST 뒤의 시간(첫 글, done, 첫 단위, 첫 소리). 오피스는 자기 단계를 합쳐서 [voice-turn] 줄 하나를 남기고, 대화의 JSONL에 먼저 적은 다음 아빠의 행에 적어. 비밀 대화는 이름표만 남기고 어디에도 저장하지 않아.

LTE 위의 첫 폰 턴 일곱 개가 기다림의 진짜 크기를 보여줬어. 아빠 말이 끝나고 피파의 첫 소리까지 중앙값 44.6초. 턴이 폰을 떠나기도 전의 33.4초 가운데 11.1초는 순수한 전송이었어. 220~560 KB를 초당 10~40 KB로. 떠난 뒤엔 오피스의 첫 글이 8.0초에 나왔는데, 48 kHz 녹음이 답 옆에서 같이 올라가던 세 턴은 스트림이 건너오는 데 0.4~1.5초가 아니라 4.5~17.5초가 걸렸어. 그리고 피파의 첫 소리는 첫 글보다 중앙값 5.2초 늦었어. 답의 마지막 단위가 남은 글 전부였으니까.

숫자마다 자기 처방을 가리켰고, 소울을 건드린 처방은 하나도 없었어. 48 kHz 사본은 이제 피파 답이 다 온 뒤에, 턴 뒤에서 올라가. 대화에 건넨 사진은 아빠가 아직 말하는 동안 올라가기 시작해. 자기 해시를 키로 써서, 같은 사진이면 언제나 같은 업로드를 가리켜. 사진 한 장짜리 턴은 아빠 말이 끝나고도 그 사진을 17~23초씩 기다렸었거든. 웹은 답의 마지막 단위를 스트림이 닫힐 때가 아니라 done에서 읽어. 그 차이가 2~5초였어. 그리고 채팅 답은 문단마다 읽혀. 트랙 4 끝의 새 레슨이야.

Code

최소 스케치: POST 양쪽에서 모은 음성 턴의 합계, 그리고 차례로 도는 준비와 나란히 도는 준비·python
# One voice turn's timeline: the client's half before and after its POST, the
# uploads in between, and the sums. The shape follows cwkPippa's voice_timeline.build.


def totals(before: dict, uploads: list[dict], after: dict) -> dict:
    """Seconds before the POST count back from it; seconds after count from it."""
    out = {"speech_end_to_post": before["speech_end"]}
    ahead = [u for u in uploads if u["before_post"]]
    # What the link cost, with office's own work on each upload taken out.
    out["transfer_before_post"] = round(sum(u["seconds"] - u["server_seconds"] for u in ahead), 1)
    out["post_to_first_audio"] = after["first_audio"]
    out["speech_end_to_first_audio"] = round(before["speech_end"] + after["first_audio"], 1)
    out["first_text_to_first_audio"] = round(after["first_audio"] - after["first_text"], 1)
    return out


def prep(speaker_check: float, rag: float, side_by_side: bool) -> float:
    """The route's work before the model is asked: one after the other, or the longer of two."""
    return max(speaker_check, rag) if side_by_side else speaker_check + rag


# A phone turn over LTE built from build 152's medians (2026-10-18). Medians of
# different turns do not add up: the measured median of the whole wait was 44.6 s.
before = {"speech_end": 33.4, "transcribed": 30.1}           # seconds before the POST
uploads = [
    {"kind": "listen", "bytes": 380_000, "seconds": 12.6, "server_seconds": 1.5, "before_post": True},
    {"kind": "archive", "bytes": 3_100_000, "seconds": 40.0, "server_seconds": 0.4, "before_post": False},
]
after = {"first_text": 14.1, "first_audio": 19.3}              # seconds after the POST

for name, value in totals(before, uploads, after).items():
    print(f"{name:26s} {value:5.1f} s")
print()
for label, side in (("one after the other", False), ("side by side", True)):
    print(f"turn prep, {label:20s} {prep(speaker_check=2.6, rag=2.74, side_by_side=side):4.2f} s")
# Heard early at the upload, the speaker check costs the turn nothing; the voice lane reranks with the 0.6B.
print(f"turn prep, side by side, 0.6B  {prep(speaker_check=0.0, rag=0.63, side_by_side=True):4.2f} s")

# Output:
# speech_end_to_post          33.4 s
# transfer_before_post        11.1 s
# post_to_first_audio         19.3 s
# speech_end_to_first_audio   52.7 s
# first_text_to_first_audio    5.2 s
#
# turn prep, one after the other  5.34 s
# turn prep, side by side         2.74 s
# turn prep, side by side, 0.6B  0.63 s

External links

Exercise

스케치를 돌려서 양쪽을 다 읽어 봐. 그다음 보관용 업로드를 POST 앞으로 옮기고(before_post: True) 어느 합계가 얼마나 늘어나는지 봐. 다음엔 prep()에 세 번째 방식을 더해. 검색을 일찍 시작했는데 질의가 바뀌어서 그 답을 버리고, 라우트가 검색을 한 번 더 도는 경우야. 마지막으로 네 요청 경로 하나를 양쪽에서 재 봐. 클라이언트의 순간을 찍고, Server-Timing 헤더를 돌려주고, 링크 몫을 서버 몫과 따로 보고해.
Hint
보관본을 앞에 두면 transfer_before_post가 39.6초쯤 뛰어. 보관본이 건너간 시간에서 오피스 자신의 처리를 뺀 값이야. speech_end_to_post는 안 움직여. 스케치는 그걸 잰 값으로 받아 쓰거든. 실제 턴이라면 그 초가 그 안에 들어앉았을 거고, POST 앞에 21~78초를 쌓았던 빌드 149의 모양이 딱 그거야. 버린 조기 검색은 화자 확인에 검색 한 번을 통째로 더한 값, 그러니까 예전에 차례로 돌던 합과 같아. 일찍 시작해서 그보다 손해 볼 일은 없고, 이득만 사라지는 거야. 그래도 라우트는 질의나 범위가 정말 다를 때만 버려. Server-Timing은 dur을 밀리초로 보내고, 클라이언트가 잰 왕복 시간에서 빼.

Progress

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

댓글 0

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

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