MedExplain
환자가 진료실에서 의사의 설명을 녹음하면 AI가 핵심 내용을 요약하고, 어려운 의료 용어를 쉽게 풀어서 설명해주는 앱이다. Flutter로 만든 안드로이드 앱이 프론트엔드고, 이 글에서 다룰 FastAPI 서버가 백엔드다.
이 글에서는 오디오가 서버에 도착한 이후, 최종 요약이 앱에 돌아가기까지의 전체 백엔드 파이프라인을 단계별로 설명한다.
전체 흐름
Flutter 앱 (오디오 파일 전송)
↓ WebSocket
FastAPI 서버
↓
[1단계] Google STT → 텍스트 변환
↓
[2단계] 후처리 → 구어체/약어 정규화, 필러 단어 제거
↓
[3단계] STT 교정 → 규칙 기반 + Gemini 의료 문맥 교정
↓
[4단계] Gemini 요약 → 환자 친화적 요약 + 구조화 정보 추출
↓
Flutter 앱 (결과 표시)
1단계 — Google STT 의료 용어 인식 최적화
Google Cloud Speech-to-Text API를 기본 설정으로 쓰면 의료 용어 인식률이 낮다. SpeechContext로 의료 용어 힌트를 미리 넣어줘야 한다.
def _medical_phrase_hints() -> list[str]:
return [
"CRP", "CBC", "CT", "MRI", "HbA1c", "eGFR", "BMI",
"위선암", "위암", "내시경", "항암화학요법", "림프절",
"혈당", "공복 혈당", "당화혈색소", "혈압",
"고혈압", "당뇨", "고지혈증", "갑상선",
]
def _build_recognition_config(sample_rate: int, channels: int):
speech_context = speech.SpeechContext(
phrases=_medical_phrase_hints(),
boost=25.0, # 해당 단어 인식 시 가중치 높임
)
return speech.RecognitionConfig(
encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
sample_rate_hertz=sample_rate,
language_code="ko-KR",
alternative_language_codes=["en-US"], # 영어 의료 용어도 인식
enable_automatic_punctuation=True,
model="latest_long",
use_enhanced=True,
speech_contexts=[speech_context],
)
boost=25.0으로 설정하면 힌트 목록에 있는 단어를 우선 인식한다. alternative_language_codes에 en-US를 추가해서 CRP, MRI 같은 영어 약어도 자연스럽게 인식되도록 했다.
2단계 — STT 후처리
Google STT가 텍스트를 뱉어줘도 그대로 쓰기엔 문제가 있다. 구어체가 그대로 살아있고 필러 단어도 섞여있다.
예시) "음 그 HbA1c 수치가 어 육점팔 퍼센트 나왔고요" → "HbA1c 수치가 6.8% 나왔고요"
# 의료 약어 구어체 교정
_ABBR_CORRECTIONS = {
"씨알피": "CRP",
"엠알아이": "MRI",
"에이치비에이원씨": "HbA1c",
"당화혈색소": "HbA1c",
}
# 단위 구어체 표준화
_UNIT_CORRECTIONS = {
"밀리그램": "mg",
"퍼센트": "%",
"프로": "%",
"밀리리터": "mL",
}
# 소수점 구어체: "영점오" → "0.5"
_DECIMAL_PATTERN = re.compile(
r"([영일이삼사오육칠팔구십]+)점([영일이삼사오육칠팔구십]+)"
)
# 필러 단어 제거
_FILLER_PATTERN = re.compile(r"\b(음+|아+|어+|그+|저+|뭐+|에+)\b\.?\s*")
def postprocess_stt_text(text: str) -> str:
for ko, en in _ABBR_CORRECTIONS.items():
text = re.sub(ko, en, text, flags=re.IGNORECASE)
for ko, unit in _UNIT_CORRECTIONS.items():
text = re.sub(ko, unit, text)
text = _DECIMAL_PATTERN.sub(_fix_decimal, text)
text = _FILLER_PATTERN.sub("", text)
text = re.sub(r" {2,}", " ", text).strip()
return text
이 함수는 STT 스트리밍 중 최종 결과(is_final=True)에만 적용한다.
for result in resp.results:
text = result.alternatives[0].transcript
processed = postprocess_stt_text(text) if result.is_final else text
self._emit_result(processed, result.is_final)
3단계 — STT 교정 파이프라인
후처리로도 못 잡는 오류가 있다. 대표적인 게 척추 레벨 표기다. "L4 L5"를 STT가 "L4 LO"로 인식하는 경우가 실제로 발생하는데, O(알파벳)와 0(숫자)을 혼동하기 때문이다.
이를 위해 규칙 기반 교정과 LLM 기반 교정을 합친 stt_corrector.py를 별도로 만들었다.
규칙 기반 교정 (1차)
_SPINAL_RULES = [
# L4 LO → L4 L5
(re.compile(r'\bL([1-4])\s+L[Oo0]\b'), r'L\1 L5'),
(re.compile(r'\bL([1-4])-L[Oo0]\b'), r'L\1-L5'),
# 단독 LO → L5 (요추는 L1~L5, L0이 없음)
(re.compile(r'\bL[Oo0]\b'), 'L5'),
# 소문자 붙여쓰기 교정: l4l5 → L4-L5
(re.compile(r'\bl([1-5])l([1-5])\b', re.IGNORECASE), r'L\1-L\2'),
]
def rule_based_correct(text: str) -> str:
for pattern, repl in _SPINAL_RULES:
text = pattern.sub(repl, text)
return text
LLM 기반 교정 (2차)
규칙으로 잡히지 않는 문맥적 오류는 Gemini에게 넘긴다. 프롬프트에서 "의미가 통하지 않는 단어만 교정하고 내용은 절대 추가하지 말 것"을 명시하는 게 핵심이다. 넣지 않으면 LLM이 내용을 임의로 채워넣어서 의료 정보가 왜곡될 수 있다.
def llm_correct(text: str) -> str:
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
prompt = f"""
너는 의료 STT 교정 시스템이다.
STT 오류로 의료 용어가 잘못 인식된 부분만 교정하라.
교정 규칙:
1. 척추 레벨 표기 교정 (예: LO→L5, C0→C6)
2. 약어 오인식 교정 (예: 씨알피→CRP)
3. 의미가 통하지 않는 단어만 교정할 것
4. 원문 내용을 추가하거나 삭제하지 말 것
5. 출력은 교정된 텍스트만 반환
원문:
\"\"\"{text}\"\"\"
"""
response = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config=types.GenerateContentConfig(temperature=0.1),
)
corrected = (response.text or "").strip()
return corrected if corrected else text
def correct_stt_text(text: str, use_llm: bool = True) -> str:
if not text or not text.strip():
return text
text = rule_based_correct(text) # 1차: 빠르고 비용 없음
if use_llm:
text = llm_correct(text) # 2차: 문맥 이해
return text
4단계 — Gemini로 구조화된 요약 만들기
교정된 텍스트를 Gemini에 넘겨서 요약을 만든다. 처음에는 원문을 줄이는 방식으로 프롬프트를 짰는데 결과가 아래처럼 나왔다.
"공복 혈당이 128이 나왔고 HbA1c가 6.8%이며 당뇨 전 단계입니다."
환자 입장에서 128이 높은 건지 낮은 건지, 어떻게 해야 하는지 여전히 모른다. 원문 축약이 아니라 환자 언어로 재구성하되 수치는 반드시 그대로 보존하도록 프롬프트를 바꿨다.
def _build_prompt(text: str) -> str:
return f"""
너는 환자가 진료 내용을 이해할 수 있도록 돕는 요약 도우미다.
요약 방식:
- 원문을 그대로 줄이는 것이 아니라, 핵심 정보를 환자 언어로 재구성할 것
- 의학 용어는 쉬운 말로 풀되, 실제 수치는 반드시 괄호로 병기할 것
(예: "혈당이 당뇨 직전 수준이다 (공복혈당 128, HbA1c 6.8%)")
- 환자에게 중요한 순서로 작성 (진단/결과 → 치료 방향 → 주의사항)
- 원문에 없는 내용은 추가하지 말 것
- 최대 3개 bullet, 출력은 JSON만 할 것
출력 형식:
{{
"summary": ["문장1", "문장2", "문장3"],
"symptoms": ["증상1"],
"measurements": ["HbA1c 6.8%", "공복혈당 128"],
"medications": ["메트포르민 500mg"]
}}
입력 텍스트:
\"\"\"{text}\"\"\"
"""
5단계 — FastAPI 엔드포인트에서 연결
지금까지 만든 모듈들이 /summary 엔드포인트 하나에서 순서대로 호출된다.
# models/schemas.py
class StructuredInfo(BaseModel):
symptoms: List[str] = []
measurements: List[str] = []
medications: List[str] = []
class SummaryResponse(BaseModel):
summary: List[str]
structured: Optional[StructuredInfo] = None
# routes/summary.py
@router.post("/summary", response_model=SummaryResponse)
def summarize(request: TextRequest):
corrected_text = correct_stt_text(request.text, use_llm=True) # STT 교정
result = summarize_text(corrected_text) # 요약
structured = StructuredInfo(
symptoms=result.get("symptoms", []),
measurements=result.get("measurements", []),
medications=result.get("medications", []),
)
return SummaryResponse(
summary=result.get("summary", []),
structured=structured
)
용어 추출도 동일하게 교정 후 처리한다.
# routes/explain.py
@router.post("/explain", response_model=ExplainResponse)
def explain_terms(request: TextRequest):
corrected_text = correct_stt_text(request.text, use_llm=True)
terms_result = extract_terms(corrected_text)
return ExplainResponse(terms=[TermItem(**t) for t in terms_result])




