Update app.py
Browse files
app.py
CHANGED
|
@@ -5,81 +5,214 @@ import streamlit as st
|
|
| 5 |
from streamlit_extras.colored_header import colored_header
|
| 6 |
from streamlit_extras.add_vertical_space import add_vertical_space
|
| 7 |
import markdown
|
|
|
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# 모델 설정
|
| 13 |
generation_config = {
|
| 14 |
"temperature": 0.7,
|
| 15 |
"top_p": 0.95,
|
| 16 |
"top_k": 64,
|
| 17 |
-
"max_output_tokens":
|
| 18 |
"response_mime_type": "text/plain",
|
| 19 |
}
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
prompt = f"""
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
try:
|
| 39 |
response = model.generate_content(prompt, stream=True)
|
| 40 |
for chunk in response:
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
| 46 |
|
| 47 |
except Exception as e:
|
| 48 |
-
st.error(f"에러 발생: {str(e)}")
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
#
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
|
| 58 |
-
|
|
|
|
| 59 |
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
#
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
)
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
st.
|
| 72 |
-
grade = st.selectbox("학년", [f"초등학교 {i}학년" for i in range(1, 7)]) # 드롭다운 수정
|
| 73 |
-
num_paragraphs = st.number_input("문단 수", min_value=1, value=3)
|
| 74 |
-
sentences_per_paragraph = st.number_input("문단 당 문장 수", min_value=1, value=3) # 드롭다운 수정
|
| 75 |
-
structure = st.selectbox("설명문 구조", ["정의와 예시", "비교와 대조", "분류", "분석", "인과", "순서"], index=0)
|
| 76 |
-
topic = st.text_area("✏️ 주제 및 내용을 입력하세요 ", height=200)
|
| 77 |
|
| 78 |
-
generate_button = st.button("읽기 자료 생성")
|
| 79 |
|
| 80 |
-
#
|
| 81 |
-
|
|
|
|
| 82 |
|
|
|
|
|
|
|
| 83 |
|
|
|
|
| 84 |
if generate_button:
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from streamlit_extras.colored_header import colored_header
|
| 6 |
from streamlit_extras.add_vertical_space import add_vertical_space
|
| 7 |
import markdown
|
| 8 |
+
import pyperclip # pyperclip 임포트 추가
|
| 9 |
|
| 10 |
+
# --- 설정 ---
|
| 11 |
+
|
| 12 |
+
# Google Gemini API 값 설정 (Streamlit secrets 또는 환경 변수 사용 권장)
|
| 13 |
+
# 로컬 테스트 시: os.environ["GEMINI_API_KEY"] = "YOUR_API_KEY"
|
| 14 |
+
try:
|
| 15 |
+
genai.configure(api_key=st.secrets["GEMINI_API_KEY"])
|
| 16 |
+
except Exception: # 로컬 테스트 또는 secrets 미설정 시 환경변수 확인
|
| 17 |
+
try:
|
| 18 |
+
genai.configure(api_key=os.environ["GEMINI_API_KEY"])
|
| 19 |
+
except KeyError:
|
| 20 |
+
st.error("GEMINI_API_KEY가 설정되지 않았습니다. Streamlit secrets 또는 환경 변수를 확인하세요.")
|
| 21 |
+
st.stop() # API 키 없으면 앱 중지
|
| 22 |
|
| 23 |
# 모델 설정
|
| 24 |
generation_config = {
|
| 25 |
"temperature": 0.7,
|
| 26 |
"top_p": 0.95,
|
| 27 |
"top_k": 64,
|
| 28 |
+
"max_output_tokens": 8192, # 충분한 토큰 확보 (본문 + 문제 + 어휘)
|
| 29 |
"response_mime_type": "text/plain",
|
| 30 |
}
|
| 31 |
|
| 32 |
+
# 최신 안정 모델 사용 권장 (필요시 모델명 변경)
|
| 33 |
+
model_name = "gemini-2.5-flash-preview-04-17"
|
| 34 |
+
|
| 35 |
+
try:
|
| 36 |
+
model = genai.GenerativeModel(
|
| 37 |
+
model_name=model_name,
|
| 38 |
+
generation_config=generation_config,
|
| 39 |
+
# safety_settings 설정 가능 (필요시)
|
| 40 |
+
)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
st.error(f"Gemini 모델 로딩 중 오류 발생: {e}")
|
| 43 |
+
st.stop()
|
| 44 |
+
|
| 45 |
+
# --- 함수 정의 ---
|
| 46 |
+
|
| 47 |
+
def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph, structure, topic):
|
| 48 |
+
"""
|
| 49 |
+
주어진 조건에 따라 설명문 텍스트, 어휘 목록, 독해 문제를 생성하고 스트리밍으로 출력합니다.
|
| 50 |
+
"""
|
| 51 |
+
# 상세화된 프롬프트: 설명문 작성 + 어휘 목록 + 독해 문제 출제 요청 추가
|
| 52 |
prompt = f"""
|
| 53 |
+
# 지시사항
|
| 54 |
+
|
| 55 |
+
## 1. 설명문 작성
|
| 56 |
+
- **대상 독자:** 초등학교 {grade}학년 학생
|
| 57 |
+
- **주제:** '{topic}'
|
| 58 |
+
- **글의 구조:** '{structure}' 구조를 명확히 따를 것
|
| 59 |
+
- **분량:** 전체 {num_paragraphs}개 문단 내외, 문단 당 {sentences_per_paragraph}개 문장 내외
|
| 60 |
+
- **스타일:**
|
| 61 |
+
- 초등학생이 이해하기 쉬운 단어와 문장 사용
|
| 62 |
+
- 줄글 형식으로 작성, 문단 구분 명확히 (빈 줄 사용)
|
| 63 |
+
- 각 문단의 첫 문장 또는 마지막 문장이 중심 문장이 되도록 작성
|
| 64 |
+
- **제목:** 글의 맨 처음에 내용을 잘 나타내는 제목을 **크고 굵은 글씨**로 작성 (예: '**제목**')
|
| 65 |
+
|
| 66 |
+
## 2. 어휘 목록 작성
|
| 67 |
+
- **선정 기준:** 본문 내용 중 초등학교 {grade}학년에게 어려울 수 있는 단어, 한자어, 학습 용어
|
| 68 |
+
- **설명 방식:** 해당 학년 수준에 맞는 쉬운 유의어 또는 풀이 제공
|
| 69 |
+
- **형식:** 설명문 본문 뒤에 '### 어휘 목록' 제목 아래 글머리 기호 목록으로 작성
|
| 70 |
+
```
|
| 71 |
+
### 어휘 목록
|
| 72 |
+
* 단어1: 쉬운 설명/유의어
|
| 73 |
+
* 단어2: 쉬운 설명/유의어
|
| 74 |
+
...
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
## 3. 독해 문제 및 정답 출제
|
| 78 |
+
- **출제 기반:** **반드시 위에서 생성된 설명문 내용만을 바탕으로** 출제
|
| 79 |
+
- **문제 수:** 총 7~8개
|
| 80 |
+
- **문제 유형 (균형 있게 포함):**
|
| 81 |
+
- **사실적 이해:** 글에 명시된 정보 확인 (누가, 무엇을, 언제, 어디서 등)
|
| 82 |
+
- **추론적 이해:** 글에 명시되지 않았지만 내용을 바탕으로 논리적으로 판단 가능한 내용 (왜, 그래서 어떻게 될까 등)
|
| 83 |
+
- **어휘:** 본문 또는 어휘 목록에 나온 단어의 문맥상 의미 파악
|
| 84 |
+
- **글의 구조:** 글 전체의 짜임, 문단 간 관계, 사용된 설명 방식 등 파악
|
| 85 |
+
- **문제 형식 (혼��� 사용):**
|
| 86 |
+
- **단답형:** 간단한 단어, 구, 짧은 문장으로 답하는 문제
|
| 87 |
+
- **5지 선다형:** 5개의 선택지(①, ②, ③, ④, ⑤) 중 정답을 고르는 문제 (오답 선택지도 그럴듯하게 구성)
|
| 88 |
+
- **형식:**
|
| 89 |
+
- '### 독해 문제' 제목 아래 문제 번호(1., 2., ...)와 함께 문제 제시
|
| 90 |
+
- 모든 문제 출제 후, '### 정답' 제목 아래 문제 번호와 정답 명확히 제시
|
| 91 |
+
```
|
| 92 |
+
### 독해 문제
|
| 93 |
+
1. [문제 내용 - 단답형 또는 5지 선다형]
|
| 94 |
+
① 선택지1 ② 선택지2 ③ 선택지3 ④ 선택지4 ⑤ 선택지5 (선다형의 경우)
|
| 95 |
+
2. [문제 내용 - 단답형 또는 5지 선다형]
|
| 96 |
+
... (총 7~8개)
|
| 97 |
+
|
| 98 |
+
### 정답
|
| 99 |
+
1. [정답 내용 또는 번호]
|
| 100 |
+
2. [정답 내용 또는 번호]
|
| 101 |
+
...
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## # 출력 요구사항
|
| 105 |
+
- 위의 모든 지시사항(설명문, 어휘 목록, 독해 문제, 정답)을 **하나의 응답**으로 이어서 생성해주세요.
|
| 106 |
+
- 각 섹션(제목, 본문, 어휘 목록, 독해 문제, 정답) 구분을 명확히 해주세요.
|
| 107 |
+
- 모든 내용은 초등학교 {grade}학년 눈높이에 맞춰 작성해주세요.
|
| 108 |
"""
|
| 109 |
|
| 110 |
+
full_response_text = "" # 전체 응답 저장 변수 초기화
|
| 111 |
+
output_area = st.empty() # 스트리밍 출력을 위한 빈 공간 생성
|
| 112 |
+
|
| 113 |
try:
|
| 114 |
response = model.generate_content(prompt, stream=True)
|
| 115 |
for chunk in response:
|
| 116 |
+
if chunk.text: # 빈 청크가 아닌 경우에만 처리
|
| 117 |
+
full_response_text += chunk.text
|
| 118 |
+
# Markdown을 HTML로 변환하여 스트리밍 출력 (표 확장 기능 포함)
|
| 119 |
+
html_text = markdown.markdown(full_response_text, extensions=['markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.nl2br'])
|
| 120 |
+
# nl2br 확장 기능으로 마크다운 줄바꿈(\n)을 <br>로 변환
|
| 121 |
+
output_area.markdown(html_text, unsafe_allow_html=True)
|
| 122 |
+
time.sleep(0.05) # 스트리밍 지연 (조절 가능)
|
| 123 |
|
| 124 |
except Exception as e:
|
| 125 |
+
st.error(f"텍스트 생성 중 에러 발생: {str(e)}")
|
| 126 |
+
# 오류 발생 시 이전 내용을 유지하기 위해 output_area를 그대로 둠
|
| 127 |
+
return "" # 오류 시 빈 문자열 반환
|
| 128 |
+
|
| 129 |
+
# 생성 완료 후 복사 버튼 표시
|
| 130 |
+
st.markdown("---") # 구분선 추가
|
| 131 |
+
if full_response_text: # 생성된 내용이 있을 때만 복사 버튼 표시
|
| 132 |
+
copy_button = st.button("📄 생성된 내용 전체 복사")
|
| 133 |
+
if copy_button:
|
| 134 |
+
try:
|
| 135 |
+
pyperclip.copy(full_response_text)
|
| 136 |
+
st.success("클립보드에 복사되었습니다!")
|
| 137 |
+
except Exception as e:
|
| 138 |
+
st.warning(f"클립보드 복사 중 오류 발생: {e}\n수동으로 복사해주세요.")
|
| 139 |
+
# 복사 실패 시 수동 복사를 위해 텍스트 영역 추가 표시
|
| 140 |
+
st.text_area("복사할 내용:", full_response_text, height=200)
|
| 141 |
+
|
| 142 |
+
return full_response_text # 전체 생성된 텍스트 반환
|
| 143 |
+
|
| 144 |
+
# --- Streamlit 인터페이스 설정 ---
|
| 145 |
+
|
| 146 |
+
# 헤더 설정
|
| 147 |
+
colored_header(
|
| 148 |
+
label="📜 초등학생 맞춤 읽기 자료 생성기+",
|
| 149 |
+
description="주제와 조건을 입력하면 설명문, 어휘 학습, 독해 문제까지 한번에 만들어줍니다.",
|
| 150 |
+
color_name="blue-70", # 색상 변경
|
| 151 |
+
)
|
| 152 |
+
add_vertical_space(1)
|
| 153 |
|
| 154 |
+
# 사이드바: 옵션 설정
|
| 155 |
+
with st.sidebar:
|
| 156 |
+
st.header("⚙️ 생성 옵션 설정")
|
| 157 |
+
add_vertical_space(1)
|
| 158 |
|
| 159 |
+
# 학년 선택 (초1 ~ 초6)
|
| 160 |
+
grade_options = [f"{i}학년" for i in range(1, 7)]
|
| 161 |
+
# grade_full = st.selectbox("대상 학년", [f"초등학교 {i}학년" for i in range(1, 7)])
|
| 162 |
+
# grade_number = grade_full.split(" ")[1][0] # 숫자만 추출 ('1', '2', ... '6') -> 모델에게 전달할 때는 'N학년' 형태가 더 자연스러울 수 있음
|
| 163 |
+
grade = st.selectbox("대상 학년", grade_options, index=2) # 기본값 3학년
|
| 164 |
|
| 165 |
+
# 문단 수 설정
|
| 166 |
+
num_paragraphs = st.number_input("문단 수 (권장 3~5)", min_value=2, max_value=10, value=3)
|
| 167 |
|
| 168 |
+
# 문단 당 문장 수 설정
|
| 169 |
+
sentences_per_paragraph = st.number_input("문단 당 문장 수 (권장 3~5)", min_value=2, max_value=10, value=4)
|
| 170 |
|
| 171 |
+
# 설명문 구조 선택
|
| 172 |
+
structure = st.selectbox(
|
| 173 |
+
"설명 방식",
|
| 174 |
+
["정의와 예시", "비교와 대조", "분류와 구분", "분석 (구성 요소)", "인과 (원인과 결과)", "서사 (시간 순서 또는 과정)"],
|
| 175 |
+
index=0 # 기본값 '정의와 예시'
|
| 176 |
+
)
|
| 177 |
|
| 178 |
+
# 주제 입력
|
| 179 |
+
topic = st.text_area(
|
| 180 |
+
"✏️ 글의 주제를 입력하세요",
|
| 181 |
+
height=150,
|
| 182 |
+
placeholder="예) 지구 온난화, 스마트폰의 장단점, 우리나라의 사계절, 재활용의 중요성"
|
| 183 |
+
)
|
| 184 |
|
| 185 |
+
add_vertical_space(2)
|
| 186 |
+
st.caption("ℹ️ 주제가 명확하고 구체적일수록 좋은 결과가 나옵니다.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
|
|
|
| 188 |
|
| 189 |
+
# 메인 화면: 생성 버튼 및 출력 영역
|
| 190 |
+
st.subheader("✨ 읽기 자료 생성 결과")
|
| 191 |
+
add_vertical_space(1)
|
| 192 |
|
| 193 |
+
# 생성 버튼
|
| 194 |
+
generate_button = st.button("🚀 읽기 자료 생성하기", type="primary", use_container_width=True)
|
| 195 |
|
| 196 |
+
# 생성 버튼 클릭 시 로직 실행
|
| 197 |
if generate_button:
|
| 198 |
+
if not topic:
|
| 199 |
+
st.warning("⚠️ 주제를 입력해주세요!")
|
| 200 |
+
elif not genai.api_key:
|
| 201 |
+
st.error("API 키가 설정되지 않았습니다. 사이드바 안내를 확인하세요.")
|
| 202 |
+
else:
|
| 203 |
+
with st.spinner("AI가 열심히 글을 쓰고 문제를 만들고 있어요... 잠시만 기다려주세요! 🤔✍️"):
|
| 204 |
+
# 함수 호출 (출력은 함수 내부에서 스트리밍으로 처리됨)
|
| 205 |
+
generated_content = generate_text_and_questions(
|
| 206 |
+
grade,
|
| 207 |
+
num_paragraphs,
|
| 208 |
+
sentences_per_paragraph,
|
| 209 |
+
structure,
|
| 210 |
+
topic
|
| 211 |
+
)
|
| 212 |
+
# 생성 완료 후 별도 메시지 (필요시)
|
| 213 |
+
# if generated_content:
|
| 214 |
+
# st.success("읽기 자료 생성이 완료되었습니다!")
|
| 215 |
+
|
| 216 |
+
# 만약 스트리밍 출력을 함수 밖에서 제어하고 싶다면,
|
| 217 |
+
# output_area = st.empty() 를 여기 두고, 함수는 text chunk 만 반환하게 수정 필요
|
| 218 |
+
# 하지만 현재 구조(함수 내에서 스트리밍 처리 및 버튼 표시)가 더 간결함.
|