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>
This commit is contained in:
+913
@@ -0,0 +1,913 @@
|
||||
"""Собирает 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)")
|
||||
Reference in New Issue
Block a user