v2 — real PowerPoint animations (fly-in, zoom, spin, bounce)

This commit is contained in:
Dauren777 2026-07-07 13:33:06 +00:00
parent d377502bc1
commit 3ab4d76e63
2 changed files with 401 additions and 283 deletions

View File

@ -1,13 +1,18 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Презентация о Казахстане — чистый PPTX без кривых XML-анимаций.""" """
Презентация о Казахстане с настоящими PowerPoint-анимациями.
Подход: создаём PPTX через python-pptx, затем распаковываем,
вставляем правильный <p:timing> XML и запаковываем обратно.
"""
from pptx import Presentation from pptx import Presentation
from pptx.util import Inches, Pt, Emu from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE from pptx.enum.shapes import MSO_SHAPE
from pptx.oxml.ns import qn from pptx.oxml.ns import qn
import lxml.etree as etree import zipfile, shutil, os, copy, tempfile
from lxml import etree
# ── Colors ── # ── Colors ──
INK = RGBColor(0x0F, 0x12, 0x18) INK = RGBColor(0x0F, 0x12, 0x18)
@ -15,329 +20,442 @@ CYAN = RGBColor(0x00, 0xE5, 0xFF)
GOLD = RGBColor(0xE8, 0xB7, 0x30) GOLD = RGBColor(0xE8, 0xB7, 0x30)
WHITE = RGBColor(0xFF, 0xFF, 0xFF) WHITE = RGBColor(0xFF, 0xFF, 0xFF)
GRAY = RGBColor(0x5B, 0x65, 0x73) GRAY = RGBColor(0x5B, 0x65, 0x73)
BG_DARK = RGBColor(0x14, 0x1C, 0x28) BG = RGBColor(0x14, 0x1C, 0x28)
prs = Presentation() prs = Presentation()
prs.slide_width = Inches(13.333) prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5) prs.slide_height = Inches(7.5)
W = prs.slide_width W = prs.slide_width
H = prs.slide_height H = prs.slide_height
BL = prs.slide_layouts[6]
BLANK_LAYOUT = prs.slide_layouts[6] # ── helpers ──
def bg(s, c):
s.background.fill.solid()
s.background.fill.fore_color.rgb = c
def txt(s, t, l, tp, w, h, sz=24, b=False, c=INK, a=PP_ALIGN.LEFT, fn='Calibri'):
tb = s.shapes.add_textbox(l, tp, w, h)
tf = tb.text_frame; tf.word_wrap = True
p = tf.paragraphs[0]; p.text = t
p.font.size = Pt(sz); p.font.bold = b; p.font.color.rgb = c; p.font.name = fn
p.alignment = a
return tb
def set_slide_transition(slide, transition_type='push', speed='med', advance_ms=None): def box(s, l, tp, w, h, fc=BG, bc=None):
"""Set slide transition effect.""" sh = s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, l, tp, w, h)
timing = slide._element.find(qn('p:transition')) sh.fill.solid(); sh.fill.fore_color.rgb = fc
if timing is None: if bc: sh.line.color.rgb = bc; sh.line.width = Pt(2)
timing = etree.SubElement(slide._element, qn('p:transition')) else: sh.line.fill.background()
return sh
transitions = { def circ(s, l, tp, sz, c):
'push': ('push', {'advClick': '1'}), sh = s.shapes.add_shape(MSO_SHAPE.OVAL, l, tp, sz, sz)
'fade': ('fade', {'advClick': '1'}), sh.fill.solid(); sh.fill.fore_color.rgb = c; sh.line.fill.background()
'wipe': ('wipe', {'dir': 'd', 'advClick': '1'}), return sh
'cover': ('cover', {'dir': 'l', 'advClick': '1'}),
'cut': ('cut', {'advClick': '1'}),
'dissolve': ('dissolve', {'advClick': '1'}),
'zoom': ('zoom', {'advClick': '1'}),
}
tag_name, attrs = transitions.get(transition_type, transitions['push'])
el = etree.SubElement(timing, qn(f'p:{tag_name}'))
for k, v in attrs.items():
el.set(k, v)
if advance_ms:
timing.set('advTm', str(advance_ms))
def rect(s, l, tp, w, h, c):
sh = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, l, tp, w, h)
sh.fill.solid(); sh.fill.fore_color.rgb = c; sh.line.fill.background()
return sh
def add_bg(slide, color): # counter for unique shape IDs
bg = slide.background _shape_id = [100]
fill = bg.fill def sid():
fill.solid() _shape_id[0] += 1
fill.fore_color.rgb = color return _shape_id[0]
# track shapes with their IDs for animation
anim_shapes = [] # list of (slide_idx, shape_id, animation_type, delay_ms, dur_ms)
def add_text(slide, text, left, top, width, height, def add_shape_with_anim(slide, slide_idx, shape, anim_type, delay_ms=0, dur_ms=500):
font_size=24, bold=False, color=INK, align=PP_ALIGN.LEFT, """Register a shape for animation. Returns the shape's spId."""
font_name='Calibri'): sp = shape.shape_id
txBox = slide.shapes.add_textbox(left, top, width, height) anim_shapes.append((slide_idx, sp, anim_type, delay_ms, dur_ms))
tf = txBox.text_frame return sp
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_box(slide, left, top, width, height, fill_color=BG_DARK,
border_color=None):
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
def add_circle(slide, left, top, size, color):
c = slide.shapes.add_shape(MSO_SHAPE.OVAL, left, top, size, size)
c.fill.solid()
c.fill.fore_color.rgb = color
c.line.fill.background()
return c
def add_rect(slide, left, top, width, height, color):
r = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
r.fill.solid()
r.fill.fore_color.rgb = color
r.line.fill.background()
return r
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 1 — Title # SLIDE 1 — Title
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, BG)
add_bg(s, BG_DARK) add_shape_with_anim(s, 0, circ(s, Inches(10), Inches(0.5), Inches(3), RGBColor(0,61,92)), 'spin', 300, 2000)
set_slide_transition(s, 'zoom') add_shape_with_anim(s, 0, circ(s, Inches(10.5), Inches(1), Inches(2), CYAN), 'spin', 500, 2000)
add_shape_with_anim(s, 0, txt(s, '🇰🇿', Inches(1), Inches(1.2), Inches(2), Inches(1.5), sz=80, c=WHITE), 'zoom', 100, 800)
add_circle(s, Inches(9.5), Inches(0.8), Inches(4.5), RGBColor(0x00, 0x3D, 0x5C)) add_shape_with_anim(s, 0, txt(s, 'КАЗАХСТАН', Inches(1), Inches(2.8), Inches(8), Inches(1.5), sz=72, b=True, c=WHITE), 'zoom', 400, 1000)
add_circle(s, Inches(10.2), Inches(1.5), Inches(3), CYAN) add_shape_with_anim(s, 0, txt(s, 'Сердце Евразии — девятая по территории страна мира', Inches(1), Inches(4.3), Inches(8), Inches(0.8), sz=24, c=GRAY), 'fly', 700, 800)
add_circle(s, Inches(-0.5), Inches(5.5), Inches(3), RGBColor(0x00, 0x3D, 0x5C)) for i, (v, l) in enumerate([('2.7 млн км²','Площадь'),('20 млн','Население'),('130+','Народов'),('1 космодром','Байконур')]):
add_circle(s, Inches(0), Inches(6), Inches(2), GOLD) x = Inches(1 + i*3); y = Inches(5.6)
b = box(s, x, y, Inches(2.7), Inches(1.2), RGBColor(28,38,54))
add_text(s, '🇰🇿', Inches(1), Inches(1.2), Inches(2), Inches(1.5), font_size=80, color=WHITE) txt(s, v, x+Inches(.2), y+Inches(.1), Inches(2.3), Inches(.6), sz=24, b=True, c=CYAN)
add_text(s, 'КАЗАХСТАН', Inches(1), Inches(2.8), Inches(8), Inches(1.5), txt(s, l, x+Inches(.2), y+Inches(.65), Inches(2.3), Inches(.4), sz=13, c=GRAY)
font_size=72, bold=True, color=WHITE) add_shape_with_anim(s, 0, b, 'bounce', 900+i*150, 600)
add_text(s, 'Сердце Евразии — девятая по территории страна мира',
Inches(1), Inches(4.3), Inches(8), Inches(0.8),
font_size=24, color=GRAY)
stats = [('2.7 млн км²', 'Площадь'), ('20 млн', 'Население'),
('130+', 'Народов'), ('1 космодром', 'Байконур')]
for i, (val, lbl) in enumerate(stats):
x = Inches(1 + i * 3)
y = Inches(5.6)
add_box(s, x, y, Inches(2.7), Inches(1.2), RGBColor(0x1C, 0x26, 0x36))
add_text(s, val, x + Inches(0.2), y + Inches(0.1), Inches(2.3), Inches(0.6),
font_size=24, bold=True, color=CYAN)
add_text(s, lbl, x + Inches(0.2), y + Inches(0.65), Inches(2.3), Inches(0.4),
font_size=13, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 2 — Geography # SLIDE 2 — Geography
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, WHITE)
add_bg(s, WHITE) rect(s, 0, 0, W, Inches(1.4), BG)
set_slide_transition(s, 'push') add_shape_with_anim(s, 1, txt(s, 'ГЕОГРАФИЯ', Inches(.8), Inches(.3), Inches(8), Inches(.8), sz=36, b=True, c=CYAN), 'fly', 100, 600)
geo = [('🏔️','Горы','Тянь-Шань, Алтай. Хан-Тенгри 7010 м'),
add_rect(s, 0, 0, W, Inches(1.4), BG_DARK) ('🌊','Каспий','Крупнейшее озеро мира. 1894 км'),
add_text(s, 'ГЕОГРАФИЯ', Inches(0.8), Inches(0.3), Inches(8), Inches(0.8), ('🏜️','Степи','Родина кочевников'),
font_size=36, bold=True, color=CYAN) ('🌾','Озёра','45 000+ озёр по всей стране')]
add_text(s, 'От каспийских берегов до алтайских гор', Inches(0.8), Inches(1.4),
Inches(8), Inches(0.5), font_size=16, color=GRAY)
geo = [
('🏔️', 'Горы', 'Тянь-Шань, Чу-Илийские горы, Алтай.\nВысшая точка — Хан-Тенгри (7010 м)'),
('🌊', 'Каспийское море', 'Крупнейшее озеро мира.\nПобережье тянется на 1894 км'),
('🏜️', 'Степи', 'Бескрайние равнины —\nисторическая родина кочевников'),
('🌾', 'Озёра', 'Балхаш, Алаколь, Маркаколь —\nболее 45 000 озёр'),
]
for i,(em,nm,ds) in enumerate(geo): for i,(em,nm,ds) in enumerate(geo):
col, row = i % 2, i // 2 c2,r2 = i%2, i//2
x = Inches(0.8 + col * 6.2) x = Inches(.8+c2*6.2); y = Inches(2.2+r2*2.5)
y = Inches(2.2 + row * 2.5) b = box(s, x, y, Inches(5.6), Inches(2.2), RGBColor(245,247,250))
add_box(s, x, y, Inches(5.6), Inches(2.2), RGBColor(0xF5, 0xF7, 0xFA)) txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=38)
add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7), txt(s, nm, x+Inches(1.3), y+Inches(.2), Inches(4), Inches(.5), sz=22, b=True, c=INK)
font_size=38) txt(s, ds, x+Inches(1.3), y+Inches(.75), Inches(4), Inches(1.2), sz=14, c=GRAY)
add_text(s, nm, x + Inches(1.3), y + Inches(0.2), Inches(4), Inches(0.5), add_shape_with_anim(s, 1, b, 'fly', 300+i*200, 700)
font_size=22, bold=True, color=INK)
add_text(s, ds, x + Inches(1.3), y + Inches(0.75), Inches(4), Inches(1.2),
font_size=14, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 3 — Facts # SLIDE 3 — Facts
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, BG)
add_bg(s, BG_DARK) add_shape_with_anim(s, 2, txt(s, 'ЦИФРЫ И ФАКТЫ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=WHITE), 'zoom', 100, 600)
set_slide_transition(s, 'fade') facts = [('9','по величине\nв мире'),('1991','год\nнезависимости'),('70+','тонн золота\nв год'),('1','космодром\nБайконур'),('3','столицы\nв истории')]
for i,(n,t) in enumerate(facts):
add_text(s, 'ЦИФРЫ И ФАКТЫ', Inches(0.8), Inches(0.6), Inches(10), Inches(1), x = Inches(.5+i*2.5)
font_size=40, bold=True, color=WHITE) b = box(s, x, Inches(2.5), Inches(2.2), Inches(3.5), RGBColor(28,38,54), CYAN)
add_text(s, 'Казахстан в цифрах', Inches(0.8), Inches(1.4), Inches(6), Inches(0.5), txt(s, n, x+Inches(.2), Inches(2.9), Inches(1.8), Inches(1.2), sz=52, b=True, c=CYAN, a=PP_ALIGN.CENTER)
font_size=18, color=GRAY) txt(s, t, x+Inches(.2), Inches(4.2), Inches(1.8), Inches(1.4), sz=14, c=GRAY, a=PP_ALIGN.CENTER)
add_shape_with_anim(s, 2, b, 'zoom', 200+i*200, 800)
facts = [('9', 'по величине\nв мире'), ('1991', 'год\nнезависимости'),
('70+', 'тонн золота\nв год'), ('1', 'космодром\nБайконур'),
('3', 'столицы\nв истории')]
for i, (num, txt) in enumerate(facts):
x = Inches(0.5 + i * 2.5)
add_box(s, x, Inches(2.5), Inches(2.2), Inches(3.5),
RGBColor(0x1C, 0x26, 0x36), CYAN)
add_text(s, num, x + Inches(0.2), Inches(2.9), Inches(1.8), Inches(1.2),
font_size=52, bold=True, color=CYAN, align=PP_ALIGN.CENTER)
add_text(s, txt, x + Inches(0.2), Inches(4.2), Inches(1.8), Inches(1.4),
font_size=14, color=GRAY, align=PP_ALIGN.CENTER)
add_rect(s, Inches(0.8), Inches(6.5), Inches(11.7), Inches(0.04), CYAN)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 4 — Culture # SLIDE 4 — Culture
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, RGBColor(240,244,248))
add_bg(s, RGBColor(0xF0, 0xF4, 0xF8)) rect(s, 0, 0, W, Inches(1.4), BG)
set_slide_transition(s, 'cover') add_shape_with_anim(s, 3, txt(s, 'КУЛЬТУРА', Inches(.8), Inches(.3), Inches(10), Inches(.8), sz=36, b=True, c=GOLD), 'fly', 100, 600)
cult = [('🦅','Беркутчи','Охота с орлами — ЮНЕСКО'),('🎪','Юрта','Символ свободы кочевников'),
add_rect(s, 0, 0, W, Inches(1.4), BG_DARK) ('🎶','Домбра','Душа казахских кюев'),('🤼','Байге','Конные скачки'),
add_text(s, 'КУЛЬТУРА И ТРАДИЦИИ', Inches(0.8), Inches(0.3), Inches(10), Inches(0.8), ('🎭','Наурыз','7 блюд, 22 марта'),('📖','Абай','Поэт и мыслитель')]
font_size=36, bold=True, color=GOLD) for i,(em,nm,ds) in enumerate(cult):
c2,r2 = i%3, i//3
culture = [ x = Inches(.8+c2*4.1); y = Inches(1.8+r2*2.6)
('🦅', 'Беркутчи', 'Соколиная охота с орлами — наследие ЮНЕСКО'), b = box(s, x, y, Inches(3.7), Inches(2.2), WHITE)
('🎪', 'Юрта', 'Легко разборное жилище — символ свободы'), txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=36)
('🎶', 'Домбра', 'Двухструнный инструмент — душа кюев'), txt(s, nm, x+Inches(1.2), y+Inches(.2), Inches(2.2), Inches(.5), sz=20, b=True, c=INK)
('🤼', 'Байге', 'Конные скачки на длинные дистанции'), txt(s, ds, x+Inches(.3), y+Inches(1), Inches(3.2), Inches(1), sz=14, c=GRAY)
('🎭', 'Наурыз', 'Праздник весны — 7 блюд, 22 марта'), add_shape_with_anim(s, 3, b, 'fly', 250+i*180, 700)
('📖', 'Абай', 'Великий поэт, философ, мыслитель'),
]
for i, (em, nm, ds) in enumerate(culture):
col, row = i % 3, i // 3
x = Inches(0.8 + col * 4.1)
y = Inches(1.8 + row * 2.6)
add_box(s, x, y, Inches(3.7), Inches(2.2), WHITE)
add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7),
font_size=36)
add_text(s, nm, x + Inches(1.2), y + Inches(0.2), Inches(2.2), Inches(0.5),
font_size=20, bold=True, color=INK)
add_text(s, ds, x + Inches(0.3), y + Inches(1), Inches(3.2), Inches(1),
font_size=14, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 5 — Cities # SLIDE 5 — Cities
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, WHITE)
add_bg(s, WHITE) add_shape_with_anim(s, 4, txt(s, 'ГОРОДА', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
set_slide_transition(s, 'push') cities = [('АСТАНА','Столица. Байтерек,\nХан-Шатыр',CYAN),('АЛМАТЫ','Горы, Медеу,\nШымбулак',GOLD),('ШЫМКЕНТ','Южные ворота,\nдревний город',RGBColor(42,157,143))]
for i,(nm,ds,cl) in enumerate(cities):
add_text(s, 'ГОРОДА', Inches(0.8), Inches(0.6), Inches(10), Inches(1), x = Inches(.8+i*4.1)
font_size=40, bold=True, color=INK) b = box(s, x, Inches(2.2), Inches(3.7), Inches(4.5), RGBColor(245,247,250))
add_text(s, 'Мегаполисы Казахстана', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5), rect(s, x, Inches(2.2), Inches(3.7), Inches(.12), cl)
font_size=18, color=GRAY) txt(s, nm, x+Inches(.4), Inches(2.7), Inches(3), Inches(.6), sz=28, b=True, c=INK)
txt(s, ds, x+Inches(.4), Inches(3.5), Inches(3), Inches(2.5), sz=15, c=GRAY)
cities = [ add_shape_with_anim(s, 4, b, 'fly', 300+i*250, 800)
('АСТАНА', 'Столица с 1997 г.\nБайтерек, Хан-Шатыр,\nфонтаны «Танкіри»', CYAN),
('АЛМАТЫ', 'Бывшая столица\nГоры, Медеу, Шымбулак,\nалматинские яблоки', GOLD),
('ШЫМКЕНТ', 'Третий город\nЮжные ворота,\nжаркий климат', RGBColor(0x2A, 0x9D, 0x8F)),
]
for i, (nm, ds, clr) in enumerate(cities):
x = Inches(0.8 + i * 4.1)
add_box(s, x, Inches(2.2), Inches(3.7), Inches(4.5), RGBColor(0xF5, 0xF7, 0xFA))
add_rect(s, x, Inches(2.2), Inches(3.7), Inches(0.12), clr)
add_text(s, nm, x + Inches(0.4), Inches(2.7), Inches(3), Inches(0.6),
font_size=28, bold=True, color=INK)
add_text(s, ds, x + Inches(0.4), Inches(3.5), Inches(3), Inches(2.5),
font_size=15, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 6 — Food # SLIDE 6 — Food
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, RGBColor(253,248,239))
add_bg(s, RGBColor(0xFD, 0xF8, 0xEF)) add_shape_with_anim(s, 5, txt(s, 'КУХНЯ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
set_slide_transition(s, 'wipe') foods = [('🥟','Бешбармак','Мясо с лапшой — едят руками'),('🫕','Кумыс','Кобылье молоко'),
('🫓','Баурсаки','Тестовые шарики на празднике'),('🍵','Чай с молоком','С солью — знак уважения')]
add_text(s, 'КУХНЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1),
font_size=40, bold=True, color=INK)
add_text(s, 'Вкус степи — просто и сытно', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5),
font_size=18, color=GRAY)
foods = [
('🥟', 'Бешбармак', 'Главное блюдо — мясо с лапшой.\n«Пять пальцев» — едят руками'),
('🫕', 'Кумыс', 'Ферментированное кобылье\nмолоко — тонизирующий напиток'),
('🫓', 'Баурсаки', 'Жареные тестовые шарики\nна каждом празднике'),
('🍵', 'Чай с молоком', 'С солью — гостям наливают\nпервым, знак уважения'),
]
for i,(em,nm,ds) in enumerate(foods): for i,(em,nm,ds) in enumerate(foods):
col, row = i % 2, i // 2 c2,r2 = i%2, i//2
x = Inches(0.8 + col * 6.2) x = Inches(.8+c2*6.2); y = Inches(2.2+r2*2.4)
y = Inches(2.2 + row * 2.4) b = box(s, x, y, Inches(5.6), Inches(2), WHITE, GOLD)
add_box(s, x, y, Inches(5.6), Inches(2), WHITE, GOLD) txt(s, em, x+Inches(.3), y+Inches(.15), Inches(1), Inches(.7), sz=38)
add_text(s, em, x + Inches(0.3), y + Inches(0.15), Inches(1), Inches(0.7), txt(s, nm, x+Inches(1.3), y+Inches(.2), Inches(3.5), Inches(.5), sz=22, b=True, c=INK)
font_size=38) txt(s, ds, x+Inches(1.3), y+Inches(.8), Inches(3.8), Inches(1), sz=14, c=GRAY)
add_text(s, nm, x + Inches(1.3), y + Inches(0.2), Inches(3.5), Inches(0.5), add_shape_with_anim(s, 5, b, 'bounce', 200+i*200, 700)
font_size=22, bold=True, color=INK)
add_text(s, ds, x + Inches(1.3), y + Inches(0.8), Inches(3.8), Inches(1),
font_size=14, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 7 — History # SLIDE 7 — History
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, WHITE)
add_bg(s, WHITE) add_shape_with_anim(s, 6, txt(s, 'ИСТОРИЯ', Inches(.8), Inches(.6), Inches(10), Inches(1), sz=40, b=True, c=INK), 'zoom', 100, 600)
set_slide_transition(s, 'dissolve') rect(s, Inches(1.6), Inches(2.2), Inches(.06), Inches(4.8), CYAN)
tl = [('1465','Казахское ханство'),('XVIII','Российская империя'),('1955','Байконур'),
add_text(s, 'ИСТОРИЯ', Inches(0.8), Inches(0.6), Inches(10), Inches(1), ('1961','Гагарин в космосе'),('1991','Независимость'),('1997','Новая столица')]
font_size=40, bold=True, color=INK) for i,(yr,ttl) in enumerate(tl):
add_text(s, 'От кочевников до космоса', Inches(0.8), Inches(1.3), Inches(6), Inches(0.5), y = Inches(2.2+i*.8)
font_size=18, color=GRAY) add_shape_with_anim(s, 6, circ(s, Inches(1.4), y+Inches(.08), Inches(.45), CYAN if i%2==0 else GOLD), 'bounce', 200+i*150, 500)
txt(s, yr, Inches(2.2), y, Inches(1.2), Inches(.6), sz=16, b=True, c=INK)
add_rect(s, Inches(1.6), Inches(2.2), Inches(0.06), Inches(4.8), CYAN) txt(s, ttl, Inches(3.5), y, Inches(9), Inches(.6), sz=14, c=GRAY)
timeline = [
('1465', 'Казахское ханство', 'Формирование народа и кочевого уклада'),
('XVIII', 'Российская империя', 'Присоединение, оседлый образ жизни'),
('1955', 'Байконур', 'Запуск первого космодрома в мире'),
('1961', 'Гагарин', 'Первый полёт в космос с Байконура'),
('1991', 'Независимость', '16 декабря — последняя республика СССР'),
('1997', 'Новая столица', 'Перенос столицы в Акмолу (Астану)'),
]
for i, (yr, ttl, dsc) in enumerate(timeline):
y = Inches(2.2 + i * 0.8)
dot = add_circle(s, Inches(1.4), y + Inches(0.08), Inches(0.45),
CYAN if i % 2 == 0 else GOLD)
add_text(s, yr, Inches(2.2), y, Inches(1.2), Inches(0.6),
font_size=16, bold=True, color=INK)
add_text(s, f'{ttl}{dsc}', Inches(3.5), y, Inches(9), Inches(0.6),
font_size=14, color=GRAY)
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
# SLIDE 8 — Thank You # SLIDE 8 — Thank You
# ══════════════════════════════════════════════════════════════ # ══════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK_LAYOUT) s = prs.slides.add_slide(BL); bg(s, BG)
add_bg(s, BG_DARK) add_shape_with_anim(s, 7, circ(s, Inches(10), Inches(.5), Inches(3), RGBColor(0,61,92)), 'spin', 300, 2000)
set_slide_transition(s, 'zoom') add_shape_with_anim(s, 7, circ(s, Inches(10.5), Inches(1), Inches(2), CYAN), 'spin', 500, 2000)
add_shape_with_anim(s, 7, circ(s, Inches(.5), Inches(5), Inches(2.5), RGBColor(0,61,92)), 'spin', 400, 2000)
add_shape_with_anim(s, 7, circ(s, Inches(1), Inches(5.5), Inches(1.5), GOLD), 'spin', 600, 2000)
add_shape_with_anim(s, 7, txt(s, '🇰🇿', Inches(5.5), Inches(1), Inches(2.5), Inches(1.5), sz=80, c=WHITE, a=PP_ALIGN.CENTER), 'zoom', 200, 1000)
add_shape_with_anim(s, 7, txt(s, 'СПАСИБО ЗА ВНИМАНИЕ!', Inches(1), Inches(2.8), Inches(11.3), Inches(1.2), sz=48, b=True, c=WHITE, a=PP_ALIGN.CENTER), 'fly', 500, 900)
add_circle(s, Inches(10), Inches(0.5), Inches(3), RGBColor(0x00, 0x3D, 0x5C)) # ── Save initial PPTX ──
add_circle(s, Inches(10.5), Inches(1), Inches(2), CYAN) tmp_pptx = '/tmp/kz_initial.pptx'
add_circle(s, Inches(0.5), Inches(5), Inches(2.5), RGBColor(0x00, 0x3D, 0x5C)) prs.save(tmp_pptx)
add_circle(s, Inches(1), Inches(5.5), Inches(1.5), GOLD) print(f'✅ Created {len(prs.slides)} slides, {len(anim_shapes)} animated shapes')
add_text(s, '🇰🇿', Inches(5.5), Inches(1), Inches(2.5), Inches(1.5),
font_size=80, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, 'СПАСИБО ЗА ВНИМАНИЕ!', Inches(1), Inches(2.8), Inches(11.3), Inches(1.2),
font_size=48, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
add_text(s, 'Казахстан — древние традиции и современные мегаполисы,\nбескрайние степи и высокие горы',
Inches(2), Inches(4.2), Inches(9.3), Inches(1.2),
font_size=18, color=GRAY, align=PP_ALIGN.CENTER)
# ── Save ── # ══════════════════════════════════════════════════════════════
out = '/srv/opencode/workspaces/users/literally.ww/kazakhstan/Казахстан_Презентация.pptx' # STEP 2: Inject proper animation XML
prs.save(out) # ══════════════════════════════════════════════════════════════
print(f'✅ Saved: {out}')
print(f' Slides: {len(prs.slides)}') WORK = '/tmp/kz_pptx_work'
if os.path.exists(WORK):
shutil.rmtree(WORK)
os.makedirs(WORK)
# Unzip
with zipfile.ZipFile(tmp_pptx, 'r') as z:
z.extractall(WORK)
P = 'http://schemas.openxmlformats.org/presentationml/2006/main'
A = 'http://schemas.openxmlformats.org/drawingml/2006/main'
def make_fly_in_xml(sp_id, delay, dur):
"""Fly-in from left animation."""
return f'''<p:timing xmlns:p="{P}" xmlns:a="{A}">
<p:tnLst>
<p:par>
<p:cTn id="1" dur="indefinite" restart="never" nodeType="timingRoot">
<p:childTnLst>
<p:seq concurrent="1" nextAc="seek">
<p:cTn id="2" dur="indefinite" nodeType="mainSeq">
<p:childTnLst>'''
def make_anim_block(sp_id, delay, dur, anim_type):
"""Generate animation block for one shape."""
if anim_type == 'fly':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:animMotion origin="layout" path="left">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:vectOml><p:strRef><p:f>0</p:f></p:strRef></p:vectOml>
</p:animMotion>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'zoom':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:anim calcmode="lin" valueType="num">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:varLst>
<p:numVal formula="0"><p:ptVal/></p:numVal>
<p:numVal formula="1"><p:ptVal/></p:numVal>
</p:varLst>
</p:anim>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'spin':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="hold"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="1"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:animRotation>
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="3600000"/></p:to>
</p:animRotation>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
elif anim_type == 'bounce':
return f'''
<p:par>
<p:cTn id="{sp_id*10+1}" fill="hold">
<p:stCondLst><p:cond delay="{delay}"/></p:stCondLst>
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+2}" fill="hold">
<p:childTnLst>
<p:par>
<p:cTn id="{sp_id*10+3}" fill="hold">
<p:childTnLst>
<p:set>
<p:cBhvr>
<p:cTn id="{sp_id*10+4}" dur="1" fill="fail"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:to><p:strVal val="visible"/></p:to>
</p:set>
</p:childTnLst>
</p:cTn>
</p:par>
<p:anim calcmode="lin" valueType="num">
<p:cBhvr>
<p:cTn id="{sp_id*10+5}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:varLst>
<p:numVal formula="0"><p:ptVal/></p:numVal>
<p:numVal formula="1.2"><p:ptVal/></p:numVal>
<p:numVal formula="0.85"><p:ptVal/></p:numVal>
<p:numVal formula="1.05"><p:ptVal/></p:numVal>
<p:numVal formula="0.97"><p:ptVal/></p:numVal>
<p:numVal formula="1"><p:ptVal/></p:numVal>
</p:varLst>
</p:anim>
<p:animEffect transition="with" filter="appear">
<p:cBhvr>
<p:cTn id="{sp_id*10+6}" dur="{dur}"/>
<p:tgtEl><p:spTgt spid="{sp_id}"/></p:tgtEl>
</p:cBhvr>
<p:filter val="appear"/>
</p:animEffect>
</p:childTnLst>
</p:cTn>
</p:par>
</p:childTnLst>
</p:cTn>
</p:par>'''
return ''
# Process each slide
for slide_idx in range(8):
slide_path = os.path.join(WORK, 'ppt', 'slides', f'slide{slide_idx+1}.xml')
tree = etree.parse(slide_path)
root = tree.getroot()
# Get shapes on this slide
shapes_on_slide = [(sp_id, anim, delay, dur)
for (si, sp_id, anim, delay, dur) in anim_shapes
if si == slide_idx]
if not shapes_on_slide:
continue
# Build timing XML
timing_parts = []
timing_parts.append(make_fly_in_xml(0, 0, 0)) # header
for sp_id, anim, delay, dur in shapes_on_slide:
timing_parts.append(make_anim_block(sp_id, delay, dur, anim))
timing_parts.append('''
</p:childTnLst>
</p:cTn>
<p:prevCondLst><p:cond evt="onPrev" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:prevCondLst>
<p:nextCondLst><p:cond evt="onNext" delay="0"><p:tgtEl><p:sldTgt/></p:tgtEl></p:cond></p:nextCondLst>
</p:seq>
</p:childTnLst>
</p:cTn>
</p:par>
</p:tnLst>
</p:timing>''')
timing_xml = ''.join(timing_parts)
timing_el = etree.fromstring(timing_xml)
root.append(timing_el)
tree.write(slide_path, xml_declaration=True, encoding='UTF-8', standalone=True)
print(f' ✓ Slide {slide_idx+1}: {len(shapes_on_slide)} animations injected')
# ══════════════════════════════════════════════════════════════
# STEP 3: Repackage
# ══════════════════════════════════════════════════════════════
out_pptx = '/srv/opencode/workspaces/users/literally.ww/kazakhstan/Казахстан_Презентация.pptx'
with zipfile.ZipFile(out_pptx, 'w', zipfile.ZIP_DEFLATED) as zout:
for dirpath, dirnames, filenames in os.walk(WORK):
for fn in filenames:
full = os.path.join(dirpath, fn)
arc = os.path.relpath(full, WORK)
zout.write(full, arc)
print(f'\n🎉 Saved: {out_pptx}')
print(f' {len(anim_shapes)} shape animations across 8 slides')
print(f' Animation types: fly-in, zoom, spin, bounce')
print(f' Open in PowerPoint → F5 → animations play on click/advance')