alohaboy
docs: Update report note text in gradio_app.py and app.py to match text_processor.py
2c7fc26 | import gradio as gr | |
| import os | |
| import base64 | |
| import tempfile | |
| from openai import OpenAI | |
| from typing import List, Tuple, Dict, Any, Optional | |
| import json | |
| from datetime import datetime, timedelta | |
| import sqlite3 | |
| from dotenv import load_dotenv | |
| from pathlib import Path | |
| # RAG 관련 import (선택적) | |
| try: | |
| import chromadb | |
| from chromadb.utils import embedding_functions | |
| CHROMADB_AVAILABLE = True | |
| except ImportError: | |
| CHROMADB_AVAILABLE = False | |
| chromadb = None | |
| embedding_functions = None | |
| try: | |
| from sentence_transformers import SentenceTransformer | |
| SENTENCE_TRANSFORMERS_AVAILABLE = True | |
| except ImportError: | |
| SENTENCE_TRANSFORMERS_AVAILABLE = False | |
| SentenceTransformer = None | |
| # Load environment variables | |
| load_dotenv() | |
| # Assessment service import | |
| import sys | |
| sys.path.append(os.path.dirname(__file__)) | |
| # Clarification module import | |
| from src.clarification import ClarificationModule, AmbiguityType | |
| # New 3-phase pipeline import | |
| from src.pipeline.text_processor import process_text_with_pipeline | |
| # 데이터베이스 초기화 | |
| def init_database(): | |
| """데이터베이스 초기화""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| # 사용자 프로필 테이블 | |
| cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS user_profiles ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| child_name TEXT NOT NULL, | |
| age INTEGER NOT NULL, | |
| vocabulary_level TEXT NOT NULL, | |
| cars_score REAL, | |
| language_habits TEXT, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| ''') | |
| # CARS 검사 결과 테이블 | |
| cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS cars_assessments ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| profile_id INTEGER, | |
| total_score REAL NOT NULL, | |
| assessment_data TEXT NOT NULL, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (profile_id) REFERENCES user_profiles (id) | |
| ) | |
| ''') | |
| # 기존 테이블에 assessment_data 컬럼이 없으면 추가 | |
| try: | |
| cursor.execute("ALTER TABLE cars_assessments ADD COLUMN assessment_data TEXT") | |
| conn.commit() | |
| print("✅ assessment_data 컬럼이 추가되었습니다.") | |
| except sqlite3.OperationalError: | |
| # 컬럼이 이미 존재하는 경우 | |
| pass | |
| # 분석 결과 이력 테이블 | |
| cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS analysis_history ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| profile_id INTEGER NOT NULL, | |
| analysis_type TEXT NOT NULL, -- 'text', 'audio', 'echo_test' | |
| input_summary TEXT NOT NULL, -- 입력 요약 (예: "아이스크림 반복 발화") | |
| context_situation TEXT, | |
| is_echo BOOLEAN, | |
| confidence REAL, | |
| anchor_text TEXT, | |
| analysis_summary TEXT NOT NULL, -- 분석 요약 (핵심 결과만) | |
| key_recommendations TEXT, -- 주요 권장사항 (JSON 형태) | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (profile_id) REFERENCES user_profiles (id) | |
| ) | |
| ''') | |
| # 사용자 피드백 테이블 (PPO 학습용) | |
| cursor.execute(''' | |
| CREATE TABLE IF NOT EXISTS user_feedback ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| analysis_id INTEGER NOT NULL, | |
| user_rating INTEGER NOT NULL, -- 1-5 스케일 (1: 매우 부적절, 5: 매우 적절) | |
| feedback_text TEXT, -- 선택적 추가 피드백 | |
| user_session_id TEXT, -- 사용자 세션 추적용 | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (analysis_id) REFERENCES analysis_history (id) | |
| ) | |
| ''') | |
| conn.commit() | |
| conn.close() | |
| def migrate_database(): | |
| """데이터베이스 마이그레이션 - 기존 테이블을 새 구조로 업데이트""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| try: | |
| # 기존 cars_assessments 테이블이 있는지 확인 | |
| cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='cars_assessments'") | |
| table_exists = cursor.fetchone() | |
| if table_exists: | |
| # 기존 테이블의 구조 확인 | |
| cursor.execute("PRAGMA table_info(cars_assessments)") | |
| columns = [column[1] for column in cursor.fetchall()] | |
| # 필요한 컬럼들이 모두 있는지 확인 | |
| required_columns = ['id', 'profile_id', 'total_score', 'assessment_data', 'created_at'] | |
| missing_columns = [col for col in required_columns if col not in columns] | |
| if missing_columns: | |
| print(f"🔄 cars_assessments 테이블에 누락된 컬럼: {missing_columns}") | |
| print("🔄 테이블을 새 구조로 재생성합니다...") | |
| # 기존 데이터 백업 | |
| cursor.execute("SELECT * FROM cars_assessments") | |
| old_data = cursor.fetchall() | |
| # 기존 테이블 삭제 | |
| cursor.execute("DROP TABLE cars_assessments") | |
| # 새 테이블 생성 | |
| cursor.execute(''' | |
| CREATE TABLE cars_assessments ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| profile_id INTEGER, | |
| total_score REAL NOT NULL, | |
| assessment_data TEXT NOT NULL, | |
| created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
| FOREIGN KEY (profile_id) REFERENCES user_profiles (id) | |
| ) | |
| ''') | |
| # 기존 데이터 복원 (assessment_data는 빈 문자열로) | |
| for row in old_data: | |
| cursor.execute(''' | |
| INSERT INTO cars_assessments (id, profile_id, total_score, assessment_data, created_at) | |
| VALUES (?, ?, ?, ?, ?) | |
| ''', (row[0], row[1], row[2], '', row[3])) | |
| print("✅ cars_assessments 테이블이 성공적으로 업데이트되었습니다.") | |
| else: | |
| print("✅ cars_assessments 테이블 구조가 올바릅니다.") | |
| conn.commit() | |
| except Exception as e: | |
| print(f"❌ 데이터베이스 마이그레이션 중 오류: {e}") | |
| conn.rollback() | |
| finally: | |
| conn.close() | |
| # 사용자 프로필 관리 클래스 | |
| class UserProfileManager: | |
| def __init__(self): | |
| init_database() | |
| migrate_database() | |
| def create_profile(self, child_name: str, age: int, vocabulary_level: str, | |
| cars_score: float = None, language_habits: str = ""): | |
| """새 프로필 생성""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| INSERT INTO user_profiles (child_name, age, vocabulary_level, cars_score, language_habits) | |
| VALUES (?, ?, ?, ?, ?) | |
| ''', (child_name, age, vocabulary_level, cars_score, language_habits)) | |
| profile_id = cursor.lastrowid | |
| conn.commit() | |
| conn.close() | |
| return profile_id | |
| def get_profile(self, profile_id: int): | |
| """프로필 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute('SELECT * FROM user_profiles WHERE id = ?', (profile_id,)) | |
| result = cursor.fetchone() | |
| conn.close() | |
| if result: | |
| return { | |
| 'id': result[0], | |
| 'child_name': result[1], | |
| 'age': result[2], | |
| 'vocabulary_level': result[3], | |
| 'cars_score': result[4], | |
| 'language_habits': result[5], | |
| 'created_at': result[6], | |
| 'updated_at': result[7] | |
| } | |
| return None | |
| def update_profile(self, profile_id: int, **kwargs): | |
| """프로필 업데이트""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| update_fields = [] | |
| values = [] | |
| for key, value in kwargs.items(): | |
| if key in ['child_name', 'age', 'vocabulary_level', 'cars_score', 'language_habits']: | |
| update_fields.append(f"{key} = ?") | |
| values.append(value) | |
| if update_fields: | |
| values.append(profile_id) | |
| cursor.execute(f''' | |
| UPDATE user_profiles | |
| SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP | |
| WHERE id = ? | |
| ''', values) | |
| conn.commit() | |
| conn.close() | |
| def list_profiles(self): | |
| """모든 프로필 목록 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute('SELECT id, child_name, age, vocabulary_level, cars_score FROM user_profiles ORDER BY updated_at DESC') | |
| results = cursor.fetchall() | |
| conn.close() | |
| return [{'id': r[0], 'child_name': r[1], 'age': r[2], 'vocabulary_level': r[3], 'cars_score': r[4]} for r in results] | |
| def get_cars_assessments(self, profile_id: int): | |
| """특정 프로필의 CARS 검사 결과 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT id, profile_id, total_score, assessment_data, created_at | |
| FROM cars_assessments | |
| WHERE profile_id = ? | |
| ORDER BY created_at DESC | |
| ''', (profile_id,)) | |
| results = cursor.fetchall() | |
| conn.close() | |
| assessments = [] | |
| for result in results: | |
| # CARS 점수에 따른 진단 결과 계산 | |
| total_score = result[2] | |
| if total_score >= 37: | |
| diagnosis = "추가 지원이 필요한 수준" | |
| elif total_score >= 30: | |
| diagnosis = "관심이 필요한 수준" | |
| elif total_score >= 15: | |
| diagnosis = "경미한 특성 관찰" | |
| else: | |
| diagnosis = "정상 범위" | |
| assessments.append({ | |
| 'id': result[0], | |
| 'profile_id': result[1], | |
| 'total_score': result[2], | |
| 'diagnosis': diagnosis, | |
| 'individual_scores': result[3], # assessment_data에 개별 점수가 저장됨 | |
| 'assessment_data': result[3], | |
| 'assessment_date': result[4] | |
| }) | |
| return assessments | |
| def get_recent_analyses(self, profile_id: int, limit: int = 10): | |
| """최근 분석 결과들 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations, created_at | |
| FROM analysis_history | |
| WHERE profile_id = ? | |
| ORDER BY created_at DESC | |
| LIMIT ? | |
| ''', (profile_id, limit)) | |
| results = cursor.fetchall() | |
| conn.close() | |
| analyses = [] | |
| for result in results: | |
| analyses.append({ | |
| 'analysis_type': result[0], | |
| 'input_summary': result[1], | |
| 'context_situation': result[2], | |
| 'is_echo': result[3], | |
| 'confidence': result[4], | |
| 'anchor_text': result[5], | |
| 'analysis_summary': result[6], | |
| 'key_recommendations': result[7], | |
| 'created_at': result[8] | |
| }) | |
| return analyses | |
| def get_analysis_statistics(self, profile_id: int): | |
| """분석 통계 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| # 전체 분석 통계 | |
| cursor.execute(''' | |
| SELECT | |
| COUNT(*) as total_analyses, | |
| SUM(CASE WHEN is_echo = 1 THEN 1 ELSE 0 END) as echo_count, | |
| AVG(confidence) as avg_confidence, | |
| MIN(created_at) as first_analysis, | |
| MAX(created_at) as last_analysis | |
| FROM analysis_history | |
| WHERE profile_id = ? | |
| ''', (profile_id,)) | |
| stats = cursor.fetchone() | |
| # 카테고리별 통계 | |
| cursor.execute(''' | |
| SELECT context_situation, COUNT(*) as count, | |
| SUM(CASE WHEN is_echo = 1 THEN 1 ELSE 0 END) as echo_count | |
| FROM analysis_history | |
| WHERE profile_id = ? | |
| GROUP BY context_situation | |
| ''', (profile_id,)) | |
| category_stats = cursor.fetchall() | |
| conn.close() | |
| return { | |
| 'total_analyses': stats[0] or 0, | |
| 'echo_count': stats[1] or 0, | |
| 'avg_confidence': stats[2] or 0, | |
| 'first_analysis': stats[3], | |
| 'last_analysis': stats[4], | |
| 'category_stats': [ | |
| { | |
| 'category': cat[0], | |
| 'total_count': cat[1], | |
| 'echo_count': cat[2], | |
| 'echo_rate': (cat[2] / cat[1] * 100) if cat[1] > 0 else 0 | |
| } | |
| for cat in category_stats | |
| ] | |
| } | |
| def save_analysis_result(self, profile_id: int, analysis_type: str, input_summary: str, | |
| context_situation: str, is_echo: bool, confidence: float, | |
| anchor_text: str, analysis_summary: str, key_recommendations: str): | |
| """분석 결과 저장""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| INSERT INTO analysis_history | |
| (profile_id, analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| ''', (profile_id, analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations)) | |
| conn.commit() | |
| conn.close() | |
| def save_user_feedback(self, analysis_id: int, user_rating: int, feedback_text: str = "", user_session_id: str = ""): | |
| """사용자 피드백 저장 (PPO 학습용)""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| INSERT INTO user_feedback | |
| (analysis_id, user_rating, feedback_text, user_session_id) | |
| VALUES (?, ?, ?, ?) | |
| ''', (analysis_id, user_rating, feedback_text, user_session_id)) | |
| conn.commit() | |
| conn.close() | |
| def get_user_feedback_for_analysis(self, analysis_id: int): | |
| """특정 분석에 대한 사용자 피드백 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT id, user_rating, feedback_text, user_session_id, created_at | |
| FROM user_feedback | |
| WHERE analysis_id = ? | |
| ORDER BY created_at DESC | |
| ''', (analysis_id,)) | |
| results = cursor.fetchall() | |
| conn.close() | |
| feedbacks = [] | |
| for result in results: | |
| feedbacks.append({ | |
| 'id': result[0], | |
| 'user_rating': result[1], | |
| 'feedback_text': result[2], | |
| 'user_session_id': result[3], | |
| 'created_at': result[4] | |
| }) | |
| return feedbacks | |
| def get_user_feedback_statistics(self, profile_id: int = None): | |
| """사용자 피드백 통계 조회 (PPO 학습용)""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| if profile_id: | |
| # 특정 프로필의 피드백 통계 | |
| cursor.execute(''' | |
| SELECT | |
| COUNT(*) as total_feedbacks, | |
| AVG(user_rating) as avg_rating, | |
| SUM(CASE WHEN user_rating >= 4 THEN 1 ELSE 0 END) as high_rating_count, | |
| SUM(CASE WHEN user_rating <= 2 THEN 1 ELSE 0 END) as low_rating_count | |
| FROM user_feedback uf | |
| JOIN analysis_history ah ON uf.analysis_id = ah.id | |
| WHERE ah.profile_id = ? | |
| ''', (profile_id,)) | |
| else: | |
| # 전체 피드백 통계 | |
| cursor.execute(''' | |
| SELECT | |
| COUNT(*) as total_feedbacks, | |
| AVG(user_rating) as avg_rating, | |
| SUM(CASE WHEN user_rating >= 4 THEN 1 ELSE 0 END) as high_rating_count, | |
| SUM(CASE WHEN user_rating <= 2 THEN 1 ELSE 0 END) as low_rating_count | |
| FROM user_feedback | |
| ''') | |
| stats = cursor.fetchone() | |
| conn.close() | |
| total = stats[0] or 0 | |
| avg_rating = stats[1] or 0 | |
| high_rating_count = stats[2] or 0 | |
| low_rating_count = stats[3] or 0 | |
| return { | |
| 'total_feedbacks': total, | |
| 'avg_rating': avg_rating, | |
| 'high_rating_count': high_rating_count, | |
| 'low_rating_count': low_rating_count, | |
| 'satisfaction_rate': (high_rating_count / total * 100) if total > 0 else 0 | |
| } | |
| def get_analysis_history(self, profile_id: int, limit: int = 10): | |
| """특정 프로필의 분석 이력 조회""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| SELECT id, analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations, created_at | |
| FROM analysis_history | |
| WHERE profile_id = ? | |
| ORDER BY created_at DESC | |
| LIMIT ? | |
| ''', (profile_id, limit)) | |
| results = cursor.fetchall() | |
| conn.close() | |
| history = [] | |
| for result in results: | |
| history.append({ | |
| 'id': result[0], | |
| 'analysis_type': result[1], | |
| 'input_summary': result[2], | |
| 'context_situation': result[3], | |
| 'is_echo': result[4], | |
| 'confidence': result[5], | |
| 'anchor_text': result[6], | |
| 'analysis_summary': result[7], | |
| 'key_recommendations': result[8], | |
| 'created_at': result[9] | |
| }) | |
| return history | |
| # CARS 검사 관리 클래스 | |
| class CARSAssessment: | |
| def __init__(self): | |
| self.questions = [ | |
| "1. 사람과의 관계", | |
| "2. 모방", | |
| "3. 정서적 반응", | |
| "4. 신체사용", | |
| "5. 물체 사용", | |
| "6. 적응력의 변화", | |
| "7. 시각적 반응", | |
| "8. 청각적 반응", | |
| "9. 미각, 후각, 촉각 반응", | |
| "10. 공포와 불안", | |
| "11. 언어적 의사소통", | |
| "12. 비언어적 의사소통", | |
| "13. 활동수준", | |
| "14. 지적 기능의 수준과 항상성", | |
| "15. 일반적인 인상" | |
| ] | |
| self.scoring_options = [1, 1.5, 2, 2.5, 3, 3.5, 4] | |
| self.score_ranges = { | |
| (15, 29.5): "자폐 아님", | |
| (30, 36.5): "경증, 중간정도 자폐", | |
| (37, 60): "중증 자폐" | |
| } | |
| def calculate_total_score(self, scores: List[float]) -> Tuple[float, str]: | |
| """총점 계산 및 진단""" | |
| total = sum(scores) | |
| for (min_score, max_score), diagnosis in self.score_ranges.items(): | |
| if min_score <= total <= max_score: | |
| return total, diagnosis | |
| return total, "정상 범위" | |
| def save_assessment(self, profile_id: int, scores: List[float], total_score: float): | |
| """검사 결과 저장""" | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| assessment_data = json.dumps({ | |
| 'scores': scores, | |
| 'total_score': total_score, | |
| 'timestamp': datetime.now().isoformat() | |
| }) | |
| cursor.execute(''' | |
| INSERT INTO cars_assessments (profile_id, total_score, assessment_data) | |
| VALUES (?, ?, ?) | |
| ''', (profile_id, total_score, assessment_data)) | |
| # 프로필의 CARS 점수도 업데이트 | |
| cursor.execute(''' | |
| UPDATE user_profiles | |
| SET cars_score = ?, updated_at = CURRENT_TIMESTAMP | |
| WHERE id = ? | |
| ''', (total_score, profile_id)) | |
| conn.commit() | |
| conn.close() | |
| # 전역 인스턴스 | |
| profile_manager = UserProfileManager() | |
| cars_assessment = CARSAssessment() | |
| # 서비스 클래스들을 직접 정의 (Hugging Face Spaces 호환) | |
| class AssessmentService: | |
| def __init__(self): | |
| self.categories = [ | |
| {"name": "언어 이해", "key": "language_comprehension"}, | |
| {"name": "언어 표현", "key": "language_expression"}, | |
| {"name": "사회적 의사소통", "key": "social_communication"}, | |
| {"name": "인지 기능", "key": "cognitive_function"} | |
| ] | |
| self.questions = { | |
| "language_comprehension": [ | |
| "엄마가 '물 마실래?'라고 물어 봤을 때, 어떻게 답하시나요?", | |
| "선생님이 '안녕하세요'라고 인사했을 때, 어떻게 답하시나요?", | |
| "친구가 '놀자'라고 했을 때, 어떻게 답하시나요?" | |
| ], | |
| "language_expression": [ | |
| "배가 고플 때 어떻게 말하시나요?", | |
| "화가 날 때 어떻게 표현하시나요?", | |
| "기분이 좋을 때 어떻게 말하시나요?" | |
| ], | |
| "social_communication": [ | |
| "새로운 친구를 만났을 때 어떻게 인사하시나요?", | |
| "도움이 필요할 때 어떻게 요청하시나요?", | |
| "감사할 때 어떻게 표현하시나요?" | |
| ], | |
| "cognitive_function": [ | |
| "이것이 무엇인지 모를 때 어떻게 물어보시나요?", | |
| "잘 모르겠을 때 어떻게 말하시나요?", | |
| "이해가 안 될 때 어떻게 표현하시나요?" | |
| ] | |
| } | |
| def get_categories(self): | |
| return self.categories | |
| def create_assessment_session(self, selected_categories, questions_per_category): | |
| """새로운 검사 세션 생성""" | |
| print(f"DEBUG: AssessmentService.create_assessment_session called with questions_per_category={questions_per_category}") | |
| if not selected_categories: | |
| selected_categories = [cat["key"] for cat in self.categories] | |
| session = { | |
| "selected_categories": selected_categories, | |
| "questions_per_category": questions_per_category, | |
| "current_category_index": 0, | |
| "current_question_index": 0, | |
| "responses": [], | |
| "created_at": datetime.now().isoformat() | |
| } | |
| # 총 질문 수 계산 | |
| total_questions = 0 | |
| for category_key in selected_categories: | |
| category_questions = self.questions.get(category_key, []) | |
| questions_to_use = min(questions_per_category, len(category_questions)) | |
| total_questions += questions_to_use | |
| print(f"DEBUG: Category {category_key}: {len(category_questions)} available, using {questions_to_use}") | |
| session["total_questions"] = total_questions | |
| session["remaining_questions"] = total_questions | |
| print(f"DEBUG: Final total_questions: {total_questions}") | |
| return session | |
| def get_current_question_info(self, session): | |
| """현재 질문 정보 반환""" | |
| if not session: | |
| return None, None, None, None, None, None, None, None | |
| current_category_index = session.get("current_category_index", 0) | |
| current_question_index = session.get("current_question_index", 0) | |
| selected_categories = session.get("selected_categories", []) | |
| if current_category_index >= len(selected_categories): | |
| return None, None, None, None, None, None, None, None | |
| category_key = selected_categories[current_category_index] | |
| category_questions = self.questions.get(category_key, []) | |
| if current_question_index >= len(category_questions): | |
| return None, None, None, None, None, None, None, None | |
| question_text = category_questions[current_question_index] | |
| category_name = next((cat["name"] for cat in self.categories if cat["key"] == category_key), category_key) | |
| return ( | |
| question_text, | |
| f"{category_name} 영역", | |
| f"질문 {current_question_index + 1}", | |
| f"카테고리: {category_name}", | |
| f"진행률: {current_category_index + 1}/{len(selected_categories)}", | |
| f"남은 질문: {session.get('remaining_questions', 0)}", | |
| f"총 질문: {session.get('total_questions', 0)}", | |
| f"완료율: {((session.get('total_questions', 0) - session.get('remaining_questions', 0)) / max(session.get('total_questions', 1), 1) * 100):.1f}%" | |
| ) | |
| def move_to_next_question(self, session): | |
| """다음 질문으로 이동""" | |
| if not session: | |
| return session | |
| current_category_index = session.get("current_category_index", 0) | |
| current_question_index = session.get("current_question_index", 0) | |
| selected_categories = session.get("selected_categories", []) | |
| questions_per_category = session.get("questions_per_category", 3) | |
| if current_category_index >= len(selected_categories): | |
| return session | |
| category_key = selected_categories[current_category_index] | |
| category_questions = self.questions.get(category_key, []) | |
| # 현재 카테고리에서 질문 수 제한 확인 | |
| if current_question_index + 1 >= min(questions_per_category, len(category_questions)): | |
| # 다음 카테고리로 이동 | |
| session["current_category_index"] = current_category_index + 1 | |
| session["current_question_index"] = 0 | |
| else: | |
| # 같은 카테고리의 다음 질문으로 이동 | |
| session["current_question_index"] = current_question_index + 1 | |
| # 남은 질문 수 업데이트 | |
| session["remaining_questions"] = max(0, session.get("remaining_questions", 0) - 1) | |
| return session | |
| 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, []) | |
| print(f"DEBUG: ScriptService.get_scripts_by_category - category_key: {category_key}") | |
| print(f"DEBUG: ScriptService.get_scripts_by_category - raw scripts: {scripts}") | |
| # Gradio CheckboxGroup 형식: [(label, value), ...] | |
| # script[0]은 제목, script[1]은 설명이므로 제목을 label과 value로 사용 | |
| result = [(script[0], script[0]) for script in scripts] | |
| print(f"DEBUG: ScriptService.get_scripts_by_category - result: {result}") | |
| return result | |
| def create_recording_session(self, selected_scripts): | |
| return { | |
| "scripts": selected_scripts, | |
| "current_script_index": 0, | |
| "current_question_index": 0, | |
| "recordings": [], | |
| "created_at": datetime.now().isoformat() | |
| } | |
| def get_current_script(self, session): | |
| print(f"DEBUG: ScriptService.get_current_script called with session: {session}") | |
| if not session or "scripts" not in session: | |
| print("DEBUG: No session or scripts, returning None") | |
| return None | |
| current_index = session.get("current_script_index", 0) | |
| if current_index >= len(session["scripts"]): | |
| print(f"DEBUG: Current index {current_index} >= scripts length {len(session['scripts'])}, returning None") | |
| return None | |
| script_name = session["scripts"][current_index] | |
| # script_name이 리스트인 경우 첫 번째 요소만 사용 | |
| if isinstance(script_name, list): | |
| script_name = script_name[0] if script_name else "알 수 없는 대본" | |
| result = { | |
| "id": f"script_{current_index}", | |
| "title": script_name, | |
| "situation": f"{script_name} 상황", | |
| "difficulty": "L1-L2" | |
| } | |
| print(f"DEBUG: ScriptService.get_current_script returning: {result}") | |
| return result | |
| 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_data, transcript): | |
| if not session: | |
| return session | |
| if "recordings" not in session: | |
| session["recordings"] = [] | |
| session["recordings"].append({ | |
| "script_index": session.get("current_script_index", 0), | |
| "question_index": session.get("current_question_index", 0), | |
| "audio_data": audio_data, | |
| "transcript": transcript, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| 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 or "scripts" not in session: | |
| return 0, 0 | |
| current_script_index = session.get("current_script_index", 0) | |
| total_scripts = len(session["scripts"]) | |
| return current_script_index, total_scripts | |
| class TreatmentService: | |
| def __init__(self): | |
| self.treatment_plans = { | |
| "echo_shape": { | |
| "name": "반향어 형태 분석", | |
| "description": "반향어의 형태적 특성을 분석합니다.", | |
| "steps": ["즉시 반향어 감지", "반향어 형태 분석", "기능적 대안 제시"] | |
| }, | |
| "functional_communication": { | |
| "name": "기능적 의사소통 훈련", | |
| "description": "기능적 의사소통 능력을 향상시킵니다.", | |
| "steps": ["기본 의사소통 기술", "사회적 상황 연습", "일상 대화 훈련"] | |
| } | |
| } | |
| def get_treatment_plans(self): | |
| return self.treatment_plans | |
| def create_treatment_plan(self, plan_type, user_info): | |
| if plan_type not in self.treatment_plans: | |
| return None | |
| plan = self.treatment_plans[plan_type] | |
| return { | |
| "type": plan_type, | |
| "name": plan["name"], | |
| "description": plan["description"], | |
| "steps": plan["steps"], | |
| "user_info": user_info, | |
| "created_at": datetime.now().isoformat(), | |
| "progress": 0 | |
| } | |
| def generate_treatment_plan(self, assessment_summary, patient_info): | |
| """검사 결과를 바탕으로 개별화된 치료 계획 생성""" | |
| try: | |
| # 반향어 감지율에 따른 목표 설정 | |
| echo_rate = assessment_summary.get('echo_detection_rate', 0) | |
| completed_questions = assessment_summary.get('completed_questions', 0) | |
| echo_detected_count = assessment_summary.get('echo_detected_count', 0) | |
| total_questions = assessment_summary.get('total_questions', 0) | |
| # 심각도 결정 | |
| if echo_rate > 70: | |
| severity = "높음" | |
| severity_level = "high" | |
| elif echo_rate > 40: | |
| severity = "중간" | |
| severity_level = "medium" | |
| else: | |
| severity = "낮음" | |
| severity_level = "low" | |
| # 기본 치료 계획 구조 (format_treatment_plan에서 기대하는 구조) | |
| treatment_plan = { | |
| "patient_info": patient_info, | |
| "created_date": datetime.now().isoformat(), | |
| "pattern_analysis": { | |
| "echo_rate": echo_rate, | |
| "overall_severity": severity, | |
| "total_questions": total_questions, | |
| "echo_detected": echo_detected_count | |
| }, | |
| "goals": [], | |
| "strategies": [], | |
| "home_activities": [], | |
| "timeline": [], | |
| "progress_indicators": { | |
| "communication_skills": [], | |
| "social_skills": [], | |
| "daily_living": [] | |
| }, | |
| "next_review_date": (datetime.now() + timedelta(days=30)).isoformat() | |
| } | |
| # 반향어 감지율에 따른 목표 설정 | |
| if echo_rate > 70: | |
| # 높은 반향어 비율 - 집중적 개입 필요 | |
| treatment_plan["goals"] = [ | |
| { | |
| "description": "즉시 반향어 감소를 위한 기본 의사소통 기술 습득", | |
| "targets": [ | |
| "반향어 사용 빈도를 50% 이하로 감소", | |
| "기본 요청 표현 10개 이상 습득", | |
| "간단한 질문에 적절히 응답" | |
| ] | |
| }, | |
| { | |
| "description": "기능적 의사소통으로의 전환 연습", | |
| "targets": [ | |
| "상황에 맞는 응답 연습", | |
| "감정 표현 기술 향상", | |
| "자발적 의사소통 시도 증가" | |
| ] | |
| }, | |
| { | |
| "description": "사회적 상황에서의 적절한 응답 연습", | |
| "targets": [ | |
| "친구와의 상호작용 개선", | |
| "가족과의 대화 참여도 증가", | |
| "사회적 규칙 이해 및 적용" | |
| ] | |
| } | |
| ] | |
| elif echo_rate > 40: | |
| # 중간 반향어 비율 - 점진적 개선 | |
| treatment_plan["goals"] = [ | |
| { | |
| "description": "반향어 패턴 인식 및 대안 연습", | |
| "targets": [ | |
| "반향어 패턴 인식 능력 향상", | |
| "기능적 대안 표현 습득", | |
| "반향어 사용 빈도 점진적 감소" | |
| ] | |
| }, | |
| { | |
| "description": "상황별 적절한 응답 기술 향상", | |
| "targets": [ | |
| "다양한 상황에서의 응답 연습", | |
| "맥락 이해 능력 향상", | |
| "적절한 대화 기술 습득" | |
| ] | |
| }, | |
| { | |
| "description": "자발적 의사소통 능력 개발", | |
| "targets": [ | |
| "자발적 질문하기 연습", | |
| "의견 표현 기술 향상", | |
| "대화 주도하기 연습" | |
| ] | |
| } | |
| ] | |
| else: | |
| # 낮은 반향어 비율 - 유지 및 향상 | |
| treatment_plan["goals"] = [ | |
| { | |
| "description": "기존 의사소통 기술 유지", | |
| "targets": [ | |
| "현재 의사소통 수준 유지", | |
| "일상 대화 능력 강화", | |
| "언어 표현 정확도 향상" | |
| ] | |
| }, | |
| { | |
| "description": "사회적 의사소통 능력 향상", | |
| "targets": [ | |
| "사회적 상황에서의 대화 기술 향상", | |
| "공감 능력 개발", | |
| "갈등 해결 기술 습득" | |
| ] | |
| }, | |
| { | |
| "description": "복잡한 상황에서의 대화 기술 개발", | |
| "targets": [ | |
| "추상적 개념 이해 및 표현", | |
| "논리적 사고 표현", | |
| "창의적 표현 능력 향상" | |
| ] | |
| } | |
| ] | |
| # 치료 전략 설정 | |
| treatment_plan["strategies"] = [ | |
| { | |
| "type": "immediate_echolalia", | |
| "name": "즉시 반향어 대응 전략", | |
| "severity_level": severity_level, | |
| "description": f"반향어 비율 {echo_rate:.1f}%에 따른 맞춤형 치료 전략입니다.", | |
| "strategies": [ | |
| "반향어 발생 시 즉시 기능적 대안 제시", | |
| "시각적 단서를 활용한 응답 유도", | |
| "단계적 의사소통 기술 습득", | |
| "긍정적 강화를 통한 동기 부여" | |
| ], | |
| "home_activities": [ | |
| "매일 15분 일대일 대화 시간", | |
| "그림책을 활용한 질문-응답 연습", | |
| "일상 상황에서의 기능적 표현 연습", | |
| "가족 구성원과의 역할 놀이" | |
| ] | |
| } | |
| ] | |
| # 가정 활동 계획 | |
| age = patient_info.get('age', 0) | |
| if age < 3: | |
| treatment_plan["home_activities"] = [ | |
| { | |
| "frequency": "매일", | |
| "duration": "15-20분", | |
| "activities": [ | |
| "놀이를 통한 기본 의사소통 연습", | |
| "간단한 요청과 응답 연습", | |
| "감정 표현 연습", | |
| "기본 어휘 습득" | |
| ] | |
| }, | |
| { | |
| "frequency": "주 3회", | |
| "duration": "20-30분", | |
| "activities": [ | |
| "그림책 읽기 및 질문하기", | |
| "음악과 함께 언어 연습", | |
| "감각 놀이를 통한 표현 연습" | |
| ] | |
| } | |
| ] | |
| elif age < 6: | |
| treatment_plan["home_activities"] = [ | |
| { | |
| "frequency": "매일", | |
| "duration": "20-30분", | |
| "activities": [ | |
| "게임을 통한 의사소통 연습", | |
| "친구와의 상호작용 연습", | |
| "상황별 대화 연습", | |
| "기본 사회적 기술 연습" | |
| ] | |
| }, | |
| { | |
| "frequency": "주 4회", | |
| "duration": "30-40분", | |
| "activities": [ | |
| "역할 놀이를 통한 사회적 상황 연습", | |
| "창작 활동을 통한 표현 연습", | |
| "그룹 활동 참여 연습" | |
| ] | |
| } | |
| ] | |
| else: | |
| treatment_plan["home_activities"] = [ | |
| { | |
| "frequency": "매일", | |
| "duration": "30-45분", | |
| "activities": [ | |
| "학습 상황에서의 의사소통 연습", | |
| "복잡한 감정 표현 연습", | |
| "논리적 사고 표현 연습", | |
| "자기 주장 기술 연습" | |
| ] | |
| }, | |
| { | |
| "frequency": "주 3회", | |
| "duration": "45-60분", | |
| "activities": [ | |
| "토론 및 의견 교환 연습", | |
| "창의적 글쓰기 연습", | |
| "프레젠테이션 기술 연습" | |
| ] | |
| } | |
| ] | |
| # 치료 일정 설정 (8주간) | |
| start_date = datetime.now() | |
| for week in range(1, 9): | |
| week_date = start_date + timedelta(weeks=week-1) | |
| if week <= 2: | |
| focus = "기본 의사소통 기술 습득" | |
| activities = ["반향어 패턴 인식", "기본 요청 표현 연습", "감정 표현 연습"] | |
| elif week <= 4: | |
| focus = "기능적 의사소통 연습" | |
| activities = ["상황별 응답 연습", "자발적 표현 연습", "대화 기술 향상"] | |
| elif week <= 6: | |
| focus = "사회적 의사소통 강화" | |
| activities = ["친구와의 상호작용", "갈등 해결 기술", "공감 능력 개발"] | |
| else: | |
| focus = "고급 의사소통 기술" | |
| activities = ["복잡한 상황 대응", "창의적 표현", "자기 주장 기술"] | |
| treatment_plan["timeline"].append({ | |
| "week": f"{week}", | |
| "date": week_date.strftime("%Y-%m-%d"), | |
| "focus": focus, | |
| "activities": activities | |
| }) | |
| # 진행 상황 측정 지표 | |
| treatment_plan["progress_indicators"] = { | |
| "communication_skills": [ | |
| "반향어 사용 빈도 감소", | |
| "기능적 표현 사용 증가", | |
| "자발적 질문하기 증가", | |
| "대화 참여도 향상", | |
| "언어 표현 정확도 향상" | |
| ], | |
| "social_skills": [ | |
| "친구와의 상호작용 개선", | |
| "사회적 규칙 준수", | |
| "공감 능력 향상", | |
| "갈등 해결 기술 습득", | |
| "협력 능력 개발" | |
| ], | |
| "daily_living": [ | |
| "일상 요청 표현 능력 향상", | |
| "감정 조절 기술 습득", | |
| "독립적 의사소통 시도 증가", | |
| "가족과의 대화 참여도 향상", | |
| "학습 상황 적응 개선" | |
| ] | |
| } | |
| return treatment_plan | |
| except Exception as e: | |
| print(f"치료 계획 생성 오류: {e}") | |
| return { | |
| "patient_info": patient_info, | |
| "created_date": datetime.now().isoformat(), | |
| "pattern_analysis": { | |
| "echo_rate": 0, | |
| "overall_severity": "알 수 없음", | |
| "total_questions": 0, | |
| "echo_detected": 0 | |
| }, | |
| "goals": [ | |
| { | |
| "description": "기본 의사소통 기술 향상", | |
| "targets": ["일상 대화 연습", "기본 요청 표현 습득", "감정 표현 연습"] | |
| }, | |
| { | |
| "description": "사회적 의사소통 능력 개발", | |
| "targets": ["친구와의 상호작용", "가족과의 대화 참여", "사회적 규칙 이해"] | |
| }, | |
| { | |
| "description": "자발적 의사소통 시도 증가", | |
| "targets": ["질문하기 연습", "의견 표현 연습", "대화 주도하기"] | |
| } | |
| ], | |
| "strategies": [ | |
| { | |
| "type": "immediate_echolalia", | |
| "name": "기본 치료 전략", | |
| "severity_level": "low", | |
| "description": "기본적인 의사소통 기술 향상을 위한 치료 전략입니다.", | |
| "strategies": [ | |
| "일대일 대화 시간 확보", | |
| "긍정적 강화를 통한 동기 부여", | |
| "단계적 의사소통 기술 습득", | |
| "시각적 단서를 활용한 응답 유도" | |
| ], | |
| "home_activities": [ | |
| "매일 15분 일대일 대화 시간", | |
| "그림책을 활용한 질문-응답 연습", | |
| "일상 상황에서의 기능적 표현 연습", | |
| "가족 구성원과의 역할 놀이" | |
| ] | |
| } | |
| ], | |
| "home_activities": [ | |
| { | |
| "frequency": "매일", | |
| "duration": "20-30분", | |
| "activities": [ | |
| "일상 대화 연습", | |
| "기본 요청 표현 연습", | |
| "감정 표현 연습", | |
| "기본 어휘 습득" | |
| ] | |
| }, | |
| { | |
| "frequency": "주 3회", | |
| "duration": "30-40분", | |
| "activities": [ | |
| "그림책 읽기 및 질문하기", | |
| "놀이를 통한 언어 연습", | |
| "가족과의 상호작용 연습" | |
| ] | |
| } | |
| ], | |
| "timeline": [ | |
| { | |
| "week": "1", | |
| "date": datetime.now().strftime("%Y-%m-%d"), | |
| "focus": "기본 의사소통 기술 습득", | |
| "activities": ["반향어 패턴 인식", "기본 요청 표현 연습", "감정 표현 연습"] | |
| } | |
| ], | |
| "progress_indicators": { | |
| "communication_skills": [ | |
| "기본 의사소통 기술 향상", | |
| "일상 대화 능력 강화", | |
| "언어 표현 정확도 향상" | |
| ], | |
| "social_skills": [ | |
| "친구와의 상호작용 개선", | |
| "사회적 규칙 준수", | |
| "공감 능력 향상" | |
| ], | |
| "daily_living": [ | |
| "일상 요청 표현 능력 향상", | |
| "감정 조절 기술 습득", | |
| "가족과의 대화 참여도 향상" | |
| ] | |
| }, | |
| "next_review_date": (datetime.now() + timedelta(days=30)).isoformat(), | |
| "error": str(e) | |
| } | |
| # OpenAI 클라이언트 초기화 | |
| client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) | |
| MODEL = os.getenv("OPENAI_MODEL", "gpt-4o-mini") | |
| # RAG 전역 변수 | |
| _rag_client = None | |
| _rag_collection = None | |
| _rag_embedding_model = None | |
| _rag_available = False | |
| def init_rag() -> bool: | |
| """RAG 초기화 함수""" | |
| global _rag_client, _rag_collection, _rag_embedding_model, _rag_available | |
| if not CHROMADB_AVAILABLE: | |
| print("⚠️ RAG: chromadb가 설치되지 않았습니다. RAG 기능을 사용할 수 없습니다.") | |
| _rag_available = False | |
| return False | |
| try: | |
| index_dir = Path("rag_memory/index") | |
| if not index_dir.exists(): | |
| print("⚠️ RAG: 인덱스 디렉토리가 없습니다. RAG 기능을 사용할 수 없습니다.") | |
| _rag_available = False | |
| return False | |
| _rag_client = chromadb.PersistentClient(path=str(index_dir)) | |
| collection_name = "echolalia_rag" | |
| try: | |
| _rag_collection = _rag_client.get_collection(name=collection_name) | |
| print(f"✅ RAG: '{collection_name}' 컬렉션을 로드했습니다.") | |
| except Exception as e: | |
| print(f"⚠️ RAG: 컬렉션 '{collection_name}'을 찾을 수 없습니다: {e}") | |
| _rag_available = False | |
| return False | |
| # 임베딩 모델은 필요시에만 로드 (검색 시) | |
| _rag_available = True | |
| print("✅ RAG 초기화 완료") | |
| return True | |
| except Exception as e: | |
| print(f"❌ RAG 초기화 실패: {e}") | |
| _rag_available = False | |
| return False | |
| def is_rag_available() -> bool: | |
| """RAG 사용 가능 여부 확인""" | |
| return _rag_available and _rag_collection is not None | |
| def search_rag_internal(query: str, top_k: int = 3) -> List[Dict[str, Any]]: | |
| """RAG 검색 내부 함수""" | |
| global _rag_embedding_model | |
| if not is_rag_available(): | |
| return [] | |
| try: | |
| # 쿼리 임베딩 생성 | |
| if _rag_embedding_model is None: | |
| if not SENTENCE_TRANSFORMERS_AVAILABLE: | |
| print("⚠️ RAG: sentence-transformers가 설치되지 않았습니다.") | |
| return [] | |
| _rag_embedding_model = SentenceTransformer('sentence-transformers/all-MiniLM-L6-v2') | |
| query_embedding = _rag_embedding_model.encode(query, convert_to_numpy=True).tolist() | |
| # ChromaDB에서 검색 | |
| results = _rag_collection.query( | |
| query_embeddings=[query_embedding], | |
| n_results=top_k | |
| ) | |
| # 결과 포맷팅 | |
| formatted_results = [] | |
| if results['ids'] and len(results['ids'][0]) > 0: | |
| for i in range(len(results['ids'][0])): | |
| formatted_results.append({ | |
| 'id': results['ids'][0][i], | |
| 'text': results['documents'][0][i], | |
| 'metadata': results['metadatas'][0][i] if results['metadatas'] else {}, | |
| 'similarity': 1.0 - results['distances'][0][i] if results['distances'] else 0.0 | |
| }) | |
| return formatted_results | |
| except Exception as e: | |
| print(f"❌ RAG 검색 오류: {e}") | |
| return [] | |
| def enhance_prompt_with_rag(prompt: str, query: str, top_k: int = 2) -> str: | |
| """프롬프트에 RAG 검색 결과 추가""" | |
| if not is_rag_available(): | |
| return prompt | |
| results = search_rag_internal(query, top_k=top_k) | |
| if not results: | |
| return prompt | |
| # RAG 참고 자료 섹션 구성 | |
| rag_context = "\n\n## 참고 자료 (의학 자문 검증 데이터베이스):\n" | |
| for i, result in enumerate(results, 1): | |
| source = result['metadata'].get('filename', '알 수 없음') | |
| text = result['text'][:300] # 처음 300자만 | |
| rag_context += f"\n[{i}] 출처: {source}\n{text}\n" | |
| enhanced_prompt = prompt + rag_context | |
| return enhanced_prompt | |
| # ============================================================ | |
| # 보안 및 토큰 제어 시스템 | |
| # ============================================================ | |
| # API 사용량 추적 | |
| api_usage_tracker = { | |
| 'total_calls': 0, | |
| 'total_tokens': 0, | |
| 'daily_calls': 0, | |
| 'daily_tokens': 0, | |
| 'last_reset': datetime.now().date(), | |
| 'session_calls': {} | |
| } | |
| # API 제한 설정 (환경 변수로 설정 가능) | |
| API_LIMIT_DAILY_CALLS = int(os.getenv("API_LIMIT_DAILY_CALLS", "500")) | |
| API_LIMIT_DAILY_TOKENS = int(os.getenv("API_LIMIT_DAILY_TOKENS", "200000")) # ~$10 per day | |
| API_LIMIT_SESSION_CALLS = int(os.getenv("API_LIMIT_SESSION_CALLS", "100")) | |
| MAX_INPUT_LENGTH = int(os.getenv("MAX_INPUT_LENGTH", "1000")) # 최대 입력 길이 | |
| MAX_CONVERSATION_LENGTH = int(os.getenv("MAX_CONVERSATION_LENGTH", "500")) # 최대 대화 길이 | |
| def reset_daily_usage(): | |
| """일일 사용량 초기화""" | |
| today = datetime.now().date() | |
| if api_usage_tracker['last_reset'] < today: | |
| api_usage_tracker['daily_calls'] = 0 | |
| api_usage_tracker['daily_tokens'] = 0 | |
| api_usage_tracker['last_reset'] = today | |
| print(f"📊 일일 API 사용량 초기화됨 (날짜: {today})") | |
| def check_api_limits(session_id: str = "default") -> Tuple[bool, str]: | |
| """API 사용 한도 확인""" | |
| reset_daily_usage() | |
| # 일일 호출 제한 | |
| if api_usage_tracker['daily_calls'] >= API_LIMIT_DAILY_CALLS: | |
| return False, "일일 API 호출 한도에 도달했습니다. 내일 다시 시도해주세요." | |
| # 일일 토큰 제한 | |
| if api_usage_tracker['daily_tokens'] >= API_LIMIT_DAILY_TOKENS: | |
| return False, "일일 API 토큰 한도에 도달했습니다. 내일 다시 시도해주세요." | |
| # 세션별 호출 제한 | |
| if session_id not in api_usage_tracker['session_calls']: | |
| api_usage_tracker['session_calls'][session_id] = 0 | |
| if api_usage_tracker['session_calls'][session_id] >= API_LIMIT_SESSION_CALLS: | |
| return False, "세션 API 호출 한도에 도달했습니다." | |
| return True, "" | |
| def track_api_usage(tokens: int, session_id: str = "default"): | |
| """API 사용량 추적""" | |
| api_usage_tracker['total_calls'] += 1 | |
| api_usage_tracker['daily_calls'] += 1 | |
| api_usage_tracker['total_tokens'] += tokens | |
| api_usage_tracker['daily_tokens'] += tokens | |
| api_usage_tracker['session_calls'][session_id] = api_usage_tracker['session_calls'].get(session_id, 0) + 1 | |
| # 주기적으로 로그 출력 (매 50회마다) | |
| if api_usage_tracker['daily_calls'] % 50 == 0: | |
| print(f"📊 API 사용량: 일일 {api_usage_tracker['daily_calls']}회 호출, {api_usage_tracker['daily_tokens']:,} 토큰") | |
| def sanitize_input(text: str) -> Tuple[str, bool, str]: | |
| """입력 텍스트 검증 및 정제 - 악의적 입력 방지""" | |
| if not text: | |
| return "", False, "입력 텍스트가 비어있습니다." | |
| # 길이 제한 | |
| if len(text) > MAX_INPUT_LENGTH: | |
| return "", False, f"입력이 너무 깁니다 (최대 {MAX_INPUT_LENGTH}자)." | |
| # 프롬프트 주입 시도 감지 | |
| prompt_injection_patterns = [ | |
| '당신은', '역할을 맡아서', 'role:', 'system:', 'ignore all', | |
| 'forget', '무시해', '다른', '새로운', '전문가', | |
| 'translate', '번역', '코드', 'code', 'python' | |
| ] | |
| text_lower = text.lower() | |
| for pattern in prompt_injection_patterns: | |
| # 문장 시작에 패턴이 있는 경우만 체크 | |
| if text_lower.startswith(pattern.lower()) and len(text.split()) > 3: | |
| return "", False, f"적절하지 않은 입력입니다: 시스템 프롬프트로 해석될 수 있는 내용을 감지했습니다." | |
| # 특수 문자 과다 사용 감지 | |
| if len(text) > 0: | |
| special_char_ratio = sum(1 for c in text if not c.isalnum() and c not in ' .,!?' and ord(c) < 128) / len(text) | |
| if special_char_ratio > 0.3: | |
| return "", False, "특수 문자 사용이 과도합니다." | |
| # 연속된 공백 제거 | |
| text = ' '.join(text.split()) | |
| return text, True, "" | |
| def validate_for_echolalia_analysis(text: str) -> Tuple[bool, str]: | |
| """반향어 분석 목적에 맞는 입력인지 검증""" | |
| # 너무 긴 경우 | |
| word_count = len(text.split()) | |
| if word_count > 100: | |
| return False, "발화가 너무 깁니다. 간단한 문장으로 나누어 입력해주세요." | |
| # 너무 짧은 경우 | |
| if len(text.strip()) < 2: | |
| return False, "발화가 너무 짧습니다." | |
| return True, "" | |
| # API 호출 안전성을 위한 데코레이터 (업그레이드 버전) | |
| def safe_api_call_with_limits(session_id: str = "default"): | |
| """API 호출 시 오류를 안전하게 처리하는 데코레이터 (토큰 제한 포함)""" | |
| def decorator(func): | |
| def wrapper(*args, **kwargs): | |
| # 1. API 한도 확인 | |
| can_proceed, limit_msg = check_api_limits(session_id) | |
| if not can_proceed: | |
| print(f"🚫 API 한도 초과: {limit_msg}") | |
| if func.__name__ == "detect_echo_ai": | |
| return False, "", 0.1 | |
| return None | |
| try: | |
| result = func(*args, **kwargs) | |
| # 2. 사용량 추적 (토큰 계산이 복잡하므로 근사값 사용) | |
| # max_tokens가 제공된 경우 사용, 아니면 추정 | |
| max_tokens = kwargs.get('max_tokens', 200) | |
| estimated_tokens = max_tokens * 2 # 입력+출력 추정 | |
| track_api_usage(estimated_tokens, session_id) | |
| return result | |
| except Exception as e: | |
| print(f"❌ API 호출 오류 ({func.__name__}): {e}") | |
| # 기본값 반환 (함수 시그니처에 따라 조정 필요) | |
| if func.__name__ == "detect_echo_ai": | |
| return False, "", 0.1 | |
| return None | |
| return wrapper | |
| return decorator | |
| # 기존 데코레이터 호환성 유지 | |
| def safe_api_call(func): | |
| """API 호출 시 오류를 안전하게 처리하는 데코레이터 (레거시)""" | |
| def wrapper(*args, **kwargs): | |
| try: | |
| return func(*args, **kwargs) | |
| except Exception as e: | |
| print(f"❌ API 호출 오류 ({func.__name__}): {e}") | |
| # 기본값 반환 (함수 시그니처에 따라 조정 필요) | |
| if func.__name__ == "detect_echo_ai": | |
| return False, "", 0.1 | |
| return None | |
| return wrapper | |
| # Assessment service 초기화 | |
| 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]: | |
| """AI 기반 반향어 감지 (룰베이스 폴백 포함)""" | |
| if not text: | |
| return False, "", 0.0 | |
| text = text.strip() | |
| # context가 없어도 텍스트 자체에서 반향어 패턴 감지 | |
| if not context: | |
| context = "" | |
| # 먼저 명확한 룰베이스 패턴 확인 (빠른 처리) | |
| quick_result = detect_echo_rule_based(text, context, situation) | |
| if quick_result[0] and quick_result[2] >= 0.8: # 높은 신뢰도면 바로 반환 | |
| return quick_result | |
| # AI 기반 분석 시도 | |
| try: | |
| ai_result = detect_echo_ai(text, context, situation) | |
| if ai_result[0]: # AI가 반향어로 판단하면 AI 결과 사용 | |
| return ai_result | |
| except Exception as e: | |
| print(f"AI 반향어 감지 오류: {e}") | |
| print("룰베이스 폴백 사용") | |
| # 룰베이스 결과 반환 | |
| return quick_result | |
| def detect_echo_rule_based(text: str, context: str, situation: str = "기타") -> Tuple[bool, str, float]: | |
| """룰베이스 반향어 감지 (빠른 처리용)""" | |
| # 반복 패턴 감지 (쉼표로 구분된 같은 단어 반복) | |
| words = [word.strip() for word in text.split(",")] | |
| if len(words) >= 2: | |
| # 모든 단어가 같은지 확인 | |
| if all(word == words[0] for word in words): | |
| return True, words[0], 0.9 # 높은 신뢰도로 반향어로 판단 | |
| # 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 | |
| return False, "", 0.1 | |
| def detect_echo_ai(text: str, context: str, situation: str = "기타") -> Tuple[bool, str, float]: | |
| """AI 기반 반향어 감지""" | |
| try: | |
| # 반향어 감지 전용 프롬프트 | |
| system_prompt = """당신은 언어치료 전문가입니다. 아동의 발화가 반향어(echolalia)인지 판단해주세요. | |
| 반향어의 특징: | |
| 1. 질문이나 말을 그대로 반복하는 경우 (예: "뭐 했어?" → "뭐 했어?") | |
| 2. 의미 없이 단어를 반복하는 경우 (예: "아이스크림, 아이스크림, 아이스크림") | |
| 3. 상황과 전혀 맞지 않는 말을 하는 경우 | |
| 4. 대화 맥락을 고려하지 않는 발화 | |
| 정상 발화의 특징: | |
| 1. 상황에 맞는 적절한 질문이나 응답 | |
| 2. 의미 있는 의사소통 | |
| 3. 맥락에 맞는 발화 | |
| 중요한 구분: | |
| - "밥 먹었어?" (정상 질문) ≠ "밥 먹었어?" (질문 반복) | |
| - 상황에 맞는 발화는 반향어가 아님 | |
| 판단 기준: | |
| - 완전한 반향어: 질문을 그대로 반복하거나 의미 없는 반복 (신뢰도 0.8-0.9) | |
| - 부분적 반향어: 질문의 일부를 반복하거나 관련 없는 말 (신뢰도 0.5-0.7) | |
| - 정상 발화: 상황에 맞는 적절한 응답이나 질문 (신뢰도 0.1-0.3) | |
| 응답 형식: | |
| IS_ECHO: [true/false] | |
| CONFIDENCE: [0.0-1.0] | |
| ANCHOR_TEXT: [반복된 핵심 단어나 구문] | |
| REASON: [판단 근거]""" | |
| # 상황 정보 추가 | |
| situation_context = f"상황: {situation}" if situation != "기타" else "상황: 일반적인 대화" | |
| user_prompt = f"""다음 발화를 분석해주세요: | |
| 상황/컨텍스트: "{context}" | |
| 아동의 발화: "{text}" | |
| {situation_context} | |
| 분석 기준: | |
| 1. 아동이 상황에 맞는 적절한 질문이나 응답을 했는가? | |
| 2. 아니면 이전에 들은 말을 그대로 반복했는가? | |
| 3. 의미 있는 의사소통인가, 아니면 의미 없는 반복인가? | |
| 위 발화가 반향어인지 판단해주세요.""" | |
| # RAG 검색 및 프롬프트 강화 | |
| rag_used = False | |
| if is_rag_available(): | |
| rag_query = f"{text} {context} 반향어" | |
| enhanced_prompt = enhance_prompt_with_rag(user_prompt, rag_query, top_k=2) | |
| if enhanced_prompt != user_prompt: | |
| rag_used = True | |
| user_prompt = enhanced_prompt | |
| print(f"RAG 사용: 반향어 감지 분석에 RAG 검색 결과 포함 ({rag_query[:50]}...)", flush=True) | |
| if not rag_used: | |
| print("RAG 미사용: 반향어 감지 분석 (RAG 비활성화 또는 검색 결과 없음)", flush=True) | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| max_tokens=200, | |
| temperature=0.3 # 낮은 온도로 일관된 판단 | |
| ) | |
| # 응답 파싱 | |
| content = response.choices[0].message.content | |
| # 결과 파싱 | |
| is_echo = False | |
| confidence = 0.1 | |
| anchor_text = text | |
| reason = "" | |
| lines = content.split('\n') | |
| for line in lines: | |
| line = line.strip() | |
| if line.startswith('IS_ECHO:'): | |
| is_echo = 'true' in line.lower() | |
| elif line.startswith('CONFIDENCE:'): | |
| try: | |
| confidence = float(line.split(':')[1].strip()) | |
| except: | |
| confidence = 0.5 | |
| elif line.startswith('ANCHOR_TEXT:'): | |
| anchor_text = line.split(':', 1)[1].strip().strip('"') | |
| elif line.startswith('REASON:'): | |
| reason = line.split(':', 1)[1].strip() | |
| print(f"🤖 AI 반향어 분석: {is_echo} (신뢰도: {confidence:.2f}) - {reason}") | |
| return is_echo, anchor_text, confidence | |
| except Exception as e: | |
| print(f"❌ AI 반향어 감지 오류: {e}") | |
| # 오류 시 기본값 반환 | |
| return False, text, 0.1 | |
| def shape_echolalia(anchor_text: str, level: str = "L1", situation: str = "기타") -> List[str]: | |
| """반향어를 기능적 의사소통으로 전환 (발화 내용 우선 고려)""" | |
| print(f"DEBUG: shape_echolalia called with anchor_text='{anchor_text}', level='{level}', situation='{situation}'") | |
| if not anchor_text: | |
| print("DEBUG: No anchor_text provided") | |
| return ["입력된 반향어가 없습니다", "텍스트를 다시 확인해주세요", "올바른 입력을 해주세요"] | |
| # 데모용 기본 변환 결과 (OpenAI API 없이도 작동) | |
| demo_responses = { | |
| "밥 먹었어": [ | |
| "L1: 네, 먹었어요", | |
| "L2: 네, 점심을 맛있게 먹었어요", | |
| "L3: 네, 엄마가 만든 김치찌개를 맛있게 먹었어요" | |
| ], | |
| "밥": [ | |
| "L1: 밥 주세요", | |
| "L2: 배고파서 밥을 먹고 싶어요", | |
| "L3: 지금 정말 배가 고파서 맛있는 밥을 먹고 싶어요" | |
| ], | |
| "놀자": [ | |
| "L1: 놀고 싶어요", | |
| "L2: 친구들과 함께 놀고 싶어요", | |
| "L3: 지루해서 재미있는 놀이를 하고 싶어요" | |
| ], | |
| "가자": [ | |
| "L1: 가고 싶어요", | |
| "L2: 공원에 가고 싶어요", | |
| "L3: 날씨가 좋아서 공원에 산책하러 가고 싶어요" | |
| ], | |
| "싫어": [ | |
| "L1: 싫어요", | |
| "L2: 그건 정말 싫어요", | |
| "L3: 그건 정말 싫어서 하고 싶지 않아요" | |
| ], | |
| "아이스크림": [ | |
| "L1: 아이스크림을 먹고 싶어요", | |
| "L2: 밥을 먹고 나서 아이스크림을 먹고 싶어요", | |
| "L3: 지금은 밥을 먹을 시간이니까 나중에 아이스크림을 먹어요" | |
| ] | |
| } | |
| # OpenAI API를 먼저 시도 (API 키가 유효한 경우) | |
| try: | |
| prompt = load_prompt() | |
| # 상황 정보를 포함한 더 구체적인 프롬프트 | |
| situation_context = "" | |
| if situation != "기타": | |
| situation_context = f"\n상황: {situation}" | |
| user_input = f"""반향어: "{anchor_text}"{situation_context} | |
| 레벨: {level} | |
| 위 반향어를 3단계로 변환하세요: | |
| - L1: "{anchor_text}"과 관련된 간단한 요청 | |
| - L2: "{anchor_text}"과 관련된 구체적인 요청 | |
| - L3: "{anchor_text}"과 관련된 감정이나 상태 표현 | |
| 반드시 "{anchor_text}"을 포함한 내용으로 변환하세요.""" | |
| 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 [f"{anchor_text}이 필요해요", f"{anchor_text}을 주세요", f"{anchor_text}을 원해요"] | |
| except Exception as e: | |
| print(f"❌ OpenAI API 오류: {e}") | |
| print("🔄 룰베이스 폴백 시스템 사용") | |
| # 상황별 맞춤 룰베이스 응답 생성 | |
| return generate_rule_based_response(anchor_text, situation, level) | |
| def generate_rule_based_response(anchor_text: str, situation: str, level: str) -> List[str]: | |
| """상황을 고려한 지능적인 룰베이스 응답 생성""" | |
| print(f"DEBUG: generate_rule_based_response called with anchor='{anchor_text}', situation='{situation}', level='{level}'") | |
| # 기본 데모 응답 확인 (더 정교한 패턴 매칭) | |
| demo_responses = { | |
| # 과거형 패턴 (이미 먹었다는 의미) | |
| "밥을 먹었어": [ | |
| "L1: 네, 먹었어요", | |
| "L2: 네, 점심을 맛있게 먹었어요", | |
| "L3: 네, 엄마가 만든 김치찌개를 맛있게 먹었어요" | |
| ], | |
| "밥 먹었어": [ | |
| "L1: 네, 먹었어요", | |
| "L2: 네, 점심을 맛있게 먹었어요", | |
| "L3: 네, 엄마가 만든 김치찌개를 맛있게 먹었어요" | |
| ], | |
| "밥을 먹었다": [ | |
| "L1: 네, 먹었어요", | |
| "L2: 네, 점심을 맛있게 먹었어요", | |
| "L3: 네, 엄마가 만든 김치찌개를 맛있게 먹었어요" | |
| ], | |
| # 현재형/미래형 패턴 (밥을 요청하는 의미) | |
| "밥": [ | |
| "L1: 밥 주세요", | |
| "L2: 배고파서 밥을 먹고 싶어요", | |
| "L3: 지금 정말 배가 고파서 맛있는 밥을 먹고 싶어요" | |
| ], | |
| "밥 먹고 싶어": [ | |
| "L1: 밥 먹고 싶어요", | |
| "L2: 배고파서 밥을 먹고 싶어요", | |
| "L3: 지금 정말 배가 고파서 맛있는 밥을 먹고 싶어요" | |
| ], | |
| "놀자": [ | |
| "L1: 놀고 싶어요", | |
| "L2: 친구들과 함께 놀고 싶어요", | |
| "L3: 지루해서 재미있는 놀이를 하고 싶어요" | |
| ], | |
| "가자": [ | |
| "L1: 가고 싶어요", | |
| "L2: 공원에 가고 싶어요", | |
| "L3: 날씨가 좋아서 공원에 산책하러 가고 싶어요" | |
| ], | |
| "싫어": [ | |
| "L1: 싫어요", | |
| "L2: 그건 정말 싫어요", | |
| "L3: 그건 정말 싫어서 하고 싶지 않아요" | |
| ], | |
| "아이스크림": [ | |
| "L1: 아이스크림을 먹고 싶어요", | |
| "L2: 밥을 먹고 나서 아이스크림을 먹고 싶어요", | |
| "L3: 지금은 밥을 먹을 시간이니까 나중에 아이스크림을 먹어요" | |
| ] | |
| } | |
| # 더 지능적인 패턴 매칭 (정확한 매칭 우선) | |
| for pattern, responses in demo_responses.items(): | |
| if pattern == anchor_text.strip(): # 정확한 매칭 우선 | |
| print(f"DEBUG: Using exact demo response for pattern '{pattern}'") | |
| return responses | |
| # 부분 매칭 (정확한 매칭이 없을 때) | |
| for pattern, responses in demo_responses.items(): | |
| if pattern in anchor_text: | |
| print(f"DEBUG: Using partial demo response for pattern '{pattern}'") | |
| return responses | |
| # 상황별 템플릿 기반 응답 생성 (더 세분화) | |
| situation_templates = { | |
| "식사 시간": { | |
| "food_related": [ | |
| f"L1: {anchor_text}을 먹고 싶어요", | |
| f"L2: 밥을 먹고 나서 {anchor_text}을 먹고 싶어요", | |
| f"L3: 지금은 밥을 먹을 시간이니까 나중에 {anchor_text}을 먹어요" | |
| ], | |
| "non_food": [ | |
| f"L1: {anchor_text}을 하고 싶어요", | |
| f"L2: 밥을 먹고 나서 {anchor_text}을 하고 싶어요", | |
| f"L3: 지금은 밥을 먹을 시간이니까 나중에 {anchor_text}을 해요" | |
| ], | |
| "past_tense": [ # 과거형 패턴 (이미 먹었다는 의미) | |
| f"L1: 네, {anchor_text}", | |
| f"L2: 네, 맛있게 {anchor_text}", | |
| f"L3: 네, 정말 맛있게 {anchor_text}" | |
| ] | |
| }, | |
| "놀이 시간": [ | |
| f"L1: {anchor_text}을 하고 싶어요", | |
| f"L2: 친구들과 함께 {anchor_text}을 하고 싶어요", | |
| f"L3: 지루해서 재미있는 {anchor_text}을 하고 싶어요" | |
| ], | |
| "수업 시간": [ | |
| f"L1: {anchor_text}을 배우고 싶어요", | |
| f"L2: 선생님과 함께 {anchor_text}을 공부하고 싶어요", | |
| f"L3: {anchor_text}을 잘 배워서 나중에 써먹고 싶어요" | |
| ], | |
| "기타": [ | |
| f"L1: {anchor_text}이 필요해요", | |
| f"L2: {anchor_text}을 구체적으로 요청해요", | |
| f"L3: {anchor_text}에 대한 감정을 표현해요" | |
| ] | |
| } | |
| # 상황별 템플릿 선택 (과거형 감지 포함) | |
| if situation in situation_templates: | |
| if situation == "식사 시간": | |
| # 과거형 패턴 감지 (이미 먹었다는 의미) | |
| past_tense_patterns = ["먹었어", "먹었다", "먹었어요", "먹었습니다"] | |
| if any(pattern in anchor_text for pattern in past_tense_patterns): | |
| print(f"DEBUG: Detected past tense pattern in '{anchor_text}'") | |
| return situation_templates[situation]["past_tense"] | |
| # 음식 관련 키워드 확인 | |
| food_keywords = ["밥", "음식", "먹", "아이스크림", "사탕", "과자", "우유", "물"] | |
| if any(keyword in anchor_text for keyword in food_keywords): | |
| return situation_templates[situation]["food_related"] | |
| else: | |
| return situation_templates[situation]["non_food"] | |
| else: | |
| return situation_templates[situation] | |
| else: | |
| return situation_templates["기타"] | |
| 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_situation: str, situation: str = "기타", profile_id: int = None, analysis_mode: str = "detailed", chat_state: dict = None) -> Tuple[bool, str, str, str, str, str, str]: | |
| """텍스트 처리: 반향어 감지 및 전환 (Phase 1 업데이트: 빠른/상세 모드 지원)""" | |
| print(f"DEBUG: process_text called with text='{text}', context_situation='{context_situation}', situation='{situation}', profile_id={profile_id}, analysis_mode={analysis_mode}") | |
| # ===== 보안 검증 ===== | |
| # 1. 입력 텍스트 정제 및 검증 | |
| sanitized_text, is_valid, error_msg = sanitize_input(text) | |
| if not is_valid: | |
| error_html = f"<div style='background: #fee; border: 2px solid #f44; border-radius: 8px; padding: 20px; margin: 15px 0; text-align: center;'><h3>❌ 입력 오류</h3><p>{error_msg}</p></div>" | |
| return False, error_html, "", "", "", "", 0 | |
| # 2. 반향어 분석 목적 검증 | |
| is_valid_analysis, analysis_error_msg = validate_for_echolalia_analysis(text) | |
| if not is_valid_analysis: | |
| error_html = f"<div style='background: #fee; border: 2px solid #f44; border-radius: 8px; padding: 20px; margin: 15px 0; text-align: center;'><h3>❌ 입력 오류</h3><p>{analysis_error_msg}</p></div>" | |
| return False, error_html, "", "", "", "", 0 | |
| # 정제된 텍스트 사용 | |
| text = sanitized_text | |
| # ===== 채팅 모드에서 수집한 정보 통합 ===== | |
| if chat_state and isinstance(chat_state, dict) and chat_state.get('collected_info'): | |
| collected_info = chat_state.get('collected_info', {}) | |
| # 수집한 정보를 context_situation에 추가 | |
| if collected_info and isinstance(collected_info, dict): | |
| info_parts = [] | |
| # collected_info의 키-값을 처리하여 Phase 1에 전달할 컨텍스트 구성 | |
| for key, value in collected_info.items(): | |
| if value and str(value).strip(): | |
| # 키가 Enum인 경우 value를 사용, 아니면 그대로 사용 | |
| key_str = key.value if hasattr(key, 'value') else str(key) | |
| # 값이 문자열인 경우 그대로 사용 | |
| value_str = str(value).strip() | |
| if value_str: | |
| # 키에 따라 적절한 형식으로 추가 | |
| if key_str == 'context_situation': | |
| # context_situation은 직접 통합 | |
| if not context_situation or context_situation == "": | |
| context_situation = value_str | |
| else: | |
| context_situation = f"{context_situation} | {value_str}" | |
| elif key_str == 'previous_utterance': | |
| # 이전 발화는 별도로 표시 | |
| info_parts.append(f"이전 발화: {value_str}") | |
| elif key_str == 'child_age': | |
| info_parts.append(f"아이 나이: {value_str}") | |
| elif key_str == 'vocabulary_level': | |
| info_parts.append(f"어휘 수준: {value_str}") | |
| elif key_str == 'emotional_state': | |
| info_parts.append(f"감정 상태: {value_str}") | |
| else: | |
| # 기타 정보 | |
| info_parts.append(f"{key_str}: {value_str}") | |
| # 추가 정보가 있으면 context_situation에 통합 | |
| if info_parts: | |
| additional_context = " | ".join(info_parts) | |
| if context_situation: | |
| context_situation = f"{context_situation} | {additional_context}" | |
| else: | |
| context_situation = additional_context | |
| print(f"DEBUG: 채팅 모드 정보 통합됨 - context_situation: {context_situation}") | |
| # ===== 새로운 3단계 Phase 파이프라인 사용 ===== | |
| try: | |
| # 새로운 파이프라인으로 분석 수행 | |
| is_echo, confidence_html, quick_summary_text, three_step_text, detailed_report, save_message, analysis_id = process_text_with_pipeline( | |
| text, context_situation, situation, profile_id, analysis_mode | |
| ) | |
| # 분석 결과 저장 (프로필이 선택된 경우에만, analysis_id가 None인 경우) | |
| if profile_id and analysis_id is None: | |
| # 파이프라인 결과에서 anchor 추출 (기존 로직 유지) | |
| anchor = text # 기본값 | |
| candidates = [] # 3단계 변환 제거 | |
| confidence_percent = 90.0 if is_echo else 30.0 # 기본값 (실제로는 파이프라인에서 가져와야 함) | |
| input_summary, analysis_summary, key_recommendations = create_analysis_summary( | |
| text, is_echo, confidence_percent, anchor, "", candidates | |
| ) | |
| # 분석 결과 저장하고 ID 받기 | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| INSERT INTO analysis_history | |
| (profile_id, analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| ''', (profile_id, 'text', input_summary, context_situation, is_echo, | |
| confidence_percent, anchor, analysis_summary, key_recommendations)) | |
| analysis_id = cursor.lastrowid | |
| conn.commit() | |
| conn.close() | |
| print(f"DEBUG: Analysis result saved for profile {profile_id} with ID {analysis_id}") | |
| print(f"DEBUG: Final result - is_echo={is_echo}, analysis_id={analysis_id}") | |
| return is_echo, confidence_html, quick_summary_text, three_step_text, detailed_report, save_message, analysis_id | |
| except Exception as e: | |
| # 파이프라인 오류 시 기존 로직으로 폴백 | |
| print(f"⚠️ 새로운 파이프라인 오류: {e}, 기존 로직으로 폴백") | |
| import traceback | |
| traceback.print_exc() | |
| # 기존 로직 (폴백) | |
| profile_context = "" | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| profile_context = f"아이 정보: {profile['child_name']}({profile['age']}세), 어휘능력 {profile['vocabulary_level']}" | |
| if profile['language_habits']: | |
| profile_context += f", 언어습관: {profile['language_habits']}" | |
| if latest_cars: | |
| profile_context += f", CARS 점수: {latest_cars['total_score']}점 ({latest_cars['diagnosis']})" | |
| combined_context = profile_context if profile_context else "" | |
| if context_situation: | |
| combined_context += f" | 상황: {context_situation}" if combined_context else f"상황: {context_situation}" | |
| is_echo, anchor, confidence = detect_echo(text, combined_context, situation) | |
| confidence_percent = confidence * 100 | |
| if is_echo: | |
| candidates = shape_echolalia(anchor, "L1", situation) | |
| else: | |
| candidates = shape_echolalia(text, "L1", situation) | |
| analysis_id = None | |
| if profile_id: | |
| input_summary, analysis_summary, key_recommendations = create_analysis_summary( | |
| text, is_echo, confidence_percent, anchor, "", candidates | |
| ) | |
| conn = sqlite3.connect('echolalia_assistant.db') | |
| cursor = conn.cursor() | |
| cursor.execute(''' | |
| INSERT INTO analysis_history | |
| (profile_id, analysis_type, input_summary, context_situation, is_echo, | |
| confidence, anchor_text, analysis_summary, key_recommendations) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) | |
| ''', (profile_id, 'text', input_summary, context_situation, is_echo, | |
| confidence_percent, anchor, analysis_summary, key_recommendations)) | |
| analysis_id = cursor.lastrowid | |
| conn.commit() | |
| conn.close() | |
| if analysis_mode == "quick": | |
| latest_cars = None | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| confidence_html = generate_confidence_display_html(is_echo, confidence_percent) | |
| quick_summary_text = generate_quick_summary(text, is_echo, confidence_percent, context_situation, latest_cars) | |
| three_step_text = "" # 3단계 변환 제거 | |
| detailed_report = "" | |
| else: | |
| detailed_report = generate_enhanced_analysis_report(text, context_situation, "", situation, is_echo, confidence_percent, anchor, candidates, profile_id, analysis_id) | |
| latest_cars = None | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| confidence_html = generate_confidence_display_html(is_echo, confidence_percent) | |
| quick_summary_text = generate_quick_summary(text, is_echo, confidence_percent, context_situation, latest_cars) | |
| three_step_text = "" # 3단계 변환 제거 | |
| save_message = "분석 결과가 프로필에 저장되었습니다." if profile_id else "프로필을 선택하면 분석 결과가 자동으로 저장됩니다." | |
| return is_echo, confidence_html, quick_summary_text, three_step_text, detailed_report, save_message, analysis_id | |
| def convert_confidence_to_level(confidence: float) -> str: | |
| """신뢰도를 3단계로 변환""" | |
| if confidence >= 70: | |
| return "높은 수준" | |
| elif confidence >= 40: | |
| return "보통 수준" | |
| else: | |
| return "낮은 수준" | |
| def generate_confidence_display_html(is_echo: bool, confidence: float) -> str: | |
| """Phase 1: 신뢰도 색상 코딩된 HTML 생성""" | |
| confidence_level = convert_confidence_to_level(confidence) | |
| # 신뢰도에 따른 색상 결정 | |
| # confidence는 "반향어로 판정될 확률" (0-100%) | |
| if confidence >= 70: | |
| color = "#ef4444" # 빨간색 | |
| bg_color = "#fee2e2" # 연한 빨강 | |
| icon = "⚠️" | |
| urgency = "관찰 필요" | |
| interpretation = "반향어 가능성이 높습니다" | |
| elif confidence >= 40: | |
| color = "#f59e0b" # 주황색 | |
| bg_color = "#fef3c7" # 연한 주황 | |
| icon = "📊" | |
| urgency = "지속 관찰" | |
| interpretation = "부분적 반향어 특성" | |
| else: | |
| color = "#10b981" # 초록색 | |
| bg_color = "#d1fae5" # 연한 초록 | |
| icon = "✅" | |
| urgency = "양호" | |
| interpretation = "정상적인 의사소통" | |
| # 불확실성 경고 (신뢰도 < 70%) | |
| uncertainty_warning = "" | |
| if confidence < 70 and confidence >= 40: | |
| uncertainty_warning = """ | |
| <div style='background: #fffbeb; border: 2px solid #fbbf24; border-radius: 8px; padding: 12px; margin-top: 10px;'> | |
| <strong>🔍 검증 권장</strong>: 신뢰도가 중간 수준입니다. 보호자 검증이 필요할 수 있습니다. | |
| </div> | |
| """ | |
| elif confidence < 40: | |
| uncertainty_warning = "" | |
| html = f""" | |
| <div style='background: {bg_color}; border: 2px solid {color}; border-radius: 12px; padding: 20px; margin: 15px 0; text-align: center;'> | |
| <div style='font-size: 48px; margin-bottom: 10px;'>{icon}</div> | |
| <h2 style='color: {color}; margin: 5px 0;'>{confidence:.0f}%</h2> | |
| <p style='color: {color}; font-size: 1.2rem; margin: 5px 0;'><strong>{confidence_level}</strong> - {urgency}</p> | |
| <p style='margin-top: 15px; color: #333;'> | |
| {interpretation if is_echo else '정상적인 의사소통으로 보입니다'} | |
| </p> | |
| {uncertainty_warning} | |
| </div> | |
| """ | |
| return html | |
| def generate_quick_summary(text: str, is_echo: bool, confidence: float, context: str, latest_cars: dict = None) -> str: | |
| """Phase 1: 빠른 분석 결과 요약 생성 (CARS 점수 기반 임계값 사용)""" | |
| if not is_echo: | |
| return f""" | |
| ### ✅ 분석 결과 요약 | |
| **아이의 말**: "{text}" | |
| 정상적인 의사소통 패턴으로 보입니다. 🎉 | |
| """ | |
| # CARS 점수에 따른 임계값 결정 (generate_personalized_analysis와 동일한 로직) | |
| if latest_cars: | |
| cars_score = latest_cars['total_score'] | |
| if cars_score >= 37: # 중증 자폐 | |
| threshold_high = 80 | |
| threshold_medium = 60 | |
| elif cars_score >= 30: # 경증-중간 자폐 | |
| threshold_high = 70 | |
| threshold_medium = 50 | |
| else: # 정상 또는 미검사 | |
| threshold_high = 60 | |
| threshold_medium = 40 | |
| else: | |
| # CARS 점수 없을 때는 기본값 (빠른 요약과 일치) | |
| threshold_high = 70 | |
| threshold_medium = 40 | |
| # 반향어인 경우 | |
| if confidence >= threshold_high: | |
| interpretation = "반향어 가능성이 높습니다. 전문가 상담을 권장합니다." | |
| action = "💡 **권장사항**: 전문가와 함께 대화 접근 방식을 논의하세요." | |
| elif confidence >= threshold_medium: | |
| interpretation = "부분적 반향어 특성이 관찰됩니다. 지속적인 관찰이 필요합니다." | |
| action = "💡 **권장사항**: 가정에서 안정감 있는 환경을 만들어주고, 반복에 대해 긍정적으로 반응하세요." | |
| else: | |
| interpretation = "일부 반복 패턴이 있지만 전반적으로 양호합니다." | |
| action = "💡 **권장사항**: 지속적인 관찰을 유지하세요." | |
| return f""" | |
| ### {'⚠️' if confidence >= threshold_high else '📊'} 분석 결과 요약 | |
| **아이의 말**: "{text}" | |
| {interpretation} | |
| {action} | |
| """ | |
| def generate_three_step_conversion_highlight(candidates: List[str], is_echo: bool) -> str: | |
| """3단계 변환 제거됨""" | |
| return "" | |
| def _calculate_advanced_confidence_internal(text: str, context_situation: str, profile: dict = None): | |
| """고급 신뢰도 계산 시스템 - 실제 반향어 특성을 기반으로 한 다차원 분석 (내부 함수)""" | |
| import re | |
| import math | |
| # 기본 신뢰도 점수들 | |
| scores = { | |
| 'linguistic_patterns': 0.0, | |
| 'contextual_fit': 0.0, | |
| 'developmental_appropriateness': 0.0, | |
| 'repetition_analysis': 0.0, | |
| 'semantic_coherence': 0.0 | |
| } | |
| words = text.split() | |
| text_length = len(words) | |
| # 1. 언어학적 패턴 분석 (실제 반향어 특성 기반) | |
| # 단어 반복 패턴 감지 | |
| if text_length > 1: | |
| word_counts = {} | |
| for word in words: | |
| word_counts[word] = word_counts.get(word, 0) + 1 | |
| # 같은 단어가 2번 이상 반복되는 경우 | |
| max_repetition = max(word_counts.values()) | |
| if max_repetition >= 2: | |
| scores['linguistic_patterns'] = min(0.8, max_repetition * 0.3) | |
| # 질문 반향어 패턴 (질문이 아닌데 ?로 끝나는 경우) | |
| if text.endswith('?') and not any(q_word in text for q_word in ['어떻게', '왜', '언제', '어디', '누구', '무엇']): | |
| scores['linguistic_patterns'] += 0.6 | |
| # 대명사 역전 패턴 (간단한 휴리스틱) | |
| if any(pronoun in text for pronoun in ['나', '너', '그', '그녀']) and text_length <= 3: | |
| scores['linguistic_patterns'] += 0.4 | |
| # 2. 맥락 적합성 분석 (상황과 발화의 일치성) | |
| context_keywords = { | |
| '식사 시간': ['밥', '먹', '맛', '배고', '음식', '식사', '점심', '저녁'], | |
| '놀이 시간': ['놀', '장난', '게임', '재미', '놀이', '친구', '만들기'], | |
| '외출 준비': ['나가', '옷', '신발', '가방', '준비', '외출'], | |
| '수업 시간': ['공부', '책', '숙제', '선생님', '학교', '학습'], | |
| '휴식 시간': ['쉬', '잠', '휴식', '편안', '조용'] | |
| } | |
| # 상황별 키워드 매칭 | |
| context_match_score = 0.0 | |
| for situation, keywords in context_keywords.items(): | |
| if situation in context_situation: | |
| keyword_matches = sum(1 for keyword in keywords if keyword in text) | |
| context_match_score = min(0.8, keyword_matches * 0.3) | |
| break | |
| scores['contextual_fit'] = context_match_score | |
| # 3. 발달 적절성 분석 (연령대별 적절한 발화 수준) | |
| if profile and profile.get('age'): | |
| age = profile['age'] | |
| if age <= 3: | |
| # 3세 이하: 짧고 단순한 발화가 정상 | |
| if text_length <= 3: | |
| scores['developmental_appropriateness'] = 0.2 # 정상 | |
| else: | |
| scores['developmental_appropriateness'] = 0.7 # 비정상적으로 복잡 | |
| elif age <= 6: | |
| # 3-6세: 중간 길이 발화가 적절 | |
| if 2 <= text_length <= 6: | |
| scores['developmental_appropriateness'] = 0.2 # 정상 | |
| else: | |
| scores['developmental_appropriateness'] = 0.6 # 부적절 | |
| else: | |
| # 6세 이상: 더 복잡한 발화가 정상 | |
| if text_length >= 3: | |
| scores['developmental_appropriateness'] = 0.2 # 정상 | |
| else: | |
| scores['developmental_appropriateness'] = 0.7 # 너무 단순 | |
| # 4. 반복 분석 (단어/구문 반복 패턴) | |
| if text_length > 1: | |
| unique_words = len(set(words)) | |
| repetition_ratio = 1 - (unique_words / text_length) | |
| # 반복 비율이 높을수록 반향어 가능성 증가 | |
| if repetition_ratio > 0.5: # 50% 이상 반복 | |
| scores['repetition_analysis'] = 0.8 | |
| elif repetition_ratio > 0.3: # 30% 이상 반복 | |
| scores['repetition_analysis'] = 0.6 | |
| elif repetition_ratio > 0.1: # 10% 이상 반복 | |
| scores['repetition_analysis'] = 0.3 | |
| else: | |
| scores['repetition_analysis'] = 0.1 | |
| # 5. 의미적 일관성 분석 (문장의 의미적 완성도) | |
| if text_length > 1: | |
| # 문장이 완전한 의미를 가지는지 간단한 휴리스틱 체크 | |
| has_verb = any(word.endswith(('다', '어', '아', '해', '해요', '합니다')) for word in words) | |
| has_noun = any(len(word) > 1 for word in words) # 단순한 명사 체크 | |
| if has_verb and has_noun: | |
| scores['semantic_coherence'] = 0.2 # 의미적으로 완성된 문장 | |
| elif has_noun: | |
| scores['semantic_coherence'] = 0.5 # 부분적으로 의미 있음 | |
| else: | |
| scores['semantic_coherence'] = 0.8 # 의미가 불명확하거나 단순 반복 | |
| # 가중 평균으로 최종 신뢰도 계산 | |
| weights = { | |
| 'linguistic_patterns': 0.35, # 언어학적 패턴 (가장 중요) | |
| 'repetition_analysis': 0.25, # 반복 분석 | |
| 'semantic_coherence': 0.20, # 의미적 일관성 | |
| 'contextual_fit': 0.15, # 맥락 적합성 | |
| 'developmental_appropriateness': 0.05 # 발달 적절성 (보조적) | |
| } | |
| final_confidence = sum(scores[key] * weights[key] for key in scores.keys()) | |
| result = { | |
| 'confidence': min(final_confidence, 1.0), | |
| 'detailed_scores': scores, | |
| 'confidence_level': convert_confidence_to_level(final_confidence * 100), | |
| 'analysis_quality': 'high' if final_confidence > 0.7 else 'medium' if final_confidence > 0.4 else 'low' | |
| } | |
| return result | |
| def create_analysis_summary(text: str, is_echo: bool, confidence: float, anchor: str, | |
| analysis_report: str, candidates: List[str]) -> tuple: | |
| """분석 결과를 요약하여 저장용 데이터 생성""" | |
| import json | |
| # 입력 요약 (20자 이내) | |
| if len(text) > 20: | |
| input_summary = text[:17] + "..." | |
| else: | |
| input_summary = text | |
| # 분석 요약 (핵심 결과만) | |
| if is_echo: | |
| analysis_summary = f"반향어 감지됨 (신뢰도: {confidence:.1f}%) - 앵커: '{anchor}'" | |
| else: | |
| analysis_summary = f"정상 발화로 판단됨 (신뢰도: {confidence:.1f}%)" | |
| # 주요 권장사항 추출 (상위 3개만) | |
| key_recommendations = [] | |
| if candidates: | |
| key_recommendations = candidates[:3] | |
| return input_summary, analysis_summary, json.dumps(key_recommendations, ensure_ascii=False) | |
| def generate_context_analysis(text: str, context_situation: str, context_previous: str, profile: dict) -> str: | |
| """AI 기반 맥락 분석""" | |
| try: | |
| # 프로필 정보 구성 | |
| profile_info = "" | |
| if profile: | |
| profile_info = f""" | |
| 아동 정보: | |
| - 이름: {profile.get('child_name', '미지정')} | |
| - 나이: {profile.get('age', '미지정')}세 | |
| - 어휘능력: {profile.get('vocabulary_level', '미지정')} | |
| - 언어습관: {profile.get('language_habits', '미지정')}""" | |
| system_prompt = """당신은 언어치료 전문가입니다. 아동의 발화와 주어진 맥락을 분석하여 발화의 의미와 의도를 파악해주세요. | |
| 분석 기준: | |
| 1. 발화가 주어진 상황에 적절한가? | |
| 2. 이전 발화와의 연관성이 있는가? | |
| 3. 아동의 나이와 발달 수준에 맞는가? | |
| 4. 의사소통 의도가 명확한가? | |
| 응답 형식: | |
| CONTEXT_ANALYSIS: [맥락 분석 결과] | |
| COMMUNICATION_INTENT: [의사소통 의도] | |
| SITUATION_APPROPRIATENESS: [상황 적절성] | |
| DEVELOPMENTAL_LEVEL: [발달 수준 평가]""" | |
| user_prompt = f"""다음 발화와 맥락을 분석해주세요: | |
| 발화: "{text}" | |
| 상황: "{context_situation}" | |
| 이전 발화: "{context_previous}" | |
| {profile_info} | |
| 위 정보를 바탕으로 발화의 맥락과 의도를 분석해주세요.""" | |
| # RAG 검색 및 프롬프트 강화 | |
| rag_used = False | |
| if is_rag_available(): | |
| rag_query = f"{text} {context_situation} 맥락 분석" | |
| enhanced_prompt = enhance_prompt_with_rag(user_prompt, rag_query, top_k=2) | |
| if enhanced_prompt != user_prompt: | |
| rag_used = True | |
| user_prompt = enhanced_prompt | |
| print(f"RAG 사용: 맥락 분석에 RAG 검색 결과 포함 ({rag_query[:50]}...)", flush=True) | |
| if not rag_used: | |
| print("RAG 미사용: 맥락 분석 (RAG 비활성화 또는 검색 결과 없음)", flush=True) | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| max_tokens=300, | |
| temperature=0.3 | |
| ) | |
| content = response.choices[0].message.content | |
| # 응답 파싱 | |
| context_analysis = "" | |
| communication_intent = "" | |
| situation_appropriateness = "" | |
| developmental_level = "" | |
| lines = content.split('\n') | |
| for line in lines: | |
| line = line.strip() | |
| if line.startswith('CONTEXT_ANALYSIS:'): | |
| context_analysis = line.split(':', 1)[1].strip() | |
| elif line.startswith('COMMUNICATION_INTENT:'): | |
| communication_intent = line.split(':', 1)[1].strip() | |
| elif line.startswith('SITUATION_APPROPRIATENESS:'): | |
| situation_appropriateness = line.split(':', 1)[1].strip() | |
| elif line.startswith('DEVELOPMENTAL_LEVEL:'): | |
| developmental_level = line.split(':', 1)[1].strip() | |
| return f""" | |
| **맥락 분석**: {context_analysis} | |
| **의사소통 의도**: {communication_intent} | |
| **상황 적절성**: {situation_appropriateness} | |
| **발달 수준**: {developmental_level}""" | |
| except Exception as e: | |
| print(f"❌ AI 맥락 분석 오류: {e}") | |
| return f""" | |
| **맥락 분석**: 주어진 상황 '{context_situation}'에서의 발화로 보입니다. | |
| **의사소통 의도**: 상황에 맞는 의사소통을 시도한 것으로 판단됩니다. | |
| **상황 적절성**: 일반적인 상황에서 관찰되는 발화 패턴입니다. | |
| **발달 수준**: 현재 발달 단계에 적합한 의사소통 수준으로 보입니다.""" | |
| def save_user_feedback(analysis_id: int, user_rating: int, feedback_text: str = "", user_session_id: str = ""): | |
| """사용자 피드백 저장 함수 (PPO 학습용)""" | |
| try: | |
| profile_manager.save_user_feedback(analysis_id, user_rating, feedback_text, user_session_id) | |
| return "✅ 피드백이 성공적으로 저장되었습니다. 감사합니다!" | |
| except Exception as e: | |
| return f"❌ 피드백 저장 중 오류가 발생했습니다: {str(e)}" | |
| def get_user_feedback_summary(analysis_id: int): | |
| """분석에 대한 사용자 피드백 요약 생성""" | |
| try: | |
| feedbacks = profile_manager.get_user_feedback_for_analysis(analysis_id) | |
| if not feedbacks: | |
| return "아직 피드백이 없습니다." | |
| # 피드백 통계 계산 | |
| total = len(feedbacks) | |
| avg_rating = sum(f['user_rating'] for f in feedbacks) / total | |
| summary = f"""## 📊 사용자 피드백 요약 (총 {total}개) | |
| ### ⭐ 평균 만족도 | |
| **{avg_rating:.1f}/5.0** {get_rating_stars(avg_rating)} | |
| ### 📈 만족도 분포 | |
| """ | |
| # 만족도별 분포 계산 | |
| rating_counts = {i: 0 for i in range(1, 6)} | |
| for feedback in feedbacks: | |
| rating_counts[feedback['user_rating']] += 1 | |
| for rating in range(5, 0, -1): | |
| count = rating_counts[rating] | |
| percentage = (count / total * 100) if total > 0 else 0 | |
| stars = "⭐" * rating | |
| summary += f"- {stars} ({rating}점): {count}개 ({percentage:.1f}%)\n" | |
| # 최근 피드백 표시 | |
| summary += "\n### 💬 최근 피드백\n" | |
| for feedback in feedbacks[:3]: | |
| stars = "⭐" * feedback['user_rating'] | |
| summary += f"**{stars} ({feedback['user_rating']}점)** ({feedback['created_at'][:10]})\n" | |
| if feedback['feedback_text']: | |
| summary += f"- {feedback['feedback_text'][:100]}{'...' if len(feedback['feedback_text']) > 100 else ''}\n" | |
| return summary | |
| except Exception as e: | |
| return f"❌ 피드백 요약 생성 중 오류가 발생했습니다: {str(e)}" | |
| def get_rating_stars(rating): | |
| """평점에 따른 별표 반환""" | |
| full_stars = int(rating) | |
| half_star = 1 if rating - full_stars >= 0.5 else 0 | |
| empty_stars = 5 - full_stars - half_star | |
| stars = "⭐" * full_stars | |
| if half_star: | |
| stars += "⭐" | |
| stars += "☆" * empty_stars | |
| return stars | |
| def generate_enhanced_analysis_report(text: str, context_situation: str, context_previous: str, | |
| situation: str, is_echo: bool, confidence: float, anchor: str, | |
| candidates: List[str], profile_id: int = None, analysis_id: int = None) -> str: | |
| """향상된 분석 보고서 생성 (다차원 분석 포함)""" | |
| # 프로필 정보 및 CARS 검사 결과 가져오기 | |
| profile = None | |
| latest_cars = None | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| # 고급 신뢰도 계산 | |
| advanced_confidence = _calculate_advanced_confidence_internal(text, context_situation, profile) | |
| # AI 기반 맥락 분석 | |
| context_analysis = generate_context_analysis(text, context_situation, context_previous, profile) | |
| # 시각적 분석 차트 생성 | |
| visual_analysis = generate_visual_analysis(advanced_confidence['detailed_scores']) | |
| # 개인화된 권장사항 생성 | |
| personalized_recommendations = generate_personalized_recommendations( | |
| text, is_echo, confidence, profile, latest_cars, advanced_confidence | |
| ) | |
| # 발달 단계별 분석 | |
| developmental_analysis = generate_developmental_analysis(text, profile, is_echo) | |
| # 진행 상황 추적 | |
| progress_tracking = generate_progress_tracking(profile_id, is_echo, confidence) | |
| # 실시간 품질 피드백 (내부적으로 사용, 보고서에는 필터링된 버전 사용) | |
| quality_feedback_full = generate_quality_feedback(text, confidence, advanced_confidence['analysis_quality']) | |
| # 사용자에게는 "낮은 신뢰도" 메시지를 숨김 (시스템 내부적으로는 유지) | |
| quality_feedback = quality_feedback_full.replace("🔍 **분석 결과**: 낮은 신뢰도입니다. 더 구체적인 맥락 정보를 제공해보세요.\n\n", "") | |
| # 입력 품질 검증 | |
| input_validation = _validate_input_quality_internal(text, context_situation) | |
| # RAG 검색 수행 및 사용된 분석 단계 추적 (보고서에 표시용) | |
| rag_results = [] | |
| rag_queries_used = [] | |
| rag_used_in_sections = [] # RAG가 사용된 보고서 섹션 추적 | |
| if is_rag_available(): | |
| # 반향어 감지 관련 RAG 검색 (이미 detect_echo_ai에서 수행됨, 여기서는 보고서 표시용으로 재검색) | |
| echo_query = f"{text} {context_situation} 반향어" | |
| echo_results = search_rag_internal(echo_query, top_k=2) | |
| if echo_results: | |
| rag_results.extend(echo_results) | |
| rag_queries_used.append(echo_query) | |
| rag_used_in_sections.append("반향어 감지 및 신뢰도 평가") | |
| # 맥락 분석 관련 RAG 검색 (이미 generate_context_analysis에서 수행됨, 여기서는 보고서 표시용으로 재검색) | |
| context_query = f"{text} {context_situation} 맥락 분석" | |
| context_results = search_rag_internal(context_query, top_k=2) | |
| if context_results: | |
| # 중복 제거 (같은 문서는 한 번만 표시) | |
| existing_ids = {r['id'] for r in rag_results} | |
| for result in context_results: | |
| if result['id'] not in existing_ids: | |
| rag_results.append(result) | |
| if context_query not in rag_queries_used: | |
| rag_queries_used.append(context_query) | |
| rag_used_in_sections.append("맥락 분석 및 의사소통 의도 파악") | |
| # 새로운 3단 구조 보고서 생성 (analysis_id 포함, RAG 정보 전달) | |
| report = generate_three_stage_report( | |
| text, context_situation, context_previous, situation, | |
| is_echo, confidence, profile, latest_cars, advanced_confidence, | |
| quality_feedback, visual_analysis, context_analysis, | |
| developmental_analysis, personalized_recommendations, | |
| progress_tracking, input_validation, anchor, candidates, analysis_id, | |
| rag_results=rag_results, rag_queries_used=rag_queries_used, | |
| rag_used_in_sections=rag_used_in_sections | |
| ) | |
| # RAG 사용 여부 터미널 출력 | |
| print("\n" + "=" * 60, flush=True) | |
| print("📊 보고서 생성 완료 - RAG 사용 정보", flush=True) | |
| print("=" * 60, flush=True) | |
| if is_rag_available(): | |
| if rag_results: | |
| rag_status = "✅ RAG 사용됨" | |
| print(f"RAG 상태: {rag_status}", flush=True) | |
| print(f"\n📋 RAG가 사용된 보고서 섹션:", flush=True) | |
| for i, section in enumerate(rag_used_in_sections, 1): | |
| print(f" {i}. {section}", flush=True) | |
| print(f"\n🔍 RAG 검색 결과: {len(rag_results)}개 문서 발견", flush=True) | |
| for i, result in enumerate(rag_results, 1): | |
| source = result.get('metadata', {}).get('filename', '알 수 없음') | |
| similarity = result.get('similarity', 0.0) | |
| print(f" {i}. {source} (유사도: {similarity:.3f})", flush=True) | |
| if rag_queries_used: | |
| print(f"\n🔎 검색 쿼리: {len(rag_queries_used)}개", flush=True) | |
| for i, query in enumerate(rag_queries_used, 1): | |
| print(f" 쿼리 {i}: {query[:60]}...", flush=True) | |
| print(f"\n💡 참고: 위 섹션들의 분석 내용은 RAG 검색 결과를 기반으로 생성되었습니다.", flush=True) | |
| else: | |
| print("RAG 상태: ⚠️ RAG는 활성화되어 있으나 검색 결과가 없습니다.", flush=True) | |
| else: | |
| print("RAG 상태: ❌ RAG가 비활성화되어 있습니다.", flush=True) | |
| print("=" * 60 + "\n", flush=True) | |
| return report | |
| def generate_visual_analysis(detailed_scores: dict) -> str: | |
| """실제 알고리즘 기반 시각적 분석 차트 생성""" | |
| scores = detailed_scores | |
| # 실제 점수 기반 차트 생성 | |
| chart = "```\n" | |
| chart += "📊 다차원 분석 결과 (0.0 ~ 1.0)\n" | |
| chart += "=" * 50 + "\n" | |
| # 각 점수별로 실제 분석 결과 표시 | |
| for category, score in scores.items(): | |
| # 한국어 카테고리명 매핑 | |
| category_names = { | |
| 'linguistic_patterns': '언어학적 패턴', | |
| 'contextual_fit': '맥락 적합성', | |
| 'developmental_appropriateness': '발달 적절성', | |
| 'repetition_analysis': '반복 분석', | |
| 'semantic_coherence': '의미적 일관성' | |
| } | |
| category_name = category_names.get(category, category) | |
| # 점수에 따른 시각적 표현 | |
| bar_length = int(score * 25) # 25자 기준으로 확장 | |
| if score >= 0.7: | |
| bar = "█" * bar_length + "░" * (25 - bar_length) | |
| level = "높음" | |
| elif score >= 0.4: | |
| bar = "▓" * bar_length + "░" * (25 - bar_length) | |
| level = "보통" | |
| else: | |
| bar = "▒" * bar_length + "░" * (25 - bar_length) | |
| level = "낮음" | |
| chart += f"{category_name:<12} │{bar}│ {score:.2f} ({level})\n" | |
| chart += "=" * 50 + "\n" | |
| chart += "범례: █ 높음(0.7+) ▓ 보통(0.4-0.7) ▒ 낮음(0.0-0.4)\n" | |
| chart += "```\n" | |
| # 실제 알고리즘 설명 추가 | |
| chart += "\n**🔬 분석 알고리즘 설명**:\n" | |
| chart += "- **언어학적 패턴**: 반향어 특성 패턴 감지\n" | |
| chart += "- **맥락 적합성**: 상황에 맞는 발화 여부\n" | |
| chart += "- **발달 적절성**: 연령대별 적절한 발화 수준\n" | |
| chart += "- **반복 분석**: 단어/구문 반복 패턴 분석\n" | |
| chart += "- **의미적 일관성**: 문장의 의미적 완성도\n" | |
| # 점수 해석 가이드 추가 | |
| chart += "\n**📖 점수 해석 가이드**:\n" | |
| chart += "- **0.0 ~ 0.4 (낮음)**: ✅ 정상 범위, 반향어 특성이 거의 없음\n" | |
| chart += "- **0.4 ~ 0.7 (보통)**: ⚠️ 관찰 필요, 일부 반향어 패턴 보임\n" | |
| chart += "- **0.7 ~ 1.0 (높음)**: 🔴 주의 필요, 명확한 반향어 패턴 감지\n" | |
| chart += "\n> 💡 **참고**: 높은 점수일수록 반향어 의심도가 높아집니다. 각 지표별로 높은 점수가 나오면 해당 영역에서 반향어 특성이 강하게 나타나고 있음을 의미합니다.\n" | |
| return chart | |
| def generate_personalized_recommendations(text: str, is_echo: bool, confidence: float, | |
| profile: dict, latest_cars: dict, advanced_confidence: dict) -> str: | |
| """개인화된 권장사항 생성""" | |
| recommendations = [] | |
| # 신뢰도 기반 권장사항 | |
| if confidence >= 70: | |
| recommendations.extend([ | |
| "💙 **관심**: 언어치료사와 상담을 통해 더 나은 지원을 받아보세요", | |
| "📚 **교육**: 일상에서 명확하고 간단한 질문으로 대화를 유도하세요", | |
| "🎯 **전략**: 반향어를 즉시 교정하지 말고, 올바른 표현을 모델링하세요" | |
| ]) | |
| elif confidence >= 40: | |
| recommendations.extend([ | |
| "🟡 **관찰**: 지속적인 관찰과 기록을 통해 패턴을 파악하세요", | |
| "💬 **소통**: 아이의 의도를 파악하려 노력하고 적절히 반응하세요", | |
| "📈 **발달**: 단계적으로 더 복잡한 의사소통을 유도하세요" | |
| ]) | |
| else: | |
| recommendations.extend([ | |
| "🟢 **정상**: 현재 의사소통 패턴을 지속적으로 관찰하세요", | |
| "🌟 **격려**: 아이의 의사소통 시도를 적극적으로 격려하세요", | |
| "🎪 **환경**: 자연스러운 대화 환경을 조성하세요" | |
| ]) | |
| # 프로필 기반 맞춤 권장사항 | |
| if profile: | |
| if profile.get('age') and profile['age'] <= 3: | |
| recommendations.append("👶 **연령별**: 3세 이하 아동의 경우 반향어는 정상적인 발달 과정일 수 있습니다") | |
| elif profile.get('vocabulary_level') == '초급': | |
| recommendations.append("📝 **어휘**: 간단한 단어와 문장으로 소통하는 것이 효과적입니다") | |
| # CARS 점수 기반 권장사항 | |
| if latest_cars: | |
| cars_score = latest_cars['total_score'] | |
| if cars_score >= 37: | |
| recommendations.append("🏥 **전문가**: 추가적인 지원을 위해 전문가와의 상담을 권장합니다") | |
| elif cars_score >= 30: | |
| recommendations.append("👨⚕️ **상담**: 지속적인 관찰을 위해 정기적인 상담을 고려해보세요") | |
| return "\n".join([f"{i+1}. {rec}" for i, rec in enumerate(recommendations)]) | |
| def generate_developmental_analysis(text: str, profile: dict, is_echo: bool) -> str: | |
| """발달 단계별 분석""" | |
| if not profile: | |
| return "프로필 정보가 없어 발달 단계별 분석을 수행할 수 없습니다." | |
| age = profile.get('age', 0) | |
| text_length = len(text.split()) | |
| analysis = f"**연령**: {age}세\n\n" | |
| analysis += f"**발화 길이**: {text_length}단어\n\n" | |
| if age <= 3: | |
| analysis += "**발달 단계**: 초기 언어 발달기\n\n" | |
| if text_length <= 2: | |
| analysis += "✅ **평가**: 연령에 적합한 짧은 발화 패턴입니다\n\n" | |
| else: | |
| analysis += "⚠️ **평가**: 연령 대비 긴 발화로 관찰이 필요합니다\n\n" | |
| elif age <= 6: | |
| analysis += "**발달 단계**: 언어 발달 중기\n\n" | |
| if 2 <= text_length <= 5: | |
| analysis += "✅ **평가**: 연령에 적합한 발화 길이입니다\n\n" | |
| else: | |
| analysis += "⚠️ **평가**: 발화 길이가 연령과 맞지 않을 수 있습니다\n\n" | |
| else: | |
| analysis += "**발달 단계**: 언어 발달 후기\n\n" | |
| if text_length >= 4: | |
| analysis += "✅ **평가**: 연령에 적합한 복잡한 발화입니다\n\n" | |
| else: | |
| analysis += "⚠️ **평가**: 연령 대비 단순한 발화 패턴입니다\n\n" | |
| if is_echo: | |
| analysis += "🔍 **반향어 특성**: 반향어 패턴이 관찰되어 추가 관심이 필요합니다\n\n" | |
| else: | |
| analysis += "✅ **정상 발화**: 정상적인 의사소통 패턴을 보입니다\n\n" | |
| return analysis | |
| def generate_progress_tracking(profile_id: int, is_echo: bool, confidence: float) -> str: | |
| """진행 상황 추적""" | |
| if not profile_id: | |
| return "프로필이 선택되지 않아 진행 상황을 추적할 수 없습니다." | |
| try: | |
| # 최근 분석 결과들 가져오기 | |
| recent_analyses = profile_manager.get_recent_analyses(profile_id, limit=10) | |
| if not recent_analyses: | |
| return "아직 충분한 분석 데이터가 없습니다. 더 많은 분석을 통해 진행 상황을 추적할 수 있습니다." | |
| # 통계 계산 | |
| total_analyses = len(recent_analyses) | |
| echo_count = sum(1 for analysis in recent_analyses if analysis.get('is_echo', False)) | |
| avg_confidence = sum(analysis.get('confidence', 0) for analysis in recent_analyses) / total_analyses | |
| # 진행 추세 분석 | |
| recent_echo_rate = echo_count / total_analyses * 100 | |
| progress_text = f"**총 분석 횟수**: {total_analyses}회\n\n" | |
| progress_text += f"**반향어 감지 횟수**: {echo_count}회\n\n" | |
| progress_text += f"**평균 신뢰도**: {avg_confidence:.1f}%\n\n" | |
| progress_text += f"**최근 반향어 비율**: {recent_echo_rate:.1f}%\n\n" | |
| # 추세 분석 | |
| if recent_echo_rate < 30: | |
| progress_text += "📈 **추세**: 반향어 패턴이 감소하고 있어 긍정적인 변화를 보입니다\n\n" | |
| elif recent_echo_rate > 70: | |
| progress_text += "📊 **추세**: 반향어 패턴이 일정 수준을 유지하고 있어 지속적인 관찰이 필요합니다\n\n" | |
| else: | |
| progress_text += "📊 **추세**: 반향어 패턴이 일정 수준을 유지하고 있어 지속적인 관찰이 필요합니다\n\n" | |
| return progress_text | |
| except Exception as e: | |
| return f"진행 상황 추적 중 오류가 발생했습니다: {str(e)}" | |
| def generate_quality_feedback(text: str, confidence: float, analysis_quality: str) -> str: | |
| """실시간 품질 피드백 생성""" | |
| feedback_items = [] | |
| # 입력 품질 평가 | |
| if len(text.strip()) < 3: | |
| feedback_items.append("⚠️ **입력 품질**: 텍스트가 너무 짧습니다. 더 긴 문장을 입력해보세요.\n\n") | |
| elif len(text.split()) > 20: | |
| feedback_items.append("⚠️ **입력 품질**: 텍스트가 너무 깁니다. 간단한 문장으로 나누어 입력해보세요.\n\n") | |
| else: | |
| feedback_items.append("✅ **입력 품질**: 적절한 길이의 텍스트입니다.\n\n") | |
| # 분석 품질 평가 | |
| if analysis_quality == 'high': | |
| feedback_items.append("🎯 **분석 결과**: 높은 신뢰도로 분석되었습니다.\n\n") | |
| elif analysis_quality == 'medium': | |
| feedback_items.append("📊 **분석 결과**: 보통 수준의 신뢰도로 분석되었습니다.\n\n") | |
| else: | |
| feedback_items.append("🔍 **분석 결과**: 낮은 신뢰도입니다. 더 구체적인 맥락 정보를 제공해보세요.\n\n") | |
| # 신뢰도 기반 피드백 | |
| if confidence >= 80: | |
| feedback_items.append("💙 **관심**: 높은 신뢰도로 반향어 특성이 관찰되었습니다.\n\n") | |
| elif confidence >= 60: | |
| feedback_items.append("🟡 **관심**: 중간 신뢰도로 반향어 특성이 관찰되었습니다.\n\n") | |
| elif confidence >= 40: | |
| feedback_items.append("🟢 **관찰**: 낮은 신뢰도로 일부 특성이 관찰되었습니다.\n\n") | |
| else: | |
| feedback_items.append("✅ **양호**: 정상적인 의사소통 패턴으로 보입니다.\n\n") | |
| return "\n".join(feedback_items) | |
| def _validate_input_quality_internal(text: str, context_situation: str): | |
| """입력 품질 검증 (내부 함수)""" | |
| quality_score = 1.0 | |
| issues = [] | |
| suggestions = [] | |
| # 텍스트 길이 검증 | |
| if len(text.strip()) < 2: | |
| quality_score -= 0.5 | |
| issues.append("텍스트가 너무 짧음") | |
| suggestions.append("더 긴 문장을 입력해보세요") | |
| elif len(text.split()) > 15: | |
| quality_score -= 0.2 | |
| issues.append("텍스트가 너무 김") | |
| suggestions.append("간단한 문장으로 나누어 입력해보세요") | |
| # 특수문자 및 숫자 검증 | |
| special_chars = sum(1 for c in text if not c.isalnum() and c not in ' .,!?') | |
| if special_chars > len(text) * 0.3: | |
| quality_score -= 0.3 | |
| issues.append("특수문자가 많음") | |
| suggestions.append("일반적인 한국어 문장을 입력해보세요") | |
| # 맥락 정보 검증 | |
| if not context_situation or context_situation == "기타": | |
| quality_score -= 0.1 | |
| issues.append("맥락 정보 부족") | |
| suggestions.append("구체적인 상황을 선택해보세요") | |
| result = { | |
| 'quality_score': max(quality_score, 0.0), | |
| 'issues': issues, | |
| 'suggestions': suggestions, | |
| 'is_acceptable': quality_score >= 0.6 | |
| } | |
| return result | |
| def generate_personalized_analysis(text: str, context_situation: str, context_previous: str, | |
| is_echo: bool, confidence: float, anchor: str, candidates: List[str], | |
| profile_id: int = None) -> str: | |
| """개인화된 분석 보고서 생성 (신뢰도 수준별 차별화)""" | |
| # 프로필 정보 및 CARS 검사 결과 가져오기 | |
| profile = None | |
| latest_cars = None | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| # 신뢰도 수준 결정 (개인화 고려) | |
| cars_score = None | |
| if latest_cars: | |
| cars_score = latest_cars['total_score'] | |
| if cars_score >= 37: # 중증 자폐 | |
| confidence_thresholds = (60, 80) | |
| elif cars_score >= 30: # 경증-중간 자폐 | |
| confidence_thresholds = (50, 70) | |
| else: # 정상 또는 미검사 | |
| confidence_thresholds = (40, 60) | |
| else: | |
| confidence_thresholds = (40, 70) | |
| # 신뢰도 수준별 차별화된 분석 | |
| if confidence >= confidence_thresholds[1]: | |
| confidence_level = "높은 수준" | |
| confidence_desc = "반향어를 사용하는 성향이 강하게 나타납니다" | |
| analysis_focus = "반향어 패턴이 명확하게 관찰되며, 더욱 세심한 관찰이 필요합니다" | |
| urgency_level = "높음" | |
| elif confidence >= confidence_thresholds[0]: | |
| confidence_level = "보통 수준" | |
| confidence_desc = "반향어 특성이 부분적으로 관찰됩니다" | |
| analysis_focus = "반향어와 정상 발화가 혼재되어 있어 지속적인 관찰이 필요합니다" | |
| urgency_level = "보통" | |
| else: | |
| confidence_level = "낮은 수준" | |
| confidence_desc = "정상적인 의사소통 패턴을 보입니다" | |
| analysis_focus = "대부분 정상적인 의사소통이지만 일부 반향어 특성이 관찰됩니다" | |
| urgency_level = "낮음" | |
| # AI 기반 맥락 분석 추가 | |
| context_analysis = generate_context_analysis(text, context_situation, context_previous, profile) | |
| # 신뢰도 수준별 맞춤 권장사항 | |
| if confidence >= confidence_thresholds[1]: # 높은 수준 | |
| recommendations = [ | |
| "언어치료사와 상담을 통해 더 나은 지원을 받아보세요", | |
| "일상에서 명확하고 간단한 질문으로 대화를 유도하세요", | |
| "반향어를 즉시 교정하지 말고, 올바른 표현을 모델링하세요", | |
| "구체적인 상황에서의 대화 연습을 강화하세요", | |
| "긍정적인 강화를 통해 적절한 의사소통을 격려하세요" | |
| ] | |
| meaning_analysis = f"이 발화는 {analysis_focus}. 언어치료 상담을 고려해보세요." | |
| elif confidence >= confidence_thresholds[0]: # 보통 수준 | |
| recommendations = [ | |
| "지속적인 관찰과 기록을 통해 패턴을 파악하세요", | |
| "상황별로 적절한 대화 모델을 제공하세요", | |
| "아이의 의도를 파악하려 노력하고 적절히 반응하세요", | |
| "단계적으로 더 복잡한 의사소통을 유도하세요", | |
| "언어치료사와 정기적인 상담을 고려하세요" | |
| ] | |
| meaning_analysis = f"이 발화는 {analysis_focus}. 지속적인 관찰과 적절한 개입이 필요합니다." | |
| else: # 낮은 수준 | |
| recommendations = [ | |
| "현재 의사소통 패턴을 지속적으로 관찰하세요", | |
| "자연스러운 대화 환경을 조성하세요", | |
| "아이의 의사소통 시도를 적극적으로 격려하세요", | |
| "다양한 상황에서의 대화 기회를 제공하세요", | |
| "정기적인 발달 평가를 받으시기 바랍니다" | |
| ] | |
| meaning_analysis = f"이 발화는 {analysis_focus}. 현재로서는 정상적인 발달 범위 내에 있습니다." | |
| # 나이별 맞춤 분석 | |
| age_context = "" | |
| if profile and profile.get('age'): | |
| age = profile['age'] | |
| if age <= 3: | |
| age_context = "3세 이하 아동의 경우 반향어는 정상적인 언어 발달 과정의 일부일 수 있습니다." | |
| elif age <= 6: | |
| age_context = "3-6세 아동의 경우 반향어가 지속되면 관찰이 필요합니다." | |
| else: | |
| age_context = "6세 이상 아동의 경우 반향어가 지속되면 전문적 개입이 권장됩니다." | |
| # 어휘능력별 맞춤 분석 | |
| vocabulary_context = "" | |
| if profile and profile.get('vocabulary_level'): | |
| vocab_level = profile['vocabulary_level'] | |
| if vocab_level == "초급": | |
| vocabulary_context = "어휘능력이 초급 수준이므로 간단한 단어와 문장으로 소통하는 것이 효과적입니다." | |
| elif vocab_level == "중급": | |
| vocabulary_context = "어휘능력이 중급 수준이므로 구체적이고 명확한 표현을 사용해보세요." | |
| elif vocab_level == "고급": | |
| vocabulary_context = "어휘능력이 고급 수준이므로 감정과 경험을 포함한 복잡한 표현도 시도해볼 수 있습니다." | |
| # CARS 점수별 맞춤 권장사항 | |
| cars_recommendations = [] | |
| if cars_score is not None: | |
| if cars_score >= 37: # 중증 자폐 | |
| cars_recommendations.extend([ | |
| "구조화된 환경에서 일관된 반응 제공하기", | |
| "시각적 단서와 함께 간단한 언어 사용하기", | |
| "전문가와의 상담을 통한 체계적 개입 고려하기" | |
| ]) | |
| elif cars_score >= 30: # 경증-중간 자폐 | |
| cars_recommendations.extend([ | |
| "아이의 의사소통 시도를 긍정적으로 강화하기", | |
| "구체적인 질문으로 대화 확장하기", | |
| "일상 루틴에서 자연스러운 언어 모델링하기" | |
| ]) | |
| else: # 정상 범위 | |
| cars_recommendations.extend([ | |
| "아이의 정상적인 의사소통을 격려하기", | |
| "더 복잡한 표현으로 확장하기", | |
| "감정과 경험을 포함한 대화 나누기" | |
| ]) | |
| # 상황별 의미 해석 (완곡한 표현으로 개선) | |
| situation_meanings = { | |
| "식사 시간": { | |
| "밥": "배고픔을 표현하거나 식사 요청의 의미일 가능성이 높으며", | |
| "먹었어": "식사 완료를 알리거나 만족감을 표현하려는 의도일 수 있으며", | |
| "맛있어": "음식에 대한 만족감이나 선호도를 나타내려는 시도일 가능성이 높습니다" | |
| }, | |
| "놀이 시간": { | |
| "놀자": "놀이 활동에 대한 요청이나 관심을 표현하려는 의도일 가능성이 높으며", | |
| "가자": "특정 장소로 이동하고 싶은 의도를 나타내려는 시도일 수 있으며", | |
| "싫어": "특정 활동에 대한 거부감을 표현하려는 의도일 가능성이 높습니다" | |
| }, | |
| "기타": { | |
| "default": "상황에 맞는 의사소통을 시도하려는 의도일 가능성이 높으며" | |
| } | |
| } | |
| # 추가 고려사항 생성 (상황별) | |
| additional_considerations = [] | |
| if context_situation == "밥 먹기 전에" and "밥" in text: | |
| additional_considerations = ["배가 고픈 상태", "식사 시간에 대한 인식", "음식에 대한 관심"] | |
| elif context_situation == "놀이 시간" and "놀" in text: | |
| additional_considerations = ["놀이에 대한 욕구", "사회적 상호작용", "활동적 참여"] | |
| else: | |
| additional_considerations = ["상황 인식", "의사소통 시도", "감정 표현"] | |
| # 개인화된 권장사항 생성 (완곡한 표현으로 개선) | |
| recommendations = [] | |
| if is_echo and confidence >= confidence_thresholds[1]: | |
| recommendations.extend([ | |
| "아이의 발화를 긍정적으로 받아들이며 '네, 알겠어요'라고 반응하는 것을 권장합니다", | |
| "구체적인 질문으로 교정하는 것을 추천합니다 (예: '무엇을 원해요?', '어떤 기분이에요?')", | |
| "L1 수준의 간단한 응답부터 연습해보시는 것이 좋겠습니다" | |
| ]) | |
| elif is_echo and confidence >= confidence_thresholds[0]: | |
| recommendations.extend([ | |
| "아이의 의사소통 시도를 격려하는 것을 권장합니다", | |
| "더 구체적인 표현 방법을 모델링해보시는 것이 좋겠습니다", | |
| "상황에 맞는 대안 표현을 제시해보시는 것을 추천합니다" | |
| ]) | |
| else: | |
| recommendations.extend([ | |
| "아이의 정상적인 의사소통을 격려하는 것을 권장합니다", | |
| "더 복잡한 표현으로 확장해보시는 것이 좋겠습니다", | |
| "감정과 경험을 포함한 대화를 나누어보시는 것을 추천합니다" | |
| ]) | |
| # CARS 기반 권장사항 추가 | |
| if cars_recommendations: | |
| recommendations.extend(cars_recommendations[:2]) # 상위 2개만 추가 | |
| # 보고서 생성 | |
| profile_info = "" | |
| if profile: | |
| cars_info = "" | |
| if latest_cars: | |
| cars_info = f"**CARS 점수**: {latest_cars['total_score']}점 ({latest_cars['diagnosis']})" | |
| else: | |
| cars_info = "**CARS 점수**: 미검사" | |
| profile_info = f""" | |
| **아이 정보**: {profile['child_name']} ({profile['age']}세) | |
| **어휘능력**: {profile['vocabulary_level']} | |
| {cars_info} | |
| **언어습관**: {profile['language_habits'] or '미입력'}""" | |
| # 추가 고려사항을 자연스럽게 연결 | |
| additional_text = "" | |
| if additional_considerations: | |
| additional_text = f", {', '.join(additional_considerations)}도 고려할 여지가 있습니다" | |
| # 상황별 의미 해석 (기존 코드와 통합) | |
| situation_meanings = { | |
| "밥 먹기 전에": { | |
| "밥": "식사에 대한 관심이나 배고픔을 표현하려는 의도", | |
| "먹": "음식에 대한 욕구나 식사 준비에 대한 인식", | |
| "default": "식사 상황에 대한 인식이나 관심 표현" | |
| }, | |
| "놀이 시간": { | |
| "놀": "놀이에 대한 욕구나 참여 의사", | |
| "게임": "놀이 활동에 대한 관심이나 참여 요청", | |
| "default": "놀이 활동에 대한 관심이나 참여 의도" | |
| }, | |
| "외출 준비": { | |
| "나가": "외출에 대한 욕구나 기대감", | |
| "산책": "야외 활동에 대한 관심이나 참여 의사", | |
| "default": "외출이나 야외 활동에 대한 관심" | |
| }, | |
| "수업 시간": { | |
| "공부": "학습에 대한 관심이나 참여 의도", | |
| "책": "학습 자료에 대한 관심이나 학습 욕구", | |
| "default": "학습 활동에 대한 관심이나 참여 의도" | |
| }, | |
| "휴식 시간": { | |
| "쉬": "휴식에 대한 욕구나 피로감 표현", | |
| "잠": "수면에 대한 욕구나 피로감 인식", | |
| "default": "휴식이나 안정에 대한 욕구" | |
| } | |
| } | |
| # 상황별 의미 해석 추가 (신뢰도 수준별 meaning_analysis와 통합) | |
| situation_context = "" | |
| if context_situation in situation_meanings: | |
| situation_dict = situation_meanings[context_situation] | |
| for keyword, meaning in situation_dict.items(): | |
| if keyword in text: | |
| situation_context = f" 구체적으로는 {meaning}일 가능성이 높습니다." | |
| break | |
| if not situation_context and "default" in situation_dict: | |
| situation_context = f" 구체적으로는 {situation_dict['default']}일 가능성이 높습니다." | |
| else: | |
| situation_context = " 구체적으로는 상황에 맞는 의사소통을 시도하려는 의도일 가능성이 높습니다." | |
| # 최종 의미 해석 구성 (자연스러운 마무리) | |
| if situation_context: | |
| meaning_analysis = f"{meaning_analysis}{situation_context}" | |
| else: | |
| meaning_analysis = f"{meaning_analysis} 추가적인 관찰이 필요합니다." | |
| # 프로필 정보 구성 | |
| profile_info = "" | |
| if profile: | |
| profile_info = f""" | |
| **아동 정보**: {profile['child_name']}({profile['age']}세), 어휘능력 {profile['vocabulary_level']}""" | |
| if profile.get('language_habits'): | |
| profile_info += f", 언어습관: {profile['language_habits']}" | |
| if latest_cars: | |
| profile_info += f", CARS 점수: {latest_cars['total_score']}점 ({latest_cars['diagnosis']})" | |
| # 3단계 변환 제거됨 | |
| # 권장사항을 줄바꿈 처리 (각 항목 사이에 빈 줄 추가) | |
| recommendations_text = chr(10).join([f"• {rec}" for rec in recommendations[:5]]).replace("• ", "\n• ") | |
| # 신뢰도 수준별 보고서 생성 | |
| if confidence >= confidence_thresholds[1]: # 높은 수준 | |
| report = f"""## ✅ 개인화 분석 보고서{profile_info} | |
| ### ⚠️ 반향어 분석 (높은 수준) | |
| 아이의 발화에서 반향어 사용이 '{confidence_level}'으로 관찰되었습니다. | |
| **신뢰도**: {confidence:.1f}% | |
| **발화**: "{text}" | |
| **상황**: {context_situation or "미지정"} | |
| **이전 발화**: {context_previous or "없음"} | |
| {context_analysis} | |
| ### 🔍 의미 해석 | |
| {meaning_analysis} | |
| {age_context} | |
| {vocabulary_context} | |
| ### 🎯 즉시 권장사항 | |
| {recommendations_text} | |
| """ | |
| elif confidence >= confidence_thresholds[0]: # 보통 수준 | |
| report = f"""## 📊 개인화 분석 보고서{profile_info} | |
| ### ⚖️ 반향어 분석 (보통 수준) | |
| 아이의 발화에서 반향어 특성이 '{confidence_level}'으로 관찰되었습니다. | |
| **관심도**: {urgency_level} | |
| **신뢰도**: {confidence:.1f}% | |
| **발화**: "{text}" | |
| **상황**: {context_situation or "미지정"} | |
| **이전 발화**: {context_previous or "없음"} | |
| {context_analysis} | |
| ### 🔍 의미 해석 | |
| {meaning_analysis} | |
| {age_context} | |
| {vocabulary_context} | |
| ### 🎯 권장사항 | |
| {recommendations_text} | |
| """ | |
| else: # 낮은 수준 | |
| report = f"""## ✅ 개인화 분석 보고서{profile_info} | |
| ### 👍 반향어 분석 (낮은 수준) | |
| 아이의 발화에서 반향어 특성이 '{confidence_level}'으로 관찰되었습니다. | |
| **관심도**: {urgency_level} | |
| **신뢰도**: {confidence:.1f}% | |
| **발화**: "{text}" | |
| **상황**: {context_situation or "미지정"} | |
| **이전 발화**: {context_previous or "없음"} | |
| {context_analysis} | |
| ### 🔍 의미 해석 | |
| {meaning_analysis} | |
| {age_context} | |
| {vocabulary_context} | |
| ### 🎯 권장사항 | |
| {recommendations_text} | |
| """ | |
| return report | |
| def generate_analysis_report(text: str, context_situation: str, context_previous: str, is_echo: bool, confidence: float, anchor: str, candidates: List[str]) -> str: | |
| """분석 보고서 생성""" | |
| # 신뢰도 수준 결정 | |
| if confidence >= 70: | |
| confidence_level = "높은 수준" | |
| confidence_desc = "반향어 가능성이 높습니다" | |
| elif confidence >= 40: | |
| confidence_level = "중간 수준" | |
| confidence_desc = "부분적 반향어 특성을 보입니다" | |
| else: | |
| confidence_level = "낮은 수준" | |
| confidence_desc = "정상적인 의사소통으로 보입니다" | |
| # 상황별 의미 해석 | |
| situation_meanings = { | |
| "식사 시간": { | |
| "밥": "배고픔을 표현하거나 식사 요청", | |
| "먹었어": "식사 완료를 알리거나 만족감 표현", | |
| "맛있어": "음식에 대한 만족감이나 선호도 표현" | |
| }, | |
| "놀이 시간": { | |
| "놀자": "놀이 활동에 대한 요청이나 관심", | |
| "가자": "특정 장소로 이동하고 싶은 의도", | |
| "싫어": "특정 활동에 대한 거부감 표현" | |
| }, | |
| "기타": { | |
| "default": "상황에 맞는 의사소통 시도" | |
| } | |
| } | |
| # 의미 해석 생성 | |
| meaning_analysis = "" | |
| if context_situation in situation_meanings: | |
| situation_dict = situation_meanings[context_situation] | |
| for keyword, meaning in situation_dict.items(): | |
| if keyword in text: | |
| meaning_analysis = meaning | |
| break | |
| if not meaning_analysis and "default" in situation_dict: | |
| meaning_analysis = situation_dict["default"] | |
| else: | |
| meaning_analysis = "상황에 맞는 의사소통 시도" | |
| # 보호자 권장사항 | |
| recommendations = [] | |
| if is_echo and confidence >= 70: | |
| recommendations.extend([ | |
| "아이의 발화를 긍정적으로 받아들이며 '네, 알겠어요'라고 반응하기", | |
| "구체적인 질문으로 교정하기 (예: '무엇을 원해요?', '어떤 기분이에요?')", | |
| "L1 수준의 간단한 응답부터 연습하기" | |
| ]) | |
| elif is_echo and confidence >= 40: | |
| recommendations.extend([ | |
| "아이의 의사소통 시도를 격려하기", | |
| "더 구체적인 표현 방법을 모델링하기", | |
| "상황에 맞는 대안 표현 제시하기" | |
| ]) | |
| else: | |
| recommendations.extend([ | |
| "아이의 정상적인 의사소통을 격려하기", | |
| "더 복잡한 표현으로 확장하기", | |
| "감정과 경험을 포함한 대화 나누기" | |
| ]) | |
| # 단계별 변환 제안을 미리 정의 | |
| l1_text = candidates[0] if candidates else "L1: 기본적인 의사소통 시도" | |
| l2_text = candidates[1] if len(candidates) > 1 else "L2: 구체적인 요청 표현" | |
| l3_text = candidates[2] if len(candidates) > 2 else "L3: 감정과 경험을 포함한 표현" | |
| # 추가 고려사항을 미리 정의 | |
| additional_considerations = ", ".join(candidates[:2]) if candidates else "구체적인 의사소통" | |
| # 권장사항을 미리 정의하여 줄바꿈 처리 | |
| recommendations_text = chr(10).join([f"• {rec}" for rec in recommendations[:3]]) | |
| # 보고서 생성 | |
| report = f"""## 📊 분석 보고서 | |
| ### 1️⃣ 반향어 분석 | |
| 주어진 맥락과 아이의 발화를 분석하였을 때, 아이는 반향어를 **'{confidence_level}'**으로 사용하는 성향입니다. | |
| **발화**: "{text}" | |
| **상황**: {context_situation or "미지정"} | |
| **이전 발화**: {context_previous or "없음"} | |
| ### 2️⃣ 의미 해석 | |
| 해당 상황에서 발화는 **"{meaning_analysis}"**를 의미할 가능성이 높으며, {additional_considerations}도 고려할 여지가 있습니다. | |
| ### 3️⃣ 보호자 권장사항 | |
| 해당 상황에서, 보호자가 다음 중 하나의 반응을 하거나 질문으로 교정하는 것을 추천합니다: | |
| {recommendations_text} | |
| ### 4️⃣ 단계별 변환 제안 | |
| {l1_text} | |
| {l2_text} | |
| {l3_text}""" | |
| return report | |
| # 프로필 관리 함수들 | |
| def create_profile(child_name: str, age: int, vocabulary_level: str, language_habits: str): | |
| """새 프로필 생성""" | |
| if not child_name or not age: | |
| return gr.Dropdown(choices=[]), gr.Dropdown(choices=[]), gr.Dropdown(choices=[]) | |
| try: | |
| profile_id = profile_manager.create_profile(child_name, age, vocabulary_level, None, language_habits) | |
| profiles = profile_manager.list_profiles() | |
| choices = [(f"{p['child_name']} ({p['age']}세)", p['id']) for p in profiles] | |
| return gr.Dropdown(choices=choices, value=profile_id), gr.Dropdown(choices=choices, value=profile_id), gr.Dropdown(choices=choices, value=profile_id) | |
| except Exception as e: | |
| return gr.Dropdown(choices=[]), gr.Dropdown(choices=[]), gr.Dropdown(choices=[]) | |
| def load_profiles(): | |
| """프로필 목록 로드""" | |
| profiles = profile_manager.list_profiles() | |
| choices = [(f"{p['child_name']} ({p['age']}세)", p['id']) for p in profiles] | |
| return gr.Dropdown(choices=choices) | |
| def get_profile_choices(): | |
| """프로필 선택 옵션 반환""" | |
| profiles = profile_manager.list_profiles() | |
| choices = [(f"{p['child_name']} ({p['age']}세)", p['id']) for p in profiles] | |
| return gr.Dropdown(choices=choices), gr.Dropdown(choices=choices), gr.Dropdown(choices=choices) | |
| def update_profile_dropdown(): | |
| """프로필 드롭다운 업데이트""" | |
| profiles = profile_manager.list_profiles() | |
| choices = [(f"{p['child_name']} ({p['age']}세)", p['id']) for p in profiles] | |
| return gr.Dropdown(choices=choices) | |
| def load_profile(profile_id): | |
| """프로필 정보 로드""" | |
| if not profile_id: | |
| return "프로필을 선택해주세요." | |
| profile = profile_manager.get_profile(profile_id) | |
| if not profile: | |
| return "프로필을 찾을 수 없습니다." | |
| cars_status = f"CARS 점수: {profile['cars_score']}" if profile['cars_score'] else "CARS 점수: 미검사" | |
| return f"""## 👤 {profile['child_name']}의 프로필 | |
| **나이**: {profile['age']}세 | |
| **어휘능력**: {profile['vocabulary_level']} | |
| **{cars_status}** | |
| **언어습관**: {profile['language_habits'] or '미입력'} | |
| **생성일**: {profile['created_at']} | |
| **수정일**: {profile['updated_at']}""" | |
| def load_profile_for_analysis(profile_id): | |
| """텍스트 분석용 상세 프로필 정보 로드""" | |
| if not profile_id: | |
| return "프로필을 선택해주세요." | |
| profile = profile_manager.get_profile(profile_id) | |
| if not profile: | |
| return "프로필을 찾을 수 없습니다." | |
| # CARS 검사 결과 가져오기 | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| # 기본 프로필 정보 | |
| profile_info = f"""## 👤 {profile['child_name']}의 프로필 정보 | |
| **나이**: {profile['age']}세 | |
| **어휘능력 수준**: {profile['vocabulary_level']} | |
| **언어습관**: {profile['language_habits'] or '미입력'} | |
| **프로필 생성일**: {profile['created_at']}""" | |
| # CARS 검사 결과 추가 | |
| if latest_cars: | |
| cars_info = f""" | |
| ## 🧠 CARS 검사 결과 (최신) | |
| **검사일**: {latest_cars['assessment_date']} | |
| **총점**: {latest_cars['total_score']}점 | |
| **진단 결과**: {latest_cars['diagnosis']}""" | |
| profile_info += cars_info | |
| # 분석 컨텍스트 추가 | |
| analysis_context = f""" | |
| ## 📊 분석 컨텍스트 | |
| **아이의 특성**: | |
| - {profile['age']}세 {profile['vocabulary_level']} 수준의 어휘능력 | |
| - 언어습관: {profile['language_habits'] or '특별한 언어습관 없음'} | |
| **CARS 검사 결과**: | |
| - 총점 {latest_cars['total_score']}점 ({latest_cars['diagnosis']}) | |
| - 이 정보를 바탕으로 반향어 분석 시 아이의 발달 수준과 특성을 고려하여 분석합니다.""" | |
| profile_info += analysis_context | |
| else: | |
| profile_info += f""" | |
| ## 📊 분석 컨텍스트 | |
| **아이의 특성**: | |
| - {profile['age']}세 {profile['vocabulary_level']} 수준의 어휘능력 | |
| - 언어습관: {profile['language_habits'] or '특별한 언어습관 없음'} | |
| **참고사항**: CARS 검사 결과가 없어 기본적인 프로필 정보만 활용됩니다.""" | |
| return profile_info | |
| def clear_profile_info(): | |
| """프로필 정보 초기화""" | |
| return "프로필을 선택하고 불러오기를 클릭하면 아이의 정보와 CARS 검사 결과가 표시됩니다." | |
| def submit_user_feedback(analysis_id: int, user_rating: int, user_feedback_text: str): | |
| """사용자 피드백 제출 처리""" | |
| if not analysis_id: | |
| return "❌ 분석 ID가 없습니다. 먼저 텍스트를 분석해주세요.", 3, "" | |
| try: | |
| import uuid | |
| user_session_id = str(uuid.uuid4())[:8] # 간단한 세션 ID 생성 | |
| result = save_user_feedback( | |
| analysis_id, int(user_rating), user_feedback_text.strip(), user_session_id | |
| ) | |
| # 피드백 제출 후 입력 필드 초기화 | |
| return result, 3, "" | |
| except Exception as e: | |
| return f"❌ 피드백 제출 중 오류가 발생했습니다: {str(e)}", user_rating, user_feedback_text | |
| def get_user_feedback_statistics_display(): | |
| """전체 피드백 통계 표시""" | |
| try: | |
| stats = profile_manager.get_user_feedback_statistics() | |
| if stats['total_feedbacks'] == 0: | |
| return "아직 피드백이 없습니다. 리뷰어들이 피드백을 제공하면 통계가 표시됩니다." | |
| return f"""## 📊 전체 피드백 통계 | |
| ### 📈 정확도 분석 | |
| - **총 피드백 수**: {stats['total_feedbacks']}개 | |
| - **정확도**: {stats['accuracy_rate']:.1f}% | |
| - **정확함**: {stats['accurate_count']}개 ({stats['accurate_count']/stats['total_feedbacks']*100:.1f}%) | |
| - **부분적 정확함**: {stats['partially_accurate_count']}개 ({stats['partially_accurate_count']/stats['total_feedbacks']*100:.1f}%) | |
| - **부정확함**: {stats['inaccurate_count']}개 ({stats['inaccurate_count']/stats['total_feedbacks']*100:.1f}%) | |
| ### 🎯 시스템 신뢰도 | |
| {get_accuracy_emoji(stats['accuracy_rate'])} **{get_accuracy_level(stats['accuracy_rate'])}** | |
| ### 📝 개선 권장사항 | |
| {get_improvement_recommendations(stats)} | |
| """ | |
| except Exception as e: | |
| return f"❌ 통계 조회 중 오류가 발생했습니다: {str(e)}" | |
| def get_profile_user_feedback_stats(profile_id): | |
| """특정 프로필의 피드백 통계""" | |
| if not profile_id: | |
| return "프로필을 선택해주세요." | |
| try: | |
| stats = profile_manager.get_user_feedback_statistics(profile_id) | |
| if stats['total_feedbacks'] == 0: | |
| return f"프로필 ID {profile_id}에 대한 피드백이 아직 없습니다." | |
| return f"""## 👤 프로필별 피드백 통계 | |
| ### 📈 정확도 분석 | |
| - **총 피드백 수**: {stats['total_feedbacks']}개 | |
| - **정확도**: {stats['accuracy_rate']:.1f}% | |
| - **정확함**: {stats['accurate_count']}개 | |
| - **부분적 정확함**: {stats['partially_accurate_count']}개 | |
| - **부정확함**: {stats['inaccurate_count']}개 | |
| ### 🎯 이 프로필의 신뢰도 | |
| {get_accuracy_emoji(stats['accuracy_rate'])} **{get_accuracy_level(stats['accuracy_rate'])}** | |
| """ | |
| except Exception as e: | |
| return f"❌ 프로필 통계 조회 중 오류가 발생했습니다: {str(e)}" | |
| def get_accuracy_emoji(accuracy_rate): | |
| """정확도에 따른 이모지 반환""" | |
| if accuracy_rate >= 80: | |
| return "🟢" | |
| elif accuracy_rate >= 60: | |
| return "🟡" | |
| else: | |
| return "🔴" | |
| def get_accuracy_level(accuracy_rate): | |
| """정확도에 따른 수준 반환""" | |
| if accuracy_rate >= 80: | |
| return "높은 신뢰도" | |
| elif accuracy_rate >= 60: | |
| return "보통 신뢰도" | |
| else: | |
| return "낮은 신뢰도" | |
| def get_improvement_recommendations(stats): | |
| """통계를 바탕으로 한 개선 권장사항""" | |
| total = stats['total_feedbacks'] | |
| inaccurate_rate = stats['inaccurate_count'] / total * 100 | |
| if inaccurate_rate > 30: | |
| return "⚠️ 부정확한 분석이 많습니다. 더 많은 맥락 정보나 프로필 정보가 필요할 수 있습니다." | |
| elif stats['partially_accurate_count'] > stats['accurate_count']: | |
| return "📊 부분적 정확한 분석이 많습니다. 분석 알고리즘의 세밀함을 높일 필요가 있습니다." | |
| else: | |
| return "✅ 전반적으로 높은 정확도를 보이고 있습니다. 현재 시스템이 잘 작동하고 있습니다." | |
| def load_profile_for_echo_test(profile_id): | |
| """반향어 검사용 프로필 정보 로드""" | |
| if not profile_id: | |
| return "프로필을 선택해주세요." | |
| profile = profile_manager.get_profile(profile_id) | |
| if not profile: | |
| return "프로필을 찾을 수 없습니다." | |
| # CARS 검사 결과 가져오기 | |
| cars_assessments = profile_manager.get_cars_assessments(profile_id) | |
| latest_cars = cars_assessments[0] if cars_assessments else None | |
| # 기본 프로필 정보 | |
| profile_info = f"""## 👤 {profile['child_name']}의 프로필 정보 | |
| **나이**: {profile['age']}세 | |
| **어휘능력 수준**: {profile['vocabulary_level']} | |
| **언어습관**: {profile['language_habits'] or '미입력'} | |
| **프로필 생성일**: {profile['created_at']}""" | |
| # CARS 검사 결과 추가 | |
| if latest_cars: | |
| cars_info = f""" | |
| ## 🧠 CARS 검사 결과 (최신) | |
| **검사일**: {latest_cars['assessment_date']} | |
| **총점**: {latest_cars['total_score']}점 | |
| **진단 결과**: {latest_cars['diagnosis']}""" | |
| profile_info += cars_info | |
| # 분석 컨텍스트 추가 | |
| analysis_context = f""" | |
| ## 📊 검사 컨텍스트 | |
| **아이의 특성**: | |
| - {profile['age']}세 {profile['vocabulary_level']} 수준의 어휘능력 | |
| - 언어습관: {profile['language_habits'] or '특별한 언어습관 없음'} | |
| **CARS 검사 결과**: | |
| - 총점 {latest_cars['total_score']}점 ({latest_cars['diagnosis']}) | |
| - 이 정보를 바탕으로 반향어 검사 시 아이의 발달 수준과 특성을 고려하여 분석합니다.""" | |
| profile_info += analysis_context | |
| else: | |
| profile_info += f""" | |
| ## 📊 검사 컨텍스트 | |
| **아이의 특성**: | |
| - {profile['age']}세 {profile['vocabulary_level']} 수준의 어휘능력 | |
| - 언어습관: {profile['language_habits'] or '특별한 언어습관 없음'} | |
| **참고사항**: CARS 검사 결과가 없어 기본적인 프로필 정보만 활용됩니다.""" | |
| return profile_info | |
| def calculate_cars_score(*scores): | |
| """CARS 점수 계산 (15문항 직접 입력)""" | |
| try: | |
| scores_list = list(scores) | |
| # 15문항을 직접 사용 | |
| if len(scores_list) != 15: | |
| return f"❌ 15개 문항의 점수를 모두 입력해주세요. 현재 {len(scores_list)}개 입력됨" | |
| total_score, diagnosis = cars_assessment.calculate_total_score(scores_list) | |
| # 점수별 상세 분석 | |
| if total_score <= 29.5: | |
| color = "🟢" | |
| recommendation = "정상 범위입니다. 지속적인 관찰을 권장합니다." | |
| next_steps = [ | |
| "정상적인 발달 과정을 지속적으로 관찰하기", | |
| "아이의 의사소통 능력 향상을 위한 활동 참여", | |
| "정기적인 발달 검사 받기" | |
| ] | |
| elif total_score <= 36.5: | |
| color = "🟡" | |
| recommendation = "경증-중간 정도의 자폐 특성을 보입니다. 전문가 상담을 권장합니다." | |
| next_steps = [ | |
| "소아정신과 또는 발달센터 전문가 상담", | |
| "ADOS, ADI-R 등 정밀 검사 고려", | |
| "언어치료, 작업치료 등 조기 개입 프로그램 참여", | |
| "가족 구성원 대상 상담 및 교육" | |
| ] | |
| else: | |
| color = "🔴" | |
| recommendation = "중증 자폐 특성을 보입니다. 즉시 전문가 상담이 필요합니다." | |
| next_steps = [ | |
| "즉시 소아정신과 전문의 상담", | |
| "종합적인 발달 평가 및 진단", | |
| "집중적인 조기 개입 프로그램 시작", | |
| "가족 전체를 위한 상담 및 지원 서비스" | |
| ] | |
| # 문항별 점수 분석 | |
| score_analysis = "" | |
| high_score_items = [] | |
| for i, score in enumerate(scores_list): | |
| if score >= 3.0: # 3점 이상인 문항들 | |
| high_score_items.append(f"문항 {i+1}: {score}점") | |
| if high_score_items: | |
| score_analysis = f"\n**높은 점수 문항**: {', '.join(high_score_items[:5])}" # 상위 5개만 표시 | |
| return f"""## 🧠 CARS 검사 결과 | |
| **총점**: {total_score:.1f}점 (15문항 기준) | |
| **진단**: {color} **{diagnosis}** | |
| ### 📊 상세 분석 | |
| - **15개 문항 총점**: {total_score:.1f}점 | |
| - **평균 점수**: {total_score/15:.2f}점 | |
| - **평가 기준**: | |
| - 15-29.5점: 정상 범위 | |
| - 30-36.5점: 관심이 필요한 수준 | |
| - 37-60점: 추가 지원이 필요한 수준{score_analysis} | |
| ### 💡 권장사항 | |
| {recommendation} | |
| ### 📋 다음 단계 | |
| {chr(10).join([f"{i+1}. {step}" for i, step in enumerate(next_steps)])} | |
| ### 🔍 추가 정보 | |
| - **검사 신뢰도**: 내적 합치도 α=.94, 평정자간 상관 γ=.71 | |
| - **타당도**: 준거관련 상관 γ=.84 | |
| - **검사-재검사**: 상관 γ=.88 | |
| **참고**: 이 검사는 15개 문항을 모두 포함한 완전한 CARS 검사입니다. 정확한 진단을 위해서는 전문가와의 상담을 권장합니다.""" | |
| except Exception as e: | |
| return f"❌ 점수 계산 중 오류가 발생했습니다: {str(e)}" | |
| def quick_cars_assessment(): | |
| """빠른 CARS 평가 (모든 문항 2점)""" | |
| scores = [2.0] * 15 # 15개 문항 모두 2점 | |
| return calculate_cars_score(*scores) | |
| def save_cars_result(profile_id, *scores): | |
| """CARS 검사 결과를 프로필에 저장""" | |
| if not profile_id: | |
| return "프로필을 선택해주세요.", gr.Dropdown(choices=[]) | |
| try: | |
| scores_list = list(scores) | |
| if len(scores_list) != 15: | |
| return f"❌ 15개 문항의 점수를 모두 입력해주세요. 현재 {len(scores_list)}개 입력됨", gr.Dropdown(choices=[]) | |
| total_score, diagnosis = cars_assessment.calculate_total_score(scores_list) | |
| # CARS 검사 결과 저장 | |
| cars_assessment.save_assessment(profile_id, scores_list, total_score) | |
| # 프로필 목록 업데이트 | |
| profiles = profile_manager.list_profiles() | |
| choices = [(f"{p['child_name']} ({p['age']}세)", p['id']) for p in profiles] | |
| success_message = f"""✅ CARS 검사 결과가 저장되었습니다! | |
| **총점**: {total_score}점 | |
| **진단**: {diagnosis} | |
| 프로필이 업데이트되어 다음 분석부터 개인화된 결과를 받을 수 있습니다.""" | |
| return success_message, gr.Dropdown(choices=choices, value=profile_id) | |
| except Exception as e: | |
| return f"❌ 저장 중 오류가 발생했습니다: {str(e)}", gr.Dropdown(choices=[]) | |
| def process_voice(audio_file, profile_id: int = None) -> Tuple[str, bool, str]: | |
| """음성 처리: 음성 인식 → 반향어 감지 → 전환 → 음성 합성""" | |
| if audio_file is None: | |
| return "", False, None | |
| # 1. 음성 인식 | |
| transcribed_text = transcribe_audio(audio_file) | |
| if not transcribed_text: | |
| return "", False, None | |
| # 2. 반향어 감지 (간단한 컨텍스트로) | |
| is_echo, anchor, confidence = detect_echo(transcribed_text, transcribed_text) | |
| confidence_percent = confidence * 100 | |
| # 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 | |
| # 4. 분석 결과 저장 (프로필이 선택된 경우에만) | |
| if profile_id: | |
| input_summary, analysis_summary, key_recommendations = create_analysis_summary( | |
| transcribed_text, is_echo, confidence_percent, anchor, "", candidates | |
| ) | |
| profile_manager.save_analysis_result( | |
| profile_id=profile_id, | |
| analysis_type='audio', | |
| input_summary=input_summary, | |
| context_situation='음성 입력', | |
| is_echo=is_echo, | |
| confidence=confidence_percent, | |
| anchor_text=anchor, | |
| analysis_summary=analysis_summary, | |
| key_recommendations=key_recommendations | |
| ) | |
| print(f"DEBUG: Audio analysis result saved for profile {profile_id}") | |
| return transcribed_text, is_echo, audio_response | |
| def process_voice_with_status(audio_file) -> Tuple[str, bool, str, str]: | |
| """음성 처리 및 저장 상태 반환""" | |
| transcribed_text, is_echo, audio_response = process_voice(audio_file) | |
| # 저장 상태 메시지 (음성 분석은 현재 프로필 선택 기능이 없으므로 기본 메시지) | |
| save_message = "ℹ️ 음성 분석 결과는 현재 저장되지 않습니다. 프로필 선택 기능이 추가되면 자동 저장됩니다." | |
| return transcribed_text, is_echo, audio_response, save_message | |
| # 구조화된 검사 관련 함수들 | |
| def get_assessment_categories(): | |
| """검사 카테고리 목록 반환""" | |
| return assessment_service.get_categories() | |
| def create_new_assessment(selected_categories, questions_per_category): | |
| """새로운 검사 세션 생성""" | |
| print(f"DEBUG: create_new_assessment called with categories={selected_categories}, questions_per_category={questions_per_category}") | |
| print(f"DEBUG: questions_per_category type: {type(questions_per_category)}") | |
| if not selected_categories: | |
| selected_categories = None | |
| print("DEBUG: No categories selected, using default") | |
| # questions_per_category가 None이거나 잘못된 값인 경우 기본값 사용 | |
| if questions_per_category is None or questions_per_category not in [1, 2, 3, 4, 5]: | |
| print(f"DEBUG: Invalid questions_per_category ({questions_per_category}), using default value 3") | |
| questions_per_category = 3 | |
| session = assessment_service.create_assessment_session(selected_categories, questions_per_category) | |
| print(f"DEBUG: Created session: {session}") | |
| print(f"DEBUG: Total questions in session: {session.get('total_questions', 'N/A')}") | |
| return session | |
| 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 | |
| print(f"DEBUG: Parsed session: {session}") | |
| result = assessment_service.get_current_question_info(session) | |
| print(f"DEBUG: get_current_question_info returned: {result}") | |
| return result | |
| 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_info = assessment_service.get_current_question_info(session) | |
| if not current_question_info or not current_question_info[0]: | |
| 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 | |
| # 현재 질문 정보 추출 | |
| question_text, context, question_num, category, progress, remaining, total, completion_rate = current_question_info | |
| # 반향어 감지 및 변환 | |
| is_echo, anchor, confidence = detect_echo(response_text, question_text, context) | |
| converted_responses = shape_echolalia(anchor, "L1", context) if is_echo else [] | |
| # 응답 데이터 추가 | |
| response_data = { | |
| "question": question_text, | |
| "response": response_text, | |
| "echo_detected": is_echo, | |
| "confidence": confidence, | |
| "converted_responses": converted_responses, | |
| "category": session["selected_categories"][session.get("current_category_index", 0)], | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| session["responses"].append(response_data) | |
| session["completed_questions"] = len(session["responses"]) | |
| # 다음 질문으로 이동 | |
| session = assessment_service.move_to_next_question(session) | |
| # 다음 질문 정보 가져오기 | |
| next_question_info = assessment_service.get_current_question_info(session) | |
| if next_question_info and next_question_info[0]: | |
| next_question_text, next_context, next_question_num, next_category, next_progress, next_remaining, next_total, next_completion_rate = next_question_info | |
| return ( | |
| json.dumps(session), | |
| next_question_text, | |
| next_context, | |
| next_question_num, | |
| next_category, | |
| next_progress, | |
| next_remaining, | |
| next_total, | |
| next_completion_rate, | |
| "", # 응답 입력란 초기화 | |
| is_echo, # 반향어 감지 결과 | |
| convert_confidence_to_level(confidence * 100), # 신뢰도 수준 (3단계) | |
| "\n".join(converted_responses) if converted_responses else "반향어가 감지되지 않았습니다." # 변환된 응답 | |
| ) | |
| else: | |
| # 검사 완료 | |
| total_questions = session.get('total_questions', 0) | |
| completed_questions = len(session.get('responses', [])) | |
| echo_detected_count = sum(1 for response in session.get('responses', []) if response.get('echo_detected', False)) | |
| echo_detection_rate = (echo_detected_count / completed_questions * 100) if completed_questions > 0 else 0 | |
| return ( | |
| json.dumps(session), | |
| f"🎉 검사 완료! 총 {total_questions}개 질문 중 {completed_questions}개 완료", | |
| f"반향어 감지율: {echo_detection_rate:.1f}%", | |
| "완료", | |
| "완료", | |
| "완료", | |
| total_questions, | |
| total_questions, | |
| completed_questions, | |
| f"✅ 검사가 완료되었습니다!\n\n📊 검사 결과 요약 버튼을 클릭하여 상세한 결과를 확인하세요.\n\n반향어 감지율: {echo_detection_rate:.1f}%", | |
| is_echo, # 마지막 질문의 반향어 감지 결과 | |
| convert_confidence_to_level(confidence * 100), # 마지막 질문의 신뢰도 수준 | |
| "\n".join(converted_responses) if converted_responses else "반향어가 감지되지 않았습니다." # 마지막 질문의 변환된 응답 | |
| ) | |
| def update_progress_info(session_data): | |
| """진행 상황 정보 업데이트""" | |
| if not session_data: | |
| return "검사를 시작해주세요" | |
| try: | |
| session = json.loads(session_data) if isinstance(session_data, str) else session_data | |
| completed = session.get('completed_questions', 0) | |
| total = session.get('total_questions', 1) | |
| progress_rate = (completed / total * 100) if total > 0 else 0 | |
| return f"진행률: {completed}/{total} ({progress_rate:.1f}%)" | |
| except Exception as e: | |
| print(f"진행 상황 업데이트 오류: {e}") | |
| return "진행 상황을 불러올 수 없습니다" | |
| def get_assessment_summary(session_data): | |
| """검사 결과 요약 반환""" | |
| if not session_data: | |
| return "검사 데이터가 없습니다." | |
| try: | |
| session = json.loads(session_data) if isinstance(session_data, str) else session_data | |
| # 기본 통계 계산 | |
| total_questions = session.get('total_questions', 0) | |
| responses = session.get('responses', []) | |
| completed_questions = len(responses) | |
| echo_detected_count = sum(1 for response in responses if response.get('echo_detected', False)) | |
| echo_detection_rate = (echo_detected_count / completed_questions * 100) if completed_questions > 0 else 0 | |
| completion_rate = (completed_questions / total_questions * 100) if total_questions > 0 else 0 | |
| # 카테고리별 통계 계산 | |
| category_stats = {} | |
| for response in responses: | |
| category = response.get('category', 'unknown') | |
| if category not in category_stats: | |
| category_stats[category] = {'total': 0, 'completed': 0, 'echo_detected': 0} | |
| category_stats[category]['completed'] += 1 | |
| if response.get('echo_detected', False): | |
| category_stats[category]['echo_detected'] += 1 | |
| # 카테고리별 감지율 계산 | |
| for category in category_stats: | |
| stats = category_stats[category] | |
| stats['echo_rate'] = (stats['echo_detected'] / stats['completed'] * 100) if stats['completed'] > 0 else 0 | |
| # 카테고리 이름 매핑 | |
| category_names = { | |
| 'language_comprehension': '언어 이해', | |
| 'language_expression': '언어 표현', | |
| 'social_communication': '사회적 의사소통', | |
| 'cognitive_function': '인지 기능' | |
| } | |
| result_text = f""" | |
| ## 📊 검사 결과 요약 | |
| **세션 ID**: {session.get('created_at', 'N/A')[:10]} | |
| **총 질문 수**: {total_questions}개 | |
| **완료된 질문**: {completed_questions}개 | |
| **완료율**: {completion_rate:.1f}% | |
| **반향어 감지**: {echo_detected_count}개 | |
| **반향어 감지율**: {echo_detection_rate:.1f}% | |
| ### 📈 카테고리별 결과 | |
| """ | |
| for category, stats in category_stats.items(): | |
| category_name = category_names.get(category, category) | |
| result_text += f""" | |
| **{category_name}**: | |
| - 질문 수: {stats['completed']}개 | |
| - 완료: {stats['completed']}개 | |
| - 반향어 감지: {stats['echo_detected']}개 | |
| - 감지율: {stats['echo_rate']:.1f}% | |
| """ | |
| # 개별 응답 상세 정보 | |
| result_text += "\n### 📝 개별 응답 상세\n" | |
| for i, response in enumerate(responses, 1): | |
| echo_status = "✅ 반향어 감지" if response.get('echo_detected', False) else "❌ 반향어 미감지" | |
| confidence = response.get('confidence', 0) * 100 | |
| result_text += f""" | |
| **질문 {i}**: {response.get('question', 'N/A')} | |
| - 응답: {response.get('response', 'N/A')} | |
| - 상태: {echo_status} | |
| - 신뢰도: {confidence:.1f}% | |
| """ | |
| if response.get('converted_responses'): | |
| result_text += f"- 변환된 응답: {', '.join(response['converted_responses'])}\n" | |
| return result_text | |
| except Exception as e: | |
| print(f"검사 결과 요약 생성 오류: {e}") | |
| return f"검사 결과 요약 생성 중 오류가 발생했습니다: {str(e)}" | |
| # 대본 기반 녹음 관련 함수들 | |
| def get_script_categories(): | |
| """대본 카테고리 목록 반환""" | |
| print(f"DEBUG: get_script_categories called") | |
| categories = script_service.get_categories() | |
| print(f"DEBUG: get_script_categories returned: {categories}") | |
| return categories | |
| def get_scripts_by_category(category_key): | |
| """특정 카테고리의 대본 목록 반환""" | |
| print(f"DEBUG: get_scripts_by_category called with category_key='{category_key}'") | |
| scripts = script_service.get_scripts_by_category(category_key) | |
| print(f"DEBUG: get_scripts_by_category returned: {scripts}") | |
| return scripts | |
| def create_script_session(selected_scripts): | |
| """새로운 대본 녹음 세션 생성""" | |
| print(f"DEBUG: create_script_session called with selected_scripts: {selected_scripts}") | |
| if not selected_scripts: | |
| print("DEBUG: No scripts selected, returning None") | |
| return None | |
| # CheckboxGroup에서 선택된 값들을 정리 | |
| # selected_scripts가 리스트의 리스트 형태일 수 있으므로 평면화 | |
| clean_scripts = [] | |
| for script in selected_scripts: | |
| if isinstance(script, list): | |
| # 리스트인 경우 첫 번째 요소만 사용 (중복 제거) | |
| clean_scripts.append(script[0]) | |
| else: | |
| clean_scripts.append(script) | |
| # 중복 제거 | |
| clean_scripts = list(set(clean_scripts)) | |
| print(f"DEBUG: Cleaned scripts: {clean_scripts}") | |
| session = script_service.create_recording_session(clean_scripts) | |
| print(f"DEBUG: Created session: {session}") | |
| return session | |
| def get_current_script_info(session_data): | |
| """현재 대본 정보 반환""" | |
| print(f"DEBUG: get_current_script_info called with session_data: {session_data}") | |
| if not session_data: | |
| print("DEBUG: No session_data, returning None values") | |
| return None, None, None, None, None, None, None, None | |
| session = json.loads(session_data) if isinstance(session_data, str) else session_data | |
| print(f"DEBUG: Parsed session: {session}") | |
| current_script = script_service.get_current_script(session) | |
| print(f"DEBUG: Current script: {current_script}") | |
| if not current_script: | |
| print("DEBUG: No current script, returning None values") | |
| 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) | |
| result = ( | |
| current_script["title"], | |
| current_script["situation"], | |
| question, | |
| current_script["difficulty"], | |
| question_num, | |
| total_questions, | |
| progress_text, | |
| current_script["id"] | |
| ) | |
| print(f"DEBUG: get_current_script_info returning: {result}") | |
| return result | |
| 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 "검사 데이터가 없습니다." | |
| try: | |
| session = json.loads(session_data) if isinstance(session_data, str) else session_data | |
| # 기본 통계 계산 (get_assessment_summary와 동일한 로직) | |
| total_questions = session.get('total_questions', 0) | |
| responses = session.get('responses', []) | |
| completed_questions = len(responses) | |
| echo_detected_count = sum(1 for response in responses if response.get('echo_detected', False)) | |
| echo_detection_rate = (echo_detected_count / completed_questions * 100) if completed_questions > 0 else 0 | |
| completion_rate = (completed_questions / total_questions * 100) if total_questions > 0 else 0 | |
| # 카테고리별 통계 계산 | |
| category_stats = {} | |
| for response in responses: | |
| category = response.get('category', 'unknown') | |
| if category not in category_stats: | |
| category_stats[category] = {'total': 0, 'completed': 0, 'echo_detected': 0} | |
| category_stats[category]['completed'] += 1 | |
| if response.get('echo_detected', False): | |
| category_stats[category]['echo_detected'] += 1 | |
| # 카테고리별 감지율 계산 | |
| for category in category_stats: | |
| stats = category_stats[category] | |
| stats['echo_rate'] = (stats['echo_detected'] / stats['completed'] * 100) if stats['completed'] > 0 else 0 | |
| # 요약 데이터 구성 | |
| summary = { | |
| 'session_id': session.get('created_at', 'N/A'), | |
| 'created_at': session.get('created_at', datetime.now().isoformat()), | |
| 'total_questions': total_questions, | |
| 'completed_questions': completed_questions, | |
| 'completion_rate': completion_rate, | |
| 'echo_detected_count': echo_detected_count, | |
| 'echo_detected': echo_detected_count, | |
| 'echo_detection_rate': echo_detection_rate, | |
| 'category_stats': category_stats | |
| } | |
| # 환자 정보 구성 | |
| patient_info = { | |
| 'name': patient_name, | |
| 'age': patient_age, | |
| 'gender': patient_gender, | |
| 'assessment_date': summary.get('created_at', datetime.now().isoformat()) | |
| } | |
| # 치료 계획 생성 | |
| treatment_plan = treatment_service.generate_treatment_plan(summary, patient_info) | |
| return treatment_plan | |
| except Exception as e: | |
| print(f"치료 계획 생성 오류: {e}") | |
| return f"치료 계획 생성 중 오류가 발생했습니다: {str(e)}" | |
| 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(): | |
| # 시스템 기본 테마 사용 (사용자 환경에 따라 자동 조정) | |
| with gr.Blocks( | |
| title="Coconut AI", | |
| theme=gr.themes.Default(), # 시스템 기본 테마 | |
| css=""" | |
| /* 최소한의 스타일링만 적용 - 배경색은 시스템 기본값 사용 */ | |
| .gradio-container { | |
| max-width: 1200px !important; | |
| margin: 0 auto !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: #333 !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; | |
| } | |
| /* 보고서 컨테이너 스타일 */ | |
| .report-container { | |
| background: #f8f9fa !important; | |
| border: 1px solid #e9ecef !important; | |
| border-radius: 8px !important; | |
| padding: 20px !important; | |
| margin-top: 10px !important; | |
| color: #212529 !important; | |
| } | |
| .report-container h2 { | |
| color: #495057 !important; | |
| border-bottom: 2px solid #007bff !important; | |
| padding-bottom: 10px !important; | |
| } | |
| .report-container h3 { | |
| color: #6c757d !important; | |
| margin-top: 20px !important; | |
| } | |
| .report-container p { | |
| color: #212529 !important; | |
| } | |
| .report-container strong { | |
| color: #495057 !important; | |
| } | |
| .report-container ul { | |
| color: #212529 !important; | |
| } | |
| .report-container li { | |
| color: #212529 !important; | |
| } | |
| /* 버튼 스타일 - 푸른색 계열(보라색) - 강력한 오버라이드 */ | |
| .gr-button, | |
| .gr-button.primary, | |
| button[data-testid="primary-button"], | |
| button[class*="primary"], | |
| .gradio-container .gr-button, | |
| .gradio-container .gr-button.primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| background-color: #667eea !important; | |
| color: #333 !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: 8px 0 !important; | |
| min-height: 50px !important; | |
| } | |
| .gr-button:hover, | |
| .gr-button.primary:hover, | |
| button[data-testid="primary-button"]:hover, | |
| button[class*="primary"]:hover, | |
| .gradio-container .gr-button:hover, | |
| .gradio-container .gr-button.primary:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4) !important; | |
| background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important; | |
| background-color: #5a67d8 !important; | |
| } | |
| .gr-button:focus, | |
| .gr-button.primary:focus, | |
| button[data-testid="primary-button"]:focus, | |
| button[class*="primary"]:focus, | |
| .gradio-container .gr-button:focus, | |
| .gradio-container .gr-button.primary:focus { | |
| outline: none !important; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2) !important; | |
| } | |
| /* Primary 버튼 특별 스타일 - 더 강력한 오버라이드 */ | |
| .gr-button.primary, | |
| button[data-testid="primary-button"], | |
| button[class*="primary"] { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| background-color: #667eea !important; | |
| box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4) !important; | |
| } | |
| .gr-button.primary:hover, | |
| button[data-testid="primary-button"]:hover, | |
| button[class*="primary"]:hover { | |
| background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important; | |
| background-color: #5a67d8 !important; | |
| box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5) !important; | |
| } | |
| /* Secondary 버튼 스타일 */ | |
| .gr-button.secondary, | |
| button[data-testid="secondary-button"], | |
| button[class*="secondary"] { | |
| background: linear-gradient(135deg, #a78bfa 0%, #c084fc 100%) !important; | |
| background-color: #a78bfa !important; | |
| color: #333 !important; | |
| box-shadow: 0 4px 15px rgba(167, 139, 250, 0.3) !important; | |
| } | |
| .gr-button.secondary:hover, | |
| button[data-testid="secondary-button"]:hover, | |
| button[class*="secondary"]:hover { | |
| background: linear-gradient(135deg, #9333ea 0%, #a855f7 100%) !important; | |
| background-color: #9333ea !important; | |
| box-shadow: 0 8px 25px rgba(167, 139, 250, 0.4) !important; | |
| } | |
| /* Gradio 기본 테마 오버라이드 */ | |
| .gradio-container [data-testid="primary-button"], | |
| .gradio-container button[class*="primary"], | |
| .gradio-container .gr-button[class*="primary"] { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| background-color: #667eea !important; | |
| } | |
| .gradio-container [data-testid="primary-button"]:hover, | |
| .gradio-container button[class*="primary"]:hover, | |
| .gradio-container .gr-button[class*="primary"]:hover { | |
| background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%) !important; | |
| background-color: #5a67d8 !important; | |
| } | |
| /* 피드백 섹션 스타일 */ | |
| .feedback-section { | |
| background: #f8f9fa !important; | |
| border: 1px solid #e9ecef !important; | |
| border-radius: 8px !important; | |
| padding: 20px !important; | |
| margin-top: 15px !important; | |
| } | |
| .stats-container { | |
| background: #ffffff !important; | |
| border: 1px solid #dee2e6 !important; | |
| border-radius: 8px !important; | |
| padding: 20px !important; | |
| margin: 10px 0 !important; | |
| } | |
| .stats-container h2 { | |
| color: #495057 !important; | |
| border-bottom: 2px solid #007bff !important; | |
| padding-bottom: 10px !important; | |
| } | |
| .stats-container h3 { | |
| color: #6c757d !important; | |
| margin-top: 15px !important; | |
| } | |
| /* 나머지 스타일은 시스템 기본값 사용 */ | |
| """ | |
| ) as demo: | |
| # 메인 헤더 | |
| with gr.Row(elem_classes=["main-header"]): | |
| gr.HTML(""" | |
| <h1>🗣️ COCONUT AI</h1> | |
| <p style="font-size: 1.3rem; margin-top: 0.5rem;">아이의 말을 이해하고 의미 있는 대화로 바꾸는 AI 도우미</p> | |
| <div style="background: rgba(255,255,255,0.15); padding: 20px; border-radius: 15px; margin: 25px auto; max-width: 600px; backdrop-filter: blur(10px);"> | |
| <h3 style="margin-top: 0; font-size: 1.4rem;">✨ 시작하기</h3> | |
| <div style="text-align: left; font-size: 1rem;"> | |
| <p style="margin: 5px 0;"><strong>1️⃣</strong> 프로필 만들기 (선택사항)</p> | |
| <p style="margin: 5px 0;"><strong>2️⃣</strong> 텍스트 분석에 아이의 말 입력</p> | |
| </div> | |
| <p style="font-size: 0.95rem; opacity: 0.9; margin-bottom: 0; margin-top: 15px;"> | |
| 💡 <strong>더 간단하게</strong>: 프로필 없이 바로 텍스트 분석부터 시작할 수 있어요 | |
| </p> | |
| </div> | |
| """) | |
| # 탭 네비게이션 | |
| with gr.Tabs(): | |
| # 사용자 프로필 탭 (우선순위 1) | |
| with gr.Tab("👤 사용자 프로필"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 👶 아이 정보 관리 | |
| 아이의 기본 정보를 입력하여 개인화된 분석을 받아보세요. | |
| """) | |
| # 프로필 생성 | |
| with gr.Group(): | |
| gr.Markdown("#### 새 프로필 생성") | |
| new_child_name = gr.Textbox(label="아이 이름", placeholder="예: 민수") | |
| new_age = gr.Number(label="나이", value=5, minimum=1, maximum=18) | |
| new_vocabulary = gr.Dropdown( | |
| label="어휘능력 수준", | |
| choices=["초급", "중급", "고급"], | |
| value="중급" | |
| ) | |
| new_language_habits = gr.Textbox( | |
| label="언어습관 (선택사항)", | |
| placeholder="예: 반복 말하기, 짧은 문장 선호 등", | |
| lines=2 | |
| ) | |
| create_profile_btn = gr.Button("프로필 생성", variant="primary") | |
| # 기존 프로필 선택 | |
| with gr.Group(): | |
| gr.Markdown("#### 기존 프로필 선택") | |
| profile_dropdown = gr.Dropdown( | |
| label="프로필 선택", | |
| choices=[], | |
| value=None | |
| ) | |
| load_profile_btn = gr.Button("프로필 불러오기", variant="secondary") | |
| with gr.Column(scale=2): | |
| # 프로필 정보 표시 | |
| profile_display = gr.Markdown("프로필을 선택하거나 새로 생성해주세요.") | |
| # CARS 검사 섹션 (컴팩트 버전) | |
| with gr.Group(): | |
| gr.Markdown("#### 🧠 CARS 검사 (아동기 자폐증 평정척도)") | |
| with gr.Accordion("📋 검사 안내", open=False): | |
| gr.Markdown(""" | |
| **검사 방법**: 각 문항을 읽고 아이의 상태에 가장 적합한 점수를 선택해주세요. | |
| - **1점**: 정상 범위 | |
| - **2점**: 약간의 문제 | |
| - **3점**: 명확한 문제 | |
| - **4점**: 심각한 문제 | |
| **15개 문항**: 사람과의 관계, 모방, 정서적 반응, 신체사용, 물체 사용, 적응력의 변화, 시각적 반응, 청각적 반응, 미각/후각/촉각 반응, 공포와 불안, 언어적 의사소통, 비언어적 의사소통, 활동수준, 지적 기능의 수준과 항상성, 일반적인 인상 | |
| """) | |
| # 완전한 CARS 검사 (15개 문항) | |
| gr.Markdown("**완전한 CARS 검사** (15개 문항)") | |
| cars_scores = [] | |
| # CARS 문항별 상세 설명과 평가 기준 | |
| cars_questions = [ | |
| { | |
| "title": "1. 사람과의 관계", | |
| "description": "어른과의 상호작용 정도", | |
| "criteria": { | |
| "1": "수줍음, 부산함, 성가심이 연령상 적절함", | |
| "2": "어른과의 눈맞춤을 피하고 상호작용을 강요하면 어른을 피하고 당황해 함", | |
| "3": "때때로 어른을 의식하지 못하는 듯이 혼자 떨어져 있으며 아동의 주의를 끌기 위해서는 지속적이고도 강력한 시도가 필요", | |
| "4": "어른의 일로부터 지속적으로 떨어져 있고, 알지 못하는 어른에게 절대 반응 없음" | |
| } | |
| }, | |
| { | |
| "title": "2. 모방", | |
| "description": "소리, 단어, 움직임을 모방하는 능력", | |
| "criteria": { | |
| "1": "아동의 능력수준에 적절한 소리, 단어, 움직임을 모방할 수 있음", | |
| "2": "박수를 치거나 단음소리와 같은 간단한 행동을 늘 모방함", | |
| "3": "어떤 때만 모방하고 어른의 도움과 지속적인 노력이 필요함", | |
| "4": "어른의 도움과 자극이 있을 때조차 소리나 단어, 움직임 모방이 결코 없음" | |
| } | |
| }, | |
| { | |
| "title": "3. 정서적 반응", | |
| "description": "얼굴표현, 자세, 태도의 변화로 보여주는 정서적 반응", | |
| "criteria": { | |
| "1": "얼굴표현, 자세, 태도의 변화로써 보여주는 정서적 반응의 정도와 형태가 적절함", | |
| "2": "때때로 다소 부적절한 형태나 부적절한 정도의 정서적 반응으로 보임", | |
| "3": "확실히 부적절한 형태나 부적절한 정도의 정서적 반응을 보임", | |
| "4": "반응들이 거의 그 상황에 적절하지 않음" | |
| } | |
| }, | |
| { | |
| "title": "4. 신체사용", | |
| "description": "신체의 안정성, 민감성, 협응성", | |
| "criteria": { | |
| "1": "연령상 적절한 안정성, 민감성, 협응성을 지니고 움직임", | |
| "2": "둔하고, 반복적으로 움직이거나 반응력이 다소 약함", | |
| "3": "연령상 분명한 부적절한 행동이 있음", | |
| "4": "이상한 손가락 운동, 신체의 자세, 몸흔들기, 돌기 등 심각한 문제" | |
| } | |
| }, | |
| { | |
| "title": "5. 물체 사용", | |
| "description": "물체를 적절하게 사용하는 능력", | |
| "criteria": { | |
| "1": "물체를 적절한 방법으로 사용함", | |
| "2": "물체 사용이 다소 부적절하거나 제한적임", | |
| "3": "물체를 부적절하게 사용하거나 특이한 방식으로 사용함", | |
| "4": "물체를 전혀 적절하게 사용하지 못함" | |
| } | |
| }, | |
| { | |
| "title": "6. 적응력의 변화", | |
| "description": "환경 변화에 대한 적응 능력", | |
| "criteria": { | |
| "1": "환경 변화에 적절히 적응함", | |
| "2": "환경 변화에 다소 어려움을 보임", | |
| "3": "환경 변화에 명확한 어려움을 보임", | |
| "4": "환경 변화에 심각한 어려움을 보임" | |
| } | |
| }, | |
| { | |
| "title": "7. 시각적 반응", | |
| "description": "시각적 자극에 대한 반응", | |
| "criteria": { | |
| "1": "시각적 자극에 적절히 반응함", | |
| "2": "시각적 자극에 다소 부적절한 반응을 보임", | |
| "3": "시각적 자극에 명확한 부적절한 반응을 보임", | |
| "4": "시각적 자극에 심각한 부적절한 반응을 보임" | |
| } | |
| }, | |
| { | |
| "title": "8. 청각적 반응", | |
| "description": "청각적 자극에 대한 반응", | |
| "criteria": { | |
| "1": "청각적 자극에 적절히 반응함", | |
| "2": "청각적 자극에 다소 부적절한 반응을 보임", | |
| "3": "청각적 자극에 명확한 부적절한 반응을 보임", | |
| "4": "청각적 자극에 심각한 부적절한 반응을 보임" | |
| } | |
| }, | |
| { | |
| "title": "9. 미각, 후각, 촉각 반응", | |
| "description": "미각, 후각, 촉각 자극에 대한 반응", | |
| "criteria": { | |
| "1": "미각, 후각, 촉각 자극에 적절히 반응함", | |
| "2": "미각, 후각, 촉각 자극에 다소 부적절한 반응을 보임", | |
| "3": "미각, 후각, 촉각 자극에 명확한 부적절한 반응을 보임", | |
| "4": "미각, 후각, 촉각 자극에 심각한 부적절한 반응을 보임" | |
| } | |
| }, | |
| { | |
| "title": "10. 공포와 불안", | |
| "description": "공포와 불안에 대한 반응", | |
| "criteria": { | |
| "1": "공포와 불안에 적절히 반응함", | |
| "2": "공포와 불안에 다소 부적절한 반응을 보임", | |
| "3": "공포와 불안에 명확한 부적절한 반응을 보임", | |
| "4": "공포와 불안에 심각한 부적절한 반응을 보임" | |
| } | |
| }, | |
| { | |
| "title": "11. 언어적 의사소통", | |
| "description": "언어를 통한 의사소통 능력", | |
| "criteria": { | |
| "1": "언어적 의사소통이 적절함", | |
| "2": "언어적 의사소통에 다소 문제가 있음", | |
| "3": "언어적 의사소통에 명확한 문제가 있음", | |
| "4": "언어적 의사소통에 심각한 문제가 있음" | |
| } | |
| }, | |
| { | |
| "title": "12. 비언어적 의사소통", | |
| "description": "비언어적 의사소통 능력", | |
| "criteria": { | |
| "1": "비언어적 의사소통이 적절함", | |
| "2": "비언어적 의사소통에 다소 문제가 있음", | |
| "3": "비언어적 의사소통에 명확한 문제가 있음", | |
| "4": "비언어적 의사소통에 심각한 문제가 있음" | |
| } | |
| }, | |
| { | |
| "title": "13. 활동수준", | |
| "description": "일반적인 활동 수준", | |
| "criteria": { | |
| "1": "같은 연령의 정상아동보다 비슷한 상황에서 활동수준이 과잉적이거나 과소적이지 않음", | |
| "2": "약간 안절부절하거나 다소 느리게 움직임", | |
| "3": "매우 활동적이고 제지하기가 어렵거나 다소 무기력함", | |
| "4": "활동성과 비활동성의 극한성을 보이며 양극단으로 옮겨감" | |
| } | |
| }, | |
| { | |
| "title": "14. 지적 기능의 수준과 항상성", | |
| "description": "지적 기능의 수준과 일관성", | |
| "criteria": { | |
| "1": "같은 연령에 비교했을 때 특이한 지적기술이나 다른 문제점이 없음", | |
| "2": "같은 연령의 아동들만큼 똑똑하지 않고 전 영역에 전체능력이 상당히 지체됨", | |
| "3": "하나 혹은 그 이상의 지적 영역에서 거의 정상적으로 가능함", | |
| "4": "하나 이상의 영역에서 정상아동보다 더 잘 기능함" | |
| } | |
| }, | |
| { | |
| "title": "15. 일반적인 인상", | |
| "description": "전반적인 자폐증 증상의 정도", | |
| "criteria": { | |
| "1": "자폐증이 아님", | |
| "2": "아주 약간의 증상이 보이며 경한 정도의 자폐증", | |
| "3": "다소 많은 증상을 보이며 중간정도의 자폐증", | |
| "4": "많은 증상을 보이며 심한(중증의) 자폐증" | |
| } | |
| } | |
| ] | |
| for i, question_data in enumerate(cars_questions): | |
| with gr.Group(): | |
| gr.Markdown(f"### {question_data['title']}") | |
| gr.Markdown(f"**설명**: {question_data['description']}") | |
| with gr.Row(): | |
| with gr.Column(scale=3): | |
| gr.Markdown("**평가 기준**:") | |
| for score, criteria in question_data['criteria'].items(): | |
| gr.Markdown(f"• **{score}점**: {criteria}") | |
| with gr.Column(scale=1): | |
| score = gr.Radio( | |
| choices=[1, 1.5, 2, 2.5, 3, 3.5, 4], | |
| value=2, | |
| label="점수 선택" | |
| ) | |
| cars_scores.append(score) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| cars_calculate_btn = gr.Button("CARS 점수 계산", variant="primary") | |
| with gr.Column(scale=1): | |
| cars_save_btn = gr.Button("💾 CARS 결과 저장", variant="secondary") | |
| cars_result = gr.Markdown("검사를 완료하고 점수를 계산해주세요.") | |
| cars_save_result = gr.Markdown("") | |
| # 프로필 관리 탭 이벤트 연결 (text_profile_dropdown 정의 후에 연결) | |
| load_profile_btn.click( | |
| fn=load_profile, | |
| inputs=[profile_dropdown], | |
| outputs=[profile_display] | |
| ) | |
| cars_calculate_btn.click( | |
| fn=calculate_cars_score, | |
| inputs=cars_scores, | |
| outputs=[cars_result] | |
| ) | |
| cars_save_btn.click( | |
| fn=save_cars_result, | |
| inputs=[profile_dropdown] + cars_scores, | |
| outputs=[cars_save_result, profile_dropdown] | |
| ) | |
| # 텍스트 분석 탭 (우선순위 2) | |
| with gr.Tab("📝 텍스트 분석"): | |
| # Phase 1: 분석 모드 선택 추가 (숨김) | |
| with gr.Row(visible=False): | |
| analysis_mode = gr.Radio( | |
| label="⚡ 분석 모드 선택", | |
| choices=[ | |
| ("🚀 빠른 분석", "quick"), | |
| ("📊 상세 분석", "detailed") | |
| ], | |
| value="detailed", | |
| info="빠른 분석: 핵심 결과만, 상세 분석: 3단계 분석 전체" | |
| ) | |
| # 프로필 선택 섹션 | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| # 프로필 선택 | |
| text_profile_dropdown = gr.Dropdown( | |
| label="👤 아이 프로필 (선택사항)", | |
| choices=[], | |
| value=None, | |
| info="프로필을 선택하면 개인화된 분석을 받을 수 있습니다" | |
| ) | |
| with gr.Column(scale=2): | |
| pass | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| # 프로필 불러오기 버튼 (컴팩트) | |
| with gr.Row(): | |
| load_profile_btn = gr.Button("📋 프로필 불러오기", variant="secondary", scale=1) | |
| clear_profile_btn = gr.Button("🗑️ 초기화", variant="secondary", scale=1) | |
| # 프로필 정보 표시 | |
| profile_info_display = gr.Markdown( | |
| "프로필을 선택하고 불러오기를 클릭하면 아이의 정보와 CARS 검사 결과가 표시됩니다.", | |
| elem_classes=["profile-info"] | |
| ) | |
| text_input = gr.Textbox( | |
| label="입력 텍스트", | |
| placeholder="아이가 말한 발화를 적어주세요", | |
| lines=6, | |
| value="다음 시간에 또 만나요!" | |
| ) | |
| context_situation = gr.Textbox( | |
| label="상황 설명 (선택사항)", | |
| placeholder="예: 엄마가 방을 나가려 할 때 갑자기 이 말을 반복함", | |
| lines=3, | |
| value="엄마가 방을 나가려 할 때 갑자기 이 말을 반복함" | |
| ) | |
| situation_input = gr.Dropdown( | |
| label="상황 (선택사항)", | |
| choices=["기타", "식사 시간", "놀이 시간", "외출 준비", "수업 시간", "휴식 시간", "이별/분리 상황"], | |
| value="이별/분리 상황", | |
| ) | |
| process_btn = gr.Button("🔍 분석하기", variant="primary", elem_classes=["soft-button"]) | |
| # 채팅 모드 열기 버튼 (채팅 섹션 바로 위) | |
| chat_mode_btn = gr.Button("💬 채팅 모드 열기", variant="secondary") | |
| # 채팅 모드 섹션 (Accordion으로 접었다 펼 수 있게) | |
| with gr.Accordion("💬 채팅 모드 - 추가 정보 수집", open=False, visible=False) as chat_mode_popup: | |
| chat_mode_chatbot = gr.Chatbot( | |
| label="대화", | |
| height=400, | |
| show_label=True | |
| ) | |
| chat_mode_input = gr.Textbox( | |
| label="메시지 입력", | |
| placeholder="질문에 답변하거나 추가 정보를 입력하세요...", | |
| lines=2 | |
| ) | |
| with gr.Row(): | |
| chat_mode_send_btn = gr.Button("전송", variant="primary") | |
| chat_mode_close_btn = gr.Button("닫기", variant="secondary") | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 📖 사용방법 | |
| 1. **입력 텍스트**에 반향어가 의심되는 문장 입력 | |
| 2. **상황 설명**에 구체적인 상황 정보 입력 (선택사항) | |
| 3. **상황** 드롭다운에서 적절한 상황 선택 | |
| 4. **"🔍 분석하기"** 버튼 클릭 | |
| ### 🧪 테스트 케이스 예시 | |
| **지연반향어 케이스:** | |
| - "다음 시간에 또 만나요!" (TV 광고 문장을 분리 상황에 사용) | |
| - "좋은 아침이에요!" (저녁에 반복) | |
| - "오늘 날씨 참 좋네요" (실내에서 반복) | |
| **즉시반향어 케이스:** | |
| - "밥 먹었어?" (질문 그대로 반복) | |
| - "뭐 하고 있어?, 뭐 하고 있어?" (질문 즉시 반복) | |
| **정상 발화 케이스:** | |
| - "엄마, 가지 마" (직접적인 감정 표현) | |
| - "지금 배고파요" (상황에 맞는 표현) | |
| - "나랑 같이 있어줘" (구체적인 요청) | |
| """) | |
| # Phase 1: 신뢰도 색상 코딩 및 불확실성 표시 | |
| confidence_display = gr.HTML( | |
| value="<div style='text-align: center; padding: 20px;'><h3>분석 대기 중...</h3></div>" | |
| ) | |
| is_echo_output = gr.Checkbox( | |
| label="반향어 감지", | |
| interactive=False, | |
| ) | |
| # 빠른 분석 결과 영역 | |
| quick_summary = gr.Markdown( | |
| value="", | |
| visible=True | |
| ) | |
| # 3단계 변환 제거됨 (UI에서 숨김) | |
| three_step_conversion = gr.Markdown( | |
| value="", | |
| visible=False, | |
| elem_classes=["report-container"] | |
| ) | |
| # 상세 분석 보고서 (접을 수 있게) | |
| with gr.Accordion("📋 상세 분석 보고서", open=True): | |
| report_output = gr.Markdown( | |
| value="분석 결과가 여기에 표시됩니다.", | |
| elem_classes=["report-container"] | |
| ) | |
| # 저장 상태 표시 | |
| save_status = gr.Markdown( | |
| value="", | |
| elem_classes=["save-status"] | |
| ) | |
| # 사용자 피드백 섹션 추가 (숨김) | |
| with gr.Group(elem_classes=["feedback-section"], visible=False): | |
| gr.Markdown("### ⭐ 만족도 평가") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| user_rating = gr.Radio( | |
| label="분석 결과 만족도", | |
| choices=[ | |
| ("⭐ 매우 부적절", 1), | |
| ("⭐⭐ 부적절", 2), | |
| ("⭐⭐⭐ 보통", 3), | |
| ("⭐⭐⭐⭐ 적절", 4), | |
| ("⭐⭐⭐⭐⭐ 매우 적절", 5) | |
| ], | |
| value=3, | |
| info="분석 결과가 얼마나 도움이 되었는지 평가해주세요" | |
| ) | |
| with gr.Column(scale=2): | |
| user_feedback_text = gr.Textbox( | |
| label="추가 피드백 (선택사항)", | |
| placeholder="분석 결과에 대한 의견이나 개선 제안이 있다면 작성해주세요...", | |
| lines=3 | |
| ) | |
| with gr.Row(): | |
| submit_user_feedback_btn = gr.Button("⭐ 피드백 제출", variant="secondary") | |
| user_feedback_status = gr.Markdown(value="") | |
| # 보호자를 위한 상세 설명 추가 | |
| gr.Markdown(""" | |
| ### 💡 보호자를 위한 안내 | |
| **🔍 신뢰도란?** | |
| AI가 분석한 "반향어로 판정될 확률"입니다. | |
| - **높은 수준 (70%+)**: 반향어 가능성이 높음 → 전문가 상담 권장 ⚠️ | |
| - **보통 수준 (40-70%)**: 부분적 반향어 → 지속적 관찰 필요 📊 | |
| - **낮은 수준 (40% 미만)**: 정상적인 의사소통 ✅ | |
| """) | |
| # 분석 ID를 저장할 상태 변수 | |
| current_analysis_id = gr.State() | |
| # 채팅 모드 상태 관리 | |
| chat_mode_state = gr.State({ | |
| 'active': False, | |
| 'clarification_module': None, | |
| 'current_questions': [], | |
| 'collected_info': {} | |
| }) | |
| # 이벤트 연결 (Phase 1 업데이트) | |
| process_btn.click( | |
| fn=process_text, | |
| inputs=[text_input, context_situation, situation_input, text_profile_dropdown, analysis_mode, chat_mode_state], | |
| outputs=[is_echo_output, confidence_display, quick_summary, three_step_conversion, report_output, save_status, current_analysis_id] # three_step_conversion은 hidden | |
| ) | |
| # 사용자 피드백 제출 이벤트 연결 | |
| submit_user_feedback_btn.click( | |
| fn=submit_user_feedback, | |
| inputs=[current_analysis_id, user_rating, user_feedback_text], | |
| outputs=[user_feedback_status, user_rating, user_feedback_text] | |
| ) | |
| # 프로필 불러오기 이벤트 연결 | |
| load_profile_btn.click( | |
| fn=load_profile_for_analysis, | |
| inputs=[text_profile_dropdown], | |
| outputs=[profile_info_display] | |
| ) | |
| # 프로필 초기화 이벤트 연결 | |
| clear_profile_btn.click( | |
| fn=clear_profile_info, | |
| inputs=[], | |
| outputs=[profile_info_display] | |
| ) | |
| # 채팅 모드 이벤트 연결 | |
| def open_chat_mode(text_input_val, context_situation_val, situation_input_val, chat_state): | |
| """채팅 모드 열기""" | |
| try: | |
| if chat_state is None or not chat_state.get('clarification_module'): | |
| chat_state = { | |
| 'active': True, | |
| 'clarification_module': ClarificationModule(), | |
| 'current_questions': [], | |
| 'collected_info': {}, | |
| 'original_query': text_input_val or "" | |
| } | |
| else: | |
| # 기존 상태 업데이트 | |
| chat_state['active'] = True | |
| chat_state['original_query'] = text_input_val or "" | |
| # 기존 정보 수집 | |
| existing_info = { | |
| 'context_situation': context_situation_val or "", | |
| 'situation': situation_input_val or "" | |
| } | |
| # 모호성 탐지 | |
| should_clarify, ambiguity_result = chat_state['clarification_module'].should_clarify( | |
| text_input_val or "", | |
| existing_info | |
| ) | |
| # 초기 메시지 생성 (LLM 기반) | |
| initial_messages = [] | |
| try: | |
| # LLM으로 초기 인사 메시지 생성 | |
| context_summary = "" | |
| if text_input_val: | |
| context_summary = f"분석 중인 발화: {text_input_val}\n" | |
| if context_situation_val: | |
| context_summary += f"상황: {context_situation_val}\n" | |
| system_prompt = """너는 보호자의 불안을 줄이고, 아이의 주체적 발화를 돕는 언어 코치 AI이다. | |
| 사용자에게 친절하게 인사하고, 반향어 분석에 도움이 될 수 있도록 격려해주세요. | |
| - 진단을 시사하는 표현은 절대 사용하지 말 것 | |
| - 부모를 격려하고 긍정적인 톤으로 답변 | |
| - 간단하고 따뜻한 톤으로 1-2문장으로 답변해주세요. | |
| 한국어로 답변해주세요.""" | |
| user_prompt = f"안녕하세요. 반향어 분석을 시작하려고 합니다." | |
| if context_summary: | |
| user_prompt += f"\n\n{context_summary}" | |
| user_prompt += "\n\n반향어 분석에 대해 도움을 주시거나 필요한 정보를 물어봐주세요." | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ], | |
| max_tokens=200, | |
| temperature=0.7 | |
| ) | |
| initial_greeting = response.choices[0].message.content.strip() | |
| initial_messages.append(("assistant", initial_greeting)) | |
| except Exception as e: | |
| print(f"초기 메시지 생성 오류: {e}") | |
| initial_messages.append(("assistant", "안녕하세요! 반향어 분석을 도와드리겠습니다. 아이의 발화나 상황에 대해 질문해주세요.")) | |
| # 룰베이스 clarification은 정보 수집용으로만 유지 (필요시) | |
| if should_clarify and ambiguity_result: | |
| questions = chat_state['clarification_module'].get_clarifying_questions( | |
| ambiguity_result, | |
| text_input_val or "" | |
| ) | |
| chat_state['current_questions'] = questions | |
| return gr.update(visible=True, open=True), chat_state, initial_messages | |
| except Exception as e: | |
| print(f"채팅 모드 열기 오류: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| return gr.update(visible=True, open=True), chat_state or {}, [("assistant", "채팅 모드를 열었습니다. 질문해주세요.")] | |
| def close_chat_mode(chat_state): | |
| """채팅 모드 닫기""" | |
| try: | |
| if chat_state and chat_state.get('clarification_module'): | |
| chat_state['active'] = False | |
| if hasattr(chat_state['clarification_module'], 'reset'): | |
| chat_state['clarification_module'].reset() | |
| except Exception as e: | |
| print(f"채팅 모드 닫기 오류: {e}") | |
| return gr.update(visible=False, open=False), chat_state or {}, [] | |
| def handle_chat_message(message, history, chat_state): | |
| """채팅 메시지 처리 (LLM 기반)""" | |
| if not message.strip() or not chat_state or not chat_state.get('active'): | |
| return history, "" | |
| try: | |
| # 사용자 메시지 추가 | |
| history = history or [] | |
| history.append((message, None)) | |
| # LLM을 위한 시스템 프롬프트 (파이프라인 프롬프트 스타일 반영) | |
| system_prompt = """너는 보호자의 불안을 줄이고, 아이의 주체적 발화를 돕는 언어 코치 AI이다. | |
| 주요 역할: | |
| 1. 반향어에 대한 전문적이지만 이해하기 쉬운 설명 | |
| 2. 아이의 발화 패턴이나 언어 발달에 대한 구체적이고 실용적인 조언 | |
| 3. 상황에 맞는 즉시 실행 가능한 상호작용 팁 제공 | |
| 4. 보호자를 격려하고 긍정적인 피드백 제공 | |
| 중요한 지침: | |
| - 진단을 시사하는 표현(자폐, 장애, 문제 행동 등)은 절대 사용하지 말 것 | |
| - 부모의 잘못처럼 느껴지지 않도록, '이미 잘하고 계신 부분'을 인정해 주는 톤을 사용 | |
| - 문장은 짧고 구체적으로, 실제로 입 밖으로 말해볼 수 있는 표현으로 작성 | |
| - 전문 용어를 사용할 때는 쉽게 설명해주세요 | |
| - 부모의 불안을 완화하고 아이의 언어 발달을 지원하는 긍정적인 관점 유지 | |
| 답변은 간결하고 실용적이며, 보호자가 바로 실행할 수 있는 구체적인 제안을 포함해주세요. | |
| 한국어로 답변해주세요.""" | |
| # 컨텍스트 정보 구성 | |
| context_info = [] | |
| original_query = chat_state.get('original_query', "") | |
| if original_query: | |
| context_info.append(f"분석 중인 발화: {original_query}") | |
| collected_info = chat_state.get('collected_info', {}) | |
| if collected_info: | |
| info_str = ", ".join([f"{k}: {v}" for k, v in collected_info.items() if v]) | |
| if info_str: | |
| context_info.append(f"수집된 정보: {info_str}") | |
| # 대화 히스토리를 LLM 형식으로 변환 | |
| messages = [{"role": "system", "content": system_prompt}] | |
| # 컨텍스트 정보 추가 | |
| if context_info: | |
| context_text = "\n".join(context_info) | |
| messages.append({ | |
| "role": "system", | |
| "content": f"현재 상황:\n{context_text}" | |
| }) | |
| # 이전 대화 히스토리 추가 | |
| if len(history) > 1: # 현재 메시지 제외 | |
| for user_msg, bot_msg in history[:-1]: | |
| if user_msg: | |
| messages.append({"role": "user", "content": user_msg}) | |
| if bot_msg: | |
| messages.append({"role": "assistant", "content": bot_msg}) | |
| # 현재 사용자 메시지 추가 | |
| messages.append({"role": "user", "content": message}) | |
| # LLM 호출 | |
| response = client.chat.completions.create( | |
| model=MODEL, | |
| messages=messages, | |
| max_tokens=500, | |
| temperature=0.7 | |
| ) | |
| bot_response = response.choices[0].message.content.strip() | |
| except Exception as e: | |
| print(f"채팅 모드 LLM 호출 오류: {e}") | |
| import traceback | |
| traceback.print_exc() | |
| bot_response = "죄송합니다. 일시적인 오류가 발생했습니다. 다시 시도해주세요." | |
| # 봇 응답 추가 | |
| history[-1] = (message, bot_response) | |
| return history, "" | |
| chat_mode_btn.click( | |
| fn=open_chat_mode, | |
| inputs=[text_input, context_situation, situation_input, chat_mode_state], | |
| outputs=[chat_mode_popup, chat_mode_state, chat_mode_chatbot] | |
| ) | |
| chat_mode_close_btn.click( | |
| fn=close_chat_mode, | |
| inputs=[chat_mode_state], | |
| outputs=[chat_mode_popup, chat_mode_state, chat_mode_chatbot] | |
| ) | |
| chat_mode_send_btn.click( | |
| fn=handle_chat_message, | |
| inputs=[chat_mode_input, chat_mode_chatbot, chat_mode_state], | |
| outputs=[chat_mode_chatbot, chat_mode_input] | |
| ) | |
| chat_mode_input.submit( | |
| fn=handle_chat_message, | |
| inputs=[chat_mode_input, chat_mode_chatbot, chat_mode_state], | |
| outputs=[chat_mode_chatbot, chat_mode_input] | |
| ) | |
| # 프로필 생성 시 모든 탭의 드롭다운 업데이트 (echo_profile_dropdown 정의 후에 연결) | |
| # 음성 처리 탭 | |
| with gr.Tab("🎤 음성 분석"): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| voice_input = gr.Audio( | |
| label="음성 입력", | |
| type="filepath", | |
| format="wav" | |
| ) | |
| voice_process_btn = gr.Button("🎤 음성 분석하기", variant="primary", elem_classes=["soft-button"]) | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 📖 음성 분석 사용방법 | |
| 1. **음성 입력**에 녹음 파일 업로드 | |
| 2. **"🎤 음성 분석하기"** 버튼 클릭 | |
| 3. 음성 인식 → 반향어 감지 → 변환 → 음성 합성 | |
| **지원 형식**: WAV, MP3, M4A | |
| **최대 길이**: 30초 | |
| """) | |
| transcribed_output = gr.Textbox( | |
| label="음성 인식 결과", | |
| interactive=False, | |
| ) | |
| voice_is_echo_output = gr.Checkbox( | |
| label="반향어 감지", | |
| interactive=False, | |
| ) | |
| voice_audio_output = gr.Audio( | |
| label="음성 합성 결과", | |
| interactive=False | |
| ) | |
| # 음성 분석 저장 상태 표시 | |
| voice_save_status = gr.Markdown( | |
| value="", | |
| elem_classes=["save-status"] | |
| ) | |
| # 보호자를 위한 상세 설명 추가 | |
| gr.Markdown(""" | |
| ### 💡 보호자를 위한 안내 | |
| **🔍 신뢰도란?** | |
| - **높은 수준**: 반향어 가능성이 높음 (치료가 필요할 수 있음) | |
| - **보통 수준**: 부분적 반향어 (관찰 필요) | |
| - **낮은 수준**: 정상적인 의사소통 (치료 불필요) | |
| """) | |
| # 이벤트 연결 | |
| voice_process_btn.click( | |
| fn=process_voice_with_status, | |
| inputs=[voice_input], | |
| outputs=[transcribed_output, voice_is_echo_output, voice_audio_output, voice_save_status] | |
| ) | |
| # 음성 처리 탭 | |
| # 반향어 검사 탭 | |
| 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()], | |
| ) | |
| # 카테고리별 질문 수 설정 | |
| questions_per_category = gr.Radio( | |
| label="카테고리당 질문 수", | |
| choices=[("1개", 1), ("2개", 2), ("3개", 3), ("4개", 4), ("5개", 5)], | |
| value=3, | |
| ) | |
| # 새 검사 시작 버튼 | |
| start_assessment_btn = gr.Button( | |
| "🚀 새 검사 시작", | |
| variant="primary", | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 📖 사용방법 | |
| 1. **검사 카테고리** 선택 (언어 이해, 언어 표현 등) | |
| 2. **카테고리당 질문 수** 설정 (1-5개) | |
| 3. **"🚀 새 검사 시작"** 버튼 클릭 | |
| 4. 질문에 대해 아이의 응답을 텍스트 또는 음성으로 입력 | |
| 5. **"📝 응답 처리"** 버튼으로 각 질문 처리 | |
| **목적**: 표준화된 검사로 반향어 패턴 분석 | |
| """) | |
| # 검사 진행 상태 | |
| gr.Markdown("### 📊 검사 진행 상태") | |
| progress_info = gr.Textbox( | |
| label="진행 상황", | |
| value="검사를 시작해주세요", | |
| interactive=False, | |
| lines=3, | |
| ) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### ❓ 현재 질문") | |
| # 질문 정보 표시 | |
| question_text = gr.Textbox( | |
| label="질문", | |
| interactive=False, | |
| lines=3, | |
| ) | |
| question_context = gr.Textbox( | |
| label="상황/맥락", | |
| interactive=False, | |
| lines=2, | |
| ) | |
| question_meta = gr.Textbox( | |
| label="질문 정보", | |
| interactive=False, | |
| lines=1, | |
| ) | |
| # 응답 입력 | |
| gr.Markdown("### 💬 응답 입력") | |
| response_text = gr.Textbox( | |
| label="텍스트 응답", | |
| placeholder="아이의 응답을 텍스트로 입력하거나 음성으로 녹음하세요", | |
| lines=3, | |
| ) | |
| response_audio = gr.Audio( | |
| label="음성 응답", | |
| type="filepath", | |
| ) | |
| # 응답 처리 버튼 | |
| process_response_btn = gr.Button( | |
| "📝 응답 처리", | |
| variant="primary", | |
| ) | |
| # 반향어 감지 결과 | |
| gr.Markdown("### 🔍 분석 결과") | |
| echo_detected = gr.Checkbox( | |
| label="반향어 감지", | |
| interactive=False, | |
| ) | |
| confidence_level = gr.Textbox( | |
| label="신뢰도 수준", | |
| interactive=False, | |
| value="분석 대기 중...", | |
| ) | |
| converted_responses = gr.Textbox( | |
| label="변환된 응답", | |
| interactive=False, | |
| lines=4, | |
| ) | |
| # 검사 결과 요약 | |
| with gr.Row(): | |
| summary_btn = gr.Button( | |
| "📊 검사 결과 요약", | |
| variant="primary", | |
| elem_classes=["soft-button"] | |
| ) | |
| summary_output = gr.Markdown( | |
| value="검사를 완료한 후 결과 요약을 확인하세요.", | |
| ) | |
| # 치료 계획 생성 | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 👤 프로필 선택") | |
| # 프로필 선택 드롭다운 | |
| echo_profile_dropdown = gr.Dropdown( | |
| label="프로필 선택", | |
| choices=[], | |
| value=None, | |
| interactive=True, | |
| ) | |
| # 프로필 불러오기 버튼 | |
| load_echo_profile_btn = gr.Button( | |
| "📋 프로필 불러오기", | |
| variant="secondary", | |
| ) | |
| # 프로필 정보 표시 | |
| echo_profile_info = gr.Markdown( | |
| value="프로필을 선택하고 불러오기를 클릭하세요.", | |
| ) | |
| treatment_plan_btn = gr.Button( | |
| "🎯 치료 계획 생성", | |
| variant="primary", | |
| ) | |
| with gr.Column(scale=2): | |
| treatment_plan_output = gr.Markdown( | |
| value="검사 완료 후 프로필을 선택하고 치료 계획을 생성하세요.", | |
| ) | |
| # 세션 데이터 저장 (숨김) | |
| session_data = gr.State() | |
| # 이벤트 연결 | |
| 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, echo_detected, confidence_level, converted_responses] | |
| ).then( | |
| fn=update_progress_info, | |
| inputs=[session_data], | |
| outputs=[progress_info] | |
| ).then( | |
| fn=lambda session: "🎉 검사가 완료되었습니다! 아래 '검사 결과 요약' 버튼을 클릭하여 상세한 결과를 확인하세요." if session and (json.loads(session) if isinstance(session, str) else session).get('completed_questions', 0) >= (json.loads(session) if isinstance(session, str) else session).get('total_questions', 1) else None, | |
| inputs=[session_data], | |
| outputs=[gr.Textbox(visible=False)] | |
| ) | |
| summary_btn.click( | |
| fn=get_assessment_summary, | |
| inputs=[session_data], | |
| outputs=[summary_output] | |
| ) | |
| # 프로필 불러오기 이벤트 | |
| load_echo_profile_btn.click( | |
| fn=load_profile_for_echo_test, | |
| inputs=[echo_profile_dropdown], | |
| outputs=[echo_profile_info] | |
| ) | |
| # 프로필 생성 시 모든 탭의 드롭다운 업데이트 (feedback_profile_dropdown은 나중에 정의됨) | |
| create_profile_btn.click( | |
| fn=create_profile, | |
| inputs=[new_child_name, new_age, new_vocabulary, new_language_habits], | |
| outputs=[profile_dropdown, text_profile_dropdown, echo_profile_dropdown] | |
| ) | |
| def generate_and_format_treatment_plan(session_data, profile_id): | |
| """치료 계획 생성 및 포맷팅을 한 번에 처리""" | |
| # 프로필 정보 가져오기 | |
| profile = None | |
| if profile_id: | |
| profile = profile_manager.get_profile(profile_id) | |
| if profile: | |
| patient_name = profile['child_name'] | |
| patient_age = profile['age'] | |
| patient_gender = "남성" # 기본값, 프로필에 성별 정보가 없으므로 | |
| else: | |
| patient_name = "아이" | |
| patient_age = 5 | |
| 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, echo_profile_dropdown], | |
| 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, | |
| ) | |
| # 대본 목록 (첫 번째 카테고리로 초기화) | |
| initial_category = get_script_categories()[0]["key"] if get_script_categories() else None | |
| initial_scripts = get_scripts_by_category(initial_category) if initial_category else [] | |
| script_checkboxes = gr.CheckboxGroup( | |
| label="대본 선택", | |
| choices=initial_scripts, | |
| ) | |
| # 대본 시작 버튼 | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 📖 사용방법 | |
| 1. **대본 카테고리** 선택 (일상 루틴, 놀이 활동 등) | |
| 2. **원하는 대본들** 선택 (여러 개 선택 가능) | |
| 3. **"🎬 대본 시작"** 버튼 클릭 | |
| 4. 화면에 표시된 질문을 아이에게 읽어주기 | |
| 5. 아이의 응답을 **음성으로 녹음** | |
| 6. **"📝 녹음 처리"** 버튼으로 각 응답 처리 | |
| **목적**: 체계적인 대화 연습으로 반향어 개선 | |
| """) | |
| start_script_btn = gr.Button( | |
| "🎬 대본 시작", | |
| variant="primary", | |
| ) | |
| # 녹음 진행 상태 | |
| gr.Markdown("### 📊 녹음 진행 상태") | |
| script_progress_info = gr.Textbox( | |
| label="진행 상황", | |
| value="대본을 선택하고 녹음을 시작해주세요", | |
| interactive=False, | |
| lines=3, | |
| ) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### 📝 현재 대본") | |
| # 대본 정보 표시 | |
| script_title = gr.Textbox( | |
| label="대본 제목", | |
| interactive=False, | |
| lines=1, | |
| ) | |
| script_situation = gr.Textbox( | |
| label="상황", | |
| interactive=False, | |
| lines=1, | |
| ) | |
| script_question = gr.Textbox( | |
| label="읽어줄 질문", | |
| interactive=False, | |
| lines=3, | |
| ) | |
| script_meta = gr.Textbox( | |
| label="대본 정보", | |
| interactive=False, | |
| lines=1, | |
| ) | |
| # 녹음 섹션 | |
| gr.Markdown("### 🎤 아이 응답 녹음") | |
| script_audio_input = gr.Audio( | |
| label="아이의 응답 녹음", | |
| type="filepath", | |
| ) | |
| # 녹음 처리 버튼 | |
| process_script_btn = gr.Button( | |
| "📝 녹음 처리", | |
| variant="primary", | |
| ) | |
| # 반향어 감지 결과 | |
| gr.Markdown("### 🔍 분석 결과") | |
| script_echo_detected = gr.Checkbox( | |
| label="반향어 감지", | |
| interactive=False, | |
| ) | |
| script_confidence_level = gr.Textbox( | |
| label="신뢰도 수준", | |
| interactive=False, | |
| value="분석 대기 중...", | |
| ) | |
| script_converted_responses = gr.Textbox( | |
| label="변환된 응답", | |
| interactive=False, | |
| lines=4, | |
| ) | |
| script_transcribed_text = gr.Textbox( | |
| label="음성 인식 결과", | |
| interactive=False, | |
| lines=2, | |
| ) | |
| # 녹음 결과 요약 | |
| with gr.Row(): | |
| script_summary_btn = gr.Button( | |
| "📊 녹음 결과 요약", | |
| variant="primary", | |
| elem_classes=["soft-button"] | |
| ) | |
| script_summary_output = gr.Markdown( | |
| value="녹음을 완료한 후 결과 요약을 확인하세요.", | |
| ) | |
| # 세션 데이터 저장 (숨김) | |
| script_session_data = gr.State() | |
| # 이벤트 연결 | |
| 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(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"진행률: {session.get('completed_scripts', 0)}/{session.get('total_scripts', 0)} ({(session.get('completed_scripts', 0)/session.get('total_scripts', 1)*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("📊 사용자 피드백 통계", visible=False): | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| gr.Markdown(""" | |
| ### ⭐ 사용자 만족도 통계 | |
| 사용자들이 제공한 피드백을 바탕으로 시스템의 만족도와 개선 방향을 확인할 수 있습니다. | |
| """) | |
| # 전체 통계 | |
| overall_stats = gr.Markdown( | |
| value="통계를 불러오는 중...", | |
| elem_classes=["stats-container"] | |
| ) | |
| # 프로필별 통계 | |
| feedback_profile_dropdown = gr.Dropdown( | |
| label="프로필별 피드백 통계", | |
| choices=[], | |
| value=None, | |
| info="특정 프로필의 피드백 통계를 확인할 수 있습니다" | |
| ) | |
| profile_stats = gr.Markdown( | |
| value="프로필을 선택하면 해당 프로필의 피드백 통계가 표시됩니다.", | |
| elem_classes=["stats-container"] | |
| ) | |
| with gr.Column(scale=1): | |
| gr.Markdown(""" | |
| ### 📈 만족도 평가 가이드 | |
| **만족도 평가 기준:** | |
| - ⭐⭐⭐⭐⭐ **매우 적절**: 분석이 매우 정확하고 도움이 됨 | |
| - ⭐⭐⭐⭐ **적절**: 분석이 대체로 정확하고 도움이 됨 | |
| - ⭐⭐⭐ **보통**: 분석이 어느 정도 도움이 됨 | |
| - ⭐⭐ **부적절**: 분석이 부정확하거나 도움이 안 됨 | |
| - ⭐ **매우 부적절**: 분석이 완전히 부정확함 | |
| **피드백 작성 팁:** | |
| - 구체적인 상황 설명 | |
| - 개선이 필요한 부분 명시 | |
| - 실제 관찰된 아이의 반응 포함 | |
| **통계 해석:** | |
| - 만족도가 높을수록 시스템 신뢰성 증가 | |
| - 피드백이 많은 분석일수록 검증된 결과 | |
| - 낮은 만족도 영역은 시스템 업데이트 우선순위 | |
| """) | |
| # 통계 새로고침 버튼 | |
| refresh_stats_btn = gr.Button("🔄 통계 새로고침", variant="secondary") | |
| # 이벤트 연결 | |
| refresh_stats_btn.click( | |
| fn=lambda: get_user_feedback_statistics_display(), | |
| inputs=[], | |
| outputs=[overall_stats] | |
| ) | |
| feedback_profile_dropdown.change( | |
| fn=lambda profile_id: get_profile_user_feedback_stats(profile_id), | |
| inputs=[feedback_profile_dropdown], | |
| outputs=[profile_stats] | |
| ) | |
| # 프로필 생성 시 피드백 통계 탭의 드롭다운도 업데이트 | |
| create_profile_btn.click( | |
| fn=lambda name, age, vocab, habits: get_profile_choices()[0], # 첫 번째 드롭다운만 반환 | |
| inputs=[new_child_name, new_age, new_vocabulary, new_language_habits], | |
| outputs=[feedback_profile_dropdown] | |
| ) | |
| # 사용 설명서 탭 | |
| with gr.Tab("📖 사용 설명서"): | |
| with gr.Accordion("🎯 서비스 개요", open=True, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 🗣️ COCONUT AI - 반향어 치료 보조 어시스턴트 | |
| **AI 기반 반향어 감지 및 기능적 의사소통 전환 도구** | |
| 이 서비스는 자폐 스펙트럼 장애나 언어 발달 지연을 가진 아동의 반향어를 감지하고, | |
| 기능적 의사소통으로 전환하는 치료 보조 도구입니다. | |
| **주요 대상**: 언어치료사, 특수교육 교사, 부모, 보호자 | |
| **개발 목적**: 아동의 의사소통 능력 향상 및 반향어 치료 지원 | |
| """) | |
| with gr.Accordion("⚡ 주요 기능", open=True, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 🔍 반향어 감지 | |
| - 정확한 반향어 감지 및 분석 | |
| - 신뢰도 점수 제공 (0-100%) | |
| ### 📝 3단계 분석 보고서 | |
| - **아이의 언어 성향 분석**: 개인별 특성 파악 | |
| - **대화 내용 분석**: 상황과 맥락 분석 | |
| - **보완 방향 제시**: 실용적 가이드 제공 | |
| ### 👤 개인화된 분석 | |
| - 사용자 프로필 기반 맞춤형 분석 | |
| - 연령대별 발달 단계 고려 | |
| - CARS 검사 결과 통합 | |
| """) | |
| with gr.Accordion("🚀 사용 방법", open=True, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 1단계: 프로필 설정 (개인화된 분석을 위해) | |
| 1. **"👤 사용자 프로필"** 탭 선택 | |
| 2. **새 프로필 생성**에서 아동 정보 입력 | |
| 3. **CARS 검사** 진행 (15문항 완전판) | |
| 4. 개인화된 분석을 위한 기초 정보 설정 | |
| ### 2단계: 텍스트 분석하기 (메인 기능) | |
| 1. **"📝 텍스트 분석"** 탭 선택 | |
| 2. **입력 텍스트**에 반향어가 의심되는 문장 입력 | |
| 3. **상황 설명**에 구체적인 상황 정보 입력 (선택사항) | |
| 4. **이전 발화**에 아이에게 했던 질문이나 대화 입력 (선택사항) | |
| 5. **상황** 드롭다운에서 적절한 상황 선택 | |
| 6. **👤 분석할 아이 프로필** 선택 (1단계에서 생성한 프로필) | |
| 7. **"🔍 분석하기"** 버튼 클릭 | |
| ### 3단계: 분석 보고서 확인 | |
| #### 🌟 **아이의 말에 담긴 마음 - 3단계 분석 보고서** | |
| #### 1️⃣ 아이의 언어 성향 분석 (Personal Profile) | |
| - **👶 기본 정보**: 나이, 언어 수준, 반향어 경향 | |
| - **🧠 언어 지도**: 반향어 유형 및 주요 기능 분석 | |
| - **💡 언어 특성 이해**: 사회적 상호작용, 언어적 유연성, 감정 표현 | |
| #### 2️⃣ 입력 발화 및 대화 분석 (Context & Function) | |
| - **🗣️ 발화 내용**: 입력한 텍스트 내용 | |
| - **📍 맥락 분석**: 상황과의 적합성 평가 | |
| - **🔍 기능 분석**: 의미 전달 vs 단순 모방 구분 | |
| - **💭 구체적 해석**: 아이의 말에 담긴 마음 이해 | |
| #### 3️⃣ 보완 방향 (Alternative & Strategy) | |
| - **🧡 보호자 반응 방식**: 해야 할 것 vs 하지 말아야 할 것 | |
| - **💬 대안 발화 모델링**: 상황별 적절한 표현 제안 | |
| - **🎯 추천 상담 기술**: 스캐폴딩, 모델링, 확장, DIR Floortime | |
| ### 4단계: 추가 기능 활용 | |
| #### 🎤 **음성 분석** (선택사항) | |
| 1. **"🎤 음성 분석"** 탭 선택 | |
| 2. **음성 입력**에 녹음 파일 업로드 | |
| 3. **"🎤 음성 분석하기"** 버튼 클릭 | |
| 4. 음성 인식 → 반향어 감지 → 변환 → 음성 합성 자동 진행 | |
| #### 🎬 **대본 기반 녹음 연습** (실습 도구) | |
| 1. **"🎬 대본 기반 녹음"** 탭 선택 | |
| 2. **대본 카테고리** 선택 (일상 루틴, 놀이 활동 등) | |
| 3. **대본 선택** 및 녹음 연습 | |
| 4. 체계적인 발화 연습 및 피드백 | |
| #### 🔬 **전문 도구** (고급 사용자) | |
| 1. **"🔬 반향어 검사"** 탭에서 추가 검사 | |
| 2. **"📖 사용 설명서"** 탭에서 상세 가이드 참조 | |
| ### 📊 상세 분석 정보 확인 | |
| - 📊 **실시간 품질 피드백**: 즉시 도움이 되는 피드백 | |
| - 🔬 **다차원 분석 결과**: 실제 알고리즘 기반 상세 분석 차트 | |
| - 📈 **발달 분석**: 연령대별 적절한 발화 수준 분석 | |
| - 📊 **진행 상황 추적**: 장기적 발전 추적 및 통계 | |
| """) | |
| with gr.Accordion("💡 실제 사용 예시", open=False, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 예시 1: 완전 반향어 - 3단 구조 분석 | |
| ``` | |
| 입력: "아이스크림, 아이스크림, 아이스크림" | |
| 상황 설명: "밥을 먹다가" | |
| 이전 발화: "밥 먹었어?" | |
| 상황: 식사 시간 | |
| 🌟 아이의 말에 담긴 마음 - 3단계 분석 보고서 | |
| ### 1️⃣ 아이의 언어 성향 분석 (Personal Profile) | |
| 👶 기본 정보 | |
| - 나이: 5세 | |
| - 언어 수준: 기본 | |
| - 반향어 경향: 높은 경향 (지연반향어) | |
| 🧠 언어 지도 (Language Map) | |
| 아이는 5세의 발달 단계에서 지연반향어를 주로 사용하며, | |
| 반복 언어를 통해 자기조절 및 감정 표현을 하고 있습니다. | |
| ### 2️⃣ 입력 발화 및 대화 분석 (Context & Function) | |
| 🗣️ 발화 내용: "아이스크림, 아이스크림, 아이스크림" | |
| 📍 맥락 분석 | |
| - 상황: 식사 시간 | |
| - 상황 설명: 밥을 먹다가 | |
| - 이전 발화: 밥 먹었어? | |
| 상황과 무관한 발화일 가능성이 높습니다. | |
| 아이가 이전에 들은 말을 현재 상황과 연결하지 못하고 있습니다. | |
| 🔍 기능 분석 | |
| - 발화 유형: 단순 모방 | |
| - 기능적 의미: 아이가 이전에 들은 말을 그대로 반복하고 있습니다. | |
| 💭 구체적 해석 | |
| '아이스크림' 반복은 실제 아이스크림을 원하는 것이 아니라 | |
| 불안 완화나 주의 집중을 위한 신호일 수 있습니다. | |
| ### 3️⃣ 보완 방향 (Alternative & Strategy) | |
| 🧡 보호자 반응 방식 | |
| ✅ 해야 할 것 | |
| - 아이의 말을 중단하지 말고 들어주세요 | |
| - 반복하는 말에 감정 이름을 붙여주세요 ("지금 불안해?", "무서워?") | |
| - 안정감을 주는 환경을 마련해주세요 | |
| ❌ 하지 말아야 할 것 | |
| - "똑같은 말 하지 마"라고 말하지 마세요 | |
| - 아이의 말을 무시하거나 교정하려 하지 마세요 | |
| 💬 아이를 위한 대안 발화 모델링 | |
| - '무서워요' (불안할 때) | |
| - '도움이 필요해요' (도움이 필요할 때) | |
| - '조금 쉬고 싶어요' (휴식이 필요할 때) | |
| 🎯 추천 상담 기술 | |
| - 기법: 스캐폴딩 (Scaffolding) | |
| - 설명: 아이가 반복하는 말에 의미를 조금씩 확장해서 응답하는 방법 | |
| ``` | |
| ### 예시 2: 정상 의사소통 - 3단 구조 분석 | |
| ``` | |
| 입력: "물 마시고 싶어요" | |
| 상황 설명: "놀이 시간" | |
| 이전 발화: "뭐 하고 싶어?" | |
| 상황: 놀이 시간 | |
| 🌟 아이의 말에 담긴 마음 - 3단계 분석 보고서 | |
| ### 1️⃣ 아이의 언어 성향 분석 (Personal Profile) | |
| 👶 기본 정보 | |
| - 나이: 5세 | |
| - 언어 수준: 기본 | |
| - 반향어 경향: 낮은 경향 (정상 의사소통) | |
| 🧠 언어 지도 (Language Map) | |
| 아이는 5세의 발달 단계에서 정상 의사소통를 주로 사용하며, | |
| 반복 언어를 통해 시간 벌기 및 안정감 추구를 하고 있습니다. | |
| ### 2️⃣ 입력 발화 및 대화 분석 (Context & Function) | |
| 🗣️ 발화 내용: "물 마시고 싶어요" | |
| 📍 맥락 분석 | |
| - 상황: 놀이 시간 | |
| - 상황 설명: 놀이 시간 | |
| - 이전 발화: 뭐 하고 싶어? | |
| 상황과 잘 맞는 발화입니다. | |
| 아이가 현재 상황을 이해하고 적절하게 반응하고 있습니다. | |
| 🔍 기능 분석 | |
| - 발화 유형: 의미 전달 | |
| - 기능적 의미: 아이가 명확한 의미를 전달하려고 시도하고 있습니다. | |
| 💭 구체적 해석 | |
| 정상적인 의사소통 패턴으로 보이며, 아이가 상황에 적절하게 반응하고 있습니다. | |
| ### 3️⃣ 보완 방향 (Alternative & Strategy) | |
| 🧡 보호자 반응 방식 | |
| ✅ 계속해서 해야 할 것 | |
| - 아이의 의사소통 시도에 즉시 반응해주세요 | |
| - 긍정적인 피드백을 많이 주세요 ("잘 말했어!", "이해했어!") | |
| - 대화를 확장해서 더 많은 소통 기회를 만들어주세요 | |
| 💬 아이를 위한 대안 발화 모델링 | |
| - 아이의 현재 표현이 적절합니다 | |
| - 계속해서 이런 식으로 의사소통하도록 격려해주세요 | |
| - 더 복잡한 표현으로 확장해볼 수 있습니다 | |
| 🎯 추천 상담 기술 | |
| - 기법: 확장 (Expansion) | |
| - 설명: 아이의 말을 자연스럽게 확장해서 더 완전한 문장으로 만들어주는 방법 | |
| ``` | |
| ### 📊 새로운 3단 구조 특징 | |
| - **공감적 접근**: 아이의 말에 담긴 마음을 이해하려는 따뜻한 시각 | |
| - **실용적 가이드**: 보호자가 바로 이해하고 실천할 수 있는 구체적 방향 | |
| - **개인화된 분석**: 아이의 언어 성향과 발달 단계를 고려한 맞춤형 분석 | |
| - **즉시 활용 가능**: 상담 기술과 대안 발화를 통한 실질적 도움 | |
| """) | |
| with gr.Accordion("👩⚕️ 치료사 가이드", open=False, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 🎯 3단 구조 분석 활용법 | |
| #### 1️⃣ 아이의 언어 성향 분석 활용 | |
| - **언어 지도 (Language Map)** 파악을 통한 맞춤형 접근 | |
| - **반향어 유형** (즉시반향어 vs 지연반향어) 확인 | |
| - **5가지 기능** 분석을 통한 의사소통 의도 파악 | |
| #### 2️⃣ 대화 분석을 통한 개입점 찾기 | |
| - **맥락 분석**으로 상황 이해도 평가 | |
| - **기능 분석**으로 실제 의사소통 시도 확인 | |
| - **구체적 해석**을 통한 아이의 마음 읽기 | |
| #### 3️⃣ 보완 방향 적용 | |
| - **보호자 교육**: 올바른 반응 방식 안내 | |
| - **대안 발화 모델링**: 단계적 언어 발달 지원 | |
| - **상담 기술 적용**: 상황별 맞춤 기법 활용 | |
| ### 📚 상담 기술 상세 안내 | |
| #### 🔨 **스캐폴딩 (Scaffolding)** | |
| - **정의**: 아이의 현재 수준에서 조금씩 도움을 주어 발전시키는 방법 | |
| - **적용**: 반복하는 말에 의미를 조금씩 확장해서 응답 | |
| - **예시**: "아이스크림" → "아이스크림 먹고 싶어?" → "지금 뭐가 먹고 싶어?" | |
| #### 🎭 **모델링 (Modeling)** | |
| - **정의**: 적절한 언어 표현을 직접 보여주고 따라하게 하는 방법 | |
| - **적용**: 아이가 사용할 수 있는 적절한 표현을 직접 시연 | |
| - **예시**: "이렇게 말해볼까? '물 주세요'" | |
| #### 📈 **확장 (Expansion)** | |
| - **정의**: 아이의 단어나 짧은 문장을 더 완전한 문장으로 확장 | |
| - **적용**: 아이의 말을 자연스럽게 확장해서 더 완전한 문장으로 만들기 | |
| - **예시**: "물" → "물 마시고 싶어요" | |
| #### 🎪 **DIR Floortime** | |
| - **정의**: 아이의 관심사에 따라 함께 놀이하며 상호작용하는 방법 | |
| - **적용**: 아이가 관심 있는 놀이를 따라가며 자연스러운 대화 유도 | |
| - **예시**: 아이가 자동차를 좋아한다면 자동차 놀이를 통한 대화 | |
| ### 📊 세션 진행 가이드 | |
| #### 세션 준비 | |
| - [ ] **3단 구조 분석** 사전 실행 | |
| - [ ] **아이의 언어 성향** 파악 | |
| - [ ] **맞춤형 상담 기술** 선택 | |
| #### 세션 진행 | |
| - [ ] **실시간 분석** 활용 | |
| - [ ] **공감적 접근** 적용 | |
| - [ ] **즉시 피드백** 제공 | |
| #### 세션 마무리 | |
| - [ ] **3단 구조 보고서** 검토 | |
| - [ ] **보호자 교육** 진행 | |
| - [ ] **다음 단계** 계획 수립 | |
| """) | |
| with gr.Accordion("⚠️ 주의사항", open=False, elem_classes=["soft-accordion"]): | |
| gr.Markdown(""" | |
| ### 감지 한계 | |
| - 단순 반복과 반향어 구분 필요 | |
| - 상황 정보 정확 입력이 분석 정확도에 중요 | |
| - 개인차 및 발달 단계 고려 필요 | |
| - 문화적, 언어적 맥락 고려 필요 | |
| ### 사용 시 주의사항 | |
| - AI 도구에만 의존하지 말고 전문가 판단 병행 | |
| - 아동 및 가족 정보 보호 필수 (개인정보보호법 준수) | |
| - 정기적 치료 효과 평가 및 계획 수정 | |
| - 아동의 편안함과 안전을 최우선으로 고려 | |
| ### 문제 해결 가이드 | |
| | 문제 | 해결 방법 | | |
| |------|-----------| | |
| | 반향어 감지 안됨 | 상황 설명과 이전 발화 상세 입력 | | |
| | 신뢰도 수준 낮음 | 상황 정보 정확 선택 및 맥락 제공 | | |
| | 변환 결과 부적절 | 아동 수준에 맞는 단계 선택 (L1부터) | | |
| | 음성 인식 안됨 | 명확한 발음으로 녹음, 지원 형식 확인 | | |
| | 개인화 분석 부족 | 사용자 프로필 설정 및 CARS 검사 완료 | | |
| | 앱 접속 안됨 | 브라우저 새로고침, 네트워크 연결 확인 | | |
| ### 연락처 및 지원 | |
| - **기술 지원**: 앱 내 사용 설명서 참조 | |
| - **전문 상담**: 언어치료사, 소아정신과 전문의 | |
| - **가족 지원**: 지역 자폐인 가족 지원센터 | |
| """) | |
| # 앱 시작 시 프로필 드롭다운 초기화 | |
| demo.load( | |
| fn=get_profile_choices, | |
| inputs=[], | |
| outputs=[profile_dropdown, text_profile_dropdown, echo_profile_dropdown] | |
| ) | |
| return demo | |
| def generate_three_stage_report(text: str, context_situation: str, context_previous: str, | |
| situation: str, is_echo: bool, confidence: float, | |
| profile: dict, latest_cars: dict, advanced_confidence: dict, | |
| quality_feedback: str, visual_analysis: str, | |
| context_analysis: str, developmental_analysis: str, | |
| personalized_recommendations: str, progress_tracking: str, | |
| input_validation: str, anchor: str, candidates: list, | |
| analysis_id: int = None, rag_results: list = None, | |
| rag_queries_used: list = None, rag_used_in_sections: list = None) -> str: | |
| """3단 구조 분석 보고서 생성: 아이성향분석-대화내용분석-보완방향제시""" | |
| # 1단계: 아이의 언어 성향 분석 (Personal Profile) | |
| personal_profile = generate_personal_profile_analysis(profile, is_echo, confidence, latest_cars, advanced_confidence) | |
| # 2단계: 입력 발화 및 대화 분석 (Context & Function) | |
| context_function_analysis = generate_context_function_analysis( | |
| text, context_situation, context_previous, situation, is_echo, confidence, advanced_confidence | |
| ) | |
| # 3단계: 보완 방향 (Alternative & Strategy) | |
| alternative_strategy = generate_alternative_strategy( | |
| text, is_echo, confidence, profile, advanced_confidence, personalized_recommendations | |
| ) | |
| # 피드백 섹션 추가 | |
| feedback_section = "" | |
| if analysis_id: | |
| feedback_summary = get_user_feedback_summary(analysis_id) | |
| feedback_section = f""" | |
| --- | |
| ### 💬 사용자 피드백 | |
| {feedback_summary} | |
| --- | |
| ### ⭐ 만족도 평가 | |
| 이 분석 결과가 얼마나 도움이 되었는지 평가해주세요. 여러분의 피드백은 AI 모델 개선에 중요한 역할을 합니다. | |
| """ | |
| # RAG 사용 여부 임시 섹션 (테스트용) - 보고서에서 숨김 처리 | |
| # 터미널 출력은 유지하되, 보고서에는 표시하지 않음 | |
| rag_section = "" | |
| return f"""## 🌟 아이의 말에 담긴 마음 - 3단계 분석 보고서 | |
| **📌 참고사항**: 이 보고서는 coconut AI가 의학 자문을 통해 검증한 고유 데이터베이스를 기반으로 생성되었습니다. | |
| {personal_profile} | |
| {context_function_analysis} | |
| {alternative_strategy} | |
| --- | |
| ### 📊 상세 분석 정보 | |
| {quality_feedback} | |
| {visual_analysis} | |
| ### 🔬 기술적 세부사항 | |
| - **앵커 텍스트**: "{anchor}" (감지됨: {'예' if is_echo else '아니오'}) | |
| - **변환 후보**: {', '.join(candidates[:3]) if candidates else '없음'} | |
| {feedback_section} | |
| {rag_section} | |
| --- | |
| *이 보고서는 공감적 접근을 바탕으로 한 AI 분석 시스템을 통해 생성되었습니다. 정확한 진단을 위해서는 전문가와의 상담을 권장합니다.*""" | |
| def generate_personal_profile_analysis(profile: dict, is_echo: bool, confidence: float, latest_cars: dict, advanced_confidence: dict) -> str: | |
| """1단계: 아이의 언어 성향 분석 (Personal Profile) - CARS 점수 기반 임계값 사용""" | |
| # confidence 값 정규화 (0-100 범위를 0-1로 변환) | |
| if confidence > 1.0: | |
| confidence_normalized = confidence / 100.0 | |
| else: | |
| confidence_normalized = confidence | |
| if not profile: | |
| return """ | |
| ### 1️⃣ 아이의 언어 성향 분석 (Personal Profile) | |
| 프로필 정보가 없어 개인화된 분석을 제공할 수 없습니다. | |
| 프로필을 생성하시면 더 정확한 언어 성향 분석을 받을 수 있습니다. | |
| """ | |
| age = profile.get('age', 0) | |
| vocabulary_level = profile.get('vocabulary_level', '기본') | |
| # CARS 점수에 따른 임계값 결정 (generate_quick_summary와 동일한 로직) | |
| if latest_cars: | |
| cars_score = latest_cars['total_score'] | |
| if cars_score >= 37: # 중증 자폐 | |
| threshold_high = 0.8 # 80% | |
| threshold_medium = 0.6 # 60% | |
| elif cars_score >= 30: # 경증-중간 자폐 | |
| threshold_high = 0.7 # 70% | |
| threshold_medium = 0.5 # 50% | |
| else: # 정상 또는 미검사 | |
| threshold_high = 0.6 # 60% | |
| threshold_medium = 0.4 # 40% | |
| else: | |
| # CARS 점수 없을 때는 기본값 (빠른 요약과 일치) | |
| threshold_high = 0.7 # 70% | |
| threshold_medium = 0.4 # 40% | |
| # 반향어 유형 분석 (정규화된 값과 CARS 기반 임계값 사용) | |
| if confidence_normalized >= threshold_high: | |
| echo_type = "즉시반향어" if advanced_confidence['detailed_scores']['linguistic_patterns'] > 0.7 else "지연반향어" | |
| echo_tendency = "높은 경향" | |
| elif confidence_normalized >= threshold_medium: | |
| echo_tendency = "보통 경향" | |
| echo_type = "혼합형" | |
| else: | |
| echo_tendency = "낮은 경향" | |
| echo_type = "정상 의사소통" | |
| # 반복 언어의 기능 분석 | |
| repetition_score = advanced_confidence['detailed_scores']['repetition_analysis'] | |
| semantic_score = advanced_confidence['detailed_scores']['semantic_coherence'] | |
| if repetition_score > 0.7: | |
| primary_function = "**자기조절 및 감정 표현**" | |
| function_desc = "아이가 반복을 통해 내적 상태를 조절하고 감정을 표현하고 있습니다." | |
| elif semantic_score < 0.3: | |
| primary_function = "**언어 학습 및 이해 신호**" | |
| function_desc = "아이가 새로운 언어를 학습하고 이해도를 확인하는 과정에 있습니다." | |
| else: | |
| primary_function = "**시간 벌기 및 안정감 추구**" | |
| function_desc = "아이가 상황을 파악하고 적절한 응답을 준비하는 시간을 벌고 있습니다." | |
| return f""" | |
| ### 1️⃣ 아이의 언어 성향 분석 (Personal Profile) | |
| **👶 기본 정보** | |
| - **나이**: {age}세 | |
| - **언어 수준**: {vocabulary_level} | |
| - **반향어 경향**: {echo_tendency} ({echo_type}) | |
| **🧠 언어 지도 ** | |
| 아이는 {age}세의 발달 단계에서 **{echo_type}**를 주로 사용하며, | |
| 반복 언어를 통해 **{primary_function}**을 하고 있습니다. | |
| {function_desc} | |
| **💡 언어 특성 이해** | |
| - **사회적 상호작용**: {echo_type}를 통한 의사소통 시도 | |
| - **언어적 유연성**: 반복을 통한 안정감 추구 | |
| - **감정 표현**: 반복 언어를 통한 내적 상태 전달 | |
| """ | |
| def generate_context_function_analysis(text: str, context_situation: str, context_previous: str, | |
| situation: str, is_echo: bool, confidence: float, | |
| advanced_confidence: dict) -> str: | |
| """2단계: 입력 발화 및 대화 분석 (Context & Function)""" | |
| # 발화의 맥락 분석 | |
| context_score = advanced_confidence['detailed_scores']['contextual_fit'] | |
| semantic_score = advanced_confidence['detailed_scores']['semantic_coherence'] | |
| if context_score > 0.6: | |
| context_interpretation = "**상황과 잘 맞는 발화**입니다." | |
| context_desc = "아이가 현재 상황을 이해하고 적절하게 반응하고 있습니다." | |
| elif context_score > 0.3: | |
| context_interpretation = "**부분적으로 상황과 연관된 발화**입니다." | |
| context_desc = "아이가 상황의 일부를 이해하고 있지만 완전한 연결은 어려워 보입니다." | |
| else: | |
| context_interpretation = "**상황과 무관한 발화**일 가능성이 높습니다." | |
| context_desc = "아이가 이전에 들은 말을 현재 상황과 연결하지 못하고 있습니다." | |
| # 발화의 기능 분석 | |
| if semantic_score > 0.7: | |
| function_type = "**의미 전달**" | |
| function_desc = "아이가 명확한 의미를 전달하려고 시도하고 있습니다." | |
| elif semantic_score > 0.4: | |
| function_type = "**부분적 의미 전달**" | |
| function_desc = "아이가 의미를 전달하려 하지만 완전하지는 않습니다." | |
| else: | |
| function_type = "**단순 모방**" | |
| function_desc = "아이가 이전에 들은 말을 그대로 반복하고 있습니다." | |
| # 구체적 해석 | |
| if is_echo: | |
| if "아이스크림" in text: | |
| specific_interpretation = "**'아이스크림' 반복**은 실제 아이스크림을 원하는 것이 아니라 **불안 완화나 주의 집중**을 위한 신호일 수 있습니다." | |
| elif "목 말라" in text: | |
| specific_interpretation = "**'목 말라' 반복**은 실제 갈증보다는 **상황 변화에 대한 불안감**이나 **안정감 추구**를 나타낼 수 있습니다." | |
| else: | |
| specific_interpretation = f"**'{text}' 반복**은 단순 모방이 아닌 **감정 표현이나 자기조절**의 수단일 가능성이 높습니다." | |
| else: | |
| specific_interpretation = "정상적인 의사소통 패턴으로 보이며, 아이가 상황에 적절하게 반응하고 있습니다." | |
| return f""" | |
| ### 2️⃣ 입력 발화 및 대화 분석 (Context & Function) | |
| **🗣️ 발화 내용**: "{text}" | |
| **📍 맥락 분석** | |
| - **상황**: {situation} | |
| - **상황 설명**: {context_situation if context_situation else '제공되지 않음'} | |
| - **이전 발화**: {context_previous if context_previous else '없음'} | |
| {context_interpretation} | |
| {context_desc} | |
| **🔍 기능 분석** | |
| - **발화 유형**: {function_type} | |
| - **기능적 의미**: {function_desc} | |
| **💭 구체적 해석** | |
| {specific_interpretation} | |
| """ | |
| def generate_alternative_strategy(text: str, is_echo: bool, confidence: float, | |
| profile: dict, advanced_confidence: dict, | |
| personalized_recommendations: str) -> str: | |
| """3단계: 보완 방향""" | |
| # 상담 기술 추천 | |
| repetition_score = advanced_confidence['detailed_scores']['repetition_analysis'] | |
| semantic_score = advanced_confidence['detailed_scores']['semantic_coherence'] | |
| if repetition_score > 0.7 and semantic_score < 0.3: | |
| recommended_technique = "**스캐폴딩 (Scaffolding)**" | |
| technique_desc = "아이가 반복하는 말에 의미를 조금씩 확장해서 응답하는 방법" | |
| elif repetition_score > 0.5: | |
| recommended_technique = "**모델링 (Modeling)**" | |
| technique_desc = "아이가 사용할 수 있는 적절한 표현을 직접 보여주는 방법" | |
| else: | |
| recommended_technique = "**확장 (Expansion)**" | |
| technique_desc = "아이의 말을 자연스럽게 확장해서 더 완전한 문장으로 만들어주는 방법" | |
| # 보호자용 반응 방식 | |
| if is_echo: | |
| caregiver_response = """ | |
| **🧡 보호자 반응 방식** | |
| ✅ **해야 할 것** | |
| - 아이의 말을 **중단하지 말고** 들어주세요 | |
| - 반복하는 말에 **감정 이름을 붙여주세요** ("지금 불안해?", "무서워?") | |
| - **안정감을 주는 환경**을 마련해주세요 (조용한 공간, 편안한 자세) | |
| - 아이의 **의도나 감정을 추측해서** 확인해주세요 | |
| ❌ **하지 말아야 할 것** | |
| - "똑같은 말 하지 마"라고 말하지 마세요 | |
| - 아이의 말을 무시하거나 교정하려 하지 마세요 | |
| - 서두르거나 답답해하는 표정을 보이지 마세요 | |
| """ | |
| else: | |
| caregiver_response = """ | |
| **🧡 보호자 반응 방식** | |
| ✅ **계속해서 해야 할 것** | |
| - 아이의 의사소통 시도에 **즉시 반응**해주세요 | |
| - **긍정적인 피드백**을 많이 주세요 ("잘 말했어!", "이해했어!") | |
| - **대화를 확장**해서 더 많은 소통 기회를 만들어주세요 | |
| - **아이의 관심사**를 파악해서 대화 주제로 활용하세요 | |
| """ | |
| # 아이용 대안 발화 모델링 | |
| if is_echo: | |
| if "아이스크림" in text: | |
| alternative_phrases = [ | |
| "**아이에게 제안할 수 있는 표현**", | |
| "- '무서워요' (불안할 때)", | |
| "- '도움이 필요해요' (도움이 필요할 때)", | |
| "- '조금 쉬고 싶어요' (휴식이 필요할 때)" | |
| ] | |
| elif "목 말라" in text: | |
| alternative_phrases = [ | |
| "**아이에게 제안할 수 있는 표현**", | |
| "- '물 주세요' (물을 원할 때)", | |
| "- '불안해요' (불안할 때)", | |
| "- '힘들어요' (어려울 때)" | |
| ] | |
| else: | |
| alternative_phrases = [ | |
| "**아이에게 제안할 수 있는 표현**", | |
| "- '도움이 필요해요' (도움이 필요할 때)", | |
| "- '무서워요' (두려울 때)", | |
| "- '이해했어요' (이해했을 때)" | |
| ] | |
| else: | |
| alternative_phrases = [ | |
| "**아이의 현재 표현이 적절합니다**", | |
| "- 계속해서 이런 식으로 의사소통하도록 격려해주세요", | |
| "- 더 복잡한 표현으로 확장해볼 수 있습니다" | |
| ] | |
| return f""" | |
| ### 3️⃣ 보완 방향 (Alternative & Strategy) | |
| {caregiver_response} | |
| **💬 아이를 위한 대안 발화 모델링** | |
| {chr(10).join(alternative_phrases)} | |
| **🎯 추천 상담 기술** | |
| - **기법**: {recommended_technique} | |
| - **설명**: {technique_desc} | |
| - **적용 방법**: 아이가 반복하는 말에 의미를 조금씩 확장해서 응답하고, 점진적으로 더 적절한 표현으로 이끌어가세요 | |
| **📚 추가 상담 기술 안내** | |
| - **DIR Floortime**: 아이의 관심사에 따라 함께 놀이하며 상호작용하는 방법 | |
| - **확장 (Expansion)**: 아이의 단어나 짧은 문장을 더 완전한 문장으로 확장 | |
| - **모델링 (Modeling)**: 적절한 언어 표현을 직접 보여주고 따라하게 하는 방법 | |
| - **스캐폴딩 (Scaffolding)**: 아이의 현재 수준에서 조금씩 도움을 주어 발전시키는 방법 | |
| """ | |
| def get_satisfaction_emoji(avg_rating): | |
| """평균 만족도에 따른 이모지 반환""" | |
| if avg_rating >= 4.0: | |
| return "🟢" | |
| elif avg_rating >= 3.0: | |
| return "🟡" | |
| else: | |
| return "🔴" | |
| def get_satisfaction_level(avg_rating): | |
| """평균 만족도에 따른 수준 반환""" | |
| if avg_rating >= 4.0: | |
| return "높은 만족도" | |
| elif avg_rating >= 3.0: | |
| return "보통 만족도" | |
| else: | |
| return "낮은 만족도" | |
| def get_ppo_data_quality(stats): | |
| """PPO 학습 데이터 품질 평가""" | |
| total = stats['total_feedbacks'] | |
| satisfaction_rate = stats['satisfaction_rate'] | |
| if satisfaction_rate >= 80: | |
| return "✅ 높은 품질의 학습 데이터입니다. PPO 학습에 적합합니다." | |
| elif satisfaction_rate >= 60: | |
| return "📊 보통 품질의 학습 데이터입니다. 추가 데이터 수집을 권장합니다." | |
| else: | |
| return "⚠️ 낮은 품질의 학습 데이터입니다. 시스템 개선이 필요합니다." | |
| if __name__ == "__main__": | |
| # Hugging Face Space 환경 감지 | |
| is_space = bool(os.getenv("SPACE_ID") or os.getenv("SYSTEM") == "spaces") | |
| # 환경 변수 확인 | |
| if not os.getenv("OPENAI_API_KEY"): | |
| print("❌ OPENAI_API_KEY가 설정되지 않았습니다.") | |
| if is_space: | |
| print("⚠️ Hugging Face Space의 Settings > Secrets에 OPENAI_API_KEY를 설정해주세요.") | |
| else: | |
| print("환경 변수를 설정하거나 .env 파일을 확인하세요.") | |
| exit(1) | |
| # RAG 초기화 | |
| print("=" * 60) | |
| print("RAG 초기화 중...") | |
| print("=" * 60) | |
| rag_init_success = init_rag() | |
| if rag_init_success: | |
| print("✅ RAG가 활성화되었습니다. 분석 시 RAG 검색이 사용됩니다.") | |
| else: | |
| print("⚠️ RAG가 비활성화되었습니다. 기본 분석만 수행됩니다.") | |
| print("=" * 60) | |
| print() | |
| # Gradio 앱 실행 | |
| demo = create_interface() | |
| if is_space: | |
| # Hugging Face Space 환경 | |
| # API 정보 생성을 비활성화하여 JSON 스키마 파싱 오류 방지 | |
| demo.launch(server_name="0.0.0.0", share=True, show_api=False) | |
| else: | |
| # 로컬 환경 - 포트 충돌 시 자동으로 다른 포트 사용 | |
| import socket | |
| def find_free_port(start_port=7860, max_attempts=10): | |
| for i in range(max_attempts): | |
| port = start_port + i | |
| with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | |
| if s.connect_ex(('localhost', port)) != 0: | |
| return port | |
| return start_port # 기본 포트 반환 | |
| port = find_free_port() | |
| print(f"🚀 Gradio 앱을 포트 {port}에서 실행합니다...") | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=port, | |
| share=False, | |
| debug=True | |
| ) | |