ginipick commited on
Commit
df39903
Β·
verified Β·
1 Parent(s): 1976b86

Delete app-BACKUP2.py

Browse files
Files changed (1) hide show
  1. app-BACKUP2.py +0 -432
app-BACKUP2.py DELETED
@@ -1,432 +0,0 @@
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()