#!/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""" style.visibility """.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()