| """ |
| 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) |