Skip to content
Tech4GoodHACKERTHONPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

9 Commits

Folders and files

Repository files navigation

개화 (Gaehwa) — AI 백엔드

개화

발화가 어려운 사람의 '입'이 되는 AAC 앱. 주변 대화를 듣고, 사용자가 탭 한 번으로 말할 완성 문장 후보 4개를 만듭니다.

로컬 지연 모순 라이선스 모델

🔗 배포 https://gaehwa.vercel.app · 📋 심사위원 참고용 부록 ← 3분 안에 직접 확인하는 법

⚠️ 배포 URL은 AI 서버(GPU 기기)가 켜져 있을 때만 답변이 나옵니다 — Vercel엔 화면만 있습니다(이유).


바로 가기 — 프로젝트 소개 · 주요 기능 · 사용 기술 · 이 프로젝트가 푼 문제 · 실행 · 확인 · 정직한 한계 · 문서 · 팀


📌 프로젝트 소개

병원에 간 후두적출 환자가 있습니다. 간호사가 묻습니다 — "어디가 불편해서 오셨어요?" 머릿속엔 답이 있는데 입이 안 움직입니다. 종이에 쓰려면 1분, 그 사이 간호사는 다른 일을 하러 갑니다.

개화는 그 1분을 3초로 만듭니다.

간호사: "어디가 불편해서 오셨어요?"       ← 앱이 듣고
                ↓
   [답변] 허리가 아파서 왔어요            ← 후보 4개를 띄우면
   [답변] 사흘째 허리가 아파요
   [보류] 음… 잠시만요
   [추가] 검사도 해야 하나요?
                ↓
            탭 한 번                     ← 앱이 소리 내어 말합니다

기존 AAC와 무엇이 다른가

기존 AAC 개화
문장 사용자가 미리 등록해두고 찾아서 누름 앱이 지금 이 대화를 듣고 만듦
상황 사용자가 판단 앱이 파악해서 보여줌
말투 앱이 정한 문어체 내 말투 예시를 학습해 따라함

지키는 원칙 3가지

원칙 구현
🔒 대화가 기기 밖으로 나가지 않는다 외부 API 0개. STT·LLM·화자분리 전부 로컬
🖐️ AI가 마음대로 말하지 않는다 사용자가 탭하기 전엔 절대 발화하지 않음. 호흡 신호(「음… 잠시만요」)만 예외
🚫 거절할 권리 후보 4개 안에는 항상 거절하거나 보류할 수 있는 것이 있음 (코드로 강제)

✨ 주요 기능

1. 상황 인식 답변 추천

상대 발화를 듣고 행위를 분류한 뒤(질문/제안/안내), 그 행위에 유효한 응답만 만듭니다.

"드시는 약 있으세요?"  → [답변] [조건] [보류] [추가]    ← 거절 자리가 없음
"적립 도와드릴까요?"   → [수락] [거절] [조건] [질문]    ← 거절이 유효한 자리

이 설계가 이 프로젝트의 핵심입니다. 왜 이렇게 했는지는 아래 「이 프로젝트가 푼 문제」를 보세요.

2. 다중 화자 구분

목소리 지문(임베딩)으로 화자 A~F를 자동 구분합니다. 답할 대상을 골라 그 사람에게만 답할 수 있습니다.

  • 신뢰도 3단계: confirmed(확실) / estimated(추정) / unknown(미확인)
  • 목소리 지문은 저장하지 않습니다 — 대화가 끝나면 사라집니다

3. 꾹 누른 채 듣기 + 3초 프리롤

말이 언제 끝나는지 AI가 추측하지 않습니다. 사용자가 버튼을 쥔 동안만 듣습니다.

문제: 상대가 말을 시작한 뒤 버튼을 누르면 앞부분을 놓칩니다. 해결: 링버퍼가 항상 최근 3초를 기억합니다. 버튼을 누르면 3초 전부터 소급합니다. (블랙박스와 같은 원리)

4. 중요한 대화 미리 준비

병원·은행처럼 실수가 부담스러운 대화는 목적·말할 것·물어볼 것을 미리 적어둡니다. 후보가 그 방향으로만 나오고, 전달된 항목은 자동으로 체크됩니다.

5. 에코 가드

앱이 방금 말한 문장을 마이크가 다시 듣고 "상대가 말했다"고 착각하는 것을 막습니다. TTS 중 STT 정지 + 자기 발화 유사도 필터.

6. 자가진단 (🩺)

GPU OOM·CPU 폴백·마이크 권한 실패가 조용히 일어나면 "그냥 안 되는" 상태가 됩니다. /api/version이 엔진이 GPU/CPU 중 무엇으로 떴는지 노출해 무엇이 막혔는지 보이게 합니다.


