import gradio as gr import random import os import re import openai import textwrap from fpdf import FPDF from datetime import datetime from zoneinfo import ZoneInfo from sklearn.feature_extraction.text import CountVectorizer from weasyprint import HTML, CSS from urllib.request import urlopen import tempfile import markdown2 from bs4 import BeautifulSoup from PIL import Image # Pretendard OTF 폰트 파일 경로 설정 FONT_REGULAR_PATH = os.path.join("Pretendard-Regular.otf") FONT_BOLD_PATH = os.path.join("Pretendard-Bold.otf") # OpenAI API 클라이언트 설정 openai.api_key = os.getenv("OPENAI_API_KEY") def call_api(content, system_message, max_tokens, temperature, top_p, previous_messages=None): try: if previous_messages is None: previous_messages = [] messages = [] if system_message: messages.append({"role": "system", "content": system_message}) messages.extend(previous_messages) messages.append({"role": "user", "content": content}) response = openai.ChatCompletion.create( model="gpt-4o-mini", messages=messages, max_tokens=max_tokens, temperature=temperature, top_p=top_p, request_timeout=50 ) return response.choices[0].message['content'] except Exception as e: print(f"API 호출 중 오류 발생: {str(e)}") raise def analyze_info(data): return f"선택한 카테고리: {data['category']}\n선택한 포스팅 스타일: {data['style']}\n참고 글1: {data['references1']}\n참고 글2: {data['references2']}\n참고 글3: {data['references3']}" def generate_outline(category, style, references1, references2, references3): data = { 'category': category, 'style': style, 'references1': references1, 'references2': references2, 'references3': references3 } full_content = analyze_info(data) content = full_content system_prompt = get_outline_prompt(data['category']) + "\n\n" + get_style_prompt(data['style']) modified_text = call_api(content, system_prompt, 2000, 0.7, 0.95) return modified_text def remove_unwanted_phrases(text): unwanted_phrases = [ '여러분', '최근', '마지막으로', '결론적으로', '결국', '종합적으로', '따라서', '마무리', '요약' ] words = re.findall(r'\S+|\n', text) result_words = [word for word in words if not any(phrase in word for phrase in unwanted_phrases)] return ' '.join(result_words).replace(' \n ', '\n').replace(' \n', '\n').replace('\n ', '\n') def extract_keywords(text, top_n=5): vectorizer = CountVectorizer(stop_words='english', ngram_range=(1,2)) count_matrix = vectorizer.fit_transform([text]) terms = vectorizer.get_feature_names_out() counts = count_matrix.sum(axis=0).A1 term_counts = sorted(zip(terms, counts), key=lambda x: x[1], reverse=True) return [term for term, count in term_counts[:top_n]] def get_group_instruction(group_index): instructions = [ """ **전체 콘텐츠를 생성하기 위한 아웃라인 섹션의 일부이다.** 1. 주어진 아웃라인 1그룹의 첫번째줄은 도입부이다. 2. 반드시 도입부에 대해서만 간략하게 작성하라. 3. 도입부에서는 주제를 소개하고, 독자의 관심을 끌어야 한다(반드시 200자 이내로 작성) 4. 주어진 아웃라인 1그룹의 두번째줄은 본문1 이다. 5. 반드시 본문1로서만 작성하라. 6. 반드시 다음 섹션으로의 전환을 염두에 두고 작성하되, 현재 섹션을 완전히 마무리하지 말 것. 7. 본문1 주제의 내용을 상세 담아 약 190단어로 작성하라. """, """ **전체 콘텐츠를 생성하기 위한 아웃라인 섹션의 일부이다.** 1. 주어진 아웃라인 2그룹은 모두 본론(본론2~4)이다. 2. 반드시 본론의 형태로만 내용과 정보를 매우 자세하게 작성하라. 3. 이전 섹션에서 제시된 내용과 자연스럽게 연결되게 흐름을 유지하라. 4. 반드시 결론으로 넘어가지 않도록 주의하라. 5. 반드시 각 섹션별 주제에 맞는 내용으로로 약 280~330단어로 작성하라. """, """ **전체 콘텐츠를 생성하기 위한 아웃라인 섹션의 일부이다.** 1. 주어진 아웃라인 3그룹의 첫번째줄은 본론5(마지막 본론)이다. 2. 반드시 본론의 형태로만 작성하라. 3. 본론은 반드시 190단어 이하로 작성하라. 4. 주어진 아웃라인 3그룹의 두번째줄은 전체 콘텐츠의 결론부이다. 5. 반드시 결론의 형태로만 작성하라(인사말 금지) 6. 반드시 결론에서는 전체 내용을 요약하고 최종 메시지를 전달하라. 7. 결론은 200자 이내로 작성하라. """ ] return instructions[group_index] def generate_blog_post(category, style, references1, references2, references3, outline): try: data = { 'category': category, 'style': style, 'references1': references1, 'references2': references2, 'references3': references3, 'outline': outline } system_prompt = get_blog_post_prompt(data['category']) style_prompt = get_style_prompt(data['style']) outline_sections = data['outline'].split('\n') grouped_sections = [ outline_sections[:2], # 도입부 + 본문1 outline_sections[2:5], # 본문2 + 본문3 + 본문4 outline_sections[5:] # 본문5 + 결론 ] sections = [] previous_content = "" previous_messages = [] for i, section_group in enumerate(grouped_sections): print(f"섹션 그룹 {i+1}/{len(grouped_sections)} 생성 중...") max_tokens = [1000, 5000, 1000][i] group_instruction = get_group_instruction(i) user_prompt = f""" 카테고리: {data['category']} 포스팅 스타일: {data['style']} 참고글1: {data['references1']} 참고글2: {data['references2']} 참고글3: {data['references3']} 현재 섹션 그룹: {' / '.join(section_group)} 이전 섹션의 마지막 내용: {previous_content} 위의 정보를 바탕으로 '{' / '.join(section_group)}' 섹션 그룹의 내용을 작성하되, 다음 지침을 반드시 준수하라: 1. {group_instruction} 2. 전체 아웃라인의 흐름을 고려하여 일관성 있게 작성. 3. 반드시 참고글의 내용을 바탕으로 구성하되, 표현을 그대로 복사하지 말 것. 4. 각 섹션의 주요 내용을 충분히 다루되, 전체 글의 흐름을 해치지 않도록 주의. 5. 이전 섹션과의 연결부분을 자연스럽게 이어지도록 작성. 6. 각 섹션의 제목을 작성하라. """ try: section_content = call_api( user_prompt, system_prompt + "\n" + style_prompt, max_tokens, 0.7, 0.95, previous_messages=previous_messages ) sections.append(section_content) lines = section_content.splitlines() previous_content = "\n".join(lines[-2:]) previous_messages.append({"role": "user", "content": user_prompt}) previous_messages.append({"role": "assistant", "content": section_content}) except Exception as e: print(f"섹션 그룹 {i+1} 생성 중 오류 발생: {str(e)}") sections.append(f"섹션 그룹 {i+1} 생성 중 오류 발생") full_post = "\n\n".join(sections) print(f"GPT가 생성한 원본 블로그 글:\n{full_post}") filtered_post = remove_unwanted_phrases(full_post) filtered_post = filtered_post.lstrip() # HTML로 변환 html_post = convert_to_html(filtered_post) return html_post except Exception as e: print(f"글 생성 중 오류 발생: {str(e)}") return "" def convert_to_html(text): lines = text.split('\n') html_lines = [] for line in lines: line = line.strip() if line.startswith('####'): html_lines.append(f"