실제 결과
STT 원문
"이번에 혈액 검사 결과 보면요 공복 혈당이 128이 나왔고 음 에이치비에이원씨가 육점팔 퍼센트예요."
후처리 + 교정 후
"이번에 혈액 검사 결과 보면요 공복 혈당이 128이 나왔고 HbA1c가 6.8%예요."
최종 요약
"혈당이 당뇨 직전 수준으로 나왔습니다 (공복혈당 128, HbA1c 6.8%). 아직 약 처방은 없지만 지금부터 관리하지 않으면 당뇨로 빠르게 진행될 수 있습니다."
마무리
이번 파이프라인에서 핵심은 두 가지였다.
첫째, STT 결과를 그냥 쓰지 않는 것이다. 구어체 정규화 → 규칙 교정 → LLM 교정을 단계별로 쌓아야 실제로 쓸 수 있는 텍스트가 나온다.
둘째, LLM 프롬프트에서 하지 말 것을 명확히 하는 것이다. 교정 단계와 요약 단계 모두 원문에 없는 내용 추가 금지를 명시하지 않으면 의료 정보가 왜곡될 수 있다.
전체 코드는 https://github.com/kelly96110jung/Growth_Backend 에서 확인할 수 있다.
'졸업프로젝트' 카테고리의 다른 글
| 5/5 STT 품질 개선 + 실험 (0) | 2026.05.06 |
|---|---|
| 4/3 데모 직전 수정하기 (0) | 2026.04.03 |
| 3/26 서버(STT/저장/기록 조회) 안정화 및 예외 처리 보완 작업 보고서 (0) | 2026.03.26 |
| 3/19 제미나이 연결해서 요약 하기 + 프엔 코드 살짝 수정 (0) | 2026.03.19 |
| 3/12 응 결국 수정 (0) | 2026.03.12 |