🛠 사용 기술

AI 3종 — 전부 이 기기에서

   상대방 목소리
        │
        ▼
 ┌─────────────────┐
 │ ① 음성 → 글자    │  faster-whisper large-v3-turbo   GPU (int8_float16)
 └────────┬────────┘
          ├──────────────┐
          ▼              ▼
 ┌─────────────────┐  ┌──────────────────┐
 │ ② 목소리 지문    │  │ ③ 답변 생성       │  SKT A.X 4.0 Light 7B Q4
 │   eres2net  CPU │  │   ollama, GPU 상주 │
 └─────────────────┘  └──────────────────┘
역할 모델 장치 라이선스 선정 근거
음성 인식 faster-whisper large-v3-turbo GPU (int8_float16, beam=1) MIT 한국어 정확도 · 0.2~0.4초 실측
답변 생성 SKT A.X 4.0 Light 7B Q4_K_M GPU 상주 (keep_alive=-1) Apache-2.0 골든셋 97.4% vs kanana-8B 52.8% — 큰 모델이 항상 낫지 않았다
화자 지문 eres2net (3D-Speaker, ONNX) CPU Apache-2.0 3파전 실측 — 분리 간격 2.2배로 선정

외부 API를 하나도 쓰지 않습니다. AAC 사용자의 대화는 병명·복용약·재정 같은 가장 사적인 정보이기 때문입니다.

서버

언어 Python 3.12
웹 FastAPI 0.136.3 + Uvicorn 0.48.0
계약 Pydantic — API 계약을 코드로 (app/contract.py)
오디오 ffmpeg (webm/opus → wav 16kHz mono)
스트리밍 NDJSON — type 기반 분기. 실제 순서는 candidate×N → situation → done (첫 후보를 앞당기려 SITUATION을 뒤로 뺐다). done만 마지막이 보장된다

전체 목록과 라이선스 의무: LICENSES/REGISTER.md

아키텍처 — 4층

app/
├── policy/       사람이 바꾸는 값. 코드 수정 없이 변경.
│   ├── policy.yaml          ★ 유일한 정본. 모든 숫자에 실측 근거 주석
│   └── prompts/
│
├── engines/      외부 자원 (whisper·eres2net·ollama·ffmpeg)
│                 인터페이스 뒤 → 테스트에서 스텁 가능
│
├── core/         순수 로직. I/O 없음 → 0.7초 테스트
│   ├── acts.py            ★ 인접쌍 — 이 프로젝트의 핵심
│   ├── prompt.py · parse.py · filters.py · speaker_policy.py
│
├── pipeline/     조립 (엔진 + 코어)
└── contract.py   Pydantic = API 계약의 코드화

왜 정책과 코드를 나눴나: 감사에서 라벨 어휘가 3곳(서버/프론트/채점기), 임계값이 코드 곳곳에 사본으로 흩어져 있었습니다. "한 곳 고치면 나머지가 남는" 상태였고, 실제로 채점기가 사본을 들고 있어 서버가 통과시킨 정답을 오판했습니다. 지금은 policy.yaml이 유일한 정본이고 /api/policy로 프론트에 내려갑니다.


🎯 이 프로젝트가 푼 문제 — "AI가 거짓말을 만들었다"

발견

사용자 프로필: "김OO, 65세, 후두적출. 혈압약 복용 중"
약사:         "드시는 약 있으세요?"

앱이 제시한 후보:
  [확인] 약은 잘 챙겨 먹고 있어요    ← 정답
  [거절] 저는 약 안 먹어요          ← ⚠️ 프로필과 정반대

측정: 후보의 28%(5/18)에 이런 모순이 있었습니다.

AAC 사용자는 이 버튼을 탭합니다. 약국에서 복약 사실을 부정하는 버튼이 제공되고 있었습니다. 품질 문제가 아니라 안전 문제입니다. 그리고 골든셋 98%는 이걸 한 번도 못 잡았습니다.

원인 — 우리가 쓴 프롬프트였다

세 제약이 충돌했습니다:

  1. "라벨이 서로 달라야 한다" (형식 — 검증 가능)
  2. "최소 2개는 답변" (의미)
  3. "1개는 거절 계열" (의미)

7B는 검증 가능한 제약을 지키고 의미를 버립니다. 그리고 거절 슬롯에 넣을 재료가 없으면 지어냅니다. 즉 *"거절할 권리를 주자"*는 좋은 의도가 거짓말 생산기를 만들었습니다.

