diff --git a/combined_app.py b/combined_app.py index 876f8be..ab8ee54 100644 --- a/combined_app.py +++ b/combined_app.py @@ -21,7 +21,6 @@ import difflib wORK_DIR = Path(__file__).parent 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) IN_DIR.mkdir(exist_ok=True) OUT_DIR.mkdir(exist_ok=True) @@ -148,8 +147,8 @@ def show_auth_page(): st.success(f"회원님의 아이디는 **{username}** 입니다.") else: st.error("입력하신 정보와 일치하는 계정이 없습니다.") - - else: # 비밀번호 재설정 + # 비밀번호 재설정 + else: with st.form("reset_password_form", border=True): username = st.text_input("아이디", placeholder="아이디를 입력하세요") email = st.text_input("이메일", placeholder="가입 시 등록한 이메일을 입력하세요") @@ -189,7 +188,6 @@ def init_score(): st.session_state["total_questions"] = 0 if "learning_history" not in st.session_state: st.session_state["learning_history"] = [] - # Initialize quiz-related session state variables if "quiz" not in st.session_state: st.session_state["quiz"] = [] if "answ" not in st.session_state: @@ -332,14 +330,12 @@ def uploaded_image(on_change=None, args=None) -> Image.Image | None: return None -# Utility functions def img_to_base64(img: Image.Image) -> str: buf = BytesIO() img.save(buf, format="PNG") return base64.b64encode(buf.getvalue()).decode() def get_prompt(group: str, difficulty: str = None) -> Path: - # Map group to exam type exam_mapping = { "yle": "YLE", "toefl_junior": "TOEFL_JUNIOR", @@ -347,7 +343,6 @@ def get_prompt(group: str, difficulty: str = None) -> Path: "toefl": "TOEFL" } - # Map difficulty to exam level difficulty_mapping = { "easy": "easy", "normal": "medium", @@ -356,8 +351,6 @@ def get_prompt(group: str, difficulty: str = None) -> Path: exam_type = exam_mapping.get(group, "default") exam_level = difficulty_mapping.get(difficulty, "medium") - - # First try to get the specific exam type and difficulty prompt if difficulty: prompt_file = f"prompt_{exam_type.lower()}_{exam_level}.txt" 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(): return None, None, None, None - max_retries = 10 # 재시도 횟수 증가 + max_retries = 15 # 재시도 횟수 더 증가 retry_delay = 2 def is_complete_sentence(s): s = s.strip() - if len(s) < 5: + if s.lower() in {"t", "f", "true", "false"}: return False - if len(s.split()) < 3: + if len(s) < 15: + return False + if len(s.split()) < 5: return False if not s[0].isalpha() or not s[0].isupper(): return False if not s.endswith('.'): 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 for attempt in range(max_retries): @@ -554,9 +554,16 @@ Choices: [ if correct_answer.strip().lower() in {"t", "f", "true", "false"}: 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): - continue # 재생성 + # 정답/선택지 모두 더 엄격하게 완전한 문장인지 검증 + if ( + 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)가 있으면 예외 발생시켜 재생성 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) 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("이미지 퀴즈를 준비 중입니다...🦜"): # 이미지가 변경되었을 때 question_count 초기화 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 if isinstance(choices[0], list): 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") path = OUT_DIR / f"{Path(__file__).stem}.wav" with open(path, "wb") as fp: @@ -710,9 +743,9 @@ def show_quiz(difficulty="medium"): update_score(quiz, is_correct) if is_correct: - feedback = "✅ 정답입니다! 🎉" + feedback = "정답입니다." 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 @@ -721,16 +754,17 @@ def show_quiz(difficulty="medium"): with st.expander("📚 해설 보기", expanded=True): if is_valid_sentence(answ[idx]): st.markdown(f"**정답:** {answ[idx]}") - # else: 아무것도 표시하지 않음 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): init_score() - # Only update if this question hasn't been answered before if question not in st.session_state["answered_questions"]: st.session_state["answered_questions"].add(question) + st.session_state["total_questions"] += 1 # 총 문제 수 증가 if is_correct: st.session_state["correct_answers"] += 1 @@ -742,9 +776,8 @@ def update_score(question: str, is_correct: bool): feedback = generate_feedback( st.session_state.get("last_user_choice", ""), 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({ "question": question, "correct": is_correct, @@ -756,7 +789,6 @@ def update_score(question: str, is_correct: bool): "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"): # 현재까지 푼 문제 수 계산 (중복 제외) 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})" def show_score_summary(): - if "quiz_data" not in st.session_state or not st.session_state["quiz_data"]: - return - - # Get the latest counts from session state - total = st.session_state["total_questions"] - correct = st.session_state["correct_answers"] + total = st.session_state.get("question_count", 0) + correct = st.session_state.get("correct_answers", 0) + score = st.session_state.get("total_score", 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("---") - - # Score header with emoji - st.markdown(""" -
-

