from __future__ import annotations from pathlib import Path from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE from pptx.enum.text import PP_ALIGN, MSO_ANCHOR from pptx.util import Inches, Pt OUT_DIR = Path("public/ai-courseware-templates") W, H = 13.333, 7.5 def rgb(value: str) -> RGBColor: value = value.lstrip("#") return RGBColor(int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16)) def box(slide, x, y, w, h, color, radius=False, alpha=None, line=None): shape_type = MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE if radius else MSO_AUTO_SHAPE_TYPE.RECTANGLE shape = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h)) shape.fill.solid() shape.fill.fore_color.rgb = rgb(color) if alpha is not None: shape.fill.transparency = alpha shape.line.fill.background() if line is None else setattr(shape.line.color, "rgb", rgb(line)) return shape def circle(slide, x, y, d, color, alpha=None): shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.OVAL, Inches(x), Inches(y), Inches(d), Inches(d)) shape.fill.solid() shape.fill.fore_color.rgb = rgb(color) if alpha is not None: shape.fill.transparency = alpha shape.line.fill.background() return shape def line(slide, x1, y1, x2, y2, color, width=1.5, alpha=None): shape = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2)) shape.line.color.rgb = rgb(color) shape.line.width = Pt(width) if alpha is not None: shape.line.transparency = alpha return shape def text(slide, value, x, y, w, h, size=20, color="#1F2937", bold=False, font="Microsoft YaHei", align=PP_ALIGN.LEFT): shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) frame = shape.text_frame frame.clear() frame.word_wrap = True frame.vertical_anchor = MSO_ANCHOR.MIDDLE paragraph = frame.paragraphs[0] paragraph.alignment = align run = paragraph.add_run() run.text = value run.font.name = font run.font.size = Pt(size) run.font.bold = bold run.font.color.rgb = rgb(color) return shape def placeholder(slide, label, x, y, w, h, border, fill="#FFFFFF"): card = box(slide, x, y, w, h, fill, radius=True) card.line.color.rgb = rgb(border) card.line.width = Pt(1.2) text(slide, label, x + 0.18, y + h / 2 - 0.18, w - 0.36, 0.36, 12, border, True, align=PP_ALIGN.CENTER) return card def header(slide, title, eyebrow, palette, page_no, light=False): primary, secondary, accent, background, text_color = palette color = "#FFFFFF" if light else text_color text(slide, eyebrow.upper(), 0.65, 0.38, 3.8, 0.28, 9, secondary if not light else "#D7E8E5", True) text(slide, title, 0.65, 0.68, 8.7, 0.55, 25, color, True) text(slide, f"{page_no:02d}", 12.05, 0.46, 0.55, 0.35, 11, accent if not light else "#F7CE77", True, align=PP_ALIGN.RIGHT) line(slide, 0.65, 1.35, 12.65, 1.35, secondary if not light else "#FFFFFF", 1, 0.45) def add_notes(slide, primary="#3478C8"): text(slide, "可替换标题", 0.7, 6.9, 1.3, 0.2, 8, primary, True) text(slide, "可替换正文 / 图片 / 互动题", 2.1, 6.9, 3.3, 0.2, 8, "#8A96A3") def fairytale(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, bg) circle(slide, 10.9, -0.55, 3.0, "#F9D86D", 0.18) for x, y, d, c in [(0.0, 5.95, 2.2, "#A8D7A5"), (1.7, 6.2, 2.7, "#77BDA7"), (9.8, 6.05, 3.2, "#A8D7A5"), (11.3, 5.8, 2.4, "#77BDA7")]: circle(slide, x, y, d, c, 0.1) if page == 1: text(slide, "音乐小森林", 0.72, 1.05, 6.2, 0.7, 34, dark, True) text(slide, "用耳朵发现声音,用身体回应节奏", 0.75, 1.83, 5.6, 0.4, 17, primary) box(slide, 0.75, 2.7, 5.2, 0.9, "#FFFFFF", True) text(slide, "课题名称 / 班级 / 执教教师", 1.05, 2.95, 4.6, 0.36, 16, "#61736F", True, align=PP_ALIGN.CENTER) circle(slide, 8.3, 1.35, 2.1, "#F07F6A") circle(slide, 9.55, 2.45, 1.6, "#F7C96D") circle(slide, 7.35, 3.5, 1.25, "#49A795") text(slide, "♪", 8.88, 1.72, 0.7, 0.7, 42, "#FFFFFF", True, align=PP_ALIGN.CENTER) text(slide, "♫", 9.96, 2.77, 0.7, 0.7, 31, "#FFFFFF", True, align=PP_ALIGN.CENTER) elif page == 2: header(slide, "今天我们要做什么?", "学习目标", palette, page) for idx, value in enumerate(["听见声音的变化", "用动作表现节奏", "和同伴合作展示"]): circle(slide, 1.15, 2.05 + idx * 1.2, 0.58, [primary, secondary, accent][idx]) text(slide, str(idx + 1), 1.15, 2.14 + idx * 1.2, 0.58, 0.28, 14, "#FFFFFF", True, align=PP_ALIGN.CENTER) placeholder(slide, value, 1.95, 1.95 + idx * 1.2, 8.2, 0.72, "#BFD8D1", "#FFFFFF") placeholder(slide, "主视觉 / 情境插画", 10.55, 1.95, 2.0, 3.95, "#6EBAA4", "#E7F4EE") else: header(slide, ["听一听,找一找", "动一动,拍一拍", "收获小星星", "课后继续玩音乐", "课堂回声"][page - 3], "课堂任务", palette, page) placeholder(slide, "主视觉 / 图片", 0.72, 1.75, 5.25, 4.4, "#BFD8D1", "#FFFFFF") placeholder(slide, "一句学生任务", 6.42, 1.75, 5.95, 0.75, "#BFD8D1", "#FFFFFF") for idx in range(3): placeholder(slide, f"步骤 {idx + 1} / 可替换短句", 6.42, 2.85 + idx * 0.95, 5.95, 0.66, "#BFD8D1", "#FFFFFF") add_notes(slide, primary) def warm(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, bg) box(slide, 0, 0, W, 0.18, secondary) circle(slide, 10.6, 0.8, 1.55, "#F8D5B1", 0.15) circle(slide, 11.75, 1.72, 0.92, "#E9A7A7", 0.18) if page == 1: text(slide, "把音乐带进今天", 0.78, 1.0, 6.6, 0.72, 33, dark, True) text(slide, "温暖、清晰、适合师生共同阅读的音乐课堂", 0.82, 1.83, 6.4, 0.38, 16, "#5D6E76") placeholder(slide, "课题名称", 0.82, 2.73, 4.9, 0.8, "#8FBFB3", "#FFFFFF") box(slide, 7.25, 1.22, 4.8, 4.25, "#FFFFFF", True) text(slide, "♪", 8.25, 1.75, 2.6, 1.4, 66, primary, True, align=PP_ALIGN.CENTER) text(slide, "用一段旋律,开启一节课", 7.8, 3.55, 3.7, 0.42, 15, "#5B6E75", True, align=PP_ALIGN.CENTER) else: titles = ["学习地图", "感受这段音乐", "发现音乐小秘密", "合作练习", "我的课堂收获", "课后聆听"] header(slide, titles[page - 2], "温暖手绘", palette, page) if page in {2, 6}: for idx in range(3): placeholder(slide, f"{idx + 1}. 可替换学习要点", 0.9, 1.95 + idx * 1.05, 7.0, 0.68, "#9ECABD", "#FFFFFF") placeholder(slide, "插画 / 图片", 8.45, 1.95, 3.8, 3.75, "#9ECABD", "#ECF6F1") elif page == 5: for idx in range(4): box(slide, 0.9 + idx * 2.9, 2.15, 2.35, 1.8, "#FFFFFF", True) text(slide, f"第 {idx + 1} 步", 1.12 + idx * 2.9, 2.45, 1.9, 0.28, 12, secondary, True, align=PP_ALIGN.CENTER) text(slide, "可替换任务", 1.12 + idx * 2.9, 2.95, 1.9, 0.35, 15, dark, True, align=PP_ALIGN.CENTER) else: placeholder(slide, "主视觉 / 听赏素材", 0.9, 1.9, 5.2, 3.95, "#9ECABD", "#FFFFFF") placeholder(slide, "课堂问题", 6.65, 1.9, 5.55, 0.75, "#9ECABD", "#FFFFFF") placeholder(slide, "学生观察 / 回答区", 6.65, 2.95, 5.55, 2.9, "#9ECABD", "#FFFFFF") add_notes(slide, primary) def doodle(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, "#17233D") for x, y, symbol, size in [(0.7, 0.6, "♪", 22), (11.75, 0.62, "♫", 25), (11.65, 6.55, "♩", 19), (0.72, 6.5, "♬", 18)]: text(slide, symbol, x, y, 0.6, 0.5, size, "#FFFFFF", True) if page == 1: text(slide, "音符涂鸦课堂", 0.85, 1.05, 6.4, 0.72, 34, "#FFFFFF", True) text(slide, "让节奏、旋律和想法在黑板上跳起来", 0.88, 1.85, 6.5, 0.36, 16, "#D5E0F1") box(slide, 0.88, 2.72, 5.0, 0.85, "#FFFFFF", True) text(slide, "课题名称", 1.15, 2.95, 4.45, 0.36, 16, "#17233D", True, align=PP_ALIGN.CENTER) circle(slide, 8.5, 1.55, 2.15, accent) circle(slide, 9.85, 3.05, 1.35, secondary) text(slide, "♪", 9.12, 2.0, 0.85, 0.55, 36, "#17233D", True, align=PP_ALIGN.CENTER) else: titles = ["节奏挑战", "旋律发现", "听辨 A / B", "小组创作", "快速检测", "一句话收获"] header(slide, titles[page - 2], "music doodle", palette, page, light=True) if page in {2, 4, 6}: placeholder(slide, "可替换任务标题", 0.85, 1.85, 7.1, 0.75, "#93B0D8", "#FFFFFF") for idx in range(3): box(slide, 0.85, 2.95 + idx * 0.86, 0.55, 0.55, [accent, secondary, primary][idx], True) text(slide, str(idx + 1), 0.85, 3.07 + idx * 0.86, 0.55, 0.22, 12, "#17233D", True, align=PP_ALIGN.CENTER) placeholder(slide, "可替换步骤或选项", 1.62, 2.88 + idx * 0.86, 6.33, 0.68, "#93B0D8", "#FFFFFF") placeholder(slide, "节奏谱 / 图片", 8.45, 1.85, 3.7, 3.7, "#93B0D8", "#FFFFFF") else: placeholder(slide, "主视觉", 0.85, 1.85, 5.15, 3.95, "#93B0D8", "#FFFFFF") placeholder(slide, "观察问题", 6.5, 1.85, 5.65, 0.72, "#93B0D8", "#FFFFFF") placeholder(slide, "学生回答 / 对比结果", 6.5, 2.9, 5.65, 2.9, "#93B0D8", "#FFFFFF") add_notes(slide, "#FFFFFF") def ink(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, "#F7F1E7") for x, y, w, h, c, a in [(8.4, 5.8, 4.9, 1.2, "#36594B", 0.25), (9.7, 5.25, 3.7, 1.25, "#36594B", 0.38), (10.7, 4.85, 2.6, 1.1, "#9D2B22", 0.78)]: box(slide, x, y, w, h, c, True, a) circle(slide, 11.75, 0.48, 0.48, "#9D2B22") if page == 1: text(slide, "听见东方的留白", 0.82, 1.1, 6.6, 0.76, 34, "#332A24", True, font="Microsoft YaHei") text(slide, "民乐、戏曲与传统文化主题的音乐课件", 0.85, 1.92, 6.2, 0.36, 16, "#746A5F") line(slide, 0.85, 2.6, 4.9, 2.6, accent, 2) placeholder(slide, "课题名称", 0.85, 3.0, 4.85, 0.78, "#B99058", "#FFFDF8") text(slide, "印", 11.86, 0.58, 0.27, 0.22, 9, "#FFFFFF", True, align=PP_ALIGN.CENTER) else: titles = ["入境:一幅声音画卷", "初听:听见什么", "乐器与音色", "文化线索", "表现与分享", "余韵"] header(slide, titles[page - 2], "东方音乐", palette, page) if page in {2, 5}: placeholder(slide, "意境主视觉 / 山水或乐器", 0.85, 1.85, 5.35, 3.95, "#B99058", "#FFFDF8") placeholder(slide, "一条核心问题", 6.7, 1.85, 5.35, 0.75, "#B99058", "#FFFDF8") for idx in range(3): placeholder(slide, f"可替换文化线索 {idx + 1}", 6.7, 2.95 + idx * 0.9, 5.35, 0.62, "#B99058", "#FFFDF8") else: for idx in range(3): box(slide, 1.05 + idx * 3.85, 2.05, 3.1, 2.65, "#FFFDF8", True) text(slide, f"线索 {idx + 1}", 1.35 + idx * 3.85, 2.35, 2.5, 0.3, 12, primary, True, align=PP_ALIGN.CENTER) text(slide, "可替换内容", 1.35 + idx * 3.85, 3.15, 2.5, 0.4, 16, dark, True, align=PP_ALIGN.CENTER) add_notes(slide, primary) def stage(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, "#1C2940") circle(slide, 8.2, -1.05, 4.0, "#D8B45B", 0.7) circle(slide, 10.6, -0.5, 3.3, "#6B7E9E", 0.72) if page == 1: text(slide, "舞台正在发生", 0.85, 1.08, 7.0, 0.75, 35, "#FFFFFF", True) text(slide, "用现场感打开世界音乐与演出欣赏", 0.88, 1.95, 6.7, 0.36, 16, "#DDE6F4") box(slide, 0.88, 2.85, 5.2, 0.82, "#FFFFFF", True) text(slide, "课题名称", 1.15, 3.07, 4.65, 0.36, 16, dark, True, align=PP_ALIGN.CENTER) line(slide, 0.88, 4.35, 5.05, 4.35, accent, 2) else: titles = ["演出现场:先看再听", "声音从哪里来", "音乐要素拆解", "文化背景", "编排一段小演出", "谢幕与回响"] header(slide, titles[page - 2], "stage documentary", palette, page, light=True) if page in {2, 3, 5}: placeholder(slide, "横版演出照片 / 主视觉", 0.85, 1.85, 7.25, 3.85, "#C7D2E2", "#FFFFFF") placeholder(slide, "作品信息 / 核心问题", 8.55, 1.85, 3.65, 0.78, "#C7D2E2", "#FFFFFF") placeholder(slide, "听赏提示", 8.55, 2.98, 3.65, 1.25, "#C7D2E2", "#FFFFFF") placeholder(slide, "感受或证据", 8.55, 4.55, 3.65, 1.15, "#C7D2E2", "#FFFFFF") else: for idx in range(3): placeholder(slide, f"演出线索 {idx + 1}", 0.9 + idx * 3.9, 2.05, 3.2, 2.7, "#C7D2E2", "#FFFFFF") add_notes(slide, "#FFFFFF") def geometry(slide, page, palette): primary, secondary, accent, bg, dark = palette box(slide, 0, 0, W, H, "#F8FBFA") box(slide, 8.7, 0, 4.63, H, "#E7F3EF") box(slide, 9.55, 1.05, 2.8, 1.2, secondary, True, 0.15) box(slide, 10.85, 5.55, 1.8, 1.1, accent, True, 0.2) if page == 1: text(slide, "看见旋律的结构", 0.82, 1.1, 7.3, 0.74, 34, dark, True) text(slide, "用清晰关系图、时间线与对比框架组织知识", 0.86, 1.93, 6.8, 0.36, 16, "#657871") placeholder(slide, "课题名称", 0.86, 2.9, 4.85, 0.78, "#6FAE99", "#FFFFFF") for idx, x in enumerate([8.95, 10.25, 11.55]): circle(slide, x, 3.0 + idx * 0.58, 0.42, [primary, secondary, accent][idx]) line(slide, x + 0.42, 3.21 + idx * 0.58, x + 1.0, 3.21 + idx * 0.58, primary, 1.2, 0.35) else: titles = ["学习路线", "旋律时间线", "结构对比", "规律发现", "项目任务", "结论与迁移"] header(slide, titles[page - 2], "modern geometry", palette, page) if page in {2, 3, 4}: placeholder(slide, "概念 / 主问题", 0.85, 1.78, 11.6, 0.72, "#7BB6A4", "#FFFFFF") for idx in range(4): x = 0.95 + idx * 3.0 circle(slide, x, 3.15, 0.58, [primary, secondary, accent, primary][idx]) text(slide, str(idx + 1), x, 3.3, 0.58, 0.18, 11, "#FFFFFF", True, align=PP_ALIGN.CENTER) placeholder(slide, f"结构节点 {idx + 1}", x - 0.35, 4.0, 1.28, 0.9, "#7BB6A4", "#FFFFFF") if idx < 3: line(slide, x + 0.58, 3.43, x + 2.75, 3.43, secondary, 1.5, 0.35) else: placeholder(slide, "左侧:素材 / 图表 / 旋律片段", 0.85, 1.85, 5.35, 3.85, "#7BB6A4", "#FFFFFF") placeholder(slide, "右侧:推理步骤与结论", 6.7, 1.85, 5.55, 3.85, "#7BB6A4", "#FFFFFF") add_notes(slide, primary) BUILDERS = { "fairytale-forest": (fairytale, ("#3478C8", "#32A07C", "#F08A3C", "#F7FBF8", "#24495C")), "warm-handdrawn": (warm, ("#347D73", "#8FC6B6", "#E98B73", "#F7FBFA", "#284C52")), "music-doodle": (doodle, ("#5164A7", "#80B9D7", "#F6C945", "#17233D", "#17233D")), "ink-wash": (ink, ("#9D2B22", "#36594B", "#C58A35", "#F7F1E7", "#332A24")), "stage-documentary": (stage, ("#D8B45B", "#8AA1C2", "#E7C469", "#1C2940", "#1C2940")), "modern-geometry": (geometry, ("#287B72", "#4FAD93", "#E07A35", "#F8FBFA", "#20342F")), } def build_template(template_id: str, builder, palette) -> None: presentation = Presentation() presentation.slide_width = Inches(W) presentation.slide_height = Inches(H) presentation.core_properties.title = f"{template_id} 音乐课件母版" presentation.core_properties.author = "系统模板" presentation.core_properties.subject = "可编辑音乐课件母版" blank = presentation.slide_layouts[6] for page in range(1, 8): slide = presentation.slides.add_slide(blank) builder(slide, page, palette) presentation.save(OUT_DIR / f"{template_id}.pptx") def main() -> None: OUT_DIR.mkdir(parents=True, exist_ok=True) for template_id, (builder, palette) in BUILDERS.items(): build_template(template_id, builder, palette) print(f"{template_id}.pptx") if __name__ == "__main__": main()