Files
ONE-OS/scripts/generate_lingniu_cyberpunk_ppt_v2.py
王冕 a27e3b8e43 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>
2026-06-09 18:12:25 +08:00

611 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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,
[
("数智中心未来35年核心数字基础设施", 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()