|
|
""" |
|
|
AI κΈ°λ° μκΆ λΆμ μμ€ν
- Comic Classic Theme λ²μ |
|
|
Dataset: https://huggingface.co/datasets/ginipick/market |
|
|
""" |
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
from typing import Dict, List, Tuple |
|
|
import json |
|
|
from datasets import load_dataset |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from plotly.subplots import make_subplots |
|
|
import folium |
|
|
from folium.plugins import HeatMap, MarkerCluster |
|
|
import requests |
|
|
from collections import Counter |
|
|
import re |
|
|
import os |
|
|
import time |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BraveSearchClient: |
|
|
"""Brave Search API ν΄λΌμ΄μΈνΈ""" |
|
|
|
|
|
def __init__(self, api_key: str = None): |
|
|
self.api_key = api_key or os.getenv("BRAVE_API_KEY") |
|
|
self.base_url = "https://api.search.brave.com/res/v1/web/search" |
|
|
|
|
|
def search(self, query: str, count: int = 5) -> str: |
|
|
"""μΉ κ²μ μν""" |
|
|
if not self.api_key: |
|
|
return "β οΈ Brave Search API ν€κ° μ€μ λμ§ μμμ΅λλ€." |
|
|
|
|
|
headers = { |
|
|
"Accept": "application/json", |
|
|
"X-Subscription-Token": self.api_key |
|
|
} |
|
|
|
|
|
params = { |
|
|
"q": query, |
|
|
"count": count, |
|
|
"text_decorations": False, |
|
|
"search_lang": "ko" |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.get(self.base_url, headers=headers, params=params, timeout=10) |
|
|
if response.status_code == 200: |
|
|
data = response.json() |
|
|
results = [] |
|
|
|
|
|
if 'web' in data and 'results' in data['web']: |
|
|
for item in data['web']['results'][:count]: |
|
|
title = item.get('title', '') |
|
|
description = item.get('description', '') |
|
|
url = item.get('url', '') |
|
|
results.append(f"π **{title}**\n{description}\nπ {url}") |
|
|
|
|
|
return "\n\n".join(results) if results else "κ²μ κ²°κ³Όκ° μμ΅λλ€." |
|
|
else: |
|
|
return f"β οΈ κ²μ μ€ν¨: {response.status_code}" |
|
|
except Exception as e: |
|
|
return f"β οΈ κ²μ μ€λ₯: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MarketDataLoader: |
|
|
"""νκΉ
νμ΄μ€ μκΆ λ°μ΄ν° λ‘λ""" |
|
|
|
|
|
REGIONS = { |
|
|
'μμΈ': 'μμΈ_202506', 'κ²½κΈ°': 'κ²½κΈ°_202506', 'λΆμ°': 'λΆμ°_202506', |
|
|
'λꡬ': 'λꡬ_202506', 'μΈμ²': 'μΈμ²_202506', 'κ΄μ£Ό': 'κ΄μ£Ό_202506', |
|
|
'λμ ': 'λμ _202506', 'μΈμ°': 'μΈμ°_202506', 'μΈμ’
': 'μΈμ’
_202506', |
|
|
'κ²½λ¨': 'κ²½λ¨_202506', 'κ²½λΆ': 'κ²½λΆ_202506', 'μ λ¨': 'μ λ¨_202506', |
|
|
'μ λΆ': 'μ λΆ_202506', 'μΆ©λ¨': 'μΆ©λ¨_202506', 'μΆ©λΆ': 'μΆ©λΆ_202506', |
|
|
'κ°μ': 'κ°μ_202506', 'μ μ£Ό': 'μ μ£Ό_202506' |
|
|
} |
|
|
|
|
|
|
|
|
CATEGORY_MAPPING = { |
|
|
'G2': 'μλ§€μ
', |
|
|
'I1': 'μλ°μ
', |
|
|
'I2': 'μμμ μ
', |
|
|
'L1': 'λΆλμ°μ
', |
|
|
'M1': 'μ λ¬Έ/κ³Όν/κΈ°μ ', |
|
|
'N1': 'μ¬μ
μ§μ/μλ', |
|
|
'P1': 'κ΅μ‘μλΉμ€', |
|
|
'Q1': '보건μλ£', |
|
|
'R1': 'μμ /μ€ν¬μΈ /μ¬κ°', |
|
|
'S2': 'μ리/κ°μΈμλΉμ€' |
|
|
} |
|
|
|
|
|
@staticmethod |
|
|
def load_region_data(region: str, sample_size: int = 30000) -> pd.DataFrame: |
|
|
"""μ§μλ³ λ°μ΄ν° λ‘λ""" |
|
|
try: |
|
|
file_name = f"μμ곡μΈμμ₯μ§ν₯곡λ¨_μκ°(μκΆ)μ 보_{MarketDataLoader.REGIONS[region]}.csv" |
|
|
dataset = load_dataset("ginipick/market", data_files=file_name, split="train") |
|
|
df = dataset.to_pandas() |
|
|
|
|
|
if len(df) > sample_size: |
|
|
df = df.sample(n=sample_size, random_state=42) |
|
|
|
|
|
return df |
|
|
except Exception as e: |
|
|
print(f"λ°μ΄ν° λ‘λ μ€ν¨: {str(e)}") |
|
|
return pd.DataFrame() |
|
|
|
|
|
@staticmethod |
|
|
def load_multiple_regions(regions: List[str], sample_per_region: int = 30000) -> pd.DataFrame: |
|
|
"""μ¬λ¬ μ§μ λ°μ΄ν° λ‘λ""" |
|
|
dfs = [] |
|
|
for region in regions: |
|
|
df = MarketDataLoader.load_region_data(region, sample_per_region) |
|
|
if not df.empty: |
|
|
dfs.append(df) |
|
|
|
|
|
if dfs: |
|
|
return pd.concat(dfs, ignore_index=True) |
|
|
return pd.DataFrame() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MarketAnalyzer: |
|
|
"""μκΆ λ°μ΄ν° λΆμ μμ§""" |
|
|
|
|
|
def __init__(self, df: pd.DataFrame): |
|
|
self.df = df |
|
|
self.prepare_data() |
|
|
|
|
|
def prepare_data(self): |
|
|
"""λ°μ΄ν° μ μ²λ¦¬""" |
|
|
if 'κ²½λ' in self.df.columns: |
|
|
self.df['κ²½λ'] = pd.to_numeric(self.df['κ²½λ'], errors='coerce') |
|
|
if 'μλ' in self.df.columns: |
|
|
self.df['μλ'] = pd.to_numeric(self.df['μλ'], errors='coerce') |
|
|
self.df = self.df.dropna(subset=['κ²½λ', 'μλ']) |
|
|
|
|
|
|
|
|
if 'μΈ΅μ 보' in self.df.columns: |
|
|
self.df['μΈ΅μ 보_μ«μ'] = self.df['μΈ΅μ 보'].apply(self._parse_floor) |
|
|
|
|
|
def _parse_floor(self, floor_str): |
|
|
"""μΈ΅ μ 보λ₯Ό μ«μλ‘ λ³ν""" |
|
|
if pd.isna(floor_str): |
|
|
return None |
|
|
floor_str = str(floor_str) |
|
|
if 'μ§ν' in floor_str or 'B' in floor_str: |
|
|
match = re.search(r'\d+', floor_str) |
|
|
return -int(match.group()) if match else -1 |
|
|
elif '1μΈ΅' in floor_str or floor_str == '1': |
|
|
return 1 |
|
|
else: |
|
|
match = re.search(r'\d+', floor_str) |
|
|
return int(match.group()) if match else None |
|
|
|
|
|
def get_comprehensive_insights(self) -> List[Dict]: |
|
|
"""ν¬κ΄μ μΈ μΈμ¬μ΄νΈ μμ±""" |
|
|
insights = [] |
|
|
|
|
|
insights.append(self._create_top_categories_chart()) |
|
|
insights.append(self._create_major_category_pie()) |
|
|
insights.append(self._create_floor_analysis()) |
|
|
insights.append(self._create_diversity_index()) |
|
|
insights.append(self._create_franchise_analysis()) |
|
|
insights.append(self._create_floor_preference()) |
|
|
insights.append(self._create_district_density()) |
|
|
insights.append(self._create_category_correlation()) |
|
|
insights.append(self._create_subcategory_trends()) |
|
|
insights.append(self._create_regional_specialization()) |
|
|
|
|
|
return insights |
|
|
|
|
|
def _create_top_categories_chart(self) -> Dict: |
|
|
"""μ
μ’
λ³ μ ν¬ μ μ°¨νΈ""" |
|
|
if 'μκΆμ
μ’
μ€λΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
top_categories = self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].value_counts().head(15) |
|
|
fig = px.bar( |
|
|
x=top_categories.values, |
|
|
y=top_categories.index, |
|
|
orientation='h', |
|
|
labels={'x': 'μ ν¬ μ', 'y': 'μ
μ’
'}, |
|
|
title='π μμ μ
μ’
TOP 15', |
|
|
color=top_categories.values, |
|
|
color_continuous_scale='blues' |
|
|
) |
|
|
fig.update_layout(showlegend=False, height=500) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μ
μ’
λ³ μ ν¬ μ λΆμ'} |
|
|
|
|
|
def _create_major_category_pie(self) -> Dict: |
|
|
"""λλΆλ₯λ³ λΆν¬""" |
|
|
if 'μκΆμ
μ’
λλΆλ₯μ½λ' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
major_counts = self.df['μκΆμ
μ’
λλΆλ₯μ½λ'].value_counts() |
|
|
labels = [MarketDataLoader.CATEGORY_MAPPING.get(code, code) for code in major_counts.index] |
|
|
|
|
|
fig = px.pie( |
|
|
values=major_counts.values, |
|
|
names=labels, |
|
|
title='π μ
μ’
λλΆλ₯ λΆν¬', |
|
|
hole=0.4, |
|
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
|
) |
|
|
fig.update_traces(textposition='inside', textinfo='percent+label') |
|
|
return {'type': 'plot', 'data': fig, 'title': 'λλΆλ₯λ³ μκΆ κ΅¬μ±'} |
|
|
|
|
|
def _create_floor_analysis(self) -> Dict: |
|
|
"""μΈ΅λ³ λΆν¬ μμΈ λΆμ""" |
|
|
if 'μΈ΅μ 보_μ«μ' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
floor_data = self.df['μΈ΅μ 보_μ«μ'].dropna() |
|
|
floor_counts = floor_data.value_counts().sort_index() |
|
|
|
|
|
underground = floor_counts[floor_counts.index < 0].sum() |
|
|
first_floor = floor_counts.get(1, 0) |
|
|
upper_floors = floor_counts[floor_counts.index > 1].sum() |
|
|
|
|
|
fig = go.Figure(data=[ |
|
|
go.Bar( |
|
|
x=['μ§ν', '1μΈ΅', '2μΈ΅ μ΄μ'], |
|
|
y=[underground, first_floor, upper_floors], |
|
|
text=[f'{underground:,}<br>({underground/len(floor_data)*100:.1f}%)', |
|
|
f'{first_floor:,}<br>({first_floor/len(floor_data)*100:.1f}%)', |
|
|
f'{upper_floors:,}<br>({upper_floors/len(floor_data)*100:.1f}%)'], |
|
|
textposition='auto', |
|
|
marker_color=['#e74c3c', '#3498db', '#95a5a6'] |
|
|
) |
|
|
]) |
|
|
fig.update_layout( |
|
|
title='π’ μΈ΅λ³ μ ν¬ λΆν¬ (μ§ν vs 1μΈ΅ vs μμΈ΅)', |
|
|
xaxis_title='μΈ΅ ꡬλΆ', |
|
|
yaxis_title='μ ν¬ μ', |
|
|
height=400 |
|
|
) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μΈ΅λ³ μ
μ§ λΆμ'} |
|
|
|
|
|
def _create_diversity_index(self) -> Dict: |
|
|
"""μ§μλ³ μ
μ’
λ€μμ± μ§μ""" |
|
|
if 'μꡰꡬλͺ
' not in self.df.columns or 'μκΆμ
μ’
μ€λΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
diversity_data = [] |
|
|
for district in self.df['μꡰꡬλͺ
'].unique()[:20]: |
|
|
district_df = self.df[self.df['μꡰꡬλͺ
'] == district] |
|
|
num_categories = district_df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].nunique() |
|
|
total_stores = len(district_df) |
|
|
diversity_score = (num_categories / total_stores) * 100 |
|
|
diversity_data.append({ |
|
|
'μ§μ': district, |
|
|
'λ€μμ±μ§μ': diversity_score, |
|
|
'μ
μ’
μ': num_categories, |
|
|
'μ ν¬μ': total_stores |
|
|
}) |
|
|
|
|
|
diversity_df = pd.DataFrame(diversity_data).sort_values('λ€μμ±μ§μ', ascending=False) |
|
|
|
|
|
fig = px.bar( |
|
|
diversity_df, |
|
|
x='λ€μμ±μ§μ', |
|
|
y='μ§μ', |
|
|
orientation='h', |
|
|
title='π¨ μ§μλ³ μ
μ’
λ€μμ± μ§μ (μ
μ’
μ / μ ν¬ μ Γ 100)', |
|
|
labels={'λ€μμ±μ§μ': 'λ€μμ± μ§μ', 'μ§μ': 'μꡰꡬ'}, |
|
|
color='λ€μμ±μ§μ', |
|
|
color_continuous_scale='viridis' |
|
|
) |
|
|
fig.update_layout(height=500) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μκΆ λ€μμ± λΆμ'} |
|
|
|
|
|
def _create_franchise_analysis(self) -> Dict: |
|
|
"""νλμ°¨μ΄μ¦ vs κ°μΈμ¬μ
μ λΆμ""" |
|
|
if 'λΈλλλͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
franchise_count = self.df['λΈλλλͺ
'].notna().sum() |
|
|
individual_count = self.df['λΈλλλͺ
'].isna().sum() |
|
|
|
|
|
fig = go.Figure(data=[ |
|
|
go.Pie( |
|
|
labels=['κ°μΈμ¬μ
μ', 'νλμ°¨μ΄μ¦'], |
|
|
values=[individual_count, franchise_count], |
|
|
hole=0.4, |
|
|
marker_colors=['#3498db', '#e74c3c'], |
|
|
textinfo='label+percent+value', |
|
|
texttemplate='%{label}<br>%{value:,}κ°<br>(%{percent})' |
|
|
) |
|
|
]) |
|
|
|
|
|
fig.update_layout( |
|
|
title='πͺ κ°μΈμ¬μ
μ vs νλμ°¨μ΄μ¦ λΉμ¨', |
|
|
height=400 |
|
|
) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μ¬μ
μ μ ν λΆμ'} |
|
|
|
|
|
def _create_floor_preference(self) -> Dict: |
|
|
"""μ
μ’
λ³ μΈ΅ μ νΈλ""" |
|
|
if 'μΈ΅μ 보_μ«μ' not in self.df.columns or 'μκΆμ
μ’
μ€λΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
top_categories = self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].value_counts().head(10).index |
|
|
floor_pref_data = [] |
|
|
|
|
|
for category in top_categories: |
|
|
cat_df = self.df[self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'] == category] |
|
|
floor_dist = cat_df['μΈ΅μ 보_μ«μ'].dropna() |
|
|
|
|
|
if len(floor_dist) > 0: |
|
|
underground = (floor_dist < 0).sum() |
|
|
first_floor = (floor_dist == 1).sum() |
|
|
upper_floors = (floor_dist > 1).sum() |
|
|
|
|
|
floor_pref_data.append({ |
|
|
'μ
μ’
': category, |
|
|
'μ§ν': underground, |
|
|
'1μΈ΅': first_floor, |
|
|
'2μΈ΅ μ΄μ': upper_floors |
|
|
}) |
|
|
|
|
|
pref_df = pd.DataFrame(floor_pref_data) |
|
|
|
|
|
fig = go.Figure() |
|
|
fig.add_trace(go.Bar(name='μ§ν', x=pref_df['μ
μ’
'], y=pref_df['μ§ν'], marker_color='#e74c3c')) |
|
|
fig.add_trace(go.Bar(name='1μΈ΅', x=pref_df['μ
μ’
'], y=pref_df['1μΈ΅'], marker_color='#3498db')) |
|
|
fig.add_trace(go.Bar(name='2μΈ΅ μ΄μ', x=pref_df['μ
μ’
'], y=pref_df['2μΈ΅ μ΄μ'], marker_color='#95a5a6')) |
|
|
|
|
|
fig.update_layout( |
|
|
title='π’ μ
μ’
λ³ μΈ΅ μ νΈλ (μμ 10κ° μ
μ’
)', |
|
|
xaxis_title='μ
μ’
', |
|
|
yaxis_title='μ ν¬ μ', |
|
|
barmode='stack', |
|
|
height=500, |
|
|
xaxis_tickangle=-45 |
|
|
) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μΈ΅λ³ μ νΈλ λΆμ'} |
|
|
|
|
|
def _create_district_density(self) -> Dict: |
|
|
"""μκ΅°κ΅¬λ³ μκΆ λ°μ§λ""" |
|
|
if 'μꡰꡬλͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
district_counts = self.df['μꡰꡬλͺ
'].value_counts().head(20) |
|
|
|
|
|
fig = px.bar( |
|
|
x=district_counts.values, |
|
|
y=district_counts.index, |
|
|
orientation='h', |
|
|
title='π μκ΅°κ΅¬λ³ μ ν¬ λ°μ§λ TOP 20', |
|
|
labels={'x': 'μ ν¬ μ', 'y': 'μꡰꡬ'}, |
|
|
color=district_counts.values, |
|
|
color_continuous_scale='reds' |
|
|
) |
|
|
fig.update_layout(showlegend=False, height=600) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μ§μ λ°μ§λ λΆμ'} |
|
|
|
|
|
def _create_category_correlation(self) -> Dict: |
|
|
"""μ
μ’
μκ΄κ΄κ³""" |
|
|
if 'μꡰꡬλͺ
' not in self.df.columns or 'μκΆμ
μ’
μ€λΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
top_categories = self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].value_counts().head(10).index.tolist() |
|
|
districts = self.df['μꡰꡬλͺ
'].unique() |
|
|
correlation_matrix = np.zeros((len(top_categories), len(top_categories))) |
|
|
|
|
|
for i, cat1 in enumerate(top_categories): |
|
|
for j, cat2 in enumerate(top_categories): |
|
|
if i != j: |
|
|
coexist_count = 0 |
|
|
for district in districts: |
|
|
district_df = self.df[self.df['μꡰꡬλͺ
'] == district] |
|
|
has_cat1 = cat1 in district_df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].values |
|
|
has_cat2 = cat2 in district_df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].values |
|
|
if has_cat1 and has_cat2: |
|
|
coexist_count += 1 |
|
|
correlation_matrix[i][j] = coexist_count |
|
|
|
|
|
fig = go.Figure(data=go.Heatmap( |
|
|
z=correlation_matrix, |
|
|
x=top_categories, |
|
|
y=top_categories, |
|
|
colorscale='Blues', |
|
|
text=np.round(correlation_matrix, 1), |
|
|
texttemplate='%{text}', |
|
|
textfont={"size": 10} |
|
|
)) |
|
|
|
|
|
fig.update_layout( |
|
|
title='π μ
μ’
μκ΄κ΄κ³ λ§€νΈλ¦μ€ (κ°μ μ§μ λμ μΆνμ¨)', |
|
|
xaxis_title='μ
μ’
', |
|
|
yaxis_title='μ
μ’
', |
|
|
height=600, |
|
|
xaxis_tickangle=-45 |
|
|
) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μ
μ’
곡쑴 λΆμ'} |
|
|
|
|
|
def _create_subcategory_trends(self) -> Dict: |
|
|
"""μλΆλ₯ νΈλ λ""" |
|
|
if 'μκΆμ
μ’
μλΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
subcat_counts = self.df['μκΆμ
μ’
μλΆλ₯λͺ
'].value_counts().head(20) |
|
|
|
|
|
fig = px.treemap( |
|
|
names=subcat_counts.index, |
|
|
parents=[''] * len(subcat_counts), |
|
|
values=subcat_counts.values, |
|
|
title='π μλΆλ₯ μ
μ’
νΈλ λ TOP 20', |
|
|
color=subcat_counts.values, |
|
|
color_continuous_scale='greens' |
|
|
) |
|
|
fig.update_layout(height=600) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μΈλΆ μ
μ’
λΆμ'} |
|
|
|
|
|
def _create_regional_specialization(self) -> Dict: |
|
|
"""μ§μλ³ νΉν μ
μ’
""" |
|
|
if 'μλλͺ
' not in self.df.columns or 'μκΆμ
μ’
μ€λΆλ₯λͺ
' not in self.df.columns: |
|
|
return None |
|
|
|
|
|
specialization_data = [] |
|
|
for region in self.df['μλλͺ
'].unique(): |
|
|
region_df = self.df[self.df['μλλͺ
'] == region] |
|
|
top_categories = region_df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].value_counts().head(3) |
|
|
for category, count in top_categories.items(): |
|
|
specialization_data.append({ |
|
|
'μ§μ': region, |
|
|
'νΉνμ
μ’
': category, |
|
|
'μ ν¬μ': count |
|
|
}) |
|
|
|
|
|
spec_df = pd.DataFrame(specialization_data) |
|
|
|
|
|
fig = px.sunburst( |
|
|
spec_df, |
|
|
path=['μ§μ', 'νΉνμ
μ’
'], |
|
|
values='μ ν¬μ', |
|
|
title='π― μ§μλ³ νΉν μ
μ’
(κ° μ§μ TOP 3)', |
|
|
color='μ ν¬μ', |
|
|
color_continuous_scale='oranges' |
|
|
) |
|
|
fig.update_layout(height=700) |
|
|
return {'type': 'plot', 'data': fig, 'title': 'μ§μ νΉν λΆμ'} |
|
|
|
|
|
def create_density_map(self, sample_size: int = 1000) -> str: |
|
|
"""μ ν¬ λ°μ§λ μ§λ μμ±""" |
|
|
df_sample = self.df.sample(n=min(sample_size, len(self.df)), random_state=42) |
|
|
|
|
|
center_lat = df_sample['μλ'].mean() |
|
|
center_lon = df_sample['κ²½λ'].mean() |
|
|
|
|
|
m = folium.Map(location=[center_lat, center_lon], zoom_start=11, tiles='OpenStreetMap') |
|
|
|
|
|
heat_data = [[row['μλ'], row['κ²½λ']] for _, row in df_sample.iterrows()] |
|
|
HeatMap(heat_data, radius=15, blur=25, max_zoom=13).add_to(m) |
|
|
|
|
|
return m._repr_html_() |
|
|
|
|
|
def analyze_for_llm(self) -> Dict: |
|
|
"""LLM 컨ν
μ€νΈμ© λΆμ λ°μ΄ν°""" |
|
|
context = { |
|
|
'μ΄_μ ν¬_μ': len(self.df), |
|
|
'μ§μ_μ': self.df['μλλͺ
'].nunique() if 'μλλͺ
' in self.df.columns else 0, |
|
|
'μ
μ’
_μ': self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].nunique() if 'μκΆμ
μ’
μ€λΆλ₯λͺ
' in self.df.columns else 0, |
|
|
} |
|
|
|
|
|
if 'μκΆμ
μ’
μ€λΆλ₯λͺ
' in self.df.columns: |
|
|
context['μμ_μ
μ’
_5'] = self.df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].value_counts().head(5).to_dict() |
|
|
|
|
|
if 'μΈ΅μ 보_μ«μ' in self.df.columns: |
|
|
first_floor_ratio = (self.df['μΈ΅μ 보_μ«μ'] == 1).sum() / len(self.df) * 100 |
|
|
context['1μΈ΅_λΉμ¨'] = f"{first_floor_ratio:.1f}%" |
|
|
|
|
|
return context |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LLMQueryProcessor: |
|
|
"""Fireworks AI κΈ°λ° μμ°μ΄ μ²λ¦¬ (μ€νΈλ¦¬λ° μ§μ + μΉκ²μ)""" |
|
|
|
|
|
def __init__(self, api_key: str = None): |
|
|
self.api_key = api_key or os.getenv("FIREWORKS_API_KEY") |
|
|
self.base_url = "https://api.fireworks.ai/inference/v1/chat/completions" |
|
|
|
|
|
if not self.api_key: |
|
|
raise ValueError("β FIREWORKS_API_KEY νκ²½λ³μλ₯Ό μ€μ νκ±°λ API ν€λ₯Ό μ
λ ₯ν΄μ£ΌμΈμ!") |
|
|
|
|
|
def process_query_stream(self, query: str, data_context: Dict, chat_history: List = None, web_search_results: str = None): |
|
|
"""μμ°μ΄ 쿼리 μ²λ¦¬ (μ€νΈλ¦¬λ° λͺ¨λ) - μΉκ²μ κ²°κ³Ό ν¬ν¨""" |
|
|
|
|
|
web_context = "" |
|
|
if web_search_results and "β οΈ" not in web_search_results: |
|
|
web_context = f""" |
|
|
|
|
|
π **μ΅μ μΉ κ²μ μ 보** |
|
|
{web_search_results} |
|
|
|
|
|
μ μΉ κ²μ κ²°κ³Όλ₯Ό μ°Έκ³ νμ¬ μ΅μ μ 보μ νΈλ λλ₯Ό λ°μν΄μ£ΌμΈμ. |
|
|
""" |
|
|
|
|
|
system_prompt = f"""λΉμ μ νκ΅ μκΆ λ°μ΄ν° λΆμ μ λ¬Έκ°μ
λλ€. |
|
|
|
|
|
π **νμ¬ λΆμ λ°μ΄ν°** |
|
|
{json.dumps(data_context, ensure_ascii=False, indent=2)} |
|
|
{web_context} |
|
|
|
|
|
ꡬ체μ μΈ μ«μμ λΉμ¨λ‘ μ λμ λΆμμ μ 곡νμΈμ. |
|
|
μ°½μ
, ν¬μ, κ²½μ λΆμ κ΄μ μμ μ€μ©μ μΈμ¬μ΄νΈλ₯Ό μ 곡νμΈμ. |
|
|
μΉ κ²μ κ²°κ³Όκ° μ 곡λ κ²½μ° μ΅μ νΈλ λμ ν¨κ» λΆμνμΈμ. |
|
|
λ°λμ νκ΅μ΄λ‘ λ΅λ³νμΈμ.""" |
|
|
|
|
|
messages = [{"role": "system", "content": system_prompt}] |
|
|
if chat_history: |
|
|
messages.extend(chat_history[-6:]) |
|
|
messages.append({"role": "user", "content": query}) |
|
|
|
|
|
payload = { |
|
|
"model": "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507", |
|
|
"max_tokens": 4800, |
|
|
"temperature": 0.7, |
|
|
"messages": messages, |
|
|
"stream": True |
|
|
} |
|
|
|
|
|
headers = { |
|
|
"Authorization": f"Bearer {self.api_key}", |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
|
|
|
try: |
|
|
response = requests.post( |
|
|
self.base_url, |
|
|
headers=headers, |
|
|
json=payload, |
|
|
timeout=60, |
|
|
stream=True |
|
|
) |
|
|
|
|
|
if response.status_code == 200: |
|
|
for line in response.iter_lines(): |
|
|
if line: |
|
|
line_text = line.decode('utf-8') |
|
|
if line_text.startswith('data: '): |
|
|
data_str = line_text[6:] |
|
|
if data_str.strip() == '[DONE]': |
|
|
break |
|
|
try: |
|
|
data = json.loads(data_str) |
|
|
if 'choices' in data and len(data['choices']) > 0: |
|
|
delta = data['choices'][0].get('delta', {}) |
|
|
content = delta.get('content', '') |
|
|
if content: |
|
|
yield content |
|
|
except json.JSONDecodeError: |
|
|
continue |
|
|
else: |
|
|
yield f"β οΈ API μ€λ₯: {response.status_code}" |
|
|
|
|
|
except requests.exceptions.Timeout: |
|
|
yield "β οΈ API μλ΅ μκ° μ΄κ³Ό. μ μ ν λ€μ μλν΄μ£ΌμΈμ." |
|
|
except requests.exceptions.ConnectionError: |
|
|
yield "β οΈ λ€νΈμν¬ μ°κ²° μ€λ₯. μΈν°λ· μ°κ²°μ νμΈν΄μ£ΌμΈμ." |
|
|
except Exception as e: |
|
|
yield f"β μ€λ₯: {str(e)}" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AppState: |
|
|
def __init__(self): |
|
|
self.analyzer = None |
|
|
self.llm_processor = None |
|
|
self.brave_client = None |
|
|
self.chat_history = [] |
|
|
|
|
|
app_state = AppState() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_data(regions): |
|
|
"""λ°μ΄ν° λ‘λ""" |
|
|
if not regions: |
|
|
return "β μ΅μ 1κ° μ§μμ μ νν΄μ£ΌμΈμ!", None, None, None |
|
|
|
|
|
try: |
|
|
df = MarketDataLoader.load_multiple_regions(regions, sample_per_region=30000) |
|
|
if df.empty: |
|
|
return "β λ°μ΄ν° λ‘λ μ€ν¨!", None, None, None |
|
|
|
|
|
app_state.analyzer = MarketAnalyzer(df) |
|
|
|
|
|
stats = f""" |
|
|
β
**λ°μ΄ν° λ‘λ μλ£!** |
|
|
{'=' * 40} |
|
|
π **λΆμ ν΅κ³** |
|
|
β’ μ΄ μ ν¬: {len(df):,}κ° |
|
|
β’ λΆμ μ§μ: {', '.join(regions)} |
|
|
β’ μ
μ’
μ: {df['μκΆμ
μ’
μ€λΆλ₯λͺ
'].nunique()}κ° |
|
|
β’ λλΆλ₯: {df['μκΆμ
μ’
λλΆλ₯λͺ
'].nunique()}κ° |
|
|
{'=' * 40} |
|
|
π‘ μ΄μ μΈμ¬μ΄νΈλ₯Ό νμΈνκ±°λ AIμκ² μ§λ¬ΈνμΈμ! |
|
|
""" |
|
|
|
|
|
return stats, gr.update(visible=True), gr.update(visible=True), gr.update(visible=True) |
|
|
except Exception as e: |
|
|
return f"β μ€λ₯: {str(e)}", None, None, None |
|
|
|
|
|
|
|
|
def generate_insights(): |
|
|
"""μΈμ¬μ΄νΈ μμ±""" |
|
|
if app_state.analyzer is None: |
|
|
return [None] * 11 |
|
|
|
|
|
insights = app_state.analyzer.get_comprehensive_insights() |
|
|
map_html = app_state.analyzer.create_density_map(sample_size=2000) |
|
|
|
|
|
result = [map_html] |
|
|
for insight in insights: |
|
|
if insight and insight['type'] == 'plot': |
|
|
result.append(insight['data']) |
|
|
else: |
|
|
result.append(None) |
|
|
|
|
|
while len(result) < 11: |
|
|
result.append(None) |
|
|
|
|
|
return result[:11] |
|
|
|
|
|
|
|
|
def chat_respond(message, history): |
|
|
"""μ±λ΄ μλ΅ (μ€νΈλ¦¬λ° λͺ¨λ + μΉκ²μ)""" |
|
|
if app_state.analyzer is None: |
|
|
yield history + [[message, "β λ¨Όμ λ°μ΄ν°λ₯Ό λ‘λν΄μ£ΌμΈμ!"]] |
|
|
return |
|
|
|
|
|
data_context = app_state.analyzer.analyze_for_llm() |
|
|
|
|
|
try: |
|
|
if app_state.llm_processor is None: |
|
|
app_state.llm_processor = LLMQueryProcessor() |
|
|
|
|
|
if app_state.brave_client is None: |
|
|
try: |
|
|
app_state.brave_client = BraveSearchClient() |
|
|
except: |
|
|
app_state.brave_client = None |
|
|
|
|
|
web_results = None |
|
|
if app_state.brave_client and app_state.brave_client.api_key: |
|
|
search_query = f"νκ΅ μκΆ μ°½μ
νΈλ λ {message}" |
|
|
web_results = app_state.brave_client.search(search_query, count=3) |
|
|
|
|
|
chat_hist = [] |
|
|
for user_msg, bot_msg in history: |
|
|
chat_hist.append({"role": "user", "content": user_msg}) |
|
|
chat_hist.append({"role": "assistant", "content": bot_msg}) |
|
|
|
|
|
history = history + [[message, ""]] |
|
|
|
|
|
if web_results and "β οΈ" not in web_results: |
|
|
history[-1][1] = "π μΉ κ²μ μ€...\n\n" |
|
|
yield history |
|
|
|
|
|
full_response = "" |
|
|
for chunk in app_state.llm_processor.process_query_stream(message, data_context, chat_hist, web_results): |
|
|
full_response += chunk |
|
|
history[-1][1] = full_response |
|
|
yield history |
|
|
|
|
|
except ValueError as e: |
|
|
response = f"""π **κΈ°λ³Έ λ°μ΄ν° λΆμ κ²°κ³Ό** |
|
|
|
|
|
**μ 체 νν©** |
|
|
- μ΄ μ ν¬ μ: {data_context['μ΄_μ ν¬_μ']:,}κ° |
|
|
- μ
μ’
μ’
λ₯: {data_context['μ
μ’
_μ']}κ° |
|
|
- 1μΈ΅ λΉμ¨: {data_context.get('1μΈ΅_λΉμ¨', 'N/A')} |
|
|
|
|
|
β οΈ **AI λΆμ μ¬μ© λ°©λ²** |
|
|
νκ²½λ³μλ₯Ό μ€μ νμΈμ: |
|
|
```bash |
|
|
export FIREWORKS_API_KEY="your_api_key_here" |
|
|
export BRAVE_API_KEY="your_brave_api_key_here" |
|
|
```""" |
|
|
|
|
|
history = history + [[message, response]] |
|
|
yield history |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
css = """ |
|
|
/* ===== π¨ Google Fonts Import ===== */ |
|
|
@import url('https://fonts.googleapis.com/css2?family=Bangers&family=Comic+Neue:wght@400;700&family=Noto+Sans+KR:wght@400;700&display=swap'); |
|
|
|
|
|
/* ===== π¨ Comic Classic λ°°κ²½ - λΉν°μ§ νμ΄νΌ + λνΈ ν¨ν΄ ===== */ |
|
|
.gradio-container { |
|
|
background-color: #FEF9C3 !important; |
|
|
background-image: |
|
|
radial-gradient(#1F2937 1px, transparent 1px) !important; |
|
|
background-size: 20px 20px !important; |
|
|
min-height: 100vh !important; |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive, sans-serif !important; |
|
|
} |
|
|
|
|
|
/* ===== νκΉ
νμ΄μ€ μλ¨ μμ μ¨κΉ ===== */ |
|
|
.huggingface-space-header, |
|
|
#space-header, |
|
|
.space-header, |
|
|
[class*="space-header"], |
|
|
.svelte-1ed2p3z, |
|
|
.space-header-badge, |
|
|
.header-badge, |
|
|
[data-testid="space-header"], |
|
|
.svelte-kqij2n, |
|
|
.svelte-1ax1toq, |
|
|
.embed-container > div:first-child { |
|
|
display: none !important; |
|
|
visibility: hidden !important; |
|
|
height: 0 !important; |
|
|
width: 0 !important; |
|
|
overflow: hidden !important; |
|
|
opacity: 0 !important; |
|
|
pointer-events: none !important; |
|
|
} |
|
|
|
|
|
/* ===== Footer μμ μ¨κΉ ===== */ |
|
|
footer, |
|
|
.footer, |
|
|
.gradio-container footer, |
|
|
.built-with, |
|
|
[class*="footer"], |
|
|
.gradio-footer, |
|
|
.main-footer, |
|
|
div[class*="footer"], |
|
|
.show-api, |
|
|
.built-with-gradio, |
|
|
a[href*="gradio.app"], |
|
|
a[href*="huggingface.co/spaces"] { |
|
|
display: none !important; |
|
|
visibility: hidden !important; |
|
|
height: 0 !important; |
|
|
padding: 0 !important; |
|
|
margin: 0 !important; |
|
|
} |
|
|
|
|
|
/* ===== λ©μΈ 컨ν
μ΄λ ===== */ |
|
|
#col-container { |
|
|
max-width: 1400px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
/* ===== π¨ ν€λ νμ΄ν - μ½λ―Ή μ€νμΌ ===== */ |
|
|
.header-text h1 { |
|
|
font-family: 'Bangers', cursive !important; |
|
|
color: #1F2937 !important; |
|
|
font-size: 3.2rem !important; |
|
|
font-weight: 400 !important; |
|
|
text-align: center !important; |
|
|
margin-bottom: 0.5rem !important; |
|
|
text-shadow: |
|
|
4px 4px 0px #FACC15, |
|
|
6px 6px 0px #1F2937 !important; |
|
|
letter-spacing: 3px !important; |
|
|
-webkit-text-stroke: 2px #1F2937 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μλΈνμ΄ν ===== */ |
|
|
.subtitle { |
|
|
text-align: center !important; |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
font-size: 1.1rem !important; |
|
|
color: #1F2937 !important; |
|
|
margin-bottom: 1.5rem !important; |
|
|
font-weight: 700 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μΉ΄λ/ν¨λ - λ§ν νλ μ μ€νμΌ ===== */ |
|
|
.gr-panel, |
|
|
.gr-box, |
|
|
.gr-form, |
|
|
.block, |
|
|
.gr-group { |
|
|
background: #FFFFFF !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 6px 6px 0px #1F2937 !important; |
|
|
transition: all 0.2s ease !important; |
|
|
} |
|
|
|
|
|
.gr-panel:hover, |
|
|
.block:hover { |
|
|
transform: translate(-2px, -2px) !important; |
|
|
box-shadow: 8px 8px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μ
λ ₯ νλ (Textbox) ===== */ |
|
|
textarea, |
|
|
input[type="text"], |
|
|
input[type="number"] { |
|
|
background: #FFFFFF !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
color: #1F2937 !important; |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
font-size: 1rem !important; |
|
|
font-weight: 700 !important; |
|
|
transition: all 0.2s ease !important; |
|
|
} |
|
|
|
|
|
textarea:focus, |
|
|
input[type="text"]:focus, |
|
|
input[type="number"]:focus { |
|
|
border-color: #3B82F6 !important; |
|
|
box-shadow: 4px 4px 0px #3B82F6 !important; |
|
|
outline: none !important; |
|
|
} |
|
|
|
|
|
textarea::placeholder { |
|
|
color: #9CA3AF !important; |
|
|
font-weight: 400 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Primary λ²νΌ - μ½λ―Ή λΈλ£¨ ===== */ |
|
|
.gr-button-primary, |
|
|
button.primary, |
|
|
.gr-button.primary { |
|
|
background: #3B82F6 !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
color: #FFFFFF !important; |
|
|
font-family: 'Noto Sans KR', 'Bangers', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 1.2rem !important; |
|
|
letter-spacing: 1px !important; |
|
|
padding: 14px 28px !important; |
|
|
box-shadow: 5px 5px 0px #1F2937 !important; |
|
|
transition: all 0.1s ease !important; |
|
|
text-shadow: 1px 1px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-button-primary:hover, |
|
|
button.primary:hover, |
|
|
.gr-button.primary:hover { |
|
|
background: #2563EB !important; |
|
|
transform: translate(-2px, -2px) !important; |
|
|
box-shadow: 7px 7px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-button-primary:active, |
|
|
button.primary:active, |
|
|
.gr-button.primary:active { |
|
|
transform: translate(3px, 3px) !important; |
|
|
box-shadow: 2px 2px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Secondary λ²νΌ - μ½λ―Ή λ λ ===== */ |
|
|
.gr-button-secondary, |
|
|
button.secondary { |
|
|
background: #EF4444 !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
color: #FFFFFF !important; |
|
|
font-family: 'Noto Sans KR', 'Bangers', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 1rem !important; |
|
|
letter-spacing: 1px !important; |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
transition: all 0.1s ease !important; |
|
|
text-shadow: 1px 1px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-button-secondary:hover, |
|
|
button.secondary:hover { |
|
|
background: #DC2626 !important; |
|
|
transform: translate(-2px, -2px) !important; |
|
|
box-shadow: 6px 6px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Small λ²νΌ ===== */ |
|
|
button.sm, |
|
|
.gr-button-sm { |
|
|
background: #10B981 !important; |
|
|
border: 2px solid #1F2937 !important; |
|
|
border-radius: 6px !important; |
|
|
color: #FFFFFF !important; |
|
|
font-family: 'Noto Sans KR', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 0.9rem !important; |
|
|
padding: 8px 16px !important; |
|
|
box-shadow: 3px 3px 0px #1F2937 !important; |
|
|
transition: all 0.1s ease !important; |
|
|
} |
|
|
|
|
|
button.sm:hover, |
|
|
.gr-button-sm:hover { |
|
|
background: #059669 !important; |
|
|
transform: translate(-1px, -1px) !important; |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ λ‘κ·Έ μΆλ ₯ μμ ===== */ |
|
|
.info-log textarea { |
|
|
background: #1F2937 !important; |
|
|
color: #10B981 !important; |
|
|
font-family: 'Courier New', monospace !important; |
|
|
font-size: 0.9rem !important; |
|
|
font-weight: 400 !important; |
|
|
border: 3px solid #10B981 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 4px 4px 0px #10B981 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μμ½λμΈ - λ§νμ μ€νμΌ ===== */ |
|
|
.gr-accordion { |
|
|
background: #FACC15 !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-accordion-header { |
|
|
color: #1F2937 !important; |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 1.1rem !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ 체ν¬λ°μ€ κ·Έλ£Ή ===== */ |
|
|
.gr-checkbox-group { |
|
|
background: #FFFFFF !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
padding: 10px !important; |
|
|
} |
|
|
|
|
|
input[type="checkbox"] { |
|
|
accent-color: #3B82F6 !important; |
|
|
width: 18px !important; |
|
|
height: 18px !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ ν μ€νμΌ ===== */ |
|
|
.gr-tab-nav { |
|
|
background: #FACC15 !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px 8px 0 0 !important; |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-tab-nav button { |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
color: #1F2937 !important; |
|
|
border: none !important; |
|
|
padding: 12px 20px !important; |
|
|
} |
|
|
|
|
|
.gr-tab-nav button.selected { |
|
|
background: #3B82F6 !important; |
|
|
color: #FFFFFF !important; |
|
|
border-radius: 6px 6px 0 0 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μ±λ΄ μ€νμΌ ===== */ |
|
|
.gr-chatbot { |
|
|
background: #FFFFFF !important; |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 6px 6px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-chatbot .message { |
|
|
font-family: 'Noto Sans KR', sans-serif !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ λΌλ²¨ μ€νμΌ ===== */ |
|
|
label, |
|
|
.gr-input-label, |
|
|
.gr-block-label { |
|
|
color: #1F2937 !important; |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
font-weight: 700 !important; |
|
|
font-size: 1rem !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Markdown μ€νμΌ ===== */ |
|
|
.gr-markdown { |
|
|
font-family: 'Noto Sans KR', 'Comic Neue', cursive !important; |
|
|
color: #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-markdown h1, |
|
|
.gr-markdown h2, |
|
|
.gr-markdown h3 { |
|
|
font-family: 'Bangers', 'Noto Sans KR', cursive !important; |
|
|
color: #1F2937 !important; |
|
|
text-shadow: 2px 2px 0px #FACC15 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Plot μμ ===== */ |
|
|
.gr-plot { |
|
|
border: 3px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
background: #FFFFFF !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ HTML μμ (μ§λ) ===== */ |
|
|
.gr-html { |
|
|
border: 4px solid #1F2937 !important; |
|
|
border-radius: 8px !important; |
|
|
box-shadow: 6px 6px 0px #FACC15 !important; |
|
|
overflow: hidden !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μ€ν¬λ‘€λ° - μ½λ―Ή μ€νμΌ ===== */ |
|
|
::-webkit-scrollbar { |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-track { |
|
|
background: #FEF9C3; |
|
|
border: 2px solid #1F2937; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb { |
|
|
background: #3B82F6; |
|
|
border: 2px solid #1F2937; |
|
|
border-radius: 0px; |
|
|
} |
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
|
background: #EF4444; |
|
|
} |
|
|
|
|
|
/* ===== π¨ μ ν νμ΄λΌμ΄νΈ ===== */ |
|
|
::selection { |
|
|
background: #FACC15; |
|
|
color: #1F2937; |
|
|
} |
|
|
|
|
|
/* ===== π¨ λ§ν¬ μ€νμΌ ===== */ |
|
|
a { |
|
|
color: #3B82F6 !important; |
|
|
text-decoration: none !important; |
|
|
font-weight: 700 !important; |
|
|
} |
|
|
|
|
|
a:hover { |
|
|
color: #EF4444 !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Row/Column κ°κ²© ===== */ |
|
|
.gr-row { |
|
|
gap: 1.5rem !important; |
|
|
} |
|
|
|
|
|
.gr-column { |
|
|
gap: 1rem !important; |
|
|
} |
|
|
|
|
|
/* ===== π¨ Badge μ€νμΌ ===== */ |
|
|
.badge-container { |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
gap: 15px; |
|
|
flex-wrap: wrap; |
|
|
margin: 20px 0; |
|
|
} |
|
|
|
|
|
.comic-badge { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 8px; |
|
|
padding: 12px 24px; |
|
|
border: 3px solid #1F2937; |
|
|
border-radius: 8px; |
|
|
text-decoration: none; |
|
|
font-weight: 700; |
|
|
font-size: 1em; |
|
|
transition: all 0.2s ease; |
|
|
box-shadow: 4px 4px 0px #1F2937; |
|
|
font-family: 'Noto Sans KR', sans-serif; |
|
|
} |
|
|
|
|
|
.comic-badge:hover { |
|
|
transform: translate(-2px, -2px); |
|
|
box-shadow: 6px 6px 0px #1F2937; |
|
|
} |
|
|
|
|
|
.comic-badge-yellow { |
|
|
background: #FACC15; |
|
|
color: #1F2937; |
|
|
} |
|
|
|
|
|
.comic-badge-blue { |
|
|
background: #3B82F6; |
|
|
color: #FFFFFF; |
|
|
} |
|
|
|
|
|
.comic-badge-green { |
|
|
background: #10B981; |
|
|
color: #FFFFFF; |
|
|
} |
|
|
|
|
|
/* ===== λ°μν μ‘°μ ===== */ |
|
|
@media (max-width: 768px) { |
|
|
.header-text h1 { |
|
|
font-size: 2rem !important; |
|
|
text-shadow: |
|
|
3px 3px 0px #FACC15, |
|
|
4px 4px 0px #1F2937 !important; |
|
|
} |
|
|
|
|
|
.gr-button-primary, |
|
|
button.primary { |
|
|
padding: 12px 20px !important; |
|
|
font-size: 1rem !important; |
|
|
} |
|
|
|
|
|
.gr-panel, |
|
|
.block { |
|
|
box-shadow: 4px 4px 0px #1F2937 !important; |
|
|
} |
|
|
} |
|
|
|
|
|
/* ===== π¨ λ€ν¬λͺ¨λ λΉνμ±ν ===== */ |
|
|
@media (prefers-color-scheme: dark) { |
|
|
.gradio-container { |
|
|
background-color: #FEF9C3 !important; |
|
|
} |
|
|
} |
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks(title="AI μκΆ λΆμ μμ€ν
", css=css) as demo: |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div style="text-align: center; margin: 20px 0 10px 0;"> |
|
|
<a href="https://www.humangen.ai" target="_blank" style="text-decoration: none;"> |
|
|
<img src="https://img.shields.io/static/v1?label=π HOME&message=HUMANGEN.AI&color=0000ff&labelColor=ffcc00&style=for-the-badge" alt="HOME"> |
|
|
</a> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
gr.Markdown( |
|
|
""" |
|
|
# πͺ AI μκΆ λΆμ μμ€ν
PRO π |
|
|
""", |
|
|
elem_classes="header-text" |
|
|
) |
|
|
|
|
|
gr.Markdown( |
|
|
""" |
|
|
<p class="subtitle">β‘ μ κ΅ μκ°(μκΆ) λ°μ΄ν° μ€μκ° λΆμ | μ€νΈλ¦¬λ° + μΉκ²μ π | 10κ°μ§ μ¬μΈ΅ μΈμ¬μ΄νΈ π</p> |
|
|
""", |
|
|
) |
|
|
|
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="badge-container"> |
|
|
<a href="https://open.kakao.com/o/peIe8KWh" target="_blank" class="comic-badge comic-badge-yellow"> |
|
|
<span>π¬</span> |
|
|
<span>μ€νμ±ν
λ°λ‘κ°κΈ°</span> |
|
|
</a> |
|
|
<a href="https://ginigen.ai" target="_blank" class="comic-badge comic-badge-blue"> |
|
|
<span>π</span> |
|
|
<span>λλ
Έ λ°λλ μ λμ¨ λ¬΄λ£ μλΉμ€</span> |
|
|
</a> |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
api_status = "β
μ€μ λ¨" if os.getenv("FIREWORKS_API_KEY") else "β οΈ λ―Έμ€μ " |
|
|
brave_status = "β
νμ±ν" if os.getenv("BRAVE_API_KEY") else "β οΈ λΉνμ±ν" |
|
|
|
|
|
with gr.Row(equal_height=False): |
|
|
|
|
|
with gr.Column(scale=1, min_width=300): |
|
|
gr.Markdown("### βοΈ λΆμ μ€μ ") |
|
|
|
|
|
gr.Markdown(f""" |
|
|
**π API μν** |
|
|
- Fireworks AI: {api_status} |
|
|
- Brave Search: {brave_status} |
|
|
""") |
|
|
|
|
|
region_select = gr.CheckboxGroup( |
|
|
choices=list(MarketDataLoader.REGIONS.keys()), |
|
|
value=['μμΈ'], |
|
|
label="π λΆμ μ§μ μ ν (μ΅λ 5κ° κΆμ₯)" |
|
|
) |
|
|
|
|
|
load_btn = gr.Button( |
|
|
"π λ°μ΄ν° λ‘λνκΈ°!", |
|
|
variant="primary", |
|
|
size="lg" |
|
|
) |
|
|
|
|
|
with gr.Accordion("π λ‘λ μν", open=True): |
|
|
status_box = gr.Markdown( |
|
|
"π μ§μμ μ ννκ³ λ°μ΄ν°λ₯Ό λ‘λνμΈμ!", |
|
|
elem_classes="info-log" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Column(scale=3, min_width=600): |
|
|
with gr.Tabs() as tabs: |
|
|
|
|
|
with gr.Tab("π μΈμ¬μ΄νΈ λμ보λ", id=0) as tab1: |
|
|
insights_content = gr.Column(visible=False) |
|
|
|
|
|
with insights_content: |
|
|
gr.Markdown("### πΊοΈ μ ν¬ λ°μ§λ ννΈλ§΅") |
|
|
map_output = gr.HTML() |
|
|
|
|
|
gr.Markdown("---") |
|
|
gr.Markdown("### π 10κ°μ§ μ¬μΈ΅ μκΆ μΈμ¬μ΄νΈ") |
|
|
|
|
|
with gr.Row(): |
|
|
chart1 = gr.Plot(label="π μ
μ’
λ³ μ ν¬ μ") |
|
|
chart2 = gr.Plot(label="π λλΆλ₯ λΆν¬") |
|
|
|
|
|
with gr.Row(): |
|
|
chart3 = gr.Plot(label="π’ μΈ΅λ³ λΆν¬") |
|
|
chart4 = gr.Plot(label="π¨ μ
μ’
λ€μμ±") |
|
|
|
|
|
with gr.Row(): |
|
|
chart5 = gr.Plot(label="πͺ νλμ°¨μ΄μ¦ λΆμ") |
|
|
chart6 = gr.Plot(label="π μΈ΅ μ νΈλ") |
|
|
|
|
|
with gr.Row(): |
|
|
chart7 = gr.Plot(label="π₯ μ§μ λ°μ§λ") |
|
|
chart8 = gr.Plot(label="π μ
μ’
μκ΄κ΄κ³") |
|
|
|
|
|
with gr.Row(): |
|
|
chart9 = gr.Plot(label="π μλΆλ₯ νΈλ λ") |
|
|
chart10 = gr.Plot(label="π― μ§μ νΉν") |
|
|
|
|
|
|
|
|
with gr.Tab("π€ AI λΆμ μ±λ΄ β‘π", id=1) as tab2: |
|
|
chat_content = gr.Column(visible=False) |
|
|
|
|
|
with chat_content: |
|
|
gr.Markdown(""" |
|
|
### π‘ μμ μ§λ¬Έ |
|
|
κ°λ¨μμ μΉ΄ν μ°½μ
? | μΉν¨μ§ ν¬ν μ§μ? | 1μΈ΅μ΄ μ 리ν μ
μ’
? | νλμ°¨μ΄μ¦ μ μ μ¨? |
|
|
|
|
|
β‘ **μ€νΈλ¦¬λ°**: AI μλ΅μ΄ μ€μκ°μΌλ‘ νμλ©λλ€! |
|
|
π **μΉκ²μ**: μ΅μ μκΆ νΈλ λλ₯Ό μλ λ°μν©λλ€! |
|
|
""") |
|
|
|
|
|
chatbot = gr.Chatbot( |
|
|
height=450, |
|
|
label="AI μκΆ λΆμ μ΄μμ€ν΄νΈ" |
|
|
) |
|
|
|
|
|
with gr.Row(): |
|
|
msg_input = gr.Textbox( |
|
|
placeholder="무μμ΄λ λ¬Όμ΄λ³΄μΈμ! (μ: κ°λ¨μμ μΉ΄ν μ°½μ
νλ €λ©΄?)", |
|
|
show_label=False, |
|
|
scale=4 |
|
|
) |
|
|
submit_btn = gr.Button("π μ μ‘", variant="primary", scale=1) |
|
|
|
|
|
with gr.Row(): |
|
|
sample_btn1 = gr.Button("β κ°λ¨ μΉ΄ν μ°½μ
?", size="sm") |
|
|
sample_btn2 = gr.Button("π μΉν¨μ§ ν¬ν μ§μ?", size="sm") |
|
|
sample_btn3 = gr.Button("π’ 1μΈ΅ μ 리ν μ
μ’
?", size="sm") |
|
|
sample_btn4 = gr.Button("πͺ νλμ°¨μ΄μ¦ μ μ μ¨?", size="sm") |
|
|
|
|
|
|
|
|
gr.Markdown(""" |
|
|
--- |
|
|
### π μ¬μ© κ°μ΄λ |
|
|
1οΈβ£ μ§μ μ ν β 2οΈβ£ λ°μ΄ν° λ‘λ β 3οΈβ£ 10κ°μ§ μΈμ¬μ΄νΈ νμΈ λλ AIμκ² μ§λ¬Έ! |
|
|
|
|
|
### π μ 곡λλ 10κ°μ§ λΆμ |
|
|
| λΆμ νλͺ© | μ€λͺ
| |
|
|
|----------|------| |
|
|
| π μ
μ’
λ³ μ ν¬ μ | κ°μ₯ λ§μ μ
μ’
TOP 15 | |
|
|
| π λλΆλ₯ λΆν¬ | μλ§€/μμ/μλΉμ€ λ± λΉμ¨ | |
|
|
| π’ μΈ΅λ³ λΆν¬ | μ§ν/1μΈ΅/μμΈ΅ μ
μ§ λΆμ | |
|
|
| π¨ μ
μ’
λ€μμ± | μ§μλ³ μ
μ’
λ€μμ± μ§μ | |
|
|
| πͺ νλμ°¨μ΄μ¦ λΆμ | κ°μΈ vs νλμ°¨μ΄μ¦ λΉμ¨ | |
|
|
| π μΈ΅ μ νΈλ | μ
μ’
λ³ μ νΈ μΈ΅μ | |
|
|
| π₯ μ§μ λ°μ§λ | μ ν¬ μ μμ μ§μ | |
|
|
| π μ
μ’
μκ΄κ΄κ³ | κ°μ΄ λνλλ μ
μ’
ν¨ν΄ | |
|
|
| π μλΆλ₯ νΈλ λ | μΈλΆ μ
μ’
λΆν¬ | |
|
|
| π― μ§μ νΉν | κ° μ§μμ νΉν μ
μ’
| |
|
|
|
|
|
π‘ **Tip**: API ν€ μμ΄λ 10κ°μ§ μκ°ν λΆμκ³Ό κΈ°λ³Έ ν΅κ³λ₯Ό νμΈν μ μμ΅λλ€! |
|
|
""") |
|
|
|
|
|
|
|
|
load_btn.click( |
|
|
fn=load_data, |
|
|
inputs=[region_select], |
|
|
outputs=[status_box, insights_content, chat_content, tab1] |
|
|
).then( |
|
|
fn=generate_insights, |
|
|
outputs=[map_output, chart1, chart2, chart3, chart4, chart5, chart6, chart7, chart8, chart9, chart10] |
|
|
) |
|
|
|
|
|
|
|
|
submit_btn.click( |
|
|
fn=chat_respond, |
|
|
inputs=[msg_input, chatbot], |
|
|
outputs=[chatbot] |
|
|
).then( |
|
|
fn=lambda: "", |
|
|
outputs=[msg_input] |
|
|
) |
|
|
|
|
|
msg_input.submit( |
|
|
fn=chat_respond, |
|
|
inputs=[msg_input, chatbot], |
|
|
outputs=[chatbot] |
|
|
).then( |
|
|
fn=lambda: "", |
|
|
outputs=[msg_input] |
|
|
) |
|
|
|
|
|
|
|
|
def create_sample_click(text): |
|
|
def handler(history): |
|
|
for result in chat_respond(text, history or []): |
|
|
yield result |
|
|
return handler |
|
|
|
|
|
sample_btn1.click(fn=create_sample_click("κ°λ¨μμ μΉ΄ν μ°½μ
νλ €λ©΄ μ΄λ»κ² ν΄μΌ νλμ?"), inputs=[chatbot], outputs=[chatbot]) |
|
|
sample_btn2.click(fn=create_sample_click("μΉν¨μ§μ΄ κ°μ₯ ν¬νλ μ§μμ μ΄λμΈκ°μ?"), inputs=[chatbot], outputs=[chatbot]) |
|
|
sample_btn3.click(fn=create_sample_click("1μΈ΅μ΄ μ 리ν μ
μ’
μ 무μμΈκ°μ?"), inputs=[chatbot], outputs=[chatbot]) |
|
|
sample_btn4.click(fn=create_sample_click("νλμ°¨μ΄μ¦ μ μ μ¨μ΄ λμ μ
μ’
μ?"), inputs=[chatbot], outputs=[chatbot]) |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch(server_name="0.0.0.0", server_port=7860, share=False) |