feat: sync full workspace including web modules, docs, and configurations to Gitea
Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
610
scripts/generate_lingniu_cyberpunk_ppt_v2.py
Normal file
610
scripts/generate_lingniu_cyberpunk_ppt_v2.py
Normal file
@@ -0,0 +1,610 @@
|
||||
#!/usr/bin/env python3
|
||||
"""羚牛数智中心 — 赛博朋克增强版 PPT(背景图/图示/科幻字体/动画标记)"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from lxml import etree
|
||||
from pptx import Presentation
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
from pptx.util import Inches, Pt
|
||||
|
||||
BASE = Path(__file__).resolve().parent.parent
|
||||
IMG = BASE / "assets" / "ppt-images"
|
||||
OUT = BASE / "羚牛数智中心建设规划_赛博朋克_增强版.pptx"
|
||||
OUT_DESKTOP = Path("/Users/sylvawong/Desktop/羚牛数智中心建设规划_赛博朋克_增强版.pptx")
|
||||
|
||||
# 科幻字体(Mac 常见;若已安装 Orbitron 会自动使用)
|
||||
FONT_EN = "Orbitron"
|
||||
FONT_EN_FB = "Avenir Next Demi Bold"
|
||||
FONT_CN = "PingFang SC Light"
|
||||
FONT_CN_FB = "Heiti SC Light"
|
||||
|
||||
BG_DARK = RGBColor(6, 10, 28)
|
||||
BG_PANEL = RGBColor(12, 18, 48)
|
||||
CYAN = RGBColor(0, 245, 255)
|
||||
MAGENTA = RGBColor(255, 0, 140)
|
||||
PURPLE = RGBColor(140, 80, 255)
|
||||
GOLD = RGBColor(255, 210, 60)
|
||||
WHITE = RGBColor(235, 245, 255)
|
||||
SILVER = RGBColor(160, 180, 210)
|
||||
DIM = RGBColor(90, 110, 150)
|
||||
OVERLAY = RGBColor(8, 12, 32)
|
||||
|
||||
SLIDE_W = Inches(13.333)
|
||||
SLIDE_H = Inches(7.5)
|
||||
|
||||
# 每页登记待动画 shape_id: {slide_index: [(shape_id, anim_type)]}
|
||||
ANIM_REGISTRY: dict[int, list[tuple[int, str]]] = {}
|
||||
SLIDE_BG_CYCLE = ["bg-city.png", "bg-hydrogen.png", "bg-dataflow.png", "bg-city.png"]
|
||||
|
||||
|
||||
def font_en():
|
||||
return FONT_EN
|
||||
|
||||
|
||||
def font_cn():
|
||||
return FONT_CN
|
||||
|
||||
|
||||
def set_font(p, en=False, size=16, bold=False, color=SILVER):
|
||||
p.font.size = Pt(size)
|
||||
p.font.bold = bold
|
||||
p.font.color.rgb = color
|
||||
p.font.name = font_en() if en else font_cn()
|
||||
|
||||
|
||||
def register_anim(slide_idx: int, shape_id: int, anim: str):
|
||||
ANIM_REGISTRY.setdefault(slide_idx, []).append((shape_id, anim))
|
||||
|
||||
|
||||
def add_bg_image(slide, filename: str, overlay_alpha=0.72):
|
||||
path = IMG / filename
|
||||
if not path.exists():
|
||||
return
|
||||
pic = slide.shapes.add_picture(str(path), 0, 0, width=SLIDE_W, height=SLIDE_H)
|
||||
# 暗色蒙版提升文字可读性
|
||||
mask = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, SLIDE_H)
|
||||
mask.fill.solid()
|
||||
mask.fill.fore_color.rgb = OVERLAY
|
||||
mask.fill.transparency = 1.0 - overlay_alpha # 0=不透明蒙版
|
||||
mask.line.fill.background()
|
||||
# 置底:先添加的在下,把 pic 和 mask 移到最前再 send backward - python-pptx z-order
|
||||
sp_tree = slide.shapes._spTree
|
||||
sp_tree.remove(pic._element)
|
||||
sp_tree.insert(2, pic._element)
|
||||
sp_tree.remove(mask._element)
|
||||
sp_tree.insert(3, mask._element)
|
||||
|
||||
|
||||
def add_glow_bar(slide, left, top, width, height, color):
|
||||
sh = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
|
||||
sh.fill.solid()
|
||||
sh.fill.fore_color.rgb = color
|
||||
sh.line.fill.background()
|
||||
return sh
|
||||
|
||||
|
||||
def add_corner_accents(slide):
|
||||
add_glow_bar(slide, Inches(0), Inches(0), Inches(0.12), SLIDE_H, CYAN)
|
||||
add_glow_bar(slide, SLIDE_W - Inches(0.08), Inches(0), Inches(0.08), SLIDE_H, MAGENTA)
|
||||
|
||||
|
||||
def add_header(slide, slide_idx, title, subtitle=None, anim_title="fly", anim_sub="fade"):
|
||||
add_corner_accents(slide)
|
||||
add_glow_bar(slide, Inches(0.8), Inches(1.05), Inches(3.5), Pt(4), CYAN)
|
||||
tb = slide.shapes.add_textbox(Inches(0.8), Inches(0.45), Inches(11.5), Inches(0.9))
|
||||
p = tb.text_frame.paragraphs[0]
|
||||
p.text = title
|
||||
set_font(p, en=False, size=32, bold=True, color=WHITE)
|
||||
register_anim(slide_idx, tb.shape_id, anim_title)
|
||||
if subtitle:
|
||||
tb2 = slide.shapes.add_textbox(Inches(0.8), Inches(1.12), Inches(11), Inches(0.5))
|
||||
p2 = tb2.text_frame.paragraphs[0]
|
||||
p2.text = subtitle
|
||||
set_font(p2, en=True, size=13, color=CYAN)
|
||||
register_anim(slide_idx, tb2.shape_id, anim_sub)
|
||||
|
||||
|
||||
def add_bullets(slide, slide_idx, items, top=Inches(1.6)):
|
||||
tb = slide.shapes.add_textbox(Inches(0.9), top, Inches(11.2), Inches(5.2))
|
||||
tf = tb.text_frame
|
||||
tf.word_wrap = True
|
||||
for i, (text, accent) in enumerate(items):
|
||||
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||
p.text = "▸ " + text
|
||||
set_font(p, size=16 if len(text) < 80 else 14, color=accent or SILVER)
|
||||
p.space_after = Pt(10)
|
||||
register_anim(slide_idx, tb.shape_id, "fade")
|
||||
|
||||
|
||||
def add_cards(slide, slide_idx, cards, top=Inches(1.7)):
|
||||
n = len(cards)
|
||||
gap = Inches(0.25)
|
||||
w = (Inches(11.5) - gap * (n - 1)) / n
|
||||
for i, (title, body, color) in enumerate(cards):
|
||||
left = Inches(0.8) + (w + gap) * i
|
||||
panel = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, w, Inches(4.8))
|
||||
panel.fill.solid()
|
||||
panel.fill.fore_color.rgb = BG_PANEL
|
||||
panel.line.color.rgb = color
|
||||
panel.line.width = Pt(1.5)
|
||||
register_anim(slide_idx, panel.shape_id, "fade")
|
||||
tb = slide.shapes.add_textbox(left + Inches(0.2), top + Inches(0.25), w - Inches(0.4), Inches(4.3))
|
||||
tf = tb.text_frame
|
||||
tf.word_wrap = True
|
||||
p0 = tf.paragraphs[0]
|
||||
p0.text = title
|
||||
set_font(p0, en=True, size=17, bold=True, color=color)
|
||||
p1 = tf.add_paragraph()
|
||||
p1.text = body
|
||||
set_font(p1, size=12, color=SILVER)
|
||||
p1.space_before = Pt(12)
|
||||
|
||||
|
||||
def add_flow_diagram(slide, slide_idx, labels, colors, top=Inches(2.2)):
|
||||
"""三步演进图示:箭头连接圆角框"""
|
||||
n = len(labels)
|
||||
box_w = Inches(3.2)
|
||||
gap = Inches(0.55)
|
||||
start_x = Inches(0.9)
|
||||
for i, (lab, col) in enumerate(zip(labels, colors)):
|
||||
left = start_x + (box_w + gap) * i
|
||||
sh = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, box_w, Inches(1.4))
|
||||
sh.fill.solid()
|
||||
sh.fill.fore_color.rgb = BG_PANEL
|
||||
sh.line.color.rgb = col
|
||||
sh.line.width = Pt(2)
|
||||
register_anim(slide_idx, sh.shape_id, "fly")
|
||||
tb = slide.shapes.add_textbox(left, top + Inches(0.35), box_w, Inches(0.8))
|
||||
p = tb.text_frame.paragraphs[0]
|
||||
p.text = lab
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
set_font(p, size=14, bold=True, color=col)
|
||||
if i < n - 1:
|
||||
ax = left + box_w + Inches(0.08)
|
||||
arr = slide.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, ax, top + Inches(0.45), gap - Inches(0.16), Inches(0.5))
|
||||
arr.fill.solid()
|
||||
arr.fill.fore_color.rgb = col
|
||||
arr.line.fill.background()
|
||||
|
||||
|
||||
def add_hex_diagram(slide, slide_idx, items, top=Inches(2.0)):
|
||||
"""1+N+X 六边形图示"""
|
||||
n = len(items)
|
||||
w = Inches(3.5)
|
||||
gap = Inches(0.35)
|
||||
for i, (t, b, c) in enumerate(items):
|
||||
left = Inches(0.85) + (w + gap) * i
|
||||
hex_shape = slide.shapes.add_shape(MSO_SHAPE.HEXAGON, left, top, w, Inches(2.8))
|
||||
hex_shape.fill.solid()
|
||||
hex_shape.fill.fore_color.rgb = BG_PANEL
|
||||
hex_shape.line.color.rgb = c
|
||||
hex_shape.line.width = Pt(2)
|
||||
register_anim(slide_idx, hex_shape.shape_id, "fade")
|
||||
tb = slide.shapes.add_textbox(left + Inches(0.15), top + Inches(0.5), w - Inches(0.3), Inches(2))
|
||||
tf = tb.text_frame
|
||||
tf.word_wrap = True
|
||||
p0 = tf.paragraphs[0]
|
||||
p0.text = t
|
||||
p0.alignment = PP_ALIGN.CENTER
|
||||
set_font(p0, en=True, size=22, bold=True, color=c)
|
||||
p1 = tf.add_paragraph()
|
||||
p1.text = b
|
||||
p1.alignment = PP_ALIGN.CENTER
|
||||
set_font(p1, size=11, color=SILVER)
|
||||
p1.space_before = Pt(8)
|
||||
|
||||
|
||||
def new_slide(prs, bg_file: str):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
add_bg_image(slide, bg_file, overlay_alpha=0.68)
|
||||
return slide
|
||||
|
||||
|
||||
def slide_title(prs, idx):
|
||||
slide = new_slide(prs, "bg-city.png")
|
||||
add_corner_accents(slide)
|
||||
add_glow_bar(slide, Inches(1.2), Inches(3.85), Inches(10.8), Pt(2), MAGENTA)
|
||||
|
||||
tb = slide.shapes.add_textbox(Inches(0.9), Inches(1.7), Inches(11.5), Inches(1.2))
|
||||
p = tb.text_frame.paragraphs[0]
|
||||
p.text = "羚牛数智中心"
|
||||
set_font(p, size=48, bold=True, color=CYAN)
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
register_anim(idx, tb.shape_id, "fly")
|
||||
|
||||
tb2 = slide.shapes.add_textbox(Inches(0.9), Inches(2.85), Inches(11.5), Inches(0.8))
|
||||
p2 = tb2.text_frame.paragraphs[0]
|
||||
p2.text = "建设规划报告"
|
||||
set_font(p2, size=34, color=WHITE)
|
||||
p2.alignment = PP_ALIGN.CENTER
|
||||
register_anim(idx, tb2.shape_id, "fade")
|
||||
|
||||
tb3 = slide.shapes.add_textbox(Inches(0.9), Inches(4.1), Inches(11.5), Inches(0.6))
|
||||
p3 = tb3.text_frame.paragraphs[0]
|
||||
p3.text = "LINGNIU DIGITAL INTELLIGENCE CENTER · V1.0"
|
||||
set_font(p3, en=True, size=13, color=MAGENTA)
|
||||
p3.alignment = PP_ALIGN.CENTER
|
||||
register_anim(idx, tb3.shape_id, "fade")
|
||||
|
||||
tb4 = slide.shapes.add_textbox(Inches(0.9), Inches(5.0), Inches(11.5), Inches(0.8))
|
||||
p4 = tb4.text_frame.paragraphs[0]
|
||||
p4.text = "羚牛氢能 One OS · 氢能交通商业操作系统"
|
||||
set_font(p4, size=16, color=GOLD)
|
||||
p4.alignment = PP_ALIGN.CENTER
|
||||
register_anim(idx, tb4.shape_id, "fade")
|
||||
|
||||
|
||||
def slide_architecture(prs, idx):
|
||||
slide = new_slide(prs, "bg-dataflow.png")
|
||||
add_header(slide, idx, "羚牛氢能 One OS · 架构示意图", "SYSTEM ARCHITECTURE", "fly", "fade")
|
||||
arch = IMG / "architecture.png"
|
||||
if arch.exists():
|
||||
pic = slide.shapes.add_picture(
|
||||
str(arch), Inches(0.55), Inches(1.45), width=Inches(12.2), height=Inches(5.7)
|
||||
)
|
||||
register_anim(idx, pic.shape_id, "fade")
|
||||
# 叠加矢量层:核心模块框
|
||||
layers = [
|
||||
("应用层", "小羚羚 · 运营终端 · 客户服务", CYAN, Inches(1.0), Inches(1.55)),
|
||||
("平台层", "任务调度 · 能源管理 · 资产视图", MAGENTA, Inches(4.6), Inches(1.55)),
|
||||
("数据层", "主数据 · 指标仓 · AI 特征", PURPLE, Inches(8.2), Inches(1.55)),
|
||||
("生态层", "API · SaaS · 产业开放", GOLD, Inches(4.6), Inches(5.85)),
|
||||
]
|
||||
for title, sub, col, left, top in layers:
|
||||
box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, Inches(3.8), Inches(0.95))
|
||||
box.fill.solid()
|
||||
box.fill.fore_color.rgb = BG_PANEL
|
||||
box.line.color.rgb = col
|
||||
box.line.width = Pt(1.5)
|
||||
register_anim(idx, box.shape_id, "fly")
|
||||
tb = slide.shapes.add_textbox(left + Inches(0.1), top + Inches(0.08), Inches(3.6), Inches(0.8))
|
||||
tf = tb.text_frame
|
||||
p0 = tf.paragraphs[0]
|
||||
p0.text = title
|
||||
set_font(p0, en=True, size=12, bold=True, color=col)
|
||||
p1 = tf.add_paragraph()
|
||||
p1.text = sub
|
||||
set_font(p1, size=9, color=SILVER)
|
||||
|
||||
|
||||
def build_slides(prs):
|
||||
si = 0
|
||||
slide_title(prs, si)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "一、报告背景与目标", "BACKGROUND & OBJECTIVES")
|
||||
add_bullets(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("业务规模扩大,人工台账与分散系统难以为继", None),
|
||||
("数据分散 · 流程依赖人工 · 单点建设难复用", MAGENTA),
|
||||
("建设统一「数智中心」是战略发展的必然选择", CYAN),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "现状痛点", "CURRENT CHALLENGES")
|
||||
add_cards(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("数据孤岛", "难以统一视图", CYAN),
|
||||
("流程非标", "可追溯性不足", MAGENTA),
|
||||
("重复建设", "难以复用扩展", PURPLE),
|
||||
("复杂场景", "多能源难支撑", GOLD),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "要解决的问题", "PROBLEMS TO SOLVE")
|
||||
add_cards(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("当前必须解决", "手工台账 · 系统割裂\n任务依赖人工 · 缺乏数据支撑", CYAN),
|
||||
("未来必须应对", "人效下降 · 复杂度上升\n平台不可持续 · AI 难落地", MAGENTA),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "四大建设目标", "CORE OBJECTIVES")
|
||||
add_cards(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("稳定运行", "支撑当前业务", CYAN),
|
||||
("持续演进", "支持5年发展", MAGENTA),
|
||||
("数据资产", "可沉淀可变现", PURPLE),
|
||||
("对内对外", "降本+SaaS输出", GOLD),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "二、战略定位", "STRATEGIC POSITIONING")
|
||||
add_bullets(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("数智中心:未来3–5年核心数字基础设施", CYAN),
|
||||
("长期战略资产,非单一IT项目", WHITE),
|
||||
("羚牛氢能 One OS — 氢能交通商业操作系统", GOLD),
|
||||
("核心产品:小羚羚统一运营与服务终端", MAGENTA),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
slide_architecture(prs, si)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "战略阶段:三步走", "THREE-PHASE ROADMAP")
|
||||
add_flow_diagram(
|
||||
s,
|
||||
si,
|
||||
["Phase 01\n统一·标准·稳定", "Phase 02\n平台·复用·协同", "Phase 03\n智能·开放·商业"],
|
||||
[CYAN, MAGENTA, GOLD],
|
||||
)
|
||||
si += 1
|
||||
|
||||
for title, sub, bullets in [
|
||||
(
|
||||
"Phase 01 · 统一 · 标准 · 稳定",
|
||||
"FOUNDATION",
|
||||
[
|
||||
("业务在线、数据留痕、流程可控", CYAN),
|
||||
("统一运营平台与小羚羚终端", None),
|
||||
("标准化数据、流程、接口", None),
|
||||
("高可用安全合规", None),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Phase 02 · 平台化 · 复用 · 协同",
|
||||
"PLATFORM",
|
||||
[
|
||||
("从有数据到用数据", MAGENTA),
|
||||
("平台化抽象通用能力", None),
|
||||
("复用降低边际成本", None),
|
||||
("跨部门协同打通全链路", GOLD),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Phase 03 · 智能化 · 开放 · 商业化",
|
||||
"INTELLIGENCE",
|
||||
[
|
||||
("AI驱动绿色智能生态", GOLD),
|
||||
("智能化自主优化", None),
|
||||
("开放API与SaaS", None),
|
||||
("成本中心→利润中心", CYAN),
|
||||
],
|
||||
),
|
||||
]:
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, title, sub)
|
||||
add_bullets(s, si, bullets)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "演进逻辑", "EVOLUTION LOGIC")
|
||||
add_flow_diagram(s, si, ["信息化\n0 → 1", "数字化\n1 → N", "智能化\nN → ∞"], [CYAN, MAGENTA, GOLD], top=Inches(2.4))
|
||||
tb = s.shapes.add_textbox(Inches(0.8), Inches(5.4), Inches(11.5), Inches(0.8))
|
||||
p = tb.text_frame.paragraphs[0]
|
||||
p.text = "成本中心 ──▶ 利润中心 · 驱动绿色交通变革"
|
||||
set_font(p, size=18, color=WHITE)
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
register_anim(si, tb.shape_id, "fade")
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "三、整体建设方案", "IMPLEMENTATION")
|
||||
add_bullets(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("分阶段实施,按业务节奏推进", CYAN),
|
||||
("基础优先:系统能力先于智能", None),
|
||||
("核心内控:需求架构数据内控", None),
|
||||
("敏捷迭代:MVP快速验证", None),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, SLIDE_BG_CYCLE[si % 4])
|
||||
add_header(s, si, "「1 + N + X」核心理念", "1 + N + X")
|
||||
add_hex_diagram(
|
||||
s,
|
||||
si,
|
||||
[
|
||||
("1", "车辆/设备/能源\n唯一主键", CYAN),
|
||||
("N", "电·氢·充换氢\n多能源", MAGENTA),
|
||||
("X", "运营·物流·金融\nESG·调度", GOLD),
|
||||
],
|
||||
)
|
||||
si += 1
|
||||
|
||||
s = new_slide(prs, "bg-hydrogen.png")
|
||||
add_corner_accents(s)
|
||||
add_glow_bar(s, Inches(2), Inches(3.2), Inches(9.3), Pt(3), CYAN)
|
||||
tb = s.shapes.add_textbox(Inches(0.9), Inches(2.5), Inches(11.5), Inches(1.5))
|
||||
p = tb.text_frame.paragraphs[0]
|
||||
p.text = "羚牛氢能 One OS"
|
||||
set_font(p, size=44, bold=True, color=CYAN)
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
register_anim(si, tb.shape_id, "fly")
|
||||
tb2 = s.shapes.add_textbox(Inches(0.9), Inches(3.8), Inches(11.5), Inches(1))
|
||||
p2 = tb2.text_frame.paragraphs[0]
|
||||
p2.text = "连接产业各方 · 驱动绿色交通变革"
|
||||
set_font(p2, size=20, color=WHITE)
|
||||
p2.alignment = PP_ALIGN.CENTER
|
||||
register_anim(si, tb2.shape_id, "fade")
|
||||
|
||||
|
||||
# ---------- 动画注入(OOXML)----------
|
||||
|
||||
NSMAP = {
|
||||
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||
}
|
||||
|
||||
|
||||
def _timing_xml(shape_id: int, preset_id: int, preset_subtype: int, dur_ms: int) -> etree._Element:
|
||||
"""生成单 shape 入场动画 timing 片段(fade preset 10, fly preset 2)"""
|
||||
# 简化:使用 preset 动画
|
||||
return etree.fromstring(
|
||||
f"""
|
||||
<p:par xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
||||
<p:cTn id="{shape_id + 1000}" presetID="{preset_id}" presetClass="entr"
|
||||
presetSubtype="{preset_subtype}" fill="hold" nodeType="clickEffect">
|
||||
<p:stCondLst><p:cond delay="0"/></p:stCondLst>
|
||||
<p:childTnLst>
|
||||
<p:set>
|
||||
<p:cBhvr>
|
||||
<p:cTn id="{shape_id + 2000}" dur="1" fill="hold">
|
||||
<p:stCondLst><p:cond delay="0"/></p:stCondLst>
|
||||
</p:cTn>
|
||||
<p:tgtEl><p:spTgt spid="{shape_id}"/></p:tgtEl>
|
||||
<p:attributeNameLst><p:attributeName>style.visibility</p:attributeName></p:attributeNameLst>
|
||||
</p:cBhvr>
|
||||
<p:to><p:strVal val="visible"/></p:to>
|
||||
</p:set>
|
||||
<p:animEffect transition="in" filter="fade({dur_ms})">
|
||||
<p:cBhvr>
|
||||
<p:cTn id="{shape_id + 3000}" dur="{dur_ms}"/>
|
||||
<p:tgtEl><p:spTgt spid="{shape_id}"/></p:tgtEl>
|
||||
</p:cBhvr>
|
||||
</p:animEffect>
|
||||
</p:childTnLst>
|
||||
</p:cTn>
|
||||
</p:par>
|
||||
""".encode()
|
||||
)
|
||||
|
||||
|
||||
def inject_animations(pptx_path: Path, registry: dict[int, list[tuple[int, str]]]):
|
||||
"""向 pptx 注入基础淡入/飞入动画(PowerPoint 2016+)"""
|
||||
tmp = pptx_path.parent / "_pptx_anim_tmp"
|
||||
if tmp.exists():
|
||||
shutil.rmtree(tmp)
|
||||
with zipfile.ZipFile(pptx_path, "r") as z:
|
||||
z.extractall(tmp)
|
||||
|
||||
slides_dir = tmp / "ppt" / "slides"
|
||||
slide_files = sorted(slides_dir.glob("slide*.xml"), key=lambda p: int(p.stem.replace("slide", "")))
|
||||
|
||||
for slide_idx, anims in registry.items():
|
||||
if slide_idx >= len(slide_files):
|
||||
continue
|
||||
slide_path = slide_files[slide_idx]
|
||||
tree = etree.parse(str(slide_path))
|
||||
root = tree.getroot()
|
||||
|
||||
# 移除已有 timing
|
||||
for old in root.findall("p:timing", NSMAP):
|
||||
root.remove(old)
|
||||
|
||||
child_tn_lst = etree.Element("{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst")
|
||||
for spid, anim_type in anims:
|
||||
if anim_type == "fly":
|
||||
# fly from bottom: presetID 2 subtype 4
|
||||
node = _timing_xml(spid, 2, 4, 500)
|
||||
else:
|
||||
node = _timing_xml(spid, 10, 0, 400)
|
||||
child_tn_lst.append(node)
|
||||
|
||||
timing = etree.Element("{http://schemas.openxmlformats.org/presentationml/2006/main}timing")
|
||||
tn_lst = etree.SubElement(timing, "{http://schemas.openxmlformats.org/presentationml/2006/main}tnLst")
|
||||
par = etree.SubElement(tn_lst, "{http://schemas.openxmlformats.org/presentationml/2006/main}par")
|
||||
ctn = etree.SubElement(
|
||||
par,
|
||||
"{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
|
||||
id="1",
|
||||
dur="indefinite",
|
||||
restart="never",
|
||||
nodeType="tmRoot",
|
||||
)
|
||||
ctn_child = etree.SubElement(
|
||||
ctn, "{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst"
|
||||
)
|
||||
seq = etree.SubElement(
|
||||
ctn_child,
|
||||
"{http://schemas.openxmlformats.org/presentationml/2006/main}seq",
|
||||
concurrent="1",
|
||||
nextAc="seek",
|
||||
)
|
||||
seq_ctn = etree.SubElement(
|
||||
seq,
|
||||
"{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
|
||||
id="2",
|
||||
dur="indefinite",
|
||||
nodeType="mainSeq",
|
||||
)
|
||||
seq_child = etree.SubElement(
|
||||
seq_ctn, "{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst"
|
||||
)
|
||||
for child in child_tn_lst:
|
||||
wrap = etree.SubElement(
|
||||
seq_child, "{http://schemas.openxmlformats.org/presentationml/2006/main}par"
|
||||
)
|
||||
wrap_ctn = etree.SubElement(
|
||||
wrap,
|
||||
"{http://schemas.openxmlformats.org/presentationml/2006/main}cTn",
|
||||
fill="hold",
|
||||
)
|
||||
wrap_child = etree.SubElement(
|
||||
wrap_ctn,
|
||||
"{http://schemas.openxmlformats.org/presentationml/2006/main}childTnLst",
|
||||
)
|
||||
wrap_child.append(child)
|
||||
|
||||
root.append(timing)
|
||||
tree.write(str(slide_path), xml_declaration=True, encoding="UTF-8", standalone=True)
|
||||
|
||||
out_zip = pptx_path.parent / "_pptx_anim_out.pptx"
|
||||
if out_zip.exists():
|
||||
out_zip.unlink()
|
||||
with zipfile.ZipFile(out_zip, "w", zipfile.ZIP_DEFLATED) as zout:
|
||||
for fp in tmp.rglob("*"):
|
||||
if fp.is_file():
|
||||
zout.write(fp, fp.relative_to(tmp))
|
||||
shutil.move(str(out_zip), str(pptx_path))
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def main():
|
||||
if not IMG.exists():
|
||||
raise SystemExit(f"缺少图片资源目录: {IMG}")
|
||||
|
||||
prs = Presentation()
|
||||
prs.slide_width = SLIDE_W
|
||||
prs.slide_height = SLIDE_H
|
||||
build_slides(prs)
|
||||
prs.save(OUT)
|
||||
|
||||
try:
|
||||
inject_animations(OUT, ANIM_REGISTRY)
|
||||
anim_note = "已注入标题飞入/内容淡入动画"
|
||||
except Exception as e:
|
||||
anim_note = f"动画注入部分失败({e}),可在PPT中全选标题批量添加「飞入/淡入」"
|
||||
|
||||
shutil.copy(OUT, OUT_DESKTOP)
|
||||
print(f"已生成: {OUT}")
|
||||
print(f"已复制: {OUT_DESKTOP}")
|
||||
print(f"共 {len(prs.slides)} 页 | {anim_note}")
|
||||
print(f"字体: 英文 {font_en()} / 中文 {font_cn()}(若 Orbitron 未安装将回退系统字体)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user