더 나빴던 것: 코드가 프롬프트를 배신하고 있었습니다.

if key in state.seen_texts or cand["label"] in state.seen_labels:  # 라벨 중복 제거

프롬프트는 "답변 2개"를 요구하는데 이 줄이 두 번째 답변을 버렸습니다.

해법 — 1973년 대화분석 이론

  • Schegloff & Sacks (1973) — conditional relevance: 질문의 응답 자리에는 답이 온다
  • Pomerantz (1984) — preference organization: 거절은 제안에 대한 유효한 응답이다. 질문에 대한 응답이 아니다

→ 사실 질문에서 거절권의 올바른 형태는 **「말씀드리고 싶지 않아요」(보류)**이지 **「약 안 먹어요」(거짓)**가 아닙니다.

거절권을 삭제하지 않고 행위별로 재정의했습니다:

app/policy/policy.yaml:

acts:
  plans:
    WH_QUESTION: [답변, 답변, 보류, 추가, 확인, 의견]   # 거절 없음
    YN_QUESTION: [답변, 조건, 보류, 추가, 확인, 의견]   # 답변 1개 ★
    OFFER:       [수락, 거절, 조건, 질문, 확인, 보류]   # 거절이 유효한 유일한 자리
    INFORM:      [확인, 의견, 질문, 보류, 추가, 조율]

★ YN 질문의 답변 슬롯이 1개인 이유: 예/아니오 질문의 참인 답은 하나입니다. 슬롯을 2개 주면 모델이 네/아니요 쌍으로 채워 두 번째가 필연적으로 사실을 뒤집습니다.

결과 (실측)

지표 이전 이후
프로필 모순(거짓말) 28% (5/18) 0% (0/18)
거절권 보장 1/3 누락 10/10
후보 개수 2.8~4.0 4.00
상대 질문 되묻기 5~10% 0%
행위 분류 정확도 — 16/16
첫 후보 지연 — 중앙값 246ms (8회, 205~446ms)

측정으로 배제한 것들

방법 배제 사유 (실측)
JSON schema 강제 첫 토큰 116ms → 1,930ms (16배). 제품 지표 파괴
Cross-encoder 재순위 여유 VRAM 1,230MiB < 필요 1.6GB. 스트리밍과도 상충
7B 파인튜닝 선행연구가 동급 7B로 실패 (arXiv 2509.13671, 수용률 2.3%)
화행 분류기 모델 정규식으로 16/16. 3i4k는 CC-BY-NC(상업 불가). AAC 화행 SOTA 38.6%

🚀 실행 방법

요구사항

Python 3.12
GPU NVIDIA, VRAM 8GB 이상 (CUDA)
시스템 ffmpeg, ollama

설치·실행

pip install -r requirements.txt      # 버전 고정됨
./pull_models.sh                     # ollama + A.X 4.0 Light 내려받기
python3 server.py                    # HTTP 8737 (+ 인증서 있으면 HTTPS 8443)

models/eres2net.onnx는 sherpa-onnx 배포본에서 받습니다 (라이선스 원본 배포처 존중 — 저장소에 포함하지 않음).

⚠️ 마이크는 HTTPS(secure context)에서만 동작합니다. getUserMedia의 브라우저 정책입니다.


✅ 확인 방법

1. 서버가 GPU로 떴는지

curl -s localhost:8737/api/version
{"code":"9b7cb43b","started":"2026-07-16 05:04:39",
 "device":{"llm":"gpu:A.X-4.0-Light-gguf:Q4_K_","asr":"cuda","speaker":"cpu"}}

asr이 cpu면 조용한 CPU 폴백입니다(0.3초 → 5초). 이걸 눈에 보이게 하려고 노출합니다.

2. 답변 생성 (핵심 기능)

curl -s -X POST localhost:8737/api/candidates -H 'Content-Type: application/json' -d '{
  "mode":"reply",
  "profile":{"basic":"김OO, 65세, 후두적출. 혈압약 복용 중","style_examples":["부탁드립니다"]},
  "turns":[{"role":"partner","text":"드시는 약 있으세요?"}]
}'
{"type":"candidate","index":0,"label":"답변","text":"혈압약 복용 중이에요."}
{"type":"candidate","index":1,"label":"조건","text":"약은 꼭 챙겨 먹어야 해요"}
{"type":"candidate","index":2,"label":"보류","text":"음… 잠깐 생각해 볼게요"}
{"type":"candidate","index":3,"label":"추가","text":"혹시 부작용은 없나요?"}
{"type":"situation","summary":"약국에서 복용약 확인 중","tags":["약국","복약"],...}
{"type":"done","count":4,"first_ms":307,"act":"YN_QUESTION",...}

