kazakhstan/create_pptx.py

508 lines
26 KiB
Python
Raw 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.

#!/usr/bin/env python3
"""Презентация о Казахстане с 3D-анимациями (fly, zoom, spin, bounce)."""
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
import copy
from lxml import etree
# ── Colors ──
INK = RGBColor(0x0F, 0x12, 0x18)
CYAN = RGBColor(0x00, 0xE5, 0xFF)
GOLD = RGBColor(0xE8, 0xB7, 0x30)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GRAY = RGBColor(0x5B, 0x65, 0x73)
BG_DARK = RGBColor(0x14, 0x1C, 0x28)
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
W = prs.slide_width
H = prs.slide_height
# ── Animation XML builder ──
P = "http://schemas.openxmlformats.org/presentationml/2006/main"
A = "http://schemas.openxmlformats.org/drawingml/2006/main"
R = "http://schemas.openxmlformats.org/officeDocument/2006/relationships"
MC = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xml_ns = f'xmlns:p="{P}" xmlns:a="{A}" xmlns:r="{R}" xmlns:mc="{MC}"'
def _anim_seq_node():
"""Create <p:timing><p:tnLst><p:par>...</p:par></p:tnLst></p:timing>."""
return etree.SubElement(
etree.Element("dummy"),
f"{{{P}}}timing"
)
def add_animation(slide, anim_type, target_idx, delay_ms=0, dur_ms=500, **kwargs):
"""
Add entrance animation to shape at target_idx.
anim_type: 'flyIn', 'zoom', 'spin', 'float', 'bounce', 'appear', 'fade'
"""
nsmap = {'p': P, 'a': A, 'r': R, 'mc': MC}
# Find or create timing element
timing = slide._element.find(f'{{{P}}}timing')
if timing is None:
timing = etree.SubElement(slide._element, f'{{{P}}}timing')
tnLst = timing.find(f'{{{P}}}tnLst')
if tnLst is None:
tnLst = etree.SubElement(timing, f'{{{P}}}tnLst')
par_main = tnLst.find(f'{{{P}}}par')
if par_main is None:
par_main = etree.SubElement(tnLst, f'{{{P}}}par')
cTn_main = etree.SubElement(par_main, f'{{{P}}}cTn', id='1', dur='indefinite',
restart='never', nodeType='timingRoot')
etree.SubElement(cTn_main, f'{{{P}}}childTnLst')
childTnLst = par_main.find(f'{{{P}}}cTn/{{{P}}}childTnLst')
# Container par for this animation
par = etree.SubElement(childTnLst, f'{{{P}}}par')
cTn = etree.SubElement(par, f'{{{P}}}cTn', id=str(10 + target_idx * 3),
dur='indefinite', nodeType='withEffect')
stCondLst = etree.SubElement(cTn, f'{{{P}}}stCondLst')
etree.SubElement(stCondLst, f'{{{P}}}cond', delay='0')
childTn = etree.SubElement(cTn, f'{{{P}}}childTnLst')
# Inner par (the actual animation)
par2 = etree.SubElement(childTn, f'{{{P}}}par')
cTn2 = etree.SubElement(par2, f'{{{P}}}cTn', id=str(11 + target_idx * 3),
fill='hold')
stCondLst2 = etree.SubElement(cTn2, f'{{{P}}}stCondLst')
etree.SubElement(stCondLst2, f'{{{P}}}cond', delay=str(delay_ms))
childTn2 = etree.SubElement(cTn2, f'{{{P}}}childTnLst')
# Animation sequence
par3 = etree.SubElement(childTn2, f'{{{P}}}par')
cTn3 = etree.SubElement(par3, f'{{{P}}}cTn', id=str(12 + target_idx * 3),
fill='hold')
stCondLst3 = etree.SubElement(cTn3, f'{{{P}}}stCondLst')
etree.SubElement(stCondLst3, f'{{{P}}}cond', delay='0')
childTn3 = etree.SubElement(cTn3, f'{{{P}}}childTnLst')
# Set (make visible)
par_set = etree.SubElement(childTn3, f'{{{P}}}par')
cTn_set = etree.SubElement(par_set, f'{{{P}}}cTn', id=str(20 + target_idx),
fill='hold')
childTn_set = etree.SubElement(cTn_set, f'{{{P}}}childTnLst')
par_set2 = etree.SubElement(childTn_set, f'{{{P}}}par')
cTn_set2 = etree.SubElement(par_set2, f'{{{P}}}cTn', id=str(21 + target_idx))
stCondLst_set = etree.SubElement(cTn_set2, f'{{{P}}}stCondLst')
etree.SubElement(stCondLst_set, f'{{{P}}}cond', delay='0')
childTn_set2 = etree.SubElement(cTn_set2, f'{{{P}}}childTnLst')
set_el = etree.SubElement(childTn_set2, f'{{{P}}}set')
cBhvr_set = etree.SubElement(set_el, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_set, f'{{{P}}}cTn', id=str(22 + target_idx), dur='1')
tgtEl = etree.SubElement(cBhvr_set, f'{{{P}}}tgtEl')
spTgt = etree.SubElement(tgtEl, f'{{{P}}}spTgt', spid=str(target_idx + 2))
to = etree.SubElement(set_el, f'{{{P}}}to')
val_el = etree.SubElement(to, f'{{{P}}}val')
val_el.set(f'{{{A}}}val', '1')
# Main animation effect (animEffect)
anim_effect = etree.SubElement(childTn3, f'{{{P}}}animEffect')
anim_effect.set('transition', 'with')
anim_effect.set('filter', 'appear')
cBhvr_ae = etree.SubElement(anim_effect, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_ae, f'{{{P}}}cTn', id=str(30 + target_idx), dur=str(dur_ms))
tgtEl_ae = etree.SubElement(cBhvr_ae, f'{{{P}}}tgtEl')
spTgt_ae = etree.SubElement(tgtEl_ae, f'{{{P}}}spTgt', spid=str(target_idx + 2))
etree.SubElement(anim_effect, f'{{{P}}}filter', val='appear')
# Add actual motion/animation based on type
if anim_type == 'flyIn':
anim_motion = etree.SubElement(childTn3, f'{{{P}}}animMotion')
anim_motion.set('origin', 'layout')
anim_motion.set('path', 'left')
cBhvr_m = etree.SubElement(anim_motion, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_m, f'{{{P}}}cTn', id=str(31 + target_idx), dur=str(dur_ms))
etree.SubElement(cBhvr_m, f'{{{P}}}tgtEl')
ptLst = etree.SubElement(anim_motion, f'{{{P}}}ptLst')
pt_from = etree.SubElement(ptLst, f'{{{P}}}pt')
pt_from.set(f'{{{A}}}type', 'from')
from_x = kwargs.get('from_x', Inches(-6))
from_y = kwargs.get('from_y', 0)
to_el = etree.SubElement(ptLst, f'{{{P}}}pt')
to_el.set(f'{{{A}}}type', 'to')
pt_to = etree.SubElement(to_el, f'{{{P}}}pt')
pt_to.set('x', str(from_x))
pt_to.set('y', str(from_y))
elif anim_type == 'zoom':
anim_seq_zoom = etree.SubElement(childTn3, f'{{{P}}}animEffect')
anim_seq_zoom.set('transition', 'with')
anim_seq_zoom.set('filter', 'appear')
cBhvr_z = etree.SubElement(anim_seq_zoom, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_z, f'{{{P}}}cTn', id=str(31 + target_idx), dur=str(dur_ms))
tgtEl_z = etree.SubElement(cBhvr_z, f'{{{P}}}tgtEl')
etree.SubElement(tgtEl_z, f'{{{P}}}spTgt', spid=str(target_idx + 2))
etree.SubElement(anim_seq_zoom, f'{{{P}}}filter', val='appear')
elif anim_type == 'spin':
anim_spin = etree.SubElement(childTn3, f'{{{P}}}animRotation')
cBhvr_s = etree.SubElement(anim_spin, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_s, f'{{{P}}}cTn', id=str(31 + target_idx), dur=str(dur_ms))
etree.SubElement(cBhvr_s, f'{{{P}}}tgtEl')
anim_spin_to = etree.SubElement(anim_spin, f'{{{P}}}to')
anim_spin_to.set(f'{{{A}}}val', '3600000')
elif anim_type == 'bounce':
anim_bounce = etree.SubElement(childTn3, f'{{{P}}}animEffect')
anim_bounce.set('transition', 'with')
anim_bounce.set('filter', 'appear')
cBhvr_b = etree.SubElement(anim_bounce, f'{{{P}}}cBhvr')
etree.SubElement(cBhvr_b, f'{{{P}}}cTn', id=str(31 + target_idx), dur=str(dur_ms))
tgtEl_b = etree.SubElement(cBhvr_b, f'{{{P}}}tgtEl')
etree.SubElement(tgtEl_b, f'{{{P}}}spTgt', spid=str(target_idx + 2))
etree.SubElement(anim_bounce, f'{{{P}}}filter', val='appear')
def add_bg(slide, color):
bg = slide.background
fill = bg.fill
fill.solid()
fill.fore_color.rgb = color
def add_text(slide, text, left, top, width, height,
font_size=24, bold=False, color=INK, align=PP_ALIGN.LEFT,
font_name='Calibri'):
txBox = slide.shapes.add_textbox(left, top, width, height)
tf = txBox.text_frame
tf.word_wrap = True
p = tf.paragraphs[0]
p.text = text
p.font.size = Pt(font_size)
p.font.bold = bold
p.font.color.rgb = color
p.font.name = font_name
p.alignment = align
return txBox
def add_shape_box(slide, left, top, width, height, fill_color=BG_DARK,
border_color=None, radius=Inches(0.2)):
shape = slide.shapes.add_shape(
MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height
)
shape.fill.solid()
shape.fill.fore_color.rgb = fill_color
if border_color:
shape.line.color.rgb = border_color
shape.line.width = Pt(2)
else:
shape.line.fill.background()
return shape
# ══════════════════════════════════════════════════════════════
# SLIDE 1 — Title
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
add_bg(slide, BG_DARK)
# Decorative circle
circle = slide.shapes.add_shape(MSO_SHAPE.OVAL, Inches(9.5), Inches(1), Inches(4), Inches(4))
circle.fill.solid()
circle.fill.fore_color.rgb = RGBColor(0x00, 0x3D, 0x5C)
circle.line.fill.background()
s = slide.shapes.add_shape(MSO_SHAPE.OVAL, Inches(10), Inches(1.5), Inches(3), Inches(3))
s.fill.solid()
s.fill.fore_color.rgb = CYAN
s.line.fill.background()
add_animation(slide, 'spin', len(slide.shapes) - 1, delay_ms=300, dur_ms=1500)
# Flag emoji
flag = add_text(slide, '🇰🇿', Inches(0.8), Inches(1.5), Inches(2), Inches(1.5),
font_size=80, color=WHITE, align=PP_ALIGN.LEFT)
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=100, dur_ms=800)
# Title
title = add_text(slide, 'КАЗАХСТАН', Inches(0.8), Inches(3), Inches(8), Inches(1.5),
font_size=72, bold=True, color=WHITE, font_name='Calibri')
add_animation(slide, 'zoom', len(slide.shapes) - 1, delay_ms=400, dur_ms=1000)
# Subtitle
sub = add_text(slide, 'Сердце Евразии', Inches(0.8), Inches(4.5), Inches(6), Inches(1),
font_size=28, color=GRAY, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=700, dur_ms=800)
# Stats boxes
stats = [
('2.7 млн км²', 'Площадь'),
('20 млн', 'Население'),
('130+', 'Народов'),
('1 космодром', 'Байконур'),
]
for i, (val, label) in enumerate(stats):
x = Inches(0.8 + i * 3.1)
y = Inches(5.8)
box = add_shape_box(slide, x, y, Inches(2.8), Inches(1.2), RGBColor(0x1C, 0x26, 0x36))
add_text(slide, val, x + Inches(0.2), y + Inches(0.15), Inches(2.4), Inches(0.6),
font_size=26, bold=True, color=CYAN, font_name='Calibri')
add_text(slide, label, x + Inches(0.2), y + Inches(0.7), Inches(2.4), Inches(0.4),
font_size=13, color=GRAY, font_name='Calibri')
add_animation(slide, 'bounce', len(slide.shapes) - 1, delay_ms=900 + i * 150, dur_ms=600)
# ══════════════════════════════════════════════════════════════
# SLIDE 2 — Geography
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, WHITE)
# Section header bar
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, W, Inches(1.4))
bar.fill.solid()
bar.fill.fore_color.rgb = BG_DARK
bar.line.fill.background()
add_text(slide, 'ГЕОГРАФИЯ', Inches(0.8), Inches(0.3), Inches(6), Inches(0.8),
font_size=36, bold=True, color=CYAN, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=100, dur_ms=600)
geo_items = [
('🏔️', 'Горы', 'Тянь-Шань, Алтай. Высшая точка — Хан-Тенгри (7010 м)'),
('🌊', 'Каспийское море', 'Крупнейшее озеро мира. 1894 км побережья'),
('🏜️', 'Степи', 'Бескрайние равнины — историческая родина кочевников'),
('🌾', 'Озёра', 'Балхаш, Алаколь — более 45 000 озёр'),
]
for i, (emoji, name, desc) in enumerate(geo_items):
x = Inches(0.8 + (i % 2) * 6.2)
y = Inches(1.8 + (i // 2) * 2.6)
box = add_shape_box(slide, x, y, Inches(5.6), Inches(2.2),
RGBColor(0xF5, 0xF7, 0xFA))
add_text(slide, emoji, x + Inches(0.3), y + Inches(0.2), Inches(1), Inches(0.8),
font_size=40, font_name='Calibri')
add_text(slide, name, x + Inches(1.2), y + Inches(0.25), Inches(4), Inches(0.5),
font_size=22, bold=True, color=INK, font_name='Calibri')
add_text(slide, desc, x + Inches(1.2), y + Inches(0.8), Inches(4.2), Inches(1.2),
font_size=15, color=GRAY, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=300 + i * 200, dur_ms=700)
# ══════════════════════════════════════════════════════════════
# SLIDE 3 — Facts & Figures
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, BG_DARK)
add_text(slide, 'ЦИФРЫ И ФАКТЫ', Inches(0.8), Inches(0.6), Inches(10), Inches(1),
font_size=40, bold=True, color=WHITE, font_name='Calibri')
add_text(slide, 'Казахстан в цифрах', Inches(0.8), Inches(1.4), Inches(6), Inches(0.6),
font_size=18, color=GRAY, font_name='Calibri')
facts = [
('9', 'по величине\nв мире'),
('1991', 'год\nнезависимости'),
('70+', 'тонн золота\nв год'),
('1', 'космодром\nБайконур'),
('3', 'столицы\nв истории'),
]
for i, (num, txt) in enumerate(facts):
x = Inches(0.6 + i * 2.5)
y = Inches(2.5)
box = add_shape_box(slide, x, y, Inches(2.2), Inches(3.5),
RGBColor(0x1C, 0x26, 0x36), CYAN)
add_text(slide, num, x + Inches(0.2), y + Inches(0.4), Inches(1.8), Inches(1.2),
font_size=52, bold=True, color=CYAN, align=PP_ALIGN.CENTER, font_name='Calibri')
add_text(slide, txt, x + Inches(0.2), y + Inches(1.8), Inches(1.8), Inches(1.4),
font_size=14, color=GRAY, align=PP_ALIGN.CENTER, font_name='Calibri')
add_animation(slide, 'zoom', len(slide.shapes) - 1, delay_ms=200 + i * 200, dur_ms=800)
# Bottom decorative line
line = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
Inches(0.8), Inches(6.5), Inches(11.7), Inches(0.04))
line.fill.solid()
line.fill.fore_color.rgb = CYAN
line.line.fill.background()
# ══════════════════════════════════════════════════════════════
# SLIDE 4 — Culture
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, RGBColor(0xF0, 0xF4, 0xF8))
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, W, Inches(1.4))
bar.fill.solid()
bar.fill.fore_color.rgb = BG_DARK
bar.line.fill.background()
add_text(slide, 'КУЛЬТУРА И ТРАДИЦИИ', Inches(0.8), Inches(0.3), Inches(10), Inches(0.8),
font_size=36, bold=True, color=GOLD, font_name='Calibri')
culture_items = [
('🦅', 'Беркутчи', 'Соколиная охота с орлами — наследие ЮНЕСКО'),
('🎪', 'Юрта', 'Легко разборное жилище — символ свободы'),
('🎶', 'Домбра', 'Двухструнный инструмент — душа кюев'),
('🤼', 'Байге', 'Конные скачки на длинные дистанции'),
('🎭', 'Наурыз', 'Праздник весны — 7 блюд, 22 марта'),
('📖', 'Абай', 'Великий поэт, философ, мыслитель'),
]
for i, (emoji, name, desc) in enumerate(culture_items):
col = i % 3
row = i // 3
x = Inches(0.8 + col * 4.1)
y = Inches(1.8 + row * 2.6)
box = add_shape_box(slide, x, y, Inches(3.7), Inches(2.2), WHITE)
add_text(slide, emoji, x + Inches(0.3), y + Inches(0.2), Inches(1), Inches(0.8),
font_size=36, font_name='Calibri')
add_text(slide, name, x + Inches(1.2), y + Inches(0.25), Inches(2.2), Inches(0.5),
font_size=20, bold=True, color=INK, font_name='Calibri')
add_text(slide, desc, x + Inches(0.3), y + Inches(1), Inches(3.2), Inches(1),
font_size=14, color=GRAY, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=250 + i * 180, dur_ms=700)
# ══════════════════════════════════════════════════════════════
# SLIDE 5 — Cities
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, WHITE)
add_text(slide, 'ГОРОДА', Inches(0.8), Inches(0.6), Inches(10), Inches(1),
font_size=40, bold=True, color=INK, font_name='Calibri')
add_text(slide, 'Мегаполисы Казахстана', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5),
font_size=18, color=GRAY, font_name='Calibri')
cities = [
('АСТАНА', 'Столица с 1997 г.\nБайтерек, Хан-Шатыр,\nсамые высокие фонтаны Евразии', CYAN),
('АЛМАТЫ', 'Бывшая столица\nГоры за городом, Медеу,\nШымбулак, алматинские яблоки', GOLD),
('ШЫМКЕНТ', 'Третий город\nЮжные ворота, жаркий\nклимат, древняя история', RGBColor(0x2A, 0x9D, 0x8F)),
]
for i, (name, desc, accent) in enumerate(cities):
x = Inches(0.8 + i * 4.1)
y = Inches(2.2)
box = add_shape_box(slide, x, y, Inches(3.7), Inches(4.5),
RGBColor(0xF5, 0xF7, 0xFA))
# Color accent bar at top
accent_bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
x, y, Inches(3.7), Inches(0.12))
accent_bar.fill.solid()
accent_bar.fill.fore_color.rgb = accent
accent_bar.line.fill.background()
add_text(slide, name, x + Inches(0.4), y + Inches(0.5), Inches(3), Inches(0.6),
font_size=28, bold=True, color=INK, font_name='Calibri')
add_text(slide, desc, x + Inches(0.4), y + Inches(1.3), Inches(3), Inches(2.5),
font_size=15, color=GRAY, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=300 + i * 250, dur_ms=800)
# ══════════════════════════════════════════════════════════════
# SLIDE 6 — Food
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, RGBColor(0xFD, 0xF8, 0xEF))
add_text(slide, 'КУХНЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1),
font_size=40, bold=True, color=INK, font_name='Calibri')
add_text(slide, 'Вкус степи — просто и сытно', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5),
font_size=18, color=GRAY, font_name='Calibri')
foods = [
('🥟', 'Бешбармак', 'Главное блюдо — мясо с лапшой.\n«Пять пальцев» — едят руками'),
('🫕', 'Кумыс', 'Ферментированное кобылье\nмолоко — тонизирующий напиток'),
('🫓', 'Баурсаки', 'Жареные тестовые шарики\nна каждом празднике'),
('🍵', 'Чай с молоком', 'С солью — гостям наливают\nпервым, знак уважения'),
]
for i, (emoji, name, desc) in enumerate(foods):
col = i % 2
row = i // 2
x = Inches(0.8 + col * 6.2)
y = Inches(2.2 + row * 2.4)
box = add_shape_box(slide, x, y, Inches(5.6), Inches(2), WHITE,
RGBColor(0xE8, 0xB7, 0x30))
add_text(slide, emoji, x + Inches(0.3), y + Inches(0.2), Inches(1), Inches(0.8),
font_size=40, font_name='Calibri')
add_text(slide, name, x + Inches(1.3), y + Inches(0.2), Inches(3.5), Inches(0.5),
font_size=22, bold=True, color=INK, font_name='Calibri')
add_text(slide, desc, x + Inches(1.3), y + Inches(0.8), Inches(3.8), Inches(1),
font_size=14, color=GRAY, font_name='Calibri')
add_animation(slide, 'bounce', len(slide.shapes) - 1, delay_ms=200 + i * 200, dur_ms=700)
# ══════════════════════════════════════════════════════════════
# SLIDE 7 — History timeline
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, WHITE)
add_text(slide, 'ИСТОРИЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1),
font_size=40, bold=True, color=INK, font_name='Calibri')
add_text(slide, 'От кочевников до космоса', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5),
font_size=18, color=GRAY, font_name='Calibri')
# Vertical line
vline = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE,
Inches(1.6), Inches(2.2), Inches(0.06), Inches(4.8))
vline.fill.solid()
vline.fill.fore_color.rgb = CYAN
vline.line.fill.background()
timeline = [
('1465', 'Казахское ханство', 'Формирование народа и кочевого уклада'),
('XVIII', 'Российская империя', 'Присоединение, оседлый образ жизни'),
('1955', 'Байконур', 'Запуск первого космодрома в мире'),
('1961', 'Гагарин', 'Первый полёт в космос с Байконура'),
('1991', 'Независимость', '16 декабря — последняя республика СССР'),
('1997', 'Новая столица', 'Перенос столицы в Акмолу (Астану)'),
]
for i, (year, title, desc) in enumerate(timeline):
y = Inches(2.2 + i * 0.8)
# Dot
dot = slide.shapes.add_shape(MSO_SHAPE.OVAL,
Inches(1.4), y + Inches(0.08), Inches(0.45), Inches(0.45))
dot.fill.solid()
dot.fill.fore_color.rgb = CYAN if i % 2 == 0 else GOLD
dot.line.fill.background()
add_text(slide, year, Inches(2.2), y, Inches(1.2), Inches(0.6),
font_size=16, bold=True, color=INK, font_name='Calibri')
add_text(slide, f'{title}{desc}', Inches(3.5), y, Inches(9), Inches(0.6),
font_size=14, color=GRAY, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=200 + i * 150, dur_ms=600)
# ══════════════════════════════════════════════════════════════
# SLIDE 8 — Thank You
# ══════════════════════════════════════════════════════════════
slide = prs.slides.add_slide(prs.slide_layouts[6])
add_bg(slide, BG_DARK)
# Decorative circles
for cx, cy, sz, clr in [(10, 0.5, 3, RGBColor(0x00, 0x3D, 0x5C)),
(10.5, 1, 2, CYAN),
(0.5, 5, 2.5, RGBColor(0x00, 0x3D, 0x5C)),
(1, 5.5, 1.5, GOLD)]:
c = slide.shapes.add_shape(MSO_SHAPE.OVAL,
Inches(cx), Inches(cy), Inches(sz), Inches(sz))
c.fill.solid()
c.fill.fore_color.rgb = clr
c.line.fill.background()
add_animation(slide, 'spin', len(slide.shapes) - 1, delay_ms=500, dur_ms=2000)
flag = add_text(slide, '🇰🇿', Inches(5.5), Inches(1.2), Inches(2.5), Inches(1.5),
font_size=80, color=WHITE, align=PP_ALIGN.CENTER)
add_animation(slide, 'zoom', len(slide.shapes) - 1, delay_ms=200, dur_ms=1000)
add_text(slide, 'СПАСИБО ЗА ВНИМАНИЕ!', Inches(1), Inches(2.8), Inches(11.3), Inches(1.2),
font_size=48, bold=True, color=WHITE, align=PP_ALIGN.CENTER, font_name='Calibri')
add_animation(slide, 'flyIn', len(slide.shapes) - 1, delay_ms=500, dur_ms=900)
add_text(slide, 'Казахстан — древние традиции и современные мегаполисы,\nбескрайние степи и высокие горы',
Inches(2), Inches(4.2), Inches(9.3), Inches(1.2),
font_size=18, color=GRAY, align=PP_ALIGN.CENTER, font_name='Calibri')
add_animation(slide, 'fade', len(slide.shapes) - 1, delay_ms=800, dur_ms=800)
# ── Save ──
out = '/srv/opencode/workspaces/users/literally.ww/kazakhstan/Казахстан_Презентация.pptx'
prs.save(out)
print(f'✅ Saved: {out}')
print(f' Slides: {len(prs.slides)}')