ppt-template-reuse 0.4.0-rc.2

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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +8 -0
  3. package/README.md +233 -0
  4. package/adapters/README.md +13 -0
  5. package/adapters/claude/.claude-plugin/plugin.json +16 -0
  6. package/adapters/claude/.mcp.json +8 -0
  7. package/adapters/claude/skills/learn-ppt-template/SKILL.md +105 -0
  8. package/adapters/kimi/kimi.plugin.json +12 -0
  9. package/adapters/kimi/skills/learn-ppt-template/SKILL.md +105 -0
  10. package/adapters/kimi-work/mcp.json +8 -0
  11. package/adapters/kimi-work/skills/learn-ppt-template/SKILL.md +105 -0
  12. package/adapters/workbuddy/mcp.json +8 -0
  13. package/adapters/workbuddy/skills/learn-ppt-template/SKILL.md +105 -0
  14. package/package.json +53 -0
  15. package/plugins/ppt-template-reuse/.codex-plugin/plugin.json +38 -0
  16. package/plugins/ppt-template-reuse/.mcp.json +9 -0
  17. package/plugins/ppt-template-reuse/skills/learn-ppt-template/SKILL.md +105 -0
  18. package/plugins/ppt-template-reuse/skills/learn-ppt-template/agents/openai.yaml +6 -0
  19. package/skills/ppt-template-reuse/SKILL.md +105 -0
  20. package/skills/ppt-template-reuse/agents/openai.yaml +6 -0
  21. package/src/cli/ppt-reuse.mjs +203 -0
  22. package/src/core/adaptive-planner.mjs +1192 -0
  23. package/src/core/content-audit.mjs +573 -0
  24. package/src/core/frame-map.mjs +91 -0
  25. package/src/core/index.mjs +38 -0
  26. package/src/core/io.mjs +81 -0
  27. package/src/core/legacy-capsule-import.mjs +382 -0
  28. package/src/core/ooxml-editor.mjs +753 -0
  29. package/src/core/paths.mjs +60 -0
  30. package/src/core/planner-lib.mjs +59 -0
  31. package/src/core/pptx-package.mjs +227 -0
  32. package/src/core/renderer.mjs +402 -0
  33. package/src/core/source-extractor.mjs +412 -0
  34. package/src/core/template-inspector.mjs +489 -0
  35. package/src/core/template-intelligence.mjs +675 -0
  36. package/src/core/universal-engine.mjs +1075 -0
  37. package/src/index.mjs +2 -0
  38. package/src/install/installer.mjs +293 -0
  39. package/src/mcp/server.mjs +377 -0
