저는 환자를 만나고 방문 기록을 구술하며, 여전히 구조화된 기록을 만드는 데 시간을 잃습니다. 필드, 치아 번호, 모든 과정이 그렇습니다. DentaNote는 제가 이 단계를 줄이기 위해 만든 도구입니다: 오디오 입력으로 구조화된 임상 기록 출력, 오직 로컬에서 100% 실행되며 제 기기를 벗어나는 데이터가 단 한 바이트도 없습니다.
무엇을 만들었고 얼마나 많은 노력을 절약하는지
Whisper를 사용한 전사(transcription), Gemma를 통한 Ollama 기반 추출, 스키마 검증, 그리고 렌더링까지의 전체 파이프라인은 30초 오디오 클립을 완전한 기록으로 변환하는 데 11.2초가 걸렸습니다 (전사: 7.7초; 추출: 2.1초; 렌더링은 결정론적 템플릿이라 시간 측정에 포함되지 않습니다).
CPU에서 작동하는 작은 Whisper 모델은 실시간보다 약 3.8배 빠르게 전사합니다. 따라서 3분 분량의 구술 기록은 약 45초 만에 텍스트가 되고, 여기에 추출에 약 2초가 더해져 — 제가 완전히 구술로 작성했을 기록을 한 분 이내에 처리할 수 있습니다.
저 자신의 기준으로는: 구조화된 방문 기록을 타이핑하는 데 약 10분이 걸립니다. 파이프라인의 1분 미만 소요 시간과 비교하면, 이는 진료당 대략 9분의 절약이며 — 하루 종일 여러 번의 진료를 거치면 이 시간이 한 건의 진료에 해당하는 작업량 이상으로 누적됩니다.
데모
▶ 2분짜리 데모 시청하기 — 터미널에서 dentanote run visit.wav를 실행하면 완성된 기록, 항상 보이는 초안 경고 및 검토가 필요한 uncertain_items이 표시되는 Streamlit 인터페이스가 열립니다.
제가 실제로 실행해 본 데모: 두 가지 합성 케이스, 오디오를 기록으로 변환했으며, 타이밍은 metrics.jsonl에 기록되어 있습니다 (지속 시간과 카운트만 — PHI는 없으며, 자동화된 테스트가 이를 보장합니다).
uv sync && dentanote doctor # Ollama, 모델 및 GPU 확인
dentanote run visit.wav # 오디오 -> 구조화된 임상 기록
uv run streamlit run src/dentanote/app.py # 업로드 인터페이스
왜 오픈 소스 및 로컬 방식이 여기서 폐쇄형 API보다 나은가
- 개인정보 보호 / LGPD (브라질의 데이터 보호법): 예약 오디오는 환자 데이터입니다. 로컬 파이프라인에서는 마이크에서 Whisper로 동일한 장치 내에서 처리됩니다. 유일한 네트워크 호출은
localhost:11434입니다. 이는 "믿어달라"는 것이 아니라 아키텍처 문제입니다.
- 오프라인: 인터넷 연결 없이 작동합니다 (테스트 스위트가 소켓을 차단하여 실행되는데, 이는 이를 증명하기 위함입니다).
- 비용: Ollama + 오픈 웨이트(open weights)를 사용하므로 토큰당 비용이 없습니다.
- 모델 교체 및 비교: 동일한 12개의 합성 사례 코퍼스에 대해 두 모델을 벤치마킹했습니다:
| Model | Precision (정밀도) | Recall (재현율) | F1 | Hallucination (환각) | uncertain_items | Avg. latency (평균 지연 시간) |
|---|
gemma4:e4b | 0.92 | 0.89 | 1.00 | 11.1% | 100% | 1.70 s |
gemma3:4b | 0.75 | 0.65 | 0.92 | 31.8% | 100% | 5.59 s |
두 모델 모두 첫 시도에서 100%의 유효한 JSON을 생성했습니다. 두 모델 모두 동일한 사례(urgencia_dor, 치아 47 대신 치아 37로 예측 — 모델이 아치에 있는 이웃을 "선택"함)에서 실패했습니다. 폐쇄형 API를 사용하면 저처럼 모델을 교체하고 비교할 수 없었습니다. (저의 코퍼스, 저의 하드웨어, 원하는 만큼 여러 번).
구축 방법
flowchart LR
A[Audio wav/mp3] --> B[Local Whisper]
B --> C[Ollama + Gemma]
...
고전적인 "LLM이 치과 사실을 지어내는" 실패를 방지하는 세 가지 결정:
- Ollama의
format을 통한 구조화된 출력 (JSON Schema) — 제약 디코딩(constrained decoding)은 스키마 외부 토큰에 0 확률을 부여합니다. 그럼에도 불구하고, 클라이언트는 Pydantic으로 검증하는데, 이는 '사고하는' 모델이 스키마를 건너뛸 수 있기 때문입니다 (네, 정말로 발생합니다 — 레포지토리의 ADR-001에서 문서화됨).
- 최종 단계에서는 LLM이 아닌 결정론적 템플릿 사용. 생성된 기록은 이미 검증된 필드만 전사(transcribe)합니다. 만약 어떤 필드가 오디오에서 나오지 않았다면, 그것은 나타나지 않습니다 — 침묵에도 불구하고 '환각' 현상이 발생하지 않습니다.
uncertain_items를 안전장치로 사용. 유효하지 않은 FDI 번호? 모호하게 언급된 치아? 이는 임상의가 확인하도록 목록화되며, 절대 조용히 해결되지 않습니다. 제가 겪은 사례 중 하나에서는 FDI 56 (잘못된 사분면의 어금니)이 폐기되어 정확히 그 형태로 노출되었습니다.
구조: 7단계(phases), 단계별 커밋(commit), 매 단계마다 린트 및 테스트 수행(오늘 153개 테스트), 레포지토리에 짧은 ADR 작성.
잘못된 점과 솔직한 한계점
- 평가는 제가 직접 작성한 전사본과 합성 오디오(piper-tts)를 사용한 12개의 합성 사례를 다룹니다. 이는 임상 연구가 아니며, 통계적 유의성이 없고, 실제 음성은 훨씬 더 복잡합니다.
- 환각은 0이 아닙니다:
gemma4:e4b는 11.1%의 사례에서 최소한 하나의 항목을 발명했으며 (gemma3:4b는 31.8%), 이것이 출력이 **항상 수정 가능한 초안(revisable draft)**인 이유입니다 — 이는 서명된 기록이 아니라 사전 채우기(pre-filling)입니다.
- 이웃 치아 교체 오류: 긴급성 사례에서 37 → 47. 프레임워크 자체로는 이를 포착하지 못하며, 오직 인간의 검토만이 가능합니다.
- Whisper의 CUDA 스택은 제 기기에서 고장났었습니다 (
libcublas.so.12 누락) — CPU 폴백이 실시간으로 작동했지만, '로컬'이라는 것이 곧 '당신이 기술 지원 담당자'라는 것을 상기시켜 줍니다.
- 실제 데이터는 사용되지 않았습니다, 이는 결정이자 규칙에 따른 것입니다. 실제 데이터를 테스트하려면 LGPD 및 전문 위원회 규정 준수가 필요하며 — 이 프로젝트의 범위를 벗어납니다.
다음 계획과 기여 요청
- UI에서 수동 필드 편집을 통해 최종 텍스트를 작성하기 전에 임상 의사가 Streamlit에서 항목별로 조정하고 그 후에 초안을 생성합니다.
scripts/make_synthetic_audio.py에 더 많은 TTS 음성 및 언어가 추가되었습니다 (ES/EN의 이중 언어 파이프라인 테스트를 위한 합성 오디오).
- 더 큰 코퍼스: 일종(implantology, periodontology, orthodontics)별로 더 많은 합성 시나리오와 이에 맞는
expected/ 폴더가 패키징되었으며, scripts/evaluate.py에서 각각의 영향을 측정합니다.
이 저장소는 MIT 라이선스 하에 오픈 소스로 공개되었습니다. 만약 임상 기록을 다루고 절차 어휘를 조정하거나 자신의 하루 케이스(필연적으로 합성)를 추가하고 싶다면, 이슈와 CONTRIBUTING.md 파일을 참고하세요.
GitHub에서 저장소 확인 · README의 법적 및 윤리적 고지
결정에 영향을 준 자료들
왜 중요한가: 환자 데이터의 경우, '오픈'은 미적인 선호도가 아니라 오프라인에서 감사할 수 있는 유일한 아키텍처입니다.
AI 에이전트의 도움을 받아 작성된 게시물입니다 ("some_ai" 공개 표시 on DEV).
#hf26challenge #devchallenge