Download app.py from ssboost/672gjsds: direct link, hf CLI and curl.
- Browser
- Download file 34.8 kB
-
https://huggingface.co/spaces/ssboost/672gjsds/resolve/main/app.py
- Command line
-
hf download hf://spaces/ssboost/672gjsds/app.py
-
curl -L -o app.py https://huggingface.co/spaces/ssboost/672gjsds/resolve/main/app.py
34.8 kB
| """ | |
| AI μν μμ± λΆμ μμ€ν v2.10 - κ°λ΅λ²μ + μΆλ ₯κΈ°λ₯ + λ©ν°μ¬μ©μ μμ | |
| - 2λ¨κ³: μμ§λ ν€μλ λͺ©λ‘ κΈ°λ₯μ μ κ±°νλ€. | |
| - 3λ¨κ³: λΆμν ν€μλ μ νμμ π μ°κ΄κ²μμ΄ λΆμμ μνμΆμΆ λ° λΆμκΈ°λ₯μμ κ±°νλ€. | |
| - 3λ¨κ³: λΆμν ν€μλ μ νμ λͺ μΉμ "ν€μλ μ¬μΈ΅λΆμ μ λ ₯"μ΄λΌκ³ λ°κΎΌλ€. | |
| - π κ²μλ νΈλ λ λΆμ, π― ν€μλ λΆμμ΄ λμμΌνλ€. | |
| - μΆλ ₯ κΈ°λ₯ μΆκ°: HTML νμΌ μμ± λ° ZIP λ€μ΄λ‘λ | |
| - Gemini API ν€ λλ€ μ μ© (api_utils ν΅ν© κ΄λ¦¬) | |
| - νκ΅μκ° μ μ© | |
| - λ©ν° μ¬μ©μ μμ : gr.State μ¬μ©μΌλ‘ μΈμ λ³ λ°μ΄ν° κ΄λ¦¬ | |
| """ | |
| import gradio as gr | |
| import pandas as pd | |
| import os | |
| import logging | |
| import google.generativeai as genai | |
| from datetime import datetime, timedelta | |
| import time | |
| import re | |
| import zipfile | |
| import tempfile | |
| # νκ΅μκ° μ μ©μ μν λͺ¨λ (μ νμ ) | |
| try: | |
| import pytz | |
| PYTZ_AVAILABLE = True | |
| except ImportError: | |
| PYTZ_AVAILABLE = False | |
| # λ‘κΉ μ€μ | |
| logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| # λͺ¨λ μν¬νΈ (κ°λ΅ λ²μ μμ νμν κ²λ§) | |
| import api_utils | |
| import keyword_search | |
| import keyword_analysis | |
| import trend_analysis_v2 | |
| # ===== Gemini API μ€μ ===== | |
| def setup_gemini_model(): | |
| """Gemini λͺ¨λΈ μ΄κΈ°ν - api_utilsμμ κ΄λ¦¬ (λλ€ ν€ μ μ©)""" | |
| try: | |
| # api_utilsμμ Gemini λͺ¨λΈ κ°μ Έμ€κΈ° (λλ€ ν€ μ μ©) | |
| model = api_utils.get_gemini_model() | |
| if model: | |
| logger.info("Gemini λͺ¨λΈ μ΄κΈ°ν μ±κ³΅ (api_utils ν΅ν© κ΄λ¦¬ - λλ€ ν€)") | |
| return model | |
| else: | |
| logger.warning("Gemini API ν€κ° μ€μ λμ§ μμμ΅λλ€.") | |
| return None | |
| except Exception as e: | |
| logger.error(f"Gemini λͺ¨λΈ μ΄κΈ°ν μ€ν¨: {e}") | |
| return None | |
| # Gemini λͺ¨λΈ μ΄κΈ°ν | |
| gemini_model = setup_gemini_model() | |
| # ===== νκ΅μκ° κ΄λ ¨ ν¨μ ===== | |
| def get_korean_time(): | |
| """νκ΅μκ° λ°ν""" | |
| if PYTZ_AVAILABLE: | |
| try: | |
| korea_tz = pytz.timezone('Asia/Seoul') | |
| return datetime.now(korea_tz) | |
| except: | |
| pass | |
| # pytzκ° μκ±°λ μ€λ₯ μ μμ€ν μκ° μ¬μ© | |
| return datetime.now() | |
| def format_korean_datetime(dt=None, format_type="filename"): | |
| """νκ΅μκ° ν¬λ§·ν """ | |
| if dt is None: | |
| dt = get_korean_time() | |
| if format_type == "filename": | |
| return dt.strftime("%y%m%d_%H%M") | |
| elif format_type == "display": | |
| return dt.strftime('%Yλ %mμ %dμΌ %Hμ %MλΆ') | |
| elif format_type == "full": | |
| return dt.strftime('%Y-%m-%d %H:%M:%S') | |
| else: | |
| return dt.strftime("%y%m%d_%H%M") | |
| # ===== λ‘λ© μ λλ©μ΄μ ===== | |
| def create_loading_animation(): | |
| """λ‘λ© μ λλ©μ΄μ HTML""" | |
| return """ | |
| <div style="display: flex; flex-direction: column; align-items: center; padding: 40px; background: white; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);"> | |
| <div style="width: 60px; height: 60px; border: 4px solid #f3f3f3; border-top: 4px solid #FB7F0D; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px;"></div> | |
| <h3 style="color: #FB7F0D; margin: 10px 0; font-size: 18px;">λΆμ μ€μ λλ€...</h3> | |
| <p style="color: #666; margin: 5px 0; text-align: center;">λ€μ΄λ² λ°μ΄ν°λ₯Ό μμ§νκ³ AIκ° λΆμνκ³ μμ΅λλ€.<br>μ μλ§ κΈ°λ€λ €μ£ΌμΈμ.</p> | |
| <div style="width: 200px; height: 4px; background: #f0f0f0; border-radius: 2px; margin-top: 15px; overflow: hidden;"> | |
| <div style="width: 100%; height: 100%; background: linear-gradient(90deg, #FB7F0D, #ff9a8b); border-radius: 2px; animation: progress 2s ease-in-out infinite;"></div> | |
| </div> | |
| </div> | |
| <style> | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| @keyframes progress { | |
| 0% { transform: translateX(-100%); } | |
| 100% { transform: translateX(100%); } | |
| } | |
| </style> | |
| """ | |
| # ===== μλ¬ μ²λ¦¬ ν¨μ ===== | |
| def generate_error_response(error_message): | |
| """μλ¬ μλ΅ μμ±""" | |
| return f''' | |
| <div style="color: red; padding: 30px; text-align: center; width: 100%; | |
| background-color: #f8d7da; border-radius: 12px; border: 1px solid #f5c6cb;"> | |
| <h3 style="margin-bottom: 15px;">β λΆμ μ€λ₯</h3> | |
| <p style="margin-bottom: 20px;">{error_message}</p> | |
| <div style="background: white; padding: 15px; border-radius: 8px; color: #333;"> | |
| <h4>ν΄κ²° λ°©λ²:</h4> | |
| <ul style="text-align: left; padding-left: 20px;"> | |
| <li>ν€μλ μ² μλ₯Ό νμΈν΄μ£ΌμΈμ</li> | |
| <li>λ κ°λ¨ν ν€μλλ₯Ό μ¬μ©ν΄λ³΄μΈμ</li> | |
| <li>λ€νΈμν¬ μ°κ²°μ νμΈν΄μ£ΌμΈμ</li> | |
| <li>μ μ ν λ€μ μλν΄μ£ΌμΈμ</li> | |
| </ul> | |
| </div> | |
| </div> | |
| ''' | |
| # ===== λ©μΈ ν€μλ λΆμ ν¨μ ===== | |
| def safe_keyword_analysis(analysis_keyword): | |
| """μλ¬ λ°©μ§λ₯Ό μν μμ ν ν€μλ λΆμ - λ©ν°μ¬μ©μ μμ """ | |
| # μ λ ₯κ° κ²μ¦ | |
| if not analysis_keyword or not analysis_keyword.strip(): | |
| return generate_error_response("λΆμν ν€μλλ₯Ό μ λ ₯ν΄μ£ΌμΈμ."), {} | |
| analysis_keyword = analysis_keyword.strip() | |
| try: | |
| # κ²μλ μ‘°ν - μλ¬ λ°©μ§ | |
| api_keyword = keyword_analysis.normalize_keyword_for_api(analysis_keyword) | |
| search_volumes = keyword_search.fetch_all_search_volumes([api_keyword]) | |
| volume_data = search_volumes.get(api_keyword, {"PCκ²μλ": 0, "λͺ¨λ°μΌκ²μλ": 0, "μ΄κ²μλ": 0}) | |
| # κ²μλμ΄ 0μ΄κ±°λ ν€μλκ° μ‘΄μ¬νμ§ μλ κ²½μ° μ²λ¦¬ | |
| if volume_data['μ΄κ²μλ'] == 0: | |
| logger.warning(f"'{analysis_keyword}' ν€μλμ κ²μλμ΄ 0μ΄κ±°λ μ‘΄μ¬νμ§ μμ΅λλ€.") | |
| error_result = f""" | |
| <div style="padding: 30px; text-align: center; background: #fff3cd; border-radius: 12px; border: 1px solid #ffeaa7;"> | |
| <h3 style="color: #856404; margin-bottom: 15px;">β οΈ ν€μλ λΆμ λΆκ°</h3> | |
| <p style="color: #856404; margin-bottom: 10px;"><strong>'{analysis_keyword}'</strong> ν€μλλ κ²μλμ΄ μκ±°λ μ¬λ°λ₯΄μ§ μμ ν€μλμ λλ€.</p> | |
| <div style="background: white; padding: 15px; border-radius: 8px; margin-top: 15px;"> | |
| <h4 style="color: #333; margin-bottom: 10px;">π‘ κΆμ₯μ¬ν</h4> | |
| <ul style="text-align: left; color: #666; padding-left: 20px;"> | |
| <li>ν€μλ μ² μλ₯Ό νμΈν΄μ£ΌμΈμ</li> | |
| <li>λ μΌλ°μ μΈ ν€μλλ₯Ό μ¬μ©ν΄λ³΄μΈμ</li> | |
| <li>ν€μλλ₯Ό λμ΄μ°κΈ°λ‘ ꡬλΆν΄λ³΄μΈμ (μ: 'μ¬μ± μ¬λ¦¬νΌ')</li> | |
| </ul> | |
| </div> | |
| </div> | |
| """ | |
| return error_result, {} | |
| logger.info(f"'{analysis_keyword}' νμ¬ κ²μλ: {volume_data['μ΄κ²μλ']:,}") | |
| # νΈλ λ λΆμ μλ | |
| monthly_data_1year = {} | |
| monthly_data_3year = {} | |
| trend_available = False | |
| try: | |
| # λ°μ΄ν°λ© API ν€ νμΈ (λλ€ μ μ©) | |
| datalab_config = api_utils.get_next_datalab_api_config() | |
| if datalab_config and not datalab_config["CLIENT_ID"].startswith("YOUR_"): | |
| logger.info("λ°μ΄ν°λ© API ν€κ° μ€μ λμ΄ μμ΄ 1λ , 3λ νΈλ λ λΆμμ μλν©λλ€.") | |
| # μ΅μ νλ API ν¨μ μ¬μ© | |
| # 1λ νΈλ λ λ°μ΄ν° | |
| trend_data_1year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "1year", max_retries=3) | |
| if trend_data_1year: | |
| current_volumes = {api_keyword: volume_data} | |
| monthly_data_1year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_1year, "1year") | |
| # 3λ νΈλ λ λ°μ΄ν° | |
| trend_data_3year = trend_analysis_v2.get_naver_trend_data_v5([analysis_keyword], "3year", max_retries=3) | |
| if trend_data_3year: | |
| current_volumes = {api_keyword: volume_data} | |
| monthly_data_3year = trend_analysis_v2.calculate_monthly_volumes_v7([analysis_keyword], current_volumes, trend_data_3year, "3year") | |
| # 3λ λ°μ΄ν°κ° μλ κ²½μ° 1λ λ°μ΄ν°λ‘ νμ₯ | |
| if not monthly_data_3year and monthly_data_1year: | |
| logger.info("3λ λ°μ΄ν°κ° μμ΄ 1λ λ°μ΄ν°λ₯Ό κΈ°λ°μΌλ‘ 3λ μ°¨νΈ μμ±") | |
| keyword = analysis_keyword | |
| if keyword in monthly_data_1year: | |
| data_1y = monthly_data_1year[keyword] | |
| # 3λ λΆλμ λ μ§ μμ± (24κ°μ μΆκ°) | |
| extended_dates = [] | |
| extended_volumes = [] | |
| # κΈ°μ‘΄ 1λ λ°μ΄ν° μ΄μ μ 24κ°μ μΆκ° (λͺ¨λ 0μΌλ‘) | |
| start_date = datetime.strptime(data_1y["dates"][0], "%Y-%m-%d") | |
| for i in range(24, 0, -1): | |
| prev_date = start_date - timedelta(days=30 * i) | |
| extended_dates.append(prev_date.strftime("%Y-%m-%d")) | |
| extended_volumes.append(0) | |
| # κΈ°μ‘΄ 1λ λ°μ΄ν° μΆκ° (μμ λ°μ΄ν° μ μΈ) | |
| actual_count = data_1y.get("actual_count", len(data_1y["dates"])) | |
| extended_dates.extend(data_1y["dates"][:actual_count]) | |
| extended_volumes.extend(data_1y["monthly_volumes"][:actual_count]) | |
| monthly_data_3year = { | |
| keyword: { | |
| "monthly_volumes": extended_volumes, | |
| "dates": extended_dates, | |
| "current_volume": data_1y["current_volume"], | |
| "growth_rate": trend_analysis_v2.calculate_3year_growth_rate_improved(extended_volumes), | |
| "volume_per_percent": data_1y["volume_per_percent"], | |
| "current_ratio": data_1y["current_ratio"], | |
| "actual_count": len(extended_volumes), | |
| "predicted_count": 0 | |
| } | |
| } | |
| if monthly_data_1year or monthly_data_3year: | |
| trend_available = True | |
| logger.info("νΈλ λ λΆμ μ±κ³΅") | |
| else: | |
| logger.info("νΈλ λ λ°μ΄ν° μ²λ¦¬ μ€ν¨") | |
| else: | |
| logger.info("λ°μ΄ν°λ© API ν€κ° μ€μ λμ§ μμ") | |
| except Exception as e: | |
| logger.info(f"νΈλ λ λΆμ 건λλ: {str(e)[:100]}") | |
| # === π κ²μλ νΈλ λ λΆμ μΉμ === | |
| if trend_available and (monthly_data_1year or monthly_data_3year): | |
| try: | |
| trend_chart = trend_analysis_v2.create_trend_chart_v7(monthly_data_1year, monthly_data_3year) | |
| except Exception as e: | |
| logger.warning(f"νΈλ λ μ°¨νΈ μμ± μ€ν¨, κΈ°λ³Έ μ°¨νΈ μ¬μ©: {e}") | |
| trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword) | |
| else: | |
| trend_chart = trend_analysis_v2.create_enhanced_current_chart(volume_data, analysis_keyword) | |
| # νΈλ λ μΉμ | |
| trend_section = f""" | |
| <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;"> | |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 18px; color: white;">π κ²μλ νΈλ λ λΆμ</h3> | |
| </div> | |
| <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);"> | |
| {trend_chart} | |
| </div> | |
| </div> | |
| """ | |
| # === π― ν€μλ λΆμ μΉμ (AI λΆμ) === | |
| # api_utilsμμ Gemini λͺ¨λΈ κ°μ Έμ€κΈ° (λλ€ ν€ μ μ©) | |
| current_gemini_model = api_utils.get_gemini_model() | |
| keyword_analysis_html = keyword_analysis.analyze_keyword_for_sourcing( | |
| analysis_keyword, volume_data, monthly_data_1year, monthly_data_3year, | |
| None, [], current_gemini_model # κ°λ΅ λ²μ μμλ μΆκ° ν€μλ λ°μ΄ν° μμ | |
| ) | |
| keyword_analysis_section = f""" | |
| <div style="width: 100%; margin: 30px auto; font-family: 'Pretendard', sans-serif;"> | |
| <div style="background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); padding: 15px; border-radius: 10px 10px 0 0; color: white; text-align: center;"> | |
| <h3 style="margin: 0; font-size: 18px; color: white;">π― ν€μλ λΆμ</h3> | |
| </div> | |
| <div style="background: white; padding: 20px; border-radius: 0 0 10px 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden;"> | |
| {keyword_analysis_html} | |
| </div> | |
| </div> | |
| """ | |
| # κ²½κ³ μΉμ (νμν κ²½μ°) | |
| warning_section = "" | |
| if not trend_available: | |
| warning_section = f""" | |
| <div style="width: 100%; margin: 20px auto; padding: 15px; background: #fff3cd; border: 1px solid #ffeaa7; border-radius: 8px; font-family: 'Pretendard', sans-serif;"> | |
| <div style="display: flex; align-items: center;"> | |
| <span style="font-size: 20px; margin-right: 10px;">β οΈ</span> | |
| <div> | |
| <strong style="color: #856404;">μΌλΆ κΈ°λ₯ μ ν</strong> | |
| <div style="font-size: 14px; color: #856404; margin-top: 5px;"> | |
| νΈλ λ λΆμμ μ νμ΄ μμ΅λλ€. νμ¬ κ²μλ λΆμκ³Ό AI μΆμ²μ μ μ μ 곡λ©λλ€.<br> | |
| <small>μμ ν μ λ°μ΄ν° κΈ°μ€μΌλ‘ λΆμνκΈ° μν΄ μ΅μ μλ£λ μκΉμ§λ§ νμλ©λλ€.</small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| # μ΅μ’ κ²°κ³Ό μ‘°ν© | |
| final_result = warning_section + trend_section + keyword_analysis_section | |
| # μΈμ λ³ μΆλ ₯ λ°μ΄ν° λ°ν (λ©ν° μ¬μ©μ μμ ) | |
| session_export_data = { | |
| "analysis_keyword": analysis_keyword, | |
| "analysis_html": final_result | |
| } | |
| return final_result, session_export_data | |
| except Exception as e: | |
| logger.error(f"ν€μλ λΆμ μ€ μ 체 μ€λ₯: {e}") | |
| error_result = generate_error_response(f"ν€μλ λΆμ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}") | |
| return error_result, {} | |
| # ===== νμΌ μΆλ ₯ ν¨μλ€ ===== | |
| def create_timestamp_filename(analysis_keyword): | |
| """νμμ€ν¬νκ° ν¬ν¨λ νμΌλͺ μμ± - νκ΅μκ° μ μ©""" | |
| timestamp = format_korean_datetime(format_type="filename") | |
| safe_keyword = re.sub(r'[^\w\s-]', '', analysis_keyword).strip() | |
| safe_keyword = re.sub(r'[-\s]+', '_', safe_keyword) | |
| return f"{safe_keyword}_{timestamp}_λΆμκ²°κ³Ό" | |
| def export_to_html(analysis_html, filename_base): | |
| """HTML νμΌλ‘ μΆλ ₯ - νκ΅μκ° μ μ©""" | |
| try: | |
| html_filename = f"{filename_base}.html" | |
| html_path = os.path.join(tempfile.gettempdir(), html_filename) | |
| # νκ΅μκ°μΌλ‘ μμ± μκ° νμ | |
| korean_time = format_korean_datetime(format_type="display") | |
| # μμ ν HTML λ¬Έμ μμ± | |
| full_html = f""" | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>ν€μλ μ¬μΈ΅λΆμ κ²°κ³Ό</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"> | |
| <style> | |
| body {{ | |
| font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| line-height: 1.6; | |
| }} | |
| .container {{ | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| }} | |
| .header {{ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| }} | |
| .header h1 {{ | |
| margin: 0; | |
| font-size: 28px; | |
| font-weight: 700; | |
| }} | |
| .header p {{ | |
| margin: 10px 0 0 0; | |
| font-size: 16px; | |
| opacity: 0.9; | |
| }} | |
| .content {{ | |
| padding: 30px; | |
| }} | |
| .timestamp {{ | |
| text-align: center; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| color: #6c757d; | |
| font-size: 14px; | |
| border-top: 1px solid #dee2e6; | |
| }} | |
| /* μ°¨νΈ μ€νμΌ κ°μ */ | |
| .chart-container {{ | |
| margin: 20px 0; | |
| padding: 20px; | |
| background: white; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| }} | |
| /* λ°μν μ€νμΌ */ | |
| @media (max-width: 768px) {{ | |
| .container {{ | |
| margin: 10px; | |
| border-radius: 8px; | |
| }} | |
| .header {{ | |
| padding: 20px; | |
| }} | |
| .header h1 {{ | |
| font-size: 24px; | |
| }} | |
| .content {{ | |
| padding: 20px; | |
| }} | |
| }} | |
| /* μ λλ©μ΄μ */ | |
| @keyframes spin {{ | |
| 0% {{ transform: rotate(0deg); }} | |
| 100% {{ transform: rotate(360deg); }} | |
| }} | |
| @keyframes progress {{ | |
| 0% {{ transform: translateX(-100%); }} | |
| 100% {{ transform: translateX(100%); }} | |
| }} | |
| /* νλ¦°νΈ μ€νμΌ */ | |
| @media print {{ | |
| body {{ | |
| background: white; | |
| padding: 0; | |
| }} | |
| .container {{ | |
| box-shadow: none; | |
| border-radius: 0; | |
| }} | |
| .header {{ | |
| background: #667eea !important; | |
| -webkit-print-color-adjust: exact; | |
| }} | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1><i class="fas fa-chart-line"></i> ν€μλ μ¬μΈ΅λΆμ κ²°κ³Ό</h1> | |
| <p>AI μν μμ± λΆμ μμ€ν v2.10</p> | |
| </div> | |
| <div class="content"> | |
| {analysis_html} | |
| </div> | |
| <div class="timestamp"> | |
| <i class="fas fa-clock"></i> μμ± μκ°: {korean_time} (νκ΅μκ°) | |
| </div> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| with open(html_path, 'w', encoding='utf-8') as f: | |
| f.write(full_html) | |
| logger.info(f"HTML νμΌ μμ± μλ£: {html_path}") | |
| return html_path | |
| except Exception as e: | |
| logger.error(f"HTML νμΌ μμ± μ€λ₯: {e}") | |
| return None | |
| def create_zip_file(html_path, filename_base): | |
| """μμΆ νμΌ μμ± (HTMLλ§)""" | |
| try: | |
| zip_filename = f"{filename_base}.zip" | |
| zip_path = os.path.join(tempfile.gettempdir(), zip_filename) | |
| with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf: | |
| if html_path and os.path.exists(html_path): | |
| zipf.write(html_path, f"{filename_base}.html") | |
| logger.info(f"HTML νμΌ μμΆ μΆκ°: {filename_base}.html") | |
| logger.info(f"μμΆ νμΌ μμ± μλ£: {zip_path}") | |
| return zip_path | |
| except Exception as e: | |
| logger.error(f"μμΆ νμΌ μμ± μ€λ₯: {e}") | |
| return None | |
| def export_analysis_results(export_data): | |
| """λΆμ κ²°κ³Ό μΆλ ₯ λ©μΈ ν¨μ - μΈμ λ³ λ°μ΄ν° μ²λ¦¬""" | |
| try: | |
| # μΆλ ₯ν λ°μ΄ν° νμΈ | |
| if not export_data or not isinstance(export_data, dict): | |
| return None, "λΆμ λ°μ΄ν°κ° μμ΅λλ€. λ¨Όμ ν€μλ μ¬μΈ΅λΆμμ μ€νν΄μ£ΌμΈμ." | |
| analysis_keyword = export_data.get("analysis_keyword", "") | |
| analysis_html = export_data.get("analysis_html", "") | |
| if not analysis_keyword: | |
| return None, "λΆμν ν€μλκ° μ€μ λμ§ μμμ΅λλ€. λ¨Όμ ν€μλ λΆμμ μ€νν΄μ£ΌμΈμ." | |
| if not analysis_html: | |
| return None, "λΆμ κ²°κ³Όκ° μμ΅λλ€. λ¨Όμ ν€μλ μ¬μΈ΅λΆμμ μ€νν΄μ£ΌμΈμ." | |
| # νμΌλͺ μμ± (νκ΅μκ° μ μ©) | |
| filename_base = create_timestamp_filename(analysis_keyword) | |
| logger.info(f"μΆλ ₯ νμΌλͺ : {filename_base}") | |
| # HTML νμΌ μμ± | |
| html_path = export_to_html(analysis_html, filename_base) | |
| # μμΆ νμΌ μμ± | |
| if html_path: | |
| zip_path = create_zip_file(html_path, filename_base) | |
| if zip_path: | |
| return zip_path, f"β λΆμ κ²°κ³Όκ° μ±κ³΅μ μΌλ‘ μΆλ ₯λμμ΅λλ€!\nνμΌλͺ : {filename_base}.zip" | |
| else: | |
| return None, "μμΆ νμΌ μμ±μ μ€ν¨νμ΅λλ€." | |
| else: | |
| return None, "μΆλ ₯ν νμΌμ΄ μμ΅λλ€." | |
| except Exception as e: | |
| logger.error(f"λΆμ κ²°κ³Ό μΆλ ₯ μ€λ₯: {e}") | |
| return None, f"μΆλ ₯ μ€ μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}" | |
| # ===== κ·ΈλΌλμ€ μΈν°νμ΄μ€ ===== | |
| def create_interface(): | |
| # CSS νμΌ λ‘λ | |
| try: | |
| with open('style.css', 'r', encoding='utf-8') as f: | |
| custom_css = f.read() | |
| with open('keyword_analysis_report.css', 'r', encoding='utf-8') as f: | |
| keyword_css = f.read() | |
| custom_css += "\n" + keyword_css | |
| except: | |
| custom_css = """ | |
| :root { --primary-color: #FB7F0D; --secondary-color: #ff9a8b; } | |
| .custom-button { | |
| background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important; | |
| color: white !important; border-radius: 30px !important; height: 45px !important; | |
| font-size: 16px !important; font-weight: bold !important; width: 100% !important; | |
| } | |
| .export-button { | |
| background: linear-gradient(135deg, #28a745, #20c997) !important; | |
| color: white !important; border-radius: 25px !important; height: 50px !important; | |
| font-size: 17px !important; font-weight: bold !important; width: 100% !important; | |
| margin-top: 20px !important; | |
| } | |
| """ | |
| with gr.Blocks( | |
| css=custom_css, | |
| title="π AI μν μμ± λΆμκΈ° v2.10", | |
| theme=gr.themes.Default(primary_hue="orange", secondary_hue="orange") | |
| ) as interface: | |
| # ν°νΈ λ° μμ΄μ½ λ‘λ | |
| gr.HTML(""" | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css"> | |
| """) | |
| # μΈμ λ³ μν κ΄λ¦¬ (λ©ν° μ¬μ©μ μμ ) | |
| export_data_state = gr.State({}) | |
| # === ν€μλ μ¬μΈ΅λΆμ μ λ ₯ === | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-bullseye"></i> ν€μλ μ¬μΈ΅λΆμ μ λ ₯</div>') | |
| analysis_keyword_input = gr.Textbox( | |
| label="λΆμν ν€μλ", | |
| placeholder="μ¬μΈ΅ λΆμν ν€μλλ₯Ό μ λ ₯νμΈμ (μ: ν΅κ΅½ μ¬λ¦¬νΌ)", | |
| value="", | |
| elem_id="analysis_keyword_input" | |
| ) | |
| analyze_keyword_btn = gr.Button("ν€μλ μ¬μΈ΅λΆμ νκΈ°", elem_classes="custom-button", size="lg") | |
| # === ν€μλ μ¬μΈ΅λΆμ === | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-chart-line"></i> ν€μλ μ¬μΈ΅λΆμ</div>') | |
| analysis_result = gr.HTML(label="ν€μλ μ¬μΈ΅λΆμ") | |
| # === κ²°κ³Ό μΆλ ₯ μΉμ === | |
| with gr.Column(elem_classes="custom-frame fade-in"): | |
| gr.HTML('<div class="section-title"><i class="fas fa-download"></i> λΆμ κ²°κ³Ό μΆλ ₯</div>') | |
| export_btn = gr.Button("π λΆμκ²°κ³Ό μΆλ ₯νκΈ°", elem_classes="export-button", size="lg") | |
| export_result = gr.HTML() | |
| download_file = gr.File(label="λ€μ΄λ‘λ", visible=False) | |
| # ===== μ΄λ²€νΈ νΈλ€λ¬ ===== | |
| def on_analyze_keyword(analysis_keyword): | |
| if not analysis_keyword.strip(): | |
| return "λΆμν ν€μλλ₯Ό μ λ ₯ν΄μ£ΌμΈμ.", {} | |
| # λ‘λ© μν νμ | |
| yield create_loading_animation(), {} | |
| # μ€μ ν€μλ λΆμ μ€ν | |
| keyword_result, session_export_data = safe_keyword_analysis(analysis_keyword) | |
| # π κ²μλ νΈλ λ λΆμκ³Ό π― ν€μλ λΆμ νμ | |
| yield keyword_result, session_export_data | |
| def on_export_results(export_data): | |
| """λΆμ κ²°κ³Ό μΆλ ₯ νΈλ€λ¬ - μΈμ λ³ λ°μ΄ν° μ²λ¦¬""" | |
| try: | |
| zip_path, message = export_analysis_results(export_data) | |
| if zip_path: | |
| # μ±κ³΅ λ©μμ§μ ν¨κ» λ€μ΄λ‘λ νμΌ μ 곡 | |
| success_html = f""" | |
| <div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #155724; margin: 0 0 15px 0;"><i class="fas fa-check-circle"></i> μΆλ ₯ μλ£!</h4> | |
| <p style="color: #155724; margin: 0; line-height: 1.6;"> | |
| {message}<br> | |
| <strong>ν¬ν¨ νμΌ:</strong><br> | |
| β’ π HTML νμΌ: ν€μλ μ¬μΈ΅λΆμ κ²°κ³Ό (κ·Έλν ν¬ν¨)<br> | |
| <br> | |
| <i class="fas fa-download"></i> μλ λ€μ΄λ‘λ λ²νΌμ ν΄λ¦νμ¬ νμΌμ μ μ₯νμΈμ.<br> | |
| <small style="color: #666;">β° νκ΅μκ° κΈ°μ€μΌλ‘ νμΌλͺ μ΄ μμ±λ©λλ€.</small> | |
| </p> | |
| </div> | |
| """ | |
| return success_html, gr.update(value=zip_path, visible=True) | |
| else: | |
| # μ€ν¨ λ©μμ§ | |
| error_html = f""" | |
| <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> μΆλ ₯ μ€ν¨</h4> | |
| <p style="color: #721c24; margin: 0;">{message}</p> | |
| </div> | |
| """ | |
| return error_html, gr.update(visible=False) | |
| except Exception as e: | |
| logger.error(f"μΆλ ₯ νΈλ€λ¬ μ€λ₯: {e}") | |
| error_html = f""" | |
| <div style="background: #f8d7da; border: 1px solid #f5c6cb; padding: 20px; border-radius: 8px; margin: 10px 0;"> | |
| <h4 style="color: #721c24; margin: 0 0 10px 0;"><i class="fas fa-exclamation-triangle"></i> μμ€ν μ€λ₯</h4> | |
| <p style="color: #721c24; margin: 0;">μΆλ ₯ μ€ μμ€ν μ€λ₯κ° λ°μνμ΅λλ€: {str(e)}</p> | |
| </div> | |
| """ | |
| return error_html, gr.update(visible=False) | |
| # ===== μ΄λ²€νΈ μ°κ²° ===== | |
| analyze_keyword_btn.click( | |
| fn=on_analyze_keyword, | |
| inputs=[analysis_keyword_input], | |
| outputs=[analysis_result, export_data_state] | |
| ) | |
| export_btn.click( | |
| fn=on_export_results, | |
| inputs=[export_data_state], | |
| outputs=[export_result, download_file] | |
| ) | |
| return interface | |
| # ===== API μ€μ νμΈ ν¨μ ===== | |
| def check_datalab_api_config(): | |
| """λ€μ΄λ² λ°μ΄ν°λ© API μ€μ νμΈ""" | |
| logger.info("=== λ€μ΄λ² λ°μ΄ν°λ© API μ€μ νμΈ ===") | |
| datalab_config = api_utils.get_next_datalab_api_config() | |
| if not datalab_config: | |
| logger.warning("β λ°μ΄ν°λ© API ν€κ° μ€μ λμ§ μμμ΅λλ€.") | |
| logger.info("νΈλ λ λΆμ κΈ°λ₯μ΄ λΉνμ±νλ©λλ€.") | |
| return False | |
| client_id = datalab_config["CLIENT_ID"] | |
| client_secret = datalab_config["CLIENT_SECRET"] | |
| logger.info(f"μ΄ {len(api_utils.NAVER_DATALAB_CONFIGS)}κ°μ λ°μ΄ν°λ© API μ€μ μ¬μ© μ€") | |
| logger.info(f"νμ¬ μ νλ API:") | |
| logger.info(f" CLIENT_ID: {client_id[:8]}***{client_id[-4:] if len(client_id) > 12 else '***'}") | |
| logger.info(f" CLIENT_SECRET: {client_secret[:4]}***{client_secret[-2:] if len(client_secret) > 6 else '***'}") | |
| # κΈ°λ³Έκ° μ²΄ν¬ | |
| if client_id.startswith("YOUR_"): | |
| logger.error("β CLIENT_IDκ° κΈ°λ³Έκ°μΌλ‘ μ€μ λμ΄ μμ΅λλ€!") | |
| return False | |
| if client_secret.startswith("YOUR_"): | |
| logger.error("β CLIENT_SECRETμ΄ κΈ°λ³Έκ°μΌλ‘ μ€μ λμ΄ μμ΅λλ€!") | |
| return False | |
| # κΈΈμ΄ μ²΄ν¬ | |
| if len(client_id) < 10: | |
| logger.warning("β οΈ CLIENT_IDκ° μ§§μ΅λλ€. μ¬λ°λ₯Έ ν€μΈμ§ νμΈν΄μ£ΌμΈμ.") | |
| if len(client_secret) < 5: | |
| logger.warning("β οΈ CLIENT_SECRETμ΄ μ§§μ΅λλ€. μ¬λ°λ₯Έ ν€μΈμ§ νμΈν΄μ£ΌμΈμ.") | |
| logger.info("β λ°μ΄ν°λ© API ν€ νμ κ²μ¦ μλ£") | |
| return True | |
| def check_gemini_api_config(): | |
| """Gemini API μ€μ νμΈ - λλ€ ν€ μ μ©""" | |
| logger.info("=== Gemini API μ€μ νμΈ ===") | |
| is_valid, message = api_utils.validate_gemini_config() | |
| if is_valid: | |
| logger.info(f"β {message}") | |
| # 첫 λ²μ§Έ μ¬μ© κ°λ₯ν ν€ ν μ€νΈ (λλ€) | |
| test_key = api_utils.get_next_gemini_api_key() | |
| if test_key: | |
| logger.info(f"νμ¬ μ¬μ© μ€μΈ Gemini API ν€: {test_key[:8]}***{test_key[-4:]}") | |
| return True | |
| else: | |
| logger.warning(f"β {message}") | |
| logger.info("AI λΆμ κΈ°λ₯μ΄ μ νλ μ μμ΅λλ€.") | |
| return False | |
| # ===== λ©μΈ μ€ν ===== | |
| if __name__ == "__main__": | |
| # pytz λͺ¨λ μ€μΉ νμΈ | |
| if PYTZ_AVAILABLE: | |
| logger.info("β pytz λͺ¨λ λ‘λ μ±κ³΅ - νκ΅μκ° μ§μ") | |
| else: | |
| logger.warning("β οΈ pytz λͺ¨λμ΄ μ€μΉλμ§ μμ - pip install pytz μ€ν νμ") | |
| logger.info("μμ€ν μκ°μ μ¬μ©ν©λλ€.") | |
| # API μ€μ μ΄κΈ°ν | |
| api_utils.initialize_api_configs() | |
| logger.info("===== μν μμ± λΆμ μμ€ν v2.10 (κ°λ΅λ²μ + μΆλ ₯κΈ°λ₯ + λλ€ν€ + λ©ν°μ¬μ©μ μμ ) μμ =====") | |
| # λ€μ΄λ² λ°μ΄ν°λ© API μ€μ νμΈ | |
| datalab_available = check_datalab_api_config() | |
| # Gemini API μ€μ νμΈ (λλ€ ν€) | |
| gemini_available = check_gemini_api_config() | |
| # νμν ν¨ν€μ§ μλ΄ | |
| print("π¦ νμν ν¨ν€μ§:") | |
| print(" pip install gradio google-generativeai pandas requests xlsxwriter markdown plotly pytz") | |
| print() | |
| # API ν€ μ€μ μλ΄ | |
| if not gemini_available: | |
| print("β οΈ GEMINI_API_KEY λλ GOOGLE_API_KEY νκ²½λ³μλ₯Ό μ€μ νμΈμ.") | |
| print(" export GEMINI_API_KEY='your-api-key'") | |
| print(" λλ") | |
| print(" export GOOGLE_API_KEY='your-api-key'") | |
| print() | |
| if not datalab_available: | |
| print("β οΈ λ€μ΄λ² λ°μ΄ν°λ© API νΈλ λ λΆμμ μν΄μλ:") | |
| print(" 1. λ€μ΄λ² κ°λ°μμΌν°(https://developers.naver.com)μμ μ ν리μΌμ΄μ λ±λ‘") | |
| print(" 2. 'λ°μ΄ν°λ©(κ²μμ΄ νΈλ λ)' API μΆκ°") | |
| print(" 3. λ°κΈλ°μ CLIENT_IDμ CLIENT_SECRETμ api_utils.pyμ NAVER_DATALAB_CONFIGSμ μ€μ ") | |
| print(" 4. νμ¬λ νμ¬ κ²μλ μ λ³΄λ§ νμλ©λλ€.") | |
| print() | |
| else: | |
| print("β λ°μ΄ν°λ© API μ€μ μλ£ - 1λ , 3λ νΈλ λ λΆμμ΄ κ°λ₯ν©λλ€!") | |
| print() | |
| if gemini_available: | |
| print("β Gemini API μ€μ μλ£ - λλ€ ν€ λ‘ν μ΄μ μ μ©λ©λλ€!") | |
| print() | |
| print("π v2.10 κ°μ μ¬ν:") | |
| print(" β’ 2λ¨κ³: μμ§λ ν€μλ λͺ©λ‘ κΈ°λ₯ μ κ±°") | |
| print(" β’ 3λ¨κ³: μ°κ΄κ²μμ΄ λΆμμ μνμΆμΆ λ° λΆμ κΈ°λ₯ μ κ±°") | |
| print(" β’ 3λ¨κ³: 'λΆμν ν€μλ μ ν' β 'ν€μλ μ¬μΈ΅λΆμ μ λ ₯'μΌλ‘ λͺ μΉ λ³κ²½") | |
| print(" β’ π κ²μλ νΈλ λ λΆμκ³Ό π― ν€μλ λΆμλ§ νμ") | |
| print(" β’ β μΆλ ₯ κΈ°λ₯ μΆκ°: HTML νμΌ μμ± λ° ZIP λ€μ΄λ‘λ") | |
| print(" β’ β Gemini API ν€ λλ€ λ‘ν μ΄μ μ μ©") | |
| print(" β’ β λ€μ΄λ² λ°μ΄ν°λ© API ν€ λλ€ λ‘ν μ΄μ μ μ©") | |
| print(" β’ β νκ΅μκ° κΈ°μ€ νμΌλͺ μμ±") | |
| print(" β’ β λ©ν° μ¬μ©μ μμ : gr.Stateλ‘ μΈμ λ³ λ°μ΄ν° κ΄λ¦¬") | |
| print(" β’ λΆνμν λͺ¨λ μν¬νΈ μ κ±°λ‘ μμ μ± ν₯μ") | |
| print() | |
| # μ± μ€ν | |
| app = create_interface() | |
| app.launch(server_name="0.0.0.0", server_port=7860, share=True) |