import os import random import re import requests import logging import tempfile from bs4 import BeautifulSoup from datetime import datetime from zoneinfo import ZoneInfo import html from PIL import Image from urllib.request import urlopen import markdown2 import gradio as gr # 로깅 설정 (INFO 레벨) logging.basicConfig(level=logging.INFO) # 상수 정의 TARGET_CHAR_LENGTH = 4000 MIN_SECTION_LENGTH = 600 MAX_TOKENS = 15000 TEMPERATURE = 0.75 TOP_P = 0.95 # API 관련 설정 gemini_api_key = os.getenv("GEMINI_API_KEY") # --- Google Gemini SDK 초기화 --- from google import genai from google.genai import types client = genai.Client(api_key=gemini_api_key) # ------------------------------- # 기본 도우미 함수들 # ------------------------------- 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 convert_to_html(text): """마크다운 형식을 HTML로 변환 - 개선된 버전""" # 제목 형식 변환 (# -> h1, ## -> h2, 등) for i in range(6, 0, -1): pattern = r'^' + r'#' * i + r'\s+(.+)$' text = re.sub(pattern, r'\1', text, flags=re.MULTILINE) # 볼드체 변환 (**text** -> text) text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # 이탤릭체 변환 (*text* -> text) text = re.sub(r'\*([^*]+?)\*', r'\1', text) # 목록 변환 (- item ->
  • item
  • ) lines = text.split('\n') in_list = False result_lines = [] for line in lines: list_match = re.match(r'^[\*\-+]\s+(.+)$', line) if list_match: if not in_list: result_lines.append('') in_list = False # 단락 처리 if line.strip() and not re.match(r'^|') # 최종 HTML 결합 html_content = '\n'.join(result_lines) # HTML 스타일 적용 (제목과 소재목에 Bold 및 폰트 지정) styled_html = f"""
    {html_content}
    """ return styled_html def post_process_blog(blog_content, style="친근한"): """블로그 컨텐츠 후처리 함수""" try: # 마크다운 제거 (# 형식의 제목 제외) blog_content = re.sub(r'^\s*[\*\-+]\s+', '- ', blog_content, flags=re.MULTILINE) # 글머리 기호 통일 # 스타일에 따른 어투 조정 if style == "친근한": blog_content = re.sub(r'([가-힣]+)고요', r'\1구요', blog_content) blog_content = re.sub(r'답니다', '어요', blog_content) blog_content = re.sub(r'였답니다', '였어요', blog_content) blog_content = re.sub(r'했답니다', '했어요', blog_content) blog_content = re.sub(r'습니다', '요', blog_content) blog_content = re.sub(r'합니다', '해요', blog_content) blog_content = re.sub(r'됩니다', '돼요', blog_content) blog_content = re.sub(r'입니다', '이에요', blog_content) # 핵심기능 전문용어 강화 및 구체적 수치 강조 tech_terms = [ (r'성능\b(?!\s*분석|\s*테스트|\s*측정)', r'기술적 성능'), (r'속도\b(?!\s*측정|\s*테스트)', r'처리 속도'), (r'화면\b(?!\s*크기|\s*밝기)', r'디스플레이'), (r'카메라\b(?!\s*모듈|\s*센서)', r'이미지 센서 시스템'), (r'배터리\b(?!\s*용량|\s*수명)', r'전력 관리 시스템'), (r'사용\b(?!\s*방법|\s*설명|\s*사례)', r'운용'), (r'좋다\b(?!\s*고)', r'효율적이다'), (r'빠르다\b', r'고성능이다') ] for pattern, replacement in tech_terms: blog_content = re.sub(pattern, replacement, blog_content) # 분석적 표현 강화 blog_content = re.sub(r'제 생각에는', r'분석 결과에 따르면', blog_content) blog_content = re.sub(r'제가 봤을 때', r'기술적 관점에서', blog_content) blog_content = re.sub(r'느낌이 들어요', r'확인됩니다', blog_content) # 수치와 단위 사이에 공백 추가 및 강조 def add_space_to_numbers(match): number, unit = match.groups() return f"{number} {unit}" blog_content = re.sub(r'(\d+(?:\.\d+)?)([가-힣]+)', add_space_to_numbers, blog_content) # 주요 숫자 데이터 볼드 처리 blog_content = re.sub(r'(\d+(?:\.\d+)?(?:\s*%|\s*dB|\s*Hz|\s*시간|\s*mAh|\s*GB|\s*MB|\s*TB|\s*MP))', r'**\1**', blog_content) # 과장된 표현 정리 exaggerated_expressions = [ (r'필수적인', r'중요한'), (r'혁명적인', r'주목할만한'), (r'놀라운', r'주목할 만한'), (r'기적의', r'효과적인'), (r'최고의', r'우수한'), (r'세계적인', r'주요'), (r'완벽한', r'우수한'), (r'극적인', r'상당한'), (r'무한한', r'다양한'), (r'절대적인', r'중요한'), (r'혁신적인', r'새로운'), (r'환상적인', r'우수한'), (r'근본적인', r'기본적인'), (r'획기적인', r'중요한'), (r'전례없는', r'특별한'), (r'압도적인', r'두드러진'), (r'기가 막힌', r'효과적인'), (r'끝판왕', r'최상위 모델') ] for pattern, replacement in exaggerated_expressions: blog_content = re.sub(r'\b' + pattern + r'\b', replacement, blog_content, flags=re.IGNORECASE) # 참고글 관련 표현 정리 blog_content = re.sub(r'참고글에 따르면', r'기술 분석에 따르면', blog_content) blog_content = re.sub(r'참고글', r'기술 문서', blog_content) return blog_content except Exception as e: logging.error(f"블로그 글 후처리 중 오류 발생: {str(e)}") return blog_content def generate_outline(category, style, references1, references2, references3): """핵심기능 선별 함수 - 간단한 설명 추가""" try: # 참고글 정보 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 meaningful_refs = [ref for ref in references if ref != "참고 자료 없음"] if not meaningful_refs: return ["참고 자료가 없습니다"] * 5 # 스타일 프롬프트 가져오기 style_prompt = get_style_prompt(style) # 모든 참고글 결합 combined_refs = "\n\n".join(meaningful_refs) # LLM에 핵심기능 추출 요청 extract_prompt = f""" [핵심기능 선별 요청] 제공된 참고글에서 가장 중요하고 핵심적인 기능 5가지를 선별하고 각각에 간단한 설명을 추가해주세요. 참고글: {combined_refs} [시스템 역할] 당신은 수년간의 경험을 가진 상품 기능 전문 분석가입니다. 제품의 단일 핵심 기능을 심층적으로 분석하고 다양한 측면에서 평가하여 많은 독자들의 신뢰를 받고 있습니다. [분석 단계] 1. 참고 자료 3개를 철저히 분석하여 제품의 단일 핵심 기능 식별 2. 선정한 핵심 기능의 5가지 주요 측면 파악 (성능, 사용성, 효율성, 기술적 특징, 활용 가치 등) 3. 선정한 핵심 기능이 제품 전체에서 갖는 중요도와 차별성 평가 [아웃라인 구성 원칙] 1. 본론(5개) - 참고 자료 분석을 통해 발견한 핵심 기능의 5가지 중요 측면을 담은 소제목 - 핵심 기능의 기술적 원리와 작동 메커니즘 - 핵심 기능의 실제 성능 및 측정 데이터 분석 - 사용자 경험 측면에서의 기능 평가 - 경쟁 제품과의 해당 기능 비교 분석 - 핵심 기능의 실생활 활용 가치와 한계점 - (위 항목들은 선정된 핵심 기능에 따라 유연하게 조정) [핵심 지침] 1. 완전히 한국어로만 작성할 것 2. 소제목은 최대 30자 이내로 간결하게 작성 3. 선정된 핵심 기능의 중요 측면을 명확히 드러내는 표현 사용 (예: "정밀 측정 실험으로 본 성능 한계", "일상 환경에서의 기능 안정성 분석") 4. 기술적 정확성과 심층적 분석이 소제목에 반영되도록 구성 5. 키워드는 소제목 결정에 영향을 주지 않음 (본문 작성 시 참고사항으로만 활용) 6. 본론 5개 항목만으로 구성 (도입부와 결론 불필요) 7. 다양한 제품 카테고리의 단일 기능 분석에 유연하게 적용할 수 있도록 구성 8. 특수문자(**, :, #, ## 등)를 사용하지 말고 일반 텍스트로만 작성하세요. [출력 형식] 1. 참고 자료 분석을 통해 선정된 핵심 기능의 5가지 중요 측면을 파악하여 자유롭게 아웃라인 구성 2. 반드시 본론 5개 항목으로만 구성할 것:(각 항목당 1번 엔터를 적용하여 빈칸이 나오지 않도록하라.) - 본론1: [핵심 기능의 기술적 원리/작동 메커니즘 관련 제목] - 본론2: [핵심 기능의 성능/측정 데이터 관련 제목] - 본론3: [사용자 경험 측면의 기능 평가 관련 제목] - 본론4: [경쟁 제품과의 기능 비교 관련 제목] - 본론5: [실생활 활용 가치/한계점 관련 제목] 3. 소제목은 선정된 핵심 기능의 특성에 맞게 자유롭게 구성 4. 키워드에 맞추지 말고, 참고 자료 분석을 통해 발견한 핵심 기능의 중요 측면 기반으로 구성 5. 예시 형식 (참고용일 뿐, 내용은 선정된 핵심 기능과 참고 자료에 따라 완전히 달라질 수 있음): - 본론1: [핵심 기능의 기술적 원리/메커니즘 관련 제목] - 본론2: [실측 테스트 결과/성능 데이터 관련 제목] - 본론3: [실사용 환경에서의 사용성/효율성 관련 제목] - 본론4: [타 제품 동일 기능과의 차별점 관련 제목] - 본론5: [기능의 미래 발전 가능성/개선점 관련 제목] {style_prompt} """ # Gemini API 호출 outline_result = call_gemini_api(extract_prompt, temperature=0.3) # 결과에서 핵심기능 추출 features = [] for line in outline_result.strip().split('\n'): if line.strip(): # 특수문자와 불필요한 형식 제거 clean_feature = re.sub(r'^\s*[-*#]\s*', '', line) # 번호, 불릿 제거 clean_feature = re.sub(r'\*\*|\*|##|#', '', clean_feature) # 특수문자 제거 clean_feature = clean_feature.strip() if clean_feature: features.append(clean_feature) # 5개가 안 되면 빈 값으로 채우기 while len(features) < 5: features.append(f"특징 {len(features) + 1} - 추가 설명 필요") return features[:5] # 최대 5개만 반환 except Exception as e: logging.error(f"핵심기능 선별 중 오류 발생: {str(e)}") return ["특징을 추출하지 못했습니다"] * 5 def generate_blog_post(category, style, references1, references2, references3, selected_feature): """핵심기능집중형 블로그 글 생성 함수 - 유연한 소재 구성""" try: # 참고글 준비 references = [ references1.strip() if references1.strip() else "참고 자료 없음", references2.strip() if references2.strip() else "참고 자료 없음", references3.strip() if references3.strip() else "참고 자료 없음" ] # 의미 있는 참고글만 필터링 references = [ref for ref in references if ref != "참고 자료 없음"] if not references: return "

    참고 자료가 없습니다. 최소 하나 이상의 참고 자료를 입력해주세요.

    " if not selected_feature.strip(): return "

    핵심기능이 선택되지 않았습니다. 핵심기능을 입력해주세요.

    " # 스타일 프롬프트 가져오기 style_prompt = get_style_prompt(style) # 블로그 글 생성 프롬프트 구성 blog_prompt = f""" [핵심기능 집중형 상품리뷰 작성 요청] 선택한 핵심기능: {selected_feature} [리뷰 작성 형식] 1. 리뷰는 '도입부', '5가지 소재', '마무리' 구조로 작성하세요. 2. 마크다운 형식은 최소한으로 사용하고, 가능한 한 일반 텍스트로 작성하세요. 3. 각 부분은 명확히 구분되어야 하며, 서술형 문장으로 자연스럽게 이어지도록 작성하세요. 4. 단락은 적절히 나누되, 너무 짧은 단락을 많이 만들지 마세요. [리뷰 내용 구조] 1. 도입부 (전체의 10%) - 선택한 핵심기능의 중요성과 특징을 간략히 소개 - 이 기능이 상품에서 어떤 가치를 제공하는지 설명 - 마지막 문장에서 본문에서 다룰 내용을 예고 2. 5가지 소재 (전체의 80%) - 선택한 핵심기능에 가장 적합한 5가지 소재를 자유롭게 선정하세요 - 각 소재는 기능의 서로 다른 측면을 다루어야 합니다 - 예시 소재: 기술적 원리, 작동 방식, 성능 분석, 경쟁 제품 비교, 활용 방법, 사용 경험, 설정 팁, 업데이트 이력, 산업 표준과의 비교, 사용 시나리오, 호환성, 한계점과 개선 방향 등 - 각 소재는 비슷한 분량으로 자연스럽게 연결되어야 합니다 3. 마무리 (전체의 10%) - 이 기능의 종합적 평가와 가치 - 어떤 유형의 사용자에게 특히 유용한지 - 핵심기능과 제품 전체에 대한 최종 견해 [중요 작성 지침] 1. 전체 글은 최소 4000자 이상으로 작성하세요. 2. 각 소재는 최소 600자 이상 작성하고, 서로 유기적으로 연결되게 하세요. 3. 제목은 따로 사용하지 말고, 도입부, 소재, 마무리를 하나의 연결된 글로 작성하세요. 4. 기술적 정확성을 유지하면서 선택한 핵심기능에 대한 심층적 분석과 구체적인 정보를 제공하세요. 5. 마케팅적 과장 표현보다는 사실적이고 분석적인 표현을 사용하세요. 6. 불필요한 반복이나 장황한 설명은 피하고, 핵심 정보와 통찰을 강조하세요. 7. 전체 글의 일관성을 유지하고, 문단 간 자연스러운 흐름을 만드세요. 8. 소재는 핵심기능의 성격에 맞게 가장 적절한 것을 자유롭게 선정하세요. 참고글: {references[0]} {references[1] if len(references) > 1 else ""} {references[2] if len(references) > 2 else ""} {style_prompt} """ # Gemini API 호출 logging.info("핵심기능 집중형 블로그 글 생성 시작") blog_content = call_gemini_api(blog_prompt, temperature=0.7) logging.info(f"생성된 원본 글 길이: {len(blog_content)}") # 후처리 processed_content = post_process_blog(blog_content, style) # 글자 수 체크 char_count = len(processed_content) logging.info(f"처리 후 블로그 글 글자 수: {char_count}") # 글자 수가 목표에 미달하면 확장 시도 if char_count < TARGET_CHAR_LENGTH: logging.info(f"글자 수 부족 ({char_count} < {TARGET_CHAR_LENGTH}), 확장 시도") expansion_prompt = f""" [핵심기능 분석 확장 요청] 현재 글은 목표 글자수인 4000자에 미치지 못합니다. 현재 글자수는 약 {char_count}자입니다. 선택한 핵심기능: {selected_feature} [확장 지침] 1. 원래 글의 구조(도입부, 5가지 소재, 마무리)를 유지하면서 내용을 확장하세요. 2. 각 소재에 더 구체적인 정보, 예시, 분석 내용을 추가하세요. 3. 서술적 흐름을 유지하고, 불필요한 마크다운 사용은 피하세요. 4. 글의 전체 일관성과 응집성을 유지하세요. 원본 글: {processed_content} """ # 확장 시도 expanded_content = call_gemini_api(expansion_prompt, temperature=0.75) processed_content = post_process_blog(expanded_content, style) # 다시 글자 수 체크 char_count = len(processed_content) logging.info(f"확장 후 블로그 글 글자 수: {char_count}") # HTML 변환 final_html = convert_to_html(processed_content) return final_html except Exception as e: logging.error(f"블로그 글 생성 중 오류 발생: {str(e)}") return f"

    블로그 글 생성 중 오류 발생: {str(e)}

    " def get_style_prompt(style="친근한"): """블로그 글의 스타일 프롬프트를 반환""" prompts = { "친근한": """ [친근한 핵심기능 리뷰 스타일 가이드] 1. 톤과 어조 - 대화하듯 편안하고 친근한 말투 사용 (예: "오늘은 ~에 대해 알아볼게요") - 1인칭 시점으로 직접 사용한 경험을 생생하게 표현 - 구어체와 일상적인 표현 사용하여 친근함 유지 2. 문장 및 어투 - '해요체'로 작성 (예: "~했어요", "~인 것 같아요") - 문장은 길지 않게 자연스럽게 연결 - 기술적 내용도 쉽고 이해하기 편한 표현으로 설명 3. 정보 전달 방식 - 개인 경험과 체감을 중심으로 정보 전달 - 전문적인 내용도 일상적인 비유와 예시로 풀어서 설명 - "제가 사용해보니~", "실제로 경험해보면~"과 같은 표현 활용 - 독자에게 직접 말하듯 중간중간 "~하시면 좋아요" 같은 조언 추가 """, "일반": """ [일반적인 핵심기능 리뷰 스타일 가이드] 1. 톤과 어조 - 객관적이고 중립적인 톤 유지 - 직접적인 경험과 객관적 데이터를 균형 있게 활용 - 존댓말 사용하되 딱딱하지 않게 표현 2. 문장 및 어투 - '합니다체' 사용 (예: "~합니다", "~입니다") - 명확하고 간결한 문장 구성 - 내용의 논리적 흐름을 중시 3. 정보 전달 방식 - 사실과 데이터를 중심으로 내용 전개 - 개인 경험과 객관적 분석을 적절히 혼합 - 불필요한 과장이나 주관적 평가 최소화 - 실용적인 관점에서 기능의 장단점 균형있게 서술 """, "전문적인": """ [전문적인 핵심기능 리뷰 스타일 가이드] 1. 톤과 어조 - 전문적이고 분석적인 톤 사용 - 기술적 깊이와 정확성 강조 - 존중과 권위를 느낄 수 있는 표현 사용 2. 문장 및 어투 - '합니다체'로 일관성 있게 작성 - 논리적이고 체계적인 문장 구성 - 전문 용어를 적절히 활용하되 필요시 간략한 설명 제공 3. 정보 전달 방식 - 기술적 원리와 메커니즘에 대한 심층 분석 - 벤치마크 데이터와 구체적 수치를 활용한 객관적 평가 - 경쟁 제품과의 세부적인 기술 비교 제공 - 기능의 기술적 한계와 발전 가능성에 대한 통찰 제시 """ } return prompts.get(style, prompts["친근한"]) def call_gemini_api(prompt, temperature=TEMPERATURE, top_p=TOP_P): """Gemini API 호출 함수""" try: logging.info("Gemini API 호출 시작") response = client.models.generate_content( model="gemini-2.0-flash", contents=[prompt], config=types.GenerateContentConfig( max_output_tokens=MAX_TOKENS, temperature=temperature, top_p=top_p ) ) logging.info("Gemini API 호출 완료") return response.text.strip() except Exception as e: logging.error(f"Gemini API 호출 중 오류 발생: {str(e)}") return f"API 호출 중 오류 발생: {str(e)}" # API 함수들 def generate_outline_4(category, style, ref1, ref2, ref3): features = generate_outline(category, style, ref1, ref2, ref3) # 3개의 문자열 튜플 반환 return (features[0], features[1], features[2]) def generate_blog_post_4(category, style, ref1, ref2, ref3, outline): # outline은 선택된 핵심기능 이름 return generate_blog_post(category, style, ref1, ref2, ref3, outline)