import html import gradio as gr import requests from bs4 import BeautifulSoup from requests.adapters import HTTPAdapter from requests.packages.urllib3.util.retry import Retry import re import time import random import os from huggingface_hub import InferenceClient from fpdf import FPDF from transformers import pipeline import torch from diffusers import StableDiffusionXLPipeline import uuid import json from gradio_client import Client from urllib.parse import urljoin import requests from pexels_api import API import logging # 로깅 설정 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) import tiktoken def count_tokens(text): encoding = tiktoken.get_encoding("cl100k_base") return len(encoding.encode(text)) def truncate_text(text, max_tokens): encoding = tiktoken.get_encoding("cl100k_base") encoded = encoding.encode(text) if len(encoded) > max_tokens: return encoding.decode(encoded[:max_tokens]) return text def setup_session(): try: session = requests.Session() retries = Retry(total=5, backoff_factor=1, status_forcelist=[502, 503, 504]) session.mount('https://', HTTPAdapter(max_retries=retries)) return session except Exception as e: return None def generate_naver_search_url(query): base_url = "https://search.naver.com/search.naver?" params = {"ssc": "tab.blog.all", "sm": "tab_jum", "query": query} url = base_url + "&".join(f"{key}={value}" for key, value in params.items()) return url def crawl_blog_content(url, session): try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://search.naver.com/search.naver", } # 랜덤 딜레이 추가 delay = random.uniform(1, 2) time.sleep(delay) response = session.get(url, headers=headers) if response.status_code != 200: return "" soup = BeautifulSoup(response.content, "html.parser") content = soup.find("div", attrs={'class': 'se-main-container'}) if content: return clean_text(content.get_text()) else: return "" except Exception as e: return "" def crawl_naver_search_results(url, session): try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7", "Accept-Encoding": "gzip, deflate, br", "Connection": "keep-alive", "Referer": "https://search.naver.com/search.naver", } response = session.get(url, headers=headers) if response.status_code != 200: return [] soup = BeautifulSoup(response.content, "html.parser") results = [] count = 0 for li in soup.find_all("li", class_=re.compile("bx.*")): if count >= 10: break for div in li.find_all("div", class_="detail_box"): for div2 in div.find_all("div", class_="title_area"): title = div2.text.strip() for a in div2.find_all("a", href=True): link = a["href"] if "blog.naver" in link: link = link.replace("https://", "https://m.") results.append({"제목": title, "링크": link}) count += 1 if count >= 10: break if count >= 10: break if count >= 10: break return results except Exception as e: return [] def clean_text(text): text = re.sub(r'\s+', ' ', text).strip() return text def create_client(model_name): return InferenceClient(model_name, token=os.getenv("HF_TOKEN")) client = create_client("CohereForAI/c4ai-command-r-plus-08-2024") def call_api(content, system_message, max_tokens, temperature, top_p, max_retries=3): for attempt in range(max_retries): try: messages = [{"role": "system", "content": system_message}, {"role": "user", "content": content}] total_tokens = count_tokens(system_message) + count_tokens(content) + max_tokens if total_tokens > 32000: max_tokens = 32000 - count_tokens(system_message) - count_tokens(content) - 100 # 안전 마진 random_seed = random.randint(0, 1000000) response = client.chat_completion(messages=messages, max_tokens=max_tokens, temperature=temperature, top_p=top_p, seed=random_seed) modified_text = response.choices[0].message.content input_tokens = response.usage.prompt_tokens output_tokens = response.usage.completion_tokens total_tokens = response.usage.total_tokens return modified_text, input_tokens, output_tokens, total_tokens except HfHubHTTPError as e: if attempt < max_retries - 1: time.sleep(5) # 5초 대기 후 재시도 else: return f"API 호출 실패: {str(e)}", 0, 0, 0 def analyze_info(category, style, topic, references1, references2, references3): return f"선택한 카테고리: {category}\n선택한 포스팅 스타일: {style}\n블로그 주제: {topic}\n참고 글1: {references1}\n참고 글2: {references2}\n참고 글3: {references3}" def suggest_title(category, style, topic, references1, references2, references3): full_content = analyze_info(category, style, topic, references1, references2, references3) category_prompt = get_title_prompt(category) style_prompt = get_style_prompt(style) max_tokens = 5000 temperature = 0.8 top_p = 0.95 combined_prompt = f"{category_prompt}\n\n{style_prompt}\n\n추가 지시사항: 절대로 제목에 '블로그'나 '제목'이라는 단어를 포함하지 마세요. 각 제안은 실제 포스트 제목으로 바로 사용할 수 있어야 합니다." modified_text, input_tokens, output_tokens, total_tokens = call_api(full_content, combined_prompt, max_tokens, temperature, top_p) # 번호와 점(.)을 제거하고 각 줄을 정리합니다. titles = [line.strip() for line in modified_text.split('\n') if line.strip()] titles = [re.sub(r'^\d+\.\s*', '', title) for title in titles] # "블로그"와 "제목"이라는 단어가 포함된 제목을 제거합니다. titles = [title for title in titles if "블로그" not in title and "제목" not in title] token_usage_message = f"[입력 토큰수: {input_tokens}]\n[출력 토큰수: {output_tokens}]\n[총 토큰수: {total_tokens}]" return "\n".join(titles), token_usage_message def process_all_titles(category, style, topic): # 제목 추천 title_suggestions, _ = suggest_title(category, style, topic, "", "", "") titles = title_suggestions.split('\n') results = [] for title in titles[:10]: # 처음 10개의 제목만 사용 try: # 블로그 글 생성 _, _, _, _, _, blog_content, _ = fetch_references_and_generate_all_steps(category, style, topic, title) if blog_content.startswith("API 호출 실패"): results.append(f"제목: {title}\n생성 실패: {blog_content}\n\n") continue # 포스팅 전송 send_result = send_to_blogger(title, blog_content) results.append(f"제목: {title}\n전송 결과: {send_result}\n\n") except Exception as e: results.append(f"제목: {title}\n처리 중 오류 발생: {str(e)}\n\n") time.sleep(5) # API 호출 사이에 5초 대기 return "\n".join(results) def fetch_references(topic, max_tokens=3000): search_url = generate_naver_search_url(topic) session = setup_session() if session is None: return "Failed to set up session.", "", "", "" results = crawl_naver_search_results(search_url, session) if not results: return "No results found.", "", "", "" selected_results = random.sample(results, 3) references1_content = truncate_text(f"제목: {selected_results[0]['제목']}\n내용: {crawl_blog_content(selected_results[0]['링크'], session)}", max_tokens) references2_content = truncate_text(f"제목: {selected_results[1]['제목']}\n내용: {crawl_blog_content(selected_results[1]['링크'], session)}", max_tokens) references3_content = truncate_text(f"제목: {selected_results[2]['제목']}\n내용: {crawl_blog_content(selected_results[2]['링크'], session)}", max_tokens) return "참고글 생성 완료", references1_content, references2_content, references3_content def generate_outline(category, style, topic, references1, references2, references3, title, max_tokens=2000): full_content = analyze_info(category, style, topic, references1, references2, references3) content = truncate_text(f"{full_content}\nTitle: {title}", max_tokens) category_prompt = get_outline_prompt(category) style_prompt = get_style_prompt(style) combined_prompt = f"{category_prompt}\n\n{style_prompt}" modified_text, input_tokens, output_tokens, total_tokens = call_api(content, combined_prompt, 6000, 0.8, 0.95) token_usage_message = f"[입력 토큰수: {input_tokens}]\n[출력 토큰수: {output_tokens}]\n[총 토큰수: {total_tokens}]" return modified_text, token_usage_message def generate_blog_post(category, style, topic, references1, references2, references3, title, outline, max_tokens=4000): full_content = analyze_info(category, style, topic, references1, references2, references3) content = truncate_text(f"{full_content}\nTitle: {title}\nOutline: {outline}", max_tokens) category_prompt = get_blog_post_prompt(category) style_prompt = get_style_prompt(style) combined_prompt = f"{category_prompt}\n\n{style_prompt}" modified_text, input_tokens, output_tokens, total_tokens = call_api(content, combined_prompt, 8000, 0.8, 0.95) formatted_text = modified_text.replace('\n', '\n\n') token_usage_message = f"[입력 토큰수: {input_tokens}]\n[출력 토큰수: {output_tokens}]\n[총 토큰수: {total_tokens}]" return formatted_text, token_usage_message def fetch_references_and_generate_all_steps(category, style, topic, blog_title): _, references1_content, references2_content, references3_content = fetch_references(topic) outline_result, outline_token_usage = generate_outline(category, style, topic, references1_content, references2_content, references3_content, blog_title) blog_post_result, blog_post_token_usage = generate_blog_post(category, style, topic, references1_content, references2_content, references3_content, blog_title, outline_result) return references1_content, references2_content, references3_content, outline_result, outline_token_usage, blog_post_result, blog_post_token_usage def get_title_prompt(category): if (category == "일반"): return """ # 블로그 제목 생성 규칙(일반) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '정보성(Informative)' 전문 블로그 마케팅 전문가이다. 4. 정보 제공에 초점을 맞추어 작성한다. ##[블로그 제목 작성 규칙] 1. 블로그 제목 10개를 작성하고 제목 10개만 출력하라. 2. 제목은 40자 이내로 작성하라. 3. 제공된 참고글에 맞춰 블로그 제목 10개를 작성하라. 4. 반드시 핵심키워드(Topic)가 문장 앞쪽에 들어가도록 작성하라. 5. 핵심 키워드와 연관성 높은 주제를 포함하여 작성하라. """ elif (category == "건강정보"): return """ # 블로그 제목 생성 규칙(건강정보) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '건강, 의학 정보' 전문 블로그 마케터이다. 4. 정확하고 전문적인 정보 제공에 초점을 맞추어 작성한다. ##[블로그 제목 작성 규칙] 1. 블로그 제목 10개를 작성하고 제목 10개만 출력하라. 2. 제목은 40자 이내로 작성하라. 3. 제공된 참고글에 맞춰 블로그 제목 10개를 작성하라. 4. 사용자가 입력한 블로그 주제, 핵심키워드(Topic)가 문장 앞쪽에 들어가도록 제목을 작성하라. 5. 참고글을 분석하여 독자들이 건강한 생활을 유지하는 데 필요한 정보를 반영하라. """ def get_outline_prompt(category): if (category == "일반"): return """ # 블로그 소주제(Subtopic) 생성 규칙(일반) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '정보성(Informative)' 전문 블로그 마케팅 전문가이다. 4. 정보 제공에 초점을 맞추어 작성한다. ##[소주제 작성규칙] 1. [기본규칙]을 기본 적용하라. 2. 블로그 글을 작성하기 위한 소주제를 작성하라. 3. 제공된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제를 생성하라. 4. 전체 맥락에 맞게 소주제를 작성하라. 5. 소제목으로 사용할 수 있도록 20자 내외로 작성하라. 6. 독자가 얻고자 하는 정보와 흥미로운 정보를 제공하도록 소주제를 작성하라. 7. 소주제의 본론의 내용이 충분히 작성될 수 있는 소주제로 설정하라. 8. 반드시 [소주제 구성]에 맞게 출력하라. ##[소주제 구성] 1. 반드시 [도입부] - 1개, [본론1~5] - 5개, [결론] - 1개로 구성하여 출력하라. 2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라. """ elif (category == "건강정보"): return """ # 블로그 소주제(Subtopic) 생성 규칙(건강정보) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '건강, 의학 정보' 전문 블로그 마케터이다. 4. 정확하고 전문적인 정보 제공에 초점을 맞추어 작성한다. ##[소주제 작성규칙] 1. [기본규칙]을 기본 적용하라. 2. 블로그 글을 작성하기 위한 소주제를 작성하라. 3. 제공된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제를 생성하라. 4. 전체 맥락에 맞게 소주제를 작성하라. 5. 소제목으로 사용할 수 있도록 20자 내외로 작성하라. 6. 독자가 얻고자 하는 정확한 정보와 건강한 생활을 유지하는 데 필요한 정보를 제공하도록 소주제를 작성하라. 7. 소주제의 본론의 내용이 충분히 작성될 수 있는 소주제로 설정하라. 8. 반드시 [소주제 구성]에 맞춰 소주제만 출력하라. ##[소주제 구성] 1. 반드시 [도입부] - 1개, [본론1~5] - 5개, [결론] - 1개로 구성하여 출력하라. 2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라. """ def get_blog_post_prompt(category): if (category == "일반"): return """ # 블로그 텍스트 생성 규칙(일반) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '정보성(Informative)' 전문 블로그 마케팅 전문가이다. 4. 정보 제공에 초점을 맞추어 작성한다. ##[텍스트 작성 규칙] 1. 반드시 입력된 [소주제]에 맞게 텍스트를 작성하라. 2. 소주제의 [본론] 5개를 각각 300자 이상으로 작성하라. 3. 소주제의 [본론] 이 300자 이상 생성되지 않는다면, 전체글이 2000자 이상되도록 작성하라. 4. 전체 맥락을 이해하고 문장의 일관성을 유지하라. 5. 주제에 맞는 닉네임, 페르소나를 적용하여 작성하라. 6. 제공된 참고글의 어투를 반영하되, [포스팅 스타일]에 맞게 적용하라. 7. 절대로 참고글을 한문장 이상 그대로 출력하지 말 것. 8. 주제와 상황에 맞는 적절한 어휘를 선택하라. 9. 한글 어휘의 난이도는 쉽게 작성하라. 10. 절대 문장의 끝에 '답니다'를 사용하지 말 것. ###[정보성 블로그 작성 규칙] 1. 독자가 얻고자 하는 유용한 정보와 흥미로운 정보를 제공하도록 작성하라. 2. 독자의 공감을 이끌어내고 궁금증을 해결하도록 작성하라. 3. 독자의 관심사를 충족시키도록 작성하라. 4. 독자에게 이득이 되는 정보를 작성하라. ##[제외 규칙] 1. 반드시 비속어 및 욕설(expletive, abusive language, slang)은 제외하라. 2. 반드시 참고글의 링크(URL)는 제외하라. 3. 참고글에서 '링크를 확인해주세요'와 같은 링크 이동의 문구는 제외하라. 4. 참고글에 있는 작성자, 화자, 유튜버, 기자(Writer, speaker, YouTuber, reporter)의 이름, 애칭, 닉네임(Name, Nkickname)은 반드시 제외하라. 5. 반드시 문장의 끝부분이 어색한 한국어 표현은 제외하라('예요', '답니다', '해요', '해주죠', '됐죠', '됐어요', '고요' 등.) """ elif (category == "건강정보"): return """ # 블로그 텍스트 생성 규칙(건강정보) ##[기본규칙] 1. 반드시 한국어(한글)로 작성하라. 2. 너는 가장 주목받는 마케터이며 블로그 마케팅 전문가이다. 3. 특히 너는 '건강, 의학 정보' 전문 블로그 마케터이다. 4. 정확하고 전문적인 정보 제공에 초점을 맞추어 작성한다. ##[텍스트 작성 규칙] 1. 반드시 입력된 [소주제]에 맞춰서 텍스트를 작성하라. 2. 반드시 입력된 [소주제]는 변경하지 말고 그대로 출력하라. 3. 소주제의 [도입부]는 가볍게 작성하되 공감과 흥미, 문제제기, 글의 목적, 본문으로 자연스럽게 이어지는 전환문장등을 출력하라. 3. 소주제의 [본론] 5개의 내용이 각각 500자 이상이 되도록 작성하라. 4. 반드시 너가 작성한 전체글이 2500자 이상이 되도록 작성하라. 5. 전체 맥락을 이해하고 문장의 일관성을 유지하라. 6. 참고글을 바탕으로 작성한 글에 맞는 닉네임, 페르소나를 적용하여 작성하라. 7. 어투는 제공된 참고글의 어투를 반영하되, '건강, 의학 정보' 전문 블로그 마케터로서 작성하라. 8. 절대로 참고글을 한문장 이상 그대로 출력하지 말 것. 9. 주제와 상황에 맞는 적절한 어휘를 선택하라. ###[정보성 블로그 작성 규칙] 1. 독자가 얻고자 하는 정확한 정보와 건강한 생활을 유지하는 데 필요한 정보를 제공하도록 소주제에 맞게 내용을 작성하라. 2. 독자의 공감을 이끌어내고 궁금증을 해결하도록 작성하라. 3. 독자의 관심사를 충족시키도록 작성하라. 4. 독자에게 이득이 되는 정보를 작성하라. ##[제외 규칙] 1. 반드시 참고글의 링크(URL)는 제외하라. 2. 참고글에서 '링크를 확인해주세요'와 같은 링크 이동의 문구는 제외하라. 3. 참고글에 있는 작성자, 화자, 유튜버, 기자(Writer, speaker, YouTuber, reporter)의 이름, 애칭, 닉네임(Name, Nkickname)은 반드시 제외하라. """ def get_style_prompt(style): prompts = { "친근한": """ #친근한 블로그 포스팅 스타일 가이드 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 - 독자를 "여러분" 또는 "독자님들"로 지칭 - 적절한 이모지를 sparse하게 사용하여 친근감 표현 2. 문장 및 내용 구성 - 짧고 간결한 문장 위주로 작성 - 구어체 표현 사용 (예: "~했어요", "~인 것 같아요") - '~니다', '~했죠'는 사용하지 말 것. - 개인적인 경험이나 일화로 시작 - 실생활에서 쉽게 적용할 수 있는 팁 제공 - 독자의 공감을 얻을 수 있는 사례 포함 3. 용어 및 설명 방식 - 전문 용어 대신 쉬운 단어로 풀어서 설명 - 비유나 은유를 활용하여 복잡한 개념 설명 - 수사의문문 활용하여 독자와 소통하는 느낌 주기 4. 독자와의 상호작용 - 독자의 의견을 물어보는 질문 포함 - 댓글 달기를 독려하는 문구 사용 5. 이모지 활용 - 주요 포인트나 새로운 섹션을 시작할 때만 관련 이모지 사용 - 전체 글에서 3-5개 정도의 이모지만 사용하여 과도하지 않게 유지 6. 마무리 - 친근하고 격려하는 톤으로 마무리 - 다음 포스팅에 대한 기대감 유발 주의사항: 너무 가벼운 톤은 지양하고, 주제의 중요성을 해치지 않는 선에서 친근함 유지 예시: "여러분, 오늘 하루는 어떠셨나요? 😊 저는 오늘 재미있는 경험을 했어요. 바로 '미니멀 라이프'에 도전해본 건데요. 처음에는 좀 막막했지만, 생각보다 즐거운 경험이었어요! 여러분도 한번 따라해보시겠어요? 제가 알게 된 꿀팁들을 하나하나 알려드릴게요. """, "일반": """ #일반적인 블로그 포스팅 스타일 가이드 1. 톤과 어조 - 중립적이고 객관적인 톤 유지 - 적절한 존댓말 사용 (예: "~합니다", "~입니다") 2. 내용 구조 및 전개 - 명확한 주제 제시로 시작 - 논리적인 순서로 정보 전개 - 주요 포인트를 강조하는 소제목 활용 - 적절한 길이의 단락으로 구성 3. 용어 및 설명 방식 - 일반적으로 이해하기 쉬운 용어 선택 - 필요시 간단한 설명 추가 - 객관적인 정보 제공에 중점 4. 텍스트 구조화 - 불릿 포인트나 번호 매기기를 활용하여 정보 구조화 - 중요한 정보는 굵은 글씨나 기울임꼴로 강조 5. 독자 상호작용 - 적절히 독자의 생각을 묻는 질문 포함 - 추가 정보를 찾을 수 있는 키워드 제시 6. 마무리 - 주요 내용 간단히 요약 - 추가 정보에 대한 안내 제공 주의사항: 너무 딱딱하거나 지루하지 않도록 균형 유지 예시: "최근 환경 문제가 대두되면서 '제로 웨이스트' 라이프스타일에 대한 관심이 높아지고 있습니다. 제로 웨이스트란 일상생활에서 발생하는 쓰레기를 최소화하는 생활 방식을 말합니다. 이 글에서는 제로 웨이스트의 개념, 실천 방법, 그리고 그 효과에 대해 알아보겠습니다. 먼저 제로 웨이스트의 정의부터 살펴보면... """, "전문적인": """ #전문적인 블로그 포스팅 스타일 가이드 1. 톤과 구조 - 공식적이고 학술적인 톤 사용 - 객관적이고 분석적인 접근 유지 - 명확한 서론, 본론, 결론 구조 - 체계적인 논점 전개 - 세부 섹션을 위한 명확한 소제목 사용 2. 내용 구성 및 전개 - 복잡한 개념을 정확히 전달할 수 있는 문장 구조 사용 - 논리적 연결을 위한 전환어 활용 - 해당 분야의 전문 용어 적극 활용 (필요시 간략한 설명 제공) - 심층적인 분석과 비판적 사고 전개 - 다양한 관점 제시 및 비교 3. 데이터 및 근거 활용 - 통계, 연구 결과, 전문가 의견 등 신뢰할 수 있는 출처 인용 - 필요시 각주나 참고문헌 목록 포함 - 수치 데이터는 텍스트로 명확히 설명 4. 텍스트 구조화 - 논리적 구조를 강조하기 위해 번호 매기기 사용 - 핵심 개념이나 용어는 기울임꼴로 강조 - 긴 인용문은 들여쓰기로 구분 5. 마무리 - 핵심 논점 재강조 - 향후 연구 방향이나 실무적 함의 제시 주의사항: 전문성을 유지하되, 완전히 이해하기 어려운 수준은 지양 예시: "본 연구에서는 인공지능(AI)의 윤리적 함의에 대해 고찰한다. 특히, 자율주행 자동차의 의사결정 알고리즘에서 발생할 수 있는 윤리적 딜레마에 초점을 맞춘다. Bonnefon et al. (2016)의 연구에 따르면, 자율주행 차량의 알고리즘이 직면할 수 있는 윤리적 선택의 복잡성이 지적된 바 있다. 본고에서는 이러한 윤리적 딜레마를 세 가지 주요 관점에서 분석한다: 1) 공리주의적 접근, 2) 의무론적 접근, 3) 덕 윤리적 접근. 각 접근법의 장단점을 비교 분석하고, 이를 바탕으로 자율주행 차량의 윤리적 의사결정 프레임워크를 제안하고자 한다... """ } return prompts.get(style, "포스팅 스타일 프롬프트") def get_style_description(style): descriptions = { "친근한": "독자와 가까운 친구처럼 대화하는 듯한 친근한 스타일입니다.", "일반": "일반적이고 중립적인 톤으로 정보를 전달하는 스타일입니다.", "전문적인": "전문가의 시각에서 깊이 있는 정보를 전달하는 스타일입니다." } return descriptions.get(style, "포스팅 스타일을 선택하세요.") def update_prompts_and_description(category, style): title_prompt = get_title_prompt(category) outline_prompt = get_outline_prompt(category) blog_post_prompt = get_blog_post_prompt(category) style_prompt = get_style_prompt(style) style_description = get_style_description(style) return style_description, #PDF파일 만들 class PDF(FPDF): def __init__(self): super().__init__() current_dir = os.path.dirname(__file__) self.add_font("NanumGothic", "", os.path.join(current_dir, "NanumGothic.ttf"), uni=True) self.add_font("NanumGothic", "B", os.path.join(current_dir, "NanumGothicBold.ttf"), uni=True) self.add_font("NanumGothicExtraBold", "", os.path.join(current_dir, "NanumGothicExtraBold.ttf"), uni=True) self.add_font("NanumGothicLight", "", os.path.join(current_dir, "NanumGothicLight.ttf"), uni=True) def header(self): self.set_font('NanumGothic', '', 10) def footer(self): self.set_y(-15) self.set_font('NanumGothic', '', 8) self.cell(0, 10, f'Page {self.page_no()}', 0, 0, 'C') def save_to_pdf(blog_post): pdf = PDF() pdf.add_page() lines = blog_post.split('\n') title = lines[0].strip() content = '\n'.join(lines[1:]).strip() filename = format_filename(title) + ".pdf" pdf.set_font("NanumGothic", 'B', size=14) pdf.cell(0, 10, title, ln=True, align='C') pdf.ln(10) pdf.set_font("NanumGothic", '', size=11) pdf.multi_cell(0, 5, content) print(f"Saving PDF as: {filename}") pdf.output(filename) return filename def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() # save_content_to_pdf 함수도 간단하게 수정 def save_content_to_pdf(blog_post): return save_to_pdf(blog_post) title = "블로거(구글) 자동 포스팅-자동-One ID-복수 포스팅 생성/ 포스팅 게재(예) 링크 클릭: https://carecures.blogspot.com \n" def update_prompts_and_description(category, style): title_prompt = get_title_prompt(category) outline_prompt = get_outline_prompt(category) blog_post_prompt = get_blog_post_prompt(category) style_prompt = get_style_prompt(style) style_description = get_style_description(style) return style_description WEBHOOK_URL = os.getenv("WEBHOOK_URL") BLOGGER_ID = os.getenv("BLOGGER_ID") PEXELS_API_KEY = "5woz23MGx1QrSY0WHFb0BRi29JvbXPu97Hg0xnklYgHUI8G0w23FKH62" PEXELS_API_URL = "https://api.pexels.com/v1/search" # 번역 모델 설정 translator = pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en") def get_image_url(topic): try: translated = translator(topic, max_length=40)[0]['translation_text'] keywords = ' '.join(translated.split()[:2]) except Exception as e: logger.error(f"번역 중 오류 발생: {str(e)}") keywords = ' '.join(topic.split()[:2]) logger.debug(f"검색 키워드: {keywords}") headers = { "Authorization": PEXELS_API_KEY } params = { "query": keywords, "per_page": 60, # 검색 결과 수 60개로 증가 } try: response = requests.get(PEXELS_API_URL, headers=headers, params=params) logger.debug(f"Pexels API 응답 상태 코드: {response.status_code}") if response.status_code == 200: data = response.json() logger.debug(f"Pexels API 응답 데이터: {data}") if data['photos']: # 랜덤하게 1장의 이미지 선택 random_photo = random.choice(data['photos']) return random_photo['src']['large2x'] else: logger.warning(f"'{keywords}' 관련 이미지를 찾을 수 없습니다.") return None else: logger.error(f"Pexels API 오류: {response.status_code}") return None except Exception as e: logger.error(f"이미지 검색 중 오류 발생: {str(e)}") return None def convert_markdown_images(content): logger.debug("원본 내용: %s", content) # Markdown 이미지 문법을 HTML 태그로 변환 markdown_pattern = r'!\[([^\]]*)\]\(([^)]+)\)' content = re.sub(markdown_pattern, r'\1', content) # 단순 URL을 태그로 변환 url_pattern = r'(https?://\S+?\.(jpg|jpeg|png|gif))' content = re.sub(url_pattern, r'', content) logger.debug("변환된 내용: %s", content) return content def process_all_titles(category, style, topic, num_titles, progress=gr.Progress()): title_suggestions, _ = suggest_title(category, style, topic, "", "", "") titles = title_suggestions.split('\n') titles = [title.strip() for title in titles if title.strip() and not title.strip().startswith('###')] titles = [title for title in titles if "블로그" not in title and "제목" not in title] results = [] for i, title in enumerate(titles[:num_titles], 1): progress(i / num_titles, desc=f"처리 중: {i}/{num_titles}") try: image_url_1 = get_image_url(topic) image_url_2 = get_image_url(topic) _, _, _, _, _, blog_content, _ = fetch_references_and_generate_all_steps(category, style, topic, title) if blog_content.startswith("API 호출 실패"): result = f"제목: {title}\n생성 실패: {blog_content}\n\n" else: if image_url_1: blog_content = f'{topic} 1\n\n{blog_content}' if image_url_2: blog_content = f'{blog_content}\n\n{topic} 2' send_result = send_to_blogger(title, blog_content) result = f"제목: {title}\n전송 결과: {send_result}\n사용된 이미지 URL 1: {image_url_1 if image_url_1 else '이미지 없음'}\n사용된 이미지 URL 2: {image_url_2 if image_url_2 else '이미지 없음'}\n\n" results.append(result) yield f"진행 상황: {i}/{num_titles}\n\n" + "\n".join(results) except Exception as e: logger.error("제목 '%s' 처리 중 오류 발생: %s", title, str(e)) result = f"제목: {title}\n처리 중 오류 발생: {str(e)}\n\n" results.append(result) yield f"진행 상황: {i}/{num_titles}\n\n" + "\n".join(results) time.sleep(5) yield f"완료: {num_titles}/{num_titles}\n\n" + "\n".join(results) def send_to_blogger(blog_title, blog_content): if not WEBHOOK_URL: logger.error("WEBHOOK_URL이 설정되지 않았습니다.") return "WEBHOOK_URL이 설정되지 않아 포스팅을 전송할 수 없습니다." # HTML 엔티티 디코딩 blog_content = html.unescape(blog_content) payload = { "blogger_id": BLOGGER_ID, "title": blog_title, "content": blog_content } try: logger.debug("블로거에 전송할 페이로드: %s", payload) response = requests.post(WEBHOOK_URL, json=payload) logger.debug("블로거 응답: %s, %s", response.status_code, response.text) if response.status_code == 200: return "포스팅이 성공적으로 전송되었습니다." else: return f"포스팅 전송 실패. 상태 코드: {response.status_code}, 응답: {response.text}" except requests.exceptions.RequestException as e: logger.error("블로거에 전송 중 오류 발생: %s", str(e)) return f"네트워크 오류 발생: {str(e)}" except Exception as e: logger.error("블로그에 전송 중 예상치 못한 오류 발생: %s", str(e)) return f"오류 발생: {str(e)}" css = """ footer { visibility: hidden; } """ with gr.Blocks(theme="Nymbo/Nymbo_Theme", css=css) as demo: gr.Markdown(f"# {title}") gr.Markdown("### 1단계 : 포스팅 카테고리를 지정해주세요") category = gr.Radio(choices=["일반", "건강정보"], label="포스팅 카테고리", value="일반") gr.Markdown("### 2단계: 포스팅 스타일을 선택해주세요") style = gr.Radio(choices=["친근한", "일반", "전문적인"], label="포스팅 스타일", value="친근한") style_description = gr.Markdown(f"_{get_style_description('친근한')}_", elem_id="style-description") gr.Markdown("### 3단계 : 블로그 주제, 또는 키워드를 상세히 입력하세요") topic = gr.Textbox(label="블로그 주제", placeholder="예시: 8월 국내 여행지 추천") gr.Markdown("### 4단계 : 자동 생성 및 전송할 블로그 수를 선택하세요") num_titles = gr.Slider(minimum=1, maximum=2, value=2, step=1, label="포스팅 건") start_btn = gr.Button("시작") result_output = gr.Textbox(label="처리 결과", lines=20) start_btn.click( fn=process_all_titles, inputs=[category, style, topic, num_titles], outputs=[result_output] ) category.change(fn=update_prompts_and_description, inputs=[category, style], outputs=[style_description]) style.change(fn=update_prompts_and_description, inputs=[category, style], outputs=[style_description]) demo.launch()