bit-ppt-generator 0.3.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.
@@ -0,0 +1,315 @@
1
+ const writingRules = [
2
+ "Keep slide text short and concrete; use validation warnings as rewrite hints.",
3
+ "Prefer editable PPTX objects: text boxes, shapes, tables, charts, OMML formulas, and native images.",
4
+ "Use `speakerNotes` for presenter scripts; these notes are written to the PPTX notes pane, not the slide canvas.",
5
+ "When an image is unavailable, use `image.mode: placeholder` with a concrete prompt instead of inventing a local file path.",
6
+ "Use local image paths relative to the project root unless an integration provides assets separately.",
7
+ "Run `bit-ppt check <deck.yaml> --json` before generation and use `repairPrompt` to revise content.",
8
+ ];
9
+
10
+ const speakerNotesGuide = {
11
+ topic: "speaker-notes",
12
+ purpose: "Add PowerPoint/WPS speaker notes to any slide without rendering them on the slide canvas.",
13
+ fields: {
14
+ speakerNotes: { type: "string | string[]", required: false },
15
+ },
16
+ aliases: ["speaker_notes", "speakerScript", "speaker_script"],
17
+ limits: {
18
+ speakerNotes: { recommendedChars: 1200, recommendedLines: 30 },
19
+ },
20
+ notes: [
21
+ "Use a YAML block scalar for paragraph-style presenter scripts.",
22
+ "Use a string list when the script is naturally segmented into short beats.",
23
+ "Formula text in speaker notes is kept as plain text for now, for example `$L(\\theta)$`.",
24
+ "`notes` remains layout-specific slide content in some layouts; use `speakerNotes` for the actual notes pane.",
25
+ ],
26
+ example: {
27
+ layout: "bullets",
28
+ title: "核心观点",
29
+ bullets: ["输出是可编辑 PPTX。"],
30
+ speakerNotes: "这一页先解释为什么选择 YAML 到 PPTX 的路线。\n公式暂时按普通文本保留,例如 $L(\\theta)$。",
31
+ },
32
+ };
33
+
34
+ const imagePlaceholderGuide = {
35
+ topic: "image-placeholder",
36
+ purpose: "Describe missing images with editable placeholders that users can replace later.",
37
+ whenToUse: "Use when the user has no image yet, cannot upload images, or wants AI to draft an image prompt inside the deck.",
38
+ fields: {
39
+ image: { type: "{ mode: placeholder, prompt, aspectRatio?, placement?, variants? }", required: true },
40
+ prompt: { type: "string", required: true },
41
+ aspectRatio: { type: "16:9 | 4:3 | 3:2 | 1:1 | 3:4 | 9:16 | auto", required: false },
42
+ placement: { type: "top | side | auto", required: false },
43
+ variants: { type: "boolean", required: false },
44
+ },
45
+ limits: {
46
+ prompt: { recommendedChars: 160 },
47
+ },
48
+ notes: [
49
+ "This is a common image object mode, not a standalone slide layout.",
50
+ "Supported by imageText, caseStudy, and imageGrid.",
51
+ "Use `aspectRatio` when the likely image shape is known.",
52
+ "For imageText with unknown aspect ratio, leave it as auto so preflight emits top and side variants.",
53
+ "The generated placeholder is editable PowerPoint text and shapes, not a raster image.",
54
+ ],
55
+ example: {
56
+ layout: "imageText",
57
+ title: "系统架构示意",
58
+ image: {
59
+ mode: "placeholder",
60
+ aspectRatio: "auto",
61
+ prompt: "A clean architecture diagram showing YAML input, validation, PPTX generation, and OMML post-processing.",
62
+ },
63
+ text: ["用户后续可替换占位图。"],
64
+ },
65
+ };
66
+
67
+ const layoutGuides = {
68
+ imageText: {
69
+ layout: "imageText",
70
+ purpose: "Show one image with a short explanatory bullet list.",
71
+ whenToUse: "Use for visual evidence, screenshots, diagrams, and result images that need concise interpretation.",
72
+ fields: {
73
+ layout: { type: "literal", required: true, value: "imageText" },
74
+ title: { type: "string", required: true },
75
+ image: { type: "string | { path, placement, fit } | { mode: placeholder, prompt, aspectRatio?, placement? }", required: true },
76
+ caption: { type: "string", required: false },
77
+ text: { type: "string[]", required: true },
78
+ },
79
+ limits: {
80
+ text: { maxItems: 5, recommendedChars: 38 },
81
+ imagePrompt: { recommendedChars: 160 },
82
+ },
83
+ notes: [
84
+ "Very wide images are automatically placed above the text.",
85
+ "Ordinary landscape, portrait, and square images stay side-by-side so text space remains usable.",
86
+ "`placement: top` or `placement: side` overrides automatic placement.",
87
+ "`fit: contain` avoids cropping; `fit: cover` fills the image box.",
88
+ "If no image is available, set `image.mode: placeholder` and provide a prompt for the future image.",
89
+ "When placeholder aspectRatio is omitted or `auto`, imageText preflight creates top and side layout variants for user selection.",
90
+ ],
91
+ example: {
92
+ layout: "imageText",
93
+ title: "视觉结果说明",
94
+ image: {
95
+ path: "assets/bit-campus-photo.png",
96
+ placement: "auto",
97
+ fit: "contain",
98
+ },
99
+ caption: "图片说明控制在一行内。",
100
+ text: ["图像展示关键现象。", "右侧或下方保留少量解释。"],
101
+ },
102
+ },
103
+ chart: {
104
+ layout: "chart",
105
+ purpose: "Create an editable native PowerPoint chart.",
106
+ whenToUse: "Use for quantitative comparisons, trends, composition, and metric summaries.",
107
+ fields: {
108
+ layout: { type: "literal", required: true, value: "chart" },
109
+ title: { type: "string", required: true },
110
+ type: { type: "bar | line | pie | doughnut | scatter | area", required: false },
111
+ categories: { type: "string[]", required: true },
112
+ series: { type: "{ name, values }[]", required: false },
113
+ values: { type: "number[]", required: false },
114
+ caption: { type: "string", required: false },
115
+ },
116
+ limits: {
117
+ categories: { maxItems: 8, pieMaxItems: 6, recommendedChars: 18 },
118
+ seriesName: { recommendedChars: 16 },
119
+ caption: { recommendedChars: 58 },
120
+ },
121
+ notes: [
122
+ "Every series values list must match the categories length.",
123
+ "Use `values` as a shortcut for a single unnamed series.",
124
+ "Pie and doughnut charts work best with six or fewer categories.",
125
+ ],
126
+ example: {
127
+ layout: "chart",
128
+ title: "指标对比",
129
+ type: "bar",
130
+ categories: ["Baseline", "Ours"],
131
+ series: [{ name: "Accuracy", values: [82.1, 87.4] }],
132
+ caption: "Ours improves accuracy under the same setting.",
133
+ },
134
+ },
135
+ flowchart: {
136
+ layout: "flowchart",
137
+ purpose: "Draw an editable flowchart with PowerPoint shapes and arrow lines.",
138
+ whenToUse: "Use for pipelines, decision paths, model stages, and process diagrams.",
139
+ fields: {
140
+ layout: { type: "literal", required: true, value: "flowchart" },
141
+ title: { type: "string", required: true },
142
+ nodes: { type: "{ id, text, note?, x?, y?, w?, h? }[]", required: true },
143
+ edges: { type: "{ from, to }[]", required: false },
144
+ note: { type: "string", required: false },
145
+ },
146
+ limits: {
147
+ nodes: { maxItems: 10, recommendedTextChars: 12, recommendedNoteChars: 22 },
148
+ note: { recommendedChars: 56 },
149
+ },
150
+ notes: [
151
+ "Node ids must be unique.",
152
+ "Edges must reference existing node ids.",
153
+ "If edges are omitted, nodes are connected in order.",
154
+ "Optional x/y coordinates are relative to the flowchart area.",
155
+ ],
156
+ example: {
157
+ layout: "flowchart",
158
+ title: "生成流程",
159
+ nodes: [
160
+ { id: "yaml", text: "YAML" },
161
+ { id: "check", text: "校验" },
162
+ { id: "pptx", text: "PPTX" },
163
+ ],
164
+ edges: [
165
+ { from: "yaml", to: "check" },
166
+ { from: "check", to: "pptx" },
167
+ ],
168
+ },
169
+ },
170
+ table: {
171
+ layout: "table",
172
+ purpose: "Create an editable comparison or data table.",
173
+ whenToUse: "Use for compact structured comparisons with a small number of columns.",
174
+ fields: {
175
+ layout: { type: "literal", required: true, value: "table" },
176
+ title: { type: "string", required: true },
177
+ columns: { type: "string[]", required: true },
178
+ rows: { type: "array[]", required: true },
179
+ },
180
+ limits: {
181
+ columns: { maxItems: 5 },
182
+ cell: { recommendedChars: 42 },
183
+ },
184
+ notes: [
185
+ "Rows may be split across multiple slides during preflight if the table is too tall.",
186
+ "Keep wide tables under five columns, or split the content by topic.",
187
+ "Inline formulas in table cells are converted to native OMML.",
188
+ ],
189
+ example: {
190
+ layout: "table",
191
+ title: "方法对比",
192
+ columns: ["方法", "输入", "输出"],
193
+ rows: [
194
+ ["Baseline", "文本", "普通 PPTX"],
195
+ ["Ours", "YAML", "可编辑 BIT 风格 PPTX"],
196
+ ],
197
+ },
198
+ },
199
+ formula: {
200
+ layout: "formula",
201
+ purpose: "Show a display formula with short explanatory notes.",
202
+ whenToUse: "Use for core equations, objective functions, and derivations that need native Office Math.",
203
+ fields: {
204
+ layout: { type: "literal", required: true, value: "formula" },
205
+ title: { type: "string", required: true },
206
+ formula: { type: "string | { latex, caption? }", required: true },
207
+ caption: { type: "string", required: false },
208
+ explanation: { type: "string[]", required: false },
209
+ },
210
+ limits: {
211
+ explanation: { maxItems: 4, recommendedChars: 48 },
212
+ },
213
+ notes: [
214
+ "LaTeX is converted to native OMML during PPTX post-processing.",
215
+ "Inline `$...$` and `\\(...\\)` formulas are supported in text-heavy layouts.",
216
+ "Avoid image formulas unless explicitly using a fallback outside this generator.",
217
+ ],
218
+ example: {
219
+ layout: "formula",
220
+ title: "优化目标",
221
+ formula: {
222
+ latex: "\\mathcal{L}=\\sum_i (y_i-\\hat{y}_i)^2",
223
+ },
224
+ explanation: ["目标函数衡量预测误差。"],
225
+ },
226
+ },
227
+ };
228
+
229
+ function listGuideLayouts() {
230
+ return Object.keys(layoutGuides);
231
+ }
232
+
233
+ function getLayoutGuide(layout) {
234
+ return layoutGuides[layout] || null;
235
+ }
236
+
237
+ function getLayoutSchema(layout) {
238
+ const guide = getLayoutGuide(layout);
239
+ if (!guide) return null;
240
+ return {
241
+ layout: guide.layout,
242
+ commonFields: speakerNotesGuide.fields,
243
+ fields: guide.fields,
244
+ limits: guide.limits,
245
+ };
246
+ }
247
+
248
+ function getLayoutExample(layout) {
249
+ const guide = getLayoutGuide(layout);
250
+ return guide?.example || null;
251
+ }
252
+
253
+ function getGuideOverview() {
254
+ return {
255
+ name: "BIT PPT Generator",
256
+ purpose: "Generate editable Beijing Institute of Technology style PPTX files from YAML.",
257
+ workflow: [
258
+ "Choose layouts with `bit-ppt list-layouts`.",
259
+ "Inspect one layout with `bit-ppt guide layout <name>`.",
260
+ "Draft YAML and run `bit-ppt check <deck.yaml> --json`.",
261
+ "Fix validation errors using `repairPrompt`.",
262
+ "Generate with `bit-ppt generate <deck.yaml> <output.pptx>`.",
263
+ ],
264
+ commands: [
265
+ "bit-ppt guide",
266
+ "bit-ppt guide layouts",
267
+ "bit-ppt guide layout <name>",
268
+ "bit-ppt guide schema <name> --json",
269
+ "bit-ppt guide example <name>",
270
+ "bit-ppt guide speaker-notes",
271
+ "bit-ppt guide image-placeholder",
272
+ "bit-ppt guide writing-rules",
273
+ ],
274
+ guidedLayouts: listGuideLayouts(),
275
+ };
276
+ }
277
+
278
+ function getSpeakerNotesGuide() {
279
+ return { ...speakerNotesGuide };
280
+ }
281
+
282
+ function getImagePlaceholderGuide() {
283
+ return { ...imagePlaceholderGuide };
284
+ }
285
+
286
+ function getWritingRules() {
287
+ return [...writingRules];
288
+ }
289
+
290
+ function getGuideWorkflow() {
291
+ return getGuideOverview().workflow;
292
+ }
293
+
294
+ function getAllGuides() {
295
+ return {
296
+ overview: getGuideOverview(),
297
+ speakerNotes: getSpeakerNotesGuide(),
298
+ imagePlaceholder: getImagePlaceholderGuide(),
299
+ writingRules: getWritingRules(),
300
+ layouts: Object.fromEntries(listGuideLayouts().map((layout) => [layout, getLayoutGuide(layout)])),
301
+ };
302
+ }
303
+
304
+ export {
305
+ getAllGuides,
306
+ getGuideOverview,
307
+ getGuideWorkflow,
308
+ getImagePlaceholderGuide,
309
+ getLayoutExample,
310
+ getLayoutGuide,
311
+ getLayoutSchema,
312
+ getSpeakerNotesGuide,
313
+ getWritingRules,
314
+ listGuideLayouts,
315
+ };
@@ -0,0 +1,197 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import * as z from "zod/v4";
6
+ import { parseDeckYaml } from "./core/yaml-parse.mjs";
7
+ import {
8
+ ROOT,
9
+ checkDeck,
10
+ checkDeckFile,
11
+ generateDeckFile,
12
+ listLayouts,
13
+ validateDeck,
14
+ } from "./generate.mjs";
15
+ import {
16
+ getAllGuides,
17
+ getGuideOverview,
18
+ getGuideWorkflow,
19
+ getImagePlaceholderGuide,
20
+ getLayoutExample,
21
+ getLayoutGuide,
22
+ getLayoutSchema,
23
+ getSpeakerNotesGuide,
24
+ getWritingRules,
25
+ listGuideLayouts,
26
+ } from "./layout-guides.mjs";
27
+
28
+ function packageVersion() {
29
+ try {
30
+ const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8"));
31
+ return pkg.version || "0.0.0";
32
+ } catch {
33
+ return "0.0.0";
34
+ }
35
+ }
36
+
37
+ function resolveLocalPath(value) {
38
+ const source = String(value || "");
39
+ return path.isAbsolute(source) ? source : path.resolve(ROOT, source);
40
+ }
41
+
42
+ function loadDeckArgs(args) {
43
+ if (args.deckYaml) return parseDeckYaml(args.deckYaml, "deckYaml");
44
+ if (args.inputPath) return parseDeckYaml(fs.readFileSync(resolveLocalPath(args.inputPath), "utf8"), args.inputPath);
45
+ throw new Error("Provide either inputPath or deckYaml.");
46
+ }
47
+
48
+ function textResult(data) {
49
+ return {
50
+ structuredContent: data,
51
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
52
+ };
53
+ }
54
+
55
+ function guideResult(topic = "overview", name) {
56
+ switch (topic) {
57
+ case "overview":
58
+ return getGuideOverview();
59
+ case "all":
60
+ return getAllGuides();
61
+ case "workflow":
62
+ return getGuideWorkflow();
63
+ case "layouts":
64
+ return listGuideLayouts();
65
+ case "layout": {
66
+ if (!name) throw new Error("get_guide topic=layout requires name.");
67
+ const guide = getLayoutGuide(name);
68
+ if (!guide) throw new Error(`No guide available for layout: ${name}`);
69
+ return guide;
70
+ }
71
+ case "schema": {
72
+ if (!name) throw new Error("get_guide topic=schema requires name.");
73
+ const schema = getLayoutSchema(name);
74
+ if (!schema) throw new Error(`No schema available for layout: ${name}`);
75
+ return schema;
76
+ }
77
+ case "example": {
78
+ if (!name) throw new Error("get_guide topic=example requires name.");
79
+ const example = getLayoutExample(name);
80
+ if (!example) throw new Error(`No example available for layout: ${name}`);
81
+ return example;
82
+ }
83
+ case "speaker-notes":
84
+ return getSpeakerNotesGuide();
85
+ case "image-placeholder":
86
+ return getImagePlaceholderGuide();
87
+ case "writing-rules":
88
+ return getWritingRules();
89
+ default:
90
+ throw new Error(`Unknown guide topic: ${topic}`);
91
+ }
92
+ }
93
+
94
+ const deckInputSchema = {
95
+ inputPath: z.string().optional().describe("Path to a YAML deck file. Relative paths resolve from the project root."),
96
+ deckYaml: z.string().optional().describe("Raw YAML deck content. Use this instead of inputPath for generated drafts."),
97
+ };
98
+
99
+ const fontOptionsSchema = {
100
+ fontCn: z.string().optional().describe("Chinese/CJK font override."),
101
+ fontCnLight: z.string().optional().describe("Light Chinese/CJK font override."),
102
+ fontEn: z.string().optional().describe("Latin font override."),
103
+ fontSerif: z.string().optional().describe("Serif font override."),
104
+ fontCode: z.string().optional().describe("Code font override."),
105
+ };
106
+
107
+ export function createBitPptMcpServer() {
108
+ const server = new McpServer({
109
+ name: "bit-ppt-template",
110
+ version: packageVersion(),
111
+ });
112
+
113
+ server.registerTool("list_layouts", {
114
+ title: "List Layouts",
115
+ description: "List supported BIT PPT slide layouts.",
116
+ inputSchema: {},
117
+ }, async () => textResult({ layouts: listLayouts() }));
118
+
119
+ server.registerTool("get_guide", {
120
+ title: "Get Progressive Guide",
121
+ description: "Return focused guide content for layouts, schemas, examples, speaker notes, image placeholders, or writing rules.",
122
+ inputSchema: {
123
+ topic: z.enum(["overview", "all", "workflow", "layouts", "layout", "schema", "example", "speaker-notes", "image-placeholder", "writing-rules"]).optional().default("overview"),
124
+ name: z.string().optional().describe("Layout name for topic layout, schema, or example."),
125
+ },
126
+ }, async ({ topic, name }) => textResult(guideResult(topic, name)));
127
+
128
+ server.registerTool("validate_deck", {
129
+ title: "Validate Deck",
130
+ description: "Validate a YAML deck and return validation errors, warnings, and repairPrompt.",
131
+ inputSchema: deckInputSchema,
132
+ }, async (args) => {
133
+ const deck = loadDeckArgs(args);
134
+ const validation = validateDeck(deck);
135
+ return textResult({
136
+ validation: {
137
+ errors: validation.errors,
138
+ warnings: validation.warnings,
139
+ },
140
+ repairPrompt: validation.repairPrompt,
141
+ });
142
+ });
143
+
144
+ server.registerTool("preflight_deck", {
145
+ title: "Preflight Deck",
146
+ description: "Run validation plus preflight planning such as automatic splits and placeholder layout variants.",
147
+ inputSchema: deckInputSchema,
148
+ }, async (args) => textResult(checkDeck(loadDeckArgs(args))));
149
+
150
+ server.registerTool("get_repair_prompt", {
151
+ title: "Get Repair Prompt",
152
+ description: "Return only the repairPrompt for a YAML deck, plus validation counts.",
153
+ inputSchema: deckInputSchema,
154
+ }, async (args) => {
155
+ const result = checkDeck(loadDeckArgs(args));
156
+ return textResult({
157
+ repairPrompt: result.repairPrompt,
158
+ errors: result.validation.errors.length,
159
+ warnings: result.validation.warnings.length,
160
+ });
161
+ });
162
+
163
+ server.registerTool("generate_pptx", {
164
+ title: "Generate PPTX",
165
+ description: "Generate an editable PPTX from a YAML file. Generation stops on validation errors; strict mode also stops on warnings.",
166
+ inputSchema: {
167
+ inputPath: z.string().describe("Path to the input YAML deck file. Relative paths resolve from the project root."),
168
+ outputPath: z.string().describe("Path to the output PPTX file. Relative paths resolve from the project root."),
169
+ strict: z.boolean().optional().describe("Treat validation warnings as failures before generation."),
170
+ ...fontOptionsSchema,
171
+ },
172
+ }, async (args) => {
173
+ const input = resolveLocalPath(args.inputPath);
174
+ const output = resolveLocalPath(args.outputPath);
175
+ if (args.strict) {
176
+ const check = checkDeckFile(input);
177
+ if (check.validation.errors.length || check.validation.warnings.length) {
178
+ return textResult({
179
+ generated: false,
180
+ error: "Deck validation failed strict mode.",
181
+ validation: check.validation,
182
+ repairPrompt: check.repairPrompt,
183
+ });
184
+ }
185
+ }
186
+ const result = await generateDeckFile(input, output, args);
187
+ return textResult({ generated: true, ...result });
188
+ });
189
+
190
+ return server;
191
+ }
192
+
193
+ export async function startStdioMcpServer() {
194
+ const server = createBitPptMcpServer();
195
+ const transport = new StdioServerTransport();
196
+ await server.connect(transport);
197
+ }