build_music_template_masters.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. from __future__ import annotations
  2. from pathlib import Path
  3. from pptx import Presentation
  4. from pptx.dml.color import RGBColor
  5. from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
  6. from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
  7. from pptx.util import Inches, Pt
  8. OUT_DIR = Path("public/ai-courseware-templates")
  9. W, H = 13.333, 7.5
  10. def rgb(value: str) -> RGBColor:
  11. value = value.lstrip("#")
  12. return RGBColor(int(value[0:2], 16), int(value[2:4], 16), int(value[4:6], 16))
  13. def box(slide, x, y, w, h, color, radius=False, alpha=None, line=None):
  14. shape_type = MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE if radius else MSO_AUTO_SHAPE_TYPE.RECTANGLE
  15. shape = slide.shapes.add_shape(shape_type, Inches(x), Inches(y), Inches(w), Inches(h))
  16. shape.fill.solid()
  17. shape.fill.fore_color.rgb = rgb(color)
  18. if alpha is not None:
  19. shape.fill.transparency = alpha
  20. shape.line.fill.background() if line is None else setattr(shape.line.color, "rgb", rgb(line))
  21. return shape
  22. def circle(slide, x, y, d, color, alpha=None):
  23. shape = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.OVAL, Inches(x), Inches(y), Inches(d), Inches(d))
  24. shape.fill.solid()
  25. shape.fill.fore_color.rgb = rgb(color)
  26. if alpha is not None:
  27. shape.fill.transparency = alpha
  28. shape.line.fill.background()
  29. return shape
  30. def line(slide, x1, y1, x2, y2, color, width=1.5, alpha=None):
  31. shape = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2))
  32. shape.line.color.rgb = rgb(color)
  33. shape.line.width = Pt(width)
  34. if alpha is not None:
  35. shape.line.transparency = alpha
  36. return shape
  37. def text(slide, value, x, y, w, h, size=20, color="#1F2937", bold=False, font="Microsoft YaHei", align=PP_ALIGN.LEFT):
  38. shape = slide.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h))
  39. frame = shape.text_frame
  40. frame.clear()
  41. frame.word_wrap = True
  42. frame.vertical_anchor = MSO_ANCHOR.MIDDLE
  43. paragraph = frame.paragraphs[0]
  44. paragraph.alignment = align
  45. run = paragraph.add_run()
  46. run.text = value
  47. run.font.name = font
  48. run.font.size = Pt(size)
  49. run.font.bold = bold
  50. run.font.color.rgb = rgb(color)
  51. return shape
  52. def placeholder(slide, label, x, y, w, h, border, fill="#FFFFFF"):
  53. card = box(slide, x, y, w, h, fill, radius=True)
  54. card.line.color.rgb = rgb(border)
  55. card.line.width = Pt(1.2)
  56. text(slide, label, x + 0.18, y + h / 2 - 0.18, w - 0.36, 0.36, 12, border, True, align=PP_ALIGN.CENTER)
  57. return card
  58. def header(slide, title, eyebrow, palette, page_no, light=False):
  59. primary, secondary, accent, background, text_color = palette
  60. color = "#FFFFFF" if light else text_color
  61. text(slide, eyebrow.upper(), 0.65, 0.38, 3.8, 0.28, 9, secondary if not light else "#D7E8E5", True)
  62. text(slide, title, 0.65, 0.68, 8.7, 0.55, 25, color, True)
  63. 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)
  64. line(slide, 0.65, 1.35, 12.65, 1.35, secondary if not light else "#FFFFFF", 1, 0.45)
  65. def add_notes(slide, primary="#3478C8"):
  66. text(slide, "可替换标题", 0.7, 6.9, 1.3, 0.2, 8, primary, True)
  67. text(slide, "可替换正文 / 图片 / 互动题", 2.1, 6.9, 3.3, 0.2, 8, "#8A96A3")
  68. def fairytale(slide, page, palette):
  69. primary, secondary, accent, bg, dark = palette
  70. box(slide, 0, 0, W, H, bg)
  71. circle(slide, 10.9, -0.55, 3.0, "#F9D86D", 0.18)
  72. 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")]:
  73. circle(slide, x, y, d, c, 0.1)
  74. if page == 1:
  75. text(slide, "音乐小森林", 0.72, 1.05, 6.2, 0.7, 34, dark, True)
  76. text(slide, "用耳朵发现声音,用身体回应节奏", 0.75, 1.83, 5.6, 0.4, 17, primary)
  77. box(slide, 0.75, 2.7, 5.2, 0.9, "#FFFFFF", True)
  78. text(slide, "课题名称 / 班级 / 执教教师", 1.05, 2.95, 4.6, 0.36, 16, "#61736F", True, align=PP_ALIGN.CENTER)
  79. circle(slide, 8.3, 1.35, 2.1, "#F07F6A")
  80. circle(slide, 9.55, 2.45, 1.6, "#F7C96D")
  81. circle(slide, 7.35, 3.5, 1.25, "#49A795")
  82. text(slide, "♪", 8.88, 1.72, 0.7, 0.7, 42, "#FFFFFF", True, align=PP_ALIGN.CENTER)
  83. text(slide, "♫", 9.96, 2.77, 0.7, 0.7, 31, "#FFFFFF", True, align=PP_ALIGN.CENTER)
  84. elif page == 2:
  85. header(slide, "今天我们要做什么?", "学习目标", palette, page)
  86. for idx, value in enumerate(["听见声音的变化", "用动作表现节奏", "和同伴合作展示"]):
  87. circle(slide, 1.15, 2.05 + idx * 1.2, 0.58, [primary, secondary, accent][idx])
  88. text(slide, str(idx + 1), 1.15, 2.14 + idx * 1.2, 0.58, 0.28, 14, "#FFFFFF", True, align=PP_ALIGN.CENTER)
  89. placeholder(slide, value, 1.95, 1.95 + idx * 1.2, 8.2, 0.72, "#BFD8D1", "#FFFFFF")
  90. placeholder(slide, "主视觉 / 情境插画", 10.55, 1.95, 2.0, 3.95, "#6EBAA4", "#E7F4EE")
  91. else:
  92. header(slide, ["听一听,找一找", "动一动,拍一拍", "收获小星星", "课后继续玩音乐", "课堂回声"][page - 3], "课堂任务", palette, page)
  93. placeholder(slide, "主视觉 / 图片", 0.72, 1.75, 5.25, 4.4, "#BFD8D1", "#FFFFFF")
  94. placeholder(slide, "一句学生任务", 6.42, 1.75, 5.95, 0.75, "#BFD8D1", "#FFFFFF")
  95. for idx in range(3):
  96. placeholder(slide, f"步骤 {idx + 1} / 可替换短句", 6.42, 2.85 + idx * 0.95, 5.95, 0.66, "#BFD8D1", "#FFFFFF")
  97. add_notes(slide, primary)
  98. def warm(slide, page, palette):
  99. primary, secondary, accent, bg, dark = palette
  100. box(slide, 0, 0, W, H, bg)
  101. box(slide, 0, 0, W, 0.18, secondary)
  102. circle(slide, 10.6, 0.8, 1.55, "#F8D5B1", 0.15)
  103. circle(slide, 11.75, 1.72, 0.92, "#E9A7A7", 0.18)
  104. if page == 1:
  105. text(slide, "把音乐带进今天", 0.78, 1.0, 6.6, 0.72, 33, dark, True)
  106. text(slide, "温暖、清晰、适合师生共同阅读的音乐课堂", 0.82, 1.83, 6.4, 0.38, 16, "#5D6E76")
  107. placeholder(slide, "课题名称", 0.82, 2.73, 4.9, 0.8, "#8FBFB3", "#FFFFFF")
  108. box(slide, 7.25, 1.22, 4.8, 4.25, "#FFFFFF", True)
  109. text(slide, "♪", 8.25, 1.75, 2.6, 1.4, 66, primary, True, align=PP_ALIGN.CENTER)
  110. text(slide, "用一段旋律,开启一节课", 7.8, 3.55, 3.7, 0.42, 15, "#5B6E75", True, align=PP_ALIGN.CENTER)
  111. else:
  112. titles = ["学习地图", "感受这段音乐", "发现音乐小秘密", "合作练习", "我的课堂收获", "课后聆听"]
  113. header(slide, titles[page - 2], "温暖手绘", palette, page)
  114. if page in {2, 6}:
  115. for idx in range(3):
  116. placeholder(slide, f"{idx + 1}. 可替换学习要点", 0.9, 1.95 + idx * 1.05, 7.0, 0.68, "#9ECABD", "#FFFFFF")
  117. placeholder(slide, "插画 / 图片", 8.45, 1.95, 3.8, 3.75, "#9ECABD", "#ECF6F1")
  118. elif page == 5:
  119. for idx in range(4):
  120. box(slide, 0.9 + idx * 2.9, 2.15, 2.35, 1.8, "#FFFFFF", True)
  121. text(slide, f"第 {idx + 1} 步", 1.12 + idx * 2.9, 2.45, 1.9, 0.28, 12, secondary, True, align=PP_ALIGN.CENTER)
  122. text(slide, "可替换任务", 1.12 + idx * 2.9, 2.95, 1.9, 0.35, 15, dark, True, align=PP_ALIGN.CENTER)
  123. else:
  124. placeholder(slide, "主视觉 / 听赏素材", 0.9, 1.9, 5.2, 3.95, "#9ECABD", "#FFFFFF")
  125. placeholder(slide, "课堂问题", 6.65, 1.9, 5.55, 0.75, "#9ECABD", "#FFFFFF")
  126. placeholder(slide, "学生观察 / 回答区", 6.65, 2.95, 5.55, 2.9, "#9ECABD", "#FFFFFF")
  127. add_notes(slide, primary)
  128. def doodle(slide, page, palette):
  129. primary, secondary, accent, bg, dark = palette
  130. box(slide, 0, 0, W, H, "#17233D")
  131. 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)]:
  132. text(slide, symbol, x, y, 0.6, 0.5, size, "#FFFFFF", True)
  133. if page == 1:
  134. text(slide, "音符涂鸦课堂", 0.85, 1.05, 6.4, 0.72, 34, "#FFFFFF", True)
  135. text(slide, "让节奏、旋律和想法在黑板上跳起来", 0.88, 1.85, 6.5, 0.36, 16, "#D5E0F1")
  136. box(slide, 0.88, 2.72, 5.0, 0.85, "#FFFFFF", True)
  137. text(slide, "课题名称", 1.15, 2.95, 4.45, 0.36, 16, "#17233D", True, align=PP_ALIGN.CENTER)
  138. circle(slide, 8.5, 1.55, 2.15, accent)
  139. circle(slide, 9.85, 3.05, 1.35, secondary)
  140. text(slide, "♪", 9.12, 2.0, 0.85, 0.55, 36, "#17233D", True, align=PP_ALIGN.CENTER)
  141. else:
  142. titles = ["节奏挑战", "旋律发现", "听辨 A / B", "小组创作", "快速检测", "一句话收获"]
  143. header(slide, titles[page - 2], "music doodle", palette, page, light=True)
  144. if page in {2, 4, 6}:
  145. placeholder(slide, "可替换任务标题", 0.85, 1.85, 7.1, 0.75, "#93B0D8", "#FFFFFF")
  146. for idx in range(3):
  147. box(slide, 0.85, 2.95 + idx * 0.86, 0.55, 0.55, [accent, secondary, primary][idx], True)
  148. text(slide, str(idx + 1), 0.85, 3.07 + idx * 0.86, 0.55, 0.22, 12, "#17233D", True, align=PP_ALIGN.CENTER)
  149. placeholder(slide, "可替换步骤或选项", 1.62, 2.88 + idx * 0.86, 6.33, 0.68, "#93B0D8", "#FFFFFF")
  150. placeholder(slide, "节奏谱 / 图片", 8.45, 1.85, 3.7, 3.7, "#93B0D8", "#FFFFFF")
  151. else:
  152. placeholder(slide, "主视觉", 0.85, 1.85, 5.15, 3.95, "#93B0D8", "#FFFFFF")
  153. placeholder(slide, "观察问题", 6.5, 1.85, 5.65, 0.72, "#93B0D8", "#FFFFFF")
  154. placeholder(slide, "学生回答 / 对比结果", 6.5, 2.9, 5.65, 2.9, "#93B0D8", "#FFFFFF")
  155. add_notes(slide, "#FFFFFF")
  156. def ink(slide, page, palette):
  157. primary, secondary, accent, bg, dark = palette
  158. box(slide, 0, 0, W, H, "#F7F1E7")
  159. 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)]:
  160. box(slide, x, y, w, h, c, True, a)
  161. circle(slide, 11.75, 0.48, 0.48, "#9D2B22")
  162. if page == 1:
  163. text(slide, "听见东方的留白", 0.82, 1.1, 6.6, 0.76, 34, "#332A24", True, font="Microsoft YaHei")
  164. text(slide, "民乐、戏曲与传统文化主题的音乐课件", 0.85, 1.92, 6.2, 0.36, 16, "#746A5F")
  165. line(slide, 0.85, 2.6, 4.9, 2.6, accent, 2)
  166. placeholder(slide, "课题名称", 0.85, 3.0, 4.85, 0.78, "#B99058", "#FFFDF8")
  167. text(slide, "印", 11.86, 0.58, 0.27, 0.22, 9, "#FFFFFF", True, align=PP_ALIGN.CENTER)
  168. else:
  169. titles = ["入境:一幅声音画卷", "初听:听见什么", "乐器与音色", "文化线索", "表现与分享", "余韵"]
  170. header(slide, titles[page - 2], "东方音乐", palette, page)
  171. if page in {2, 5}:
  172. placeholder(slide, "意境主视觉 / 山水或乐器", 0.85, 1.85, 5.35, 3.95, "#B99058", "#FFFDF8")
  173. placeholder(slide, "一条核心问题", 6.7, 1.85, 5.35, 0.75, "#B99058", "#FFFDF8")
  174. for idx in range(3):
  175. placeholder(slide, f"可替换文化线索 {idx + 1}", 6.7, 2.95 + idx * 0.9, 5.35, 0.62, "#B99058", "#FFFDF8")
  176. else:
  177. for idx in range(3):
  178. box(slide, 1.05 + idx * 3.85, 2.05, 3.1, 2.65, "#FFFDF8", True)
  179. text(slide, f"线索 {idx + 1}", 1.35 + idx * 3.85, 2.35, 2.5, 0.3, 12, primary, True, align=PP_ALIGN.CENTER)
  180. text(slide, "可替换内容", 1.35 + idx * 3.85, 3.15, 2.5, 0.4, 16, dark, True, align=PP_ALIGN.CENTER)
  181. add_notes(slide, primary)
  182. def stage(slide, page, palette):
  183. primary, secondary, accent, bg, dark = palette
  184. box(slide, 0, 0, W, H, "#1C2940")
  185. circle(slide, 8.2, -1.05, 4.0, "#D8B45B", 0.7)
  186. circle(slide, 10.6, -0.5, 3.3, "#6B7E9E", 0.72)
  187. if page == 1:
  188. text(slide, "舞台正在发生", 0.85, 1.08, 7.0, 0.75, 35, "#FFFFFF", True)
  189. text(slide, "用现场感打开世界音乐与演出欣赏", 0.88, 1.95, 6.7, 0.36, 16, "#DDE6F4")
  190. box(slide, 0.88, 2.85, 5.2, 0.82, "#FFFFFF", True)
  191. text(slide, "课题名称", 1.15, 3.07, 4.65, 0.36, 16, dark, True, align=PP_ALIGN.CENTER)
  192. line(slide, 0.88, 4.35, 5.05, 4.35, accent, 2)
  193. else:
  194. titles = ["演出现场:先看再听", "声音从哪里来", "音乐要素拆解", "文化背景", "编排一段小演出", "谢幕与回响"]
  195. header(slide, titles[page - 2], "stage documentary", palette, page, light=True)
  196. if page in {2, 3, 5}:
  197. placeholder(slide, "横版演出照片 / 主视觉", 0.85, 1.85, 7.25, 3.85, "#C7D2E2", "#FFFFFF")
  198. placeholder(slide, "作品信息 / 核心问题", 8.55, 1.85, 3.65, 0.78, "#C7D2E2", "#FFFFFF")
  199. placeholder(slide, "听赏提示", 8.55, 2.98, 3.65, 1.25, "#C7D2E2", "#FFFFFF")
  200. placeholder(slide, "感受或证据", 8.55, 4.55, 3.65, 1.15, "#C7D2E2", "#FFFFFF")
  201. else:
  202. for idx in range(3):
  203. placeholder(slide, f"演出线索 {idx + 1}", 0.9 + idx * 3.9, 2.05, 3.2, 2.7, "#C7D2E2", "#FFFFFF")
  204. add_notes(slide, "#FFFFFF")
  205. def geometry(slide, page, palette):
  206. primary, secondary, accent, bg, dark = palette
  207. box(slide, 0, 0, W, H, "#F8FBFA")
  208. box(slide, 8.7, 0, 4.63, H, "#E7F3EF")
  209. box(slide, 9.55, 1.05, 2.8, 1.2, secondary, True, 0.15)
  210. box(slide, 10.85, 5.55, 1.8, 1.1, accent, True, 0.2)
  211. if page == 1:
  212. text(slide, "看见旋律的结构", 0.82, 1.1, 7.3, 0.74, 34, dark, True)
  213. text(slide, "用清晰关系图、时间线与对比框架组织知识", 0.86, 1.93, 6.8, 0.36, 16, "#657871")
  214. placeholder(slide, "课题名称", 0.86, 2.9, 4.85, 0.78, "#6FAE99", "#FFFFFF")
  215. for idx, x in enumerate([8.95, 10.25, 11.55]):
  216. circle(slide, x, 3.0 + idx * 0.58, 0.42, [primary, secondary, accent][idx])
  217. line(slide, x + 0.42, 3.21 + idx * 0.58, x + 1.0, 3.21 + idx * 0.58, primary, 1.2, 0.35)
  218. else:
  219. titles = ["学习路线", "旋律时间线", "结构对比", "规律发现", "项目任务", "结论与迁移"]
  220. header(slide, titles[page - 2], "modern geometry", palette, page)
  221. if page in {2, 3, 4}:
  222. placeholder(slide, "概念 / 主问题", 0.85, 1.78, 11.6, 0.72, "#7BB6A4", "#FFFFFF")
  223. for idx in range(4):
  224. x = 0.95 + idx * 3.0
  225. circle(slide, x, 3.15, 0.58, [primary, secondary, accent, primary][idx])
  226. text(slide, str(idx + 1), x, 3.3, 0.58, 0.18, 11, "#FFFFFF", True, align=PP_ALIGN.CENTER)
  227. placeholder(slide, f"结构节点 {idx + 1}", x - 0.35, 4.0, 1.28, 0.9, "#7BB6A4", "#FFFFFF")
  228. if idx < 3:
  229. line(slide, x + 0.58, 3.43, x + 2.75, 3.43, secondary, 1.5, 0.35)
  230. else:
  231. placeholder(slide, "左侧:素材 / 图表 / 旋律片段", 0.85, 1.85, 5.35, 3.85, "#7BB6A4", "#FFFFFF")
  232. placeholder(slide, "右侧:推理步骤与结论", 6.7, 1.85, 5.55, 3.85, "#7BB6A4", "#FFFFFF")
  233. add_notes(slide, primary)
  234. BUILDERS = {
  235. "fairytale-forest": (fairytale, ("#3478C8", "#32A07C", "#F08A3C", "#F7FBF8", "#24495C")),
  236. "warm-handdrawn": (warm, ("#347D73", "#8FC6B6", "#E98B73", "#F7FBFA", "#284C52")),
  237. "music-doodle": (doodle, ("#5164A7", "#80B9D7", "#F6C945", "#17233D", "#17233D")),
  238. "ink-wash": (ink, ("#9D2B22", "#36594B", "#C58A35", "#F7F1E7", "#332A24")),
  239. "stage-documentary": (stage, ("#D8B45B", "#8AA1C2", "#E7C469", "#1C2940", "#1C2940")),
  240. "modern-geometry": (geometry, ("#287B72", "#4FAD93", "#E07A35", "#F8FBFA", "#20342F")),
  241. }
  242. def build_template(template_id: str, builder, palette) -> None:
  243. presentation = Presentation()
  244. presentation.slide_width = Inches(W)
  245. presentation.slide_height = Inches(H)
  246. presentation.core_properties.title = f"{template_id} 音乐课件母版"
  247. presentation.core_properties.author = "系统模板"
  248. presentation.core_properties.subject = "可编辑音乐课件母版"
  249. blank = presentation.slide_layouts[6]
  250. for page in range(1, 8):
  251. slide = presentation.slides.add_slide(blank)
  252. builder(slide, page, palette)
  253. presentation.save(OUT_DIR / f"{template_id}.pptx")
  254. def main() -> None:
  255. OUT_DIR.mkdir(parents=True, exist_ok=True)
  256. for template_id, (builder, palette) in BUILDERS.items():
  257. build_template(template_id, builder, palette)
  258. print(f"{template_id}.pptx")
  259. if __name__ == "__main__":
  260. main()