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
# 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 suggest_title(category, style, references1, references2, references3):
data = {
'category': category,
'style': style,
'references1': references1,
'references2': references2,
'references3': references3
}
full_content = analyze_info(data)
system_prompt = get_title_prompt(data['category']) + "\n\n" + get_style_prompt(data['style'])
modified_text = call_api(full_content, system_prompt, 1500, 0.75, 0.95)
return modified_text
def generate_outline(category, style, references1, references2, references3, title):
data = {
'category': category,
'style': style,
'references1': references1,
'references2': references2,
'references3': references3,
'title': title
}
full_content = analyze_info(data)
content = f"{full_content}\nTitle: {data['title']}"
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, title, outline):
try:
data = {
'category': category,
'style': style,
'references1': references1,
'references2': references2,
'references3': references3,
'title': title,
'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 = data['title'] + "\n\n" + "\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_title_prompt(category):
if (category == "일반"):
return """
제목 추천은 사용하지 않습니다.
"""
elif (category == "생활건강"):
return """
제목 추천은 사용하지 않습니다.
"""
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. 필요한 경우 연구 결과나 통계등을 적절히 활용하라
7. 개인의 경험이나 사례를 적절히 활용하라
8. 문제에 대한 원인, 증상, 예방 또는 치료 방법에 대한 실용적인 정보를 포함하라
9. 전문적인 용어는 적절한 설명과 쉬운 어휘로 보충하라
10. 반드시 [소주제 구성]에 맞게 소주제만 출력하라
[아웃라인 구성]
1. 반드시 [도입부]-1개, [본론]-5개, [결론]-1개로 구성하여 출력하라
2. 반드시 [도입부]와 [결론]의 제목이 중복되지 않도록 작성하라
출력예시:
- 도입부: 제목
- 본론1: 제목
...
- 본론5: 제목
- 결론: 제목
"""
def get_blog_post_prompt(category):
if (category == "일반"):
return """
[상품리뷰 콘텐츠 생성 규칙]
[기본규칙]
1. 반드시 한국어(한글)로 작성하라
2. 너는 전문적인 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라
3. 각 섹션을 독립적인 정보 단위로 취급하라
4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라
5. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라
[텍스트 작성 규칙]
1. 반드시 입력된 소주제(아웃라인)에 맞게 글을 작성하라
2. 반드시 입력된 참고글의 내용으로만 구성
3. 전체 맥락을 이해하고 문장의 일관성을 유지하라
4. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것
5. 쉽게 읽을 수 있도록 쉬운 어휘로 작성
6. 참고글을 기반으로 글의 주제와 유용한 정보를 파악하여 작성
7. 독자에게 도움이되고 이득이 되는 정보를 작성
8. 독자의 공감을 얻고 궁금증을 해결하도록 작성
"""
elif (category == "생활건강"):
return """
[상품리뷰 콘텐츠 생성 규칙]
[기본규칙]
1. 반드시 한국어(한글)로 작성하라
2. 너는 전문적인 생활건강 콘텐츠 작가이다. 주어진 주제에 대해 풍부하고 매력적인 내용을 작성하라
3. 각 섹션을 독립적인 정보 단위로 취급하라
4. 다른 섹션과의 연결성을 고려하되, 각 섹션이 독립적으로도 이해될 수 있게 작성하라
5. 이 섹션은 더 큰 콘텐츠의 일부임을 명심하라, 전체적인 흐름을 해치지 않으면서 주어진 주제를 철저히 작성하라라
6. 반드시 생활건강 관련 정보를 제공하도록 작성
[텍스트 작성 규칙]
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, "포스팅 스타일 프롬프트")
def split_titles(suggested_titles):
titles = suggested_titles.split('\n')
titles = [re.sub(r'^(1\.|2\.|3\.|4\.|5\.|6\.|7\.|8\.|9\.|10\.|## |# |\* |\*\* |\*\*\*)', '', title.strip()) for title in titles if title.strip()]
titles = titles[::-1] # 리스트를 역순으로 정렬
titles += [""] * (10 - len(titles)) # 10개보다 적으면 빈 문자열로 채우기
return titles[:10] # 최대 10개의 제목만 반환
class PDF(FPDF):
def __init__(self):
super().__init__(orientation='P', unit='mm', format='A4')
self.set_margins(10, 10, 10)
current_dir = os.path.dirname(__file__)
self.add_font("NanumGothic", "", os.path.join(current_dir, "NanumGothic.ttf"))
self.add_font("NanumGothic", "B", os.path.join(current_dir, "NanumGothicBold.ttf"))
self.add_font("NanumGothicExtraBold", "", os.path.join(current_dir, "NanumGothicExtraBold.ttf"))
self.add_font("NanumGothicLight", "", os.path.join(current_dir, "NanumGothicLight.ttf"))
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 chapter_title(self, title):
self.set_font("NanumGothic", 'B', 12)
self.cell(0, 6, title, 0, 1, 'L')
self.ln(4)
def chapter_body(self, body):
self.set_font("NanumGothic", '', 11)
self.multi_cell(0, 5, body)
self.ln()
def print_chapter(self, title, body):
self.add_page()
self.chapter_title(title)
self.chapter_body(body)
def format_filename(text):
if not isinstance(text, str):
text = str(text) # 문자열이 아닌 경우 문자열로 변환
text = re.sub(r'[^\w\s-]', '', text)
return text[:50].strip()
def save_to_pdf(blog_post, user_topic):
try:
pdf = PDF()
pdf.add_page()
pdf.set_auto_page_break(auto=True, margin=15)
# HTML 태그를 파싱하기 위한 정규표현식
tag_pattern = re.compile(r'<(/?)(\w+)([^>]*)>')
# 현재 날짜와 시간을 가져옵니다 (대한민국 시간 기준)
now = datetime.now(ZoneInfo("Asia/Seoul"))
date_str = now.strftime("%y%m%d")
time_str = now.strftime("%H%M")
# 첫 번째 제목을 찾아 파일명으로 사용
title_match = re.search(r']*>(.*?)', blog_post)
title = title_match.group(1) if title_match else "Untitled"
filename = f"{date_str}_{time_str}_{format_filename(title)}.pdf"
# HTML 내용을 순회하며 PDF에 작성
current_tag = ''
buffer = ''
is_bold = False
# 스타일 속성 초기화
font_size = 11
color = (0, 0, 0) # 기본 글자 색 (검정)
for part in re.split(tag_pattern, blog_post):
# 태그 시작 부분
if part in ['h1', 'h2', 'h3', 'p', 'strong', 'li', 'br']:
if buffer:
if current_tag in ['h1', 'h2', 'h3']:
pdf.set_font("NanumGothic", 'B', 16 if current_tag == 'h1' else 14)
pdf.multi_cell(0, 10, buffer.strip(), align='L')
pdf.ln(5)
elif current_tag == 'p':
pdf.set_font("NanumGothic", '', font_size)
pdf.set_text_color(*color) # 설정된 색상 적용
pdf.multi_cell(0, 6, buffer.strip(), align='J')
pdf.ln(5)
elif current_tag == 'li':
pdf.set_font("NanumGothic", '', font_size)
pdf.set_text_color(*color)
pdf.multi_cell(0, 6, "• " + buffer.strip(), align='J')
elif current_tag == 'br':
pdf.ln(5)
buffer = ''
current_tag = part
# 스타일 속성 반영
elif 'style=' in part:
styles = re.findall(r'style="(.*?)"', part)
for style in styles:
if 'color:' in style:
color_match = re.search(r'color: (#\w+);', style)
if color_match:
color_hex = color_match.group(1)
r, g, b = tuple(int(color_hex[i:i+2], 16) for i in (1, 3, 5))
color = (r, g, b) # RGB 값으로 변환하여 적용
if 'font-size:' in style:
size_match = re.search(r'font-size: (\d+)px;', style)
if size_match:
font_size = int(size_match.group(1)) * 0.75 # pt 단위로 변환 (1px = 0.75pt)
elif part == 'strong':
is_bold = True
pdf.set_font("NanumGothic", 'B', font_size)
elif part == '/strong':
is_bold = False
pdf.set_font("NanumGothic", '', font_size)
elif part.startswith('/') or part == 'div':
continue
elif not tag_pattern.match(part) and part.strip():
buffer += part.strip() + ' '
# 마지막 버퍼 처리
if buffer:
pdf.set_font("NanumGothic", '', font_size)
pdf.set_text_color(*color)
pdf.multi_cell(0, 6, buffer.strip(), align='J')
# PDF 저장
print(f"Saving PDF as: {filename}")
pdf.output(filename, 'F')
return filename
except Exception as e:
print(f"PDF 생성 중 오류 발생: {str(e)}")
import traceback
traceback.print_exc()
return None
def save_content_to_pdf(blog_post):
pdf_filename = save_to_pdf(blog_post, "")
if pdf_filename:
return pdf_filename
else:
return None # 오류 발생 시 None 반환
title = "일반"
with gr.Blocks() as demo:
gr.Markdown(f"# {title}")
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")
title_suggestions = gr.Textbox(label="제목 추천", lines=10)
# 10개의 텍스트 출력창 추가
title_outputs = [gr.Textbox(label=f"제목 {i+1}", lines=1) for i in range(10)]
title_btn = gr.Button("제목 추천하기")
title_btn.click(
fn=suggest_title,
inputs=[category, style, references1, references2, references3],
outputs=[title_suggestions]
)
title_suggestions.change(
fn=split_titles,
inputs=[title_suggestions],
outputs=title_outputs
)
gr.Markdown("---\n\n")
gr.Markdown("### 5단계: 블로그 제목을 입력하세요", elem_id="step-title")
blog_title = gr.Textbox(label="블로그 제목", placeholder="블로그 제목을 입력해주세요")
gr.Markdown("---\n\n")
gr.Markdown("### 6단계: 아웃라인을 작성해주세요", 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, blog_title],
outputs=[outline_result]
)
gr.Markdown("---\n\n")
gr.Markdown("### 7단계: 글 생성하기", 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, blog_title, 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("""
""")