시나리오 내용 반영

This commit is contained in:
KIM SU BO committed 2026-10-08 16:43:48 +09:00
1 parent d9287de729
commit 350bb92bf9
270 files changed
+198867

No files matched your search

+316
View File
@@ -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()
+220
View File
@@ -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()
+693
View File
@@ -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)
+34
View File
@@ -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()
+21
View File
@@ -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! ===")
+454
View File
@@ -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
+34
View File
@@ -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.")
+21
View File
@@ -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})")
+102
View File
@@ -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)