import os import pandas as pd import gradio as gr import torch from transformers import AutoTokenizer, AutoModelForSequenceClassification # ---------------------------------------------------- # 1. Model & LoRA Adapter Setup # ---------------------------------------------------- # 기본 모델 지정 (예: kcbert-base 또는 klue/bert-base 등) BASE_MODEL_NAME = "beomi/kcbert-base" LORA_ADAPTER_DIR = "./lora_adapter" # LoRA 어댑터 폴더가 없을 경우 시뮬레이션용 더미 파일 생성 예시 if not os.path.exists(LORA_ADAPTER_DIR): os.makedirs(LORA_ADAPTER_DIR, exist_ok=True) tokenizer = None model = None def load_ai_model(): global tokenizer, model try: tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_NAME) # Sequence Classification (0: 정상, 1: 위험) base_model = AutoModelForSequenceClassification.from_pretrained(BASE_MODEL_NAME, num_labels=2) # LoRA 어댑터 적용 if os.path.exists(os.path.join(LORA_ADAPTER_DIR, "adapter_model.bin")) or os.path.exists(os.path.join(LORA_ADAPTER_DIR, "adapter_model.safetensors")): from peft import PeftModel model = PeftModel.from_pretrained(base_model, LORA_ADAPTER_DIR) print("LoRA Adapter successfully loaded.") else: model = base_model print("Base model loaded (LoRA adapter fallback).") model.eval() except Exception as e: print(f"Model load notice: {e}") tokenizer = None model = None # 모델 로드 시도 load_ai_model() # ---------------------------------------------------- # 2. Inference Logic # ---------------------------------------------------- def analyze_text(text): if not text or len(text.strip()) == 0: return "텍스트를 입력해주세요.", "대기 중", 0.0, "입력값이 없습니다." # 16자 이상 제한 가이드 체크 (프롬프트 설계 규칙 적용) length_warning = "" if len(text) < 16: length_warning = "⚠️ [주의] 입력된 텍스트가 16자 미만입니다. 프롬프트 규칙(16자 이상)을 확인하세요." else: length_warning = "✅ 프롬프트 제약조건(16자 이상) 만족" if model is not None and tokenizer is not None: try: inputs = tokenizer(text, return_tensors="pt", truncation=True, max_length=128) with torch.no_grad(): outputs = model(**inputs) probs = torch.softmax(outputs.logits, dim=-1)[0] risk_score = probs[1].item() # 1: 위험 확률 label_code = 1 if risk_score > 0.5 else 0 label_str = "1 (위험)" if label_code == 1 else "0 (정상)" confidence = risk_score if label_code == 1 else (1 - risk_score) return label_str, f"{confidence*100:.1f}%", risk_score, length_warning except Exception as e: pass # 모델 준비 전 또는 로컬 폴백 모드 (Rule-based / Keyword heuristic) danger_keywords = ["위험", "유해", "폭력", "편향", "공격", "욕설", "불법", "차별"] has_danger = any(kw in text for kw in danger_keywords) if has_danger: return "1 (위험)", "88.5%", 0.885, length_warning else: return "0 (정상)", "94.2%", 0.058, length_warning # 데이터 저장용 글로벌 데이터프레임 (검수 이력 관리) review_dataset = [] def add_to_dataset(input_text, ai_label, user_label, edit_note): if not input_text: return "❌ 입력 텍스트가 없습니다.", pd.DataFrame(review_dataset) # 규칙 체크: text, label 명칭 준수 / label은 0 또는 1 숫자 try: clean_label = int(user_label) except: clean_label = 1 if "위험" in str(user_label) or "1" in str(user_label) else 0 record = { "text": input_text, "label": clean_label, # 0(정상) 또는 1(위험) "ai_predicted": ai_label, "user_edited": "수정됨" if edit_note else "원본 유지", "review_note": edit_note if edit_note else "인간 검수 완료" } review_dataset.append(record) df = pd.DataFrame(review_dataset) return f"✅ 성공적으로 검수 데이터셋에 반영되었습니다. (총 {len(review_dataset)}건)", df def export_csv(): if not review_dataset: df_empty = pd.DataFrame(columns=["text", "label"]) df_empty.to_csv("dataset.csv", index=False, encoding="utf-8-sig") return "dataset.csv" # 8차시 pd.read_csv 호환 표준 규격 export (text, label 두 컬럼 필수) export_df = pd.DataFrame(review_dataset)[["text", "label"]] file_path = "dataset_export.csv" export_df.to_csv(file_path, index=False, encoding="utf-8-sig") return file_path # ---------------------------------------------------- # 3. Gradio Interface Definition # ---------------------------------------------------- custom_css = """ .model-card-box { background-color: #fff3cd; border: 1px solid #ffeeba; border-radius: 8px; padding: 15px; margin-bottom: 15px; } .rule-box { background-color: #e2e3e5; border-left: 4px solid #383d41; padding: 10px 15px; font-size: 0.9em; } """ with gr.Blocks(title="책임·안전 AI 데이터 검수 웹 인터페이스", css=custom_css) as demo: gr.Markdown("# 🛡️ 책임·안전 AI (고등학교 모듈 3-1) - 데이터 수집 & 전략적 검수") gr.Markdown("### 내가 설계한 기준으로 AI의 연료를 만들다") # === [Model Card 한계 고지 영역] === with gr.Accordion("⚠️ [필독] Model Card & 한계 고지 (Limitations & Disclaimers)", open=True): gr.HTML("""

