ginipick commited on
Commit
f025afc
Β·
verified Β·
1 Parent(s): 14de218

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +19 -886
app.py CHANGED
@@ -1,893 +1,26 @@
1
- # ──────────────────────────────── Imports ────────────────────────────────
2
- import os, json, re, logging, requests, markdown, time, io
3
- from datetime import datetime
4
-
5
  import streamlit as st
6
- from openai import OpenAI # OpenAI 라이브러리
7
-
8
- from gradio_client import Client
9
- import pandas as pd
10
- import PyPDF2 # For handling PDF files
11
-
12
- # ──────────────────────────────── Environment Variables / Constants ─────────────────────────
13
- OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
14
- BRAVE_KEY = os.getenv("SERPHOUSE_API_KEY", "") # Keep this name
15
- BRAVE_ENDPOINT = "https://api.search.brave.com/res/v1/web/search"
16
- IMAGE_API_URL = "http://211.233.58.201:7896"
17
- MAX_TOKENS = 7999
18
-
19
- # Blog template and style definitions (in English)
20
- BLOG_TEMPLATES = {
21
- "ginigen": "Recommended style by Ginigen",
22
- "standard": "Standard 8-step framework blog",
23
- "tutorial": "Step-by-step tutorial format",
24
- "review": "Product/service review format",
25
- "storytelling": "Storytelling format",
26
- "seo_optimized": "SEO-optimized blog"
27
- }
28
-
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 ────────────────────────────────
44
- logging.basicConfig(level=logging.INFO,
45
- format="%(asctime)s - %(levelname)s - %(message)s")
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."""
53
- if not OPENAI_API_KEY:
54
- raise RuntimeError("⚠️ OPENAI_API_KEY ν™˜κ²½ λ³€μˆ˜κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€.")
55
- return OpenAI(
56
- api_key=OPENAI_API_KEY,
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
- You are an expert English SEO copywriter.
73
- β—† Purpose
74
- - Create a blog post based on the given YouTube video script that captivates both search engines and readers.
75
- - Always follow the 4 writing principles: **[Lead with the main point β†’ Keep it simple and short β†’ Emphasize reader benefits β†’ Call to action]**.
76
- β—† Complete Format (Use markdown, avoid unnecessary explanations)
77
- 1. **Title**
78
- - Emoji + Curiosity question/exclamation + Core keywords (Within 70 characters)
79
- - Example: `# 🧬 Can Reducing Inflammation Help You Lose Weight?! 5 Amazing Benefits of Quercetin`
80
- 2. **Hook (2-3 lines)**
81
- - Present problem β†’ Mention solution keyword β†’ Summarize the benefit of reading this post
82
- 3. `---` Divider
83
- 4. **Section 1: Core Concept Introduction**
84
- - `## 🍏 What is [Keyword]?`
85
- - 1-2 paragraphs definition + πŸ“Œ One-line summary
86
- 5. `---`
87
- 6. **Section 2: 5 Benefits/Reasons**
88
- - `## πŸ’ͺ 5 Reasons Why [Keyword] Is Beneficial`
89
- - Each subsection format:
90
-
91
- ### 1. [Keyword-focused subheading]
92
- 1-2 paragraphs explanation
93
- > βœ” One-line key point emphasis
94
- - Total of 5 items
95
- 7. **Section 3: Consumption/Usage Methods**
96
- - `## πŸ₯— How to Use [Keyword] Effectively!`
97
- - Emoji bullet list of around 5 items + Additional tips
98
- 8. `---`
99
- 9. **Concluding Call to Action**
100
- - `## πŸ“Œ Conclusion – Start Using [Keyword] Today!`
101
- - 2-3 sentences on benefits/changes β†’ **Action directive** (purchase, subscribe, share, etc.)
102
- 10. `---`
103
- 11. **Key Summary Table**
104
- | Item | Effect |
105
- |---|---|
106
- | [Keyword] | [Effect summary] |
107
- | Key foods/products | [List] |
108
- 12. `---`
109
- 13. **Quiz & CTA**
110
- - Simple Q&A quiz (1 question) β†’ Reveal answer
111
- - "If you found this helpful, please share/comment" phrase
112
- - Preview of next post
113
- β—† Additional Guidelines
114
- - Total length 1,200-1,800 words.
115
- - Use simple vocabulary and short sentences, enhance readability with emojis, bold text, and quoted sections.
116
- - Increase credibility with specific numbers, research results, and analogies.
117
- - No meta-mentions of "prompts" or "instructions".
118
- - Use conversational but professional tone throughout.
119
- - Minimize expressions like "according to research" if no external sources are provided.
120
- β—† Output
121
- - Return **only the completed blog post** in the above format. No additional text.
122
- """
123
-
124
- # Standard 8-step framework (English version)
125
- base_prompt = """
126
- 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:
127
-
128
- Reader Connection Phase
129
- 1.1. Friendly greeting to build rapport
130
- 1.2. Reflect actual reader concerns through introductory questions
131
- 1.3. Stimulate immediate interest in the topic
132
-
133
- Problem Definition Phase
134
- 2.1. Define the reader's pain points in detail
135
- 2.2. Analyze the urgency and impact of the problem
136
- 2.3. Build a consensus on why it needs to be solved
137
-
138
- Establish Expertise Phase
139
- 3.1. Analyze based on objective data
140
- 3.2. Cite expert views and research findings
141
- 3.3. Use real-life examples to further clarify the issue
142
-
143
- Solution Phase
144
- 4.1. Provide step-by-step guidance
145
- 4.2. Suggest practical tips that can be applied immediately
146
- 4.3. Mention potential obstacles and how to overcome them
147
-
148
- Build Trust Phase
149
- 5.1. Present actual success stories
150
- 5.2. Quote real user feedback
151
- 5.3. Use objective data to prove effectiveness
152
-
153
- Action Phase
154
- 6.1. Suggest the first clear step the reader can take
155
- 6.2. Urge timely action by emphasizing urgency
156
- 6.3. Motivate by highlighting incentives or benefits
157
-
158
- Authenticity Phase
159
- 7.1. Transparently disclose any limits of the solution
160
- 7.2. Admit that individual experiences may vary
161
- 7.3. Mention prerequisites or cautionary points
162
-
163
- Relationship Continuation Phase
164
- 8.1. Conclude with sincere gratitude
165
- 8.2. Preview upcoming content to build anticipation
166
- 8.3. Provide channels for further communication
167
- """
168
-
169
- # Additional guidelines for each template
170
- template_guides = {
171
- "tutorial": """
172
- This blog should be in a tutorial style:
173
- - Clearly state the goal and the final outcome first
174
- - Provide step-by-step explanations with clear separations
175
- - Indicate where images could be inserted for each step
176
- - Mention approximate time requirements and difficulty level
177
- - List necessary tools or prerequisite knowledge
178
- - Give troubleshooting tips and common mistakes to avoid
179
- - Conclude with suggestions for next steps or advanced applications
180
- """,
181
- "review": """
182
- This blog should be in a review style:
183
- - Separate objective facts from subjective opinions
184
- - Clearly list your evaluation criteria
185
- - Discuss both pros and cons in a balanced way
186
- - Compare with similar products/services
187
- - Specify the target audience for whom it is suitable
188
- - Provide concrete use cases and outcomes
189
- - Conclude with a final recommendation or alternatives
190
- """,
191
- "storytelling": """
192
- This blog should be in a storytelling style:
193
- - Start with a real or hypothetical person or case
194
- - Emphasize emotional connection with the problem scenario
195
- - Follow a narrative structure centered on conflict and resolution
196
- - Include meaningful insights or lessons learned
197
- - Maintain an emotional thread the reader can relate to
198
- - Balance storytelling with useful information
199
- - Encourage the reader to reflect on their own story
200
- """,
201
- "seo_optimized": """
202
- This blog should be SEO-optimized:
203
- - Include the main keyword in the title, headings, and first paragraph
204
- - Spread related keywords naturally throughout the text
205
- - Keep paragraphs around 300-500 characters
206
- - Use question-based subheadings
207
- - Make use of lists, tables, and bold text to diversify formatting
208
- - Indicate where internal links could be inserted
209
- - Provide sufficient content of at least 2000-3000 characters
210
- """
211
- }
212
-
213
- # Additional guidelines for each tone
214
- tone_guides = {
215
- "professional": "Use a professional, authoritative voice. Clearly explain any technical terms and present data or research to maintain a logical flow.",
216
- "casual": "Use a relaxed, conversational style. Employ personal experiences, relatable examples, and a friendly voice (e.g., 'It's super useful!').",
217
- "humorous": "Use humor and witty expressions. Add funny analogies or jokes while preserving accuracy and usefulness.",
218
- "storytelling": "Write as if telling a story, with emotional depth and narrative flow. Incorporate characters, settings, conflicts, and resolutions."
219
- }
220
-
221
- # Guidelines for using search results
222
- search_guide = """
223
- Guidelines for Using Search Results:
224
- - Accurately incorporate key information from the search results into the blog
225
- - Include recent data, statistics, and case studies from the search results
226
- - When quoting, specify the source within the text (e.g., "According to XYZ website...")
227
- - At the end of the blog, add a "References" section and list major sources with links
228
- - If there are conflicting pieces of information, present multiple perspectives
229
- - Make sure to reflect the latest trends and data from the search results
230
- """
231
-
232
- # Guidelines for using uploaded files
233
- upload_guide = """
234
- Guidelines for Using Uploaded Files (Highest Priority):
235
- - The uploaded files must be a main source of information for the blog
236
- - Carefully examine the data, statistics, or examples in the file and integrate them
237
- - Directly quote and thoroughly explain any key figures or claims from the file
238
- - Highlight the file content as a crucial aspect of the blog
239
- - Mention the source clearly, e.g., "According to the uploaded data..."
240
- - For CSV files, detail important stats or numerical data in the blog
241
- - For PDF files, quote crucial segments or statements
242
- - For text files, integrate relevant content effectively
243
- - Even if the file content seems tangential, do your best to connect it to the blog topic
244
- - Keep consistency throughout and ensure the file's data is appropriately reflected
245
- """
246
-
247
- # Choose base prompt
248
- if template == "ginigen":
249
- final_prompt = ginigen_prompt
250
- else:
251
- final_prompt = base_prompt
252
-
253
- # If the user chose a specific template (and not ginigen), append the relevant guidelines
254
- if template != "ginigen" and template in template_guides:
255
- final_prompt += "\n" + template_guides[template]
256
-
257
- # If a specific tone is selected, append that guideline
258
- if tone in tone_guides:
259
- final_prompt += f"\n\nTone and Manner: {tone_guides[tone]}"
260
 