{line[4:].strip()}

") elif line.startswith('###'): html_lines.append(f"

{line[3:].strip()}

") elif line.startswith('##'): html_lines.append(f"

{line[2:].strip()}

") elif line.startswith('#'): html_lines.append(f"

{line[1:].strip()}

") elif line.startswith('- '): # 리스트 아이템 html_lines.append(f"
  • {line[2:]}
  • ") elif line: # 일반 텍스트 (빈 줄 제외) # '**'로 감싸진 부분을 태그로 변환 line = re.sub(r'\*\*(.*?)\*\*', r'\1', line) html_lines.append(f"

    {line}

    ") else: # 빈 줄 html_lines.append("
    ") html_content = f"""
    {"".join(html_lines)}
    """ return html_content 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 get_outline_prompt(category): if (category == "일반"): return """ [상품리뷰 소주제(Outline) 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 가장 주목받는 '상품리뷰' 전문 블로거 마케터이다. 3. 전문적인 상품 정보와 후기 제공에 초점을 맞춰 작성하라. 5. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라 [아웃라인 작성 규칙] 1. 블로그 글을 작성하기 위한 소주제(섹션)만을 작성하라 2. 반드시 입력된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제(섹션)를 구성하라 3. 전체 맥락에 맞게 소주제(섹션)를 작성 4. 반드시 소제목으로 사용할 수 있도록 30자 이내로 작성하라 5. 참고글을 분석하여 독자가 얻고자 하는 상품의 흥미로운 전문적인 정보를 제공하도록 소주제를 작성하라. - 구매 배경 또는 사용 계기, 시각적요소(디자인), 스펙(특장점), 장단점, 구매과정, 구매처, 할인정보 등 - 디테일한 특징 설명, 사용 경험, 팁등 다양한 정보 등 **이러한 정보들뿐 아니라 참고글을 분석하여 소주제를 확장하여 선정하라** 6. 반드시 [소주제 구성]에 맞게 소주제만 출력하라 [아웃라인 구성] 1. 반드시 [도입부]-1개, [본론]-5개, [결론]-1개로 구성하여 출력하라 2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라 출력예시: - 도입부: 제목 - 본론1: 제목 ... - 본론5: 제목 - 결론: 제목 """ elif (category == "기능집중형"): return """ [상품리뷰 소주제(Outline) 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 가장 주목받는 '상품리뷰' 전문 블로거 마케터이다. 3. 반드시 상품의 기능적 측면에서 전문적인 정보 제공에 초점을 맞춰 작성하라. 4. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라 [아웃라인 작성 규칙] 1. 블로그 글을 작성하기 위한 소주제(섹션)만을 작성하라 2. 반드시 입력된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제(섹션)를 구성하라 3. 전체 맥락에 맞게 소주제(섹션)를 작성 4. 반드시 소제목으로 사용할 수 있도록 30자 이내로 작성하라 5. 참고글을 분석하여 독자가 얻고자 하는 상품의 기능 중에서 흥미롭고 전문적인 정보를 제공하도록 소주제를 작성하라. 6. 반드시 [소주제 구성]에 맞게 소주제만 출력하라 [아웃라인 구성] 1. 반드시 [도입부]-1개, [본론]-5개, [결론]-1개로 구성하여 출력하라 2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라 출력예시: - 도입부: 제목 - 본론1: 제목 ... - 본론5: 제목 - 결론: 제목 """ elif (category == "고객반응형"): return """ [상품리뷰 소주제(Outline) 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 가장 주목받는 '상품리뷰' 전문 블로거 마케터이다. 3. 반드시 상품에 대한 고객의 실제 반응에만 초점을 맞춰 작성하라. 4. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라 [아웃라인 작성 규칙] 1. 블로그 글을 작성하기 위한 소주제(섹션)만을 작성하라 2. 반드시 입력된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제(섹션)를 구성하라 3. 전체 맥락에 맞게 소주제(섹션)를 작성 4. 반드시 소제목으로 사용할 수 있도록 30자 이내로 작성하라 5. 참고글을 분석하여 상품에 대한 실제 고객반응(후기, 리뷰) 정보를 제공하도록 소주제를 작성하라. 6. 반드시 [소주제 구성]에 맞게 소주제만 출력하라 [아웃라인 구성] 1. 반드시 [도입부]-1개, [본론]-5개, [결론]-1개로 구성하여 출력하라 2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라 출력예시: - 도입부: 제목 - 본론1: 제목 ... - 본론5: 제목 - 결론: 제목 """ def get_blog_post_prompt(category): if (category == "일반"): return """ [상품리뷰 콘텐츠 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 전문적인 상품리뷰 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라 3. 각 섹션을 독립적인 정보 단위로 취급하라 4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라 5. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라 6. 상품의 특장점, 각종 정보, 팁등을 자세히 설명하라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 반드시 입력된 참고글의 내용으로만 구성 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것 5. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성 6. 참고글을 기반으로 소비자 타겟을 분석하여 작성 7. 시각적(디자인, 외관 등)인 부분, 스펙, 기능, 성능, 사용경험, 장단점, 가격 대비 성능(가성비), 추가비용등을 고려 8. 다른 제품과의 비교가 가능하다면 반영하라(수치, 데이터 포함) 9. 구체적으로 상품이 주는 유익(일상생활, 업무 등에서)에 대한 분석, 평가, 전후 비교, 경험, 추천등을 포함 10. 상품의 유지 관리 방법, 레시피 등을 공유하라 """ elif (category == "기능집중형"): return """ [상품리뷰 콘텐츠 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 전문적인 상품리뷰 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라 3. 각 섹션을 독립적인 정보 단위로 취급하라 4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라 5. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라 6. 반드시 상품의 기능적인 측면에 집중하여 작성하라 7. 상품의 기능중 특장점, 각종 정보, 팁 등을 자세히 설명하라 8. 전문 용어를 사용할 경우 일반 사용자도 이해가 되도록 풀어서 설명하라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 반드시 입력된 참고글의 내용으로만 구성 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것 5. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성 6. 참고글을 기반으로 소비자 타겟을 분석하여 작성 7. 사용자의 니즈에 따른 기능의 활용에 대해 설명 8. 스펙, 기능, 성능, 장단점, 가격 대비 성능(가성비), 팁, 주의사항 등 기능적인 측면에 집중되게 작성 9. 기능과 관련된 구체적인 수치(데이터)가 있다면 반영하라 """ elif (category == "고객반응형"): return """ [상품리뷰 콘텐츠 생성 규칙] [기본규칙] 1. 반드시 한국어(한글)로 작성하라 2. 너는 전문적인 상품리뷰 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라 3. 각 섹션을 독립적인 정보 단위로 취급하라 4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라 5. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라 6. 반드시 상품에 대한 실제 반응에 초점을 맞추어 작성하라 7. 상품에 대한 실제반응을 바탕으로 특장점, 각종 정보, 팁 등을 자세히 설명하라(다양한 후기를 포함하라) 8. 전문 용어를 사용할 경우 일반 사용자도 이해가 되도록 풀어서 설명하라 [텍스트 작성 규칙] 1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라 2. 반드시 입력된 참고글의 내용으로만 구성 3. 전체 맥락을 이해하고 문장의 일관성을 유지하라 4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것 5. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성 6. 참고글을 기반으로 실제반응이 포함된 소비자 타겟을 분석하여 작성 7. 실제 반응이 반영된 사용자의 니즈, 활용에 대해 설명 8. 실제 반응을 바탕으로 스펙, 기능, 성능, 장단점, 가격 대비 성능(가성비), 팁, 주의사항 등을 작성 9. 실제 반응을 바탕으로 상품과 관련된 구체적인 수치(데이터)가 있다면 반영하라 """ def get_style_prompt(style): prompts = { "친근한": """ [친근한 포스팅 스타일 가이드] 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 2. 문장 및 어투 - 반드시 '해요체'로 작성, 절대 '습니다'체를 사용하지 말 것. - '~요'로 끝나도록 작성, '~다'로 끝나지 않게 하라 - 구어체 표현 사용 (예: "~했어요", "~인 것 같아요") 3. 용어 및 설명 방식 - 전문 용어 대신 쉬운 단어로 풀어서 설명 - 비유나 은유를 활용하여 복잡한 개념 설명 - 수사의문문 활용하여 독자와 소통하는 느낌 주기 주의사항: 너무 가벼운 톤은 지양하고, 주제의 중요성을 해치지 않는 선에서 친근함 유지 (예시: 잇님들~ 오레오 코카콜라맛이새로 출시가 됐다는거 알고 계셨나요?!ㅎ 오레오 코카콜라맛은 어떤지 솔직평과구매정보, 가격, 칼로리 등에 대해 자세~ 히 적어보도록 할께요! 오레오를 좋아하는 아들에게간식으로 오레오 코카콜라맛을 줬더니맛있다고 좋아하더라구요. 콜라향이 나서 더 마음에 든다며ㅎ개인적으로는 별 ⭐️⭐️⭐️.요건 개인차가 있을거 같아요~) """, "일반": """ #일반적인 블로그 포스팅 스타일 가이드 1. 톤과 어조 - 중립적이고 객관적인 톤 유지 - 적절한 존댓말 사용 (예: "~합니다", "~입니다") 2. 내용 구조 및 전개 - 명확한 주제 제시로 시작 - 논리적인 순서로 정보 전개 - 주요 포인트를 강조하는 소제목 활용 - 적절한 길이의 단락으로 구성 3. 용어 및 설명 방식 - 일반적으로 이해하기 쉬운 용어 선택 - 필요시 간단한 설명 추가 - 객관적인 정보 제공에 중점 4. 텍스트 구조화 - 불릿 포인트나 번호 매기기를 활용하여 정보 구조화 - 중요한 정보는 굵은 글씨나 기울임꼴로 강조 5. 독자 상호작용 - 적절히 독자의 생각을 묻는 질문 포함 - 추가 정보를 찾을 수 있는 키워드 제시 6. 마무리 - 주요 내용 간단히 요약 - 추가 정보에 대한 안내 제공 주의사항: 너무 딱딱하거나 지루하지 않도록 균형 유지 예시: "최근 환경 문제가 대두되면서 '제로 웨이스트' 라이프스타일에 대한 관심이 높아지고 있습니다. 제로 웨이스트란 일상생활에서 발생하는 쓰레기를 최소화하는 생활 방식을 말합니다. 이 글에서는 제로 웨이스트의 개념, 실천 방법, 그리고 그 효과에 대해 알아보겠습니다. 먼저 제로 웨이스트의 정의부터 살펴보면... """, "전문적인": """ #전문적인 블로그 포스팅 스타일 가이드 1. 톤과 구조 - 공식적이고 학술적인 톤 사용 - 객관적이고 분석적인 접근 유지 - 명확한 서론, 본론, 결론 구조 - 체계적인 논점 전개 - 세부 섹션을 위한 명확한 소제목 사용 2. 내용 구성 및 전개 - 복잡한 개념을 정확히 전달할 수 있는 문장 구조 사용 - 논리적 연결을 위한 전환어 활용 - 해당 분야의 전문 용어 적극 활용 (필요시 간략한 설명 제공) - 심층적인 분석과 비판적 사고 전개 - 다양한 관점 제시 및 비교 3. 데이터 및 근거 활용 - 통계, 연구 결과, 전문가 의견 등 신뢰할 수 있는 출처 인용 - 필요시 각주나 참고문헌 목록 포함 - 수치 데이터는 텍스트로 명확히 설명 4. 텍스트 구조화 - 논리적 구조를 강조하기 위해 번호 매기기 사용 - 핵심 개념이나 용어는 기울임꼴로 강조 - 긴 인용문은 들여쓰기로 구분 5. 마무리 - 핵심 논점 재강조 - 향후 연구 방향이나 실무적 함의 제시 주의사항: 전문성을 유지하되, 완전히 이해하기 어려운 수준은 지양 예시: "본 연구에서는 인공지능(AI)의 윤리적 함의에 대해 고찰한다. 특히, 자율주행 자동차의 의사결정 알고리즘에서 발생할 수 있는 윤리적 딜레마에 초점을 맞춘다. Bonnefon et al. (2016)의 연구에 따르면, 자율주행 차량의 알고리즘이 직면할 수 있는 윤리적 선택의 복잡성이 지적된 바 있다. 본고에서는 이러한 윤리적 딜레마를 세 가지 주요 관점에서 분석한다: 1) 공리주의적 접근, 2) 의무론적 접근, 3) 덕 윤리적 접근. 각 접근법의 장단점을 비교 분석하고, 이를 바탕으로 자율주행 차량의 윤리적 의사결정 프레임워크를 제안하고자 한다... """ } return prompts.get(style, "일반적인 포스팅 스타일을 사용하세요.") # PDF 클래스 정의 class PDF(FPDF): def __init__(self): super().__init__() self.add_font("Pretendard", "", FONT_REGULAR_PATH, uni=True) self.add_font("Pretendard", "B", FONT_BOLD_PATH, uni=True) def header(self): self.set_font("Pretendard", "", 10) def footer(self): self.set_y(-15) self.set_font("Pretendard", "", 8) self.cell(0, 10, f'Page {self.page_no()}', align='C') def save_to_pdf(blog_post, user_topic): pdf = PDF() pdf.add_page() # HTML에서 텍스트 추출 soup = BeautifulSoup(blog_post, 'html.parser') # 도입부 제목에서 "도입부:"를 제외하고 추출 title_tag = soup.find("h3") title = title_tag.text.replace("도입부:", "").strip() if title_tag else "블로그 글" # 페이지 및 이미지 크기 설정 page_width = pdf.w - 2 * pdf.l_margin image_width = page_width # 상단 이미지 경로와 링크 설정 image_url1 = "https://finalendai.com/wp-content/uploads/2024/10/pdf-banner-top.png" image_url2 = "https://finalendai.com/wp-content/uploads/2024/10/pdf-banner-bottom.png" # 첫 번째 이미지 삽입 (상단) with urlopen(image_url1) as response: with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file: tmp_file.write(response.read()) tmp_file_path = tmp_file.name try: img_width, img_height = Image.open(tmp_file_path).size ratio = img_height / img_width image_height = image_width * ratio x = (pdf.w - image_width) / 2 y = pdf.get_y() pdf.link(x, y, image_width, image_height, "https://finalendai.com") pdf.image(tmp_file_path, x=x, y=y, w=image_width) pdf.ln(image_height + 10) finally: os.unlink(tmp_file_path) # 제목 출력 (한 번만) pdf.set_font("Pretendard", "B", 16) pdf.multi_cell(0, 10, title, align='C') pdf.ln(10) # 본문 내용 추가 pdf.set_font("Pretendard", "", 12) for tag in soup.find_all(["h2", "h3", "p", "ul", "li"]): if tag.name == "h2": pdf.set_font("Pretendard", "B", 14) pdf.multi_cell(0, 8, tag.get_text().strip()) pdf.ln(4) elif tag.name == "h3": pdf.set_font("Pretendard", "B", 12) pdf.multi_cell(0, 6, tag.get_text().strip()) pdf.ln(3) elif tag.name == "p": pdf.set_font("Pretendard", "", 12) pdf.multi_cell(0, 8, tag.get_text().strip()) pdf.ln(4) elif tag.name == "ul" or tag.name == "li": pdf.set_font("Pretendard", "", 12) pdf.multi_cell(0, 8, f"• {tag.get_text().strip()}") pdf.ln(4) # 하단 이미지 삽입 (링크 포함) with urlopen(image_url2) as response: with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp_file: tmp_file.write(response.read()) tmp_file_path = tmp_file.name try: if pdf.get_y() + image_height > pdf.page_break_trigger: pdf.add_page() x = (pdf.w - image_width) / 2 y = pdf.get_y() pdf.link(x, y, image_width, image_height, "https://finalendai.com/story/") pdf.image(tmp_file_path, x=x, y=y, w=image_width) finally: os.unlink(tmp_file_path) # 파일 저장 now = datetime.now(ZoneInfo("Asia/Seoul")) filename = f"{now.strftime('%y%m%d_%H%M')}_{format_filename(title)}.pdf" pdf.output(filename) return filename def format_filename(text): text = re.sub(r'[^\w\s-]', '', text) return text[:50].strip() def save_content_to_pdf(blog_post, user_topic): # 함수 수정 return save_to_pdf(blog_post, user_topic) with gr.Blocks() as demo: gr.Markdown(f"# 블로그 포스팅 생성기") gr.Markdown("### 1단계: 포스팅 카테고리를 지정해주세요", elem_id="step-title") category = gr.Radio(choices=["일반", "기능집중형", "고객반응형"], label="포스팅 카테고리", value="일반") gr.Markdown("---\n\n") gr.Markdown("### 2단계: 포스팅 스타일을 선택해주세요", elem_id="step-title") style = gr.Radio(choices=["친근한", "일반", "전문적인"], label="포스팅 스타일", value="친근한") gr.Markdown("---\n\n") gr.Markdown("### 3단계: 참고 글을 입력하세요", elem_id="step-title") references1 = gr.Textbox(label="참고 글 1", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) references2 = gr.Textbox(label="참고 글 2", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) references3 = gr.Textbox(label="참고 글 3", placeholder="참고할 글을 복사하여 붙여넣으세요", lines=10) gr.Markdown("---\n\n") gr.Markdown("### 4단계: 아웃라인을 작성해주세요", elem_id="step-title") gr.HTML("[아웃라인에서 나온 결과를 수정해서 사용해주세요]") outline_generate_btn = gr.Button("아웃라인 생성하기") outline_result = gr.Textbox(label="아웃라인 결과", lines=15) outline_input = gr.Textbox(label="작성할 아웃라인을 입력해주세요", placeholder="생성된 아웃라인 복사, 수정해서 사용하세요", lines=10) outline_generate_btn.click( fn=generate_outline, inputs=[category, style, references1, references2, references3], outputs=[outline_result] ) gr.Markdown("---\n\n") gr.Markdown("### 5단계: 글 생성하기", elem_id="step-title") gr.HTML("[아웃라인을 확인하세요]") generate_btn = gr.Button("블로그 글 생성하기") output = gr.HTML(label="생성된 블로그 글") generate_btn.click( fn=generate_blog_post, inputs=[category, style, references1, references2, references3, outline_input], outputs=[output], show_progress=True ) save_pdf_btn = gr.Button("PDF로 저장하기") pdf_output = gr.File(label="생성된 PDF 파일") save_pdf_btn.click( fn=save_content_to_pdf, inputs=[output], outputs=[pdf_output], show_progress=True ) demo.launch() gr.HTML(""" """)