<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>ayoonistory 님의 블로그</title>
    <link>https://ayoonistory.tistory.com/</link>
    <description>ayoonistory 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 9 Aug 2026 17:37:37 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ayoonistory</managingEditor>
    <item>
      <title>FastAPI + Google STT + Gemini로 진료 음성 분석 백엔드 만들기</title>
      <link>https://ayoonistory.tistory.com/9</link>
      <description>&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MedExplain&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;환자가 진료실에서 의사의 설명을 녹음하면 AI가 핵심 내용을 요약하고, 어려운 의료 용어를 쉽게 풀어서 설명해주는 앱이다. Flutter로 만든 안드로이드 앱이 프론트엔드고, 이 글에서 다룰 FastAPI 서버가 백엔드다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 글에서는 오디오가 서버에 도착한 이후, 최종 요약이 앱에 돌아가기까지의 전체 백엔드 파이프라인을 단계별로 설명한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;전체 흐름&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div style=&quot;background-color: #ffffff;&quot; data-gutter=&quot;&quot;&gt;
&lt;pre id=&quot;code_1778339024802&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Flutter 앱 (오디오 파일 전송)
&amp;darr; WebSocket
FastAPI 서버
&amp;darr;
[1단계] Google STT &amp;rarr; 텍스트 변환
&amp;darr;
[2단계] 후처리 &amp;rarr; 구어체/약어 정규화, 필러 단어 제거
&amp;darr;
[3단계] STT 교정 &amp;rarr; 규칙 기반 + Gemini 의료 문맥 교정
&amp;darr;
[4단계] Gemini 요약 &amp;rarr; 환자 친화적 요약 + 구조화 정보 추출
&amp;darr;
Flutter 앱 (결과 표시)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div data-content=&quot;&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1단계 &amp;mdash; Google STT 의료 용어 인식 최적화&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Google Cloud Speech-to-Text API를 기본 설정으로 쓰면 의료 용어 인식률이 낮다. SpeechContext로 의료 용어 힌트를 미리 넣어줘야 한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;22&quot; data-line-type=&quot;context&quot; data-line=&quot;23&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339105571&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def _medical_phrase_hints() -&amp;gt; list[str]:
return [
&quot;CRP&quot;, &quot;CBC&quot;, &quot;CT&quot;, &quot;MRI&quot;, &quot;HbA1c&quot;, &quot;eGFR&quot;, &quot;BMI&quot;,
&quot;위선암&quot;, &quot;위암&quot;, &quot;내시경&quot;, &quot;항암화학요법&quot;, &quot;림프절&quot;,
&quot;혈당&quot;, &quot;공복 혈당&quot;, &quot;당화혈색소&quot;, &quot;혈압&quot;,
&quot;고혈압&quot;, &quot;당뇨&quot;, &quot;고지혈증&quot;, &quot;갑상선&quot;,
]

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=&quot;ko-KR&quot;,
alternative_language_codes=[&quot;en-US&quot;], # 영어 의료 용어도 인식
enable_automatic_punctuation=True,
model=&quot;latest_long&quot;,
use_enhanced=True,
speech_contexts=[speech_context],
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;boost=25.0으로 설정하면 힌트 목록에 있는 단어를 우선 인식한다. alternative_language_codes에 en-US를 추가해서 CRP, MRI 같은 영어 약어도 자연스럽게 인식되도록 했다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2단계 &amp;mdash; STT 후처리&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Google STT가 텍스트를 뱉어줘도 그대로 쓰기엔 문제가 있다. 구어체가 그대로 살아있고 필러 단어도 섞여있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;예시) &quot;음 그 HbA1c 수치가 어 육점팔 퍼센트 나왔고요&quot; &amp;rarr; &quot;HbA1c 수치가 6.8% 나왔고요&quot;&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;32&quot; data-line-type=&quot;context&quot; data-line=&quot;33&quot;&gt;
&lt;pre id=&quot;code_1778339131554&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 의료 약어 구어체 교정
_ABBR_CORRECTIONS = {
&quot;씨알피&quot;: &quot;CRP&quot;,
&quot;엠알아이&quot;: &quot;MRI&quot;,
&quot;에이치비에이원씨&quot;: &quot;HbA1c&quot;,
&quot;당화혈색소&quot;: &quot;HbA1c&quot;,
}

# 단위 구어체 표준화
_UNIT_CORRECTIONS = {
&quot;밀리그램&quot;: &quot;mg&quot;,
&quot;퍼센트&quot;: &quot;%&quot;,
&quot;프로&quot;: &quot;%&quot;,
&quot;밀리리터&quot;: &quot;mL&quot;,
}

# 소수점 구어체: &quot;영점오&quot; &amp;rarr; &quot;0.5&quot;
_DECIMAL_PATTERN = re.compile(
r&quot;([영일이삼사오육칠팔구십]+)점([영일이삼사오육칠팔구십]+)&quot;
)

# 필러 단어 제거
_FILLER_PATTERN = re.compile(r&quot;\b(음+|아+|어+|그+|저+|뭐+|에+)\b\.?\s*&quot;)

def postprocess_stt_text(text: str) -&amp;gt; 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(&quot;&quot;, text)
text = re.sub(r&quot; {2,}&quot;, &quot; &quot;, text).strip()
return text&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 함수는 STT 스트리밍 중 최종 결과(is_final=True)에만 적용한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;3&quot; data-line-type=&quot;context&quot; data-line=&quot;4&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339186431&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;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)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;3단계 &amp;mdash; STT 교정 파이프라인&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;후처리로도 못 잡는 오류가 있다. 대표적인 게 척추 레벨 표기다. &quot;L4 L5&quot;를 STT가 &quot;L4 LO&quot;로 인식하는 경우가 실제로 발생하는데, O(알파벳)와 0(숫자)을 혼동하기 때문이다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 규칙 기반 교정과 LLM 기반 교정을 합친 stt_corrector.py를 별도로 만들었다.&lt;/p&gt;
&lt;h3 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;규칙 기반 교정 (1차)&lt;/h3&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;13&quot; data-line-type=&quot;context&quot; data-line=&quot;14&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #1a1a1a;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #1a1a1a;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339212618&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;_SPINAL_RULES = [
# L4 LO &amp;rarr; 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 &amp;rarr; L5 (요추는 L1~L5, L0이 없음)
(re.compile(r'\bL[Oo0]\b'), 'L5'),
# 소문자 붙여쓰기 교정: l4l5 &amp;rarr; 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) -&amp;gt; str:
for pattern, repl in _SPINAL_RULES:
text = pattern.sub(repl, text)
return text&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;LLM 기반 교정 (2차)&lt;/h3&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;규칙으로 잡히지 않는 문맥적 오류는 Gemini에게 넘긴다. 프롬프트에서 &quot;의미가 통하지 않는 단어만 교정하고 내용은 절대 추가하지 말 것&quot;을 명시하는 게 핵심이다. 넣지 않으면 LLM이 내용을 임의로 채워넣어서 의료 정보가 왜곡될 수 있다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;30&quot; data-line-type=&quot;context&quot; data-line=&quot;31&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #1a1a1a;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #1a1a1a;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339230475&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def llm_correct(text: str) -&amp;gt; str:
client = genai.Client(api_key=os.getenv(&quot;GEMINI_API_KEY&quot;))
prompt = f&quot;&quot;&quot;
너는 의료 STT 교정 시스템이다.
STT 오류로 의료 용어가 잘못 인식된 부분만 교정하라.

교정 규칙:
1. 척추 레벨 표기 교정 (예: LO&amp;rarr;L5, C0&amp;rarr;C6)
2. 약어 오인식 교정 (예: 씨알피&amp;rarr;CRP)
3. 의미가 통하지 않는 단어만 교정할 것
4. 원문 내용을 추가하거나 삭제하지 말 것
5. 출력은 교정된 텍스트만 반환

원문:
\&quot;\&quot;\&quot;{text}\&quot;\&quot;\&quot;
&quot;&quot;&quot;
response = client.models.generate_content(
model=&quot;gemini-2.5-flash&quot;,
contents=prompt,
config=types.GenerateContentConfig(temperature=0.1),
)
corrected = (response.text or &quot;&quot;).strip()
return corrected if corrected else text

def correct_stt_text(text: str, use_llm: bool = True) -&amp;gt; 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&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;4단계 &amp;mdash; Gemini로 구조화된 요약 만들기&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;교정된 텍스트를 Gemini에 넘겨서 요약을 만든다. 처음에는 원문을 줄이는 방식으로 프롬프트를 짰는데 결과가 아래처럼 나왔다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&quot;공복 혈당이 128이 나왔고 HbA1c가 6.8%이며 당뇨 전 단계입니다.&quot;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;환자 입장에서 128이 높은 건지 낮은 건지, 어떻게 해야 하는지 여전히 모른다. 원문 축약이 아니라 환자 언어로 재구성하되 수치는 반드시 그대로 보존하도록 프롬프트를 바꿨다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;22&quot; data-line-type=&quot;context&quot; data-line=&quot;23&quot;&gt;
&lt;pre id=&quot;code_1778339254765&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;def _build_prompt(text: str) -&amp;gt; str:
return f&quot;&quot;&quot;
너는 환자가 진료 내용을 이해할 수 있도록 돕는 요약 도우미다.

요약 방식:
- 원문을 그대로 줄이는 것이 아니라, 핵심 정보를 환자 언어로 재구성할 것
- 의학 용어는 쉬운 말로 풀되, 실제 수치는 반드시 괄호로 병기할 것
(예: &quot;혈당이 당뇨 직전 수준이다 (공복혈당 128, HbA1c 6.8%)&quot;)
- 환자에게 중요한 순서로 작성 (진단/결과 &amp;rarr; 치료 방향 &amp;rarr; 주의사항)
- 원문에 없는 내용은 추가하지 말 것
- 최대 3개 bullet, 출력은 JSON만 할 것

출력 형식:
{{
&quot;summary&quot;: [&quot;문장1&quot;, &quot;문장2&quot;, &quot;문장3&quot;],
&quot;symptoms&quot;: [&quot;증상1&quot;],
&quot;measurements&quot;: [&quot;HbA1c 6.8%&quot;, &quot;공복혈당 128&quot;],
&quot;medications&quot;: [&quot;메트포르민 500mg&quot;]
}}

