import { readFile } from 'node:fs/promises' import JSZip from 'jszip' import { XMLParser } from 'fast-xml-parser' const parser = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true }) const list = (value: T | T[] | undefined): T[] => value === undefined ? [] : Array.isArray(value) ? value : [value] function textNodes(value: unknown): string[] { if (!value || typeof value !== 'object') return [] return Object.entries(value).flatMap(([key, child]) => key === 't' ? [typeof child === 'string' ? child : String(child ?? '')] : Array.isArray(child) ? child.flatMap(textNodes) : textNodes(child)) } export async function extractPptxSpeakerNotes(path: string): Promise { const zip = await JSZip.loadAsync(await readFile(path), { checkCRC32: true }) const files = Object.keys(zip.files).filter(name => /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(name)) .sort((a, b) => Number(a.match(/\d+(?=\.xml$)/)![0]) - Number(b.match(/\d+(?=\.xml$)/)![0])) if (files.length > 500) throw new Error('PPTX 页数超出备注解析限制') const notes: string[] = [] for (const name of files) { const xml = await zip.file(name)!.async('string') const shapes = list(parser.parse(xml)?.notes?.cSld?.spTree?.sp) const body = shapes.find((shape: any) => shape?.nvSpPr?.nvPr?.ph?.['@_type'] === 'body') as any const paragraphs = list(body?.txBody?.p) .map((paragraph: unknown) => textNodes(paragraph).join('').trim()) .filter(Boolean) notes.push(paragraphs.join('\n')) } return notes }