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