프론트엔드·백엔드 팀이 AdoptAI 파이프라인을 연동할 때 참고하는 문서입니다.
역할 분담 (현재)
- 프론트엔드: TTS 재생, 마이크 녹음, API 호출 순서 제어
- STT/AI API (본 레포): Whisper 전사,
SlotExtractor슬롯 추출,AskBackEngine재질문·슬롯 갱신, 공고 생성llm/voice_session.run_voice_session()은 로컬 데모 전용이며 서비스 API 경로에서 호출되지 않습니다.
프론트가 TTS/마이크를 담당할 때의 필수 순서입니다. TTS 재생 중에는 마이크(STT)를 켜지 마세요.
sequenceDiagram
participant FE as Frontend
participant API as STT/AI API
participant SE as SlotExtractor
participant AB as AskBackEngine
Note over FE: TTS 안내 (슬롯 목록 나열 금지)
FE->>FE: TTS 종료 후 마이크 ON
FE->>FE: 자유 발화 녹음
FE->>FE: 마이크 OFF
FE->>API: POST /stt/transcribe-and-start (audio)
API->>SE: extract_slots(stt_text)
SE-->>API: slots, missing_slots
API-->>FE: session_id, pending_slot=_confirm, confirmation/question
Note over FE: 요약 확인 TTS
FE->>FE: TTS(confirmation) → 마이크 ON → 답변 녹음
FE->>API: POST /pipeline/answer {session_id, answer}
API-->>FE: slots, missing_slots, question?
alt question != null (누락 슬롯 있음)
loop 필수 슬롯이 채워질 때까지
FE->>FE: TTS로 question 재생 → TTS 종료
FE->>FE: 마이크 ON → 답변 녹음 → 마이크 OFF
FE->>API: POST /pipeline/answer {session_id, answer}
API->>AB: update_slots + detect_missing_slots
API-->>FE: slots, missing_slots, question?
end
end
Note over FE: question == null && ready_for_notice
FE->>API: POST /pipeline/complete {session_id}
API-->>FE: slots + notice (title, body, info_table, faithfulness)
| 단계 | 프론트 동작 | API |
|---|---|---|
| 0 | TTS 안내 4문장 + get_initial_guide() 재생 (아래 문구) |
없음 |
| 1 | 자유 발화 녹음 | POST /stt/transcribe-and-start |
| 1b | pending_slot="_confirm"이면 요약 확인 TTS → 답변 녹음 ("네 맞아요" 또는 수정) |
POST /pipeline/answer |
| 2a | 응답의 question을 TTS 재생 → 답변 녹음 |
POST /pipeline/answer (반복) |
| 2b | question == null 이면 재질문 종료 |
— |
| 3 | 공고 생성 요청 | POST /pipeline/complete |
안내 TTS 권장 문구 (슬롯 이름 나열 금지):
- 안녕하세요.
- 사진을 제외한 나머지 정보는 음성으로 입력받겠습니다.
- 입양 공고 작성에 필요한 정보를 자유롭게 말씀해주세요.
- 부족한 정보는 제가 추가로 질문드리겠습니다.
초기 안내 (슬롯 추출 전, 권장) — llm/askback.py의 get_initial_guide():
품종, 나이, 성별, 체중, 구조 지역, 보호소 연락처를 말씀해주시면 바로 공고를 작성해드릴게요!
위 4문장 뒤에 재생하거나, FE에서 동일 문구를 TTS로 읽어주면 됩니다.
대안 (프론트가 자체 STT를 쓰는 경우)
녹음 → 자체 전사 후 POST /pipeline/start { "stt_text": "..." } → 이후는 동일하게 /answer → /complete.
[FE TTS 안내 + get_initial_guide()] → [FE 자유 발화 녹음]
→ POST /stt/transcribe-and-start (또는 /pipeline/start)
→ pending_slot=_confirm 이면 요약 확인 TTS → POST /pipeline/answer
→ question 있으면 FE TTS + 녹음 → POST /pipeline/answer (반복)
→ POST /pipeline/complete
→ 공고문 + Faithfulness
POST /notice/generate?platform=naver_cafe (AdoptionNoticeSlots JSON)
uv run uvicorn api.app:app --host 0.0.0.0 --port 8000 --reload- Swagger UI:
http://localhost:8000/docs - OpenAPI JSON:
http://localhost:8000/openapi.json(코드 생성·타입 정의용)
| 메서드 | 경로 | 설명 |
|---|---|---|
GET |
/health |
헬스체크 |
POST |
/stt/transcribe |
음성 → STT 텍스트만 |
POST |
/stt/transcribe-and-start |
음성 → STT → SlotExtractor → 세션 |
POST |
/pipeline/start |
STT 텍스트 → SlotExtractor → 세션 |
POST |
/pipeline/answer |
AskBackEngine으로 답변 반영 → 다음 question |
GET |
/pipeline/status/{session_id} |
세션 상태 조회 |
POST |
/pipeline/complete |
공고문 생성 + Faithfulness |
POST |
/notice/generate |
슬롯만으로 공고문 생성 (세션 불필요) |
POST |
/notice/generate-all |
3개 플랫폼 일괄 생성 |
GET |
/slots/schema |
슬롯 JSON Schema |
GET |
/slots/example |
예시 슬롯 |
프론트는 아래 JSON 필드만 보면 됩니다. 내부적으로 /start·/transcribe-and-start는 SlotExtractor, /answer는 AskBackEngine을 사용합니다.
POST /stt/transcribe-and-start, POST /pipeline/start, POST /pipeline/answer 응답의 핵심:
| 필드 | 타입 | 의미 |
|---|---|---|
session_id |
string | 이후 /answer, /complete에 사용 |
stt_text |
string? | /transcribe-and-start에만 있음 — Whisper 원문 |
corrected_text |
string | 도메인 보정된 텍스트 |
slots |
object | 현재까지 채워진 AdoptionNoticeSlots |
missing_slots |
string[] | 아직 부족한 필수 슬롯 이름 목록 |
pending_slot |
string | null | 다음 처리 대상. "_confirm"이면 요약 확인 단계, 그 외는 재질의 슬롯명 |
confirmation |
string | null | 요약 확인 TTS 문구. _confirm 단계에서는 question과 동일 |
question |
string | null | 다음에 TTS로 읽을 문장 (확인 또는 재질문). null이면 재질문 없음 |
completed |
bool | missing_slots가 비고 pending_slot이 없으면 true |
ready_for_notice |
bool | 공고 생성 가능 여부 (pending 재질문·확인 없을 때) |
ttl_remaining_seconds |
int | 세션 남은 TTL |
question은 다음 중 하나입니다.
- 요약 확인 (
pending_slot="_confirm"):"다음처럼 들었어요. 푸들 수컷 3살, 4kg, 대현동. 맞나요?" - 재질의 (
AskBackEngine.generate_question): 예)"성별을 알려주세요.","중성화 여부를 알려주세요."
POST /stt/transcribe-and-start
Content-Type: multipart/form-data
file: (자유 발화 오디오)
use_prompt: true요약 확인 단계 (일반적 — 슬롯이 하나라도 채워진 경우):
{
"stt_text": "이름은 초코이고 3살 푸들이에요. 중성화 되어 있고 어제 대현동에서 구조했어요.",
"use_prompt": true,
"session_id": "a1b2c3d4-...",
"completed": false,
"pending_slot": "_confirm",
"confirmation": "다음처럼 들었어요. 푸들 3살, 대현동. 맞나요?",
"question": "다음처럼 들었어요. 푸들 3살, 대현동. 맞나요?",
"slots": { "...": "..." },
"corrected_text": "이름은 초코이고 3살 푸들이에요. ...",
"missing_slots": ["sex", "weight_kg", "contact_methods"],
"ready_for_notice": false,
"ttl_remaining_seconds": 3600
}프론트 동작:
confirmation(또는question)을 TTS로 재생- TTS 종료 후 마이크 ON →
"네 맞아요"또는 수정 발화 녹음 /pipeline/answer로 전송- 긍정: 다음
question(누락 슬롯 재질문) 또는ready_for_notice: true - 수정: 원문+수정 발화로 슬롯 재추출 후 누락 슬롯 재질문
- 긍정: 다음
누락 슬롯 재질문 예시 (/pipeline/answer 확인 이후):
{
"session_id": "a1b2c3d4-...",
"completed": false,
"pending_slot": "sex",
"question": "성별을 알려주세요.",
"missing_slots": ["sex", "weight_kg", "contact_methods"],
"ready_for_notice": false,
"slots": { "...": "..." },
"corrected_text": "...",
"ttl_remaining_seconds": 3590
}프론트 동작:
question을 TTS로 재생 (재생 중 마이크 OFF)- TTS 종료 후 마이크 ON → 답변 녹음
- 답변 텍스트(또는 오디오→STT)를
/pipeline/answer로 전송
필수 슬롯이 모두 채워진 경우:
{
"session_id": "a1b2c3d4-...",
"completed": true,
"question": null,
"missing_slots": [],
"ready_for_notice": true,
"slots": { "...": "완성된 슬롯" },
"corrected_text": "...",
"ttl_remaining_seconds": 3600
}→ 재질문 없이 바로 POST /pipeline/complete.
POST /pipeline/answer
Content-Type: application/json
{
"session_id": "a1b2c3d4-...",
"answer": "수컷이에요"
}answer는 사용자가 말한 자연어 문자열입니다. AskBackEngine이 해당 pending_slot 값으로 파싱·병합합니다.
pending_slot="_confirm":"네 맞아요"등 긍정 → 확인 완료 후 누락 슬롯 재질문으로 진행. 부정·수정 발화 → 원문+수정으로 슬롯 재추출.- 그 외 슬롯명: 해당 필드만 갱신.
breed/rescue_region이 빈 문자열이면 내부적으로"미상"placeholder로 정규화한 뒤missing_slots에 남깁니다.
다음 누락이 남아 있을 때:
{
"session_id": "a1b2c3d4-...",
"completed": false,
"question": "체중을 알려주세요.",
"missing_slots": ["weight_kg", "contact_methods"],
"ready_for_notice": false,
"slots": { "sex": "수컷", "...": "..." },
"corrected_text": "... 수컷이에요",
"ttl_remaining_seconds": 3590
}→ 다시 question TTS → 녹음 → /pipeline/answer 반복.
더 이상 누락이 없을 때:
{
"session_id": "a1b2c3d4-...",
"completed": true,
"question": null,
"missing_slots": [],
"ready_for_notice": true,
"slots": { "...": "..." },
"corrected_text": "...",
"ttl_remaining_seconds": 3500
}question == null 이고 ready_for_notice: true 일 때만 호출합니다. (pending 재질문이 있으면 409)
POST /pipeline/complete
Content-Type: application/json
{ "session_id": "a1b2c3d4-..." }{
"session_id": "a1b2c3d4-...",
"slots": { "...": "..." },
"notice": {
"title": "...",
"body": "...",
"info_table": {
"품종": "푸들",
"추정나이": "3년",
"성별": "수컷",
"중성화": "완료",
"체중": "4kg",
"외형": "-",
"건강상태": "-",
"성격/특이사항": "-",
"구조지역": "대현동",
"구조일자": "2026-07-21",
"연락처": "064-710-4805"
},
"platform": "naver_cafe",
"faithfulness": {
"passed": true,
"flagged_sentences": []
}
}
}누락 시 아래 순서의 첫 슬롯만 question으로 반환합니다. 이미 채워진 슬롯은 다시 묻지 않습니다.
breed → sex → estimated_age → weight_kg → is_neutered → rescue_region → rescue_date → contact_methods
HTTP 파이프라인은 필수 슬롯이 모두 채워질 때까지 한 번에 한 항목씩 계속 질문합니다.
ready_for_notice는 missing_slots가 비었을 때만 true이며, 미완성 상태에서
POST /pipeline/complete를 호출하면 409를 반환합니다. 인프로세스 헬퍼의 기본
재질문 제한은 5회(DEFAULT_MAX_ROUNDS)이고 제한 도달 시 미완성 결과로 종료합니다.
아래는 FE 폼 등으로 따로 받는다고 가정합니다. SlotExtractor/AskBack 대상이 아닙니다.
shelter_nameprotection_start_datevaccination_statuscheckup_status
POST /stt/transcribe-and-start
Content-Type: multipart/form-data
file: (음성 파일)
use_prompt: truepending_slot="_confirm"이면 먼저 요약 확인 TTS → /pipeline/answer 1회.
그다음 completed: false 이고 question이 있으면 → TTS로 question 재생 → 답변 녹음 →:
POST /pipeline/answer
Content-Type: application/json
{
"session_id": "uuid-...",
"answer": "중성화 안 했어요"
}question이 null이고 ready_for_notice: true이면 /pipeline/complete로 진행합니다.
POST /pipeline/complete
Content-Type: application/json
{ "session_id": "uuid-..." }GET /pipeline/status/{session_id}| 항목 | 값 |
|---|---|
| 저장소 | 서버 인메모리 (api/session_store.py) |
| TTL | 기본 3600초 (1시간), SESSION_TTL_SECONDS 환경변수로 변경 |
| 만료 시 | 404 반환 — 처음부터 다시 시작 필요 |
| 서버 재시작 | 모든 세션 소멸 |
프론트엔드는 session_id를 로컬에 보관하고, ttl_remaining_seconds를 참고해 만료 전에 완료하세요.
GET /slots/schema 또는 GET /slots/example로 필드 구조를 확인하세요.
필수 필드: breed, estimated_age, sex, is_neutered, weight_kg, rescue_region, rescue_date, contact_methods
| HTTP | 상황 |
|---|---|
400 |
잘못된 음성 파일 형식 / 빈 파일 |
404 |
세션 없음 또는 만료 |
409 |
재질의 진행 중인데 /pipeline/complete 호출 |
422 |
재질의 답변 파싱 실패 / 빈 STT 결과 |
500 |
Vertex AI / STT 내부 오류 |
- 필수: GCP 프로젝트 + 서비스 계정 키 (
gcp_key.json) - GPU 불필요 (클라우드 API 호출)
- 환경변수:
GCP_PROJECT_ID,GOOGLE_APPLICATION_CREDENTIALS - 선택:
GCP_LOCATION(기본us-central1),MODEL_TIER(flash|pro|auto), 역할별 기본은 slot/askback/checker=flash, notice=pro
| 환경 | 디바이스 | compute_type | 권장 |
|---|---|---|---|
| GPU 있음 | cuda |
float16 |
권장 — 실시간에 가까운 속도 |
| GPU 없음 | cpu |
int8 |
동작 가능, 느림 (수십 초~) |
보호소 배경소음 대응 (stt/transcribe.py 기본값):
vad_filter=True— Silero VAD로 비음성 구간 제외condition_on_previous_text=False— 환청 감소hallucination_silence_threshold=0.5,no_speech_threshold=0.6
TTS 재생 중에는 마이크(STT)를 켜지 마세요. FE에서 녹음 타이밍을 제어합니다.
- STT API(
POST /stt/*)를 쓰는 서버에 NVIDIA GPU + CUDA가 있으면 응답 속도가 크게 개선됩니다. - STT를 별도 마이크로서비스로 분리할 경우, GPU 서버에 STT만 배치하고 LLM API 서버는 CPU만으로도 충분합니다.
- STT 모델 로드: GPU VRAM ~3GB 이상 권장 (large-v3)
- LLM: Vertex AI 원격 호출이므로 로컬 GPU 불필요
텍스트 파이프라인:
from llm.pipeline import run_full_pipeline
from time_utils import korea_today
result = run_full_pipeline(
"믹스견 추정 3살 수컷, 5kg, 슬개골 탈구 2기. 어제 대현동에서 구조",
reference_date=korea_today(),
get_answer=lambda slot, q: {"is_neutered": "중성화 안 했어요"}.get(slot, ""),
)로컬 핸즈프리 시뮬레이션 (서비스 API와 별개):
uv run python llm/voice_session.py
uv run python evaluation/e2e_test.py| 팀 | 담당 |
|---|---|
| 프론트엔드 | TTS 안내/재질문 재생, 마이크 녹음, /stt/*·/pipeline/* 호출 순서, 공고문 UI |
| 백엔드 (BFF) | 이 API 프록시, 세션 ID 매핑, TTL 정책 |
| AI (본 레포) | Whisper STT, SlotExtractor, AskBackEngine, 공고 생성, Vertex AI 키 |
OpenAPI 스펙(openapi.json)을 백엔드 팀에 전달하면 클라이언트 SDK 자동 생성이 가능합니다.