261
- # If web search results should be included
262
- if include_search_results:
263
- final_prompt += f"\n\n{search_guide}"
264
 
265
- # If uploaded files should be included
266
- if include_uploaded_files:
267
- final_prompt += f"\n\n{upload_guide}"
268
-
269
- # Word count guidelines
270
- final_prompt += (
271
- f"\n\nWriting Requirements:\n"
272
- f"9.1. Word Count: around {word_count-250}-{word_count+250} characters\n"
273
- f"9.2. Paragraph Length: 3-4 sentences each\n"
274
- f"9.3. Visual Cues: Use subheadings, separators, and bullet/numbered lists\n"
275
- f"9.4. Data: Cite all sources\n"
276
- f"9.5. Readability: Use clear paragraph breaks and highlights where necessary"
277
- )
278
-
279
- return final_prompt
280
-
281
- # ──────────────────────────────── Brave Search API ────────────────────────
282
- @st.cache_data(ttl=3600)
283
- def brave_search(query: str, count: int = 20):
284
- """
285
- Call the Brave Web Search API β†’ list[dict]
286
- Returns fields: index, title, link, snippet, displayed_link
287
- """
288
- if not BRAVE_KEY:
289
- raise RuntimeError("⚠️ SERPHOUSE_API_KEY (Brave API Key) environment variable is empty.")
290
-
291
- headers = {
292
- "Accept": "application/json",
293
- "Accept-Encoding": "gzip",
294
- "X-Subscription-Token": BRAVE_KEY
295
- }
296
- params = {"q": query, "count": str(count)}
297
-
298
- for attempt in range(3):
299
- try:
300
- r = requests.get(BRAVE_ENDPOINT, headers=headers, params=params, timeout=15)
301
- r.raise_for_status()
302
- data = r.json()
303
-
304
- logging.info(f"Brave search result data structure: {list(data.keys())}")
305
-
306
- raw = data.get("web", {}).get("results") or data.get("results", [])
307
- if not raw:
308
- logging.warning(f"No Brave search results found. Response: {data}")
309
- raise ValueError("No search results found.")
310
-
311
- arts = []
312
- for i, res in enumerate(raw[:count], 1):
313
- url = res.get("url", res.get("link", ""))
314
- host = re.sub(r"https?://(www\.)?", "", url).split("/")[0]
315
- arts.append({
316
- "index": i,
317
- "title": res.get("title", "No title"),
318
- "link": url,
319
- "snippet": res.get("description", res.get("text", "No snippet")),
320
- "displayed_link": host
321
- })
322
-
323
- logging.info(f"Brave search success: {len(arts)} results")
324
- return arts
325
-
326
- except Exception as e:
327
- logging.error(f"Brave search failure (attempt {attempt+1}/3): {e}")
328
- if attempt < 2:
329
- time.sleep(2)
330
-
331
- return []
332
-
333
- def mock_results(query: str) -> str:
334
- """Fallback search results if API fails"""
335
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
336
- return (f"# Fallback Search Content (Generated: {ts})\n\n"
337
- f"The search API request failed. Please generate the blog based on any pre-existing knowledge about '{query}'.\n\n"
338
- f"You may consider the following points:\n\n"
339
- f"- Basic concepts and importance of {query}\n"
340
- f"- Commonly known related statistics or trends\n"
341
- f"- Typical expert opinions on this subject\n"
342
- f"- Questions that readers might have\n\n"
343
- f"Note: This is fallback guidance, not real-time data.\n\n")
344
-
345
- def do_web_search(query: str) -> str:
346
- """Perform web search and format the results."""
347
  try:
348
- arts = brave_search(query, 20)
349
- if not arts:
350
- logging.warning("No search results, using fallback content")
351
- return mock_results(query)
352
-
353
- 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"
354
- body = "\n".join(
355
- f"### Result {a['index']}: {a['title']}\n\n{a['snippet']}\n\n"
356
- f"**Source**: [{a['displayed_link']}]({a['link']})\n\n---\n"
357
- for a in arts
358
- )
359
- return hdr + body
360
- except Exception as e:
361
- logging.error(f"Web search process failed: {str(e)}")
362
- return mock_results(query)
363
-
364
- # ──────────────────────────────── File Upload Handling ─────────────────────
365
- def process_text_file(file):
366
- """Handle text file"""
367
- try:
368
- content = file.read()
369
- file.seek(0)
370
-
371
- text = content.decode('utf-8', errors='ignore')
372
- if len(text) > 10000:
373
- text = text[:9700] + "...(truncated)..."
374
-
375
- result = f"## Text File: {file.name}\n\n"
376
- result += text
377
- return result
378
- except Exception as e:
379
- logging.error(f"Error processing text file: {str(e)}")
380
- return f"Error processing text file: {str(e)}"
381
-
382
- def process_csv_file(file):
383
- """Handle CSV file"""
384
- try:
385
- content = file.read()
386
- file.seek(0)
387
-
388
- df = pd.read_csv(io.BytesIO(content))
389
- result = f"## CSV File: {file.name}\n\n"
390
- result += f"- Rows: {len(df)}\n"
391
- result += f"- Columns: {len(df.columns)}\n"
392
- result += f"- Column Names: {', '.join(df.columns.tolist())}\n\n"
393
-
394
- result += "### Data Preview\n\n"
395
- preview_df = df.head(10)
396
- try:
397
- markdown_table = preview_df.to_markdown(index=False)
398
- if markdown_table:
399
- result += markdown_table + "\n\n"
400
- else:
401
- result += "Unable to display CSV data.\n\n"
402
- except Exception as e:
403
- logging.error(f"Markdown table conversion error: {e}")
404
- result += "Displaying data as text:\n\n"
405
- result += str(preview_df) + "\n\n"
406
-
407
- num_cols = df.select_dtypes(include=['number']).columns
408
- if len(num_cols) > 0:
409
- result += "### Basic Statistical Information\n\n"
410
- try:
411
- stats_df = df[num_cols].describe().round(2)
412
- stats_markdown = stats_df.to_markdown()
413
- if stats_markdown:
414
- result += stats_markdown + "\n\n"
415
- else:
416
- result += "Unable to display statistical information.\n\n"
417
- except Exception as e:
418
- logging.error(f"Statistical info conversion error: {e}")
419
- result += "Unable to generate statistical information.\n\n"
420
-
421
- return result
422
- except Exception as e:
423
- logging.error(f"CSV file processing error: {str(e)}")
424
- return f"Error processing CSV file: {str(e)}"
425
-
426
- def process_pdf_file(file):
427
- """Handle PDF file"""
428
- try:
429
- # Read file in bytes
430
- file_bytes = file.read()
431
- file.seek(0)
432
-
433
- # Use PyPDF2
434
- pdf_file = io.BytesIO(file_bytes)
435
- reader = PyPDF2.PdfReader(pdf_file, strict=False)
436
-
437
- # Basic info
438
- result = f"## PDF File: {file.name}\n\n"
439
- result += f"- Total pages: {len(reader.pages)}\n\n"
440
-
441
- # Extract text by page (limit to first 5 pages)
442
- max_pages = min(5, len(reader.pages))
443
- all_text = ""
444
-
445
- for i in range(max_pages):
446
- try:
447
- page = reader.pages[i]
448
- page_text = page.extract_text()
449
-
450
- current_page_text = f"### Page {i+1}\n\n"
451
- if page_text and len(page_text.strip()) > 0:
452
- # Limit to 1500 characters per page
453
- if len(page_text) > 1500:
454
- current_page_text += page_text[:1500] + "...(truncated)...\n\n"
455
- else:
456
- current_page_text += page_text + "\n\n"
457
- else:
458
- current_page_text += "(No text could be extracted from this page)\n\n"
459
-
460
- all_text += current_page_text
461
-
462
- # If total text is too long, break
463
- if len(all_text) > 8000:
464
- all_text += "...(truncating remaining pages; PDF is too large)...\n\n"
465
- break
466
-
467
- except Exception as page_err:
468
- logging.error(f"Error processing PDF page {i+1}: {str(page_err)}")
469
- all_text += f"### Page {i+1}\n\n(Error extracting content: {str(page_err)})\n\n"
470
-
471
- if len(reader.pages) > max_pages:
472
- all_text += f"\nNote: Only the first {max_pages} pages are shown out of {len(reader.pages)} total.\n\n"
473
-
474
- result += "### PDF Content\n\n" + all_text
475
- return result
476
-
477
- except Exception as e:
478
- logging.error(f"PDF file processing error: {str(e)}")
479
- return f"## PDF File: {file.name}\n\nError occurred: {str(e)}\n\nThis PDF file cannot be processed."
480
-
481
- def process_uploaded_files(files):
482
- """Combine the contents of all uploaded files into one string."""
483
- if not files:
484
- return None
485
-
486
- result = "# Uploaded File Contents\n\n"
487
- 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"
488
-
489
- for file in files:
490
- try:
491
- ext = file.name.split('.')[-1].lower()
492
- if ext == 'txt':
493
- result += process_text_file(file) + "\n\n---\n\n"
494
- elif ext == 'csv':
495
- result += process_csv_file(file) + "\n\n---\n\n"
496
- elif ext == 'pdf':
497
- result += process_pdf_file(file) + "\n\n---\n\n"
498
- else:
499
- result += f"### Unsupported File: {file.name}\n\n---\n\n"
500
- except Exception as e:
501
- logging.error(f"File processing error {file.name}: {e}")
502
- result += f"### File processing error: {file.name}\n\nError: {e}\n\n---\n\n"
503
-
504
- return result
505
-
506
- # ──────────────────────────────── Image & Utility ─────────────────────────
507
- def generate_image(prompt, w=768, h=768, g=3.5, steps=30, seed=3):
508
- """Image generation function."""
509
- if not prompt:
510
- return None, "Insufficient prompt"
511
- try:
512
- res = Client(IMAGE_API_URL).predict(
513
- prompt=prompt, width=w, height=h, guidance=g,
514
- inference_steps=steps, seed=seed,
515
- do_img2img=False, init_image=None,
516
- image2image_strength=0.8, resize_img=True,
517
- api_name="/generate_image"
518
- )
519
- return res[0], f"Seed: {res[1]}"
520
- except Exception as e:
521
- logging.error(e)
522
- return None, str(e)
523
-
524
- def extract_image_prompt(blog_text: str, topic: str):
525
- """
526
- Generate a single-line English image prompt from the blog content.
527
- """
528
- client = get_openai_client()
529
-
530
- try:
531
- response = client.chat.completions.create(
532
- model="gpt-4.1-mini", # 일반적으둜 μ‚¬μš© κ°€λŠ₯ν•œ λͺ¨λΈλ‘œ μ„€μ •
533
- messages=[
534
- {"role": "system", "content": "Generate a single-line English image prompt from the following text. Return only the prompt text, nothing else."},
535
- {"role": "user", "content": f"Topic: {topic}\n\n---\n{blog_text}\n\n---"}
536
- ],
537
- temperature=1,
538
- max_tokens=80,
539
- top_p=1
540
- )
541
 
