본문 바로가기
C.W.K.
Stream
Lesson 02 of 04 · published

받아쓰기 한 번을 뜯어보면

~12 min · loop-anatomy, target-snapshot, pre-roll, capture

Level 0불씨 없음
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"제일 위험한 두 순간은 첫 밀리초와 마지막 밀리초야. 나머지는 다 기록 관리야."

누르고, 말하고, 놓고

받아쓰기 한 번은 단축키를 기준으로 깔끔하게 쪼개져. 키를 누르는 순간 두 가지를 동시에 잡아. 타깃 스냅샷(지금 누가 포커스를 쥐고 있나) 이랑 오디오 스트림(마이크는 이미 데워져 있고). 키를 놓으면 전사를 확정하고, 정리하고, 아까 잡아둔 타깃에 삽입해. 컨트롤러는 일부러 작게 뒀어.

DictationController.start(mode):
    snapshot = TargetCapture.snapshot()   # 최전면 앱, 포커스된 요소, 선택
    Audio.start()                          # 짧은 프리롤과 함께 캡처 시작

DictationController.stop():
    audio = Audio.finish()                 # 마지막 음절 살리게 짧은 꼬리 추가
    text  = STT.finalize(audio)
    clean = Cleanup.run(text, snapshot)
    Insertion.insert(clean, into: snapshot)

첫 밀리초, 프리롤

사람은 손가락이 다 내려가기도 전에 말을 시작해. 캡처가 키 누르는 그 순간에야 시작하면 첫 음절이 이미 날아가 있어. "보내" 가 "내" 가 되는 거야. 그래서 프리롤을 둬. 짧은 롤링 버퍼를 계속 돌려서, 확정하는 오디오에 키가 눌리기 반 초쯤이 같이 들어가게 하는 거지. 그런데 프리롤은 프라이버시랑 부딪혀. 진짜 롤링 버퍼를 돌린다는 건 받아쓰기 사이사이에도 마이크가 계속 살아 있다는 뜻이니까. Firekeeper 는 여기서 솔직하게 타협했어. 늘 켜 두는 버퍼 대신, 앱 실행 때 마이크 권한을 미리 받고 엔진을 빨리 띄우는 쪽으로. 몰래 계속 듣느니 녹음할 때만 눈에 보이게 녹음하는 게 나으니까.

마지막 밀리초, 꼬리

끝에도 똑같은 문제가 거울처럼 있어. 사람은 마지막 단어가 끝나는 순간 키를 놓거든. 가끔은 살짝 먼저 놓고. 오디오를 키 놓는 그 순간에 딱 자르면 마지막 음절이 잘려. 그래서 캡처는 키를 놓은 뒤에도 짧은 꼬리 동안 더 돌다가 확정해. Firekeeper 는 4분의 1초쯤 둬. 앞뒤로 살짝씩 시간을 밀어 준 이 둘 때문에 '전사가 고장 났어' 가 '전사가 완벽해' 로 바뀌는 거야.

가운데 말고 가장자리를 지켜. 말 가운데 부분의 STT 품질은 모델이 할 일이야. 첫 음절과 마지막 음절은 이 할 일이고. 프리롤이 시작을 지키고, 놓음 꼬리가 끝을 지켜. 가장자리를 틀리면 유저는 네가 만든 버그를 모델 탓으로 돌려.
침묵 감지기가 첫 단어를 먹게 절대 두지 마. 음성 활동 감지(VAD)는 빈 공간을 다듬고 끝점을 잡는 데 좋아. 그런데 프리롤보다 먼저 돌면 조용한 도입부를 '아무것도 없음' 으로 판단해서 네가 꼭 필요했던 그 음절을 버릴 수 있어. 순서가 중요해. 프리롤을 먼저 챙기고 그다음에 침묵을 다듬어. 절대 반대로 하지 마.

Code

코어 컨트롤러, 타깃을 먼저 잡고 그다음 오디오·swift
func start(mode: DictationMode) {
    // 오버레이가 포커스를 옮기기 전에 타깃을 스냅샷.
    self.snapshot = targetCapture.snapshot()   // 앱, 요소, 선택
    audio.start()                              // 실행 때부터 엔진 이미 데워짐
    phase = .recording
}

func stop() async {
    // 마지막 음절 안 잘리게 짧은 꼬리 유지.
    let clip = await audio.finish(tail: .milliseconds(280))
    guard clip.hasSpeech else { return }       // 침묵 게이트: 아무것도 없으면 STT 안 부름
    let raw = await stt.finalize(clip)
    let result = await cleanup.run(raw, context: snapshot)
    await insertion.insert(result, into: snapshot)
}

External links

Exercise

짧은 문구를 말하면서 마지막 단어가 끝나는 순간에 일부러 '정지' 를 눌러 녹음해봐. 다시 들어봐. 마지막 자음이 살아 있어? 이제 코드에 놓음 꼬리가 없다고 쳐봐. 유저가 어떻게 버그를 신고할지, 그리고 왜 엉뚱한 컴포넌트를 탓하게 될지 설명해봐.
Hint
꼬리가 없으면 유저는 '자꾸 마지막 단어를 잘라먹어' 라고 신고하면서 음성 모델을 탓해. 그런데 모델은 마지막 단어를 들은 적조차 없어. 앱이 마이크를 너무 일찍 끈 거지. 이렇게 원인이 엉뚱한 데로 돌아가니까, 가장자리를 지키는 건 모델 튜닝이 아니라 앱이 질 책임인 거야.

Progress

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

댓글 0

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

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