Update combined_app.py
parent
915c783fa2
commit
ba7878220e
205
combined_app.py
205
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,6 +629,32 @@ def set_quiz(img: ImageFile.ImageFile, group: str, difficulty: str):
|
|||
return
|
||||
if isinstance(choices[0], list):
|
||||
choices = choices[0]
|
||||
# 시험 유형과 난이도에 따른 음성 길이/스타일 조절 (모든 조합 커버, 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"
|
||||
|
|
@ -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("---")
|
||||
st.markdown(f"<div style='text-align:center;'><b>현재 푼 문제 수:</b> {total} <b>정답률:</b> {accuracy}% <b>현재 점수:</b> {score}점</div>", unsafe_allow_html=True)
|
||||
|
||||
# Score header with emoji
|
||||
st.markdown("""
|
||||
<div style='text-align: center; margin-bottom: 20px;'>
|
||||
<h2 style='color: #4B89DC;'>🏆 최종 점수</h2>
|
||||
</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점입니다! 완벽한 성적이에요!")
|
||||
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("<br>", 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"""
|
||||
<div style='text-allow: center; padding: 10px; background-color: #f0f8ff; border-radius: 10px; margin-bottom: 20px;'>
|
||||
<h3 style='color: #4B89DC;'>👤 {st.session_state.get('nickname', '')}</h3>
|
||||
<div style='text-align: center; padding: 10px; background-color: #f0f8ff; border-radius: 10px; margin-bottom: 20px;'>
|
||||
<h3 style='color: #4B89DC; text-align: center; margin: 0; font-size: 28px;'>{st.session_state.get('nickname', '')}</h3>
|
||||
</div>
|
||||
""", 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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue