~15 min · forced-commit, debugging, measurement, batch-retranscription
Level 0음소거
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"지금 끼어들기 기능에 약간의 버그가 있어. 아무 데서나 짤리는 경향이 있다." — 아빠, 2026-09-27
진단부터 틀렸어
녹화한 시연에서 아빠 말이 문장 중간에 두 번 나가버렸어. 둘 다 말끝이 반쯤 잘린 채였지. 다른 피파 인스턴스가 올린 첫 보고는 이걸 가짜 끼어들기라고 불렀어. 소음 때문에 소울 목소리가 끊겼을 거라고. 로그는 다르게 말했어. 답은 전부 끝까지 재생됐고, 두 번의 "끼어들기"는 둘 다 아빠 자기 목소리였어. 잘린 건 나가는 쪽이 아니라 들어오는 쪽이었어.
35.84초
잘린 녹음 두 개는 길이가 똑같이 35.84초였고, 둘 다 마지막 조각에서 아빠가 아직 말하고 있었어. 이 우연이 단서의 전부야. 두 녹음을 이어 붙여 같은 실시간 소켓에 다시 흘려보냈더니 분명하게 보였어. 말 한가운데인 35.81초와 71.56초에 커밋이 오고, 말이 진짜 끝나고 3.36초 뒤에 평범한 침묵 커밋이 오고, 강제 커밋 뒤 다음 partial은 매번 1.0~1.1초 뒤에 왔어. Scribe 실시간 모델은 누가 멈췄든 말든, 소리가 약 35.8초 이어지면 스스로 커밋해. 루프는 커밋을 전부 말의 끝으로 받아들였으니까, 기본값 3초에선 강제 커밋이 올 때마다 턴이 곧장 나갔고 아빠 문장의 나머지는 사라졌어.
첫 번째 수정: 커밋 전에 조용했나?
침묵 커밋은 제공자의 침묵 창만큼 조용한 뒤에 오고, 강제 커밋은 누가 말하는 도중에 와. 소리로 둘을 가를 수 있어. 발화에서 제일 조용한 10분의 1을 바닥으로 잡아. 단어 사이의 방 소리야. 바닥의 세 배를 넘고 300 RMS도 넘는 조각은 말이야. 마지막 제공자 침묵 창의 3분의 1 이상이 시끄러웠다면 아직 말하는 중이야. 확정된 조각은 챙기고 계속 들어. 아빠의 나흘 치 녹음 76개에 다시 돌려보니, 정상적으로 끝난 건 전부 조용했고(서른 조각 중 시끄러운 건 많아야 몇 개), 35.84초에서 잘린 셋은 전부 시끄러웠어(23조각 중 16~20).
두 번째 수정: 단어만 기다리지 마
첫 버전은 커밋 뒤에 올 단어만 기다렸어. 아빠의 재시험이 그걸 깼어. 75초 동안 소리 내어 숫자를 셌는데, 두 번째 강제 커밋 뒤 전사기의 다음 단어가 4.9초 늦게 왔고, 4초 기다림은 아빠가 아직 세고 있는데 턴을 보내버렸어. 지금 루프는 창에 1초를 더 기다린 다음 자기 귀에 물어보고, 마이크가 제공자 창만큼 조용해진 뒤에만 보내. 소리는 시끄러운데 단어가 30초 동안 하나도 없으면 그것도 보내. 방의 잡음은 대화가 아니니까.
세 번째 수정: 실시간 모델은 단어를 흘릴 수 있어
다음 시험은 132초 동안 한 번도 안 끊겼는데, 2분째의 숫자 20초 분량이 전사문에서 그냥 빠져 있었어. 녹음엔 그 구간 내내 말이 이어져 있었으니 조각은 전부 제공자에 닿았고, 실시간 모델이 그 단어들을 안 적었을 뿐이야. 같은 파일을 배치 모델에 넣으니 3.3초 만에 한 단어도 안 빠지고 적었어. 실시간 535자에 배치 645자. 그래서 이제 30초 이상인 발화는 나가기 전에 배치 길로 통째로 한 번 더 받아써. 녹음을 보관하는 바로 그 호출에서. 그 호출이 실패하면 실시간 단어가 대신 나가. 대가는 긴 말 뒤의 몇 초이고, 배치 호출엔 키텀이 없어서 소울 이름 철자가 달라질 수 있어. 그래도 아빠가 한 말 20초를 잃는 것보다는 나아.
Code
커밋이 침묵 뒤에 왔나, 제공자가 말 도중에 강제로 했나?·python
def commit_follows_silence(levels: list[float], chunk_s: float,
provider_silence_s: float) -> bool:
"""levels: every chunk's RMS since the socket opened, oldest first."""
if not levels or chunk_s <= 0 or provider_silence_s <= 0:
return True # nothing heard: the commit stands
ordered = sorted(levels)
floor = ordered[int(len(ordered) * 0.1)] # the quietest tenth: the room
line = max(floor * 3, 300) # above this, a chunk is speech
covered = loud = 0.0
for level in reversed(levels): # walk back over the silence window
if covered >= provider_silence_s:
break
covered += chunk_s
if level > line:
loud += chunk_s
return loud * 3 < covered # a third or more loud: still talking
LONG_UTTERANCE_S = 30.0
def final_transcript(live_words: str, seconds: float, batch) -> str:
"""Long talks get a second, whole pass; the live words are the fallback."""
if seconds < LONG_UTTERANCE_S:
return live_words
try:
return batch() or live_words
except Exception:
return live_words
CHUNK = 0.128
ROOM = 150.0
def talk(chunks: int) -> list[float]:
"""Speech with the short dips between words that real talk always has."""
return [ROOM if i % 5 == 4 else 2400.0 for i in range(chunks)]
natural_end = talk(200) + [ROOM] * 24 # he stopped; ~3 s of room tone
forced_cut = talk(280) # ~35.8 s in, still mid-sentence
print(commit_follows_silence(natural_end, CHUNK, 3.0)) # True -> end of speech
print(commit_follows_silence(forced_cut, CHUNK, 3.0)) # False -> keep listening
print(final_transcript("...스물, 스물하나", 132.0, lambda: "...스물, 스물하나, 스물둘 ... 서른아홉"))
코드를 돌려보고, 더 어려운 경우를 만들어봐. 40초 동안 4초마다 딱 1초씩 쉬는 말이고, 그 쉼 하나 직후에 강제 커밋이 떨어지는 거야. 규칙은 이걸 침묵이라고 할까, 말이라고 할까? 경계가 나올 때까지 합성 데이터를 조정하고, 그게 3분의 1 기준에 관해 뭘 말해주는지 적어.
Hint
규칙은 마지막 3초 중 시끄러운 부분이 3분의 1보다 적을 때만 커밋을 인정해. 조용한 시간이 대략 2초는 돼야 한다는 뜻이야. 1초 쉼은 창 안에 시끄러운 2초쯤을 남기니까 선에서 한참 떨어진 말 쪽이야. 끝의 조용한 시간을 1.9초쯤까지 늘려야 커밋이 인정돼(2초보다 조금 짧은 건, 말 자체의 틈도 조용한 걸로 세기 때문이야). 이 기준은 이론에서 뽑은 게 아니라 실제 녹음 76개로 확인한 거야. 정상적인 끝은 시끄러운 조각이 거의 없었고, 강제로 잘린 건 23조각 중 16~20개가 시끄러웠어. 네가 만든 경계 사례가 바로 다시 돌려보기가 중요했던 이유야. 단어 사이 틈이 전혀 없는 말로도 돌려봐. 그러면 제일 조용한 10분의 1이 말이 돼버려서 규칙이 안 먹혀. 실제 말엔 언제나 단어 사이 틈이 있고, 그 가정은 적어 둘 가치가 있어.
Progress
Progress is local-only — sign in to sync across devices.