입력 텍스트:
\&quot;\&quot;\&quot;{text}\&quot;\&quot;\&quot;
&quot;&quot;&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;5단계 &amp;mdash; FastAPI 엔드포인트에서 연결&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 만든 모듈들이 /summary 엔드포인트 하나에서 순서대로 호출된다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;8&quot; data-line-type=&quot;context&quot; data-line=&quot;9&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #cd2054;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #cd2054;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339277131&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 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&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;13&quot; data-line-type=&quot;context&quot; data-line=&quot;14&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339299148&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# routes/summary.py
@router.post(&quot;/summary&quot;, 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(&quot;symptoms&quot;, []),
measurements=result.get(&quot;measurements&quot;, []),
medications=result.get(&quot;medications&quot;, []),
)
return SummaryResponse(
summary=result.get(&quot;summary&quot;, []),
structured=structured
)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;용어 추출도 동일하게 교정 후 처리한다.&lt;/p&gt;
&lt;div style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot;&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-content=&quot;&quot;&gt;
&lt;div style=&quot;background-color: #ffffff; color: #1a1a1a;&quot; data-line-index=&quot;5&quot; data-line-type=&quot;context&quot; data-line=&quot;6&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;span style=&quot;background-color: #000000; color: #737373;&quot;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;pre id=&quot;code_1778339317552&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# routes/explain.py
@router.post(&quot;/explain&quot;, 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])&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20260510_104641495_02.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boelzO/dJMcagey8FO/FsO1yHypbidWcbNVDmCDI1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boelzO/dJMcagey8FO/FsO1yHypbidWcbNVDmCDI1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boelzO/dJMcagey8FO/FsO1yHypbidWcbNVDmCDI1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboelzO%2FdJMcagey8FO%2FFsO1yHypbidWcbNVDmCDI1%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2220&quot; data-filename=&quot;KakaoTalk_20260510_104641495_02.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20260510_104641495_03.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b0VkDk/dJMcaaFsdwu/OMboydtSWnRmvGxAuuGUBK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b0VkDk/dJMcaaFsdwu/OMboydtSWnRmvGxAuuGUBK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b0VkDk/dJMcaaFsdwu/OMboydtSWnRmvGxAuuGUBK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb0VkDk%2FdJMcaaFsdwu%2FOMboydtSWnRmvGxAuuGUBK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2220&quot; data-filename=&quot;KakaoTalk_20260510_104641495_03.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20260510_104641495_04.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bENQEG/dJMcacJ2mtM/TEBTf2q4iExW4FrkCCI740/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bENQEG/dJMcacJ2mtM/TEBTf2q4iExW4FrkCCI740/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bENQEG/dJMcacJ2mtM/TEBTf2q4iExW4FrkCCI740/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbENQEG%2FdJMcacJ2mtM%2FTEBTf2q4iExW4FrkCCI740%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2220&quot; data-filename=&quot;KakaoTalk_20260510_104641495_04.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;KakaoTalk_20260510_104641495_05.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bp7G06/dJMcacJ2mtL/ESdk3am121MktQmGGbUiOK/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bp7G06/dJMcacJ2mtL/ESdk3am121MktQmGGbUiOK/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bp7G06/dJMcacJ2mtL/ESdk3am121MktQmGGbUiOK/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbp7G06%2FdJMcacJ2mtL%2FESdk3am121MktQmGGbUiOK%2Fimg.jpg&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1080&quot; height=&quot;2220&quot; data-filename=&quot;KakaoTalk_20260510_104641495_05.jpg&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;2220&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;실제 결과&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;STT 원문&lt;/b&gt;&lt;br /&gt;&quot;이번에 혈액 검사 결과 보면요 공복 혈당이 128이 나왔고 음 에이치비에이원씨가 육점팔 퍼센트예요.&quot;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;후처리 + 교정 후&lt;/b&gt;&lt;br /&gt;&quot;이번에 혈액 검사 결과 보면요 공복 혈당이 128이 나왔고 HbA1c가 6.8%예요.&quot;&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;최종 요약&lt;/b&gt;&lt;br /&gt;&quot;혈당이 당뇨 직전 수준으로 나왔습니다 (공복혈당 128, HbA1c 6.8%). 아직 약 처방은 없지만 지금부터 관리하지 않으면 당뇨로 빠르게 진행될 수 있습니다.&quot;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #fdfdfc; color: #6e6c68; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;마무리&lt;/b&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 파이프라인에서 핵심은 두 가지였다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫째,&lt;/b&gt; STT 결과를 그냥 쓰지 않는 것이다. 구어체 정규화 &amp;rarr; 규칙 교정 &amp;rarr; LLM 교정을 단계별로 쌓아야 실제로 쓸 수 있는 텍스트가 나온다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;둘째,&lt;/b&gt; LLM 프롬프트에서 하지 말 것을 명확히 하는 것이다. 교정 단계와 요약 단계 모두 원문에 없는 내용 추가 금지를 명시하지 않으면 의료 정보가 왜곡될 수 있다.&lt;/p&gt;
&lt;p style=&quot;background-color: #fdfdfc; color: #0a0a0a; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;전체 코드는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;a href=&quot;https://github.com/kelly96110jung/Growth_Backend&quot;&gt;https://github.com/kelly96110jung/Growth_Backend&lt;/a&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;에서 확인할 수 있다.&lt;/p&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/9</guid>
      <comments>https://ayoonistory.tistory.com/9#entry9comment</comments>
      <pubDate>Sun, 10 May 2026 00:11:15 +0900</pubDate>
    </item>
    <item>
      <title>5/5 STT 품질 개선 + 실험</title>
      <link>https://ayoonistory.tistory.com/8</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 이번 작업에서 구현한 기능&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. STT 후처리 파이프라인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;음성 인식(STT) 과정에서 발생하는 오류를 자동으로 교정하는 파이프라인을 신규 구현하였다 (stt_corrector.py).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;교정은 두 단계로 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계 &amp;mdash; 규칙 기반 교정 (정규식)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 발생하는 오탈자 패턴을 정규식으로 사전 정의하여 즉시 교정한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;척추 기호 오류: L4 LO, L4 L0 &amp;rarr; L4 L5 로 자동 변환&lt;/li&gt;