📌 모델 카드 (Model Card) & 유의사항

""") with gr.Tabs(): # TAB 1: AI 모델 실행 및 인간 검수 (UI 스케치 구현) with gr.TabItem("📱 UI 스케치 기반 AI 검수 인터페이스"): gr.Markdown("### STEP 1 & 2. 프롬프트 입력 및 AI 진단") with gr.Row(): with gr.Column(scale=2): input_text = gr.Textbox( label="입력 텍스트 (Prompt)", placeholder="검수할 텍스트를 입력하세요. (프롬프트 제약: 16자 이상 작성 권장)", lines=4 ) btn_analyze = gr.Button("🔍 AI 분석 및 판단 실행", variant="primary") rule_info = gr.Markdown("ℹ️ **프롬프트 5요소 제약**: 16자 이상의 텍스트를 입력하여 테스트하세요.") with gr.Column(scale=2): output_label = gr.Textbox(label="AI 예측 라벨 (ai_predicted)", interactive=False) output_conf = gr.Textbox(label="신뢰도 (Confidence)", interactive=False) length_check = gr.Textbox(label="프롬프트 검증 결과", interactive=False) gr.Markdown("---") gr.Markdown("### STEP 3. 생성 AI 비판적 검수 (인간 판정 & 수정)") with gr.Row(): with gr.Column(): user_label = gr.Radio( choices=["0 (정상)", "1 (위험)"], value="0 (정상)", label="학생 직접 라벨 판정 (label: 0 또는 1)", info="AI 라벨을 무조건 신뢰하지 말고 직접 비판적으로 선택하세요." ) edit_note = gr.Textbox( label="수정 / 삭제 / 검토 사유 작성 (검토 이력 관리)", placeholder="예: AI는 위험으로 분류했으나 정상적인 시사 논평문으로 확인되어 0으로 수정함." ) btn_save = gr.Button("💾 검수 데이터셋에 추가 (Save)", variant="success") save_status = gr.Markdown("") gr.Markdown("### 📊 수집 & 검수 완료된 데이터셋 (CSV 미리보기)") dataset_table = gr.DataFrame( headers=["text", "label", "ai_predicted", "user_edited", "review_note"], datatype=["str", "number", "str", "str", "str"], interactive=False ) btn_export = gr.Button("📥 8차시용 CSV 데이터셋 다운로드 (text, label 컬럼)") csv_file_output = gr.File(label="다운로드할 CSV 파일") # TAB 2: 알고리즘 흐름도 (Algorithm Flowchart) with gr.TabItem("🔄 알고리즘 흐름도 (Algorithm Flowchart)"): gr.Markdown(""" ### 📌 데이터 수집 및 전략적 검수 파이프라인 흐름도 ``` [START: 프롬프트 입력] │ ▼ [STEP 1: 프롬프트 제약조건 검증] ──(16자 미만)──► [경고 메시지 출력] │ (16자 이상) ▼ [STEP 2: LoRA 어댑터 적용 AI 모델 추론] │ ▼ [AI 1차 판정 출력 (0:정상 / 1:위험 & 신뢰도)] │ ▼ [STEP 3: 생성 AI 비판적 활용 원칙 적용 (Human-in-the-Loop)] ├── 1. 인간 검수자(학생)가 AI 판정 검토 ├── 2. 라벨 직접 수정 (0:정상, 1:위험 숫자 부여) └── 3. 수정/삭제 사유 이력 기록 │ ▼ [STEP 4: 데이터셋 수집 및 CSV 내보내기] └── 규격: text, label (UTF-8 인코딩, pd.read_csv 호환) │ ▼ [END: 8차시 학습 모델 데이터 활용 준비 완료] ``` """) # TAB 3: Model Card 상세 with gr.TabItem("📜 Model Card 상세 정보"): gr.Markdown(""" ## Model Card: Responsible AI High School Fine-Tuned Model ### 1. Model Details - **Base Model:** `beomi/kcbert-base` / Transformers Sequence Classification - **Adapter:** LoRA (Low-Rank Adaptation) `peft==0.9.0` - **Task:** Text Classification (Binary: 0=Normal, 1=Hazardous) - **Language:** Korean ### 2. Intended Use - 고등학교 책임·안전 AI 교육과정(모듈 3-1) 학습용 - 학생들의 프롬프트 설계 및 생성형 AI 비판적 검수 실습 ### 3. Key Limitations & Risk Disclaimers (한계 및 유의사항) 1. **편향성 (Bias):** 사전 학습된 한국어 웹 데이터 특성상 편향된 어휘에 대해 편향된 라벨을 제안할 수 있습니다. 2. **환각 및 오분류 (Misclassification):** AI의 예측 라벨을 최종 데이터로 사용해서는 안 되며, **반드시 인간 검수자의 최종 수동 검수**가 수반되어야 합니다. 3. **데이터 표준 규격:** Output CSV는 `text`와 `label` 컬럼만을 포함하며, `label`은 반드시 정수형 `0` 또는 `1`이어야 합니다. """) # 이벤트 바인딩 btn_analyze.click( fn=analyze_text, inputs=[input_text], outputs=[output_label, output_conf, gr.State(), length_check] ) btn_save.click( fn=add_to_dataset, inputs=[input_text, output_label, user_label, edit_note], outputs=[save_status, dataset_table] ) btn_export.click( fn=export_csv, inputs=[], outputs=[csv_file_output] ) if __name__ == "__main__": demo.launch()