dsh-courseware 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/cordis.patch.yml +9 -0
  4. package/engine/pptxgen/__init__.py +8 -0
  5. package/engine/pptxgen/__main__.py +4 -0
  6. package/engine/pptxgen/builtin_template.py +218 -0
  7. package/engine/pptxgen/cli.py +575 -0
  8. package/engine/pptxgen/examples.py +290 -0
  9. package/engine/pptxgen/imagegen.py +564 -0
  10. package/engine/pptxgen/layoutmap.py +211 -0
  11. package/engine/pptxgen/patterns.py +106 -0
  12. package/engine/pptxgen/preview.py +126 -0
  13. package/engine/pptxgen/profile.py +456 -0
  14. package/engine/pptxgen/render.py +1673 -0
  15. package/engine/pptxgen/review.py +184 -0
  16. package/engine/pptxgen/schemacheck.py +122 -0
  17. package/engine/pptxgen/schemas/XAdES.xsd +466 -0
  18. package/engine/pptxgen/schemas/XAdESv141.xsd +15 -0
  19. package/engine/pptxgen/schemas/chartEx.xsd +838 -0
  20. package/engine/pptxgen/schemas/dml-chart.xsd +1499 -0
  21. package/engine/pptxgen/schemas/dml-chartDrawing.xsd +146 -0
  22. package/engine/pptxgen/schemas/dml-diagram.xsd +1085 -0
  23. package/engine/pptxgen/schemas/dml-drawing.xsd +63 -0
  24. package/engine/pptxgen/schemas/dml-lockedCanvas.xsd +11 -0
  25. package/engine/pptxgen/schemas/dml-main.xsd +3081 -0
  26. package/engine/pptxgen/schemas/dml-picture.xsd +23 -0
  27. package/engine/pptxgen/schemas/dml-spreadsheetDrawing.xsd +185 -0
  28. package/engine/pptxgen/schemas/dml-wordprocessingDrawing.xsd +287 -0
  29. package/engine/pptxgen/schemas/drawing-chart2012.xsd +129 -0
  30. package/engine/pptxgen/schemas/markup-compatibility.xsd +95 -0
  31. package/engine/pptxgen/schemas/opc-digSig.xsd +49 -0
  32. package/engine/pptxgen/schemas/opc-relationships.xsd +33 -0
  33. package/engine/pptxgen/schemas/pml.xsd +1676 -0
  34. package/engine/pptxgen/schemas/shared-additionalCharacteristics.xsd +28 -0
  35. package/engine/pptxgen/schemas/shared-bibliography.xsd +144 -0
  36. package/engine/pptxgen/schemas/shared-commonSimpleTypes.xsd +172 -0
  37. package/engine/pptxgen/schemas/shared-customXmlDataProperties.xsd +25 -0
  38. package/engine/pptxgen/schemas/shared-customXmlSchemaProperties.xsd +18 -0
  39. package/engine/pptxgen/schemas/shared-documentPropertiesCustom.xsd +59 -0
  40. package/engine/pptxgen/schemas/shared-documentPropertiesExtended.xsd +56 -0
  41. package/engine/pptxgen/schemas/shared-documentPropertiesVariantTypes.xsd +195 -0
  42. package/engine/pptxgen/schemas/shared-math.xsd +582 -0
  43. package/engine/pptxgen/schemas/shared-relationshipReference.xsd +25 -0
  44. package/engine/pptxgen/schemas/signatureInfo.xsd +103 -0
  45. package/engine/pptxgen/schemas/sml.xsd +4439 -0
  46. package/engine/pptxgen/schemas/visio.xsd +829 -0
  47. package/engine/pptxgen/schemas/vml-main.xsd +570 -0
  48. package/engine/pptxgen/schemas/vml-officeDrawing.xsd +509 -0
  49. package/engine/pptxgen/schemas/vml-presentationDrawing.xsd +12 -0
  50. package/engine/pptxgen/schemas/vml-spreadsheetDrawing.xsd +108 -0
  51. package/engine/pptxgen/schemas/vml-wordprocessingDrawing.xsd +96 -0
  52. package/engine/pptxgen/schemas/vmlDrawing.xsd +36 -0
  53. package/engine/pptxgen/schemas/wml.xsd +3643 -0
  54. package/engine/pptxgen/schemas/word12.xsd +66 -0
  55. package/engine/pptxgen/schemas/xlThreadedComments.xsd +59 -0
  56. package/engine/pptxgen/schemas/xlThreadedComments2.xsd +22 -0
  57. package/engine/pptxgen/schemas/xmldsig-core-schema.xsd +318 -0
  58. package/engine/pptxgen/shapes.py +417 -0
  59. package/engine/pptxgen/spec.py +529 -0
  60. package/engine/pptxgen/template.py +834 -0
  61. package/engine/pptxgen/util.py +218 -0
  62. package/engine/pptxgen/xmlutil.py +298 -0
  63. package/engine/pptxgen.sh +7 -0
  64. package/engine/requirements.txt +9 -0
  65. package/lib/index.js +186 -0
  66. package/package.json +34 -0
  67. package/skills/courseware/SKILL.md +130 -0
  68. package/skills/courseware/references/content-polish.md +121 -0
  69. package/skills/courseware/references/deck-spec.md +155 -0
  70. package/skills/courseware/references/design-review.md +121 -0
  71. package/skills/courseware/references/image-backends.md +94 -0
  72. package/skills/courseware/references/quality-gates.md +71 -0
  73. package/skills/courseware/references/roadmap.md +55 -0
  74. package/skills/courseware/references/template-fidelity.md +100 -0
  75. package/skills/courseware/workflows/generate.md +172 -0
  76. package/skills/courseware/workflows/quick.md +41 -0
  77. package/skills/courseware/workflows/revise.md +40 -0
  78. package/skills/courseware/workflows/routing.md +49 -0
  79. package/skills/courseware/workflows/stages/generate-images.md +74 -0
  80. package/skills/courseware/workflows/template-intake.md +112 -0