&lt;li&gt;한글 숫자/단위 표현 정규화: &quot;이 백&quot; &amp;rarr; &quot;200&quot;, &quot;삼십 밀리그램&quot; &amp;rarr; &quot;30mg&quot;&lt;/li&gt;
&lt;li&gt;필러 단어 제거: &quot;음&quot;, &quot;어&quot;, &quot;그&quot;, &quot;저&quot; 등 불필요한 간투사 삭제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계 &amp;mdash; LLM 기반 교정 (Gemini 2.5-flash, temperature=0.3)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;규칙으로 처리되지 않는 문맥적 오류를 LLM이 의료 문맥을 이해하고 교정한다. 원문의 구조와 내용은 유지하고 오인식된 부분만 수정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 교정 파이프라인은 요약(/summary), 용어 추출(/explain), 실시간 스트리밍 결과 모두에 적용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 요약 품질 개선&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 요약은 원문을 거의 그대로 줄이는 방식이어서 환자가 이해하기 어렵고, 소수점 숫자에서 문장이 잘리는 버그가 있었다. 이를 아래와 같이 개선하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요약 방식 변경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원문 축약 방식에서 환자 친화적 재구성 방식으로 프롬프트를 전면 수정하였다. 수치는 반드시 괄호로 함께 표기하도록 하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;변경 전: &quot;공복 혈당 128이 나왔고 HbA1c 6.8%이며 당뇨 전 단계입니다.&quot;&lt;/li&gt;
&lt;li&gt;변경 후: &quot;혈당이 당뇨 직전 수준으로 나왔습니다 (공복혈당 128, HbA1c 6.8%). 지금 관리하지 않으면 당뇨로 진행될 수 있습니다.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;소수점 버그 수정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0.5와 같은 소수점 숫자에서 문장이 끊기던 문제를 정규식 조건 분기로 수정하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;출력 구조 확장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 요약 문장 목록만 반환하던 것을 아래와 같이 구조화된 형태로 확장하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;summary: 요약 문장 (최대 3개)&lt;/li&gt;
&lt;li&gt;symptoms: 언급된 증상 목록&lt;/li&gt;
&lt;li&gt;measurements: 검사 수치 목록&lt;/li&gt;
&lt;li&gt;medications: 약물 및 용량 목록&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. 의료 용어 사전 구축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM을 통한 용어 추출이 실패할 경우를 대비한 fallback 용어 사전을 직접 구축하였다 (medical_terms.json). 내과, 정형외과, 심장내과, 신경과 등 주요 진료과 기준으로 60개 이상의 한국어 의료 용어와 쉬운 설명을 수록하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HbA1c: 지난 2~3개월간 평균 혈당 수준을 나타내는 지표&lt;/li&gt;
&lt;li&gt;공복혈당: 8시간 이상 금식 후 측정한 혈당 수치&lt;/li&gt;
&lt;li&gt;L4-L5: 허리뼈 4번과 5번 사이 디스크 위치&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-4. 진료 기록 API 확장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 기록 저장/조회 기능에 다음을 추가하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DELETE /records/{id} 신규 추가&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 진료 기록을 삭제하는 엔드포인트를 구현하였다. 존재하지 않는 id 요청 시 404를 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;저장 항목 확장&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기록 저장 및 조회 시 다음 두 필드를 추가로 처리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;memo: 환자가 직접 입력하는 자유 메모&lt;/li&gt;
&lt;li&gt;structured: 구조화된 증상/수치/약물 정보 (3-2에서 확장한 요약 결과와 연동)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-5. 질문 자동 분류 기능&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;환자가 질문을 입력하면 AI가 내용을 분석하여 아래 세 가지로 자동 분류하고, AI 답변 가능한 경우 즉시 답변까지 생성한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ai_answer: AI가 바로 답변 가능한 정보성 질문 (예: &quot;HbA1c가 뭔가요?&quot;)&lt;/li&gt;
&lt;li&gt;ask_doctor: 다음 진료 때 의사에게 직접 확인이 필요한 질문 (예: &quot;이 약 오래 먹어도 되나요?&quot;)&lt;/li&gt;
&lt;li&gt;urgent: 즉시 병원 또는 응급 확인이 필요한 상황 (예: &quot;가슴이 갑자기 너무 아파요&quot;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 별도로 카테고리를 선택하지 않아도 AI가 자동 판단한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-6. UI 목업 제작&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프론트엔드 개발 방향 결정을 위한 HTML 목업을 두 종류 제작하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;홈 화면 목업 (mockup.html)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 화면과 개선안을 나란히 배치하여 비교하였다. 개선안은 녹음 전 / 녹음 중 / 녹음 완료 세 가지 상태를 각각 별도 화면으로 구성하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;분석 결과 및 질문하기 탭 목업 (mockup_result.html)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 결과 탭에서 AI가 자동 추정한 진료과를 표시하는 방식으로 두 가지 안을 제시하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A안: 상단 배너 카드 방식 &amp;mdash; AI 추정 진료과를 카드 형태로 표시하고 변경 버튼 포함&lt;/li&gt;
&lt;li&gt;B안: 인라인 칩 방식 &amp;mdash; 날짜 옆에 작은 칩으로 진료과를 표시하고 탭하여 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문하기 탭은 단일 입력창 &amp;rarr; 자동 분류 &amp;rarr; AI 답변 카드 / 다음 진료 체크리스트 / 긴급 항목 섹션으로 구성하였다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 파일별 변경 내역&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;신규 생성&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;medexplain/server/app/services/stt_corrector.py&lt;/li&gt;
&lt;li&gt;medexplain/server/app/data/medical_terms.json&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;medexplain/server/app/services/summarizer.py &amp;mdash; 요약 프롬프트 개선, 소수점 버그 수정, temperature=0.3&lt;/li&gt;
&lt;li&gt;medexplain/server/app/services/term_extractor.py &amp;mdash; medical_terms.json fallback 추가, temperature=0.3&lt;/li&gt;
&lt;li&gt;medexplain/server/app/services/question_analyzer.py &amp;mdash; temperature=0.3&lt;/li&gt;
&lt;li&gt;medexplain/server/app/routes/summary.py &amp;mdash; STT 교정 연동, structured 반환&lt;/li&gt;
&lt;li&gt;medexplain/server/app/routes/explain.py &amp;mdash; STT 교정 연동&lt;/li&gt;
&lt;li&gt;medexplain/server/app/routes/records.py &amp;mdash; DELETE 엔드포인트 추가, memo/structured 저장&lt;/li&gt;
&lt;li&gt;medexplain/server/app/models/schemas.py &amp;mdash; StructuredInfo 모델, memo/structured 필드 추가&lt;/li&gt;
&lt;li&gt;medexplain/server/app/stt_google_streaming.py &amp;mdash; 숫자/단위 교정, 필러 단어 제거 후처리 추가&lt;/li&gt;
&lt;li&gt;lib/api/app_config.dart &amp;mdash; 서버 IP 업데이트&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 앞으로 해야 할 작업&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;백엔드&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;진료과 자동 추출 서비스 구현 &amp;mdash; STT 텍스트에서 Gemini로 내과/정형외과 등 자동 추론&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프론트엔드 (개발자 직접 구현)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;홈 화면 UI 개선 (mockup.html 참고)&lt;/li&gt;
&lt;li&gt;분석 결과 탭에 structured 정보 표시 (증상/수치/약물)&lt;/li&gt;
&lt;li&gt;진료과 표시 및 변경 기능 (mockup_result.html A안 또는 B안 선택 후 구현)&lt;/li&gt;
&lt;li&gt;질문하기 탭 전체 구현 (mockup_result.html 참고)&lt;/li&gt;
&lt;li&gt;진료 기록 삭제 버튼 연동 (DELETE /records/{id})&lt;/li&gt;
&lt;li&gt;memo 입력 및 수정 기능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/8</guid>
      <comments>https://ayoonistory.tistory.com/8#entry8comment</comments>
      <pubDate>Wed, 6 May 2026 10:40:59 +0900</pubDate>
    </item>
    <item>
      <title>4/3 데모 직전 수정하기</title>
      <link>https://ayoonistory.tistory.com/7</link>
      <description>&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;1. 버그 수정&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1-1. 진료 기록 저장 오류 (Critical)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;app/routes/records.py&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;용어 저장 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;description&lt;span&gt;&amp;nbsp;&lt;/span&gt;키 대신&lt;span&gt;&amp;nbsp;&lt;/span&gt;easy&lt;span&gt;&amp;nbsp;&lt;/span&gt;키를 참조해서 용어 설명이 항상 빈 문자열로 저장됨&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;item.get(&quot;easy&quot;, &quot;&quot;)&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;item.get(&quot;description&quot;, &quot;&quot;) or item.get(&quot;easy&quot;, &quot;&quot;)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;영향:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;저장 후 진료 기록 상세 조회 시 용어 설명이 정상적으로 표시됨&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;1-2. records.json 데이터 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;중복 레코드 5개 제거&lt;/li&gt;
&lt;li&gt;description&lt;span&gt;&amp;nbsp;&lt;/span&gt;없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;easy: &quot;&quot;만 있는 오염 레코드 정리&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. STT 인식 품질 개선&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;app/stt_google_streaming.py&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;language_code=&quot;ko-KR&quot;&lt;span&gt;&amp;nbsp;&lt;/span&gt;단일 언어 설정으로 인해 영어 의료 용어가 한국어 음성으로 인식됨 (예: CRP &amp;rarr; 씨알피)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정 1:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;alternative_language_codes=[&quot;en-US&quot;]&lt;span&gt;&amp;nbsp;&lt;/span&gt;추가 &amp;rarr; 영어 단어는 영어로 인식&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정 2:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;phrase hints에서 한국어 음성 표기 제거 (씨알피, 씨비씨 등) &amp;rarr; 해당 힌트가 오히려 한국어 음성 인식을 유도하고 있었음&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 의료 용어 추출 개선&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;app/services/term_extractor.py&lt;/li&gt;
&lt;li&gt;&lt;b&gt;문제:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;4개 단어(CRP, 내시경, 염증, 검사)만 하드코딩되어 있어 대부분의 진료 텍스트에서 용어 추출 불가&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;Gemini 기반 동적 추출로 전환
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스트에 등장한 의료 용어를 자동 감지 및 설명 생성 (최대 6개)&lt;/li&gt;
&lt;li&gt;Gemini 실패 시 18개 단어 fallback 사전으로 대체&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;4. 신규 기능: 사전 질문 정리&lt;/h2&gt;
&lt;p style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;스펙 기준 B안(MVP) 구현&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;추가 파일&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;파일내용
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;app/services/question_analyzer.py&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;Gemini 기반 질문 분류 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;app/routes/questions.py&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;질문 관련 API 라우터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;app/data/questions.json&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;질문 데이터 저장소&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;추가 API&lt;/h3&gt;
&lt;div style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;MethodEndpoint설명
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;/questions/analyze&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;질문 텍스트 분류 (저장 없음)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;POST&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;/questions&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;분류 결과 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;GET&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;/questions&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;목록 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;GET&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;/questions/{id}&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;상세 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;분류 기준&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;general_info:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;의학 기본 지식으로 설명 가능한 것 &amp;rarr; 한두 줄 설명 제공&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ask_doctor:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;담당 의사에게 직접 물어봐야 하는 것 &amp;rarr; 질문 리스트로 정리&lt;/li&gt;
&lt;li&gt;&lt;b&gt;caution:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;응급/즉각 주의가 필요한 것&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장 시&lt;span&gt;&amp;nbsp;&lt;/span&gt;record_id&lt;span&gt;&amp;nbsp;&lt;/span&gt;연결 가능 (진료 기록과 연결, B안)&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;5. 환경 설정 개선&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;app/main.py,&lt;span&gt;&amp;nbsp;&lt;/span&gt;server/.env&lt;/li&gt;
&lt;li&gt;Gemini API 키를&lt;span&gt;&amp;nbsp;&lt;/span&gt;.env&lt;span&gt;&amp;nbsp;&lt;/span&gt;파일에 저장하도록 변경&lt;/li&gt;
&lt;li&gt;서버 시작 시 자동 로드 &amp;rarr; 매번 환경변수 수동 설정 불필요&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;6. 주소 수정&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;파일:&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;lib/main.dart,&lt;span&gt;&amp;nbsp;&lt;/span&gt;lib/api/app_config.dart&lt;/li&gt;
&lt;li&gt;WebSocket URL:&lt;span&gt;&amp;nbsp;&lt;/span&gt;ws://10.240.66.72:8000&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;ws://127.0.0.1:8000&lt;/li&gt;
&lt;li&gt;API URL:&lt;span&gt;&amp;nbsp;&lt;/span&gt;http://10.240.66.72:8000&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;rarr;&lt;span&gt;&amp;nbsp;&lt;/span&gt;http://127.0.0.1:8000&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;현재 미완성 항목&lt;/h2&gt;
&lt;div style=&quot;background-color: #ffffff; color: #141413; text-align: start;&quot;&gt;항목상태
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;사전 질문 정리 &amp;mdash; Flutter UI&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;미구현 (백엔드만 완료)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;진료 기록 상세화면에 질문 연결 표시&lt;/td&gt;
&lt;td style=&quot;color: #3d3d3a;&quot;&gt;미구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/7</guid>
      <comments>https://ayoonistory.tistory.com/7#entry7comment</comments>
      <pubDate>Fri, 3 Apr 2026 11:13:09 +0900</pubDate>
    </item>
    <item>
      <title>3/26 서버(STT/저장/기록 조회) 안정화 및 예외 처리 보완 작업 보고서</title>
      <link>https://ayoonistory.tistory.com/6</link>
      <description>&lt;h2 data-end=&quot;136&quot; data-start=&quot;122&quot; data-ke-size=&quot;size26&quot;&gt;1. 오늘 작업 목표&lt;/h2&gt;
&lt;p data-end=&quot;264&quot; data-start=&quot;137&quot; data-ke-size=&quot;size16&quot;&gt;오늘 작업의 목표는 MedExplain의 서버 흐름 중에서&lt;br /&gt;&lt;b&gt;STT 결과 처리, 빈 결과 예외 처리, 기록 저장/조회 안정화&lt;/b&gt;를 점검하고,&lt;br /&gt;프론트와 연동되는 records 흐름이 실제로 동작하는지 확인하는 것이었다.&lt;/p&gt;
&lt;p data-end=&quot;403&quot; data-start=&quot;266&quot; data-ke-size=&quot;size16&quot;&gt;특히 MedExplain은 단순히 결과를 한 번 보여주는 데서 끝나는 것이 아니라,&lt;br /&gt;&lt;b&gt;진료 날짜 / 진료과 / 원문 텍스트 / 요약 / 의료 용어가 기록으로 저장되고 다시 조회되는 구조&lt;/b&gt;가 중요하기 때문에 이 흐름을 우선적으로 확인하였다.&lt;/p&gt;
&lt;hr data-end=&quot;408&quot; data-start=&quot;405&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;428&quot; data-start=&quot;410&quot; data-ke-size=&quot;size26&quot;&gt;2. 오늘 진행한 주요 작업&lt;/h2&gt;
&lt;h3 data-end=&quot;461&quot; data-start=&quot;430&quot; data-ke-size=&quot;size23&quot;&gt;(1) 프론트 최신 코드 반영 및 실행 환경 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;648&quot; data-start=&quot;462&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;501&quot; data-start=&quot;462&quot;&gt;GitHub에 올라온 프론트 최신 코드를 로컬에 pull하여 반영함&lt;/li&gt;
&lt;li data-end=&quot;575&quot; data-start=&quot;502&quot;&gt;실행 과정에서 누락된 파일(section_card.dart) 문제를 확인하였고, A가 push한 후 다시 pull하여 해결함&lt;/li&gt;
&lt;li data-end=&quot;648&quot; data-start=&quot;576&quot;&gt;웹소켓 주소가 팀원 로컬 IPv4로 설정되어 있어 연결 오류가 발생하였고, 이를 사용자 환경에 맞게 수정하여 실행되도록 조정함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;687&quot; data-start=&quot;650&quot; data-ke-size=&quot;size23&quot;&gt;(2) WebSocket STT 연결 및 빈 결과 처리 보완&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;925&quot; data-start=&quot;688&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;718&quot; data-start=&quot;688&quot;&gt;STT 웹소켓 연결이 정상적으로 이루어지는지 확인함&lt;/li&gt;
&lt;li data-end=&quot;782&quot; data-start=&quot;719&quot;&gt;무음 또는 실질적 인식 실패 시 text='' 형태의 final STT 결과가 반환되는 것을 로그로 확인함&lt;/li&gt;
&lt;li data-end=&quot;855&quot; data-start=&quot;783&quot;&gt;이 경우 그냥 정상 흐름처럼 넘어가지 않도록, 서버에서 &lt;b&gt;warning 이벤트&lt;/b&gt;를 전송하도록 ws_stt.py를 수정함&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;856&quot;&gt;warning 메시지는&lt;br /&gt;&lt;b&gt;&amp;ldquo;음성이 인식되지 않았습니다. 다시 녹음해주세요.&amp;rdquo;&lt;/b&gt;&lt;br /&gt;형태로 내려가도록 처리함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;950&quot; data-start=&quot;927&quot; data-ke-size=&quot;size23&quot;&gt;(3) 빈 STT 결과의 저장 차단&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1093&quot; data-start=&quot;951&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1037&quot; data-start=&quot;951&quot;&gt;기록 저장 API인 POST /records에서 clean_text가 비어 있거나 공백뿐인 경우 저장되지 않도록 records.py를 수정함&lt;/li&gt;
&lt;li data-end=&quot;1093&quot; data-start=&quot;1038&quot;&gt;이를 통해 무음/인식 실패 상태가 records 목록에 의미 없는 기록으로 쌓이는 문제를 방지함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1123&quot; data-start=&quot;1095&quot; data-ke-size=&quot;size23&quot;&gt;(4) records 저장/조회 구조 안정화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1359&quot; data-start=&quot;1124&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1154&quot; data-start=&quot;1124&quot;&gt;records.py에서 저장/조회 흐름을 점검함&lt;/li&gt;
&lt;li data-end=&quot;1203&quot; data-start=&quot;1155&quot;&gt;records 목록은 &lt;b&gt;날짜 / 진료과 / 요약 한 줄 카드&lt;/b&gt; 형태로 표시되고,&lt;/li&gt;
&lt;li data-end=&quot;1267&quot; data-start=&quot;1204&quot;&gt;상세 화면은 &lt;b&gt;날짜 / 진료과 / 원문 텍스트 / 요약 / 의료 용어&lt;/b&gt;를 보여주는 구조임을 기준으로 삼았음&lt;/li&gt;
&lt;li data-end=&quot;1359&quot; data-start=&quot;1268&quot;&gt;저장 데이터의 summary, terms 구조를 점검하고, 프론트가 쓰기 쉽게 summary와 terms가 항상 안정적인 형태로 내려가도록 정리함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1392&quot; data-start=&quot;1361&quot; data-ke-size=&quot;size23&quot;&gt;(5) records 상세 조회 500 에러 수정&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1757&quot; data-start=&quot;1393&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1454&quot; data-start=&quot;1393&quot;&gt;저장은 성공했지만 GET /records/{record_id}에서 500 에러가 발생하는 문제를 확인함&lt;/li&gt;
&lt;li data-end=&quot;1527&quot; data-start=&quot;1455&quot;&gt;원인은 저장된 terms 구조와 RecordDetailResponse 스키마가 기대하는 필드명이 서로 달랐기 때문이었음&lt;/li&gt;
&lt;li data-end=&quot;1601&quot; data-start=&quot;1528&quot;&gt;저장된 terms는 easy 기반이었고, 상세 조회 응답은 description을 요구하고 있어 mismatch가 발생함&lt;/li&gt;
&lt;li data-end=&quot;1670&quot; data-start=&quot;1602&quot;&gt;이를 수정하여, 상세 조회 시 기존 저장 데이터도 읽을 수 있도록 easy -&amp;gt; description 매핑을 보완함&lt;/li&gt;
&lt;li data-end=&quot;1757&quot; data-start=&quot;1671&quot;&gt;이후 POST /records, GET /records, GET /records/{record_id}가 모두 200으로 동작하는 것을 확인함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1783&quot; data-start=&quot;1759&quot; data-ke-size=&quot;size23&quot;&gt;(6) WebSocket 전송 안정화&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2080&quot; data-start=&quot;1784&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1925&quot; data-start=&quot;1784&quot;&gt;기존 ws_stt.py에서는 asyncio.run_coroutine_threadsafe(...).result(timeout=5) 구조를 사용하고 있었고,&lt;br /&gt;이 때문에 push_stt thread-safe failed와 같은 로그가 발생함&lt;/li&gt;
&lt;li data-end=&quot;1975&quot; data-start=&quot;1926&quot;&gt;실제 전송은 되었지만 timeout 대기 구조 때문에 실패 로그가 남는 것으로 판단됨&lt;/li&gt;
&lt;li data-end=&quot;2080&quot; data-start=&quot;1976&quot;&gt;이를 submit-only 방식으로 수정하여 unnecessary timeout을 제거하였고,&lt;br /&gt;이후 push_stt thread-safe failed 로그가 사라진 것을 확인함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2106&quot; data-start=&quot;2082&quot; data-ke-size=&quot;size23&quot;&gt;(7) 영어 의료용어 인식 개선 실험&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2464&quot; data-start=&quot;2107&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2180&quot; data-start=&quot;2107&quot;&gt;stt_google_streaming.py에서 한국어 문장 속 영어 의료용어 인식 개선을 위해 phrase hint를 추가함&lt;/li&gt;
&lt;li data-end=&quot;2288&quot; data-start=&quot;2181&quot;&gt;PRP, CRP, biopsy, gastrectomy, chemotherapy, adenocarcinoma 등 핵심 용어를 영어 표기와 한국식 발음 형태로 일부 보강함&lt;/li&gt;
&lt;li data-end=&quot;2414&quot; data-start=&quot;2289&quot;&gt;테스트 결과 일부 짧은 용어는 약간 개선되었으나,&lt;br /&gt;gastric adenocarcinoma, gastrectomy, chemotherapy 등 긴 영어 의료용어는 여전히 부정확하게 인식되는 한계가 확인됨&lt;/li&gt;
&lt;li data-end=&quot;2464&quot; data-start=&quot;2415&quot;&gt;이 부분은 추후 고도화 과제로 남기고, 오늘은 핵심 저장/조회 안정화에 우선순위를 둠&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2469&quot; data-start=&quot;2466&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2489&quot; data-start=&quot;2471&quot; data-ke-size=&quot;size26&quot;&gt;3. 오늘 확인된 동작 결과&lt;/h2&gt;
&lt;h3 data-end=&quot;2501&quot; data-start=&quot;2491&quot; data-ke-size=&quot;size23&quot;&gt;정상 케이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2634&quot; data-start=&quot;2502&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2521&quot; data-start=&quot;2502&quot;&gt;STT 결과가 정상적으로 반환됨&lt;/li&gt;
&lt;li data-end=&quot;2540&quot; data-start=&quot;2522&quot;&gt;/summary 호출 성공&lt;/li&gt;
&lt;li data-end=&quot;2559&quot; data-start=&quot;2541&quot;&gt;/explain 호출 성공&lt;/li&gt;
&lt;li data-end=&quot;2578&quot; data-start=&quot;2560&quot;&gt;/records 저장 성공&lt;/li&gt;
&lt;li data-end=&quot;2600&quot; data-start=&quot;2579&quot;&gt;/records 목록 조회 성공&lt;/li&gt;
&lt;li data-end=&quot;2634&quot; data-start=&quot;2601&quot;&gt;/records/{record_id} 상세 조회 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;2646&quot; data-start=&quot;2636&quot; data-ke-size=&quot;size23&quot;&gt;예외 케이스&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2705&quot; data-start=&quot;2647&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2680&quot; data-start=&quot;2647&quot;&gt;무음 또는 인식 실패 시 warning 이벤트 전송 성공&lt;/li&gt;
&lt;li data-end=&quot;2705&quot; data-start=&quot;2681&quot;&gt;빈 clean_text는 저장 차단됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2710&quot; data-start=&quot;2707&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2727&quot; data-start=&quot;2712&quot; data-ke-size=&quot;size26&quot;&gt;4. 오늘 작업의 의미&lt;/h2&gt;
&lt;p data-end=&quot;2835&quot; data-start=&quot;2728&quot; data-ke-size=&quot;size16&quot;&gt;오늘 작업을 통해 MedExplain은 단순히 STT 결과를 보여주는 수준에서 벗어나,&lt;br /&gt;&lt;b&gt;실제 진료 기록을 저장하고 다시 조회할 수 있는 구조&lt;/b&gt;가 서버 기준으로 상당 부분 안정화되었다.&lt;/p&gt;
&lt;p data-end=&quot;2932&quot; data-start=&quot;2837&quot; data-ke-size=&quot;size16&quot;&gt;또한 무음/인식 실패 상태를 별도로 처리하고,&lt;br /&gt;빈 결과가 records에 저장되지 않도록 방어 로직을 추가하여&lt;br /&gt;사용자 경험과 데이터 품질을 함께 보완할 수 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;2937&quot; data-start=&quot;2934&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;2953&quot; data-start=&quot;2939&quot; data-ke-size=&quot;size26&quot;&gt;5. 아직 남은 과제&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;3255&quot; data-start=&quot;2954&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;3016&quot; data-start=&quot;2954&quot;&gt;프론트에서 warning, translate, stt, error 이벤트를 정확히 분기 처리&lt;/li&gt;
&lt;li data-end=&quot;3057&quot; data-start=&quot;3017&quot;&gt;warning 상태에서는 자동 저장이 타지 않도록 프론트 로직 보완&lt;/li&gt;
&lt;li data-end=&quot;3116&quot; data-start=&quot;3058&quot;&gt;department를 자동 확정이 아니라 &lt;b&gt;추천 후보 + 사용자 수정 가능&lt;/b&gt;한 형태로 UI 개선&lt;/li&gt;
&lt;li data-end=&quot;3167&quot; data-start=&quot;3117&quot;&gt;의료 용어(terms)는 하단 목록 UI로 정리하고, 추후 의료용 사전/API와 연동&lt;/li&gt;
&lt;li data-end=&quot;3255&quot; data-start=&quot;3168&quot;&gt;영어 의료용어 STT 정확도 향상을 위한 후속 실험
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3255&quot; data-start=&quot;3203&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3222&quot; data-start=&quot;3203&quot;&gt;phrase hint 추가 보강&lt;/li&gt;
&lt;li data-end=&quot;3234&quot; data-start=&quot;3226&quot;&gt;후처리 치환&lt;/li&gt;
&lt;li data-end=&quot;3255&quot; data-start=&quot;3238&quot;&gt;bilingual 설정 검토&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/6</guid>
      <comments>https://ayoonistory.tistory.com/6#entry6comment</comments>
      <pubDate>Thu, 26 Mar 2026 17:11:49 +0900</pubDate>
    </item>
    <item>
      <title>3/19 제미나이 연결해서 요약 하기 + 프엔 코드 살짝 수정</title>
      <link>https://ayoonistory.tistory.com/5</link>
      <description>&lt;h3 data-end=&quot;1375&quot; data-start=&quot;1363&quot; data-section-id=&quot;4l9e6w&quot; data-ke-size=&quot;size23&quot;&gt;1. 작업 목표&lt;/h3&gt;
&lt;p data-end=&quot;1523&quot; data-start=&quot;1376&quot; data-ke-size=&quot;size16&quot;&gt;MedExplain의 녹음 &amp;rarr; STT &amp;rarr; 요약 &amp;rarr; 의료 용어 표시 흐름을 실제로 연결하고, 프론트와 백엔드 간 데이터 전달 문제를 해결하는 것을 목표로 작업하였다.&lt;br /&gt;특히 결과 화면에서 원문 텍스트, 요약, 의료 용어가 비어 보이던 문제를 중점적으로 수정하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;1543&quot; data-start=&quot;1525&quot; data-section-id=&quot;u72s93&quot; data-ke-size=&quot;size23&quot;&gt;2. 프론트엔드 작업 내용&lt;/h3&gt;
&lt;p data-end=&quot;1764&quot; data-start=&quot;1544&quot; data-ke-size=&quot;size16&quot;&gt;초기 확인 결과, 프론트는 Windows 데스크톱 환경에서 실행 중이었으나 websocket 및 summary API 일부가 Android emulator용 주소인 10.0.2.2를 사용하고 있었다.&lt;br /&gt;이를 Windows 로컬 서버 주소인 127.0.0.1로 수정하여 websocket /ws/stt 및 HTTP /summary 요청이 정상적으로 백엔드로 전달되도록 변경하였다.&lt;/p&gt;
&lt;p data-end=&quot;2049&quot; data-start=&quot;1766&quot; data-ke-size=&quot;size16&quot;&gt;또한 녹음 종료 직후 transcript를 바로 결과 화면에 넘기고 있었는데, 실제 final STT 결과는 websocket으로 조금 뒤에 도착하기 때문에 결과 화면으로 빈 문자열이 전달되는 문제가 있었다.&lt;br /&gt;이를 해결하기 위해 녹음 종료 시점에는 transcript를 확정하지 않고 &amp;ldquo;STT 결과를 기다리는 중...&amp;rdquo; 상태만 표시하도록 수정하였다. 이후 websocket 이벤트 수신 시 transcriptStore를 갱신하고, 최신 transcript를 기준으로 결과 화면으로 이동하도록 변경하였다.&lt;/p&gt;
&lt;p data-end=&quot;2125&quot; data-start=&quot;2051&quot; data-ke-size=&quot;size16&quot;&gt;추가로 결과 보기 버튼도 실제 transcript가 존재할 때만 보이도록 조건을 수정하여, 빈 결과 화면으로 진입하는 문제를 줄였다.&lt;/p&gt;
&lt;h3 data-end=&quot;2150&quot; data-start=&quot;2127&quot; data-section-id=&quot;zst7ug&quot; data-ke-size=&quot;size23&quot;&gt;3. STT 이벤트 파싱 관련 수정&lt;/h3&gt;
&lt;p data-end=&quot;2350&quot; data-start=&quot;2151&quot; data-ke-size=&quot;size16&quot;&gt;프론트 models.dart와 transcript_store.dart를 점검하여 websocket으로 들어오는 STT 이벤트를 파싱하는 로직을 수정하였다.&lt;br /&gt;초기에는 isFinal, text 필드가 기대한 형태로 들어오지 않아 transcriptStore에 값이 누적되지 않았고, 그 결과 combinedText가 빈 값으로 유지되었다.&lt;/p&gt;
&lt;p data-end=&quot;2466&quot; data-start=&quot;2352&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 STT 이벤트 파싱 시 isFinal / final 값을 모두 처리할 수 있도록 보완하고, text가 실제로 존재하는 경우만 transcriptStore에 반영되도록 수정하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;2494&quot; data-start=&quot;2468&quot; data-section-id=&quot;1nrfoh5&quot; data-ke-size=&quot;size23&quot;&gt;4. 백엔드 websocket 처리 수정&lt;/h3&gt;
&lt;p data-end=&quot;2706&quot; data-start=&quot;2495&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 ws_stt.py에서 websocket 이벤트 형식과 전송 타이밍 문제를 수정하였다.&lt;br /&gt;기존에는 STT 및 translation 이벤트를 payload 내부 중첩 구조로 전송하고 있었는데, 프론트는 top-level text, isFinal 등의 필드를 기대하고 있어 형식 불일치가 발생하였다.&lt;br /&gt;이를 프론트가 읽을 수 있는 flat 구조로 수정하였다.&lt;/p&gt;
&lt;p data-end=&quot;2915&quot; data-start=&quot;2708&quot; data-ke-size=&quot;size16&quot;&gt;또한 recognize_once() 이후 결과 전송을 asyncio.create_task()로 처리하면서 websocket 연결 종료 후 send를 시도하는 문제가 발생하였다.&lt;br /&gt;이를 asyncio.run_coroutine_threadsafe() 기반으로 바꿔 STT 및 translation 결과가 event loop에 즉시 안전하게 전달되도록 수정하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;2932&quot; data-start=&quot;2917&quot; data-section-id=&quot;12c7rx&quot; data-ke-size=&quot;size23&quot;&gt;5. 요약 기능 개선&lt;/h3&gt;
&lt;p data-end=&quot;3055&quot; data-start=&quot;2933&quot; data-ke-size=&quot;size16&quot;&gt;백엔드 /summary 라우터를 확인한 결과, 기존 요약은 문장 분리 후 앞 3문장을 반환하는 단순 규칙 기반 로직이었다.&lt;br /&gt;이를 유지하면서도 고도화를 위해 외부 LLM 요약을 도입하는 방향으로 구조를 정리하였다.&lt;/p&gt;
&lt;p data-end=&quot;3071&quot; data-start=&quot;3057&quot; data-ke-size=&quot;size16&quot;&gt;현재 방향은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3216&quot; data-start=&quot;3073&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3103&quot; data-start=&quot;3073&quot; data-section-id=&quot;lpbkw6&quot;&gt;/summary는 그대로 백엔드 엔드포인트 유지&lt;/li&gt;
&lt;li data-end=&quot;3144&quot; data-start=&quot;3104&quot; data-section-id=&quot;1vlgk2f&quot;&gt;내부 summarizer.py에서 Gemini API를 우선 호출&lt;/li&gt;
&lt;li data-end=&quot;3177&quot; data-start=&quot;3145&quot; data-section-id=&quot;mghtcg&quot;&gt;응답 실패 시 기존 규칙 기반 요약으로 fallback&lt;/li&gt;
&lt;li data-end=&quot;3216&quot; data-start=&quot;3178&quot; data-section-id=&quot;l1ekm8&quot;&gt;프론트는 별도 수정 없이 기존 /summary 호출 구조 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3328&quot; data-start=&quot;3218&quot; data-ke-size=&quot;size16&quot;&gt;이를 위해 Gemini API 키를 발급받고, 백엔드 실행 cmd 창에서 set GEMINI_API_KEY=... 방식으로 환경변수를 설정한 뒤 서버에서 외부 요약 호출이 가능하도록 준비하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;3349&quot; data-start=&quot;3330&quot; data-section-id=&quot;1c3m9z1&quot; data-ke-size=&quot;size23&quot;&gt;6. STT 언어 설정 확인&lt;/h3&gt;
&lt;p data-end=&quot;3545&quot; data-start=&quot;3350&quot; data-ke-size=&quot;size16&quot;&gt;stt_google_streaming.py를 확인한 결과 language_code가 ko-KR로 고정되어 있어 영어 발화 테스트 시 빈 결과가 나올 가능성이 있었다.&lt;br /&gt;테스트 과정에서 영어/한국어 발화에 따라 STT 결과 차이가 발생할 수 있음을 확인하였고, 필요 시 language_code를 테스트 언어에 맞게 조정해야 함을 점검하였다.&lt;/p&gt;
&lt;h3 data-end=&quot;3559&quot; data-start=&quot;3547&quot; data-section-id=&quot;1595i2y&quot; data-ke-size=&quot;size23&quot;&gt;7. 최종 결과&lt;/h3&gt;
&lt;p data-end=&quot;3592&quot; data-start=&quot;3560&quot; data-ke-size=&quot;size16&quot;&gt;최종적으로 다음 흐름이 정상 동작하는 상태까지 확인하였다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3779&quot; data-start=&quot;3594&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3612&quot; data-start=&quot;3594&quot; data-section-id=&quot;8nep93&quot;&gt;프론트 Windows 앱 실행&lt;/li&gt;
&lt;li data-end=&quot;3640&quot; data-start=&quot;3613&quot; data-section-id=&quot;1s4n71a&quot;&gt;websocket /ws/stt 정상 연결&lt;/li&gt;
&lt;li data-end=&quot;3652&quot; data-start=&quot;3641&quot; data-section-id=&quot;9sl824&quot;&gt;녹음 데이터 전송&lt;/li&gt;
&lt;li data-end=&quot;3674&quot; data-start=&quot;3653&quot; data-section-id=&quot;17hojla&quot;&gt;백엔드 STT final 결과 생성&lt;/li&gt;
&lt;li data-end=&quot;3697&quot; data-start=&quot;3675&quot; data-section-id=&quot;10hi8s1&quot;&gt;프론트 결과 화면에 원문 텍스트 표시&lt;/li&gt;
&lt;li data-end=&quot;3721&quot; data-start=&quot;3698&quot; data-section-id=&quot;1kvo4bm&quot;&gt;/summary 호출 후 요약 표시&lt;/li&gt;
&lt;li data-end=&quot;3757&quot; data-start=&quot;3722&quot; data-section-id=&quot;12iirdl&quot;&gt;/explain 호출 후 의료 용어 영역 표시 구조 유지&lt;/li&gt;
&lt;li data-end=&quot;3779&quot; data-start=&quot;3758&quot; data-section-id=&quot;2swr4c&quot;&gt;백엔드와 프론트 간 기본 연동 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;3843&quot; data-start=&quot;3781&quot; data-ke-size=&quot;size16&quot;&gt;즉, MedExplain의 핵심 1차 흐름인 녹음 &amp;rarr; STT &amp;rarr; 요약 출력까지는 동작 가능한 상태로 정리되었다.&lt;/p&gt;
&lt;h3 data-end=&quot;3857&quot; data-start=&quot;3845&quot; data-section-id=&quot;15v7tog&quot; data-ke-size=&quot;size23&quot;&gt;8. 이후 과제&lt;/h3&gt;
&lt;p data-end=&quot;3878&quot; data-start=&quot;3858&quot; data-ke-size=&quot;size16&quot;&gt;이후에는 다음 작업을 진행하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4030&quot; data-start=&quot;3880&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3921&quot; data-start=&quot;3880&quot; data-section-id=&quot;bcr9me&quot;&gt;explain 기능을 의료 사전 기반 또는 외부 API 기반으로 고도화&lt;/li&gt;
&lt;li data-end=&quot;3940&quot; data-start=&quot;3922&quot; data-section-id=&quot;11twjt5&quot;&gt;Gemini 요약 품질 안정화&lt;/li&gt;
&lt;li data-end=&quot;3973&quot; data-start=&quot;3941&quot; data-section-id=&quot;usbqk&quot;&gt;websocket/session 종료 처리 추가 안정화&lt;/li&gt;
&lt;li data-end=&quot;4003&quot; data-start=&quot;3974&quot; data-section-id=&quot;179rd0a&quot;&gt;저장된 records 화면과 상세 화면 연결 점검&lt;/li&gt;
&lt;li data-end=&quot;4030&quot; data-start=&quot;4004&quot; data-section-id=&quot;g2s3o4&quot;&gt;실제 의료 시나리오 기반 테스트 데이터 확장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;4035&quot; data-start=&quot;4032&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;4051&quot; data-start=&quot;4037&quot; data-section-id=&quot;1ufk40h&quot;&gt;6. 커밋 메시지 추천&lt;/h1&gt;
&lt;p data-end=&quot;4057&quot; data-start=&quot;4053&quot; data-ke-size=&quot;size16&quot;&gt;프론트:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;git commit -m &quot;STT 결과 반영 흐름 수정 및 결과 화면 연동 개선&quot;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4120&quot; data-start=&quot;4116&quot; data-ke-size=&quot;size16&quot;&gt;백엔드:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;div&gt;&lt;span&gt;git commit -m &quot;웹소켓 STT 이벤트 형식 수정 및 Gemini 요약 구조 반영&quot;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/5</guid>
      <comments>https://ayoonistory.tistory.com/5#entry5comment</comments>
      <pubDate>Thu, 19 Mar 2026 15:47:47 +0900</pubDate>
    </item>
    <item>
      <title>코테 공부 시작</title>
      <link>https://ayoonistory.tistory.com/4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;지피티가 시키는대로 가볼게요,,,하다 보면 되겄지 샤갈&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0주차 (3/12, 3/16)&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GOAT - Ch.05 읽기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;784&quot; data-start=&quot;766&quot; data-section-id=&quot;19rdrm5&quot;&gt;2557 Hello World&lt;br /&gt;이걸 왜 시키나 라고 한동안 고민했음 공대도서관에서 하기엔 민망했어&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;795&quot; data-start=&quot;785&quot; data-section-id=&quot;1qa9vmp&quot;&gt;1000 A+B&lt;br /&gt;map(int, input().split())&amp;nbsp; 알기&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;805&quot; data-start=&quot;796&quot; data-section-id=&quot;iafbu3&quot;&gt;2753 윤년&lt;br /&gt;python에서는 &amp;amp;&amp;amp;가 아니라 and, or&lt;br /&gt;조건문 끝날때 :&lt;br /&gt;문자열일때 '' 잊지 말기&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;821&quot; data-start=&quot;806&quot; data-section-id=&quot;6pcnz4&quot;&gt;2438 별 찍기 - 1&lt;br /&gt;1부터 해서 n까지 하기 range&amp;nbsp;&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li data-end=&quot;838&quot; data-start=&quot;822&quot; data-section-id=&quot;1f2p312&quot;&gt;10871 X보다 작은 수&lt;br /&gt;문제를 똑바로 읽자,,,,주어진다고 해도 random 생성인줄,,,,&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>코딩공부해보기</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/4</guid>
      <comments>https://ayoonistory.tistory.com/4#entry4comment</comments>
      <pubDate>Mon, 16 Mar 2026 13:15:10 +0900</pubDate>
    </item>
    <item>
      <title>3/12 응 결국 수정</title>
      <link>https://ayoonistory.tistory.com/3</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MedExplain&amp;nbsp;서버&amp;nbsp;개발&amp;nbsp;진행&amp;nbsp;보고서 &lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;작업&amp;nbsp;개요 &lt;br /&gt;이번&amp;nbsp;작업에서는&amp;nbsp;MedExplain&amp;nbsp;프로젝트의&amp;nbsp;서버(B)&amp;nbsp;파트에서&amp;nbsp;모바일(A)와&amp;nbsp;연결&amp;nbsp;가능한&amp;nbsp;1차&amp;nbsp;API&amp;nbsp;뼈대를&amp;nbsp;구축하였다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;기존에는&amp;nbsp;STT&amp;nbsp;연결&amp;nbsp;및&amp;nbsp;WebSocket&amp;nbsp;기반&amp;nbsp;텍스트&amp;nbsp;수신&amp;nbsp;구조가&amp;nbsp;중심이었다면,&amp;nbsp;이번&amp;nbsp;작업에서는&amp;nbsp;STT&amp;nbsp;이후&amp;nbsp;결과를&amp;nbsp;사용자가&amp;nbsp;실제로&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;형태로&amp;nbsp;가공하고&amp;nbsp;저장하는&amp;nbsp;기능을&amp;nbsp;우선&amp;nbsp;구현하는&amp;nbsp;방향으로&amp;nbsp;범위를&amp;nbsp;확장하였다. &lt;br /&gt;&lt;br /&gt;2.&amp;nbsp;변경&amp;nbsp;배경&amp;nbsp;및&amp;nbsp;이유 &lt;br /&gt;초기&amp;nbsp;구상에서는&amp;nbsp;음성&amp;nbsp;입력&amp;nbsp;이후&amp;nbsp;STT&amp;nbsp;결과를&amp;nbsp;얻는&amp;nbsp;것&amp;nbsp;자체에&amp;nbsp;초점이&amp;nbsp;맞춰져&amp;nbsp;있었다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;하지만&amp;nbsp;프로젝트&amp;nbsp;방향이&amp;nbsp;점차&amp;nbsp;&amp;ldquo;환자/보호자가&amp;nbsp;진료&amp;nbsp;내용을&amp;nbsp;이해하고&amp;nbsp;다시&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;돕는&amp;nbsp;앱&amp;rdquo;&amp;nbsp;쪽으로&amp;nbsp;구체화되면서,&amp;nbsp;단순&amp;nbsp;전사&amp;nbsp;결과만으로는&amp;nbsp;사용자&amp;nbsp;가치가&amp;nbsp;부족하다는&amp;nbsp;문제가&amp;nbsp;있었다. &lt;br /&gt;&lt;br /&gt;또한&amp;nbsp;교수님&amp;nbsp;피드백&amp;nbsp;및&amp;nbsp;팀&amp;nbsp;논의&amp;nbsp;과정에서&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;점이&amp;nbsp;중요해졌다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;단순&amp;nbsp;음성&amp;nbsp;인식&amp;nbsp;기능만으로는&amp;nbsp;프로젝트&amp;nbsp;차별성이&amp;nbsp;약함 &lt;br /&gt;-&amp;nbsp;사용자가&amp;nbsp;실제로&amp;nbsp;필요로&amp;nbsp;하는&amp;nbsp;것은&amp;nbsp;긴&amp;nbsp;진료&amp;nbsp;내용을&amp;nbsp;이해&amp;nbsp;가능한&amp;nbsp;형태로&amp;nbsp;정리해주는&amp;nbsp;기능임 &lt;br /&gt;-&amp;nbsp;모바일&amp;nbsp;화면&amp;nbsp;구현을&amp;nbsp;위해서는&amp;nbsp;서버가&amp;nbsp;정리된&amp;nbsp;데이터&amp;nbsp;구조를&amp;nbsp;먼저&amp;nbsp;제공해야&amp;nbsp;함 &lt;br /&gt;-&amp;nbsp;STT&amp;nbsp;고도화보다&amp;nbsp;요약,&amp;nbsp;용어&amp;nbsp;설명,&amp;nbsp;기록&amp;nbsp;저장&amp;nbsp;같은&amp;nbsp;후속&amp;nbsp;처리&amp;nbsp;구조를&amp;nbsp;먼저&amp;nbsp;만드는&amp;nbsp;것이&amp;nbsp;전체&amp;nbsp;구현&amp;nbsp;흐름상&amp;nbsp;더&amp;nbsp;효율적임 &lt;br /&gt;&lt;br /&gt;이러한&amp;nbsp;이유로&amp;nbsp;이번&amp;nbsp;단계에서는&amp;nbsp;STT&amp;nbsp;이후&amp;nbsp;파이프라인&amp;nbsp;중에서도&amp;nbsp;모바일과&amp;nbsp;직접&amp;nbsp;연결되는&amp;nbsp;기능을&amp;nbsp;우선&amp;nbsp;구현하였다. &lt;br /&gt;&lt;br /&gt;3.&amp;nbsp;이번&amp;nbsp;작업&amp;nbsp;목표 &lt;br /&gt;이번&amp;nbsp;작업의&amp;nbsp;목표는&amp;nbsp;다음과&amp;nbsp;같았다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;summary&amp;nbsp;API&amp;nbsp;구현 &lt;br /&gt;-&amp;nbsp;explain&amp;nbsp;API&amp;nbsp;구현 &lt;br /&gt;-&amp;nbsp;records&amp;nbsp;저장&amp;nbsp;및&amp;nbsp;조회&amp;nbsp;API&amp;nbsp;구현 &lt;br /&gt;-&amp;nbsp;A가&amp;nbsp;Flutter에서&amp;nbsp;바로&amp;nbsp;붙일&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;요청/응답&amp;nbsp;구조&amp;nbsp;확정 &lt;br /&gt;-&amp;nbsp;Swagger에서&amp;nbsp;각&amp;nbsp;기능&amp;nbsp;정상&amp;nbsp;동작&amp;nbsp;확인 &lt;br /&gt;&lt;br /&gt;4.&amp;nbsp;구현&amp;nbsp;내용 &lt;br /&gt;&lt;br /&gt;4-1.&amp;nbsp;데이터&amp;nbsp;구조&amp;nbsp;통일 &lt;br /&gt;A와&amp;nbsp;B가&amp;nbsp;동일한&amp;nbsp;형식으로&amp;nbsp;데이터를&amp;nbsp;주고받을&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;API&amp;nbsp;응답&amp;nbsp;구조를&amp;nbsp;먼저&amp;nbsp;확정하였다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;summary&amp;nbsp;응답:&amp;nbsp;배열(list) &lt;br /&gt;-&amp;nbsp;terms&amp;nbsp;응답:&amp;nbsp;{term,&amp;nbsp;description}&amp;nbsp;객체&amp;nbsp;배열 &lt;br /&gt;-&amp;nbsp;records&amp;nbsp;저장&amp;nbsp;필드:&amp;nbsp;record_id,&amp;nbsp;date,&amp;nbsp;department,&amp;nbsp;clean_text,&amp;nbsp;summary,&amp;nbsp;terms &lt;br /&gt;-&amp;nbsp;records&amp;nbsp;목록&amp;nbsp;조회:&amp;nbsp;summary_preview&amp;nbsp;포함 &lt;br /&gt;-&amp;nbsp;records&amp;nbsp;상세&amp;nbsp;조회:&amp;nbsp;clean_text,&amp;nbsp;summary,&amp;nbsp;terms&amp;nbsp;전체&amp;nbsp;포함 &lt;br /&gt;&lt;br /&gt;이&amp;nbsp;과정을&amp;nbsp;통해&amp;nbsp;모바일과&amp;nbsp;서버가&amp;nbsp;같은&amp;nbsp;기준으로&amp;nbsp;개발될&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;하였다. &lt;br /&gt;&lt;br /&gt;4-2.&amp;nbsp;스키마&amp;nbsp;파일&amp;nbsp;작성 &lt;br /&gt;app/models/schemas.py를&amp;nbsp;생성하여&amp;nbsp;요청&amp;nbsp;및&amp;nbsp;응답&amp;nbsp;형식을&amp;nbsp;Pydantic&amp;nbsp;모델로&amp;nbsp;정의하였다. &lt;br /&gt;&lt;br /&gt;구현한&amp;nbsp;모델 &lt;br /&gt;-&amp;nbsp;TextRequest &lt;br /&gt;-&amp;nbsp;TermItem &lt;br /&gt;-&amp;nbsp;SummaryResponse &lt;br /&gt;-&amp;nbsp;ExplainResponse &lt;br /&gt;-&amp;nbsp;RecordCreateRequest &lt;br /&gt;-&amp;nbsp;RecordListItem &lt;br /&gt;-&amp;nbsp;RecordListResponse &lt;br /&gt;-&amp;nbsp;RecordDetailResponse &lt;br /&gt;&lt;br /&gt;이를&amp;nbsp;통해&amp;nbsp;서버&amp;nbsp;응답&amp;nbsp;형식을&amp;nbsp;일관되게&amp;nbsp;유지할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;하였다. &lt;br /&gt;&lt;br /&gt;4-3.&amp;nbsp;summary&amp;nbsp;기능&amp;nbsp;구현 &lt;br /&gt;app/services/summarizer.py를&amp;nbsp;생성하여&amp;nbsp;입력&amp;nbsp;텍스트를&amp;nbsp;문장&amp;nbsp;단위로&amp;nbsp;분리하고&amp;nbsp;최대&amp;nbsp;3문장까지&amp;nbsp;배열로&amp;nbsp;반환하는&amp;nbsp;1차&amp;nbsp;더미&amp;nbsp;요약&amp;nbsp;기능을&amp;nbsp;구현하였다. &lt;br /&gt;&lt;br /&gt;app/routes/summary.py를&amp;nbsp;생성하여&amp;nbsp;POST&amp;nbsp;/summary&amp;nbsp;엔드포인트를&amp;nbsp;연결하였다. &lt;br /&gt;&lt;br /&gt;Swagger&amp;nbsp;테스트&amp;nbsp;결과 &lt;br /&gt;-&amp;nbsp;요청&amp;nbsp;성공 &lt;br /&gt;-&amp;nbsp;summary&amp;nbsp;배열&amp;nbsp;형식으로&amp;nbsp;응답&amp;nbsp;확인&amp;nbsp;완료 &lt;br /&gt;&lt;br /&gt;4-4.&amp;nbsp;explain&amp;nbsp;기능&amp;nbsp;구현 &lt;br /&gt;app/services/term_extractor.py를&amp;nbsp;생성하여&amp;nbsp;내부&amp;nbsp;dictionary&amp;nbsp;기반&amp;nbsp;의료&amp;nbsp;용어&amp;nbsp;추출&amp;nbsp;기능을&amp;nbsp;구현하였다. &lt;br /&gt;&lt;br /&gt;현재는 &lt;br /&gt;-&amp;nbsp;CRP &lt;br /&gt;-&amp;nbsp;내시경 &lt;br /&gt;-&amp;nbsp;염증 &lt;br /&gt;-&amp;nbsp;검사 &lt;br /&gt;등&amp;nbsp;일부&amp;nbsp;용어를&amp;nbsp;예시로&amp;nbsp;등록해두고,&amp;nbsp;입력&amp;nbsp;텍스트에&amp;nbsp;포함된&amp;nbsp;용어를&amp;nbsp;탐지하여&amp;nbsp;설명과&amp;nbsp;함께&amp;nbsp;반환하도록&amp;nbsp;하였다. &lt;br /&gt;&lt;br /&gt;app/routes/explain.py를&amp;nbsp;생성하여&amp;nbsp;POST&amp;nbsp;/explain&amp;nbsp;엔드포인트를&amp;nbsp;연결하였다. &lt;br /&gt;&lt;br /&gt;Swagger&amp;nbsp;테스트&amp;nbsp;결과 &lt;br /&gt;-&amp;nbsp;요청&amp;nbsp;성공 &lt;br /&gt;-&amp;nbsp;terms&amp;nbsp;배열&amp;nbsp;형식으로&amp;nbsp;응답&amp;nbsp;확인&amp;nbsp;완료 &lt;br /&gt;&lt;br /&gt;4-5.&amp;nbsp;records&amp;nbsp;기능&amp;nbsp;구현 &lt;br /&gt;app/data/records.json&amp;nbsp;파일을&amp;nbsp;생성하여&amp;nbsp;로컬&amp;nbsp;JSON&amp;nbsp;기반&amp;nbsp;저장소를&amp;nbsp;마련하였다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;app/routes/records.py를&amp;nbsp;생성하여&amp;nbsp;다음&amp;nbsp;기능을&amp;nbsp;구현하였다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;POST&amp;nbsp;/records&amp;nbsp;:&amp;nbsp;기록&amp;nbsp;저장 &lt;br /&gt;-&amp;nbsp;GET&amp;nbsp;/records&amp;nbsp;:&amp;nbsp;기록&amp;nbsp;목록&amp;nbsp;조회 &lt;br /&gt;-&amp;nbsp;GET&amp;nbsp;/records/{record_id}&amp;nbsp;:&amp;nbsp;기록&amp;nbsp;상세&amp;nbsp;조회 &lt;br /&gt;&lt;br /&gt;Swagger&amp;nbsp;테스트&amp;nbsp;결과 &lt;br /&gt;-&amp;nbsp;기록&amp;nbsp;저장&amp;nbsp;성공 &lt;br /&gt;-&amp;nbsp;목록&amp;nbsp;조회&amp;nbsp;성공 &lt;br /&gt;-&amp;nbsp;상세&amp;nbsp;조회&amp;nbsp;성공 &lt;br /&gt;&lt;br /&gt;5.&amp;nbsp;테스트&amp;nbsp;결과 &lt;br /&gt;Swagger를&amp;nbsp;이용해&amp;nbsp;직접&amp;nbsp;엔드포인트를&amp;nbsp;테스트한&amp;nbsp;결과,&amp;nbsp;아래&amp;nbsp;기능이&amp;nbsp;모두&amp;nbsp;정상&amp;nbsp;동작함을&amp;nbsp;확인하였다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;POST&amp;nbsp;/summary &lt;br /&gt;-&amp;nbsp;POST&amp;nbsp;/explain &lt;br /&gt;-&amp;nbsp;POST&amp;nbsp;/records &lt;br /&gt;-&amp;nbsp;GET&amp;nbsp;/records &lt;br /&gt;-&amp;nbsp;GET&amp;nbsp;/records/{record_id} &lt;br /&gt;&lt;br /&gt;6.&amp;nbsp;현재&amp;nbsp;단계의&amp;nbsp;의미 &lt;br /&gt;이번&amp;nbsp;작업으로&amp;nbsp;서버는&amp;nbsp;단순&amp;nbsp;STT&amp;nbsp;처리&amp;nbsp;수준을&amp;nbsp;넘어서,&amp;nbsp;모바일&amp;nbsp;앱이&amp;nbsp;바로&amp;nbsp;활용할&amp;nbsp;수&amp;nbsp;있는&amp;nbsp;구조화된&amp;nbsp;데이터&amp;nbsp;제공이&amp;nbsp;가능해졌다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;특히&amp;nbsp;A&amp;nbsp;파트에서는&amp;nbsp;현재&amp;nbsp;구현된&amp;nbsp;API를&amp;nbsp;바탕으로&amp;nbsp;다음&amp;nbsp;기능을&amp;nbsp;바로&amp;nbsp;연결할&amp;nbsp;수&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;진료&amp;nbsp;요약&amp;nbsp;화면&amp;nbsp;표시 &lt;br /&gt;-&amp;nbsp;의료&amp;nbsp;용어&amp;nbsp;설명&amp;nbsp;UI&amp;nbsp;표시 &lt;br /&gt;-&amp;nbsp;기록&amp;nbsp;저장&amp;nbsp;기능 &lt;br /&gt;-&amp;nbsp;기록&amp;nbsp;목록&amp;nbsp;화면 &lt;br /&gt;-&amp;nbsp;기록&amp;nbsp;상세&amp;nbsp;화면 &lt;br /&gt;&lt;br /&gt;7.&amp;nbsp;한계점 &lt;br /&gt;이번&amp;nbsp;단계는&amp;nbsp;1차&amp;nbsp;뼈대&amp;nbsp;구현이므로&amp;nbsp;다음과&amp;nbsp;같은&amp;nbsp;한계가&amp;nbsp;있다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;summary는&amp;nbsp;실제&amp;nbsp;AI&amp;nbsp;요약&amp;nbsp;모델이&amp;nbsp;아닌&amp;nbsp;단순&amp;nbsp;분리&amp;nbsp;기반&amp;nbsp;더미&amp;nbsp;로직임 &lt;br /&gt;-&amp;nbsp;explain은&amp;nbsp;외부&amp;nbsp;의료&amp;nbsp;용어&amp;nbsp;API가&amp;nbsp;아닌&amp;nbsp;내부&amp;nbsp;dictionary&amp;nbsp;기반임 &lt;br /&gt;-&amp;nbsp;records는&amp;nbsp;DB가&amp;nbsp;아닌&amp;nbsp;JSON&amp;nbsp;파일&amp;nbsp;로컬&amp;nbsp;저장&amp;nbsp;방식임 &lt;br /&gt;-&amp;nbsp;STT&amp;nbsp;결과와&amp;nbsp;summary/explain/records가&amp;nbsp;자동&amp;nbsp;연동된&amp;nbsp;상태는&amp;nbsp;아님 &lt;br /&gt;&lt;br /&gt;8.&amp;nbsp;향후&amp;nbsp;계획 &lt;br /&gt;다음&amp;nbsp;단계에서는&amp;nbsp;아래&amp;nbsp;항목들을&amp;nbsp;순차적으로&amp;nbsp;개선할&amp;nbsp;예정이다. &lt;br /&gt;&lt;br /&gt;-&amp;nbsp;STT&amp;nbsp;결과&amp;nbsp;후처리용&amp;nbsp;text_cleaner&amp;nbsp;추가 &lt;br /&gt;-&amp;nbsp;summary를&amp;nbsp;실제&amp;nbsp;요약&amp;nbsp;모델&amp;nbsp;또는&amp;nbsp;LLM과&amp;nbsp;연결 &lt;br /&gt;-&amp;nbsp;explain&amp;nbsp;기능을&amp;nbsp;의료&amp;nbsp;용어&amp;nbsp;API&amp;nbsp;또는&amp;nbsp;확장&amp;nbsp;사전과&amp;nbsp;연결 &lt;br /&gt;-&amp;nbsp;records&amp;nbsp;저장&amp;nbsp;구조를&amp;nbsp;DB&amp;nbsp;기반으로&amp;nbsp;전환 &lt;br /&gt;-&amp;nbsp;STT&amp;nbsp;결과가&amp;nbsp;생성되면&amp;nbsp;summary/explain/records로&amp;nbsp;자연스럽게&amp;nbsp;이어지는&amp;nbsp;서버&amp;nbsp;흐름&amp;nbsp;구성 &lt;br /&gt;-&amp;nbsp;Flutter(A)와의&amp;nbsp;실제&amp;nbsp;연결&amp;nbsp;및&amp;nbsp;UI&amp;nbsp;테스트&amp;nbsp;진행 &lt;br /&gt;&lt;br /&gt;9.&amp;nbsp;결론 &lt;br /&gt;이번&amp;nbsp;작업에서는&amp;nbsp;MedExplain&amp;nbsp;서버&amp;nbsp;파트에서&amp;nbsp;모바일과&amp;nbsp;연동&amp;nbsp;가능한&amp;nbsp;핵심&amp;nbsp;API&amp;nbsp;뼈대를&amp;nbsp;우선&amp;nbsp;구현하였다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;이를&amp;nbsp;통해&amp;nbsp;프로젝트의&amp;nbsp;방향을&amp;nbsp;단순&amp;nbsp;음성&amp;nbsp;인식&amp;nbsp;중심에서,&amp;nbsp;환자/보호자가&amp;nbsp;진료&amp;nbsp;내용을&amp;nbsp;이해하고&amp;nbsp;다시&amp;nbsp;확인할&amp;nbsp;수&amp;nbsp;있도록&amp;nbsp;돕는&amp;nbsp;구조로&amp;nbsp;구체화하였다.&amp;nbsp;&amp;nbsp; &lt;br /&gt;현재는&amp;nbsp;더미&amp;nbsp;및&amp;nbsp;로컬&amp;nbsp;저장&amp;nbsp;기반이지만,&amp;nbsp;전체&amp;nbsp;시스템&amp;nbsp;연결의&amp;nbsp;출발점이&amp;nbsp;되는&amp;nbsp;핵심&amp;nbsp;구조는&amp;nbsp;확보한&amp;nbsp;상태이며,&amp;nbsp;이후에는&amp;nbsp;이를&amp;nbsp;실제&amp;nbsp;모델&amp;nbsp;및&amp;nbsp;저장&amp;nbsp;구조와&amp;nbsp;연결하여&amp;nbsp;기능을&amp;nbsp;고도화할&amp;nbsp;예정이다.&lt;/p&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/3</guid>
      <comments>https://ayoonistory.tistory.com/3#entry3comment</comments>
      <pubDate>Thu, 12 Mar 2026 17:29:28 +0900</pubDate>
    </item>
    <item>
      <title>1/25 API 계약 최종 고정,JSON 깨짐 방어 로직</title>
      <link>https://ayoonistory.tistory.com/2</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 담당 역할 개요 (B 담당)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 프로젝트에서 B 담당은 FastAPI 기반 서버를 구축하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;STT 결과 텍스트를 입력받아 &lt;b&gt;환자 친화적 요약 JSON&lt;/b&gt;으로 변환하는 /summarize API를 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 프론트엔드와의 안정적인 연동을 위해 &lt;b&gt;API 계약 고정 및 JSON 안정성 확보&lt;/b&gt;를 핵심 목표로 두었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. API 설계 및 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) API 계약 고정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;shared/schema.md를 단일 기준으로 삼아, 요청&amp;middot;응답 구조를 변경하지 않고 구현하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;요청&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;POST /summarize
{
&quot;raw_text&quot;:&quot;string&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;응답&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
&quot;summary_3sent&quot;:&quot;string&quot;,
&quot;easy_terms&quot;:[
{&quot;term&quot;:&quot;string&quot;,&quot;easy&quot;:&quot;string&quot;}
],
&quot;next_actions&quot;:[
{&quot;title&quot;:&quot;string&quot;,&quot;detail&quot;:&quot;string&quot;}
],
&quot;warnings&quot;:[&quot;string&quot;]
}

&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FastAPI의 Pydantic 모델을 활용하여 필드 누락이나 타입 오류가 발생하지 않도록 강제하였다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 요약 로직 및 방어 설계&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요약 결과는 &lt;b&gt;항상 3문장&lt;/b&gt;이 되도록 보정 로직을 적용하였다.&lt;/li&gt;
&lt;li&gt;어떤 입력이 들어와도 &lt;b&gt;응답 JSON 구조가 깨지지 않도록&lt;/b&gt; 설계하였다.&lt;/li&gt;
&lt;li&gt;실제 LLM 연동 이전 단계로, 현재는 더미 요약 결과를 사용하되&lt;/li&gt;
&lt;li&gt;구조와 흐름은 그대로 유지되도록 구현하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 서버 안정성 확보&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;/health 엔드포인트를 통해 서버 상태를 즉시 확인할 수 있도록 구성하였다.&lt;/li&gt;
&lt;li&gt;Swagger UI(/docs)를 통해 프론트엔드 및 팀원들이 직접 API 테스트 가능하도록 하였다.&lt;/li&gt;
&lt;li&gt;curl / Swagger 테스트 결과, &lt;b&gt;여러 차례 호출에서도 모두 200 OK 응답&lt;/b&gt;을 반환함을 확인하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 테스트 결과&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Swagger UI 및 curl을 이용해 다양한 의료 텍스트 입력 테스트 수행&lt;/li&gt;
&lt;li&gt;모든 테스트에서 JSON 구조가 유지됨&lt;/li&gt;
&lt;li&gt;프론트엔드에서 바로 사용 가능한 형태의 응답 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 한계점 및 향후 개선 방향&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 의료 용어 설명(easy_terms)은 더미 데이터 수준으로 제한적임&lt;/li&gt;
&lt;li&gt;의료 용어 사전 API 연동 또는 LLM 기반 동적 설명 생성이 필요함&lt;/li&gt;
&lt;li&gt;추후 프롬프트 설계를 통해 의료 표현의 정확성과 범용성을 개선할 예정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본 단계에서는 서버의 &lt;b&gt;구조적 안정성과 계약 준수&lt;/b&gt;를 최우선으로 구현을 완료하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의료 내용의 품질 개선은 다음 단계에서 확장 가능하도록 설계되어 있으며,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 상태로 프론트엔드 연동 및 데모 시연이 가능한 수준임을 확인하였다.&lt;/p&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/2</guid>
      <comments>https://ayoonistory.tistory.com/2#entry2comment</comments>
      <pubDate>Sat, 7 Mar 2026 20:58:18 +0900</pubDate>
    </item>
    <item>
      <title>1/19 서버 Google Cloud Speech-to-Text(STT) 연동 설정 및 REST 요청/응답 확정</title>
      <link>https://ayoonistory.tistory.com/1</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 목표&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google Cloud Speech-to-Text API를 프로젝트에 연결하고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;WAV 파일을 REST로 전송했을 때 어떤 JSON이 오며&lt;/b&gt;,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답에서 최종 텍스트(transcript)를 추출하는 방법&lt;/b&gt;을 확정한다.&lt;/li&gt;
&lt;li&gt;&amp;ldquo;샘플 오디오로 STT 1회 성공&amp;rdquo;을 기준으로 기능 검증을 완료한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. GCP 콘솔 설정 절차&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 프로젝트 생성 및 결제 연결&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Google Cloud Console에서 신규 프로젝트 생성(예: medexplain-stt)&lt;/li&gt;
&lt;li&gt;결제(Billing) 계정 생성/연결&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;STT API는 유료 과금 가능성이 있으므로 결제 계정 연결이 필요함&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;비용 통제 목적의 Budget/알림 설정은 선택(추가로 설정 가능)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 Speech-to-Text API 활성화&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API 및 서비스 &amp;rarr; 라이브러리&lt;/li&gt;
&lt;li&gt;Cloud Speech-to-Text API 검색 후 &amp;ldquo;사용&amp;rdquo; 활성화&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 인증(키) 준비&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;API 및 서비스 &amp;rarr; 사용자 인증 정보&lt;/li&gt;
&lt;li&gt;API 키 생성&lt;/li&gt;
&lt;li&gt;보안상 권장 사항&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;생성한 키는 &lt;b&gt;Cloud Speech-to-Text API만 호출 가능하도록 제한&lt;/b&gt;(API 제한사항 설정)&lt;/li&gt;
&lt;li&gt;키 노출 위험이 있으므로 테스트 후 &lt;b&gt;키 회전(재생성)&lt;/b&gt; 권장&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 테스트 오디오 준비 및 로컬 테스트 방식&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 오디오 파일 준비&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Windows 기본 &amp;ldquo;음성 녹음기&amp;rdquo;로 녹음 시 기본 저장은 m4a가 많아,&lt;/li&gt;
&lt;li&gt;STT 테스트를 위해 온라인 변환 도구 등을 이용해 wav로 변환하였다.&lt;/li&gt;
&lt;li&gt;최종 테스트 파일: test.wav&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.2 로컬에서 REST 호출 테스트 환경&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CMD에서 멀티라인 JSON 작성이 깨지는 문제가 있어(줄 이어쓰기/따옴표 이슈),&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PowerShell 기반&lt;/b&gt;으로 다음을 수행하였다:
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;wav 파일을 byte로 읽기&lt;/li&gt;
&lt;li&gt;base64로 인코딩&lt;/li&gt;
&lt;li&gt;요청 JSON을 생성&lt;/li&gt;
&lt;li&gt;Invoke-RestMethod로 POST 호출&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. STT REST 호출 규격 확정(요청/응답)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 Endpoint&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;POST &amp;lt;&lt;a href=&quot;https://speech.googleapis.com/v1/speech:recognize?key=API_KEY&quot;&gt;https://speech.googleapis.com/v1/speech:recognize?key=API_KEY&lt;/a&gt;&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 Request JSON 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WAV 파일을 base64로 담아 전송한다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
&quot;config&quot;:{
&quot;encoding&quot;:&quot;LINEAR16&quot;,
&quot;languageCode&quot;:&quot;ko-KR&quot;,
&quot;audioChannelCount&quot;:2
},
&quot;audio&quot;:{
&quot;content&quot;:&quot;&amp;lt;BASE64_ENCODED_WAV_BYTES&amp;gt;&quot;
}
}

