coconut / gradio_app.py
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
@safe_api_call
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)
@safe_api_call
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
)