@@ -0,0 +1,489 @@
1
+ import path from "node:path";
2
+
3
+ import { fingerprint, sha256File } from "./io.mjs";
4
+ import {
5
+ attr,
6
+ elements,
7
+ emuToPoints,
8
+ loadPptx,
9
+ parseXml,
10
+ shapeRecords,
11
+ slideRecords,
12
+ xmlFrom,
13
+ } from "./pptx-package.mjs";
14
+
15
+ const VISUAL_CUE =
16
+ /(插入图片|相关图片|照片|图片|image|photo|picture|icon|图标|占位)/i;
17
+
18
+ function normalized(value) {
19
+ return String(value || "").replace(/\s+/g, "").toLowerCase();
20
+ }
21
+
22
+ function textUnits(value) {
23
+ let count = 0;
24
+ for (const character of String(value || "")) {
25
+ if (/\s/.test(character)) count += 0.3;
26
+ else if (/[\u3400-\u9fff\u3000-\u303f\uff00-\uffef]/u.test(character)) {
27
+ count += 1;
28
+ } else count += 0.55;
29
+ }
30
+ return Number(count.toFixed(2));
31
+ }
32
+
33
+ function isGrayFill(value) {
34
+ const match = String(value || "").match(/^([0-9a-f]{6})$/i);
35
+ if (!match) return false;
36
+ const channels = [0, 2, 4].map((offset) =>
37
+ Number.parseInt(match[1].slice(offset, offset + 2), 16),
38
+ );
39
+ return (
40
+ Math.max(...channels) - Math.min(...channels) <= 12 &&
41
+ channels[0] >= 70 &&
42
+ channels[0] <= 225
43
+ );
44
+ }
45
+
46
+ function purposeForShape(shape, titleId) {
47
+ if (shape.id === titleId) return "title";
48
+ if (/title|标题/i.test(shape.placeholderType || shape.name)) return "title";
49
+ if (/subtitle|副标题/i.test(shape.placeholderType || shape.name)) {
50
+ return "subtitle";
51
+ }
52
+ if (/footer|页脚/i.test(shape.placeholderType || shape.name)) return "footer";
53
+ if (/date|日期/i.test(shape.placeholderType || shape.name)) return "date";
54
+ if (/sldnum|页码/i.test(shape.placeholderType || shape.name)) {
55
+ return "pageNumber";
56
+ }
57
+ if (shape.kind === "image") return "image";
58
+ if (shape.kind === "chart") return "chart";
59
+ if (shape.kind === "table") return "table";
60
+ return "body";
61
+ }
62
+
63
+ function titleShape(shapes) {
64
+ const explicit = shapes.find(
65
+ (shape) =>
66
+ shape.kind === "text" &&
67
+ /title|ctrTitle|标题/i.test(shape.placeholderType || shape.name),
68
+ );
69
+ if (explicit) return explicit;
70
+ return shapes
71
+ .filter((shape) => shape.kind === "text" && shape.text)
72
+ .sort(
73
+ (left, right) =>
74
+ right.fontSize - left.fontSize ||
75
+ left.bbox.y - right.bbox.y ||
76
+ left.bbox.x - right.bbox.x,
77
+ )[0];
78
+ }
79
+
80
+ function classifySlide(slideNumber, slideCount, shapes) {
81
+ const text = shapes.map((shape) => shape.text).join(" ");
82
+ const compact = normalized(text);
83
+ const textShapes = shapes.filter((shape) => shape.kind === "text" && shape.text);
84
+ const bodyShapes = textShapes.filter((shape) =>
85
+ /body|content|正文|内容/i.test(
86
+ `${shape.placeholderType || ""} ${shape.name || ""}`,
87
+ ),
88
+ );
89
+ const contentCue =
90
+ /(分析|依据|证据|事实|规则|理由|影响|结论|问题|方法|数据|说明|analysis|evidence|reason|finding)/i.test(
91
+ text,
92
+ );
93
+ const visuals = shapes.filter((shape) =>
94
+ ["image", "chart", "table"].includes(shape.kind),
95
+ );
96
+ if (slideNumber === 1) {
97
+ return {
98
+ layoutFamily: "cover",
99
+ communicationFunctions: ["opening", "identity"],
100
+ narrativeRoles: ["opening"],
101
+ };
102
+ }
103
+ if (/(谢谢|thank|q&a|questions|答疑)/i.test(text)) {
104
+ return {
105
+ layoutFamily: "closing",
106
+ communicationFunctions: ["closing"],
107
+ narrativeRoles: ["closing"],
108
+ };
109
+ }
110
+ if (/(目录|议程|agenda|contents|路线)/i.test(text)) {
111
+ return {
112
+ layoutFamily: "agenda",
113
+ communicationFunctions: ["roadmap", "agenda"],
114
+ narrativeRoles: ["agenda", "roadmap"],
115
+ };
116
+ }
117
+ if (/(时间轴|timeline|历程|里程碑)/i.test(text)) {
118
+ return {
119
+ layoutFamily: "timeline",
120
+ communicationFunctions: ["sequence", "chronology"],
121
+ narrativeRoles: ["sequence"],
122
+ };
123
+ }
124
+ if (
125
+ /(封面|答辩人|汇报人|制作人|指导教师)/i.test(text) &&
126
+ !/(part|chapter|section|目录|议程|contents)/i.test(text) &&
127
+ Math.max(...textShapes.map((shape) => shape.fontSize || 0), 0) >= 36
128
+ ) {
129
+ return {
130
+ layoutFamily: "cover",
131
+ communicationFunctions: ["opening", "identity"],
132
+ narrativeRoles: ["opening"],
133
+ };
134
+ }
135
+ if (shapes.some((shape) => shape.kind === "chart")) {
136
+ return {
137
+ layoutFamily: "chart",
138
+ communicationFunctions: ["data", "evidence"],
139
+ narrativeRoles: ["evidence", "analysis"],
140
+ };
141
+ }
142
+ if (shapes.some((shape) => shape.kind === "table")) {
143
+ return {
144
+ layoutFamily: "table",
145
+ communicationFunctions: ["comparison", "data"],
146
+ narrativeRoles: ["evidence", "analysis"],
147
+ };
148
+ }
149
+ if (/^(part|chapter|section|\d{1,2})/i.test(compact)) {
150
+ return {
151
+ layoutFamily: "divider",
152
+ communicationFunctions: ["section"],
153
+ narrativeRoles: ["section"],
154
+ };
155
+ }
156
+ if (
157
+ textShapes.length <= 3 &&
158
+ visuals.length === 0 &&
159
+ Math.max(...textShapes.map((shape) => shape.fontSize || 0), 0) >= 24 &&
160
+ !contentCue &&
161
+ (bodyShapes.length === 0 ||
162
+ bodyShapes.reduce((sum, shape) => sum + shape.text.length, 0) < 18)
163
+ ) {
164
+ return {
165
+ layoutFamily: "divider",
166
+ communicationFunctions: ["section"],
167
+ narrativeRoles: ["section"],
168
+ };
169
+ }
170
+ if (visuals.some((shape) => shape.kind === "image")) {
171
+ return {
172
+ layoutFamily: "image",
173
+ communicationFunctions: ["example", "evidence", "visual"],
174
+ narrativeRoles: ["evidence", "analysis"],
175
+ };
176
+ }
177
+ if (
178
+ textShapes.length >= 4 &&
179
+ new Set(textShapes.map((shape) => Math.round(shape.bbox.x / 80))).size >= 2
180
+ ) {
181
+ return {
182
+ layoutFamily: "comparison",
183
+ communicationFunctions: ["comparison", "analysis"],
184
+ narrativeRoles: ["analysis"],
185
+ };
186
+ }
187
+ return {
188
+ layoutFamily: "analysis",
189
+ communicationFunctions: ["claim", "analysis", "evidence"],
190
+ narrativeRoles: ["analysis"],
191
+ };
192
+ }
193
+
194
+ function inferUseCases(allText) {
195
+ const primary = [];
196
+ if (/(大学|university|学院|答辩|论文|研究|文献)/i.test(allText)) {
197
+ primary.push("academic-defense");
198
+ }
199
+ if (/(课程|作业|课堂|汇报|course|homework|assignment)/i.test(allText)) {
200
+ primary.push("course-presentation");
201
+ }
202
+ if (/(法院|民法|法律|合同|诉讼|法条)/i.test(allText)) {
203
+ primary.push("legal-analysis");
204
+ }
205
+ if (/(产品|市场|融资|商业|客户|增长)/i.test(allText)) {
206
+ primary.push("product-proposal", "business-pitch");
207
+ }
208
+ if (/(数据|同比|环比|指标|报告)/i.test(allText)) primary.push("data-report");
209
+ if (/(教学|培训|学习目标|练习)/i.test(allText)) {
210
+ primary.push("teaching-training");
211
+ }
212
+ if (/(年度|季度|业绩|部门|经营)/i.test(allText)) {
213
+ primary.push("corporate-report");
214
+ }
215
+ if (/(政策|治理|监管|改革|政府)/i.test(allText)) {
216
+ primary.push("policy-public-affairs");
217
+ }
218
+ if (/(作品集|摄影|品牌|视觉|portfolio)/i.test(allText)) {
219
+ primary.push("visual-portfolio");
220
+ }
221
+ return [...new Set(primary.length ? primary : ["general-report"])];
222
+ }
223
+
224
+ export async function inspectTemplate(templatePath) {
225
+ const resolved = path.resolve(templatePath);
226
+ const extension = path.extname(resolved).toLowerCase();
227
+ if (extension === ".pptm") {
228
+ const error = new Error("Macro-enabled PPTM templates are rejected by default");
229
+ error.code = "MACRO_TEMPLATE_REJECTED";
230
+ throw error;
231
+ }
232
+ if (![".pptx", ".potx"].includes(extension)) {
233
+ throw new Error("Template must be a native PPTX or POTX file");
234
+ }
235
+ const zip = await loadPptx(resolved);
236
+ const order = await slideRecords(zip);
237
+ const slideSize = elements(order.presentation, "sldSz")[0];
238
+ const canvas = {
239
+ width: emuToPoints(attr(slideSize, "cx")),
240
+ height: emuToPoints(attr(slideSize, "cy")),
241
+ };
242
+ const slides = [];
243
+ const textCorpus = [];
244
+ for (const record of order.slides) {
245
+ const xml = await xmlFrom(zip, record.partName);
246
+ const document = parseXml(xml);
247
+ const shapes = shapeRecords(document);
248
+ const title = titleShape(shapes);
249
+ const classification = classifySlide(
250
+ record.slideNumber,
251
+ order.slides.length,
252
+ shapes,
253
+ );
254
+ const slotSchema = [];
255
+ const assetRequirements = [];
256
+ let ambiguousGrayPlaceholder = false;
257
+ for (const shape of shapes) {
258
+ const purpose = purposeForShape(shape, title?.id);
259
+ const visualInstruction =
260
+ shape.kind === "text" &&
261
+ VISUAL_CUE.test(`${shape.name} ${shape.description} ${shape.text}`);
262
+ const largeGrayShape =
263
+ shape.kind === "text" &&
264
+ isGrayFill(shape.fillColor) &&
265
+ shape.bbox.width * shape.bbox.height >=
266
+ canvas.width * canvas.height * 0.08;
267
+ const isEditableText =
268
+ shape.kind === "text" &&
269
+ !visualInstruction &&
270
+ !largeGrayShape &&
271
+ !["pageNumber", "date"].includes(purpose) &&
272
+ Boolean(shape.text);
273
+ const visualPlaceholder =
274
+ (["image", "chart", "table"].includes(shape.kind) &&
275
+ VISUAL_CUE.test(`${shape.name} ${shape.description} ${shape.text}`)) ||
276
+ visualInstruction ||
277
+ largeGrayShape;
278
+ if (largeGrayShape && !visualInstruction) ambiguousGrayPlaceholder = true;
279
+ if (isEditableText || visualPlaceholder) {
280
+ const type = visualPlaceholder
281
+ ? ["chart", "table"].includes(shape.kind)
282
+ ? shape.kind
283
+ : "image"
284
+ : "text";
285
+ const semanticType =
286
+ visualInstruction && /icon|图标/i.test(`${shape.name} ${shape.text}`)
287
+ ? "icon"
288
+ : type;
289
+ slotSchema.push({
290
+ slotId: `slide-${record.slideNumber}-shape-${shape.id}`,
291
+ sourceShapeId: shape.id,
292
+ sourceElementName: shape.name,
293
+ type,
294
+ semanticType,
295
+ purpose: visualPlaceholder ? type : purpose,
296
+ required: purpose === "title" || purpose === "body" || visualPlaceholder,
297
+ maxTextUnits:
298
+ type === "text"
299
+ ? Math.max(
300
+ textUnits(shape.text) * 1.35,
301
+ (shape.bbox.width / Math.max(8, shape.fontSize || 14)) * 12,
302
+ )
303
+ : undefined,
304
+ sourceText: shape.text || undefined,
305
+ bbox: shape.bbox,
306
+ });
307
+ }
308
+ if (visualPlaceholder) {
309
+ assetRequirements.push({
310
+ type:
311
+ visualInstruction && /icon|图标/i.test(`${shape.name} ${shape.text}`)
312
+ ? "icon"
313
+ : ["chart", "table"].includes(shape.kind)
314
+ ? shape.kind
315
+ : "image",
316
+ slotId: `slide-${record.slideNumber}-shape-${shape.id}`,
317
+ required: true,
318
+ });
319
+ }
320
+ if (shape.text) textCorpus.push(shape.text);
321
+ }
322
+ const hasTitle = slotSchema.some((slot) => slot.purpose === "title");
323
+ const hasBody = slotSchema.some((slot) => slot.purpose === "body");
324
+ const safe =
325
+ hasTitle &&
326
+ !ambiguousGrayPlaceholder &&
327
+ (hasBody ||
328
+ ["cover", "closing", "divider", "agenda"].includes(
329
+ classification.layoutFamily,
330
+ ));
331
+ slides.push({
332
+ sourceSlide: record.slideNumber,
333
+ sourcePart: record.partName,
334
+ layoutFamily: classification.layoutFamily,
335
+ layoutVariant: `${classification.layoutFamily}-${slotSchema.length}`,
336
+ communicationFunctions: classification.communicationFunctions,
337
+ narrativeRoles: classification.narrativeRoles,
338
+ slotSchema,
339
+ assetRequirements,
340
+ contentRecipe: {
341
+ recommendedSourceUnits: {
342
+ min: ["cover", "closing", "divider"].includes(
343
+ classification.layoutFamily,
344
+ )
345
+ ? 0
346
+ : 1,
347
+ max: Math.max(
348
+ 1,
349
+ slotSchema.filter((slot) => slot.type === "text").length - 1,
350
+ ),
351
+ },
352
+ },
353
+ safetyStatus: safe ? "automatic" : "review",
354
+ confidence: safe ? 0.86 : 0.58,
355
+ editability: "native-ooxml",
356
+ protectedElements: shapes
357
+ .filter(
358
+ (shape) =>
359
+ !slotSchema.some(
360
+ (slot) => String(slot.sourceShapeId) === String(shape.id),
361
+ ),
362
+ )
363
+ .map((shape) => ({ id: shape.id, name: shape.name, kind: shape.kind })),
364
+ });
365
+ }
366
+ const variantSelections = {};
367
+ for (const [role, family] of [
368
+ ["opening", "cover"],
369
+ ["closing", "closing"],
370
+ ]) {
371
+ const variants = slides.filter(
372
+ (slide) =>
373
+ slide.layoutFamily === family && slide.safetyStatus === "automatic",
374
+ );
375
+ if (variants.length > 1) {
376
+ variantSelections[role] = variants.map((slide) => slide.sourceSlide);
377
+ for (const slide of variants) {
378
+ slide.safetyStatus = "review";
379
+ slide.confidence = Math.min(Number(slide.confidence || 0.58), 0.68);
380
+ }
381
+ }
382
+ }
383
+ const automatic = slides.filter((slide) => slide.safetyStatus === "automatic");
384
+ const automaticFamilies = new Set(
385
+ automatic.map((slide) => slide.layoutFamily),
386
+ );
387
+ const capability = (families) => ({
388
+ safeCount: automatic.filter((slide) =>
389
+ families.includes(slide.layoutFamily),
390
+ ).length,
391
+ });
392
+ const familyCount = new Set(slides.map((slide) => slide.layoutFamily)).size;
393
+ const placeholderCount = slides.reduce(
394
+ (sum, slide) => sum + slide.assetRequirements.length,
395
+ 0,
396
+ );
397
+ const templateKind =
398
+ placeholderCount > 0
399
+ ? "blank-template"
400
+ : slides.length >= 8 && familyCount >= 5
401
+ ? "layout-gallery"
402
+ : "filled-example";
403
+ const useCases = inferUseCases(textCorpus.join(" "));
404
+ const profile = {
405
+ schemaVersion: 3,
406
+ source: {
407
+ path: resolved,
408
+ sha256: await sha256File(resolved),
409
+ extension,
410
+ },
411
+ canvas,
412
+ templateKind,
413
+ templateKindConfidence: 0.82,
414
+ purposeProfile: {
415
+ primaryUseCases: useCases,
416
+ secondaryUseCases: ["general-report"],
417
+ incompatibleUseCases: [],
418
+ confidence: useCases[0] === "general-report" ? 0.66 : 0.82,
419
+ },
420
+ capabilities: {
421
+ cover: capability(["cover"]),
422
+ closing: capability(["closing"]),
423
+ analysis: capability(["analysis", "comparison", "table"]),
424
+ issue: capability(["analysis", "comparison"]),
425
+ counterargument: capability(["comparison", "analysis"]),
426
+ conclusion: capability(["closing", "analysis"]),
427
+ recommendation: capability(["closing", "analysis"]),
428
+ summary: capability(["analysis", "closing"]),
429
+ background: capability(["analysis", "image"]),
430
+ problem: capability(["analysis", "comparison"]),
431
+ solution: capability(["analysis", "comparison"]),
432
+ process: capability(["timeline"]),
433
+ comparison: capability(["comparison", "table"]),
434
+ chronology: capability(["timeline"]),
435
+ timeline: capability(["timeline"]),
436
+ data: capability(["chart", "table"]),
437
+ evidence: capability(["analysis", "image", "chart", "table"]),
438
+ imagery: capability(["image"]),
439
+ "image-evidence": capability(["image"]),
440
+ roadmap: capability(["agenda"]),
441
+ section: capability(["divider"]),
442
+ },
443
+ densityRange:
444
+ automatic.some(
445
+ (slide) =>
446
+ slide.slotSchema.filter((slot) => slot.type === "text").length >= 5,
447
+ )
448
+ ? "high"
449
+ : automatic.some(
450
+ (slide) =>
451
+ slide.slotSchema.filter((slot) => slot.type === "text").length >=
452
+ 3,
453
+ )
454
+ ? "medium"
455
+ : "low",
456
+ visualBalance:
457
+ [...automaticFamilies].some((family) =>
458
+ ["image", "chart", "table"].includes(family),
459
+ )
460
+ ? "balanced"
461
+ : "text-led",
462
+ variantSelections,
463
+ slides,
464
+ reviewSummary: {
465
+ automaticSlides: automatic.map((slide) => slide.sourceSlide),
466
+ reviewSlides: slides
467
+ .filter((slide) => slide.safetyStatus === "review")
468
+ .map((slide) => slide.sourceSlide),
469
+ blockedSlides: [],
470
+ },
471
+ };
472
+ profile.profileFingerprint = fingerprint(profile);
473
+ profile.actionRequired =
474
+ profile.reviewSummary.reviewSlides.length ||
475
+ profile.purposeProfile.confidence < 0.75
476
+ ? {
477
+ type: "template-semantics",
478
+ slides: profile.reviewSummary.reviewSlides,
479
+ instructions: [
480
+ "Review every flagged slide at full size.",
481
+ "Confirm layoutFamily, communicationFunctions, narrativeRoles, slot purpose, asset requirements, protected elements, and safe editability.",
482
+ "When opening or closing variants are listed, approve exactly one variant for that role and block the alternatives so repeated runs do not choose by accident.",
483
+ "Do not authorize chart, diagram, icon, SmartArt, animation, or media edits unless the native object and slot are explicit.",
484
+ ],
485
+ variantSelections,
486
+ }
487
+ : null;
488
+ return profile;
489
+ }