Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -13,31 +13,170 @@ import PyPDF2 # For handling PDF files
|
|
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 |
-
#
|
20 |
-
|
21 |
-
"
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
}
|
28 |
|
29 |
-
|
30 |
-
"
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
41 |
}
|
42 |
|
43 |
# ──────────────────────────────── Logging ────────────────────────────────
|
@@ -46,7 +185,6 @@ logging.basicConfig(level=logging.INFO,
|
|
46 |
|
47 |
# ──────────────────────────────── OpenAI Client ──────────────────────────
|
48 |
|
49 |
-
# OpenAI 클라이언트에 타임아웃과 재시도 로직 추가
|
50 |
@st.cache_resource
|
51 |
def get_openai_client():
|
52 |
"""Create an OpenAI client with timeout and retry settings."""
|
@@ -57,246 +195,49 @@ def get_openai_client():
|
|
57 |
timeout=60.0, # 타임아웃 60초로 설정
|
58 |
max_retries=3 # 재시도 횟수 3회로 설정
|
59 |
)
|
60 |
-
|
61 |
-
# ────────────────────────────────
|
62 |
-
def
|
63 |
"""
|
64 |
-
|
65 |
-
-
|
66 |
-
-
|
67 |
-
-
|
|
|
68 |
"""
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
> ✔ One-line key point emphasis
|
103 |
-
|
104 |
-
- Total of 5 items
|
105 |
-
|
106 |
-
7. **Section 3: Consumption/Usage Methods**
|
107 |
-
- `## 🥗 How to Use [Keyword] Effectively!`
|
108 |
-
- Emoji bullet list of around 5 items + Additional tips
|
109 |
-
|
110 |
-
8. `---`
|
111 |
-
|
112 |
-
9. **Concluding Call to Action**
|
113 |
-
- `## 📌 Conclusion – Start Using [Keyword] Today!`
|
114 |
-
- 2-3 sentences on benefits/changes → **Action directive** (purchase, subscribe, share, etc.)
|
115 |
-
|
116 |
-
10. `---`
|
117 |
-
|
118 |
-
11. **Key Summary Table**
|
119 |
-
| Item | Effect |
|
120 |
-
|---|---|
|
121 |
-
| [Keyword] | [Effect summary] |
|
122 |
-
| Key foods/products | [List] |
|
123 |
-
|
124 |
-
12. `---`
|
125 |
-
|
126 |
-
13. **Quiz & CTA**
|
127 |
-
- Simple Q&A quiz (1 question) → Reveal answer
|
128 |
-
- "If you found this helpful, please share/comment" phrase
|
129 |
-
- Preview of next post
|
130 |
-
|
131 |
-
◆ Additional Guidelines
|
132 |
-
- Total length 1,200-1,800 words.
|
133 |
-
- Use simple vocabulary and short sentences, enhance readability with emojis, bold text, and quoted sections.
|
134 |
-
- Increase credibility with specific numbers, research results, and analogies.
|
135 |
-
- No meta-mentions of "prompts" or "instructions".
|
136 |
-
- Use conversational but professional tone throughout.
|
137 |
-
- Minimize expressions like "according to research" if no external sources are provided.
|
138 |
-
|
139 |
-
◆ Output
|
140 |
-
- Return **only the completed blog post** in the above format. No additional text.
|
141 |
-
|
142 |
-
"""
|
143 |
-
|
144 |
-
# Standard 8-step framework (English version)
|
145 |
-
base_prompt = """
|
146 |
-
You are an expert in writing professional blog posts. For every blog writing request, strictly follow this 8-step framework to produce a coherent, engaging post:
|
147 |
-
|
148 |
-
Reader Connection Phase
|
149 |
-
1.1. Friendly greeting to build rapport
|
150 |
-
1.2. Reflect actual reader concerns through introductory questions
|
151 |
-
1.3. Stimulate immediate interest in the topic
|
152 |
-
|
153 |
-
Problem Definition Phase
|
154 |
-
2.1. Define the reader's pain points in detail
|
155 |
-
2.2. Analyze the urgency and impact of the problem
|
156 |
-
2.3. Build a consensus on why it needs to be solved
|
157 |
-
|
158 |
-
Establish Expertise Phase
|
159 |
-
3.1. Analyze based on objective data
|
160 |
-
3.2. Cite expert views and research findings
|
161 |
-
3.3. Use real-life examples to further clarify the issue
|
162 |
-
|
163 |
-
Solution Phase
|
164 |
-
4.1. Provide step-by-step guidance
|
165 |
-
4.2. Suggest practical tips that can be applied immediately
|
166 |
-
4.3. Mention potential obstacles and how to overcome them
|
167 |
-
|
168 |
-
Build Trust Phase
|
169 |
-
5.1. Present actual success stories
|
170 |
-
5.2. Quote real user feedback
|
171 |
-
5.3. Use objective data to prove effectiveness
|
172 |
-
|
173 |
-
Action Phase
|
174 |
-
6.1. Suggest the first clear step the reader can take
|
175 |
-
6.2. Urge timely action by emphasizing urgency
|
176 |
-
6.3. Motivate by highlighting incentives or benefits
|
177 |
-
|
178 |
-
Authenticity Phase
|
179 |
-
7.1. Transparently disclose any limits of the solution
|
180 |
-
7.2. Admit that individual experiences may vary
|
181 |
-
7.3. Mention prerequisites or cautionary points
|
182 |
-
|
183 |
-
Relationship Continuation Phase
|
184 |
-
8.1. Conclude with sincere gratitude
|
185 |
-
8.2. Preview upcoming content to build anticipation
|
186 |
-
8.3. Provide channels for further communication
|
187 |
-
"""
|
188 |
-
|
189 |
-
# Additional guidelines for each template
|
190 |
-
template_guides = {
|
191 |
-
"tutorial": """
|
192 |
-
This blog should be in a tutorial style:
|
193 |
-
- Clearly state the goal and the final outcome first
|
194 |
-
- Provide step-by-step explanations with clear separations
|
195 |
-
- Indicate where images could be inserted for each step
|
196 |
-
- Mention approximate time requirements and difficulty level
|
197 |
-
- List necessary tools or prerequisite knowledge
|
198 |
-
- Give troubleshooting tips and common mistakes to avoid
|
199 |
-
- Conclude with suggestions for next steps or advanced applications
|
200 |
-
""",
|
201 |
-
"review": """
|
202 |
-
This blog should be in a review style:
|
203 |
-
- Separate objective facts from subjective opinions
|
204 |
-
- Clearly list your evaluation criteria
|
205 |
-
- Discuss both pros and cons in a balanced way
|
206 |
-
- Compare with similar products/services
|
207 |
-
- Specify the target audience for whom it is suitable
|
208 |
-
- Provide concrete use cases and outcomes
|
209 |
-
- Conclude with a final recommendation or alternatives
|
210 |
-
""",
|
211 |
-
"storytelling": """
|
212 |
-
This blog should be in a storytelling style:
|
213 |
-
- Start with a real or hypothetical person or case
|
214 |
-
- Emphasize emotional connection with the problem scenario
|
215 |
-
- Follow a narrative structure centered on conflict and resolution
|
216 |
-
- Include meaningful insights or lessons learned
|
217 |
-
- Maintain an emotional thread the reader can relate to
|
218 |
-
- Balance storytelling with useful information
|
219 |
-
- Encourage the reader to reflect on their own story
|
220 |
-
""",
|
221 |
-
"seo_optimized": """
|
222 |
-
This blog should be SEO-optimized:
|
223 |
-
- Include the main keyword in the title, headings, and first paragraph
|
224 |
-
- Spread related keywords naturally throughout the text
|
225 |
-
- Keep paragraphs around 300-500 characters
|
226 |
-
- Use question-based subheadings
|
227 |
-
- Make use of lists, tables, and bold text to diversify formatting
|
228 |
-
- Indicate where internal links could be inserted
|
229 |
-
- Provide sufficient content of at least 2000-3000 characters
|
230 |
-
"""
|
231 |
-
}
|
232 |
-
|
233 |
-
# Additional guidelines for each tone
|
234 |
-
tone_guides = {
|
235 |
-
"professional": "Use a professional, authoritative voice. Clearly explain any technical terms and present data or research to maintain a logical flow.",
|
236 |
-
"casual": "Use a relaxed, conversational style. Employ personal experiences, relatable examples, and a friendly voice (e.g., 'It's super useful!').",
|
237 |
-
"humorous": "Use humor and witty expressions. Add funny analogies or jokes while preserving accuracy and usefulness.",
|
238 |
-
"storytelling": "Write as if telling a story, with emotional depth and narrative flow. Incorporate characters, settings, conflicts, and resolutions."
|
239 |
-
}
|
240 |
-
|
241 |
-
# Guidelines for using search results
|
242 |
-
search_guide = """
|
243 |
-
Guidelines for Using Search Results:
|
244 |
-
- Accurately incorporate key information from the search results into the blog
|
245 |
-
- Include recent data, statistics, and case studies from the search results
|
246 |
-
- When quoting, specify the source within the text (e.g., "According to XYZ website...")
|
247 |
-
- At the end of the blog, add a "References" section and list major sources with links
|
248 |
-
- If there are conflicting pieces of information, present multiple perspectives
|
249 |
-
- Make sure to reflect the latest trends and data from the search results
|
250 |
-
"""
|
251 |
-
|
252 |
-
# Guidelines for using uploaded files
|
253 |
-
upload_guide = """
|
254 |
-
Guidelines for Using Uploaded Files (Highest Priority):
|
255 |
-
- The uploaded files must be a main source of information for the blog
|
256 |
-
- Carefully examine the data, statistics, or examples in the file and integrate them
|
257 |
-
- Directly quote and thoroughly explain any key figures or claims from the file
|
258 |
-
- Highlight the file content as a crucial aspect of the blog
|
259 |
-
- Mention the source clearly, e.g., "According to the uploaded data..."
|
260 |
-
- For CSV files, detail important stats or numerical data in the blog
|
261 |
-
- For PDF files, quote crucial segments or statements
|
262 |
-
- For text files, integrate relevant content effectively
|
263 |
-
- Even if the file content seems tangential, do your best to connect it to the blog topic
|
264 |
-
- Keep consistency throughout and ensure the file's data is appropriately reflected
|
265 |
-
"""
|
266 |
-
|
267 |
-
# Choose base prompt
|
268 |
-
if template == "ginigen":
|
269 |
-
final_prompt = ginigen_prompt
|
270 |
-
else:
|
271 |
-
final_prompt = base_prompt
|
272 |
-
|
273 |
-
# If the user chose a specific template (and not ginigen), append the relevant guidelines
|
274 |
-
if template != "ginigen" and template in template_guides:
|
275 |
-
final_prompt += "\n" + template_guides[template]
|
276 |
-
|
277 |
-
# If a specific tone is selected, append that guideline
|
278 |
-
if tone in tone_guides:
|
279 |
-
final_prompt += f"\n\nTone and Manner: {tone_guides[tone]}"
|
280 |
-
|
281 |
-
# If web search results should be included
|
282 |
-
if include_search_results:
|
283 |
-
final_prompt += f"\n\n{search_guide}"
|
284 |
-
|
285 |
-
# If uploaded files should be included
|
286 |
-
if include_uploaded_files:
|
287 |
-
final_prompt += f"\n\n{upload_guide}"
|
288 |
-
|
289 |
-
# Word count guidelines
|
290 |
-
final_prompt += (
|
291 |
-
f"\n\nWriting Requirements:\n"
|
292 |
-
f"9.1. Word Count: around {word_count-250}-{word_count+250} characters\n"
|
293 |
-
f"9.2. Paragraph Length: 3-4 sentences each\n"
|
294 |
-
f"9.3. Visual Cues: Use subheadings, separators, and bullet/numbered lists\n"
|
295 |
-
f"9.4. Data: Cite all sources\n"
|
296 |
-
f"9.5. Readability: Use clear paragraph breaks and highlights where necessary"
|
297 |
-
)
|
298 |
-
|
299 |
-
return final_prompt
|
300 |
|
301 |
# ──────────────────────────────── Brave Search API ────────────────────────
|
302 |
@st.cache_data(ttl=3600)
|
@@ -306,7 +247,7 @@ def brave_search(query: str, count: int = 20):
|
|
306 |
Returns fields: index, title, link, snippet, displayed_link
|
307 |
"""
|
308 |
if not BRAVE_KEY:
|
309 |
-
raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key)
|
310 |
|
311 |
headers = {
|
312 |
"Accept": "application/json",
|
@@ -351,15 +292,14 @@ def brave_search(query: str, count: int = 20):
|
|
351 |
return []
|
352 |
|
353 |
def mock_results(query: str) -> str:
|
354 |
-
"""Fallback search
|
355 |
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
356 |
return (f"# Fallback Search Content (Generated: {ts})\n\n"
|
357 |
-
f"The search API request failed. Please generate the
|
358 |
-
f"You may consider
|
359 |
-
f"- Basic
|
360 |
-
f"- Commonly known
|
361 |
-
f"-
|
362 |
-
f"- Questions that readers might have\n\n"
|
363 |
f"Note: This is fallback guidance, not real-time data.\n\n")
|
364 |
|
365 |
def do_web_search(query: str) -> str:
|
@@ -370,7 +310,7 @@ def do_web_search(query: str) -> str:
|
|
370 |
logging.warning("No search results, using fallback content")
|
371 |
return mock_results(query)
|
372 |
|
373 |
-
hdr = "# Web Search Results\nUse the information below to
|
374 |
body = "\n".join(
|
375 |
f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
|
376 |
f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
|
@@ -446,19 +386,15 @@ def process_csv_file(file):
|
|
446 |
def process_pdf_file(file):
|
447 |
"""Handle PDF file"""
|
448 |
try:
|
449 |
-
# Read file in bytes
|
450 |
file_bytes = file.read()
|
451 |
file.seek(0)
|
452 |
|
453 |
-
# Use PyPDF2
|
454 |
pdf_file = io.BytesIO(file_bytes)
|
455 |
reader = PyPDF2.PdfReader(pdf_file, strict=False)
|
456 |
|
457 |
-
# Basic info
|
458 |
result = f"## PDF File: {file.name}\n\n"
|
459 |
result += f"- Total pages: {len(reader.pages)}\n\n"
|
460 |
|
461 |
-
# Extract text by page (limit to first 5 pages)
|
462 |
max_pages = min(5, len(reader.pages))
|
463 |
all_text = ""
|
464 |
|
@@ -469,17 +405,15 @@ def process_pdf_file(file):
|
|
469 |
|
470 |
current_page_text = f"### Page {i+1}\n\n"
|
471 |
if page_text and len(page_text.strip()) > 0:
|
472 |
-
# Limit to 1500 characters per page
|
473 |
if len(page_text) > 1500:
|
474 |
current_page_text += page_text[:1500] + "...(truncated)...\n\n"
|
475 |
else:
|
476 |
current_page_text += page_text + "\n\n"
|
477 |
else:
|
478 |
-
current_page_text += "(No text could be extracted
|
479 |
|
480 |
all_text += current_page_text
|
481 |
|
482 |
-
# If total text is too long, break
|
483 |
if len(all_text) > 8000:
|
484 |
all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
|
485 |
break
|
@@ -504,7 +438,7 @@ def process_uploaded_files(files):
|
|
504 |
return None
|
505 |
|
506 |
result = "# Uploaded File Contents\n\n"
|
507 |
-
result += "Below is the content from the files provided by the user. Integrate this data as
|
508 |
|
509 |
for file in files:
|
510 |
try:
|
@@ -541,95 +475,35 @@ def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
|
|
541 |
logging.error(e)
|
542 |
return None, str(e)
|
543 |
|
544 |
-
def
|
545 |
-
"""
|
546 |
-
Generate a single-line English image prompt from the blog content.
|
547 |
-
"""
|
548 |
-
client = get_openai_client()
|
549 |
-
|
550 |
-
try:
|
551 |
-
response = client.chat.completions.create(
|
552 |
-
model="gpt-4.1-mini", # 일반적으로 사용 가능한 모델로 설정
|
553 |
-
messages=[
|
554 |
-
{"role": "system", "content": "Generate a single-line English image prompt from the following text. Return only the prompt text, nothing else."},
|
555 |
-
{"role": "user", "content": f"Topic: {topic}\n\n---\n{blog_text}\n\n---"}
|
556 |
-
],
|
557 |
-
temperature=1,
|
558 |
-
max_tokens=80,
|
559 |
-
top_p=1
|
560 |
-
)
|
561 |
-
|
562 |
-
return response.choices[0].message.content.strip()
|
563 |
-
except Exception as e:
|
564 |
-
logging.error(f"OpenAI image prompt generation error: {e}")
|
565 |
-
return f"A professional photo related to {topic}, high quality"
|
566 |
-
|
567 |
-
def md_to_html(md: str, title="Ginigen Blog"):
|
568 |
"""Convert Markdown to HTML."""
|
569 |
return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
|
570 |
|
571 |
def keywords(text: str, top=5):
|
572 |
-
"""Simple keyword extraction."""
|
573 |
cleaned = re.sub(r"[^가-힣a-zA-Z0-9\s]", "", text)
|
574 |
return " ".join(cleaned.split()[:top])
|
575 |
|
576 |
# ──────────────────────────────── Streamlit UI ────────────────────────────
|
577 |
-
def
|
578 |
-
st.title("
|
579 |
|
580 |
# Set default session state
|
581 |
if "ai_model" not in st.session_state:
|
582 |
-
st.session_state.ai_model = "gpt-4.1-mini"
|
583 |
if "messages" not in st.session_state:
|
584 |
st.session_state.messages = []
|
585 |
if "auto_save" not in st.session_state:
|
586 |
st.session_state.auto_save = True
|
587 |
if "generate_image" not in st.session_state:
|
588 |
-
st.session_state.generate_image =
|
589 |
if "web_search_enabled" not in st.session_state:
|
590 |
st.session_state.web_search_enabled = True
|
591 |
-
if "blog_template" not in st.session_state:
|
592 |
-
st.session_state.blog_template = "ginigen" # Ginigen recommended style by default
|
593 |
-
if "blog_tone" not in st.session_state:
|
594 |
-
st.session_state.blog_tone = "professional"
|
595 |
-
if "word_count" not in st.session_state:
|
596 |
-
st.session_state.word_count = 1750
|
597 |
|
598 |
# Sidebar UI
|
599 |
sb = st.sidebar
|
600 |
-
sb.title("
|
601 |
-
|
602 |
-
# 모델 선택 제거 (고정 모델 사용)
|
603 |
-
|
604 |
-
sb.subheader("Blog Style Settings")
|
605 |
-
sb.selectbox(
|
606 |
-
"Blog Template",
|
607 |
-
options=list(BLOG_TEMPLATES.keys()),
|
608 |
-
format_func=lambda x: BLOG_TEMPLATES[x],
|
609 |
-
key="blog_template"
|
610 |
-
)
|
611 |
-
|
612 |
-
sb.selectbox(
|
613 |
-
"Blog Tone",
|
614 |
-
options=list(BLOG_TONES.keys()),
|
615 |
-
format_func=lambda x: BLOG_TONES[x],
|
616 |
-
key="blog_tone"
|
617 |
-
)
|
618 |
-
|
619 |
-
sb.slider("Blog Length (word count)", 800, 3000, key="word_count")
|
620 |
|
621 |
-
|
622 |
-
# Example topics
|
623 |
-
sb.subheader("Example Topics")
|
624 |
-
c1, c2, c3 = sb.columns(3)
|
625 |
-
if c1.button("Real Estate Tax", key="ex1"):
|
626 |
-
process_example(EXAMPLE_TOPICS["example1"])
|
627 |
-
if c2.button("Summer Festivals", key="ex2"):
|
628 |
-
process_example(EXAMPLE_TOPICS["example2"])
|
629 |
-
if c3.button("Investment Guide", key="ex3"):
|
630 |
-
process_example(EXAMPLE_TOPICS["example3"])
|
631 |
-
|
632 |
-
sb.subheader("Other Settings")
|
633 |
sb.toggle("Auto Save", key="auto_save")
|
634 |
sb.toggle("Auto Image Generation", key="generate_image")
|
635 |
|
@@ -637,22 +511,38 @@ def ginigen_app():
|
|
637 |
st.session_state.web_search_enabled = web_search_enabled
|
638 |
|
639 |
if web_search_enabled:
|
640 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
641 |
|
642 |
-
|
643 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
644 |
(m["content"] for m in reversed(st.session_state.messages)
|
645 |
if m["role"] == "assistant" and m["content"].strip()),
|
646 |
None
|
647 |
)
|
648 |
-
if
|
649 |
-
title_match = re.search(r"# (.*?)(\n|$)",
|
650 |
-
title = title_match.group(1).strip() if title_match else "
|
651 |
-
sb.subheader("Download Latest
|
652 |
d1, d2 = sb.columns(2)
|
653 |
-
d1.download_button("Download as Markdown",
|
654 |
file_name=f"{title}.md", mime="text/markdown")
|
655 |
-
d2.download_button("Download as HTML", md_to_html(
|
656 |
file_name=f"{title}.html", mime="text/html")
|
657 |
|
658 |
# JSON conversation record upload
|
@@ -667,16 +557,16 @@ def ginigen_app():
|
|
667 |
# JSON conversation record download
|
668 |
if sb.button("Download Conversation as JSON"):
|
669 |
sb.download_button(
|
670 |
-
"Save",
|
671 |
data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
|
672 |
file_name="chat_history.json",
|
673 |
mime="application/json"
|
674 |
)
|
675 |
|
676 |
# File Upload
|
677 |
-
st.subheader("File Upload")
|
678 |
uploaded_files = st.file_uploader(
|
679 |
-
"Upload files to
|
680 |
type=["txt", "csv", "pdf"],
|
681 |
accept_multiple_files=True,
|
682 |
key="file_uploader"
|
@@ -684,7 +574,7 @@ def ginigen_app():
|
|
684 |
|
685 |
if uploaded_files:
|
686 |
file_count = len(uploaded_files)
|
687 |
-
st.success(f"{file_count} files uploaded.
|
688 |
|
689 |
with st.expander("Preview Uploaded Files", expanded=False):
|
690 |
for idx, file in enumerate(uploaded_files):
|
@@ -721,7 +611,7 @@ def ginigen_app():
|
|
721 |
if pc > 0:
|
722 |
try:
|
723 |
page_text = reader.pages[0].extract_text()
|
724 |
-
preview = page_text[:500] if page_text else "(No text
|
725 |
st.text_area("Preview of the first page", preview + "...", height=150)
|
726 |
except:
|
727 |
st.warning("Failed to extract text from the first page")
|
@@ -731,27 +621,25 @@ def ginigen_app():
|
|
731 |
if idx < file_count - 1:
|
732 |
st.divider()
|
733 |
|
734 |
-
# Display existing messages
|
735 |
for m in st.session_state.messages:
|
736 |
with st.chat_message(m["role"]):
|
737 |
st.markdown(m["content"])
|
738 |
if "image" in m:
|
739 |
st.image(m["image"], caption=m.get("image_caption", ""))
|
740 |
|
741 |
-
# User input
|
742 |
-
prompt = st.chat_input("Enter a
|
743 |
if prompt:
|
744 |
process_input(prompt, uploaded_files)
|
745 |
|
746 |
-
|
747 |
-
# 사이드바 하단 배지(링크) 추가
|
748 |
sb.markdown("---")
|
749 |
-
sb.markdown("Created by [
|
750 |
-
|
751 |
|
752 |
|
753 |
def process_example(topic):
|
754 |
-
"""
|
755 |
process_input(topic, [])
|
756 |
|
757 |
def process_input(prompt: str, uploaded_files):
|
@@ -771,116 +659,120 @@ def process_input(prompt: str, uploaded_files):
|
|
771 |
has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
|
772 |
|
773 |
try:
|
774 |
-
|
775 |
-
status = st.status("Preparing to generate blog...")
|
776 |
status.update(label="Initializing client...")
|
777 |
-
|
778 |
client = get_openai_client()
|
779 |
|
780 |
-
# Prepare
|
781 |
-
|
782 |
-
|
783 |
-
#
|
784 |
search_content = None
|
785 |
if use_web_search:
|
786 |
status.update(label="Performing web search...")
|
787 |
with st.spinner("Searching the web..."):
|
788 |
search_content = do_web_search(keywords(prompt, top=5))
|
789 |
-
|
790 |
-
#
|
791 |
file_content = None
|
792 |
if has_uploaded_files:
|
793 |
status.update(label="Processing uploaded files...")
|
794 |
with st.spinner("Analyzing files..."):
|
795 |
file_content = process_uploaded_files(uploaded_files)
|
796 |
-
|
797 |
-
# Build
|
798 |
-
status.update(label="Preparing blog draft...")
|
799 |
-
sys_prompt = get_system_prompt(
|
800 |
-
template=st.session_state.blog_template,
|
801 |
-
tone=st.session_state.blog_tone,
|
802 |
-
word_count=st.session_state.word_count,
|
803 |
-
include_search_results=use_web_search,
|
804 |
-
include_uploaded_files=has_uploaded_files
|
805 |
-
)
|
806 |
-
|
807 |
-
# OpenAI API 호출 준비
|
808 |
-
status.update(label="Writing blog content...")
|
809 |
-
|
810 |
-
# 메시지 구성
|
811 |
-
api_messages = [
|
812 |
-
{"role": "system", "content": sys_prompt}
|
813 |
-
]
|
814 |
-
|
815 |
user_content = prompt
|
816 |
-
|
817 |
-
# 검색 결과가 있으면 사용자 프롬프트에 추가
|
818 |
if search_content:
|
819 |
user_content += "\n\n" + search_content
|
820 |
-
|
821 |
-
# 파일 내용이 있으면 사용자 프롬프트에 추가
|
822 |
if file_content:
|
823 |
user_content += "\n\n" + file_content
|
824 |
-
|
825 |
-
|
826 |
-
|
827 |
-
|
828 |
-
|
|
|
|
|
|
|
829 |
try:
|
830 |
-
# 스트리밍 방식으로 API 호출
|
831 |
stream = client.chat.completions.create(
|
832 |
-
model="gpt-4.1-mini",
|
833 |
messages=api_messages,
|
834 |
temperature=1,
|
835 |
max_tokens=MAX_TOKENS,
|
836 |
top_p=1,
|
837 |
-
stream=True
|
838 |
)
|
839 |
-
|
840 |
-
# 스트리밍 응답 처리
|
841 |
for chunk in stream:
|
842 |
-
if
|
|
|
|
|
843 |
content_delta = chunk.choices[0].delta.content
|
844 |
full_response += content_delta
|
845 |
message_placeholder.markdown(full_response + "▌")
|
846 |
-
|
847 |
-
# 최종 응답 표시 (커서 제거)
|
848 |
message_placeholder.markdown(full_response)
|
849 |
-
status.update(label="
|
850 |
-
|
851 |
except Exception as api_error:
|
852 |
error_message = str(api_error)
|
853 |
logging.error(f"API error: {error_message}")
|
854 |
status.update(label=f"Error: {error_message}", state="error")
|
855 |
-
raise Exception(f"
|
856 |
-
|
857 |
-
#
|
858 |
answer_entry_saved = False
|
|
|
|
|
859 |
if st.session_state.generate_image and full_response:
|
860 |
-
|
861 |
-
|
862 |
-
|
863 |
-
|
864 |
-
|
865 |
-
|
866 |
-
|
867 |
-
|
868 |
-
|
869 |
-
|
870 |
-
|
871 |
-
|
872 |
-
|
873 |
-
|
874 |
-
|
875 |
-
|
876 |
-
|
877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
878 |
if not answer_entry_saved and full_response:
|
|
|
879 |
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
880 |
|
881 |
# Download buttons
|
882 |
if full_response:
|
883 |
-
st.subheader("Download This
|
884 |
c1, c2 = st.columns(2)
|
885 |
c1.download_button(
|
886 |
"Markdown",
|
@@ -895,7 +787,7 @@ def process_input(prompt: str, uploaded_files):
|
|
895 |
mime="text/html"
|
896 |
)
|
897 |
|
898 |
-
# Auto
|
899 |
if st.session_state.auto_save and st.session_state.messages:
|
900 |
try:
|
901 |
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
|
@@ -911,10 +803,9 @@ def process_input(prompt: str, uploaded_files):
|
|
911 |
ans = f"An error occurred while processing your request: {error_message}"
|
912 |
st.session_state.messages.append({"role": "assistant", "content": ans})
|
913 |
|
914 |
-
|
915 |
# ──────────────────────────────── main ────────────────────────────────────
|
916 |
def main():
|
917 |
-
|
918 |
|
919 |
if __name__ == "__main__":
|
920 |
-
main()
|
|
|
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" # 이미지 생성용 API
|
17 |
MAX_TOKENS = 7999
|
18 |
|
19 |
+
# ──────────────────────────────── Physical Transformation Categories (KR & EN) ─────────────────
|
20 |
+
physical_transformation_categories = {
|
21 |
+
"센서 기능": [
|
22 |
+
"시각 센서/감지", "청각 센서/감지", "촉각 센서/감지", "미각 센서/감지", "후각 센서/감지",
|
23 |
+
"온도 센서/감지", "습도 센서/감지", "압력 센서/감지", "가속도 센서/감지", "회전 센서/감지",
|
24 |
+
"근접 센서/감지", "위치 센서/감지", "운동 센서/감지", "가스 센서/감지", "적외선 센서/감지",
|
25 |
+
"자외선 센서/감지", "방사선 센서/감지", "자기장 센서/감지", "전기장 센서/감지", "화학물질 센서/감지",
|
26 |
+
"생체신호 센서/감지", "진동 센서/감지", "소음 센서/감지", "빛 세기 센서/감지", "빛 파장 센서/감지",
|
27 |
+
"기울기 센서/감지", "pH 센서/감지", "전류 센서/감지", "전압 센서/감지", "이미지 센서/감지",
|
28 |
+
"거리 센서/감지", "깊이 센서/감지", "중력 센서/감지", "속도 센서/감지", "흐름 센서/감지",
|
29 |
+
"수위 센서/감지", "탁도 센서/감지", "염도 센서/감지", "금속 감지", "압전 센서/감지",
|
30 |
+
"광전 센서/감지", "열전대 센서/감지", "홀 효과 센서/감지", "초음파 센서/감지", "레이더 센서/감지",
|
31 |
+
"라이다 센서/감지", "터치 센서/감지", "제스처 센서/감지", "심박 센서/감지", "혈압 센서/감지"
|
32 |
+
],
|
33 |
+
"크기와 형태 변화": [
|
34 |
+
"부피 늘어남/줄어듦", "길이 늘어남/줄어듦", "너비 늘어남/줄어듦", "높이 늘어남/줄어듦",
|
35 |
+
"밀도 변화", "무게 증가/감소", "모양 변형", "상태 변화", "불균등 변형",
|
36 |
+
"복잡한 형태 변형", "비틀림/꼬임", "불균일한 확장/축소", "모서리 둥글게/날카롭게",
|
37 |
+
"깨짐/갈라짐", "여러 조각 나눠짐", "물 저항", "먼지 저항", "찌그러짐/복원",
|
38 |
+
"접힘/펼쳐짐", "압착/팽창", "늘어남/수축", "구겨짐/평평해짐", "뭉개짐/단단해짐",
|
39 |
+
"말림/펴짐", "꺾임/구부러짐"
|
40 |
+
],
|
41 |
+
"표면 및 외관 변화": [
|
42 |
+
"색상 변화", "질감 변화", "투명/불투명 변화", "반짝임/무광 변화",
|
43 |
+
"빛 반사 정도 변화", "무늬 변화", "각도에 따른 색상 변화", "빛에 따른 색상 변화",
|
44 |
+
"온도에 따른 색상 변화", "홀로그램 효과", "표면 각도별 빛 반사", "표면 모양 변형",
|
45 |
+
"초미세 표면 구조 변화", "자가 세정 효과", "얼룩/패턴 생성", "흐림/선명함 변화",
|
46 |
+
"광택/윤기 변화", "색조/채도 변화", "발광/형광", "빛 산란 효과",
|
47 |
+
"빛 흡수 변화", "반투명 효과", "그림자 효과 변화", "자외선 반응 변화",
|
48 |
+
"야광 효과"
|
49 |
+
],
|
50 |
+
"물질의 상태 변화": [
|
51 |
+
"고체/액체/기체 전환", "결정화/용해", "산화/부식", "딱딱해짐/부드러워짐",
|
52 |
+
"특수 상태 전환", "무정형/결정형 전환", "성분 분리", "미세 입자 형성/분해",
|
53 |
+
"젤 형성/풀어짐", "준안정 상태 변화", "분자 자가 정렬/분해", "상태변화 지연 현상",
|
54 |
+
"녹음", "굳음", "증발/응축", "승화/증착", "침전/부유", "분산/응집",
|
55 |
+
"건조/습윤", "팽윤/수축", "동결/해동", "풍화/침식", "충전/방전",
|
56 |
+
"결합/분리", "발효/부패"
|
57 |
+
],
|
58 |
+
"움직임 특성 변화": [
|
59 |
+
"가속/감속", "일정 속도 유지", "진동/진동 감소", "부딪힘/튕김",
|
60 |
+
"회전 속도 증가/감소", "회전 방향 변화", "불규칙 움직임", "멈췄다 미끄러지는 현상",
|
61 |
+
"공진/반공진", "유체 속 저항/양력 변화", "움직임 저항 변화", "복합 진동 움직임",
|
62 |
+
"특수 유체 속 움직임", "회전-이동 연계 움직임", "관성 정지", "충격 흡수",
|
63 |
+
"충격 전달", "운동량 보존", "마찰력 변화", "관성 탈출", "불안정 균형",
|
64 |
+
"동적 안정성", "흔들림 감쇠", "경로 예측성", "회피 움직임"
|
65 |
+
],
|
66 |
+
"구조적 변화": [
|
67 |
+
"부품 추가/제거", "조립/분해", "접기/펴기", "변형/원상복구", "최적 구조 변화",
|
68 |
+
"자가 재배열", "자연 패턴 형성/소멸", "규칙적 패턴 변화", "모듈식 변형",
|
69 |
+
"복잡성 증가 구조", "원래 모양 기억 효과", "시간에 따른 형태 변화", "부분 제거",
|
70 |
+
"부분 교체", "결합", "분리", "분할/통합", "중첩/겹침", "내부 구조 변화",
|
71 |
+
"외부 구조 변화", "중심축 이동", "균형점 변화", "계층 구조 변화", "지지 구조 변화",
|
72 |
+
"응력 분산 구조", "충격 흡수 구조", "그리드/매트릭스 구조 변화", "상호 연결성 변화"
|
73 |
+
],
|
74 |
+
"공간 이동": [
|
75 |
+
"앞/뒤 이동", "좌/우 이동", "위/아래 이동", "세로축 회전(고개 끄덕임)",
|
76 |
+
"가로축 회전(고개 젓기)", "길이축 회전(옆으로 기울임)", "원 운동", "나선형 이동",
|
77 |
+
"관성에 의한 미끄러짐", "회전축 변화", "불규칙 회전", "흔들림 운동", "포물선 이동",
|
78 |
+
"무중력 부유", "수면 위 부유", "점프/도약", "슬라이딩", "롤링", "자유 낙하",
|
79 |
+
"왕복 운동", "탄성 튕김", "관통", "회피 움직임", "지그재그 이동", "스윙 운동"
|
80 |
+
],
|
81 |
+
"시간 관련 변화": [
|
82 |
+
"노화/풍화", "마모/부식", "색 바램/변색", "손상/회복", "수명 주기 변화",
|
83 |
+
"사용자 상호작용에 따른 적응", "학습 기반 형태 최적화", "시간에 따른 물성 변화",
|
84 |
+
"집단 기억 효과", "문화적 의미 변화", "지연 반응", "이전 상태 의존 변화",
|
85 |
+
"점진적 시간 변화", "진화적 변화", "주기적 재생", "계절 변화 적응",
|
86 |
+
"생체리듬 변화", "생애 주기 단계", "성장/퇴화", "자가 복구/재생",
|
87 |
+
"자연 순환 적응", "지속성/일시성", "기억 효과", "지연된 작용", "누적 효과"
|
88 |
+
],
|
89 |
+
"빛과 시각 효과": [
|
90 |
+
"발광/소등", "빛 투과/차단", "빛 산란/집중", "색상 스펙트럼 변화", "빛 회절",
|
91 |
+
"빛 간섭", "홀로그램 생성", "레이저 효과", "빛 편광", "형광/인광",
|
92 |
+
"자외선/적외선 발광", "광학적 착시", "빛 굴절", "그림자 생성/제거",
|
93 |
+
"색수차 효과", "무지개 효과", "글로우 효과", "플래시 효과", "조명 패턴",
|
94 |
+
"빔 효과", "광 필터 효과", "빛의 방향성 변화", "투영 효과", "빛 감지/반응",
|
95 |
+
"광도 변화"
|
96 |
+
],
|
97 |
+
"소리와 진동 효과": [
|
98 |
+
"소리 발생/소멸", "음 높낮이 변화", "음량 변화", "음색 변화",
|
99 |
+
"공명/반공명", "음향 진동", "초음파/저음파 발생", "소리 집중/분산",
|
100 |
+
"음향 반사/흡수", "음향 도플러 효과", "음파 간섭", "음향 공진",
|
101 |
+
"진동 패턴 변화", "타악 효과", "음향 피드백", "음향 차폐/증폭",
|
102 |
+
"소리 지향성", "소리 왜곡", "비트 생성", "배음 생성", "주파수 변조",
|
103 |
+
"음향 충격파", "음향 필터링"
|
104 |
+
],
|
105 |
+
"열 관련 변화": [
|
106 |
+
"온도 상승/하강", "열 팽창/수축", "열 전달/차단", "압력 상승/하강",
|
107 |
+
"열 변화에 따른 자화", "엔트로피 변화", "열전기 효과", "자기장에 의한 열 변화",
|
108 |
+
"상태 변화 중 열 저장/방출", "열 스트레스 발생/해소", "급격한 온도 변화 영향",
|
109 |
+
"복사 냉각/가열", "발열/흡열", "열 분포 변화", "열 반사/흡수",
|
110 |
+
"냉각 응축", "열 활성화", "열 변색", "열 팽창 계수 변화", "열 안정성 변화",
|
111 |
+
"내열성/내한성", "자가 발열", "열적 평형/불균형", "열적 변형", "열 분산/집중"
|
112 |
+
],
|
113 |
+
"전기 및 자기 변화": [
|
114 |
+
"자성 생성/소멸", "전하량 증가/감소", "전기장 생성/소멸", "자기장 생성/소멸",
|
115 |
+
"초전도 상태 전환", "강유전체 특성 변화", "양자 상태 변화", "플라즈마 형성/소멸",
|
116 |
+
"스핀파 전달", "빛에 의한 전기 발생", "압력에 의한 전기 발생", "자기장 내 전류 변화",
|
117 |
+
"전기 저항 변화", "전기 전도성 변화", "정전기 발생/방전", "전자기 유도",
|
118 |
+
"전자기파 방출/흡수", "전기 용량 변화", "자기 이력 현상", "전기적 분극",
|
119 |
+
"전자 흐름 방향 변화", "전기적 공명", "전기적 차폐/노출", "자기 차폐/노출",
|
120 |
+
"자기장 정렬"
|
121 |
+
],
|
122 |
+
"화학적 변화": [
|
123 |
+
"표면 코팅 변화", "물질 성분 변화", "화학 반응 변화", "촉매 작용 시작/중단",
|
124 |
+
"빛에 의한 화학 반응", "전기에 의한 화학 반응", "단분자막 형성", "분자 수준 구조 변화",
|
125 |
+
"생체 모방 표면 변화", "환경 반응형 물질 변화", "주기적 화학 반응", "산화", "환원",
|
126 |
+
"고분자화", "물 분해", "화합", "방사선 영향", "산-염기 반응", "중화 반응",
|
127 |
+
"이온화", "화학적 흡착/탈착", "촉매 효율 변화", "효소 활성 변화", "발�� 반응",
|
128 |
+
"pH 변화", "화학적 평형 이동", "결합 형성/분해", "용해도 변화"
|
129 |
+
],
|
130 |
+
"생물학적 변화": [
|
131 |
+
"성장/위축", "세포 분열/사멸", "생물 발광", "신진대사 변화", "면역 반응",
|
132 |
+
"호르몬 분비", "신경 반응", "유전적 발현", "적응/진화", "생체리듬 변화",
|
133 |
+
"재생/치유", "노화/성숙", "생체 모방 변화", "바이오필름 형성", "생물학적 분해",
|
134 |
+
"효소 활성화/비활성화", "생물학적 신호 전달", "스트레스 반응", "체온 조절", "생물학적 시계 변화",
|
135 |
+
"세포외 기질 변화", "생체 역학적 반응", "세포 운동성", "세포 극성 변화", "영양 상태 변화"
|
136 |
+
],
|
137 |
+
"환경 상호작용": [
|
138 |
+
"온도 반응", "습도 반응", "기압 반응", "중력 반응", "자기장 반응",
|
139 |
+
"빛 반응", "소리 반응", "화학 물질 감지", "기계적 자극 감지", "전기 자극 반응",
|
140 |
+
"방사선 반응", "진동 감지", "pH 반응", "용매 반응", "기체 교환",
|
141 |
+
"환경 오염 반응", "날씨 반응", "계절 반응", "일주기 반응", "생태계 상호작용",
|
142 |
+
"공생/경쟁 반응", "포식/피식 관계", "군집 형성", "영역 설정", "이주/정착 패턴"
|
143 |
+
],
|
144 |
+
"비즈니스 아이디어": [
|
145 |
+
"시장 재정의/신규 시장 개척",
|
146 |
+
"비즈니스 모델 혁신/디지털 전환",
|
147 |
+
"고객 경험 혁신/서비스 혁신",
|
148 |
+
"협력 및 파트너십 강화/생태계 구축",
|
149 |
+
"글로벌 확장/지역화 전략",
|
150 |
+
"운영 효율성 증대/원가 절감",
|
151 |
+
"브랜드 리포지셔닝/이미지 전환",
|
152 |
+
"지속 가능한 성장/사회적 가치 창출",
|
153 |
+
"데이터 기반 의사결정/AI 도입",
|
154 |
+
"신기술 융합/혁신 투자"
|
155 |
+
]
|
156 |
}
|
157 |
|
158 |
+
physical_transformation_categories_en = {
|
159 |
+
"Sensor Functions": [
|
160 |
+
"Visual sensor/detection", "Auditory sensor/detection", "Tactile sensor/detection", "Taste sensor/detection", "Olfactory sensor/detection",
|
161 |
+
"Temperature sensor/detection", "Humidity sensor/detection", "Pressure sensor/detection", "Acceleration sensor/detection", "Rotational sensor/detection",
|
162 |
+
"Proximity sensor/detection", "Position sensor/detection", "Motion sensor/detection", "Gas sensor/detection", "Infrared sensor/detection",
|
163 |
+
"Ultraviolet sensor/detection", "Radiation sensor/detection", "Magnetic sensor/detection", "Electric field sensor/detection", "Chemical sensor/detection",
|
164 |
+
"Biosignal sensor/detection", "Vibration sensor/detection", "Noise sensor/detection", "Light intensity sensor/detection", "Light wavelength sensor/detection",
|
165 |
+
"Tilt sensor/detection", "pH sensor/detection", "Current sensor/detection", "Voltage sensor/detection", "Image sensor/detection",
|
166 |
+
"Distance sensor/detection", "Depth sensor/detection", "Gravity sensor/detection", "Speed sensor/detection", "Flow sensor/detection",
|
167 |
+
"Water level sensor/detection", "Turbidity sensor/detection", "Salinity sensor/detection", "Metal detection", "Piezoelectric sensor/detection",
|
168 |
+
"Photovoltaic sensor/detection", "Thermocouple sensor/detection", "Hall effect sensor/detection", "Ultrasonic sensor/detection", "Radar sensor/detection",
|
169 |
+
"Lidar sensor/detection", "Touch sensor/detection", "Gesture sensor/detection", "Heart rate sensor/detection", "Blood pressure sensor/detection"
|
170 |
+
],
|
171 |
+
"Size and Shape Change": [
|
172 |
+
"Volume increase/decrease", "Length increase/decrease", "Width increase/decrease", "Height increase/decrease",
|
173 |
+
"Density change", "Weight increase/decrease", "Shape deformation", "State change", "Uneven deformation",
|
174 |
+
"Complex shape deformation", "Twisting/entwining", "Non-uniform expansion/contraction", "Rounded/sharpened edges",
|
175 |
+
"Cracking/splitting", "Fragmentation", "Water resistance", "Dust resistance", "Denting/recovery",
|
176 |
+
"Folding/unfolding", "Compression/expansion", "Stretching/contraction", "Wrinkling/flattening", "Crushing/hardening",
|
177 |
+
"Rolling/unrolling", "Bending/curving"
|
178 |
+
],
|
179 |
+
# 이하 동일 패턴으로 영문 버전 생략 (상동)
|
180 |
}
|
181 |
|
182 |
# ──────────────────────────────── Logging ────────────────────────────────
|
|
|
185 |
|
186 |
# ──────────────────────────────── OpenAI Client ──────────────────────────
|
187 |
|
|
|
188 |
@st.cache_resource
|
189 |
def get_openai_client():
|
190 |
"""Create an OpenAI client with timeout and retry settings."""
|
|
|
195 |
timeout=60.0, # 타임아웃 60초로 설정
|
196 |
max_retries=3 # 재시도 횟수 3회로 설정
|
197 |
)
|
198 |
+
|
199 |
+
# ──────────────────────────────── New System Prompt for Idea Generation ─────────────────────
|
200 |
+
def get_idea_system_prompt() -> str:
|
201 |
"""
|
202 |
+
아이디어 생성용 시스템 프롬프트:
|
203 |
+
- 제공된 physical_transformation_categories를 활용
|
204 |
+
- 3가지 발상 접근: (1) 유사성 기반 증강, (2) 반유사성(대조) 기반 발상의 전환, (3) 무작위(우연적 착안)
|
205 |
+
- 각 아이디어를 논리적으로 설명 가능하도록 지시
|
206 |
+
- 아이디어마다 이미지 프롬프트도 함께 제시할 것
|
207 |
"""
|
208 |
+
prompt = """
|
209 |
+
You are an advanced Idea Generation AI. Your goal is to take the user's prompt (topic, concept, or question) and create **3 distinct ideas** around it, drawing from the vast categories of **physical or conceptual transformations**. Specifically, you must produce:
|
210 |
+
|
211 |
+
1. **Similarity-based Augmentation**
|
212 |
+
- Build upon the user's original concept by closely aligning to its core attributes.
|
213 |
+
- Reference relevant categories or subcategories that naturally extend or amplify the original idea.
|
214 |
+
- Provide a practical, logical explanation.
|
215 |
+
|
216 |
+
2. **Contrarian/Inverse Approach**
|
217 |
+
- Intentionally invert or contradict the core assumptions of the original concept.
|
218 |
+
- Use contrasting categories to spark unconventional thinking.
|
219 |
+
- Clearly justify why this contrarian perspective might reveal novel possibilities.
|
220 |
+
|
221 |
+
3. **Random (Fortuitous Spark)**
|
222 |
+
- Randomly select one or more categories or subcategories from any domain to introduce surprising or serendipitous ideas.
|
223 |
+
- Show how this unexpected combination can yield fresh insights or opportunities.
|
224 |
+
|
225 |
+
For each of the 3 approaches:
|
226 |
+
- Provide a concise name or title for the idea.
|
227 |
+
- Offer a clear, structured explanation (a few paragraphs) on how this idea works or could be realized.
|
228 |
+
- Incorporate references to the relevant category or categories from the provided classification, explaining how they link to the idea.
|
229 |
+
- Suggest a short, single-line **English prompt for generating an image** that best represents that idea.
|
230 |
+
|
231 |
+
Finally:
|
232 |
+
- Return your answer in Markdown format with headings (e.g., "## Idea 1: ...", "## Idea 2: ...", "## Idea 3: ...").
|
233 |
+
- Do **not** mention that you are responding with "3 ideas" because of a prompt or instruction. Simply present them as the final structured output.
|
234 |
+
- Never mention "blog," "SEO," or "prompt" instructions in your final text.
|
235 |
+
- Do **not** mention that you're an AI. Present the content as a straightforward creative text for each idea.
|
236 |
+
|
237 |
+
---
|
238 |
+
**Important**: You have access to a list of "physical_transformation_categories" (in Korean) and possibly an English version. Use them to anchor or inspire each idea, but do not simply copy the entire list. Instead, selectively reference a few relevant subcategories.
|
239 |
+
"""
|
240 |
+
return prompt.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
241 |
|
242 |
# ──────────────────────────────── Brave Search API ────────────────────────
|
243 |
@st.cache_data(ttl=3600)
|
|
|
247 |
Returns fields: index, title, link, snippet, displayed_link
|
248 |
"""
|
249 |
if not BRAVE_KEY:
|
250 |
+
raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) 환경 변수가 비어있습니다.")
|
251 |
|
252 |
headers = {
|
253 |
"Accept": "application/json",
|
|
|
292 |
return []
|
293 |
|
294 |
def mock_results(query: str) -> str:
|
295 |
+
"""Fallback if search API fails"""
|
296 |
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
297 |
return (f"# Fallback Search Content (Generated: {ts})\n\n"
|
298 |
+
f"The web search API request failed. Please generate the ideas based on general knowledge about '{query}'.\n\n"
|
299 |
+
f"You may consider aspects such as:\n\n"
|
300 |
+
f"- Basic definition or concept of {query}\n"
|
301 |
+
f"- Commonly known facts or challenges\n"
|
302 |
+
f"- Potential categories from the transformation list\n\n"
|
|
|
303 |
f"Note: This is fallback guidance, not real-time data.\n\n")
|
304 |
|
305 |
def do_web_search(query: str) -> str:
|
|
|
310 |
logging.warning("No search results, using fallback content")
|
311 |
return mock_results(query)
|
312 |
|
313 |
+
hdr = "# Web Search Results\nUse the information below to inspire or validate your ideas.\n\n"
|
314 |
body = "\n".join(
|
315 |
f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
|
316 |
f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
|
|
|
386 |
def process_pdf_file(file):
|
387 |
"""Handle PDF file"""
|
388 |
try:
|
|
|
389 |
file_bytes = file.read()
|
390 |
file.seek(0)
|
391 |
|
|
|
392 |
pdf_file = io.BytesIO(file_bytes)
|
393 |
reader = PyPDF2.PdfReader(pdf_file, strict=False)
|
394 |
|
|
|
395 |
result = f"## PDF File: {file.name}\n\n"
|
396 |
result += f"- Total pages: {len(reader.pages)}\n\n"
|
397 |
|
|
|
398 |
max_pages = min(5, len(reader.pages))
|
399 |
all_text = ""
|
400 |
|
|
|
405 |
|
406 |
current_page_text = f"### Page {i+1}\n\n"
|
407 |
if page_text and len(page_text.strip()) > 0:
|
|
|
408 |
if len(page_text) > 1500:
|
409 |
current_page_text += page_text[:1500] + "...(truncated)...\n\n"
|
410 |
else:
|
411 |
current_page_text += page_text + "\n\n"
|
412 |
else:
|
413 |
+
current_page_text += "(No text could be extracted)\n\n"
|
414 |
|
415 |
all_text += current_page_text
|
416 |
|
|
|
417 |
if len(all_text) > 8000:
|
418 |
all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
|
419 |
break
|
|
|
438 |
return None
|
439 |
|
440 |
result = "# Uploaded File Contents\n\n"
|
441 |
+
result += "Below is the content from the files provided by the user. Integrate this data as needed for generating ideas.\n\n"
|
442 |
|
443 |
for file in files:
|
444 |
try:
|
|
|
475 |
logging.error(e)
|
476 |
return None, str(e)
|
477 |
|
478 |
+
def md_to_html(md: str, title="Idea Output"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
"""Convert Markdown to HTML."""
|
480 |
return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
|
481 |
|
482 |
def keywords(text: str, top=5):
|
483 |
+
"""Simple keyword extraction (for web search)."""
|
484 |
cleaned = re.sub(r"[^가-힣a-zA-Z0-9\s]", "", text)
|
485 |
return " ".join(cleaned.split()[:top])
|
486 |
|
487 |
# ──────────────────────────────── Streamlit UI ────────────────────────────
|
488 |
+
def idea_generator_app():
|
489 |
+
st.title("Creative Idea Generator")
|
490 |
|
491 |
# Set default session state
|
492 |
if "ai_model" not in st.session_state:
|
493 |
+
st.session_state.ai_model = "gpt-4.1-mini"
|
494 |
if "messages" not in st.session_state:
|
495 |
st.session_state.messages = []
|
496 |
if "auto_save" not in st.session_state:
|
497 |
st.session_state.auto_save = True
|
498 |
if "generate_image" not in st.session_state:
|
499 |
+
st.session_state.generate_image = True # 기본값: True
|
500 |
if "web_search_enabled" not in st.session_state:
|
501 |
st.session_state.web_search_enabled = True
|
|
|
|
|
|
|
|
|
|
|
|
|
502 |
|
503 |
# Sidebar UI
|
504 |
sb = st.sidebar
|
505 |
+
sb.title("Idea Generator Settings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
507 |
sb.toggle("Auto Save", key="auto_save")
|
508 |
sb.toggle("Auto Image Generation", key="generate_image")
|
509 |
|
|
|
511 |
st.session_state.web_search_enabled = web_search_enabled
|
512 |
|
513 |
if web_search_enabled:
|
514 |
+
sb.info("✅ Web search results will be integrated.")
|
515 |
+
|
516 |
+
# 예시 주제들 (원래 예시 블로그 토픽 -> 이제는 예시 아이디어 주제로 전환)
|
517 |
+
example_topics = {
|
518 |
+
"example1": "AI로 인한 비즈니스 모델 변화",
|
519 |
+
"example2": "스마트 소재를 활용한 친환경 건축",
|
520 |
+
"example3": "미래형 교육 서비스 구상"
|
521 |
+
}
|
522 |
|
523 |
+
sb.subheader("Example Prompts")
|
524 |
+
c1, c2, c3 = sb.columns(3)
|
525 |
+
if c1.button("AI & 비즈니스", key="ex1"):
|
526 |
+
process_example(example_topics["example1"])
|
527 |
+
if c2.button("스마트 소재 건축", key="ex2"):
|
528 |
+
process_example(example_topics["example2"])
|
529 |
+
if c3.button("미래형 교육", key="ex3"):
|
530 |
+
process_example(example_topics["example3"])
|
531 |
+
|
532 |
+
# Download the latest ideas
|
533 |
+
latest_ideas = next(
|
534 |
(m["content"] for m in reversed(st.session_state.messages)
|
535 |
if m["role"] == "assistant" and m["content"].strip()),
|
536 |
None
|
537 |
)
|
538 |
+
if latest_ideas:
|
539 |
+
title_match = re.search(r"# (.*?)(\n|$)", latest_ideas)
|
540 |
+
title = title_match.group(1).strip() if title_match else "ideas"
|
541 |
+
sb.subheader("Download Latest Ideas")
|
542 |
d1, d2 = sb.columns(2)
|
543 |
+
d1.download_button("Download as Markdown", latest_ideas,
|
544 |
file_name=f"{title}.md", mime="text/markdown")
|
545 |
+
d2.download_button("Download as HTML", md_to_html(latest_ideas, title),
|
546 |
file_name=f"{title}.html", mime="text/html")
|
547 |
|
548 |
# JSON conversation record upload
|
|
|
557 |
# JSON conversation record download
|
558 |
if sb.button("Download Conversation as JSON"):
|
559 |
sb.download_button(
|
560 |
+
"Save JSON",
|
561 |
data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
|
562 |
file_name="chat_history.json",
|
563 |
mime="application/json"
|
564 |
)
|
565 |
|
566 |
# File Upload
|
567 |
+
st.subheader("File Upload (Optional)")
|
568 |
uploaded_files = st.file_uploader(
|
569 |
+
"Upload files to reference in the idea generation (txt, csv, pdf)",
|
570 |
type=["txt", "csv", "pdf"],
|
571 |
accept_multiple_files=True,
|
572 |
key="file_uploader"
|
|
|
574 |
|
575 |
if uploaded_files:
|
576 |
file_count = len(uploaded_files)
|
577 |
+
st.success(f"{file_count} files uploaded.")
|
578 |
|
579 |
with st.expander("Preview Uploaded Files", expanded=False):
|
580 |
for idx, file in enumerate(uploaded_files):
|
|
|
611 |
if pc > 0:
|
612 |
try:
|
613 |
page_text = reader.pages[0].extract_text()
|
614 |
+
preview = page_text[:500] if page_text else "(No text)"
|
615 |
st.text_area("Preview of the first page", preview + "...", height=150)
|
616 |
except:
|
617 |
st.warning("Failed to extract text from the first page")
|
|
|
621 |
if idx < file_count - 1:
|
622 |
st.divider()
|
623 |
|
624 |
+
# Display existing messages in chat
|
625 |
for m in st.session_state.messages:
|
626 |
with st.chat_message(m["role"]):
|
627 |
st.markdown(m["content"])
|
628 |
if "image" in m:
|
629 |
st.image(m["image"], caption=m.get("image_caption", ""))
|
630 |
|
631 |
+
# User input for idea generation
|
632 |
+
prompt = st.chat_input("Enter a topic or concept to generate 3 new ideas.")
|
633 |
if prompt:
|
634 |
process_input(prompt, uploaded_files)
|
635 |
|
636 |
+
# Sidebar footer
|
|
|
637 |
sb.markdown("---")
|
638 |
+
sb.markdown("Created by [Ginigen.com](https://ginigen.com) | [YouTube](https://www.youtube.com/@ginipickaistudio)")
|
|
|
639 |
|
640 |
|
641 |
def process_example(topic):
|
642 |
+
"""Handle example prompts."""
|
643 |
process_input(topic, [])
|
644 |
|
645 |
def process_input(prompt: str, uploaded_files):
|
|
|
659 |
has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
|
660 |
|
661 |
try:
|
662 |
+
status = st.status("Preparing to generate ideas...")
|
|
|
663 |
status.update(label="Initializing client...")
|
664 |
+
|
665 |
client = get_openai_client()
|
666 |
|
667 |
+
# Prepare system prompt
|
668 |
+
sys_prompt = get_idea_system_prompt()
|
669 |
+
|
670 |
+
# Optional: gather search results
|
671 |
search_content = None
|
672 |
if use_web_search:
|
673 |
status.update(label="Performing web search...")
|
674 |
with st.spinner("Searching the web..."):
|
675 |
search_content = do_web_search(keywords(prompt, top=5))
|
676 |
+
|
677 |
+
# File contents
|
678 |
file_content = None
|
679 |
if has_uploaded_files:
|
680 |
status.update(label="Processing uploaded files...")
|
681 |
with st.spinner("Analyzing files..."):
|
682 |
file_content = process_uploaded_files(uploaded_files)
|
683 |
+
|
684 |
+
# Build messages
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
685 |
user_content = prompt
|
|
|
|
|
686 |
if search_content:
|
687 |
user_content += "\n\n" + search_content
|
|
|
|
|
688 |
if file_content:
|
689 |
user_content += "\n\n" + file_content
|
690 |
+
|
691 |
+
api_messages = [
|
692 |
+
{"role": "system", "content": sys_prompt},
|
693 |
+
{"role": "user", "content": user_content}
|
694 |
+
]
|
695 |
+
|
696 |
+
# OpenAI API streaming
|
697 |
+
status.update(label="Generating creative ideas...")
|
698 |
try:
|
|
|
699 |
stream = client.chat.completions.create(
|
700 |
+
model="gpt-4.1-mini",
|
701 |
messages=api_messages,
|
702 |
temperature=1,
|
703 |
max_tokens=MAX_TOKENS,
|
704 |
top_p=1,
|
705 |
+
stream=True
|
706 |
)
|
|
|
|
|
707 |
for chunk in stream:
|
708 |
+
if (chunk.choices
|
709 |
+
and len(chunk.choices) > 0
|
710 |
+
and chunk.choices[0].delta.content is not None):
|
711 |
content_delta = chunk.choices[0].delta.content
|
712 |
full_response += content_delta
|
713 |
message_placeholder.markdown(full_response + "▌")
|
|
|
|
|
714 |
message_placeholder.markdown(full_response)
|
715 |
+
status.update(label="Ideas created!", state="complete")
|
716 |
+
|
717 |
except Exception as api_error:
|
718 |
error_message = str(api_error)
|
719 |
logging.error(f"API error: {error_message}")
|
720 |
status.update(label=f"Error: {error_message}", state="error")
|
721 |
+
raise Exception(f"Idea generation error: {error_message}")
|
722 |
+
|
723 |
+
# Store final text
|
724 |
answer_entry_saved = False
|
725 |
+
|
726 |
+
# 자동 이미지 생성이 활성화되어 있다면
|
727 |
if st.session_state.generate_image and full_response:
|
728 |
+
# 아이디어를 3개로 구분하고, 각 아이디어마다 이미지 프롬프트를 추출
|
729 |
+
# 간단한 방식: 정규식으로 `Image Prompt:` 형태를 찾는다고 가정 (시스템프롬프트에 지시)
|
730 |
+
# 혹은 단순히 한 번만 추출 -> 여기서는 3개 아이디어 각각을 찾기 위해 나눠본다.
|
731 |
+
# 일단은 전체 답변에서 "English prompt for generating an image" 부분을 찾는다.
|
732 |
+
|
733 |
+
# 매우 단순한 파싱 예시 (개선 가능)
|
734 |
+
idea_sections = re.split(r"(## Idea \d+:)", full_response)
|
735 |
+
# idea_sections는 ['', '## Idea 1:', ' ...', '## Idea 2:', ' ...', '## Idea 3:', ' ...'] 형태
|
736 |
+
|
737 |
+
# 다시 묶어서 [('## Idea 1:', idea_text), ('## Idea 2:', idea_text), ...] 형태로
|
738 |
+
pairs = []
|
739 |
+
for i in range(1, len(idea_sections), 2):
|
740 |
+
idea_title = idea_sections[i].strip()
|
741 |
+
idea_body = idea_sections[i+1].strip() if i+1 < len(idea_sections) else ""
|
742 |
+
pairs.append((idea_title, idea_body))
|
743 |
+
|
744 |
+
# 각 아이디어마다 이미지 생성
|
745 |
+
for idx, (title, text_block) in enumerate(pairs, start=1):
|
746 |
+
image_prompt_match = re.search(r"(?i)(image prompt\s*\:\s*)(.+)", text_block)
|
747 |
+
if image_prompt_match:
|
748 |
+
raw_prompt = image_prompt_match.group(2).strip()
|
749 |
+
# 만약 문장 끝에 불필요한 구두점이나 줄바꿈이 있을 시 제거
|
750 |
+
raw_prompt = re.sub(r"[\r\n]+", " ", raw_prompt)
|
751 |
+
raw_prompt = re.sub(r"[\"'`]", "", raw_prompt)
|
752 |
+
# 이미지 생성
|
753 |
+
with st.spinner(f"Generating image for {title}..."):
|
754 |
+
img, cap = generate_image(raw_prompt)
|
755 |
+
if img:
|
756 |
+
st.image(img, caption=f"{title} - {cap}")
|
757 |
+
# 대화에 저장
|
758 |
+
st.session_state.messages.append({
|
759 |
+
"role": "assistant",
|
760 |
+
"content": "",
|
761 |
+
"image": img,
|
762 |
+
"image_caption": f"{title} - {cap}"
|
763 |
+
})
|
764 |
+
|
765 |
+
# 3개 이미지 생성 프로세스�� 마친 후 최종 텍스트를 저장
|
766 |
+
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
767 |
+
answer_entry_saved = True
|
768 |
+
|
769 |
if not answer_entry_saved and full_response:
|
770 |
+
# 이미지 생성이 비활성화거나 실패했을 경우 텍스트만 저장
|
771 |
st.session_state.messages.append({"role": "assistant", "content": full_response})
|
772 |
|
773 |
# Download buttons
|
774 |
if full_response:
|
775 |
+
st.subheader("Download This Output")
|
776 |
c1, c2 = st.columns(2)
|
777 |
c1.download_button(
|
778 |
"Markdown",
|
|
|
787 |
mime="text/html"
|
788 |
)
|
789 |
|
790 |
+
# Auto-save
|
791 |
if st.session_state.auto_save and st.session_state.messages:
|
792 |
try:
|
793 |
fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
|
|
|
803 |
ans = f"An error occurred while processing your request: {error_message}"
|
804 |
st.session_state.messages.append({"role": "assistant", "content": ans})
|
805 |
|
|
|
806 |
# ──────────────────────────────── main ────────────────────────────────────
|
807 |
def main():
|
808 |
+
idea_generator_app()
|
809 |
|
810 |
if __name__ == "__main__":
|
811 |
+
main()
|