Spaces:
Running
Running
| # ββββββββββββββββββββββββββββββββ 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() |