from __future__ import annotations import os import sys import time import zipfile import xml.etree.ElementTree as ET from pathlib import Path from pptx import Presentation from pptx.enum.shapes import MSO_SHAPE_TYPE TARGET_DIR = Path("public/ai-courseware-templates") TEMPLATES = [ ( "fairytale-forest.pptx", Path(r"H:\音乐教材PPT\人教版(2024)\1年级下册(新)\【2025新教材】人教版音乐一年级下册-《动画音乐会》-课件.pptx"), ), ("warm-handdrawn.pptx", Path(r"H:\音乐教材PPT\人音版(2024)\3年级上册(新)\音乐小屋.pptx")), ( "music-doodle.pptx", Path(r"H:\音乐教材PPT\人音版(2024)\7年级上册(新)\课件《溜冰圆舞曲》新版人音版七年级上册第二单元.pptx"), ), ("ink-wash.pptx", Path(r"H:\音乐教材PPT\人音版(2024)\8年级上册(新)\《姑苏行》《十面埋伏》.pptx")), ("stage-documentary.pptx", Path(r"H:\音乐教材PPT\人音版(2024)\8年级上册(新)\《鼓乐》.pptx")), ( "modern-geometry.pptx", Path(r"H:\音乐教材PPT\人教版(2024)\8年级上册(新)\学习项目二 探索旋律结构的规律.pptx"), ), ] def select_slide_indexes(total: int) -> set[int]: """保留正文版式,避开通常包含版权页的首尾页。""" if total <= 4: return set(range(total)) candidates = [1, round(total * 0.35), round(total * 0.6), total - 2] return {min(total - 1, max(0, index)) for index in candidates} def delete_shape(shape) -> None: element = shape._element element.getparent().remove(element) def clear_shape_text(shape, ordinal: list[int]) -> None: if shape.shape_type == MSO_SHAPE_TYPE.GROUP: for child in list(shape.shapes): clear_shape_text(child, ordinal) return if shape.shape_type in {MSO_SHAPE_TYPE.MEDIA, MSO_SHAPE_TYPE.WEB_VIDEO}: delete_shape(shape) return if not getattr(shape, "has_text_frame", False): return text_frame = shape.text_frame if not text_frame.text.strip(): return text_frame.clear() placeholder = "章节标题" if ordinal[0] == 0 else "在此填写教学内容" paragraph = text_frame.paragraphs[0] paragraph.text = placeholder ordinal[0] += 1 def remove_media_relationships(slide) -> None: for relation_id, relation in list(slide.part.rels.items()): relation_type = relation.reltype.lower() if any(token in relation_type for token in ("audio", "video", "media")): slide.part.drop_rel(relation_id) def remove_notes_parts(path: Path) -> None: """删除备注、课件标题等不可见信息,避免原课程信息随模板保留。""" temp_path = path.with_suffix(".cleaning.pptx") with zipfile.ZipFile(path, "r") as source, zipfile.ZipFile(temp_path, "w", zipfile.ZIP_DEFLATED) as target: for entry in source.infolist(): name = entry.filename if name.startswith(("ppt/notesSlides/", "ppt/notesMasters/")) or name == "docProps/app.xml": continue content = source.read(entry) if name.endswith(".rels"): root = ET.fromstring(content) for relation in list(root): relation_type = relation.attrib.get("Type", "") if relation_type.endswith(("/notesSlide", "/notesMaster", "/extended-properties")): root.remove(relation) content = ET.tostring(root, encoding="utf-8", xml_declaration=True) elif name == "[Content_Types].xml": root = ET.fromstring(content) for override in list(root): part_name = override.attrib.get("PartName", "") if "notesSlides" in part_name or "notesMasters" in part_name or part_name == "/docProps/app.xml": root.remove(override) content = ET.tostring(root, encoding="utf-8", xml_declaration=True) elif name.startswith("ppt/slides/slide") and name.endswith(".xml"): root = ET.fromstring(content) presentation_ns = "{http://schemas.openxmlformats.org/presentationml/2006/main}" drawing_ns = "{http://schemas.openxmlformats.org/drawingml/2006/main}" media_shape_tags = { f"{drawing_ns}audioFile", f"{drawing_ns}videoFile", "{http://schemas.microsoft.com/office/powerpoint/2010/main}media" } for parent in root.iter(): for child in list(parent): if child.tag != f"{presentation_ns}pic": continue if any(node.tag in media_shape_tags for node in child.iter()): parent.remove(child) for text_node in root.iter("{http://schemas.openxmlformats.org/drawingml/2006/main}t"): if text_node.text and text_node.text.strip(): text_node.text = "在此填写教学内容" for properties in root.iter(f"{presentation_ns}cNvPr"): properties.attrib.pop("descr", None) properties.attrib.pop("title", None) content = ET.tostring(root, encoding="utf-8", xml_declaration=True) target.writestr(entry, content) for attempt in range(5): try: os.replace(temp_path, path) return except PermissionError: if attempt == 4: raise time.sleep(1) def retain_selected_slides(presentation: Presentation, keep_indexes: set[int]) -> None: for index in reversed(range(len(presentation.slides))): if index in keep_indexes: continue relation_id = presentation.slides._sldIdLst[index].rId presentation.part.drop_rel(relation_id) del presentation.slides._sldIdLst[index] def sanitize(source: Path, output: Path) -> None: presentation = Presentation(source) retain_selected_slides(presentation, select_slide_indexes(len(presentation.slides))) for slide in presentation.slides: ordinal = [0] for shape in list(slide.shapes): clear_shape_text(shape, ordinal) remove_media_relationships(slide) presentation.core_properties.author = "系统模板" presentation.core_properties.last_modified_by = "系统模板" presentation.core_properties.subject = "音乐课件模板" presentation.core_properties.keywords = "" presentation.core_properties.comments = "" presentation.save(output) remove_notes_parts(output) def main() -> None: TARGET_DIR.mkdir(parents=True, exist_ok=True) if "--clean-output" in sys.argv: for output in sorted(TARGET_DIR.glob("*.pptx")): remove_notes_parts(output) print(f"{output.name}\t{output.stat().st_size / 1024 / 1024:.1f} MB") return for filename, source in TEMPLATES: output = TARGET_DIR / filename sanitize(source, output) print(f"{filename}\t{output.stat().st_size / 1024 / 1024:.1f} MB") if __name__ == "__main__": main()