coconut / main.py
alohaboy
feat: Add LLM-based chat mode and integrate YJ pipeline
caf53ab
import gradio as gr
import os
import base64
import tempfile
from openai import OpenAI
from typing import List, Tuple, Dict, Any
import json
from datetime import datetime
# Assessment service import
import sys
sys.path.append(os.path.dirname(__file__))
# 간단한 서비스 클래스들 정의
class AssessmentService:
def __init__(self):
self.categories = [
{"name": "기본 인사", "key": "greeting"},
{"name": "감정 표현", "key": "emotion"},
{"name": "일상 대화", "key": "daily"},
{"name": "선호도 표현", "key": "preference"},
{"name": "의사 표현", "key": "opinion"}
]
def get_categories(self):
return self.categories
def generate_questions(self, categories, questions_per_category):
questions = []
for category in categories:
for i in range(questions_per_category):
questions.append({
"category": category,
"question": f"{category} 관련 질문 {i+1}",
"context": f"{category} 상황에서의 질문입니다.",
"level": "L1"
})
return questions
def analyze_response(self, response, question, context):
return {
"is_echo": False,
"anchor": "",
"confidence": 0.0,
"converted": response
}
class ScriptService:
def __init__(self):
self.categories = [
{"name": "일상 루틴", "key": "daily"},
{"name": "놀이 활동", "key": "play"},
{"name": "감정 표현", "key": "emotion"},
{"name": "의사 표현", "key": "opinion"},
{"name": "선호도 표현", "key": "preference"}
]
self.scripts = {
"daily": [
("아침 인사", "아침에 만났을 때의 인사말", [
"좋은 아침이야!",
"오늘 기분은 어때?",
"아침에 뭐 했어?"
]),
("식사 시간", "식사 시간에 하는 대화", [
"맛있게 먹고 있어?",
"이 음식 어때?",
"배불러?"
]),
("외출 준비", "밖에 나가기 전 준비", [
"나갈 준비 다 됐어?",
"뭘 가져가야 할까?",
"신발 신었어?"
]),
("잠자리 준비", "잠자리 준비할 때의 대화", [
"이제 자러 갈 시간이야",
"오늘 하루는 어땠어?",
"좋은 꿈 꿔!"
])
],
"play": [
("장난감 놀이", "장난감으로 놀 때의 대화", [
"어떤 장난감으로 놀고 싶어?",
"이 장난감 재미있어?",
"함께 놀자!"
]),
("그림 그리기", "그림 그릴 때의 대화", [
"뭘 그려볼까?",
"이 색깔 예쁘지?",
"그림 완성했어?"
]),
("책 읽기", "책을 읽을 때의 대화", [
"어떤 책 읽고 싶어?",
"이 이야기 재미있어?",
"다음에 뭐가 일어날까?"
]),
("블록 놀이", "블록으로 놀 때의 대화", [
"뭘 만들고 싶어?",
"이 블록 여기다 놓을까?",
"높은 탑을 만들어보자!"
])
],
"emotion": [
("기쁨 표현", "기쁠 때의 감정 표현", [
"지금 정말 기뻐 보여!",
"왜 이렇게 기뻐?",
"기분이 좋을 때 어떻게 표현해?"
]),
("슬픔 표현", "슬플 때의 감정 표현", [
"왜 슬퍼 보여?",
"슬플 때 어떻게 해?",
"울고 싶어?"
]),
("화남 표현", "화날 때의 감정 표현", [
"왜 화가 났어?",
"화날 때 어떻게 해야 할까?",
"심호흡 해볼까?"
]),
("두려움 표현", "무서울 때의 감정 표현", [
"뭐가 무서워?",
"무서울 때 어떻게 해?",
"엄마/아빠가 있어서 괜찮아"
])
],
"opinion": [
("선택하기", "무엇을 선택할지 결정", [
"이거랑 저거 중에 뭐를 선택할까?",
"어떤 게 더 좋아 보여?",
"네가 정해도 돼"
]),
("평가하기", "무엇에 대한 평가", [
"이거 어때?",
"좋아하는 것 같아?",
"다음에도 하고 싶어?"
]),
("의견 말하기", "자신의 의견 표현", [
"너는 어떻게 생각해?",
"네 의견을 말해줄래?",
"다른 사람과 다른 생각이야?"
]),
("비교하기", "두 가지를 비교", [
"이거랑 저거 중에 어떤 게 더 좋아?",
"차이점이 뭐야?",
"비슷한 점이 있어?"
])
],
"preference": [
("음식 선호", "좋아하는 음식과 싫어하는 음식", [
"좋아하는 음식이 뭐야?",
"이 음식은 어때?",
"싫어하는 음식도 있어?"
]),
("놀이 선호", "좋아하는 놀이와 싫어하는 놀이", [
"가장 좋아하는 놀이가 뭐야?",
"이 놀이는 어때?",
"같이 놀고 싶은 놀이가 있어?"
]),
("색깔 선호", "좋아하는 색깔과 싫어하는 색깔", [
"좋아하는 색깔이 뭐야?",
"이 색깔은 어때?",
"무슨 색깔로 칠할까?"
]),
("활동 선호", "좋아하는 활동과 싫어하는 활동", [
"집에서 뭐 하는 걸 좋아해?",
"밖에서 뭐 하는 걸 좋아해?",
"가장 재미있는 활동이 뭐야?"
])
]
}
def get_categories(self):
return self.categories
def get_scripts_by_category(self, category_key):
scripts = self.scripts.get(category_key, [])
# Gradio CheckboxGroup에서 사용할 수 있는 형태로 변환 (title, description)
return [(script[0], script[1]) for script in scripts]
def create_recording_session(self, selected_scripts):
if not selected_scripts:
return None
session = {
"scripts": selected_scripts,
"current_script_index": 0,
"current_question_index": 0,
"recordings": []
}
return json.dumps(session)
def get_current_script(self, session):
if not session or "scripts" not in session:
return None
current_index = session.get("current_script_index", 0)
if current_index >= len(session["scripts"]):
return None
script_name = session["scripts"][current_index]
return {
"id": f"script_{current_index}", # 스크립트 ID 추가
"title": script_name,
"situation": f"{script_name} 상황",
"difficulty": "L1-L2" # 기본 난이도 추가
}
def get_current_question(self, session):
if not session:
return None, 0, 0
current_script_index = session.get("current_script_index", 0)
current_question_index = session.get("current_question_index", 0)
if current_script_index >= len(session["scripts"]):
return None, 0, 0
script_name = session["scripts"][current_script_index]
# 선택된 스크립트의 실제 질문들을 찾기
questions = None
for category_key, category_scripts in self.scripts.items():
for script in category_scripts:
if script[0] == script_name:
questions = script[2] # 세 번째 요소가 질문 리스트
break
if questions:
break
if not questions:
questions = [
f"{script_name}에 대해 어떻게 생각해?",
f"{script_name}을 좋아해?",
f"{script_name}을 할 때 어떤 기분이야?"
]
if current_question_index >= len(questions):
return None, current_question_index, len(questions)
return questions[current_question_index], current_question_index + 1, len(questions)
def add_recording(self, session, audio_file, question, response_text):
if not session:
return session
recording = {
"question": question,
"response": response_text,
"timestamp": datetime.now().isoformat()
}
if "recordings" not in session:
session["recordings"] = []
session["recordings"].append(recording)
return session
def move_to_next_question(self, session):
if not session:
return session
current_script_index = session.get("current_script_index", 0)
current_question_index = session.get("current_question_index", 0)
if current_script_index >= len(session["scripts"]):
return session
script_name = session["scripts"][current_script_index]
# 현재 스크립트의 질문 개수 찾기
questions = None
for category_key, category_scripts in self.scripts.items():
for script in category_scripts:
if script[0] == script_name:
questions = script[2]
break
if questions:
break
total_questions = len(questions) if questions else 3
session["current_question_index"] = current_question_index + 1
# 현재 스크립트의 모든 질문을 완료했으면 다음 스크립트로
if session["current_question_index"] >= total_questions:
session["current_script_index"] = current_script_index + 1
session["current_question_index"] = 0
return session
def get_script_progress(self, session):
if not session:
return "진행 상황을 불러올 수 없습니다."
current_script_index = session.get("current_script_index", 0)
current_question_index = session.get("current_question_index", 0)
total_scripts = len(session.get("scripts", []))
if current_script_index >= total_scripts:
return f"🎉 모든 대본 완료! (총 {total_scripts}개 대본)"
return f"📊 진행 상황: {current_script_index + 1}/{total_scripts}번째 대본, {current_question_index + 1}/3번째 질문"
def get_session_summary(self, session):
if not session:
return {"total_scripts": 0, "completed_scripts": 0, "total_recordings": 0}
total_scripts = len(session.get("scripts", []))
completed_scripts = session.get("current_script_index", 0)
total_recordings = len(session.get("recordings", []))
return {
"total_scripts": total_scripts,
"completed_scripts": completed_scripts,
"total_recordings": total_recordings
}
class TreatmentService:
def __init__(self):
pass
def generate_treatment_plan(self, session_data, patient_name, patient_age, patient_gender):
return f"""
# 🎯 {patient_name}님의 개인화된 치료 계획
## 📋 기본 정보
- **이름**: {patient_name}
- **나이**: {patient_age}
- **성별**: {patient_gender}
## 🎯 치료 목표
1. 반향어 감소 및 기능적 의사소통 증진
2. 상황별 적절한 언어 사용 능력 향상
3. 자기표현 능력 개발
## 📅 치료 계획
- **주 2-3회**, **회기당 30-40분**
- **기간**: 3-6개월
- **방법**: 구조화된 언어 활동, 놀이 중심 치료
## 🎪 주요 활동
1. 일상 상황 대화 연습
2. 감정 표현 연습
3. 선호도 표현 연습
4. 의사 표현 연습
*이 계획은 개인의 발달 수준과 필요에 따라 조정될 수 있습니다.*
"""
# OpenAI 클라이언트 초기화
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini")
# 서비스 인스턴스 초기화
assessment_service = AssessmentService()
script_service = ScriptService()
treatment_service = TreatmentService()
def load_prompt():
"""프롬프트 파일 로드"""
prompt_path = os.path.join(os.path.dirname(__file__), "prompts", "echo_shape_ko.txt")
try:
with open(prompt_path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
return """당신은 언어치료 전문가입니다.
반향어(echolalia)를 기능적 의사소통 문장으로 변환하세요.
규칙:
- 짧고(≤12자), 구체적이며, 감정/요구를 명확히 표현
- 3개 수준(Level):
L1 = 최소 (간단 요청) → "물 주세요"
L2 = 확장 (구체적 요청) → "차가운 물 주세요"
L3 = 자기표현 (상태/요구 설명) → "지금 쉬고 싶어요"
출력 형식(각 줄 하나씩):
L1: ...
L2: ...
L3: ...
금지:
- 은유/속담/관용어 사용 금지
- 3개 레벨 외 추가 설명 금지"""
def detect_echo(text: str, context: str, situation: str = "기타") -> Tuple[bool, str, float]:
"""반향어 감지 (발화 내용 우선 고려)"""
if not text or not context:
return False, "", 0.0
text = text.strip()
context = context.strip()
# 상황별 반향어 패턴 분석
situation_keywords = {
"식사 시간": ["밥", "먹", "식사", "음식"],
"놀이 시간": ["놀", "게임", "장난감"],
"외출 준비": ["나가", "외출", "산책"],
"수업 시간": ["공부", "책", "학습"],
"휴식 시간": ["쉬", "잠", "휴식"]
}
# 1. 완전 일치 (높은 신뢰도) - 질문을 그대로 반복
if text == context:
return True, context, 0.9
# 2. 단어 반복 패턴 감지 (아이스크림, 아이스크림, 아이스크림)
words = text.split()
if len(words) >= 2:
# 같은 단어가 2번 이상 반복되는 경우
word_counts = {}
for word in words:
word_counts[word] = word_counts.get(word, 0) + 1
# 가장 많이 반복된 단어 찾기
most_repeated = max(word_counts.items(), key=lambda x: x[1])
if most_repeated[1] >= 2: # 2번 이상 반복
repeated_word = most_repeated[0]
# 반복된 단어가 질문에 없으면 반향어로 판단
if repeated_word not in context:
return True, repeated_word, 0.8
# 3. 상황과 맞지 않는 말 (반향어 가능성 높음)
if situation != "기타" and situation in situation_keywords:
situation_words = situation_keywords[situation]
text_has_situation_words = any(word in text for word in situation_words)
context_has_situation_words = any(word in context for word in situation_words)
# 상황과 맞지 않는 말이면 반향어 가능성 높음
if not text_has_situation_words and context_has_situation_words:
return True, context, 0.7
# 4. 부분 일치 (중간 신뢰도)
if context.split() and text.endswith(context.split()[-1]):
return True, context, 0.6
# 5. 문장 구조가 비슷하지만 내용이 다름 (낮은 신뢰도)
if len(text.split()) == len(context.split()) and text != context:
return True, context, 0.4
return False, "", 0.1
def shape_echolalia(anchor_text: str, level: str = "L1", situation: str = "기타") -> List[str]:
"""반향어를 기능적 의사소통으로 전환 (발화 내용 우선 고려)"""
if not anchor_text:
return ["물 주세요", "차가운 물 주세요", "지금 쉬고 싶어요"]
try:
prompt = load_prompt()
# 상황 정보를 포함한 더 구체적인 프롬프트
situation_context = ""
if situation != "기타":
situation_context = f"\n상황: {situation}"
user_input = f"""입력: {anchor_text}{situation_context}
레벨: {level}
중요: 아이가 말한 단어나 내용을 우선 고려하여 변환하세요.
예: "아이스크림"을 반복했다면 "아이스크림"과 관련된 요청으로 변환하세요."""
response = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "system", "content": prompt},
{"role": "user", "content": user_input}
],
max_tokens=200,
temperature=0.7
)
# 응답 파싱
content = response.choices[0].message.content
lines = content.split('\n')
candidates = []
for line in lines:
if ':' in line:
candidate = line.split(':', 1)[1].strip()
if candidate:
candidates.append(candidate)
return candidates[:3] if candidates else ["물 주세요", "차가운 물 주세요", "지금 쉬고 싶어요"]
except Exception as e:
print(f"❌ 오류: {e}")
return ["물 주세요", "차가운 물 주세요", "지금 쉬고 싶어요"]
def transcribe_audio(audio_file) -> str:
"""음성을 텍스트로 변환"""
if audio_file is None:
return ""
try:
with open(audio_file, "rb") as f:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=f,
language="ko"
)
return transcript.text
except Exception as e:
print(f"❌ 음성 인식 오류: {e}")
return ""
def synthesize_speech(text: str, voice: str = "alloy") -> str:
"""텍스트를 음성으로 변환"""
if not text:
return None
try:
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
# 임시 파일로 저장
with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file:
tmp_file.write(response.content)
return tmp_file.name
except Exception as e:
print(f"❌ 음성 합성 오류: {e}")
return None
def process_text(text: str, context: str, situation: str = "기타") -> Tuple[bool, str, float, List[str]]:
"""텍스트 처리: 반향어 감지 및 전환"""
is_echo, anchor, confidence = detect_echo(text, context, situation)
if is_echo:
candidates = shape_echolalia(anchor, "L1", situation)
else:
candidates = []
# 신뢰도를 퍼센트로 변환
confidence_percent = confidence * 100
return is_echo, anchor, confidence_percent, candidates
def process_voice(audio_file) -> Tuple[str, bool, str, float, List[str], str]:
"""음성 처리: 음성 인식 → 반향어 감지 → 전환 → 음성 합성"""
if audio_file is None:
return "", False, "", 0.0, [], None
# 1. 음성 인식
transcribed_text = transcribe_audio(audio_file)
if not transcribed_text:
return "", False, "", 0.0, [], None
# 2. 반향어 감지 (간단한 컨텍스트로)
is_echo, anchor, confidence = detect_echo(transcribed_text, transcribed_text)
# 3. 기능적 의사소통으로 전환
if is_echo:
candidates = shape_echolalia(anchor)
# 첫 번째 후보를 음성으로 합성
if candidates:
audio_response = synthesize_speech(candidates[0])
else:
audio_response = None
else:
candidates = []
audio_response = None
return transcribed_text, is_echo, anchor, confidence, candidates, audio_response
# 구조화된 검사 관련 함수들
def get_assessment_categories():
"""검사 카테고리 목록 반환"""
return assessment_service.get_categories()
def create_new_assessment(selected_categories, questions_per_category):
"""새로운 검사 세션 생성"""
if not selected_categories:
selected_categories = None
return assessment_service.create_assessment_session(selected_categories, questions_per_category)
def get_current_question(session_data):
"""현재 질문 정보 반환"""
if not session_data:
return None, None, None, None, None, None, None, None
session = json.loads(session_data) if isinstance(session_data, str) else session_data
current_question = assessment_service.get_next_question(session)
if not current_question:
return None, None, None, None, None, None, None, None
return (
current_question["question"],
current_question["context"],
current_question["difficulty"],
current_question["category_key"],
current_question["id"],
session["current_question_index"] + 1,
session["total_questions"],
session["completed_questions"]
)
def process_assessment_response(session_data, response_text, response_audio):
"""검사 응답 처리"""
if not session_data:
return None, None, None, None, None, None, None, None, None, None
session = json.loads(session_data) if isinstance(session_data, str) else session_data
current_question = assessment_service.get_next_question(session)
if not current_question:
return session_data, None, None, None, None, None, None, None, None, None
# 음성 응답이 있으면 텍스트로 변환
if response_audio and not response_text:
response_text = transcribe_audio(response_audio)
if not response_text:
return session_data, None, None, None, None, None, None, None, None, None
# 반향어 감지 및 변환
is_echo, anchor, confidence = detect_echo(response_text, current_question["question"], current_question["context"])
converted_responses = shape_echolalia(anchor, "L1", current_question["context"]) if is_echo else []
# 세션 업데이트
question_index = session["current_question_index"]
session = assessment_service.update_question_response(
session, question_index, response_text, is_echo, confidence, converted_responses
)
# 다음 질문으로 이동
session = assessment_service.move_to_next_question(session)
# 다음 질문 정보 가져오기
next_question = assessment_service.get_next_question(session)
if next_question:
return (
json.dumps(session),
next_question["question"],
next_question["context"],
next_question["difficulty"],
next_question["category_key"],
next_question["id"],
session["current_question_index"] + 1,
session["total_questions"],
session["completed_questions"],
"" # 응답 입력란 초기화
)
else:
# 검사 완료
summary = assessment_service.get_session_summary(session)
return (
json.dumps(session),
f"🎉 검사 완료! 총 {summary['total_questions']}개 질문 중 {summary['completed_questions']}개 완료",
f"반향어 감지율: {summary['echo_detection_rate']:.1f}%",
"완료",
"완료",
"완료",
summary["total_questions"],
summary["total_questions"],
summary["completed_questions"],
f"✅ 검사가 완료되었습니다!\n\n📊 검사 결과 요약 버튼을 클릭하여 상세한 결과를 확인하세요.\n\n반향어 감지율: {summary['echo_detection_rate']:.1f}%"
)
def get_assessment_summary(session_data):
"""검사 결과 요약 반환"""
if not session_data:
return "검사 데이터가 없습니다."
session = json.loads(session_data) if isinstance(session_data, str) else session_data
summary = assessment_service.get_session_summary(session)
result_text = f"""
## 📊 검사 결과 요약
**세션 ID**: {summary['session_id']}
**총 질문 수**: {summary['total_questions']}
**완료된 질문**: {summary['completed_questions']}
**완료율**: {summary['completion_rate']:.1f}%
**반향어 감지**: {summary['echo_detected_count']}
**반향어 감지율**: {summary['echo_detection_rate']:.1f}%
### 📈 카테고리별 결과
"""
for category, stats in summary['category_stats'].items():
category_name = assessment_service.questions_data['assessment_categories'][category]['name']
result_text += f"""
**{category_name}**
- 총 질문: {stats['total']}
- 반향어 감지: {stats['echo_detected']}
- 평균 신뢰도: {stats['avg_confidence']:.1f}%
"""
return result_text
# 대본 기반 녹음 관련 함수들
def get_script_categories():
"""대본 카테고리 목록 반환"""
return script_service.get_categories()
def get_scripts_by_category(category_key):
"""특정 카테고리의 대본 목록 반환"""
return script_service.get_scripts_by_category(category_key)
def create_script_session(selected_scripts):
"""새로운 대본 녹음 세션 생성"""
if not selected_scripts:
return None
return script_service.create_recording_session(selected_scripts)
def get_current_script_info(session_data):
"""현재 대본 정보 반환"""
if not session_data:
return None, None, None, None, None, None, None, None
session = json.loads(session_data) if isinstance(session_data, str) else session_data
current_script = script_service.get_current_script(session)
if not current_script:
return None, None, None, None, None, None, None, None
question, question_num, total_questions = script_service.get_current_question(session)
progress_text = script_service.get_script_progress(session)
return (
current_script["title"],
current_script["situation"],
question,
current_script["difficulty"],
question_num,
total_questions,
progress_text,
current_script["id"]
)
def process_script_recording(session_data, audio_file):
"""대본 녹음 처리"""
if not session_data or not audio_file:
return session_data, None, None, None, None, None, None, None, None, None
session = json.loads(session_data) if isinstance(session_data, str) else session_data
current_script = script_service.get_current_script(session)
if not current_script:
return session_data, None, None, None, None, None, None, None, None, None
# 현재 질문 가져오기
question, question_num, total_questions = script_service.get_current_question(session)
# 음성 인식
response_text = transcribe_audio(audio_file)
# 녹음 추가
session = script_service.add_recording(session, audio_file, question, response_text)
# 반향어 감지 및 변환
is_echo, anchor, confidence = detect_echo(response_text, question, current_script["situation"])
converted_responses = shape_echolalia(anchor, "L1", current_script["situation"]) if is_echo else []
# 다음 질문으로 이동
session = script_service.move_to_next_question(session)
# 다음 질문 정보 가져오기
next_question, next_question_num, next_total_questions = script_service.get_current_question(session)
next_progress = script_service.get_script_progress(session)
if next_question:
return (
json.dumps(session),
current_script["title"],
current_script["situation"],
next_question,
current_script["difficulty"],
next_question_num,
next_total_questions,
next_progress,
response_text
)
else:
# 녹음 완료
summary = script_service.get_session_summary(session)
return (
json.dumps(session),
f"🎉 녹음 완료! 총 {summary['total_scripts']}개 대본 중 {summary['completed_scripts']}개 완료",
f"총 {summary['total_recordings']}개 녹음 완료",
"모든 녹음이 완료되었습니다.",
"완료",
summary["total_scripts"],
summary["total_scripts"],
"완료",
f"✅ 녹음이 완료되었습니다!\n\n📊 녹음 결과 요약 버튼을 클릭하여 상세한 결과를 확인하세요.\n\n총 녹음 수: {summary['total_recordings']}개"
)
def get_script_summary(session_data):
"""대본 녹음 결과 요약 반환"""
if not session_data:
return "녹음 데이터가 없습니다."
session = json.loads(session_data) if isinstance(session_data, str) else session_data
summary = script_service.get_session_summary(session)
result_text = f"""
## 🎬 대본 녹음 결과 요약
**세션 ID**: {summary['session_id']}
**총 대본 수**: {summary['total_scripts']}
**완료된 대본**: {summary['completed_scripts']}
**완료율**: {summary['completion_rate']:.1f}%
**총 녹음 수**: {summary['total_recordings']}
### 📈 카테고리별 결과
"""
for category, stats in summary['category_stats'].items():
result_text += f"""
**{category}**
- 총 질문: {stats['total_questions']}
- 녹음 완료: {stats['recordings']}
- 완료율: {stats['completion_rate']:.1f}%
"""
return result_text
# 치료 계획 생성 관련 함수들
def generate_treatment_plan(session_data, patient_name, patient_age, patient_gender):
"""치료 계획 생성"""
if not session_data:
return "검사 데이터가 없습니다."
session = json.loads(session_data) if isinstance(session_data, str) else session_data
summary = assessment_service.get_session_summary(session)
# 환자 정보 구성
patient_info = {
'name': patient_name,
'age': patient_age,
'gender': patient_gender,
'assessment_date': summary['created_at']
}
# 치료 계획 생성
treatment_plan = treatment_service.generate_treatment_plan(summary, patient_info)
return treatment_plan
def format_treatment_plan(treatment_plan):
"""치료 계획을 사용자 친화적 형식으로 변환"""
if isinstance(treatment_plan, str):
return treatment_plan
plan_text = f"""
# 🎯 개별화된 치료 계획
**아이 정보**
- 이름: {treatment_plan['patient_info'].get('name', 'N/A')}
- 나이: {treatment_plan['patient_info'].get('age', 'N/A')}
- 성별: {treatment_plan['patient_info'].get('gender', 'N/A')}
- 계획 생성일: {treatment_plan['created_date'][:10]}
## 📊 반향어 패턴 분석
- **전체 반향어 비율**: {treatment_plan['pattern_analysis']['echo_rate']:.1f}%
- **심각도**: {treatment_plan['pattern_analysis']['overall_severity']}
- **총 질문 수**: {treatment_plan['pattern_analysis']['total_questions']}
- **반향어 감지**: {treatment_plan['pattern_analysis']['echo_detected']}
## 🎯 치료 목표
### 단기 목표 (1-3개월)
{treatment_plan['goals'][0]['description']}
"""
for i, target in enumerate(treatment_plan['goals'][0]['targets'], 1):
plan_text += f"{i}. {target}\n"
plan_text += f"""
### 중기 목표 (3-6개월)
{treatment_plan['goals'][1]['description']}
"""
for i, target in enumerate(treatment_plan['goals'][1]['targets'], 1):
plan_text += f"{i}. {target}\n"
plan_text += f"""
### 장기 목표 (6개월 이상)
{treatment_plan['goals'][2]['description']}
"""
for i, target in enumerate(treatment_plan['goals'][2]['targets'], 1):
plan_text += f"{i}. {target}\n"
plan_text += """
## 🛠️ 치료 전략
### 즉시 반향어 대응
"""
for strategy in treatment_plan['strategies']:
if strategy['type'] == 'immediate_echolalia':
plan_text += f"**{strategy['name']}** ({strategy['severity_level']})\n"
plan_text += f"{strategy['description']}\n\n"
plan_text += "**치료 방법:**\n"
for i, method in enumerate(strategy['strategies'], 1):
plan_text += f"{i}. {method}\n"
plan_text += "\n**가정 활동:**\n"
for i, activity in enumerate(strategy['home_activities'], 1):
plan_text += f"{i}. {activity}\n"
break
plan_text += """
## 🏠 가정 활동 계획
"""
for activity in treatment_plan['home_activities']:
plan_text += f"**{activity['frequency']} ({activity['duration']})**\n"
for i, act in enumerate(activity['activities'], 1):
plan_text += f"{i}. {act}\n"
plan_text += "\n"
plan_text += """
## 📅 치료 일정
"""
for timeline in treatment_plan['timeline']:
plan_text += f"**{timeline['week']}주차 ({timeline['date']})**\n"
plan_text += f"집중 영역: {timeline['focus']}\n"
plan_text += "활동:\n"
for i, activity in enumerate(timeline['activities'], 1):
plan_text += f"{i}. {activity}\n"
plan_text += "\n"
plan_text += f"""
## 📈 진행 상황 측정 지표
### 의사소통 기술
"""
for indicator in treatment_plan['progress_indicators']['communication_skills']:
plan_text += f"- {indicator}\n"
plan_text += "\n### 사회적 기술\n"
for indicator in treatment_plan['progress_indicators']['social_skills']:
plan_text += f"- {indicator}\n"
plan_text += "\n### 일상생활 기술\n"
for indicator in treatment_plan['progress_indicators']['daily_living']:
plan_text += f"- {indicator}\n"
plan_text += f"""
## 📋 다음 검토 일정
{treatment_plan['next_review_date'][:10]}
---
*이 치료 계획은 검사 결과를 바탕으로 개별화되었습니다. 치료 과정에서 지속적으로 평가하고 조정할 필요가 있습니다.*
"""
return plan_text
# Gradio 인터페이스 생성
def create_interface():
# 커스텀 테마 설정
custom_theme = gr.themes.Soft(
primary_hue="purple",
secondary_hue="blue",
neutral_hue="gray",
font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
)
with gr.Blocks(
title="Coconut AI",
theme=custom_theme,
css="""
.gradio-container {
max-width: 1200px !important;
margin: 0 auto !important;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%) !important;
min-height: 100vh !important;
padding: 20px !important;
}
.main-header {
text-align: center !important;
margin-bottom: 2rem !important;
padding: 2.5rem 0 !important;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border-radius: 20px !important;
box-shadow: 0 15px 35px rgba(102, 126, 234, 0.3) !important;
}
.main-header h1 {
font-size: 2.8rem;
font-weight: 600;
margin: 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.2);
letter-spacing: -0.5px;
}
.main-header p {
font-size: 1.1rem;
margin: 0.8rem 0 0 0;
opacity: 0.9;
font-weight: 300;
}
.tab-nav {
background: rgba(15, 23, 42, 0.95) !important;
border-radius: 15px !important;
box-shadow: 0 8px 32px rgba(0,0,0,0.3) !important;
margin-bottom: 1.5rem !important;
border: 2px solid rgba(102, 126, 234, 0.3) !important;
}
.tab-nav .gr-tab {
color: #e2e8f0 !important;
background: transparent !important;
border: none !important;
padding: 12px 24px !important;
font-weight: 600 !important;
font-size: 16px !important;
transition: all 0.3s ease !important;
}
.tab-nav .gr-tab:hover {
color: #f1f5f9 !important;
background: rgba(102, 126, 234, 0.1) !important;
}
.tab-nav .gr-tab.selected {
color: #ffffff !important;
background: rgba(102, 126, 234, 0.2) !important;
border-bottom: 3px solid #667eea !important;
}
.tab-nav .gr-tab .gr-label {
color: inherit !important;
}
.soft-input input, .soft-input textarea {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
color: #f1f5f9 !important;
border-radius: 12px !important;
padding: 16px !important;
font-size: 16px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
transition: all 0.3s ease !important;
min-height: 120px !important;
}
.soft-input input:focus, .soft-input textarea:focus {
border-color: #667eea !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
outline: none !important;
}
.soft-input .gr-label {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 16px !important;
margin-bottom: 8px !important;
white-space: nowrap !important;
overflow: visible !important;
background: rgba(102, 126, 234, 0.2) !important;
padding: 8px 12px !important;
border-radius: 8px !important;
display: inline-block !important;
}
.soft-input input::placeholder, .soft-input textarea::placeholder {
color: #94a3b8 !important;
font-style: italic !important;
}
.soft-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: white !important;
border: none !important;
border-radius: 12px !important;
padding: 16px 24px !important;
font-size: 16px !important;
font-weight: 600 !important;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3) !important;
transition: all 0.3s ease !important;
margin: 16px 0 !important;
min-height: 60px !important;
}
.soft-button:hover {
transform: translateY(-2px) !important;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4) !important;
}
.soft-checkbox {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
border-radius: 8px !important;
padding: 16px !important;
margin: 8px 0 !important;
min-height: 60px !important;
}
.soft-checkbox .gr-label {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 16px !important;
margin-bottom: 8px !important;
white-space: nowrap !important;
overflow: visible !important;
}
.soft-checkbox .gr-checkbox {
color: #e2e8f0 !important;
}
.soft-checkbox .gr-checkbox input[type="checkbox"] {
accent-color: #667eea !important;
}
.soft-slider {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
border-radius: 12px !important;
padding: 16px !important;
margin: 12px 0 !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
}
.soft-slider .gr-slider {
background: linear-gradient(90deg, #ff6b6b 0%, #ffd93d 50%, #6bcf7f 100%) !important;
border-radius: 8px !important;
height: 12px !important;
margin: 8px 0 !important;
}
.soft-slider .gr-slider-track {
background: linear-gradient(90deg, #ff6b6b 0%, #ffd93d 50%, #6bcf7f 100%) !important;
border-radius: 8px !important;
height: 12px !important;
}
.soft-slider .gr-slider-thumb {
background: white !important;
border: 3px solid #667eea !important;
border-radius: 50% !important;
width: 24px !important;
height: 24px !important;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important;
}
.soft-slider .gr-label {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 16px !important;
margin-bottom: 8px !important;
background: rgba(102, 126, 234, 0.2) !important;
padding: 8px 12px !important;
border-radius: 8px !important;
display: inline-block !important;
}
.soft-slider .gr-slider-value {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 14px !important;
margin-top: 8px !important;
background: rgba(102, 126, 234, 0.2) !important;
padding: 4px 8px !important;
border-radius: 6px !important;
display: inline-block !important;
}
.soft-output textarea, .soft-output input {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
color: #f1f5f9 !important;
border-radius: 12px !important;
padding: 16px !important;
font-size: 16px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
min-height: 120px !important;
}
.soft-output .gr-label {
color: #92400e !important;
font-weight: 600 !important;
font-size: 16px !important;
margin-bottom: 8px !important;
white-space: nowrap !important;
overflow: visible !important;
background: rgba(102, 126, 234, 0.2) !important;
padding: 8px 12px !important;
border-radius: 8px !important;
display: inline-block !important;
}
.soft-dropdown select {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
border-radius: 12px !important;
padding: 16px !important;
margin: 8px 0 !important;
font-size: 16px !important;
color: #f1f5f9 !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
transition: all 0.3s ease !important;
}
.soft-dropdown select:focus {
border-color: #667eea !important;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1) !important;
outline: none !important;
}
.soft-dropdown .gr-label {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 16px !important;
margin-bottom: 8px !important;
white-space: nowrap !important;
overflow: visible !important;
background: rgba(102, 126, 234, 0.2) !important;
padding: 8px 12px !important;
border-radius: 8px !important;
display: inline-block !important;
}
.soft-accordion {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
border-radius: 12px !important;
padding: 20px !important;
margin: 12px 0 !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
}
.soft-accordion .gr-label {
color: #e2e8f0 !important;
font-weight: 600 !important;
font-size: 18px !important;
margin-bottom: 12px !important;
}
/* 모든 텍스트 색상 통일 */
.gr-label, .gr-text, .gr-markdown {
color: #e2e8f0 !important;
}
/* 기본 Gradio 컴포넌트 오버라이드 */
.gr-form, .gr-box, .gr-panel {
background: rgba(15, 23, 42, 0.95) !important;
border: 2px solid rgba(102, 126, 234, 0.4) !important;
border-radius: 12px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.4) !important;
}
/* 슬라이더 썸 색상 수정 */
.soft-slider .gr-slider-thumb {
background: white !important;
border: 3px solid #667eea !important;
border-radius: 50% !important;
width: 24px !important;
height: 24px !important;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3) !important;
}
/* 플레이스홀더 색상 수정 */
.soft-input input::placeholder, .soft-input textarea::placeholder {
color: #94a3b8 !important;
font-style: italic !important;
}
"""
) as demo:
# 메인 헤더
with gr.Row(elem_classes=["main-header"]):
gr.HTML("""
<h1>🗣️ COCONUT AI</h1>
<p>반향어를 감지, 검사하고 기능적 의사소통으로 전환하는 종합 AI 어시스턴트입니다.</p>
""")
# 탭 네비게이션
with gr.Tabs(elem_classes=["tab-nav"]):
# 텍스트 처리 탭
with gr.Tab("📝 텍스트 처리"):
text_input = gr.Textbox(
label="입력 텍스트",
placeholder="아이가 말한 발화를 적어주세요",
lines=6,
elem_classes=["soft-input"]
)
context_input = gr.Textbox(
label="컨텍스트 (선택사항)",
placeholder="어떤 상황에서 말을 했는지 적어주세요",
lines=8,
elem_classes=["soft-input"]
)
situation_input = gr.Dropdown(
label="상황 (선택사항)",
choices=["기타", "식사 시간", "놀이 시간", "외출 준비", "수업 시간", "휴식 시간"],
value="기타",
elem_classes=["soft-dropdown"]
)
process_btn = gr.Button("🔍 분석하기", variant="primary", elem_classes=["soft-button"])
is_echo_output = gr.Checkbox(
label="반향어 감지",
interactive=False,
elem_classes=["soft-checkbox"]
)
confidence_output = gr.Slider(
label="신뢰도",
minimum=0,
maximum=100,
step=5,
interactive=False,
elem_classes=["soft-slider"]
)
anchor_output = gr.Textbox(
label="감지된 앵커 텍스트",
interactive=False,
lines=2,
elem_classes=["soft-output"]
)
candidates_output = gr.Textbox(
label="📊 3단계 변환 결과 (L1: 기본 → L2: 확장 → L3: 자기표현)",
info="반향어를 기능적 의사소통으로 전환한 결과입니다. L1(간단한 응답) → L2(세부사항 포함) → L3(감정/경험 포함) 순으로 복잡도가 증가합니다.",
interactive=False,
lines=6,
elem_classes=["soft-output"]
)
# 이벤트 연결
process_btn.click(
fn=process_text,
inputs=[text_input, context_input, situation_input],
outputs=[is_echo_output, anchor_output, confidence_output, candidates_output]
)
# 음성 처리 탭
with gr.Tab("🎤 음성 처리"):
voice_input = gr.Audio(
label="음성 입력",
type="filepath",
elem_classes=["soft-input"]
)
voice_process_btn = gr.Button("🎵 음성 분석하기", variant="primary", elem_classes=["soft-button"])
transcribed_output = gr.Textbox(
label="음성 인식 결과",
interactive=False,
lines=4,
elem_classes=["soft-output"]
)
voice_is_echo_output = gr.Checkbox(
label="반향어 감지",
interactive=False,
elem_classes=["soft-checkbox"]
)
voice_confidence_output = gr.Slider(
label="신뢰도",
minimum=0,
maximum=100,
step=5,
interactive=False,
elem_classes=["soft-slider"]
)
voice_anchor_output = gr.Textbox(
label="감지된 앵커 텍스트",
interactive=False,
lines=2,
elem_classes=["soft-output"]
)
voice_candidates_output = gr.Textbox(
label="📊 3단계 변환 결과 (L1: 기본 → L2: 확장 → L3: 자기표현)",
info="반향어를 기능적 의사소통으로 전환한 결과입니다. L1(간단한 응답) → L2(세부사항 포함) → L3(감정/경험 포함) 순으로 복잡도가 증가합니다.",
interactive=False,
lines=6,
elem_classes=["soft-output"]
)
voice_audio_output = gr.Audio(
label="음성 합성 결과",
interactive=False,
elem_classes=["soft-output"]
)
# 이벤트 연결
voice_process_btn.click(
fn=process_voice,
inputs=[voice_input],
outputs=[transcribed_output, voice_is_echo_output, voice_anchor_output, voice_confidence_output, voice_candidates_output, voice_audio_output]
)
# 반향어 검사 탭
with gr.Tab("🔬 반향어 검사"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🎯 검사 설정")
# 검사 카테고리 선택
category_checkboxes = gr.CheckboxGroup(
label="검사 카테고리 선택",
choices=[(cat["name"], cat["key"]) for cat in get_assessment_categories()],
value=[cat["key"] for cat in get_assessment_categories()],
elem_classes=["soft-checkbox"]
)
# 카테고리별 질문 수 설정
questions_per_category = gr.Slider(
label="카테고리당 질문 수",
minimum=1,
maximum=5,
step=1,
value=3,
elem_classes=["soft-slider"]
)
# 새 검사 시작 버튼
start_assessment_btn = gr.Button(
"🚀 새 검사 시작",
variant="primary",
elem_classes=["soft-button"]
)
# 검사 진행 상태
gr.Markdown("### 📊 검사 진행 상태")
progress_info = gr.Textbox(
label="진행 상황",
value="검사를 시작해주세요",
interactive=False,
lines=3,
elem_classes=["soft-output"]
)
with gr.Column(scale=2):
gr.Markdown("### ❓ 현재 질문")
# 질문 정보 표시
question_text = gr.Textbox(
label="질문",
interactive=False,
lines=3,
elem_classes=["soft-output"]
)
question_context = gr.Textbox(
label="상황/맥락",
interactive=False,
lines=2,
elem_classes=["soft-output"]
)
question_meta = gr.Textbox(
label="질문 정보 (L1: 기본 | L2: 확장 | L3: 자기표현)",
interactive=False,
lines=1,
elem_classes=["soft-output"]
)
# 응답 입력
gr.Markdown("### 💬 응답 입력")
response_text = gr.Textbox(
label="텍스트 응답",
placeholder="아이의 응답을 텍스트로 입력하거나 음성으로 녹음하세요",
lines=3,
elem_classes=["soft-input"]
)
response_audio = gr.Audio(
label="음성 응답",
type="filepath",
elem_classes=["soft-input"]
)
# 응답 처리 버튼
process_response_btn = gr.Button(
"📝 응답 처리",
variant="primary",
elem_classes=["soft-button"]
)
# 반향어 감지 결과
gr.Markdown("### 🔍 분석 결과")
echo_detected = gr.Checkbox(
label="반향어 감지",
interactive=False,
elem_classes=["soft-checkbox"]
)
confidence_score = gr.Slider(
label="신뢰도",
minimum=0,
maximum=100,
step=5,
interactive=False,
elem_classes=["soft-slider"]
)
converted_responses = gr.Textbox(
label="변환된 응답 (L1 → L2 → L3)",
interactive=False,
lines=4,
elem_classes=["soft-output"]
)
# 검사 결과 요약
with gr.Row():
summary_btn = gr.Button(
"📊 검사 결과 요약",
variant="primary",
elem_classes=["soft-button"]
)
summary_output = gr.Markdown(
value="검사를 완료한 후 결과 요약을 확인하세요.",
elem_classes=["soft-output"]
)
# 치료 계획 생성
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 👤 아이 정보 입력")
patient_name = gr.Textbox(
label="아이 이름",
placeholder="아이 이름을 입력하세요",
elem_classes=["soft-input"]
)
patient_age = gr.Number(
label="나이",
value=5,
minimum=1,
maximum=18,
elem_classes=["soft-input"]
)
patient_gender = gr.Dropdown(
label="성별",
choices=["남성", "여성"],
value="남성",
elem_classes=["soft-dropdown"]
)
treatment_plan_btn = gr.Button(
"🎯 치료 계획 생성",
variant="primary",
elem_classes=["soft-button"]
)
with gr.Column(scale=2):
treatment_plan_output = gr.Markdown(
value="검사 완료 후 아이 정보를 입력하고 치료 계획을 생성하세요.",
elem_classes=["soft-output"]
)
# 세션 데이터 저장 (숨김)
session_data = gr.State(value=None)
# 이벤트 연결
start_assessment_btn.click(
fn=create_new_assessment,
inputs=[category_checkboxes, questions_per_category],
outputs=[session_data]
).then(
fn=get_current_question,
inputs=[session_data],
outputs=[question_text, question_context, question_meta, gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False)]
).then(
fn=lambda session: f"검사 시작! 총 {session['total_questions']}개 질문이 준비되었습니다." if session else "검사를 시작해주세요",
inputs=[session_data],
outputs=[progress_info]
)
process_response_btn.click(
fn=process_assessment_response,
inputs=[session_data, response_text, response_audio],
outputs=[session_data, question_text, question_context, question_meta, gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), response_text]
).then(
fn=lambda session: f"진행률: {json.loads(session)['completed_questions']}/{json.loads(session)['total_questions']} ({(json.loads(session)['completed_questions']/json.loads(session)['total_questions']*100):.1f}%)" if session else "검사를 시작해주세요",
inputs=[session_data],
outputs=[progress_info]
).then(
fn=lambda session: "🎉 검사가 완료되었습니다! 아래 '검사 결과 요약' 버튼을 클릭하여 상세한 결과를 확인하세요." if session and json.loads(session)['completed_questions'] >= json.loads(session)['total_questions'] else None,
inputs=[session_data],
outputs=[gr.Textbox(visible=False)]
)
summary_btn.click(
fn=get_assessment_summary,
inputs=[session_data],
outputs=[summary_output]
)
def generate_and_format_treatment_plan(session_data, patient_name, patient_age, patient_gender):
"""치료 계획 생성 및 포맷팅을 한 번에 처리"""
treatment_plan = generate_treatment_plan(session_data, patient_name, patient_age, patient_gender)
return format_treatment_plan(treatment_plan)
treatment_plan_btn.click(
fn=generate_and_format_treatment_plan,
inputs=[session_data, patient_name, patient_age, patient_gender],
outputs=[treatment_plan_output]
)
# 대본 기반 녹음 탭
with gr.Tab("🎬 대본 기반 녹음"):
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 🎭 대본 선택")
# 대본 카테고리 선택
script_category_dropdown = gr.Dropdown(
label="대본 카테고리",
choices=[(cat["name"], cat["key"]) for cat in get_script_categories()],
value=get_script_categories()[0]["key"] if get_script_categories() else None,
elem_classes=["soft-dropdown"]
)
# 대본 목록
script_checkboxes = gr.CheckboxGroup(
label="대본 선택",
choices=get_scripts_by_category(get_script_categories()[0]["key"]) if get_script_categories() else [],
elem_classes=["soft-checkbox"]
)
# 대본 시작 버튼
start_script_btn = gr.Button(
"🎬 녹음 시작",
variant="primary",
elem_classes=["soft-button"]
)
# 녹음 진행 상태
gr.Markdown("### 📊 녹음 진행 상태")
script_progress_info = gr.Textbox(
label="진행 상황",
value="대본을 선택하고 녹음을 시작해주세요",
interactive=False,
lines=3,
elem_classes=["soft-output"]
)
with gr.Column(scale=2):
gr.Markdown("### 📝 현재 대본")
# 대본 정보 표시
script_title = gr.Textbox(
label="대본 제목",
interactive=False,
lines=1,
elem_classes=["soft-output"]
)
script_situation = gr.Textbox(
label="상황",
interactive=False,
lines=1,
elem_classes=["soft-output"]
)
script_question = gr.Textbox(
label="읽어줄 질문",
interactive=False,
lines=3,
elem_classes=["soft-output"]
)
script_meta = gr.Textbox(
label="대본 정보",
interactive=False,
lines=1,
elem_classes=["soft-output"]
)
# 녹음 섹션
gr.Markdown("### 🎤 아이 응답 녹음")
script_audio_input = gr.Audio(
label="아이의 응답 녹음",
type="filepath",
elem_classes=["soft-input"]
)
# 녹음 처리 버튼
process_script_btn = gr.Button(
"📝 녹음 처리",
variant="primary",
elem_classes=["soft-button"]
)
# 반향어 감지 결과
gr.Markdown("### 🔍 분석 결과")
script_echo_detected = gr.Checkbox(
label="반향어 감지",
interactive=False,
elem_classes=["soft-checkbox"]
)
script_confidence_score = gr.Slider(
label="신뢰도",
minimum=0,
maximum=100,
step=5,
interactive=False,
elem_classes=["soft-slider"]
)
script_converted_responses = gr.Textbox(
label="변환된 응답 (L1 → L2 → L3)",
interactive=False,
lines=4,
elem_classes=["soft-output"]
)
script_transcribed_text = gr.Textbox(
label="음성 인식 결과",
interactive=False,
lines=2,
elem_classes=["soft-output"]
)
# 녹음 결과 요약
with gr.Row():
script_summary_btn = gr.Button(
"📊 녹음 결과 요약",
variant="primary",
elem_classes=["soft-button"]
)
script_summary_output = gr.Markdown(
value="녹음을 완료한 후 결과 요약을 확인하세요.",
elem_classes=["soft-output"]
)
# 세션 데이터 저장 (숨김)
script_session_data = gr.State(value=None)
# 이벤트 연결
script_category_dropdown.change(
fn=get_scripts_by_category,
inputs=[script_category_dropdown],
outputs=[script_checkboxes]
)
start_script_btn.click(
fn=create_script_session,
inputs=[script_checkboxes],
outputs=[script_session_data]
).then(
fn=get_current_script_info,
inputs=[script_session_data],
outputs=[script_title, script_situation, script_question, script_meta, gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False)]
).then(
fn=lambda session: f"녹음 시작! 총 {len(json.loads(session)['scripts'])}개 대본이 준비되었습니다." if session else "대본을 선택하고 녹음을 시작해주세요",
inputs=[script_session_data],
outputs=[script_progress_info]
)
process_script_btn.click(
fn=process_script_recording,
inputs=[script_session_data, script_audio_input],
outputs=[script_session_data, script_title, script_situation, script_question, script_meta, gr.Textbox(visible=False), gr.Textbox(visible=False), gr.Textbox(visible=False), script_transcribed_text]
).then(
fn=lambda session: f"진행률: {json.loads(session)['completed_scripts']}/{json.loads(session)['total_scripts']} ({(json.loads(session)['completed_scripts']/json.loads(session)['total_scripts']*100):.1f}%)" if session else "대본을 선택하고 녹음을 시작해주세요",
inputs=[script_session_data],
outputs=[script_progress_info]
)
script_summary_btn.click(
fn=get_script_summary,
inputs=[script_session_data],
outputs=[script_summary_output]
)
# 사용 설명서 탭
with gr.Tab("📖 사용 설명서"):
with gr.Accordion("🎯 서비스 개요", open=True, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 🗣️ 반향어 치료 보조 어시스턴트
**AI 기반 반향어 감지 및 기능적 의사소통 전환 도구**
이 서비스는 자폐 스펙트럼 장애나 언어 발달 지연을 가진 환자의 반향어를 감지하고,
3단계 변환 시스템을 통해 기능적 의사소통으로 전환하는 치료 보조 도구입니다.
**주요 대상**: 언어치료사, 특수교육 교사, 부모
""")
with gr.Accordion("⚡ 주요 기능", open=True, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 📝 텍스트 분석
- 반향어 자동 감지
- 상황별 맥락 분석
- 신뢰도 점수 제공 (0-100%)
### 🎤 음성 분석
- 음성 → 텍스트 변환
- 실시간 반향어 감지
- 음성 합성 출력
### 🔬 구조화된 검사
- 표준화된 검사 질문 제공
- 카테고리별 체계적 평가
- 진행률 추적 및 결과 요약
### 🎬 대본 기반 녹음
- 상황별 대본 템플릿 제공
- 체계적인 녹음 가이드
- 일관된 평가 기준 적용
### 📊 3단계 변환 시스템
- **L1**: 기본 수준 (간단한 응답)
- **L2**: 확장 수준 (세부사항 포함)
- **L3**: 자기표현 수준 (감정, 경험 포함)
""")
with gr.Accordion("🚀 사용 방법", open=True, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 1단계: 텍스트 분석하기
1. **"📝 텍스트 처리"** 탭 선택
2. **입력 텍스트**에 반향어가 의심되는 문장 입력
3. **컨텍스트**에 이전 대화나 상황 정보 입력 (선택사항)
4. **상황** 드롭다운에서 적절한 상황 선택
5. **"🔍 분석하기"** 버튼 클릭
### 2단계: 음성 분석하기
1. **"🎤 음성 처리"** 탭 선택
2. **음성 입력**에 음성 파일 업로드
3. **"🎵 음성 분석하기"** 버튼 클릭
4. 자동으로 음성 인식 → 반향어 감지 → 변환 → 음성 합성 진행
### 3단계: 구조화된 검사하기
1. **"🔬 구조화된 검사"** 탭 선택
2. **검사 카테고리** 선택 (기본 의사소통, 일상 활동 등)
3. **카테고리당 질문 수** 설정 (1-5개)
4. **"🚀 새 검사 시작"** 버튼 클릭
5. 질문에 대해 아이의 응답을 텍스트 또는 음성으로 입력
6. **"📝 응답 처리"** 버튼으로 각 질문 처리
7. **"📊 검사 결과 요약"**으로 전체 결과 확인
### 4단계: 대본 기반 녹음하기
1. **"🎬 대본 기반 녹음"** 탭 선택
2. **대본 카테고리** 선택 (일상 루틴, 놀이 활동 등)
3. **원하는 대본들** 선택 (여러 개 선택 가능)
4. **"🎬 녹음 시작"** 버튼 클릭
5. 화면에 표시된 질문을 아이에게 읽어주기
6. 아이의 응답을 **음성으로 녹음**
7. **"📝 녹음 처리"** 버튼으로 각 응답 처리
8. **"📊 녹음 결과 요약"**으로 전체 결과 확인
### 5단계: 결과 확인하기
- ✅ **반향어 감지**: 체크박스로 감지 여부 확인
- 📊 **신뢰도**: 슬라이더로 정확도 확인 (0-100%)
- 📝 **3단계 변환 결과**: L1, L2, L3 수준의 대안 문장 확인
""")
with gr.Accordion("📈 3단계 변환 시스템 상세", open=False, elem_classes=["soft-accordion"]):
gr.Markdown("""
| 단계 | 특징 | 목적 | 예시 |
|------|------|------|------|
| **L1** | 기본 수준 | 핵심 메시지 전달 | "네, 먹었어요" |
| **L2** | 확장 수준 | 세부사항 포함 | "네, 점심을 맛있게 먹었어요" |
| **L3** | 자기표현 수준 | 감정, 경험 포함 | "네, 엄마가 만든 김치찌개를 맛있게 먹었어요" |
**치료적 목적**: 환자의 현재 수준에 맞는 단계부터 시작하여 점진적으로 더 복잡한 의사소통으로 발전
""")
with gr.Accordion("💡 실제 사용 예시", open=False, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 예시 1: 완전 반향어
```
입력: "오늘 기분이 어때?"
컨텍스트: "오늘 기분이 어때?"
결과: 반향어 감지 (90%)
L1: "좋아요"
L2: "오늘 기분이 좋아요"
L3: "오늘 날씨가 좋아서 기분이 좋아요"
```
### 예시 2: 상황적 반향어
```
입력: "놀자"
컨텍스트: "공부하자"
상황: 수업 시간
결과: 반향어 감지 (85%)
L1: "공부해요"
L2: "지금은 공부할 시간이에요"
L3: "수업 시간이니까 공부를 먼저 하고 나중에 놀아요"
```
### 예시 3: 부분 반향어
```
입력: "밥 먹었어?"
컨텍스트: "점심 먹었어?"
결과: 부분 반향어 감지 (60%)
L1: "네, 먹었어요"
L2: "네, 점심을 먹었어요"
L3: "네, 12시에 점심을 맛있게 먹었어요"
```
""")
with gr.Accordion("👩‍⚕️ 치료사 가이드", open=False, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 세션 준비
- [ ] 환자 현재 수준 파악
- [ ] 세션 목표 설정
- [ ] 이전 기록 확인
### 세션 진행
1. **관찰** → 환자 반향어 패턴 관찰
2. **분석** → AI 도구로 감지 및 분석
3. **연습** → 3단계 변환 결과 활용
4. **평가** → 환자 반응 및 개선 정도 평가
### 기록 관리
- 세션별 반향어 패턴 기록
- 신뢰도 변화 추적
- 환자별 맞춤 목표 설정
""")
with gr.Accordion("⚠️ 주의사항", open=False, elem_classes=["soft-accordion"]):
gr.Markdown("""
### 감지 한계
- 단순 반복과 구분 필요
- 상황 정보 정확 입력 중요
- 개인차 고려 필요
### 사용 시 주의
- AI 도구에만 의존하지 말 것
- 환자 정보 보호 필수
- 정기적 치료 효과 평가
### 문제 해결
| 문제 | 해결 방법 |
|------|-----------|
| 반향어 감지 안됨 | 컨텍스트 정보 상세 입력 |
| 신뢰도 낮음 | 상황 정보 정확 선택 |
| 변환 결과 부적절 | 환자 수준에 맞는 단계 선택 |
| 음성 인식 안됨 | 명확한 발음으로 녹음 |
""")
return demo
if __name__ == "__main__":
# 환경 변수 확인
if not os.getenv("OPENAI_API_KEY"):
print("❌ OPENAI_API_KEY가 설정되지 않았습니다.")
print("환경 변수를 설정하거나 .env 파일을 확인하세요.")
exit(1)
# Gradio 앱 실행
demo = create_interface()
demo.launch(
server_name="0.0.0.0",
server_port=7863,
share=True,
debug=True
)