Update app.py
Browse files
app.py
CHANGED
|
@@ -926,6 +926,156 @@ def validate_questions(questions: List[Dict], page_contents: List[PageContent],
|
|
| 926 |
|
| 927 |
return validated
|
| 928 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 929 |
# ============================================================
|
| 930 |
# PDF Generation
|
| 931 |
# ============================================================
|
|
@@ -1009,7 +1159,7 @@ def create_exam_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1009 |
return temp_file.name
|
| 1010 |
|
| 1011 |
def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
| 1012 |
-
"""Create answer key PDF with explanations"""
|
| 1013 |
|
| 1014 |
lang = LANG_TEXT[language]
|
| 1015 |
|
|
@@ -1037,7 +1187,8 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1037 |
fontSize=11,
|
| 1038 |
spaceAfter=8,
|
| 1039 |
textColor=colors.HexColor('#1F2937'),
|
| 1040 |
-
fontName=font_name
|
|
|
|
| 1041 |
)
|
| 1042 |
|
| 1043 |
answer_style = ParagraphStyle(
|
|
@@ -1046,7 +1197,9 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1046 |
fontSize=11,
|
| 1047 |
leftIndent=15,
|
| 1048 |
textColor=colors.HexColor('#059669'),
|
| 1049 |
-
fontName=font_name
|
|
|
|
|
|
|
| 1050 |
)
|
| 1051 |
|
| 1052 |
explanation_style = ParagraphStyle(
|
|
@@ -1056,7 +1209,43 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1056 |
leftIndent=15,
|
| 1057 |
spaceAfter=5,
|
| 1058 |
textColor=colors.HexColor('#374151'),
|
| 1059 |
-
fontName=font_name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
)
|
| 1061 |
|
| 1062 |
wrong_style = ParagraphStyle(
|
|
@@ -1065,7 +1254,8 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1065 |
fontSize=9,
|
| 1066 |
leftIndent=25,
|
| 1067 |
textColor=colors.HexColor('#DC2626'),
|
| 1068 |
-
fontName=font_name
|
|
|
|
| 1069 |
)
|
| 1070 |
|
| 1071 |
normal_korean = ParagraphStyle(
|
|
@@ -1080,12 +1270,25 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1080 |
fontName=font_name
|
| 1081 |
)
|
| 1082 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
story = []
|
| 1084 |
|
| 1085 |
story.append(Paragraph(f"{lang['pdf_answer_title']}", title_style))
|
| 1086 |
story.append(Paragraph(f"{lang['pdf_date']}: {datetime.now().strftime('%Y-%m-%d')}", normal_korean))
|
| 1087 |
story.append(Spacer(1, 20))
|
| 1088 |
|
|
|
|
| 1089 |
story.append(Paragraph(f"<b>{lang['pdf_quick_summary']}</b>", heading_korean))
|
| 1090 |
|
| 1091 |
answer_summary = []
|
|
@@ -1116,31 +1319,97 @@ def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
|
| 1116 |
story.append(Paragraph(f"<b>{lang['pdf_detailed']}</b>", heading_korean))
|
| 1117 |
story.append(Spacer(1, 15))
|
| 1118 |
|
|
|
|
| 1119 |
for i, q in enumerate(questions, 1):
|
|
|
|
| 1120 |
q_text = f"<b>Q{i}.</b> {q['question']}"
|
| 1121 |
story.append(Paragraph(q_text, question_style))
|
| 1122 |
|
| 1123 |
-
|
|
|
|
| 1124 |
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
if q.get('source_quote'):
|
| 1128 |
-
quote_text = q['source_quote'][:200] + "..." if len(q.get('source_quote', '')) > 200 else q.get('source_quote', '')
|
| 1129 |
-
story.append(Paragraph(f"{lang['pdf_reference']}: \"{quote_text}\"", explanation_style))
|
| 1130 |
|
| 1131 |
explanation = q.get('explanation', {})
|
| 1132 |
-
if explanation.get('correct_reason'):
|
| 1133 |
-
story.append(Paragraph(f"{lang['pdf_why_correct']}: {explanation['correct_reason']}", explanation_style))
|
| 1134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
story.append(Spacer(1, 5))
|
| 1136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1137 |
|
| 1138 |
-
|
| 1139 |
-
if explanation.get(key) and explanation[key].strip():
|
| 1140 |
-
letter = key.split('_')[1]
|
| 1141 |
-
story.append(Paragraph(f"X {letter}: {explanation[key]}", wrong_style))
|
| 1142 |
|
| 1143 |
-
|
|
|
|
|
|
|
| 1144 |
|
| 1145 |
doc.build(story)
|
| 1146 |
return temp_file.name
|
|
@@ -1195,14 +1464,18 @@ def process_exam(
|
|
| 1195 |
if not validated:
|
| 1196 |
return lang["error_no_questions"], "", None, None, "", []
|
| 1197 |
|
| 1198 |
-
|
| 1199 |
-
|
| 1200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
|
| 1202 |
progress(0.95, desc=lang["phase_final"])
|
| 1203 |
|
| 1204 |
answer_distribution = {}
|
| 1205 |
-
for q in
|
| 1206 |
label = q.get('correct_label', 'A')
|
| 1207 |
answer_distribution[label] = answer_distribution.get(label, 0) + 1
|
| 1208 |
|
|
@@ -1213,11 +1486,12 @@ def process_exam(
|
|
| 1213 |
📁 File Type: {file_type_display}
|
| 1214 |
{'=' * 50}
|
| 1215 |
{lang['stats_title']}
|
| 1216 |
-
• {lang['total_questions']}: {len(
|
| 1217 |
• {lang['choices_per_q']}: {num_choices}
|
| 1218 |
• {lang['difficulty']}: {difficulty}
|
| 1219 |
• {lang['doc_pages']}: {len(page_contents)}
|
| 1220 |
• Total Characters: {total_chars:,}
|
|
|
|
| 1221 |
{'=' * 50}
|
| 1222 |
{lang['answer_dist']}
|
| 1223 |
{dist_text}
|
|
@@ -1229,11 +1503,11 @@ def process_exam(
|
|
| 1229 |
|
| 1230 |
🎯 Now go to "Take Exam" tab to start your interactive exam!"""
|
| 1231 |
|
| 1232 |
-
display_text = format_questions_display(
|
| 1233 |
|
| 1234 |
progress(1.0, desc=lang["phase_complete"])
|
| 1235 |
|
| 1236 |
-
return info_log, display_text, exam_pdf, answer_pdf, "",
|
| 1237 |
|
| 1238 |
except Exception as e:
|
| 1239 |
import traceback
|
|
@@ -1241,7 +1515,7 @@ def process_exam(
|
|
| 1241 |
return f"❌ Error: {str(e)}", "", None, None, "", []
|
| 1242 |
|
| 1243 |
def format_questions_display(questions: List[Dict], language: str) -> str:
|
| 1244 |
-
"""Format questions for text display"""
|
| 1245 |
|
| 1246 |
lang = LANG_TEXT[language]
|
| 1247 |
|
|
@@ -1261,7 +1535,22 @@ def format_questions_display(questions: List[Dict], language: str) -> str:
|
|
| 1261 |
else:
|
| 1262 |
lines.append(f" ○ {opt}")
|
| 1263 |
|
| 1264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1265 |
lines.append("-" * 60)
|
| 1266 |
|
| 1267 |
return "\n".join(lines)
|
|
@@ -1939,19 +2228,77 @@ with gr.Blocks(fill_height=True, css=css, title="PDF/HWP TO EXAM | SOMA") as dem
|
|
| 1939 |
user_answers[current_idx] = selected_answer
|
| 1940 |
|
| 1941 |
is_correct = selected_answer == q['answer']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1942 |
|
| 1943 |
if is_correct:
|
| 1944 |
feedback_text = f"""## {lang['correct_feedback']}
|
| 1945 |
|
| 1946 |
-
**{lang['explanation_label']}:** {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1947 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1948 |
feedback_text = f"""## {lang['incorrect_feedback']}
|
| 1949 |
|
| 1950 |
**{lang['your_answer']}:** {selected_answer}
|
| 1951 |
|
| 1952 |
**{lang['correct_answer_is']}:** {q['answer']}
|
| 1953 |
|
| 1954 |
-
**{lang['explanation_label']}:** {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1955 |
|
| 1956 |
return user_answers, gr.update(value=feedback_text, visible=True)
|
| 1957 |
|
|
@@ -2018,6 +2365,7 @@ with gr.Blocks(fill_height=True, css=css, title="PDF/HWP TO EXAM | SOMA") as dem
|
|
| 2018 |
for i, q in enumerate(questions):
|
| 2019 |
user_answer = user_answers.get(i, None)
|
| 2020 |
is_correct = user_answer == q['answer']
|
|
|
|
| 2021 |
|
| 2022 |
if user_answer is None:
|
| 2023 |
skipped_count += 1
|
|
@@ -2029,15 +2377,45 @@ with gr.Blocks(fill_height=True, css=css, title="PDF/HWP TO EXAM | SOMA") as dem
|
|
| 2029 |
incorrect_count += 1
|
| 2030 |
status = f"❌ {lang['incorrect_feedback']}"
|
| 2031 |
|
| 2032 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2033 |
---
|
| 2034 |
-
**{lang['question_review'].format(num=i+1)}:** {q['question'][:
|
| 2035 |
|
| 2036 |
- **{lang['your_answer']}:** {user_answer or lang['not_answered']}
|
| 2037 |
- **{lang['correct_answer_is']}:** {q['answer']}
|
| 2038 |
- **Status:** {status}
|
| 2039 |
-
- **{lang['explanation_label']}:** {
|
| 2040 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2041 |
|
| 2042 |
percentage = (correct_count / total) * 100 if total > 0 else 0
|
| 2043 |
grade = calculate_grade(percentage)
|
|
|
|
| 926 |
|
| 927 |
return validated
|
| 928 |
|
| 929 |
+
# ============================================================
|
| 930 |
+
# Enhanced Explanation Generation using GROQ API
|
| 931 |
+
# ============================================================
|
| 932 |
+
def enhance_explanations(
|
| 933 |
+
questions: List[Dict],
|
| 934 |
+
page_contents: List[PageContent],
|
| 935 |
+
language: str
|
| 936 |
+
) -> List[Dict]:
|
| 937 |
+
"""
|
| 938 |
+
GROQ API를 사용하여 각 문제에 대한 상세한 논리적 설명을 생성
|
| 939 |
+
- 정답의 논리적 근거
|
| 940 |
+
- 관련 핵심 개념 및 팩트
|
| 941 |
+
- 오답별 구체적인 오류 설명
|
| 942 |
+
- 학습 포인트
|
| 943 |
+
"""
|
| 944 |
+
|
| 945 |
+
print(f"\n🧠 [설명 강화] {len(questions)}개 문제에 대한 상세 설명 생성 중...")
|
| 946 |
+
|
| 947 |
+
# 문서 컨텍스트 준비
|
| 948 |
+
full_text = "\n\n".join([f"[Page {p.page_num}]\n{p.text}" for p in page_contents])
|
| 949 |
+
doc_context = full_text[:8000] # 컨텍스트 제한
|
| 950 |
+
|
| 951 |
+
enhanced_questions = []
|
| 952 |
+
|
| 953 |
+
for i, q in enumerate(questions):
|
| 954 |
+
try:
|
| 955 |
+
print(f" 📝 문제 {i+1}/{len(questions)} 설명 강화 중...")
|
| 956 |
+
|
| 957 |
+
# 해당 문제의 소스 페이지 컨텍스트 찾기
|
| 958 |
+
source_page = q.get('source_page', 1)
|
| 959 |
+
relevant_context = ""
|
| 960 |
+
for p in page_contents:
|
| 961 |
+
if p.page_num == source_page:
|
| 962 |
+
relevant_context = p.text[:2000]
|
| 963 |
+
break
|
| 964 |
+
|
| 965 |
+
if not relevant_context:
|
| 966 |
+
relevant_context = doc_context[:2000]
|
| 967 |
+
|
| 968 |
+
if language == "한국어":
|
| 969 |
+
prompt = f"""당신은 교육 전문가입니다. 다음 시험 문제에 대해 상세하고 논리적인 설명을 작성하세요.
|
| 970 |
+
|
| 971 |
+
📚 관련 문서 내용 (페이지 {source_page}):
|
| 972 |
+
{relevant_context}
|
| 973 |
+
|
| 974 |
+
📋 문제:
|
| 975 |
+
{q['question']}
|
| 976 |
+
|
| 977 |
+
📝 선택지:
|
| 978 |
+
{chr(10).join(q['options'])}
|
| 979 |
+
|
| 980 |
+
✅ 정답: {q['answer']}
|
| 981 |
+
|
| 982 |
+
다음 JSON 형식으로만 응답하세요:
|
| 983 |
+
{{
|
| 984 |
+
"detailed_explanation": "정답이 맞는 이유에 대한 3-5문장의 상세한 논리적 설명. 문서의 구체적인 내용을 인용하여 설명.",
|
| 985 |
+
"key_facts": ["관련 핵심 팩트 1", "관련 핵심 팩트 2", "관련 핵심 팩트 3"],
|
| 986 |
+
"wrong_explanations": {{
|
| 987 |
+
"A": "A가 오답인 구체적 이유 (정답이면 빈 문자열)",
|
| 988 |
+
"B": "B가 오답인 구체적 이유 (정답이면 빈 문자열)",
|
| 989 |
+
"C": "C가 오답인 구체적 이유 (정답이면 빈 문자열)",
|
| 990 |
+
"D": "D가 오답인 구체적 이유 (정답이면 빈 문자열)",
|
| 991 |
+
"E": "E가 오답인 구체적 이유 (정답이면 빈 문자열)"
|
| 992 |
+
}},
|
| 993 |
+
"learning_point": "이 문제를 통해 학습해야 할 핵심 포인트 1-2문장",
|
| 994 |
+
"related_concepts": ["관련 개념 1", "관련 개념 2"]
|
| 995 |
+
}}
|
| 996 |
+
|
| 997 |
+
중요: 추측하지 말고 문서 내용을 기반으로 팩트에 근거한 설명을 작성하세요."""
|
| 998 |
+
else:
|
| 999 |
+
prompt = f"""You are an education expert. Write detailed, logical explanations for the following exam question.
|
| 1000 |
+
|
| 1001 |
+
📚 Relevant Document Content (Page {source_page}):
|
| 1002 |
+
{relevant_context}
|
| 1003 |
+
|
| 1004 |
+
📋 Question:
|
| 1005 |
+
{q['question']}
|
| 1006 |
+
|
| 1007 |
+
📝 Options:
|
| 1008 |
+
{chr(10).join(q['options'])}
|
| 1009 |
+
|
| 1010 |
+
✅ Correct Answer: {q['answer']}
|
| 1011 |
+
|
| 1012 |
+
Respond ONLY in JSON format:
|
| 1013 |
+
{{
|
| 1014 |
+
"detailed_explanation": "3-5 sentence detailed logical explanation of why the answer is correct. Quote specific content from the document.",
|
| 1015 |
+
"key_facts": ["Key fact 1", "Key fact 2", "Key fact 3"],
|
| 1016 |
+
"wrong_explanations": {{
|
| 1017 |
+
"A": "Specific reason why A is wrong (empty string if correct)",
|
| 1018 |
+
"B": "Specific reason why B is wrong (empty string if correct)",
|
| 1019 |
+
"C": "Specific reason why C is wrong (empty string if correct)",
|
| 1020 |
+
"D": "Specific reason why D is wrong (empty string if correct)",
|
| 1021 |
+
"E": "Specific reason why E is wrong (empty string if correct)"
|
| 1022 |
+
}},
|
| 1023 |
+
"learning_point": "1-2 sentence key learning point from this question",
|
| 1024 |
+
"related_concepts": ["Related concept 1", "Related concept 2"]
|
| 1025 |
+
}}
|
| 1026 |
+
|
| 1027 |
+
IMPORTANT: Do not speculate. Base explanations on facts from the document."""
|
| 1028 |
+
|
| 1029 |
+
completion = client.chat.completions.create(
|
| 1030 |
+
model="openai/gpt-oss-120b",
|
| 1031 |
+
messages=[{"role": "user", "content": prompt}],
|
| 1032 |
+
temperature=0.3,
|
| 1033 |
+
max_completion_tokens=2048,
|
| 1034 |
+
top_p=1,
|
| 1035 |
+
)
|
| 1036 |
+
|
| 1037 |
+
response = completion.choices[0].message.content
|
| 1038 |
+
if response is None:
|
| 1039 |
+
response = ""
|
| 1040 |
+
response = response.strip()
|
| 1041 |
+
|
| 1042 |
+
# JSON 파싱
|
| 1043 |
+
if "```json" in response:
|
| 1044 |
+
response = response.split("```json")[1].split("```")[0]
|
| 1045 |
+
elif "```" in response:
|
| 1046 |
+
response = response.split("```")[1].split("```")[0]
|
| 1047 |
+
|
| 1048 |
+
enhanced_data = json.loads(response.strip())
|
| 1049 |
+
|
| 1050 |
+
# 기존 explanation 업데이트
|
| 1051 |
+
if 'explanation' not in q:
|
| 1052 |
+
q['explanation'] = {}
|
| 1053 |
+
|
| 1054 |
+
q['explanation']['detailed_explanation'] = enhanced_data.get('detailed_explanation', '')
|
| 1055 |
+
q['explanation']['key_facts'] = enhanced_data.get('key_facts', [])
|
| 1056 |
+
q['explanation']['learning_point'] = enhanced_data.get('learning_point', '')
|
| 1057 |
+
q['explanation']['related_concepts'] = enhanced_data.get('related_concepts', [])
|
| 1058 |
+
|
| 1059 |
+
# 오답 설명 업데이트
|
| 1060 |
+
wrong_expl = enhanced_data.get('wrong_explanations', {})
|
| 1061 |
+
for letter in ['A', 'B', 'C', 'D', 'E']:
|
| 1062 |
+
if wrong_expl.get(letter):
|
| 1063 |
+
q['explanation'][f'wrong_{letter}'] = wrong_expl[letter]
|
| 1064 |
+
|
| 1065 |
+
# 기존 correct_reason이 없거나 짧으면 업데이트
|
| 1066 |
+
if not q['explanation'].get('correct_reason') or len(q['explanation'].get('correct_reason', '')) < 50:
|
| 1067 |
+
q['explanation']['correct_reason'] = enhanced_data.get('detailed_explanation', q['explanation'].get('correct_reason', ''))
|
| 1068 |
+
|
| 1069 |
+
enhanced_questions.append(q)
|
| 1070 |
+
|
| 1071 |
+
except Exception as e:
|
| 1072 |
+
print(f" ⚠️ 문제 {i+1} 설명 강화 실패: {e}")
|
| 1073 |
+
enhanced_questions.append(q) # 원본 유지
|
| 1074 |
+
|
| 1075 |
+
print(f"✅ 설명 강화 완료: {len(enhanced_questions)}개 문제")
|
| 1076 |
+
return enhanced_questions
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
# ============================================================
|
| 1080 |
# PDF Generation
|
| 1081 |
# ============================================================
|
|
|
|
| 1159 |
return temp_file.name
|
| 1160 |
|
| 1161 |
def create_answer_pdf(questions: List[Dict], language: str) -> str:
|
| 1162 |
+
"""Create answer key PDF with enhanced explanations"""
|
| 1163 |
|
| 1164 |
lang = LANG_TEXT[language]
|
| 1165 |
|
|
|
|
| 1187 |
fontSize=11,
|
| 1188 |
spaceAfter=8,
|
| 1189 |
textColor=colors.HexColor('#1F2937'),
|
| 1190 |
+
fontName=font_name,
|
| 1191 |
+
leading=14
|
| 1192 |
)
|
| 1193 |
|
| 1194 |
answer_style = ParagraphStyle(
|
|
|
|
| 1197 |
fontSize=11,
|
| 1198 |
leftIndent=15,
|
| 1199 |
textColor=colors.HexColor('#059669'),
|
| 1200 |
+
fontName=font_name,
|
| 1201 |
+
spaceBefore=5,
|
| 1202 |
+
spaceAfter=5
|
| 1203 |
)
|
| 1204 |
|
| 1205 |
explanation_style = ParagraphStyle(
|
|
|
|
| 1209 |
leftIndent=15,
|
| 1210 |
spaceAfter=5,
|
| 1211 |
textColor=colors.HexColor('#374151'),
|
| 1212 |
+
fontName=font_name,
|
| 1213 |
+
leading=13
|
| 1214 |
+
)
|
| 1215 |
+
|
| 1216 |
+
detailed_style = ParagraphStyle(
|
| 1217 |
+
'DetailedExplanation',
|
| 1218 |
+
parent=styles['Normal'],
|
| 1219 |
+
fontSize=10,
|
| 1220 |
+
leftIndent=15,
|
| 1221 |
+
spaceAfter=8,
|
| 1222 |
+
textColor=colors.HexColor('#1E40AF'),
|
| 1223 |
+
fontName=font_name,
|
| 1224 |
+
leading=14,
|
| 1225 |
+
backColor=colors.HexColor('#EFF6FF'),
|
| 1226 |
+
borderPadding=8
|
| 1227 |
+
)
|
| 1228 |
+
|
| 1229 |
+
fact_style = ParagraphStyle(
|
| 1230 |
+
'KeyFact',
|
| 1231 |
+
parent=styles['Normal'],
|
| 1232 |
+
fontSize=9,
|
| 1233 |
+
leftIndent=25,
|
| 1234 |
+
spaceAfter=3,
|
| 1235 |
+
textColor=colors.HexColor('#065F46'),
|
| 1236 |
+
fontName=font_name,
|
| 1237 |
+
bulletIndent=15
|
| 1238 |
+
)
|
| 1239 |
+
|
| 1240 |
+
learning_style = ParagraphStyle(
|
| 1241 |
+
'LearningPoint',
|
| 1242 |
+
parent=styles['Normal'],
|
| 1243 |
+
fontSize=10,
|
| 1244 |
+
leftIndent=15,
|
| 1245 |
+
spaceAfter=5,
|
| 1246 |
+
textColor=colors.HexColor('#7C3AED'),
|
| 1247 |
+
fontName=font_name,
|
| 1248 |
+
leading=13
|
| 1249 |
)
|
| 1250 |
|
| 1251 |
wrong_style = ParagraphStyle(
|
|
|
|
| 1254 |
fontSize=9,
|
| 1255 |
leftIndent=25,
|
| 1256 |
textColor=colors.HexColor('#DC2626'),
|
| 1257 |
+
fontName=font_name,
|
| 1258 |
+
leading=12
|
| 1259 |
)
|
| 1260 |
|
| 1261 |
normal_korean = ParagraphStyle(
|
|
|
|
| 1270 |
fontName=font_name
|
| 1271 |
)
|
| 1272 |
|
| 1273 |
+
subheading_style = ParagraphStyle(
|
| 1274 |
+
'SubHeading',
|
| 1275 |
+
parent=styles['Normal'],
|
| 1276 |
+
fontSize=10,
|
| 1277 |
+
leftIndent=15,
|
| 1278 |
+
spaceBefore=8,
|
| 1279 |
+
spaceAfter=5,
|
| 1280 |
+
textColor=colors.HexColor('#1F2937'),
|
| 1281 |
+
fontName=font_name,
|
| 1282 |
+
fontWeight='bold'
|
| 1283 |
+
)
|
| 1284 |
+
|
| 1285 |
story = []
|
| 1286 |
|
| 1287 |
story.append(Paragraph(f"{lang['pdf_answer_title']}", title_style))
|
| 1288 |
story.append(Paragraph(f"{lang['pdf_date']}: {datetime.now().strftime('%Y-%m-%d')}", normal_korean))
|
| 1289 |
story.append(Spacer(1, 20))
|
| 1290 |
|
| 1291 |
+
# Quick Answer Summary
|
| 1292 |
story.append(Paragraph(f"<b>{lang['pdf_quick_summary']}</b>", heading_korean))
|
| 1293 |
|
| 1294 |
answer_summary = []
|
|
|
|
| 1319 |
story.append(Paragraph(f"<b>{lang['pdf_detailed']}</b>", heading_korean))
|
| 1320 |
story.append(Spacer(1, 15))
|
| 1321 |
|
| 1322 |
+
# Detailed explanations for each question
|
| 1323 |
for i, q in enumerate(questions, 1):
|
| 1324 |
+
# Question text
|
| 1325 |
q_text = f"<b>Q{i}.</b> {q['question']}"
|
| 1326 |
story.append(Paragraph(q_text, question_style))
|
| 1327 |
|
| 1328 |
+
# Correct answer
|
| 1329 |
+
story.append(Paragraph(f"<b>{lang['pdf_correct_answer']}:</b> {q['answer']}", answer_style))
|
| 1330 |
|
| 1331 |
+
# Source page
|
| 1332 |
+
story.append(Paragraph(f"<b>{lang['pdf_source']}:</b> {q.get('source_page', 'N/A')}", explanation_style))
|
|
|
|
|
|
|
|
|
|
| 1333 |
|
| 1334 |
explanation = q.get('explanation', {})
|
|
|
|
|
|
|
| 1335 |
|
| 1336 |
+
# Detailed Explanation (NEW - Enhanced)
|
| 1337 |
+
detailed_expl = explanation.get('detailed_explanation', '')
|
| 1338 |
+
if detailed_expl:
|
| 1339 |
+
if language == "한국어":
|
| 1340 |
+
story.append(Paragraph("<b>📖 상세 설명:</b>", subheading_style))
|
| 1341 |
+
else:
|
| 1342 |
+
story.append(Paragraph("<b>📖 Detailed Explanation:</b>", subheading_style))
|
| 1343 |
+
story.append(Paragraph(detailed_expl, detailed_style))
|
| 1344 |
+
elif explanation.get('correct_reason'):
|
| 1345 |
+
# Fallback to correct_reason if no detailed explanation
|
| 1346 |
+
if language == "한국어":
|
| 1347 |
+
story.append(Paragraph("<b>📖 정답 근거:</b>", subheading_style))
|
| 1348 |
+
else:
|
| 1349 |
+
story.append(Paragraph("<b>📖 Why Correct:</b>", subheading_style))
|
| 1350 |
+
story.append(Paragraph(explanation['correct_reason'], detailed_style))
|
| 1351 |
+
|
| 1352 |
+
# Key Facts (NEW)
|
| 1353 |
+
key_facts = explanation.get('key_facts', [])
|
| 1354 |
+
if key_facts:
|
| 1355 |
+
if language == "한국어":
|
| 1356 |
+
story.append(Paragraph("<b>📌 핵심 팩트:</b>", subheading_style))
|
| 1357 |
+
else:
|
| 1358 |
+
story.append(Paragraph("<b>📌 Key Facts:</b>", subheading_style))
|
| 1359 |
+
for fact in key_facts[:5]: # Limit to 5 facts
|
| 1360 |
+
story.append(Paragraph(f"• {fact}", fact_style))
|
| 1361 |
+
|
| 1362 |
+
# Learning Point (NEW)
|
| 1363 |
+
learning_point = explanation.get('learning_point', '')
|
| 1364 |
+
if learning_point:
|
| 1365 |
+
if language == "한국어":
|
| 1366 |
+
story.append(Paragraph("<b>💡 학습 포인트:</b>", subheading_style))
|
| 1367 |
+
else:
|
| 1368 |
+
story.append(Paragraph("<b>💡 Learning Point:</b>", subheading_style))
|
| 1369 |
+
story.append(Paragraph(learning_point, learning_style))
|
| 1370 |
+
|
| 1371 |
+
# Related Concepts (NEW)
|
| 1372 |
+
related_concepts = explanation.get('related_concepts', [])
|
| 1373 |
+
if related_concepts:
|
| 1374 |
+
concepts_text = ", ".join(related_concepts[:5])
|
| 1375 |
+
if language == "한국어":
|
| 1376 |
+
story.append(Paragraph(f"<b>🔗 관련 개념:</b> {concepts_text}", explanation_style))
|
| 1377 |
+
else:
|
| 1378 |
+
story.append(Paragraph(f"<b>🔗 Related Concepts:</b> {concepts_text}", explanation_style))
|
| 1379 |
+
|
| 1380 |
+
# Source Quote
|
| 1381 |
+
if q.get('source_quote'):
|
| 1382 |
+
quote_text = q['source_quote'][:300] + "..." if len(q.get('source_quote', '')) > 300 else q.get('source_quote', '')
|
| 1383 |
+
if language == "한국어":
|
| 1384 |
+
story.append(Paragraph(f"<b>📝 문서 인용:</b> \"{quote_text}\"", explanation_style))
|
| 1385 |
+
else:
|
| 1386 |
+
story.append(Paragraph(f"<b>📝 Document Quote:</b> \"{quote_text}\"", explanation_style))
|
| 1387 |
+
|
| 1388 |
+
# Why Wrong Options (Enhanced)
|
| 1389 |
story.append(Spacer(1, 5))
|
| 1390 |
+
if language == "한국어":
|
| 1391 |
+
story.append(Paragraph(f"<b><i>❌ 오답 분석:</i></b>", subheading_style))
|
| 1392 |
+
else:
|
| 1393 |
+
story.append(Paragraph(f"<b><i>❌ Why Other Options Are Wrong:</i></b>", subheading_style))
|
| 1394 |
+
|
| 1395 |
+
has_wrong_explanations = False
|
| 1396 |
+
for letter in ['A', 'B', 'C', 'D', 'E']:
|
| 1397 |
+
wrong_key = f'wrong_{letter}'
|
| 1398 |
+
if explanation.get(wrong_key) and explanation[wrong_key].strip():
|
| 1399 |
+
has_wrong_explanations = True
|
| 1400 |
+
story.append(Paragraph(f"<b>{letter}:</b> {explanation[wrong_key]}", wrong_style))
|
| 1401 |
+
|
| 1402 |
+
if not has_wrong_explanations:
|
| 1403 |
+
if language == "한국어":
|
| 1404 |
+
story.append(Paragraph("(오답 설명 없음)", wrong_style))
|
| 1405 |
+
else:
|
| 1406 |
+
story.append(Paragraph("(No wrong answer explanations available)", wrong_style))
|
| 1407 |
|
| 1408 |
+
story.append(Spacer(1, 25))
|
|
|
|
|
|
|
|
|
|
| 1409 |
|
| 1410 |
+
# Add page break every 3 questions to avoid cramped layout
|
| 1411 |
+
if i % 3 == 0 and i < len(questions):
|
| 1412 |
+
story.append(PageBreak())
|
| 1413 |
|
| 1414 |
doc.build(story)
|
| 1415 |
return temp_file.name
|
|
|
|
| 1464 |
if not validated:
|
| 1465 |
return lang["error_no_questions"], "", None, None, "", []
|
| 1466 |
|
| 1467 |
+
# NEW: Enhance explanations using GROQ API
|
| 1468 |
+
progress(0.8, desc="🧠 Enhancing explanations..." if language == "English" else "🧠 설명 강화 중...")
|
| 1469 |
+
enhanced = enhance_explanations(validated, page_contents, language)
|
| 1470 |
+
|
| 1471 |
+
progress(0.9, desc=lang["phase_pdf"])
|
| 1472 |
+
exam_pdf = create_exam_pdf(enhanced, language)
|
| 1473 |
+
answer_pdf = create_answer_pdf(enhanced, language)
|
| 1474 |
|
| 1475 |
progress(0.95, desc=lang["phase_final"])
|
| 1476 |
|
| 1477 |
answer_distribution = {}
|
| 1478 |
+
for q in enhanced:
|
| 1479 |
label = q.get('correct_label', 'A')
|
| 1480 |
answer_distribution[label] = answer_distribution.get(label, 0) + 1
|
| 1481 |
|
|
|
|
| 1486 |
📁 File Type: {file_type_display}
|
| 1487 |
{'=' * 50}
|
| 1488 |
{lang['stats_title']}
|
| 1489 |
+
• {lang['total_questions']}: {len(enhanced)}
|
| 1490 |
• {lang['choices_per_q']}: {num_choices}
|
| 1491 |
• {lang['difficulty']}: {difficulty}
|
| 1492 |
• {lang['doc_pages']}: {len(page_contents)}
|
| 1493 |
• Total Characters: {total_chars:,}
|
| 1494 |
+
• 🧠 Enhanced Explanations: ✅
|
| 1495 |
{'=' * 50}
|
| 1496 |
{lang['answer_dist']}
|
| 1497 |
{dist_text}
|
|
|
|
| 1503 |
|
| 1504 |
🎯 Now go to "Take Exam" tab to start your interactive exam!"""
|
| 1505 |
|
| 1506 |
+
display_text = format_questions_display(enhanced, language)
|
| 1507 |
|
| 1508 |
progress(1.0, desc=lang["phase_complete"])
|
| 1509 |
|
| 1510 |
+
return info_log, display_text, exam_pdf, answer_pdf, "", enhanced
|
| 1511 |
|
| 1512 |
except Exception as e:
|
| 1513 |
import traceback
|
|
|
|
| 1515 |
return f"❌ Error: {str(e)}", "", None, None, "", []
|
| 1516 |
|
| 1517 |
def format_questions_display(questions: List[Dict], language: str) -> str:
|
| 1518 |
+
"""Format questions for text display with enhanced explanations"""
|
| 1519 |
|
| 1520 |
lang = LANG_TEXT[language]
|
| 1521 |
|
|
|
|
| 1535 |
else:
|
| 1536 |
lines.append(f" ○ {opt}")
|
| 1537 |
|
| 1538 |
+
explanation = q.get('explanation', {})
|
| 1539 |
+
|
| 1540 |
+
# Detailed explanation
|
| 1541 |
+
detailed = explanation.get('detailed_explanation', explanation.get('correct_reason', 'N/A'))
|
| 1542 |
+
lines.append(f"\n 📖 {detailed[:200]}{'...' if len(detailed) > 200 else ''}")
|
| 1543 |
+
|
| 1544 |
+
# Key facts
|
| 1545 |
+
key_facts = explanation.get('key_facts', [])
|
| 1546 |
+
if key_facts:
|
| 1547 |
+
lines.append(f" 📌 핵심 팩트: {', '.join(key_facts[:3])}" if language == "한국어" else f" 📌 Key Facts: {', '.join(key_facts[:3])}")
|
| 1548 |
+
|
| 1549 |
+
# Learning point
|
| 1550 |
+
learning_point = explanation.get('learning_point', '')
|
| 1551 |
+
if learning_point:
|
| 1552 |
+
lines.append(f" 💡 {learning_point[:150]}{'...' if len(learning_point) > 150 else ''}")
|
| 1553 |
+
|
| 1554 |
lines.append("-" * 60)
|
| 1555 |
|
| 1556 |
return "\n".join(lines)
|
|
|
|
| 2228 |
user_answers[current_idx] = selected_answer
|
| 2229 |
|
| 2230 |
is_correct = selected_answer == q['answer']
|
| 2231 |
+
explanation = q.get('explanation', {})
|
| 2232 |
+
|
| 2233 |
+
# Get enhanced explanation content
|
| 2234 |
+
detailed_expl = explanation.get('detailed_explanation', explanation.get('correct_reason', 'N/A'))
|
| 2235 |
+
key_facts = explanation.get('key_facts', [])
|
| 2236 |
+
learning_point = explanation.get('learning_point', '')
|
| 2237 |
+
related_concepts = explanation.get('related_concepts', [])
|
| 2238 |
|
| 2239 |
if is_correct:
|
| 2240 |
feedback_text = f"""## {lang['correct_feedback']}
|
| 2241 |
|
| 2242 |
+
**{lang['explanation_label']}:** {detailed_expl}"""
|
| 2243 |
+
|
| 2244 |
+
# Add key facts if available
|
| 2245 |
+
if key_facts:
|
| 2246 |
+
if language == "한국어":
|
| 2247 |
+
feedback_text += "\n\n**📌 핵심 팩트:**"
|
| 2248 |
+
else:
|
| 2249 |
+
feedback_text += "\n\n**📌 Key Facts:**"
|
| 2250 |
+
for fact in key_facts[:3]:
|
| 2251 |
+
feedback_text += f"\n• {fact}"
|
| 2252 |
+
|
| 2253 |
+
# Add learning point if available
|
| 2254 |
+
if learning_point:
|
| 2255 |
+
if language == "한국어":
|
| 2256 |
+
feedback_text += f"\n\n**💡 학습 포인트:** {learning_point}"
|
| 2257 |
+
else:
|
| 2258 |
+
feedback_text += f"\n\n**💡 Learning Point:** {learning_point}"
|
| 2259 |
else:
|
| 2260 |
+
# Get wrong answer explanation
|
| 2261 |
+
selected_letter = selected_answer[0] if selected_answer else 'A'
|
| 2262 |
+
wrong_reason = explanation.get(f'wrong_{selected_letter}', '')
|
| 2263 |
+
|
| 2264 |
feedback_text = f"""## {lang['incorrect_feedback']}
|
| 2265 |
|
| 2266 |
**{lang['your_answer']}:** {selected_answer}
|
| 2267 |
|
| 2268 |
**{lang['correct_answer_is']}:** {q['answer']}
|
| 2269 |
|
| 2270 |
+
**{lang['explanation_label']}:** {detailed_expl}"""
|
| 2271 |
+
|
| 2272 |
+
# Add why the selected answer was wrong
|
| 2273 |
+
if wrong_reason:
|
| 2274 |
+
if language == "한국어":
|
| 2275 |
+
feedback_text += f"\n\n**❌ 선택한 답이 틀린 이유:** {wrong_reason}"
|
| 2276 |
+
else:
|
| 2277 |
+
feedback_text += f"\n\n**❌ Why your answer was wrong:** {wrong_reason}"
|
| 2278 |
+
|
| 2279 |
+
# Add key facts if available
|
| 2280 |
+
if key_facts:
|
| 2281 |
+
if language == "한국어":
|
| 2282 |
+
feedback_text += "\n\n**📌 핵심 팩트:**"
|
| 2283 |
+
else:
|
| 2284 |
+
feedback_text += "\n\n**📌 Key Facts:**"
|
| 2285 |
+
for fact in key_facts[:3]:
|
| 2286 |
+
feedback_text += f"\n• {fact}"
|
| 2287 |
+
|
| 2288 |
+
# Add learning point if available
|
| 2289 |
+
if learning_point:
|
| 2290 |
+
if language == "한국어":
|
| 2291 |
+
feedback_text += f"\n\n**💡 학습 포인트:** {learning_point}"
|
| 2292 |
+
else:
|
| 2293 |
+
feedback_text += f"\n\n**💡 Learning Point:** {learning_point}"
|
| 2294 |
+
|
| 2295 |
+
# Add related concepts
|
| 2296 |
+
if related_concepts:
|
| 2297 |
+
concepts_text = ", ".join(related_concepts[:5])
|
| 2298 |
+
if language == "한국어":
|
| 2299 |
+
feedback_text += f"\n\n**🔗 관련 개념:** {concepts_text}"
|
| 2300 |
+
else:
|
| 2301 |
+
feedback_text += f"\n\n**🔗 Related Concepts:** {concepts_text}"
|
| 2302 |
|
| 2303 |
return user_answers, gr.update(value=feedback_text, visible=True)
|
| 2304 |
|
|
|
|
| 2365 |
for i, q in enumerate(questions):
|
| 2366 |
user_answer = user_answers.get(i, None)
|
| 2367 |
is_correct = user_answer == q['answer']
|
| 2368 |
+
explanation = q.get('explanation', {})
|
| 2369 |
|
| 2370 |
if user_answer is None:
|
| 2371 |
skipped_count += 1
|
|
|
|
| 2377 |
incorrect_count += 1
|
| 2378 |
status = f"❌ {lang['incorrect_feedback']}"
|
| 2379 |
|
| 2380 |
+
# Get enhanced explanation content
|
| 2381 |
+
detailed_expl = explanation.get('detailed_explanation', explanation.get('correct_reason', 'N/A'))
|
| 2382 |
+
key_facts = explanation.get('key_facts', [])
|
| 2383 |
+
learning_point = explanation.get('learning_point', '')
|
| 2384 |
+
|
| 2385 |
+
review_entry = f"""
|
| 2386 |
---
|
| 2387 |
+
**{lang['question_review'].format(num=i+1)}:** {q['question'][:150]}{'...' if len(q['question']) > 150 else ''}
|
| 2388 |
|
| 2389 |
- **{lang['your_answer']}:** {user_answer or lang['not_answered']}
|
| 2390 |
- **{lang['correct_answer_is']}:** {q['answer']}
|
| 2391 |
- **Status:** {status}
|
| 2392 |
+
- **{lang['explanation_label']}:** {detailed_expl[:300]}{'...' if len(detailed_expl) > 300 else ''}"""
|
| 2393 |
+
|
| 2394 |
+
# Add why user's answer was wrong (if incorrect)
|
| 2395 |
+
if user_answer and not is_correct:
|
| 2396 |
+
selected_letter = user_answer[0] if user_answer else 'A'
|
| 2397 |
+
wrong_reason = explanation.get(f'wrong_{selected_letter}', '')
|
| 2398 |
+
if wrong_reason:
|
| 2399 |
+
if language == "한국어":
|
| 2400 |
+
review_entry += f"\n- **오답 이유:** {wrong_reason[:200]}{'...' if len(wrong_reason) > 200 else ''}"
|
| 2401 |
+
else:
|
| 2402 |
+
review_entry += f"\n- **Why Wrong:** {wrong_reason[:200]}{'...' if len(wrong_reason) > 200 else ''}"
|
| 2403 |
+
|
| 2404 |
+
# Add key facts
|
| 2405 |
+
if key_facts:
|
| 2406 |
+
if language == "한국어":
|
| 2407 |
+
review_entry += f"\n- **핵심 팩트:** {', '.join(key_facts[:2])}"
|
| 2408 |
+
else:
|
| 2409 |
+
review_entry += f"\n- **Key Facts:** {', '.join(key_facts[:2])}"
|
| 2410 |
+
|
| 2411 |
+
# Add learning point
|
| 2412 |
+
if learning_point:
|
| 2413 |
+
if language == "한국어":
|
| 2414 |
+
review_entry += f"\n- **💡 학습 포인트:** {learning_point[:150]}{'...' if len(learning_point) > 150 else ''}"
|
| 2415 |
+
else:
|
| 2416 |
+
review_entry += f"\n- **💡 Learning Point:** {learning_point[:150]}{'...' if len(learning_point) > 150 else ''}"
|
| 2417 |
+
|
| 2418 |
+
review_lines.append(review_entry)
|
| 2419 |
|
| 2420 |
percentage = (correct_count / total) * 100 if total > 0 else 0
|
| 2421 |
grade = calculate_grade(percentage)
|