import os import random import re import requests import logging import tempfile from bs4 import BeautifulSoup from datetime import datetime from zoneinfo import ZoneInfo import html from PIL import Image from urllib.request import urlopen import markdown2 import gradio as gr # 로깅 설정 (INFO 레벨) logging.basicConfig(level=logging.INFO) # 상수 정의 TARGET_CHAR_LENGTH = 4000 MIN_SECTION_LENGTH = 600 MAX_TOKENS = 15000 TEMPERATURE = 0.75 TOP_P = 0.95 # API 관련 설정 gemini_api_key = os.getenv("GEMINI_API_KEY") # --- Google Gemini SDK 초기화 --- from google import genai from google.genai import types client = genai.Client(api_key=gemini_api_key) # ------------------------------- # 기본 도우미 함수들 # ------------------------------- def remove_unwanted_phrases(text): """불필요한 표현 제거 함수""" unwanted_phrases = [ '여러분', '최근', '마지막으로', '결론적으로', '결국', '종합적으로', '따라서', '마무리', '끝으로', '요약', '한 줄 요약', '정리하자면', '총정리', '글을 마치며', '이상으로', '추천드립니다', '참고하세요', '도움이 되셨길', '좋은 하루 되세요', '다음 글에서', '도움이 되었길', '즐거운 하루 되세요', '감사합니다' ] # 문단별로 나누어 처리 lines = text.split('\n') result_lines = [] for line in lines: if "다음 섹션에서는" in line: parts = line.split("다음 섹션에서는") if parts[0].strip(): result_lines.append(parts[0].strip()) else: # 불필요한 표현 제거 (구두점 포함) for phrase in unwanted_phrases: # 불필요한 표현 앞뒤의 구두점과 공백까지 포함하여 제거 pattern = rf'(\b{re.escape(phrase)}\b[\s,.!?]*)|([,.!?]*\b{re.escape(phrase)}\b)' line = re.sub(pattern, '', line) # 문장 내 잔여 공백 및 구두점 정리 line = re.sub(r'\s{2,}', ' ', line) # 연속 공백 제거 line = line.strip() # 앞뒤 공백 제거 result_lines.append(line) return '\n'.join(result_lines) def convert_to_html(text): """마크다운 형식을 HTML로 변환""" text = re.sub(r'^\s*[-*]\s+', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE) text = re.sub(r'^\s*#{1,6}\s+', '', text, flags=re.MULTILINE) return markdown2.markdown(text) def format_blog_post(blog_post, query="", with_title=False): """블로그 포스트 포맷팅 함수 - 소제목 강화 버전""" blog_post = re.sub(r'^#+\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^\d+\.\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^[\*\-]\s+', '', blog_post, flags=re.MULTILINE) # 첫 줄(원본 제목)과 비슷한 패턴이 있다면 제거 lines = blog_post.split('\n') if lines and len(lines) > 0: first_line = lines[0].strip() # 첫 줄이 제목인 경우, 비슷한 내용의 라인을 모두 제거 if first_line and len(first_line) > 5: # 첫 줄과 유사한 내용을 가진 라인 찾아 제거 filtered_lines = [] for line in lines: # 첫 줄과 유사하면 제거 if line.strip() and (first_line in line or line in first_line): continue filtered_lines.append(line) lines = filtered_lines # 도입부, 결론 소제목 패턴 intro_pattern = r'(?i)도입부\s*[:]?\s*(.*?)$' conclusion_pattern = r'(?i)결론\s*[:]?\s*(.*?)$' # 도입부, 결론 소제목 제거 filtered_lines = [] for line in lines: if re.match(intro_pattern, line) or re.match(conclusion_pattern, line): continue filtered_lines.append(line) lines = filtered_lines # 본론 소제목 패턴 강화 section_patterns = [ r'^본론\d+\s*[:]?\s*(.*?)$', # 본론1: 내용 패턴 r'^.{5,50}의 [가-힣\s]+$', # ~의 ~ 패턴 r'^[가-힣\s]{5,30}(이란|이란\?|이란\s무엇인가|이란\s무엇일까)[\?\s]*$', # ~이란? 패턴 r'^[가-힣\s]{5,50}\s[-–]\s.{5,30}$', # 강조 표현 패턴 (예: 효과적인 방법 - 실천하기) r'^[가-힣A-Za-z\s]{10,50}[\.!\?]$', # 긴 문장으로 된 소제목 패턴 ] formatted_lines = [] in_paragraph = False # 본론 섹션 번호 추적 section_number = 1 for i, line in enumerate(lines): line = line.strip() if not line: if in_paragraph: formatted_lines.append("
") in_paragraph = False formatted_lines.append("") in_paragraph = True content = html.escape(line) bold_content = re.sub(r'\*\*(.*?)\*\*', r'\1', content) formatted_lines.append(bold_content) if in_paragraph: formatted_lines.append("
") return '\n'.join(formatted_lines) # ------------------------------- # 스타일 및 프롬프트 가이드 함수 # ------------------------------- def get_style_prompt(style="친근한"): """블로그 글의 스타일 프롬프트를 반환""" prompts = { "친근한": """ [친근한 여행 블로그 스타일 가이드] 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 - 여행지에 대한 관심과 호기심을 담은 표현 사용 - 실제 여행을 경험한 것처럼 생생한 묘사 2. 문장 및 어투 - 반드시 '해요체'로 작성, 절대 '습니다'체를 사용하지 말 것 - '~요'로 끝나도록 작성, '~다'로 끝나지 않게 하라 - 구어체 표현 사용 (예: "~했어요", "~인 것 같아요") - 적절한 감정 표현과 공감대 형성 3. 용어 및 설명 방식 - 여행 관련 전문 용어는 쉬운 단어로 풀어서 설명 - 비유나 은유를 활용하여 장소와 경험 묘사 - 수사의문문 활용하여 독자와 소통하는 느낌 주기 (예: "어떻게 생각하세요?", "이런 경험 있으신가요?") - 구체적 사례와 실제 경험에 기반한 팁 제공 4. 정보 전달 방식 - 개인적인 관점에 녹여 자연스럽게 정보 전달 - 실제 여행자의 시선으로 장소와 경험 묘사 - 독자가 실제로 활용할 수 있는 실용적 정보 제공 (교통, 비용, 시간, 추천 코스 등) 5. 독자와의 상호작용 - 독자의 의견을 물어보는 질문 포함 - 실제 여행에 적용할 수 있는 팁이나 조언 제공 주의사항: 자연스러운 대화체를 유지하면서 정보의 질과 내용의 깊이를 잃지 않도록 한다 """, "일반": """ [일반적인 여행 블로그 스타일 가이드] 1. 톤과 어조 - 중립적이고 객관적인 톤 유지 - 적절한 존댓말 사용 (예: "~합니다", "~입니다") - 여행 정보 전달 중심의 명확한 어투 2. 내용 구조 및 전개 - 명확한 여행지 소개로 시작 - 논리적인 순서로 정보 전개 (교통 → 숙박 → 관광지 → 음식 등) - 핵심 포인트를 강조하는 소제목 활용 - 적절한 길이의 단락으로 구성 3. 용어 및 설명 방식 - 일반적으로 이해하기 쉬운 용어 선택 - 필요시 지역 특유의 용어에 간단한 설명 추가 - 객관적인 여행 정보 제공에 중점 - 균형 잡힌 시각에서 여행지의 장단점 제시 4. 정보 전달 방식 - 여행지의 기본 정보와 특징 명확하게 제공 - 구체적인 예시와 추천 코스 포함 - 최신 여행 정보와 동향 참고 5. 독자 상호작용 - 적절히 독자의 생각을 묻는 질문 포함 - 추가 정보를 찾을 수 있는 키워드 제시 - 실용적인 여행 팁 제공 주의사항: 객관적 정보 제공을 중심으로 하되, 독자의 여행 계획과 실행을 도울 수 있는 맥락과 설명을 충분히 제공한다 """, "전문적인": """ [전문적인 여행 블로그 스타일 가이드] 1. 톤과 구조 - 공식적이고 전문적인 톤 사용 - 객관적이고 분석적인 접근 유지 - 명확한 서론(여행지 개요), 본론(상세 분석), 결론(종합 평가) 구조 - 체계적인 여행 정보 전개 - 세부 섹션을 위한 명확한 소제목 사용 2. 내용 구성 및 전개 - 여행지의 역사적 배경, 문화적 특징, 현재 동향 등 심층적 정보 포함 - 논리적 연결을 위한 전환어 활용 - 여행 용어 적절히 활용 (필요시 간략한 설명 제공) - 심층적인 분석과 비판적 평가 제공 - 다양한 관점에서 여행지 분석 3. 데이터 및 근거 활용 - 통계, 시즌별 정보, 실제 사례 등 객관적 데이터 활용 - 여행지 분석을 위한 체계적인 프레임워크 제시 - 객관적 정보와 전문가 관점의 균형 4. 전문적 정보 제공 - 최신 여행 동향 및 변화 분석 - 문화, 역사적 맥락에서의 여행지 분석 - 여행 관련 쟁점과 고려사항 소개 - 체계적인 여행 계획 접근법 제시 주의사항: 전문성과 깊이를 유지하면서도 이해 가능한 용어와 설명을 통해 접근성을 높인다 """ } return prompts.get(style, prompts["친근한"]) def get_category_outline_prompt(category="여행 단일"): """카테고리별 아웃라인 생성 프롬프트""" prompts = { "여행 단일": """ [여행 단일지점 소주제(Outline) 생성 규칙] [시스템 역할] 당신은 수년간의 경험을 가진 전문 여행 블로거입니다. 특정 명소, 축제, 이벤트 등 단일 여행 코스에 대한 생생한 경험과 유용한 정보로 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1.참고 자료 3개를 철저히 분석하여 해당 명소/축제의 핵심 특징과 중요 정보 파악 2. 여행 명소/축제의 유형과 특성 식별 (계절 축제, 문화 행사, 자연 명소, 역사 유적지 등) 3. 포스팅의 핵심이 될 5가지 주요 요소 파악 (행사 특징, 볼거리, 즐길거리, 방문 팁, 교통/편의시설 등) [아웃라인 구성 원칙] 1. 도입부(1개) - 해당 명소/축제의 매력을 집약한 흥미로운 제목으로 시작 2. 본론(4-5개) - 참고 자료 분석을 통해 발견한 명소/축제의 핵심 특징과 방문 정보를 담은 소제목 - 명소/축제의 주요 특징과 역사적/문화적 의미 - 반드시 놓치지 말아야 할 핵심 볼거리와 즐길거리 - 방문 시기, 시간대, 계절별 특징 - 교통, 입장료, 주차, 편의시설 등 실용적 정보 - 방문 시 유용한 꿀팁과 주의사항 - (위 항목들은 예시일 뿐, 참고 자료 분석을 통해 자유롭게 결정) 3. 결론(1개) - 전체 경험을 요약하고 방문 가치를 강조하는 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 독자의 호기심과 방문 욕구를 자극하는 표현 사용 (예: "놓치면 후회할", "숨겨진 즐길거리", "현지인도 추천하는") 4. 명소/축제의 가장 독특하고 매력적인 특징이 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 [출력 형식] 1. 참고 자료 분석을 통해 가장 핵심적인 명소/축제 특징과 정보를 파악하여 자유롭게 아웃라인 구성 2. 하지만 반드시 다음 구조를 유지할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) 3. 도입부: 1개 (명소/축제의 매력을 담은 흥미로운 제목) 4. 본론: 4-5개 (명소/축제의 핵심 특징과 실용 정보를 반영한 제목) 5. 결론: 1개 (전체 경험 요약 및 방문 가치 강조 제목) 6. 소제목은 해당 명소/축제의 실제 특징과 방문 정보에 맞게 자유롭게 구성 7. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 가치와 정보 기반으로 구성 8. 예시 형식 (참고용일 뿐, 내용은 참고 자료에 따라 완전히 달라질 수 있음): - 도입부: [명소/축제의 매력 소개 제목] - 본론1: [핵심 특징/역사적 의미 관련 제목] - 본론2: [주요 볼거리/즐길거리 관련 제목] - 본론3: [최적의 방문 시기/시간 관련 제목] - 본론4: [교통/편의시설 정보 관련 제목] - 본론5: [방문 꿀팁/주의사항 관련 제목] (필요시) - 결론: [경험 요약 및 방문 가치 강조 제목] """, "여행 코스": """ [여행 코스 소주제(Outline) 생성 규칙] [시스템 역할] 당신은 수년간의 경험을 가진 전문 여행 코스 기획자입니다. 효율적이고 매력적인 여행 코스 설계와 실용적인 여행 팁으로 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1. 참고 자료 3개를 철저히 분석하여 해당 지역의 핵심 명소와 여행 정보 파악 2. 여행 지역의 유형과 특성 식별 (도시 여행, 자연 여행, 문화 탐방, 맛집 투어 등) 3. 포스팅의 핵심이 될 5가지 주요 요소 파악 (여행 기간, 주요 명소, 이동 방법, 숙박 옵션, 예산 등) [아웃라인 구성 원칙] 1. 도입부(1개) - 해당 지역의 매력과 코스의 특징을 집약한 흥미로운 제목으로 시작 2. 본론(4-5개) - 참고 자료 분석을 통해 설계한 여행 코스의 주요 일정과 정보를 담은 소제목 3. 코스 개요와 여행 기간별 추천 일정 4. 일차별 또는 테마별 세부 코스 구성 5. 효율적인 이동 방법과 교통 정보 6. 숙소 및 식당 추천과 예약 팁 7. 여행 예산과 비용 절약 방법 8. (위 항목들은 예시일 뿐, 참고 자료 분석을 통해 자유롭게 결정) 9. 결론(1개) - 전체 코스의 특징을 요약하고 여행 계획 수립에 도움을 주는 제목 [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 독자의 여행 계획 수립을 돕는 실용적인 표현 사용 (예: "완벽한 3박 4일 코스", "효율적인 동선 설계", "현지인이 알려주는 꿀코스") 4. 코스의 효율성과 매력적인 특징이 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 전체 아웃라인은 도입부(1) + 본론(최대 5개) + 결론(1)으로 구성 [출력 형식] 1. 참고 자료 분석을 통해 가장 효율적이고 매력적인 여행 코스를 설계하여 자유롭게 아웃라인 구성 2. 하지만 반드시 다음 구조를 유지할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) 3. 도입부: 1개 (지역 매력과 코스 특징을 담은 흥미로운 제목) 4. 본론: 4-5개 (효율적인 여행 동선과 실용 정보를 반영한 제목) 5. 결론: 1개 (전체 코스 요약 및 계획 수립 도움 제목) 6. 소제목은 해당 지역의 실제 특징과 코스 정보에 맞게 자유롭게 구성 7. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 코스와 정보 기반으로 구성 8. 예시 형식 (참고용일 뿐, 내용은 참고 자료에 따라 완전히 달라질 수 있음): - 도입부: [지역 매력과 코스 특징 소개 제목] - 본론1: [여행 기간별 추천 일정 관련 제목] - 본론2: [일차별/테마별 세부 코스 관련 제목] - 본론3: [교통/이동 방법 관련 제목] - 본론4: [숙소/식당 추천 관련 제목] - 본론5: [예산/비용 절약 팁 관련 제목] (필요시) - 결론: [코스 요약 및 계획 수립 도움 제목] """ } return prompts.get(category, prompts["여행 단일"]) def get_category_blog_prompt(category="여행 단일"): """카테고리별 블로그 글 생성 프롬프트""" prompts = { "여행 단일": """ [여행 단일지점 블로그 작성 가이드] 1. 너는 최고의 여행 블로그 작가이자 여행 정보 전달 전문가이다. 2. 주어진 아웃라인과 참고글을 바탕으로 독자에게 가치 있는 여행 정보를 제공하라. 3. 정보의 정확성과 깊이를 유지하면서도 이해하기 쉽게 설명하라. [콘텐츠 작성 규칙] 1. 여행지에 대한 객관적인 정보 제공에 중점을 두되, 독자의 관심과 참여를 유도하라. 2. 여행지의 특징과 매력을 생생하게 묘사하라. 3. 여행지에 대한 다양한 관점과 측면을 균형 있게 다루어라. 4. 독자가 실제 여행에 적용할 수 있는 실용적인 정보와 팁을 제공하라. - 교통 정보: 대중교통, 주차, 택시 등 - 비용 정보: 입장료, 평균 식사 비용 등 - 시간 정보: 최적 방문 시간, 소요 시간 등 - 추천 코스: 관광지 내 동선, 인근 연계 관광지 등 5. 참고글의 정보를 재구성하되, 표현을 단순히 복사하지 말고 창의적으로 재구성하라. 6. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 7. 글 전체 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. 8. 글 전체 길이는 {TARGET_CHAR_LENGTH}자에서 {TARGET_CHAR_LENGTH + 1000}자 사이가 되도록 작성하라. [중요 규칙] 1. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 2. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 3. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 4. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 5. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. """, "여행 코스": """ [여행 코스 블로그 작성 가이드] 1. 너는 최고의 여행 코스 블로그 작가이자 여행 일정 기획 전문가이다. 2. 주어진 아웃라인과 참고글을 바탕으로 독자에게 최적화된 여행 코스 정보를 제공하라. 3. 정보의 정확성과 깊이를 유지하면서도 실제 여행에 적용하기 쉽게 설명하라. [콘텐츠 작성 규칙] 1. 여행 코스에 대한 객관적인 정보 제공에 중점을 두되, 독자의 관심과 참여를 유도하라. 2. 여행 코스의 논리적 흐름과 효율성을 명확히 설명하라. 3. 시간, 거리, 교통 정보를 정확하게 제시하여 실제 여행 계획에 도움을 주라. 4. 독자가 실제 여행에 적용할 수 있는 실용적인 정보와 팁을 제공하라. - 일자별 세부 일정 및 소요 시간 - 장소간 이동 방법 및 소요 시간 - 각 장소별 주요 볼거리 및 추천 활동 - 식사 및 휴식 장소 추천 - 예산 정보 및 예약 팁 5. 참고글의 정보를 재구성하되, 표현을 단순히 복사하지 말고 창의적으로 재구성하라. 6. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 7. 글 전체 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. 8. 글 전체 길이는 {TARGET_CHAR_LENGTH}자에서 {TARGET_CHAR_LENGTH + 1000}자 사이가 되도록 작성하라. [중요 규칙] 1. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 2. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 3. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 4. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 5. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. """ } return prompts.get(category, prompts["여행 단일"]) def generate_blog_post(category, style, outline_input, references1, references2, references3): """한 번의 호출로 전체 블로그 글 생성 함수 (퇴고 및 확장 기능 포함)""" try: # 참고글 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 references = [ref for ref in references if ref != "참고 자료 없음"] if not references: return "참고 자료가 없습니다. 최소 하나 이상의 참고 자료를 입력해주세요.
", 0 if not outline_input.strip(): return "아웃라인이 없습니다. 아웃라인을 입력해주세요.
", 0 # 카테고리 및 스타일 프롬프트 가져오기 category_prompt = get_category_blog_prompt(category) style_prompt = get_style_prompt(style) # Phase 1: 초기 블로그 글 생성 blog_prompt = f""" [여행 블로그 글 작성 요청] 카테고리: {category} 포스팅 스타일: {style} 아웃라인: {outline_input} 참고글: {references[0]} {references[1] if len(references) > 1 else ""} {references[2] if len(references) > 2 else ""} {category_prompt} {style_prompt} [소제목 작성 가이드] 1. 본론의 각 부분마다 명확한 소제목을 사용하세요. 2. 소제목은 10~20자 내외로 명확하고 간결하게 작성하세요. 3. 소제목은 독립된 줄에 위치하고 앞뒤에 빈 줄이 있어야 합니다. 4. 소제목 예시: '제주도의 숨은 명소', '교통 및 이동 가이드', '맛집 탐방 코스' [중요 작성 규칙] 1. 반드시 위의 아웃라인 순서와 구조에 따라 작성하라. 2. 각 섹션은 명확히 구분되어야 하며, 섹션 제목을 포함하라. 3. 도입부는 여행자의 관심을 끌고 여행지나 코스를 매력적으로 소개하는 방식으로 작성하라. 4. 본론 각 부분은 여행의 서로 다른 측면(교통, 볼거리, 음식, 숙박 등)을 다루며, 구체적인 정보와 예시를 포함하라. 5. 결론은 핵심 내용을 요약하고 최종 추천이나 통찰을 제공하라. 6. 전체 글의 길이는 최소 {TARGET_CHAR_LENGTH}자가 되도록 작성하라. 7. 각 섹션은 최소 {MIN_SECTION_LENGTH}자 이상의 충분한 내용으로 작성하라. 8. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 9. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 10. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 11. "참고글", "참고글에 따르면" 등의 표현을 사용하지 말라. 12. "여러분", "독자 여러분" 등의 직접적인 호칭을 지양하라. 13. 과장된 표현이나 불필요한 반복을 피하라. 14. 각 섹션 사이에 자연스러운 연결성을 유지하라. 15. 글의 처음에 전체 글의 매력적인 제목을 반드시 추가하라. 16. 반드시 구체적인 여행 정보(위치, 교통, 시간, 비용, 예약방법, 팁 등)를 포함하라. """ # Gemini API 호출 (한 번의 호출로 전체 글 생성) logging.info("전체 블로그 글 생성 시작") blog_content = call_gemini_api(blog_prompt, temperature=0.7) logging.info(f"생성된 원본 글 길이: {len(blog_content)}") # 후처리 processed_content = post_process_blog(blog_content, style) # HTML 변환하여 글자 수 체크 temp_html = format_blog_post(processed_content) soup = BeautifulSoup(temp_html, 'html.parser') char_count = len(soup.get_text()) logging.info(f"초기 블로그 글 글자 수: {char_count}") # Phase 2: 글자 수가 목표에 미달하면 퇴고 및 확장 if char_count < TARGET_CHAR_LENGTH * 0.8: # 목표의 80% 미만이면 확장 logging.info(f"글자 수 부족 ({char_count} < {TARGET_CHAR_LENGTH * 0.8}), 확장 시도") # 가장 긴 참고글 선택 longest_ref = max(references, key=len) expansion_prompt = f""" [여행 블로그 글 확장 요청] 카테고리: {category} 포스팅 스타일: {style} 원본 글: {processed_content} 참고글: {longest_ref} 문제점: 이 글은 목표 글자수인 {TARGET_CHAR_LENGTH}자에 미치지 못합니다. 현재 글자수는 약 {char_count}자입니다. 내용이 부실하여 확장이 필요합니다. {style_prompt} [확장 요구사항] 1. 원본 글의 구조와 아웃라인을 유지하면서 각 섹션의 내용을 대폭 확장하라. 2. 각 섹션에 더 구체적인 정보, 예시, 사례, 통계 등을 추가하라. - 교통 정보, 위치 정보, 비용 정보, 영업시간, 예약 방법 등 실용적 정보 추가 - 계절별/시간별 특징, 관광객 수, 소요 시간 등 구체적 정보 추가 - 주변 관광지, 연계 가능한 장소 등 추가 정보 제공 3. 전체 글자 수를 최소 {TARGET_CHAR_LENGTH}자 이상 달성하라. 4. 스타일과 어조는 일관성을 유지하라. 5. 마크다운 형식(#, *, -, 1., 2. 등)을 사용하지 말고 일반 텍스트로 작성하라. 6. 소제목과 결론은 번호 없이 일반 문장 형태로 작성하라. 7. 목록은 불릿이나 번호 대신 자연스러운 문장으로 서술하라. 8. "참고글" 관련 표현을 사용하지 말라. 9. 부자연스러운 반복이나 과장된 표현을 피하라. """ # 확장 시도 expanded_content = call_gemini_api(expansion_prompt, temperature=0.75) processed_content = post_process_blog(expanded_content, style) # 다시 글자 수 체크 temp_html = format_blog_post(processed_content) soup = BeautifulSoup(temp_html, 'html.parser') char_count = len(soup.get_text()) logging.info(f"확장 후 블로그 글 글자 수: {char_count}") # Phase 3: 여전히 부족하면 추가 확장 시도 if char_count < TARGET_CHAR_LENGTH * 0.9: # 목표의 90% 미만이면 추가 확장 logging.info(f"여전히 글자 수 부족 ({char_count} < {TARGET_CHAR_LENGTH * 0.9}), 추가 확장 시도") additional_expansion_prompt = f""" [여행 블로그 글 추가 확장 요청] 카테고리: {category} 포스팅 스타일: {style} 원본 글: {processed_content} 문제점: 이 글은 여전히 목표 글자수인 {TARGET_CHAR_LENGTH}자에 미치지 못합니다. 현재 글자수는 약 {char_count}자입니다. [추가 확장 요구사항] 1. 본론 부분을 중심으로 세부 내용을 크게 확장하라. 2. 다음 항목에 대한 더 깊은 설명과 실용적인 적용 방법을 추가하라: - 여행지 특유의 문화나 역사적 배경 - 현지인만 아는 숨은 명소나 팁 - 여행지 방문 시 주의사항이나 에티켓 - 시간대별/날씨별 방문 팁 - 구체적인 교통편, 소요시간, 비용 정보 3. 여행자에게 유용한 핵심 정보와 인사이트를 더 풍부하게 제공하라. 4. 전체 글자 수를 최소 {TARGET_CHAR_LENGTH}자 이상으로 확장하라. 5. 스타일과 어조의 일관성을 유지하라. 6. 반복되는 내용이나 중복은 피하라. """ # 추가 확장 시도 further_expanded_content = call_gemini_api(additional_expansion_prompt, temperature=0.8) processed_content = post_process_blog(further_expanded_content, style) # 최종 HTML 변환 final_html = format_blog_post(processed_content) # 최종 글자 수 계산 soup = BeautifulSoup(final_html, 'html.parser') final_char_count = len(soup.get_text()) logging.info(f"최종 블로그 글 글자 수: {final_char_count}") return final_html, final_char_count except Exception as e: logging.error(f"블로그 글 생성 중 오류 발생: {str(e)}") return f"블로그 글 생성 중 오류 발생: {str(e)}
", 0 def post_process_blog(blog_content, style="친근한"): """블로그 컨텐츠 후처리 함수""" try: # 번호 목록, 불릿, 헤딩 등 제거 blog_content = re.sub(r'^\d+\.\s+', '', blog_content, flags=re.MULTILINE) blog_content = re.sub(r'^[\*\-\•]\s+', '', blog_content, flags=re.MULTILINE) blog_content = re.sub(r'^#+\s+', '', blog_content, flags=re.MULTILINE) # 스타일에 따른 어투 조정 if style == "친근한": blog_content = re.sub(r'([가-힣]+)고요', r'\1구요', blog_content) blog_content = re.sub(r'답니다', '어요', blog_content) blog_content = re.sub(r'였답니다', '였어요', blog_content) blog_content = re.sub(r'했답니다', '했어요', blog_content) blog_content = re.sub(r'습니다', '요', blog_content) blog_content = re.sub(r'합니다', '해요', blog_content) blog_content = re.sub(r'됩니다', '돼요', blog_content) blog_content = re.sub(r'입니다', '이에요', blog_content) # 과장된 표현 정리 exaggerated_expressions = [ (r'필수적인', r'중요한'), (r'혁명적인', r'중요한'), (r'놀라운', r'주목할 만한'), (r'기적의', r'효과적인'), (r'최고의', r'좋은'), (r'세계적인', r'유명한'), (r'완벽한', r'우수한'), (r'극적인', r'상당한'), (r'무한한', r'많은'), (r'절대적인', r'상당한'), (r'혁신적인', r'새로운'), (r'환상적인', r'좋은'), (r'근본적인', r'기본적인'), (r'획기적인', r'중요한'), (r'전례없는', r'특별한'), (r'압도적인', r'주목할 만한'), (r'황홀한', r'좋은'), (r'천상의', r'우수한'), (r'기가 막힌', r'효과적인'), (r'끝판왕', r'최상위'), (r'그 자체', r''), (r'이 .{1,10} 그 자체였어요', r'이 \1였어요'), (r'가 .{1,10} 그 자체였어요', r'가 \1였어요'), (r'압도적인', r'중요한'), (r'천국', r'좋은 곳'), (r'황홀했어요', r'좋았어요'), (r'환상의', r'좋은'), (r'최적의', r'좋은'), (r'완벽한', r'좋은'), (r'꼭 가봐야 할', r'추천하는'), (r'꼭 먹어봐야 할', r'추천하는'), (r'꼭 해봐야 할', r'추천하는') ] for pattern, replacement in exaggerated_expressions: blog_content = re.sub(pattern, replacement, blog_content, flags=re.IGNORECASE) blog_content = re.sub(r'참고글에 따르면', r'알려진 바로는', blog_content) blog_content = re.sub(r'참고글', r'관련 정보', blog_content) # 여행 관련 표현 정리 travel_specific_adjustments = [ (r'방문해보세요', r'방문해 보세요'), (r'경험해보세요', r'경험해 보세요'), (r'먹어보세요', r'먹어 보세요'), (r'타보세요', r'타 보세요'), (r'해보세요', r'해 보세요'), (r'여행객들에게 (.*?)을 강력 추천합니다', r'여행객들에게 \1을 추천해요'), (r'여행객들에게 (.*?)를 강력 추천합니다', r'여행객들에게 \1를 추천해요'), (r'여행객들에게 (.*?)을 추천합니다', r'여행객들에게 \1을 추천해요'), (r'여행객들에게 (.*?)를 추천합니다', r'여행객들에게 \1를 추천해요'), (r'여행객에게 (.*?)을 강력 추천합니다', r'여행객에게 \1을 추천해요'), (r'여행객에게 (.*?)를 강력 추천합니다', r'여행객에게 \1를 추천해요') ] for pattern, replacement in travel_specific_adjustments: blog_content = re.sub(pattern, replacement, blog_content) return blog_content except Exception as e: logging.error(f"블로그 글 후처리 중 오류 발생: {str(e)}") return blog_content def format_blog_post(blog_post, query="", with_title=False): """블로그 포스트 포맷팅 함수 - 소제목 강화 버전""" blog_post = re.sub(r'^#+\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^\d+\.\s+', '', blog_post, flags=re.MULTILINE) blog_post = re.sub(r'^[\*\-]\s+', '', blog_post, flags=re.MULTILINE) # 첫 줄(원본 제목)과 비슷한 패턴이 있다면 제거 lines = blog_post.split('\n') if lines and len(lines) > 0: first_line = lines[0].strip() # 첫 줄이 제목인 경우, 비슷한 내용의 라인을 모두 제거 if first_line and len(first_line) > 5: # 첫 줄과 유사한 내용을 가진 라인 찾아 제거 filtered_lines = [] for line in lines: # 첫 줄과 유사하면 제거 if line.strip() and (first_line in line or line in first_line): continue filtered_lines.append(line) lines = filtered_lines # 도입부, 결론 소제목 패턴 intro_pattern = r'(?i)도입부\s*[:]?\s*(.*?)$' conclusion_pattern = r'(?i)결론\s*[:]?\s*(.*?)$' # 도입부, 결론 소제목 제거 filtered_lines = [] for line in lines: if re.match(intro_pattern, line) or re.match(conclusion_pattern, line): continue filtered_lines.append(line) lines = filtered_lines # 본론 소제목 패턴 강화 section_patterns = [ r'^본론\d+\s*[:]?\s*(.*?)$', # 본론1: 내용 패턴 r'^.{5,50}의 [가-힣\s]+$', # ~의 ~ 패턴 r'^[가-힣\s]{5,30}(이란|이란\?|이란\s무엇인가|이란\s무엇일까)[\?\s]*$', # ~이란? 패턴 r'^[가-힣\s]{5,50}\s[-–]\s.{5,30}$', # 강조 표현 패턴 (예: 효과적인 방법 - 실천하기) r'^[가-힣A-Za-z\s]{10,50}[\.!\?]$', # 긴 문장으로 된 소제목 패턴 ] formatted_lines = [] in_paragraph = False # 본론 섹션 번호 추적 section_number = 1 for i, line in enumerate(lines): line = line.strip() if not line: if in_paragraph: formatted_lines.append("") in_paragraph = False formatted_lines.append("") in_paragraph = True content = html.escape(line) bold_content = re.sub(r'\*\*(.*?)\*\*', r'\1', content) formatted_lines.append(bold_content) if in_paragraph: formatted_lines.append("
") return '\n'.join(formatted_lines) def call_gemini_api(prompt, temperature=TEMPERATURE, top_p=TOP_P): """Gemini API 호출 함수""" try: logging.info("Gemini API 호출 시작") response = client.models.generate_content( model="gemini-2.0-flash", contents=[prompt], config=types.GenerateContentConfig( max_output_tokens=MAX_TOKENS, temperature=temperature, top_p=top_p ) ) logging.info("Gemini API 호출 완료") return response.text.strip() except Exception as e: logging.error(f"Gemini API 호출 중 오류 발생: {str(e)}") return f"API 호출 중 오류 발생: {str(e)}" def get_style_prompt(style="친근한"): """블로그 글의 스타일 프롬프트를 반환""" prompts = { "친근한": """ [친근한 여행 블로그 스타일 가이드] 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 - 여행지에 대한 관심과 호기심을 담은 표현 사용 - 실제 여행을 경험한 것처럼 생생한 묘사 2. 문장 및 어투 - 반드시 '해요체'로 작성, 절대 '습니다'체를 사용하지 말 것 - '~요'로 끝나도록 작성, '~다'로 끝나지 않게 하라 - 구어체 표현 사용 (예: "~했어요", "~인 것 같아요") - 적절한 감정 표현과 공감대 형성 3. 용어 및 설명 방식 - 여행 관련 전문 용어는 쉬운 단어로 풀어서 설명 - 비유나 은유를 활용하여 장소와 경험 묘사 - 수사의문문 활용하여 독자와 소통하는 느낌 주기 (예: "어떻게 생각하세요?", "이런 경험 있으신가요?") - 구체적 사례와 실제 경험에 기반한 팁 제공 4. 정보 전달 방식 - 개인적인 관점에 녹여 자연스럽게 정보 전달 - 실제 여행자의 시선으로 장소와 경험 묘사 - 독자가 실제로 활용할 수 있는 실용적 정보 제공 (교통, 비용, 시간, 추천 코스 등) 5. 독자와의 상호작용 - 독자의 의견을 물어보는 질문 포함 - 실제 여행에 적용할 수 있는 팁이나 조언 제공 주의사항: 자연스러운 대화체를 유지하면서 정보의 질과 내용의 깊이를 잃지 않도록 한다 """, "일반": """ [일반적인 여행 블로그 스타일 가이드] 1. 톤과 어조 - 중립적이고 객관적인 톤 유지 - 적절한 존댓말 사용 (예: "~합니다", "~입니다") - 여행 정보 전달 중심의 명확한 어투 2. 내용 구조 및 전개 - 명확한 여행지 소개로 시작 - 논리적인 순서로 정보 전개 (교통 → 숙박 → 관광지 → 음식 등) - 핵심 포인트를 강조하는 소제목 활용 - 적절한 길이의 단락으로 구성 3. 용어 및 설명 방식 - 일반적으로 이해하기 쉬운 용어 선택 - 필요시 지역 특유의 용어에 간단한 설명 추가 - 객관적인 여행 정보 제공에 중점 - 균형 잡힌 시각에서 여행지의 장단점 제시 4. 정보 전달 방식 - 여행지의 기본 정보와 특징 명확하게 제공 - 구체적인 예시와 추천 코스 포함 - 최신 여행 정보와 동향 참고 5. 독자 상호작용 - 적절히 독자의 생각을 묻는 질문 포함 - 추가 정보를 찾을 수 있는 키워드 제시 - 실용적인 여행 팁 제공 주의사항: 객관적 정보 제공을 중심으로 하되, 독자의 여행 계획과 실행을 도울 수 있는 맥락과 설명을 충분히 제공한다 """, "전문적인": """ [전문적인 여행 블로그 스타일 가이드] 1. 톤과 구조 - 공식적이고 전문적인 톤 사용 - 객관적이고 분석적인 접근 유지 - 명확한 서론(여행지 개요), 본론(상세 분석), 결론(종합 평가) 구조 - 체계적인 여행 정보 전개 - 세부 섹션을 위한 명확한 소제목 사용 2. 내용 구성 및 전개 - 여행지의 역사적 배경, 문화적 특징, 현재 동향 등 심층적 정보 포함 - 논리적 연결을 위한 전환어 활용 - 여행 용어 적절히 활용 (필요시 간략한 설명 제공) - 심층적인 분석과 비판적 평가 제공 - 다양한 관점에서 여행지 분석 3. 데이터 및 근거 활용 - 통계, 시즌별 정보, 실제 사례 등 객관적 데이터 활용 - 여행지 분석을 위한 체계적인 프레임워크 제시 - 객관적 정보와 전문가 관점의 균형 4. 전문적 정보 제공 - 최신 여행 동향 및 변화 분석 - 문화, 역사적 맥락에서의 여행지 분석 - 여행 관련 쟁점과 고려사항 소개 - 체계적인 여행 계획 접근법 제시 주의사항: 전문성과 깊이를 유지하면서도 이해 가능한 용어와 설명을 통해 접근성을 높인다 """ } return prompts.get(style, prompts["친근한"]) def get_category_outline_prompt(category="여행 단일"): """카테고리별 아웃라인 생성 프롬프트""" prompts = { "여행 단일": """ [여행 단일지점 소주제(Outline) 생성 규칙] 1. 너는 최고의 여행 블로그 작가이자 여행 콘텐츠 구성 전문가이다. 2. 주어진 참고글을 깊이 분석하여 해당 여행지에 대한 가장 효과적인 콘텐츠 구조를 만들어라. 3. 여행자의 관심을 끌고 유용한 정보를 전달하는 데 초점을 맞춰라. 4. 여행지에 대한 통찰력 있는 분석과 실용적인 정보를 제공하는 구조를 설계하라. [아웃라인 작성 규칙] 1. 반드시 참고글을 깊이 분석하여 여행지의 핵심 특징과 가치 있는 정보를 파악하라. 2. 여행자의 관심을 끌기 위한 매력적인 소개로 시작하라. 3. 여행지에 대한 심층적 이해를 제공하는 논리적 구조를 만들어라. 4. 각 소주제는 20자 이내의 명확하고 매력적인 제목으로 작성하라. 5. 소주제들은 전체적으로 일관된 흐름과 연결성을 가져야 한다. 6. 여행자에게 실질적인 가치와 통찰을 제공하는 정보 중심으로 구성하라. 7. 참고글의 내용과 특성에 따라 가장 적합한 소주제를 유연하게 구성하라. [아웃라인 구성] 반드시 다음 구조로 소주제를 생성하고 그대로 출력하라: - 도입부: [여행지 소개 및 특징] - 본론1~5: [참고글에서 파악된 주요 내용에 따라 유연하게 구성] - 결론: [여행지 최종 평가 및 추천] """, "여행 코스": """ [여행 코스 소주제(Outline) 생성 규칙] 1. 너는 최고의 여행 블로그 작가이자 여행 코스 기획 전문가이다. 2. 주어진 참고글을 깊이 분석하여 최적의 여행 코스 구성과 콘텐츠 구조를 만들어라. 3. 여행자의 시간과 경험을 최적화할 수 있는 코스 구성에 초점을 맞춰라. 4. 실용적이고 효율적인 여행 계획을 제공하는 구조를 설계하라. [아웃라인 작성 규칙] 1. 참고글을 철저히 분석하여 여행 코스의 핵심 요소와 흐름을 파악하라. 2. 여행 코스의 매력과 특징을 강조하는 소개로 시작하라. 3. 여행 일정의 논리적 흐름을 반영한 구조를 만들어라. 4. 각 소주제는 20자 이내의 명확하고 매력적인 제목으로 작성하라. 5. 소주제들이 전체 여행 흐름을 자연스럽게 이끌어갈 수 있도록 구성하라. 6. 여행자가 실제로 활용할 수 있는 구체적인 정보 중심으로 구성하라. 7. 참고글의 내용과 특성에 따라 가장 적합한 소주제를 유연하게 구성하라. [아웃라인 구성] 반드시 다음 구조로 소주제를 생성하고 그대로 출력하라: - 도입부: [여행 코스 개요 및 특징] - 본론1~5: [참고글에서 파악된 주요 내용에 따라 유연하게 구성] - 결론: [코스의 장점 및 최종 조언] """ } return prompts.get(category, prompts["여행 단일"]) def generate_outline(category, style, references1, references2, references3): """아웃라인 생성 함수""" try: category_prompt = get_category_outline_prompt(category) style_prompt = get_style_prompt(style) # 참고글 정보 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 meaningful_refs = [ref for ref in references if ref != "참고 자료 없음"] if not meaningful_refs: return "참고 자료가 없습니다. 최소 하나 이상의 참고 자료를 입력해주세요." outline_prompt = f""" [여행 블로그 아웃라인 생성 요청] 카테고리: {category} 포스팅 스타일: {style} 참고글: {references[0]} {references[1] if len(meaningful_refs) > 1 else ""} {references[2] if len(meaningful_refs) > 2 else ""} {category_prompt} 아웃라인 생성 시 추가 지침: 1. 참고글의 핵심 주제와 가치 있는 정보를 정확히 파악하라. 2. 각 소주제는 20자 이내로 명확하고 매력적으로 작성하라. 3. 전체 아웃라인이 논리적 흐름과 일관성을 가지도록 구성하라. 4. 독자의 관심과 호기심을 유발하는 소주제를 설계하라. 5. 소주제만 간결하게 출력하고 설명은 포함하지 말라. 6. 각 소주제가 약속된 섹션(도입부, 본론1~5, 결론)에 적합한지 확인하라. 7. 백틱(```)이나 코드 블록 표시를 사용하지 말라. 8. 정확히 7줄로 구성하여 '도입부', '본론1~5', '결론'만 출력하라. """ # Gemini API 호출 outline_result = call_gemini_api(outline_prompt, temperature=0.7) # 결과 후처리 (불필요한 형식 제거) outline_result = re.sub(r'^\s*[-*]\s+', '', outline_result, flags=re.MULTILINE) outline_result = re.sub(r'^\s*\d+\.\s+', '', outline_result, flags=re.MULTILINE) # 백틱 및 코드 블록 제거 outline_result = re.sub(r'```[a-zA-Z]*\n?', '', outline_result) outline_result = re.sub(r'```', '', outline_result) # 정확히 7줄 형식으로 정리 lines = outline_result.strip().split('\n') clean_lines = [] for line in lines: line = line.strip() if line and (line.startswith('도입부:') or line.startswith('본론') or line.startswith('결론:')): clean_lines.append(line) # 정확히 7줄이 나오도록 조정 if len(clean_lines) > 7: clean_lines = clean_lines[:7] elif len(clean_lines) < 7: sections = ['도입부:', '본론1:', '본론2:', '본론3:', '본론4:', '본론5:', '결론:'] while len(clean_lines) < 7: missing_section = sections[len(clean_lines)] clean_lines.append(f"{missing_section} 추가 내용 필요") return '\n'.join(clean_lines) except Exception as e: logging.error(f"아웃라인 생성 중 오류 발생: {str(e)}") return f"아웃라인 생성 중 오류 발생: {str(e)}" def save_content_to_pdf(blog_post, user_topic=""): # 사용하지 않지만 호환성을 위해 남겨둠 return None def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() # API 함수들 def generate_outline_2(category, style, ref1, ref2, ref3): return generate_outline(category, style, ref1, ref2, ref3) def generate_blog_post_2(category, style, ref1, ref2, ref3, outline): result = generate_blog_post(category, style, outline, ref1, ref2, ref3) return result[0] # 튜플에서 HTML만 반환