4G23WAS3 / app.py
ssboost's picture
Update app.py
aec6db2 verified
raw
history blame
32.9 kB
import gradio as gr
import pandas as pd
import os
import time
import threading
import tempfile
import logging
import random
import uuid
import shutil
import glob
from datetime import datetime
from gradio_client import Client
# ๋กœ๊น… ์„ค์ •
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('control_tower_app.log', mode='a')
]
)
logger = logging.getLogger(__name__)
# API ํด๋ผ์ด์–ธํŠธ ์„ค์ •
def get_client():
# ํ™˜๊ฒฝ๋ณ€์ˆ˜์—์„œ API ์—”๋“œํฌ์ธํŠธ ์ฝ๊ธฐ
endpoint = os.environ.get('API_ENDPOINT', '')
# ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ํŒŒ์ผ ๋‚ด์šฉ์ด๋‚˜ ๋ถˆํ•„์š”ํ•œ ํ…์ŠคํŠธ๊ฐ€ ๋“ค์–ด์˜จ ๊ฒฝ์šฐ ์ •๋ฆฌ
if endpoint:
# ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๋ถ„๋ฆฌํ•ด์„œ ์ฒซ ๋ฒˆ์งธ ์œ ํšจํ•œ ๋ผ์ธ ์ฐพ๊ธฐ
lines = endpoint.split('\n')
for line in lines:
line = line.strip()
# ์ฃผ์„์ด๋‚˜ ๋น„์–ด์žˆ๋Š” ๋ผ์ธ ์ œ์™ธ
if line and not line.startswith('#') and '/' in line:
# API_ENDPOINT= ๊ฐ™์€ ํ‚ค๊ฐ’ ์ œ๊ฑฐ
if '=' in line:
line = line.split('=', 1)[1].strip()
# ๋”ฐ์˜ดํ‘œ ์ œ๊ฑฐ
line = line.strip('"\'')
if line and '/' in line and len(line) < 50:
return Client(line)
raise ValueError("์˜ฌ๋ฐ”๋ฅธ API_ENDPOINT๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š” (์˜ˆ: username/repo-name)")
# ์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•œ ๋”•์…”๋„ˆ๋ฆฌ
session_temp_files = {}
session_data = {}
def cleanup_huggingface_temp_folders():
"""ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ดˆ๊ธฐ ์ •๋ฆฌ"""
try:
temp_dirs = [
tempfile.gettempdir(),
"/tmp",
"/var/tmp",
os.path.join(os.getcwd(), "temp"),
os.path.join(os.getcwd(), "tmp"),
"/gradio_cached_examples",
"/flagged"
]
cleanup_count = 0
for temp_dir in temp_dirs:
if os.path.exists(temp_dir):
try:
session_files = glob.glob(os.path.join(temp_dir, "session_*.xlsx"))
session_files.extend(glob.glob(os.path.join(temp_dir, "session_*.csv")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "*keyword*.csv")))
session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.xlsx")))
session_files.extend(glob.glob(os.path.join(temp_dir, "tmp*.csv")))
for file_path in session_files:
try:
if os.path.getmtime(file_path) < time.time() - 3600:
os.remove(file_path)
cleanup_count += 1
logger.info(f"์ดˆ๊ธฐ ์ •๋ฆฌ: ์˜ค๋ž˜๋œ ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ - {file_path}")
except Exception as e:
logger.warning(f"ํŒŒ์ผ ์‚ญ์ œ ์‹คํŒจ (๋ฌด์‹œ๋จ): {file_path} - {e}")
except Exception as e:
logger.warning(f"์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ •๋ฆฌ ์‹คํŒจ (๋ฌด์‹œ๋จ): {temp_dir} - {e}")
logger.info(f"โœ… ํ—ˆ๊น…ํŽ˜์ด์Šค ์ž„์‹œ ํด๋” ์ดˆ๊ธฐ ์ •๋ฆฌ ์™„๋ฃŒ - {cleanup_count}๊ฐœ ํŒŒ์ผ ์‚ญ์ œ")
try:
gradio_temp_dir = os.path.join(os.getcwd(), "gradio_cached_examples")
if os.path.exists(gradio_temp_dir):
shutil.rmtree(gradio_temp_dir, ignore_errors=True)
logger.info("Gradio ์บ์‹œ ํด๋” ์ •๋ฆฌ ์™„๋ฃŒ")
except Exception as e:
logger.warning(f"Gradio ์บ์‹œ ํด๋” ์ •๋ฆฌ ์‹คํŒจ (๋ฌด์‹œ๋จ): {e}")
except Exception as e:
logger.error(f"์ดˆ๊ธฐ ์ž„์‹œ ํด๋” ์ •๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ (๊ณ„์† ์ง„ํ–‰): {e}")
def setup_clean_temp_environment():
"""๊นจ๋—ํ•œ ์ž„์‹œ ํ™˜๊ฒฝ ์„ค์ •"""
try:
cleanup_huggingface_temp_folders()
app_temp_dir = os.path.join(tempfile.gettempdir(), "keyword_app")
if os.path.exists(app_temp_dir):
shutil.rmtree(app_temp_dir, ignore_errors=True)
os.makedirs(app_temp_dir, exist_ok=True)
os.environ['KEYWORD_APP_TEMP'] = app_temp_dir
logger.info(f"โœ… ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์„ค์ •: {app_temp_dir}")
return app_temp_dir
except Exception as e:
logger.error(f"์ž„์‹œ ํ™˜๊ฒฝ ์„ค์ • ์‹คํŒจ: {e}")
return tempfile.gettempdir()
def get_app_temp_dir():
"""์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์šฉ ์ž„์‹œ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ˜ํ™˜"""
return os.environ.get('KEYWORD_APP_TEMP', tempfile.gettempdir())
def get_session_id():
"""์„ธ์…˜ ID ์ƒ์„ฑ"""
return str(uuid.uuid4())
def cleanup_session_files(session_id, delay=300):
"""์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ์ •๋ฆฌ ํ•จ์ˆ˜"""
def cleanup():
time.sleep(delay)
if session_id in session_temp_files:
files_to_remove = session_temp_files[session_id].copy()
del session_temp_files[session_id]
for file_path in files_to_remove:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"์„ธ์…˜ {session_id[:8]}... ์ž„์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
except Exception as e:
logger.error(f"์„ธ์…˜ {session_id[:8]}... ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
threading.Thread(target=cleanup, daemon=True).start()
def register_session_file(session_id, file_path):
"""์„ธ์…˜๋ณ„ ํŒŒ์ผ ๋“ฑ๋ก"""
if session_id not in session_temp_files:
session_temp_files[session_id] = []
session_temp_files[session_id].append(file_path)
def cleanup_old_sessions():
"""์˜ค๋ž˜๋œ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์ •๋ฆฌ"""
current_time = time.time()
sessions_to_remove = []
for session_id, data in session_data.items():
if current_time - data.get('last_activity', 0) > 3600:
sessions_to_remove.append(session_id)
for session_id in sessions_to_remove:
if session_id in session_temp_files:
for file_path in session_temp_files[session_id]:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"์˜ค๋ž˜๋œ ์„ธ์…˜ {session_id[:8]}... ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
except Exception as e:
logger.error(f"์˜ค๋ž˜๋œ ์„ธ์…˜ ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
del session_temp_files[session_id]
if session_id in session_data:
del session_data[session_id]
logger.info(f"์˜ค๋ž˜๋œ ์„ธ์…˜ ๋ฐ์ดํ„ฐ ์‚ญ์ œ: {session_id[:8]}...")
def update_session_activity(session_id):
"""์„ธ์…˜ ํ™œ๋™ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ"""
if session_id not in session_data:
session_data[session_id] = {}
session_data[session_id]['last_activity'] = time.time()
def create_session_temp_file(session_id, suffix='.xlsx'):
"""์„ธ์…˜๋ณ„ ์ž„์‹œ ํŒŒ์ผ ์ƒ์„ฑ (์ „์šฉ ๋””๋ ‰ํ† ๋ฆฌ ์‚ฌ์šฉ)"""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
random_suffix = str(random.randint(1000, 9999))
temp_dir = get_app_temp_dir()
filename = f"session_{session_id[:8]}_{timestamp}_{random_suffix}{suffix}"
temp_file_path = os.path.join(temp_dir, filename)
with open(temp_file_path, 'w') as f:
pass
register_session_file(session_id, temp_file_path)
return temp_file_path
def wrapper_modified(keyword, korean_only, apply_main_keyword_option, exclude_zero_volume, session_id):
"""ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰ ๋ฐ ์ฒ˜๋ฆฌ ๋ž˜ํผ ํ•จ์ˆ˜ (API ์‚ฌ์šฉ)"""
update_session_activity(session_id)
try:
client = get_client()
result = client.predict(
keyword=keyword,
korean_only=korean_only,
apply_main_keyword=apply_main_keyword_option,
exclude_zero_volume=exclude_zero_volume,
api_name="/process_search_results"
)
# API ์‘๋‹ต ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ (6๊ฐœ ๊ฐ’)
logger.info(f"API ์‘๋‹ต ํƒ€์ž…: {type(result)}, ๊ธธ์ด: {len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
logger.info(f"API ์‘๋‹ต ๋‚ด์šฉ: {result}")
if isinstance(result, (list, tuple)) and len(result) >= 6:
table_html, cat_choices, vol_choices, selected_cat, download_file, extra = result[:6]
logger.info(f"table_html ํƒ€์ž…: {type(table_html)}")
logger.info(f"cat_choices: {cat_choices}")
logger.info(f"vol_choices: {vol_choices}")
logger.info(f"selected_cat: {selected_cat}")
logger.info(f"download_file: {download_file}")
elif isinstance(result, (list, tuple)) and len(result) >= 5:
table_html, cat_choices, vol_choices, selected_cat, download_file = result[:5]
extra = None
else:
# ์‘๋‹ต์ด ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ
logger.warning(f"์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ API ์‘๋‹ต: {result}")
table_html = "<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</p>"
cat_choices = ["์ „์ฒด ๋ณด๊ธฐ"]
vol_choices = ["์ „์ฒด"]
selected_cat = "์ „์ฒด ๋ณด๊ธฐ"
download_file = None
# table_html ์ฒ˜๋ฆฌ (dict์ธ ๊ฒฝ์šฐ value ์ถ”์ถœ)
if isinstance(table_html, dict) and 'value' in table_html:
table_html = table_html['value']
elif isinstance(table_html, dict):
table_html = str(table_html) # dict๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
# choices ํ˜•์‹ ์ฒ˜๋ฆฌ (์ค‘์ฒฉ ๋ฆฌ์ŠคํŠธ์ธ ๊ฒฝ์šฐ ์ฒซ ๋ฒˆ์งธ ๊ฐ’๋งŒ ์‚ฌ์šฉ)
if isinstance(cat_choices, dict) and 'choices' in cat_choices:
cat_choices = [choice[0] if isinstance(choice, list) else choice for choice in cat_choices['choices']]
elif isinstance(cat_choices, list) and cat_choices and isinstance(cat_choices[0], list):
cat_choices = [choice[0] for choice in cat_choices]
if isinstance(vol_choices, dict) and 'choices' in vol_choices:
vol_choices = [choice[0] if isinstance(choice, list) else choice for choice in vol_choices['choices']]
elif isinstance(vol_choices, list) and vol_choices and isinstance(vol_choices[0], list):
vol_choices = [choice[0] for choice in vol_choices]
# selected_cat ์ฒ˜๋ฆฌ
if isinstance(selected_cat, dict) and 'value' in selected_cat:
selected_cat = selected_cat['value']
elif isinstance(selected_cat, list):
selected_cat = selected_cat[0] if selected_cat else "์ „์ฒด ๋ณด๊ธฐ"
logger.info(f"์ฒ˜๋ฆฌ๋œ cat_choices: {cat_choices}")
logger.info(f"์ฒ˜๋ฆฌ๋œ vol_choices: {vol_choices}")
logger.info(f"์ฒ˜๋ฆฌ๋œ selected_cat: {selected_cat}")
local_file = None
if download_file:
try:
local_file = create_session_temp_file(session_id, '.xlsx')
shutil.copy(download_file, local_file)
logger.info(f"ํŒŒ์ผ ๋ณต์‚ฌ ์™„๋ฃŒ: {local_file}")
except Exception as file_error:
logger.error(f"ํŒŒ์ผ ๋ณต์‚ฌ ์˜ค๋ฅ˜: {file_error}")
return (
table_html,
gr.update(choices=cat_choices),
gr.update(choices=vol_choices),
None,
gr.update(choices=cat_choices, value=selected_cat), # ๋ถ„์„์šฉ ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ์ง€์™€ ๊ฐ’ ์—…๋ฐ์ดํŠธ
local_file,
gr.update(visible=True),
gr.update(visible=True),
keyword
)
except Exception as e:
logger.error(f"API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
import traceback
logger.error(f"์ƒ์„ธ ์˜ค๋ฅ˜: {traceback.format_exc()}")
return (
gr.update(value="<p>๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ‚ค์›Œ๋“œ๋กœ ์‹œ๋„ํ•ด๋ณด์„ธ์š”.</p>"),
gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"]),
gr.update(choices=["์ „์ฒด"]),
None,
gr.update(choices=["์ „์ฒด ๋ณด๊ธฐ"], value="์ „์ฒด ๋ณด๊ธฐ"),
None,
gr.update(visible=False),
gr.update(visible=False),
keyword
)
def analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id):
"""์นดํ…Œ๊ณ ๋ฆฌ ์ผ์น˜ ๋ถ„์„ ์‹คํ–‰ ๋ฐ ์ž๋™ ๋‹ค์šด๋กœ๋“œ (API ์‚ฌ์šฉ)"""
update_session_activity(session_id)
try:
client = get_client()
result = client.predict(
analysis_keywords=analysis_keywords,
selected_category=selected_category,
api_name="/process_analyze_results"
)
# API ์‘๋‹ต ํ™•์ธ ๋ฐ ์ฒ˜๋ฆฌ
logger.info(f"๋ถ„์„ API ์‘๋‹ต ํƒ€์ž…: {type(result)}, ๊ธธ์ด: {len(result) if isinstance(result, (list, tuple)) else 'N/A'}")
if isinstance(result, (list, tuple)) and len(result) >= 2:
analysis_result, download_file = result[:2]
elif isinstance(result, str):
analysis_result = result
download_file = None
else:
logger.warning(f"์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅธ ๋ถ„์„ API ์‘๋‹ต: {result}")
analysis_result = "๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
download_file = None
local_file = None
if download_file:
local_file = create_session_temp_file(session_id, '.xlsx')
shutil.copy(download_file, local_file)
return analysis_result, local_file, gr.update(visible=True)
except Exception as e:
logger.error(f"๋ถ„์„ API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
return "๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.", None, gr.update(visible=False)
def filter_and_sort_table(df, selected_cat, keyword_sort, total_volume_sort, usage_count_sort, selected_volume_range, exclude_zero_volume, session_id):
"""ํ…Œ์ด๋ธ” ํ•„ํ„ฐ๋ง ๋ฐ ์ •๋ ฌ ํ•จ์ˆ˜ (API ์‚ฌ์šฉ)"""
update_session_activity(session_id)
try:
client = get_client()
result = client.predict(
selected_cat=selected_cat,
keyword_sort=keyword_sort,
total_volume_sort=total_volume_sort,
usage_count_sort=usage_count_sort,
selected_volume_range=selected_volume_range,
exclude_zero_volume=exclude_zero_volume,
api_name="/filter_and_sort_table"
)
return result
except Exception as e:
logger.error(f"ํ•„ํ„ฐ๋ง API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
return ""
def update_category_selection(selected_cat, session_id):
"""์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ ์„ ํƒ ์‹œ ๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ๋„ ๊ฐ™์€ ๊ฐ’์œผ๋กœ ์—…๋ฐ์ดํŠธ"""
update_session_activity(session_id)
logger.info(f"์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ ๋ณ€๊ฒฝ: {selected_cat}")
# ๋กœ์ปฌ์—์„œ ์ง์ ‘ ์ฒ˜๋ฆฌ (API ํ˜ธ์ถœ ์—†์ด)
return gr.update(value=selected_cat)
def reset_interface(session_id):
"""์ธํ„ฐํŽ˜์ด์Šค ๋ฆฌ์…‹ ํ•จ์ˆ˜ - ์„ธ์…˜๋ณ„ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™”"""
update_session_activity(session_id)
if session_id in session_temp_files:
for file_path in session_temp_files[session_id]:
try:
if os.path.exists(file_path):
os.remove(file_path)
logger.info(f"์„ธ์…˜ {session_id[:8]}... ๋ฆฌ์…‹ ์‹œ ํŒŒ์ผ ์‚ญ์ œ: {file_path}")
except Exception as e:
logger.error(f"์„ธ์…˜ {session_id[:8]}... ๋ฆฌ์…‹ ์‹œ ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜: {e}")
session_temp_files[session_id] = []
try:
client = get_client()
result = client.predict(api_name="/reset_interface")
return result
except Exception as e:
logger.error(f"๋ฆฌ์…‹ API ํ˜ธ์ถœ ์˜ค๋ฅ˜: {e}")
return (
"", True, False, "๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ", "", ["์ „์ฒด ๋ณด๊ธฐ"], "์ „์ฒด ๋ณด๊ธฐ",
["์ „์ฒด"], "์ „์ฒด", "์ •๋ ฌ ์—†์Œ", "์ •๋ ฌ ์—†์Œ", None, ["์ „์ฒด ๋ณด๊ธฐ"],
"์ „์ฒด ๋ณด๊ธฐ", "", "", None, gr.update(visible=False),
gr.update(visible=False), ""
)
def search_with_loading(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
update_session_activity(session_id)
return (gr.update(visible=True), gr.update(visible=False))
def process_search_results(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id):
update_session_activity(session_id)
result = wrapper_modified(keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id)
table_html, cat_choices, vol_choices, df, selected_cat, excel, keyword_section_vis, cat_section_vis, new_keyword_state = result
# ํ…Œ์ด๋ธ”์ด ์žˆ์œผ๋ฉด ์„น์…˜๋“ค์„ ํ‘œ์‹œ
if table_html and table_html.get('value') if isinstance(table_html, dict) else table_html:
empty_placeholder_vis = False
keyword_section_visibility = True
execution_section_visibility = True
logger.info("ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์Œ - ์„น์…˜๋“ค์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค")
else:
empty_placeholder_vis = True
keyword_section_visibility = False
execution_section_visibility = False
logger.info("ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์Œ - ๊ธฐ๋ณธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค")
return (
table_html, cat_choices, vol_choices, df, selected_cat, excel,
gr.update(visible=keyword_section_visibility),
gr.update(visible=True), # ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„์„ ์„น์…˜์€ ํ•ญ์ƒ ํ‘œ์‹œ
gr.update(visible=False), # ์ง„ํ–‰ ์ƒํƒœ๋Š” ์ˆจ๊น€
gr.update(visible=empty_placeholder_vis),
gr.update(visible=execution_section_visibility),
new_keyword_state
)
def analyze_with_loading(analysis_keywords, selected_category, state_df, session_id):
update_session_activity(session_id)
return gr.update(visible=True)
def process_analyze_results(analysis_keywords, selected_category, state_df, session_id):
update_session_activity(session_id)
results = analyze_with_auto_download(analysis_keywords, selected_category, state_df, session_id)
return results + (gr.update(visible=False),)
def start_session_cleanup_scheduler():
"""์„ธ์…˜ ์ •๋ฆฌ ์Šค์ผ€์ค„๋Ÿฌ ์‹œ์ž‘"""
def cleanup_scheduler():
while True:
time.sleep(600)
cleanup_old_sessions()
cleanup_huggingface_temp_folders()
threading.Thread(target=cleanup_scheduler, daemon=True).start()
def cleanup_on_startup():
"""์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ์ „์ฒด ์ •๋ฆฌ"""
logger.info("๐Ÿงน ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ - ์ดˆ๊ธฐ ์ •๋ฆฌ ์ž‘์—… ์‹œ์ž‘...")
cleanup_huggingface_temp_folders()
app_temp_dir = setup_clean_temp_environment()
global session_temp_files, session_data
session_temp_files.clear()
session_data.clear()
logger.info(f"โœ… ์ดˆ๊ธฐ ์ •๋ฆฌ ์ž‘์—… ์™„๋ฃŒ - ์•ฑ ์ „์šฉ ๋””๋ ‰ํ† ๋ฆฌ: {app_temp_dir}")
return app_temp_dir
def create_app():
fontawesome_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">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap">
"""
try:
with open('style.css', 'r', encoding='utf-8') as f:
custom_css = f.read()
except:
custom_css = ""
with gr.Blocks(css=custom_css, theme=gr.themes.Default(
primary_hue="orange",
secondary_hue="orange",
font=[gr.themes.GoogleFont("Noto Sans KR"), "ui-sans-serif", "system-ui"]
)) as demo:
gr.HTML(fontawesome_html)
session_id = gr.State(get_session_id)
keyword_state = gr.State("")
with gr.Column(elem_classes="custom-frame fade-in"):
gr.HTML('<div class="section-title"><i class="fas fa-search"></i> ๊ฒ€์ƒ‰ ์ž…๋ ฅ</div>')
with gr.Row():
with gr.Column(scale=1):
keyword = gr.Textbox(label="๋ฉ”์ธ ํ‚ค์›Œ๋“œ", placeholder="์˜ˆ: ์˜ค์ง•์–ด")
with gr.Column(scale=1):
search_btn = gr.Button("๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„", elem_classes="custom-button")
with gr.Accordion("์˜ต์…˜ ์„ค์ •", open=False):
with gr.Row():
with gr.Column(scale=1):
korean_only = gr.Checkbox(label="ํ•œ๊ธ€๋งŒ ์ถ”์ถœ", value=True)
with gr.Column(scale=1):
exclude_zero_volume = gr.Checkbox(label="๊ฒ€์ƒ‰๋Ÿ‰ 0 ํ‚ค์›Œ๋“œ ์ œ์™ธ", value=False)
with gr.Row():
with gr.Column(scale=1):
apply_main_keyword = gr.Radio(
["๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ", "๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ฏธ์ ์šฉ"],
label="์กฐํ•ฉ ๋ฐฉ์‹",
value="๋ฉ”์ธํ‚ค์›Œ๋“œ ์ ์šฉ"
)
with gr.Column(scale=1):
gr.HTML("")
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as progress_section:
gr.HTML('<div class="section-title"><i class="fas fa-spinner"></i> ๋ถ„์„ ์ง„ํ–‰ ์ƒํƒœ</div>')
progress_html = gr.HTML("""
<div style="padding: 15px; background-color: #f9f9f9; border-radius: 5px; margin: 10px 0; border: 1px solid #ddd;">
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<i class="fas fa-spinner fa-spin" style="color: #FB7F0D; margin-right: 10px;"></i>
<span>ํ‚ค์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถ„์„์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ๋งŒ ๊ธฐ๋‹ค๋ ค์ฃผ์„ธ์š”...</span>
</div>
<div style="background-color: #e9ecef; height: 10px; border-radius: 5px; overflow: hidden;">
<div class="progress-bar"></div>
</div>
</div>
""")
with gr.Column(elem_classes="custom-frame fade-in") as main_keyword_section:
gr.HTML('<div class="section-title"><i class="fas fa-table"></i> ๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„ ๊ฒฐ๊ณผ</div>')
empty_table_html = gr.HTML("""
<table class="empty-table">
<thead>
<tr>
<th>์ˆœ๋ฒˆ</th>
<th>์กฐํ•ฉ ํ‚ค์›Œ๋“œ</th>
<th>PC๊ฒ€์ƒ‰๋Ÿ‰</th>
<th>๋ชจ๋ฐ”์ผ๊ฒ€์ƒ‰๋Ÿ‰</th>
<th>์ด๊ฒ€์ƒ‰๋Ÿ‰</th>
<th>๊ฒ€์ƒ‰๋Ÿ‰๊ตฌ๊ฐ„</th>
<th>ํ‚ค์›Œ๋“œ ์‚ฌ์šฉ์ž์ˆœ์œ„</th>
<th>ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜</th>
<th>์ƒํ’ˆ ๋“ฑ๋ก ์นดํ…Œ๊ณ ๋ฆฌ</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="9" style="padding: 30px; text-align: center;">
๊ฒ€์ƒ‰์„ ์‹คํ–‰ํ•˜๋ฉด ์—ฌ๊ธฐ์— ๊ฒฐ๊ณผ๊ฐ€ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค
</td>
</tr>
</tbody>
</table>
""")
with gr.Column(visible=False) as keyword_analysis_section:
with gr.Row():
with gr.Column(scale=1):
category_filter = gr.Dropdown(
choices=["์ „์ฒด ๋ณด๊ธฐ"],
label="์นดํ…Œ๊ณ ๋ฆฌ ํ•„ํ„ฐ",
value="์ „์ฒด ๋ณด๊ธฐ",
interactive=True,
allow_custom_value=True # ์‚ฌ์šฉ์ž ์ •์˜ ๊ฐ’ ํ—ˆ์šฉ
)
with gr.Column(scale=1):
total_volume_sort = gr.Dropdown(
choices=["์ •๋ ฌ ์—†์Œ", "์˜ค๋ฆ„์ฐจ์ˆœ", "๋‚ด๋ฆผ์ฐจ์ˆœ"],
label="์ด๊ฒ€์ƒ‰๋Ÿ‰ ์ •๋ ฌ",
value="์ •๋ ฌ ์—†์Œ",
interactive=True
)
with gr.Row():
with gr.Column(scale=1):
search_volume_filter = gr.Dropdown(
choices=["์ „์ฒด"],
label="๊ฒ€์ƒ‰๋Ÿ‰ ๊ตฌ๊ฐ„ ํ•„ํ„ฐ",
value="์ „์ฒด",
interactive=True
)
with gr.Column(scale=1):
usage_count_sort = gr.Dropdown(
choices=["์ •๋ ฌ ์—†์Œ", "์˜ค๋ฆ„์ฐจ์ˆœ", "๋‚ด๋ฆผ์ฐจ์ˆœ"],
label="ํ‚ค์›Œ๋“œ ์‚ฌ์šฉํšŸ์ˆ˜ ์ •๋ ฌ",
value="์ •๋ ฌ ์—†์Œ",
interactive=True
)
gr.HTML("<div class='data-container' id='table_container'></div>")
table_output = gr.HTML(elem_classes="fade-in")
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as category_analysis_section:
gr.HTML('<div class="section-title"><i class="fas fa-chart-bar"></i> ํ‚ค์›Œ๋“œ ๋ถ„์„</div>')
with gr.Row():
with gr.Column(scale=1):
analysis_keywords = gr.Textbox(
label="ํ‚ค์›Œ๋“œ ์ž…๋ ฅ (์ตœ๋Œ€ 20๊ฐœ, ์‰ผํ‘œ ๋˜๋Š” ์—”ํ„ฐ๋กœ ๊ตฌ๋ถ„)",
placeholder="์˜ˆ: ์˜ค์ง•์–ด๋ณถ์Œ, ์˜ค์ง•์–ด ์†์งˆ, ์˜ค์ง•์–ด ์š”๋ฆฌ...",
lines=5
)
with gr.Column(scale=1):
selected_category = gr.Dropdown(
label="๋ถ„์„ํ•  ์นดํ…Œ๊ณ ๋ฆฌ(๋ถ„์„ ์ „ ๋ฐ˜๋“œ์‹œ ์„ ํƒํ•ด์ฃผ์„ธ์š”)",
choices=["์ „์ฒด ๋ณด๊ธฐ"],
value="์ „์ฒด ๋ณด๊ธฐ",
interactive=True,
allow_custom_value=True # ์‚ฌ์šฉ์ž ์ •์˜ ๊ฐ’ ํ—ˆ์šฉ
)
with gr.Column(elem_classes="execution-section", visible=False) as execution_section:
gr.HTML('<div class="section-title"><i class="fas fa-play-circle"></i> ์‹คํ–‰</div>')
with gr.Row():
with gr.Column(scale=1):
analyze_btn = gr.Button(
"์นดํ…Œ๊ณ ๋ฆฌ ์ผ์น˜ ๋ถ„์„",
elem_classes=["execution-button", "primary-button"]
)
with gr.Column(scale=1):
reset_btn = gr.Button(
"๋ชจ๋“  ์ž…๋ ฅ ์ดˆ๊ธฐํ™”",
elem_classes=["execution-button", "secondary-button"]
)
with gr.Column(elem_classes="custom-frame fade-in", visible=False) as analysis_output_section:
gr.HTML('<div class="section-title"><i class="fas fa-list-ul"></i> ๋ถ„์„ ๊ฒฐ๊ณผ ์š”์•ฝ</div>')
analysis_result = gr.HTML(elem_classes="fade-in")
with gr.Row():
download_output = gr.File(label="ํ‚ค์›Œ๋“œ ๋ชฉ๋ก ๋‹ค์šด๋กœ๋“œ", visible=True)
state_df = gr.State()
search_btn.click(
fn=search_with_loading,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
outputs=[progress_section, empty_table_html]
).then(
fn=process_search_results,
inputs=[keyword, korean_only, apply_main_keyword, exclude_zero_volume, session_id],
outputs=[
table_output, category_filter, search_volume_filter,
state_df, selected_category, download_output, # selected_category๊ฐ€ ๋ถ„์„์šฉ ์นดํ…Œ๊ณ ๋ฆฌ
keyword_analysis_section, category_analysis_section,
progress_section, empty_table_html, execution_section,
keyword_state
]
)
category_filter.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
category_filter.change(
fn=update_category_selection,
inputs=[category_filter, session_id],
outputs=[selected_category]
)
total_volume_sort.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
usage_count_sort.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
search_volume_filter.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
exclude_zero_volume.change(
fn=filter_and_sort_table,
inputs=[
state_df, category_filter, gr.Textbox(value="์ •๋ ฌ ์—†์Œ", visible=False),
total_volume_sort, usage_count_sort,
search_volume_filter, exclude_zero_volume, session_id
],
outputs=[table_output]
)
analyze_btn.click(
fn=analyze_with_loading,
inputs=[analysis_keywords, selected_category, state_df, session_id],
outputs=[progress_section]
).then(
fn=process_analyze_results,
inputs=[analysis_keywords, selected_category, state_df, session_id],
outputs=[analysis_result, download_output, analysis_output_section, progress_section]
)
reset_btn.click(
fn=reset_interface,
inputs=[session_id],
outputs=[
keyword, korean_only, exclude_zero_volume, apply_main_keyword,
table_output, category_filter, category_filter,
search_volume_filter, search_volume_filter,
total_volume_sort, usage_count_sort,
state_df, selected_category, selected_category,
analysis_keywords, analysis_result, download_output,
keyword_analysis_section, analysis_output_section,
keyword_state
]
)
return demo
if __name__ == "__main__":
logger.info("๐Ÿš€ ๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘...")
app_temp_dir = cleanup_on_startup()
start_session_cleanup_scheduler()
logger.info("===== ๋ฉ€ํ‹ฐ์œ ์ € ๋ฉ”์ธํ‚ค์›Œ๋“œ ๋ถ„์„ Application Startup at %s =====", time.strftime("%Y-%m-%d %H:%M:%S"))
logger.info(f"๐Ÿ“ ์ž„์‹œ ํŒŒ์ผ ์ €์žฅ ์œ„์น˜: {app_temp_dir}")
try:
app = create_app()
app.launch(
share=False,
server_name="0.0.0.0",
server_port=7860,
max_threads=40,
auth=None,
show_error=True,
quiet=False,
favicon_path=None,
ssl_verify=False
)
except Exception as e:
logger.error(f"์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹คํ–‰ ์‹คํŒจ: {e}")
raise
finally:
logger.info("๐Ÿงน ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ข…๋ฃŒ - ์ตœ์ข… ์ •๋ฆฌ ์ž‘์—…...")
try:
cleanup_huggingface_temp_folders()
if os.path.exists(app_temp_dir):
shutil.rmtree(app_temp_dir, ignore_errors=True)
logger.info("โœ… ์ตœ์ข… ์ •๋ฆฌ ์™„๋ฃŒ")
except Exception as e:
logger.error(f"์ตœ์ข… ์ •๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}")