음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

맥이 자꾸 잠들 때 — caffeinate로 안 되면 커서를 아주 조금 움직인다 (메뉴바 앱)

시리즈 · 직접 만든 macOS 도구 — 메뉴바에 사는 작은 앱들을 하나씩 만들어 온 기록입니다.

  1. Claude Code 사용량 메뉴바 모니터
  2. AI 에이전트 상태 모니터 (파이썬 rumps)
  3. 맥북 배터리 건강·메뉴바 유틸 3선
  4. 말하면 노션에 꽂히는 온디바이스 받아쓰기 앱
  5. 맥 절전·자리비움 방지 (마우스 지글러) — 지금 보는 글

1. caffeinate로 안 되는 순간

맥이 잠들지 않게 하는 방법은 이미 있습니다. 터미널에서 caffeinate를 실행하면 시스템은 화면도 안 끄고 잠도 안 잡니다. 오래 걸리는 빌드나 다운로드를 걸어 두고 자리를 뜰 때 이만한 게 없습니다.

그런데 여기에 안 걸리는 게 하나 있었습니다. 협업 툴에 뜨는 자리비움(Away) 표시입니다. caffeinate가 막는 건 시스템의 유휴 상태지, 사용자의 유휴 상태가 아닙니다. 상태 표시를 켜는 쪽 프로그램들은 시스템이 자느냐가 아니라 마지막 입력이 언제였는지를 봅니다. 그래서 화면은 멀쩡히 켜져 있어도, 몇 분간 키보드·마우스를 안 건드리면 아이콘이 노랗게 바뀝니다.

잠깐 화면을 지켜보기만 하는 시간에도 "자리 비움"으로 찍히는 게 거슬렸습니다. 시스템을 깨우는 걸로는 부족했고, 실제 입력이 있었던 것처럼 보여야 했습니다. 방법은 단순합니다. 아주 가끔, 커서를 몇 픽셀 움직이면 됩니다.

2. 딱 필요한 만큼만 움직이는 워커

핵심은 백그라운드에서 도는 작은 루프 하나입니다. 지금 커서 위치를 읽어, 몇 픽셀 옮겼다가 바로 제자리로 되돌립니다.

def _worker(self):
    while self.is_active:
        try:
            x, y = pyautogui.position()
            offset = random.choice([-3, -2, -1, 1, 2, 3])
            pyautogui.moveTo(x + offset, y + offset, duration=0.2)
            pyautogui.moveTo(x, y, duration=0.2)
        except Exception:
            pass
        time.sleep(self.interval)

설계에서 신경 쓴 건 "티가 안 나게"입니다. 이동 폭을 3픽셀 이내로 잡고 곧바로 원위치로 돌려, 마침 그 순간 뭔가를 하고 있어도 방해가 거의 없게 했습니다. 오프셋을 매번 무작위로 고르는 것도 같은 이유입니다. 늘 똑같이 오른쪽 아래로만 튀면 부자연스러우니까요.

간격은 메뉴에서 3초부터 10분까지 고를 수 있게 두고 기본값은 30초로 잡았습니다. 자리비움 타이머가 보통 몇 분 단위라, 그보다 짧게만 움직여 주면 충분합니다. 너무 자주 흔들 이유는 없습니다.

INTERVALS = [("3 sec", 3), ("5 sec", 5), ("10 sec", 10),
             ("30 sec", 30), ("1 min", 60), ("5 min", 300), ("10 min", 600)]

워커는 데몬 스레드로 띄웁니다. 앱이 꺼지면 스레드도 같이 사라지므로, 종료 후에 커서가 혼자 움직이는 사고는 없습니다.

3. 커서를 자동으로 움직이는 앱에 반드시 필요한 것

프로그램이 내 손을 대신해 커서를 움직인다는 건, 뒤집어 보면 통제를 잃기 쉽다는 뜻이기도 합니다. 그래서 pyautogui의 안전장치를 켜 두었습니다.

pyautogui.FAILSAFE = True

이걸 켜면 커서가 화면 맨 왼쪽 위 모서리에 닿는 순간 pyautogui가 예외를 던지며 동작을 멈춥니다. 뭔가 이상하게 돌아간다 싶으면 커서를 왼쪽 위로 확 던지면 되는, 물리적인 비상 정지 버튼인 셈입니다. 자동으로 입력을 흉내 내는 도구를 만들 때는 이런 탈출구를 먼저 마련해 두는 편이 마음이 편합니다.

4. 메뉴바에 올리고 .app으로 굽기

UI는 rumps로 만든 메뉴바 앱입니다. Dock에는 안 뜨고 상단 바에만 아이콘 하나로 상주합니다. 클릭하면 켜고 끄는 토글과 간격 선택 메뉴가 나옵니다. 상태가 바뀔 때는 알림으로 알려 줍니다 — 지금 켜져 있는지 꺼져 있는지를 아이콘만으로도 구분되게 두 가지로 나눴습니다.

self.menu = [
    rumps.MenuItem("Prevent Sleep", callback=self.toggle),
    None,
    self.interval_menu,
]

배포는 PyInstaller로 .app 번들을 만들었습니다. 파이썬이 안 깔린 맥에서도 더블클릭으로 실행되게 하려는 거였는데, 이 부분은 앞서 만든 다른 메뉴바 도구들과 같은 방식이라 새로 고생할 건 없었습니다.

pyinstaller --onedir --windowed --name "Keep Awake" --noconfirm keep_awake_app.py

마치며

이런 도구는 보통 "잔꾀" 취급을 받습니다. 실제로 하는 일이라곤 몇 분에 한 번 커서를 3픽셀 흔드는 게 전부니까요. 그런데 만들어 두고 보니, 자리비움 표시를 신경 쓰느라 괜히 마우스를 툭툭 건드리던 습관이 사라졌습니다. 필요할 때 켜고, 자리를 완전히 뜰 때 끄면 그만입니다.

기술적으로 대단한 건 없습니다. 다만 "시스템이 자는 것"과 "내가 자리에 없는 것"이 다른 신호라는 걸 구분하고 나니, 왜 caffeinate 하나로는 부족했는지가 분명해졌습니다. 필요한 건 시스템을 깨우는 게 아니라, 아주 약한 입력 하나를 대신 넣어 주는 일이었습니다.

자주 묻는 질문

Q. caffeinate 명령이 있는데 왜 앱까지 만드나요? caffeinate는 시스템 슬립·화면 꺼짐을 막습니다. 하지만 마지막 입력 시각을 기준으로 자리비움을 표시하는 프로그램에는 영향을 주지 못합니다. 그 표시까지 막으려면 실제 커서 입력이 필요해서, 마우스를 미세하게 움직이는 방식을 택했습니다.

Q. 커서가 움직이면 작업에 방해되지 않나요? 이동 폭을 3픽셀 이내로 두고 곧바로 원위치로 되돌리기 때문에 체감은 거의 없습니다. 그래도 신경 쓰이면 간격을 1분·5분처럼 길게 잡으면 됩니다. 자리비움 타이머보다만 짧으면 목적은 달성됩니다.

댓글

이 블로그의 인기 게시물

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기