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 |