10m-marketing / app-backup-last.py
ginipick's picture
Create app-backup-last.py
0498441 verified
# ──────────────────────────────── Imports ────────────────────────────────
import os, json, re, logging, requests, markdown, time, io
from datetime import datetime
import streamlit as st
from openai import OpenAI # OpenAI 라이브러리
from gradio_client import Client
import pandas as pd
import PyPDF2 # For handling PDF files
# ──────────────────────────────── Environment Variables / Constants ─────────────────────────
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name
BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
IMAGE_API_URL = "http://211.233.58.201:7896"
MAX_TOKENS = 7999
# Blog template and style definitions (in English)
BLOG_TEMPLATES = {
"ginigen": "Recommended style by Ginigen", # ← 볡ꡬ
"standard": "Standard 8-step framework blog",
"tutorial": "Step-by-step tutorial format",
"review": "Product/service review format",
"storytelling": "Storytelling format",
"seo_optimized": "SEO-optimized blog",
# New specialized templates
"insta": "Instagram Reels script",
"thread": "SNS Thread post",
"shortform": "60-sec Short-form video",
"youtube": "YouTube script",
}
# ───────── Blog tone definitions ─────────
BLOG_TONES = {
"professional": "Professional and formal tone",
"casual": "Friendly and conversational tone",
"humorous": "Humorous approach",
"storytelling": "Story-driven approach",
}
# Example blog topics
EXAMPLE_TOPICS = {
"example1": "Changes to the real estate tax system in 2025: Impact on average households and tax-saving strategies",
"example2": "Summer festivals in 2025: A comprehensive guide to major regional events and hidden attractions",
"example3": "Emerging industries to watch in 2025: An investment guide focused on AI opportunities"
}
# ──────────────────────────────── Logging ────────────────────────────────
logging.basicConfig(level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s")
# ──────────────────────────────── OpenAI Client ──────────────────────────
# OpenAI ν΄λΌμ΄μ–ΈνŠΈμ— νƒ€μž„μ•„μ›ƒκ³Ό μž¬μ‹œλ„ 둜직 μΆ”κ°€
@st.cache_resource
def get_openai_client():
"""Create an OpenAI client with timeout and retry settings."""
if not OPENAI_API_KEY:
raise RuntimeError("⚠️ OPENAI_API_KEY ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
return OpenAI(
api_key=OPENAI_API_KEY,
timeout=60.0, # νƒ€μž„μ•„μ›ƒ 60초둜 μ„€μ •
max_retries=3 # μž¬μ‹œλ„ 횟수 3회둜 μ„€μ •
)
# ──────────────────────────────── Blog Creation System Prompt ─────────────
def get_system_prompt(template="ginigen", tone="professional", word_count=1750, include_search_results=False, include_uploaded_files=False) -> str:
"""
Generate a system prompt that includes:
- The 8-step blog writing framework
- The selected template and tone
- Guidelines for using web search results and uploaded files
"""
# Ginigen recommended style prompt (English version)
ginigen_prompt = """
당신은 λ›°μ–΄λ‚œ ν•œκ΅­μ–΄ SEO μΉ΄ν”ΌλΌμ΄ν„°μž…λ‹ˆλ‹€.
β—† λͺ©μ 
'Blog Template'의 선택에 따라 λΈ”λ‘œκ·Έ λ˜λŠ” 릴슀, μ“°λ ˆλ“œ, 유튜브 κ΄€λ ¨ μ „λ¬Έ 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
항상 **[핡심뢀터 μ œμ‹œ β†’ κ°„κ²°β€§λͺ…λ£Œν•˜κ²Œ β†’ λ…μž ν˜œνƒ κ°•μ‘° β†’ 행동 μœ λ„]**의 4원칙을 λ”°λ₯΄μ„Έμš”.
β—† μ™„μ„± ν˜•μ‹ (Markdown μ‚¬μš©, λΆˆν•„μš”ν•œ μ„€λͺ… κΈˆμ§€)
제λͺ©
이λͺ¨μ§€ + ꢁ금증 질문/감탄사 + 핡심 ν‚€μ›Œλ“œ (70자 이내)
μ˜ˆμ‹œ: # 🧬 μ—Όμ¦λ§Œ 쀄여도 살이 λΉ μ§„λ‹€?! ν€˜λ₯΄μ„Έν‹΄ 5κ°€μ§€ λ†€λΌμš΄ 효λŠ₯
Hook (2~3쀄)
문제 μ œμ‹œ β†’ ν•΄κ²° ν‚€μ›Œλ“œ μ–ΈκΈ‰ β†’ 이 글을 읽어야 ν•˜λŠ” 이유 μš”μ•½
--- ꡬ뢄선
μ„Ήμ…˜ 1: 핡심 κ°œλ… μ†Œκ°œ
## 🍏 [ν‚€μ›Œλ“œ]λž€ 무엇인가?
1~2문단 μ •μ˜ + πŸ“Œ ν•œμ€„ μš”μ•½
---
μ„Ήμ…˜ 2: 5κ°€μ§€ 이점/이유
## πŸ’ͺ [ν‚€μ›Œλ“œ]κ°€ μœ μ΅ν•œ 5κ°€μ§€ 이유
각 μ†Œμ œλͺ© ν˜•μ‹:
1. [ν‚€μ›Œλ“œ 쀑심 μ†Œμ œλͺ©]
1~2문단 μ„€λͺ…
βœ” 핡심 포인트 ν•œμ€„ κ°•μ‘°
총 5개 ν•­λͺ©
μ„Ήμ…˜ 3: μ„­μ·¨/ν™œμš© 방법
## πŸ₯— [ν‚€μ›Œλ“œ] μ œλŒ€λ‘œ ν™œμš©ν•˜λŠ” 법!
이λͺ¨μ§€ 뢈릿 5개 정도 + μΆ”κ°€ 팁
---
마무리 행동 μœ λ„
## πŸ“Œ κ²°λ‘  – μ§€κΈˆ λ°”λ‘œ [ν‚€μ›Œλ“œ] μ‹œμž‘ν•˜μ„Έμš”!
2~3λ¬Έμž₯으둜 ν˜œνƒ/λ³€ν™”λ₯Ό μš”μ•½ β†’ 행동 촉ꡬ (ꡬ맀, ꡬ독, 곡유 λ“±)
---
핡심 μš”μ•½ ν‘œ
ν•­λͺ© 효과
[ν‚€μ›Œλ“œ] [효과 μš”μ•½]
μ£Όμš” μŒμ‹/μ œν’ˆ [λͺ©λ‘]
---
ν€΄μ¦ˆ & CTA
κ°„λ‹¨ν•œ Q&A ν€΄μ¦ˆ (1λ¬Έν•­) β†’ μ •λ‹΅ 곡개
β€œλ„μ›€μ΄ λ˜μ…¨λ‹€λ©΄ 곡유/λŒ“κΈ€ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€β€ 문ꡬ
λ‹€μŒ κΈ€ 예고
β—† μΆ”κ°€ μ§€μΉ¨
전체 λΆ„λŸ‰ 1,200~1,800단어.
μ‰¬μš΄ μ–΄νœ˜Β·μ§§μ€ λ¬Έμž₯ μ‚¬μš©, 이λͺ¨μ§€Β·κ΅΅μ€ κΈ€μ”¨Β·μΈμš©μœΌλ‘œ 가독성 κ°•ν™”.
ꡬ체적 수치, 연ꡬ κ²°κ³Ό, λΉ„μœ λ‘œ 신뒰도 ↑.
β€œν”„λ‘¬ν”„νŠΈβ€, β€œμ§€μ‹œμ‚¬ν•­β€ λ“± 메타 μ–ΈκΈ‰ κΈˆμ§€.
λŒ€ν™”μ²΄μ΄λ©΄μ„œλ„ 전문성을 μœ μ§€.
μ™ΈλΆ€ μΆœμ²˜κ°€ μ—†λ‹€λ©΄ β€œμ—°κ΅¬μ— λ”°λ₯΄λ©΄β€ 같은 ν‘œν˜„ μ΅œμ†Œν™”.
β—† 좜λ ₯
μœ„ ν˜•μ‹μ„ λ”°λ₯Έ μ™„μ„± λΈ”λ‘œκ·Έ κΈ€λ§Œ λ°˜ν™˜ν•˜μ„Έμš”. μΆ”κ°€ μ„€λͺ…은 ν¬ν•¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
"""
# Standard 8-step framework (English version)
base_prompt = """
λ‹€μŒμ€ 전문적인 글을 μž‘μ„±ν•  λ•Œ λ°˜λ“œμ‹œ 따라야 ν•  8단계 ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€. 각 λ‹¨κ³„μ˜ μ„ΈλΆ€ ν•­λͺ©μ„ μΆ©μ‹€νžˆ λ°˜μ˜ν•˜μ—¬ 일관성 있고 ν₯미둜운 글을을 μ™„μ„±ν•˜μ„Έμš”.
λ…μž μ—°κ²° 단계
1.1. μΉœκ·Όν•œ μΈμ‚¬λ‘œ 라포 ν˜•μ„±
1.2. λ„μž… 질문으둜 λ…μžμ˜ μ‹€μ œ κ³ λ―Ό 반영
1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적인 ν₯λ―Έ 유발
문제 μ •μ˜ 단계
2.1. λ…μžκ°€ κ²ͺλŠ” 고좩을 ꡬ체적으둜 κ·œμ •
2.2. 문제의 μ‹œκΈ‰μ„±Β·μ˜ν–₯λ ₯ 뢄석
2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
μ „λ¬Έμ„± 확립 단계
3.1. 객관적 데이터λ₯Ό 기반으둜 뢄석
3.2. μ „λ¬Έκ°€ μ˜κ²¬Β·μ—°κ΅¬ κ²°κ³Ό 인용
3.3. μ‹€μƒν™œ μ‚¬λ‘€λ‘œ 이해도 κ°•ν™”
ν•΄κ²°μ±… μ œμ‹œ 단계
4.1. 단계별 κ°€μ΄λ“œ 제곡
4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ μ‹€μš© 팁 μ œμ•ˆ
4.3. μ˜ˆμƒ μž₯μ• λ¬Ό 및 극볡 방법 μ–ΈκΈ‰
μ‹ λ’° ꡬ좕 단계
5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
5.2. μ‚¬μš©μž ν›„κΈ° 인용
5.3. 효과λ₯Ό μž…μ¦ν•˜λŠ” 객관적 데이터 ν™œμš©
행동 μœ λ„ 단계
6.1. λ…μžκ°€ λ‹Ήμž₯ μ‹€ν–‰ν•  수 μžˆλŠ” 첫걸음 μ œμ•ˆ
6.2. 긴박감을 κ°•μ‘°ν•˜μ—¬ μ‹ μ†ν•œ 행동 촉ꡬ
6.3. ν˜œνƒΒ·λ³΄μƒμ„ κ°•μ‘°ν•΄ 동기 λΆ€μ—¬
μ§„μ •μ„± 단계
7.1. ν•΄κ²°μ±…μ˜ ν•œκ³„μ  투λͺ…ν•˜κ²Œ 곡개
7.2. κ°œμΈλ³„ 차이가 μžˆμ„ 수 μžˆμŒμ„ 인정
7.3. μ „μ œ 쑰건·주의 사항 λͺ…μ‹œ
관계 지속 단계
8.1. 진심 μ–΄λ¦° 감사 μΈμ‚¬λ‘œ 마무리
8.2. λ‹€μŒ μ½˜ν…μΈ  예고둜 κΈ°λŒ€κ° μ‘°μ„±
8.3. μΆ”κ°€ μ†Œν†΅ 채널 μ•ˆλ‚΄
"""
template_guides = {
"insta": """
λ„ˆλŠ” μΈμŠ€νƒ€κ·Έλž¨ 릴슀 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
당신은 **γ€ˆUniversal Reels Strategist GPT〉**λ‹€.
λͺ©ν‘œ: μ‚¬μš©μžκ°€ μ œμ‹œν•œ μ£Όμ œΒ·μ œν’ˆΒ·μ„œλΉ„μŠ€λ₯Ό λ°”νƒ•μœΌλ‘œ μ €μž₯β€§κ³΅μœ β€§ν–‰λ™μ„ μœ λ„ν•˜λŠ” 60초 μ΄ν•˜ 숏폼 μ˜μƒμ„ **ν•œ λ²ˆμ— μ™„μ„±**ν•΄ μ£ΌλŠ” 것.
────────────── κΈ°λ³Έ 원칙 ──────────────
1. **λ³ΈλŠ₯ 4λŒ€ μš•κ΅¬ μ—°κ²°**
β‘  λˆΒ·μ‹œκ°„ μ ˆμ•½(생쑴)
β‘‘ 건강·아름닀움(생쑴+미적 만쑱)
β‘’ μΈκ°„κ΄€κ³„Β·μ‚¬λž‘Β·μ‚¬νšŒμ  인정
β‘£ 문제 ν•΄κ²°Β·μ„±μž₯(λŠ₯λ ₯·지식 ν–₯상)
β†’ μ΅œμ†Œ 1개 이상과 μ‚¬μš©μž 주제λ₯Ό 연결해라.
2. **ν‘œλ³Έ 이둠(λŒ€μ€‘ν™” ν™•μž₯)**
β€’ μ£Όμ œκ°€ 쒁으면 β€˜λˆ„κ΅¬μ—κ²Œλ‚˜ 적용 κ°€λŠ₯ν•œ μ‹€μ΅β€™μœΌλ‘œ λ„“ν˜€λΌ.
예) μ§€λ°© μ†Œν˜• ν—¬μŠ€μž₯ 홍보 β†’ β€œν•˜λ£¨ 5λΆ„ 뱃살 νƒœμš°λŠ” ν™ˆνŠΈβ€.
3. **6단계 μ œμž‘ ν”„λ‘œμ„ΈμŠ€**
β‘  레퍼런슀·경쟁 사둀 뢄석
β‘‘ μ£Όμ œΒ·ν¬μ§€μ…”λ‹ ν™•μ •(ν‘œλ³Έ ν™•μž₯ 포함)
β‘’ ν›„ν‚Ή+μ‹œν€€μŠ€ 슀크립트 μž‘μ„± (μ•„λž˜ 아웃풋 ν˜•μ‹ μ‚¬μš©)
β‘£ μ΄¬μ˜Β·νŽΈμ§‘ κ°€μ΄λ“œ(ν•„μš” μž₯비·ꡬ도·BGM λ“±)
β‘€ μΉ΄ν”Ό 보완(제λͺ©Β·λ³Έλ¬ΈΒ·μΊ‘μ…˜)
β‘₯ 행동 μœ λ„ 문ꡬ(CTA) μ‚½μž…
4. **ν›„ν‚Ή 3초 κ·œμΉ™**
β€’ μ‹œμž‘ 3초 μ•ˆμ— **λ…Όλž€, ν˜ΈκΈ°μ‹¬, μˆ˜μΉ˜ν™”λœ 이득** 쀑 ν•˜λ‚˜λ₯Ό 폭발적으둜 μ œμ‹œ.
β€’ 숫자·ꡬ체 λ‹¨μ–΄Β·κ°•ν•œ 동사 μ‚¬μš©. (예: β€œ7일 λ§Œμ— 맀좜 두 λ°°?”)
5. **CTA ν•„μˆ˜**
β€’ μ €μž₯, 곡유, λŒ“κΈ€, ꡬ맀, μ‹ μ²­, μ˜ˆμ•½ λ“± μ΅œμ†Œ 1개λ₯Ό λͺ…μ‹œμ  λ¬Έμž₯으둜 μš”κ΅¬.
6. **ν†€β€§μŠ€νƒ€μΌ**
β€’ 친ꡬ처럼 직섀·간결.
β€’ λΆˆν•„μš”ν•œ 이λͺ¨μ§€Β·νŠΉμˆ˜λ¬Έμž κΈˆμ§€(β€˜!’ β€˜?’ 만 ν—ˆμš©).
β€’ ν•œκ΅­μ–΄κ°€ κΈ°λ³Έμ΄μ§€λ§Œ, μ‚¬μš©μžκ°€ μ˜μ–΄λ‘œ μš”μ²­ν•˜λ©΄ 동일 κ·œμΉ™μ„ μ˜μ–΄λ‘œ 제곡.
7. **정보 μˆ˜μ§‘**
β€’ μ—…μ’…Β·νƒ€κΉƒΒ·μ „ν™˜ λͺ©ν‘œΒ·μ˜ˆμ‚°Β·μ΄¬μ˜ κ°€λŠ₯ μž₯λΉ„κ°€ 뢈λͺ…ν™•ν•˜λ©΄ **ν•œ λ²ˆμ— λ¬Άμ–΄** λ¬Όμ–΄λ³Έλ‹€.
8. **좜λ ₯ ν˜•μ‹** (λͺ¨λ“  ν•­λͺ©μ€ 1~2쀄 λ‚΄μ™Έ, 번호 κ·ΈλŒ€λ‘œ μœ μ§€)
1) 제λͺ©(20자 μ΄ν•˜)
2) ν›„ν‚Ή λŒ€μ‚¬(첫 3초)
3) μ‹œν€€μŠ€ 슀크립트(μž₯면별 핡심 λŒ€μ‚¬Β·μžλ§‰)
4) 핡심 λ©”μ‹œμ§€ μš”μ•½
5) CTA 문ꡬ
6) μΊ‘μ…˜ μ˜ˆμ‹œ(이득→곡감→행동, 3λ¬Έμž₯)
7) ν•΄μ‹œνƒœκ·Έ(μ‰Όν‘œλ‘œ ꡬ뢄, 특수문자 μ œμ™Έ)
8) μ΄¬μ˜Β·νŽΈμ§‘ 팁(ν•„μš”μ‹œ)
9. **검증 체크리슀트**
β€’ λ³ΈλŠ₯ 자극 포인트 쑴재?
β€’ ν›„ν‚Ή 3초 κ·œμΉ™ μΆ©μ‘±?
β€’ CTA 포함? β†’ ν•˜λ‚˜λΌλ„ β€˜μ•„λ‹ˆμ˜€β€™λ©΄ 슀슀둜 μˆ˜μ • ν›„ 좜λ ₯.
───────── μ˜ˆμ‹œ μž…λ ₯ & μš”μ•½ 좜λ ₯ ─────────
μ‚¬μš©μž: β€œμ£Όμ œ: 1인 세무사 사무싀 μ‹ κ·œ 고객 확보”
GPT 좜λ ₯(μš”μ•½):
1) 제λͺ©: μ„Έλ¬΄λΉ„μš© 30% μ€„μ΄λŠ” 법
2) ν›„ν‚Ή: β€œ10λΆ„ μ „ν™”λ‘œ μ„ΈκΈˆ 300만 원 μ•„κΌˆμ–΄μš”?”
3) μ‹œν€€μŠ€: μž₯λ©΄1 μ„ΈκΈˆκ³ μ§€μ„œ 쇼크 β†’ β€œβ‘  λΆˆν•„μš” 곡제 찾기” …
… μ΄ν•˜ ν˜•μ‹ 동일
────────────────────────────
**λͺ¨λ“  닡변은 μœ„ κ·œμΉ™μ„ μ–΄κΈ°λ©΄ μžλ™μœΌλ‘œ μž¬κ²€ν† ν•˜κ³  μˆ˜μ •ν•˜λΌ.**
데이터가 μ—†λŠ”κ²ƒμ€ μ›Ήκ²€μƒ‰μœΌλ‘œ 정보λ₯Ό μ„œμΉ˜ν•΄μ„œ μ°Ύμ•„λ‚΄μ•Ό ν•œλ‹€.
""",
"thread": """
λ„ˆλŠ” μ“°λ ˆλ“œ 포슀트 생성 μ „λ¬Έκ°€ 역할이닀 :
λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
You are a Korean tech‑savvy copywriter who writes short, hype‑driven SNS thread posts.
When given a {product_name} and its {key_highlights}, output a thread in the following style:
[1] μ‹œμž‘
– ν•œ 쀄 ν›…: πŸ”₯ 같은 이λͺ¨μ§€ + 타깃 λ…μž μ†Œν™˜ + 짧은 감탄
– 두 번째 쀄: β€œ{product_name}κ°€/이 μ§„μ§œ 일 λƒˆλ‹€β€Β λ˜λŠ” λ™λ“±ν•œ μž„νŒ©νŠΈ λ¬Έμž₯
[2] μ •μ˜ & λ§₯락
– β€œ{unique_point}? 그게 뭐야?” 식 질문
– 1~2λ¬Έμž₯으둜 κ°œλ… μ„€λͺ…, 세계적 μ‚¬λ‘€Β·λ ˆνΌλŸ°μŠ€ ν•œ 쀄
[3] numbered 핡심 포인트
– 각 ν¬μΈνŠΈλŠ” β€œ{번호}/ {μ†Œμ œλͺ©}” ν˜•μ‹
– 이후 1~3μ€„λ‘œ {μ†Œμ œλͺ©}λ₯Ό 상세 μ„€λͺ…
– μ„€λͺ…은 ꡬ어체, λ¬Έμž₯ 짧게, β€˜!’ ν™œμš©
– ꡬ체 μ˜ˆμ‹œΒ·λΉ„κ΅Β·λ°μ΄ν„°λ₯Ό ν¬ν•¨ν•˜λ˜ ν•œ 문단 ≀3쀄
– μ΅œμ†Œ 3개, μ΅œλŒ€ 6개 포인트
[4] κ²°λ‘ 
– β€œ{λ§ˆμ§€λ§‰λ²ˆν˜Έ+1}/ κ²°λ‘  : …” ν˜•μ‹
– 문제 ν•΄κ²°Β·κ°€μΉ˜ μš”μ•½
– β€˜β€˜μ΄μ œ {call_to_action}’’ 식 직접 행동 μœ λ„
μŠ€νƒ€μΌ κ·œμΉ™:
- ν•œκ΅­μ–΄ μœ„μ£Ό, ν•„μš” μ‹œ μ˜μ–΄ κΈ°μˆ μš©μ–΄ κ·ΈλŒ€λ‘œ μ‚½μž…
- λ¬Έμž₯λ§ˆλ‹€ μ—”ν„°, 블둝 단락 ꡬ뢄
- νŠΉμˆ˜λ¬ΈμžλŠ” β€˜!’ β€˜?’ μ™Έ μ΅œμ†Œν™”
- 전체 길이 250~400자
- 이λͺ¨μ§€λŠ” 제λͺ©Β·μ€‘μš” ν¬μΈνŠΈμ—λ§Œ 1~3개 μ‚¬μš©
- μ‘΄λŒ“λ§ λŒ€μ‹  μΉœκ·Όν•œ 반말
""",
"shortform": """
λ„ˆλŠ” 숏폼 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
### πŸŽ›οΈ GPTS μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈβ€Šβ€”β€Š1λΆ„ 숏폼 μ˜μƒ λŒ€λ³Έ μž‘μ„±κΈ°
λ„ˆλŠ” **β€œ1 λΆ„ 숏폼 μ˜μƒ λŒ€λ³Έ μžλ™ν™” AI”**λ‹€.
μ‚¬μš©μžκ°€ μ£Όμ œΒ·μ œν’ˆΒ·μ„œλΉ„μŠ€Β·νƒ€κΉƒ μ‹œμ²­μžΒ·ν†€(선택)을 μž…λ ₯ν•˜λ©΄, μ•„λž˜ 포맷을 **ν•œκ΅­μ–΄**둜 μ™„μ„±λœ λŒ€λ³ΈμœΌλ‘œ 좜λ ₯ν•œλ‹€.
- 총 κΈΈμ΄λŠ” **60 초 이내**.
- 각 ꡬ간은 **νƒ€μž„μ½”λ“œ(초)**와 **ꡬ간λͺ…**을 λŒ€κ΄„ν˜Έλ‘œ ν‘œκΈ°.
- λ¬Έμž₯은 μ§§κ³  μž„νŒ©νŠΈ 있게, 1 λ¬Έμž₯ β‰ˆ 1.5 초 κΈ°μ€€.
- 이λͺ¨μ§€ μ‚¬μš©μ€ μžμœ μ§€λ§Œ κ³Όλ„ν•˜μ§€ μ•Šκ²Œ(0–2개).
- νŠΉμˆ˜λ¬ΈμžλŠ” β€˜!’와 β€˜?β€™λ§Œ ν—ˆμš©.
🟑 **좜λ ₯ 포맷**
[0-3초 | Hook]
{μ‹œμ²­μž μŠ€ν¬λ‘€μ„ 멈좜 ν•œλ§ˆλ””}
[4-15초 | Problem]
{μ‹œμ²­μž 곡감 포인트λ₯Ό μ •ν™•νžˆ 짚기}
[16-30초 | Solution]
{μ œν’ˆ/μ„œλΉ„μŠ€/아이디어 μ†Œκ°œ + 핡심 κΈ°λŠ₯}
[31-45초 | Proof]
{효과 증λͺ…·데이터·후기 + 경쟁 μ œν’ˆκ³Ό 차별점}
[46-55초 | Callback/Emotion]
{Hookλ₯Ό μžμ—°μŠ€λŸ½κ²Œ νšŒμˆ˜ν•˜κ±°λ‚˜ 감정 자극}
[56-60초 | CTA]
{κ΅¬λ§€Β·ν΄λ¦­Β·νŒ”λ‘œμš° λ“± λͺ…ν™•ν•œ 행동 μœ λ„}
🟑 **μž‘μ„± κ·œμΉ™**
1. **Hook** – λ†€λΌμ›€Β·κΆκΈˆμ¦Β·κ³΅κ° 쀑 ν•˜λ‚˜λ‘œ κ°•λ ¬ν•œ ν•œ λ¬Έμž₯.
2. **Problem** – λŒ€μƒ μ‹œμ²­μžμ˜ λΆˆνŽΈΒ·κ³ λ―Όμ„ ꡬ체적으둜 μ–ΈκΈ‰.
3. **Solution** – μ œν’ˆΒ·μ„œλΉ„μŠ€λ‘œ 문제 ν•΄κ²°, 핡심 κΈ°λŠ₯을 μ‰¬μš΄ ν‘œν˜„μœΌλ‘œ.
4. **Proof** – μˆ˜μΉ˜Β·ν›„κΈ°Β·μ „λ¬Έκ°€ μ–ΈκΈ‰ λ“± μ‹ λ’° μš”μ†Œ 1-2개 + 차별점.
5. **Callback/Emotion** – 훅을 λ³€μ£Όν•˜κ±°λ‚˜ 희망·긴급 감정 자극.
6. **CTA** – ꡬ체적 행동 + ν•œμ •μ„±Β·κΈ΄κΈ‰μ„± μ–Έμ–΄.
🟑 **ν”„λ‘¬ν”„νŠΈ μž…λ ₯ μ˜ˆμ‹œ**
주제: 슀마트 무선 μ²­μ†ŒκΈ°
톀: μΉœκ·Όν•˜κ³  유머러슀
-μ‚¬μš©μžμ˜ μ˜μƒ λͺ©μ (예:μ œν’ˆ 홍보, μ‚¬μš©λ²• μ•ˆλ‚΄, μœ μš©μ„± μ„€λͺ… λ“±)κ³Ό 타깃 μ‹œμ²­μž 그리고 μ‹œμ²­μžμ—κ²Œ μ „λ‹¬ν•˜κ³  싢은 μ£Όμš” λ©”μ‹œμ§€μ— λŒ€ν•œ 정보λ₯Ό λ°›μ„μˆ˜ μžˆμ–΄μ•Όν•΄ λ‹΅λ³€ μ˜ˆμ‹œλ„ ν•¨κ»˜ 보여주고
-μ΅œλŒ€ 4개의 이λͺ¨μ§€λ₯Ό μ‚¬μš©ν•΄μ€˜
-μ‹œλ‚˜λ¦¬μ˜€λŠ” μ˜μƒκ³Ό λŒ€λ³Έμ„ κ΅¬λΆ„ν• μˆ˜ 있게 좜λ ₯ν•΄μ€˜
-κ²°κ³Όλ¬Ό 좜λ ₯μ‹œ ν•˜λ‹¨μ— λ”°λ‘œ 이미지도 ν•¨κ»˜ μƒμ„±ν•΄μ€˜ κ΄€λ ¨ λ°°κ²½μ΄λ―Έμ§€λ‘œ μƒμ„±ν•˜λ˜ μ œν’ˆμ€ μƒμ„±ν•˜μ§€λ§κ²ƒ. 그리고 μΊ‘μ…˜/μΉ΄ν”ΌλΌμ΄νŒ…λ“± ν…μŠ€νŠΈλ₯Ό 이미지 μ•ˆμ— μ ˆλŒ€ 생성 ν•˜μ§€λ§ˆ
""",
"youtube": """
λ„ˆλŠ” 유튜브 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
"""
}
# μ–΄μ‘°(톀)별 μΆ”κ°€ μ§€μΉ¨
tone_guides = {
"professional": "전문적이고 κΆŒμœ„ μžˆλŠ” 문체λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. 기술 μš©μ–΄λ₯Ό λͺ…ν™•νžˆ μ„€λͺ…ν•˜κ³ , λ°μ΄ν„°λ‚˜ 연ꡬ κ²°κ³Όλ₯Ό μ œμ‹œν•˜μ—¬ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
"casual": "νŽΈμ•ˆν•˜κ³  λŒ€ν™”μ²΄μ— κ°€κΉŒμš΄ μŠ€νƒ€μΌμ„ μ‚¬μš©ν•©λ‹ˆλ‹€. 개인 κ²½ν—˜Β·κ³΅κ° κ°€λŠ” μ˜ˆμ‹œλ₯Ό λ“€κ³ , μΉœκ·Όν•œ μ–΄μ‘°(예: '정말 μœ μš©ν•΄μš”!')λ₯Ό ν™œμš©ν•˜μ„Έμš”.",
"humorous": "μœ λ¨Έμ™€ 재치 μžˆλŠ” ν‘œν˜„μ„ μ‚¬μš©ν•©λ‹ˆλ‹€. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ 농담을 μΆ”κ°€ν•˜λ˜, μ •ν™•μ„±κ³Ό μœ μš©μ„±μ„ μœ μ§€ν•˜μ„Έμš”.",
"storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― μ„œμˆ ν•©λ‹ˆλ‹€. 감정 κΉŠμ΄μ™€ μ„œμ‚¬μ  흐름을 μœ μ§€ν•˜κ³ , μΈλ¬ΌΒ·λ°°κ²½Β·κ°ˆλ“±Β·ν•΄κ²°μ„ λ…Ήμ—¬λ‚΄μ„Έμš”."
}
# μ›Ή 검색 κ²°κ³Ό μ‚¬μš© μ§€μΉ¨
search_guide = """
[μ›Ή 검색 κ²°κ³Ό ν™œμš© κ°€μ΄λ“œ]
- 검색 결과의 핡심 정보λ₯Ό λΈ”λ‘œκ·Έμ— μ •ν™•νžˆ ν†΅ν•©ν•˜μ„Έμš”.
- μ΅œμ‹  데이터, 톡계, 사둀λ₯Ό ν¬ν•¨ν•˜μ„Έμš”.
- 인용 μ‹œ λ³Έλ¬Έμ—μ„œ 좜처λ₯Ό λͺ…ν™•νžˆ ν‘œκΈ°ν•˜μ„Έμš” (예: "XYZ μ›Ήμ‚¬μ΄νŠΈμ— λ”°λ₯΄λ©΄ …").
- κΈ€ λ§ˆμ§€λ§‰μ— 'μ°Έκ³  자료' μ„Ήμ…˜μ„ 두고 μ£Όμš” μΆœμ²˜μ™€ 링크λ₯Ό λ‚˜μ—΄ν•˜μ„Έμš”.
- μƒλ°˜λ˜λŠ” 정보가 μžˆλ‹€λ©΄ λ‹€μ–‘ν•œ 관점을 ν•¨κ»˜ μ œμ‹œν•˜μ„Έμš”.
- μ΅œμ‹  νŠΈλ Œλ“œμ™€ 데이터λ₯Ό λ°˜λ“œμ‹œ λ°˜μ˜ν•˜μ„Έμš”.
"""
# μ—…λ‘œλ“œ 파일 μ‚¬μš© μ§€μΉ¨
upload_guide = """
[μ—…λ‘œλ“œλœ 파일 ν™œμš© μ§€μΉ¨ (μ΅œμš°μ„ )]
- μ—…λ‘œλ“œλœ νŒŒμΌμ€ λΈ”λ‘œκ·Έμ˜ 핡심 정보원이어야 ν•©λ‹ˆλ‹€.
- 파일 속 λ°μ΄ν„°Β·ν†΅κ³„Β·μ˜ˆμ‹œλ₯Ό λ©΄λ°€νžˆ κ²€ν† ν•΄ ν†΅ν•©ν•˜μ„Έμš”.
- μ£Όμš” 수치·주μž₯은 직접 μΈμš©ν•˜κ³  μΆ©λΆ„νžˆ μ„€λͺ…ν•˜μ„Έμš”.
- 파일 λ‚΄μš©μ„ λΈ”λ‘œκ·Έμ˜ 핡심 μš”μ†Œλ‘œ κ°•μ‘°ν•˜μ„Έμš”.
- 좜처λ₯Ό λͺ…ν™•νžˆ ν‘œκΈ°ν•˜μ„Έμš” (예: "μ—…λ‘œλ“œλœ 데이터에 λ”°λ₯΄λ©΄ …").
- CSV νŒŒμΌμ€ μ€‘μš”ν•œ μˆ˜μΉ˜λ‚˜ 톡계λ₯Ό μƒμ„Ένžˆ λ‹€λ£¨μ„Έμš”.
- PDF νŒŒμΌμ€ 핡심 λ¬Έμž₯μ΄λ‚˜ μ§„μˆ μ„ μΈμš©ν•˜μ„Έμš”.
- ν…μŠ€νŠΈ 파일의 κ΄€λ ¨ λ‚΄μš©μ„ 효과적으둜 ν†΅ν•©ν•˜μ„Έμš”.
- 파일 λ‚΄μš©μ΄ λ‹€μ†Œ λ²—μ–΄λ‚˜ 보여도 μ£Όμ œμ™€ 연결고리λ₯Ό μ°Ύμ•„ μ„œμˆ ν•˜μ„Έμš”.
- κΈ€ μ „λ°˜μ— 걸쳐 μΌκ΄€λ˜κ²Œ 파일 데이터λ₯Ό λ°˜μ˜ν•˜μ„Έμš”.
"""
# Choose base prompt
if template == "ginigen":
final_prompt = ginigen_prompt
else:
final_prompt = base_prompt
# If the user chose a specific template (and not ginigen), append the relevant guidelines
if template != "ginigen" and template in template_guides:
final_prompt += "\n" + template_guides[template]
# If a specific tone is selected, append that guideline
if tone in tone_guides:
final_prompt += f"\n\nTone and Manner: {tone_guides[tone]}"
# If web search results should be included
if include_search_results:
final_prompt += f"\n\n{search_guide}"
# If uploaded files should be included
if include_uploaded_files:
final_prompt += f"\n\n{upload_guide}"
# Word count guidelines
final_prompt += (
f"\n\nWriting Requirements:\n"
f"9.1. Word Count: around {word_count-250}-{word_count+250} characters\n"
f"9.2. Paragraph Length: 3-4 sentences each\n"
f"9.3. Visual Cues: Use subheadings, separators, and bullet/numbered lists\n"
f"9.4. Data: Cite all sources\n"
f"9.5. Readability: Use clear paragraph breaks and highlights where necessary"
)
return final_prompt
# ──────────────────────────────── Brave Search API ────────────────────────
@st.cache_data(ttl=3600)
def brave_search(query: str, count: int = 20):
"""
Call the Brave Web Search API β†’ list[dict]
Returns fields: index, title, link, snippet, displayed_link
"""
if not BRAVE_KEY:
raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
headers = {
"Accept": "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": BRAVE_KEY
}
params = {"q": query, "count": str(count)}
for attempt in range(3):
try:
r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
r.raise_for_status()
data = r.json()
logging.info(f"Brave search result data structure: {list(data.keys())}")
raw = data.get("web", {}).get("results") or data.get("results", [])
if not raw:
logging.warning(f"No Brave search results found. Response: {data}")
raise ValueError("No search results found.")
arts = []
for i, res in enumerate(raw[:count], 1):
url = res.get("url", res.get("link", ""))
host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
arts.append({
"index": i,
"title": res.get("title", "No title"),
"link": url,
"snippet": res.get("description", res.get("text", "No snippet")),
"displayed_link": host
})
logging.info(f"Brave search success: {len(arts)} results")
return arts
except Exception as e:
logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}")
if attempt < 2:
time.sleep(2)
return []
def mock_results(query: str) -> str:
"""Fallback search results if API fails"""
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return (f"# Fallback Search Content (Generated: {ts})\n\n"
f"The search API request failed. Please generate the blog based on any pre-existing knowledge about '{query}'.\n\n"
f"You may consider the following points:\n\n"
f"- Basic concepts and importance of {query}\n"
f"- Commonly known related statistics or trends\n"
f"- Typical expert opinions on this subject\n"
f"- Questions that readers might have\n\n"
f"Note: This is fallback guidance, not real-time data.\n\n")
def do_web_search(query: str) -> str:
"""Perform web search and format the results."""
try:
arts = brave_search(query, 20)
if not arts:
logging.warning("No search results, using fallback content")
return mock_results(query)
hdr = "# Web Search Results\nUse the information below to enhance the reliability of your blog. When you quote, please cite the source, and add a References section at the end of the blog.\n\n"
body = "\n".join(
f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
for a in arts
)
return hdr + body
except Exception as e:
logging.error(f"Web search process failed: {str(e)}")
return mock_results(query)
# ──────────────────────────────── File Upload Handling ─────────────────────
def process_text_file(file):
"""Handle text file"""
try:
content = file.read()
file.seek(0)
text = content.decode('utf-8', errors='ignore')
if len(text) > 10000:
text = text[:9700] + "...(truncated)..."
result = f"## Text File: {file.name}\n\n"
result += text
return result
except Exception as e:
logging.error(f"Error processing text file: {str(e)}")
return f"Error processing text file: {str(e)}"
def process_csv_file(file):
"""Handle CSV file"""
try:
content = file.read()
file.seek(0)
df = pd.read_csv(io.BytesIO(content))
result = f"## CSV File: {file.name}\n\n"
result += f"- Rows: {len(df)}\n"
result += f"- Columns: {len(df.columns)}\n"
result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n"
result += "### Data Preview\n\n"
preview_df = df.head(10)
try:
markdown_table = preview_df.to_markdown(index=False)
if markdown_table:
result += markdown_table + "\n\n"
else:
result += "Unable to display CSV data.\n\n"
except Exception as e:
logging.error(f"Markdown table conversion error: {e}")
result += "Displaying data as text:\n\n"
result += str(preview_df) + "\n\n"
num_cols = df.select_dtypes(include=['number']).columns
if len(num_cols) > 0:
result += "### Basic Statistical Information\n\n"
try:
stats_df = df[num_cols].describe().round(2)
stats_markdown = stats_df.to_markdown()
if stats_markdown:
result += stats_markdown + "\n\n"
else:
result += "Unable to display statistical information.\n\n"
except Exception as e:
logging.error(f"Statistical info conversion error: {e}")
result += "Unable to generate statistical information.\n\n"
return result
except Exception as e:
logging.error(f"CSV file processing error: {str(e)}")
return f"Error processing CSV file: {str(e)}"
def process_pdf_file(file):
"""Handle PDF file"""
try:
# Read file in bytes
file_bytes = file.read()
file.seek(0)
# Use PyPDF2
pdf_file = io.BytesIO(file_bytes)
reader = PyPDF2.PdfReader(pdf_file, strict=False)
# Basic info
result = f"## PDF File: {file.name}\n\n"
result += f"- Total pages: {len(reader.pages)}\n\n"
# Extract text by page (limit to first 5 pages)
max_pages = min(5, len(reader.pages))
all_text = ""
for i in range(max_pages):
try:
page = reader.pages[i]
page_text = page.extract_text()
current_page_text = f"### Page {i+1}\n\n"
if page_text and len(page_text.strip()) > 0:
# Limit to 1500 characters per page
if len(page_text) > 1500:
current_page_text += page_text[:1500] + "...(truncated)...\n\n"
else:
current_page_text += page_text + "\n\n"
else:
current_page_text += "(No text could be extracted from this page)\n\n"
all_text += current_page_text
# If total text is too long, break
if len(all_text) > 8000:
all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
break
except Exception as page_err:
logging.error(f"Error processing PDF page {i+1}: {str(page_err)}")
all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n"
if len(reader.pages) > max_pages:
all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n"
result += "### PDF Content\n\n" + all_text
return result
except Exception as e:
logging.error(f"PDF file processing error: {str(e)}")
return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed."
def process_uploaded_files(files):
"""Combine the contents of all uploaded files into one string."""
if not files:
return None
result = "# Uploaded File Contents\n\n"
result += "Below is the content from the files provided by the user. Integrate this data as a main source of information for the blog.\n\n"
for file in files:
try:
ext = file.name.split('.')[-1].lower()
if ext == 'txt':
result += process_text_file(file) + "\n\n---\n\n"
elif ext == 'csv':
result += process_csv_file(file) + "\n\n---\n\n"
elif ext == 'pdf':
result += process_pdf_file(file) + "\n\n---\n\n"
else:
result += f"### Unsupported File: {file.name}\n\n---\n\n"
except Exception as e:
logging.error(f"File processing error {file.name}: {e}")
result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n"
return result
# ──────────────────────────────── Image & Utility ─────────────────────────
def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
"""Image generation function."""
if not prompt:
return None, "Insufficient prompt"
try:
res = Client(IMAGE_API_URL).predict(
prompt=prompt, width=w, height=h, guidance=g,
inference_steps=steps, seed=seed,
do_img2img=False, init_image=None,
image2image_strength=0.8, resize_img=True,
api_name="/generate_image"
)
return res[0], f"Seed: {res[1]}"
except Exception as e:
logging.error(e)
return None, str(e)
def extract_image_prompt(blog_text: str, topic: str):
"""
Generate a single-line English image prompt from the blog content.
"""
client = get_openai_client()
try:
response = client.chat.completions.create(
model="gpt-4.1-mini", # 일반적으둜 μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈλ‘œ μ„€μ •
messages=[
{"role": "system", "content": "Generate a single-line English image prompt from the following text. Return only the prompt text, nothing else."},
{"role": "user", "content": f"Topic: {topic}\n\n---\n{blog_text}\n\n---"}
],
temperature=1,
max_tokens=80,
top_p=1
)
return response.choices[0].message.content.strip()
except Exception as e:
logging.error(f"OpenAI image prompt generation error: {e}")
return f"A professional photo related to {topic}, high quality"
def md_to_html(md: str, title="Ginigen Blog"):
"""Convert Markdown to HTML."""
return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
def keywords(text: str, top=5):
"""Simple keyword extraction."""
cleaned = re.sub(r"[^κ°€-힣a-zA-Z0-9\s]", "", text)
return " ".join(cleaned.split()[:top])
# ──────────────────────────────── Streamlit UI ────────────────────────────
def ginigen_app():
st.title("Ginigen Blog")
# Set default session state
if "ai_model" not in st.session_state:
st.session_state.ai_model = "gpt-4.1-mini" # κ³ μ • λͺ¨λΈ μ„€μ •
if "messages" not in st.session_state:
st.session_state.messages = []
if "auto_save" not in st.session_state:
st.session_state.auto_save = True
if "generate_image" not in st.session_state:
st.session_state.generate_image = False
if "web_search_enabled" not in st.session_state:
st.session_state.web_search_enabled = True
if "blog_template" not in st.session_state:
st.session_state.blog_template = "ginigen" # Ginigen recommended style by default
if "blog_tone" not in st.session_state:
st.session_state.blog_tone = "professional"
if "word_count" not in st.session_state:
st.session_state.word_count = 1750
# Sidebar UI
sb = st.sidebar
sb.title("Blog Settings")
# λͺ¨λΈ 선택 제거 (κ³ μ • λͺ¨λΈ μ‚¬μš©)
sb.subheader("Blog Style Settings")
sb.selectbox(
"Blog Template",
options=list(BLOG_TEMPLATES.keys()),
format_func=lambda x: BLOG_TEMPLATES[x],
key="blog_template"
)
sb.selectbox(
"Blog Tone",
options=list(BLOG_TONES.keys()),
format_func=lambda x: BLOG_TONES[x],
key="blog_tone"
)
sb.slider("Blog Length (word count)", 800, 3000, key="word_count")
# Example topics
sb.subheader("Example Topics")
c1, c2, c3 = sb.columns(3)
if c1.button("Real Estate Tax", key="ex1"):
process_example(EXAMPLE_TOPICS["example1"])
if c2.button("Summer Festivals", key="ex2"):
process_example(EXAMPLE_TOPICS["example2"])
if c3.button("Investment Guide", key="ex3"):
process_example(EXAMPLE_TOPICS["example3"])
sb.subheader("Other Settings")
sb.toggle("Auto Save", key="auto_save")
sb.toggle("Auto Image Generation", key="generate_image")
web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled)
st.session_state.web_search_enabled = web_search_enabled
if web_search_enabled:
st.sidebar.info("βœ… Web search results will be integrated into the blog.")
# Download the latest blog (markdown/HTML)
latest_blog = next(
(m["content"] for m in reversed(st.session_state.messages)
if m["role"] == "assistant" and m["content"].strip()),
None
)
if latest_blog:
title_match = re.search(r"# (.*?)(\n|$)", latest_blog)
title = title_match.group(1).strip() if title_match else "blog"
sb.subheader("Download Latest Blog")
d1, d2 = sb.columns(2)
d1.download_button("Download as Markdown", latest_blog,
file_name=f"{title}.md", mime="text/markdown")
d2.download_button("Download as HTML", md_to_html(latest_blog, title),
file_name=f"{title}.html", mime="text/html")
# JSON conversation record upload
up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader")
if up:
try:
st.session_state.messages = json.load(up)
sb.success("Conversation history loaded successfully")
except Exception as e:
sb.error(f"Failed to load: {e}")
# JSON conversation record download
if sb.button("Download Conversation as JSON"):
sb.download_button(
"Save",
data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
file_name="chat_history.json",
mime="application/json"
)
# File Upload
st.subheader("File Upload")
uploaded_files = st.file_uploader(
"Upload files to be referenced in your blog (txt, csv, pdf)",
type=["txt", "csv", "pdf"],
accept_multiple_files=True,
key="file_uploader"
)
if uploaded_files:
file_count = len(uploaded_files)
st.success(f"{file_count} files uploaded. They will be referenced in the blog.")
with st.expander("Preview Uploaded Files", expanded=False):
for idx, file in enumerate(uploaded_files):
st.write(f"**File Name:** {file.name}")
ext = file.name.split('.')[-1].lower()
if ext == 'txt':
preview = file.read(1000).decode('utf-8', errors='ignore')
file.seek(0)
st.text_area(
f"Preview of {file.name}",
preview + ("..." if len(preview) >= 1000 else ""),
height=150
)
elif ext == 'csv':
try:
df = pd.read_csv(file)
file.seek(0)
st.write("CSV Preview (up to 5 rows)")
st.dataframe(df.head(5))
except Exception as e:
st.error(f"CSV preview failed: {e}")
elif ext == 'pdf':
try:
file_bytes = file.read()
file.seek(0)
pdf_file = io.BytesIO(file_bytes)
reader = PyPDF2.PdfReader(pdf_file, strict=False)
pc = len(reader.pages)
st.write(f"PDF File: {pc} pages")
if pc > 0:
try:
page_text = reader.pages[0].extract_text()
preview = page_text[:500] if page_text else "(No text extracted)"
st.text_area("Preview of the first page", preview + "...", height=150)
except:
st.warning("Failed to extract text from the first page")
except Exception as e:
st.error(f"PDF preview failed: {e}")
if idx < file_count - 1:
st.divider()
# Display existing messages
for m in st.session_state.messages:
with st.chat_message(m["role"]):
st.markdown(m["content"])
if "image" in m:
st.image(m["image"], caption=m.get("image_caption", ""))
# User input
prompt = st.chat_input("Enter a blog topic or keywords.")
if prompt:
process_input(prompt, uploaded_files)
# μ‚¬μ΄λ“œλ°” ν•˜λ‹¨ λ°°μ§€(링크) μΆ”κ°€
sb.markdown("---")
sb.markdown("Created by [https://ginigen.com](https://ginigen.com) | [YouTube Channel](https://www.youtube.com/@ginipickaistudio)")
def process_example(topic):
"""Process the selected example topic."""
process_input(topic, [])
def process_input(prompt: str, uploaded_files):
# Add user's message
if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
with st.chat_message("assistant"):
placeholder = st.empty()
message_placeholder = st.empty()
full_response = ""
use_web_search = st.session_state.web_search_enabled
has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
try:
# μƒνƒœ ν‘œμ‹œλ₯Ό μœ„ν•œ μƒνƒœ μ»΄ν¬λ„ŒνŠΈ
status = st.status("Preparing to generate blog...")
status.update(label="Initializing client...")
client = get_openai_client()
# Prepare conversation messages
messages = []
# Web search
search_content = None
if use_web_search:
status.update(label="Performing web search...")
with st.spinner("Searching the web..."):
search_content = do_web_search(keywords(prompt, top=5))
# Process uploaded files β†’ content
file_content = None
if has_uploaded_files:
status.update(label="Processing uploaded files...")
with st.spinner("Analyzing files..."):
file_content = process_uploaded_files(uploaded_files)
# Build system prompt
status.update(label="Preparing blog draft...")
sys_prompt = get_system_prompt(
template=st.session_state.blog_template,
tone=st.session_state.blog_tone,
word_count=st.session_state.word_count,
include_search_results=use_web_search,
include_uploaded_files=has_uploaded_files
)
# OpenAI API 호좜 μ€€λΉ„
status.update(label="Writing blog content...")
# λ©”μ‹œμ§€ ꡬ성
api_messages = [
{"role": "system", "content": sys_prompt}
]
user_content = prompt
# 검색 κ²°κ³Όκ°€ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
if search_content:
user_content += "\n\n" + search_content
# 파일 λ‚΄μš©μ΄ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
if file_content:
user_content += "\n\n" + file_content
# μ‚¬μš©μž λ©”μ‹œμ§€ μΆ”κ°€
api_messages.append({"role": "user", "content": user_content})
# OpenAI API 슀트리밍 호좜 - κ³ μ • λͺ¨λΈ "gpt-4.1-mini" μ‚¬μš©
try:
# 슀트리밍 λ°©μ‹μœΌλ‘œ API 호좜
stream = client.chat.completions.create(
model="gpt-4.1-mini", # κ³ μ • λͺ¨λΈ μ‚¬μš©
messages=api_messages,
temperature=1,
max_tokens=MAX_TOKENS,
top_p=1,
stream=True # 슀트리밍 ν™œμ„±ν™”
)
# 슀트리밍 응닡 처리
for chunk in stream:
if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
content_delta = chunk.choices[0].delta.content
full_response += content_delta
message_placeholder.markdown(full_response + "β–Œ")
# μ΅œμ’… 응닡 ν‘œμ‹œ (μ»€μ„œ 제거)
message_placeholder.markdown(full_response)
status.update(label="Blog completed!", state="complete")
except Exception as api_error:
error_message = str(api_error)
logging.error(f"API error: {error_message}")
status.update(label=f"Error: {error_message}", state="error")
raise Exception(f"Blog generation error: {error_message}")
# 이미지 생성
answer_entry_saved = False
if st.session_state.generate_image and full_response:
with st.spinner("Generating image..."):
try:
ip = extract_image_prompt(full_response, prompt)
img, cap = generate_image(ip)
if img:
st.image(img, caption=cap)
st.session_state.messages.append({
"role": "assistant",
"content": full_response,
"image": img,
"image_caption": cap
})
answer_entry_saved = True
except Exception as img_error:
logging.error(f"Image generation error: {str(img_error)}")
st.warning("이미지 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λΈ”λ‘œκ·Έ μ½˜ν…μΈ λ§Œ μ €μž₯λ©λ‹ˆλ‹€.")
# Save the answer if not saved above
if not answer_entry_saved and full_response:
st.session_state.messages.append({"role": "assistant", "content": full_response})
# Download buttons
if full_response:
st.subheader("Download This Blog")
c1, c2 = st.columns(2)
c1.download_button(
"Markdown",
data=full_response,
file_name=f"{prompt[:30]}.md",
mime="text/markdown"
)
c2.download_button(
"HTML",
data=md_to_html(full_response, prompt[:30]),
file_name=f"{prompt[:30]}.html",
mime="text/html"
)
# Auto save
if st.session_state.auto_save and st.session_state.messages:
try:
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
with open(fn, "w", encoding="utf-8") as fp:
json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
except Exception as e:
logging.error(f"Auto-save failed: {e}")
except Exception as e:
error_message = str(e)
placeholder.error(f"An error occurred: {error_message}")
logging.error(f"Process input error: {error_message}")
ans = f"An error occurred while processing your request: {error_message}"
st.session_state.messages.append({"role": "assistant", "content": ans})
# ──────────────────────────────── main ────────────────────────────────────
def main():
ginigen_app()
if __name__ == "__main__":
main()