noon-desktop
워치 noon의 눈을 얹은 macOS 반려 AI.
noon-desktop은?
macOS 데스크톱에 상주하는 살아있는 눈 + AI 두뇌다. 워치 noon의 눈(외계 신호 글리치)을 데스크톱으로 가져오되, 그 위에 대화(Claude) · 듣기(로컬 STT) · 말하기(TTS) · 도구 그라운딩(전문가 모드)을 얹었다. AppKit + SpriteKit 네이티브.
작동하는 프로토타입(v0.1)이다. 출시 제품이 아니라 혼자 쓰려고 만든, 눈·대화·듣기·말하기·도구·홀로그램 카드가 실제로 도는 상태까지 온 실험이다. 워치 noon과는 눈/글리치 렌더 DNA만 공유하고 비전이 다른 별개 프로젝트다.

호출 → 듣기 → 대화 → 표정 → 홀로그램 카드
왜 만들었나
워치 noon이 "반응형 존재"라면, 데스크톱은 그 위에 자발성과 쓸모를 얹어 "먼저 반응하고 도와주는 동료"를 실험한 것이다. 1인 시대의 반려 AI를 만들되, 감성만이 아니라 실제 쓸모까지 얹고 싶었다.
쓸모의 방향은 범용 비서가 아니다. "전문가 모드 = 프롬프트 + 전용 도구" 라는 방향으로, 특정 영역에 깊게 들어가고 할루시네이션 대신 라이브 위키·로컬 공식 데이터에 근거하게 했다. 음성 인식은 로컬로 돌려 네트워크 없이 동작하게 했다. 프라이버시와 오프라인을 위해서다.
전체 구조
AI 두뇌와 도구
워치와 기술 축이 완전히 다르다. 여긴 대화·음성·도구 그라운딩이 핵심이다.
NoonBrain, 표정 있는 대화
Claude와 대화하되, 범용 SDK 없이 raw HTTPS로 /v1/messages를 직접 친다. 핵심은 structured output이다. 응답을 {expression, say} JSON 스키마로 강제해서, 모델이 고른 표정(idle·happy·surprised·curious·sleepy·thinking)이 곧바로 눈 표정과 1:1로 결착된다. 그래서 "표정 있는 대화"가 된다.
페르소나는 정체성 + 성격 + 전문가를 독립적으로 합성한다. 성격은 다정(warm)·츤데레(sassy) 2종, 전문가는 none·minecraft·coding 3종으로, 말투와 지식을 따로 조립한다. 화면을 곁눈질하는 비전 호출은 환각을 막으려 Sonnet으로 고정했고, 프롬프트가 "화면 설명·중계 금지(설명충 최악)"를 강조한다.
전문가 모드 = 프롬프트 + 전용 도구
이 프로젝트의 차별점. 전문가 모드는 단순 프롬프트가 아니라 그 모드에서만 노출되는 전용 도구를 얹는다. 마인크래프트 모드일 때만 search_minecraft_wiki와 show_crafting_recipe가 추가된다.
- Tier1은 라이브 위키 검색이다.
minecraft.wiki의 MediaWiki API를 단일 호출로 검색+본문 추출까지 한다(2콜→1콜로 ~800ms 단축). 지식을 실시간 그라운딩해 할루시네이션을 피한다. - Tier2는 로컬 공식 데이터다. 크래프팅 레시피 782개를 번들 JSON으로 넣었다(공식 minecraft-data + Mojang 한국어). 3×3 격자는 "모델이 추측하지 말고, 시스템이 공식 데이터로 채운다"는 원칙이다. 못 찾으면 추측 대신 되묻는다.
MCP를 쓰지 않고 자체 tool-use 스키마를 택했다(가용한 MCP는 대부분 봇 조종용이라 맞지 않았다).
전문가 모드 — 마인크래프트 레시피 홀로그램 카드
하이브리드 로컬 STT
말하는 동안엔 실시간 자막을, 문장이 끝나면 정확한 받아쓰기를 뽑는다. 둘 다 로컬로 돈다. 말하는 중엔 Apple SFSpeechRecognizer(on-device 강제)가 부분 자막을 뿌리고, 침묵이 감지되면(VAD) SwiftWhisper(로컬 ggml-medium)가 정확본을 뽑는다.
환각 필터가 중요했다. Whisper가 조용할 때 "시청해주셔서 감사합니다 / 구독 / 좋아요" 같은 유튜브 환각을 뱉는 걸 차단한다. 한계도 있다. STT 모델(약 1.5GB)은 번들에 못 넣어 수동 배치해야 하고, Core ML 인코더가 없어 CPU 추론이 느리다(설정으로 상쇄).
말하기 + barge-in
시스템 TTS 또는 Google Cloud TTS로 말한다. 말하는 동안엔 자기 목소리를 무시하고, "잠깐·그만·멈춰" 같은 stopWord가 들리면 즉시 끊는다(barge-in). 우상단엔 절차적으로 그린 "통화 중" 알약(CallIndicator)이 뜬다.
눈·글리치 데스크톱 재구성 + 커서 추적
워치의 눈을 그대로 이식하지 않고, 정체성 값과 형태만 빌려 데스크톱용으로 새로 구성했다. 글리치 셰이더는 워치와 같은 결정(un-premultiply로 검은 테두리 제거)을 공유하되 소스는 별개다. 데스크톱에만 있는 생명력도 넣었다. 마우스 커서를 따라가는 시선, 자율 사케이드, 오래 자리를 비웠다 오면 반기는 졸음/깨어남, 실제 날씨가 비면 눈 안에 절차적으로 내리는 비.
데스크톱 UX
- 전역 핫키:
CGEventTap으로[듣기 시작 /]종료. (NSEvent 전역 모니터가 키를 안 줘서 입력 모니터링 권한이 필요.) - 상주 창: borderless·투명·전 스페이스에 뜨는
FloatingEyeWindow. Dock 아이콘 없이 상태바 "◉" 메뉴로만. 안쪽 드래그로 이동, 바깥 링으로 리사이즈. - 4가지 대화 모드: off / active(적극 듣기+자막) / ambient(상시 잠복, 별칭 불러야 반응) / call(통화처럼 다 듣기).
- 홀로그램 빔 카드: 눈에서 볼류메트릭 광선을 쏴 날씨·레시피 카드가 홀로그램처럼 맺히는 연출. 카드는 SF Symbol·이모지 없이 매트릭스 그린으로 절차 드로잉한다. noon 리소스 규칙 그대로다.
기술 스택
- 플랫폼: Swift 5.9 · AppKit · macOS 13+ · 독립 SPM 패키지
- AI: Anthropic
/v1/messages(structured output) ·claude-sonnet-4-6 - 음성: SFSpeechRecognizer(on-device) · SwiftWhisper(로컬 whisper.cpp) · AVFoundation TTS
- 도구 그라운딩: minecraft.wiki MediaWiki API · 로컬 레시피 782 · 기상청 KMA 직접
- 연출: SpriteKit(눈·셰이더) · CoreGraphics/QuartzCore(빔·홀로그램)
왜 이 스택
- AppKit + SpriteKit: 데스크톱 컴패니언이라 macOS 네이티브로. 워치 noon의 눈 렌더(SpriteKit)를 그대로 가져와 표정을 재사용했다.
- Claude structured output: 응답을 자유 텍스트가 아니라 구조화 출력으로 강제해서, 답과 함께 눈 표정을 결착했다. 파싱이 흔들리지 않고 "표정 있는 대화"가 된다.
- 하이브리드 로컬 STT (SFSpeechRecognizer + SwiftWhisper): 음성을 클라우드로 보내지 않으려고 온디바이스로 갔다. 프라이버시·오프라인이 반려 AI의 전제라고 봤다.
- 자체 tool-use 스키마: 위키 라이브 검색 같은 전용 도구를 붙였는데, 공개 MCP 서버는 대부분 봇 조종용이라 안 맞았다. 그래서 도구 그라운딩을 자체 tool-use로 짰다.
Trouble Shooting
- CPU 추론이 느림. Whisper Core ML 인코더가 없어서 그렇다.
audio_ctx축소·single-segment로 상쇄했다. - STT 모델 미번들. 1.5GB라 번들에 못 넣는다. 수동 배치해야 하고, 없으면 음성 인식이 조용히 침묵한다.
- 머신 종속. 프로토타입이라 Info.plist 절대경로·환경 폴백 경로가 하드코딩돼 다른 머신엔 그대로 안 옮겨진다.
- 모델 주석 스테일. 코드 주석은 "Haiku"라 하지만 실제 기본은 Sonnet이다.
Decision Log
범용 비서 대신 특정 영역에 깊게. 프롬프트만 바꾸는 게 아니라, 그 모드에서만 노출되는 전용 도구(위키·레시피)를 함께 얹는다.
지식은 minecraft.wiki 실시간 검색, 레시피는 로컬 공식 데이터 782개. 격자는 모델이 추측하지 못하게 시스템이 공식 데이터로 채운다. 못 찾으면 지어내지 말고 되묻는다.
응답을 {expression, say} 스키마로 강제해, 모델이 고른 표정이 곧바로 눈 표정이 되게. "표정 있는 대화"의 핵심 장치.
SwiftWhisper 로컬 + Apple on-device 인식으로 네트워크 없이 동작한다. 프라이버시와 오프라인을 위해서다. 클라우드 STT 0.
가용한 MCP 서버가 대부분 봇 조종용이라 이 용도에 맞지 않았다. Anthropic tool-use 스키마로 직접 정의했다.
워치·폰 출시 코드와 완전히 분리한 프로토타입 패키지로 격리했다. 실험을 안전하게 돌리기 위해서다.
회고, 작동하는 프로토타입
혼자 쓰려고 만들었는데, 눈 + 대화 + 듣기 + 말하기 + 도구 그라운딩 + 홀로그램 카드가 실제로 도는 프로토타입까지 왔다. AppKit과 SpriteKit으로 네이티브하게, 로컬 STT와 structured output과 라이브 위키 그라운딩을 직접 엮었다. 이 기술 축은 워치 noon과 완전히 달라서 별개 이야기로 선다.
다음 우선순위는 존재감·자발성이다. 반응형을 넘어 화면과 상황에 먼저 곁눈질하는 "동료"로. 도구는 수치 데이터(Tier2)로 더 깊게. 출시 잡일은 "잘 풀리면" 후순위다. 지금까지 온 건 "1인 개발 프로토타입"이고, 축은 로컬-우선 AI 컴패니언이다.