|
import gradio as gr |
|
import pandas as pd |
|
import os |
|
import logging |
|
from datetime import datetime |
|
import pytz |
|
import time |
|
import tempfile |
|
import zipfile |
|
import re |
|
import json |
|
|
|
|
|
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s') |
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
logging.getLogger('gradio').setLevel(logging.WARNING) |
|
logging.getLogger('gradio_client').setLevel(logging.WARNING) |
|
logging.getLogger('httpx').setLevel(logging.WARNING) |
|
logging.getLogger('urllib3').setLevel(logging.WARNING) |
|
|
|
|
|
def get_api_client(): |
|
"""νκ²½λ³μμμ API μλν¬μΈνΈλ₯Ό κ°μ Έμ ν΄λΌμ΄μΈνΈ μμ±""" |
|
try: |
|
from gradio_client import Client |
|
|
|
|
|
api_endpoint = os.getenv('API_ENDPOINT') |
|
|
|
if not api_endpoint: |
|
logger.error("API μλν¬μΈνΈκ° μ€μ λμ§ μμμ΅λλ€.") |
|
raise ValueError("API μλν¬μΈνΈκ° μ€μ λμ§ μμμ΅λλ€.") |
|
|
|
client = Client(api_endpoint) |
|
logger.info("μ격 API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μ±κ³΅") |
|
return client |
|
|
|
except Exception as e: |
|
logger.error(f"API ν΄λΌμ΄μΈνΈ μ΄κΈ°ν μ€ν¨: {e}") |
|
return None |
|
|
|
|
|
def get_korean_time(): |
|
"""νκ΅μκ° λ°ν""" |
|
try: |
|
korea_tz = pytz.timezone('Asia/Seoul') |
|
return datetime.now(korea_tz) |
|
except: |
|
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 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") |
|
|
|
|
|
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_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 call_analyze_keyword_api(analysis_keyword): |
|
"""ν€μλ μ¬μΈ΅λΆμ API νΈμΆ""" |
|
try: |
|
client = get_api_client() |
|
if not client: |
|
return generate_error_response("API ν΄λΌμ΄μΈνΈλ₯Ό μ΄κΈ°νν μ μμ΅λλ€."), {} |
|
|
|
logger.info("μ격 API νΈμΆ: ν€μλ μ¬μΈ΅λΆμ") |
|
result = client.predict( |
|
analysis_keyword=analysis_keyword, |
|
api_name="/on_analyze_keyword" |
|
) |
|
|
|
logger.info(f"ν€μλ λΆμ API κ²°κ³Ό νμ
: {type(result)}") |
|
|
|
|
|
if isinstance(result, str) and len(result) > 100: |
|
export_data = { |
|
"analysis_keyword": analysis_keyword, |
|
"analysis_html": result, |
|
"analysis_completed": True, |
|
"created_at": get_korean_time().isoformat() |
|
} |
|
return result, export_data |
|
else: |
|
return str(result), {} |
|
|
|
except Exception as e: |
|
logger.error(f"ν€μλ μ¬μΈ΅λΆμ API νΈμΆ μ€λ₯: {e}") |
|
return generate_error_response(f"μ격 μλ² μ°κ²° μ€ν¨: {str(e)}"), {} |
|
|
|
def call_export_results_api(export_data): |
|
"""λΆμ κ²°κ³Ό μΆλ ₯ API νΈμΆ""" |
|
try: |
|
client = get_api_client() |
|
if not client: |
|
return None, "API ν΄λΌμ΄μΈνΈλ₯Ό μ΄κΈ°νν μ μμ΅λλ€." |
|
|
|
logger.info("μ격 API νΈμΆ: λΆμ κ²°κ³Ό μΆλ ₯") |
|
result = client.predict( |
|
api_name="/on_export_results" |
|
) |
|
|
|
logger.info(f"μΆλ ₯ API κ²°κ³Ό νμ
: {type(result)}") |
|
|
|
|
|
if isinstance(result, tuple) and len(result) == 2: |
|
message, file_path = result |
|
if file_path: |
|
return file_path, message |
|
else: |
|
return None, message |
|
else: |
|
return None, str(result) |
|
|
|
except Exception as e: |
|
logger.error(f"λΆμ κ²°κ³Ό μΆλ ₯ API νΈμΆ μ€λ₯: {e}") |
|
return None, f"μ격 μλ² μ°κ²° μ€ν¨: {str(e)}" |
|
|
|
|
|
def create_interface(): |
|
|
|
try: |
|
with open('style.css', 'r', encoding='utf-8') as f: |
|
custom_css = f.read() |
|
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; |
|
} |
|
.custom-frame { |
|
background-color: white !important; |
|
border: 1px solid #e5e5e5 !important; |
|
border-radius: 18px; |
|
padding: 20px; |
|
margin: 10px 0; |
|
box-shadow: 0 8px 30px rgba(251, 127, 13, 0.08) !important; |
|
} |
|
.section-title { |
|
display: flex; |
|
align-items: center; |
|
font-size: 20px; |
|
font-weight: 700; |
|
color: #334155 !important; |
|
margin-bottom: 10px; |
|
padding-bottom: 5px; |
|
border-bottom: 2px solid var(--primary-color); |
|
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif; |
|
} |
|
.section-title img, .section-title i { |
|
margin-right: 10px; |
|
font-size: 20px; |
|
color: var(--primary-color); |
|
} |
|
""" |
|
|
|
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 = call_analyze_keyword_api(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: |
|
|
|
try: |
|
remote_file, remote_message = call_export_results_api(export_data) |
|
if remote_file: |
|
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;"> |
|
{remote_message}<br> |
|
<i class="fas fa-download"></i> μλ λ€μ΄λ‘λ λ²νΌμ ν΄λ¦νμ¬ νμΌμ μ μ₯νμΈμ. |
|
</p> |
|
</div> |
|
""" |
|
return success_html, gr.update(value=remote_file, 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}<br>μ격 μΆλ ₯λ μ€ν¨: {remote_message}</p> |
|
</div> |
|
""" |
|
return error_html, gr.update(visible=False) |
|
except: |
|
|
|
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 |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
try: |
|
import pytz |
|
logger.info("β
pytz λͺ¨λ λ‘λ μ±κ³΅ - νκ΅μκ° μ§μ") |
|
except ImportError: |
|
logger.warning("β οΈ pytz λͺ¨λμ΄ μ€μΉλμ§ μμ - pip install pytz μ€ν νμ") |
|
logger.info("μμ€ν
μκ°μ μ¬μ©ν©λλ€.") |
|
|
|
logger.info("===== μν μμ± λΆμ μμ€ν
v2.10 (컨νΈλ‘€ νμ λ²μ ) μμ =====") |
|
|
|
|
|
print("π¦ νμν ν¨ν€μ§:") |
|
print(" pip install gradio gradio_client pandas pytz") |
|
print() |
|
|
|
|
|
api_endpoint = os.getenv('API_ENDPOINT') |
|
if not api_endpoint: |
|
print("β οΈ API_ENDPOINT νκ²½λ³μλ₯Ό μ€μ νμΈμ.") |
|
print(" export API_ENDPOINT='your-endpoint-url'") |
|
print() |
|
else: |
|
print("β
API μλν¬μΈνΈ μ€μ μλ£!") |
|
print() |
|
|
|
print("π v2.10 컨νΈλ‘€ νμ λ²μ νΉμ§:") |
|
print(" β’ νκΉ
νμ΄μ€ κ·ΈλΌλμ€ μλν¬μΈνΈ νμ©") |
|
print(" β’ μμ ν λμΌν UIμ κΈ°λ₯ ꡬν") |
|
print(" β’ π κ²μλ νΈλ λ λΆμκ³Ό π― ν€μλ λΆμ νμ") |
|
print(" β’ β
μΆλ ₯ κΈ°λ₯: HTML νμΌ μμ± λ° ZIP λ€μ΄λ‘λ") |
|
print(" β’ β
νκ΅μκ° κΈ°μ€ νμΌλͺ
μμ±") |
|
print(" β’ β
λ©ν° μ¬μ©μ μμ : gr.Stateλ‘ μΈμ
λ³ λ°μ΄ν° κ΄λ¦¬") |
|
print(" β’ π ν΄λΌμ΄μΈνΈ μ 보 νκ²½λ³μλ‘ μμ μ¨κΉ") |
|
print(" β’ μ격 μλ²μ λ‘컬 μ²λ¦¬ νμ΄λΈλ¦¬λ λ°©μ") |
|
print() |
|
|
|
|
|
app = create_interface() |
|
app.launch(server_name="0.0.0.0", server_port=7860, share=True) |