f2sdfss-2 / app.py
ssboost's picture
Update app.py
09221d2 verified
#app.py
import gradio as gr
import pandas as pd
import openpyxl
import datetime
import tempfile
import os
import uuid
import time
from openpyxl.utils.dataframe import dataframe_to_rows
import pytz
import random
import google.generativeai as genai
# ν™˜κ²½λ³€μˆ˜μ—μ„œ μ½”λ“œ 뢈러였기
api_manager_code = os.getenv('API_MANAGER_CODE', '')
styles_code = os.getenv('STYLES_CODE', '')
review_analyzer_code = os.getenv('REVIEW_ANALYZER_CODE', '')
# λ™μ μœΌλ‘œ μ½”λ“œ μ‹€ν–‰ν•˜μ—¬ 클래슀 μ •μ˜
exec(api_manager_code)
exec(review_analyzer_code)
# μŠ€νƒ€μΌ 정보 뢈러였기
fontawesome_link = """
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
"""
# ν™˜κ²½λ³€μˆ˜μ—μ„œ CSS 뢈러였기
custom_css = os.getenv('CUSTOM_CSS', '''
:root {
--primary-color: #FB7F0D;
--secondary-color: #ff9a8b;
--accent-color: #FF6B6B;
--background-color: #FFF3E9;
--card-bg: #ffffff;
--text-color: #334155;
--border-radius: 18px;
--shadow: 0 8px 30px rgba(251, 127, 13, 0.08);
}
body {
font-family: 'Pretendard', 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
margin: 0;
padding: 0;
}
.gradio-container {
width: 100%;
margin: 0 auto;
padding: 20px;
background-color: var(--background-color);
}
.custom-header {
background: #FF7F00;
padding: 2rem;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: var(--shadow);
text-align: center;
}
.custom-header h1 {
margin: 0;
font-size: 2.5rem;
font-weight: 700;
color: black;
}
.custom-header p {
margin: 10px 0 0;
font-size: 1.2rem;
color: black;
}
.custom-frame {
background-color: var(--card-bg);
border: 1px solid rgba(0, 0, 0, 0.04);
border-radius: var(--border-radius);
padding: 20px;
margin: 10px 0;
box-shadow: var(--shadow);
}
.custom-button {
border-radius: 30px !important;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)) !important;
color: white !important;
font-size: 18px !important;
padding: 10px 20px !important;
border: none;
box-shadow: 0 4px 8px rgba(251, 127, 13, 0.25);
transition: transform 0.3s ease;
}
.custom-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(251, 127, 13, 0.3);
}
.custom-title {
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
color: var(--text-color);
border-bottom: 2px solid var(--primary-color);
padding-bottom: 5px;
}
.gr-tabs-panel {
background-color: var(--background-color) !important;
box-shadow: none !important;
}
.custom-section-group {
background-color: var(--background-color) !important;
box-shadow: none !important;
}
.gr-textbox textarea,
.gr-textbox input {
border-radius: 12px !important;
border: 2px solid rgba(251, 127, 13, 0.2) !important;
font-size: 14px;
transition: border-color 0.3s ease;
}
.gr-textbox textarea:focus,
.gr-textbox input:focus {
border-color: var(--primary-color) !important;
box-shadow: 0 0 0 3px rgba(251, 127, 13, 0.1) !important;
}
.gr-file {
border-radius: var(--border-radius) !important;
border: 2px dashed var(--primary-color) !important;
background-color: rgba(251, 127, 13, 0.05) !important;
transition: all 0.3s ease;
}
.gr-file:hover {
background-color: rgba(251, 127, 13, 0.1) !important;
border-color: var(--secondary-color) !important;
}
@media (max-width: 768px) {
.gradio-container {
padding: 10px;
}
.custom-header h1 {
font-size: 2rem;
}
.custom-title {
font-size: 24px;
}
.custom-frame {
padding: 15px;
margin: 5px 0;
}
.custom-button {
font-size: 16px !important;
padding: 8px 16px !important;
}
}
''')
class SessionManager:
"""μ„Έμ…˜λ³„ 데이터 관리"""
def __init__(self):
self.sessions = {}
def create_session(self):
"""μƒˆλ‘œμš΄ μ„Έμ…˜ 생성"""
session_id = str(uuid.uuid4())
self.sessions[session_id] = {
'created_at': time.time(),
'data': {},
'temp_files': []
}
return session_id
def get_session_data(self, session_id, key, default=None):
"""μ„Έμ…˜ 데이터 쑰회"""
if session_id in self.sessions:
return self.sessions[session_id]['data'].get(key, default)
return default
def set_session_data(self, session_id, key, value):
"""μ„Έμ…˜ 데이터 μ €μž₯"""
if session_id in self.sessions:
self.sessions[session_id]['data'][key] = value
def add_temp_file(self, session_id, file_path):
"""μ„Έμ…˜μ— μž„μ‹œ 파일 μΆ”κ°€"""
if session_id in self.sessions:
self.sessions[session_id]['temp_files'].append(file_path)
def cleanup_session(self, session_id):
"""μ„Έμ…˜ 정리"""
if session_id in self.sessions:
# μž„μ‹œ νŒŒμΌλ“€ μ‚­μ œ
for file_path in self.sessions[session_id]['temp_files']:
try:
if os.path.exists(file_path):
os.remove(file_path)
except Exception as e:
print(f"파일 μ‚­μ œ 였λ₯˜: {e}")
# μ„Έμ…˜ 데이터 μ‚­μ œ
del self.sessions[session_id]
def cleanup_old_sessions(self, max_age_hours=2):
"""였래된 μ„Έμ…˜ 정리 (2μ‹œκ°„ 이상)"""
current_time = time.time()
old_sessions = []
for session_id, session_data in self.sessions.items():
if (current_time - session_data['created_at']) > (max_age_hours * 3600):
old_sessions.append(session_id)
for session_id in old_sessions:
self.cleanup_session(session_id)
# μ „μ—­ μ„Έμ…˜ λ§€λ‹ˆμ €
session_manager = SessionManager()
def create_app():
"""메인 Gradio μ• ν”Œλ¦¬μΌ€μ΄μ…˜ 생성"""
demo = 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"]
))
with demo:
gr.HTML(fontawesome_link)
# μ„Έμ…˜ IDλ₯Ό μˆ¨κ²¨μ§„ μƒνƒœλ‘œ 관리
session_id_state = gr.State(value="")
# μ„Έμ…˜ μ΄ˆκΈ°ν™” ν•¨μˆ˜
def initialize_session():
session_id = session_manager.create_session()
# 각 μ„Έμ…˜λ§ˆλ‹€ μƒˆλ‘œμš΄ 뢄석기 μΈμŠ€ν„΄μŠ€ 생성
api_manager = APIManager()
analyzer = ReviewAnalyzer(api_manager)
session_manager.set_session_data(session_id, 'analyzer', analyzer)
return session_id
# μ„Έμ…˜λ³„ 뢄석기 κ°€μ Έμ˜€κΈ°
def get_session_analyzer(session_id):
if not session_id:
session_id = initialize_session()
analyzer = session_manager.get_session_data(session_id, 'analyzer')
if analyzer is None:
api_manager = APIManager()
analyzer = ReviewAnalyzer(api_manager)
session_manager.set_session_data(session_id, 'analyzer', analyzer)
return analyzer, session_id
# νƒ­ ꡬ성
with gr.Tabs() as tabs:
#############################
# μ—‘μ…€ 뢄석 λͺ¨λ“œ
#############################
with gr.TabItem("πŸ’Ύ μŠ€λ§ˆνŠΈμŠ€ν† μ–΄ 엑셀리뷰데이터 ν™œμš©"):
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ 데이터 μž…λ ₯</div>")
file_input = gr.File(label="원본 μ—‘μ…€ 파일 μ—…λ‘œλ“œ", file_types=[".xlsx"])
year_radio = gr.Radio(
choices=[f"{str(y)[-2:]}λ…„" for y in range(2025, 2020, -1)],
label="뢄석년도 선택",
value="25λ…„"
)
analyze_button = gr.Button("μ˜΅μ…˜ λΆ„μ„ν•˜κΈ°", elem_classes="custom-button")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ λΆ„μ„λ³΄κ³ μ„œ λ‹€μš΄λ‘œλ“œ</div>")
download_final_output = gr.File(label="λ³΄κ³ μ„œ λ‹€μš΄λ‘œλ“œ")
# 리뷰뢄석 μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame", visible=False) as review_analysis_frame:
gr.HTML("<div class='custom-title'>πŸ“ 리뷰뢄석</div>")
top20_dropdown = gr.Dropdown(
label="μ•„μ΄ν…œμ˜΅μ…˜ 뢄석",
choices=["μ „μ²΄μ˜΅μ…˜λΆ„μ„"],
value="μ „μ²΄μ˜΅μ…˜λΆ„μ„"
)
review_button = gr.Button("리뷰 λΆ„μ„ν•˜κΈ°", elem_classes="custom-button")
# 뢄석 κ²°κ³Ό μ„Ήμ…˜λ“€
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>✨ μ£Όμš”κΈμ •λ¦¬λ·°</div>")
positive_output = gr.Textbox(label="κΈμ •λ¦¬λ·°λ¦¬μŠ€νŠΈ (20개)", lines=10, value="")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>✨ μ£Όμš”λΆ€μ •λ¦¬λ·°</div>")
negative_output = gr.Textbox(label="λΆ€μ •λ¦¬λ·°λ¦¬μŠ€νŠΈ (30개)", lines=10, value="")
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“’ 긍정리뷰 뢄석</div>")
positive_analysis_output = gr.Textbox(label="긍정리뷰 뢄석", lines=8, value="")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“’ 뢀정리뷰 뢄석</div>")
negative_analysis_output = gr.Textbox(label="뢀정리뷰 뢄석", lines=8, value="")
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“Š λ‹ˆμ¦ˆμ›μΈ 뢄석</div>")
insight_analysis_output = gr.Textbox(label="λ‹ˆμ¦ˆμ›μΈ 뢄석", lines=8, value="")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ”§ μƒν’ˆνŒλ§€λ°©ν–₯μ„±</div>")
strategy_analysis_output = gr.Textbox(label="μƒν’ˆνŒλ§€λ°©ν–₯μ„±", lines=8, value="")
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ μ†Œμ‹±μ „λž΅</div>")
sourcing_analysis_output = gr.Textbox(label="μ†Œμ‹±μ „λž΅", lines=8, value="")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ–ΌοΈ λ§ˆμΌ€νŒ…μ „λž΅</div>")
detail_page_analysis_output = gr.Textbox(label="λ§ˆμΌ€νŒ…μ „λž΅", lines=8, value="")
# μ„Έμ…˜λ³„ μƒνƒœ 관리λ₯Ό μœ„ν•œ μˆ¨κ²¨μ§„ μƒνƒœ
partial_file_state = gr.State(value=None)
# μ˜΅μ…˜ 뢄석 이벀트 ν•Έλ“€λŸ¬
def on_analyze_options(uploaded_file, selected_year, session_id):
analyzer, session_id = get_session_analyzer(session_id)
try:
result = analyzer.analyze_options(uploaded_file, selected_year)
print(f"analyze_options κ²°κ³Ό νƒ€μž…: {type(result)}")
print(f"analyze_options κ²°κ³Ό λ‚΄μš©: {result}")
partial_file = None
top20_list = ["μ „μ²΄μ˜΅μ…˜λΆ„μ„"]
if result is not None:
if isinstance(result, (list, tuple)) and len(result) >= 2:
partial_file = result[0]
top20_list = result[1] if result[1] else ["μ „μ²΄μ˜΅μ…˜λΆ„μ„"]
elif hasattr(result, '__getitem__'):
try:
partial_file = result[0]
top20_list = result[1] if len(result) > 1 else ["μ „μ²΄μ˜΅μ…˜λΆ„μ„"]
except (IndexError, KeyError, TypeError):
print("κ²°κ³Ό 인덱싱 μ‹€νŒ¨, κΈ°λ³Έκ°’ μ‚¬μš©")
if not isinstance(top20_list, list):
top20_list = ["μ „μ²΄μ˜΅μ…˜λΆ„μ„"]
if len(top20_list) == 0:
top20_list = ["μ „μ²΄μ˜΅μ…˜λΆ„μ„"]
if partial_file:
session_manager.add_temp_file(session_id, partial_file)
return (
partial_file,
gr.update(visible=True if partial_file else False),
gr.update(choices=top20_list, value=top20_list[0]),
session_id
)
except Exception as e:
print(f"μ˜΅μ…˜ 뢄석 였λ₯˜: {e}")
import traceback
traceback.print_exc()
return None, gr.update(visible=False), gr.update(choices=["μ „μ²΄μ˜΅μ…˜λΆ„μ„"], value="μ „μ²΄μ˜΅μ…˜λΆ„μ„"), session_id
analyze_button.click(
fn=on_analyze_options,
inputs=[file_input, year_radio, session_id_state],
outputs=[partial_file_state, review_analysis_frame, top20_dropdown, session_id_state]
)
# 리뷰 뢄석 이벀트 ν•Έλ“€λŸ¬
def on_analyze_reviews(partial_file, selected_option, session_id):
analyzer, session_id = get_session_analyzer(session_id)
try:
result = analyzer.analyze_reviews(partial_file, selected_option)
if isinstance(result, tuple) and len(result) >= 9:
results = result
else:
results = (None, "", "", "", "", "", "", "", "")
final_file = results[0]
if final_file:
session_manager.add_temp_file(session_id, final_file)
return results + (session_id,)
except Exception as e:
print(f"리뷰 뢄석 였λ₯˜: {e}")
import traceback
traceback.print_exc()
return (None, "", "", "", "", "", "", "", "", session_id)
review_button.click(
fn=on_analyze_reviews,
inputs=[partial_file_state, top20_dropdown, session_id_state],
outputs=[download_final_output, positive_output, negative_output,
positive_analysis_output, negative_analysis_output,
insight_analysis_output, strategy_analysis_output,
sourcing_analysis_output, detail_page_analysis_output, session_id_state]
)
#############################
# 직접 μž…λ ₯ 뢄석 λͺ¨λ“œ
#############################
with gr.TabItem("πŸ“– 직접 μž…λ ₯ν•œ μžλ£Œν™œμš©"):
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ 리뷰 직접 μž…λ ₯</div>")
direct_positive_input = gr.Textbox(
label="긍정 리뷰 μž…λ ₯",
placeholder="긍정 리뷰λ₯Ό 여기에 μž…λ ₯ν•˜μ„Έμš”.(μ΅œλŒ€ 8000자)",
lines=10, max_length=8000, value=""
)
direct_negative_input = gr.Textbox(
label="λΆ€μ • 리뷰 μž…λ ₯",
placeholder="λΆ€μ • 리뷰λ₯Ό 여기에 μž…λ ₯ν•˜μ„Έμš”.(μ΅œλŒ€ 8000자)",
lines=10, max_length=8000, value=""
)
direct_review_button = gr.Button("리뷰 λΆ„μ„ν•˜κΈ°", elem_classes="custom-button")
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ λΆ„μ„λ³΄κ³ μ„œ λ‹€μš΄λ‘œλ“œ</div>")
direct_download_output = gr.File(label="뢄석 λ³΄κ³ μ„œ λ‹€μš΄λ‘œλ“œ")
# 직접 μž…λ ₯ 뢄석 κ²°κ³Ό
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“’ 긍정리뷰뢄석</div>")
direct_positive_analysis_output = gr.Textbox(
label="긍정리뷰뢄석", lines=8, value=""
)
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“’ 뢀정리뷰뢄석</div>")
direct_negative_analysis_output = gr.Textbox(
label="뢀정리뷰뢄석", lines=8, value=""
)
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“Š λ‹ˆμ¦ˆμ›μΈ 뢄석</div>")
direct_insight_analysis_output = gr.Textbox(
label="λ‹ˆμ¦ˆμ›μΈ 뢄석", lines=8, value=""
)
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ”§ μƒν’ˆνŒλ§€λ°©ν–₯μ„±</div>")
direct_strategy_analysis_output = gr.Textbox(
label="μƒν’ˆνŒλ§€λ°©ν–₯μ„±", lines=8, value=""
)
with gr.Row():
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“ μ†Œμ‹±μ „λž΅</div>")
direct_sourcing_analysis_output = gr.Textbox(
label="μ†Œμ‹±μ „λž΅", lines=8, value=""
)
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ–ΌοΈ λ§ˆμΌ€νŒ…μ „λž΅</div>")
direct_detail_page_analysis_output = gr.Textbox(
label="λ§ˆμΌ€νŒ…μ „λž΅", lines=8, value=""
)
# 직접 μž…λ ₯ 뢄석 이벀트 ν•Έλ“€λŸ¬
def on_direct_analyze(positive_input, negative_input, session_id):
analyzer, session_id = get_session_analyzer(session_id)
try:
result = analyzer.analyze_direct_reviews(positive_input, negative_input)
if isinstance(result, tuple) and len(result) >= 9:
results = result
else:
results = (None, "", "", "", "", "", "", "", "")
final_file = results[0]
if final_file:
session_manager.add_temp_file(session_id, final_file)
return results + (session_id,)
except Exception as e:
print(f"직접 뢄석 였λ₯˜: {e}")
import traceback
traceback.print_exc()
return (None, "", "", "", "", "", "", "", "", session_id)
direct_review_button.click(
fn=on_direct_analyze,
inputs=[direct_positive_input, direct_negative_input, session_id_state],
outputs=[direct_download_output, direct_positive_analysis_output, direct_negative_analysis_output,
direct_insight_analysis_output, direct_strategy_analysis_output,
direct_sourcing_analysis_output, direct_detail_page_analysis_output, session_id_state]
)
# μ˜ˆμ‹œ 적용 μ„Ήμ…˜
with gr.Column(elem_classes="custom-frame"):
gr.HTML("<div class='custom-title'>πŸ“š μ˜ˆμ‹œ μ μš©ν•˜κΈ°</div>")
with gr.Row():
example_excel_button = gr.Button("πŸ“Š μ—‘μ…€ 뢄석 μ˜ˆμ‹œ μ μš©ν•˜κΈ°", elem_classes="custom-button")
example_direct_button = gr.Button("πŸ“ 직접 μž…λ ₯ μ˜ˆμ‹œ μ μš©ν•˜κΈ°", elem_classes="custom-button")
clear_all_button = gr.Button("πŸ—‘οΈ 전체 μ΄ˆκΈ°ν™”", elem_classes="custom-button")
# μ˜ˆμ‹œ 적용 이벀트
def apply_excel_example(session_id):
analyzer, session_id = get_session_analyzer(session_id)
excel_file, year = analyzer.apply_excel_example()
return excel_file, year, session_id
example_excel_button.click(
fn=apply_excel_example,
inputs=[session_id_state],
outputs=[file_input, year_radio, session_id_state]
)
def apply_direct_example(session_id):
analyzer, session_id = get_session_analyzer(session_id)
positive_text, negative_text = analyzer.apply_direct_example()
return positive_text, negative_text, session_id
example_direct_button.click(
fn=apply_direct_example,
inputs=[session_id_state],
outputs=[direct_positive_input, direct_negative_input, session_id_state]
)
# 전체 μ΄ˆκΈ°ν™” κΈ°λŠ₯
def clear_all_data(session_id):
if session_id:
session_manager.cleanup_session(session_id)
new_session_id = initialize_session()
return (
None, "25λ…„", gr.update(visible=False),
["μ „μ²΄μ˜΅μ…˜λΆ„μ„"], "μ „μ²΄μ˜΅μ…˜λΆ„μ„", None,
"", "", "", "", "", "", "", "",
"", "", None, "", "", "", "", "", "",
new_session_id
)
clear_all_button.click(
fn=clear_all_data,
inputs=[session_id_state],
outputs=[
file_input, year_radio, review_analysis_frame,
top20_dropdown, top20_dropdown,
download_final_output,
positive_output, negative_output,
positive_analysis_output, negative_analysis_output,
insight_analysis_output, strategy_analysis_output,
sourcing_analysis_output, detail_page_analysis_output,
direct_positive_input, direct_negative_input,
direct_download_output,
direct_positive_analysis_output, direct_negative_analysis_output,
direct_insight_analysis_output, direct_strategy_analysis_output,
direct_sourcing_analysis_output, direct_detail_page_analysis_output,
session_id_state
]
)
# νŽ˜μ΄μ§€ λ‘œλ“œ μ‹œ μ™„μ „ν•œ μ΄ˆκΈ°ν™”
def init_on_load():
session_id = initialize_session()
return (
session_id, None, "25λ…„", gr.update(visible=False),
gr.update(choices=["μ „μ²΄μ˜΅μ…˜λΆ„μ„"], value="μ „μ²΄μ˜΅μ…˜λΆ„μ„"), None,
"", "", "", "", "", "", "", "",
"", "", None, "", "", "", "", "", ""
)
demo.load(
fn=init_on_load,
outputs=[
session_id_state,
file_input, year_radio, review_analysis_frame, top20_dropdown,
download_final_output,
positive_output, negative_output,
positive_analysis_output, negative_analysis_output,
insight_analysis_output, strategy_analysis_output,
sourcing_analysis_output, detail_page_analysis_output,
direct_positive_input, direct_negative_input,
direct_download_output,
direct_positive_analysis_output, direct_negative_analysis_output,
direct_insight_analysis_output, direct_strategy_analysis_output,
direct_sourcing_analysis_output, direct_detail_page_analysis_output
]
)
return demo
if __name__ == "__main__":
app = create_app()
# 주기적으둜 였래된 μ„Έμ…˜ 정리
import threading
def cleanup_timer():
while True:
time.sleep(3600)
session_manager.cleanup_old_sessions()
cleanup_thread = threading.Thread(target=cleanup_timer, daemon=True)
cleanup_thread.start()
app.launch(
share=False,
server_name="0.0.0.0",
server_port=7860,
show_error=True,
debug=False
)