ginipick commited on
Commit
b082ee3
Β·
verified Β·
1 Parent(s): 5eb15fa

Update app-BACKUP2.py

Browse files
Files changed (1) hide show
  1. app-BACKUP2.py +378 -398
app-BACKUP2.py CHANGED
@@ -1,452 +1,432 @@
1
- import os
 
 
 
2
  import streamlit as st
3
- import json
4
  import anthropic
5
- import requests
6
- import logging
7
  from gradio_client import Client
8
- import markdown
9
- import tempfile
10
- import base64
11
- from weasyprint import HTML
12
 
13
- # λ‘œκΉ… μ„€μ •
14
- logging.basicConfig(
15
- level=logging.INFO,
16
- format='%(asctime)s - %(levelname)s - %(message)s')
 
 
17
 
18
- # API μ„€μ •
19
- api_key = os.environ.get("API_KEY")
20
- client = anthropic.Anthropic(api_key=api_key)
 
 
 
 
 
21
 
22
- # 이미지 생성 API URL
23
- IMAGE_API_URL = "http://211.233.58.201:7896"
 
 
 
 
24
 
25
- # μ΅œλŒ€ 토큰 수 μ„€μ • (Claude-3 Sonnet의 μ΅œλŒ€ 토큰 수)
26
- MAX_TOKENS = 7999
 
27
 
