# ──────────────────────────────── 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"