"""Собирает MEETING_2026-04-23.pptx из содержания MEETING_2026-04-23.md. Запуск: python3 build_deck.py Выход: MEETING_2026-04-23.pptx в корне проекта """ from pptx import Presentation from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.oxml.ns import qn from copy import deepcopy # ---------- Бренд клиники ---------- PRIMARY = RGBColor(0x2B, 0xB4, 0xA8) PRIMARY_DARK = RGBColor(0x1C, 0x8A, 0x80) PRIMARY_50 = RGBColor(0xE8, 0xF7, 0xF5) ACCENT = RGBColor(0xE0, 0x4E, 0x44) WARM = RGBColor(0xF5, 0xED, 0xDF) SUCCESS = RGBColor(0x2E, 0x9B, 0x6B) WARNING = RGBColor(0xE8, 0xA1, 0x3C) DANGER = RGBColor(0xD9, 0x41, 0x41) FG = RGBColor(0x1F, 0x29, 0x37) FG2 = RGBColor(0x4B, 0x55, 0x63) FG3 = RGBColor(0x9C, 0xA3, 0xAF) BG = RGBColor(0xFF, 0xFF, 0xFF) SOFT = RGBColor(0xF3, 0xF4, 0xF6) FONT_SANS = "PT Sans" FONT_DISPLAY = "PT Sans Narrow" # ---------- Геометрия 16:9 ---------- prs = Presentation() prs.slide_width = Inches(13.333) prs.slide_height = Inches(7.5) SW = prs.slide_width SH = prs.slide_height BLANK_LAYOUT = prs.slide_layouts[6] # Blank MARGIN_L = Inches(0.6) MARGIN_R = Inches(0.6) MARGIN_T = Inches(0.45) CONTENT_W = SW - MARGIN_L - MARGIN_R FOOTER_TXT = "Клиника УГН · Приложение · 23 апр 2026" # ---------- Примитивы ---------- def add_rect(slide, x, y, w, h, fill=None, line=None, line_w=None): shp = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h) shp.shadow.inherit = False if fill is None: shp.fill.background() else: shp.fill.solid() shp.fill.fore_color.rgb = fill if line is None: shp.line.fill.background() else: shp.line.color.rgb = line if line_w is not None: shp.line.width = line_w return shp def add_text(slide, x, y, w, h, text, *, size=16, bold=False, color=FG, font=FONT_SANS, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, line_spacing=None): tb = slide.shapes.add_textbox(x, y, w, h) tf = tb.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) tf.vertical_anchor = anchor lines = text.split("\n") for i, ln in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align if line_spacing is not None: p.line_spacing = line_spacing r = p.add_run() r.text = ln r.font.name = font r.font.size = Pt(size) r.font.bold = bold r.font.color.rgb = color return tb def add_rich(slide, x, y, w, h, runs, *, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, size=14, line_spacing=1.15): """runs = [(text, {'bold':bool,'color':RGB,'size':int,'font':str}), ...]""" tb = slide.shapes.add_textbox(x, y, w, h) tf = tb.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) tf.vertical_anchor = anchor paragraphs = [[]] for text, style in runs: if text == "\n": paragraphs.append([]) else: paragraphs[-1].append((text, style)) for i, para in enumerate(paragraphs): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align p.line_spacing = line_spacing for text, style in para: r = p.add_run() r.text = text r.font.name = style.get("font", FONT_SANS) r.font.size = Pt(style.get("size", size)) r.font.bold = style.get("bold", False) r.font.color.rgb = style.get("color", FG) return tb def footer(slide, idx, total): add_rect(slide, 0, SH - Inches(0.32), SW, Inches(0.32), fill=PRIMARY_50) add_text(slide, MARGIN_L, SH - Inches(0.3), Inches(8), Inches(0.25), FOOTER_TXT, size=10, color=FG2) add_text(slide, SW - Inches(1.2), SH - Inches(0.3), Inches(0.6), Inches(0.25), f"{idx} / {total}", size=10, color=FG2, align=PP_ALIGN.RIGHT) def title_bar(slide, title, eyebrow=None): """Верхняя полоска: маленькая подпись категории + крупный заголовок.""" y = MARGIN_T if eyebrow: add_text(slide, MARGIN_L, y, CONTENT_W, Inches(0.3), eyebrow.upper(), size=11, bold=True, color=PRIMARY, font=FONT_SANS) y += Inches(0.34) add_text(slide, MARGIN_L, y, CONTENT_W, Inches(0.6), title, size=28, bold=True, color=FG, font=FONT_DISPLAY) y += Inches(0.7) # Тонкая линия add_rect(slide, MARGIN_L, y, Inches(1.2), Inches(0.04), fill=PRIMARY) return y + Inches(0.25) def new_slide(): return prs.slides.add_slide(BLANK_LAYOUT) # ---------- Стилизованная таблица ---------- def add_table(slide, x, y, w, h, data, *, header=True, col_widths=None, row_heights=None, header_fill=PRIMARY, header_fg=BG, body_fg=FG, alt_fill=PRIMARY_50, size=11, header_size=11): rows, cols = len(data), len(data[0]) tbl_shp = slide.shapes.add_table(rows, cols, x, y, w, h).table if col_widths: for i, cw in enumerate(col_widths): tbl_shp.columns[i].width = cw if row_heights: for i, rh in enumerate(row_heights): tbl_shp.rows[i].height = rh for ri, row in enumerate(data): for ci, cell_val in enumerate(row): cell = tbl_shp.cell(ri, ci) cell.margin_left = cell.margin_right = Inches(0.08) cell.margin_top = cell.margin_bottom = Inches(0.05) # Fill if header and ri == 0: cell.fill.solid(); cell.fill.fore_color.rgb = header_fill elif alt_fill and ri % 2 == 0 and not (header and ri == 0): cell.fill.solid(); cell.fill.fore_color.rgb = alt_fill else: cell.fill.solid(); cell.fill.fore_color.rgb = BG # Text tf = cell.text_frame tf.word_wrap = True tf.clear() lines = str(cell_val).split("\n") for i, ln in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.LEFT r = p.add_run() r.text = ln r.font.name = FONT_SANS if header and ri == 0: r.font.size = Pt(header_size) r.font.bold = True r.font.color.rgb = header_fg else: r.font.size = Pt(size) r.font.color.rgb = body_fg return tbl_shp # ============================================================ # СЛАЙДЫ # ============================================================ def slide_title(): s = new_slide() # Фоновая тёплая полоса слева add_rect(s, 0, 0, Inches(4.5), SH, fill=WARM) # Teal-акцент add_rect(s, Inches(4.5), 0, Inches(0.12), SH, fill=PRIMARY) # Левый блок — мета add_text(s, Inches(0.7), Inches(0.7), Inches(3.5), Inches(0.3), "КЛИНИКА УГН", size=12, bold=True, color=PRIMARY_DARK) add_text(s, Inches(0.7), Inches(1.1), Inches(3.5), Inches(0.3), "Мобильное приложение", size=13, color=FG2) add_text(s, Inches(0.7), SH - Inches(1.2), Inches(3.5), Inches(0.3), "Встреча", size=12, color=FG3) add_text(s, Inches(0.7), SH - Inches(0.9), Inches(3.5), Inches(0.4), "23 апреля 2026", size=18, bold=True, color=FG, font=FONT_DISPLAY) # Правый блок — заголовок add_text(s, Inches(5.1), Inches(2.2), Inches(7.6), Inches(1.0), "Приоритеты развития", size=48, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, Inches(5.1), Inches(3.2), Inches(7.6), Inches(0.8), "мобильного приложения", size=36, color=FG2, font=FONT_DISPLAY) # Подзаголовок add_rect(s, Inches(5.1), Inches(4.5), Inches(0.8), Inches(0.04), fill=PRIMARY) add_text(s, Inches(5.1), Inches(4.7), Inches(7.6), Inches(1.5), "Джентльменский набор для всех\nи четыре группы сегментов повторных", size=20, color=FG2, font=FONT_DISPLAY) return s def slide_agenda(): s = new_slide() y = title_bar(s, "Повестка", "Что обсуждаем") items = [ ("1", "Контекст и критерии приоритизации", "Факты о потоке пациентов, 10 бизнес-сегментов, рамка оценки"), ("2", "Текущие функции — что есть", "Приоритеты использования и точки усиления"), ("3", "Джентльменский набор для всех пациентов", "Транзакционная база (Фаза 1) + коммуникационная надстройка (Фаза 1.5)"), ("4", "10 сегментов × первичный/повторный в приложении", "Матрица и 4 группы сегментных модулей"), ("5", "Порядок внедрения и решение", "Фаза 1 → Фаза 1.5 → Фаза 2 (A → C → B → D). Что выбираем сегодня"), ] row_h = Inches(0.95) for i, (num, title, sub) in enumerate(items): yy = y + row_h * i # кружок-номер circle = s.shapes.add_shape(MSO_SHAPE.OVAL, MARGIN_L, yy, Inches(0.6), Inches(0.6)) circle.shadow.inherit = False circle.fill.solid(); circle.fill.fore_color.rgb = PRIMARY circle.line.fill.background() tf = circle.text_frame tf.margin_left = tf.margin_right = Emu(0) tf.margin_top = tf.margin_bottom = Emu(0) p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER r = p.add_run(); r.text = num r.font.name = FONT_DISPLAY; r.font.size = Pt(20); r.font.bold = True r.font.color.rgb = BG add_text(s, MARGIN_L + Inches(0.9), yy + Inches(0.02), Inches(11), Inches(0.4), title, size=18, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, MARGIN_L + Inches(0.9), yy + Inches(0.45), Inches(11), Inches(0.4), sub, size=13, color=FG2) return s def slide_facts(): s = new_slide() y = title_bar(s, "Ключевые факты о потоке", "Контекст") # Большая статистика слева add_text(s, MARGIN_L, y + Inches(0.2), Inches(5), Inches(1.6), "2 из 3", size=88, bold=True, color=PRIMARY, font=FONT_DISPLAY) add_text(s, MARGIN_L, y + Inches(1.8), Inches(5.5), Inches(0.7), "пациентов клиники — повторные", size=18, color=FG, font=FONT_DISPLAY) add_text(s, MARGIN_L, y + Inches(2.4), Inches(5.5), Inches(1.4), "Значительная доля — повторные внутри одного эпизода лечения: " "после первого визита ещё 1–3+ приёма, процедуры, контроль.", size=13, color=FG2) # Правый блок — выручка bx = Inches(7.0); bw = Inches(5.7) add_rect(s, bx, y + Inches(0.1), bw, Inches(4.8), fill=PRIMARY_50, line=PRIMARY, line_w=Pt(0.5)) add_text(s, bx + Inches(0.3), y + Inches(0.3), bw - Inches(0.6), Inches(0.4), "ВЫРУЧКА ~200 МЛН / ГОД · 10 СЕГМЕНТОВ", size=11, bold=True, color=PRIMARY_DARK) lead_rows = [ ("Амбулаторный поток", "тысячи/мес", "~120 млн"), ("Взрослая хирургия (FESS, полипы, перегородка)", "300 оп/год", "~30 млн"), ("Детская аденоидная хирургия", "400–500 оп/год", "~20–30 млн"), ("Сурдология (слуховые аппараты)", "высокая конверсия", "~20 млн"), ("Хроники, ЧБД, вазотомия, пульмо, храпуны, фониатрия, check-up", "6 сегментов", "остальное"), ] ty = y + Inches(0.8) for name, vol, rev in lead_rows: add_text(s, bx + Inches(0.3), ty, Inches(3.5), Inches(0.3), name, size=12, bold=True, color=FG) add_text(s, bx + Inches(0.3), ty + Inches(0.28), Inches(3.5), Inches(0.24), vol, size=10, color=FG3) add_text(s, bx + bw - Inches(1.8), ty, Inches(1.5), Inches(0.4), rev, size=14, bold=True, color=ACCENT, align=PP_ALIGN.RIGHT, font=FONT_DISPLAY) ty += Inches(0.76) return s def slide_frame(): s = new_slide() y = title_bar(s, "Приложение = удержание, не привлечение", "Продуктовая рамка") # Две колонки colw = Inches(6.15) gap = Inches(0.23) # Левая lx = MARGIN_L add_rect(s, lx, y + Inches(0.1), colw, Inches(4.7), fill=SOFT, line=None) add_text(s, lx + Inches(0.3), y + Inches(0.25), colw - Inches(0.6), Inches(0.4), "ПЕРВИЧНЫЕ", size=12, bold=True, color=FG3) add_text(s, lx + Inches(0.3), y + Inches(0.6), colw - Inches(0.6), Inches(0.6), "Приходят через веб", size=22, bold=True, color=FG, font=FONT_DISPLAY) add_rich(s, lx + Inches(0.3), y + Inches(1.3), colw - Inches(0.6), Inches(3.4), [ ("Главный job: ", {"bold": True, "color": FG}), ("«куда мне обратиться, как записаться»", {}), ("\n", {}), ("Источник: ", {"bold": True, "color": FG}), ("сайт, SEO, реклама, сарафан", {}), ("\n", {}), ("В приложении: ", {"bold": True, "color": FG}), ("базовый минимум, который уже есть", {}), ("\n", {}), ("Приоритет развития: ", {"bold": True, "color": FG}), ("низкий — веб работает лучше", {}), ], size=13, line_spacing=1.3) # Правая (основная) rx = lx + colw + gap add_rect(s, rx, y + Inches(0.1), colw, Inches(4.7), fill=PRIMARY_50, line=PRIMARY, line_w=Pt(1)) add_text(s, rx + Inches(0.3), y + Inches(0.25), colw - Inches(0.6), Inches(0.4), "ПОВТОРНЫЕ · 2 ИЗ 3", size=12, bold=True, color=PRIMARY_DARK) add_text(s, rx + Inches(0.3), y + Inches(0.6), colw - Inches(0.6), Inches(0.6), "Приоритет приложения", size=22, bold=True, color=FG, font=FONT_DISPLAY) add_rich(s, rx + Inches(0.3), y + Inches(1.3), colw - Inches(0.6), Inches(3.4), [ ("Главный job: ", {"bold": True, "color": FG}), ("«что делать сейчас по моему лечению»", {}), ("\n", {}), ("Источник: ", {"bold": True, "color": FG}), ("приложение уже установлено, push / сам открывает", {}), ("\n", {}), ("В приложении: ", {"bold": True, "color": FG}), ("специализированный модуль сегмента", {}), ("\n", {}), ("Приоритет развития: ", {"bold": True, "color": FG}), ("высокий — закрывает 2/3 потока", {}), ], size=13, line_spacing=1.3) # Сноска-исключения add_text(s, MARGIN_L, y + Inches(5.1), CONTENT_W, Inches(0.4), "Исключения: сурдология, хирургия, АСИТ — после первого визита пациент «уходит думать», и вернуть его может только приложение.", size=11, color=FG2) return s def slide_current_functions(): s = new_slide() y = title_bar(s, "Текущие функции — утилита и приоритет", "Что есть сегодня") # Три колонки: зелёная, жёлтая, красная groups = [ ("ЯДРО ЦЕННОСТИ", SUCCESS, [ ("Запись на приём", "Единственная функция, приводящая новых."), ("Ближайший приём", "3–10 открытий перед визитом. Самый частый экран."), ]), ("ПОДДЕРЖКА", WARNING, [ ("Список врачей", "Помогает при первом визите, повторный не открывает."), ("Чат с оператором", "Ограничен одним собеседником — основное развитие в §3."), ("Профиль", "Редко открывается, но критичен для персонализации."), ]), ("НИША", DANGER, [ ("Семейный профиль", "Критичен для 20–30% (дети, пожилые)."), ("Контакты", "1–2 открытия за всё время. Функция «галочка»."), ]), ] cw = (CONTENT_W - Inches(0.5)) / 3 for i, (label, col, items) in enumerate(groups): cx = MARGIN_L + (cw + Inches(0.25)) * i add_rect(s, cx, y, cw, Inches(0.5), fill=col) add_text(s, cx + Inches(0.3), y + Inches(0.1), cw - Inches(0.6), Inches(0.3), label, size=12, bold=True, color=BG) yy = y + Inches(0.7) for title, desc in items: add_text(s, cx, yy, cw, Inches(0.35), title, size=15, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, cx, yy + Inches(0.38), cw, Inches(0.9), desc, size=11, color=FG2) yy += Inches(1.15) # Подвал — вывод add_rect(s, MARGIN_L, Inches(6.55), CONTENT_W, Inches(0.55), fill=WARM) add_text(s, MARGIN_L + Inches(0.3), Inches(6.66), CONTENT_W - Inches(0.6), Inches(0.4), "Слабое место: нет причины открывать приложение между визитами. Это и закрывает джентльменский набор.", size=13, bold=True, color=FG, font=FONT_DISPLAY) return s def slide_three_layers(): s = new_slide() y = title_bar(s, "Три слоя работы", "План") box_h = Inches(1.75) gap = Inches(0.15) # Слой 1 y1 = y + Inches(0.1) add_rect(s, MARGIN_L, y1, CONTENT_W, box_h, fill=PRIMARY_50, line=PRIMARY, line_w=Pt(1)) add_text(s, MARGIN_L + Inches(0.3), y1 + Inches(0.2), Inches(3), Inches(0.3), "ФАЗА 1 · ТРАНЗАКЦИОННАЯ БАЗА", size=11, bold=True, color=PRIMARY_DARK) add_text(s, MARGIN_L + Inches(0.3), y1 + Inches(0.5), Inches(11), Inches(0.5), "Всем пациентам · быстрый MVP · минимум рисков", size=18, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, MARGIN_L + Inches(0.3), y1 + Inches(1.0), CONTENT_W - Inches(0.6), Inches(0.7), "План лечения · Результаты и медкарта · Заказ справок · " "(+ уже есть: запись, ближайший приём, чат с оператором, статьи)", size=13, color=FG2) # Слой 1.5 y15 = y1 + box_h + gap add_rect(s, MARGIN_L, y15, CONTENT_W, box_h, fill=WARM, line=WARNING, line_w=Pt(1)) add_text(s, MARGIN_L + Inches(0.3), y15 + Inches(0.2), Inches(4), Inches(0.3), "ФАЗА 1.5 · КОММУНИКАЦИОННАЯ НАДСТРОЙКА", size=11, bold=True, color=WARNING) add_text(s, MARGIN_L + Inches(0.3), y15 + Inches(0.5), Inches(11), Inches(0.5), "Поверх стабильной базы · отдельный регламент и безопасность", size=18, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, MARGIN_L + Inches(0.3), y15 + Inches(1.0), CONTENT_W - Inches(0.6), Inches(0.7), "Чат с медицинским консьержем (не прямой с лечащим) · AI-помощник на RAG в shadow-mode", size=13, color=FG2) # Слой 2 y2 = y15 + box_h + gap add_rect(s, MARGIN_L, y2, CONTENT_W, box_h, fill=SOFT, line=FG3, line_w=Pt(0.5)) add_text(s, MARGIN_L + Inches(0.3), y2 + Inches(0.2), Inches(3), Inches(0.3), "ФАЗА 2 · СЕГМЕНТНЫЕ МОДУЛИ", size=11, bold=True, color=FG3) add_text(s, MARGIN_L + Inches(0.3), y2 + Inches(0.5), Inches(11), Inches(0.5), "Углубление по группам сегментов", size=18, bold=True, color=FG, font=FONT_DISPLAY) # 4 мини-плитки tiles = [("A", "Самопомощь"), ("C", "Пред-оп"), ("B", "Сурдология"), ("D", "АСИТ + астма")] tw = (CONTENT_W - Inches(0.6) - Inches(0.15) * 3) / 4 for i, (code, name) in enumerate(tiles): tx = MARGIN_L + Inches(0.3) + (tw + Inches(0.15)) * i ty = y2 + Inches(1.05) add_rect(s, tx, ty, tw, Inches(0.55), fill=BG, line=PRIMARY, line_w=Pt(0.75)) add_text(s, tx + Inches(0.15), ty + Inches(0.08), Inches(0.4), Inches(0.4), code, size=18, bold=True, color=PRIMARY, font=FONT_DISPLAY) add_text(s, tx + Inches(0.55), ty + Inches(0.13), tw - Inches(0.6), Inches(0.4), name, size=11, bold=True, color=FG, font=FONT_DISPLAY) return s def slide_phase1_base(): s = new_slide() y = title_bar(s, "Фаза 1 · Транзакционная база", "Джентльменский набор, часть 1") add_text(s, MARGIN_L, y + Inches(0.0), CONTENT_W, Inches(0.4), "Детерминированные функции без LLM и прямого врачебного диалога. Быстрый MVP, минимум рисков.", size=13, color=FG2) data = [ ["Функция", "Статус", "Суть"], ["Запись на приём", "Есть", "Запись 24/7, ближайшие окна."], ["Ближайший приём", "Есть", "Что, где, когда — снимает тревогу перед визитом."], ["Чат с оператором", "Есть", "Справки, переносы, счета, расписание (человеческий канал)."], ["Статьи / база знаний", "Есть", "Источник правды для будущего RAG."], ["План лечения с приёма", "Нет — фундамент", "Структурированные назначения, календарь, напоминания, ссылки на процедуры."], ["Результаты и медкарта", "Нет — новое", "Анализы, аудиограммы, снимки, заключения. Срок годности виден — критично для пред-опа."], ["Заказ справок и финдокументов", "Нет — новое", "ФНС-вычет 13%, справки работодателю, счета. Снимает нагрузку с администраторов."], ] add_table(s, MARGIN_L, y + Inches(0.55), CONTENT_W, Inches(4.6), data, col_widths=[Inches(3.3), Inches(2.3), CONTENT_W - Inches(5.6)], size=12, header_size=12) # Порядок работ add_rect(s, MARGIN_L, Inches(6.25), CONTENT_W, Inches(0.85), fill=PRIMARY_50, line=PRIMARY, line_w=Pt(0.5)) add_rich(s, MARGIN_L + Inches(0.3), Inches(6.35), CONTENT_W - Inches(0.6), Inches(0.7), [ ("Порядок внутри Фазы 1: ", {"bold": True, "color": PRIMARY_DARK}), ("1) План лечения — фундамент · ", {}), ("2) Результаты / медкарта · ", {}), ("3) Заказ справок. ", {}), ("Критическая зависимость — МИС: ", {"bold": True, "color": FG}), ("отдаёт ли API структурированные назначения или план заполняет врач в виджете (см. вопрос №1).", {}), ], size=12, line_spacing=1.3) return s def slide_phase1_5_overlay(): s = new_slide() y = title_bar(s, "Фаза 1.5 · Коммуникационная надстройка", "Джентльменский набор, часть 2") add_text(s, MARGIN_L, y + Inches(0.0), CONTENT_W, Inches(0.6), "Живой диалог по поводу лечения. Строится поверх стабильной Фазы 1, требует отдельных регламентов.", size=13, color=FG2) # Две большие карточки cw = (CONTENT_W - Inches(0.3)) / 2 cy = y + Inches(0.7) ch = Inches(4.5) # Консьерж lx = MARGIN_L add_rect(s, lx, cy, cw, ch, fill=BG, line=PRIMARY, line_w=Pt(1)) add_rect(s, lx, cy, cw, Inches(0.55), fill=PRIMARY) add_text(s, lx + Inches(0.3), cy + Inches(0.12), cw - Inches(0.6), Inches(0.4), "ЧАТ С МЕДИЦИНСКИМ КОНСЬЕРЖЕМ", size=12, bold=True, color=BG) add_text(s, lx + Inches(0.3), cy + Inches(0.75), cw - Inches(0.6), Inches(0.5), "Не прямой с лечащим врачом", size=18, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, lx + Inches(0.3), cy + Inches(1.3), cw - Inches(0.6), Inches(2.4), "Дежурный врач / фельдшер / медсестра отвечает по протоколам " "на 80% рутины: «можно ли совмещать с X», «нормально ли, что третий день болит», " "«когда повторно сдать анализ».\n\n" "Эскалация лечащему — только клинически значимое.\n\n" "Буфер SLA и защита от выгорания врачей.", size=12, color=FG2) add_rect(s, lx + Inches(0.3), cy + ch - Inches(0.85), cw - Inches(0.6), Inches(0.7), fill=WARM) add_rich(s, lx + Inches(0.45), cy + ch - Inches(0.75), cw - Inches(0.9), Inches(0.55), [ ("Риск: ", {"bold": True, "color": DANGER}), ("перегрузка врачей. ", {}), ("Митигация: ", {"bold": True, "color": SUCCESS}), ("консьерж-слой + тарификация эскалаций.", {}), ], size=11, line_spacing=1.3) # AI rx = lx + cw + Inches(0.3) add_rect(s, rx, cy, cw, ch, fill=BG, line=WARNING, line_w=Pt(1)) add_rect(s, rx, cy, cw, Inches(0.55), fill=WARNING) add_text(s, rx + Inches(0.3), cy + Inches(0.12), cw - Inches(0.6), Inches(0.4), "AI-ПОМОЩНИК (RAG, 24/7)", size=12, bold=True, color=BG) add_text(s, rx + Inches(0.3), cy + Inches(0.75), cw - Inches(0.6), Inches(0.5), "Запуск в shadow-mode", size=18, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, rx + Inches(0.3), cy + Inches(1.3), cw - Inches(0.6), Inches(2.4), "Знает: базу знаний клиники, статьи, план лечения пациента, медкарту, историю приёмов.\n\n" "Объясняет назначения, ищет ответ в статьях, оценивает «норма или срочно», " "эскалирует консьержу/врачу.\n\n" "Отвечает ночью, когда человеческие каналы недоступны.", size=12, color=FG2) add_rect(s, rx + Inches(0.3), cy + ch - Inches(0.85), cw - Inches(0.6), Inches(0.7), fill=WARM) add_rich(s, rx + Inches(0.45), cy + ch - Inches(0.75), cw - Inches(0.9), Inches(0.55), [ ("Риск: ", {"bold": True, "color": DANGER}), ("галлюцинация LLM = юр./репутация. ", {}), ("Митигация: ", {"bold": True, "color": SUCCESS}), ("shadow-mode, валидация через консьержа, метрики точности перед прямым режимом.", {}), ], size=11, line_spacing=1.3) return s def slide_primary_vs_returning(): s = new_slide() y = title_bar(s, "Первичный vs повторный в приложении", "Фундаментальное деление") data = [ ["Линза", "Первичный в приложении", "Повторный в приложении"], ["Главный job", "«Куда обратиться и как записаться»", "«Что делать сейчас по моему лечению»"], ["Источник", "Сайт, SEO, реклама, сарафан", "Уведомление, пациент сам открывает"], ["Частота открытия", "1–3 раза до визита", "Ежедневно в активном эпизоде"], ["Что нужно в приложении", "Базовый минимум (есть)", "Специализированный модуль сегмента"], ["Приоритет развития", "Низкий — веб работает лучше", "Высокий — 2/3 потока"], ] add_table(s, MARGIN_L, y + Inches(0.2), CONTENT_W, Inches(4.4), data, col_widths=[Inches(3.0), Inches(4.55), Inches(4.55)], size=13, header_size=12, header_fill=FG) add_rect(s, MARGIN_L, Inches(6.1), CONTENT_W, Inches(1.0), fill=PRIMARY_50, line=PRIMARY, line_w=Pt(0.5)) add_rich(s, MARGIN_L + Inches(0.3), Inches(6.2), CONTENT_W - Inches(0.6), Inches(0.85), [ ("Исключения: ", {"bold": True, "color": PRIMARY_DARK}), ("сегменты, где «первичный в приложении» ≠ «первый визит» — сурдология, хирургия, АСИТ. " "После первого визита пациент уходит, и вернуть его может только приложение. " "Эти сегменты требуют отдельных «первичных» сценариев удержания.", {}), ], size=13, line_spacing=1.3) return s def slide_segment_matrix(): s = new_slide() y = title_bar(s, "10 сегментов × первичный / повторный", "Матрица") header = ["#", "Сегмент", "Объём · вклад", "Повторный — что нужно", "Группа"] rows = [ ["1", "Заблокированный нос (FESS, полипы)", "300 оп · 30 млн", "Пред-оп бегунок → восстановление → контроль 3/6/12 мес", "C"], ["2", "Амбулаторный поток (острые + хроники)", "тысячи/мес · 120 млн", "План лечения + эпизод + процедуры самопомощи", "A"], ["3", "Дети с аденоидами", "400–500 оп · 20–30 млн", "Детский бегунок → восстановление (семейный профиль)", "C"], ["4", "Потеря слуха (сурдология)", "20 млн", "Паспорт аппарата, сервисный календарь, расходники, калибровка", "B"], ["5", "Сложные хроники, ЧБД, аллергия", "часть 120 млн · LTV", "Процедуры самопомощи + АСИТ-трекер + пыльцевой календарь", "A+D"], ["6", "Зависимость от капель (вазотомия)", "высокий объём", "Пред-оп бегунок (компактный) → восстановление", "C"], ["7", "Пульмонология (кашель/астма)", "средний · сезонный", "Дневник астмы, напоминания об ингаляторах", "D"], ["8", "Социально активные храпуны", "низкий · высокий чек", "Сомнологический чек-up, СИПАП-трекер", "—"], ["9", "Фониатрия (голос)", "очень низкий", "Короткое окно, низкая повторность", "—"], ["10", "Check-up и второе мнение", "единичный", "Разовая услуга, минимальная повторность", "—"], ] data = [header] + rows # раскраска групп через цвет текста в последней колонке делается потом tbl = add_table(s, MARGIN_L, y + Inches(0.1), CONTENT_W, Inches(5.6), data, col_widths=[Inches(0.4), Inches(3.4), Inches(2.2), Inches(5.5), Inches(0.8)], size=10, header_size=11) # Подкрасить колонку «Группа» group_colors = {"A": PRIMARY_DARK, "B": PRIMARY_DARK, "C": PRIMARY_DARK, "D": PRIMARY_DARK, "A+D": PRIMARY_DARK, "—": FG3} for ri, row in enumerate(rows, start=1): cell = tbl.cell(ri, 4) tf = cell.text_frame for p in tf.paragraphs: for r in p.runs: r.font.bold = True r.font.color.rgb = group_colors.get(row[4], FG) r.font.size = Pt(12) return s def slide_groups_overview(): s = new_slide() y = title_bar(s, "Четыре группы сегментов для повторных", "Укрупнение") # 4 плитки 2×2 tiles = [ ("A", "Процедуры самопомощи", "Амбулаторный + хроники (2, 5ч)", "Библиотека техник, напоминания, трекер, дневник.\nСамый массовый охват."), ("C", "Пред-оп подготовка", "FESS + аденоиды + вазотомия (1, 3, 6)", "Две фазы: возврат сомневающихся; бегунок → восстановление.\nОдин модуль — три сегмента."), ("B", "Сурдология", "Потеря слуха (4)", "Две фазы: возврат после демо-аппарата; обслуживание аппарата.\nИзолированный модуль, высокий чек."), ("D", "АСИТ + астма", "Часть аллергии + пульмо (5ч, 7)", "Ежедневный трекер, пыльцевой календарь, навигатор побочки.\nВысокая ответственность."), ] tw = (CONTENT_W - Inches(0.3)) / 2 th = (Inches(5.0) - Inches(0.3)) / 2 for i, (code, name, segments, desc) in enumerate(tiles): col = i % 2 row = i // 2 tx = MARGIN_L + (tw + Inches(0.3)) * col ty = y + Inches(0.1) + (th + Inches(0.3)) * row add_rect(s, tx, ty, tw, th, fill=BG, line=PRIMARY, line_w=Pt(1)) # Круг с буквой circ = s.shapes.add_shape(MSO_SHAPE.OVAL, tx + Inches(0.3), ty + Inches(0.3), Inches(0.8), Inches(0.8)) circ.shadow.inherit = False circ.fill.solid(); circ.fill.fore_color.rgb = PRIMARY circ.line.fill.background() tf = circ.text_frame tf.margin_left = tf.margin_right = Emu(0); tf.margin_top = tf.margin_bottom = Emu(0) p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER r = p.add_run(); r.text = code r.font.name = FONT_DISPLAY; r.font.size = Pt(28); r.font.bold = True r.font.color.rgb = BG # Название add_text(s, tx + Inches(1.3), ty + Inches(0.3), tw - Inches(1.5), Inches(0.5), name, size=20, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, tx + Inches(1.3), ty + Inches(0.8), tw - Inches(1.5), Inches(0.3), segments, size=11, color=FG3) # Описание add_text(s, tx + Inches(0.3), ty + Inches(1.5), tw - Inches(0.6), th - Inches(1.7), desc, size=12, color=FG2) return s def slide_group_detail(code, color, title, caption, what_it_does, why_priority): s = new_slide() y = title_bar(s, title, f"Группа {code}") # Крупный код слева add_rect(s, MARGIN_L, y, Inches(1.4), Inches(5.0), fill=PRIMARY_50) add_text(s, MARGIN_L, y + Inches(1.5), Inches(1.4), Inches(2.0), code, size=120, bold=True, color=PRIMARY, font=FONT_DISPLAY, align=PP_ALIGN.CENTER) # Правый контент rx = MARGIN_L + Inches(1.6) rw = CONTENT_W - Inches(1.6) add_text(s, rx, y + Inches(0.0), rw, Inches(0.4), caption, size=13, color=FG3) # Что делает add_text(s, rx, y + Inches(0.45), rw, Inches(0.35), "ЧТО В МОДУЛЕ", size=11, bold=True, color=PRIMARY_DARK) add_text(s, rx, y + Inches(0.8), rw, Inches(2.8), what_it_does, size=13, color=FG) # Почему приоритет add_rect(s, rx, y + Inches(3.7), rw, Inches(1.3), fill=WARM) add_text(s, rx + Inches(0.3), y + Inches(3.8), Inches(5), Inches(0.3), "ПОЧЕМУ В ЭТОМ ПОРЯДКЕ", size=11, bold=True, color=FG2) add_text(s, rx + Inches(0.3), y + Inches(4.1), rw - Inches(0.6), Inches(1.1), why_priority, size=13, color=FG) return s def slide_group_A(): return slide_group_detail( "A", PRIMARY, "Процедуры самопомощи для хроников", "Амбулаторный поток + сложные хроники · сегменты 2 и 5", "• Библиотека техник (видео + шаги): промывание носа физраствором, полоскание горла, " "ингаляции, гимнастика слуховой трубы, голосовые упражнения, туалет уха.\n" "• Напоминания утром/вечером, привязка к плану лечения.\n" "• Трекер комплаенса: ежедневные отметки, стрики, сводка.\n" "• Дневник симптомов в связке: «промываю 5 дней, насморк 4 → 2».\n" "• Сводка для врача перед контрольным приёмом.", "Самый массовый охват (тысячи/мес) × ежедневная повторность. " "Напрямую опирается на план лечения из Фазы 1. " "Контент-ориентированный MVP (видео + простой трекер). " "Эффект заметен пациенту сразу." ) def slide_group_C(): return slide_group_detail( "C", PRIMARY, "Пред-операционная подготовка + восстановление", "FESS + дети-аденоиды + вазотомия · сегменты 1, 3, 6", "Две фазы:\n" "1) Кандидаты на операцию — ушли «думать» после пред-оп приёма. Возврат через материалы, " "кейсы пациентов, чат с хирургом, возвратные push (3/7/21 день), прозрачность цены/рассрочки.\n" "2) Решившиеся — маршрутная карта («бегунок») с чекбоксами, сроки годности анализов, " "автозапись из пункта, загрузка сторонних результатов, чек-лист дня операции, " "договор и оплата в приложении → передача в «Восстановление».", "Один модуль закрывает 3 сегмента. Быстрый MVP (маршрутная карта — простой список с состояниями). " "Прямой бизнес-эффект: меньше переносов операций + возврат сомневающихся кандидатов. " "Вклад сегментов: ~50–60 млн." ) def slide_group_B(): return slide_group_detail( "B", PRIMARY, "Сурдология — слухопротезирование", "Потеря слуха · сегмент 4", "Две фазы, зеркально Группе C:\n" "1) Кандидаты — после первого визита и демо уходят думать 2–3 месяца. " "Аудиограмма в профиле, аудио-демо «до/после» в любых наушниках, каталог моделей с фильтром " "по аудиограмме, калькулятор стоимости (ФСС, рассрочка, трейд-ин), шеринг близкому, " "истории пациентов, возвратные push 3/7/21 день.\n" "2) Пользователи аппарата — паспорт аппарата, календарь обслуживания, расходники, " "навигатор неполадок, ежегодная калибровка.", "Пожизненный LTV × высокий чек (60–180 тыс.) × растущий рынок (старение). " "Изолированный модуль — не пересекается с другими. " "Можно запускать параллельно с C после Фазы 1." ) def slide_group_D(): return slide_group_detail( "D", PRIMARY, "АСИТ + контроль астмы", "Часть аллергии + пульмонология · сегменты 5ч и 7", "• Календарь курса (3–5 лет) с маркером «вы здесь».\n" "• Ежедневный приём СЛИТ с push, стрики.\n" "• Журнал инъекций ПКИТ.\n" "• Дневник симптомов + пыльцевой календарь региона.\n" "• Навигатор побочки с эскалацией к врачу / скорой.\n" "• Экстренная связь, аптечка дома.\n" "• Автошеринг дневника врачу перед плановым визитом.", "Самая высокая глубина пользы — прямое влияние на медицинский исход (удержание = успех лечения). " "Но самая высокая ответственность: требует верификации контента аллергологом/пульмонологом. " "Запускаем последним; контент можно готовить параллельно." ) def slide_roadmap(): s = new_slide() y = title_bar(s, "Порядок внедрения", "Дорожная карта") # Фаза 1 бокс p1_h = Inches(0.9) add_rect(s, MARGIN_L, y + Inches(0.05), CONTENT_W, p1_h, fill=PRIMARY_50, line=PRIMARY, line_w=Pt(1)) add_text(s, MARGIN_L + Inches(0.3), y + Inches(0.15), Inches(3), Inches(0.3), "ФАЗА 1 · ТРАНЗАКЦИОННАЯ БАЗА", size=11, bold=True, color=PRIMARY_DARK) add_text(s, MARGIN_L + Inches(0.3), y + Inches(0.48), CONTENT_W - Inches(0.6), Inches(0.4), "План лечения → Результаты и медкарта → Заказ справок", size=14, bold=True, color=FG, font=FONT_DISPLAY) # Фаза 1.5 бокс y15 = y + Inches(0.05) + p1_h + Inches(0.15) add_rect(s, MARGIN_L, y15, CONTENT_W, p1_h, fill=WARM, line=WARNING, line_w=Pt(1)) add_text(s, MARGIN_L + Inches(0.3), y15 + Inches(0.1), Inches(4), Inches(0.3), "ФАЗА 1.5 · КОММУНИКАЦИОННАЯ НАДСТРОЙКА", size=11, bold=True, color=WARNING) add_text(s, MARGIN_L + Inches(0.3), y15 + Inches(0.43), CONTENT_W - Inches(0.6), Inches(0.4), "Чат с медицинским консьержем → AI-помощник (shadow-mode)", size=14, bold=True, color=FG, font=FONT_DISPLAY) # Фаза 2 — 4 шага y2 = y15 + p1_h + Inches(0.25) add_text(s, MARGIN_L, y2, Inches(6), Inches(0.3), "ФАЗА 2 · СЕГМЕНТНЫЕ МОДУЛИ", size=11, bold=True, color=FG2) # 4 карточки горизонтально со стрелками steps = [ ("1", "A", "Процедуры самопомощи", "Массовый охват · продолжение плана лечения"), ("2", "C", "Пред-оп подготовка", "3 сегмента одним модулем · быстрый эффект"), ("3", "B", "Сурдология", "Высокий чек · пожизненное удержание"), ("4", "D", "АСИТ + астма", "Верификация врачом · высокая польза"), ] step_y = y2 + Inches(0.35) step_h = Inches(2.0) step_w = (CONTENT_W - Inches(0.4) * 3) / 4 for i, (num, code, name, sub) in enumerate(steps): sx = MARGIN_L + (step_w + Inches(0.4)) * i add_rect(s, sx, step_y, step_w, step_h, fill=BG, line=PRIMARY, line_w=Pt(1)) # Номер и код группы add_text(s, sx + Inches(0.2), step_y + Inches(0.2), Inches(0.8), Inches(0.4), f"Шаг {num}", size=11, color=FG3) add_text(s, sx + Inches(0.2), step_y + Inches(0.45), step_w - Inches(0.4), Inches(0.6), code, size=36, bold=True, color=PRIMARY, font=FONT_DISPLAY) add_text(s, sx + Inches(0.2), step_y + Inches(1.0), step_w - Inches(0.4), Inches(0.4), name, size=13, bold=True, color=FG, font=FONT_DISPLAY) add_text(s, sx + Inches(0.2), step_y + Inches(1.4), step_w - Inches(0.4), Inches(0.55), sub, size=10, color=FG2) # Стрелка if i < 3: arr_x = sx + step_w + Inches(0.05) arr = s.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, arr_x, step_y + step_h / 2 - Inches(0.15), Inches(0.3), Inches(0.3)) arr.shadow.inherit = False arr.fill.solid(); arr.fill.fore_color.rgb = PRIMARY arr.line.fill.background() return s def slide_questions(): s = new_slide() y = title_bar(s, "Вопросы к обсуждению", "Что уточнить у клиники") questions = [ ("Поток", "«2 из 3 повторных» — уникальные пациенты в год или визиты?"), ("Метрики", "Процент повторных, не доходящих до контрольного приёма?"), ("Врачи", "Готовность врачей к SLA по асинхронному чату и в каком окне?"), ("МИС", "API: структурированные назначения, сроки годности анализов, расписание?"), ("Сурдо", "Сколько кандидатов/мес, % возврата за аппаратом?"), ("Хирургия", "Частая причина переноса операций — просроченные анализы?"), ("Аллерго", "Готов ли аллерголог верифицировать контент АСИТ-трекера?"), ("Контент", "Достаточно ли материалов для RAG-базы?"), ] col_h = Inches(0.6) for i, (tag, q) in enumerate(questions): col = i % 2 row = i // 2 qx = MARGIN_L + (CONTENT_W / 2 + Inches(0.15)) * col qy = y + Inches(0.2) + (col_h + Inches(0.4)) * row qw = CONTENT_W / 2 - Inches(0.15) add_rect(s, qx, qy, Inches(1.0), col_h, fill=PRIMARY) add_text(s, qx, qy, Inches(1.0), col_h, tag, size=12, bold=True, color=BG, font=FONT_DISPLAY, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE) add_rect(s, qx + Inches(1.0), qy, qw - Inches(1.0), col_h, fill=PRIMARY_50) add_text(s, qx + Inches(1.2), qy, qw - Inches(1.2), col_h, q, size=12, color=FG, anchor=MSO_ANCHOR.MIDDLE) return s def slide_final(): s = new_slide() # Большой финал: что решить сегодня add_rect(s, 0, 0, SW, SH, fill=PRIMARY) add_text(s, MARGIN_L, Inches(0.8), CONTENT_W, Inches(0.5), "ЧТО РЕШАЕМ СЕГОДНЯ", size=14, bold=True, color=PRIMARY_50) add_text(s, MARGIN_L, Inches(1.4), CONTENT_W, Inches(1.2), "Подтвердить порядок:", size=36, bold=True, color=BG, font=FONT_DISPLAY) add_text(s, MARGIN_L, Inches(2.4), CONTENT_W, Inches(1.8), "Фаза 1 · транзакционная база\n→ Фаза 1.5 · консьерж + AI\n→ Фаза 2: A → C → B → D", size=26, bold=True, color=BG, font=FONT_DISPLAY, line_spacing=1.3) # Три пункта items = [ ("1", "С чего начинаем Фазу 1?", "План лечения — фундамент. Зависит от МИС-API."), ("2", "Когда стартует Фаза 1.5?", "После стабилизации базы. AI в shadow-mode."), ("3", "Параллельно", "Контент для D, добавить аллерголога в справочники."), ] y = Inches(4.8) for i, (num, q, a) in enumerate(items): ix = MARGIN_L + (CONTENT_W / 3) * i iw = CONTENT_W / 3 - Inches(0.3) add_text(s, ix, y, Inches(0.5), Inches(0.5), num, size=36, bold=True, color=WARM, font=FONT_DISPLAY) add_text(s, ix + Inches(0.6), y + Inches(0.05), iw - Inches(0.6), Inches(0.5), q, size=14, bold=True, color=BG, font=FONT_DISPLAY) add_text(s, ix + Inches(0.6), y + Inches(0.6), iw - Inches(0.6), Inches(1.2), a, size=12, color=PRIMARY_50) # Подпись add_text(s, MARGIN_L, SH - Inches(0.7), CONTENT_W, Inches(0.4), "Клиника УГН · мобильное приложение · 23 апр 2026", size=11, color=PRIMARY_50) return s # ============================================================ # СБОРКА # ============================================================ slides = [ slide_title(), slide_agenda(), slide_facts(), slide_frame(), slide_current_functions(), slide_three_layers(), slide_phase1_base(), slide_phase1_5_overlay(), slide_primary_vs_returning(), slide_segment_matrix(), slide_groups_overview(), slide_group_A(), slide_group_C(), slide_group_B(), slide_group_D(), slide_roadmap(), slide_questions(), slide_final(), ] # Футер на всех слайдах, кроме титульного и финального (у них свой фон) total = len(slides) for idx, s in enumerate(slides, start=1): if idx == 1 or idx == total: continue footer(s, idx, total) out = "MEETING_2026-04-23.pptx" prs.save(out) print(f"OK {out} ({total} slides)")