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('
', '\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"

이 단계의 역할

" f"

{clean_text}

" f"

해야 할 일

" f"
    " f"
  1. {clean_text}에 필요한 데이터셋과 입력 파라미터를 준비한다.

  2. " f"
  3. 정답 코드 및 모듈을 실행하여 중간 출력 결과를 도출한다.

  4. " f"
  5. 출력된 지표와 로그가 기준 조건을 만족하는지 대조·검증한다.

  6. " f"
" f"

핵심 확인 지표

" f"

{clean_html_tags(expected_text)}

" f"

학습자가 제출해야 하는 내용

" f"" ) eval_html = ( f"

PASS 기준

" f"" f"

보완이 필요한 경우

" f"" ) 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"

이 단계의 역할

" f"

기본 파이프라인 수행 후 심화 튜닝 및 조건 변경을 통해 최적화 역량을 다진다.

" f"

해야 할 일

" f"

{clean_html_tags(challenge_text)}

" f"

학습자가 제출해야 하는 내용

" f"" ), "evalText": ( f"

PASS 기준

" f"" ), "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"

{target_text}

" # 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"

부대 보안 및 개인정보 보호

" f"" f"

AI 결과와 인간 최종 판단의 구분 (HITL 원칙)

" f"" ) eval_summary_html = ( f"

공통 PASS/FAIL 판정 원칙

" f"" f"

프로젝트 완료 조건

" f"

10개 미션 전체에서 요구되는 필수([필수]) 평가 기준을 모두 PASS해야 프로젝트 최종 PASS로 판정한다.

" ) # 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"

{m_scen_target}

" # Objectives 매핑: 미션 - '수행' m_objectives = f"

{m_scen_exec}

" # 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"

미션 배경 및 당위성

" f"

{desc_clean} 실제 전술 현장에서는 단순 탐지에 그치지 않고 신속한 위협 판단과 신뢰도 높은 의사결정이 필수적이므로, " f"{target_clean} 과업을 완수하여 오판을 방지하고 작전 대응력을 극대화해야 합니다.

" f"

이 문제를 해결하기 위해 본 미션에서는 {exec_phrase} 실습을 체계적으로 수행합니다.

" f"

학습자는 실습가이드에 제시된 총 {step_count}개 세부 활동(기본 기능 구현 ➔ 결과 지표 검증 ➔ 파라미터 튜닝 및 도전과제)을 순차적으로 진행하며 핵심 판정 기준을 달성합니다.

" f"

이번 결과물: {deliv_clean} 형태의 결과물을 완성한다.

" ) # 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"

평가 질문 (PASS/FAIL)

" f"

[{clean_html_tags(m_title)}] 요구된 핵심 성능 기준({clean_html_tags(m_quant_eval)})을 완벽히 달성하였는가?

" f"

PASS 증거

" f"

{clean_html_tags(m_quant_eval)} 기준을 충족하고 {clean_html_tags(m_track)}을 만족하는 실행 결과 및 로그가 제출되었다.

" f"

보완이 필요한 상태

" f"

정량적 지표가 기준치에 미달하거나 실행 로그에서 예외/결측 오류가 발생한 경우.

" f"

보완 방법

" f"

{clean_html_tags(m_feedback)}을 적용하여 파라미터를 보정하고 단위 테스트를 재수행한다.

" ), ( f"

평가 질문 (PASS/FAIL)

" f"

[정성적 타당성] 군사적 현실성 및 {clean_html_tags(m_qual_eval)} 관점의 타당성이 확보되었는가?

" f"

PASS 증거

" f"

{clean_html_tags(m_qual_eval)}이 논리적으로 검증되었으며 {clean_html_tags(m_verify)}를 통한 무결성이 확인되었다.

" f"

보완이 필요한 상태

" f"

비현실적인 가정을 적용했거나 오류 원인에 대한 정성적 설명 근거가 부족한 경우.

" f"

보완 방법

" f"

교차 검증 데이터를 보강하고 지휘관/실무자 관점의 해석 근거를 명시한다.

" ) ] # Evaluation Text eval_text = ( f"

미션 판정

" f"

평가 구분: 필수

" f"

핵심 평가영역: {clean_html_tags(m_qual_eval)}

" f"

미션 PASS 조건

" f"" f"

채점자 판정 메모

" f"

단순 코드 실행 여부뿐만 아니라 출력된 수치와 근거 자료가 군 실무 요구사항에 부합하는지 면밀히 확인한다.

" ) # 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"

미션 실습 자료

" f"" f"

활용 주의: 제공된 교육용 가상 데이터만 사용하며 실제 군사 비밀 및 개인정보는 입력하지 않습니다.

" ), "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)