# -*- coding: utf-8 -*- """고객 시연용 AI 파트 발표 PPT 생성 — 시연 시나리오 순 구성. 근거: docs/demo/AI_DEMO_SCRIPT.pptx(내부 절차서)의 4개 데모 흐름. 고객용이므로 계정·체크리스트·트러블슈팅 등 내부 정보는 제외. 출력: docs/demo/AI_시연_시나리오.pptx """ import os BASE = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "docs", "demo") KOR_FONT = "맑은 고딕" from pptx import Presentation from pptx.util import Inches, Pt from pptx.dml.color import RGBColor from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.shapes import MSO_SHAPE from pptx.oxml.ns import qn P_BLUE = RGBColor(0x00, 0x66, 0xB3) P_DARK = RGBColor(0x00, 0x4C, 0x86) P_AI = RGBColor(0x6D, 0x4A, 0xFF) P_GREY = RGBColor(0x34, 0x40, 0x54) P_LIGHT = RGBColor(0xEF, 0xF6, 0xFC) P_WHITE = RGBColor(0xFF, 0xFF, 0xFF) P_SOFT = RGBColor(0xD9, 0xEA, 0xF7) P_FOOT = RGBColor(0x66, 0x70, 0x85) P_INK = RGBColor(0x10, 0x18, 0x28) TITLE = "KINTEX AI 전시·행사시스템" def pset(run, size, bold=False, color=P_GREY): f = run.font f.size = Pt(size) f.bold = bold f.color.rgb = color f.name = KOR_FONT rPr = run._r.get_or_add_rPr() for tag in ("a:ea", "a:cs"): el = rPr.find(qn(tag)) if el is None: el = rPr.makeelement(qn(tag), {}) rPr.append(el) el.set("typeface", KOR_FONT) def add_box(slide, x, y, w, h): tb = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) tb.text_frame.word_wrap = True return tb.text_frame def add_rect(slide, x, y, w, h, fill, line=False): sh = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) sh.fill.solid() sh.fill.fore_color.rgb = fill if not line: sh.line.fill.background() sh.shadow.inherit = False return sh def footer(slide, idx, total): tf = add_box(slide, 0.5, 7.05, 12.3, 0.35) r = tf.paragraphs[0].add_run() r.text = f"{TITLE} · AI 기능 고객 시연" pset(r, 9, color=P_FOOT) tf2 = add_box(slide, 12.3, 7.05, 0.7, 0.35) p2 = tf2.paragraphs[0] p2.alignment = PP_ALIGN.RIGHT r2 = p2.add_run() r2.text = f"{idx}/{total}" pset(r2, 9, color=P_FOOT) def blank(prs): return prs.slides.add_slide(prs.slide_layouts[6]) def head(slide, kicker, title): add_rect(slide, 0, 0, 13.333, 0.18, P_BLUE) tf = add_box(slide, 0.55, 0.35, 12.2, 1.15) r = tf.paragraphs[0].add_run(); r.text = kicker; pset(r, 12, True, P_AI) p2 = tf.add_paragraph() r = p2.add_run(); r.text = title; pset(r, 25, True, P_DARK) add_rect(slide, 0.6, 1.62, 12.1, 0.03, P_SOFT) def add_table(slide, x, y, w, header, rows, col_w=None, hdr_size=11, size=10.5, row_h=0.42): shp = slide.shapes.add_table(1 + len(rows), len(header), Inches(x), Inches(y), Inches(w), Inches(row_h * (1 + len(rows)))) t = shp.table if col_w: for j, cw in enumerate(col_w): t.columns[j].width = Inches(cw) for j, htxt in enumerate(header): c = t.cell(0, j) c.fill.solid(); c.fill.fore_color.rgb = P_DARK c.vertical_anchor = MSO_ANCHOR.MIDDLE pa = c.text_frame.paragraphs[0]; pa.alignment = PP_ALIGN.CENTER r = pa.add_run(); r.text = htxt; pset(r, hdr_size, True, P_WHITE) for i, row in enumerate(rows): for j, val in enumerate(row): c = t.cell(i + 1, j) c.fill.solid() c.fill.fore_color.rgb = P_LIGHT if i % 2 == 0 else P_WHITE c.vertical_anchor = MSO_ANCHOR.MIDDLE c.margin_top = Pt(3); c.margin_bottom = Pt(3) lines = str(val).split("\n") for k, ln in enumerate(lines): pa = c.text_frame.paragraphs[0] if k == 0 else c.text_frame.add_paragraph() r = pa.add_run(); r.text = ln pset(r, size, j == 0, P_INK if j != 0 else P_DARK) return t def note_bar(slide, y, text, h=0.55): add_rect(slide, 0.6, y, 12.1, h, P_LIGHT) tf = add_box(slide, 0.8, y + 0.06, 11.8, h - 0.1) r = tf.paragraphs[0].add_run(); r.text = text; pset(r, 12, True, P_DARK) # ══════════════════════════════ 슬라이드 정의 ══════════════════════════════ DEMOS = [ dict( no="DEMO 1", tag="공개 홈 · 로그인 불필요", time="5분", title="AI 관람 도우미", sub="자연어로 묻고, 실제 행사 데이터만 근거로 답한다", divider_points=[ "관람객이 홈페이지에서 말하듯 질문 — 행사·일정·교통을 즉시 안내", "답변 근거는 실제 적재된 행사 DB — 근거가 없으면 \"모른다\"고 답변(환각 차단)", ], steps=[ ("1", "공개 홈 접속 → 관람객 트랙 진입", "-", "관람객 서브홈 · AI 도우미 노출"), ("2", "AI 도우미에 질문 ①", "\"이번 달 킨텍스에서 볼만한 전시 알려줘\"", "행사명·기간·홀을 DB 근거로 요약 답변"), ("3", "질문 ② — 구체 카테고리", "\"유아용품 관련 박람회 언제 해?\"", "해당 카테고리 행사 목록 + 근거 표기"), ("4", "질문 ③ — 교통·편의", "\"대중교통으로 가는 방법 알려줘\"", "교통 가이드 데이터 기반 안내"), ("5", "환각 차단 확인 — 근거 없는 질문", "\"킨텍스에서 BTS 콘서트 언제 해?\"", "근거 없음 → 모른다고 답변 (추측 없음)"), ], point="핵심 포인트 — 모든 답변은 실제 행사 데이터 근거. 근거가 없으면 지어내지 않습니다.", ), dict( no="DEMO 2", tag="플로어플랜 스튜디오 · ★핵심", time="12분", title="자연어 AI 자동배치", sub="말로 시키면 배치가 나온다 — 자연어 → 3안 생성 → AI 평가 → 조감 렌더", divider_points=[ "\"부스 120개, 프리미엄 3할…\" 한 문장 → AI가 조건 해석 → 배치안 3안 자동 생성", "배치안마다 활용률·통로폭 실계산 지표 근거의 AI 평가 요약", "선택한 배치안은 AI가 홀 전경(조감) 이미지로 렌더링", ], steps=[ ("1", "시연 행사 → 부스 배치(플로어플랜 스튜디오) 진입", "-", "홀 평면 캔버스 표시"), ("2", "[AI 자동배치] → 자연어로 조건 입력 → [AI 해석]", "\"부스 120개, 프리미엄 3할, 무대 1개,\n입구 2개로 3안 뽑아줘\"", "AI가 조건 해석 → 폼 자동 채움 + 해석 근거 표시"), ("3", "해석된 값 확인(수정 가능) → [생성]", "목표 부스 수만 150으로 수정해 보기", "배치안 A/B/C 3안 생성 (제약 엔진)"), ("4", "배치안별 AI 평가 요약 확인", "-", "활용률·통로폭 등 실계산 지표 근거 장단점 요약"), ("5", "홀 전경(조감) 렌더 → 마음에 드는 안 [적용]", "(재시도) \"소형 부스 위주 200개, 라운지 2개\"", "AI 조감 이미지 생성 · 선택안 캔버스 반영"), ], point="핵심 포인트 — 수일 걸리던 CAD 배치 작업이 자연어 한 문장으로. AI 해석 결과는 폼에서 수정 가능해 사람이 최종 결정합니다.", ), dict( no="DEMO 3", tag="부스설계 스튜디오 · ★핵심", time="10분", title="부스 사진 시안 (AI)", sub="사진 한 장이 시공 시안이 된다 — 업로드 → 스타일 선택 → 시공 후 사진", divider_points=[ "부스·공간 사진 한 장 업로드 → 스타일 선택 → AI가 '시공 후 결과 사진' 생성", "구조 보존 — 벽·바닥·기둥·구도는 그대로, 인테리어·조명·사이니지만 교체", "Before/After 슬라이더로 비교 — 장치공사 발주(견적) 자료로 바로 연결", ], steps=[ ("1", "부스설계 스튜디오 → [사진 시안 (AI)] 섹션", "-", "01 업로드 → 02 스타일 → 03 지시 3단 UI"), ("2", "01 부스/공간 사진 업로드", "샘플 부스 사진 (드래그앤드롭)", "미리보기 표시"), ("3", "02 스타일 선택 + 03 자연어 지시(선택)", "스타일: \"테크\"\n지시: \"로고 월과 LED 사이니지, 우드 톤 바닥\"", "[사진 시안 생성] 활성화"), ("4", "[사진 시안 생성] 클릭", "-", "생성 진행 표시 (약 30~60초)"), ("5", "Before/After 슬라이더로 비교", "(재생성) 스타일 \"럭셔리\"로 비교", "구조·구도 보존, 인테리어만 교체된 시안 + AI 워터마크"), ], point="핵심 포인트 — 참가업체가 현장 사진만 보내면 공사 발주 전에 시공 결과를 미리 봅니다. 이 시안이 그대로 견적(옥션) 근거자료가 됩니다.", ), dict( no="DEMO 4", tag="경영분석 대시보드 · AI 콘솔", time="8분", title="자연어 데이터 조회 + 규정 챗", sub="데이터와 규정에 말로 묻는다 — 조회는 DB 실집계, 규정은 룰셋 근거", divider_points=[ "\"이번 행사 부스 판매율은?\" — 자연어 질문을 DB 실조회로 답변", "\"천장 높이 제한은?\" — 등록된 규정 룰셋 버전을 근거 조항과 함께 답변", ], steps=[ ("1", "좌측 메뉴 → 경영분석 → 대시보드 하단 AI 콘솔", "-", "NL Query / 규정 챗 탭"), ("2", "자연어 조회 ①", "\"이번 행사 부스 판매율 알려줘\"", "DB 실조회 수치·표 응답"), ("3", "자연어 조회 ②", "\"홀별 배정 현황 요약해줘\"", "홀 H1~H5 배정 집계 응답"), ("4", "규정 챗 ①", "\"부스 천장 높이 제한이 몇 미터야?\"", "룰셋(규정 버전) 근거 답변 + 근거 조항"), ("5", "규정 챗 ②", "\"전기 증설 신청은 언제까지 해야 해?\"", "규정·마일스톤 근거 답변"), ], point="핵심 포인트 — 답변 근거인 규정 룰셋 버전을 관리 화면에서 함께 확인할 수 있어, AI 답변의 출처가 투명합니다.", ), ] def build(): prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) total = 4 + len(DEMOS) * 2 # 표지·개요·아젠다·클로징 + (간지+시나리오)×4 idx = 1 # ── 1. 표지 ────────────────────────────────────────────── s = blank(prs) add_rect(s, 0, 0, 13.333, 7.5, P_DARK) add_rect(s, 0, 4.9, 13.333, 0.06, P_AI) tf = add_box(s, 1.0, 2.0, 11.3, 2.6) r = tf.paragraphs[0].add_run(); r.text = TITLE; pset(r, 40, True, P_WHITE) p2 = tf.add_paragraph() r = p2.add_run(); r.text = "AI 기능 고객 시연"; pset(r, 26, True, P_SOFT) tf2 = add_box(s, 1.0, 5.1, 11.3, 1.6) r = tf2.paragraphs[0].add_run() r.text = "AI 관람 도우미 · 자연어 자동배치 · 부스 사진 시안 · 자연어 조회/규정 챗" pset(r, 15, True, P_WHITE) p2 = tf2.add_paragraph() r = p2.add_run(); r.text = "\"신청서를 내는 시간, 시공 후 사진을 먼저 본다\" · 2026. 7." pset(r, 13, False, P_SOFT) idx += 1 # ── 2. 시연 개요 ───────────────────────────────────────── s = blank(prs) head(s, "OVERVIEW", "오늘 시연에서 보실 것") add_table(s, 0.6, 2.0, 12.1, ["구분", "무엇을 보여드리나", "핵심 가치"], [ ("데모 1", "AI 관람 도우미 — 자연어 질문 → 행사·교통 안내", "근거 기반 답변 · 환각 차단"), ("데모 2 ★", "자연어 자동배치 — 한 문장 → 배치안 3안 + AI 평가 + 조감 렌더", "수일의 CAD 작업 → 수분"), ("데모 3 ★", "부스 사진 시안 — 사진 한 장 → 시공 후 결과 사진", "발주 전 결과 확인 · 견적 근거"), ("데모 4", "AI 콘솔 — 자연어 데이터 조회 + 규정 챗", "데이터·규정에 말로 질문"), ], col_w=[1.5, 7.3, 3.3], row_h=0.62) note_bar(s, 5.6, "3가지 핵심 메시지 — ① 말로 시키면 배치가 나온다 ② 사진 한 장이 시공 시안이 된다 ③ 모든 답변은 근거 기반", h=0.55) tf = add_box(s, 0.6, 6.35, 12.1, 0.5) r = tf.paragraphs[0].add_run() r.text = "시연 환경: 라이브 시스템(실서버·실데이터) — 화면 캡처가 아닌 실제 동작을 시연합니다." pset(r, 12, False, P_GREY) footer(s, idx, total); idx += 1 # ── 3. 시연 순서 ───────────────────────────────────────── s = blank(prs) head(s, "AGENDA", "시연 순서 (약 40분)") add_table(s, 0.6, 2.0, 12.1, ["순서", "내용", "시간", "위치"], [ ("데모 1", "AI 관람 도우미 — 근거 기반 자연어 응답 (로그인 불필요)", "5분", "공개 홈"), ("데모 2 ★", "자연어 AI 자동배치 → 3안 생성 → AI 평가 → 조감 렌더", "12분", "플로어플랜 스튜디오"), ("데모 3 ★", "부스 사진 시안 — 사진 업로드 → 시공 후 사진 + Before/After", "10분", "부스설계 스튜디오"), ("데모 4", "자연어 데이터 조회(NL Query) + 규정 챗", "8분", "경영분석 대시보드"), ("마무리", "질의응답 · 안전 설계(폴백) 소개", "5분", "-"), ], col_w=[1.5, 7.3, 1.2, 2.1], row_h=0.6) note_bar(s, 5.75, "각 데모는 실제 화면에서 라이브로 진행하며, 본 자료는 데모 사이 브리지(안내) 용도입니다.", h=0.55) footer(s, idx, total); idx += 1 # ── 4~11. 데모별 간지 + 시나리오 ──────────────────────── for d in DEMOS: # 간지 (라이브 시연 직전 브리지) s = blank(prs) add_rect(s, 0, 0, 13.333, 7.5, P_BLUE) add_rect(s, 0, 5.9, 13.333, 0.05, P_AI) tf = add_box(s, 1.2, 1.6, 11.0, 3.6) r = tf.paragraphs[0].add_run() r.text = f"{d['no']} · {d['tag']} · {d['time']}" pset(r, 18, True, P_SOFT) p2 = tf.add_paragraph() r = p2.add_run(); r.text = d["title"]; pset(r, 40, True, P_WHITE) p3 = tf.add_paragraph() r = p3.add_run(); r.text = d["sub"]; pset(r, 17, False, P_SOFT) tfp = add_box(s, 1.25, 4.6, 10.9, 1.9) for k, pt in enumerate(d["divider_points"]): pa = tfp.paragraphs[0] if k == 0 else tfp.add_paragraph() pa.space_after = Pt(6) r = pa.add_run(); r.text = f"▎ {pt}"; pset(r, 14, False, P_WHITE) footer(s, idx, total); idx += 1 # 시나리오 흐름 (진행자용 겸 고객 안내) s = blank(prs) head(s, f"{d['no']} · 시나리오", f"{d['title']} — 진행 흐름") add_table(s, 0.6, 1.95, 12.1, ["단계", "진행", "입력(자연어) 예시", "보실 결과"], d["steps"], col_w=[0.7, 4.0, 3.9, 3.5], row_h=0.62, size=10) note_bar(s, 6.1, d["point"], h=0.6) footer(s, idx, total); idx += 1 # ── 12. 클로징 ────────────────────────────────────────── s = blank(prs) add_rect(s, 0, 0, 13.333, 7.5, P_DARK) add_rect(s, 0, 1.9, 13.333, 0.05, P_AI) tf = add_box(s, 1.2, 1.0, 11.0, 0.8) r = tf.paragraphs[0].add_run(); r.text = "시연을 마치며 — 3가지 핵심 메시지"; pset(r, 28, True, P_WHITE) msgs = [ ("① 말로 시키면 배치가 나온다", "자연어 → 제약 엔진 → 배치안 3안 + 실계산 지표 근거 AI 평가"), ("② 사진 한 장이 시공 시안이 된다", "구조 보존 image-to-image — 발주 전 결과 확인, 견적(옥션) 근거자료로 연결"), ("③ 모든 답변은 근거 기반", "행사 DB·규정 룰셋만 근거로 답변 — 근거가 없으면 모른다고 답한다(환각 차단)"), ] y = 2.5 for t1, t2 in msgs: add_rect(s, 1.2, y, 10.9, 1.05, RGBColor(0x0A, 0x3A, 0x63)) tf = add_box(s, 1.5, y + 0.12, 10.4, 0.9) r = tf.paragraphs[0].add_run(); r.text = t1; pset(r, 17, True, P_WHITE) p2 = tf.add_paragraph() r = p2.add_run(); r.text = t2; pset(r, 12.5, False, P_SOFT) y += 1.25 tf = add_box(s, 1.2, 6.35, 10.9, 0.6) r = tf.paragraphs[0].add_run() r.text = "AI가 실패해도 화면은 멈추지 않습니다 — 모든 AI 기능은 수동 폴백을 갖춘 안전 설계 · AI 생성물 라벨·워터마크 표기" pset(r, 12, False, P_SOFT) idx += 1 out = os.path.join(BASE, "AI_시연_시나리오.pptx") prs.save(out) print("saved:", out, "slides:", len(prs.slides)) if __name__ == "__main__": build()