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(""" -
맞춘 문제: {correct} / {total}
-