Spaces:
Sleeping
Sleeping
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 |