🏆 최종 점수

-
- """, unsafe_allow_html=True) + st.markdown(f"
현재 푼 문제 수: {total}    정답률: {accuracy}%    현재 점수: {score}점
", 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""" -
-

최종 점수

-

{score}점

-
- """, unsafe_allow_html=True) - - with col2: - # Progress card with clear statistics - st.markdown(f""" -
-

정답률

-

{accuracy}%

-

맞춘 문제: {correct} / {total}

-
- """, unsafe_allow_html=True) - - # Progress bar with better visibility - st.markdown(f""" -
-
-
-
-
- """, unsafe_allow_html=True) - - # Add encouraging message based on performance - if total == 10 and correct == 10: - st.success("🎉 축하합니다! 100점입니다! 완벽한 성적이에요!") + if total == 0: + st.info("아직 문제를 풀지 않았어요! 문제를 풀고 점수를 확인해보세요.") + elif accuracy == 100 and total > 0: + st.success("🎉 완벽합니다! 계속 도전해보세요!") elif accuracy >= 80: - st.success("🎉 훌륭해요! 계속 이렇게 잘 해봐요!") + st.info("아주 잘하고 있어요! 조금만 더 힘내요!") elif accuracy >= 60: - st.info("잘하고 있어요! 조금만 더 노력해봐요!") + st.info("좋아요! 계속 연습해봐요!") else: - st.warning("💪 조금 더 연습하면 더 잘할 수 있을 거예요!") - - clear_all_scores() + st.info("조금 더 연습하면 더 좋아질 거예요!") def reset_quiz(): if st.session_state.get("quiz"): - # Add some vertical space before the button st.markdown("
", unsafe_allow_html=True) if st.button("🔄 새로운 문제", type="primary"): - # Keep authentication state auth_state = { "authenticated": st.session_state.get("authenticated", False), "username": st.session_state.get("username", ""), "user_id": st.session_state.get("user_id", None) } - # Keep image state - img_state = st.session_state.get("img_state", { - "has_image": False, - "img_bytes": None, - "img": None - }) + img_state = { + "has_image": st.session_state.get("has_image", False), + "img_bytes": st.session_state.get("img_bytes", None), + "img": st.session_state.get("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 = { "total_score": st.session_state.get("total_score", 0), "quiz_data": st.session_state.get("quiz_data", []), @@ -890,25 +870,30 @@ def reset_quiz(): "correct_answers": st.session_state.get("correct_answers", 0), "total_questions": st.session_state.get("total_questions", 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"] for key in keys_to_clear: if key in st.session_state: del st.session_state[key] - # Clear form-related states for key in list(st.session_state.keys()): if key.startswith(("submitted_", "feedback_", "choice_", "form_question_")): del st.session_state[key] - # Restore important states 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) + 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() def show_learning_history(): @@ -983,10 +968,8 @@ def clear_all_scores(): st.success("현재 점수가 초기화되었습니다.") st.rerun() -# Main application if __name__ == "__main__": try: - # Initialize page configuration first init_page() @@ -1009,8 +992,8 @@ if __name__ == "__main__": # 사이드바에 사용자 정보 표시 with st.sidebar: st.markdown(f""" -
-

👤 {st.session_state.get('nickname', '')}

+
+

{st.session_state.get('nickname', '')}

""", unsafe_allow_html=True) @@ -1038,7 +1021,6 @@ if __name__ == "__main__": st.error("이미 사용 중인 닉네임입니다.") if st.button("로그아웃", use_container_width=True): - # Clear all session state for key in list(st.session_state.keys()): del st.session_state[key] st.rerun() @@ -1109,12 +1091,27 @@ if __name__ == "__main__": 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"): show_quiz() if st.session_state.get("quiz_data"): show_score_summary() 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"): reset_quiz()