| 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 |
|
|
| |
| 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, []) |
| |
| 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}", |
| "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. 의사 표현 연습 |
| |
| *이 계획은 개인의 발달 수준과 필요에 따라 조정될 수 있습니다.* |
| """ |
|
|
| |
| 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 = { |
| "식사 시간": ["밥", "먹", "식사", "음식"], |
| "놀이 시간": ["놀", "게임", "장난감"], |
| "외출 준비": ["나가", "외출", "산책"], |
| "수업 시간": ["공부", "책", "학습"], |
| "휴식 시간": ["쉬", "잠", "휴식"] |
| } |
|
|
| |
| if text == context: |
| return True, context, 0.9 |
|
|
| |
| words = text.split() |
| if len(words) >= 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: |
| repeated_word = most_repeated[0] |
| |
| if repeated_word not in context: |
| return True, repeated_word, 0.8 |
|
|
| |
| 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 |
|
|
| |
| if context.split() and text.endswith(context.split()[-1]): |
| return True, context, 0.6 |
|
|
| |
| 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 |
| |
| |
| transcribed_text = transcribe_audio(audio_file) |
| if not transcribed_text: |
| return "", False, "", 0.0, [], None |
| |
| |
| is_echo, anchor, confidence = detect_echo(transcribed_text, transcribed_text) |
| |
| |
| 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 |
|
|
| |
| 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) |
| |
| |
| demo = create_interface() |
| demo.launch( |
| server_name="0.0.0.0", |
| server_port=7863, |
| share=True, |
| debug=True |
| ) |
|
|