@@ -0,0 +1,184 @@
1
+ """设计审查(Design Review)—— 把"好看"变成可检查的规则。
2
+
3
+ 规则来源:腾讯 PPTX 技能的设计宪法(story-principle / design-principle),
4
+ 按中小学课件的场景做了下调与改写:
5
+
6
+ · 版式多样性:相邻页不得同骨架;单一骨架不得占满全篇
7
+ · 信息密度 :内容页正文字数下限;卡片组按卡计算
8
+ · 视觉锚点 :每页至少一个"明显更大"的元素(大标题/大图/大色块),
9
+ 禁止所有元素趋近中间大小
10
+ · 润色落实 :每页关键词高亮、有图必有图注、超长列表必须分块
11
+ · 节奏 :封面/结束页以外的页面不得连续同型(由版式多样性覆盖)
12
+
13
+ 用法:
14
+ pptxgen review decks/xxx.yaml # 只查大纲层
15
+ pptxgen review decks/xxx.yaml --pptx out/xxx.pptx # 追加成品层检查
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Dict, List, Optional, Sequence, Tuple
22
+
23
+ from .patterns import collect_texts, guess_pattern, highlight_count, text_len
24
+
25
+ # 每页正文字数下限(中小学课件,比商务汇报低一档)
26
+ MIN_WORDS = {
27
+ "content": 60,
28
+ "example": 60,
29
+ "practice": 40,
30
+ "summary": 40,
31
+ "homework": 30,
32
+ "objectives": 60,
33
+ "toc": 30,
34
+ "section": 15,
35
+ "cover": 8,
36
+ "end": 8,
37
+ }
38
+
39
+ # 骨架健康度阈值
40
+ MAX_SINGLE_PATTERN_SHARE = 0.55 # 单一骨架最多占全篇比例
41
+ MAX_CARDS_PER_DECK = 0.45 # 卡片矩阵最多占全篇比例(防"千页一卡")
42
+
43
+
44
+ class Finding:
45
+ def __init__(self, level: str, page: Optional[int], text: str):
46
+ self.level = level # "错误" / "建议"
47
+ self.page = page
48
+ self.text = text
49
+
50
+ def __str__(self) -> str:
51
+ where = f"P{self.page}" if self.page else "全篇"
52
+ return f"[{self.level}] {where} {self.text}"
53
+
54
+
55
+ def review_deck(deck: dict, pptx_path: Optional[str] = None) -> Tuple[List[Finding], str]:
56
+ slides = deck.get("slides") or []
57
+ findings: List[Finding] = []
58
+ stats: List[str] = []
59
+
60
+ patterns: List[str] = []
61
+ for i, s in enumerate(slides, 1):
62
+ stype = s.get("type")
63
+ bullets = [b for b in (s.get("bullets") or []) if b.get("text")]
64
+ pat = guess_pattern(s, bullets)
65
+ if pat in ("image_left", "image_right"):
66
+ pat = "bullets"
67
+ patterns.append(pat)
68
+
69
+ texts = collect_texts(s)
70
+ n = text_len(texts)
71
+ title = (s.get("title") or "")[:18]
72
+ stats.append(f" P{i:>2} [{stype:<10}] {pat:<9} {n:>4}字 高亮{highlight_count(s)}处 {title}")
73
+
74
+ # 密度下限
75
+ floor = MIN_WORDS.get(stype, 40)
76
+ if n < floor:
77
+ findings.append(Finding("建议", i, f"内容偏薄({n} 字 < 下限 {floor} 字)"))
78
+
79
+ # 润色落实
80
+ if stype in ("content", "example") and highlight_count(s) == 0 and n >= 40:
81
+ findings.append(Finding("建议", i, "整页没有关键词高亮(`**关键词**`)"))
82
+ if (s.get("image_prompt") or s.get("image")) and not s.get("image_caption"):
83
+ findings.append(Finding("建议", i, "有配图但没有图注(image_caption)"))
84
+
85
+ # 超长列表未分块
86
+ if stype in ("content", "example") and pat == "bullets":
87
+ lvl0 = [b for b in bullets if int(b.get("level", 0) or 0) == 0]
88
+ if len(lvl0) >= 5:
89
+ findings.append(Finding("建议", i,
90
+ f"{len(lvl0)} 条一级要点仍用 bullets,建议分块"
91
+ f"(cards/steps/flow/compare)"))
92
+
93
+ # 版式多样性(借用"相邻页不得同版式")
94
+ core = [(i, p) for i, (p, s) in enumerate(zip(patterns, slides), 1)
95
+ if s.get("type") not in ("cover", "end")]
96
+ for (i1, p1), (i2, p2) in zip(core, core[1:]):
97
+ if p1 == p2 and p1 in ("bullets", "cards"):
98
+ findings.append(Finding("建议", i2, f"与上一页同为 {p1} 骨架,建议换一种"))
99
+
100
+ counts: Dict[str, int] = {}
101
+ for _, p in core:
102
+ counts[p] = counts.get(p, 0) + 1
103
+ total = max(len(core), 1)
104
+ for p, c in counts.items():
105
+ if c / total > MAX_SINGLE_PATTERN_SHARE and total >= 6:
106
+ findings.append(Finding("建议", None,
107
+ f"骨架 {p} 占 {c}/{total} 页(>{int(MAX_SINGLE_PATTERN_SHARE*100)}%),"
108
+ f"全篇偏单调"))
109
+ if counts.get("cards", 0) / total > MAX_CARDS_PER_DECK and total >= 6:
110
+ findings.append(Finding("建议", None,
111
+ f"卡片矩阵占 {counts['cards']}/{total} 页,建议穿插 steps/flow/compare"))
112
+
113
+ # 视觉锚点(需要成品才能算字号与面积)
114
+ if pptx_path:
115
+ findings.extend(_review_pptx(pptx_path))
116
+
117
+ report = "设计审查(Design Review)\n" + "\n".join(stats)
118
+ return findings, report
119
+
120
+
121
+ def _review_pptx(pptx_path: str) -> List[Finding]:
122
+ """成品层检查:每页是否有"视觉锚点"(明显更大的字号或大图)。"""
123
+ from pptx import Presentation
124
+ from pptx.util import Emu
125
+
126
+ findings: List[Finding] = []
127
+ try:
128
+ prs = Presentation(pptx_path)
129
+ except Exception as exc:
130
+ return [Finding("建议", None, f"无法读取成品:{exc}")]
131
+ W = float(prs.slide_width or 1)
132
+ H = float(prs.slide_height or 1)
133
+ for i, slide in enumerate(prs.slides, 1):
134
+ sizes: List[float] = []
135
+ max_pic = 0.0
136
+ for sh in slide.shapes:
137
+ if sh.has_text_frame:
138
+ for p in sh.text_frame.paragraphs:
139
+ for r in p.runs:
140
+ if r.font.size:
141
+ sizes.append(r.font.size.pt)
142
+ try:
143
+ if sh.shape_type is not None and "PICTURE" in str(sh.shape_type):
144
+ max_pic = max(max_pic, (float(sh.width) * float(sh.height)) / (W * H))
145
+ except Exception:
146
+ pass
147
+ if not sizes:
148
+ continue
149
+ from collections import Counter as _Counter
150
+ rounded = [round(s, 1) for s in sizes]
151
+ distinct = sorted(set(rounded), reverse=True)
152
+ top = distinct[0]
153
+ if len(distinct) == 1:
154
+ # 全页只有一个字号:靠"字号本身够大"或大图撑锚点
155
+ anchor = top >= 32 or max_pic >= 0.12
156
+ body_ref = top
157
+ else:
158
+ # 锚点 = "有没有比正文明显更大的东西",所以拿最常见字号(正文)做基准
159
+ _counts = _Counter(rounded)
160
+ _maxn = max(_counts.values())
161
+ # 并列最常见时取较小者当"正文"(封面/结束页只有标题+副标题两个字号)
162
+ body_ref = min(sz for sz, n in _counts.items() if n == _maxn)
163
+ # 借鉴来源的学术分支:主标 32-40 / 正文 22-28,比值约 1.25-1.45,取 1.35
164
+ anchor = (top >= body_ref * 1.35) or max_pic >= 0.12
165
+ if not anchor:
166
+ findings.append(Finding("建议", i,
167
+ f"缺少视觉锚点(最大字号 {top:.0f}pt / 正文字号 {body_ref:.0f}pt,"
168
+ f"最大图占比 {max_pic*100:.0f}%)"))
169
+ return findings
170
+
171
+
172
+ def format_findings(findings: Sequence[Finding]) -> str:
173
+ if not findings:
174
+ return "设计审查:未发现问题。"
175
+ errs = [f for f in findings if f.level == "错误"]
176
+ tips = [f for f in findings if f.level != "错误"]
177
+ out = []
178
+ if errs:
179
+ out.append(f"发现 {len(errs)} 个错误:")
180
+ out.extend(f" ! {f}" for f in errs)
181
+ if tips:
182
+ out.append(f"发现 {len(tips)} 条建议:")
183
+ out.extend(f" · {f}" for f in tips)
184
+ return "\n".join(out)
@@ -0,0 +1,122 @@
1
+ """严格模式校验:按 OOXML Schema(ECMA-376,与 PowerPoint 同源)检查生成文件的各部件。
2
+
3
+ LibreOffice 对很多结构错误很宽容(例如 <a:spcBef pts="..."/>),PowerPoint 则会
4
+ 直接弹"内容有问题"修复框。这里的 XSD 校验能提前抓到这一类错误。
5
+
6
+ 注意:微软扩展命名空间(p14/a14/p15/a15/p16/a16 等)不在 ECMA-376 标准 Schema 内,
7
+ 含这些扩展的部件会跳过严格校验(PowerPoint 自带处理),避免误报。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import re
14
+ import zipfile
15
+ from typing import List, Optional, Tuple
16
+
17
+ from lxml import etree
18
+
19
+ SCHEMA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "schemas")
20
+
21
+ # 微软扩展命名空间前缀 → 跳过严格校验(ECMA-376 的 XSD 不覆盖它们)
22
+ MS_EXT_NS = re.compile(
23
+ r"schemas\.microsoft\.com/office/(powerpoint|drawing|word|excel)/"
24
+ )
25
+
26
+ # pml.xsd 覆盖的部件范围(根元素在 presentationml 命名空间下的部分)
27
+ PML_PARTS = re.compile(
28
+ r"^ppt/(slides/slide\d+|notesSlides/notesSlide\d+|slideLayouts/slideLayout\d+|"
29
+ r"slideMasters/slideMaster\d+|notesMasters/notesMaster\d+)\.xml$"
30
+ )
31
+ PML_PRESENTATION = "ppt/presentation.xml"
32
+
33
+ _schema_cache: Optional[etree.XMLSchema] = None
34
+
35
+
36
+ def _load_schema() -> Optional[etree.XMLSchema]:
37
+ """加载 pml.xsd(含全部 import,缓存)。"""
38
+ global _schema_cache
39
+ if _schema_cache is not None:
40
+ return _schema_cache
41
+ if not os.path.isdir(SCHEMA_DIR):
42
+ return None
43
+ xsd = os.path.join(SCHEMA_DIR, "pml.xsd")
44
+ if not os.path.exists(xsd):
45
+ return None
46
+
47
+ class _Resolver(etree.Resolver):
48
+ def resolve(self, url, public_id, context):
49
+ base = os.path.basename(url)
50
+ p = os.path.join(SCHEMA_DIR, base)
51
+ if os.path.exists(p):
52
+ return self.resolve_filename(p, context)
53
+ return None
54
+
55
+ parser = etree.XMLParser(load_dtd=False, no_network=True)
56
+ parser.resolvers.add(_Resolver())
57
+ try:
58
+ doc = etree.parse(xsd, parser)
59
+ _schema_cache = etree.XMLSchema(doc)
60
+ except Exception:
61
+ _schema_cache = None
62
+ return _schema_cache
63
+
64
+
65
+ def _has_ms_extension(xml: bytes) -> bool:
66
+ """部件里是否含微软扩展命名空间(含则跳过严格校验)。"""
67
+ return bool(MS_EXT_NS.search(xml.decode("utf-8", "ignore")))
68
+
69
+
70
+ def check_schema(pptx_path: str) -> Tuple[List[str], List[str], bool]:
71
+ """校验 pptx 里所有 presentationml 部件。
72
+
73
+ 返回 (failures, skipped, available):
74
+ failures —— (部件, 错误) 描述列表
75
+ skipped —— 含微软扩展命名空间、跳过严格校验的部件
76
+ available —— Schema 是否可用(不可用时 failures 恒为空,不阻断 check)
77
+ """
78
+ schema = _load_schema()
79
+ if schema is None:
80
+ return [], [], False
81
+ failures: List[str] = []
82
+ skipped: List[str] = []
83
+ try:
84
+ z = zipfile.ZipFile(pptx_path)
85
+ except Exception as exc:
86
+ return [f"(无法读取 pptx:{exc})"], [], True
87
+ targets = [n for n in z.namelist()
88
+ if n.endswith(".xml") and (PML_PARTS.match(n) or n == PML_PRESENTATION)]
89
+ for n in sorted(targets):
90
+ blob = z.read(n)
91
+ if _has_ms_extension(blob):
92
+ skipped.append(n)
93
+ continue
94
+ try:
95
+ doc = etree.fromstring(blob)
96
+ except Exception as exc:
97
+ failures.append(f"{n}: XML 解析失败:{exc}")
98
+ continue
99
+ if not schema.validate(doc):
100
+ errs = "; ".join(f"line {e.line} {e.message}" for e in schema.error_log[:4])
101
+ failures.append(f"{n}: {errs}")
102
+ z.close()
103
+ return failures, skipped, True
104
+
105
+
106
+ def describe(pptx_path: str) -> str:
107
+ """供 check 命令输出的描述文本。"""
108
+ failures, skipped, available = check_schema(pptx_path)
109
+ lines = []
110
+ if not available:
111
+ lines.append("严格模式校验(OOXML Schema):不可用(缺少 Schema 文件),已跳过")
112
+ elif failures:
113
+ lines.append(f"严格模式校验(OOXML Schema):发现 {len(failures)} 个问题")
114
+ for f in failures[:12]:
115
+ lines.append(f" ! {f}")
116
+ if len(failures) > 12:
117
+ lines.append(f" ... 其余 {len(failures) - 12} 个")
118
+ else:
119
+ lines.append("严格模式校验(OOXML Schema):全部部件通过")
120
+ if skipped:
121
+ lines.append(f" ({len(skipped)} 个部件含微软扩展命名空间,已跳过:{skipped[0]} 等)")
122
+ return "\n".join(lines)