ginipick commited on
Commit
11c72d6
Β·
verified Β·
1 Parent(s): 0d64a16

Delete app-backup-last.py

Browse files
Files changed (1) hide show
  1. app-backup-last.py +0 -1076
app-backup-last.py DELETED
@@ -1,1076 +0,0 @@
1
- # ──────────────────────────────── Imports ────────────────────────────────
2
- import os, json, re, logging, requests, markdown, time, io
3
- from datetime import datetime
4
-
5
- import streamlit as st
6
- from openai import OpenAI # OpenAI 라이브러리
7
-
8
- from gradio_client import Client
9
- import pandas as pd
10
- import PyPDF2 # For handling PDF files
11
-
12
- # ──────────────────────────────── Environment Variables / Constants ─────────────────────────
13
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
14
- BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name
15
- BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
16
- IMAGE_API_URL = "http://211.233.58.201:7896"
17
- MAX_TOKENS = 7999
18
-
19
- # Blog template and style definitions (in English)
20
- BLOG_TEMPLATES = {
21
- "ginigen": "Recommended style by Ginigen", # ← 볡ꡬ
22
- "standard": "Standard 8-step framework blog",
23
- "tutorial": "Step-by-step tutorial format",
24
- "review": "Product/service review format",
25
- "storytelling": "Storytelling format",
26
- "seo_optimized": "SEO-optimized blog",
27
-
28
- # New specialized templates
29
- "insta": "Instagram Reels script",
30
- "thread": "SNS Thread post",
31
- "shortform": "60-sec Short-form video",
32
- "youtube": "YouTube script",
33
- }
34
-
35
-
36
- # ───────── Blog tone definitions ─────────
37
- BLOG_TONES = {
38
- "professional": "Professional and formal tone",
39
- "casual": "Friendly and conversational tone",
40
- "humorous": "Humorous approach",
41
- "storytelling": "Story-driven approach",
42
- }
43
-
44
- # Example blog topics
45
- EXAMPLE_TOPICS = {
46
- "example1": "Changes to the real estate tax system in 2025: Impact on average households and tax-saving strategies",
47
- "example2": "Summer festivals in 2025: A comprehensive guide to major regional events and hidden attractions",
48
- "example3": "Emerging industries to watch in 2025: An investment guide focused on AI opportunities"
49
- }
50
-
51
- # ──────────────────────────────── Logging ────────────────────────────────
52
- logging.basicConfig(level=logging.INFO,
53
- format="%(asctime)s - %(levelname)s - %(message)s")
54
-
55
- # ──────────────────────────────── OpenAI Client ──────────────────────────
56
-
57
- # OpenAI ν΄λΌμ΄μ–ΈνŠΈμ— νƒ€μž„μ•„μ›ƒκ³Ό μž¬μ‹œλ„ 둜직 μΆ”κ°€
58
- @st.cache_resource
59
- def get_openai_client():
60
- """Create an OpenAI client with timeout and retry settings."""
61
- if not OPENAI_API_KEY:
62
- raise RuntimeError("⚠️ OPENAI_API_KEY ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
63
- return OpenAI(
64
- api_key=OPENAI_API_KEY,
65
- timeout=60.0, # νƒ€μž„μ•„μ›ƒ 60초둜 μ„€μ •
66
- max_retries=3 # μž¬μ‹œλ„ 횟수 3회둜 μ„€μ •
67
- )
68
-
69
- # ──────────────────────────────── Blog Creation System Prompt ─────────────
70
- def get_system_prompt(template="ginigen", tone="professional", word_count=1750, include_search_results=False, include_uploaded_files=False) -> str:
71
- """
72
- Generate a system prompt that includes:
73
- - The 8-step blog writing framework
74
- - The selected template and tone
75
- - Guidelines for using web search results and uploaded files
76
- """
77
-
78
- # Ginigen recommended style prompt (English version)
79
- ginigen_prompt = """
80
- 당신은 λ›°μ–΄λ‚œ ν•œκ΅­μ–΄ SEO μΉ΄ν”ΌλΌμ΄ν„°μž…λ‹ˆλ‹€.
81
-
82
- β—† λͺ©μ 
83
-
84
- 'Blog Template'의 선택에 따라 λΈ”λ‘œκ·Έ λ˜λŠ” 릴슀, μ“°λ ˆλ“œ, 유튜브 κ΄€λ ¨ μ „λ¬Έ 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
85
- 항상 **[핡심뢀터 μ œμ‹œ β†’ κ°„κ²°β€§λͺ…λ£Œν•˜κ²Œ β†’ λ…μž ν˜œνƒ κ°•μ‘° β†’ 행동 μœ λ„]**의 4원칙을 λ”°λ₯΄μ„Έμš”.
86
-
87
- β—† μ™„μ„± ν˜•μ‹ (Markdown μ‚¬μš©, λΆˆν•„μš”ν•œ μ„€λͺ… κΈˆμ§€)
88
-
89
- 제λͺ©
90
- 이λͺ¨μ§€ + ꢁ금증 질문/감탄사 + 핡심 ν‚€μ›Œλ“œ (70자 이내)
91
- μ˜ˆμ‹œ: # 🧬 μ—Όμ¦λ§Œ 쀄여도 살이 λΉ μ§„λ‹€?! ν€˜λ₯΄μ„Έν‹΄ 5κ°€μ§€ λ†€λΌμš΄ 효λŠ₯
92
- Hook (2~3쀄)
93
-
94
- 문제 μ œμ‹œ β†’ ν•΄κ²° ν‚€μ›Œλ“œ μ–ΈκΈ‰ β†’ 이 글을 읽어야 ν•˜λŠ” 이유 μš”μ•½
95
-
96
- --- ꡬ뢄선
97
-
98
- μ„Ήμ…˜ 1: 핡심 κ°œλ… μ†Œκ°œ
99
-
100
- ## 🍏 [ν‚€μ›Œλ“œ]λž€ 무엇인가?
101
-
102
- 1~2문단 μ •μ˜ + πŸ“Œ ν•œμ€„ μš”μ•½
103
-
104
- ---
105
-
106
- μ„Ήμ…˜ 2: 5κ°€μ§€ 이점/이유
107
-
108
- ## πŸ’ͺ [ν‚€μ›Œλ“œ]κ°€ μœ μ΅ν•œ 5κ°€μ§€ 이유
109
-
110
- 각 μ†Œμ œλͺ© ν˜•μ‹:
111
-
112
- 1. [ν‚€μ›Œλ“œ 쀑심 μ†Œμ œλͺ©]
113
- 1~2문단 μ„€λͺ…
114
-
115
- βœ” 핡심 포인트 ν•œμ€„ κ°•μ‘°
116
-
117
- 총 5개 ν•­λͺ©
118
-
119
- μ„Ήμ…˜ 3: μ„­μ·¨/ν™œμš© 방법
120
-
121
- ## πŸ₯— [ν‚€μ›Œλ“œ] μ œλŒ€λ‘œ ν™œμš©ν•˜λŠ” 법!
122
-
123
- 이λͺ¨μ§€ 뢈릿 5개 정도 + μΆ”κ°€ 팁
124
-
125
- ---
126
-
127
- 마무리 행동 μœ λ„
128
-
129
- ## πŸ“Œ κ²°λ‘  – μ§€κΈˆ λ°”λ‘œ [οΏ½οΏ½μ›Œλ“œ] μ‹œμž‘ν•˜μ„Έμš”!
130
-
131
- 2~3λ¬Έμž₯으둜 ν˜œνƒ/λ³€ν™”λ₯Ό μš”μ•½ β†’ 행동 촉ꡬ (ꡬ맀, ꡬ독, 곡유 λ“±)
132
-
133
- ---
134
-
135
- 핡심 μš”μ•½ ν‘œ
136
-
137
- ν•­λͺ© 효과
138
- [ν‚€μ›Œλ“œ] [효과 μš”μ•½]
139
- μ£Όμš” μŒμ‹/μ œν’ˆ [λͺ©λ‘]
140
-
141
- ---
142
-
143
- ν€΄μ¦ˆ & CTA
144
-
145
- κ°„λ‹¨ν•œ Q&A ν€΄μ¦ˆ (1λ¬Έν•­) β†’ μ •λ‹΅ 곡개
146
-
147
- β€œλ„μ›€μ΄ λ˜μ…¨λ‹€λ©΄ 곡유/λŒ“κΈ€ λΆ€νƒλ“œλ¦½λ‹ˆλ‹€β€ 문ꡬ
148
-
149
- λ‹€μŒ κΈ€ 예고
150
-
151
- β—† μΆ”κ°€ μ§€μΉ¨
152
-
153
- 전체 λΆ„λŸ‰ 1,200~1,800단어.
154
-
155
- μ‰¬μš΄ μ–΄νœ˜Β·μ§§μ€ λ¬Έμž₯ μ‚¬μš©, 이λͺ¨μ§€Β·κ΅΅μ€ κΈ€μ”¨Β·μΈμš©μœΌλ‘œ 가독성 κ°•ν™”.
156
-
157
- ꡬ체적 수치, 연ꡬ κ²°κ³Ό, λΉ„μœ λ‘œ 신뒰도 ↑.
158
-
159
- β€œν”„λ‘¬ν”„νŠΈβ€, β€œμ§€μ‹œμ‚¬ν•­β€ λ“± 메타 μ–ΈκΈ‰ κΈˆμ§€.
160
-
161
- λŒ€ν™”μ²΄μ΄λ©΄μ„œλ„ 전문성을 μœ μ§€.
162
-
163
- μ™ΈλΆ€ μΆœμ²˜κ°€ μ—†λ‹€λ©΄ β€œμ—°κ΅¬μ— λ”°λ₯΄λ©΄β€ 같은 ν‘œν˜„ μ΅œμ†Œν™”.
164
-
165
- β—† 좜λ ₯
166
-
167
- μœ„ ν˜•μ‹μ„ λ”°λ₯Έ μ™„μ„± λΈ”λ‘œκ·Έ κΈ€λ§Œ λ°˜ν™˜ν•˜μ„Έμš”. μΆ”κ°€ μ„€λͺ…은 ν¬ν•¨ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
168
-
169
- """
170
-
171
- # Standard 8-step framework (English version)
172
- base_prompt = """
173
- λ‹€μŒμ€ 전문적인 글을 μž‘μ„±ν•  λ•Œ λ°˜λ“œμ‹œ 따라야 ν•  8단계 ν”„λ ˆμž„μ›Œν¬μž…λ‹ˆλ‹€. 각 λ‹¨κ³„μ˜ μ„ΈλΆ€ ν•­λͺ©μ„ μΆ©μ‹€νžˆ λ°˜μ˜ν•˜μ—¬ 일관성 있고 ν₯미둜운 글을을 μ™„μ„±ν•˜μ„Έμš”.
174
-
175
- λ…μž μ—°κ²° 단계
176
- 1.1. μΉœκ·Όν•œ μΈμ‚¬λ‘œ 라포 ν˜•μ„±
177
- 1.2. λ„μž… 질문으둜 λ…μžμ˜ μ‹€μ œ κ³ λ―Ό 반영
178
- 1.3. μ£Όμ œμ— λŒ€ν•œ 즉각적인 ν₯λ―Έ 유발
179
-
180
- 문제 μ •μ˜ 단계
181
- 2.1. λ…μžκ°€ κ²ͺλŠ” 고좩을 ꡬ체적으둜 κ·œμ •
182
- 2.2. 문제의 μ‹œκΈ‰μ„±Β·μ˜ν–₯λ ₯ 뢄석
183
- 2.3. ν•΄κ²° ν•„μš”μ„±μ— λŒ€ν•œ κ³΅κ°λŒ€ ν˜•μ„±
184
-
185
- μ „λ¬Έμ„± 확립 단계
186
- 3.1. 객관적 데이터λ₯Ό 기반으둜 뢄석
187
- 3.2. μ „λ¬Έκ°€ μ˜κ²¬Β·μ—°κ΅¬ κ²°κ³Ό 인용
188
- 3.3. μ‹€μƒν™œ μ‚¬λ‘€λ‘œ 이해도 κ°•ν™”
189
-
190
- ν•΄κ²°μ±… μ œμ‹œ 단계
191
- 4.1. 단계별 κ°€μ΄λ“œ 제곡
192
- 4.2. μ¦‰μ‹œ 적용 κ°€λŠ₯ν•œ μ‹€μš© 팁 μ œμ•ˆ
193
- 4.3. μ˜ˆμƒ μž₯μ• λ¬Ό 및 극볡 방법 μ–ΈκΈ‰
194
-
195
- μ‹ λ’° ꡬ좕 단계
196
- 5.1. μ‹€μ œ 성곡 사둀 μ œμ‹œ
197
- 5.2. μ‚¬μš©μž ν›„κΈ° 인용
198
- 5.3. 효과λ₯Ό μž…μ¦ν•˜λŠ” 객관적 데이터 ν™œμš©
199
-
200
- 행동 μœ λ„ 단계
201
- 6.1. λ…μžκ°€ λ‹Ήμž₯ μ‹€ν–‰ν•  수 μžˆλŠ” 첫걸음 μ œμ•ˆ
202
- 6.2. 긴박감을 κ°•μ‘°ν•˜μ—¬ μ‹ μ†ν•œ 행동 촉ꡬ
203
- 6.3. ν˜œνƒΒ·λ³΄μƒμ„ κ°•μ‘°ν•΄ 동기 λΆ€μ—¬
204
-
205
- μ§„μ •μ„± 단계
206
- 7.1. ν•΄κ²°μ±…μ˜ ν•œκ³„μ  투λͺ…ν•˜κ²Œ 곡개
207
- 7.2. κ°œμΈλ³„ 차이가 μžˆμ„ 수 μžˆμŒμ„ 인정
208
- 7.3. μ „μ œ 쑰건·주의 사항 λͺ…μ‹œ
209
-
210
- 관계 지속 단계
211
- 8.1. 진심 μ–΄λ¦° 감사 μΈμ‚¬λ‘œ 마무리
212
- 8.2. λ‹€μŒ μ½˜ν…μΈ  예고둜 κΈ°λŒ€κ° μ‘°μ„±
213
- 8.3. μΆ”κ°€ μ†Œν†΅ 채널 μ•ˆλ‚΄
214
- """
215
- template_guides = {
216
- "insta": """
217
-
218
- λ„ˆλŠ” μΈμŠ€νƒ€κ·Έλž¨ 릴슀 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
219
- λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
220
- 당신은 **γ€ˆUniversal Reels Strategist GPT〉**λ‹€.
221
- λͺ©ν‘œ: μ‚¬μš©μžκ°€ μ œμ‹œν•œ μ£Όμ œΒ·μ œν’ˆΒ·μ„œλΉ„μŠ€λ₯Ό λ°”νƒ•μœΌλ‘œ μ €μž₯β€§κ³΅μœ β€§ν–‰λ™μ„ μœ λ„ν•˜λŠ” 60초 μ΄ν•˜ 숏폼 μ˜μƒμ„ **ν•œ λ²ˆμ— μ™„μ„±**ν•΄ μ£ΌλŠ” 것.
222
-
223
- ────────────── κΈ°λ³Έ 원칙 ──────────────
224
- 1. **λ³ΈλŠ₯ 4λŒ€ μš•κ΅¬ μ—°κ²°**
225
- β‘  λˆΒ·μ‹œκ°„ μ ˆμ•½(생쑴)
226
- β‘‘ 건강·아름닀움(생쑴+미적 만쑱)
227
- β‘’ μΈκ°„κ΄€κ³„Β·μ‚¬λž‘Β·μ‚¬νšŒμ  인정
228
- β‘£ 문제 ν•΄κ²°Β·μ„±μž₯(λŠ₯λ ₯·지식 ν–₯상)
229
- β†’ μ΅œμ†Œ 1개 이상과 μ‚¬μš©μž 주제λ₯Ό 연결해라.
230
-
231
- 2. **ν‘œλ³Έ 이둠(λŒ€μ€‘ν™” ν™•μž₯)**
232
- β€’ μ£Όμ œκ°€ 쒁으면 β€˜λˆ„κ΅¬μ—κ²Œλ‚˜ 적용 κ°€λŠ₯ν•œ μ‹€μ΅β€™μœΌλ‘œ λ„“ν˜€λΌ.
233
- 예) μ§€λ°© μ†Œν˜• ν—¬μŠ€μž₯ 홍보 β†’ β€œν•˜λ£¨ 5λΆ„ 뱃살 νƒœμš°λŠ” ν™ˆνŠΈβ€.
234
-
235
- 3. **6단계 μ œμž‘ ν”„λ‘œμ„ΈμŠ€**
236
- β‘  레퍼런슀·경쟁 사둀 뢄석
237
- β‘‘ μ£Όμ œΒ·ν¬μ§€μ…”λ‹ ν™•μ •(ν‘œλ³Έ ν™•μž₯ 포함)
238
- β‘’ ν›„ν‚Ή+μ‹œν€€μŠ€ 슀크립트 μž‘μ„± (μ•„λž˜ 아웃풋 ν˜•μ‹ μ‚¬μš©)
239
- β‘£ μ΄¬μ˜Β·νŽΈμ§‘ κ°€μ΄λ“œ(ν•„μš” μž₯비·ꡬ도·BGM λ“±)
240
- β‘€ μΉ΄ν”Ό 보완(제λͺ©Β·λ³Έλ¬ΈΒ·μΊ‘μ…˜)
241
- β‘₯ 행동 μœ λ„ 문ꡬ(CTA) μ‚½μž…
242
-
243
- 4. **ν›„ν‚Ή 3초 κ·œμΉ™**
244
- β€’ μ‹œμž‘ 3초 μ•ˆμ— **λ…Όλž€, ν˜ΈκΈ°μ‹¬, μˆ˜μΉ˜ν™”λœ 이득** 쀑 ν•˜λ‚˜λ₯Ό 폭발적으둜 μ œμ‹œ.
245
- β€’ 숫자·ꡬ체 λ‹¨μ–΄Β·κ°•ν•œ 동사 μ‚¬μš©. (예: β€œ7일 λ§Œμ— 맀좜 두 λ°°?”)
246
-
247
- 5. **CTA ν•„μˆ˜**
248
- β€’ μ €μž₯, 곡유, λŒ“κΈ€, ꡬ맀, μ‹ μ²­, μ˜ˆμ•½ λ“± μ΅œμ†Œ 1개λ₯Ό λͺ…μ‹œμ  λ¬Έμž₯으둜 μš”κ΅¬.
249
-
250
- 6. **ν†€β€§μŠ€νƒ€μΌ**
251
- β€’ 친ꡬ처럼 직섀·간결.
252
- β€’ λΆˆν•„μš”ν•œ 이λͺ¨μ§€Β·νŠΉμˆ˜λ¬Έμž κΈˆμ§€(β€˜!’ β€˜?’ 만 ν—ˆμš©).
253
- β€’ ν•œκ΅­μ–΄κ°€ κΈ°λ³Έμ΄μ§€λ§Œ, μ‚¬μš©μžκ°€ μ˜μ–΄λ‘œ μš”μ²­ν•˜λ©΄ 동일 κ·œμΉ™μ„ μ˜μ–΄λ‘œ 제곡.
254
-
255
- 7. **정보 μˆ˜μ§‘**
256
- β€’ μ—…μ’…Β·νƒ€κΉƒΒ·μ „ν™˜ λͺ©ν‘œΒ·μ˜ˆμ‚°Β·μ΄¬μ˜ κ°€λŠ₯ μž₯λΉ„κ°€ 뢈λͺ…ν™•ν•˜λ©΄ **ν•œ λ²ˆμ— λ¬Άμ–΄** λ¬Όμ–΄λ³Έλ‹€.
257
-
258
- 8. **좜λ ₯ ν˜•μ‹** (λͺ¨λ“  ν•­λͺ©μ€ 1~2쀄 λ‚΄μ™Έ, 번호 κ·ΈλŒ€λ‘œ μœ μ§€)
259
- 1) 제λͺ©(20자 μ΄ν•˜)
260
- 2) ν›„ν‚Ή λŒ€μ‚¬(첫 3초)
261
- 3) μ‹œν€€μŠ€ 슀크립트(μž₯면별 핡심 λŒ€μ‚¬Β·μžλ§‰)
262
- 4) 핡심 λ©”μ‹œμ§€ μš”μ•½
263
- 5) CTA 문ꡬ
264
- 6) μΊ‘μ…˜ μ˜ˆμ‹œ(이득→곡감→행동, 3λ¬Έμž₯)
265
- 7) ν•΄μ‹œνƒœκ·Έ(μ‰Όν‘œλ‘œ ꡬ뢄, 특수문자 μ œμ™Έ)
266
- 8) μ΄¬μ˜Β·νŽΈμ§‘ 팁(ν•„μš”μ‹œ)
267
-
268
- 9. **검증 체크리슀트**
269
- β€’ λ³ΈλŠ₯ 자극 포인트 쑴재?
270
- β€’ ν›„ν‚Ή 3초 κ·œμΉ™ μΆ©μ‘±?
271
- β€’ CTA 포함? β†’ ν•˜λ‚˜λΌλ„ β€˜μ•„λ‹ˆμ˜€β€™λ©΄ 슀슀둜 μˆ˜μ • ν›„ 좜λ ₯.
272
-
273
- ───────── μ˜ˆμ‹œ μž…λ ₯ & μš”μ•½ 좜λ ₯ ─────────
274
- μ‚¬μš©μž: β€œμ£Όμ œ: 1인 세무사 사무싀 μ‹ κ·œ 고객 확보”
275
- GPT 좜λ ₯(μš”μ•½):
276
- 1) 제λͺ©: μ„Έλ¬΄λΉ„μš© 30% μ€„μ΄λŠ” 법
277
- 2) ν›„ν‚Ή: β€œ10λΆ„ μ „ν™”λ‘œ μ„ΈκΈˆ 300만 원 μ•„κΌˆμ–΄μš”?”
278
- 3) μ‹œν€€μŠ€: μž₯λ©΄1 μ„ΈκΈˆκ³ μ§€μ„œ 쇼크 β†’ β€œβ‘  λΆˆν•„μš” 곡제 찾기” …
279
- … μ΄ν•˜ ν˜•μ‹ 동일
280
- ────────────────────────────
281
-
282
- **λͺ¨λ“  닡변은 μœ„ κ·œμΉ™μ„ μ–΄κΈ°λ©΄ μžλ™μœΌλ‘œ μž¬κ²€ν† ν•˜κ³  μˆ˜μ •ν•˜λΌ.**
283
-
284
- 데이터가 μ—†λŠ”κ²ƒμ€ μ›Ήκ²€μƒ‰μœΌλ‘œ 정보λ₯Ό μ„œμΉ˜ν•΄μ„œ μ°Ύμ•„λ‚΄μ•Ό ν•œλ‹€.
285
- """,
286
-
287
- "thread": """
288
-
289
- λ„ˆλŠ” μ“°λ ˆλ“œ 포슀트 생성 μ „λ¬Έκ°€ 역할이닀 :
290
- λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
291
- You are a Korean tech‑savvy copywriter who writes short, hype‑driven SNS thread posts.
292
-
293
- When given a {product_name} and its {key_highlights}, output a thread in the following style:
294
-
295
- [1] μ‹œμž‘
296
- – ν•œ 쀄 ν›…: πŸ”₯ 같은 이λͺ¨μ§€ + 타깃 λ…μž μ†Œν™˜ + 짧은 감탄
297
- – 두 번째 쀄: β€œ{product_name}κ°€/이 μ§„μ§œ 일 λƒˆλ‹€β€Β λ˜λŠ” λ™λ“±ν•œ μž„νŒ©νŠΈ λ¬Έμž₯
298
-
299
- [2] μ •μ˜ & λ§₯락
300
- – β€œ{unique_point}? 그게 뭐야?” 식 질문
301
- – 1~2λ¬Έμž₯으둜 κ°œλ… μ„€λͺ…, 세계적 μ‚¬λ‘€Β·λ ˆνΌλŸ°μŠ€ ν•œ 쀄
302
-
303
- [3] numbered 핡심 포인트
304
- – 각 ν¬μΈνŠΈλŠ” β€œ{번호}/ {μ†Œμ œλͺ©}” ν˜•μ‹
305
- – 이후 1~3μ€„λ‘œ {μ†Œμ œλͺ©}λ₯Ό 상세 μ„€λͺ…
306
- – μ„€λͺ…은 ꡬ어체, λ¬Έμž₯ 짧게, β€˜!’ ν™œμš©
307
- – ꡬ체 μ˜ˆμ‹œΒ·λΉ„κ΅Β·λ°μ΄ν„°λ₯Ό ν¬ν•¨ν•˜λ˜ ν•œ 문단 ≀3쀄
308
- – μ΅œμ†Œ 3개, μ΅œλŒ€ 6개 포인트
309
-
310
- [4] κ²°λ‘ 
311
- – β€œ{λ§ˆμ§€λ§‰λ²ˆν˜Έ+1}/ κ²°λ‘  : …” ν˜•μ‹
312
- – 문제 ν•΄κ²°Β·κ°€μΉ˜ μš”μ•½
313
- – β€˜β€˜μ΄μ œ {call_to_action}’’ 식 직접 행동 μœ λ„
314
-
315
- μŠ€νƒ€μΌ κ·œμΉ™:
316
- - ν•œκ΅­μ–΄ μœ„μ£Ό, ν•„μš” μ‹œ μ˜μ–΄ κΈ°μˆ μš©μ–΄ κ·ΈλŒ€λ‘œ μ‚½μž…
317
- - λ¬Έμž₯λ§ˆλ‹€ μ—”ν„°, 블둝 단락 ꡬ뢄
318
- - νŠΉμˆ˜λ¬ΈμžλŠ” β€˜!’ β€˜?’ μ™Έ μ΅œμ†Œν™”
319
- - 전체 길이 250~400자
320
- - 이λͺ¨μ§€λŠ” 제λͺ©Β·μ€‘μš” ν¬μΈνŠΈμ—λ§Œ 1~3개 μ‚¬μš©
321
- - μ‘΄λŒ“λ§ λŒ€μ‹  μΉœκ·Όν•œ 반말
322
- """,
323
-
324
-
325
- "shortform": """
326
- λ„ˆλŠ” 숏폼 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
327
- λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
328
- ### πŸŽ›οΈ GPTS μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈβ€Šβ€”β€Š1λΆ„ 숏폼 μ˜μƒ λŒ€λ³Έ μž‘μ„±κΈ°
329
-
330
- λ„ˆλŠ” **β€œ1 λΆ„ 숏폼 μ˜μƒ λŒ€λ³Έ μžλ™ν™” AI”**λ‹€.
331
- μ‚¬μš©μžκ°€ μ£Όμ œΒ·μ œν’ˆΒ·μ„œλΉ„μŠ€Β·νƒ€κΉƒ μ‹œμ²­μžΒ·ν†€(선택)을 μž…λ ₯ν•˜λ©΄, μ•„λž˜ 포맷을 **ν•œκ΅­μ–΄**둜 μ™„μ„±λœ λŒ€λ³ΈμœΌλ‘œ 좜λ ₯ν•œλ‹€.
332
- - 총 κΈΈμ΄λŠ” **60 초 이내**.
333
- - 각 ꡬ간은 **νƒ€μž„μ½”λ“œ(초)**와 **ꡬ간λͺ…**을 λŒ€κ΄„ν˜Έλ‘œ ν‘œκΈ°.
334
- - λ¬Έμž₯은 μ§§κ³  μž„νŒ©νŠΈ 있게, 1 λ¬Έμž₯ β‰ˆ 1.5 초 κΈ°μ€€.
335
- - 이λͺ¨μ§€ μ‚¬μš©μ€ μžμœ μ§€λ§Œ κ³Όλ„ν•˜μ§€ μ•Šκ²Œ(0–2개).
336
- - νŠΉμˆ˜λ¬ΈμžλŠ” β€˜!’와 β€˜?β€™λ§Œ ν—ˆμš©.
337
-
338
- 🟑 **좜λ ₯ 포맷**
339
-
340
- [0-3초 | Hook]
341
- {μ‹œμ²­μž μŠ€ν¬λ‘€μ„ 멈좜 ν•œλ§ˆλ””}
342
-
343
- [4-15초 | Problem]
344
- {μ‹œμ²­μž 곡감 포인트λ₯Ό μ •ν™•νžˆ 짚기}
345
-
346
- [16-30초 | Solution]
347
- {μ œν’ˆ/μ„œλΉ„μŠ€/아이디어 μ†Œκ°œ + 핡심 κΈ°λŠ₯}
348
-
349
- [31-45초 | Proof]
350
- {효과 증λͺ…·데이터·후기 + 경쟁 μ œν’ˆκ³Ό 차별점}
351
-
352
- [46-55초 | Callback/Emotion]
353
- {Hookλ₯Ό μžμ—°μŠ€λŸ½κ²Œ νšŒμˆ˜ν•˜κ±°λ‚˜ 감정 자극}
354
-
355
- [56-60초 | CTA]
356
- {κ΅¬λ§€Β·ν΄λ¦­Β·νŒ”λ‘œμš° λ“± λͺ…ν™•ν•œ 행동 μœ λ„}
357
-
358
- 🟑 **μž‘μ„± κ·œμΉ™**
359
-
360
- 1. **Hook** – λ†€λΌμ›€Β·κΆκΈˆμ¦Β·κ³΅κ° 쀑 ν•˜λ‚˜λ‘œ κ°•λ ¬ν•œ ν•œ λ¬Έμž₯.
361
- 2. **Problem** – λŒ€μƒ μ‹œμ²­μžμ˜ λΆˆνŽΈΒ·κ³ λ―Όμ„ ꡬ체적으둜 μ–ΈκΈ‰.
362
- 3. **Solution** – μ œν’ˆΒ·μ„œλΉ„μŠ€λ‘œ 문제 ν•΄κ²°, 핡심 κΈ°λŠ₯을 μ‰¬μš΄ ν‘œν˜„μœΌλ‘œ.
363
- 4. **Proof** – μˆ˜μΉ˜Β·ν›„κΈ°Β·μ „λ¬Έκ°€ μ–ΈκΈ‰ λ“± μ‹ λ’° μš”μ†Œ 1-2개 + 차별점.
364
- 5. **Callback/Emotion** – 훅을 λ³€μ£Όν•˜κ±°λ‚˜ 희망·긴급 감정 자극.
365
- 6. **CTA** – ꡬ체적 행동 + ν•œμ •μ„±Β·κΈ΄κΈ‰μ„± μ–Έμ–΄.
366
-
367
- 🟑 **ν”„λ‘¬ν”„νŠΈ μž…λ ₯ μ˜ˆμ‹œ**
368
-
369
- 주제: 슀마트 무선 μ²­μ†ŒκΈ°
370
- 톀: μΉœκ·Όν•˜κ³  유머러슀
371
-
372
-
373
- -μ‚¬μš©μžμ˜ μ˜μƒ λͺ©μ (예:μ œν’ˆ 홍보, μ‚¬μš©λ²• μ•ˆλ‚΄, μœ μš©μ„± μ„€λͺ… λ“±)κ³Ό 타깃 μ‹œμ²­μž 그리고 μ‹œμ²­μžμ—κ²Œ μ „λ‹¬ν•˜κ³  싢은 μ£Όμš” λ©”μ‹œμ§€μ— λŒ€ν•œ 정보λ₯Ό λ°›μ„μˆ˜ μžˆμ–΄μ•Όν•΄ λ‹΅λ³€ μ˜ˆμ‹œλ„ ν•¨κ»˜ 보여주고
374
-
375
- -μ΅œλŒ€ 4개의 이λͺ¨μ§€λ₯Ό μ‚¬μš©ν•΄μ€˜
376
-
377
- -μ‹œλ‚˜λ¦¬μ˜€λŠ” μ˜μƒκ³Ό λŒ€λ³Έμ„ κ΅¬λΆ„ν• μˆ˜ 있게 좜λ ₯ν•΄μ€˜
378
-
379
- -κ²°κ³Όλ¬Ό 좜λ ₯μ‹œ ν•˜λ‹¨μ— λ”°λ‘œ 이미지도 ν•¨κ»˜ μƒμ„±ν•΄μ€˜ κ΄€λ ¨ λ°°κ²½μ΄λ―Έμ§€λ‘œ μƒμ„±ν•˜λ˜ μ œν’ˆμ€ μƒμ„±ν•˜μ§€λ§κ²ƒ. 그리고 μΊ‘μ…˜/μΉ΄ν”ΌλΌμ΄νŒ…λ“± ν…μŠ€νŠΈλ₯Ό 이미지 μ•ˆμ— μ ˆλŒ€ 생성 ν•˜μ§€λ§ˆ
380
- """,
381
-
382
- "youtube": """
383
- λ„ˆλŠ” 유튜브 슀크립트(λŒ€λ³Έ) 생성 μ „λ¬Έκ°€ 역할이닀 :
384
- λΈ”λ‘œκ·Έ μŠ€νƒ€μΌλ‘œ μƒμ„±ν•˜μ§€ 말고, λ„ˆλŠ” λ‹€μŒ μ§€μΉ¨λ§Œμ„ 따라 글을 μž‘μ„±ν•˜μ—¬μ•Ό ν•œλ‹€.
385
- """
386
- }
387
-
388
- # μ–΄μ‘°(톀)별 μΆ”κ°€ μ§€μΉ¨
389
- tone_guides = {
390
- "professional": "전문적이고 κΆŒμœ„ μžˆλŠ” 문체λ₯Ό μ‚¬μš©ν•©λ‹ˆλ‹€. 기술 μš©μ–΄λ₯Ό λͺ…ν™•νžˆ μ„€λͺ…ν•˜κ³ , λ°μ΄ν„°λ‚˜ 연ꡬ κ²°κ³Όλ₯Ό μ œμ‹œν•˜μ—¬ 논리적 흐름을 μœ μ§€ν•˜μ„Έμš”.",
391
- "casual": "νŽΈμ•ˆν•˜κ³  λŒ€ν™”μ²΄μ— κ°€κΉŒμš΄ μŠ€νƒ€μΌμ„ μ‚¬μš©ν•©λ‹ˆλ‹€. 개인 κ²½ν—˜Β·κ³΅κ° κ°€λŠ” μ˜ˆμ‹œλ₯Ό λ“€κ³ , μΉœκ·Όν•œ μ–΄μ‘°(예: '정말 μœ μš©ν•΄μš”!')λ₯Ό ν™œμš©ν•˜μ„Έμš”.",
392
- "humorous": "μœ λ¨Έμ™€ 재치 μžˆλŠ” ν‘œν˜„μ„ μ‚¬μš©ν•©λ‹ˆλ‹€. μž¬λ―ΈμžˆλŠ” λΉ„μœ λ‚˜ 농담을 μΆ”κ°€ν•˜λ˜, μ •ν™•μ„±κ³Ό μœ μš©μ„±μ„ μœ μ§€ν•˜μ„Έμš”.",
393
- "storytelling": "이야기λ₯Ό λ“€λ €μ£Όλ“― μ„œμˆ ν•©λ‹ˆλ‹€. 감정 κΉŠμ΄μ™€ μ„œμ‚¬μ  흐름을 μœ μ§€ν•˜κ³ , μΈλ¬ΌΒ·λ°°κ²½Β·κ°ˆλ“±Β·ν•΄κ²°μ„ λ…Ήμ—¬λ‚΄μ„Έμš”."
394
- }
395
-
396
- # μ›Ή 검색 κ²°κ³Ό μ‚¬μš© μ§€μΉ¨
397
- search_guide = """
398
- [μ›Ή 검색 κ²°κ³Ό ν™œμš© κ°€μ΄λ“œ]
399
- - 검색 결과의 핡심 정보λ₯Ό λΈ”λ‘œκ·Έμ— μ •ν™•νžˆ ν†΅ν•©ν•˜μ„Έμš”.
400
- - μ΅œμ‹  데이터, 톡계, 사둀λ₯Ό ν¬ν•¨ν•˜μ„Έμš”.
401
- - 인용 μ‹œ λ³Έλ¬Έμ—μ„œ 좜처λ₯Ό λͺ…ν™•νžˆ ν‘œκΈ°ν•˜μ„Έμš” (예: "XYZ μ›Ήμ‚¬μ΄νŠΈμ— λ”°λ₯΄λ©΄ …").
402
- - κΈ€ λ§ˆμ§€λ§‰μ— 'μ°Έκ³  자료' μ„Ήμ…˜μ„ 두고 μ£Όμš” μΆœμ²˜μ™€ 링크λ₯Ό λ‚˜μ—΄ν•˜μ„Έμš”.
403
- - μƒλ°˜λ˜λŠ” 정보가 μžˆλ‹€λ©΄ λ‹€μ–‘ν•œ 관점을 ν•¨κ»˜ μ œμ‹œν•˜μ„Έμš”.
404
- - μ΅œμ‹  νŠΈλ Œλ“œμ™€ 데이터λ₯Ό λ°˜λ“œμ‹œ λ°˜μ˜ν•˜μ„Έμš”.
405
- """
406
-
407
- # μ—…λ‘œλ“œ 파일 μ‚¬μš© μ§€μΉ¨
408
- upload_guide = """
409
- [μ—…λ‘œλ“œλœ 파일 ν™œμš© μ§€μΉ¨ (μ΅œμš°μ„ )]
410
- - μ—…λ‘œλ“œλœ νŒŒμΌμ€ λΈ”λ‘œκ·Έμ˜ 핡심 정보원이어야 ν•©λ‹ˆλ‹€.
411
- - 파일 속 λ°μ΄ν„°Β·ν†΅κ³„Β·μ˜ˆμ‹œλ₯Ό λ©΄λ°€νžˆ κ²€ν† ν•΄ ν†΅ν•©ν•˜μ„Έμš”.
412
- - μ£Όμš” 수치·주μž₯은 직접 μΈμš©ν•˜κ³  μΆ©λΆ„νžˆ μ„€λͺ…ν•˜μ„Έμš”.
413
- - 파일 λ‚΄μš©μ„ λΈ”λ‘œκ·Έμ˜ 핡심 μš”μ†Œλ‘œ κ°•μ‘°ν•˜μ„Έμš”.
414
- - 좜처λ₯Ό λͺ…ν™•νžˆ ν‘œκΈ°ν•˜μ„Έμš” (예: "μ—…λ‘œλ“œλœ 데이터에 λ”°λ₯΄λ©΄ …").
415
- - CSV νŒŒμΌμ€ μ€‘μš”ν•œ μˆ˜μΉ˜λ‚˜ 톡계λ₯Ό μƒμ„Ένžˆ λ‹€λ£¨μ„Έμš”.
416
- - PDF νŒŒμΌμ€ 핡심 λ¬Έμž₯μ΄λ‚˜ μ§„μˆ μ„ μΈμš©ν•˜μ„Έμš”.
417
- - ν…μŠ€νŠΈ 파일의 κ΄€λ ¨ λ‚΄μš©μ„ 효과적으둜 ν†΅ν•©ν•˜μ„Έμš”.
418
- - 파일 λ‚΄μš©μ΄ λ‹€μ†Œ λ²—μ–΄λ‚˜ 보여도 μ£Όμ œμ™€ 연결고리λ₯Ό μ°Ύμ•„ μ„œμˆ ν•˜μ„Έμš”.
419
- - κΈ€ μ „λ°˜μ— 걸쳐 μΌκ΄€λ˜κ²Œ 파일 데이터λ₯Ό λ°˜μ˜ν•˜μ„Έμš”.
420
- """
421
-
422
-
423
- # Choose base prompt
424
- if template == "ginigen":
425
- final_prompt = ginigen_prompt
426
- else:
427
- final_prompt = base_prompt
428
-
429
- # If the user chose a specific template (and not ginigen), append the relevant guidelines
430
- if template != "ginigen" and template in template_guides:
431
- final_prompt += "\n" + template_guides[template]
432
-
433
- # If a specific tone is selected, append that guideline
434
- if tone in tone_guides:
435
- final_prompt += f"\n\nTone and Manner: {tone_guides[tone]}"
436
-
437
- # If web search results should be included
438
- if include_search_results:
439
- final_prompt += f"\n\n{search_guide}"
440
-
441
- # If uploaded files should be included
442
- if include_uploaded_files:
443
- final_prompt += f"\n\n{upload_guide}"
444
-
445
- # Word count guidelines
446
- final_prompt += (
447
- f"\n\nWriting Requirements:\n"
448
- f"9.1. Word Count: around {word_count-250}-{word_count+250} characters\n"
449
- f"9.2. Paragraph Length: 3-4 sentences each\n"
450
- f"9.3. Visual Cues: Use subheadings, separators, and bullet/numbered lists\n"
451
- f"9.4. Data: Cite all sources\n"
452
- f"9.5. Readability: Use clear paragraph breaks and highlights where necessary"
453
- )
454
-
455
- return final_prompt
456
-
457
- # ──────────────────────────────── Brave Search API ────────────────────────
458
- @st.cache_data(ttl=3600)
459
- def brave_search(query: str, count: int = 20):
460
- """
461
- Call the Brave Web Search API β†’ list[dict]
462
- Returns fields: index, title, link, snippet, displayed_link
463
- """
464
- if not BRAVE_KEY:
465
- raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
466
-
467
- headers = {
468
- "Accept": "application/json",
469
- "Accept-Encoding": "gzip",
470
- "X-Subscription-Token": BRAVE_KEY
471
- }
472
- params = {"q": query, "count": str(count)}
473
-
474
- for attempt in range(3):
475
- try:
476
- r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
477
- r.raise_for_status()
478
- data = r.json()
479
-
480
- logging.info(f"Brave search result data structure: {list(data.keys())}")
481
-
482
- raw = data.get("web", {}).get("results") or data.get("results", [])
483
- if not raw:
484
- logging.warning(f"No Brave search results found. Response: {data}")
485
- raise ValueError("No search results found.")
486
-
487
- arts = []
488
- for i, res in enumerate(raw[:count], 1):
489
- url = res.get("url", res.get("link", ""))
490
- host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
491
- arts.append({
492
- "index": i,
493
- "title": res.get("title", "No title"),
494
- "link": url,
495
- "snippet": res.get("description", res.get("text", "No snippet")),
496
- "displayed_link": host
497
- })
498
-
499
- logging.info(f"Brave search success: {len(arts)} results")
500
- return arts
501
-
502
- except Exception as e:
503
- logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}")
504
- if attempt < 2:
505
- time.sleep(2)
506
-
507
- return []
508
-
509
- def mock_results(query: str) -> str:
510
- """Fallback search results if API fails"""
511
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
512
- return (f"# Fallback Search Content (Generated: {ts})\n\n"
513
- f"The search API request failed. Please generate the blog based on any pre-existing knowledge about '{query}'.\n\n"
514
- f"You may consider the following points:\n\n"
515
- f"- Basic concepts and importance of {query}\n"
516
- f"- Commonly known related statistics or trends\n"
517
- f"- Typical expert opinions on this subject\n"
518
- f"- Questions that readers might have\n\n"
519
- f"Note: This is fallback guidance, not real-time data.\n\n")
520
-
521
- def do_web_search(query: str) -> str:
522
- """Perform web search and format the results."""
523
- try:
524
- arts = brave_search(query, 20)
525
- if not arts:
526
- logging.warning("No search results, using fallback content")
527
- return mock_results(query)
528
-
529
- 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"
530
- body = "\n".join(
531
- f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
532
- f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
533
- for a in arts
534
- )
535
- return hdr + body
536
- except Exception as e:
537
- logging.error(f"Web search process failed: {str(e)}")
538
- return mock_results(query)
539
-
540
- # ──────────────────────────────── File Upload Handling ─────────────────────
541
- def process_text_file(file):
542
- """Handle text file"""
543
- try:
544
- content = file.read()
545
- file.seek(0)
546
-
547
- text = content.decode('utf-8', errors='ignore')
548
- if len(text) > 10000:
549
- text = text[:9700] + "...(truncated)..."
550
-
551
- result = f"## Text File: {file.name}\n\n"
552
- result += text
553
- return result
554
- except Exception as e:
555
- logging.error(f"Error processing text file: {str(e)}")
556
- return f"Error processing text file: {str(e)}"
557
-
558
- def process_csv_file(file):
559
- """Handle CSV file"""
560
- try:
561
- content = file.read()
562
- file.seek(0)
563
-
564
- df = pd.read_csv(io.BytesIO(content))
565
- result = f"## CSV File: {file.name}\n\n"
566
- result += f"- Rows: {len(df)}\n"
567
- result += f"- Columns: {len(df.columns)}\n"
568
- result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n"
569
-
570
- result += "### Data Preview\n\n"
571
- preview_df = df.head(10)
572
- try:
573
- markdown_table = preview_df.to_markdown(index=False)
574
- if markdown_table:
575
- result += markdown_table + "\n\n"
576
- else:
577
- result += "Unable to display CSV data.\n\n"
578
- except Exception as e:
579
- logging.error(f"Markdown table conversion error: {e}")
580
- result += "Displaying data as text:\n\n"
581
- result += str(preview_df) + "\n\n"
582
-
583
- num_cols = df.select_dtypes(include=['number']).columns
584
- if len(num_cols) > 0:
585
- result += "### Basic Statistical Information\n\n"
586
- try:
587
- stats_df = df[num_cols].describe().round(2)
588
- stats_markdown = stats_df.to_markdown()
589
- if stats_markdown:
590
- result += stats_markdown + "\n\n"
591
- else:
592
- result += "Unable to display statistical information.\n\n"
593
- except Exception as e:
594
- logging.error(f"Statistical info conversion error: {e}")
595
- result += "Unable to generate statistical information.\n\n"
596
-
597
- return result
598
- except Exception as e:
599
- logging.error(f"CSV file processing error: {str(e)}")
600
- return f"Error processing CSV file: {str(e)}"
601
-
602
- def process_pdf_file(file):
603
- """Handle PDF file"""
604
- try:
605
- # Read file in bytes
606
- file_bytes = file.read()
607
- file.seek(0)
608
-
609
- # Use PyPDF2
610
- pdf_file = io.BytesIO(file_bytes)
611
- reader = PyPDF2.PdfReader(pdf_file, strict=False)
612
-
613
- # Basic info
614
- result = f"## PDF File: {file.name}\n\n"
615
- result += f"- Total pages: {len(reader.pages)}\n\n"
616
-
617
- # Extract text by page (limit to first 5 pages)
618
- max_pages = min(5, len(reader.pages))
619
- all_text = ""
620
-
621
- for i in range(max_pages):
622
- try:
623
- page = reader.pages[i]
624
- page_text = page.extract_text()
625
-
626
- current_page_text = f"### Page {i+1}\n\n"
627
- if page_text and len(page_text.strip()) > 0:
628
- # Limit to 1500 characters per page
629
- if len(page_text) > 1500:
630
- current_page_text += page_text[:1500] + "...(truncated)...\n\n"
631
- else:
632
- current_page_text += page_text + "\n\n"
633
- else:
634
- current_page_text += "(No text could be extracted from this page)\n\n"
635
-
636
- all_text += current_page_text
637
-
638
- # If total text is too long, break
639
- if len(all_text) > 8000:
640
- all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
641
- break
642
-
643
- except Exception as page_err:
644
- logging.error(f"Error processing PDF page {i+1}: {str(page_err)}")
645
- all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n"
646
-
647
- if len(reader.pages) > max_pages:
648
- all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n"
649
-
650
- result += "### PDF Content\n\n" + all_text
651
- return result
652
-
653
- except Exception as e:
654
- logging.error(f"PDF file processing error: {str(e)}")
655
- return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed."
656
-
657
- def process_uploaded_files(files):
658
- """Combine the contents of all uploaded files into one string."""
659
- if not files:
660
- return None
661
-
662
- result = "# Uploaded File Contents\n\n"
663
- 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"
664
-
665
- for file in files:
666
- try:
667
- ext = file.name.split('.')[-1].lower()
668
- if ext == 'txt':
669
- result += process_text_file(file) + "\n\n---\n\n"
670
- elif ext == 'csv':
671
- result += process_csv_file(file) + "\n\n---\n\n"
672
- elif ext == 'pdf':
673
- result += process_pdf_file(file) + "\n\n---\n\n"
674
- else:
675
- result += f"### Unsupported File: {file.name}\n\n---\n\n"
676
- except Exception as e:
677
- logging.error(f"File processing error {file.name}: {e}")
678
- result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n"
679
-
680
- return result
681
-
682
- # ──────────────────────────────── Image & Utility ─────────────────────────
683
- def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
684
- """Image generation function."""
685
- if not prompt:
686
- return None, "Insufficient prompt"
687
- try:
688
- res = Client(IMAGE_API_URL).predict(
689
- prompt=prompt, width=w, height=h, guidance=g,
690
- inference_steps=steps, seed=seed,
691
- do_img2img=False, init_image=None,
692
- image2image_strength=0.8, resize_img=True,
693
- api_name="/generate_image"
694
- )
695
- return res[0], f"Seed: {res[1]}"
696
- except Exception as e:
697
- logging.error(e)
698
- return None, str(e)
699
-
700
- def extract_image_prompt(blog_text: str, topic: str):
701
- """
702
- Generate a single-line English image prompt from the blog content.
703
- """
704
- client = get_openai_client()
705
-
706
- try:
707
- response = client.chat.completions.create(
708
- model="gpt-4.1-mini", # 일반적으둜 μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈλ‘œ μ„€μ •
709
- messages=[
710
- {"role": "system", "content": "Generate a single-line English image prompt from the following text. Return only the prompt text, nothing else."},
711
- {"role": "user", "content": f"Topic: {topic}\n\n---\n{blog_text}\n\n---"}
712
- ],
713
- temperature=1,
714
- max_tokens=80,
715
- top_p=1
716
- )
717
-
718
- return response.choices[0].message.content.strip()
719
- except Exception as e:
720
- logging.error(f"OpenAI image prompt generation error: {e}")
721
- return f"A professional photo related to {topic}, high quality"
722
-
723
- def md_to_html(md: str, title="Ginigen Blog"):
724
- """Convert Markdown to HTML."""
725
- return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
726
-
727
- def keywords(text: str, top=5):
728
- """Simple keyword extraction."""
729
- cleaned = re.sub(r"[^κ°€-힣a-zA-Z0-9\s]", "", text)
730
- return " ".join(cleaned.split()[:top])
731
-
732
- # ──────────────────────────────── Streamlit UI ────────────────────────────
733
- def ginigen_app():
734
- st.title("Ginigen Blog")
735
-
736
- # Set default session state
737
- if "ai_model" not in st.session_state:
738
- st.session_state.ai_model = "gpt-4.1-mini" # κ³ μ • λͺ¨λΈ μ„€μ •
739
- if "messages" not in st.session_state:
740
- st.session_state.messages = []
741
- if "auto_save" not in st.session_state:
742
- st.session_state.auto_save = True
743
- if "generate_image" not in st.session_state:
744
- st.session_state.generate_image = False
745
- if "web_search_enabled" not in st.session_state:
746
- st.session_state.web_search_enabled = True
747
- if "blog_template" not in st.session_state:
748
- st.session_state.blog_template = "ginigen" # Ginigen recommended style by default
749
- if "blog_tone" not in st.session_state:
750
- st.session_state.blog_tone = "professional"
751
- if "word_count" not in st.session_state:
752
- st.session_state.word_count = 1750
753
-
754
- # Sidebar UI
755
- sb = st.sidebar
756
- sb.title("Blog Settings")
757
-
758
- # λͺ¨λΈ 선택 제거 (κ³ μ • λͺ¨λΈ μ‚¬μš©)
759
-
760
- sb.subheader("Blog Style Settings")
761
- sb.selectbox(
762
- "Blog Template",
763
- options=list(BLOG_TEMPLATES.keys()),
764
- format_func=lambda x: BLOG_TEMPLATES[x],
765
- key="blog_template"
766
- )
767
-
768
- sb.selectbox(
769
- "Blog Tone",
770
- options=list(BLOG_TONES.keys()),
771
- format_func=lambda x: BLOG_TONES[x],
772
- key="blog_tone"
773
- )
774
-
775
- sb.slider("Blog Length (word count)", 800, 3000, key="word_count")
776
-
777
-
778
- # Example topics
779
- sb.subheader("Example Topics")
780
- c1, c2, c3 = sb.columns(3)
781
- if c1.button("Real Estate Tax", key="ex1"):
782
- process_example(EXAMPLE_TOPICS["example1"])
783
- if c2.button("Summer Festivals", key="ex2"):
784
- process_example(EXAMPLE_TOPICS["example2"])
785
- if c3.button("Investment Guide", key="ex3"):
786
- process_example(EXAMPLE_TOPICS["example3"])
787
-
788
- sb.subheader("Other Settings")
789
- sb.toggle("Auto Save", key="auto_save")
790
- sb.toggle("Auto Image Generation", key="generate_image")
791
-
792
- web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled)
793
- st.session_state.web_search_enabled = web_search_enabled
794
-
795
- if web_search_enabled:
796
- st.sidebar.info("βœ… Web search results will be integrated into the blog.")
797
-
798
- # Download the latest blog (markdown/HTML)
799
- latest_blog = next(
800
- (m["content"] for m in reversed(st.session_state.messages)
801
- if m["role"] == "assistant" and m["content"].strip()),
802
- None
803
- )
804
- if latest_blog:
805
- title_match = re.search(r"# (.*?)(\n|$)", latest_blog)
806
- title = title_match.group(1).strip() if title_match else "blog"
807
- sb.subheader("Download Latest Blog")
808
- d1, d2 = sb.columns(2)
809
- d1.download_button("Download as Markdown", latest_blog,
810
- file_name=f"{title}.md", mime="text/markdown")
811
- d2.download_button("Download as HTML", md_to_html(latest_blog, title),
812
- file_name=f"{title}.html", mime="text/html")
813
-
814
- # JSON conversation record upload
815
- up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader")
816
- if up:
817
- try:
818
- st.session_state.messages = json.load(up)
819
- sb.success("Conversation history loaded successfully")
820
- except Exception as e:
821
- sb.error(f"Failed to load: {e}")
822
-
823
- # JSON conversation record download
824
- if sb.button("Download Conversation as JSON"):
825
- sb.download_button(
826
- "Save",
827
- data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
828
- file_name="chat_history.json",
829
- mime="application/json"
830
- )
831
-
832
- # File Upload
833
- st.subheader("File Upload")
834
- uploaded_files = st.file_uploader(
835
- "Upload files to be referenced in your blog (txt, csv, pdf)",
836
- type=["txt", "csv", "pdf"],
837
- accept_multiple_files=True,
838
- key="file_uploader"
839
- )
840
-
841
- if uploaded_files:
842
- file_count = len(uploaded_files)
843
- st.success(f"{file_count} files uploaded. They will be referenced in the blog.")
844
-
845
- with st.expander("Preview Uploaded Files", expanded=False):
846
- for idx, file in enumerate(uploaded_files):
847
- st.write(f"**File Name:** {file.name}")
848
- ext = file.name.split('.')[-1].lower()
849
-
850
- if ext == 'txt':
851
- preview = file.read(1000).decode('utf-8', errors='ignore')
852
- file.seek(0)
853
- st.text_area(
854
- f"Preview of {file.name}",
855
- preview + ("..." if len(preview) >= 1000 else ""),
856
- height=150
857
- )
858
- elif ext == 'csv':
859
- try:
860
- df = pd.read_csv(file)
861
- file.seek(0)
862
- st.write("CSV Preview (up to 5 rows)")
863
- st.dataframe(df.head(5))
864
- except Exception as e:
865
- st.error(f"CSV preview failed: {e}")
866
- elif ext == 'pdf':
867
- try:
868
- file_bytes = file.read()
869
- file.seek(0)
870
-
871
- pdf_file = io.BytesIO(file_bytes)
872
- reader = PyPDF2.PdfReader(pdf_file, strict=False)
873
-
874
- pc = len(reader.pages)
875
- st.write(f"PDF File: {pc} pages")
876
-
877
- if pc > 0:
878
- try:
879
- page_text = reader.pages[0].extract_text()
880
- preview = page_text[:500] if page_text else "(No text extracted)"
881
- st.text_area("Preview of the first page", preview + "...", height=150)
882
- except:
883
- st.warning("Failed to extract text from the first page")
884
- except Exception as e:
885
- st.error(f"PDF preview failed: {e}")
886
-
887
- if idx < file_count - 1:
888
- st.divider()
889
-
890
- # Display existing messages
891
- for m in st.session_state.messages:
892
- with st.chat_message(m["role"]):
893
- st.markdown(m["content"])
894
- if "image" in m:
895
- st.image(m["image"], caption=m.get("image_caption", ""))
896
-
897
- # User input
898
- prompt = st.chat_input("Enter a blog topic or keywords.")
899
- if prompt:
900
- process_input(prompt, uploaded_files)
901
-
902
-
903
- # μ‚¬μ΄λ“œλ°” ν•˜λ‹¨ λ°°μ§€(링크) μΆ”κ°€
904
- sb.markdown("---")
905
- sb.markdown("Created by [https://ginigen.com](https://ginigen.com) | [YouTube Channel](https://www.youtube.com/@ginipickaistudio)")
906
-
907
-
908
-
909
- def process_example(topic):
910
- """Process the selected example topic."""
911
- process_input(topic, [])
912
-
913
- def process_input(prompt: str, uploaded_files):
914
- # Add user's message
915
- if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages):
916
- st.session_state.messages.append({"role": "user", "content": prompt})
917
-
918
- with st.chat_message("user"):
919
- st.markdown(prompt)
920
-
921
- with st.chat_message("assistant"):
922
- placeholder = st.empty()
923
- message_placeholder = st.empty()
924
- full_response = ""
925
-
926
- use_web_search = st.session_state.web_search_enabled
927
- has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
928
-
929
- try:
930
- # μƒνƒœ ν‘œμ‹œλ₯Ό μœ„ν•œ μƒνƒœ μ»΄ν¬λ„ŒνŠΈ
931
- status = st.status("Preparing to generate blog...")
932
- status.update(label="Initializing client...")
933
-
934
- client = get_openai_client()
935
-
936
- # Prepare conversation messages
937
- messages = []
938
-
939
- # Web search
940
- search_content = None
941
- if use_web_search:
942
- status.update(label="Performing web search...")
943
- with st.spinner("Searching the web..."):
944
- search_content = do_web_search(keywords(prompt, top=5))
945
-
946
- # Process uploaded files β†’ content
947
- file_content = None
948
- if has_uploaded_files:
949
- status.update(label="Processing uploaded files...")
950
- with st.spinner("Analyzing files..."):
951
- file_content = process_uploaded_files(uploaded_files)
952
-
953
- # Build system prompt
954
- status.update(label="Preparing blog draft...")
955
- sys_prompt = get_system_prompt(
956
- template=st.session_state.blog_template,
957
- tone=st.session_state.blog_tone,
958
- word_count=st.session_state.word_count,
959
- include_search_results=use_web_search,
960
- include_uploaded_files=has_uploaded_files
961
- )
962
-
963
- # OpenAI API 호좜 μ€€λΉ„
964
- status.update(label="Writing blog content...")
965
-
966
- # λ©”μ‹œμ§€ ꡬ성
967
- api_messages = [
968
- {"role": "system", "content": sys_prompt}
969
- ]
970
-
971
- user_content = prompt
972
-
973
- # 검색 κ²°κ³Όκ°€ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
974
- if search_content:
975
- user_content += "\n\n" + search_content
976
-
977
- # 파일 λ‚΄μš©μ΄ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
978
- if file_content:
979
- user_content += "\n\n" + file_content
980
-
981
- # μ‚¬μš©μž λ©”μ‹œμ§€ μΆ”κ°€
982
- api_messages.append({"role": "user", "content": user_content})
983
-
984
- # OpenAI API 슀트리밍 호좜 - κ³ μ • λͺ¨λΈ "gpt-4.1-mini" μ‚¬μš©
985
- try:
986
- # 슀트리밍 λ°©μ‹μœΌλ‘œ API 호좜
987
- stream = client.chat.completions.create(
988
- model="gpt-4.1-mini", # κ³ μ • λͺ¨λΈ μ‚¬μš©
989
- messages=api_messages,
990
- temperature=1,
991
- max_tokens=MAX_TOKENS,
992
- top_p=1,
993
- stream=True # 슀트리밍 ν™œμ„±ν™”
994
- )
995
-
996
- # 슀트리밍 응닡 처리
997
- for chunk in stream:
998
- if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
999
- content_delta = chunk.choices[0].delta.content
1000
- full_response += content_delta
1001
- message_placeholder.markdown(full_response + "β–Œ")
1002
-
1003
- # μ΅œμ’… 응닡 ν‘œμ‹œ (μ»€μ„œ 제거)
1004
- message_placeholder.markdown(full_response)
1005
- status.update(label="Blog completed!", state="complete")
1006
-
1007
- except Exception as api_error:
1008
- error_message = str(api_error)
1009
- logging.error(f"API error: {error_message}")
1010
- status.update(label=f"Error: {error_message}", state="error")
1011
- raise Exception(f"Blog generation error: {error_message}")
1012
-
1013
- # 이미지 생성
1014
- answer_entry_saved = False
1015
- if st.session_state.generate_image and full_response:
1016
- with st.spinner("Generating image..."):
1017
- try:
1018
- ip = extract_image_prompt(full_response, prompt)
1019
- img, cap = generate_image(ip)
1020
- if img:
1021
- st.image(img, caption=cap)
1022
- st.session_state.messages.append({
1023
- "role": "assistant",
1024
- "content": full_response,
1025
- "image": img,
1026
- "image_caption": cap
1027
- })
1028
- answer_entry_saved = True
1029
- except Exception as img_error:
1030
- logging.error(f"Image generation error: {str(img_error)}")
1031
- st.warning("이미지 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λΈ”λ‘œκ·Έ μ½˜ν…μΈ λ§Œ μ €μž₯λ©λ‹ˆλ‹€.")
1032
-
1033
- # Save the answer if not saved above
1034
- if not answer_entry_saved and full_response:
1035
- st.session_state.messages.append({"role": "assistant", "content": full_response})
1036
-
1037
- # Download buttons
1038
- if full_response:
1039
- st.subheader("Download This Blog")
1040
- c1, c2 = st.columns(2)
1041
- c1.download_button(
1042
- "Markdown",
1043
- data=full_response,
1044
- file_name=f"{prompt[:30]}.md",
1045
- mime="text/markdown"
1046
- )
1047
- c2.download_button(
1048
- "HTML",
1049
- data=md_to_html(full_response, prompt[:30]),
1050
- file_name=f"{prompt[:30]}.html",
1051
- mime="text/html"
1052
- )
1053
-
1054
- # Auto save
1055
- if st.session_state.auto_save and st.session_state.messages:
1056
- try:
1057
- fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
1058
- with open(fn, "w", encoding="utf-8") as fp:
1059
- json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
1060
- except Exception as e:
1061
- logging.error(f"Auto-save failed: {e}")
1062
-
1063
- except Exception as e:
1064
- error_message = str(e)
1065
- placeholder.error(f"An error occurred: {error_message}")
1066
- logging.error(f"Process input error: {error_message}")
1067
- ans = f"An error occurred while processing your request: {error_message}"
1068
- st.session_state.messages.append({"role": "assistant", "content": ans})
1069
-
1070
-
1071
- # ──────────────────────────────── main ────────────────────────────────────
1072
- def main():
1073
- ginigen_app()
1074
-
1075
- if __name__ == "__main__":
1076
- main()