IDEA-DESIGN / app.py
ginipick's picture
Update app.py
62d2b15 verified
raw
history blame
21.1 kB
# ──────────────────────────────── Imports ────────────────────────────────
import os, json, re, logging, requests, markdown, time
from datetime import datetime
import streamlit as st
import anthropic
from gradio_client import Client
# from bs4 import BeautifulSoup # ν•„μš” μ‹œ 주석 ν•΄μ œ
# ──────────────────────────────── ν™˜κ²½ λ³€μˆ˜ / μƒμˆ˜ ───────────────────────────
ANTHROPIC_KEY = os.getenv("API_KEY", "")
BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # 이름 μœ μ§€
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
IMAGE_API_URL = "http://211.233.58.201:7896"
MAX_TOKENS = 7_999
# λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ μ •μ˜ (ν•œκΈ€ν™”)
BLOG_TEMPLATES = {
"standard": "ν‘œμ€€ 8단계 ν”„λ ˆμž„μ›Œν¬ λΈ”λ‘œκ·Έ",
"tutorial": "단계별 νŠœν† λ¦¬μ–Ό ν˜•μ‹",
"review": "μ œν’ˆ/μ„œλΉ„μŠ€ 리뷰 ν˜•μ‹",
"storytelling": "μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹",
"seo_optimized": "SEO μ΅œμ ν™” λΈ”λ‘œκ·Έ"
}
BLOG_TONES = {
"professional": "전문적이고 곡식적인 톀",
"casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”μ²΄ 쀑심 톀",
"humorous": "μœ λ¨ΈλŸ¬μŠ€ν•œ μ ‘κ·Ό",
"storytelling": "이야기 μ€‘μ‹¬μ˜ μ ‘κ·Ό"
}
# 예제 λΈ”λ‘œκ·Έ 주제
EXAMPLE_TOPICS = {
"example1": "2025λ…„ 바뀐 뢀동산 μ„ΈκΈˆ μ œλ„: 일반 가정에 λ―ΈμΉ˜λŠ” 영ν–₯κ³Ό μ ˆμ„Έ μ „λž΅",
"example2": "2025λ…„ 여름 μ „κ΅­ 지역별 λŒ€ν‘œ μΆ•μ œ 총정리와 μˆ¨μ€ λͺ…μ†Œ μΆ”μ²œ",
"example3": "2025λ…„ μ£Όλͺ©ν•΄μ•Ό ν•  μ‹ μ„±μž₯ μ‚°μ—… 투자 κ°€μ΄λ“œ: 인곡지λŠ₯ κ΄€λ ¨ 발꡴ μ „λž΅"
}
# ──────────────────────────────── λ‘œκΉ… ──────────────────────────────────────
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
# ──────────────────────────────── Anthropic Client ─────────────────────────
client = anthropic.Anthropic(api_key=ANTHROPIC_KEY)
# ──────────────────────────────── λΈ”λ‘œκ·Έ μž‘μ„± μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ ────────────────
def get_system_prompt(template="standard", tone="professional", word_count=1750) -> str:
base_prompt = """
당신은 μ „λ¬Έ λΈ”λ‘œκ·Έ μž‘μ„± μ „λ¬Έκ°€μž…λ‹ˆλ‹€. λͺ¨λ“  λΈ”λ‘œκ·Έ κΈ€ μž‘μ„± μš”μ²­μ— λŒ€ν•΄ λ‹€μŒμ˜ 8단계 ν”„λ ˆμž„μ›Œν¬λ₯Ό μ² μ €νžˆ λ”°λ₯΄λ˜, μžμ—°μŠ€λŸ½κ³  λ§€λ ₯적인 글이 λ˜λ„λ‘ μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€:
λ…μž μ—°κ²° 단계
1.1. κ³΅κ°λŒ€ ν˜•μ„±μ„ μœ„ν•œ μΉœκ·Όν•œ 인사
1.2. λ…μžμ˜ μ‹€μ œ 고민을 λ°˜μ˜ν•œ λ„μž… 질문
1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적 관심 μœ λ„
문제 μ •μ˜ 단계
2.1. λ…μžμ˜ 페인포인트 ꡬ체화
2.2. 문제의 μ‹œκΈ‰μ„±κ³Ό 영ν–₯도 뢄석
2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
μ „λ¬Έμ„± μž…μ¦ 단계
3.1. 객관적 데이터 기반 뢄석
3.2. μ „λ¬Έκ°€ 견해와 연ꡬ κ²°κ³Ό 인용
3.3. μ‹€μ œ 사둀λ₯Ό ν†΅ν•œ 문제 ꡬ체화
μ†”λ£¨μ…˜ 제곡 단계
4.1. 단계별 μ‹€μ²œ κ°€μ΄λ“œλΌμΈ μ œμ‹œ
4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ ꡬ체적 팁
4.3. μ˜ˆμƒ μž₯μ• λ¬Όκ³Ό 극볡 λ°©μ•ˆ 포함
신뒰도 κ°•ν™” 단계
5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
5.2. ꡬ체적 μ‚¬μš©μž ν›„κΈ° 인용
5.3. 객관적 λ°μ΄ν„°λ‘œ 효과 μž…μ¦
행동 μœ λ„ 단계
6.1. λͺ…ν™•ν•œ 첫 μ‹€μ²œ 단계 μ œμ‹œ
6.2. μ‹œκΈ‰μ„±μ„ κ°•μ‘°ν•œ 행동 촉ꡬ
6.3. μ‹€μ²œ 동기 λΆ€μ—¬ μš”μ†Œ 포함
μ§„μ •μ„± κ°•ν™” 단계
7.1. μ†”λ£¨μ…˜μ˜ ν•œκ³„ 투λͺ…ν•˜κ²Œ 곡개
7.2. κ°œμΈλ³„ 차이 쑴재 인정
7.3. ν•„μš” 쑰건과 μ£Όμ˜μ‚¬ν•­ λͺ…μ‹œ
관계 지속 단계
8.1. μ§„μ •μ„± μžˆλŠ” 감사 인사
8.2. λ‹€μŒ 컨텐츠 예고둜 κΈ°λŒ€κ° μ‘°μ„±
8.3. μ†Œν†΅ 채널 μ•ˆλ‚΄
"""
# ν…œν”Œλ¦Ώλ³„ μΆ”κ°€ μ§€μΉ¨
template_guides = {
"tutorial": """
이 λΈ”λ‘œκ·ΈλŠ” νŠœν† λ¦¬μ–Ό ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- λͺ…ν™•ν•œ λͺ©ν‘œμ™€ μ΅œμ’… κ²°κ³Όλ¬Ό λ¨Όμ € μ œμ‹œ
- λ‹¨κ³„λ³„λ‘œ λͺ…ν™•ν•˜κ²Œ κ΅¬λΆ„λœ κ³Όμ • μ„€λͺ…
- 각 λ‹¨κ³„λ§ˆλ‹€ 이미지λ₯Ό μ‚½μž…ν•  μœ„μΉ˜ ν‘œμ‹œ
- μ˜ˆμƒ μ†Œμš” μ‹œκ°„κ³Ό λ‚œμ΄λ„ λͺ…μ‹œ
- ν•„μš”ν•œ λ„κ΅¬λ‚˜ 사전 지식 μ•ˆλ‚΄
- λ¬Έμ œν•΄κ²° 팁과 자주 λ°œμƒν•˜λŠ” μ‹€μˆ˜ 포함
- μ™„λ£Œ ν›„ λ‹€μŒ λ‹¨κ³„λ‚˜ μ‘μš©λ²• μ œμ•ˆ
""",
"review": """
이 λΈ”λ‘œκ·ΈλŠ” 리뷰 ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- 객관적 사싀과 주관적 평가 ꡬ뢄
- λͺ…ν™•ν•œ 평가 κΈ°μ€€ μ œμ‹œ
- μž₯점과 단점 κ· ν˜•μžˆκ²Œ μ„œμˆ 
- μœ μ‚¬ μ œν’ˆ/μ„œλΉ„μŠ€μ™€ 비ꡐ
- λˆ„κ΅¬μ—κ²Œ μ ν•©ν•œμ§€ νƒ€κ²Ÿ μ„€λͺ…
- ꡬ체적인 μ‚¬μš© κ²½ν—˜κ³Ό κ²°κ³Ό 포함
- μ΅œμ’… μΆ”μ²œ 여뢀와 λŒ€μ•ˆ μ œμ‹œ
""",
"storytelling": """
이 λΈ”λ‘œκ·ΈλŠ” μŠ€ν† λ¦¬ν…”λ§ ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- μ‹€μ œ μΈλ¬Όμ΄λ‚˜ μ‚¬λ‘€λ‘œ μ‹œμž‘
- 문제 상황과 감정적 μ—°κ²° κ°•ν™”
- κ°ˆλ“±κ³Ό ν•΄κ²°κ³Όμ • μ€‘μ‹¬μ˜ λ‚΄λŸ¬ν‹°λΈŒ
- κ΅ν›ˆκ³Ό 배움을 μžμ—°μŠ€λŸ½κ²Œ 포함
- λ…μžκ°€ 곡감할 수 μžˆλŠ” 감정선 μœ μ§€
- 이야기와 μœ μš©ν•œ μ •λ³΄μ˜ κ· ν˜• μœ μ§€
- λ…μžμ—κ²Œ μžμ‹ μ˜ 이야기λ₯Ό μƒκ°ν•΄λ³΄κ²Œ μœ λ„
""",
"seo_optimized": """
이 λΈ”λ‘œκ·ΈλŠ” SEO μ΅œμ ν™” ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”:
- 핡심 ν‚€μ›Œλ“œλ₯Ό 제λͺ©, μ†Œμ œλͺ©, 첫 단락에 배치
- κ΄€λ ¨ ν‚€μ›Œλ“œλ₯Ό μžμ—°μŠ€λŸ½κ²Œ 본문에 λΆ„μ‚°
- 300-500자 λΆ„λŸ‰μ˜ λͺ…ν™•ν•œ 단락 ꡬ성
- 질문 ν˜•μ‹μ˜ μ†Œμ œλͺ© ν™œμš©
- λͺ©λ‘, ν‘œ, κ°•μ‘° ν…μŠ€νŠΈ λ“± λ‹€μ–‘ν•œ μ„œμ‹ ν™œμš©
- λ‚΄λΆ€ 링크 μ‚½μž… μœ„μΉ˜ ν‘œμ‹œ
- 2000-3000자 μ΄μƒμ˜ μΆ©λΆ„ν•œ μ½˜ν…μΈ  제곡
"""
}
# 톀별 μΆ”κ°€ μ§€μΉ¨
tone_guides = {
"professional": "전문적이고 κΆŒμœ„μžˆλŠ” μ–΄μ‘°λ‘œ μž‘μ„±ν•˜λ˜, μ „λ¬Έ μš©μ–΄λŠ” 적절히 μ„€λͺ…ν•΄ μ£Όμ„Έμš”. 데이터와 연ꡬ κ²°κ³Όλ₯Ό μ€‘μ‹¬μœΌλ‘œ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
"casual": "μΉœκ·Όν•˜κ³  λŒ€ν™”ν•˜λ“― νŽΈμ•ˆν•œ μ–΄μ‘°λ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. '~λ„€μš”', '~ν•΄μš”' 같은 λŒ€ν™”μ²΄λ₯Ό μ‚¬μš©ν•˜κ³ , 개인적 κ²½ν—˜κ³Ό λΉ„μœ λ₯Ό 톡해 λ‚΄μš©μ„ μ „λ‹¬ν•˜μ„Έμš”.",
"humorous": "μœ λ¨Έμ™€ μž¬μΉ˜μžˆλŠ” ν‘œν˜„μ„ 적절히 ν™œμš©ν•΄ μ£Όμ„Έμš”. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ μ˜ˆμ‹œ, κ°€λ²Όμš΄ 농담을 ν¬ν•¨ν•˜λ˜, μ •λ³΄μ˜ μ •ν™•μ„±κ³Ό μœ μš©μ„±μ€ μœ μ§€ν•˜μ„Έμš”.",
"storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― 감성적이고 λͺ°μž…감 μžˆλŠ” ν†€μœΌλ‘œ μž‘μ„±ν•΄ μ£Όμ„Έμš”. 인물, λ°°κ²½, κ°ˆλ“±, 해결과정이 λ‹΄κΈ΄ λ‚΄λŸ¬ν‹°λΈŒ ꡬ쑰λ₯Ό ν™œμš©ν•˜μ„Έμš”."
}
# μ΅œμ’… ν”„λ‘¬ν”„νŠΈ μ‘°ν•©
final_prompt = base_prompt
# μ„ νƒλœ ν…œν”Œλ¦Ώ μ§€μΉ¨ μΆ”κ°€
if template in template_guides:
final_prompt += "\n" + template_guides[template]
# μ„ νƒλœ 톀 μ§€μΉ¨ μΆ”κ°€
if tone in tone_guides:
final_prompt += f"\n\nν†€μ•€λ§€λ„ˆ: {tone_guides[tone]}"
# κΈ€μž 수 μ§€μΉ¨ μΆ”κ°€
final_prompt += f"\n\nμž‘μ„± μ‹œ μ€€μˆ˜μ‚¬ν•­\n9.1. κΈ€μž 수: {word_count-250}-{word_count+250}자 λ‚΄μ™Έ\n9.2. 문단 길이: 3-4λ¬Έμž₯ 이내\n9.3. μ‹œκ°μ  ꡬ뢄: μ†Œμ œλͺ©, ꡬ뢄선, 번호 λͺ©λ‘ ν™œμš©\n9.4. 데이터: λͺ¨λ“  μ •λ³΄μ˜ 좜처 λͺ…μ‹œ\n9.5. 가독성: λͺ…ν™•ν•œ 단락 ꡬ뢄과 강쑰점 μ‚¬μš©"
return final_prompt
# ──────────────────────────────── Brave Search API ─────────────────────────
def brave_search(query: str, count: int = 20): # 기본값을 20으둜 λ³€κ²½
"""
Brave Web Search API 호좜 β†’ list[dict]
λ°˜ν™˜ ν•„λ“œ: index, title, link, snippet, displayed_link
"""
if not BRAVE_KEY:
raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) ν™˜κ²½λ³€μˆ˜κ°€ λΉ„μ–΄ μžˆμŠ΅λ‹ˆλ‹€.")
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": BRAVE_KEY
}
params = {"q": query, "count": str(count)} # 카운트 νŒŒλΌλ―Έν„° 전달
for attempt in range(3): # μ΅œλŒ€ 3번 μž¬μ‹œλ„
try:
r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
r.raise_for_status()
data = r.json()
# κ²°κ³Ό ν˜•μ‹ 확인 및 λ‘œκΉ…
logging.info(f"Brave 검색 κ²°κ³Ό 데이터 ꡬ쑰: {list(data.keys())}")
raw = data.get("web", {}).get("results") or data.get("results", [])
if not raw:
logging.warning(f"Brave 검색 κ²°κ³Ό μ—†μŒ. 응닡: {data}")
raise ValueError("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€")
arts = []
for i, res in enumerate(raw[:count], 1): # count만큼 반볡
url = res.get("url", res.get("link", ""))
host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
arts.append({
"index": i,
"title": res.get("title", "제λͺ© μ—†μŒ"),
"link": url,
"snippet": res.get("description", res.get("text", "λ‚΄μš© μ—†μŒ")),
"displayed_link": host
})
logging.info(f"Brave 검색 성곡: {len(arts)}개 κ²°κ³Ό")
return arts
except Exception as e:
logging.error(f"Brave 검색 μ‹€νŒ¨ (μ‹œλ„ {attempt+1}/3): {e}")
if attempt < 2: # λ§ˆμ§€λ§‰ μ‹œλ„κ°€ μ•„λ‹ˆλ©΄ λŒ€κΈ° ν›„ μž¬μ‹œλ„
time.sleep(2)
return [] # λͺ¨λ“  μ‹œλ„ μ‹€νŒ¨ μ‹œ 빈 λͺ©λ‘ λ°˜ν™˜
def mock_results(query: str) -> str:
"""검색 API μ‹€νŒ¨ μ‹œ 가상 검색 κ²°κ³Ό 제곡"""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return (f"# 검색 κ²°κ³Ό λŒ€μ²΄ λ‚΄μš© (생성: {ts})\n\n"
f"검색 API 호좜이 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. 주제 '{query}'에 λŒ€ν•΄ κΈ°μ‘΄ 지식을 ν™œμš©ν•΄ λ‹΅λ³€ν•΄ μ£Όμ„Έμš”.\n\n"
f"λ‹€μŒ λ‚΄μš©μ΄ 도움이 될 수 μžˆμŠ΅λ‹ˆλ‹€:\n\n"
f"- {query}의 κΈ°λ³Έ κ°œλ…κ³Ό μ€‘μš”μ„±\n"
f"- 일반적으둜 μ•Œλ €μ§„ κ΄€λ ¨ 톡계와 νŠΈλ Œλ“œ\n"
f"- ν•΄λ‹Ή μ£Όμ œμ— λŒ€ν•œ μ „λ¬Έκ°€λ“€μ˜ 일반적인 견해\n"
f"- λ…μžλ“€μ΄ μ‹€μ œλ‘œ κΆκΈˆν•΄ν•  λ§Œν•œ μ§ˆλ¬Έλ“€\n\n"
f"μ°Έκ³ : 이 λ‚΄μš©μ€ μ‹€μ‹œκ°„ 검색 κ²°κ³Όκ°€ μ•„λ‹Œ λŒ€μ²΄ μ•ˆλ‚΄μž…λ‹ˆλ‹€.\n\n")
def do_web_search(query: str) -> str:
"""μ›Ή 검색 μˆ˜ν–‰ 및 κ²°κ³Ό ν¬λ§·νŒ…"""
try:
arts = brave_search(query, 20) # 여기도 20으둜 λ³€κ²½
if not arts:
logging.warning("검색 κ²°κ³Ό μ—†μŒ, λŒ€μ²΄ μ½˜ν…μΈ  μ‚¬μš©")
return mock_results(query)
hdr = "# μ›Ή 검색 κ²°κ³Ό\nμ•„λž˜ 정보λ₯Ό μ°Έκ³ ν•΄μ„œ λ‹΅λ³€ν•˜μ„Έμš”.\n\n"
body = "\n".join(
f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
f"**좜처**: [{a['displayed_link']}]({a['link']})\n\n---\n"
for a in arts
)
return hdr + body
except Exception as e:
logging.error(f"μ›Ή 검색 전체 ν”„λ‘œμ„ΈμŠ€ μ‹€νŒ¨: {str(e)}")
return mock_results(query)
# ──────────────────────────────── 이미지 Β· λ³€ν™˜ μœ ν‹Έ ────────────────────────
def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
if not prompt: return None, "ν”„λ‘¬ν”„νŠΈ λΆ€μ‘±"
try:
res = Client(IMAGE_API_URL).predict(
prompt=prompt, width=w, height=h, guidance=g,
inference_steps=steps, seed=seed,
do_img2img=False, init_image=None,
image2image_strength=0.8, resize_img=True,
api_name="/generate_image")
return res[0], f"Seed: {res[1]}"
except Exception as e:
logging.error(e); return None, str(e)
def extract_image_prompt(blog: str, topic: str):
sys = f"λ‹€μŒ κΈ€λ‘œλΆ€ν„° μ˜μ–΄ 1쀄 이미지 ν”„λ‘¬ν”„νŠΈ 생성:\n{topic}"
try:
res = client.messages.create(
model="claude-3-7-sonnet-20250219",
max_tokens=80, system=sys,
messages=[{"role": "user", "content": blog}]
)
return res.content[0].text.strip()
except Exception:
return f"A professional photo related to {topic}, high quality"
def md_to_html(md: str, title="Ginigen Blog"):
return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
def keywords(text: str, top=5):
return " ".join(re.sub(r"[^κ°€-힣a-zA-Z0-9\\s]", "", text).split()[:top])
# ──────────────────────────────── Streamlit UI ────────────────────────────
def ginigen_app():
st.title("μ§€λ‹ˆμ   λΈ”λ‘œκ·Έ")
# μ„Έμ…˜ κΈ°λ³Έκ°’ - μ„Έμ…˜ μƒνƒœκ°€ 이미 μžˆλŠ” 경우 μ„€μ •ν•˜μ§€ μ•ŠμŒ
if "ai_model" not in st.session_state:
st.session_state.ai_model = "claude-3-7-sonnet-20250219"
if "messages" not in st.session_state:
st.session_state.messages = []
if "auto_save" not in st.session_state:
st.session_state.auto_save = True
if "generate_image" not in st.session_state:
st.session_state.generate_image = False
if "use_web_search" not in st.session_state:
st.session_state.use_web_search = False
if "blog_template" not in st.session_state:
st.session_state.blog_template = "standard"
if "blog_tone" not in st.session_state:
st.session_state.blog_tone = "professional"
if "word_count" not in st.session_state:
st.session_state.word_count = 1750
# ── μ‚¬μ΄λ“œλ°” 컨트둀
sb = st.sidebar
sb.title("λΈ”λ‘œκ·Έ μ„€μ •")
# λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ 및 μŠ€νƒ€μΌ 선택
sb.subheader("λΈ”λ‘œκ·Έ μŠ€νƒ€μΌ μ„€μ •")
sb.selectbox("λΈ”λ‘œκ·Έ ν…œν”Œλ¦Ώ", options=list(BLOG_TEMPLATES.keys()),
format_func=lambda x: BLOG_TEMPLATES[x],
key="blog_template")
sb.selectbox("λΈ”λ‘œκ·Έ 톀", options=list(BLOG_TONES.keys()),
format_func=lambda x: BLOG_TONES[x],
key="blog_tone")
sb.slider("λΈ”λ‘œκ·Έ 길이 (단어 수)", 800, 3000, key="word_count")
# 예제 주제 선택
sb.subheader("예제 주제")
col1, col2, col3 = sb.columns(3)
# μˆ˜μ •: 예제 선택 μ‹œ 직접 μ²˜λ¦¬ν•˜λ„λ‘ λ³€κ²½
if col1.button("뢀동산 μ„ΈκΈˆ", key="ex1"):
# 예제 주제λ₯Ό μž…λ ₯으둜 μ¦‰μ‹œ 처리 (rerun 없이)
process_example(EXAMPLE_TOPICS["example1"])
if col2.button("여름 μΆ•μ œ", key="ex2"):
process_example(EXAMPLE_TOPICS["example2"])
if col3.button("투자 κ°€μ΄λ“œ", key="ex3"):
process_example(EXAMPLE_TOPICS["example3"])
sb.subheader("기타 μ„€μ •")
sb.toggle("μžλ™ μ €μž₯", key="auto_save")
sb.toggle("이미지 μžλ™ 생성", key="generate_image")
# μ›Ή 검색 ν† κΈ€ (λͺ¨λ‹ˆν„°λ§μ„ μœ„ν•΄ μœ μ§€ν•˜λ˜ 기본값은 False)
search_enabled = sb.toggle("μ›Ή 검색 μ‚¬μš©", value=False, key="use_web_search")
if search_enabled:
st.warning("⚠️ μ›Ή 검색 κΈ°λŠ₯은 ν˜„μž¬ λΆˆμ•ˆμ •ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 검색 κ²°κ³Όκ°€ μ—†μœΌλ©΄ κΈ°λ³Έ μ§€μ‹μœΌλ‘œ λŒ€μ²΄λ©λ‹ˆλ‹€.")
# ── 졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ (λ§ˆν¬λ‹€μš΄ / HTML)
latest_blog = next(
(m["content"] for m in reversed(st.session_state.messages)
if m["role"] == "assistant" and m["content"].strip()), None)
if latest_blog:
title = re.search(r"# (.*?)(\n|$)", latest_blog)
title = title.group(1).strip() if title else "blog"
sb.subheader("졜근 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
c1, c2 = sb.columns(2)
c1.download_button("λ§ˆν¬λ‹€μš΄", latest_blog,
file_name=f"{title}.md", mime="text/markdown")
c2.download_button("HTML", md_to_html(latest_blog, title),
file_name=f"{title}.html", mime="text/html")
# ── JSON λŒ€ν™” 기둝 μ—…λ‘œλ“œ
up = sb.file_uploader("λŒ€ν™” 기둝 뢈러였기 (.json)", type=["json"])
if up:
try:
st.session_state.messages = json.load(up)
sb.success("λŒ€ν™” 기둝 뢈러였기 μ™„λ£Œ")
except Exception as e:
sb.error(f"뢈러였기 μ‹€νŒ¨: {e}")
# ── JSON λŒ€ν™” 기둝 λ‹€μš΄λ‘œλ“œ
if sb.button("λŒ€ν™” 기둝 JSON λ‹€μš΄λ‘œλ“œ"):
sb.download_button("μ €μž₯", json.dumps(st.session_state.messages,
ensure_ascii=False, indent=2),
file_name="chat_history.json",
mime="application/json")
# ── κΈ°μ‘΄ λ©”μ‹œμ§€ λ Œλ”λ§
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
if "image" in m:
st.image(m["image"], caption=m.get("image_caption", ""))
# ── μ‚¬μš©μž μž…λ ₯ 처리
prompt = st.chat_input("무엇을 λ„μ™€λ“œλ¦΄κΉŒμš”?")
if prompt:
process_input(prompt)
def process_example(topic):
"""예제 주제λ₯Ό 직접 μ²˜λ¦¬ν•˜λŠ” ν•¨μˆ˜ (rerun 없이)"""
process_input(topic)
def process_input(prompt):
"""μ‚¬μš©μž μž…λ ₯ 처리 ν•¨μˆ˜ (일반 μž…λ ₯κ³Ό 예제 μž…λ ₯ λͺ¨λ‘ 처리)"""
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"): st.markdown(prompt)
with st.chat_message("assistant"):
placeholder = st.empty(); answer = ""
# μ„ νƒλœ ν…œν”Œλ¦Ώ, 톀, 단어 수둜 μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ 생성
sys_prompt = get_system_prompt(
template=st.session_state.blog_template,
tone=st.session_state.blog_tone,
word_count=st.session_state.word_count
)
if st.session_state.use_web_search:
with st.spinner("μ›Ή 검색 쀑…"):
search_md = do_web_search(keywords(prompt))
sys_prompt += f"\n\n검색 κ²°κ³Ό:\n{search_md}\n"
# Claude 슀트리밍
with client.messages.stream(
model=st.session_state.ai_model, max_tokens=MAX_TOKENS,
system=sys_prompt,
messages=[{"role": m["role"], "content": m["content"]}
for m in st.session_state.messages]
) as stream:
for t in stream.text_stream:
answer += t or ""
placeholder.markdown(answer + "β–Œ")
placeholder.markdown(answer)
# 이미지 μ˜΅μ…˜
answer_entry_saved = False
if st.session_state.generate_image:
with st.spinner("이미지 생성 쀑…"):
ip = extract_image_prompt(answer, prompt)
img, cap = generate_image(ip)
if img:
st.image(img, caption=cap)
st.session_state.messages.append(
{"role": "assistant", "content": answer,
"image": img, "image_caption": cap})
answer_entry_saved = True
if not answer_entry_saved:
st.session_state.messages.append(
{"role": "assistant", "content": answer})
# λ³Έλ¬Έ λ‹€μš΄λ‘œλ“œ λ²„νŠΌ (MD / HTML)
st.subheader("이 λΈ”λ‘œκ·Έ λ‹€μš΄λ‘œλ“œ")
b1, b2 = st.columns(2)
b1.download_button("λ§ˆν¬λ‹€μš΄", answer,
file_name=f"{prompt[:30]}.md", mime="text/markdown")
b2.download_button("HTML", md_to_html(answer, prompt[:30]),
file_name=f"{prompt[:30]}.html", mime="text/html")
# ── μžλ™ λ°±μ—… μ €μž₯
if st.session_state.auto_save and st.session_state.messages:
try:
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
with open(fn, "w", encoding="utf-8") as fp:
json.dump(st.session_state.messages, fp,
ensure_ascii=False, indent=2)
except Exception as e:
logging.error(f"μžλ™ μ €μž₯ μ‹€νŒ¨: {e}")
# ──────────────────────────────── main / requirements ──────────────────────
def main(): ginigen_app()
if __name__ == "__main__":
# requirements.txt 동적 생성
with open("requirements.txt", "w") as f:
f.write("\n".join([
"streamlit>=1.31.0",
"anthropic>=0.18.1",
"gradio-client>=1.8.0",
"requests>=2.32.3",
"markdown>=3.5.1",
"pillow>=10.1.0"
]))
main()