| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- 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()
|