Update app.py
Browse files
app.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
import os
|
| 2 |
import time
|
| 3 |
import google.generativeai as genai
|
|
@@ -46,7 +47,10 @@ generation_config = {
|
|
| 46 |
|
| 47 |
# 사용할 모델 이름 설정 (변경 가능)
|
| 48 |
# 예: "gemini-1.5-flash-latest", "gemini-1.5-pro-latest" 등
|
| 49 |
-
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
try:
|
| 52 |
model = genai.GenerativeModel(
|
|
@@ -58,6 +62,7 @@ try:
|
|
| 58 |
# st.info(f"'{model_name}' 모델 로드 성공!") # 디버깅용
|
| 59 |
except Exception as e:
|
| 60 |
st.error(f"Gemini 모델 ('{model_name}') 로딩 중 오류 발생: {e}")
|
|
|
|
| 61 |
st.stop()
|
| 62 |
|
| 63 |
# --- 함수 정의 ---
|
|
@@ -79,13 +84,13 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 79 |
- 초등학생이 이해하기 쉬운 단어와 문장 사용
|
| 80 |
- 줄글 형식으로 작성, 문단 구분 명확히 (빈 줄 사용)
|
| 81 |
- 각 문단의 첫 문장 또는 마지막 문장이 중심 문장이 되도록 작성
|
| 82 |
-
- **제목:** 글의 맨 처음에 내용을 잘 나타내는 제목을 **크고 굵은 글씨**로 작성 (
|
| 83 |
|
| 84 |
## 2. 어휘 목록 작성
|
| 85 |
- **선정 기준:** 본문 내용 중 초등학교 {grade}에게 어려울 수 있는 단어, 한자어, 학습 용어
|
| 86 |
- **설명 방식:** 해당 학년 수준에 맞는 쉬운 유의어 또는 풀이 제공
|
| 87 |
- **형식:** 설명문 본문 뒤에 '### 어휘 목록' 제목 아래 글머리 기호 목록으로 작성
|
| 88 |
-
```
|
| 89 |
### 어휘 목록
|
| 90 |
* 단어1: 쉬운 설명/유의어
|
| 91 |
* 단어2: 쉬운 설명/유의어
|
|
@@ -106,7 +111,7 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 106 |
- **형식:**
|
| 107 |
- '### 독해 문제' 제목 아래 문제 번호(1., 2., ...)와 함께 문제 제시
|
| 108 |
- 모든 문제 출제 후, '### 정답' 제목 아래 문제 번호와 정답 명확히 제시
|
| 109 |
-
```
|
| 110 |
### 독해 문제
|
| 111 |
1. [문제 내용 - 단답형 또는 5지 선다형]
|
| 112 |
① 선택지1 ② 선택지2 ③ 선택지3 ④ 선택지4 ⑤ 선택지5 (선다형의 경우)
|
|
@@ -121,53 +126,67 @@ def generate_text_and_questions(grade, num_paragraphs, sentences_per_paragraph,
|
|
| 121 |
|
| 122 |
## # 출력 요구사항
|
| 123 |
- 위의 모든 지시사항(설명문, 어휘 목록, 독해 문제, 정답)을 **하나의 응답**으로 이어서 생성해주세요.
|
| 124 |
-
- 각 섹션(제목, 본문, 어휘 목록, 독해 문제, 정답) 구분을 명확히 해주세요.
|
| 125 |
- 모든 내용은 초등학교 {grade} 눈높이에 맞춰 작성해주세요.
|
| 126 |
"""
|
| 127 |
|
| 128 |
full_response_text = "" # 전체 응답 저장 변수 초기화
|
| 129 |
output_area = st.empty() # 스트리밍 출력을 위한 빈 공간 생성
|
|
|
|
| 130 |
|
| 131 |
try:
|
| 132 |
response = model.generate_content(prompt, stream=True)
|
| 133 |
for chunk in response:
|
| 134 |
-
#
|
| 135 |
-
if chunk
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)}")
|
| 152 |
-
# 오류 발생 시
|
| 153 |
return "" # 오류 시 빈 문자열 반환
|
| 154 |
|
| 155 |
-
#
|
| 156 |
-
|
| 157 |
-
|
| 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 |
-
# 마크다운 형식으로 복사
|
| 165 |
pyperclip.copy(full_response_text)
|
| 166 |
st.success("클립보드에 복사되었습니다! (마크다운 형식)")
|
| 167 |
except Exception as e:
|
| 168 |
-
|
|
|
|
| 169 |
# 복사 실패 시 수동 복사를 위해 텍스트 영역 추가 표시
|
| 170 |
-
st.text_area("복사할 내용 (마크다운):", full_response_text, height=
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
return full_response_text # 전체 생성된 텍스트 반환 (마크다운 원본)
|
| 173 |
|
|
@@ -190,7 +209,9 @@ with st.sidebar:
|
|
| 190 |
|
| 191 |
# 학년 선택 (초1 ~ 초6)
|
| 192 |
grade_options = [f"{i}학년" for i in range(1, 7)]
|
| 193 |
-
|
|
|
|
|
|
|
| 194 |
|
| 195 |
# 문단 수 설정
|
| 196 |
num_paragraphs = st.number_input("문단 수 (권장 3~5)", min_value=2, max_value=10, value=3)
|
|
@@ -234,19 +255,22 @@ if generate_button:
|
|
| 234 |
with st.spinner("AI가 열심히 글을 쓰고 문제를 만들고 있어요... 잠시만 기다려주세요! 🤔✍️"):
|
| 235 |
# 함수 호출 (출력은 함수 내부에서 스트리밍으로 처리됨)
|
| 236 |
generated_content = generate_text_and_questions(
|
| 237 |
-
grade,
|
| 238 |
num_paragraphs,
|
| 239 |
sentences_per_paragraph,
|
| 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 만 반환하게 수정 필요
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
import os
|
| 3 |
import time
|
| 4 |
import google.generativeai as genai
|
|
|
|
| 47 |
|
| 48 |
# 사용할 모델 이름 설정 (변경 가능)
|
| 49 |
# 예: "gemini-1.5-flash-latest", "gemini-1.5-pro-latest" 등
|
| 50 |
+
# gemini-2.5-flash-preview-04-17 모델 이름이 유효하지 않을 수 있습니다.
|
| 51 |
+
# 최신 flash 모델인 "gemini-1.5-flash-latest" 또는 pro 모델 "gemini-1.5-pro-latest" 사용을 권장합니다.
|
| 52 |
+
# model_name = "gemini-2.5-flash-preview-04-17" # -> 유효하지 않을 가능성 있음
|
| 53 |
+
model_name = "gemini-2.5-flash-preview-04-17" # 안정적인 최신 flash 모델로 변경
|
| 54 |
|
| 55 |
try:
|
| 56 |
model = genai.GenerativeModel(
|
|
|
|
| 62 |
# st.info(f"'{model_name}' 모델 로드 성공!") # 디버깅용
|
| 63 |
except Exception as e:
|
| 64 |
st.error(f"Gemini 모델 ('{model_name}') 로딩 중 오류 발생: {e}")
|
| 65 |
+
st.error("올바른 모델 이름인지 확인하거나 Google AI Studio에서 사용 가능한 모델 목록을 확인하세요.")
|
| 66 |
st.stop()
|
| 67 |
|
| 68 |
# --- 함수 정의 ---
|
|
|
|
| 84 |
- 초등학생이 이해하기 쉬운 단어와 문장 사용
|
| 85 |
- 줄글 형식으로 작성, 문단 구분 명확히 (빈 줄 사용)
|
| 86 |
- 각 문단의 첫 문장 또는 마지막 문장이 중심 문장이 되도록 작성
|
| 87 |
+
- **제목:** 글의 맨 처음에 내용을 잘 나타내는 제목을 **크고 굵은 글씨**로 작성 (Markdown 형식: '** 제목 **')
|
| 88 |
|
| 89 |
## 2. 어휘 목록 작성
|
| 90 |
- **선정 기준:** 본문 내용 중 초등학교 {grade}에게 어려울 수 있는 단어, 한자어, 학습 용어
|
| 91 |
- **설명 방식:** 해당 학년 수준에 맞는 쉬운 유의어 또는 풀이 제공
|
| 92 |
- **형식:** 설명문 본문 뒤에 '### 어휘 목록' 제목 아래 글머리 기호 목록으로 작성
|
| 93 |
+
```markdown
|
| 94 |
### 어휘 목록
|
| 95 |
* 단어1: 쉬운 설명/유의어
|
| 96 |
* 단어2: 쉬운 설명/유의어
|
|
|
|
| 111 |
- **형식:**
|
| 112 |
- '### 독해 문제' 제목 아래 문제 번호(1., 2., ...)와 함께 문제 제시
|
| 113 |
- 모든 문제 출제 후, '### 정답' 제목 아래 문제 번호와 정답 명확히 제시
|
| 114 |
+
```markdown
|
| 115 |
### 독해 문제
|
| 116 |
1. [문제 내용 - 단답형 또는 5지 선다형]
|
| 117 |
① 선택지1 ② 선택지2 ③ 선택지3 ④ 선택지4 ⑤ 선택지5 (선다형의 경우)
|
|
|
|
| 126 |
|
| 127 |
## # 출력 요구사항
|
| 128 |
- 위의 모든 지시사항(설명문, 어휘 목록, 독해 문제, 정답)을 **하나의 응답**으로 이어서 생성해주세요.
|
| 129 |
+
- 각 섹션(제목, 본문, 어휘 목록, 독해 문제, 정답) 구분을 Markdown 제목(##, ###)과 빈 줄로 명확히 해주세요.
|
| 130 |
- 모든 내용은 초등학교 {grade} 눈높이에 맞춰 작성해주세요.
|
| 131 |
"""
|
| 132 |
|
| 133 |
full_response_text = "" # 전체 응답 저장 변수 초기화
|
| 134 |
output_area = st.empty() # 스트리밍 출력을 위한 빈 공간 생성
|
| 135 |
+
blocked = False # 콘텐츠 차단 플래그
|
| 136 |
|
| 137 |
try:
|
| 138 |
response = model.generate_content(prompt, stream=True)
|
| 139 |
for chunk in response:
|
| 140 |
+
# 프롬프트 피드백 확인 (차단 여부)
|
| 141 |
+
if hasattr(chunk, 'prompt_feedback') and chunk.prompt_feedback.block_reason:
|
| 142 |
+
block_reason = chunk.prompt_feedback.block_reason
|
| 143 |
+
st.error(f"콘텐츠 생성 요청이 안전상의 이유로 차단되었습니다. 이유: {block_reason}")
|
| 144 |
+
output_area.error(f"⚠️ 콘텐츠 생성 중단됨 (이유: {block_reason}). 프롬프트나 주제를 수정하여 다시 시도해 보세요.")
|
| 145 |
+
blocked = True
|
| 146 |
+
break # 차단 시 스트리밍 중단
|
| 147 |
+
|
| 148 |
+
# 응답 텍스트 처리
|
| 149 |
+
try:
|
| 150 |
+
# 최신 API는 chunk.text로 바로 접근 가능할 수 있음
|
| 151 |
+
chunk_text = chunk.text
|
| 152 |
+
except AttributeError:
|
| 153 |
+
# 이전 방식 호환 (candidates 확인)
|
| 154 |
+
if chunk.candidates and chunk.candidates[0].content and chunk.candidates[0].content.parts and chunk.candidates[0].content.parts[0].text:
|
| 155 |
+
chunk_text = chunk.candidates[0].content.parts[0].text
|
| 156 |
+
else:
|
| 157 |
+
# 예상치 못한 청크 형식일 경우 건너뛰기 또는 로깅
|
| 158 |
+
# st.warning(f"Unexpected chunk format: {chunk}") # 디버깅용
|
| 159 |
+
chunk_text = "" # 또는 continue
|
| 160 |
+
|
| 161 |
+
if chunk_text:
|
| 162 |
full_response_text += chunk_text
|
| 163 |
+
# Markdown을 HTML로 변환하여 스트리밍 출력 (표, 코드 블록, 줄바꿈 지원)
|
|
|
|
| 164 |
html_text = markdown.markdown(full_response_text, extensions=['markdown.extensions.tables', 'markdown.extensions.fenced_code', 'markdown.extensions.nl2br'])
|
| 165 |
output_area.markdown(html_text, unsafe_allow_html=True)
|
| 166 |
time.sleep(0.05) # 스트리밍 지연 (조절 가능)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
except Exception as e:
|
| 169 |
st.error(f"텍스트 생성 중 에러 발생: {str(e)}")
|
| 170 |
+
# 오류 발생 시 현재까지 생성된 내용을 유지하도록 output_area 수정하지 않음
|
| 171 |
return "" # 오류 시 빈 문자열 반환
|
| 172 |
|
| 173 |
+
# 스트리밍 완료 후 처리 (차단되지 않았을 경우)
|
| 174 |
+
if not blocked and full_response_text:
|
| 175 |
+
st.markdown("---") # 구분선 추가
|
|
|
|
|
|
|
|
|
|
| 176 |
copy_button = st.button("📄 생성된 내용 전체 복사")
|
| 177 |
if copy_button:
|
| 178 |
try:
|
| 179 |
+
# 마크다운 형식으로 복사
|
| 180 |
pyperclip.copy(full_response_text)
|
| 181 |
st.success("클립보드에 복사되었습니다! (마크다운 형식)")
|
| 182 |
except Exception as e:
|
| 183 |
+
# pyperclip 에러 처리 (예: 서버 환경 등에서 클립보드 접근 불가 시)
|
| 184 |
+
st.warning(f"클립보드 자동 복사 중 오류 발생: {e}\n서버 환경에서는 클립보드 복사가 지원되지 않을 수 있습니다.\n아래 텍스트 영역에서 수동으로 복사해주세요.")
|
| 185 |
# 복사 실패 시 수동 복사를 위해 텍스트 영역 추가 표시
|
| 186 |
+
st.text_area("복사할 내용 (마크다운):", full_response_text, height=300)
|
| 187 |
+
|
| 188 |
+
elif blocked:
|
| 189 |
+
return "" # 차단 시 빈 문자열 반환
|
| 190 |
|
| 191 |
return full_response_text # 전체 생성된 텍스트 반환 (마크다운 원본)
|
| 192 |
|
|
|
|
| 209 |
|
| 210 |
# 학년 선택 (초1 ~ 초6)
|
| 211 |
grade_options = [f"{i}학년" for i in range(1, 7)]
|
| 212 |
+
# 문자열에서 숫자만 추출하도록 수정
|
| 213 |
+
grade_str = st.selectbox("대상 학년", grade_options, index=2) # 기본값 3학년
|
| 214 |
+
grade = grade_str.replace("학년", "") # '3학년' -> '3'
|
| 215 |
|
| 216 |
# 문단 수 설정
|
| 217 |
num_paragraphs = st.number_input("문단 수 (권장 3~5)", min_value=2, max_value=10, value=3)
|
|
|
|
| 255 |
with st.spinner("AI가 열심히 글을 쓰고 문제를 만들고 있어요... 잠시만 기다려주세요! 🤔✍️"):
|
| 256 |
# 함수 호출 (출력은 함수 내부에서 스트리밍으로 처리됨)
|
| 257 |
generated_content = generate_text_and_questions(
|
| 258 |
+
grade, # 숫자 학년 전달
|
| 259 |
num_paragraphs,
|
| 260 |
sentences_per_paragraph,
|
| 261 |
structure,
|
| 262 |
topic
|
| 263 |
)
|
| 264 |
+
# 생성 완료 후 별도 메시지는 함수 내에서 처리 (복사 버튼 표�� 등)
|
| 265 |
+
# 또는 스피너 완료 자체가 성공 표시가 됨
|
| 266 |
+
# 주석 처리된 부분의 pass 삭제: IndentationError 수정
|
| 267 |
# if generated_content:
|
| 268 |
+
# st.success("읽기 자료 생성이 완료되었습니다!") # 성공 메시지 필요 시 주석 해제
|
| 269 |
# elif not generated_content: # 생성된 내용이 없는 경우 (오류 또는 차단)
|
| 270 |
# 오류 메시지는 함수 내부에서 이미 표시됨
|
| 271 |
# st.error("읽기 자료 생성에 실패했습니다.")
|
| 272 |
+
# pass # <- 이 줄이 IndentationError의 원인. 삭제됨.
|
| 273 |
+
pass # 이 pass는 else 블록에 속하며, 현재는 아무 작업도 하지 않음을 나타냄 (필요시 로직 추가 가능)
|
| 274 |
|
| 275 |
# 만약 스트리밍 출력을 함수 밖에서 제어하고 싶다면,
|
| 276 |
# output_area = st.empty() 를 여기 두고, 함수는 text chunk 만 반환하게 수정 필요
|