my-tide-env / ui.py
alwaysgood's picture
Update ui.py
d9d9c5a verified
import gradio as gr
import os
from datetime import datetime
from config import STATIONS, STATION_NAMES
from supabase_utils import get_supabase_client
from api_utils import (
api_get_tide_level,
api_get_tide_series,
api_get_extremes_info,
api_check_tide_alert,
api_compare_stations,
api_health_check
)
# API 문서 모듈 import
from api_docs import generate_api_docs
# 노이즈 시나리오 모듈 import
from noise_scenarios import apply_noise_scenario
def noise_test_handler(scenario, intensity, csv_file, station_id):
"""노이즈 테스트 실행 핸들러"""
if csv_file is None:
return None, "❌ CSV 파일을 업로드해주세요.", {}
try:
import pandas as pd
# 원본 데이터 로드
df_original = pd.read_csv(csv_file.name)
# 노이즈 시나리오 적용
df_noisy, comparison_plot = apply_noise_scenario(df_original, scenario, intensity)
# 안전한 비교를 위해 공통 컬럼과 인덱스만 사용
common_cols = set(df_original.columns) & set(df_noisy.columns)
numeric_cols = [col for col in common_cols if df_original[col].dtype in ['float64', 'int64']]
# 변화된 값 계산 (인덱스 리셋하여 안전하게 비교)
df_orig_reset = df_original.tail(len(df_noisy)).reset_index(drop=True)
df_noisy_reset = df_noisy.reset_index(drop=True)
changed_count = 0
for col in numeric_cols:
if col in df_orig_reset.columns and col in df_noisy_reset.columns:
orig_vals = df_orig_reset[col]
noisy_vals = df_noisy_reset[col]
# 길이 맞추기
min_len = min(len(orig_vals), len(noisy_vals))
orig_vals = orig_vals[:min_len]
noisy_vals = noisy_vals[:min_len]
# NaN이 아닌 값들 중에서 변화된 것 계산
mask = orig_vals.notna() & noisy_vals.notna()
if mask.any():
changed = (orig_vals[mask] != noisy_vals[mask]).sum()
changed_count += changed
# 로그 생성
log_message = f"""✅ 노이즈 테스트 완료
📁 파일: {csv_file.name}
🌪️ 시나리오: {scenario} (강도: {intensity})
📊 원본 데이터: {len(df_original)}
🔧 노이즈 데이터: {len(df_noisy)}
⚡ 변화된 값: {changed_count}개"""
# 성능 지표 계산
metrics = calculate_noise_metrics(df_original, df_noisy)
return comparison_plot, log_message, metrics
except Exception as e:
return None, f"❌ 노이즈 테스트 실패: {str(e)}", {}
def calculate_noise_metrics(df_original, df_noisy):
"""노이즈 영향 지표 계산"""
import pandas as pd
import numpy as np
metrics = {}
# 안전한 비교를 위해 인덱스 정렬 및 길이 맞추기
df_orig_tail = df_original.tail(len(df_noisy)).reset_index(drop=True)
df_noisy_reset = df_noisy.reset_index(drop=True)
# 공통 컬럼만 처리
common_cols = set(df_orig_tail.columns) & set(df_noisy_reset.columns)
numeric_cols = [col for col in common_cols if df_orig_tail[col].dtype in ['float64', 'int64'] and col != 'date']
for col in numeric_cols:
try:
original_vals = df_orig_tail[col].dropna()
noisy_vals = df_noisy_reset[col].dropna()
if len(original_vals) > 0 and len(noisy_vals) > 0:
# 길이 맞추기
min_len = min(len(original_vals), len(noisy_vals))
orig_subset = df_orig_tail[col][:min_len]
noisy_subset = df_noisy_reset[col][:min_len]
# NaN이 아닌 값들만 비교
valid_mask = orig_subset.notna() & noisy_subset.notna()
if valid_mask.any():
orig_valid = orig_subset[valid_mask]
noisy_valid = noisy_subset[valid_mask]
# 평균 절대 오차
mae = np.mean(np.abs(orig_valid - noisy_valid))
# 상대 오차 (%)
mean_abs_orig = np.mean(np.abs(orig_valid))
relative_error = (mae / mean_abs_orig * 100) if mean_abs_orig > 0 else 0
# 결측치 증가율
missing_increase = df_noisy_reset[col].isna().sum() - df_orig_tail[col].isna().sum()
# 최대/최소값 변화 (안전하게 계산)
orig_max = df_orig_tail[col].max() if df_orig_tail[col].notna().any() else 0
noisy_max = df_noisy_reset[col].max() if df_noisy_reset[col].notna().any() else 0
orig_min = df_orig_tail[col].min() if df_orig_tail[col].notna().any() else 0
noisy_min = df_noisy_reset[col].min() if df_noisy_reset[col].notna().any() else 0
metrics[col] = {
"평균_절대_오차": round(mae, 2),
"상대_오차_퍼센트": round(relative_error, 2),
"결측치_증가": missing_increase,
"최대값_변화": round(noisy_max - orig_max, 2),
"최소값_변화": round(noisy_min - orig_min, 2)
}
except Exception as e:
print(f"컬럼 {col} 메트릭 계산 오류: {e}")
continue
return metrics
def noise_prediction_handler(scenario, intensity, csv_file, station_id, prediction_handler):
"""노이즈 데이터로 예측 실행 핸들러"""
if csv_file is None:
return None, "❌ CSV 파일을 업로드해주세요.", {}
try:
import pandas as pd
import tempfile
import os
# 원본 데이터 로드
df_original = pd.read_csv(csv_file.name)
# 노이즈 시나리오 적용
df_noisy, _ = apply_noise_scenario(df_original, scenario, intensity)
# 노이즈 데이터를 임시 파일로 저장
import tempfile
temp_fd, temp_path = tempfile.mkstemp(suffix='.csv', text=True)
try:
# 파일 디스크립터를 닫고 경로만 사용
os.close(temp_fd)
df_noisy.to_csv(temp_path, index=False)
# Gradio File 형식에 맞는 객체 생성
class TempFile:
def __init__(self, path):
self.name = path
temp_file_obj = TempFile(temp_path)
print(f"📁 노이즈 임시 파일 생성: {temp_path}")
except Exception as e:
print(f"❌ 임시 파일 생성 실패: {e}")
if os.path.exists(temp_path):
os.unlink(temp_path)
raise
try:
# 파일 존재 및 유효성 확인
if not os.path.exists(temp_path):
raise FileNotFoundError(f"임시 파일이 존재하지 않습니다: {temp_path}")
file_size = os.path.getsize(temp_path)
if file_size == 0:
raise ValueError("임시 파일이 비어있습니다")
print(f"📊 임시 파일 크기: {file_size} bytes")
# 1. 원본 데이터로 예측
print("🔵 원본 데이터로 예측 중...")
original_plot, original_df, original_log = prediction_handler(station_id, csv_file)
# 2. 노이즈 데이터로 예측
print("🔴 노이즈 데이터로 예측 중...")
print(f"📁 노이즈 파일 경로: {temp_file_obj.name}")
noise_plot, noise_df, noise_log = prediction_handler(station_id, temp_file_obj)
# 디버깅: 예측 결과 구조 확인
print(f"📊 원본 예측 결과 타입: {type(original_df)}")
if original_df is not None:
if hasattr(original_df, 'columns'):
print(f"📊 원본 예측 컬럼: {list(original_df.columns)}")
else:
print(f"📊 원본 예측 내용: {original_df}")
print(f"📊 노이즈 예측 결과 타입: {type(noise_df)}")
if noise_df is not None:
if hasattr(noise_df, 'columns'):
print(f"📊 노이즈 예측 컬럼: {list(noise_df.columns)}")
else:
print(f"📊 노이즈 예측 내용: {noise_df}")
# 3. 예측 결과 비교 시각화 생성
comparison_plot = create_prediction_comparison_plot(original_df, noise_df, scenario)
# 4. 성능 비교 메트릭 계산
performance_metrics = calculate_prediction_metrics(original_df, noise_df)
# 5. 로그 생성
log_message = f"""✅ 노이즈 예측 비교 완료
🌪️ 시나리오: {scenario} (강도: {intensity})
🔵 원본 예측: {len(original_df) if original_df is not None else 0}개 포인트
🔴 노이즈 예측: {len(noise_df) if noise_df is not None else 0}개 포인트
📊 견고성 평가: {performance_metrics.get('robustness_score', 'N/A')}"""
return comparison_plot, log_message, performance_metrics
finally:
# 임시 파일 정리
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as e:
return None, f"❌ 노이즈 예측 실패: {str(e)}", {}
def create_prediction_comparison_plot(original_df, noise_df, scenario_name):
"""원본 예측 vs 노이즈 예측 비교 시각화"""
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
if original_df is None or noise_df is None:
return go.Figure().add_annotation(text="예측 데이터가 없습니다", xref="paper", yref="paper", x=0.5, y=0.5)
# DataFrame이 아닌 경우 처리
if not isinstance(original_df, pd.DataFrame):
return go.Figure().add_annotation(text="예측 데이터 형식 오류", xref="paper", yref="paper", x=0.5, y=0.5)
fig = make_subplots(
rows=2, cols=1,
subplot_titles=['🔮 조위 예측 비교', '📊 예측 차이'],
vertical_spacing=0.12
)
# 시간축 (예측 결과 인덱스)
time_axis = list(range(len(original_df)))
# 예측값 컬럼 찾기 및 숫자 변환
def get_prediction_values(df):
# 한국어 컬럼명도 포함한 가능한 예측값 컬럼 이름들
possible_cols = [
'final_tide', 'predicted', 'prediction', 'tide_level', 'residual',
'최종 조위 (cm)', '잔차 예측 (cm)', '조화 예측 (cm)'
]
for col in possible_cols:
if col in df.columns:
try:
# 괄호가 있는 경우 괄호 제거 후 숫자 변환 시도
if '(' in col:
values = df[col]
if values.dtype == 'object':
# 문자열에서 숫자 추출 시도
values = pd.to_numeric(values.astype(str).str.replace(r'[^\d.-]', '', regex=True), errors='coerce')
else:
values = pd.to_numeric(values, errors='coerce')
else:
values = pd.to_numeric(df[col], errors='coerce')
if not values.isna().all():
return values
except:
continue
# 마지막 숫자 컬럼 시도
numeric_cols = df.select_dtypes(include=[np.number]).columns
if len(numeric_cols) > 0:
return df[numeric_cols[-1]]
# 마지막 컬럼을 숫자로 변환 시도
try:
return pd.to_numeric(df.iloc[:, -1], errors='coerce')
except:
return pd.Series([0] * len(df))
original_values = get_prediction_values(original_df)
noise_values = get_prediction_values(noise_df)
# 원본 예측 결과
fig.add_trace(
go.Scatter(
x=time_axis,
y=original_values,
name='🔵 원본 예측',
line=dict(color='#2E86AB', width=3),
hovertemplate='원본 예측: %{y:.1f}cm<br>시점: %{x}<extra></extra>'
),
row=1, col=1
)
# 노이즈 예측 결과
fig.add_trace(
go.Scatter(
x=time_axis[:len(noise_values)],
y=noise_values,
name='🔴 노이즈 예측',
line=dict(color='#F24236', width=3, dash='dash'),
hovertemplate='노이즈 예측: %{y:.1f}cm<br>시점: %{x}<extra></extra>'
),
row=1, col=1
)
# 예측 차이 계산 및 시각화
if len(original_values) == len(noise_values):
try:
difference = noise_values - original_values
fig.add_trace(
go.Scatter(
x=time_axis,
y=difference,
name='📊 예측 차이',
line=dict(color='orange', width=2),
fill='tonexty',
hovertemplate='예측 차이: %{y:.1f}cm<br>시점: %{x}<extra></extra>'
),
row=2, col=1
)
# 0선 추가
fig.add_hline(y=0, line_dash="dot", line_color="gray", row=2, col=1)
except Exception as e:
print(f"차이 계산 오류: {e}")
fig.update_layout(
title={
'text': f"🔮 예측 견고성 테스트: {scenario_name}",
'x': 0.5,
'font': {'size': 18, 'color': '#2E86AB'}
},
height=700,
showlegend=True,
legend=dict(x=0.02, y=0.98, bgcolor='rgba(255,255,255,0.8)')
)
fig.update_xaxes(title_text="미래 시점", showgrid=True)
fig.update_yaxes(title_text="조위 (cm)", showgrid=True)
return fig
def calculate_prediction_metrics(original_df, noise_df):
"""예측 성능 비교 메트릭 계산"""
if original_df is None or noise_df is None:
return {"error": "예측 데이터 없음"}
import numpy as np
import pandas as pd
try:
# 예측값 추출 함수 (동일한 로직)
def get_prediction_values(df):
# 한국어 컬럼명도 포함
possible_cols = [
'final_tide', 'predicted', 'prediction', 'tide_level', 'residual',
'최종 조위 (cm)', '잔차 예측 (cm)', '조화 예측 (cm)', '예측 시간'
]
for col in possible_cols:
if col in df.columns:
try:
# 시간 컬럼은 건너뛰기
if '시간' in col or 'time' in col.lower():
continue
# 괄호가 있는 경우 괄호 제거 후 숫자 변환 시도
if '(' in col:
values = df[col]
if values.dtype == 'object':
# 문자열에서 숫자 추출 시도
values = pd.to_numeric(values.astype(str).str.replace(r'[^\d.-]', '', regex=True), errors='coerce')
else:
values = pd.to_numeric(values, errors='coerce')
else:
values = pd.to_numeric(df[col], errors='coerce')
if not values.isna().all():
return values
except:
continue
numeric_cols = df.select_dtypes(include=[np.number]).columns
if len(numeric_cols) > 0:
return df[numeric_cols[-1]]
try:
return pd.to_numeric(df.iloc[:, -1], errors='coerce')
except:
return pd.Series([0] * len(df))
original_values = get_prediction_values(original_df)
noise_values = get_prediction_values(noise_df)
# 길이 맞추기
min_len = min(len(original_values), len(noise_values))
original_values = original_values[:min_len]
noise_values = noise_values[:min_len]
# NaN 제거
mask = ~(original_values.isna() | noise_values.isna())
original_clean = original_values[mask]
noise_clean = noise_values[mask]
if len(original_clean) == 0:
return {"error": "유효한 예측 데이터 없음"}
# 메트릭 계산
mae = np.mean(np.abs(noise_clean - original_clean)) # 평균 절대 오차
rmse = np.sqrt(np.mean((noise_clean - original_clean) ** 2)) # RMSE
max_diff = np.max(np.abs(noise_clean - original_clean)) # 최대 차이
# 견고성 점수 (0-100, 100이 가장 견고함)
mean_abs_original = np.mean(np.abs(original_clean))
if mean_abs_original > 0:
relative_error = mae / mean_abs_original * 100
else:
relative_error = 0
robustness_score = max(0, 100 - relative_error)
return {
"평균_절대_오차_cm": round(mae, 2),
"RMSE_cm": round(rmse, 2),
"최대_차이_cm": round(max_diff, 2),
"상대_오차_퍼센트": round(relative_error, 2),
"견고성_점수": round(robustness_score, 1),
"평가": "견고함" if robustness_score > 80 else "보통" if robustness_score > 60 else "취약함",
"비교_데이터_수": len(original_clean)
}
except Exception as e:
return {"error": f"메트릭 계산 실패: {str(e)}"}
def create_ui(prediction_handler, chatbot_handler, api_handlers: dict):
"""Gradio UI를 생성하고 반환합니다."""
with gr.Blocks(title="통합 조위 예측 시스템", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🌊 통합 조위 예측 시스템 with Gemini")
# 연결 상태 표시
client = get_supabase_client()
supabase_status = "🟢 연결됨" if client else "🔴 연결 안됨 (환경변수 확인 필요)"
gemini_status = "🟢 연결됨" if os.getenv("GEMINI_API_KEY") else "🔴 연결 안됨 (환경변수 확인 필요)"
gr.Markdown(f"**Supabase 상태**: {supabase_status} | **Gemini 상태**: {gemini_status}")
with gr.Tabs():
# 1. 예측 탭
with gr.TabItem("🔮 통합 조위 예측"):
gr.Markdown("""
### TimeXer 모델을 활용한 조위 예측
- 과거 기상 데이터를 업로드하여 미래 72시간 조위 예측
- 잔차 예측 + 조화 예측 = 최종 조위
""")
with gr.Row():
with gr.Column(scale=1):
station_id_input = gr.Dropdown(
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
label="관측소 선택",
value="DT_0001"
)
input_csv = gr.File(label="과거 데이터 업로드 (.csv)")
predict_btn = gr.Button("🚀 예측 실행", variant="primary")
with gr.Column(scale=3):
output_plot = gr.Plot(label="예측 결과 시각화")
output_df = gr.DataFrame(label="예측 결과 데이터")
output_log = gr.Textbox(label="실행 로그", lines=5, interactive=False)
# 2. 챗봇 탭
with gr.TabItem("💬 AI 조위 챗봇"):
gr.Markdown("### 🤖 AI 조위 전문가")
gr.Markdown("조위에 대해 궁금한 점을 물어보세요.")
with gr.Row():
with gr.Column():
chatbot_display = gr.Chatbot(
label="대화창",
height=400,
show_label=True
)
with gr.Row():
user_input = gr.Textbox(
label="질문을 입력하세요",
placeholder="예: 인천 현재 조위 알려줘",
lines=1,
scale=4
)
send_btn = gr.Button("전송", variant="primary", scale=1)
gr.Examples(
examples=[
"인천 현재 조위 알려줘",
"오늘 만조 시간은?",
"부산과 인천 조위 비교해줘"
],
inputs=user_input
)
# 챗봇 이벤트 연결
def chat_response(message, history):
if not message.strip():
return history, ""
# 사용자 메시지 추가
history = history or []
history.append([message, None])
# AI 응답 생성
try:
response = chatbot_handler(message, history)
history[-1][1] = response
except Exception as e:
history[-1][1] = f"죄송합니다. 오류가 발생했습니다: {str(e)}"
return history, ""
send_btn.click(
fn=chat_response,
inputs=[user_input, chatbot_display],
outputs=[chatbot_display, user_input]
)
user_input.submit(
fn=chat_response,
inputs=[user_input, chatbot_display],
outputs=[chatbot_display, user_input]
)
# 3. 항구 혼잡도 분석 탭
with gr.TabItem("🚢 항구 혼잡도 분석"):
gr.Markdown("### 🎯 YOLO 기반 선박 탐지")
gr.Markdown("미리 업로드된 샘플 영상을 선택하여 항구 내 선박을 자동 탐지하고 혼잡도를 분석합니다.")
# 미리 업로드된 영상 목록 가져오기
import glob
video_files = glob.glob("videos/*.mp4") + glob.glob("videos/*.avi") + glob.glob("videos/*.mov")
video_choices = [("샘플 영상 없음", None)] if not video_files else [(f"📹 {os.path.basename(f)}", f) for f in video_files]
with gr.Row():
with gr.Column(scale=1):
video_dropdown = gr.Dropdown(
label="분석할 영상 선택",
choices=video_choices,
value=None
)
video_upload = gr.Video(
label="또는 새 영상 업로드",
sources=["upload"],
visible=True
)
analyze_btn = gr.Button("🔍 영상 분석 시작", variant="primary", size="lg")
gr.Markdown("""
**사용법**:
1. 드롭다운에서 미리 업로드된 영상 선택 또는
2. 새로운 영상 직접 업로드
**분석 내용**:
- 선박 개수 탐지
- 선박 위치 추적
- 항구 혼잡도 점수
""")
with gr.Column(scale=2):
video_output = gr.Video(
label="분석 결과 영상",
interactive=False
)
analysis_results = gr.Textbox(
label="분석 결과",
lines=8,
interactive=False,
placeholder="영상 분석 결과가 여기에 표시됩니다..."
)
def process_harbor_video(selected_video, uploaded_video):
"""항구 영상 분석 함수 (YOLO 기반)"""
# 선택된 영상이나 업로드된 영상 중 하나 선택
video_path = selected_video if selected_video else uploaded_video
if video_path is None:
return None, "드롭다운에서 영상을 선택하거나 새 영상을 업로드해주세요."
try:
# TODO: YOLO 모델을 사용한 선박 탐지 로직 구현
# 현재는 placeholder 결과 반환
video_name = os.path.basename(video_path) if video_path else "업로드된 영상"
results_text = f"""
📊 영상 분석 완료: {video_name}
🚢 탐지된 선박 수: 2-3척
📍 평균 선박 위치: 항구 중앙부
🟡 혼잡도 점수: 2/10 (낮음)
⏱️ 분석 시간: 80초
"""
return video_path, results_text
except Exception as e:
return None, f"영상 분석 중 오류가 발생했습니다: {str(e)}"
analyze_btn.click(
fn=process_harbor_video,
inputs=[video_dropdown, video_upload],
outputs=[video_output, analysis_results]
)
# 4. API 탭
with gr.TabItem("🔌 API"):
gr.Markdown("## RESTful API 엔드포인트\n실무에서 바로 사용 가능한 API 기능을 제공합니다.")
with gr.Tabs():
# 3-1. 특정 시간 조위
with gr.TabItem("조위 조회"):
gr.Markdown("#### 특정 관측소, 특정 시간의 조위 정보를 조회합니다.")
with gr.Row():
with gr.Column(scale=1):
api1_station = gr.Dropdown(choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS], label="관측소", value="DT_0001")
api1_time = gr.Textbox(label="조회 시간 (비워두면 현재)", placeholder="2025-08-10T15:00:00")
api1_btn = gr.Button("UI에서 테스트", variant="primary")
with gr.Column(scale=2):
api1_output = gr.JSON(label="API 응답")
api1_btn.click(
fn=lambda s, t: api_handlers["tide_level"](s, t if t else None),
inputs=[api1_station, api1_time],
outputs=api1_output
)
generate_api_docs("tide_level")
# 3-2. 시계열 데이터
with gr.TabItem("시계열"):
gr.Markdown("#### 지정된 기간의 시계열 조위 데이터를 조회합니다.")
with gr.Row():
with gr.Column(scale=1):
api2_station = gr.Dropdown(choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS], label="관측소", value="DT_0001")
api2_start = gr.Textbox(label="시작 시간", placeholder="2025-08-10T00:00:00")
api2_end = gr.Textbox(label="종료 시간", placeholder="2025-08-11T00:00:00")
api2_interval = gr.Number(label="간격(분)", value=60, minimum=5)
api2_btn = gr.Button("UI에서 테스트", variant="primary")
with gr.Column(scale=2):
api2_output = gr.JSON(label="API 응답 (공공 API 형식)")
api2_btn.click(
fn=lambda s, st, et, i: api_handlers["tide_series"](s, st if st else None, et if et else None, int(i)),
inputs=[api2_station, api2_start, api2_end, api2_interval],
outputs=api2_output
)
generate_api_docs("tide_series")
# 3-3. 만조/간조
with gr.TabItem("만조/간조"):
gr.Markdown("#### 특정 날짜의 만조/간조 정보를 조회합니다.")
with gr.Row():
with gr.Column(scale=1):
api3_station = gr.Dropdown(choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS], label="관측소", value="DT_0001")
api3_date = gr.Textbox(label="날짜 (YYYY-MM-DD)", placeholder=datetime.now().strftime("%Y-%m-%d"))
api3_secondary = gr.Checkbox(label="부차 만조/간조 포함", value=False)
api3_btn = gr.Button("UI에서 테스트", variant="primary")
with gr.Column(scale=2):
api3_output = gr.JSON(label="만조/간조 정보")
api3_btn.click(
fn=lambda s, d, sec: api_handlers["extremes"](s, d if d else None, sec),
inputs=[api3_station, api3_date, api3_secondary],
outputs=api3_output
)
generate_api_docs("extremes")
# 3-4. 위험 알림
with gr.TabItem("위험 알림"):
gr.Markdown("#### 향후 설정된 시간 동안 위험 수위 도달 여부를 체크합니다.")
with gr.Row():
with gr.Column(scale=1):
api4_station = gr.Dropdown(choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS], label="관측소", value="DT_0001")
api4_hours = gr.Number(label="확인 시간(시간)", value=24, minimum=1, maximum=72)
api4_warning = gr.Number(label="주의 수위(cm)", value=700)
api4_danger = gr.Number(label="경고 수위(cm)", value=750)
api4_btn = gr.Button("UI에서 테스트", variant="primary")
with gr.Column(scale=2):
api4_output = gr.JSON(label="위험 수위 정보")
api4_btn.click(
fn=lambda s, h, w, d: api_handlers["alert"](s, int(h), w, d),
inputs=[api4_station, api4_hours, api4_warning, api4_danger],
outputs=api4_output
)
generate_api_docs("alert")
# 3-5. 관측소 비교
with gr.TabItem("비교"):
gr.Markdown("#### 여러 관측소의 조위를 동시에 비교합니다.")
with gr.Row():
with gr.Column(scale=1):
api5_stations = gr.CheckboxGroup(choices=[(f"{STATION_NAMES[s]}", s) for s in STATIONS], label="비교할 관측소 선택", value=["DT_0001", "DT_0002", "DT_0003"])
api5_time = gr.Textbox(label="비교 시간 (비워두면 현재)", placeholder="2025-08-10T15:00:00")
api5_btn = gr.Button("UI에서 테스트", variant="primary")
with gr.Column(scale=2):
api5_output = gr.JSON(label="비교 결과")
api5_btn.click(
fn=lambda s, t: api_handlers["compare"](s, t if t else None),
inputs=[api5_stations, api5_time],
outputs=api5_output
)
generate_api_docs("compare")
# 3-6. 상태 체크
with gr.TabItem("상태"):
gr.Markdown("#### API 및 시스템의 현재 상태를 확인합니다.")
with gr.Row():
api6_btn = gr.Button("🔍 상태 확인", variant="secondary", scale=1)
api6_output = gr.JSON(label="시스템 상태", scale=2)
api6_btn.click(
fn=api_handlers["health"],
inputs=[],
outputs=api6_output
)
generate_api_docs("health")
# 4. 노이즈 테스트 탭
with gr.TabItem("🌪️ 노이즈 테스트"):
gr.Markdown("""
### 🧪 시스템 견고성 테스트
- 극한 기상 상황에서 예측 성능 평가
- 센서 오작동 및 데이터 결측 시뮬레이션
- 원본 vs 노이즈 데이터 비교 시각화
""")
with gr.Row():
with gr.Column(scale=1):
# 노이즈 시나리오 선택
noise_scenario = gr.Dropdown(
choices=[
("🌀 태풍 (폭풍해일)", "typhoon"),
("📡 센서 오작동", "sensor_malfunction"),
("❌ 연속 결측치", "burst_missing"),
("🌡️ 극한 기상", "extreme_weather")
],
label="노이즈 시나리오 선택",
value="typhoon"
)
# 노이즈 강도 조절
noise_intensity = gr.Slider(
minimum=0.5, maximum=2.0, step=0.1, value=1.0,
label="노이즈 강도 (0.5=약함, 2.0=극심)"
)
# 테스트 데이터 업로드
noise_test_csv = gr.File(label="테스트 데이터 업로드 (.csv)")
# 관측소 선택
noise_station = gr.Dropdown(
choices=[(f"{STATION_NAMES[s]} ({s})", s) for s in STATIONS],
label="관측소 선택",
value="DT_0001"
)
# 실행 버튼
noise_test_btn = gr.Button("🧪 노이즈 테스트 실행", variant="primary")
noise_predict_btn = gr.Button("🔮 노이즈 데이터로 예측", variant="secondary")
with gr.Column(scale=3):
# 노이즈 비교 시각화
noise_comparison_plot = gr.Plot(label="원본 vs 노이즈 데이터 비교")
# 예측 결과 비교 (원본 예측 vs 노이즈 예측)
noise_prediction_plot = gr.Plot(label="예측 결과 비교")
# 로그 및 성능 지표
with gr.Row():
noise_log = gr.Textbox(label="노이즈 테스트 로그", lines=4, interactive=False)
noise_metrics = gr.JSON(label="성능 지표 (정확도 비교)")
# 노이즈 테스트 이벤트 핸들러
noise_test_btn.click(
fn=noise_test_handler,
inputs=[noise_scenario, noise_intensity, noise_test_csv, noise_station],
outputs=[noise_comparison_plot, noise_log, noise_metrics]
)
# 노이즈 예측 이벤트 핸들러
noise_predict_btn.click(
fn=lambda scenario, intensity, csv_file, station_id: noise_prediction_handler(
scenario, intensity, csv_file, station_id, prediction_handler
),
inputs=[noise_scenario, noise_intensity, noise_test_csv, noise_station],
outputs=[noise_prediction_plot, noise_log, noise_metrics]
)
# 이벤트 핸들러 연결
predict_btn.click(
fn=prediction_handler,
inputs=[station_id_input, input_csv],
outputs=[output_plot, output_df, output_log],
api_name="predict"
)
return demo