Superwhisper와 Macrowhisper로 시작한 음성 제어 구성을 무료·로컬 중심으로 바꾸기 위해 Whisper Voice로 전환했다. 운영체제 제어를 맡는 Hammerspoon은 유지하고, 음성 인식과 명령 분기 계층을 교체했다.
Superwhisper와 Macrowhisper의 실행 구성, 서비스와 사용자 데이터는 전환 검증 후 제거했다. 과거 구조는 별도 역사 문서에 남겼으며, 두 도구의 이름은 과거 구성에 관한 받아쓰기를 위해 사용자 정의 단어에만 유지한다.
전체 구조
현재 흐름은 Whisper Voice가 제공하는 확장 계약과 사용자가 만든 통합 계층으로 나뉜다.
[Whisper Voice가 제공하는 범위]
F1 녹음
→ 로컬 whisper-server와 Medium 모델로 음성 인식
→ WV_* 환경 변수 구성
→ /bin/zsh -c <설정된 Post Action 명령>
[사용자 정의 통합]
post-action.zsh
→ 사용자 전용 임시 요청 파일
→ hammerspoon://whisper-voice URL 이벤트
→ router.lua
→ 허용된 macOS 동작 또는 일반 붙여넣기
Whisper Voice는 Hammerspoon이나 Action 명령을 알지 못한다. Whisper Voice의 책임은 전사를 마치고 환경 변수를 넣은 자식 프로세스를 실행하는 지점까지다. 임시 파일 형식, URL 이벤트, Lua router와 명령 문법은 이 구성에서 별도로 정의했다.
구성 요소와 책임
| 구성 요소 | 책임 |
|---|---|
| Whisper Voice | F1 녹음, 로컬 전사, Post Action 실행 |
| whisper.cpp | whisper-server에서 Medium 모델을 상주시켜 전사 제공 |
post-action.zsh | Whisper Voice 자식 프로세스의 환경 변수를 안전한 임시 요청으로 전달 |
router.lua | 요청 검증, NFC 정규화, 명령 문법, 순차 분기 |
Hammerspoon init.lua | 앱·창·디스플레이 동작, clipboard, 되돌리기 상태 구현 |
Lua router는 실제 macOS 동작을 구현하지 않는다. Hammerspoon이 허용한 voiceActions, pasteText, alert 함수를 주입받아 호출한다. 이 경계 덕분에 음성 결과 자체가 임의의 셸 명령으로 실행되지 않는다.
현재 설치 상태
Whisper Voice: 3.8.0
음성 인식: Local whisper.cpp
모델: ggml-medium.bin
언어: Auto
모드: Brut
전환 녹음: Option+Space
Push-to-Talk: F1
후처리: Hammerspoon Router
로컬 전사 흐름은 다음과 같다.
Whisper Voice가 WAV 녹음
→ /opt/homebrew/bin/whisper-server 실행
→ ggml-medium.bin을 메모리에 적재
→ http://127.0.0.1:8178/inference 요청
→ 전사 결과 수신
모델 경로:
~/Library/Application Support/WhisperVoice/models/ggml-medium.bin
사용자 정의 단어:
Action
Hammerspoon
Macrowhisper
OpenCode
Superwhisper
WezTerm
Whisper Voice 자체 설정은 앱이 요구하는 기존 위치에 둔다.
~/.whisper-voice-config.json
이 파일은 업데이트 확인 시각처럼 자주 바뀌는 값과 향후 API 키를 포함할 수 있으므로 dotfiles에서 추적하지 않는다. 파일 권한은 소유자만 읽고 쓸 수 있는 0600이다.
사용자 정의 통합 파일은 별도 설정 디렉터리에서 관리한다.
~/.config/whisper-voice/
├── post-action.zsh
├── router.lua
└── router-spec.lua
dotfiles의 원본 디렉터리를 ~/.config/whisper-voice에 심볼릭 링크로 연결한다.
Whisper Voice의 후처리 확장 계약
Whisper Voice에서 type: command Post Action을 선택하면 앱은 대략 다음 형태로 실행한다.
/bin/zsh -c "/Users/demian/.config/whisper-voice/post-action.zsh"
이때 전사와 현재 앱의 맥락을 자식 프로세스의 환경 변수로 전달한다.
WV_TRANSCRIPTION
WV_RAW_TRANSCRIPTION
WV_APP_BUNDLE_ID
WV_APP_NAME
WV_MODE
WV_PROJECT
현재 PostActions.swift 구현이 command 후처리에 전달하는 변수는 위 여섯 개다. 설계 초안에 등장했던 창 제목, 선택 텍스트, clipboard 같은 추가 맥락은 현재 구현의 확장 계약에 포함되지 않는다.
현재 통합에서 사용하는 값은 두 개다.
WV_RAW_TRANSCRIPTION:Action명령 판별WV_TRANSCRIPTION: 일반 받아쓰기 붙여넣기
Brut 모드에서는 두 값이 거의 같다. 향후 AI 문장 다듬기를 사용하더라도 원문과 최종 문장을 구분하면 명령 판별이 후처리 결과에 영향을 받지 않는다.
Whisper Voice는 Post Action 명령이 끝날 때까지 기다리고 종료 상태를 로그에 기록한다. 명령의 출력을 다시 전사 결과로 받거나 Hammerspoon 동작의 성공 여부를 확인하지는 않는다. 따라서 이 계약은 단방향이다.
사용자 정의 통합
Zsh bridge
post-action.zsh는 환경 변수를 읽어 Hammerspoon으로 운반하는 29줄짜리 전달 계층이다. 명령 문법이나 macOS 동작 정책은 포함하지 않는다.
standalone Lua 5.4.6이 설치돼 있지만 Zsh를 유지한 이유는 다음과 같다.
- Hammerspoon Lua는 별도 Hammerspoon 프로세스 안에서 실행되므로 Whisper Voice 자식 프로세스의 환경을 상속받지 못한다.
- standalone Lua를 사용해도 Hammerspoon에 데이터를 넘길 IPC는 필요하다.
- 현재 standalone Lua에는 LuaPosix, LuaFileSystem, LuaSocket이 없다.
- 표준 Lua만 사용하면
mkdir,chmod, 안전한mkstemp, URL 실행을 위해 다시os.execute()에 의존한다. - Zsh는 macOS 기본 도구만으로 권한 제한, 임시 파일과 signal 정리를 간단히 처리한다.
즉 언어 통일보다는 다음 책임 분리를 택했다.
Zsh: 프로세스 간 안전한 전달
Lua: 정규화, 명령 분류와 자동화 정책
Zsh는 원문과 최종 문장을 NUL 문자로 구분해 기록한다.
raw transcription
NUL
processed transcription
요청 파일은 사용자 전용 임시 디렉터리에 생성한다.
$TMPDIR/whisper-voice-router/request.XXXXXXXX
보안 조건:
- 디렉터리 권한
0700 - 요청 파일 권한
0600 - URL에는 음성 내용이나 절대 경로가 아닌 임의 식별자만 전달
open실패 시EXITtrap으로 즉시 삭제open성공 후 Hammerspoon이 읽지 못한 파일은 약 10초 뒤 best-effort 삭제- Hammerspoon이 정상적으로 읽은 파일은 즉시 삭제
Zsh는 Hammerspoon의 처리 성공을 되돌려 받지 않는다. 호출 URL은 다음 형태다.
hammerspoon://whisper-voice?request=request.XXXXXXXX
Lua router
router.lua는 Hammerspoon 내장 Lua에서 dofile()로 읽는 모듈이다. standalone Lua 실행 파일이 아니며 hs.* API를 사용한다.
처리 순서:
- 요청 식별자와 파일 형식 검증
/usr/bin/iconv로 UTF-8-MAC을 UTF-8 NFC로 정규화- 원문에서
Action예약 접두사와 명령 분류 - 검색어, 창 방향, 디스플레이 번호와 별칭 판별
- 주입받은 허용 동작 또는 붙여넣기 함수 호출
한글 정규화는 Whisper Voice가 가끔 다음처럼 자모 분리형 문자를 반환하는 문제를 해결한다.
오 잘 되는데?
→ 오 잘 되는데?
요청은 FIFO 대기열에서 정규화부터 동작 실행 시작까지 하나씩 처리한다. 앱 전환 같은 동작 뒤에는 0.5초, 일반 붙여넣기 뒤에는 0.05초의 완료 지연을 적용한 뒤 다음 요청을 처리한다. 빠르게 끝난 두 받아쓰기의 실행 순서가 바뀌는 것을 막기 위한 장치다.
Hammerspoon host
실제 macOS 동작과 상태는 .hammerspoon/init.lua가 소유한다.
voiceActions: 앱, 검색, 창과 디스플레이 동작pasteText: clipboard 보존과 일반 받아쓰기 붙여넣기Action Back: 앱 또는 창 상태 한 단계 복구
Router에는 필요한 함수만 넘긴다.
router.start({
voiceActions = voiceActions,
pasteText = pasteText,
alert = hs.alert.show,
})
모듈 읽기와 start() 전체를 xpcall(..., debug.traceback)으로 감싼다. Router가 없거나 초기화에 실패해도 창 관리 같은 다른 Hammerspoon 기능은 계속 로드되며, 오류는 알림과 Hammerspoon console에 남는다.
명령 체계
명령은 Action 예약 접두사를 사용한다. 한글, 영어, 음차와 구두점 변형을 함께 지원한다. 알려진 별칭으로 시작하는 명령은 뒤에 붙은 자연어를 무시하고 해당 동작을 실행하며, 어떤 별칭에도 맞지 않는 문장은 셸 코드로 평가하지 않고 일반 받아쓰기 경로로 보낸다.
앱과 검색
Action Browser
액션 터미널
Action Notes
액션 유튜브
Action Netflix
액션 검색 Hammerspoon URL event
창
Action Window Left
액션 윈도우 라이트
Action Window Up
액션 윈도우 다운
Action Window Full
액션 윈도우 미니마이즈
디스플레이와 되돌리기
Action Display One
액션 디스플레이 투
Action Back
ActionBack
액션 고백
Display One과 Display Two는 이름이나 연결 순서가 아니라 .hammerspoon/init.lua에 저장한 UUID를 사용한다.
Display One → LG ULTRAFINE
Display Two → Studio Display
모니터를 교체하거나 UUID가 바뀌면 연결 정보를 갱신해야 한다.
상태 관리
Clipboard 보존
Hammerspoon은 붙여넣기 전에 clipboard의 첫 항목에 포함된 모든 UTI 표현을 저장한다. 일반 텍스트와 HTML 같은 여러 표현을 함께 복구하며, 사용자가 그 사이에 새로 복사한 내용이 있으면 오래된 clipboard를 덮어쓰지 않는다.
여러 파일처럼 clipboard 항목이 여러 개인 경우에는 clipboard를 교체하지 않고 직접 키 입력을 사용한다. Hammerspoon이 종료되거나 설정을 다시 불러오는 순간에도 안전한 조건이면 원래 clipboard를 복원한다.
Action Back
Action Back은 최근 동작 한 단계를 메모리에 저장한다.
- 앱 전환: 이전 앱으로 돌아가기
- 창 이동·크기 변경: 이전 위치와 크기 복원
- 최소화: 창 복원과 재최소화 왕복
- 디스플레이 이동: 이전 디스플레이와 상대 위치 복원
상태는 한 개만 유지하므로 메모리가 계속 늘어나지 않는다. Hammerspoon을 다시 불러오거나 종료하면 초기화된다.
문제 해결: Whisper Voice 3.8.0 로컬 서버
이 절은 2026-07-26에 확인한 Whisper Voice 3.8.0 Apple Silicon DMG에만 해당한다. 향후 배포판에서는 해결될 수 있다.
실패 원인
내장 whisper-server는 다음 라이브러리를 @rpath로 참조한다.
@rpath/libwhisper.1.dylib
@rpath/libggml.0.dylib
@rpath/libggml-cpu.0.dylib
@rpath/libggml-blas.0.dylib
@rpath/libggml-metal.0.dylib
@rpath/libggml-base.0.dylib
하지만 실행 파일의 LC_RPATH에는 개발자의 빌드 디렉터리만 들어 있었다.
/Users/hugoblanc/Documents/dev/whisper.cpp/build/src
/Users/hugoblanc/Documents/dev/whisper.cpp/build/ggml/src
/Users/hugoblanc/Documents/dev/whisper.cpp/build/ggml/src/ggml-blas
/Users/hugoblanc/Documents/dev/whisper.cpp/build/ggml/src/ggml-metal
앱 번들에는 대응하는 .dylib와 Contents/Frameworks 경로가 없다. 따라서 dyld가 @rpath를 확장해도 라이브러리를 찾지 못하고 서버가 main()을 실행하기 전에 종료된다.
README 절차만으로 해결되지 않는 이유
README는 Local 제공자를 선택하고 모델을 내려받은 뒤 Test Setup을 실행하라고 안내한다. 이 절차는 모델 파일을 준비하지만 누락된 서버 라이브러리는 해결하지 않는다.
소스의 validateSetup()도 서버 실행 파일과 모델 파일의 존재를 검사할 뿐, 서버를 실제로 실행해 동적 라이브러리를 확인하지 않는다. 따라서 파일이 존재하는 고장 난 서버도 설정 검사를 통과할 수 있다.
서버 선택 우선순위
Whisper Voice는 다음 순서로 서버를 찾는다.
1. Contents/Resources/whisper-server
2. Contents/MacOS/whisper-server
3. whisperCliPath의 whisper-cli를 whisper-server로 치환한 경로
4. whisperCliPath 자체
고장 난 내장 서버도 실행 권한이 있으면 2번에서 선택된다. 설정에 Homebrew 경로만 추가해서는 3번까지 도달하지 않는다.
현재 우회 설정
Homebrew의 whisper.cpp를 설치했다.
brew install whisper-cpp
Whisper Voice 설정에는 다음 경로를 저장했다.
whisperCliPath=/opt/homebrew/bin/whisper-cli
앱은 문자열의 whisper-cli를 whisper-server로 바꿔 같은 디렉터리의 서버를 찾는다. 내장 서버의 실행 권한은 제거해 후보에서 건너뛰게 했다.
내장 서버 → 실행 불가이므로 건너뜀
Homebrew 서버 → /opt/homebrew/bin/whisper-server 선택
내장 서버의 Mach-O 내용은 수정하지 않았고 앱을 다시 서명하거나 공증하지도 않았다. 실행 권한을 제거한 뒤에도 이 설치본은 codesign 검증과 Gatekeeper assessment를 통과했다. 이는 서명된 파일 내용의 무결성이 유지됐다는 뜻이지, 원래 내장 서버의 runtime dependency가 정상이었다는 뜻은 아니다. 원본 DMG도 정상 서명·공증 상태에서 서버 실행에 실패했다.
Whisper Voice를 업데이트하면 앱 번들이 교체되면서 내장 서버 실행 권한이 복원될 수 있다. 새 버전에서 Contents/Frameworks, bundle-relative RPATH 또는 정적 링크가 적용됐는지 확인한 뒤 우회 설정을 제거해야 한다.
확인 방법
Lua 명령 분류 회귀 테스트:
hs -c 'dofile(os.getenv("HOME") .. "/.config/whisper-voice/router-spec.lua")'
정상 출력:
Whisper Voice router tests passed: 18
Hammerspoon 설정 다시 불러오기:
hs -c 'hs.reload()'
다시 불러오는 순간 hs 명령줄 연결 오류가 출력되는 것은 기존 IPC 연결이 종료되면서 생기는 정상적인 현상이다.
관련 문서
- Superwhisper와 Macrowhisper를 사용한 이전 구성: Whisper Voice로 전환하기 전 자동화 구조와 설계 과정을 기록한다.
출처와 참고 자료
- Whisper Voice 공식 사이트
- Whisper Voice GitHub 저장소와 Local Mode 안내
- Whisper Voice 후처리 동작 설계 초안
- Whisper Voice 현재 후처리 구현
- Whisper Voice 로컬 서버 선택과 실행 구현
- whisper.cpp
- OpenAI Whisper 논문
- Hammerspoon 공식 문서
- Hammerspoon URL 이벤트
- Hammerspoon 외부 작업 실행
- Hammerspoon pasteboard API