542
- return response.choices[0].message.content.strip()
 
 
543
  except Exception as e:
544
- logging.error(f"OpenAI image prompt generation error: {e}")
545
- return f"A professional photo related to {topic}, high quality"
546
-
547
- def md_to_html(md: str, title="Ginigen Blog"):
548
- """Convert Markdown to HTML."""
549
- return f"<!DOCTYPE html><html><head><title>{title}</title><meta charset='utf-8'></head><body>{markdown.markdown(md)}</body></html>"
550
-
551
- def keywords(text: str, top=5):
552
- """Simple keyword extraction."""
553
- cleaned = re.sub(r"[^κ°€-힣a-zA-Z0-9\s]", "", text)
554
- return " ".join(cleaned.split()[:top])
555
-
556
- # ──────────────────────────────── Streamlit UI ────────────────────────────
557
- def ginigen_app():
558
- st.title("Ginigen Blog")
559
-
560
- # Set default session state
561
- if "ai_model" not in st.session_state:
562
- st.session_state.ai_model = "gpt-4.1-mini" # κ³ μ • λͺ¨λΈ μ„€μ •
563
- if "messages" not in st.session_state:
564
- st.session_state.messages = []
565
- if "auto_save" not in st.session_state:
566
- st.session_state.auto_save = True
567
- if "generate_image" not in st.session_state:
568
- st.session_state.generate_image = False
569
- if "web_search_enabled" not in st.session_state:
570
- st.session_state.web_search_enabled = True
571
- if "blog_template" not in st.session_state:
572
- st.session_state.blog_template = "ginigen" # Ginigen recommended style by default
573
- if "blog_tone" not in st.session_state:
574
- st.session_state.blog_tone = "professional"
575
- if "word_count" not in st.session_state:
576
- st.session_state.word_count = 1750
577
-
578
- # Sidebar UI
579
- sb = st.sidebar
580
- sb.title("Blog Settings")
581
-
582
- # λͺ¨λΈ 선택 제거 (κ³ μ • λͺ¨λΈ μ‚¬μš©)
583
-
584
- sb.subheader("Blog Style Settings")
585
- sb.selectbox(
586
- "Blog Template",
587
- options=list(BLOG_TEMPLATES.keys()),
588
- format_func=lambda x: BLOG_TEMPLATES[x],
589
- key="blog_template"
590
- )
591
-
592
- sb.selectbox(
593
- "Blog Tone",
594
- options=list(BLOG_TONES.keys()),
595
- format_func=lambda x: BLOG_TONES[x],
596
- key="blog_tone"
597
- )
598
-
599
- sb.slider("Blog Length (word count)", 800, 3000, key="word_count")
600
-
601
-
602
- # Example topics
603
- sb.subheader("Example Topics")
604
- c1, c2, c3 = sb.columns(3)
605
- if c1.button("Real Estate Tax", key="ex1"):
606
- process_example(EXAMPLE_TOPICS["example1"])
607
- if c2.button("Summer Festivals", key="ex2"):
608
- process_example(EXAMPLE_TOPICS["example2"])
609
- if c3.button("Investment Guide", key="ex3"):
610
- process_example(EXAMPLE_TOPICS["example3"])
611
-
612
- sb.subheader("Other Settings")
613
- sb.toggle("Auto Save", key="auto_save")
614
- sb.toggle("Auto Image Generation", key="generate_image")
615
-
616
- web_search_enabled = sb.toggle("Use Web Search", value=st.session_state.web_search_enabled)
617
- st.session_state.web_search_enabled = web_search_enabled
618
-
619
- if web_search_enabled:
620
- st.sidebar.info("βœ… Web search results will be integrated into the blog.")
621
-
622
- # Download the latest blog (markdown/HTML)
623
- latest_blog = next(
624
- (m["content"] for m in reversed(st.session_state.messages)
625
- if m["role"] == "assistant" and m["content"].strip()),
626
- None
627
- )
628
- if latest_blog:
629
- title_match = re.search(r"# (.*?)(\n|$)", latest_blog)
630
- title = title_match.group(1).strip() if title_match else "blog"
631
- sb.subheader("Download Latest Blog")
632
- d1, d2 = sb.columns(2)
633
- d1.download_button("Download as Markdown", latest_blog,
634
- file_name=f"{title}.md", mime="text/markdown")
635
- d2.download_button("Download as HTML", md_to_html(latest_blog, title),
636
- file_name=f"{title}.html", mime="text/html")
637
-
638
- # JSON conversation record upload
639
- up = sb.file_uploader("Load Conversation History (.json)", type=["json"], key="json_uploader")
640
- if up:
641
- try:
642
- st.session_state.messages = json.load(up)
643
- sb.success("Conversation history loaded successfully")
644
- except Exception as e:
645
- sb.error(f"Failed to load: {e}")
646
-
647
- # JSON conversation record download
648
- if sb.button("Download Conversation as JSON"):
649
- sb.download_button(
650
- "Save",
651
- data=json.dumps(st.session_state.messages, ensure_ascii=False, indent=2),
652
- file_name="chat_history.json",
653
- mime="application/json"
654
- )
655
-
656
- # File Upload
657
- st.subheader("File Upload")
658
- uploaded_files = st.file_uploader(
659
- "Upload files to be referenced in your blog (txt, csv, pdf)",
660
- type=["txt", "csv", "pdf"],
661
- accept_multiple_files=True,
662
- key="file_uploader"
663
- )
664
-
665
- if uploaded_files:
666
- file_count = len(uploaded_files)
667
- st.success(f"{file_count} files uploaded. They will be referenced in the blog.")
668
-
669
- with st.expander("Preview Uploaded Files", expanded=False):
670
- for idx, file in enumerate(uploaded_files):
671
- st.write(f"**File Name:** {file.name}")
672
- ext = file.name.split('.')[-1].lower()
673
-
674
- if ext == 'txt':
675
- preview = file.read(1000).decode('utf-8', errors='ignore')
676
- file.seek(0)
677
- st.text_area(
678
- f"Preview of {file.name}",
679
- preview + ("..." if len(preview) >= 1000 else ""),
680
- height=150
681
- )
682
- elif ext == 'csv':
683
- try:
684
- df = pd.read_csv(file)
685
- file.seek(0)
686
- st.write("CSV Preview (up to 5 rows)")
687
- st.dataframe(df.head(5))
688
- except Exception as e:
689
- st.error(f"CSV preview failed: {e}")
690
- elif ext == 'pdf':
691
- try:
692
- file_bytes = file.read()
693
- file.seek(0)
694
-
695
- pdf_file = io.BytesIO(file_bytes)
696
- reader = PyPDF2.PdfReader(pdf_file, strict=False)
697
-
698
- pc = len(reader.pages)
699
- st.write(f"PDF File: {pc} pages")
700
-
701
- if pc > 0:
702
- try:
703
- page_text = reader.pages[0].extract_text()
704
- preview = page_text[:500] if page_text else "(No text extracted)"
705
- st.text_area("Preview of the first page", preview + "...", height=150)
706
- except:
707
- st.warning("Failed to extract text from the first page")
708
- except Exception as e:
709
- st.error(f"PDF preview failed: {e}")
710
-
711
- if idx < file_count - 1:
712
- st.divider()
713
-
714
- # Display existing messages
715
- for m in st.session_state.messages:
716
- with st.chat_message(m["role"]):
717
- st.markdown(m["content"])
718
- if "image" in m:
719
- st.image(m["image"], caption=m.get("image_caption", ""))
720
-
721
- # User input
722
- prompt = st.chat_input("Enter a blog topic or keywords.")
723
- if prompt:
724
- process_input(prompt, uploaded_files)
725
-
726
- def process_example(topic):
727
- """Process the selected example topic."""
728
- process_input(topic, [])
729
-
730
- def process_input(prompt: str, uploaded_files):
731
- # Add user's message
732
- if not any(m["role"] == "user" and m["content"] == prompt for m in st.session_state.messages):
733
- st.session_state.messages.append({"role": "user", "content": prompt})
734
-
735
- with st.chat_message("user"):
736
- st.markdown(prompt)
737
-
738
- with st.chat_message("assistant"):
739
- placeholder = st.empty()
740
- message_placeholder = st.empty()
741
- full_response = ""
742
-
743
- use_web_search = st.session_state.web_search_enabled
744
- has_uploaded_files = bool(uploaded_files) and len(uploaded_files) > 0
745
-
746
- try:
747
- # μƒνƒœ ν‘œμ‹œλ₯Ό μœ„ν•œ μƒνƒœ μ»΄ν¬λ„ŒνŠΈ
748
- status = st.status("Preparing to generate blog...")
749
- status.update(label="Initializing client...")
750
-
751
- client = get_openai_client()
752
-
753
- # Prepare conversation messages
754
- messages = []
755
-
756
- # Web search
757
- search_content = None
758
- if use_web_search:
759
- status.update(label="Performing web search...")
760
- with st.spinner("Searching the web..."):
761
- search_content = do_web_search(keywords(prompt, top=5))
762
-
763
- # Process uploaded files β†’ content
764
- file_content = None
765
- if has_uploaded_files:
766
- status.update(label="Processing uploaded files...")
767
- with st.spinner("Analyzing files..."):
768
- file_content = process_uploaded_files(uploaded_files)
769
-
770
- # Build system prompt
771
- status.update(label="Preparing blog draft...")
772
- sys_prompt = get_system_prompt(
773
- template=st.session_state.blog_template,
774
- tone=st.session_state.blog_tone,
775
- word_count=st.session_state.word_count,
776
- include_search_results=use_web_search,
777
- include_uploaded_files=has_uploaded_files
778
- )
779
-
780
- # OpenAI API 호좜 μ€€λΉ„
781
- status.update(label="Writing blog content...")
782
-
783
- # λ©”μ‹œμ§€ ꡬ성
784
- api_messages = [
785
- {"role": "system", "content": sys_prompt}
786
- ]
787
-
788
- user_content = prompt
789
-
790
- # 검색 κ²°κ³Όκ°€ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
791
- if search_content:
792
- user_content += "\n\n" + search_content
793
-
794
- # 파일 λ‚΄μš©μ΄ 있으면 μ‚¬μš©μž ν”„λ‘¬ν”„νŠΈμ— μΆ”κ°€
795
- if file_content:
796
- user_content += "\n\n" + file_content
797
-
798
- # μ‚¬μš©μž λ©”μ‹œμ§€ μΆ”κ°€
799
- api_messages.append({"role": "user", "content": user_content})
800
-
801
- # OpenAI API 슀트리밍 호좜 - κ³ μ • λͺ¨λΈ "gpt-4.1-mini" μ‚¬μš©
802
- try:
803
- # 슀트리밍 λ°©μ‹μœΌλ‘œ API 호좜
804
- stream = client.chat.completions.create(
805
- model="gpt-4.1-mini", # κ³ μ • λͺ¨λΈ μ‚¬μš©
806
- messages=api_messages,
807
- temperature=1,
808
- max_tokens=MAX_TOKENS,
809
- top_p=1,
810
- stream=True # 슀트리밍 ν™œμ„±ν™”
811
- )
812
-
813
- # 슀트리밍 응닡 처리
814
- for chunk in stream:
815
- if chunk.choices and len(chunk.choices) > 0 and chunk.choices[0].delta.content is not None:
816
- content_delta = chunk.choices[0].delta.content
817
- full_response += content_delta
818
- message_placeholder.markdown(full_response + "β–Œ")
819
-
820
- # μ΅œμ’… 응닡 ν‘œμ‹œ (μ»€μ„œ 제거)
821
- message_placeholder.markdown(full_response)
822
- status.update(label="Blog completed!", state="complete")
823
-
824
- except Exception as api_error:
825
- error_message = str(api_error)
826
- logging.error(f"API error: {error_message}")
827
- status.update(label=f"Error: {error_message}", state="error")
828
- raise Exception(f"Blog generation error: {error_message}")
829
-
830
- # 이미지 생성
831
- answer_entry_saved = False
832
- if st.session_state.generate_image and full_response:
833
- with st.spinner("Generating image..."):
834
- try:
835
- ip = extract_image_prompt(full_response, prompt)
836
- img, cap = generate_image(ip)
837
- if img:
838
- st.image(img, caption=cap)
839
- st.session_state.messages.append({
840
- "role": "assistant",
841
- "content": full_response,
842
- "image": img,
843
- "image_caption": cap
844
- })
845
- answer_entry_saved = True
846
- except Exception as img_error:
847
- logging.error(f"Image generation error: {str(img_error)}")
848
- st.warning("이미지 생성에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. λΈ”λ‘œκ·Έ μ½˜ν…μΈ λ§Œ μ €μž₯λ©λ‹ˆλ‹€.")
849
-
850
- # Save the answer if not saved above
851
- if not answer_entry_saved and full_response:
852
- st.session_state.messages.append({"role": "assistant", "content": full_response})
853
-
854
- # Download buttons
855
- if full_response:
856
- st.subheader("Download This Blog")
857
- c1, c2 = st.columns(2)
858
- c1.download_button(
859
- "Markdown",
860
- data=full_response,
861
- file_name=f"{prompt[:30]}.md",
862
- mime="text/markdown"
863
- )
864
- c2.download_button(
865
- "HTML",
866
- data=md_to_html(full_response, prompt[:30]),
867
- file_name=f"{prompt[:30]}.html",
868
- mime="text/html"
869
- )
870
-
871
- # Auto save
872
- if st.session_state.auto_save and st.session_state.messages:
873
- try:
874
- fn = f"chat_history_auto_{datetime.now():%Y%m%d_%H%M%S}.json"
875
- with open(fn, "w", encoding="utf-8") as fp:
876
- json.dump(st.session_state.messages, fp, ensure_ascii=False, indent=2)
877
- except Exception as e:
878
- logging.error(f"Auto-save failed: {e}")
879
-
880
- except Exception as e:
881
- error_message = str(e)
882
- placeholder.error(f"An error occurred: {error_message}")
883
- logging.error(f"Process input error: {error_message}")
884
- ans = f"An error occurred while processing your request: {error_message}"
885
- st.session_state.messages.append({"role": "assistant", "content": ans})
886
-
887
-
888
- # ──────────────────────────────── main ────────────────────────────────────
889
- def main():
890
- ginigen_app()
891
 
