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

Update app.py

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