ginipick commited on
Commit
d3caf4c
·
verified ·
1 Parent(s): 3358147

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +330 -439
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
- # Blog template and style definitions (in English)
20
- BLOG_TEMPLATES = {
21
- "ginigen": "Recommended style by Ginigen",
22
- "standard": "Standard 8-step framework blog",
23
- "tutorial": "Step-by-step tutorial format",
24
- "review": "Product/service review format",
25
- "storytelling": "Storytelling format",
26
- "seo_optimized": "SEO-optimized blog"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  }
28
 
29
- BLOG_TONES = {
30
- "professional": "Professional and formal tone",
31
- "casual": "Friendly and conversational tone",
32
- "humorous": "Humorous approach",
33
- "storytelling": "Story-driven approach"
34
- }
35
-
36
- # Example blog topics
37
- EXAMPLE_TOPICS = {
38
- "example1": "Changes to the real estate tax system in 2025: Impact on average households and tax-saving strategies",
39
- "example2": "Summer festivals in 2025: A comprehensive guide to major regional events and hidden attractions",
40
- "example3": "Emerging industries to watch in 2025: An investment guide focused on AI opportunities"
 
 
 
 
 
 
 
 
 
 
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
- # ──────────────────────────────── Blog Creation System Prompt ─────────────
62
- def get_system_prompt(template="ginigen", tone="professional", word_count=1750, include_search_results=False, include_uploaded_files=False) -> str:
63
  """
64
- Generate a system prompt that includes:
65
- - The 8-step blog writing framework
66
- - The selected template and tone
67
- - Guidelines for using web search results and uploaded files
 
68
  """
69
-
70
- # Ginigen recommended style prompt (English version)
71
- ginigen_prompt = """
72
-
73
- You are an expert English SEO copywriter.
74
-
75
- Purpose
76
- - Create a blog post based on the given YouTube video script that captivates both search engines and readers.
77
- - Always follow the 4 writing principles: **[Lead with the main point → Keep it simple and short → Emphasize reader benefits → Call to action]**.
78
-
79
- Complete Format (Use markdown, avoid unnecessary explanations)
80
- 1. **Title**
81
- - Emoji + Curiosity question/exclamation + Core keywords (Within 70 characters)
82
- - Example: `# 🧬 Can Reducing Inflammation Help You Lose Weight?! 5 Amazing Benefits of Quercetin`
83
-
84
- 2. **Hook (2-3 lines)**
85
- - Present problem → Mention solution keyword → Summarize the benefit of reading this post
86
-
87
- 3. `---` Divider
88
-
89
- 4. **Section 1: Core Concept Introduction**
90
- - `## 🍏 What is [Keyword]?`
91
- - 1-2 paragraphs definition + 📌 One-line summary
92
-
93
- 5. `---`
94
-
95
- 6. **Section 2: 5 Benefits/Reasons**
96
- - `## 💪 5 Reasons Why [Keyword] Is Beneficial`
97
- - Each subsection format:
98
-
99
-
100
- ### 1. [Keyword-focused subheading]
101
- 1-2 paragraphs explanation
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) environment variable is empty.")
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 results if API fails"""
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 blog based on any pre-existing knowledge about '{query}'.\n\n"
358
- f"You may consider the following points:\n\n"
359
- f"- Basic concepts and importance of {query}\n"
360
- f"- Commonly known related statistics or trends\n"
361
- f"- Typical expert opinions on this subject\n"
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 enhance the reliability of your blog. When you quote, please cite the source, and add a References section at the end of the blog.\n\n"
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 from this page)\n\n"
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 a main source of information for the blog.\n\n"
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 extract_image_prompt(blog_text: str, topic: str):
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 ginigen_app():
578
- st.title("Ginigen Blog")
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 = False
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("Blog Settings")
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
- st.sidebar.info("✅ Web search results will be integrated into the blog.")
 
 
 
 
 
 
 
641
 
642
- # Download the latest blog (markdown/HTML)
643
- latest_blog = next(
 
 
 
 
 
 
 
 
 
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 latest_blog:
649
- title_match = re.search(r"# (.*?)(\n|$)", latest_blog)
650
- title = title_match.group(1).strip() if title_match else "blog"
651
- sb.subheader("Download Latest Blog")
652
  d1, d2 = sb.columns(2)
653
- d1.download_button("Download as Markdown", latest_blog,
654
  file_name=f"{title}.md", mime="text/markdown")
655
- d2.download_button("Download as HTML", md_to_html(latest_blog, title),
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 be referenced in your blog (txt, csv, pdf)",
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. They will be referenced in the blog.")
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 extracted)"
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 blog topic or keywords.")
743
  if prompt:
744
  process_input(prompt, uploaded_files)
745
 
746
-
747
- # 사이드바 하단 배지(링크) 추가
748
  sb.markdown("---")
749
- sb.markdown("Created by [https://ginigen.com](https://ginigen.com) | [YouTube Channel](https://www.youtube.com/@ginipickaistudio)")
750
-
751
 
752
 
753
  def process_example(topic):
754
- """Process the selected example topic."""
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 conversation messages
781
- messages = []
782
-
783
- # Web search
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
- # Process uploaded files → content
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 system prompt
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
- api_messages.append({"role": "user", "content": user_content})
827
-
828
- # OpenAI API 스트리밍 호출 - 고정 모델 "gpt-4.1-mini" 사용
 
 
 
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 chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
 
 
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="Blog completed!", state="complete")
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"Blog generation error: {error_message}")
856
-
857
- # 이미지 생성
858
  answer_entry_saved = False
 
 
859
  if st.session_state.generate_image and full_response:
860
- with st.spinner("Generating image..."):
861
- try:
862
- ip = extract_image_prompt(full_response, prompt)
863
- img, cap = generate_image(ip)
864
- if img:
865
- st.image(img, caption=cap)
866
- st.session_state.messages.append({
867
- "role": "assistant",
868
- "content": full_response,
869
- "image": img,
870
- "image_caption": cap
871
- })
872
- answer_entry_saved = True
873
- except Exception as img_error:
874
- logging.error(f"Image generation error: {str(img_error)}")
875
- st.warning("이미지 생성에 실패했습니다. 블로그 콘텐츠만 저장됩니다.")
876
-
877
- # Save the answer if not saved above
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Blog")
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 save
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
- ginigen_app()
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()