import gradio as gr
import random
import os
import re
import openai
from fpdf import FPDF
from datetime import datetime
from zoneinfo import ZoneInfo
from sklearn.feature_extraction.text import CountVectorizer
# OpenAI API 클라이언트 설정
openai.api_key = os.getenv("OPENAI_API_KEY")
def call_api(content, system_message, max_tokens, temperature, top_p):
try:
messages = [
{"role": "system", "content": system_message},
{"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)
system_prompt = get_outline_prompt(data['category']) + "\n\n" + get_style_prompt(data['style'])
modified_text = call_api(full_content, system_prompt, 2000, 0.7, 0.95)
# 핵심기능을 분리하여 리스트로 반환
features = re.findall(r'핵심기능 : (.+)', modified_text)
return features[:3] # 최대 3개의 핵심기능만 반환
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 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'])
user_prompt = f"""
카테고리: {data['category']}
포스팅 스타일: {data['style']}
핵심기능: {data['outline']}
참고글1: {data['references1']}
참고글2: {data['references2']}
참고글3: {data['references3']}
"""
blog_content = call_api(
user_prompt,
system_prompt + "\n" + style_prompt,
5000,
0.7,
0.95
)
filtered_post = remove_unwanted_phrases(blog_content)
filtered_post = filtered_post.lstrip()
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. 반드시 핵심 기능 3개에 대한 흥미로운 소제목만 작성하라(절대 내용 작성 금지)
5. 반드시 마크다운 형식이 아닌 순수한 텍스트로 출력하라
6. 소제목은 반드시 30자 이내로 작성하라
[출력예시]
핵심기능 : 선정한 핵심기능1의 소제목
핵심기능 : 선정한 핵심기능2의 소제목
핵심기능 : 선정한 핵심기능3의 소제목
"""
def get_blog_post_prompt(category):
if (category == "핵심기능집중형"):
return """
[상품리뷰 콘텐츠 생성 규칙]
[기본규칙]
1. 반드시 한국어(한글)로 작성하라
2. 반드시 주어진 핵심기능 1개의 주제에 대해서만 작성하라
- 절대로 주어진 핵심기능 1개의 주제 이외의 내용을 작성하지 말 것
3. 주어진 핵심 기능 1개에 대한 특장점, 각종 정보, 팁 등을 자세히 설명하라
4. 전문 용어를 사용할 경우 일반 사용자도 이해가 되도록 풀어서 설명하라
5. 마크다운 형식으로 작성하라라
[텍스트 작성 규칙]
1. 반드시 입력된 참고글 안에서 핵심기능 1개에 대한 내용을 작성
2. 절대로 주어진 핵심기능 1개의 주제 이외의 내용을 작성하지 말 것
3. 제공된 참고글의 어투를 반영하되, 절대로 한 문장 이상 그대로 출력하지 말 것
4. 쉽게 읽힐 수 있도록 쉬운 어휘로 작성
5. 핵심기능에 대한 소비자 타겟을 분석하여 작성
6. 주어진 핵심기능 1개에 맞는 스펙, 기능, 성능, 장단점, 가격 대비 성능(가성비), 팁, 주의사항 등을 작성
7. 주어진 핵심기능 1개와 관련된 구체적인 수치(데이터)가 있다면 반영하라
8. 반드시 핵심기능 1개에 대한 콘텐츠만 1800자 이상 작성하라
"""
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, outline):
try:
pdf = PDF()
pdf.add_page()
# 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")
# 첫 번째 제목을 찾아 파일명으로 사용
filename = f"{date_str}_{time_str}_{format_filename(outline)}.pdf"
# HTML 내용을 순회하며 PDF에 작성
current_tag = ''
buffer = ''
for part in re.split(tag_pattern, blog_post):
# HTML 태그가 등장한 경우
if part in ['h1', 'h2', 'h3', 'p', 'strong', 'li', 'br']:
# 이전 태그의 내용을 처리
if buffer:
if current_tag in ['h1', 'h2', 'h3']:
pdf.chapter_title(buffer.strip()) # 소제목 처리
elif current_tag == 'p':
pdf.chapter_body(buffer.strip()) # 일반 문단 처리
elif current_tag == 'strong':
pdf.set_font("NanumGothic", 'B', 11) # 굵은 글씨 처리
pdf.cell(0, 5, buffer.strip(), 0, 1)
pdf.set_font("NanumGothic", '', 11) # 다시 기본 폰트로 변경
elif current_tag == 'li':
pdf.chapter_body("• " + buffer.strip()) # 리스트 아이템 처리
elif current_tag == 'br':
pdf.ln(5) # 줄바꿈 처리
# 현재 태그와 버퍼 초기화
buffer = ''
current_tag = part
# 종료 태그는 무시 (div 포함)
elif part.startswith('/'):
continue
# 텍스트 부분 처리
elif not tag_pattern.match(part) and part.strip() != 'div':
buffer += part
# 마지막 버퍼 처리
if buffer:
if current_tag in ['h1', 'h2', 'h3']:
pdf.chapter_title(buffer.strip()) # 제목 형식으로 처리
elif current_tag == 'p':
pdf.chapter_body(buffer.strip()) # 본문 형식으로 처리
# PDF 저장
print(f"Saving PDF as: {filename}")
pdf.output(filename, 'F') # 'F' 옵션 추가
return filename
except Exception as e:
print(f"PDF 생성 중 오류 발생: {str(e)}")
import traceback
traceback.print_exc() # 상세한 오류 정보 출력
return None # 오류 발생 시 None 반환
def save_content_to_pdf(blog_post, outline):
pdf_filename = save_to_pdf(blog_post, outline)
if pdf_filename:
return pdf_filename
else:
return None
title = "핵심기능집중형"
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_result1 = gr.Textbox(label="핵심기능 1", lines=2)
outline_result2 = gr.Textbox(label="핵심기능 2", lines=2)
outline_result3 = gr.Textbox(label="핵심기능 3", lines=2)
outline_input = gr.Textbox(label="작성할 핵심기능 주제 중에 1개만 입력해주세요", placeholder="핵심기능 1개만 입력하세요", lines=2)
outline_generate_btn.click(
fn=generate_outline,
inputs=[category, style, references1, references2, references3],
outputs=[outline_result1, outline_result2, outline_result3]
)
gr.Markdown("---\n\n")
gr.Markdown("### 5단계: 글 생성하기", elem_id="step-title")
gr.HTML("[핵심기능을 확인하세요]")
generate_btn = gr.Button("블로그 글 생성하기")
html_output = gr.HTML(label="생성된 블로그 글")
generate_btn.click(
fn=generate_blog_post,
inputs=[category, style, references1, references2, references3, outline_input],
outputs=[html_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=[html_output, outline_input], # outline_input 추가
outputs=[pdf_output],
show_progress=True
)
demo.launch()
gr.HTML("""
""")