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

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

I Got Tired of Alt-Tabbing to Notion Mid-Code. So I Built This.

There's a particular kind of friction that builds up when you're deep in a coding session and Notion is two clicks away. Not a big deal each time — maybe twenty seconds to switch windows, add the task, switch back. But those twenty seconds are enough to lose the thread you were holding. By the time you're back in the editor, something has already drifted.

I tried a few workarounds. A scratch notes file as a buffer. A second monitor with Notion always visible. Keyboard shortcuts to flip between apps faster. None of it fixed the core thing: I still had to leave what I was doing, even briefly, and that interruption added up over a day.

What I actually wanted was: press one key, say it, press the same key again, done. No new window. No typing. Eyes stay on the code.

So I built a small macOS menu bar app called MacDictationNotion. It uses the built-in on-device speech recognition — same engine as macOS Dictation in System Settings — and sends the result straight to a Notion database via the API. The whole interaction is about five seconds. I've been using it every day and it does exactly the one thing I needed.

What it does

The app lives in the menu bar and doesn't show up in the Dock. Press the hotkey from any app — code editor, browser, terminal, whatever is in front — and a small floating HUD appears at the bottom of the screen. You see the live transcript as you speak. Press the hotkey again and the text gets sent to Notion. Press ESC or tap the X button to cancel without sending anything.

A few things worth knowing about how it handles the awkward cases:

Offline: if Notion is unreachable, the note goes into a local queue file and sends automatically when the connection comes back.

LLM cleanup (optional): if you run Ollama locally, you can turn on a text refinement step that runs before the note is sent. It strips filler words and smooths out garbled phrases. If Ollama isn't running, the app skips the step and sends the original — it doesn't fail or block.

Hotkey: defaults to ⌘1. Change it to anything in Preferences.

Everything runs on-device. No audio leaves your machine, no external API is called for the speech recognition itself. I didn't want a paid service sitting in the middle of something this simple.

Setting up the Notion side first

Before installing the app, you need two things from Notion: an Integration token and your database ID.

Create the Integration:

  1. Go to notion.so/profile/integrations and create a new internal integration. Give it a name and copy the token — it starts with ntn_.

  2. Open the Notion database where you want the notes to land. Click the three-dot menu (top right corner of the page) and go to Connections. Connect the integration you just created.

This second step is easy to skip, and skipping it causes a confusing error — Notion returns a "not found" response even though the token is valid. The integration has no access to any specific page until you explicitly connect it. I spent a good chunk of time on this the first time.

Get the database ID:

Look at the URL when you have the database open: notion.so/.../{DATABASE-ID}?v=.... The database ID is the 32-character string before the ?. Hyphens optional — the API accepts both formats.

Installing the app

Copy MacDictationNotion.app to your Applications folder. On first launch, macOS will block it with a Gatekeeper warning because the app isn't distributed through the App Store. Clear it with one Terminal command:

xattr -cr /Applications/MacDictationNotion.app

Then open it. You'll get three permission prompts:

  • Microphone — allow it
  • Speech Recognition — allow it
  • Accessibility — this one needs to be enabled in System Settings > Privacy & Security > Accessibility. The global hotkey won't fire across apps without it.

Open Preferences from the menu bar icon, paste your Notion token and database ID, and hit Save. If your Notion database's title column has a custom name (anything other than "Name"), put that name in the Title property field.

One thing to check before you test it: System Settings > Keyboard > Dictation. Make sure your language is listed and the model has been downloaded. If it hasn't, on-device recognition will silently do nothing — the mic activates, the HUD appears, but no words come out. It looks like a bug in the app but it's just a missing language file.

How I use it day-to-day

Mostly while coding. Something comes up — a test case I almost missed, a function that needs another look, a message I need to send later — I catch it without stopping. The HUD shows the live transcript so I can confirm the recognition is working. If something sounds off, ESC and nothing is sent.

The notes land in Notion with a prefix ("Voice memo:" by default, configurable) so they're easy to filter or search later. The actual content of the note is in both the page title and the body paragraph, so it shows up in Notion search either way.

I don't use the Ollama refinement option every day. For short notes under a sentence or two, the on-device recognition is clean enough. I turn it on when I know I'm going to be talking through something more complex.

What it doesn't do

It's not a general dictation tool. If you want to type into a text editor with your voice, macOS's built-in Dictation (press Fn twice) already handles that.

It also only writes to one Notion database at a time. There's no routing by keyword or switching databases mid-session. One source, one destination — that's the design.

The app is unsigned, so it won't make it through the App Store. The xattr workaround is a one-time thing and doesn't affect anything else on your system.

Final note

The actual value of this isn't the feature set — it's that I stopped losing things. Before this, plenty of "should add that to Notion" moments just didn't happen because the friction was high enough to skip. Now the gap between thought and record is small enough that most of them do make it.

One app, one hotkey, one destination. That turned out to be exactly the right shape for this problem.


Related posts (Korean)

댓글

이 블로그의 인기 게시물

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

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

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