28
- def get_system_prompt():
29
- return """
30
- 당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
31
 
32
- λ…μž μ—°κ²° 단계 1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사 1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문 1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„
 
 
 
33
 
34
- 문제 μ •μ˜ 단계 2.1. λ…μžμ˜ 페인포인트 ꡬ체화 2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석 2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
 
 
 
35
 
36
- μ „λ¬Έμ„± μž…μ¦ 단계 3.1. 객관적 데이터 기반 뢄석 3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용 3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화
 
 
 
37
 
38
- μ†”λ£¨μ…˜ 제곡 단계 4.1. 단계별 μ‹€μ²œ κ°€μ΄λ“œλΌμΈ μ œμ‹œ 4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁 4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함
 
 
 
39
 
40
- 신뒰도 κ°•ν™” 단계 5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ 5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용 5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦
 
 
 
41
 
42
- 행동 μœ λ„ 단계 6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ 6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ 6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함
 
 
 
43
 
44
- μ§„μ •μ„± κ°•ν™” 단계 7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개 7.2. κ°œμΈλ³„ 차이 쑴재 인정 7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ
 
 
 
45
 
46
- 관계 지속 단계 8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사 8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„± 8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
 
 
 
47
 
48
- μž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­ 9.1. κΈ€μž 수: 1500-2000자 λ‚΄μ™Έ 9.2. 문단 길이: 3-4λ¬Έμž₯ 이내 9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš© 9.4. ν†€μ•€λ§€λ„ˆ: μΉœκ·Όν•˜κ³  전문적인 λŒ€ν™”μ²΄ 9.5. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ 9.6. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©
 
 
 
 
49
 
50
- μ΄λŸ¬ν•œ ν”„λ ˆμž„μ›Œν¬λ₯Ό λ°”νƒ•μœΌλ‘œ, μš”μ²­λ°›μ€ μ£Όμ œμ— λŒ€ν•΄ 체계적이고 λ§€λ ₯적인 λΈ”λ‘œκ·Έ 포슀트λ₯Ό μž‘μ„±ν•˜κ² μŠ΅λ‹ˆλ‹€.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- def test_image_api_connection():
54
- """이미지 API μ„œλ²„ μ—°κ²° ν…ŒμŠ€νŠΈ"""
55
- try:
56
- client = Client(IMAGE_API_URL)
57
- return "이미지 API μ—°κ²° 성곡: 정상 μž‘λ™ 쀑"
58
- except Exception as e:
59
- logging.error(f"이미지 API μ—°κ²° ν…ŒμŠ€νŠΈ μ‹€νŒ¨: {e}")
60
- return f"이미지 API μ—°κ²° μ‹€νŒ¨: {e}"
61
 
62
- def generate_image(prompt, width=768, height=768, guidance=3.5, inference_steps=30, seed=3):
63
- """이미지 생성 ν•¨μˆ˜"""
64
- if not prompt:
65
- return None, "였λ₯˜: ν”„λ‘¬ν”„νŠΈλ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”"
 
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  try:
68
- client = Client(IMAGE_API_URL)
69
- result = client.predict(
70
- prompt=prompt,
71
- width=int(width),
72
- height=int(height),
73
- guidance=float(guidance),
74
- inference_steps=int(inference_steps),
75
- seed=int(seed),
76
- do_img2img=False,
77
- init_image=None,
78
- image2image_strength=0.8,
79
- resize_img=True,
80
- api_name="/generate_image"
81
  )
82
- logging.info(f"이미지 생성 성곡: {result[1]}")
83
- return result[0], f"μ‚¬μš©λœ μ‹œλ“œ: {result[1]}"
84
  except Exception as e:
85
- logging.error(f"이미지 생성 μ‹€νŒ¨: {str(e)}")
86
- return None, f"였λ₯˜: {str(e)}"
87
-
88
- def extract_image_prompt(blog_content, blog_topic):
89
- """λΈ”λ‘œκ·Έ λ‚΄μš©μ—μ„œ 이미지 생성을 μœ„ν•œ ν”„λ‘¬ν”„νŠΈ μΆ”μΆœ"""
90
- image_prompt_system = f"""
91
- λ‹€μŒμ€ '{blog_topic}'에 κ΄€ν•œ λΈ”λ‘œκ·Έ κΈ€μž…λ‹ˆλ‹€. 이 λΈ”λ‘œκ·Έ κΈ€μ˜ λ‚΄μš©μ„ 기반으둜 μ μ ˆν•œ 이미지λ₯Ό μƒμ„±ν•˜κΈ° μœ„ν•œ
92
- ν”„λ‘¬ν”„νŠΈλ₯Ό μž‘μ„±ν•΄μ£Όμ„Έμš”. ν”„λ‘¬ν”„νŠΈλŠ” μ˜μ–΄λ‘œ μž‘μ„±ν•˜κ³ , ꡬ체적인 μ‹œκ°μ  μš”μ†Œλ₯Ό λ‹΄μ•„μ•Ό ν•©λ‹ˆλ‹€.
93
- ν”„λ‘¬ν”„νŠΈλ§Œ λ°˜ν™˜ν•˜μ„Έμš”(λ‹€λ₯Έ μ„€λͺ… 없이).
94
-
95
- μ˜ˆμ‹œ ν˜•μ‹:
96
- "A professional photo of [subject], [specific details], [atmosphere], [lighting], [perspective], high quality, detailed"
97
- """
98
-
99
  try:
100
- response = client.messages.create(
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  model="claude-3-7-sonnet-20250219",
102
- max_tokens=150,
103
- system=image_prompt_system,
104
- messages=[{"role": "user", "content": blog_content}]
105
  )
106
-
107
- # μ‘λ‹΅μ—μ„œ ν”„λ‘¬ν”„νŠΈ μΆ”μΆœ
108
- image_prompt = response.content[0].text.strip()
109
- logging.info(f"μƒμ„±λœ 이미지 ν”„λ‘¬ν”„νŠΈ: {image_prompt}")
110
- return image_prompt
111
- except Exception as e:
112
- logging.error(f"이미지 ν”„λ‘¬ν”„νŠΈ 생성 였λ₯˜: {e}")
113
- return f"A professional photo related to {blog_topic}, detailed, high quality"
114
-
115
- # λ§ˆν¬λ‹€μš΄μ„ HTML둜 λ³€ν™˜ν•˜λŠ” ν•¨μˆ˜
116
- def convert_md_to_html(md_text, title="Ginigen Blog"):
117
- html_content = markdown.markdown(md_text)
118
- html_doc = f"""
119
- <!DOCTYPE html>
120
- <html>
121
- <head>
122
- <title>{title}</title>
123
- <meta charset="utf-8">
124
- <style>
125
- body {{ font-family: Arial, sans-serif; line-height: 1.6; max-width: 800px; margin: 0 auto; padding: 20px; }}
126
- h1 {{ color: #2c3e50; font-size: 2.5em; margin-bottom: 20px; }}
127
- h2 {{ color: #3498db; margin-top: 25px; font-size: 1.8em; }}
128
- h3 {{ color: #2980b9; font-size: 1.5em; }}
129
- p {{ margin-bottom: 15px; font-size: 1.1em; }}
130
- blockquote {{ background: #f9f9f9; border-left: 10px solid #ccc; margin: 1.5em 10px; padding: 1em 10px; }}
131
- ul, ol {{ margin-bottom: 15px; }}
132
- li {{ margin-bottom: 5px; }}
133
- hr {{ border: 0; height: 1px; background: #ddd; margin: 20px 0; }}
134
- img {{ max-width: 100%; height: auto; display: block; margin: 20px auto; }}
135
- </style>
136
- </head>
137
- <body>
138
- {html_content}
139
- </body>
140
- </html>
141
- """
142
- return html_doc
143
 
144
- # λ§ˆν¬λ‹€μš΄μ„ PDF둜 λ³€ν™˜ν•˜λŠ” ν•¨μˆ˜
145
- def convert_md_to_pdf(md_text, title="Ginigen Blog"):
146
- html_content = convert_md_to_html(md_text, title)
147
-
148
- # μž„μ‹œ 파일 생성
149
- with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp:
150
- tmp_path = tmp.name
151
-
152
- # HTML을 PDF둜 λ³€ν™˜
153
- HTML(string=html_content).write_pdf(tmp_path)
154
-
155
- # μƒμ„±λœ PDF 읽기
156
- with open(tmp_path, 'rb') as f:
157
- pdf_data = f.read()
158
-
159
- # μž„μ‹œ 파일 μ‚­μ œ
160
- os.unlink(tmp_path)
161
-
162
- return pdf_data
163
 
164
- # λ‹€μš΄λ‘œλ“œ λ²„νŠΌ 생성 헬퍼 ν•¨μˆ˜
165
- def create_download_link(bin_data, download_filename, link_text):
166
- b64 = base64.b64encode(bin_data).decode()
167
- href = f'<a href="data:application/octet-stream;base64,{b64}" download="{download_filename}">{link_text}</a>'
168
- return href
169
 
170
- def chatbot_interface():
 
171
  st.title("Ginigen Blog")
172
-
173
- # λͺ¨λΈ κ³ μ • οΏ½οΏ½μ •
174
- if "ai_model" not in st.session_state:
175
- st.session_state["ai_model"] = "claude-3-7-sonnet-20250219"
176
-
177
- # μ„Έμ…˜ μƒνƒœ μ΄ˆκΈ°ν™”
178
- if "messages" not in st.session_state:
179
- st.session_state.messages = []
180
-
181
- # μžλ™ μ €μž₯ κΈ°λŠ₯
182
- if "auto_save" not in st.session_state:
183
- st.session_state.auto_save = True
184
-
185
- # 이미지 생성 ν† κΈ€
186
- if "generate_image" not in st.session_state:
187
- st.session_state.generate_image = False
188
-
189
- # 이미지 API μƒνƒœ
190
- if "image_api_status" not in st.session_state:
191
- st.session_state.image_api_status = test_image_api_connection()
192
 
193
- # λŒ€ν™” 기둝 관리 (μ‚¬μ΄λ“œλ°”)
194
- st.sidebar.title("λŒ€ν™” 기둝 관리")
195
-
196
- # μžλ™ μ €μž₯ ν† κΈ€
197
- st.session_state.auto_save = st.sidebar.toggle("μžλ™ μ €μž₯", value=st.session_state.auto_save)
198
-
199
- # 이미지 생성 ν† κΈ€
200
- st.session_state.generate_image = st.sidebar.toggle("λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± ν›„ 이미지 μžλ™ 생성", value=st.session_state.generate_image)
201
-
202
- # 이미지 API μƒνƒœ ν‘œμ‹œ
203
- st.sidebar.text(st.session_state.image_api_status)
 
 
 
 
 
 
204
 
205
- # 이미지 생성 μ„€μ • (토글이 켜져 μžˆμ„ λ•Œλ§Œ ν‘œμ‹œ)
206
- if st.session_state.generate_image:
207
- st.sidebar.subheader("이미지 생성 μ„€μ •")
208
- width = st.sidebar.slider("λ„ˆλΉ„", 256, 1024, 768, 64)
209
- height = st.sidebar.slider("높이", 256, 1024, 768, 64)
210
- guidance = st.sidebar.slider("κ°€μ΄λ˜μŠ€ μŠ€μΌ€μΌ", 1.0, 20.0, 3.5, 0.1)
211
- inference_steps = st.sidebar.slider("인퍼런슀 μŠ€ν…", 1, 50, 30, 1)
212
- seed = st.sidebar.number_input("μ‹œλ“œ", value=3, min_value=0, step=1)
213
- else:
214
- # κΈ°λ³Έκ°’ μ„€μ •
215
- width, height, guidance, inference_steps, seed = 768, 768, 3.5, 30, 3
216
 
217
- # λΈ”λ‘œκ·Έ λ‚΄μš© λ‹€μš΄λ‘œλ“œ μ„Ήμ…˜
218
- st.sidebar.title("λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
 
219
 
220
- # μ΅œμ‹  λΈ”λ‘œκ·Έ λ‚΄μš© κ°€μ Έμ˜€κΈ°
221
- latest_blog = None
222
- latest_blog_title = "λΈ”λ‘œκ·Έ κΈ€"
223
 
224
- if len(st.session_state.messages) > 0:
225
- # κ°€μž₯ 졜근 assistant λ©”μ‹œμ§€ μ°ΎκΈ°
226
- for msg in reversed(st.session_state.messages):
227
- if msg["role"] == "assistant" and msg["content"].strip():
228
- latest_blog = msg["content"]
229
-
230
- # 타이틀 μΆ”μΆœ μ‹œλ„ (첫 번째 제λͺ© νƒœκ·Έ μ‚¬μš©)
231
- import re
232
- title_match = re.search(r'# (.*?)(\n|$)', latest_blog)
233
- if title_match:
234
- latest_blog_title = title_match.group(1).strip()
235
- # μ‚¬μš©μž μž…λ ₯을 νƒ€μ΄ν‹€λ‘œ μ‚¬μš©
236
- elif len(st.session_state.messages) >= 2:
237
- for i in range(len(st.session_state.messages)-1, -1, -1):
238
- if st.session_state.messages[i]["role"] == "user":
239
- latest_blog_title = st.session_state.messages[i]["content"][:30].strip()
240
- if len(st.session_state.messages[i]["content"]) > 30:
241
- latest_blog_title += "..."
242
- break
243
- break
244
 
245
- # λ‹€μš΄λ‘œλ“œ λ²„νŠΌ κ·Έλ£Ή
 
 
 
 
 
 
 
 
 
246
  if latest_blog:
247
- st.sidebar.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
248
-
249
- col1, col2, col3 = st.sidebar.columns(3)
250
-
251
- # λ§ˆν¬λ‹€μš΄μœΌλ‘œ λ‹€μš΄λ‘œλ“œ
252
- with col1:
253
- st.download_button(
254
- label="λ§ˆν¬λ‹€μš΄",
255
- data=latest_blog,
256
- file_name=f"{latest_blog_title}.md",
257
- mime="text/markdown"
258
- )
259
-
260
- # HTML둜 λ‹€μš΄λ‘œλ“œ
261
- with col2:
262
- html_content = convert_md_to_html(latest_blog, latest_blog_title)
263
- st.download_button(
264
- label="HTML",
265
- data=html_content,
266
- file_name=f"{latest_blog_title}.html",
267
- mime="text/html"
268
- )
269
-
270
- # PDF둜 λ‹€μš΄λ‘œλ“œ
271
- with col3:
272
- try:
273
- pdf_data = convert_md_to_pdf(latest_blog, latest_blog_title)
274
- st.download_button(
275
- label="PDF",
276
- data=pdf_data,
277
- file_name=f"{latest_blog_title}.pdf",
278
- mime="application/pdf"
279
- )
280
- except Exception as e:
281
- st.error(f"PDF 생성 였λ₯˜: {e}")
282
- logging.error(f"PDF 생성 였λ₯˜: {e}")
283
-
284
- # λŒ€ν™” 기둝 뢈러였기
285
- uploaded_file = st.sidebar.file_uploader("λŒ€ν™” 기둝 뢈러였기", type=['json'])
286
- if uploaded_file is not None:
287
  try:
288
- content = uploaded_file.getvalue().decode()
289
- if content.strip():
290
- st.session_state.messages = json.loads(content)
291
- st.sidebar.success("λŒ€ν™” 기둝을 μ„±κ³΅μ μœΌλ‘œ λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€!")
292
- else:
293
- st.sidebar.warning("μ—…λ‘œλ“œλœ 파일이 λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
294
- except json.JSONDecodeError:
295
- st.sidebar.error("μ˜¬λ°”λ₯Έ JSON ν˜•μ‹μ˜ 파일이 μ•„λ‹™λ‹ˆλ‹€.")
296
  except Exception as e:
297
- st.sidebar.error(f"파일 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€: {str(e)}")
298
-
299
- # λŒ€ν™” 기둝 μ΄ˆκΈ°ν™” λ²„νŠΌ
300
- if st.sidebar.button("λŒ€ν™” 기둝 μ΄ˆκΈ°ν™”"):
301
- st.session_state.messages = []
302
- st.sidebar.success("λŒ€ν™” 기둝이 μ΄ˆκΈ°ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
303
-
304
- # λ©”μ‹œμ§€ ν‘œμ‹œ
305
- for message in st.session_state.messages:
306
- with st.chat_message(message["role"]):
307
- st.markdown(message["content"])
308
- # 이미지가 μžˆλŠ” 경우 ν‘œμ‹œ
309
- if "image" in message:
310
- st.image(message["image"], caption=message.get("image_caption", "μƒμ„±λœ 이미지"))
311
-
312
- # μ‚¬μš©μž μž…λ ₯
 
313
  if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
314
  st.session_state.messages.append({"role": "user", "content": prompt})
315
- with st.chat_message("user"):
316
- st.markdown(prompt)
317
 
318
- # AI 응닡 생성
319
  with st.chat_message("assistant"):
320
- message_placeholder = st.empty()
321
- full_response = ""
322
 
323
- # API 호좜
 
 
 
 
 
 
 
 
 
 
 
 
324
  with client.messages.stream(
325
- max_tokens=MAX_TOKENS,
326
- system=get_system_prompt(),
327
- messages=[{"role": m["role"], "content": m["content"]} for m in st.session_state.messages],
328
- model=st.session_state["ai_model"]
329
  ) as stream:
330
- for text in stream.text_stream:
331
- full_response += str(text) if text is not None else ""
332
- message_placeholder.markdown(full_response + "β–Œ")
333
-
334
- message_placeholder.markdown(full_response)
335
-
336
- # 이미지 생성 μ˜΅μ…˜μ΄ 켜져 μžˆλŠ” 경우
337
  if st.session_state.generate_image:
338
- with st.spinner("λΈ”λ‘œκ·Έμ— λ§žλŠ” 이미지 생성 쀑..."):
339
- # 이미지 ν”„λ‘¬ν”„νŠΈ 생성
340
- image_prompt = extract_image_prompt(full_response, prompt)
341
-
342
- # 이미지 생성
343
- image, image_caption = generate_image(
344
- image_prompt,
345
- width=width,
346
- height=height,
347
- guidance=guidance,
348
- inference_steps=inference_steps,
349
- seed=seed
350
- )
351
-
352
- if image:
353
- st.image(image, caption=image_caption)
354
- # 이미지 정보λ₯Ό 응닡에 포함
355
- st.session_state.messages.append({
356
- "role": "assistant",
357
- "content": full_response,
358
- "image": image,
359
- "image_caption": image_caption
360
- })
361
- else:
362
- st.error(f"이미지 생성 μ‹€νŒ¨: {image_caption}")
363
- st.session_state.messages.append({
364
- "role": "assistant",
365
- "content": full_response
366
- })
367
- else:
368
- # 이미지 생성 없이 μ‘λ‹΅λ§Œ μ €μž₯
369
- st.session_state.messages.append({
370
- "role": "assistant",
371
- "content": full_response
372
- })
373
-
374
- # λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ ν‘œμ‹œ (응닡 λ°”λ‘œ μ•„λž˜μ—)
375
- st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ:")
376
- col1, col2, col3 = st.columns(3)
377
-
378
- with col1:
379
- st.download_button(
380
- label="λ§ˆν¬λ‹€μš΄μœΌλ‘œ μ €μž₯",
381
- data=full_response,
382
- file_name=f"{prompt[:30]}.md",
383
- mime="text/markdown"
384
- )
385
-
386
- with col2:
387
- html_content = convert_md_to_html(full_response, prompt[:30])
388
- st.download_button(
389
- label="HTML둜 μ €μž₯",
390
- data=html_content,
391
- file_name=f"{prompt[:30]}.html",
392
- mime="text/html"
393
- )
394
-
395
- with col3:
396
- try:
397
- pdf_data = convert_md_to_pdf(full_response, prompt[:30])
398
- st.download_button(
399
- label="PDF둜 μ €μž₯",
400
- data=pdf_data,
401
- file_name=f"{prompt[:30]}.pdf",
402
- mime="application/pdf"
403
- )
404
- except Exception as e:
405
- st.error(f"PDF 생성 였λ₯˜: {e}")
406
- logging.error(f"PDF 생성 였λ₯˜: {e}")
407
-
408
- # μžλ™ μ €μž₯ κΈ°λŠ₯
409
- if st.session_state.auto_save:
410
- try:
411
- # 이미지 μ •λ³΄λŠ” μ €μž₯ν•˜μ§€ μ•ŠμŒ (JSONμ—λŠ” λ°”μ΄λ„ˆλ¦¬ 데이터λ₯Ό 직접 μ €μž₯ν•  수 μ—†μŒ)
412
- save_messages = []
413
- for msg in st.session_state.messages:
414
- save_msg = {"role": msg["role"], "content": msg["content"]}
415
- save_messages.append(save_msg)
416
-
417
- with open('chat_history_auto_save.json', 'w', encoding='utf-8') as f:
418
- json.dump(save_messages, f, ensure_ascii=False, indent=4)
419
- except Exception as e:
420
- st.sidebar.error(f"μžλ™ μ €μž₯ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
421
-
422
- # λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
423
- if st.sidebar.button("λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ"):
424
- # 이미지 μ •λ³΄λŠ” μ €μž₯ν•˜μ§€ μ•ŠμŒ
425
- save_messages = []
426
- for msg in st.session_state.messages:
427
- save_msg = {"role": msg["role"], "content": msg["content"]}
428
- save_messages.append(save_msg)
429
-
430
- json_history = json.dumps(save_messages, indent=4, ensure_ascii=False)
431
- st.sidebar.download_button(
432
- label="λŒ€ν™” 기둝 μ €μž₯ν•˜κΈ°",
433
- data=json_history,
434
- file_name="chat_history.json",
435
- mime="application/json"
436
- )
437
 
438
- def main():
439
- chatbot_interface()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
  if __name__ == "__main__":
442
- # requirements.txt 파일 생성
443
  with open("requirements.txt", "w") as f:
444
- f.write("streamlit>=1.31.0\n")
445
- f.write("anthropic>=0.18.1\n")
446
- f.write("gradio-client>=1.8.0\n")
447
- f.write("requests>=2.32.3\n")
448
- f.write("markdown>=3.5.1\n")
449
- f.write("weasyprint>=60.2\n")
450
- f.write("pillow>=10.1.0\n")
451
-
452
  main()
 
1
+ # ──────────────────────────────── Imports ────────────────────────────────
2
+ import os, json, re, logging, requests, markdown, time
3
+ from datetime import datetime
4
+
5
  import streamlit as st
 
6
  import anthropic
 
 
7
  from gradio_client import Client
8
+ # from bs4 import BeautifulSoup # ν•„μš” μ‹œ 주석 ν•΄μ œ
 
 
 
9
 
10
+ # ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
11
+ ANTHROPIC_KEY = os.getenv("API_KEY", "")
12
+ BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # 이름 μœ μ§€
13
+ BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
14
+ IMAGE_API_URL = "http://211.233.58.201:7896"
15
+ MAX_TOKENS = 7_999
16
 
17
+ # λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ μ •μ˜
18
+ BLOG_TEMPLATES = {
19
+ "standard": "μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€λ‘œμ„œ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό 따라 μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 κΈ€ μž‘μ„±",
20
+ "tutorial": "단계별 νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ, λͺ…ν™•ν•œ κ³Όμ •κ³Ό κ²°κ³Όλ₯Ό λ³΄μ—¬μ£ΌλŠ” κ°€μ΄λ“œ μž‘μ„±",
21
+ "review": "μ œν’ˆ/μ„œλΉ„μŠ€ 뢄석 μ€‘μ‹¬μ˜ 리뷰 ν˜•μ‹, μž₯단점 뢄석과 μΆ”μ²œ 포함",
22
+ "storytelling": "개인 κ²½ν—˜μ΄λ‚˜ 사둀λ₯Ό μ€‘μ‹¬μœΌλ‘œ ν•œ μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μ˜ λΈ”λ‘œκ·Έ μž‘μ„±",
23
+ "seo_optimized": "검색엔진 μ΅œμ ν™”(SEO)λ₯Ό κ³ λ €ν•œ ν‚€μ›Œλ“œ 쀑심 λΈ”λ‘œκ·Έ μž‘μ„±"
24
+ }
25
 
26
+ BLOG_TONES = {
27
+ "professional": "전문적이고 곡식적인 μ–΄μ‘°λ‘œ μž‘μ„±",
28
+ "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”μ²΄ μ€‘μ‹¬μ˜ νŽΈμ•ˆν•œ ν†€μœΌλ‘œ μž‘μ„±",
29
+ "humorous": "μœ λ¨Έμ™€ 재치λ₯Ό κ°€λ―Έν•œ κ°€λ²Όμš΄ μ–΄μ‘°λ‘œ μž‘μ„±",
30
+ "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±"
31
+ }
32
 
33
+ # ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
34
+ logging.basicConfig(level=logging.INFO,
35
+ format="%(asctime)s - %(levelname)s - %(message)s")
36
 
37
+ # ──────────────────────────────── Anthropic Client ─────────────────────────
38
+ client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
 
39
 
40
+ # ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
41
+ def get_system_prompt(template="standard", tone="professional", word_count=1750) -> str:
42
+ base_prompt = """
43
+ 당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
44
 
45
+ λ…μž μ—°κ²° 단계
46
+ 1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사
47
+ 1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문
48
+ 1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„
49
 
50
+ 문제 μ •μ˜ 단계
51
+ 2.1. λ…μžμ˜ 페인포인트 ꡬ체화
52
+ 2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석
53
+ 2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
54
 
55
+ μ „λ¬Έμ„± μž…μ¦ 단계
56
+ 3.1. 객관적 데이터 기반 뢄석
57
+ 3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용
58
+ 3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화
59
 
60
+ μ†”λ£¨μ…˜ 제곡 단계
61
+ 4.1. 단계별 ���천 κ°€μ΄λ“œλΌμΈ μ œμ‹œ
62
+ 4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁
63
+ 4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함
64
 
65
+ 신뒰도 κ°•ν™” 단계
66
+ 5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
67
+ 5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용
68
+ 5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦
69
 
70
+ 행동 μœ λ„ 단계
71
+ 6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ
72
+ 6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ
73
+ 6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함
74
 
75
+ μ§„μ •μ„± κ°•ν™” 단계
76
+ 7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개
77
+ 7.2. κ°œμΈλ³„ 차이 쑴재 인정
78
+ 7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ
79
 
80
+ 관계 지속 단계
81
+ 8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사
82
+ 8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„±
83
+ 8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
84
+ """
85
 
86
+ # ν…œν”Œλ¦Ώλ³„ μΆ”κ°€ μ§€μΉ¨
87
+ template_guides = {
88
+ "tutorial": """
89
+ 이 λΈ”λ‘œκ·ΈλŠ” νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
90
+ - λͺ…ν™•ν•œ λͺ©ν‘œμ™€ μ΅œμ’… κ²°κ³Όλ¬Ό λ¨Όμ € μ œμ‹œ
91
+ - λ‹¨κ³„λ³„λ‘œ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„λœ κ³Όμ • μ„€λͺ…
92
+ - 각 λ‹¨κ³„λ§ˆλ‹€ 이미지λ₯Ό μ‚½μž…ν•  μœ„μΉ˜ ν‘œμ‹œ
93
+ - μ˜ˆμƒ μ†Œμš” μ‹œκ°„κ³Ό λ‚œμ΄λ„ λͺ…μ‹œ
94
+ - ν•„μš”ν•œ λ„κ΅¬λ‚˜ 사전 지식 μ•ˆλ‚΄
95
+ - λ¬Έμ œν•΄κ²° 팁과 자주 λ°œμƒν•˜λŠ” μ‹€μˆ˜ 포함
96
+ - μ™„λ£Œ ν›„ λ‹€μŒ λ‹¨κ³„λ‚˜ μ‘μš©λ²• μ œμ•ˆ
97
+ """,
98
+
99
+ "review": """
100
+ 이 λΈ”λ‘œκ·ΈλŠ” 리뷰 ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
101
+ - 객관적 사싀과 주관적 평가 ꡬ뢄
102
+ - λͺ…ν™•ν•œ 평가 κΈ°μ€€ μ œμ‹œ
103
+ - μž₯점과 단점 κ· ν˜•μžˆκ²Œ μ„œμˆ 
104
+ - μœ μ‚¬ μ œν’ˆ/μ„œλΉ„μŠ€μ™€ 비ꡐ
105
+ - λˆ„κ΅¬μ—κ²Œ μ ν•©ν•œμ§€ νƒ€κ²Ÿ μ„€λͺ…
106
+ - ꡬ체적인 μ‚¬μš© κ²½ν—˜κ³Ό κ²°κ³Ό 포함
107
+ - μ΅œμ’… μΆ”μ²œ 여뢀와 λŒ€μ•ˆ μ œμ‹œ
108
+ """,
109
+
110
+ "storytelling": """
111
+ 이 λΈ”λ‘œκ·ΈλŠ” μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
112
+ - μ‹€μ œ μΈλ¬Όμ΄λ‚˜ μ‚¬λ‘€λ‘œ μ‹œμž‘
113
+ - 문제 상황과 감정적 μ—°κ²° κ°•ν™”
114
+ - κ°ˆλ“±κ³Ό ν•΄κ²°κ³Όμ • μ€‘μ‹¬μ˜ λ‚΄λŸ¬ν‹°λΈŒ
115
+ - κ΅ν›ˆκ³Ό 배움을 μžμ—°μŠ€λŸ½κ²Œ 포함
116
+ - λ…μžκ°€ 곡감할 수 μžˆλŠ” 감정선 μœ μ§€
117
+ - 이야기와 μœ μš©ν•œ μ •λ³΄μ˜ κ· ν˜• μœ μ§€
118
+ - λ…μžμ—κ²Œ μžμ‹ μ˜ 이야기λ₯Ό μƒκ°ν•΄λ³΄κ²Œ μœ λ„
119
+ """,
120
+
121
+ "seo_optimized": """
122
+ 이 λΈ”λ‘œκ·ΈλŠ” SEO μ΅œμ ν™” ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
123
+ - 핡심 ν‚€μ›Œλ“œλ₯Ό 제λͺ©, μ†Œμ œλͺ©, 첫 단락에 배치
124
+ - κ΄€λ ¨ ν‚€μ›Œλ“œλ₯Ό μžμ—°μŠ€λŸ½κ²Œ 본문에 λΆ„μ‚°
125
+ - 300-500자 λΆ„λŸ‰μ˜ λͺ…ν™•ν•œ 단락 ꡬ성
126
+ - 질문 ν˜•μ‹μ˜ μ†Œμ œλͺ© ν™œμš©
127
+ - λͺ©λ‘, ν‘œ, κ°•μ‘° ν…μŠ€νŠΈ λ“± λ‹€μ–‘ν•œ μ„œμ‹ ν™œμš©
128
+ - λ‚΄λΆ€ 링크 μ‚½μž… μœ„μΉ˜ ν‘œμ‹œ
129
+ - 2000-3000자 μ΄μƒμ˜ μΆ©λΆ„ν•œ μ½˜ν…μΈ  제곡
130
  """
131
+ }
132
+
133
+ # 톀별 μΆ”κ°€ μ§€μΉ¨
134
+ tone_guides = {
135
+ "professional": "전문적이고 κΆŒμœ„μžˆλŠ” μ–΄μ‘°λ‘œ μž‘μ„±ν•˜λ˜, μ „λ¬Έ μš©μ–΄λŠ” 적절히 μ„€λͺ…ν•΄ μ£Όμ„Έμš”. 데이터와 연ꡬ κ²°κ³Όλ₯Ό μ€‘μ‹¬μœΌλ‘œ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
136
+ "casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”ν•˜λ“― νŽΈμ•ˆν•œ μ–΄μ‘°λ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. '~λ„€μš”', '~ν•΄μš”' 같은 λŒ€ν™”μ²΄λ₯Ό μ‚¬μš©ν•˜κ³ , 개인적 κ²½ν—˜κ³Ό λΉ„μœ λ₯Ό 톡해 λ‚΄μš©μ„ μ „λ‹¬ν•˜μ„Έμš”.",
137
+ "humorous": "μœ λ¨Έμ™€ μž¬μΉ˜μžˆλŠ” ν‘œν˜„μ„ 적절히 ν™œμš©ν•΄ μ£Όμ„Έμš”. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ μ˜ˆμ‹œ, κ°€λ²Όμš΄ 농담을 ν¬ν•¨ν•˜λ˜, μ •λ³΄μ˜ μ •ν™•μ„±κ³Ό μœ μš©μ„±μ€ μœ μ§€ν•˜μ„Έμš”.",
138
+ "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. 인물, λ°°κ²½, κ°ˆλ“±, 해결과정이 λ‹΄κΈ΄ λ‚΄λŸ¬ν‹°λΈŒ ꡬ쑰λ₯Ό ν™œμš©ν•˜μ„Έμš”."
139
+ }
140
+
141
+ # μ΅œμ’… ν”„λ‘¬ν”„νŠΈ μ‘°ν•©
142
+ final_prompt = base_prompt
143
+
144
+ # μ„ νƒλœ ν…œν”Œλ¦Ώ μ§€μΉ¨ μΆ”κ°€
145
+ if template in template_guides:
146
+ final_prompt += "\n" + template_guides[template]
147
+
148
+ # μ„ νƒλœ 톀 μ§€μΉ¨ μΆ”κ°€
149
+ if tone in tone_guides:
150
+ final_prompt += f"\n\nν†€μ•€λ§€λ„ˆ: {tone_guides[tone]}"
151
+
152
+ # κΈ€μž 수 μ§€μΉ¨ μΆ”κ°€
153
+ final_prompt += f"\n\nμž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­\n9.1. κΈ€μž 수: {word_count-250}-{word_count+250}자 λ‚΄μ™Έ\n9.2. 문단 길이: 3-4λ¬Έμž₯ 이내\n9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš©\n9.4. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ\n9.5. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©"
154
+
155
+ return final_prompt
156
 
157
+ # ──────────────────────────────── Brave Search API ─────────────────────────
158
+ def brave_search(query: str, count: int = 5):
159
+ """
160
+ Brave Web Search API 호좜 β†’ list[dict]
161
+ λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
162
+ """
163
+ if not BRAVE_KEY:
164
+ raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) ν™˜κ²½λ³€μˆ˜κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
165
 
166
+ headers = {
167
+ "Accept": "application/json",
168
+ "Accept-Encoding": "gzip",
169
+ "X-Subscription-Token": BRAVE_KEY
170
+ }
171
+ params = {"q": query, "count": str(count)}
172
 
173
+ for attempt in range(3): # μ΅œλŒ€ 3번 μž¬μ‹œλ„
174
+ try:
175
+ r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
176
+ r.raise_for_status()
177
+ data = r.json()
178
+
179
+ # κ²°κ³Ό ν˜•μ‹ 확인 및 λ‘œκΉ…
180
+ logging.info(f"Brave 검색 κ²°κ³Ό 데이터 ꡬ쑰: {list(data.keys())}")
181
+
182
+ raw = data.get("web", {}).get("results") or data.get("results", [])
183
+ if not raw:
184
+ logging.warning(f"Brave 검색 κ²°κ³Ό μ—†μŒ. 응닡: {data}")
185
+ raise ValueError("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€")
186
+
187
+ arts = []
188
+ for i, res in enumerate(raw[:count], 1):
189
+ url = res.get("url", res.get("link", ""))
190
+ host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
191
+ arts.append({
192
+ "index": i,
193
+ "title": res.get("title", "제λͺ© μ—†μŒ"),
194
+ "link": url,
195
+ "snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
196
+ "displayed_link": host
197
+ })
198
+
199
+ logging.info(f"Brave 검색 성곡: {len(arts)}개 κ²°κ³Ό")
200
+ return arts
201
+
202
+ except Exception as e:
203
+ logging.error(f"Brave 검색 μ‹€νŒ¨ (μ‹œλ„ {attempt+1}/3): {e}")
204
+ if attempt < 2: # λ§ˆμ§€λ§‰ μ‹œλ„κ°€ μ•„λ‹ˆλ©΄ λŒ€κΈ° ν›„ μž¬μ‹œλ„
205
+ time.sleep(2)
206
+
207
+ return [] # λͺ¨λ“  μ‹œλ„ μ‹€νŒ¨ μ‹œ 빈 λͺ©λ‘ λ°˜ν™˜
208
+
209
+ def mock_results(query: str) -> str:
210
+ """검색 API μ‹€νŒ¨ μ‹œ 가상 검색 κ²°κ³Ό 제곡"""
211
+ ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
212
+ return (f"# 검색 κ²°κ³Ό λŒ€μ²΄ λ‚΄μš© (생성: {ts})\n\n"
213
+ f"검색 API 호좜이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 주제 '{query}'에 λŒ€ν•΄ κΈ°μ‘΄ 지식을 ν™œμš©ν•΄ λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n\n"
214
+ f"λ‹€μŒ λ‚΄μš©μ΄ 도움이 될 수 μžˆμŠ΅λ‹ˆλ‹€:\n\n"
215
+ f"- {query}의 κΈ°λ³Έ κ°œλ…κ³Ό μ€‘μš”μ„±\n"
216
+ f"- 일반적으둜 μ•Œλ €μ§„ κ΄€λ ¨ 톡계와 νŠΈλ Œλ“œ\n"
217
+ f"- ν•΄λ‹Ή μ£Όμ œμ— λŒ€ν•œ μ „λ¬Έκ°€λ“€μ˜ 일반적인 견해\n"
218
+ f"- λ…μžλ“€μ΄ μ‹€μ œλ‘œ κΆκΈˆν•΄ν•  λ§Œν•œ μ§ˆλ¬Έλ“€\n\n"
219
+ f"μ°Έκ³ : 이 λ‚΄μš©μ€ μ‹€μ‹œκ°„ 검색 κ²°κ³Όκ°€ μ•„λ‹Œ λŒ€μ²΄ μ•ˆλ‚΄μž…λ‹ˆλ‹€.\n\n")
220
+
221
+ def do_web_search(query: str) -> str:
222
+ """μ›Ή 검색 μˆ˜ν–‰ 및 κ²°κ³Ό ν¬λ§·νŒ…"""
223
  try:
224
+ arts = brave_search(query, 5)
225
+ if not arts:
226
+ logging.warning("검색 κ²°κ³Ό μ—†μŒ, λŒ€μ²΄ μ½˜ν…μΈ  μ‚¬μš©")
227
+ return mock_results(query)
228
+
229
+ hdr = "# μ›Ή 검색 κ²°κ³Ό\nμ•„λž˜ 정보λ₯Ό μ°Έκ³ ν•΄μ„œ λ‹΅λ³€ν•˜μ„Έμš”.\n\n"
230
+ body = "\n".join(
231
+ f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
232
+ f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
233
+ for a in arts
 
 
 
234
  )
235
+ return hdr + body
 
236
  except Exception as e:
237
+ logging.error(f"μ›Ή 검색 전체 ν”„λ‘œμ„ΈμŠ€ μ‹€νŒ¨: {str(e)}")
238
+ return mock_results(query)
239
+
240
+ # ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
241
+ def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
242
+ if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
 
 
 
 
 
 
 
 
243
  try:
244
+ res = Client(IMAGE_API_URL).predict(
245
+ prompt=prompt, width=w, height=h, guidance=g,
246
+ inference_steps=steps, seed=seed,
247
+ do_img2img=False, init_image=None,
248
+ image2image_strength=0.8, resize_img=True,
249
+ api_name="/generate_image")
250
+ return res[0], f"Seed: {res[1]}"
251
+ except Exception as e:
252
+ logging.error(e); return None, str(e)
253
+
254
+ def extract_image_prompt(blog: str, topic: str):
255
+ sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
256
+ try:
257
+ res = client.messages.create(
258
  model="claude-3-7-sonnet-20250219",
259
+ max_tokens=80, system=sys,
260
+ messages=[{"role": "user", "content": blog}]
 
261
  )
262
+ return res.content[0].text.strip()
263
+ except Exception:
264
+ return f"A professional photo related to {topic}, high quality"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
+ def md_to_html(md: str, title="Ginigen Blog"):
267
+ return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ def keywords(text: str, top=5):
270
+ return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])
 
 
 
271
 
272
+ # ──────────────────────────────── Streamlit UI ────────────────────────────
273
+ def ginigen_app():
274
  st.title("Ginigen Blog")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ # μ„Έμ…˜ κΈ°λ³Έκ°’
277
+ defaults = dict(
278
+ ai_model="claude-3-7-sonnet-20250219",
279
+ messages=[],
280
+ auto_save=True,
281
+ generate_image=False,
282
+ use_web_search=False,
283
+ blog_template="standard",
284
+ blog_tone="professional",
285
+ word_count=1750
286
+ )
287
+ for k, v in defaults.items():
288
+ st.session_state.setdefault(k, v)
289
+
290
+ # ── μ‚¬μ΄λ“œλ°” 컨트둀
291
+ sb = st.sidebar
292
+ sb.title("λΈ”λ‘œκ·Έ μ„€μ •")
293
 
294
+ # λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ 선택
295
+ sb.subheader("λΈ”λ‘œκ·Έ μŠ€νƒ€μΌ μ„€μ •")
296
+ sb.selectbox("λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ", options=list(BLOG_TEMPLATES.keys()),
297
+ format_func=lambda x: x.replace("_", " ").title(),
298
+ key="blog_template")
 
 
 
 
 
 
299
 
300
+ sb.selectbox("λΈ”λ‘œκ·Έ 톀", options=list(BLOG_TONES.keys()),
301
+ format_func=lambda x: x.replace("_", " ").title(),
302
+ key="blog_tone")
303
 
304
+ sb.slider("λΈ”λ‘œκ·Έ 길이 (단어 수)", 800, 3000, 1750, key="word_count")
 
 
305
 
306
+ sb.subheader("기타 μ„€μ •")
307
+ sb.toggle("μžλ™ μ €μž₯", key="auto_save")
308
+ sb.toggle("이미지 μžλ™ 생성", key="generate_image")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ # μ›Ή 검색 ν† κΈ€ (λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄ μœ μ§€ν•˜λ˜ 기본값은 False)
311
+ search_enabled = sb.toggle("μ›Ή 검색 μ‚¬μš©", value=False, key="use_web_search")
312
+ if search_enabled:
313
+ st.warning("⚠️ μ›Ή 검색 κΈ°λŠ₯은 ν˜„μž¬ λΆˆμ•ˆμ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 검색 κ²°κ³Όκ°€ μ—†μœΌλ©΄ κΈ°λ³Έ μ§€μ‹μœΌλ‘œ λŒ€μ²΄λ©λ‹ˆλ‹€.")
314
+
315
+ # ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
316
+ latest_blog = next(
317
+ (m["content"] for m in reversed(st.session_state.messages)
318
+ if m["role"] == "assistant" and m["content"].strip()), None)
319
+
320
  if latest_blog:
321
+ title = re.search(r"# (.*?)(\n|$)", latest_blog)
322
+ title = title.group(1).strip() if title else "blog"
323
+ sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
324
+ c1, c2 = sb.columns(2)
325
+ c1.download_button("Markdown", latest_blog,
326
+ file_name=f"{title}.md", mime="text/markdown")
327
+ c2.download_button("HTML", md_to_html(latest_blog, title),
328
+ file_name=f"{title}.html", mime="text/html")
329
+
330
+ # ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
331
+ up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
332
+ if up:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  try:
334
+ st.session_state.messages = json.load(up)
335
+ sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
 
 
 
 
 
 
336
  except Exception as e:
337
+ sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")
338
+
339
+ # ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
340
+ if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
341
+ sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
342
+ ensure_ascii=False, indent=2),
343
+ file_name="chat_history.json",
344
+ mime="application/json")
345
+
346
+ # ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
347
+ for m in st.session_state.messages:
348
+ with st.chat_message(m["role"]):
349
+ st.markdown(m["content"])
350
+ if "image" in m:
351
+ st.image(m["image"], caption=m.get("image_caption", ""))
352
+
353
+ # ── μ‚¬μš©μž μž…λ ₯
354
  if prompt := st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?"):
355
  st.session_state.messages.append({"role": "user", "content": prompt})
356
+ with st.chat_message("user"): st.markdown(prompt)
 
357
 
 
358
  with st.chat_message("assistant"):
359
+ placeholder = st.empty(); answer = ""
 
360
 
361
+ # μ„ νƒλœ ν…œν”Œλ¦Ώ, 톀, 단어 수둜 μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ 생성
362
+ sys_prompt = get_system_prompt(
363
+ template=st.session_state.blog_template,
364
+ tone=st.session_state.blog_tone,
365
+ word_count=st.session_state.word_count
366
+ )
367
+
368
+ if st.session_state.use_web_search:
369
+ with st.spinner("μ›Ή 검색 쀑…"):
370
+ search_md = do_web_search(keywords(prompt))
371
+ sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"
372
+
373
+ # Claude 슀트리밍
374
  with client.messages.stream(
375
+ model=st.session_state.ai_model, max_tokens=MAX_TOKENS,
376
+ system=sys_prompt,
377
+ messages=[{"role": m["role"], "content": m["content"]}
378
+ for m in st.session_state.messages]
379
  ) as stream:
380
+ for t in stream.text_stream:
381
+ answer += t or ""
382
+ placeholder.markdown(answer + "β–Œ")
383
+ placeholder.markdown(answer)
384
+
385
+ # 이미지 μ˜΅μ…˜
 
386
  if st.session_state.generate_image:
387
+ with st.spinner("이미지 생성 쀑…"):
388
+ ip = extract_image_prompt(answer, prompt)
389
+ img, cap = generate_image(ip)
390
+ if img:
391
+ st.image(img, caption=cap)
392
+ st.session_state.messages.append(
393
+ {"role": "assistant", "content": answer,
394
+ "image": img, "image_caption": cap})
395
+ answer_entry_saved = True
396
+ if not st.session_state.generate_image:
397
+ st.session_state.messages.append(
398
+ {"role": "assistant", "content": answer})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ # λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
401
+ st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
402
+ b1, b2 = st.columns(2)
403
+ b1.download_button("Markdown", answer,
404
+ file_name=f"{prompt[:30]}.md", mime="text/markdown")
405
+ b2.download_button("HTML", md_to_html(answer, prompt[:30]),
406
+ file_name=f"{prompt[:30]}.html", mime="text/html")
407
+
408
+ # ── μžλ™ λ°±μ—… μ €μž₯
409
+ if st.session_state.auto_save and st.session_state.messages:
410
+ try:
411
+ fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
412
+ with open(fn, "w", encoding="utf-8") as fp:
413
+ json.dump(st.session_state.messages, fp,
414
+ ensure_ascii=False, indent=2)
415
+ except Exception as e:
416
+ logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")
417
+
418
+ # ──────────────────────────────── main / requirements ──────────────────────
419
+ def main(): ginigen_app()
420
 
421
  if __name__ == "__main__":
422
+ # requirements.txt 동적 생성
423
  with open("requirements.txt", "w") as f:
424
+ f.write("\n".join([
425
+ "streamlit>=1.31.0",
426
+ "anthropic>=0.18.1",
427
+ "gradio-client>=1.8.0",
428
+ "requests>=2.32.3",
429
+ "markdown>=3.5.1",
430
+ "pillow>=10.1.0"
431
+ ]))
432
  main()