892
- if __name__ == "__main__":
893
- main()
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
+ import os
3
+ import base64
4
+ import types
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ # Secretμ—μ„œ μ €μž₯된 μ½”λ“œ κ°€μ Έμ˜€κΈ°
7
+ app_code = os.environ.get("APP", "")
 
8
 
9
+ # μ½”λ“œ μ‹€ν–‰ ν•¨μˆ˜
10
+ def execute_code(code_str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  try:
12
+ # μ‹€ν–‰ κ°€λŠ₯ν•œ μ½”λ“œλ‘œ λ³€ν™˜
13
+ code_module = types.ModuleType('dynamic_code')
14
+ exec(code_str, code_module.__dict__)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
+ # ν•„μš”ν•œ ν•¨μˆ˜ μ‹€ν–‰
17
+ if hasattr(code_module, 'main'):
18
+ code_module.main()
19
  except Exception as e:
20
+ st.error(f"μ½”λ“œ μ‹€ν–‰ 쀑 였λ₯˜ λ°œμƒ: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ # μ½”λ“œ μ‹€ν–‰
23
+ if app_code:
24
+ execute_code(app_code)
25
+ else:
26
+ st.error("μ½”λ“œλ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€. Secret이 μ˜¬λ°”λ₯΄κ²Œ μ„€μ •λ˜μ—ˆλŠ”μ§€ ν™•μΈν•˜μ„Έμš”.")