diff --git a/docs/demo/AI_시연_시나리오.pptx b/docs/demo/AI_시연_시나리오.pptx new file mode 100644 index 0000000..7d1cec2 Binary files /dev/null and b/docs/demo/AI_시연_시나리오.pptx differ diff --git a/scripts/gen_ai_demo_ppt.py b/scripts/gen_ai_demo_ppt.py new file mode 100644 index 0000000..99613ce --- /dev/null +++ b/scripts/gen_ai_demo_ppt.py @@ -0,0 +1,323 @@ +# -*- 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()