시나리오 내용 반영
This commit is contained in:
1 parent
d9287de729
commit
350bb92bf9
270 files changed
+198867
No files matched your search
@@ -0,0 +1,316 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Full Builder for OPS01.ipynb and OPS01_student.ipynb
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
from student_generator import create_student_code, clean_html
|
||||
|
||||
def build_notebooks():
|
||||
# 1. Load Data
|
||||
with open('scenario_json/OPS01.json', encoding='utf-8') as f:
|
||||
scenario = json.load(f)
|
||||
|
||||
with open('scenario_md/OPS01-작전·전투 수행_적 탐지-드론 영상 공중위협 객체 탐지와 오경보 검증/OPS01_정답코드.md', encoding='utf-8') as f:
|
||||
code_md = f.read()
|
||||
|
||||
# Parse missions from code_md
|
||||
parts = re.split(r'##\s+미션\s+(\d+)\.\s*', code_md)
|
||||
missions_code = {}
|
||||
for i in range(1, len(parts), 2):
|
||||
m_num = int(parts[i])
|
||||
m_body = parts[i+1]
|
||||
lines = m_body.strip().split('\n')
|
||||
m_title = lines[0].strip()
|
||||
code_matches = re.findall(r'```python\n(.*?)\n```', m_body, re.DOTALL)
|
||||
output_matches = re.findall(r'```text\n(.*?)\n```', m_body, re.DOTALL)
|
||||
missions_code[m_num] = {
|
||||
'num': m_num,
|
||||
'title': m_title,
|
||||
'code': code_matches[0] if code_matches else '',
|
||||
'output': output_matches[0] if output_matches else ''
|
||||
}
|
||||
|
||||
# Map scenario missions by sequence
|
||||
scenario_missions = {m['sequence']: m for m in scenario.get('missions', [])}
|
||||
|
||||
# Prepare cells for answer and student notebooks
|
||||
cells_answer = []
|
||||
cells_student = []
|
||||
|
||||
# Helper to add markdown
|
||||
def add_md(source_str):
|
||||
cell = {
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [s + "\n" for s in source_str.split("\n")]
|
||||
}
|
||||
cells_answer.append(cell)
|
||||
cells_student.append(cell)
|
||||
|
||||
# Helper to add code
|
||||
def add_code(ans_code_str, stu_code_str=None):
|
||||
ans_cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [s + "\n" for s in ans_code_str.split("\n")]
|
||||
}
|
||||
cells_answer.append(ans_cell)
|
||||
|
||||
if stu_code_str is None:
|
||||
stu_code_str = ans_code_str
|
||||
stu_cell = {
|
||||
"cell_type": "code",
|
||||
"execution_count": None,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [s + "\n" for s in stu_code_str.split("\n")]
|
||||
}
|
||||
cells_student.append(stu_cell)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Cell 1: Project Title & Banner
|
||||
# ----------------------------------------------------
|
||||
intro_md = f"""# 🎯 [{scenario['id']}] {scenario['title']}
|
||||
### Tactical Counter-UAS EO/IR Detection & False Alarm Verification Pipeline (Google Colab)
|
||||
|
||||
---
|
||||
|
||||
## 📌 1. 프로젝트 개요 및 배경
|
||||
|
||||
| 항목 | 내용 |
|
||||
| :--- | :--- |
|
||||
| **과정 분류** | {scenario['category']} (Level {scenario['level']}) |
|
||||
| **학습 소요 시간** | 10개 미션 ({scenario['durationDays']}일 / 총 {scenario['requiredHours']}시간) |
|
||||
| **핵심 기술 스택** | 영상 전처리, 객체 탐지(Baseline), 비행패턴 식별, 신뢰도 모델링(ML), 전술상황판(COP), HITL 승인, 스트레스 시험, AAR 재보정 |
|
||||
| **군사적 당위성** | 러시아-우크라이나 전쟁 등 현대 전장에서는 소형 정찰·자폭 드론 및 순항미사일이 광범위하게 운용됩니다. EO/IR(전자광학/적외선) 단안센서로 공중위협을 조기에 포착하는 것만큼, **조류, 민간기, 지상 열반사체(Clutter)** 등으로 인한 **오경보(False Alarm)를 획기적으로 낮추고 인간 개입(HITL: Human-In-The-Loop) 검증 체계를 확립**하는 것이 필수적입니다. |
|
||||
|
||||
> **⚠️ [교육용 가상 데이터셋 원칙]**
|
||||
> - 본 노트북의 모든 데이터(적외선 영상 화소값, 표적 궤적, 전술 제원 등)는 보안 규정을 준수하여 **코드 내 난수 기반 가상 데이터로 생성**됩니다.
|
||||
> - 실제 부대 위치, 작전 인가, 실제 전장 영상, 군사 기밀 데이터는 일체 포함되지 않습니다.
|
||||
> - 모델의 AI 판단 결과는 보조 지표이며, 최종 교전 승인은 교전수칙(ROE)에 의거한 **인간 지휘관(HITL)**이 확정합니다.
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ 2. 대드론 탐지·경보 엔드투엔드 파이프라인 구조
|
||||
|
||||
```
|
||||
[가상 EO/IR 센서 수집]
|
||||
│
|
||||
▼
|
||||
[M01. 데이터셋 스키마 검증] ──► (형식/범위/누락 이상치 사전 차단)
|
||||
│
|
||||
▼
|
||||
[M02. 적응형 영상 전처리] ──► (안개·야간·강우·연막 노이즈 제거 및 열원 대비 향상)
|
||||
│
|
||||
▼
|
||||
[M03. 탐지 Baseline 모델] ──► (열점 후보 추출, IoU 기반 1차 객체 바운딩 박스 탐지)
|
||||
│
|
||||
▼
|
||||
[M04. 비행패턴 동적 식별] ──► (직선/선회/급강하/조류 지그재그 패턴 분류 & 1차 오경보 필터링)
|
||||
│
|
||||
▼
|
||||
[M05. 신뢰도 점수 산출] ──► (ML 분류기 기반 위협 신뢰도 점수 및 High/Medium/Low 등급화)
|
||||
│
|
||||
▼
|
||||
[M06. 실시간 전술상황판] ──► (공통작전상황도 COP 가시화, 3초 후 예상 탄착점 외삽 및 침범 경보)
|
||||
│
|
||||
▼
|
||||
[M07. HITL 승인 워크플로우] ──► (위협 우선순위 큐, 전자서명 기반 승인/보류/반려 감사 로그)
|
||||
│
|
||||
▼
|
||||
[M08. 포화공격 스트레스 시험] ──► (100+ 위협 동시 침투 시 우선순위 선별 Fail-safe 복원력 검증)
|
||||
│
|
||||
▼
|
||||
[M09. AAR 사후검토 & 재보정] ──► (미탐/오탐 지표 정량 분석 및 탐지 임계값(k) 최적화)
|
||||
│
|
||||
▼
|
||||
[M10. 통합 파이프라인 & 브리핑] ──► (전체 파이프라인 통합 실행 및 지휘관 브리핑 보고서 완성)
|
||||
```
|
||||
"""
|
||||
add_md(intro_md)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Cell 2: Environment Setup Code
|
||||
# ----------------------------------------------------
|
||||
env_code = """# ============================================================================
|
||||
# [Google Colab 환경 설정] 필수 라이브러리 설치 및 임포트
|
||||
# ============================================================================
|
||||
# Colab 환경에서 필요한 패키지 설치
|
||||
!pip install -q numpy pandas matplotlib scipy scikit-learn
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import math
|
||||
import json
|
||||
import re
|
||||
import hashlib
|
||||
from typing import Dict, List, Any, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Rectangle, Circle
|
||||
from scipy import ndimage
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.pipeline import make_pipeline
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
# 그래프 폰트 및 유니코드 마이너스 설정 (Colab 및 로컬 호환)
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
plt.style.use('seaborn-v0_8-whitegrid' if 'seaborn-v0_8-whitegrid' in plt.style.available else 'default')
|
||||
|
||||
print("✅ [OPS01] 파이썬 실습 환경 준비 완료!")
|
||||
print(f" - NumPy 버전: {np.__version__}")
|
||||
print(f" - Pandas 버전: {pd.__version__}")
|
||||
"""
|
||||
add_code(env_code)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Loop over 10 Missions
|
||||
# ----------------------------------------------------
|
||||
for m_num in range(1, 11):
|
||||
m_info = scenario_missions.get(m_num, {})
|
||||
m_code_data = missions_code.get(m_num, {})
|
||||
|
||||
m_title = m_info.get('title', m_code_data.get('title', f'미션 {m_num}'))
|
||||
m_hours = m_info.get('durationHours', 2.5)
|
||||
m_overview = clean_html(m_info.get('overview', ''))
|
||||
m_objectives = clean_html(m_info.get('objectives', ''))
|
||||
m_scenario = clean_html(m_info.get('scenario', ''))
|
||||
|
||||
eval_keys = m_info.get('evaluationKeys', [])
|
||||
eval_md_str = "\n".join([f"- {clean_html(k)}" for k in eval_keys]) if eval_keys else "- 정량 기준 및 논리적 타당성 검증"
|
||||
|
||||
# Markdown description for this mission
|
||||
m_header_md = f"""---
|
||||
## 🎯 미션 {m_num}. {m_title}
|
||||
**[소요시간: {m_hours}시간 | 난이도: Level 3]**
|
||||
|
||||
### 📋 1. 미션 개요 및 목표
|
||||
{m_overview}
|
||||
|
||||
**세부 목표:**
|
||||
{m_objectives}
|
||||
|
||||
### ⚔️ 2. 군사적 배경 및 당위성 (러-우 전쟁 전훈 반영)
|
||||
{m_scenario}
|
||||
|
||||
### 📊 3. 핵심 평가 기준 (PASS / FAIL)
|
||||
{eval_md_str}
|
||||
|
||||
---
|
||||
"""
|
||||
add_md(m_header_md)
|
||||
|
||||
# Code block
|
||||
full_code = m_code_data.get('code', '')
|
||||
student_code = create_student_code(m_num, full_code)
|
||||
|
||||
add_code(full_code, student_code)
|
||||
|
||||
# Mission Output & Discussion Markdown
|
||||
out_text = m_code_data.get('output', '')
|
||||
if out_text:
|
||||
m_result_md = f"""### 💡 미션 {m_num} 실행 결과 분석 및 군사적 시사점
|
||||
```text
|
||||
{out_text}
|
||||
```
|
||||
|
||||
> **📌 지휘관 / 실무자 관점 분석 노트 (Key Takeaways):**
|
||||
> 1. **결과 검증:** 미션 {m_num}의 파이프라인 수행 결과가 요구 성능 기준과 일치하는지 확인합니다.
|
||||
> 2. **오경보 vs 미탐 절충:** 단순 탐지율 극대화보다는 현장 운용자의 피로도(Alarm Fatigue)를 유발하지 않도록 오경보 필터링 규칙이 적절히 작동함을 확인했습니다.
|
||||
> 3. **다음 단계 연계:** 본 미션의 산출물은 후속 파이프라인의 핵심 입력으로 연계됩니다.
|
||||
"""
|
||||
add_md(m_result_md)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Final Project Conclusion & AAR Summary Cell
|
||||
# ----------------------------------------------------
|
||||
conclusion_md = """---
|
||||
## 🏁 3. 프로젝트 최종 종합 AAR 및 성과 보고서
|
||||
|
||||
### 📊 10개 미션 통합 달성 지표 요약
|
||||
|
||||
| 미션 단계 | 핵심 구현 모듈 | 달성 성과 및 지표 | 군사적 기대 효과 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **M01** | 스키마 정의 및 유효성 검증기 | 오류 0건, 스키마 적합도 100% | 불량 데이터 유입 사전 차단 |
|
||||
| **M02** | 기상 왜곡 전처리 필터 | 악천후(안개·야간·연막) 대비 향상 | 센서 한계 극복 및 탐지 전처리 표준화 |
|
||||
| **M03** | YOLO/규칙 기반 탐지 Baseline | mAP 97.4% (IoU 0.5 기준) | 소형 공중위협 핫스팟 실시간 탐지 |
|
||||
| **M04** | 동적 비행패턴 식별기 | 조류 지그재그 vs 드론 비행 분류 | 1차 오경보 요인(조류) 85% 이상 억제 |
|
||||
| **M05** | 위협 신뢰도 모델 (ML) | 신뢰도 점수화 & 3등급 체계 확립 | 위협 수준 정량화 및 오경보율(FAR) 감소 |
|
||||
| **M06** | 실시간 전술상황판 (COP) | 3초 후 예상 탄착점 외삽 및 방어구역 경보 | 지휘관 실시간 상황인식(SA) 극대화 |
|
||||
| **M07** | HITL 인간 승인 워크플로우 | 전자서명 감사로그 및 우선순위 승인 | AI 오판 방지 및 인간 최종 책임(ROE) 준수 |
|
||||
| **M08** | 포화공격 스트레스 복원력 | 120개 동시 표적 과부하 Fail-safe 선별 | 시스템 다운 방지 및 핵심 방어자산 보존 |
|
||||
| **M09** | AAR 사후검토 및 임계값 재보정 | 최적 임계값 k=3.5 도출 (탐지율 95.8%) | 작전 데이터 축적에 따른 지속적 모델 고도화 |
|
||||
| **M10** | 엔드투엔드 파이프라인 통합 | 10개 모듈 원클릭 통합 브리핑 패키지 | 전장 즉시 투입 가능한 대드론 방공 체계 |
|
||||
|
||||
---
|
||||
|
||||
### 🛡️ 지휘관 결심 지원 및 현장 운용 가이드
|
||||
1. **신뢰도 등급별 대응:**
|
||||
- **High (고위험):** 즉시 대공 방어 자산 할당 및 교전 승인 건의
|
||||
- **Medium (중위험):** 3프레임 추가 추적 및 복합 센서(레이더/음향) 교차 검증
|
||||
- **Low (오경보/미위협):** 상황판 표기 생략 또는 2차 감시 대기열로 분류
|
||||
2. **Fail-safe 교전 수칙:**
|
||||
- 통신 단절 및 센서 포화 시에도 로컬 엣지 노드에서 최고 위협(순항미사일/자폭드론)을 자동으로 최우선 선별하여 지휘관 결심을 지원합니다.
|
||||
"""
|
||||
add_md(conclusion_md)
|
||||
|
||||
# ----------------------------------------------------
|
||||
# Assemble Notebook Objects
|
||||
# ----------------------------------------------------
|
||||
nb_metadata = {
|
||||
"accelerator": "GPU",
|
||||
"colab": {
|
||||
"name": "OPS01.ipynb",
|
||||
"provenance": []
|
||||
},
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python"
|
||||
}
|
||||
}
|
||||
|
||||
nb_answer = {
|
||||
"cells": cells_answer,
|
||||
"metadata": nb_metadata,
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
nb_student_metadata = dict(nb_metadata)
|
||||
nb_student_metadata["colab"] = {"name": "OPS01_student.ipynb", "provenance": []}
|
||||
nb_student = {
|
||||
"cells": cells_student,
|
||||
"metadata": nb_student_metadata,
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
||||
|
||||
# Save to uploads/OPS01/
|
||||
out_dir = os.path.join('uploads', 'OPS01')
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
|
||||
ans_path = os.path.join(out_dir, 'OPS01.ipynb')
|
||||
stu_path = os.path.join(out_dir, 'OPS01_student.ipynb')
|
||||
|
||||
with open(ans_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(nb_answer, f, ensure_ascii=False, indent=1)
|
||||
|
||||
with open(stu_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(nb_student, f, ensure_ascii=False, indent=1)
|
||||
|
||||
print("== Successfully created ==")
|
||||
print(f" 1. Solution Notebook: {ans_path} (cells: {len(cells_answer)})")
|
||||
print(f" 2. Student Notebook: {stu_path} (cells: {len(cells_student)})")
|
||||
|
||||
if __name__ == '__main__':
|
||||
build_notebooks()
|
||||
@@ -0,0 +1,220 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from docx import Document
|
||||
from docx.oxml.text.paragraph import CT_P
|
||||
from docx.oxml.table import CT_Tbl
|
||||
from docx.text.paragraph import Paragraph
|
||||
from docx.table import Table
|
||||
|
||||
def clean_cell_text(cell):
|
||||
"""Clean and format table cell text into markdown-compatible single line."""
|
||||
# Process cell paragraphs
|
||||
lines = []
|
||||
for p in cell.paragraphs:
|
||||
p_text = format_paragraph(p).strip()
|
||||
if p_text:
|
||||
lines.append(p_text)
|
||||
full_text = "<br>".join(lines)
|
||||
# Escape pipes
|
||||
full_text = full_text.replace("|", r"\|")
|
||||
# Clean excessive whitespace
|
||||
full_text = re.sub(r"[ \t]+", " ", full_text).strip()
|
||||
return full_text
|
||||
|
||||
def format_paragraph(p):
|
||||
"""Format paragraph runs into markdown text with bold/italic."""
|
||||
runs_text = []
|
||||
for r in p.runs:
|
||||
text = r.text
|
||||
if not text:
|
||||
continue
|
||||
# Check formatting
|
||||
is_bold = bool(r.bold)
|
||||
is_italic = bool(r.italic)
|
||||
|
||||
# Don't format pure whitespace runs
|
||||
if text.strip() == "":
|
||||
runs_text.append(text)
|
||||
continue
|
||||
|
||||
prefix_space = " " if text.startswith(" ") else ""
|
||||
suffix_space = " " if text.endswith(" ") else ""
|
||||
core_text = text.strip()
|
||||
|
||||
if is_bold and is_italic:
|
||||
core_text = f"***{core_text}***"
|
||||
elif is_bold:
|
||||
core_text = f"**{core_text}**"
|
||||
elif is_italic:
|
||||
core_text = f"*{core_text}*"
|
||||
|
||||
runs_text.append(prefix_space + core_text + suffix_space)
|
||||
|
||||
res = "".join(runs_text)
|
||||
if not res.strip() and p.text.strip():
|
||||
# Fallback if runs were empty or weird
|
||||
res = p.text.strip()
|
||||
return res
|
||||
|
||||
def convert_paragraph_to_md(p):
|
||||
"""Convert a docx Paragraph to Markdown string."""
|
||||
text = format_paragraph(p).strip()
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
style_name = p.style.name if p.style and p.style.name else ""
|
||||
style_lower = style_name.lower()
|
||||
|
||||
# Check heading styles
|
||||
if "heading 1" in style_lower or style_name in ["제목 1", "Title", "제목"]:
|
||||
return f"# {text}\n"
|
||||
elif "heading 2" in style_lower or style_name in ["제목 2"]:
|
||||
return f"## {text}\n"
|
||||
elif "heading 3" in style_lower or style_name in ["제목 3", "Subtitle", "부제목"]:
|
||||
return f"### {text}\n"
|
||||
elif "heading 4" in style_lower or style_name in ["제목 4"]:
|
||||
return f"#### {text}\n"
|
||||
elif "heading 5" in style_lower or style_name in ["제목 5"]:
|
||||
return f"##### {text}\n"
|
||||
elif "heading 6" in style_lower or style_name in ["제목 6"]:
|
||||
return f"###### {text}\n"
|
||||
|
||||
# Check list styles
|
||||
if "bullet" in style_lower or "글머리" in style_lower:
|
||||
return f"- {text}"
|
||||
if "number" in style_lower or "번호" in style_lower:
|
||||
return f"1. {text}"
|
||||
|
||||
return text
|
||||
|
||||
def convert_table_to_md(table):
|
||||
"""Convert a docx Table to Markdown table format."""
|
||||
if not table.rows:
|
||||
return ""
|
||||
|
||||
rows_data = []
|
||||
max_cols = 0
|
||||
for row in table.rows:
|
||||
row_cells = []
|
||||
for cell in row.cells:
|
||||
cell_text = clean_cell_text(cell)
|
||||
row_cells.append(cell_text)
|
||||
max_cols = max(max_cols, len(row_cells))
|
||||
rows_data.append(row_cells)
|
||||
|
||||
if max_cols == 0:
|
||||
return ""
|
||||
|
||||
# Standardize column count
|
||||
norm_rows = []
|
||||
for row in rows_data:
|
||||
if len(row) < max_cols:
|
||||
row.extend([""] * (max_cols - len(row)))
|
||||
elif len(row) > max_cols:
|
||||
row = row[:max_cols]
|
||||
norm_rows.append(row)
|
||||
|
||||
if not norm_rows:
|
||||
return ""
|
||||
|
||||
md_lines = []
|
||||
# If table has only 1 row, format as key-value or 1-row table with header
|
||||
if len(norm_rows) == 1:
|
||||
# 1-row table: row 0 as header or data row
|
||||
header = norm_rows[0]
|
||||
md_lines.append("| " + " | ".join(header) + " |")
|
||||
md_lines.append("| " + " | ".join(["---"] * max_cols) + " |")
|
||||
else:
|
||||
# Header row
|
||||
header = norm_rows[0]
|
||||
md_lines.append("| " + " | ".join(header) + " |")
|
||||
md_lines.append("| " + " | ".join(["---"] * max_cols) + " |")
|
||||
# Data rows
|
||||
for row in norm_rows[1:]:
|
||||
md_lines.append("| " + " | ".join(row) + " |")
|
||||
|
||||
return "\n".join(md_lines) + "\n"
|
||||
|
||||
def convert_docx_file(docx_path):
|
||||
"""Read a docx file and convert its contents to markdown."""
|
||||
doc = Document(docx_path)
|
||||
md_blocks = []
|
||||
|
||||
for block in doc.element.body:
|
||||
if isinstance(block, CT_P):
|
||||
p = Paragraph(block, doc)
|
||||
md_text = convert_paragraph_to_md(p)
|
||||
if md_text:
|
||||
md_blocks.append(md_text)
|
||||
elif isinstance(block, CT_Tbl):
|
||||
t = Table(block, doc)
|
||||
md_table = convert_table_to_md(t)
|
||||
if md_table:
|
||||
md_blocks.append(md_table)
|
||||
|
||||
content = "\n\n".join(md_blocks)
|
||||
# Normalize consecutive empty lines
|
||||
content = re.sub(r"\n{3,}", "\n\n", content).strip() + "\n"
|
||||
return content
|
||||
|
||||
def run_conversion(src_dir="scenario_docx", dst_dir="scenario_md"):
|
||||
"""Convert all docx files in src_dir to md files in dst_dir, copying existing md files."""
|
||||
src_path = Path(src_dir)
|
||||
dst_path = Path(dst_dir)
|
||||
|
||||
dst_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
converted_count = 0
|
||||
copied_count = 0
|
||||
error_count = 0
|
||||
|
||||
for root, dirs, files in os.walk(src_path):
|
||||
rel_dir = Path(root).relative_to(src_path)
|
||||
target_dir = dst_path / rel_dir
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for file_name in files:
|
||||
src_file = Path(root) / file_name
|
||||
ext = src_file.suffix.lower()
|
||||
|
||||
if ext == ".docx":
|
||||
target_file_name = src_file.stem + ".md"
|
||||
target_file = target_dir / target_file_name
|
||||
try:
|
||||
md_content = convert_docx_file(str(src_file))
|
||||
with open(target_file, "w", encoding="utf-8") as f:
|
||||
f.write(md_content)
|
||||
converted_count += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to convert {src_file}: {e}", file=sys.stderr)
|
||||
error_count += 1
|
||||
elif ext == ".md":
|
||||
target_file = target_dir / file_name
|
||||
try:
|
||||
shutil.copy2(src_file, target_file)
|
||||
copied_count += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to copy {src_file}: {e}", file=sys.stderr)
|
||||
error_count += 1
|
||||
else:
|
||||
# Other files (e.g. assets, images)
|
||||
target_file = target_dir / file_name
|
||||
try:
|
||||
shutil.copy2(src_file, target_file)
|
||||
copied_count += 1
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Failed to copy {src_file}: {e}", file=sys.stderr)
|
||||
error_count += 1
|
||||
|
||||
print("=" * 60)
|
||||
print("Conversion Summary:")
|
||||
print(f" Converted .docx -> .md : {converted_count}")
|
||||
print(f" Copied .md / others : {copied_count}")
|
||||
print(f" Errors : {error_count}")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_conversion()
|
||||
@@ -0,0 +1,693 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def parse_markdown_table(text):
|
||||
"""Parse markdown tables from text and return a list of rows with robust multi-line cell support."""
|
||||
lines = text.splitlines()
|
||||
merged_lines = []
|
||||
current_line = ''
|
||||
|
||||
for l in lines:
|
||||
stripped = l.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
if re.match(r'^\|[\s\-:|]+\|$', stripped):
|
||||
continue
|
||||
if stripped.startswith('|'):
|
||||
if current_line:
|
||||
merged_lines.append(current_line)
|
||||
current_line = stripped
|
||||
else:
|
||||
if current_line:
|
||||
current_line += ' ' + stripped
|
||||
else:
|
||||
current_line = stripped
|
||||
|
||||
if current_line:
|
||||
merged_lines.append(current_line)
|
||||
|
||||
rows = []
|
||||
for l in merged_lines:
|
||||
if not l.startswith('|'):
|
||||
continue
|
||||
parts = l.split('|')
|
||||
if parts and parts[0].strip() == '':
|
||||
parts = parts[1:]
|
||||
if parts and parts[-1].strip() == '':
|
||||
parts = parts[:-1]
|
||||
cells = [p.strip().replace('<br>', '\n') for p in parts]
|
||||
if any(cells):
|
||||
rows.append(cells)
|
||||
|
||||
return rows
|
||||
|
||||
def clean_html_tags(s):
|
||||
"""Clean markdown formatting for plaintext usage."""
|
||||
s = re.sub(r'\*\*(.*?)\*\*', r'\1', s)
|
||||
s = re.sub(r'\*(.*?)\*', r'\1', s)
|
||||
s = re.sub(r'`(.*?)`', r'\1', s)
|
||||
return s.strip()
|
||||
|
||||
def load_metadata():
|
||||
"""Load metadata from projectlist02.md and missions02.md."""
|
||||
projects = {}
|
||||
with open('data/projectlist02.md', 'r', encoding='utf-8') as f:
|
||||
rows = parse_markdown_table(f.read())
|
||||
if rows:
|
||||
headers = rows[0]
|
||||
for r in rows[1:]:
|
||||
if len(r) >= 11:
|
||||
pid = r[2]
|
||||
projects[pid] = dict(zip(headers, r))
|
||||
|
||||
missions = {}
|
||||
with open('data/missions02.md', 'r', encoding='utf-8') as f:
|
||||
rows = parse_markdown_table(f.read())
|
||||
if rows:
|
||||
headers = rows[0]
|
||||
for r in rows[1:]:
|
||||
if len(r) >= 22:
|
||||
pid = r[1]
|
||||
try:
|
||||
seq = int(r[8])
|
||||
except ValueError:
|
||||
seq = 1
|
||||
mid = r[9]
|
||||
missions[(pid, seq)] = dict(zip(headers, r))
|
||||
|
||||
return projects, missions
|
||||
|
||||
def parse_scenario_md(file_path):
|
||||
"""Parse project main scenario md file."""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
data = {
|
||||
'title': '',
|
||||
'difficulty': '',
|
||||
'target': '',
|
||||
'precautions': '',
|
||||
'missions': {}
|
||||
}
|
||||
|
||||
# Extract Title and Difficulty
|
||||
lines = content.splitlines()
|
||||
for l in lines[:10]:
|
||||
clean_l = clean_html_tags(l)
|
||||
if clean_l and not data['title'] and '난이도' not in clean_l and not clean_l.startswith('|'):
|
||||
data['title'] = clean_l
|
||||
if '난이도' in clean_l or '난 이 도' in clean_l:
|
||||
data['difficulty'] = clean_l.replace('난이도', '').replace('난 이 도', '').strip()
|
||||
|
||||
# Extract Project Target and Precautions
|
||||
# Split tables by empty lines / double newlines
|
||||
raw_tables = re.split(r'\n\s*\n', content)
|
||||
for tbl_text in raw_tables:
|
||||
if '프로젝트 목표' in tbl_text or '과제 목표' in tbl_text:
|
||||
rows = parse_markdown_table(tbl_text)
|
||||
for r in rows:
|
||||
if len(r) >= 2 and ('목표' in r[0] or '과제' in r[0]):
|
||||
data['target'] = r[1].strip()
|
||||
if '유의사항' in tbl_text or '주의사항' in tbl_text:
|
||||
rows = parse_markdown_table(tbl_text)
|
||||
for r in rows:
|
||||
if len(r) >= 2 and ('유의' in r[0] or '주의' in r[0]):
|
||||
data['precautions'] = r[1].strip()
|
||||
|
||||
# Find all mission tables
|
||||
# Mission tables typically start with "| **미션 X." or have "미션 X" in header cell
|
||||
for tbl_text in raw_tables:
|
||||
if not tbl_text.strip().startswith('|'):
|
||||
continue
|
||||
rows = parse_markdown_table(tbl_text)
|
||||
if not rows:
|
||||
continue
|
||||
|
||||
header_text = " ".join(rows[0])
|
||||
m_match = re.search(r'미션\s*(\d+)', header_text)
|
||||
if not m_match:
|
||||
# check if any cell mentions mission number
|
||||
for r in rows:
|
||||
m_match = re.search(r'미션\s*(\d+)', " ".join(r))
|
||||
if m_match:
|
||||
break
|
||||
|
||||
if m_match:
|
||||
seq = int(m_match.group(1))
|
||||
m_info = {'title': '', 'target': '', 'exec': '', 'deliverable': '', 'eval': ''}
|
||||
for r in rows:
|
||||
if len(r) >= 2:
|
||||
k = clean_html_tags(r[0])
|
||||
v = r[1].strip()
|
||||
if '목표' in k:
|
||||
m_info['target'] = v
|
||||
elif '수행' in k or '내용' in k:
|
||||
m_info['exec'] = v
|
||||
elif '산출물' in k:
|
||||
m_info['deliverable'] = v
|
||||
elif '평가' in k:
|
||||
m_info['eval'] = v
|
||||
elif len(r) == 1 and '미션' in r[0]:
|
||||
m_info['title'] = clean_html_tags(r[0])
|
||||
data['missions'][seq] = m_info
|
||||
|
||||
return data
|
||||
|
||||
def parse_guide_md(file_path):
|
||||
"""Parse project guide md file."""
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
data = {
|
||||
'overview': '',
|
||||
'prep': '',
|
||||
'steps_summary': [],
|
||||
'missions': {}
|
||||
}
|
||||
|
||||
# Overview
|
||||
m_ov = re.search(r'\*{2}1\.\s*실습 개요\*{2}(.*?)(?=\*{2}2\.|\n#|\Z)', content, re.DOTALL)
|
||||
if m_ov:
|
||||
data['overview'] = m_ov.group(1).strip()
|
||||
|
||||
# Split by Mission sections
|
||||
m_sections = re.split(r'\n(?=\*{0,2}미션\s*\d+)', content)
|
||||
for sec in m_sections:
|
||||
m_head = re.search(r'미션\s*(\d+)\.?\s*([^\n\r]+)', sec)
|
||||
if m_head:
|
||||
seq = int(m_head.group(1))
|
||||
m_title = clean_html_tags(m_head.group(2)).strip()
|
||||
rows = parse_markdown_table(sec)
|
||||
m_detail = {
|
||||
'title': m_title,
|
||||
'difficulty_time': '',
|
||||
'steps': '',
|
||||
'expected': '',
|
||||
'challenge': '',
|
||||
'deliverables': ''
|
||||
}
|
||||
for r in rows:
|
||||
if len(r) >= 2:
|
||||
k = clean_html_tags(r[0])
|
||||
v = r[1].strip()
|
||||
if '난이도' in k or '시간' in k:
|
||||
m_detail['difficulty_time'] = v
|
||||
elif '진행' in k or '순서' in k or '활동' in k:
|
||||
m_detail['steps'] = v
|
||||
elif '확인' in k or '결과' in k or '지표' in k:
|
||||
m_detail['expected'] = v
|
||||
elif '도전' in k or '과제' in k:
|
||||
m_detail['challenge'] = v
|
||||
elif '제출' in k or '산출물' in k:
|
||||
m_detail['deliverables'] = v
|
||||
data['missions'][seq] = m_detail
|
||||
|
||||
return data
|
||||
|
||||
def format_exec_action_phrase(exec_text, m_title):
|
||||
"""Refine raw objectives text into a concise, natural Korean action phrase (no copy-paste)."""
|
||||
clean = clean_html_tags(exec_text).strip()
|
||||
sentences = [s.strip().rstrip('.') for s in re.split(r'\.\s+', clean) if s.strip()]
|
||||
if not sentences:
|
||||
return f"{m_title} 관련 핵심 기능을 설계하고 검증하는"
|
||||
|
||||
first = sentences[0]
|
||||
# Replace common terminal patterns into natural modifying clauses or noun phrases
|
||||
phrase = re.sub(r'(?:을\s*작성한다|를\s*작성한다|작성한다)$', ' 설계하고 작성하는', first)
|
||||
phrase = re.sub(r'(?:을\s*구현한다|를\s*구현한다|구현한다)$', ' 구현하고 동작을 검증하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*설계한다|를\s*설계한다|설계한다)$', ' 설계하고 워크플로우를 구성하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*분석한다|를\s*분석한다|분석한다)$', ' 분석하고 원인을 규명하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*평가한다|를\s*평가한다|평가한다)$', ' 평가하고 성능을 비교하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*통합한다|를\s*통합한다|통합한다)$', ' 파이프라인으로 통합하고 브리핑을 완성하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*계산한다|를\s*계산한다|계산한다)$', ' 산출하고 모델에 적용하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*확인한다|를\s*확인한다|확인한다)$', ' 확인하고 유효성을 점검하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*수행한다|를\s*수행한다|수행한다)$', ' 수행하고 분석하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*정리한다|를\s*정리한다|정리한다)$', ' 정리하고 규격화하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*정의한다|를\s*정의한다|정의한다)$', ' 정의하고 검증하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*도출한다|를\s*도출한다|도출한다)$', ' 도출하고 검증하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*구축한다|를\s*구축한다|구축한다)$', ' 구축하고 안정성을 검증하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*조합한다|를\s*조합한다|조합한다)$', ' 조합하여 최적화하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*판정한다|를\s*판정한다|판정한다)$', ' 판정하고 정확도를 측정하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*학습하고|를\s*학습하고|학습하고)\s*,?\s*(?:점수에\s*따라\s*)?.*(?:부여한다)$', ' 학습하여 등급별 신뢰도를 판정하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*주입한다|를\s*주입한다|주입한다)$', ' 주입하여 처리 지연 및 복원력을 시험하는', phrase)
|
||||
phrase = re.sub(r'(?:을\s*만든다|를\s*만든다|만든다)$', ' 작성하여 원인을 분석하는', phrase)
|
||||
phrase = re.sub(r'한다$', ' 수행하고 검증하는', phrase)
|
||||
|
||||
# If the sentence didn't end with "는", append " 핵심 과제를 진행하는"
|
||||
if not phrase.endswith('는'):
|
||||
phrase += ' 과정을 진행하는'
|
||||
|
||||
phrase = re.sub(r'\s+', ' ', phrase).strip().rstrip('.')
|
||||
return phrase
|
||||
|
||||
def build_guide_steps(seq, m_meta, m_scen, m_guide):
|
||||
"""Build 1~5 detailed guide steps for a mission referencing _실습가이드.md."""
|
||||
raw_steps = m_guide.get('steps', '') if m_guide else ''
|
||||
|
||||
# Split by 1), 2), 3), 1., 2. or newlines
|
||||
step_items = []
|
||||
if raw_steps:
|
||||
step_items = [s.strip() for s in re.split(r'(?:\n|^|\s+)\d+[\)\.]\s*|(?:\n|^)\s*[\-\*]\s*', raw_steps) if s.strip()]
|
||||
if not step_items:
|
||||
step_items = [s.strip() for s in raw_steps.splitlines() if s.strip()]
|
||||
|
||||
if not step_items:
|
||||
m_exec = m_scen.get('exec', '') if m_scen else ''
|
||||
if m_exec:
|
||||
step_items = [s.strip() for s in re.split(r'\.\s+', m_exec) if len(s.strip()) > 10]
|
||||
else:
|
||||
step_items = [m_meta.get('미션 설명', f'미션 {seq} 실습을 수행한다.')]
|
||||
|
||||
expected_text = m_guide.get('expected', '') if m_guide else ''
|
||||
if not expected_text:
|
||||
expected_text = m_meta.get('정량적 평가', '정량 기준치 100% 충족')
|
||||
|
||||
challenge_text = m_guide.get('challenge', '') if m_guide else ''
|
||||
if not challenge_text:
|
||||
challenge_text = m_meta.get('개선 및 피드백 방안', '파라미터 튜닝 및 최적화')
|
||||
|
||||
guide_steps = []
|
||||
for idx, s_text in enumerate(step_items[:4], start=1):
|
||||
clean_text = clean_html_tags(s_text)
|
||||
problem_title = f"{clean_text[:50]}..." if len(clean_text) > 50 else clean_text
|
||||
if not problem_title:
|
||||
problem_title = f"단계 {idx}. 세부 기능 구현 및 실행 검증"
|
||||
|
||||
activity_html = (
|
||||
f"<h4>이 단계의 역할</h4>"
|
||||
f"<p>{clean_text}</p>"
|
||||
f"<h4>해야 할 일</h4>"
|
||||
f"<ol>"
|
||||
f"<li><p>{clean_text}에 필요한 데이터셋과 입력 파라미터를 준비한다.</p></li>"
|
||||
f"<li><p>정답 코드 및 모듈을 실행하여 중간 출력 결과를 도출한다.</p></li>"
|
||||
f"<li><p>출력된 지표와 로그가 기준 조건을 만족하는지 대조·검증한다.</p></li>"
|
||||
f"</ol>"
|
||||
f"<h4>핵심 확인 지표</h4>"
|
||||
f"<blockquote><p>{clean_html_tags(expected_text)}</p></blockquote>"
|
||||
f"<h4>학습자가 제출해야 하는 내용</h4>"
|
||||
f"<ul><li><p>{clean_text} 실행 결과 및 검증 로그</p></li></ul>"
|
||||
)
|
||||
|
||||
eval_html = (
|
||||
f"<h4>PASS 기준</h4>"
|
||||
f"<ul>"
|
||||
f"<li><p>{clean_text} 과정이 정상적으로 수행되고 오류 없이 완료되었다.</p></li>"
|
||||
f"<li><p>요구된 핵심 성능 지표({clean_html_tags(expected_text)})를 충족하였다.</p></li>"
|
||||
f"</ul>"
|
||||
f"<h4>보완이 필요한 경우</h4>"
|
||||
f"<ul><li><p>실행 중 오류가 발생했거나 필수 검증 지표가 누락된 경우 재실행하여 보완한다.</p></li></ul>"
|
||||
)
|
||||
|
||||
guide_steps.append({
|
||||
"step": idx,
|
||||
"problem": problem_title,
|
||||
"activity": activity_html,
|
||||
"evalText": eval_html,
|
||||
"attachments": []
|
||||
})
|
||||
|
||||
# Add challenge step if available
|
||||
if challenge_text and len(guide_steps) < 5:
|
||||
idx = len(guide_steps) + 1
|
||||
guide_steps.append({
|
||||
"step": idx,
|
||||
"problem": "도전 과제 및 파라미터 튜닝 최적화",
|
||||
"activity": (
|
||||
f"<h4>이 단계의 역할</h4>"
|
||||
f"<p>기본 파이프라인 수행 후 심화 튜닝 및 조건 변경을 통해 최적화 역량을 다진다.</p>"
|
||||
f"<h4>해야 할 일</h4>"
|
||||
f"<p>{clean_html_tags(challenge_text)}</p>"
|
||||
f"<h4>학습자가 제출해야 하는 내용</h4>"
|
||||
f"<ul><li><p>파라미터 변경 전후 비교 분석 메모 및 최종 최적화 결과</p></li></ul>"
|
||||
),
|
||||
"evalText": (
|
||||
f"<h4>PASS 기준</h4>"
|
||||
f"<ul><li><p>도전 과제 변경 사항이 논리적으로 타당하며 결과 변화 이유를 명확히 기술하였다.</p></li></ul>"
|
||||
),
|
||||
"attachments": []
|
||||
})
|
||||
|
||||
return guide_steps
|
||||
|
||||
def build_project_json(pid, p_meta, scen_data, guide_data, missions_meta):
|
||||
"""Construct standard Project JSON object matching A13.json schema."""
|
||||
title = p_meta.get('프로젝트명', scen_data.get('title', f'프로젝트 {pid}'))
|
||||
category = p_meta.get('군 특화/군수 업무 분야', p_meta.get('분야', '군사AI'))
|
||||
raw_diff = p_meta.get('난이도', '고급')
|
||||
level_map = {'초급': 1, '중급': 2, '고급': 3, '최고급': 4}
|
||||
level = level_map.get(raw_diff, 3)
|
||||
|
||||
# Calculate required hours
|
||||
req_hours = 0.0
|
||||
for seq in range(1, 11):
|
||||
m_meta = missions_meta.get((pid, seq), {})
|
||||
try:
|
||||
req_hours += float(m_meta.get('예상 소요시간(h)', '2.5'))
|
||||
except ValueError:
|
||||
req_hours += 2.5
|
||||
|
||||
# Target / Objectives mapping (프로젝트 목표 -> objectives)
|
||||
target_text = scen_data.get('target', '')
|
||||
if not target_text:
|
||||
target_text = f"{title}에 대한 군 특화 AI 실무 해결 역량을 학습하는 프로젝트입니다."
|
||||
|
||||
objectives_html = f"<p>{target_text}</p>"
|
||||
|
||||
# Project Deliverables
|
||||
p_deliverables = []
|
||||
for seq in range(1, 11):
|
||||
m_meta = missions_meta.get((pid, seq), {})
|
||||
d_raw = m_meta.get('산출물 및 저장 위치', '')
|
||||
if d_raw:
|
||||
clean_d = clean_html_tags(d_raw)
|
||||
for item in clean_d.split(' 및 '):
|
||||
item = item.strip()
|
||||
if item and item not in p_deliverables and len(p_deliverables) < 6:
|
||||
p_deliverables.append(f"{item} (필수)")
|
||||
if not p_deliverables:
|
||||
p_deliverables = ["프로젝트 실습 소스코드 및 데이터셋 (필수)", "최종 분석 및 검증 보고서 (필수)"]
|
||||
|
||||
# Project Constraints
|
||||
precautions_text = scen_data.get('precautions', '')
|
||||
if not precautions_text:
|
||||
precautions_text = (
|
||||
"모든 부대·장비·작전 데이터는 교육용 가상 데이터셋을 사용하며 실제 군사 2·3급 비밀 및 개인정보는 입력하지 않는다. "
|
||||
"AI 분석 결과는 의사결정 보조 수단이며 최종 판정 및 지휘 결심은 지휘관/실무자가 수행한다."
|
||||
)
|
||||
constraints_html = (
|
||||
f"<h4>부대 보안 및 개인정보 보호</h4>"
|
||||
f"<ul>"
|
||||
f"<li><p>실제 부대 편성, 위치, 작전 일정, 군번·성명 등 민감 군사보안 및 개인식별정보는 절대 외부 AI에 입력하지 않는다.</p></li>"
|
||||
f"<li><p>모든 실습에는 제공된 교육용 가상 데이터셋 및 시뮬레이션 표본만을 활용한다.</p></li>"
|
||||
f"</ul>"
|
||||
f"<h4>AI 결과와 인간 최종 판단의 구분 (HITL 원칙)</h4>"
|
||||
f"<ul>"
|
||||
f"<li><p>AI 모델이 제안한 분석값이나 탐지 결과는 참고 지표이며, 최종 승인·보류·교전·배치 명령은 사람이 확정한다.</p></li>"
|
||||
f"<li><p>{precautions_text}</p></li>"
|
||||
f"</ul>"
|
||||
)
|
||||
|
||||
eval_summary_html = (
|
||||
f"<h4>공통 PASS/FAIL 판정 원칙</h4>"
|
||||
f"<ul>"
|
||||
f"<li><p><strong>수행 증거:</strong> 요구된 데이터 전처리, 모델 구현, 검증 지표 산출, HITL 워크플로우 증거가 명확한가?</p></li>"
|
||||
f"<li><p><strong>판단 근거:</strong> 오경보/미탐 방지, 신뢰도 등급화, 파라미터 튜닝의 군사적·논리적 근거가 타당한가?</p></li>"
|
||||
f"<li><p><strong>앞뒤 연결성:</strong> 앞 미션의 스키마·전처리가 뒤 미션의 예측·대시보드·AAR 재보정에 정상 연계되는가?</p></li>"
|
||||
f"<li><p><strong>안전성:</strong> 보안 규정 준수, 극단 과부하 상황에서의 Fail-safe 복원력이 확보되었는가?</p></li>"
|
||||
f"</ul>"
|
||||
f"<h4>프로젝트 완료 조건</h4>"
|
||||
f"<p>10개 미션 전체에서 요구되는 필수([필수]) 평가 기준을 모두 PASS해야 프로젝트 최종 PASS로 판정한다.</p>"
|
||||
)
|
||||
|
||||
# Project Skills
|
||||
project_skills = [
|
||||
{
|
||||
"skillId": f"competency_군특화문제해결_{category}_문제분석",
|
||||
"level": level,
|
||||
"category": "군특화문제해결",
|
||||
"subCategory": f"{category} 문제분석"
|
||||
},
|
||||
{
|
||||
"skillId": f"competency_군특화문제해결_{category}_대안도출",
|
||||
"level": max(1, level - 1),
|
||||
"category": "군특화문제해결",
|
||||
"subCategory": f"{category} 대안도출"
|
||||
},
|
||||
{
|
||||
"skillId": "competency_데이터분석검증_결과검증",
|
||||
"level": level,
|
||||
"category": "데이터분석검증",
|
||||
"subCategory": "AI 결과 검증"
|
||||
},
|
||||
{
|
||||
"skillId": "competency_AI활용역량_도구활용",
|
||||
"level": max(1, level - 1),
|
||||
"category": "AI활용역량",
|
||||
"subCategory": "AI 도구 및 모델 활용"
|
||||
},
|
||||
{
|
||||
"skillId": "competency_전술의사결정_인간검토",
|
||||
"level": level,
|
||||
"category": "전술의사결정",
|
||||
"subCategory": "HITL 인간 검토 및 책임분계"
|
||||
}
|
||||
]
|
||||
|
||||
# Build 10 Missions
|
||||
missions_list = []
|
||||
for seq in range(1, 11):
|
||||
m_meta = missions_meta.get((pid, seq), {})
|
||||
m_scen = scen_data.get('missions', {}).get(seq, {})
|
||||
m_guide = guide_data.get('missions', {}).get(seq, {})
|
||||
|
||||
m_id = f"{pid}_M{seq:02d}"
|
||||
m_title = m_meta.get('미션명', m_guide.get('title', f"미션 {seq}"))
|
||||
|
||||
try:
|
||||
m_duration = float(m_meta.get('예상 소요시간(h)', '2.5'))
|
||||
except ValueError:
|
||||
m_duration = 2.5
|
||||
|
||||
m_desc = m_meta.get('미션 설명', m_scen.get('target', ''))
|
||||
m_scen_target = m_scen.get('target', m_desc)
|
||||
m_scen_exec = m_scen.get('exec', m_desc)
|
||||
m_quant_eval = m_meta.get('정량적 평가', '')
|
||||
m_qual_eval = m_meta.get('정성적 평가', '')
|
||||
m_track = m_meta.get('진행 추적 및 완료 기준', '')
|
||||
m_verify = m_meta.get('검증 방법', '')
|
||||
m_feedback = m_meta.get('개선 및 피드백 방안', '')
|
||||
m_deliverables_raw = m_meta.get('산출물 및 저장 위치', m_scen.get('deliverable', ''))
|
||||
|
||||
# Overview 매핑: 미션 - '목표'
|
||||
m_overview = f"<p>{m_scen_target}</p>"
|
||||
|
||||
# Objectives 매핑: 미션 - '수행'
|
||||
m_objectives = f"<p>{m_scen_exec}</p>"
|
||||
|
||||
# Guide Steps (미리 빌드하여 scenario 연계)
|
||||
g_steps = build_guide_steps(seq, m_meta, m_scen, m_guide)
|
||||
step_count = len(g_steps)
|
||||
|
||||
# Scenario (Why 스토리 2~3문장 + objectives 간략 정리 + 실습 흐름 + 최종 산출물)
|
||||
desc_clean = clean_html_tags(m_desc).strip()
|
||||
target_clean = clean_html_tags(m_scen_target).strip()
|
||||
exec_clean = clean_html_tags(m_scen_exec).strip()
|
||||
deliv_clean = clean_html_tags(m_deliverables_raw).strip()
|
||||
|
||||
if desc_clean and not desc_clean.endswith('.'):
|
||||
desc_clean += '.'
|
||||
|
||||
# objectives 수행 내용을 자연스러운 1개의 완결된 행동 문구로 정제
|
||||
exec_phrase = format_exec_action_phrase(exec_clean, m_title)
|
||||
|
||||
m_scenario = (
|
||||
f"<h4>미션 배경 및 당위성</h4>"
|
||||
f"<p>{desc_clean} 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, "
|
||||
f"<strong>{target_clean}</strong> 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.</p>"
|
||||
f"<p>이 문제를 해결하기 위해 본 미션에서는 <strong>{exec_phrase}</strong> 실습을 체계적으로 수행합니다.</p>"
|
||||
f"<p>학습자는 실습가이드에 제시된 총 {step_count}개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.</p>"
|
||||
f"<p><strong>이번 결과물:</strong> <code>{deliv_clean}</code> 형태의 결과물을 완성한다.</p>"
|
||||
)
|
||||
|
||||
# Deliverables list
|
||||
m_deliv_list = []
|
||||
for d in clean_html_tags(m_deliverables_raw).split(' 및 '):
|
||||
d = d.strip()
|
||||
if d:
|
||||
m_deliv_list.append(f"{d} (필수)")
|
||||
if not m_deliv_list:
|
||||
m_deliv_list = [f"M{seq} 산출물 및 검증 결과 (필수)"]
|
||||
|
||||
# Tech Stack
|
||||
tech_words = [category, p_meta.get('학습 구현/수행 유형', '코딩형'), "데이터 전처리", "알고리즘 구현", "HITL 검증"]
|
||||
|
||||
# Evaluation Keys
|
||||
eval_keys = [
|
||||
(
|
||||
f"<h4>평가 질문 (PASS/FAIL)</h4>"
|
||||
f"<p><strong>[{clean_html_tags(m_title)}] 요구된 핵심 성능 기준({clean_html_tags(m_quant_eval)})을 완벽히 달성하였는가?</strong></p>"
|
||||
f"<h4>PASS 증거</h4>"
|
||||
f"<p>{clean_html_tags(m_quant_eval)} 기준을 충족하고 {clean_html_tags(m_track)}을 만족하는 실행 결과 및 로그가 제출되었다.</p>"
|
||||
f"<h4>보완이 필요한 상태</h4>"
|
||||
f"<p>정량적 지표가 기준치에 미달하거나 실행 로그에서 예외/결측 오류가 발생한 경우.</p>"
|
||||
f"<h4>보완 방법</h4>"
|
||||
f"<p>{clean_html_tags(m_feedback)}을 적용하여 파라미터를 보정하고 단위 테스트를 재수행한다.</p>"
|
||||
),
|
||||
(
|
||||
f"<h4>평가 질문 (PASS/FAIL)</h4>"
|
||||
f"<p><strong>[정성적 타당성] 군사적 현실성 및 {clean_html_tags(m_qual_eval)} 관점의 타당성이 확보되었는가?</strong></p>"
|
||||
f"<h4>PASS 증거</h4>"
|
||||
f"<p>{clean_html_tags(m_qual_eval)}이 논리적으로 검증되었으며 {clean_html_tags(m_verify)}를 통한 무결성이 확인되었다.</p>"
|
||||
f"<h4>보완이 필요한 상태</h4>"
|
||||
f"<p>비현실적인 가정을 적용했거나 오류 원인에 대한 정성적 설명 근거가 부족한 경우.</p>"
|
||||
f"<h4>보완 방법</h4>"
|
||||
f"<p>교차 검증 데이터를 보강하고 지휘관/실무자 관점의 해석 근거를 명시한다.</p>"
|
||||
)
|
||||
]
|
||||
|
||||
# Evaluation Text
|
||||
eval_text = (
|
||||
f"<h4>미션 판정</h4>"
|
||||
f"<p><strong>평가 구분:</strong> 필수</p>"
|
||||
f"<p><strong>핵심 평가영역:</strong> {clean_html_tags(m_qual_eval)}</p>"
|
||||
f"<h4>미션 PASS 조건</h4>"
|
||||
f"<ul>"
|
||||
f"<li><p>정량 기준: {clean_html_tags(m_quant_eval)}</p></li>"
|
||||
f"<li><p>진행 기준: {clean_html_tags(m_track)}</p></li>"
|
||||
f"<li><p>검증 방법: {clean_html_tags(m_verify)}</p></li>"
|
||||
f"</ul>"
|
||||
f"<h4>채점자 판정 메모</h4>"
|
||||
f"<blockquote><p>단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.</p></blockquote>"
|
||||
)
|
||||
|
||||
# Mission Object Assembly
|
||||
mission_obj = {
|
||||
"id": m_id,
|
||||
"sequence": seq,
|
||||
"title": m_title,
|
||||
"durationHours": m_duration,
|
||||
"overview": m_overview,
|
||||
"objectives": m_objectives,
|
||||
"scenario": m_scenario,
|
||||
"attachments": [],
|
||||
"materials": [
|
||||
f"/uploads/{pid}/{pid.lower()}_m{seq:02d}_dataset.json",
|
||||
f"/uploads/{pid}/{pid.lower()}_m{seq:02d}_sample.md"
|
||||
],
|
||||
"materialText": (
|
||||
f"<p><strong>미션 실습 자료</strong></p>"
|
||||
f"<ul>"
|
||||
f"<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"/uploads/{pid}/{pid.lower()}_m{seq:02d}_dataset.json\">가상 데이터셋 샘플 (JSON)</a></li>"
|
||||
f"<li><a target=\"_blank\" rel=\"noopener noreferrer\" href=\"/uploads/{pid}/{pid.lower()}_m{seq:02d}_sample.md\">실습 및 검증 가이드 (MD)</a></li>"
|
||||
f"</ul>"
|
||||
f"<p><strong>활용 주의</strong>: 제공된 교육용 가상 데이터만 사용하며 실제 군사 비밀 및 개인정보는 입력하지 않습니다.</p>"
|
||||
),
|
||||
"techStack": tech_words,
|
||||
"deliverables": m_deliv_list,
|
||||
"evaluationKeys": eval_keys,
|
||||
"evaluationTypes": ["REQUIRED", "REQUIRED"],
|
||||
"evaluationText": eval_text,
|
||||
"aiGuidelines": {
|
||||
"allow": [
|
||||
"데이터 전처리 알고리즘 및 정규화 서식 추천받기",
|
||||
"이상치 탐지 및 모델 파라미터 튜닝 아이디어 제안받기",
|
||||
"대시보드 UI 컴포넌트 코드 및 시각화 서식 생성하기"
|
||||
],
|
||||
"rules": [
|
||||
"최종 판정 및 지휘 결심은 학습자가 직접 수행한다.",
|
||||
"실제 군사 비밀이나 개인 식별 정보는 입력하지 않는다.",
|
||||
"AI의 출력 결과를 맹신하지 않고 반드시 단위 테스트로 검증한다."
|
||||
],
|
||||
"forbid": [
|
||||
"실제 군사 작전/인사 비밀 데이터를 AI에 입력하기",
|
||||
"검증 없이 AI가 생성한 코드를 그대로 제출하기",
|
||||
"단정적인 자동 결심 결과를 인간 검토 없이 확정하기"
|
||||
]
|
||||
},
|
||||
"guideSteps": g_steps
|
||||
}
|
||||
missions_list.append(mission_obj)
|
||||
|
||||
project_json = {
|
||||
"version": "1.0.0",
|
||||
"exportedAt": "2026-10-08T00:00:00.000Z",
|
||||
"id": pid,
|
||||
"title": title,
|
||||
"category": category,
|
||||
"level": level,
|
||||
"durationDays": 5,
|
||||
"requiredHours": req_hours,
|
||||
"status": "COMPLETED",
|
||||
"isRecommended": False,
|
||||
"summary": "",
|
||||
"objectives": objectives_html,
|
||||
"deliverables": p_deliverables,
|
||||
"constraints": constraints_html,
|
||||
"evalSummary": eval_summary_html,
|
||||
"prerequisites": f"기본 프로그래밍 지식 및 {category} 데이터 분석 기초",
|
||||
"refFiles": [],
|
||||
"refText": "",
|
||||
"projectSkills": project_skills,
|
||||
"prerequisiteVods": [],
|
||||
"missions": missions_list
|
||||
}
|
||||
|
||||
return project_json
|
||||
|
||||
def run_conversion(target_project_id=None):
|
||||
"""Main function to convert md files to json (supports single project or all)."""
|
||||
output_dir = Path("scenario_json")
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
projects_meta, missions_meta = load_metadata()
|
||||
print(f"Loaded metadata for {len(projects_meta)} projects and {len(missions_meta)} missions.")
|
||||
|
||||
md_dirs = [d for d in os.listdir('scenario_md') if os.path.isdir(os.path.join('scenario_md', d))]
|
||||
print(f"Found {len(md_dirs)} project directories in scenario_md.")
|
||||
|
||||
if target_project_id:
|
||||
print(f"Filtering for single project: {target_project_id}")
|
||||
|
||||
success_count = 0
|
||||
error_count = 0
|
||||
|
||||
for d in md_dirs:
|
||||
m = re.match(r'^([A-Z0-9]+)-', d)
|
||||
if not m:
|
||||
continue
|
||||
pid = m.group(1)
|
||||
if target_project_id and pid != target_project_id:
|
||||
continue
|
||||
|
||||
dir_path = os.path.join('scenario_md', d)
|
||||
|
||||
# Locate files
|
||||
files = os.listdir(dir_path)
|
||||
main_scen_file = None
|
||||
guide_file = None
|
||||
|
||||
for f in files:
|
||||
if f.endswith('.md'):
|
||||
if '실습가이드' in f:
|
||||
guide_file = os.path.join(dir_path, f)
|
||||
elif '코드' not in f and '실습자료' not in f:
|
||||
main_scen_file = os.path.join(dir_path, f)
|
||||
|
||||
if not main_scen_file:
|
||||
for f in files:
|
||||
if f.endswith('.md') and '실습가이드' not in f and '코드' not in f:
|
||||
main_scen_file = os.path.join(dir_path, f)
|
||||
break
|
||||
|
||||
scen_data = parse_scenario_md(main_scen_file) if main_scen_file else {}
|
||||
guide_data = parse_guide_md(guide_file) if guide_file else {}
|
||||
p_meta = projects_meta.get(pid, {})
|
||||
|
||||
try:
|
||||
p_json = build_project_json(pid, p_meta, scen_data, guide_data, missions_meta)
|
||||
out_file = output_dir / f"{pid}.json"
|
||||
with open(out_file, 'w', encoding='utf-8') as fp:
|
||||
json.dump(p_json, fp, ensure_ascii=False, indent=2)
|
||||
print(f" [SUCCESS] Generated {out_file.name}")
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
print(f" [ERROR] Failed to build JSON for {pid}: {e}")
|
||||
error_count += 1
|
||||
|
||||
print("=" * 60)
|
||||
print("Conversion Completed:")
|
||||
print(f" Successfully generated: {success_count} project JSON files")
|
||||
print(f" Errors: {error_count}")
|
||||
print(f" Output directory: {output_dir.resolve()}")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
run_conversion(target)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
code_path = 'scenario_md/OPS01-작전·전투 수행_적 탐지-드론 영상 공중위협 객체 탐지와 오경보 검증/OPS01_정답코드.md'
|
||||
with open(code_path, encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
parts = re.split(r'##\s+미션\s+(\d+)\.\s*', text)
|
||||
|
||||
missions = []
|
||||
for i in range(1, len(parts), 2):
|
||||
m_num = int(parts[i])
|
||||
m_body = parts[i+1]
|
||||
|
||||
lines = m_body.strip().split('\n')
|
||||
m_title = lines[0].strip()
|
||||
|
||||
code_matches = re.findall(r'```python\n(.*?)\n```', m_body, re.DOTALL)
|
||||
output_matches = re.findall(r'```text\n(.*?)\n```', m_body, re.DOTALL)
|
||||
|
||||
missions.append({
|
||||
'num': m_num,
|
||||
'title': m_title,
|
||||
'code': code_matches[0] if code_matches else '',
|
||||
'output': output_matches[0] if output_matches else ''
|
||||
})
|
||||
|
||||
print(f"Parsed {len(missions)} missions.")
|
||||
for m in missions:
|
||||
print(f"--- Mission {m['num']}: {m['title']} ---")
|
||||
print("Code lines:", len(m['code'].split('\n')))
|
||||
print("Sample lines of code:\n" + '\n'.join(m['code'].split('\n')[:8]))
|
||||
print()
|
||||
@@ -0,0 +1,21 @@
|
||||
import json
|
||||
import traceback
|
||||
|
||||
with open('uploads/OPS01/OPS01.ipynb', encoding='utf-8') as f:
|
||||
nb = json.load(f)
|
||||
|
||||
print("=== Executing OPS01.ipynb cells sequentially ===")
|
||||
global_scope = {}
|
||||
|
||||
for i, cell in enumerate(nb['cells']):
|
||||
if cell['cell_type'] == 'code':
|
||||
code_src = ''.join(cell['source'])
|
||||
print(f"\n--- Running Code Cell {i} ---")
|
||||
try:
|
||||
exec(code_src, global_scope)
|
||||
print(" [SUCCESS]")
|
||||
except Exception as e:
|
||||
print(f" [FAILED in cell {i}]: {e}")
|
||||
traceback.print_exc()
|
||||
break
|
||||
print("\n=== All code cells executed successfully! ===")
|
||||
@@ -0,0 +1,454 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Full generation script for OPS01.ipynb and OPS01_student.ipynb
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
def clean_html(text):
|
||||
if not text:
|
||||
return ""
|
||||
text = re.sub(r'<p>', '', text)
|
||||
text = re.sub(r'</p>', '\n\n', text)
|
||||
text = re.sub(r'<h4>', '#### ', text)
|
||||
text = re.sub(r'</h4>', '\n\n', text)
|
||||
text = re.sub(r'<ul>', '', text)
|
||||
text = re.sub(r'</ul>', '', text)
|
||||
text = re.sub(r'<li>', '- ', text)
|
||||
text = re.sub(r'</li>', '\n', text)
|
||||
text = re.sub(r'<strong>', '**', text)
|
||||
text = re.sub(r'</strong>', '**', text)
|
||||
text = re.sub(r'<code>', '`', text)
|
||||
text = re.sub(r'</code>', '`', text)
|
||||
text = re.sub(r'<blockquote>', '> ', text)
|
||||
text = re.sub(r'</blockquote>', '\n', text)
|
||||
return text.strip()
|
||||
|
||||
def create_student_code(m_num, full_code):
|
||||
"""Create student exercise code with TODO blanks and clear hints"""
|
||||
if m_num == 1:
|
||||
# Blank out check and validate
|
||||
target = '''def check(name, value, rule):
|
||||
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
|
||||
if not isinstance(value, rule["type"]) or isinstance(value, bool):
|
||||
return [f"{name}: 자료형 오류({type(value).__name__})"]
|
||||
errs = []
|
||||
if "enum" in rule and value not in rule["enum"]:
|
||||
errs.append(f"{name}: 허용되지 않은 값 '{value}'")
|
||||
if "min" in rule and value < rule["min"]:
|
||||
errs.append(f"{name}: 최소값 {rule['min']} 미만")
|
||||
if "max" in rule and value > rule["max"]:
|
||||
errs.append(f"{name}: 최대값 {rule['max']} 초과")
|
||||
if "length" in rule and len(value) != rule["length"]:
|
||||
errs.append(f"{name}: 길이 {rule['length']} 필요")
|
||||
if "pattern" in rule and not re.match(rule["pattern"], value):
|
||||
errs.append(f"{name}: 형식 불일치 '{value}'")
|
||||
return errs'''
|
||||
|
||||
replacement = '''def check(name, value, rule):
|
||||
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
|
||||
# [TODO : 학생 실습 1-1] 자료형(type) 일치 여부 확인 (bool 예외 처리 포함)
|
||||
if not isinstance(value, rule["type"]) or isinstance(value, bool):
|
||||
return [f"{name}: 자료형 오류({type(value).__name__})"]
|
||||
|
||||
errs = []
|
||||
# [TODO : 학생 실습 1-2] rule의 enum, min, max, length, pattern 검증 로직을 작성하세요.
|
||||
# 힌트:
|
||||
# 1) 'enum' in rule and value not in rule['enum'] -> f"{name}: 허용되지 않은 값 '{value}'"
|
||||
# 2) 'min' in rule and value < rule['min'] -> f"{name}: 최소값 {rule['min']} 미만"
|
||||
# 3) 'max' in rule and value > rule['max'] -> f"{name}: 최대값 {rule['max']} 초과"
|
||||
# 4) 'length' in rule and len(value) != rule['length'] -> f"{name}: 길이 {rule['length']} 필요"
|
||||
# 5) 'pattern' in rule and not re.match(rule['pattern'], value) -> f"{name}: 형식 불일치 '{value}'"
|
||||
|
||||
# --- 코드 작성 영역 ---
|
||||
if "enum" in rule and value not in rule["enum"]:
|
||||
errs.append(f"{name}: 허용되지 않은 값 '{value}'")
|
||||
if "min" in rule and value < rule["min"]:
|
||||
errs.append(f"{name}: 최소값 {rule['min']} 미만")
|
||||
if "max" in rule and value > rule["max"]:
|
||||
errs.append(f"{name}: 최대값 {rule['max']} 초과")
|
||||
if "length" in rule and len(value) != rule["length"]:
|
||||
errs.append(f"{name}: 길이 {rule['length']} 필요")
|
||||
if "pattern" in rule and not re.match(rule["pattern"], value):
|
||||
errs.append(f"{name}: 형식 불일치 '{value}'")
|
||||
# ----------------------
|
||||
return errs'''
|
||||
# Let's replace only the inside logic for student
|
||||
student_replacement = '''def check(name, value, rule):
|
||||
"""값 하나를 규칙(type, enum, min, max, length, pattern)으로 검사해 오류 목록을 돌려준다."""
|
||||
# [TODO : 학생 실습 1-1] 자료형(type) 일치 여부 확인 (bool 예외 처리 포함)
|
||||
if not isinstance(value, rule["type"]) or isinstance(value, bool):
|
||||
return [f"{name}: 자료형 오류({type(value).__name__})"]
|
||||
|
||||
errs = []
|
||||
# [TODO : 학생 실습 1-2] rule의 enum, min, max, length, pattern 규칙을 검사하여 errs 리스트에 오류 메시지를 추가하세요.
|
||||
# 힌트:
|
||||
# 1) 'enum': value가 rule['enum'] 리스트에 없는 경우 -> f"{name}: 허용되지 않은 값 '{value}'"
|
||||
# 2) 'min': value가 rule['min'] 미만인 경우 -> f"{name}: 최소값 {rule['min']} 미만"
|
||||
# 3) 'max': value가 rule['max'] 초과인 경우 -> f"{name}: 최대값 {rule['max']} 초과"
|
||||
# 4) 'length': len(value)가 rule['length']와 다른 경우 -> f"{name}: 길이 {rule['length']} 필요"
|
||||
# 5) 'pattern': re.match(rule['pattern'], value)가 False인 경우 -> f"{name}: 형식 불일치 '{value}'"
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
pass # 작성 후 pass를 제거하고 완성하세요.
|
||||
return errs'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 2:
|
||||
# Preprocessing pipeline
|
||||
target = '''def preprocess(raw, weather):
|
||||
"""전처리 파이프라인: 기상에 따라 노이즈 제거와 대비 향상을 적용한다."""
|
||||
img = raw.astype(float)
|
||||
if weather == "fog":
|
||||
# 안개: 저주파 배경을 빼서 대비를 높이고 가우시안 필터로 매끄럽게 만든다.
|
||||
bg = ndimage.gaussian_filter(img, sigma=8.0)
|
||||
img = img - bg
|
||||
img = np.clip(img * 2.5 + 128, 0, 255)
|
||||
img = ndimage.gaussian_filter(img, sigma=0.8)
|
||||
elif weather == "night":
|
||||
# 야간: 센서 노이즈가 심하므로 미디언 필터로 점 잡음을 없앤다.
|
||||
img = ndimage.median_filter(img, size=3)
|
||||
img = np.clip((img - img.min()) / (img.max() - img.min() + 1e-6) * 255, 0, 255)
|
||||
elif weather == "rain":
|
||||
# 비: 줄무늬 잡음을 가우시안 필터로 줄인다.
|
||||
img = ndimage.gaussian_filter(img, sigma=1.2)
|
||||
img = np.clip(img * 1.5, 0, 255)
|
||||
elif weather == "smoke":
|
||||
# 연막: 산란 잡음을 미디언 필터와 국소 대비 향상으로 완화한다.
|
||||
img = ndimage.median_filter(img, size=3)
|
||||
bg = ndimage.gaussian_filter(img, sigma=4.0)
|
||||
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
|
||||
else:
|
||||
# 맑음: 최소한의 스케일링만 적용한다.
|
||||
img = np.clip(img, 0, 255)
|
||||
return img.astype(np.uint8)'''
|
||||
|
||||
student_replacement = '''def preprocess(raw, weather):
|
||||
"""전처리 파이프라인: 기상에 따라 노이즈 제거와 대비 향상을 적용한다."""
|
||||
img = raw.astype(float)
|
||||
# [TODO : 학생 실습 2] 기상 조건(fog, night, rain, smoke, clear)에 따른 적응형 영상 전처리 로직을 완성하세요.
|
||||
# 힌트:
|
||||
# - "fog" (안개): 저주파 배경(ndimage.gaussian_filter sigma=8.0) 감산 후 대비 증폭(np.clip(img*2.5 + 128, 0, 255))
|
||||
# - "night" (야간): 점잡음 제거를 위해 ndimage.median_filter(img, size=3) 적용 및 Min-Max 정규화
|
||||
# - "rain" (강우): 가우시안 필터(sigma=1.2) 및 스케일링
|
||||
# - "smoke" (연막): 미디언 필터(size=3) 후 국소 배경 감산(sigma=4.0)
|
||||
# - "clear" (맑음): np.clip(img, 0, 255)
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
pass # 작성 후 pass를 제거하고 완성하세요.
|
||||
return img.astype(np.uint8)'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 3:
|
||||
# Target iou and detect
|
||||
target = '''def iou(box_a, box_b):
|
||||
"""두 바운딩 박스(x, y, w, h)의 IoU(교집합/합집합)를 계산한다."""
|
||||
ax1, ay1, ax2, ay2 = box_a[0], box_a[1], box_a[0] + box_a[2], box_a[1] + box_a[3]
|
||||
bx1, by1, bx2, by2 = box_b[0], box_b[1], box_b[0] + box_b[2], box_b[1] + box_b[3]
|
||||
ix1, iy1 = max(ax1, bx1), max(ay1, by1)
|
||||
ix2, iy2 = min(ax2, bx2), min(ay2, by2)
|
||||
iw, ih = max(0, ix2 - ix1), max(0, iy2 - iy1)
|
||||
inter = iw * ih
|
||||
union = box_a[2] * box_a[3] + box_b[2] * box_b[3] - inter
|
||||
return inter / union if union > 0 else 0.0'''
|
||||
|
||||
student_replacement = '''def iou(box_a, box_b):
|
||||
"""두 바운딩 박스(x, y, w, h)의 IoU(교집합/합집합)를 계산한다."""
|
||||
# [TODO : 학생 실습 3] 바운딩 박스 간의 IoU(Intersection over Union) 계산 수식을 완성하세요.
|
||||
# 힌트:
|
||||
# 1) box_a = (x, y, w, h) -> (ax1, ay1, ax2, ay2)
|
||||
# 2) 교집합 영역 (ix1, iy1), (ix2, iy2) 구하기: ix1=max(ax1, bx1), ix2=min(ax2, bx2)...
|
||||
# 3) iw = max(0, ix2 - ix1), ih = max(0, iy2 - iy1), inter = iw * ih
|
||||
# 4) union = area_a + area_b - inter
|
||||
# 5) inter / union (union > 0일 때) 반환
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
return 0.0 # 작성 후 완성하세요.'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 4:
|
||||
# Pattern classification
|
||||
target = '''def classify_motion(track):
|
||||
"""항적의 위치·속도 변화를 분석해 비행 패턴을 판정한다."""
|
||||
pts = track["points"]
|
||||
if len(pts) < 3:
|
||||
return "too_short"
|
||||
dx = np.diff([p[0] for p in pts])
|
||||
dy = np.diff([p[1] for p in pts])
|
||||
angles = np.arctan2(dy, dx)
|
||||
angle_changes = np.abs(np.diff(angles))
|
||||
# 각도 변화가 180도를 넘어가면 반대 방향으로 회전한 것이므로 보정
|
||||
angle_changes = np.where(angle_changes > np.pi, 2 * np.pi - angle_changes, angle_changes)
|
||||
mean_turn = float(np.mean(angle_changes))
|
||||
speed = float(track["speed"])
|
||||
# 1) 급격한 방향 전환(지그재그) -> 조류 오경보
|
||||
if mean_turn > 0.8:
|
||||
return "bird_erratic"
|
||||
# 2) 고속 + 직선 비행 -> 순항미사일
|
||||
if speed >= 25.0 and mean_turn < 0.2:
|
||||
return "cruise_missile_straight"
|
||||
# 3) 완만한 회전 비행 -> 정찰 드론
|
||||
if 0.2 <= mean_turn <= 0.8:
|
||||
return "recon_drone_loitering"
|
||||
# 4) 중간 속도 직선 비행 -> 자폭 드론
|
||||
if speed >= 10.0 and mean_turn < 0.2:
|
||||
return "attack_drone_straight"
|
||||
return "unknown"'''
|
||||
|
||||
student_replacement = '''def classify_motion(track):
|
||||
"""항적의 위치·속도 변화를 분석해 비행 패턴을 판정한다."""
|
||||
pts = track["points"]
|
||||
if len(pts) < 3:
|
||||
return "too_short"
|
||||
dx = np.diff([p[0] for p in pts])
|
||||
dy = np.diff([p[1] for p in pts])
|
||||
angles = np.arctan2(dy, dx)
|
||||
angle_changes = np.abs(np.diff(angles))
|
||||
angle_changes = np.where(angle_changes > np.pi, 2 * np.pi - angle_changes, angle_changes)
|
||||
mean_turn = float(np.mean(angle_changes))
|
||||
speed = float(track["speed"])
|
||||
|
||||
# [TODO : 학생 실습 4] 비행 궤적의 회전각(mean_turn)과 비행속도(speed)를 바탕으로 비행 패턴을 분류하세요.
|
||||
# 1) 조류 (bird_erratic): mean_turn > 0.8 (급격한 지그재그 회전)
|
||||
# 2) 순항미사일 (cruise_missile_straight): speed >= 25.0 및 mean_turn < 0.2 (초고속 직선)
|
||||
# 3) 정찰드론 (recon_drone_loitering): 0.2 <= mean_turn <= 0.8 (선회 비행)
|
||||
# 4) 자폭드론 (attack_drone_straight): speed >= 10.0 및 mean_turn < 0.2 (중속 직선)
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
return "unknown" # 작성 후 완성하세요.'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 5:
|
||||
# Confidence score features and grades
|
||||
target = '''def extract_features(raw, bboxes):
|
||||
"""각 바운딩 박스에서 크기, 밝기 통계, 주변 대비를 추출한다."""
|
||||
feats = []
|
||||
for b in bboxes:
|
||||
x, y, w, h = b
|
||||
crop = raw[y:y+h, x:x+w]
|
||||
if crop.size == 0:
|
||||
feats.append([0, 0, 0, 0, 0])
|
||||
continue
|
||||
area = float(w * h)
|
||||
mean_val = float(crop.mean())
|
||||
max_val = float(crop.max())
|
||||
std_val = float(crop.std())
|
||||
# 주변 영역(1화소 확장)과의 대비
|
||||
y0, y1 = max(0, y - 2), min(raw.shape[0], y + h + 2)
|
||||
x0, x1 = max(0, x - 2), min(raw.shape[1], x + w + 2)
|
||||
surr = raw[y0:y1, x0:x1]
|
||||
contrast = float(mean_val - surr.mean())
|
||||
feats.append([area, mean_val, max_val, std_val, contrast])
|
||||
return np.array(feats)'''
|
||||
|
||||
student_replacement = '''def extract_features(raw, bboxes):
|
||||
"""각 바운딩 박스에서 크기, 밝기 통계, 주변 대비를 추출한다."""
|
||||
feats = []
|
||||
# [TODO : 학생 실습 5] 바운딩 박스 패치로부터 특징 벡터 [면적, 평균밝기, 최대밝기, 표준편차, 주변대비]를 추출하세요.
|
||||
# 힌트:
|
||||
# - crop = raw[y:y+h, x:x+w]
|
||||
# - area = w * h, mean_val = crop.mean(), max_val = crop.max(), std_val = crop.std()
|
||||
# - contrast = mean_val - surround_mean
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
for b in bboxes:
|
||||
feats.append([0.0, 0.0, 0.0, 0.0, 0.0]) # 작성 후 실제 추출 로직으로 대체
|
||||
return np.array(feats)'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 6:
|
||||
# Extrapolate impact
|
||||
target = '''# 예상 탄착점(3초 뒤 위치): 속도가 유지된다고 가정하고 외삽
|
||||
t_impact = 3.0
|
||||
tracks["impact_x"] = tracks["x"] + tracks["vx"] * t_impact
|
||||
tracks["impact_y"] = tracks["y"] + tracks["vy"] * t_impact
|
||||
|
||||
# 방어구역 침범 여부: 반경 2km 이내로 들어오는지 계산
|
||||
def check_breach(row):
|
||||
dist_now = np.sqrt(row["x"] ** 2 + row["y"] ** 2)
|
||||
dist_future = np.sqrt(row["impact_x"] ** 2 + row["impact_y"] ** 2)
|
||||
return "침범 경보" if (dist_now < 2.0 or dist_future < 2.0) else "정상 비행"'''
|
||||
|
||||
student_replacement = '''# [TODO : 학생 실습 6-1] 3초 뒤 예상 탄착점(impact_x, impact_y)을 선형 외삽 공식으로 계산하세요.
|
||||
# 힌트: 미래 위치 = 현재 위치 + 속도 * 시간(t_impact)
|
||||
t_impact = 3.0
|
||||
# <<< 코드 작성 >>>
|
||||
tracks["impact_x"] = tracks["x"] + tracks["vx"] * t_impact
|
||||
tracks["impact_y"] = tracks["y"] + tracks["vy"] * t_impact
|
||||
|
||||
# [TODO : 학생 실습 6-2] 현재 위치 또는 예상 탄착점이 방어구역(반경 2.0km) 이내인지 검사하는 함수를 완성하세요.
|
||||
def check_breach(row):
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
dist_now = np.sqrt(row["x"] ** 2 + row["y"] ** 2)
|
||||
dist_future = np.sqrt(row["impact_x"] ** 2 + row["impact_y"] ** 2)
|
||||
return "침범 경보" if (dist_now < 2.0 or dist_future < 2.0) else "정상 비행"'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 7:
|
||||
# HITL workflow
|
||||
target = '''def decide(item, operator_id):
|
||||
"""운용관의 판정에 따라 서명을 만들고 감사 로그 레코드를 돌려준다."""
|
||||
conf = item["confidence"]
|
||||
grade = item["grade"]
|
||||
# 고신뢰도 위협은 즉시 승인 권고, 저신뢰도는 반려 권고
|
||||
if grade == "High":
|
||||
action = "APPROVE"
|
||||
reason = f"고신뢰도({conf:.2f}) 위협, 방어구역 진입 확인"
|
||||
elif grade == "Medium":
|
||||
action = "HOLD"
|
||||
reason = f"중간신뢰도({conf:.2f}), 추가 관측(3프레임) 필요"
|
||||
else:
|
||||
action = "REJECT"
|
||||
reason = f"저신뢰도({conf:.2f}), 오경보(조류/반사체)로 판정"
|
||||
sig = make_signature(item["track_id"], operator_id, action)
|
||||
return {
|
||||
"track_id": item["track_id"],
|
||||
"class": item["class"],
|
||||
"confidence": conf,
|
||||
"grade": grade,
|
||||
"operator": operator_id,
|
||||
"action": action,
|
||||
"action_desc": ACTIONS[action],
|
||||
"reason": reason,
|
||||
"signature": sig,
|
||||
"timestamp": time.time(),
|
||||
}'''
|
||||
|
||||
student_replacement = '''def decide(item, operator_id):
|
||||
"""운용관의 판정에 따라 서명을 만들고 감사 로그 레코드를 돌려준다."""
|
||||
conf = item["confidence"]
|
||||
grade = item["grade"]
|
||||
# [TODO : 학생 실습 7] 위협 등급(grade)에 따른 HITL 의사결정 규칙을 완성하세요.
|
||||
# 1) High -> action: 'APPROVE', reason: '고신뢰도({conf:.2f}) 위협, 방어구역 진입 확인'
|
||||
# 2) Medium -> action: 'HOLD', reason: '중간신뢰도({conf:.2f}), 추가 관측(3프레임) 필요'
|
||||
# 3) Low (기타) -> action: 'REJECT', reason: '저신뢰도({conf:.2f}), 오경보(조류/반사체)로 판정'
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
action = "HOLD"
|
||||
reason = "검토 중"
|
||||
sig = make_signature(item["track_id"], operator_id, action)
|
||||
return {
|
||||
"track_id": item["track_id"],
|
||||
"class": item["class"],
|
||||
"confidence": conf,
|
||||
"grade": grade,
|
||||
"operator": operator_id,
|
||||
"action": action,
|
||||
"action_desc": ACTIONS.get(action, ""),
|
||||
"reason": reason,
|
||||
"signature": sig,
|
||||
"timestamp": time.time(),
|
||||
}'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 8:
|
||||
# Stress test failsafe
|
||||
target = '''# 3) Fail-safe 복원력 시험: 과부하(120건) 시 우선순위(중요도) 기반 처리
|
||||
# 지휘관 지침: 자폭드론/순항미사일 우선, 조류/반사체 후순위, 고신뢰도 우선
|
||||
overload_n = 120
|
||||
rng_test = np.random.default_rng(99)
|
||||
targets = [{"id": f"T{i:03d}",
|
||||
"class": rng_test.choice(["recon_drone", "attack_drone", "cruise_missile", "bird", "clutter"]),
|
||||
"conf": float(rng_test.uniform(0.3, 0.99)),
|
||||
"dist_km": float(rng_test.uniform(0.5, 5.0))} for i in range(overload_n)]
|
||||
|
||||
# 우선순위 점수: 위협 클래스 가중치 + 신뢰도 + 거리 역수
|
||||
weight = {"cruise_missile": 3.0, "attack_drone": 2.5, "recon_drone": 1.5, "bird": 0.1, "clutter": 0.0}
|
||||
for t in targets:
|
||||
t["priority"] = weight[t["class"]] * 0.5 + t["conf"] * 0.3 + (5.0 - t["dist_km"]) / 5.0 * 0.2
|
||||
|
||||
# 처리 용량이 30건으로 제한된 경우 최우선 30건 선별
|
||||
capacity = 30
|
||||
selected = sorted(targets, key=lambda x: x["priority"], reverse=True)[:capacity]'''
|
||||
|
||||
student_replacement = '''# [TODO : 학생 실습 8] 과부하(120건 동시 유입) 상황에서 우선순위 가중치(위협도+신뢰도+거리)를 계산하고 상위 30건을 선별하세요.
|
||||
# 힌트:
|
||||
# 1) 위협 클래스 가중치: cruise_missile(3.0), attack_drone(2.5), recon_drone(1.5), bird(0.1), clutter(0.0)
|
||||
# 2) 우선순위 수식 = weight[class] * 0.5 + conf * 0.3 + (5.0 - dist_km)/5.0 * 0.2
|
||||
# 3) sorted(targets, key=lambda x: x['priority'], reverse=True)[:capacity]
|
||||
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
overload_n = 120
|
||||
rng_test = np.random.default_rng(99)
|
||||
targets = [{"id": f"T{i:03d}",
|
||||
"class": rng_test.choice(["recon_drone", "attack_drone", "cruise_missile", "bird", "clutter"]),
|
||||
"conf": float(rng_test.uniform(0.3, 0.99)),
|
||||
"dist_km": float(rng_test.uniform(0.5, 5.0))} for i in range(overload_n)]
|
||||
capacity = 30
|
||||
selected = [] # 작성 후 완성하세요.'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 9:
|
||||
# AAR Threshold Retuning
|
||||
target = '''# 3) 파라미터 재보정: k를 2.0에서 4.5까지 0.5 간격으로 바꾸며 최적값 탐색
|
||||
# 지휘관 기준: 실제 위협(드론) 탐지율 90% 이상 유지하면서, 오경보 건수를 최소화
|
||||
k_candidates = np.arange(2.0, 5.0, 0.5)
|
||||
retune_rows = []
|
||||
for k in k_candidates:
|
||||
rec = run_pipeline(k=k, p_high=0.75)
|
||||
retune_rows.append({"임계값(k)": k,
|
||||
"실제위협(건)": rec["tp"] + rec["fn"],
|
||||
"탐지성공(TP)": rec["tp"],
|
||||
"미탐(FN)": rec["fn"],
|
||||
"오경보(FP)": rec["fp"],
|
||||
"위협 탐지율(%)": round(rec["tp"] / (rec["tp"] + rec["fn"]) * 100, 1),
|
||||
"오경보율(%)": round(rec["fp"] / (rec["fp"] + rec["tn"]) * 100, 1),
|
||||
"지연(ms)": round(rec["latency_ms"], 2)})'''
|
||||
|
||||
student_replacement = '''# [TODO : 학생 실습 9] 임계값(k) 후보군(2.0 ~ 4.5)에 대해 파이프라인 성능(TP, FN, FP, 탐지율, 오경보율)을 평가하고 최적 파라미터를 도출하세요.
|
||||
# 힌트:
|
||||
# - 탐지율(Recall) = TP / (TP + FN) * 100
|
||||
# - 오경보율(FAR) = FP / (FP + TN) * 100
|
||||
|
||||
k_candidates = np.arange(2.0, 5.0, 0.5)
|
||||
retune_rows = []
|
||||
# <<< 여기에 코드를 작성하세요 >>>
|
||||
for k in k_candidates:
|
||||
pass # 작성 후 완성하세요.'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
elif m_num == 10:
|
||||
# Mission 10 integration
|
||||
target = '''# 파이프라인 단계별 함수
|
||||
def step_preprocess(raw, weather):
|
||||
img = raw.astype(float)
|
||||
if weather in ("fog", "smoke"):
|
||||
bg = ndimage.gaussian_filter(img, sigma=6.0)
|
||||
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
|
||||
elif weather == "night":
|
||||
img = ndimage.median_filter(img, size=3)
|
||||
return img.astype(np.uint8)'''
|
||||
|
||||
student_replacement = '''# [TODO : 학생 실습 10] 엔드투엔드 파이프라인의 전처리 및 통합 추론 흐름을 확인하고 실행하세요.
|
||||
def step_preprocess(raw, weather):
|
||||
img = raw.astype(float)
|
||||
if weather in ("fog", "smoke"):
|
||||
bg = ndimage.gaussian_filter(img, sigma=6.0)
|
||||
img = np.clip((img - bg) * 2.0 + 128, 0, 255)
|
||||
elif weather == "night":
|
||||
img = ndimage.median_filter(img, size=3)
|
||||
return img.astype(np.uint8)'''
|
||||
if target in full_code:
|
||||
return full_code.replace(target, student_replacement)
|
||||
return full_code
|
||||
|
||||
return full_code
|
||||
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Generator for OPS01.ipynb and OPS01_student.ipynb
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
def load_data():
|
||||
with open('scenario_json/OPS01.json', encoding='utf-8') as f:
|
||||
scenario = json.load(f)
|
||||
|
||||
with open('scenario_md/OPS01-작전·전투 수행_적 탐지-드론 영상 공중위협 객체 탐지와 오경보 검증/OPS01_정답코드.md', encoding='utf-8') as f:
|
||||
code_md = f.read()
|
||||
|
||||
parts = re.split(r'##\s+미션\s+(\d+)\.\s*', code_md)
|
||||
missions_code = {}
|
||||
for i in range(1, len(parts), 2):
|
||||
m_num = int(parts[i])
|
||||
m_body = parts[i+1]
|
||||
lines = m_body.strip().split('\n')
|
||||
m_title = lines[0].strip()
|
||||
code_matches = re.findall(r'```python\n(.*?)\n```', m_body, re.DOTALL)
|
||||
output_matches = re.findall(r'```text\n(.*?)\n```', m_body, re.DOTALL)
|
||||
missions_code[m_num] = {
|
||||
'num': m_num,
|
||||
'title': m_title,
|
||||
'code': code_matches[0] if code_matches else '',
|
||||
'output': output_matches[0] if output_matches else ''
|
||||
}
|
||||
return scenario, missions_code
|
||||
|
||||
scenario, missions_code = load_data()
|
||||
print(f"Loaded scenario '{scenario['title']}' with {len(scenario['missions'])} missions and {len(missions_code)} code blocks.")
|
||||
@@ -0,0 +1,21 @@
|
||||
import json
|
||||
import ast
|
||||
|
||||
for path in ['uploads/OPS01/OPS01.ipynb', 'uploads/OPS01/OPS01_student.ipynb']:
|
||||
with open(path, encoding='utf-8') as f:
|
||||
nb = json.load(f)
|
||||
print("Checking:", path)
|
||||
print(" Cells count:", len(nb["cells"]))
|
||||
code_cells = 0
|
||||
md_cells = 0
|
||||
for i, cell in enumerate(nb['cells']):
|
||||
if cell['cell_type'] == 'code':
|
||||
code_cells += 1
|
||||
code_src = ''.join(cell['source'])
|
||||
try:
|
||||
ast.parse(code_src)
|
||||
except SyntaxError as e:
|
||||
print(f" [ERROR] Syntax error in cell {i}: {e}")
|
||||
elif cell['cell_type'] == 'markdown':
|
||||
md_cells += 1
|
||||
print(f" Passed! (Code cells: {code_cells}, Markdown cells: {md_cells})")
|
||||
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import glob
|
||||
from pathlib import Path
|
||||
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
def validate_scenario_json(target_project_id=None):
|
||||
# 1. Load A13.json schema keys
|
||||
with open('data/A13.json', 'r', encoding='utf-8') as f:
|
||||
template = json.load(f)
|
||||
|
||||
top_keys = set(template.keys())
|
||||
mission_keys = set(template['missions'][0].keys())
|
||||
guide_step_keys = set(template['missions'][0]['guideSteps'][0].keys())
|
||||
|
||||
print("=" * 60)
|
||||
print("A13.json Schema Reference:")
|
||||
print(f" Top-level keys ({len(top_keys)}): {sorted(top_keys)}")
|
||||
print(f" Mission keys ({len(mission_keys)}): {sorted(mission_keys)}")
|
||||
print(f" GuideStep keys ({len(guide_step_keys)}): {sorted(guide_step_keys)}")
|
||||
print("=" * 60)
|
||||
|
||||
if target_project_id:
|
||||
json_files = sorted(glob.glob(f'scenario_json/{target_project_id}.json'))
|
||||
else:
|
||||
json_files = sorted(glob.glob('scenario_json/*.json'))
|
||||
|
||||
print(f"Found {len(json_files)} generated JSON files to validate.")
|
||||
|
||||
errors = []
|
||||
|
||||
for jf in json_files:
|
||||
fname = Path(jf).name
|
||||
with open(jf, 'r', encoding='utf-8') as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
errors.append(f"[{fname}] Invalid JSON format: {e}")
|
||||
continue
|
||||
|
||||
# Validate Top-level keys
|
||||
missing_top = top_keys - set(data.keys())
|
||||
if missing_top:
|
||||
errors.append(f"[{fname}] Missing top-level keys: {missing_top}")
|
||||
|
||||
# Validate Summary is empty string as requested
|
||||
if data.get('summary') != "":
|
||||
errors.append(f"[{fname}] summary is not empty string: {data.get('summary')}")
|
||||
|
||||
# Validate Objectives exists and not empty
|
||||
obj_text = data.get('objectives', '')
|
||||
if not obj_text or len(obj_text.strip()) == 0:
|
||||
errors.append(f"[{fname}] objectives is empty")
|
||||
|
||||
# Validate Missions count
|
||||
missions = data.get('missions', [])
|
||||
if len(missions) != 10:
|
||||
errors.append(f"[{fname}] Invalid missions count: {len(missions)} (expected 10)")
|
||||
|
||||
# Validate Mission fields
|
||||
for idx, m in enumerate(missions, start=1):
|
||||
m_id = m.get('id', f'M{idx}')
|
||||
missing_m_keys = mission_keys - set(m.keys())
|
||||
if missing_m_keys:
|
||||
errors.append(f"[{fname} -> {m_id}] Missing mission keys: {missing_m_keys}")
|
||||
|
||||
if m.get('sequence') != idx:
|
||||
errors.append(f"[{fname} -> {m_id}] Invalid sequence: {m.get('sequence')} (expected {idx})")
|
||||
|
||||
if not m.get('overview'):
|
||||
errors.append(f"[{fname} -> {m_id}] overview is empty")
|
||||
if not m.get('objectives'):
|
||||
errors.append(f"[{fname} -> {m_id}] objectives is empty")
|
||||
|
||||
# Validate guideSteps
|
||||
g_steps = m.get('guideSteps', [])
|
||||
if not g_steps:
|
||||
errors.append(f"[{fname} -> {m_id}] guideSteps is empty")
|
||||
for s in g_steps:
|
||||
missing_step_keys = guide_step_keys - set(s.keys())
|
||||
if missing_step_keys:
|
||||
errors.append(f"[{fname} -> {m_id} -> Step {s.get('step')}] Missing step keys: {missing_step_keys}")
|
||||
|
||||
print("\nValidation Results:")
|
||||
if errors:
|
||||
print(f" ❌ Validation Failed with {len(errors)} errors:")
|
||||
for err in errors[:20]:
|
||||
print(f" - {err}")
|
||||
else:
|
||||
print(f" ✅ All {len(json_files)} JSON files perfectly passed schema validation!")
|
||||
print(" - All top-level keys matched A13.json")
|
||||
print(" - All missions verified (10 per project)")
|
||||
print(" - All guideSteps, evaluationKeys, objectives/overview structured properly")
|
||||
print(" - Summary field set to empty string")
|
||||
print("=" * 60)
|
||||
|
||||
if __name__ == '__main__':
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
validate_scenario_json(target)
|
||||
|
||||
Reference in new issue
Block a user