&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;languageCode: ko-KR&lt;/li&gt;
&lt;li&gt;encoding: LINEAR16 (PCM WAV 기준)&lt;/li&gt;
&lt;li&gt;audioChannelCount: &lt;b&gt;파일이 스테레오(2채널)일 경우 2 명시&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;(선택) 파일 헤더가 48000Hz인 경우 sampleRateHertz: 48000을 추가할 수 있음&lt;/li&gt;
&lt;li&gt;(파일과 다르게 지정하면 오류 발생)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 시행착오 및 해결 과정(핵심)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 문제 1) JSON payload 오류(400 Invalid JSON)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CMD에서 줄바꿈/이스케이프가 꼬이면서 JSON이 깨지는 현상이 발생&lt;/li&gt;
&lt;li&gt;해결: &lt;b&gt;PowerShell&lt;/b&gt;로 전환하여 JSON 생성/요청을 처리함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 문제 2) sample_rate_hertz 불일치 오류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 메시지 요약:&lt;/li&gt;
&lt;li&gt;sample_rate_hertz(16000) must match WAV header(48000)&lt;/li&gt;
&lt;li&gt;원인: 변환된 WAV 파일의 실제 샘플레이트가 48000Hz인데 요청에 16000Hz를 넣어 충돌&lt;/li&gt;
&lt;li&gt;해결:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;sampleRateHertz를 파일에 맞추거나(48000),&lt;/li&gt;
&lt;li&gt;또는 sampleRateHertz 항목을 생략하여 해결 시도&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 문제 3) 채널 수 불일치(스테레오/모노) 오류&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;오류 메시지 요약:&lt;/li&gt;
&lt;li&gt;Must use single channel (mono) audio, but WAV header indicates 2 channels.&lt;/li&gt;
&lt;li&gt;원인: WAV 파일이 &lt;b&gt;2채널(스테레오)&lt;/b&gt; 인데 요청 config가 이를 반영하지 못함&lt;/li&gt;
&lt;li&gt;해결: audioChannelCount: 2를 요청 config에 추가하여 성공&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 성공 결과(샘플 오디오 STT 1회 성공)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.1 PowerShell 실행 흐름(요약)&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;파일 경로 설정 및 존재 확인&lt;/li&gt;
&lt;li&gt;파일을 base64로 변환&lt;/li&gt;
&lt;li&gt;config + audio(content)로 JSON 생성&lt;/li&gt;
&lt;li&gt;recognize endpoint로 POST 호출&lt;/li&gt;
&lt;li&gt;응답에서 transcript 추출&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6.2 transcript 추출 결과&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추출 경로: results[*].alternatives[0].transcript&lt;/li&gt;
&lt;li&gt;실제 출력 예시:
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Google 스피치 텍스트 테스트 음성입니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 응답에서 최종 텍스트 추출 로직(확정)&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기본 규칙: results 배열을 순회하며&lt;/li&gt;
&lt;li&gt;alternatives[0].transcript를 순서대로 이어 붙여 최종 텍스트 생성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의사코드:&lt;/p&gt;
&lt;pre class=&quot;gradle&quot;&gt;&lt;code&gt;final_text = join(&quot; &quot;, for each r in results: r.alternatives[0].transcript)

