import gradio as gr
import random
import os
import re
from huggingface_hub import InferenceClient
from youtube_transcript_api import YouTubeTranscriptApi, NoTranscriptFound
from fpdf import FPDF
from fpdf.enums import XPos, YPos
from datetime import datetime
# 클라이언트 생성 함수
def create_client(model_name):
return InferenceClient(model_name, token=os.getenv("HF_TOKEN"))
client = create_client("CohereForAI/c4ai-command-r-plus")
# API 호출 함수
def call_api(content, system_message, max_tokens, temperature, top_p):
messages = [{"role": "system", "content": system_message}, {"role": "user", "content": content}]
random_seed = random.randint(0, 1000000)
response = client.chat_completion(messages=messages, max_tokens=max_tokens, temperature=temperature, top_p=top_p, seed=random_seed)
return response.choices[0].message.content
# 정보 분석 함수
def analyze_info(category, style, transcripts):
transcript_list = transcripts.split("\n\n---\n\n")
analyzed_content = f"선택한 카테고리: {category}\n선택한 포스팅 스타일: {style}\n\n"
for i, transcript in enumerate(transcript_list, 1):
analyzed_content += f"유튜브 트랜스크립트 {i}:\n{transcript}\n\n"
return analyzed_content
# 블로그 포스트 생성 함수
def generate_blog_post(category, style, transcripts, category_prompt, style_prompt, max_tokens, temperature, top_p):
full_content = analyze_info(category, style, transcripts)
combined_prompt = f"{category_prompt}\n\n{style_prompt}\n\n{full_content}"
modified_text = call_api(combined_prompt, "", max_tokens, temperature, top_p)
return modified_text.replace('\n', '\n\n')
# 유튜브 대본 요약 함수
def summarize_transcript(transcripts, system_message, max_tokens, temperature, top_p):
summary = call_api(transcripts, system_message, max_tokens, temperature, top_p)
return summary
# 유튜브 비디오 ID 추출 함수
def get_video_id(youtube_url):
video_id_match = re.search(r"(?<=v=)[^#&?]*", youtube_url) or re.search(r"(?<=youtu.be/)[^#&?]*", youtube_url)
return video_id_match.group(0) if video_id_match else None
# 유튜브 트랜스크립트 추출 함수
def get_transcript(youtube_url):
video_id = get_video_id(youtube_url)
if not video_id:
return "Invalid YouTube URL. Please enter a valid URL."
language_order = ['ko', 'en', 'ja', 'zh-Hans', 'pt', 'es', 'it', 'fr', 'de', 'ru']
for lang in language_order:
try:
transcript = YouTubeTranscriptApi.get_transcript(video_id, languages=[lang])
return f"Transcript in {lang}:\n\n{' '.join([entry['text'] for entry in transcript])}"
except NoTranscriptFound:
continue
except Exception as e:
return f"Error: {str(e)}"
return "No transcript available in the specified languages."
# 카테고리별 프롬프트 함수
def get_blog_post_prompt(category):
if category == "일반형":
return """
#유튜브 대본을 블로그 포스팅으로 변환하는 규칙(일반형_v4)
"""
elif category == "정보성":
return """
#유튜브 대본을 블로그 포스팅으로 변환하는 규칙(정보성_v4)
"""
elif category == "1개 상품 추천형":
return """
#유튜브 대본을 블로그 포스팅으로 변환하는 규칙(추천형_v4)
"""
elif category == "큐레이션형":
return """
#유튜브 대본을 블로그 포스팅으로 변환하는 규칙(큐레이션형_v3)
"""
# 포스팅 스타일 프롬프트 함수
def get_style_prompt(style):
prompts = {
"친근한": """
제가 알게 된 꿀팁들을 하나하나 알려드릴게요.
""",
"일반":"""#일반적인 블로그 포스팅 스타일 가이드
""",
"전문적인": """
#전문적인 블로그 포스팅 스타일 가이드
"""
}
return prompts.get(style, "포스팅 스타일 프롬프트")
# 포스팅 스타일 설명 함수
def get_style_description(style):
descriptions = {
"친근한": "독자와 가까운 친구처럼 대화하는 듯한 친근한 스타일입니다.",
"일반": "일반적이고 중립적인 톤으로 정보를 전달하는 스타일입니다.",
"전문적인": "전문가의 시각에서 깊이 있는 정보를 전달하는 스타일입니다."
}
return descriptions.get(style, "포스팅 스타일을 선택하세요.")
# 프롬프트 업데이트 함수
def update_prompts_and_description(category, style):
blog_post_prompt = get_blog_post_prompt(category)
style_prompt = get_style_prompt(style)
style_description = get_style_description(style)
return blog_post_prompt, style_prompt, style_description
def format_filename(text):
text = re.sub(r'[^\w\s-]', '', text)
return text[:50].strip()
def extract_first_recommended_title(blog_post):
section_match = re.search(r'(?:#+\s*)?추천\s*제목:?\s*\n([\s\S]*?)(?=\n(?:#+|$)|$)', blog_post, re.IGNORECASE)
if section_match:
section_content = section_match.group(1)
title_match = re.search(r'(?:^|\n)\s*(?:\d+\.|-|\*|\•)?\s*(.*?)(?=\n|$)', section_content)
if title_match:
title = title_match.group(1).strip()
print(f"Extracted title: {title}")
return title
print("No title found")
return "블로그_글"
class PDF(FPDF):
def __init__(self):
super().__init__()
self.add_font("NanumGothic", "", "NanumGothic.ttf")
self.add_font("NanumGothicBold", "", "NanumGothicBold.ttf")
self.add_font("NanumGothicExtraBold", "", "NanumGothicExtraBold.ttf")
self.add_font("NanumGothicLight", "", "NanumGothicLight.ttf")
def header(self):
# 헤더를 비워둡니다
pass
def footer(self):
self.set_y(-15)
self.set_font('NanumGothicLight', '', 8)
self.cell(0, 10, f'Page {self.page_no()}', 0, new_x=XPos.RIGHT, new_y=YPos.TOP, align='C')
def save_to_pdf(summary, blog_post, file_type):
pdf = PDF()
pdf.set_auto_page_break(auto=True, margin=15)
pdf.add_page()
pdf.set_font("NanumGothicExtraBold", size=16)
pdf.cell(0, 10, "요약", new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
pdf.ln(5)
pdf.set_font("NanumGothic", size=11)
pdf.multi_cell(0, 6, summary)
pdf.add_page()
pdf.set_font("NanumGothicExtraBold", size=16)
pdf.cell(0, 10, "블로그 글", new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
pdf.ln(5)
lines = blog_post.split('\n')
for line in lines:
if line.strip() == '':
pdf.ln(3) # 빈 줄은 작은 간격만 추가
elif line.startswith('#'): # 제목으로 간주
pdf.set_font("NanumGothicBold", size=14)
pdf.multi_cell(0, 8, line.lstrip('#').strip())
pdf.ln(2)
elif line.startswith('##'): # 부제목으로 간주
pdf.set_font("NanumGothicBold", size=12)
pdf.multi_cell(0, 7, line.lstrip('#').strip())
pdf.ln(2)
else:
pdf.set_font("NanumGothic", size=11)
pdf.multi_cell(0, 6, line.strip())
pdf.ln(1)
title = extract_first_recommended_title(blog_post)
today_date = datetime.now().strftime("%Y%m%d")
filename = f"{today_date}_{format_filename(title)}.pdf"
print(f"Saving PDF as: {filename}")
pdf.output(filename)
return filename
# Gradio 인터페이스용 PDF 저장 함수
def save_content_to_pdf(summary, blog_post):
filename = save_to_pdf(summary, blog_post, "블로그")
return filename
# Gradio 인터페이스 구성
title = "유튜브로 블로그 글 생성하기"
with gr.Blocks() as demo:
gr.Markdown(f"# {title}")
# 1단계: 카테고리 선택
gr.Markdown("### 1단계: 포스팅 카테고리를 지정해주세요", elem_id="step-title")
category = gr.Radio(choices=["일반형","정보성", "1개 상품 추천형", "큐레이션형"], label="포스팅 카테고리", value="일반형")
# 구분선 추가
gr.Markdown("---\n\n")
# 2단계: 포스팅 스타일 선택
gr.Markdown("### 2단계: 포스팅 스타일을 선택해주세요", elem_id="step-title")
style = gr.Radio(choices=["친근한", "일반", "전문적인"], label="포스팅 스타일", value="친근한")
style_description = gr.Markdown(f"{get_style_description('친근한')}", elem_id="style-description")
# 구분선 추가
gr.Markdown("---\n\n")
# 3단계: 유튜브 링크를 입력하세요
gr.Markdown("### 3단계: 유튜브 링크를 입력하세요", elem_id="step-title")
with gr.Row():
youtube_url1 = gr.Textbox(label="YouTube URL 1", placeholder="첫 번째 유튜브 링크를 입력하세요")
youtube_url2 = gr.Textbox(label="YouTube URL 2", placeholder="두 번째 유튜브 링크를 입력하세요")
youtube_url3 = gr.Textbox(label="YouTube URL 3", placeholder="세 번째 유튜브 링크를 입력하세요")
# 숨겨진 텍스트박스 (사용자에게 보이지 않음)
combined_urls = gr.Textbox(visible=False)
transcript_output = gr.Textbox(label="유튜브 트랜스크립트", lines=10)
# 유튜브 트랜스크립트 가져오기 함수
def combine_and_get_transcripts(url1, url2, url3):
urls = [url for url in [url1, url2, url3] if url.strip()]
combined = ",".join(urls)
all_transcripts = []
for url in urls:
transcript = get_transcript(url.strip())
all_transcripts.append(transcript)
return combined, "\n\n---\n\n".join(all_transcripts)
# 입력 변경 시 트랜스크립트 업데이트
for url_input in [youtube_url1, youtube_url2, youtube_url3]:
url_input.change(
fn=combine_and_get_transcripts,
inputs=[youtube_url1, youtube_url2, youtube_url3],
outputs=[combined_urls, transcript_output]
)
# 요약글 생성하기 기능 추가
gr.Markdown("### 요약글 생성하기", elem_id="step-title")
with gr.Accordion("요약글 설정", open=False):
summary_system_message = gr.Textbox(
label="요약글 시스템 메시지",
value="""
#유튜브 대본 요약 규칙
""",
lines=15,
visible=True
)
summary_max_tokens = gr.Slider(label="Max Tokens", minimum=1000, maximum=7000, value=5000, step=1000)
summary_temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=1.0, value=0.7, step=0.05)
summary_top_p = gr.Slider(label="Top P", minimum=0.1, maximum=1.0, value=0.95, step=0.05)
summarize_btn = gr.Button("요약글 생성하기")
summary_output = gr.Textbox(label="요약된 글", lines=10)
def generate_summary(transcript, system_message, max_tokens, temperature, top_p):
summary = summarize_transcript(transcript, system_message, max_tokens, temperature, top_p)
return summary
summarize_btn.click(
fn=generate_summary,
inputs=[transcript_output, summary_system_message, summary_max_tokens, summary_temperature, summary_top_p],
outputs=[summary_output]
)
# 구분선 추가
gr.Markdown("---\n\n")
# 4단계: 글 생성하기
gr.Markdown("### 4단계: 글 생성하기", elem_id="step-title")
gr.HTML("[생성하기 버튼을 선택해주세요]")
with gr.Accordion("블로그 글 설정", open=False):
blog_system_message = gr.Textbox(label="카테고리 프롬프트", value=get_blog_post_prompt("일반형"), lines=20, visible=True)
style_prompt_hidden = gr.Textbox(label="스타일 프롬프트", value=get_style_prompt("친근한"), lines=10, visible=False) # 초기값 설정
blog_max_tokens = gr.Slider(label="Max Tokens", minimum=1000, maximum=12000, value=8000, step=1000)
blog_temperature = gr.Slider(label="Temperature", minimum=0.1, maximum=1.0, value=0.8, step=0.1)
blog_top_p = gr.Slider(label="Top P", minimum=0.1, maximum=1.0, value=0.95, step=0.05)
generate_btn = gr.Button("블로그 글 생성하기")
blog_output = gr.Textbox(label="생성된 블로그 글", lines=30)
def generate_blog_content(category, style, transcripts, category_prompt, style_prompt, max_tokens, temperature, top_p):
blog_post = generate_blog_post(category, style, transcripts, category_prompt, style_prompt, max_tokens, temperature, top_p)
return blog_post
generate_btn.click(
fn=generate_blog_content,
inputs=[category, style, transcript_output, blog_system_message, style_prompt_hidden, blog_max_tokens, blog_temperature, blog_top_p],
outputs=[blog_output]
)
# PDF 저장 버튼 추가
save_pdf_btn = gr.Button("PDF로 저장하기")
pdf_output = gr.File(label="생성된 PDF 파일")
save_pdf_btn.click(
fn=save_content_to_pdf,
inputs=[summary_output, blog_output],
outputs=[pdf_output]
)
# 카테고리와 스타일이 변경될 때 프롬프트 업데이트
def update_prompts_and_description(category, style):
blog_post_prompt = get_blog_post_prompt(category)
style_prompt = get_style_prompt(style)
style_description = get_style_description(style)
return blog_post_prompt, style_prompt, style_description
category.change(fn=update_prompts_and_description, inputs=[category, style], outputs=[blog_system_message, style_prompt_hidden, style_description])
style.change(fn=update_prompts_and_description, inputs=[category, style], outputs=[blog_system_message, style_prompt_hidden, style_description])
demo.launch()
# CSS 스타일 추가
gr.HTML("""
""")