Update combined_app.py

main
no 2025-05-23 01:54:40 +00:00
parent 915c783fa2
commit ba7878220e
1 changed files with 102 additions and 105 deletions

View File

@ -21,7 +21,6 @@ import difflib
wORK_DIR = Path(__file__).parent wORK_DIR = Path(__file__).parent
IMG_DIR, IN_DIR, OUT_DIR = wORK_DIR / "img", wORK_DIR / "input", wORK_DIR / "output" IMG_DIR, IN_DIR, OUT_DIR = wORK_DIR / "img", wORK_DIR / "input", wORK_DIR / "output"
# Create directories if they don't exist
IMG_DIR.mkdir(exist_ok=True) IMG_DIR.mkdir(exist_ok=True)
IN_DIR.mkdir(exist_ok=True) IN_DIR.mkdir(exist_ok=True)
OUT_DIR.mkdir(exist_ok=True) OUT_DIR.mkdir(exist_ok=True)
@ -148,8 +147,8 @@ def show_auth_page():
st.success(f"회원님의 아이디는 **{username}** 입니다.") st.success(f"회원님의 아이디는 **{username}** 입니다.")
else: else:
st.error("입력하신 정보와 일치하는 계정이 없습니다.") st.error("입력하신 정보와 일치하는 계정이 없습니다.")
# 비밀번호 재설정
else: # 비밀번호 재설정 else:
with st.form("reset_password_form", border=True): with st.form("reset_password_form", border=True):
username = st.text_input("아이디", placeholder="아이디를 입력하세요") username = st.text_input("아이디", placeholder="아이디를 입력하세요")
email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요") email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요")
@ -189,7 +188,6 @@ def init_score():
st.session_state["total_questions"] = 0 st.session_state["total_questions"] = 0
if "learning_history" not in st.session_state: if "learning_history" not in st.session_state:
st.session_state["learning_history"] = [] st.session_state["learning_history"] = []
# Initialize quiz-related session state variables
if "quiz" not in st.session_state: if "quiz" not in st.session_state:
st.session_state["quiz"] = [] st.session_state["quiz"] = []
if "answ" not in st.session_state: if "answ" not in st.session_state:
@ -332,14 +330,12 @@ def uploaded_image(on_change=None, args=None) -> Image.Image | None:
return None return None
# Utility functions
def img_to_base64(img: Image.Image) -> str: def img_to_base64(img: Image.Image) -> str:
buf = BytesIO() buf = BytesIO()
img.save(buf, format="PNG") img.save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode() return base64.b64encode(buf.getvalue()).decode()
def get_prompt(group: str, difficulty: str = None) -> Path: def get_prompt(group: str, difficulty: str = None) -> Path:
# Map group to exam type
exam_mapping = { exam_mapping = {
"yle": "YLE", "yle": "YLE",
"toefl_junior": "TOEFL_JUNIOR", "toefl_junior": "TOEFL_JUNIOR",
@ -347,7 +343,6 @@ def get_prompt(group: str, difficulty: str = None) -> Path:
"toefl": "TOEFL" "toefl": "TOEFL"
} }
# Map difficulty to exam level
difficulty_mapping = { difficulty_mapping = {
"easy": "easy", "easy": "easy",
"normal": "medium", "normal": "medium",
@ -356,8 +351,6 @@ def get_prompt(group: str, difficulty: str = None) -> Path:
exam_type = exam_mapping.get(group, "default") exam_type = exam_mapping.get(group, "default")
exam_level = difficulty_mapping.get(difficulty, "medium") exam_level = difficulty_mapping.get(difficulty, "medium")
# First try to get the specific exam type and difficulty prompt
if difficulty: if difficulty:
prompt_file = f"prompt_{exam_type.lower()}_{exam_level}.txt" prompt_file = f"prompt_{exam_type.lower()}_{exam_level}.txt"
path = IN_DIR / prompt_file path = IN_DIR / prompt_file
@ -392,19 +385,26 @@ def generate_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
if not can_generate_more_questions(): if not can_generate_more_questions():
return None, None, None, None return None, None, None, None
max_retries = 10 # 재시도 횟수 증가 max_retries = 15 # 재시도 횟수 더 증가
retry_delay = 2 retry_delay = 2
def is_complete_sentence(s): def is_complete_sentence(s):
s = s.strip() s = s.strip()
if len(s) < 5: if s.lower() in {"t", "f", "true", "false"}:
return False return False
if len(s.split()) < 3: if len(s) < 15:
return False
if len(s.split()) < 5:
return False return False
if not s[0].isalpha() or not s[0].isupper(): if not s[0].isalpha() or not s[0].isupper():
return False return False
if not s.endswith('.'): if not s.endswith('.'):
return False return False
# 주어/동사 포함 등 추가 검증
if not re.search(r'\b(I|You|He|She|They|We|It|[A-Z][a-z]+)\b', s):
return False
if not re.search(r'\b(is|are|was|were|am|be|being|been|has|have|had|does|do|did|go|goes|went|see|sees|saw|make|makes|made|play|plays|played|study|studies|studied|work|works|worked|run|runs|ran|eat|eats|ate|read|reads|read|write|writes|wrote|draw|draws|drew|paint|paints|painted|talk|talks|talked|say|says|said|give|gives|gave|take|takes|took|sit|sits|sat|stand|stands|stood|walk|walks|walked|look|looks|looked|show|shows|showed|help|helps|helped|teach|teaches|taught|learn|learns|learned|prepare|prepares|prepared|discuss|discusses|discussed|analyze|analyzes|analyzed|explain|explains|explained|organize|organizes|organized|participate|participates|participated|present|presents|presented|review|reviews|reviewed|plan|plans|planned|report|reports|reported|listen|listens|listened|watch|watches|watched|clean|cleans|cleaned)\b', s, re.IGNORECASE):
return False
return True return True
for attempt in range(max_retries): for attempt in range(max_retries):
@ -554,9 +554,16 @@ Choices: [
if correct_answer.strip().lower() in {"t", "f", "true", "false"}: if correct_answer.strip().lower() in {"t", "f", "true", "false"}:
raise ValueError(f"정답이 T/F 또는 True/False로 감지되어 문제를 재생성합니다. 정답: {correct_answer}") raise ValueError(f"정답이 T/F 또는 True/False로 감지되어 문제를 재생성합니다. 정답: {correct_answer}")
# 정답/선택지 모두 완전한 문장인지 검증 # 정답/선택지 모두 더 엄격하게 완전한 문장인지 검증
if not is_complete_sentence(correct_answer) or any(not is_complete_sentence(c) for c in choices): if (
continue # 재생성 not is_complete_sentence(correct_answer)
or any(not is_complete_sentence(c) for c in choices)
or correct_answer.strip().lower() in {"t", "f", "true", "false"}
or any(c.strip().lower() in {"t", "f", "true", "false"} for c in choices)
or len(correct_answer.strip().split()) == 1
or any(len(c.strip().split()) == 1 for c in choices)
):
continue
# 중복 문제 방지: 이미 푼 문제(question)가 있으면 예외 발생시켜 재생성 # 중복 문제 방지: 이미 푼 문제(question)가 있으면 예외 발생시켜 재생성
if "answered_questions" in st.session_state and question in st.session_state["answered_questions"]: if "answered_questions" in st.session_state and question in st.session_state["answered_questions"]:
@ -602,7 +609,7 @@ def tokenize_sent(text: str) -> list[str]:
return nltk.tokenize.sent_tokenize(text) return nltk.tokenize.sent_tokenize(text)
def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str): def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
if img and not st.session_state["quiz"]: if img and not st.session_state["quiz"] and st.session_state.get("question_count", 0) < 10:
with st.spinner("이미지 퀴즈를 준비 중입니다...🦜"): with st.spinner("이미지 퀴즈를 준비 중입니다...🦜"):
# 이미지가 변경되었을 때 question_count 초기화 # 이미지가 변경되었을 때 question_count 초기화
if "last_image" not in st.session_state or st.session_state["last_image"] != img: if "last_image" not in st.session_state or st.session_state["last_image"] != img:
@ -622,7 +629,33 @@ def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
return return
if isinstance(choices[0], list): if isinstance(choices[0], list):
choices = choices[0] choices = choices[0]
question_audio = question # 시험 유형과 난이도에 따른 음성 길이/스타일 조절 (모든 조합 커버, 12가지)
if group == "yle" and difficulty == "easy":
question_audio = f"Listen carefully. {question}"
elif group == "yle" and difficulty == "normal":
question_audio = f"Listen to the following question and think carefully before you answer. {question}"
elif group == "yle" and difficulty == "hard":
question_audio = f"Listen closely and try to understand all the details in the question. {question}"
elif group == "toefl_junior" and difficulty == "easy":
question_audio = f"Please listen to the question and choose the best answer. {question}"
elif group == "toefl_junior" and difficulty == "normal":
question_audio = f"Listen carefully to the following question and take your time to answer. {question}"
elif group == "toefl_junior" and difficulty == "hard":
question_audio = f"Listen very carefully and consider all the information before you answer. {question}"
elif group == "toeic" and difficulty == "easy":
question_audio = f"Listen to the question and select the most appropriate answer. {question}"
elif group == "toeic" and difficulty == "normal":
question_audio = f"Listen carefully and pay attention to the details in the question. {question}"
elif group == "toeic" and difficulty == "hard":
question_audio = f"Listen to the following question. Consider all the details and select the most accurate answer. {question}"
elif group == "toefl" and difficulty == "easy":
question_audio = f"Listen to the question and answer carefully. {question}"
elif group == "toefl" and difficulty == "normal":
question_audio = f"Listen carefully to the following question. Think about all the information before answering. {question}"
elif group == "toefl" and difficulty == "hard":
question_audio = f"Listen very carefully. Consider all aspects and select the most accurate answer. {question}"
else:
question_audio = question
wav_file = synth_speech(question_audio, st.session_state["voice"], "wav") wav_file = synth_speech(question_audio, st.session_state["voice"], "wav")
path = OUT_DIR / f"{Path(__file__).stem}.wav" path = OUT_DIR / f"{Path(__file__).stem}.wav"
with open(path, "wb") as fp: with open(path, "wb") as fp:
@ -710,9 +743,9 @@ def show_quiz(difficulty="medium"):
update_score(quiz, is_correct) update_score(quiz, is_correct)
if is_correct: if is_correct:
feedback = "✅ 정답입니다! 🎉" feedback = "정답입니다."
else: else:
feedback = f"❌ 오답입니다.\n\n{generate_feedback(user_choice, answ[idx])}" feedback = f"❌ 오답입니다.\n정답: {answ[idx]}\n\n{generate_feedback(user_choice, answ[idx])}"
st.session_state[key_feedback] = feedback st.session_state[key_feedback] = feedback
@ -721,16 +754,17 @@ def show_quiz(difficulty="medium"):
with st.expander("📚 해설 보기", expanded=True): with st.expander("📚 해설 보기", expanded=True):
if is_valid_sentence(answ[idx]): if is_valid_sentence(answ[idx]):
st.markdown(f"**정답:** {answ[idx]}") st.markdown(f"**정답:** {answ[idx]}")
# else: 아무것도 표시하지 않음
st.markdown(feedback, unsafe_allow_html=True) st.markdown(feedback, unsafe_allow_html=True)
# 정답을 항상 명확하게 표시
if is_valid_sentence(answ[idx]):
st.info(f"정답: {answ[idx]}")
def update_score(question: str, is_correct: bool): def update_score(question: str, is_correct: bool):
init_score() init_score()
# Only update if this question hasn't been answered before
if question not in st.session_state["answered_questions"]: if question not in st.session_state["answered_questions"]:
st.session_state["answered_questions"].add(question) st.session_state["answered_questions"].add(question)
st.session_state["total_questions"] += 1 # 총 문제 수 증가
if is_correct: if is_correct:
st.session_state["correct_answers"] += 1 st.session_state["correct_answers"] += 1
@ -742,9 +776,8 @@ def update_score(question: str, is_correct: bool):
feedback = generate_feedback( feedback = generate_feedback(
st.session_state.get("last_user_choice", ""), st.session_state.get("last_user_choice", ""),
st.session_state.get("last_correct_answer", "") st.session_state.get("last_correct_answer", "")
) if not is_correct else "정답입니다! 🎉" ) if not is_correct else "정답입니다."
# Add to current quiz data
st.session_state["quiz_data"].append({ st.session_state["quiz_data"].append({
"question": question, "question": question,
"correct": is_correct, "correct": is_correct,
@ -756,7 +789,6 @@ def update_score(question: str, is_correct: bool):
"correct_answer": st.session_state.get("last_correct_answer", "") "correct_answer": st.session_state.get("last_correct_answer", "")
}) })
# Save to database if user is authenticated
if st.session_state.get("authenticated") and st.session_state.get("user_id"): if st.session_state.get("authenticated") and st.session_state.get("user_id"):
# 현재까지 푼 문제 수 계산 (중복 제외) # 현재까지 푼 문제 수 계산 (중복 제외)
current_question_count = len(st.session_state["answered_questions"]) current_question_count = len(st.session_state["answered_questions"])
@ -794,95 +826,43 @@ def generate_feedback(user_input: str, answ: str) -> str:
return f"(⚠️ 피드백 생성 중 오류: {e})" return f"(⚠️ 피드백 생성 중 오류: {e})"
def show_score_summary(): def show_score_summary():
if "quiz_data" not in st.session_state or not st.session_state["quiz_data"]: total = st.session_state.get("question_count", 0)
return correct = st.session_state.get("correct_answers", 0)
score = st.session_state.get("total_score", 0)
# Get the latest counts from session state
total = st.session_state["total_questions"]
correct = st.session_state["correct_answers"]
accuracy = round((correct / total) * 100, 1) if total else 0.0 accuracy = round((correct / total) * 100, 1) if total else 0.0
score = st.session_state["total_score"]
# Show current progress
st.info(f"현재 {total}문제를 풀었어요! (정답률: {accuracy}%, 현재 점수: {score}점)")
# Only show detailed summary when all 10 questions are answered
if total < 10:
return
# Create a more visually appealing and accessible score display
st.markdown("---") st.markdown("---")
st.markdown(f"<div style='text-align:center;'><b>현재 푼 문제 수:</b> {total} &nbsp;&nbsp; <b>정답률:</b> {accuracy}% &nbsp;&nbsp; <b>현재 점수:</b> {score}점</div>", unsafe_allow_html=True)
# Score header with emoji if total == 0:
st.markdown(""" st.info("아직 문제를 풀지 않았어요! 문제를 풀고 점수를 확인해보세요.")
<div style='text-align: center; margin-bottom: 20px;'> elif accuracy == 100 and total > 0:
<h2 style='color: #4B89DC;'>🏆 최종 점수</h2> st.success("🎉 완벽합니다! 계속 도전해보세요!")
</div>
""", unsafe_allow_html=True)
# Create two columns for better layout
col1, col2 = st.columns(2)
with col1:
# Score card with large, clear numbers
st.markdown(f"""
<div style='background-color: #f0f8ff; padding: 20px; border-radius: 10px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
<h3 style='color: #4B89DC; margin-bottom: 10px;'>최종 점수</h3>
<h1 style='font-size: 48px; color: #2E7D32; margin: 0;'>{score}</h1>
</div>
""", unsafe_allow_html=True)
with col2:
# Progress card with clear statistics
st.markdown(f"""
<div style='background-color: #f0f8ff; padding: 20px; border-radius: 10px; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.1);'>
<h3 style='color: #4B89DC; margin-bottom: 10px;'>정답률</h3>
<h2 style='color: #2E7D32; margin: 0;'>{accuracy}%</h2>
<p style='color: #666; margin-top: 10px;'>맞춘 문제: {correct} / {total}</p>
</div>
""", unsafe_allow_html=True)
# Progress bar with better visibility
st.markdown(f"""
<div style='margin-top: 20px;'>
<div style='background-color: #e0e0e0; height: 20px; border-radius: 10px; margin-bottom: 10px;'>
<div style='background-color: #4B89DC; width: {accuracy}%; height: 20px; border-radius: 10px;'></div>
</div>
</div>
""", unsafe_allow_html=True)
# Add encouraging message based on performance
if total == 10 and correct == 10:
st.success("🎉 축하합니다! 100점입니다! 완벽한 성적이에요!")
elif accuracy >= 80: elif accuracy >= 80:
st.success("🎉 훌륭해요! 계속 이렇게 잘 해봐요!") st.info("아주 잘하고 있어요! 조금만 더 힘내요!")
elif accuracy >= 60: elif accuracy >= 60:
st.info("잘하고 있어요! 조금만 더 노력해봐요!") st.info("좋아요! 계속 연습해봐요!")
else: else:
st.warning("💪 조금 더 연습하면 더 잘할 수 있을 거예요!") st.info("조금 더 연습하면 더 좋아질 거예요!")
clear_all_scores()
def reset_quiz(): def reset_quiz():
if st.session_state.get("quiz"): if st.session_state.get("quiz"):
# Add some vertical space before the button
st.markdown("<br>", unsafe_allow_html=True) st.markdown("<br>", unsafe_allow_html=True)
if st.button("🔄 새로운 문제", type="primary"): if st.button("🔄 새로운 문제", type="primary"):
# Keep authentication state
auth_state = { auth_state = {
"authenticated": st.session_state.get("authenticated", False), "authenticated": st.session_state.get("authenticated", False),
"username": st.session_state.get("username", ""), "username": st.session_state.get("username", ""),
"user_id": st.session_state.get("user_id", None) "user_id": st.session_state.get("user_id", None)
} }
# Keep image state img_state = {
img_state = st.session_state.get("img_state", { "has_image": st.session_state.get("has_image", False),
"has_image": False, "img_bytes": st.session_state.get("img_bytes", None),
"img_bytes": None, "img": st.session_state.get("img", None),
"img": None "last_image": st.session_state.get("last_image", None),
}) "sidebar_img_choice": st.session_state.get("sidebar_img_choice", None)
}
# Keep score-related states
score_state = { score_state = {
"total_score": st.session_state.get("total_score", 0), "total_score": st.session_state.get("total_score", 0),
"quiz_data": st.session_state.get("quiz_data", []), "quiz_data": st.session_state.get("quiz_data", []),
@ -890,25 +870,30 @@ def reset_quiz():
"correct_answers": st.session_state.get("correct_answers", 0), "correct_answers": st.session_state.get("correct_answers", 0),
"total_questions": st.session_state.get("total_questions", 0), "total_questions": st.session_state.get("total_questions", 0),
"question_count": st.session_state.get("question_count", 0), "question_count": st.session_state.get("question_count", 0),
"last_image": st.session_state.get("last_image", None) "current_group": st.session_state.get("current_group", "default"),
"global_difficulty": st.session_state.get("global_difficulty", "normal")
} }
# Clear only current quiz states
keys_to_clear = ["quiz", "answ", "audio", "choices"] keys_to_clear = ["quiz", "answ", "audio", "choices"]
for key in keys_to_clear: for key in keys_to_clear:
if key in st.session_state: if key in st.session_state:
del st.session_state[key] del st.session_state[key]
# Clear form-related states
for key in list(st.session_state.keys()): for key in list(st.session_state.keys()):
if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")): if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")):
del st.session_state[key] del st.session_state[key]
# Restore important states
st.session_state.update(auth_state) st.session_state.update(auth_state)
st.session_state["img_state"] = img_state st.session_state.update(img_state)
st.session_state.update(score_state) st.session_state.update(score_state)
if st.session_state.get("img") and st.session_state.get("question_count", 0) < 10:
set_quiz(
st.session_state["img"],
st.session_state.get("current_group", "default"),
st.session_state.get("global_difficulty", "normal")
)
st.rerun() st.rerun()
def show_learning_history(): def show_learning_history():
@ -983,10 +968,8 @@ def clear_all_scores():
st.success("현재 점수가 초기화되었습니다.") st.success("현재 점수가 초기화되었습니다.")
st.rerun() st.rerun()
# Main application
if __name__ == "__main__": if __name__ == "__main__":
try: try:
# Initialize page configuration first
init_page() init_page()
@ -1009,8 +992,8 @@ if __name__ == "__main__":
# 사이드바에 사용자 정보 표시 # 사이드바에 사용자 정보 표시
with st.sidebar: with st.sidebar:
st.markdown(f""" st.markdown(f"""
<div style='text-allow: center; padding: 10px; background-color: #f0f8ff; border-radius: 10px; margin-bottom: 20px;'> <div style='text-align: center; padding: 10px; background-color: #f0f8ff; border-radius: 10px; margin-bottom: 20px;'>
<h3 style='color: #4B89DC;'>👤 {st.session_state.get('nickname', '')}</h3> <h3 style='color: #4B89DC; text-align: center; margin: 0; font-size: 28px;'>{st.session_state.get('nickname', '')}</h3>
</div> </div>
""", unsafe_allow_html=True) """, unsafe_allow_html=True)
@ -1038,7 +1021,6 @@ if __name__ == "__main__":
st.error("이미 사용 중인 닉네임입니다.") st.error("이미 사용 중인 닉네임입니다.")
if st.button("로그아웃", use_container_width=True): if st.button("로그아웃", use_container_width=True):
# Clear all session state
for key in list(st.session_state.keys()): for key in list(st.session_state.keys()):
del st.session_state[key] del st.session_state[key]
st.rerun() st.rerun()
@ -1109,12 +1091,27 @@ if __name__ == "__main__":
img = Image.open(f"img/{img_choice}") img = Image.open(f"img/{img_choice}")
# 문제 표시 (세션에 퀴즈가 있으면) # 문제 표시 (세션에 퀴즈가 있으면)
if not st.session_state.get("quiz") and st.session_state.get("question_count", 0) < 10:
set_quiz(img, st.session_state.get("current_group", "default"), st.session_state.get("global_difficulty", "normal"))
if st.session_state.get("quiz"): if st.session_state.get("quiz"):
show_quiz() show_quiz()
if st.session_state.get("quiz_data"): if st.session_state.get("quiz_data"):
show_score_summary() show_score_summary()
show_learning_history() show_learning_history()
# '새로운 문제' 버튼: 10문제까지 허용, 그 이상은 비활성화 및 안내
if st.session_state.get("question_count", 0) < 10:
if st.button("새로운 문제", type="primary"):
reset_quiz()
st.session_state["quiz_run_id"] = str(time.time())
st.rerun()
else:
st.info("이 이미지로 풀 수 있는 최대 10문제를 모두 풀었습니다! 다른 이미지를 선택해 주세요.")
except Exception as e:
st.error(f"오류가 발생했습니다: {str(e)}")
st.info("페이지를 새로고침하거나 다시 시도해주세요.")
# '새로운 문제' 버튼: 항상 퀴즈 아래에 표시 # '새로운 문제' 버튼: 항상 퀴즈 아래에 표시
if st.button("새로운 문제", type="primary"): if st.button("새로운 문제", type="primary"):
reset_quiz() reset_quiz()