&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 팀 통일 포맷 제안(추후 안정화)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 오류의 대부분이 &amp;ldquo;파일 포맷/샘플레이트/채널수 불일치&amp;rdquo;에서 발생했으므로, 팀 내 통일 권장:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;권장 통일안(A)&lt;/b&gt;: mono(1채널) + 16kHz + PCM LINEAR16 WAV
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;config 단순화 가능&lt;/li&gt;
&lt;li&gt;오류 감소&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;대안(B)&lt;/b&gt;: 원본 그대로 사용할 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파일 헤더에 맞춰 sampleRateHertz, audioChannelCount를 반드시 맞춤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 결론&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GCP 프로젝트/결제/API 활성화/키 발급/REST 규격 확정 완료&lt;/li&gt;
&lt;li&gt;샘플 wav 파일 기반으로 &lt;b&gt;STT recognize 호출 성공 및 transcript 추출 성공&lt;/b&gt; 확인&lt;/li&gt;
&lt;li&gt;보고서에 포함 가능한 수준으로 요청/응답 구조 및 파싱 로직을 확정함&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>졸업프로젝트</category>
      <author>ayoonistory</author>
      <guid isPermaLink="true">https://ayoonistory.tistory.com/1</guid>
      <comments>https://ayoonistory.tistory.com/1#entry1comment</comments>
      <pubDate>Sat, 7 Mar 2026 20:57:05 +0900</pubDate>
    </item>
  </channel>
</rss>