# 2단계: DO 구조마다 「유저가 붙여 넣을 법한 원문」을 LLM 으로 쓴다. # 사용: python3 gen_raw.py [--model gpt-5.6-luna] [--only 1,2] → raw/.json · raw/usage.jsonl · inputs.json import json, os, sys, time, urllib.request, concurrent.futures as cf ROOT = os.path.dirname(os.path.abspath(__file__)) MODEL = sys.argv[sys.argv.index('--model') + 1] if '--model' in sys.argv else 'gpt-5.6-luna' ONLY = set(map(int, sys.argv[sys.argv.index('--only') + 1].split(','))) if '--only' in sys.argv else None KEY = next(l.split('=', 1)[1].split(' #')[0].strip().strip('"') for l in open(os.path.join(ROOT, '../aippt-l3/.env.local')) if l.startswith('OPENAI_API_KEY_1=')) GLOSS = { 'AttributeComparison_Versus': '두 대상(또는 두 안)을 좌우로 맞대어 비교하는 구성', 'Enumeration_List': '제목 + 설명이 달린 항목을 나열하는 목록', 'Enumeration_ListingTable': '행 · 열로 된 목록형 표', 'Hierarchy_HierarchicalTree': '상위 → 하위로 갈라지는 조직도 · 분류 트리', 'KPIKeyMetric_BigNumber': '크게 강조한 핵심 수치(값 + 단위 + 짧은 이름)', 'KPIKeyMetric_Gauge': '0–100% 같은 달성률 · 숙련도 게이지', 'MagnitudeRanking_BarChart': '항목별 수치를 막대로 비교하는 막대그래프', 'PageNavigation_Ending': '발표 마지막 장(감사 인사 · 연락처 · 마무리 문구)', 'PageNavigation_Opening': '발표 첫 장(표지 · 제목 · 발표자 소개)', 'PortraitProfile_ProfileCard': '한 사람(또는 한 조직)의 소개 카드', 'PortraitProfile_ResumeCV': '이력 · 경력 · 학력을 정리한 이력서', 'ProcessFlow_Steps': '순서가 있는 단계 · 절차', 'ProportionComposition_DonutChart': '구성비를 보여 주는 도넛 차트', 'ProportionComposition_PieChart': '구성비를 보여 주는 원 그래프', 'QnACard_QnACard': '질문과 답변 짝', 'ReasoningStructure_ProblemSolution': '문제 → 해결책 구조', 'TextBasedCommunication_HighlightedMessage': '한 문장 · 한 문단으로 강조한 핵심 메시지', 'TextBasedCommunication_LongFormTextLayout': '제목 아래 긴 본문 문단을 배치한 글 중심 구성', 'TimelineSchedule_Timeline': '시점(연도 · 날짜) 순으로 놓인 타임라인', 'Transformation_Transformation': '전 → 후(현재 → 목표, 기존 → 개선) 변화', 'Trend_LineAreaChart': '시간에 따른 추이를 보여 주는 꺾은선 그래프', 'Utterance_DialogueChat': '두 사람 이상이 주고받는 대화', 'Utterance_QuoteTestimonial': '어떤 사람의 인용문 · 후기(말한 사람 포함)', } FIELD = {'label': '제목/이름', 'items': '설명 문장', 'value': '수치', 'unit': '단위', 'period': '시점/기간', 'organization': '소속/기관', 'source': '출처'} def cats(s): return ' + '.join(f'{c}({GLOSS.get(c, "")})' for c in s.split('+')) def fdesc(fields): parts = [] for k, v in fields.items(): if isinstance(v, list): parts.append(f'{FIELD.get(k, k)} {len(v)}개') elif v is not None: parts.append(FIELD.get(k, k)) return ', '.join(parts) if parts else '(글 칸 없음 — 안쪽 표현만 담는 자리)' def udesc(u, indent, name, lines): lines.append(f'{indent}- {name}: {fdesc(u["fields"])}') for k, c in enumerate(u.get('children', [])): udesc(c, indent + ' ', f'하위 {k + 1}', lines) def ddesc(d): if d.get('kind') == 'matrix': return f'표 {d.get("rows")}행 × {d.get("cols")}열' + (' (첫 행은 머리글)' if d.get('header') else '') return '' def skeleton(p): lines = [] host = {n['path'][0]: n for n in p['nested'] if n['path']} for i, u in enumerate(p['parent_units']): lines.append(f'- 바깥 항목 {i + 1}: {fdesc(u["fields"])}') for k, c in enumerate(u.get('children', [])): udesc(c, ' ', f'하위 {k + 1}', lines) if i in host: n = host[i] dd = ddesc(n.get('dataset') or {}) cnt = f'안쪽 항목 {len(n["units"])}개' if n['units'] else dd lines.append(f' - 이 항목 안에 들어가는 안쪽 표현 [{cats("+".join(n["categories"]))}] — {cnt}' + (' · 안쪽 표현의 짧은 제목 있음' if n.get('has_title') else '') + ':') for j, cu in enumerate(n['units']): udesc(cu, ' ', f'안쪽 항목 {j + 1}', lines) return '\n'.join(lines) SYSTEM = """너는 발표 자료를 만드는 사용자가 「AI 비주얼 요약」 입력 칸에 붙여 넣을 원문을 대신 써 주는 작가다. 주어진 디자인 컴포넌트 구조로 시각화하기에 딱 맞는 원문 한 편을 한국어로 쓴다. 규칙: 1. 내용은 새로 지어낸다. 참고 글(템플릿 자리글)의 문장 · 고유명사를 베끼지 않는다. 주제는 그럴듯한 업무 · 교육 · 행사 · 제품 · 공공 분야 중에서 고른다. 2. 실존 개인의 실명, 실존 회사 · 브랜드 이름을 쓰지 않는다. 사람이 필요하면 「김OO 팀장」처럼 가린 이름이나 직함만 쓴다. 회사는 「A사」 · 「한빛기획(가상)」 같은 가상 이름만. 3. 구조가 원문에서 뚜렷이 드러나야 한다: - 바깥 항목 수와 각 항목의 제목이 분명하다(번호 · 소제목 · 줄바꿈). - 안쪽 표현이 그 바깥 항목 **안에 속하는 내용**으로 쓰인다(그 항목의 수치, 그 항목의 단계, 그 항목의 강조문, 그 항목의 인용 등). 다른 항목으로 떼어 놓으면 뜻이 깨지게 쓴다. - 안쪽 항목 수도 구조와 같게 맞춘다. 수치 칸은 실제 숫자 + 단위로, 시점 칸은 연도 · 날짜로 쓴다. 4. 사용자가 문서 · 메모에서 복사해 온 듯한 평문으로 쓴다. 번호 · 하이픈 · 줄바꿈은 써도 되지만 마크다운 표(|)나 # 머리말, 굵게(**)는 쓰지 않는다. 표가 필요하면 문서에서 복사한 것처럼 칸을 탭으로 나눈 행으로 쓴다. 구조를 설명하는 낱말(「바깥 항목」 · 「안쪽 표현」 · 「조직도 1개」 · 「하위 1」 · 컴포넌트 이름 등)은 원문에 쓰지 않는다. 원문은 내용만 담는다. 5. 길이는 구조가 요구하는 만큼(대략 150–700자). 군더더기 서론 · 맺음말을 붙이지 않는다. 출력은 JSON 하나: {"topic": "<한 줄 주제>", "input_text": "<원문>"}""" def sat_desc(s): name = {'title': '제목', 'subtitle': '부제', 'desc': '설명 문단'} parts = [f'{name.get(k, k)}' + (f' {v}개' if k == 'desc' else '') for k, v in s.items()] return ', '.join(parts) if parts else '없음' def user_msg(p): ref = [t for t in p['all_text'] if t][:40] return f"""컴포넌트 구조 - 바깥 표현: {cats(p['outer'])} - 안쪽 표현: {cats(p['inner'])} - 꼴: {'바깥 항목 가운데 하나에만 안쪽 표현이 들어간다' if p['shape'] == 'single' else '바깥 항목마다 안쪽 표현이 하나씩 들어간다'} - 바깥 항목 {len(p['parent_units'])}개 - 바깥 표현 전체에 붙는 글: {sat_desc(p.get('parent_satellites') or {})} 항목별 구조 {skeleton(p)} 참고 글(템플릿 자리글 — 분량 · 말투만 참고하고 내용은 베끼지 말 것) {json.dumps(ref, ensure_ascii=False)}""" def call(p): body = {'model': MODEL, 'reasoning_effort': 'medium', 'response_format': {'type': 'json_object'}, 'messages': [{'role': 'system', 'content': SYSTEM}, {'role': 'user', 'content': user_msg(p)}]} for att in range(4): try: req = urllib.request.Request('https://api.openai.com/v1/chat/completions', data=json.dumps(body).encode(), headers={'Authorization': f'Bearer {KEY}', 'Content-Type': 'application/json'}) r = json.load(urllib.request.urlopen(req, timeout=300)) out = json.loads(r['choices'][0]['message']['content']) assert out.get('input_text') return p['fav_id'], out, r['usage'] except Exception as e: # noqa: BLE001 err = e time.sleep(3 * (att + 1)) return p['fav_id'], {'error': str(err)}, None def main(): st = json.load(open(os.path.join(ROOT, 'dos/structure.json'))) if ONLY: st = [p for p in st if p['fav_id'] in ONLY] os.makedirs(os.path.join(ROOT, 'raw'), exist_ok=True) json.dump({'system': SYSTEM, 'user_example': user_msg(st[0]), 'model': MODEL}, open(os.path.join(ROOT, 'raw/prompt.json'), 'w'), ensure_ascii=False, indent=1) with cf.ThreadPoolExecutor(6) as ex, open(os.path.join(ROOT, 'raw/usage.jsonl'), 'a') as uf: for fid, out, usage in ex.map(call, st): json.dump(out, open(os.path.join(ROOT, f'raw/{fid:02d}.json'), 'w'), ensure_ascii=False, indent=1) uf.write(json.dumps({'id': fid, 'model': MODEL, 'usage': usage}) + '\n') print(fid, 'ok' if usage else out.get('error'), flush=True) if __name__ == '__main__': main()