Update app.py
Browse files
app.py
CHANGED
|
@@ -11,15 +11,30 @@ import pyperclip # pyperclip 임포트 추가
|
|
| 11 |
|
| 12 |
# Google Gemini API 값 설정 (Streamlit secrets 또는 환경 변수 사용 권장)
|
| 13 |
# 로컬 테스트 시: os.environ["GEMINI_API_KEY"] = "YOUR_API_KEY"
|
|
|
|
| 14 |
try:
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
try:
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
| 19 |
except KeyError:
|
| 20 |
-
|
|
|
|
| 21 |
st.stop() # API 키 없으면 앱 중지
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
# 모델 설정
|
| 24 |
generation_config = {
|
| 25 |
"temperature": 0.7,
|
|
@@ -29,17 +44,20 @@ generation_config = {
|
|
| 29 |
"response_mime_type": "text/plain",
|
| 30 |
}
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
|
|
|
|
| 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 |
# --- 함수 정의 ---
|
|
@@ -53,7 +71,7 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 53 |
# 지시사항
|
| 54 |
|
| 55 |
## 1. 설명문 작성
|
| 56 |
-
- **대상 독자:** 초등학교 {grade}
|
| 57 |
- **주제:** '{topic}'
|
| 58 |
- **글의 구조:** '{structure}' 구조를 명확히 따를 것
|
| 59 |
- **분량:** 전체 {num_paragraphs}개 문단 내외, 문단 당 {sentences_per_paragraph}개 문장 내외
|
|
@@ -64,7 +82,7 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 64 |
- **제목:** 글의 맨 처음에 내용을 잘 나타내는 제목을 **크고 굵은 글씨**로 작성 (예: '**제목**')
|
| 65 |
|
| 66 |
## 2. 어휘 목록 작성
|
| 67 |
-
- **선정 기준:** 본문 내용 중 초등학교 {grade}
|
| 68 |
- **설명 방식:** 해당 학년 수준에 맞는 쉬운 유의어 또는 풀이 제공
|
| 69 |
- **형식:** 설명문 본문 뒤에 '### 어휘 목록' 제목 아래 글머리 기호 목록으로 작성
|
| 70 |
```
|
|
@@ -104,7 +122,7 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 104 |
## # 출력 요구사항
|
| 105 |
- 위의 모든 지시사항(설명문, 어휘 목록, 독해 문제, 정답)을 **하나의 응답**으로 이어서 생성해주세요.
|
| 106 |
- 각 섹션(제목, 본문, 어휘 목록, 독해 문제, 정답) 구분을 명확히 해주세요.
|
| 107 |
-
- 모든 내용은 초등학교 {grade}
|
| 108 |
"""
|
| 109 |
|
| 110 |
full_response_text = "" # 전체 응답 저장 변수 초기화
|
|
@@ -113,13 +131,21 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 113 |
try:
|
| 114 |
response = model.generate_content(prompt, stream=True)
|
| 115 |
for chunk in response:
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
| 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)}")
|
|
@@ -129,20 +155,26 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 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("복사할
|
| 141 |
|
| 142 |
-
return full_response_text # 전체 생성된 텍스트 반환
|
| 143 |
|
| 144 |
# --- Streamlit 인터페이스 설정 ---
|
| 145 |
|
|
|
|
|
|
|
| 146 |
# 헤더 설정
|
| 147 |
colored_header(
|
| 148 |
label="📜 초등학생 맞춤 읽기 자료 생성기+",
|
|
@@ -158,8 +190,6 @@ with st.sidebar:
|
|
| 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 |
# 문단 수 설정
|
|
@@ -179,11 +209,12 @@ with st.sidebar:
|
|
| 179 |
topic = st.text_area(
|
| 180 |
"✏️ 글의 주제를 입력하세요",
|
| 181 |
height=150,
|
| 182 |
-
placeholder="예) 지구 온난화, 스마트폰의 장단점, 우리나라의 사계절, 재활용의 중요성"
|
| 183 |
)
|
| 184 |
|
| 185 |
add_vertical_space(2)
|
| 186 |
st.caption("ℹ️ 주제가 명확하고 구체적일수록 좋은 결과가 나옵니다.")
|
|
|
|
| 187 |
|
| 188 |
|
| 189 |
# 메인 화면: 생성 버튼 및 출력 영역
|
|
@@ -197,9 +228,9 @@ generate_button = st.button("🚀 읽기 자료 생성하기", type="primary", u
|
|
| 197 |
if generate_button:
|
| 198 |
if not topic:
|
| 199 |
st.warning("⚠️ 주제를 입력해주세요!")
|
| 200 |
-
|
| 201 |
-
st.error("API 키가 설정되지 않았습니다. 사이드바 안내를 확인하세요.")
|
| 202 |
else:
|
|
|
|
| 203 |
with st.spinner("AI가 열심히 글을 쓰고 문제를 만들고 있어요... 잠시만 기다려주세요! 🤔✍️"):
|
| 204 |
# 함수 호출 (출력은 함수 내부에서 스트리밍으로 처리됨)
|
| 205 |
generated_content = generate_text_and_questions(
|
|
@@ -209,9 +240,13 @@ if generate_button:
|
|
| 209 |
structure,
|
| 210 |
topic
|
| 211 |
)
|
| 212 |
-
# 생성 완료 후 별도 메시지 (
|
| 213 |
# if generated_content:
|
| 214 |
# st.success("읽기 자료 생성이 완료되었습니다!")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
|
| 216 |
# 만약 스트리밍 출력을 함수 밖에서 제어하고 싶다면,
|
| 217 |
# output_area = st.empty() 를 여기 두고, 함수는 text chunk 만 반환하게 수정 필요
|
|
|
|
| 11 |
|
| 12 |
# Google Gemini API 값 설정 (Streamlit secrets 또는 환경 변수 사용 권장)
|
| 13 |
# 로컬 테스트 시: os.environ["GEMINI_API_KEY"] = "YOUR_API_KEY"
|
| 14 |
+
api_key_configured = False
|
| 15 |
try:
|
| 16 |
+
# secrets에서 먼저 시도
|
| 17 |
+
gemini_api_key = st.secrets["GEMINI_API_KEY"]
|
| 18 |
+
genai.configure(api_key=gemini_api_key)
|
| 19 |
+
api_key_configured = True
|
| 20 |
+
# st.success("Secrets에서 Gemini API 키 로드 성공!") # 디버깅용
|
| 21 |
+
except (KeyError, FileNotFoundError):
|
| 22 |
+
# secrets에 없으면 환경 변수에서 시도
|
| 23 |
try:
|
| 24 |
+
gemini_api_key = os.environ["GEMINI_API_KEY"]
|
| 25 |
+
genai.configure(api_key=gemini_api_key)
|
| 26 |
+
api_key_configured = True
|
| 27 |
+
# st.info("환경 변수에서 Gemini API 키 로드 성공!") # 디버깅용
|
| 28 |
except KeyError:
|
| 29 |
+
# 둘 다 없으면 오류 메시지 표시
|
| 30 |
+
st.error("⚠️ Gemini API 키가 설정되지 않았습니다. Streamlit secrets 또는 환경 변수에 'GEMINI_API_KEY'를 설정해주세요.")
|
| 31 |
st.stop() # API 키 없으면 앱 중지
|
| 32 |
|
| 33 |
+
# API 키 설정 확인 (만약을 위한 추가 검사)
|
| 34 |
+
if not api_key_configured:
|
| 35 |
+
st.error("API 키 설정에 실패했습니다. 애플리케이션을 중지합니다.")
|
| 36 |
+
st.stop()
|
| 37 |
+
|
| 38 |
# 모델 설정
|
| 39 |
generation_config = {
|
| 40 |
"temperature": 0.7,
|
|
|
|
| 44 |
"response_mime_type": "text/plain",
|
| 45 |
}
|
| 46 |
|
| 47 |
+
# 사용할 모델 이름 설정 (변경 가능)
|
| 48 |
+
# 예: "gemini-1.5-flash-latest", "gemini-1.5-pro-latest" 등
|
| 49 |
+
model_name = "gemini-2.5-flash-preview-04-17" # 최신 flash 모델 사용 권장
|
| 50 |
|
| 51 |
try:
|
| 52 |
model = genai.GenerativeModel(
|
| 53 |
model_name=model_name,
|
| 54 |
generation_config=generation_config,
|
| 55 |
+
# safety_settings = Adjust safety settings
|
| 56 |
+
# See https://ai.google.dev/gemini-api/docs/safety-settings
|
| 57 |
)
|
| 58 |
+
# st.info(f"'{model_name}' 모델 로드 성공!") # 디버깅용
|
| 59 |
except Exception as e:
|
| 60 |
+
st.error(f"Gemini 모델 ('{model_name}') 로딩 중 오류 발생: {e}")
|
| 61 |
st.stop()
|
| 62 |
|
| 63 |
# --- 함수 정의 ---
|
|
|
|
| 71 |
# 지시사항
|
| 72 |
|
| 73 |
## 1. 설명문 작성
|
| 74 |
+
- **대상 독자:** 초등학교 {grade} 학생
|
| 75 |
- **주제:** '{topic}'
|
| 76 |
- **글의 구조:** '{structure}' 구조를 명확히 따를 것
|
| 77 |
- **분량:** 전체 {num_paragraphs}개 문단 내외, 문단 당 {sentences_per_paragraph}개 문장 내외
|
|
|
|
| 82 |
- **제목:** 글의 맨 처음에 내용을 잘 나타내는 제목을 **크고 굵은 글씨**로 작성 (예: '**제목**')
|
| 83 |
|
| 84 |
## 2. 어휘 목록 작성
|
| 85 |
+
- **선정 기준:** 본문 내용 중 초등학교 {grade}에게 어려울 수 있는 단어, 한자어, 학습 용어
|
| 86 |
- **설명 방식:** 해당 학년 수준에 맞는 쉬운 유의어 또는 풀이 제공
|
| 87 |
- **형식:** 설명문 본문 뒤에 '### 어휘 목록' 제목 아래 글머리 기호 목록으로 작성
|
| 88 |
```
|
|
|
|
| 122 |
## # 출력 요구사항
|
| 123 |
- 위의 모든 지시사항(설명문, 어휘 목록, 독해 문제, 정답)을 **하나의 응답**으로 이어서 생성해주세요.
|
| 124 |
- 각 섹션(제목, 본문, 어휘 목록, 독해 문제, 정답) 구분을 명확히 해주세요.
|
| 125 |
+
- 모든 내용은 초등학교 {grade} 눈높이에 맞춰 작성해주세요.
|
| 126 |
"""
|
| 127 |
|
| 128 |
full_response_text = "" # 전체 응답 저장 변수 초기화
|
|
|
|
| 131 |
try:
|
| 132 |
response = model.generate_content(prompt, stream=True)
|
| 133 |
for chunk in response:
|
| 134 |
+
# 응답 후보(candidates)가 있고, 그 안에 내용(content)이 있고, 그 안에 부분(parts)이 있고, 그 안에 텍스트(text)가 있는지 확인
|
| 135 |
+
if chunk.candidates and chunk.candidates[0].content and chunk.candidates[0].content.parts and chunk.candidates[0].content.parts[0].text:
|
| 136 |
+
chunk_text = chunk.candidates[0].content.parts[0].text
|
| 137 |
+
full_response_text += chunk_text
|
| 138 |
# Markdown을 HTML로 변환하여 스트리밍 출력 (표 확장 기능 포함)
|
|
|
|
| 139 |
# nl2br 확장 기능으로 마크다운 줄바꿈(\n)을 <br>로 변환
|
| 140 |
+
html_text = markdown.markdown(full_response_text, extensions=['markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.nl2br'])
|
| 141 |
output_area.markdown(html_text, unsafe_allow_html=True)
|
| 142 |
time.sleep(0.05) # 스트리밍 지연 (조절 가능)
|
| 143 |
+
elif hasattr(chunk, 'prompt_feedback') and chunk.prompt_feedback.block_reason:
|
| 144 |
+
# 콘텐츠 차단 발생 시 사용자에게 알림
|
| 145 |
+
block_reason = chunk.prompt_feedback.block_reason
|
| 146 |
+
st.error(f"콘텐츠 생성 중 안전상의 이유로 차단되었습니다. 이유: {block_reason}")
|
| 147 |
+
output_area.error(f"⚠️ 콘텐츠 생성 중단됨 (이유: {block_reason}). 프롬프트나 주제를 수정하여 다시 시도해 보세요.")
|
| 148 |
+
return "" # 차단 시 빈 문자열 반환
|
| 149 |
|
| 150 |
except Exception as e:
|
| 151 |
st.error(f"텍스트 생성 중 에러 발생: {str(e)}")
|
|
|
|
| 155 |
# 생성 완료 후 복사 버튼 표시
|
| 156 |
st.markdown("---") # 구분선 추가
|
| 157 |
if full_response_text: # 생성된 내용이 있을 때만 복사 버튼 표시
|
| 158 |
+
# full_response_text 를 직접 사용 (html_text는 렌더링용)
|
| 159 |
+
# st.code(full_response_text, language='markdown') # 디버깅용: 원본 마크다운 확인
|
| 160 |
+
|
| 161 |
copy_button = st.button("📄 생성된 내용 전체 복사")
|
| 162 |
if copy_button:
|
| 163 |
try:
|
| 164 |
+
# 마크다운 형식으로 복사 (HTML 태그 제외)
|
| 165 |
pyperclip.copy(full_response_text)
|
| 166 |
+
st.success("클립보드에 복사되었습니다! (마크다운 형식)")
|
| 167 |
except Exception as e:
|
| 168 |
+
st.warning(f"클립보드 자동 복사 중 오류 발생: {e}\n아래 텍스트 영역에서 수동으로 복사해주세요.")
|
| 169 |
# 복사 실패 시 수동 복사를 위해 텍스트 영역 추가 표시
|
| 170 |
+
st.text_area("복사할 내용 (마크다운):", full_response_text, height=200)
|
| 171 |
|
| 172 |
+
return full_response_text # 전체 생성된 텍스트 반환 (마크다운 원본)
|
| 173 |
|
| 174 |
# --- Streamlit 인터페이스 설정 ---
|
| 175 |
|
| 176 |
+
st.set_page_config(layout="wide") # 넓은 레이아웃 사용
|
| 177 |
+
|
| 178 |
# 헤더 설정
|
| 179 |
colored_header(
|
| 180 |
label="📜 초등학생 맞춤 읽기 자료 생성기+",
|
|
|
|
| 190 |
|
| 191 |
# 학년 선택 (초1 ~ 초6)
|
| 192 |
grade_options = [f"{i}학년" for i in range(1, 7)]
|
|
|
|
|
|
|
| 193 |
grade = st.selectbox("대상 학년", grade_options, index=2) # 기본값 3학년
|
| 194 |
|
| 195 |
# 문단 수 설정
|
|
|
|
| 209 |
topic = st.text_area(
|
| 210 |
"✏️ 글의 주제를 입력하세요",
|
| 211 |
height=150,
|
| 212 |
+
placeholder="예) 지구 온난화, 스마트폰의 장단점, 우리나라의 사계절, 재활용의 중요성, 좋아하는 동물 설명하기, 독도의 중요성"
|
| 213 |
)
|
| 214 |
|
| 215 |
add_vertical_space(2)
|
| 216 |
st.caption("ℹ️ 주제가 명확하고 구체적일수록 좋은 결과가 나옵니다.")
|
| 217 |
+
st.caption(f"현재 사용 모델: {model_name}") # 사용 중인 모델 표시
|
| 218 |
|
| 219 |
|
| 220 |
# 메인 화면: 생성 버튼 및 출력 영역
|
|
|
|
| 228 |
if generate_button:
|
| 229 |
if not topic:
|
| 230 |
st.warning("⚠️ 주제를 입력해주세요!")
|
| 231 |
+
# API 키 존재 여부는 시작 시점에 이미 확인 및 처리되었으므로 여기서 다시 확인할 필요 없음
|
|
|
|
| 232 |
else:
|
| 233 |
+
# 주제 입력 확인 후 진행
|
| 234 |
with st.spinner("AI가 열심히 글을 쓰고 문제를 만들고 있어요... 잠시만 기다려주세요! 🤔✍️"):
|
| 235 |
# 함수 호출 (출력은 함수 내부에서 스트리밍으로 처리됨)
|
| 236 |
generated_content = generate_text_and_questions(
|
|
|
|
| 240 |
structure,
|
| 241 |
topic
|
| 242 |
)
|
| 243 |
+
# 생성 완료 후 별도 메시지 (필요시, 함수 내 복사 버튼 표시로 대체 가능)
|
| 244 |
# if generated_content:
|
| 245 |
# st.success("읽기 자료 생성이 완료되었습니다!")
|
| 246 |
+
# elif not generated_content: # 생성된 내용이 없는 경우 (오류 또는 차단)
|
| 247 |
+
# 오류 메시지는 함수 내부에서 이미 표시됨
|
| 248 |
+
# st.error("읽기 자료 생성에 실패했습니다.")
|
| 249 |
+
pass # 함수 내에서 이미 에러 또는 차단 메시지 표시
|
| 250 |
|
| 251 |
# 만약 스트리밍 출력을 함수 밖에서 제어하고 싶다면,
|
| 252 |
# output_area = st.empty() 를 여기 두고, 함수는 text chunk 만 반환하게 수정 필요
|