Files
AR 15 M4 5e50fd5679 Add priorities meeting doc (23 Apr 2026) with PPTX deck
MEETING_2026-04-23.md — product roadmap proposal: gentleman's set (Phase 1
transactional base + Phase 1.5 communication overlay), 10 business segments
matrix (primary/returning lens), four segment groups A/C/B/D.

build_deck.py — python-pptx builder generating 18-slide 16:9 deck styled
to the clinic's design system (teal, PT Sans).

Also expands README with design system tokens and screens overview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 18:15:53 +05:00

914 lines
52 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Собирает 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 млн"),
("Детская аденоидная хирургия", "400500 оп/год", "~2030 млн"),
("Сурдология (слуховые аппараты)", "высокая конверсия", "~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, [
("Семейный профиль", "Критичен для 2030% (дети, пожилые)."),
("Контакты", "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, реклама, сарафан",
"Уведомление, пациент сам открывает"],
["Частота открытия",
"13 раза до визита",
"Ежедневно в активном эпизоде"],
["Что нужно в приложении",
"Базовый минимум (есть)",
"Специализированный модуль сегмента"],
["Приоритет развития",
"Низкий — веб работает лучше",
"Высокий — 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", "Дети с аденоидами", "400500 оп · 2030 млн",
"Детский бегунок → восстановление (семейный профиль)", "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)")