act가 YN_QUESTION이므로 거절 슬롯이 없고, 답변이 프로필과 일치합니다.

3. 테스트

./tests/run_all.sh          # 단위 — 서버·GPU 불필요, 0.7초
python3 eval/run_eval.py 3  # E2E — 골든셋 76 시나리오 × 3회 (~8분, 서버 필요)
단위 E2E
잡는 것 설계 불변식(거절권·인접쌍), 필터 로직 실제 생성 품질, ★프로필 모순
시간 0.7초 ~8분

📊 정직한 한계

이 README의 모든 점수는 "우리가 만든 골든셋(76 시나리오)에서의 성능"이지 "제품 성능"이 아닙니다.

우리 점수는 한때 가짜였습니다

프롬프트에 정답(「예순다섯입니다」)이 박혀 있고 골든셋이 must_any: ["65","예순"]으로 바로 그 문자열을 검사했습니다. eval이 프롬프트 복사 능력을 재고 있었습니다. 이것이 "eval 통과인데 실사용 깨짐"의 정체였고, 오염을 걷어낸 뒤의 숫자가 지금 것입니다.

미해결 — 환각

프로필: "김OO, 65세, 후두적출. 혈압약 복용 중"   ← 수술 날짜 없음
상대:   "수술은 언제 하셨어요?"
출력:   "1년 전에 했어요"                        ← 창작 (실측 1/3)

7B의 한계이며 구조로 못 막았습니다. 골든셋의 환각-없는정보(안전) 시나리오가 회귀를 감시합니다.

테스트가 못 잡은 것들

이 결함들은 단위·E2E·화면 흐름 테스트를 전부 통과했습니다:

결함 왜 못 잡았나
GPU OOM HTTPS를 별도 프로세스로 띄워 whisper가 2번 로드. 단위 58종·E2E 98.5% 통과 중이었다
다중화자 죽음 상태 통합 때 참조가 끊김. 화면은 렌더되고 문법도 맞다
프리롤 0 AudioContext가 잠들어 있음. 브라우저 동작이라 서버 테스트로 불가능

→ 마이크를 실제로 누르는 경로만이 검출할 수 있었습니다.

검증 못 한 데이터

검증함 검증 못 함
zeroth-korean (화자 임계값) 고령자 음성 STT 정확도 ← 최대 구멍
합성 2화자 (분리 경로) 실제 다자 대화, 중첩 발화, 실제 AAC 사용자 발화


📚 문서

문서 대상 · 내용
심사위원 참고용 부록 3분 안에 직접 확인하는 법. 주장마다 "이렇게 확인하십시오"를 붙였습니다
사용 기술 딥다이브 4층 구조 — 초등학생 2분 → 비전공자 8분 → 개발자 20분 → 연구자 30분
API 명세서 946줄. 모든 예시가 실측 응답
추가기획서 인접쌍 재설계 + 근거 레지스터 14건
라이선스 레지스터 구성요소 × 라이선스 × 의무

📱 데모 화면

목업이 아니라 실제 앱을 렌더한 것입니다.

실시간 대화 다중 화자 대화 준비
상대 말을 듣고 후보 4개 제시 화자 A/B 구분 · 답할 대상 선택 목적·할 말을 미리 등록
홈 대화 종료 내 정보
준비한 대화 · 자주 쓰는 표현 달성률 확인 · 저장/삭제 선택 프로필 · 듣기 · 화자 임계값
로그인 · 회원가입 · 기록 화면 더 보기
로그인 회원가입 기록

👥 팀원별 역할

역할 팀원
AI 모델링 & 백엔드 안지홍 · 이윤지
프론트엔드 이소연 · 김현성

이 저장소는 AI 모델링 & 백엔드 산출물입니다. 프론트엔드는 별도 저장소에 있습니다.


📄 라이선스

코드 MIT — LICENSE
사용 모델·라이브러리 LICENSES/REGISTER.md (구성요소×라이선스×의무 표)
앱 고지문 LICENSES/NOTICE

사용 모델은 전부 Apache-2.0 / MIT입니다. 상업 이용이 제한되는 모델(EXAONE · 3i4k 화행분류기 · jina-reranker · Supertonic)은 검토 후 배제했고, 사유를 REGISTER.md §4에 남겼습니다.

어떤 모델도 파인튜닝하지 않습니다 → 학습 데이터 라이선스 의무가 발생하지 않습니다.



프론트엔드는 별도 저장소입니다. 이 저장소는 AI 백엔드만 담고 있습니다.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages