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. 특히 너는 '여행 단일지점 리뷰'에 대한 전문적인 블로그 마케터이다
4. 입력된 여행지의 주제와 제목을 바탕으로 해당 여행지의 매력을 부각할 수 있는 소주제만 작성하라
5. 방문 시 필요한 정보, 숨겨진 명소, 추천 포인트등을 반영한 소주제(섹션)를 구성하라
6. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라
[아웃라인 작성 규칙]
1. 블로그 글을 작성하기 위한 소주제(섹션)만을 작성하라
2. 반드시 입력된 참고글과 블로그 주제, 제목을 바탕으로 핵심 주제를 파악하여 소주제(섹션)를 구성하라
3. 전체 맥락에 맞게 소주제(섹션)를 작성
4. 반드시 소제목으로 사용할 수 있도록 30자 이내로 작성하라
5. 독자가 얻고자 하는 정보와 흥미로운 정보를 제공하도록 소주제를 작성하라.
6. 반드시 [소주제 구성]에 맞게 소주제만 출력하라
[아웃라인 구성]
1. 반드시 [도입부]-1개, [본론]-5개, [결론]-1개로 구성하여 출력하라
2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라
출력예시:
- 도입부: 제목
- 본론1: 제목
...
- 본론5: 제목
- 결론: 제목
"""
elif (category == "여행 코스"):
return """
[여행 블로그 소주제(Outline) 생성 규칙]
[기본규칙]
1. 반드시 한국어(한글)로 작성하라
2. 너는 가장 주목받는 여행 블로거이며 여행 마케팅 전문가이다
3. 특히 너는 '여행 코스 추천'에 대한 전문적인 블로그 마케터이다
4. 반드시 입력된 여행지 정보와 주제, 제목을 바탕으로 여행할 때 꼭 필요한 정보와 추천 루트등을 부각할 수 있는 소주제만 작성하라
5. 여행의 일정이나 코스, 이동 순서와 특징을 고려하여 흥미롭고 유용한 소주제(섹션)만 구성하라
6. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라
[아웃라인 작성 규칙]
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. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라
6. 여행지의 정보와 특징을 정확하게 설명하라
7. 여행지의 구체적인 방문 포인트, 숨겨진 명소, 각종정보 그리고 실제 경험을 반영한 글을 작성하라
8. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라
[텍스트 작성 규칙]
1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라
2. 반드시 입력된 참고글의 내용으로만 구성
3. 전체 맥락을 이해하고 문장의 일관성을 유지하라
4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것
5. 여행지의 숨겨진 매력과 실용적인 정보(교통, 주차, 위치(주소), 행사, 시간(운영시간), 팁 등)를 포함하여 작성하라
6. 객관적인 여행 정보와 주관적인 고객 반응을 균형있게 제시하라
7. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성
"""
elif (category == "여행 코스"):
return """
[여행 코스 텍스트 생성 규칙]
[기본규칙]
1. 반드시 한국어(한글)로 작성하라
2. 너는 전문적인 여행 코스 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라
3. 각 섹션을 독립적인 정보 단위로 취급하라
4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라
5. 여행 코스의 흐름과 특징을 부각하여 독자가 쉽게 따라갈 수 있도록 작성하라
6. 여행지의 구체적인 방문 포인트, 숨겨진 명소, 각종정보 그리고 실제 경험을 반영한 글을 작성하라
7. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라
[텍스트 작성 규칙]
1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라
2. 반드시 입력된 참고글의 내용으로만 구성
3. 전체 맥락을 이해하고 문장의 일관성을 유지하라
4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것
5. 해당 여행의 매력과 특징을 부각시키도록 작성하라
6. 객관적인 여행 정보와 주관적인 고객 반응을 균형있게 제시하라
7. 각 코스와 추천 명소를 중심으로 상세한 정보를 제공하라
- 여행 타겟에 맞는 각종 정보(팁)
- 시간, 주차, 주소(장소), 가격, 행사, 맛집(음식) 등
8. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성
"""
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("""
""")