세 가지 모델, API 호출 0회: Apple Silicon 기반의 실시간 회의 인텔리전스
요약
Thunder Kitty 1.9.0은 Apple Silicon의 Neural Engine을 활용하여 실시간 회의 주제 분할 및 아젠다 트래킹을 온디바이스로 구현했습니다. API 호출 없이 Mac의 하드웨어 자원(Neural Engine, GPU)을 최적화하여 지연 시간을 최소화한 것이 핵심입니다.
핵심 포인트
- Apple Silicon의 Neural Engine을 활용한 온디바이스 실시간 처리
- CoreML을 통한 sentence-embedding 모델 최적화 및 구현 과정
- API 비용 및 네트워크 연결 없이 로컬 하드웨어 자원만 사용
- Neural Engine과 GPU의 자원 분리를 통한 효율적인 모델 실행
원래 thunderkitty.app/learn 에서 게시되었습니다
Thunder Kitty의 Labs 기능은 토픽 세그멘테이션 (Topic Segmentation) 및 아젠다 트래킹 (Agenda Tracking)을 완전히 온디바이스 (on-device)로 실시간 실행합니다. 문장 임베딩 (sentence-embedding) 모델을 Neural Engine에 올리기까지 일곱 번의 시도와 소리 없는 CoreML 버그와의 사투가 필요했습니다.
Thunder Kitty 1.9.0은 설정(Settings)에 두 가지 실험적 기능이 포함된 Labs 섹션을 추가합니다. 녹음하는 동안 회의를 주제별로 나누는 **라이브 토픽 타임라인 (Live Topic Timeline)**과 아젠다 항목이 다뤄질 때마다 표시해 주는 **라이브 아젠다 트래킹 (Live Agenda Tracking)**입니다. 두 기능 모두 실시간으로, 전적으로 사용자의 Mac에서 실행됩니다.
이 기능들을 실행한다는 것은 세 가지 모델을 동시에 실행한다는 것을 의미합니다. 흥미로운 점은 아이디어 자체가 아니라, 그 모델 중 하나인 문장 임베딩 (sentence-embedding) 모델을 Neural Engine에 올리는 과정이었습니다. 그 과정에서 일곱 번의 시도가 필요했으며, 오류는 발생하지 않으면서 그럴싸해 보이는 쓰레기 값만 생성하는 소리 없는 CoreML 버그와 싸워야 했습니다.
이 기능들이 어떻게 작동하는지, 그리고 그 과정에서 무엇이 문제였는지 설명하겠습니다.
이 기능의 기원
두 가지 아이디어가 하나로 모였습니다.
한 초기 사용자는 라이브 전문 용어 해설기 (jargon buster)를 원했습니다. 검색창(그는 이미 Google이나 Claude에 물어볼 수 있습니다)이 아니라, 특정 용어가 자신에게 생소할 가능성이 높을 때 이를 감지하여 실시간으로 정의를 스스로 보여주는 기능을 원했습니다. 이와 별개로, 저희는 오랫동안 라이브 회의 타임라인을 원해 왔습니다. 회의가 진행됨에 따라 수직 뷰가 확장되며 주제의 흐름과 반복되는 테마를 실시간으로 보여주는 기능입니다.
공통점은 타이밍입니다. 회의는 지금 일어나고 있으므로, 인텔리전스(intelligence) 또한 모두가 전화를 끊은 후의 배치 작업 (batch job)이 아니라 지금 즉시 이루어져야 합니다.
타임라인과 아젠다 트래킹은 1.9.0 버전에 출시되었으며, 전문 용어 해설기는 아직 개발 중입니다. 이 모든 것은 네트워크 연결이나 호출당 비용 없이 온디바이스 (on-device)로 실행됩니다 — 앱의 나머지 기능과 동일한 약속입니다. 비행기 모드를 켜도 여전히 작동합니다.
아키텍처: 세 가지 모델
서로 다른 작업에는 서로 다른 모델이 필요합니다. 회의 중 및 회의 후에 실행되는 내용은 다음과 같습니다:
| 모델 | 역할 | 지연 시간 (Latency) |
|---|---|---|
| CoreML을 통한 all-mpnet-base-v2 | 주제 분할 (Topic segmentation, 어떤 문장들이 서로 연결되는지 결정) | 5–20ms |
| ... |
모델 1~2는 회의 중에 실시간으로 실행되며, 모델 3은 회의 후에 실행됩니다. Neural Engine은 임베딩 (Embedding) 및 레이블링 (Labeling) 작업을 처리하고, GPU는 요약 (Summary) 모델을 처리하며, 이들은 서로 자원을 차지하기 위해 경쟁하지 않습니다.
가장 어려웠던 부분은 모델 1이었습니다. mpnet 임베딩 모델을 CoreML을 통해 Neural Engine에서 실행하는 것이었습니다. 일상적인 작업이어야 했던 것이 일곱 번의 시도로 이어졌습니다.
주제 분할 (Topic segmentation): 왜 DeepTiling인가
CoreML 이야기에 앞서, 임베딩 모델이 실제로 무엇을 하고 있는지 설명하겠습니다.
주제 분할 (Topic segmentation) — 하나의 주제가 어디서 끝나고 다음 주제가 어디서 시작되는지 결정하는 것 — 은 오래된 문제입니다. TextTiling은 1997년에 슬라이딩 윈도우 (Sliding windows) 사이의 단어 중첩 (Word overlap)을 계산하고 그 골짜기 (Valleys)를 경계로 표시함으로써 이 문제의 한 버전을 해결했습니다. DeepTiling은 단어 중첩 대신 신경망 임베딩 (Neural embeddings)을 사용하는 동일한 알고리즘입니다. 유사도 함수 (Similarity function)만 교체하고 나머지는 그대로 유지하는 방식입니다.
각 전사 (Transcript) 라인에 대해 우리는 768차원의 임베딩을 계산합니다. i번째 라인의 경우, 이전 8개 라인의 중심점 (Centroid)을 가져와 유사도를 비교합니다. 유사도가 높으면 여전히 동일한 주제에 있는 것이고, 골짜기 (0.12 임계값 미만의 지역 최솟값)가 나타나면 주제가 전환되었음을 의미합니다. 이는 단순하고 병렬화가 가능하며, 스트리밍 (Streaming) 버전으로 깔끔하게 변환될 수 있습니다. 이것이 실시간 타임라인을 가능하게 만드는 핵심입니다.
우리는 다섯 가지 임베딩 접근 방식을 테스트했습니다: all-mpnet-base-v2, all-MiniLM-L6-v2, nomic-embed-text-v1.5, Apple의 NLEmbedding, 그리고 Apple의 NLContextualEmbedding입니다. 알고리즘은 다섯 가지 모두 동일했으며, 임베딩만 변경되었습니다. mpnet이 명확하게 승리했습니다. 더 날카로운 골짜기, 주제 내 유사도와 주제 외 유사도 간의 더 나은 분리, 그리고 더 신뢰할 수 있는 경계선을 보여주었습니다.
그것이 바로 mpnet을 CoreML에 제대로 올리는 것이 타협할 수 없는 과제였던 이유입니다.
CoreML 변환: 일곱 번의 시도
만약 당신이 Transformer 모델을 CoreML로 변환한다면 이 부분은 주의 깊게 읽을 가치가 있습니다. 왜냐하면 실패는 소리 없이 일어나고, 경고는 오해를 불러일으키기 때문입니다.
목표
sentence-transformers/all-mpnet-base-v2를 CoreML .mlpackage로 변환합니다. input_ids와 attention_mask를 입력받아 token_embeddings를 출력한 뒤, Swift에서 평균 풀링 (mean-pool) 및 L2 정규화 (L2-normalize)를 수행합니다. 목표: Neural Engine을 타겟으로 하며, 문장당 20ms 미만의 속도를 달성합니다.
시도 1: 뻔한 접근 방식
traced = torch.jit.trace(wrapper, (input_ids, attention_mask))
mlmodel = ct.convert(traced, ...)
변환에 성공했습니다. CoreML 출력값과 sentence-transformers 사이의 코사인 유사도 (Cosine similarity)는 0.17이었습니다. 사실상 무작위 값입니다.
coremltools는 변환 과정에서 두 개의 경고를 발생시켰습니다:
Core ML embedding (gather) layer does not support any inputs besides
the weights and indices. Those given will be ignored.
해석하자면: coremltools가 MPNet 임베딩 레이어(embedding layer)에서 position_ids를 소리 없이 누락시킨 것입니다. 위치 정보가 없으면 트랜스포머 (transformer)는 의미 없는 출력을 생성합니다. 이는 coremltools 9.0 기준으로도 상위 버전의 수정 사항이 없는 알려진 버그이며, 이 경고는 실제로 모델에 영향을 미치는지 여부와 상관없이 발생하기 때문에 경고만으로는 알 수 없습니다. 이를 확인하는 유일한 방법은 참조 모델과 비교하는 것입니다.
시도 2~6: 실패의 기록
- 모델 내부에서 평균 풀링 (Mean pooling) 수행 — coremltools가 풀링 코드 내의 동적 정수 연산 (dynamic integer ops)에서 충돌(crash)을 일으킵니다.
- 중간 단계로 ONNX 사용 — coremltools 8 버전부터 ONNX 지원이 중단되었습니다.
onnx-coreml은 별개의, 이미 오래전에 지원이 중단된(deprecated) 패키지임이 밝혀졌습니다. - ONNX를 사용한 coremltools 7.x — 동일한 문제가 발생하며, Python 3.11 및 numpy <2.0 버전 고정 문제까지 겹칩니다.
- torch.export (ExportedProgram) — torch 2.7과 coremltools 8.3 사이의 버전-포맷 호환성 문제; 9.0은 이를 수용하지만 여전히 쓰레기 값(garbage)을 출력합니다.
- 위치 임베딩 (position embeddings)을 상수로 사전 계산 — 두 개의 gather 경고 중 하나는 해결되었으나, 코사인 유사도는 여전히 0.17입니다.
6번째 시도에 이르렀을 때, 명백한 원인들은 모두 제거되었음에도 출력값은 여전히 쓰레기 값이었습니다.
시도 7: 돌파구
깨달음: MPNet은 임베딩 레이어(embedding layer)에서만 위치 임베딩(position embeddings)을 사용하는 것이 아니었습니다. MPNet은 모든 어텐션 레이어(attention layer)에서 **상대적 위치 편향 (relative position bias)**을 사용하며, 이는 표준 BERT와는 다르게 계산되는 또 다른 임베딩 룩업(embedding lookup)입니다. 임베딩 레이어뿐만 아니라 위치를 처리하는 전체 체인이 망가져 있었던 것입니다.
해결책: 위치 정보와 관련된 모든 것을 미리 계산(pre-compute)하여 모델 자체의 배선(wiring)을 우회합니다.
class MPNetCoreMLWrapper(nn.Module):
def __init__(self, model, seq_length):
super().__init__()
...
결과:
CoreML vs sentence-transformers: avg=0.999985, min=0.999974
PASS — CoreML embeddings match sentence-transformers
이제 모든 세그멘테이션 경계(segmentation boundary)가 Python 베이스라인과 정확히 일치했습니다.
여기서 얻을 수 있는 교훈
트랜스포머(transformer)를 CoreML로 변환할 때 코사인 유사도(cosine similarity)가 낮게 나온다면, gather 레이어가 위치 정보를 누락시키고 있을 가능성이 높습니다. 해결 방법은 아키텍처마다 다릅니다. 위치 정보를 미리 계산하기 전에 모델이 위치를 어떻게 인코딩(encode)하는지 반드시 이해해야 합니다. MPNet의 경우 두 개의 gather 연산(위치 임베딩 및 상대적 어텐션 편향)을 처리해야 했습니다. BERT는 이와 다를 것입니다. DeBERTa(고유의 위치 인코딩 방식을 가진 또 다른 트랜스포머 변형 모델)는 그 자체로 매우 까다로운 문제입니다.
그리고 무엇인가를 신뢰하기 전에 반드시 검증된 레퍼런스(reference)를 통해 확인하십시오. 변환 과정에서 발생하는 경고(conversion warnings)는 신뢰할 수 있는 신호가 아닙니다.
실시간 의제 추적 (Real-time agenda tracking)
세그멘테이션(segmentation)이 작동함에 따라, 두 번째 기능은 대화가 진행됨에 따라 실시간 전사(transcript) 내용과 회의 전 의제를 매칭합니다. 이를 통해 항목들이 실시간으로 '대기 중(pending)'에서 '진행 중(in-progress)'을 거쳐 '논의됨(discussed)' 상태로 전환됩니다.
단순한(naive) 방식은 즉시 실패합니다. 회의 시작 시 누군가 의제를 소리 내어 읽으면, 모든 항목이
- 유사도 임계값 (Similarity threshold) — 해당 문장은 의제 항목의 임베딩 (embedding)과 비교했을 때 0.25 이상의 점수를 얻어야 합니다.
- 차별성 (Distinctiveness) — 가장 잘 일치하는 항목이 두 번째로 잘 일치하는 항목보다 0.05만큼 더 높아야 합니다. 모든 것에 일치하는 일반적인 문장은 아무것에도 일치하지 않는 것으로 간주됩니다.
- 최소 일치 횟수 (Minimum matches) — 항목이
inProgress상태로 전환되기 전, 두 번의 차별화된 일치가 필요합니다. - 시간적 분산 (Temporal spread) —
discussed상태로 전환되기 전, 처음과 마지막 일치 문장 사이의 간격이 60초 이상이어야 합니다. 의제를 읽는 데는 약 30초가 걸리지만, 실제 토론은 몇 분 동안 이어집니다. - 화자 다양성 (Speaker diversity) — 두 명의 서로 다른 화자가 필요합니다. 의제를 읽는 것은 한 명의 목소리이지만, 토론은 주고받는 대화입니다.
6개의 의제 항목이 포함된 51분 길이, 721개 문장의 테스트 전사 데이터(transcript)를 대상으로 테스트한 결과: 6개 항목 모두 discussed로 표시되었으며, 여러 항목이 동시에 트리거되는 현상은 없었고, 각 항목은 고유한 관련 근거와 함께 독립적으로 실행되었습니다.
라이브 트래커(live tracker)는 녹화 중 시각적 피드백을 제공하는 빠르고 근사적인 통과 단계입니다. 전체 문맥과 LLM 추론을 포함한 권위 있는 버전은 회의 후 단계(post-meeting pass)에서 생성됩니다. 라이브 단계를 가볍게 유지하는 것은 의도적인 설계입니다. MeetMap 연구 (ACM CSCW 2025)에 따르면, 실시간 회의 AI는 대화 도중에 주의력을 요구하기보다, 그 순간의 인지 부하 (cognitive load)를 낮추고 사용자가 제어권을 유지할 수 있게 할 때 가장 잘 작동하는 것으로 나타났습니다.
이 기능들이 Labs에 있는 이유
두 기능 모두 1.9.0 버전에 출시되었으며, 두 기능 모두 Labs에 있는 데에는 이유가 있습니다. 기능은 작동하지만, 아직 완성된 것은 아닙니다.
타임라인의 데이터 레이어 (data layer)는 견고하며 세분화 (segmentation)도 정확합니다. 하지만 UI는 여전히 거칠며, 주제 레이블 (topic labels)은 특정 날의 온디바이스 레이블링 모델 (on-device labeling model) 성능에 따라 달라질 수 있습니다. 의제 추적 기능은 깨끗한 전사 데이터에서는 다섯 가지 관문을 잘 통과하지만, 지저분한 오디오, 심한 대화 겹침 (cross-talk), 또는 거의 동일한 항목들로 가득 찬 의제는 여전히 오류를 일으킬 수 있습니다. 이 기능들을 선택적 참여 (opt-in) 방식으로 제공하는 이유는, 사용자가 이러한 점을 인지한 상태에서 기능을 켜기를 바라기 때문이며, 미흡한 경험으로 사용자를 놀라게 하고 싶지 않기 때문입니다.
요약 버전
Apple Silicon 기반의 세 가지 모델 — Neural Engine 상의 mpnet 임베더 (embedder), 실시간 레이블링 (labeling)을 위한 Apple Foundation Models, 그리고 회의 후 요약을 위한 GPU 상의 Qwen 모델 — 이 모든 과정은 Mac 외부로 아무것도 나가지 않으며 호출당 비용도 발생하지 않습니다. 임베더 (embedder)를 구현하는 데 일곱 번의 시도 끝에 겨우 성공했습니다. 나머지는 타이밍을 맞추는 작업이었습니다.
이 프로젝트는 초기 단계이기 때문에 Labs에 포함되어 있습니다. 하지만 실제로 작동하며, 로컬 (local)에서 실행되고, 다른 모든 기능과 마찬가지로 비행기 모드에서도 작동합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기